UI Actions: Difference between revisions
(Create UI actions page) |
(Add example code) |
||
Line 12: | Line 12: | ||
At emulator boot the UI code calls <tt>void ui_actions_init(void)</tt> to initialize the UI actions system, sets a UI-specific dispatch function with <tt>void ui_actions_set_dispatch(void (*dispatch)(const ui_action_map_t *))</tt> and registers UI action handlers with <tt>void ui_actions_register(const ui_action_map_t *mappings)</tt>. | At emulator boot the UI code calls <tt>void ui_actions_init(void)</tt> to initialize the UI actions system, sets a UI-specific dispatch function with <tt>void ui_actions_set_dispatch(void (*dispatch)(const ui_action_map_t *))</tt> and registers UI action handlers with <tt>void ui_actions_register(const ui_action_map_t *mappings)</tt>. | ||
=== Example === | |||
Example code helps a lot, the following is taken from the Gtk3 UI code and cobbled together. | |||
Here we have a few action handler implementations (from src/arch/gtk3/actions-machine.c. Some of this is Gtk3-specific, other handlers can be moved into generic code: | |||
<syntaxhighlight lang="c" line> | |||
static void confirm_exit_callback(GtkDialog *dialog, gboolean result) | |||
{ | |||
if (result) { | |||
mainlock_release(); | |||
archdep_vice_exit(0); | |||
mainlock_obtain(); | |||
} | |||
/* mark action finished in case the user selected "No" */ | |||
ui_action_finish(ACTION_QUIT); | |||
} | |||
/** \brief Quit emulator, possibly popping up a confirmation dialog */ | |||
static void quit_action(void) | |||
{ | |||
int confirm = FALSE; | |||
resources_get_int("ConfirmOnExit", &confirm); | |||
if (!confirm) { | |||
ui_action_finish(ACTION_QUIT); | |||
archdep_vice_exit(0); | |||
return; | |||
} | |||
vice_gtk3_message_confirm( | |||
confirm_exit_callback, | |||
"Exit VICE", | |||
"Do you really wish to exit VICE?"); | |||
} | |||
/** \brief Open the monitor */ | |||
static void monitor_open_action(void) | |||
{ | |||
int server = 0; | |||
/* don't spawn the monitor if the monitor server is running */ | |||
resources_get_int("MonitorServer", &server); | |||
if (server == 0) { | |||
vsync_suspend_speed_eval(); | |||
if (ui_pause_active()) { | |||
ui_pause_enter_monitor(); | |||
} else { | |||
monitor_startup_trap(); | |||
} | |||
} | |||
} | |||
/** \brief Trigger soft reset of the machine */ | |||
static void reset_soft_action(void) | |||
{ | |||
machine_trigger_reset(MACHINE_RESET_MODE_SOFT); | |||
ui_pause_disable(); | |||
} | |||
/** \brief Trigger hard reset of the machine */ | |||
static void reset_hard_action(void) | |||
{ | |||
machine_trigger_reset(MACHINE_RESET_MODE_HARD); | |||
ui_pause_disable(); | |||
} | |||
/** \brief Toggle PET userport diagnostic pin */ | |||
static void diagnostic_pin_toggle_action(void) | |||
{ | |||
int active = 0; | |||
resources_get_int("DiagPin", &active); | |||
resources_set_int("DiagPin", active ? 0 : 1); | |||
} | |||
</syntaxhighlight> | |||
This is the list of action handlers defined above we'll be passing to <tt>ui_actions_register()</tt>: | |||
<syntaxhighlight lang="c" line> | |||
/** \brief List of machine-related actions */ | |||
static const ui_action_map_t machine_actions[] = { | |||
{ | |||
.action = ACTION_QUIT, | |||
.handler = quit_action, | |||
.blocks = true, | |||
.dialog = true | |||
}, | |||
{ | |||
.action = ACTION_MONITOR_OPEN, | |||
.handler = monitor_open_action, | |||
.uithread = true | |||
}, | |||
{ | |||
.action = ACTION_RESET_SOFT, | |||
.handler = reset_soft_action | |||
}, | |||
{ | |||
.action = ACTION_RESET_HARD, | |||
.handler = reset_hard_action | |||
}, | |||
{ | |||
.action = ACTION_DIAGNOSTIC_PIN_TOGGLE, | |||
.handler = diagnostic_pin_toggle_action, | |||
/* no need for UI thread, the status bar code will update the LED when | |||
* it runs */ | |||
.uithread = false | |||
}, | |||
UI_ACTION_MAP_TERMINATOR | |||
}; | |||
</syntaxhighlight> | |||
Here we initialize the UI actions system, set a dispatch handler and register the action handlers listed above: | |||
<syntaxhighlight lang="c" line> | |||
/* | |||
* Helper that pushes the handler on the UI thread (details will follow) | |||
*/ | |||
static gboolean ui_action_dispatch_impl(gpointer data) | |||
{ | |||
void (*handler)(void) = data; | |||
handler(); | |||
return FALSE; | |||
} | |||
/* | |||
* This is the action dispatch function, whenever ui_action_trigger() is called this | |||
* function is called in response. | |||
*/ | |||
static void ui_action_dispatch(const ui_action_map_t *map) | |||
{ | |||
if (mainlock_is_vice_thread()) { | |||
/* we're on the main thread, push to UI thread */ | |||
gdk_threads_add_timeout(0, ui_action_dispatch_impl, (gpointer)(map->handler)); | |||
} else { | |||
/* we're already on the UI thread */ | |||
map->handler(); | |||
} | |||
} | |||
/* Initialize UI actions */ | |||
ui_actions_init(); | |||
/* Set dispatch function */ | |||
ui_actions_set_dispatch(ui_action_dispatch); | |||
/* Register some actions */ | |||
ui_actions_register(machine_actions); | |||
</syntaxhighlight> |
Revision as of 11:21, 25 March 2023
Rationale
In the Gtk3 UI the concept of "UI actions" was introduced to allow for a unified interface to trigger certain actions in the emulator and its user interface . UI actions decouple hotkeys from menu items so hotkeys can be defined that do not have a corresponding menu item. This interface will also allow calling actions from customizable joystick mappings, reusing the code triggered by menu items and/or hotkeys.
Since most user interactions are independent of what UI toolkit is used it makes sense to generalize these interactions and make all current and future UIs use this UI actions system. For example "soft reset" or "attach disk to unit #8" may use different dialogs, menu items or hotkeys, the underlying logic (and code) will be the same. So providing a predefined -- and extendable -- set of UI actions will aid in development of future UIs.
Mechanism
The UI actions API code resides in src/arch/uiactions.h and src/arch/uiactions.c. The code is documented using Doxygen docblocks, so the documentation can be generated with doc/mkdoxy.sh or just read in the files themselves.
UI actions are triggered using a unique ID and calling a function: void ui_action_trigger(int action);. The IDs are available in the src/arch/uiactions.h header, these are all prefixed with ACTION_.
At emulator boot the UI code calls void ui_actions_init(void) to initialize the UI actions system, sets a UI-specific dispatch function with void ui_actions_set_dispatch(void (*dispatch)(const ui_action_map_t *)) and registers UI action handlers with void ui_actions_register(const ui_action_map_t *mappings).
Example
Example code helps a lot, the following is taken from the Gtk3 UI code and cobbled together.
Here we have a few action handler implementations (from src/arch/gtk3/actions-machine.c. Some of this is Gtk3-specific, other handlers can be moved into generic code:
static void confirm_exit_callback(GtkDialog *dialog, gboolean result)
{
if (result) {
mainlock_release();
archdep_vice_exit(0);
mainlock_obtain();
}
/* mark action finished in case the user selected "No" */
ui_action_finish(ACTION_QUIT);
}
/** \brief Quit emulator, possibly popping up a confirmation dialog */
static void quit_action(void)
{
int confirm = FALSE;
resources_get_int("ConfirmOnExit", &confirm);
if (!confirm) {
ui_action_finish(ACTION_QUIT);
archdep_vice_exit(0);
return;
}
vice_gtk3_message_confirm(
confirm_exit_callback,
"Exit VICE",
"Do you really wish to exit VICE?");
}
/** \brief Open the monitor */
static void monitor_open_action(void)
{
int server = 0;
/* don't spawn the monitor if the monitor server is running */
resources_get_int("MonitorServer", &server);
if (server == 0) {
vsync_suspend_speed_eval();
if (ui_pause_active()) {
ui_pause_enter_monitor();
} else {
monitor_startup_trap();
}
}
}
/** \brief Trigger soft reset of the machine */
static void reset_soft_action(void)
{
machine_trigger_reset(MACHINE_RESET_MODE_SOFT);
ui_pause_disable();
}
/** \brief Trigger hard reset of the machine */
static void reset_hard_action(void)
{
machine_trigger_reset(MACHINE_RESET_MODE_HARD);
ui_pause_disable();
}
/** \brief Toggle PET userport diagnostic pin */
static void diagnostic_pin_toggle_action(void)
{
int active = 0;
resources_get_int("DiagPin", &active);
resources_set_int("DiagPin", active ? 0 : 1);
}
This is the list of action handlers defined above we'll be passing to ui_actions_register():
/** \brief List of machine-related actions */
static const ui_action_map_t machine_actions[] = {
{
.action = ACTION_QUIT,
.handler = quit_action,
.blocks = true,
.dialog = true
},
{
.action = ACTION_MONITOR_OPEN,
.handler = monitor_open_action,
.uithread = true
},
{
.action = ACTION_RESET_SOFT,
.handler = reset_soft_action
},
{
.action = ACTION_RESET_HARD,
.handler = reset_hard_action
},
{
.action = ACTION_DIAGNOSTIC_PIN_TOGGLE,
.handler = diagnostic_pin_toggle_action,
/* no need for UI thread, the status bar code will update the LED when
* it runs */
.uithread = false
},
UI_ACTION_MAP_TERMINATOR
};
Here we initialize the UI actions system, set a dispatch handler and register the action handlers listed above:
/*
* Helper that pushes the handler on the UI thread (details will follow)
*/
static gboolean ui_action_dispatch_impl(gpointer data)
{
void (*handler)(void) = data;
handler();
return FALSE;
}
/*
* This is the action dispatch function, whenever ui_action_trigger() is called this
* function is called in response.
*/
static void ui_action_dispatch(const ui_action_map_t *map)
{
if (mainlock_is_vice_thread()) {
/* we're on the main thread, push to UI thread */
gdk_threads_add_timeout(0, ui_action_dispatch_impl, (gpointer)(map->handler));
} else {
/* we're already on the UI thread */
map->handler();
}
}
/* Initialize UI actions */
ui_actions_init();
/* Set dispatch function */
ui_actions_set_dispatch(ui_action_dispatch);
/* Register some actions */
ui_actions_register(machine_actions);