mirror of
https://github.com/Ardour/ardour.git
synced 2025-12-10 08:36:32 +01:00
Merge branch 'cairocanvas' into recfolder
No further development on 'recfolder' is planned (hence merge).
The first commit (5edbb2cb) can be rebased for a short to mid-term
solution. Mid-to-long term the general raid and file management
situation will need to be addressed in a more consistent and
creative manner.
Conflicts:
gtk2_ardour/session_option_editor.cc
gtk2_ardour/session_option_editor.h
This commit is contained in:
commit
905b1ebea4
13 changed files with 181 additions and 55 deletions
|
|
@ -181,6 +181,23 @@ private:
|
||||||
Gtk::Label* _label; ///< label for button, so we can use markup
|
Gtk::Label* _label; ///< label for button, so we can use markup
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Component which allows to add any GTK Widget - intended for single buttons and custom stateless objects */
|
||||||
|
class FooOption : public OptionEditorComponent
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FooOption (Gtk::Widget *w) : _w (w) {}
|
||||||
|
|
||||||
|
void add_to_page (OptionEditorPage* p) {
|
||||||
|
add_widget_to_page (p, _w);
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk::Widget& tip_widget() { return *_w; }
|
||||||
|
void set_state_from_config () {}
|
||||||
|
void parameter_changed (std::string const &) {}
|
||||||
|
private:
|
||||||
|
Gtk::Widget *_w;
|
||||||
|
};
|
||||||
|
|
||||||
/** Component which provides the UI to handle a string option using a GTK Entry */
|
/** Component which provides the UI to handle a string option using a GTK Entry */
|
||||||
class EntryOption : public Option
|
class EntryOption : public Option
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -282,50 +282,7 @@ SessionOptionEditor::SessionOptionEditor (Session* s)
|
||||||
sigc::mem_fun (*this, &SessionOptionEditor::get_use_monitor_section),
|
sigc::mem_fun (*this, &SessionOptionEditor::get_use_monitor_section),
|
||||||
sigc::mem_fun (*this, &SessionOptionEditor::set_use_monitor_section)
|
sigc::mem_fun (*this, &SessionOptionEditor::set_use_monitor_section)
|
||||||
));
|
));
|
||||||
|
/* Meterbridge */
|
||||||
/* Misc */
|
|
||||||
|
|
||||||
add_option (_("Misc"), new OptionEditorHeading (_("MIDI Options")));
|
|
||||||
|
|
||||||
add_option (_("Misc"), new BoolOption (
|
|
||||||
"midi-copy-is-fork",
|
|
||||||
_("MIDI region copies are independent"),
|
|
||||||
sigc::mem_fun (*_session_config, &SessionConfiguration::get_midi_copy_is_fork),
|
|
||||||
sigc::mem_fun (*_session_config, &SessionConfiguration::set_midi_copy_is_fork)
|
|
||||||
));
|
|
||||||
|
|
||||||
ComboOption<InsertMergePolicy>* li = new ComboOption<InsertMergePolicy> (
|
|
||||||
"insert-merge-policy",
|
|
||||||
_("Policy for handling overlapping notes\n on the same MIDI channel"),
|
|
||||||
sigc::mem_fun (*_session_config, &SessionConfiguration::get_insert_merge_policy),
|
|
||||||
sigc::mem_fun (*_session_config, &SessionConfiguration::set_insert_merge_policy)
|
|
||||||
);
|
|
||||||
|
|
||||||
li->add (InsertMergeReject, _("never allow them"));
|
|
||||||
li->add (InsertMergeRelax, _("don't do anything in particular"));
|
|
||||||
li->add (InsertMergeReplace, _("replace any overlapped existing note"));
|
|
||||||
li->add (InsertMergeTruncateExisting, _("shorten the overlapped existing note"));
|
|
||||||
li->add (InsertMergeTruncateAddition, _("shorten the overlapping new note"));
|
|
||||||
li->add (InsertMergeExtend, _("replace both overlapping notes with a single note"));
|
|
||||||
|
|
||||||
add_option (_("Misc"), li);
|
|
||||||
|
|
||||||
add_option (_("Misc"), new OptionEditorHeading (_("Glue to bars and beats")));
|
|
||||||
|
|
||||||
add_option (_("Misc"), new BoolOption (
|
|
||||||
"glue-new-markers-to-bars-and-beats",
|
|
||||||
_("Glue new markers to bars and beats"),
|
|
||||||
sigc::mem_fun (*_session_config, &SessionConfiguration::get_glue_new_markers_to_bars_and_beats),
|
|
||||||
sigc::mem_fun (*_session_config, &SessionConfiguration::set_glue_new_markers_to_bars_and_beats)
|
|
||||||
));
|
|
||||||
|
|
||||||
add_option (_("Misc"), new BoolOption (
|
|
||||||
"glue-new-regions-to-bars-and-beats",
|
|
||||||
_("Glue new regions to bars and beats"),
|
|
||||||
sigc::mem_fun (*_session_config, &SessionConfiguration::get_glue_new_regions_to_bars_and_beats),
|
|
||||||
sigc::mem_fun (*_session_config, &SessionConfiguration::set_glue_new_regions_to_bars_and_beats)
|
|
||||||
));
|
|
||||||
|
|
||||||
add_option (_("Meterbridge"), new OptionEditorHeading (_("Route Display")));
|
add_option (_("Meterbridge"), new OptionEditorHeading (_("Route Display")));
|
||||||
|
|
||||||
add_option (_("Meterbridge"), new BoolOption (
|
add_option (_("Meterbridge"), new BoolOption (
|
||||||
|
|
@ -388,6 +345,55 @@ SessionOptionEditor::SessionOptionEditor (Session* s)
|
||||||
sigc::mem_fun (*_session_config, &SessionConfiguration::set_show_name_on_meterbridge)
|
sigc::mem_fun (*_session_config, &SessionConfiguration::set_show_name_on_meterbridge)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
/* Misc */
|
||||||
|
|
||||||
|
add_option (_("Misc"), new OptionEditorHeading (_("MIDI Options")));
|
||||||
|
|
||||||
|
add_option (_("Misc"), new BoolOption (
|
||||||
|
"midi-copy-is-fork",
|
||||||
|
_("MIDI region copies are independent"),
|
||||||
|
sigc::mem_fun (*_session_config, &SessionConfiguration::get_midi_copy_is_fork),
|
||||||
|
sigc::mem_fun (*_session_config, &SessionConfiguration::set_midi_copy_is_fork)
|
||||||
|
));
|
||||||
|
|
||||||
|
ComboOption<InsertMergePolicy>* li = new ComboOption<InsertMergePolicy> (
|
||||||
|
"insert-merge-policy",
|
||||||
|
_("Policy for handling overlapping notes\n on the same MIDI channel"),
|
||||||
|
sigc::mem_fun (*_session_config, &SessionConfiguration::get_insert_merge_policy),
|
||||||
|
sigc::mem_fun (*_session_config, &SessionConfiguration::set_insert_merge_policy)
|
||||||
|
);
|
||||||
|
|
||||||
|
li->add (InsertMergeReject, _("never allow them"));
|
||||||
|
li->add (InsertMergeRelax, _("don't do anything in particular"));
|
||||||
|
li->add (InsertMergeReplace, _("replace any overlapped existing note"));
|
||||||
|
li->add (InsertMergeTruncateExisting, _("shorten the overlapped existing note"));
|
||||||
|
li->add (InsertMergeTruncateAddition, _("shorten the overlapping new note"));
|
||||||
|
li->add (InsertMergeExtend, _("replace both overlapping notes with a single note"));
|
||||||
|
|
||||||
|
add_option (_("Misc"), li);
|
||||||
|
|
||||||
|
add_option (_("Misc"), new OptionEditorHeading (_("Glue to bars and beats")));
|
||||||
|
|
||||||
|
add_option (_("Misc"), new BoolOption (
|
||||||
|
"glue-new-markers-to-bars-and-beats",
|
||||||
|
_("Glue new markers to bars and beats"),
|
||||||
|
sigc::mem_fun (*_session_config, &SessionConfiguration::get_glue_new_markers_to_bars_and_beats),
|
||||||
|
sigc::mem_fun (*_session_config, &SessionConfiguration::set_glue_new_markers_to_bars_and_beats)
|
||||||
|
));
|
||||||
|
|
||||||
|
add_option (_("Misc"), new BoolOption (
|
||||||
|
"glue-new-regions-to-bars-and-beats",
|
||||||
|
_("Glue new regions to bars and beats"),
|
||||||
|
sigc::mem_fun (*_session_config, &SessionConfiguration::get_glue_new_regions_to_bars_and_beats),
|
||||||
|
sigc::mem_fun (*_session_config, &SessionConfiguration::set_glue_new_regions_to_bars_and_beats)
|
||||||
|
));
|
||||||
|
|
||||||
|
add_option (_("Misc"), new OptionEditorHeading (_("Defaults")));
|
||||||
|
|
||||||
|
Gtk::Button* btn = Gtk::manage (new Gtk::Button (_("Use these settings as defaults")));
|
||||||
|
btn->signal_clicked().connect (sigc::mem_fun (*this, &SessionOptionEditor::save_defaults));
|
||||||
|
add_option (_("Misc"), new FooOption (btn));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
@ -470,3 +476,9 @@ std::string SessionOptionEditor::get_midi_source_dir ()
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SessionOptionEditor::save_defaults ()
|
||||||
|
{
|
||||||
|
_session->save_default_options();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ private:
|
||||||
bool set_audio_source_dir(std::string);
|
bool set_audio_source_dir(std::string);
|
||||||
std::string get_midi_source_dir();
|
std::string get_midi_source_dir();
|
||||||
bool set_midi_source_dir(std::string);
|
bool set_midi_source_dir(std::string);
|
||||||
|
|
||||||
|
void save_defaults ();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif /* __gtk_ardour_session_option_editor_h__ */
|
#endif /* __gtk_ardour_session_option_editor_h__ */
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,7 @@ class LIBARDOUR_API Session : public PBD::StatefulDestructible, public PBD::Scop
|
||||||
int rename (const std::string&);
|
int rename (const std::string&);
|
||||||
bool get_nsm_state () const { return _under_nsm_control; }
|
bool get_nsm_state () const { return _under_nsm_control; }
|
||||||
void set_nsm_state (bool state) { _under_nsm_control = state; }
|
void set_nsm_state (bool state) { _under_nsm_control = state; }
|
||||||
|
bool save_default_options ();
|
||||||
|
|
||||||
PBD::Signal1<void,std::string> StateSaved;
|
PBD::Signal1<void,std::string> StateSaved;
|
||||||
PBD::Signal0<void> StateReady;
|
PBD::Signal0<void> StateReady;
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ public:
|
||||||
XMLNode& get_variables ();
|
XMLNode& get_variables ();
|
||||||
void set_variables (XMLNode const &);
|
void set_variables (XMLNode const &);
|
||||||
|
|
||||||
|
bool load_state ();
|
||||||
|
bool save_state ();
|
||||||
|
|
||||||
/* define accessor methods */
|
/* define accessor methods */
|
||||||
|
|
||||||
#undef CONFIG_VARIABLE
|
#undef CONFIG_VARIABLE
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,9 @@ Session::Session (AudioEngine &eng,
|
||||||
throw failed_constructor ();
|
throw failed_constructor ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* load default session properties - if any */
|
||||||
|
config.load_state();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (load_state (_current_snapshot_name)) {
|
if (load_state (_current_snapshot_name)) {
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,15 @@
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
#include <glib/gstdio.h> /* for g_stat() */
|
||||||
|
#include <glibmm/miscutils.h> /* for build_filename() */
|
||||||
|
|
||||||
|
#include "pbd/file_utils.h"
|
||||||
#include "pbd/pathexpand.h"
|
#include "pbd/pathexpand.h"
|
||||||
|
|
||||||
#include "ardour/types.h"
|
#include "ardour/types.h"
|
||||||
|
#include "ardour/filesystem_paths.h"
|
||||||
#include "ardour/session_configuration.h"
|
#include "ardour/session_configuration.h"
|
||||||
#include "i18n.h"
|
#include "i18n.h"
|
||||||
|
|
||||||
|
|
@ -122,3 +128,67 @@ SessionConfiguration::map_parameters (boost::function<void (std::string)>& funct
|
||||||
#undef CONFIG_VARIABLE
|
#undef CONFIG_VARIABLE
|
||||||
#undef CONFIG_VARIABLE_SPECIAL
|
#undef CONFIG_VARIABLE_SPECIAL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
SessionConfiguration::load_state ()
|
||||||
|
{
|
||||||
|
std::string rcfile;
|
||||||
|
GStatBuf statbuf;
|
||||||
|
if (find_file (ardour_config_search_path(), "session.rc", rcfile)) {
|
||||||
|
if (g_stat (rcfile.c_str(), &statbuf)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (statbuf.st_size == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
XMLTree tree;
|
||||||
|
if (!tree.read (rcfile.c_str())) {
|
||||||
|
error << string_compose(_("%1: cannot part default session options \"%2\""), PROGRAM_NAME, rcfile) << endmsg;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
XMLNode& root (*tree.root());
|
||||||
|
if (root.name() != X_("SessionDefaults")) {
|
||||||
|
warning << _("Invalid session default XML Root.") << endmsg;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
XMLNode* node;
|
||||||
|
if (((node = find_named_node (root, X_("Config"))) != 0)) {
|
||||||
|
LocaleGuard lg (X_("POSIX"));
|
||||||
|
set_variables(*node);
|
||||||
|
info << _("Loaded custom session defaults.") << endmsg;
|
||||||
|
} else {
|
||||||
|
warning << _("Found no session defaults in XML file.") << endmsg;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CUSTOM OVERRIDES */
|
||||||
|
set_audio_search_path("");
|
||||||
|
set_midi_search_path("");
|
||||||
|
set_raid_path("");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
SessionConfiguration::save_state ()
|
||||||
|
{
|
||||||
|
const std::string rcfile = Glib::build_filename (user_config_directory(), "session.rc");
|
||||||
|
if (rcfile.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
XMLTree tree;
|
||||||
|
XMLNode* root = new XMLNode(X_("SessionDefaults"));
|
||||||
|
root->add_child_nocopy (get_variables ());
|
||||||
|
tree.set_root (root);
|
||||||
|
|
||||||
|
if (!tree.write (rcfile.c_str())) {
|
||||||
|
error << _("Could not save session options") << endmsg;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -879,6 +879,12 @@ Session::load_options (const XMLNode& node)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
Session::save_default_options ()
|
||||||
|
{
|
||||||
|
return config.save_state();
|
||||||
|
}
|
||||||
|
|
||||||
XMLNode&
|
XMLNode&
|
||||||
Session::get_state()
|
Session::get_state()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,16 @@
|
||||||
|
|
||||||
#include "ardouralsautil/reserve.h"
|
#include "ardouralsautil/reserve.h"
|
||||||
|
|
||||||
|
#ifndef ARD_PROG_NAME
|
||||||
|
#define ARD_PROG_NAME "alsa_request_device"
|
||||||
|
#endif
|
||||||
|
#ifndef ARD_APPL_NAME
|
||||||
|
#define ARD_APPL_NAME "ALSA User"
|
||||||
|
#endif
|
||||||
|
#ifndef VERSION
|
||||||
|
#define VERSION "v0.3"
|
||||||
|
#endif
|
||||||
|
|
||||||
static int run = 1;
|
static int run = 1;
|
||||||
static int release_wait_for_signal = 0;
|
static int release_wait_for_signal = 0;
|
||||||
static pid_t parent_pid = 0;
|
static pid_t parent_pid = 0;
|
||||||
|
|
@ -48,7 +58,7 @@ static int stdin_available(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void print_version(int status) {
|
static void print_version(int status) {
|
||||||
printf ("ardour-request-device 0.2\n\n");
|
printf (ARD_PROG_NAME " " VERSION "\n\n");
|
||||||
printf (
|
printf (
|
||||||
"Copyright (C) 2014 Robin Gareus <robin@gareus.org>\n"
|
"Copyright (C) 2014 Robin Gareus <robin@gareus.org>\n"
|
||||||
"This is free software; see the source for copying conditions. There is NO\n"
|
"This is free software; see the source for copying conditions. There is NO\n"
|
||||||
|
|
@ -58,8 +68,8 @@ static void print_version(int status) {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void usage(int status) {
|
static void usage(int status) {
|
||||||
printf ("ardour-request-device - DBus Audio Reservation Utility.\n");
|
printf (ARD_PROG_NAME " - DBus Audio Reservation Utility.\n");
|
||||||
printf ("Usage: ardour-request-device [ OPTIONS ] <Audio-Device-ID>\n");
|
printf ("Usage: " ARD_PROG_NAME " [ OPTIONS ] <Audio-Device-ID>\n");
|
||||||
printf ("Options:\n\
|
printf ("Options:\n\
|
||||||
-h, --help display this help and exit\n\
|
-h, --help display this help and exit\n\
|
||||||
-p, --priority <int> reservation priority (default: int32_max)\n\
|
-p, --priority <int> reservation priority (default: int32_max)\n\
|
||||||
|
|
@ -74,23 +84,23 @@ This tool issues a dbus request to reserve an ALSA Audio-device.\n\
|
||||||
If successful other users of the device (e.g. pulseaudio) will\n\
|
If successful other users of the device (e.g. pulseaudio) will\n\
|
||||||
release the device.\n\
|
release the device.\n\
|
||||||
\n\
|
\n\
|
||||||
ardour-request-device by default announces itself as \"Ardour ALSA Backend\"\n\
|
" ARD_PROG_NAME " by default announces itself as \"" ARD_APPL_NAME "\"\n\
|
||||||
and uses the maximum possible priority for requesting the device.\n\
|
and uses the maximum possible priority for requesting the device.\n\
|
||||||
These settings can be overriden using the -n and -p options respectively.\n\
|
These settings can be overriden using the -n and -p options respectively.\n\
|
||||||
\n\
|
\n\
|
||||||
If a PID is given the tool will watch the process and if that is not running\n\
|
If a PID is given the tool will watch the process and if that is not running\n\
|
||||||
release the device and exit. Otherwise ardour-request-device runs until\n\
|
release the device and exit. Otherwise " ARD_PROG_NAME " runs until\n\
|
||||||
either stdin is closed, a SIGINT or SIGTERM is received or some other\n\
|
either stdin is closed, a SIGINT or SIGTERM is received or some other\n\
|
||||||
application requests the device with a higher priority.\n\
|
application requests the device with a higher priority.\n\
|
||||||
\n\
|
\n\
|
||||||
Without the -w option, ardour-request-device yields the device after 500ms to\n\
|
Without the -w option, " ARD_PROG_NAME " yields the device after 500ms to\n\
|
||||||
any higher-priority request. With the -w option this tool waits until it\n\
|
any higher-priority request. With the -w option this tool waits until it\n\
|
||||||
for SIGINT or SIGTERM - but at most 4 sec to acknowledge before releasing.\n\
|
for SIGINT or SIGTERM - but at most 4 sec to acknowledge before releasing.\n\
|
||||||
\n\
|
\n\
|
||||||
The audio-device-id is a string e.g. 'Audio1'\n\
|
The audio-device-id is a string e.g. 'Audio1'\n\
|
||||||
\n\
|
\n\
|
||||||
Examples:\n\
|
Examples:\n\
|
||||||
ardour-request-device Audio0\n\
|
" ARD_PROG_NAME " Audio0\n\
|
||||||
\n");
|
\n");
|
||||||
|
|
||||||
printf ("Report bugs to Robin Gareus <robin@gareus.org>\n");
|
printf ("Report bugs to Robin Gareus <robin@gareus.org>\n");
|
||||||
|
|
@ -140,7 +150,7 @@ int main(int argc, char **argv) {
|
||||||
int ret, c;
|
int ret, c;
|
||||||
|
|
||||||
int32_t priority = INT32_MAX;
|
int32_t priority = INT32_MAX;
|
||||||
char *name = strdup("Ardour ALSA Backend");
|
char *name = strdup(ARD_APPL_NAME);
|
||||||
|
|
||||||
while ((c = getopt_long (argc, argv,
|
while ((c = getopt_long (argc, argv,
|
||||||
"h" /* help */
|
"h" /* help */
|
||||||
|
|
|
||||||
|
|
@ -50,4 +50,6 @@ def build(bld):
|
||||||
obj.defines = [
|
obj.defines = [
|
||||||
'_POSIX_SOURCE',
|
'_POSIX_SOURCE',
|
||||||
'_XOPEN_SOURCE=500',
|
'_XOPEN_SOURCE=500',
|
||||||
|
'ARD_PROG_NAME="ardour-request-device"',
|
||||||
|
'ARD_APPL_NAME="Ardour ALSA Backend"',
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@
|
||||||
|
|
||||||
namespace ArdourCanvas
|
namespace ArdourCanvas
|
||||||
{
|
{
|
||||||
|
struct Rect;
|
||||||
|
|
||||||
class Rect;
|
|
||||||
class Item;
|
class Item;
|
||||||
class ScrollGroup;
|
class ScrollGroup;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@
|
||||||
|
|
||||||
namespace ArdourCanvas
|
namespace ArdourCanvas
|
||||||
{
|
{
|
||||||
|
struct Rect;
|
||||||
|
|
||||||
class Canvas;
|
class Canvas;
|
||||||
class Rect;
|
|
||||||
class ScrollGroup;
|
class ScrollGroup;
|
||||||
|
|
||||||
/** The parent class for anything that goes on the canvas.
|
/** The parent class for anything that goes on the canvas.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
#include "canvas/visibility.h"
|
#include "canvas/visibility.h"
|
||||||
|
|
||||||
namespace Cairo {
|
namespace Cairo {
|
||||||
struct Context;
|
class Context;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace ArdourCanvas
|
namespace ArdourCanvas
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue