UI Actions: Difference between revisions

From vice-emu
Jump to navigation Jump to search
(Add warning about some of the docs being outdated due to the organic nature of the code)
(UI Actions)
Tag: Replaced
Line 1: Line 1:
This page explains the UI actions system used in the Gtk3 port and which is supposed to be used in the SDL port and future ports.
This page explains the UI actions system used in the Gtk3 and SDL port UIs and which is supposed to be used in any future ports.


'''Warning''': the following is slightly outdated since adjusting for use with the SDL UI code: The generic hotkeys API has moved to using the <tt>ui_action_map_t</tt> struct for its data, removing some duplication in the old <tt>vhk_map_t</tt> (which was removed). The prototype for an action handler has changed to <tt>void handler(<b>void *param</b>)</tt> to allow handlers to be reused for closely related items (like passing a drive unit number). I need a way to point to Doxygen-generated code instead of this static bullshit.
'''2023-10-01: Removed outdated docs'''
 
== 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 [https://sourceforge.net/p/vice-emu/code/HEAD/tree/trunk/vice/src/arch/shared/uiactions.h src/arch/uiactions.h] and [https://sourceforge.net/p/vice-emu/code/HEAD/tree/trunk/vice/src/arch/shared/uiactions.c src/arch/uiactions.c]. The code is documented using Doxygen docblocks, so the documentation can be generated with <tt>doc/mkdoxy.sh</tt> or just read in the files themselves.
 
UI actions are triggered using a unique ID and calling a function: <tt><b>ui_action_trigger</b>(<i>action-ID</i>)</tt>. The IDs are available in the [https://sourceforge.net/p/vice-emu/code/HEAD/tree/trunk/vice/src/arch/shared/uiactions.h src/arch/uiactions.h] header, these are all prefixed with <tt>ACTION_</tt>.
 
At emulator boot the UI code calls <tt><b>ui_actions_init</b>()</tt> to initialize the UI actions system, sets a UI-specific dispatch function with <tt><b>ui_actions_set_dispatch</b>(<i>dispatch-func</i>)</tt> and registers UI action handlers with <tt><b>ui_actions_register</b>(<i>list-of-actions</i>)</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 (map->uithread || map->dialog) {
        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();
        }
    } else {
        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>
 
On emulator shutdown call the UI actions shutdown function:
<syntaxhighlight lang="c" line>
ui_actions_shutdown();
</syntaxhighlight>
 
Some details of the API are already shown here, such as actions having certain properties: does an action pop up a dialog? does an action need to run on the UI thread? does an action need to block before it can be triggered again? This will be explained in the next section.
 
Hotkey files (<tt>.vhk</tt>) used in the Gtk3 UI refer to these actions with strings as IDs, for better readability, manual editing and to allow changing the internal integer IDs without breaking these files. A table of mappings of action integer IDs to strings can be found in [https://sourceforge.net/p/vice-emu/code/HEAD/tree/trunk/vice/src/arch/shared/uiactions.c src/arch/shared/uiactions.c], in the <tt><b>action_info_list</b>[]</tt> array, at the time of writing at [https://sourceforge.net/p/vice-emu/code/HEAD/tree/trunk/vice/src/arch/shared/uiactions.c#l67 line 67]. The table also provides descriptions of each action for use in the UI (e.g. the Gtk3 hotkeys editor widget) and a bitmask of which emulators support the action; all this is accessible through the API.
 
== API ==
 
=== Main functions ===
 
The API is exposed in the [https://sourceforge.net/p/vice-emu/code/HEAD/tree/trunk/vice/src/arch/shared/uiactions.h src/arch/uiactions.h] header. The main functions used are, as seen in the example code:
<syntaxhighlight lang="c" line>
void ui_actions_init        (void);
void ui_actions_set_dispatch(void (*dispatch)(const ui_action_map_t *));
void ui_actions_register    (const ui_action_map_t *mappings);
void ui_actions_shutdown    (void);
</syntaxhighlight>
 
==== ui_actions_init() ====
Initialize the UI actions system. Must be called on emulator boot, early in the boot sequence.
 
==== ui_actions_set_dispatch() ====
Set the function to call when <tt><b>ui_action_trigger</b>()</tt> is invoked. This function is passed a <tt><b>ui_action_map_t</b> *</tt> with information on the action being triggered. It is responsible for executing the handler (<tt>map->handler()</tt>) and dealing with thread-safety if UI/port uses threads.
 
==== ui_actions_register() ====
Register UI action handlers. This function can be called multiple times, each time actions are appended to the list of already registered actions. For any action that is already registered it will log an error but continue.
 
==== ui_actions_shutdown() ====
Clean up resources used by the UI actions systems. Must be called on emulator shutdown.
 
=== The ui_action_map_t struct ===
 
The <tt><b>ui_action_map_t</b></tt> typedef can be found in the header as well:
<syntaxhighlight lang="c" line>
/** \brief  Mapping of an action ID to a handler
*/
typedef struct ui_action_map_s {
    int    action;          /**< action ID */
    void (*handler)(void);  /**< function handling the action */
 
    /* modes */
    bool  blocks;          /**< action blocks (the same action cannot be
                                triggered again until it finishes) */
    bool  dialog;          /**< action pops up a dialog (only one dialog action
                                is allowed at a time), this implies using the
                                UI thread */
    bool  uithread;        /**< must run on the UI thread */
 
    /* state */
    bool  is_busy;        /**< action is busy */
} ui_action_map_t;
</syntaxhighlight>
 
==== Members ====
 
===== action =====
Pretty self-explanatory, an integer containing the unique ID as found in the header as <tt>ACTION_</tt> defines.
 
===== handler =====
The function to be called when <tt><b>ui_action_trigger</b>()</tt> is invoked. It takes no arguments and returns nothing.
 
===== blocks =====
A boolean indicating the same action cannot be triggered again until it is marked finished with <tt><b>ui_action_finish</b>()</tt>. Actions that require user interaction or take time to complete can set this to <tt>true</tt>.
 
===== dialog =====
A boolean indicating the action shows a dialog. Since we don't want a user pressing a controller button mapped to "smart-attach" twenty times and the UI showing twenty dialogs, the UI action system allows only a single dialog active at a time for actions having set this to <tt>true</tt>.
This flag also implies the action must be run on the UI thread.
 
===== uithread =====
A boolean indicating the action must be run on the UI thread. Due to the multi-threaded nature of current VICE any non-UI thread cannot directly call UI code, so any action that shows dialogs or updates UI elements must run on the UI thread. The <tt><i>dispatch</i></tt> function set by <tt><b>ui_actions_set_dispatch</b>()</tt> is responsible for handling thread-safety.
 
===== is_busy =====
Internal flag used for blocking actions. It is set by the UI action system when <tt><b>ui_action_trigger</b>()</tt> function is called with a blocking action and cleared when the <tt><b>ui_action_finish</b>()</tt> function is called for that same action (inside the action handler).
On registration of actions this is always set to <tt>false</tt> so just leave it out when using C99 designated initializers.
 
 
=== Utility functions ===
 
The API also provides a number of utility functions, mostly to help with UI code and debugging.
 
==== Getting information on actions ====
<syntaxhighlight lang="c" line>
int                ui_action_get_id      (const char *name);
const char *      ui_action_get_name    (int action);
const char *      ui_action_get_desc    (int action);
ui_action_info_t * ui_action_get_info_list(void);
bool              ui_action_is_valid    (int action);
</syntaxhighlight>
 
===== ui_action_get_id() =====
Get action integer ID for action name. Returns <tt>ACTION_INVALID</tt> (<tt>-1</tt>) if <tt>name</tt> isn't valid.<br>
Please note that this can return a valid ID for an action name that isn't valid for the current machine, to determine if an action ID is valid use <tt><b>ui_action_is_valid</b>()</tt>.
 
===== ui_action_get_name() =====
Get action name by integer ID. Returns <tt>NULL</tt> for invalid IDs.<br>
Please note that this can return non-<tt>NULL</tt> for an action ID that isn't valid for the current machine, to determine if an action ID is valid use <tt><b>ui_action_is_valid</b>()</tt>.
 
===== ui_action_get_desc() =====
Get action description by integer ID. Returns <tt>NULL</tt> for invalid IDs.<br>
Please note that this can return non-<tt>NULL</tt> for an action ID that isn't valid for the current machine, to determine if an action ID is valid use <tt><b>ui_action_is_valid</b>()</tt>.
 
===== ui_action_get_info_list() =====
Get a list of valid actions for the current machine. The list must be freed after use with <tt><b>lib_free</b>()</tt>.<br>
The list contains elements of type <tt><b>ui_action_info_t</b></tt>:
<syntaxhighlight lang="c" line>
typedef struct ui_action_info_s {
    int        id;    /**< action ID */
    const char *name;  /**< action name */
    const char *desc;  /**< action description */
} ui_action_info_t;
</syntaxhighlight>
The list is terminated with an element with <tt><b>id</b></tt> set to <tt>ACTION_INVALID</tt> (<tt>-1</tt>) and <tt><b>name</b></tt> and <tt><b>desc</b></tt> set to <tt>NULL</tt>.
 
===== ui_action_is_valid() =====
Determine if an action ID is valid for the current machine.
 
==== Action ID helpers ====
 
An unfortunate side-effect of not letting an action handler accept an argument is that we end up with a lot of closely related actions and handlers doing nearly the same thing. For example disk drive related operations: we have 4 units and 2 drives per unit, so we end up with 8 different action IDs for attach-to-disk(unit, drive). The drive code usually uses arguments for unit and drive, and the (Gtk3) UI code usually encodes these arguments in a void* payload for callbacks. So the API provides some functions to help out with these issues:
<syntaxhighlight lang="c" line>
int ui_action_id_fliplist_add    (int unit, int drive);
int ui_action_id_fliplist_remove  (int unit, int drive);
int ui_action_id_fliplist_next    (int unit, int drive);
int ui_action_id_fliplist_previous(int unit, int drive);
int ui_action_id_fliplist_clear  (int unit, int drive);
int ui_action_id_fliplist_load    (int unit, int drive);
int ui_action_id_fliplist_save    (int unit, int drive);
int ui_action_id_drive_attach    (int unit, int drive);
int ui_action_id_drive_detach    (int unit, int drive);
</syntaxhighlight>
All these functions take their <tt><i>unit</i></tt> argument in the range <tt>8</tt>-<tt>11</tt> and their <tt><i>drive</i></tt> argument as either <tt>0</tt> or <tt>1</tt>.
 
An example can be found in the Gtk3 fliplist code ([https://sourceforge.net/p/vice-emu/code/HEAD/tree/trunk/vice/src/arch/gtk3/uifliplist.c src/arch/gtk3/uifliplist.c]):
<syntaxhighlight lang="c" line>
static void trigger_ui_action(GtkWidget *item, gpointer action)
{
    ui_action_trigger(GPOINTER_TO_INT(action));
}
 
void ui_populate_fliplist_menu(GtkWidget *menu, int unit, int separator_count)
{
    /* ... code snipped for brevity ... */
 
    menu_item = gtk_menu_item_new_with_label(buf);
    g_signal_connect(menu_item,
                    "activate",
                    G_CALLBACK(trigger_ui_action),
                    GINT_TO_POINTER(ui_action_id_fliplist_next(unit, 0)));
    gtk_container_add(GTK_CONTAINER(menu), menu_item);
 
    /* ... snip ... */
}
</syntaxhighlight>
Here we use the <tt><b>ui_action_id_fliplist_next</b>()</tt> function to get an action ID we use as payload for the <tt><b>trigger_ui_action</b>()</tt> signal handler of a menu item. (The <tt><i>drive</i></tt> argument is currently <tt>0</tt> since the fliplist doesn't support dual drives).
 
Another example of the use of these ID helpers is in the Gtk3 status bar code ([https://sourceforge.net/p/vice-emu/code/HEAD/tree/trunk/vice/src/arch/gtk3/uistatusbar.c src/arch/gtk3/uistatusbar.c]), where we use the ID to set an accelerator label on a menu item in a popup menu:
<syntaxhighlight lang="c" line>
/* ... snip ... */
drive_menu_item = child->data;
label = gtk_bin_get_child(GTK_BIN(drive_menu_item));
 
if (drive_is_dualdrive_by_devnr(i + DRIVE_UNIT_MIN)) {
    g_snprintf(buffer, sizeof(buffer),
              "Attach disk to drive #%d:%d...",
              i + DRIVE_UNIT_MIN, drive);
} else {
    g_snprintf(buffer, sizeof(buffer),
              "Attach disk to drive #%d...",
              i + DRIVE_UNIT_MIN);
}
gtk_label_set_text(GTK_LABEL(label), buffer);
 
/* set hotkey, if any */
action = ui_action_id_drive_attach(i + DRIVE_UNIT_MIN, drive);
ui_set_menu_item_accel_label(drive_menu_item, action);
 
/* ... snip ... */
</syntaxhighlight>
The last two lines are of interest: first we get the action ID for the unit and drive, and then we call a Gtk3-specific function that uses the hotkeys API to look up the accelerator label for the hotkey for the action. So if a hotkey for that specific action is currently set then a label like "Alt+X" will be put next to the item's normal label (or "Bloemkool+X" for MacOS).<br>
<br><i>Put link here somewhere to hotkeys API page once written!</i>

Revision as of 20:29, 1 October 2023

This page explains the UI actions system used in the Gtk3 and SDL port UIs and which is supposed to be used in any future ports.

2023-10-01: Removed outdated docs