Actions and Menus
Gio.Action
is a high-level interface used throughout the GNOME platform, especially in GTK. Actions can provide a similar, but simpler interface to functionality such as methods or properties. They can be used by widgets, desktop notifications, menus or even remotely via D-Bus.
Gio.MenuModel
is a related API, used to describe a menu structure with sections, submenus and items. In the case of GTK, menu models can provide the structure and presentation for Gio.Action
. While actions are purely functional, menu items can have labels, icons and map stateful actions to elements like checkboxes and radio buttons
Related Guides
GNOME Developer Documentation
GAction
Gio.Action
is a GObject Interface that can be implemented by any object, but you will almost always use Gio.SimpleAction
. There are two fundamental types of actions, activatable and stateful, which will only succeed if Gio.Action.get_enabled()
returns true
.
Although they can be used by themselves, actions are intended to be grouped together, either by scope (e.g. app.quit
and window.close
) or by context (e.g. clipboard.copy
and clipboard.paste
). This makes them good alternatives to signal handlers for widgets and menus, as well as export over D-Bus.
Activatable Actions
Activatable actions operate much like functions, but have no return value. They may have a parameter (i.e. arguments) of any type, or none at all. They have no means to report the success of an activation.
Gio.SimpleAction
implements Gio.Action.activate()
by emitting an activate
signal. If the signal has a handler connected, it will be passed the parameter, otherwise if the action is stateful it will attempt to change the value.
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
/*
* The most basic action, which works similar to a function with no arguments.
*/
const basicAction = new Gio.SimpleAction({
name: 'basicAction',
});
basicAction.connect('activate', (action, _parameter) => {
console.log(`${action.name} activated!`);
});
basicAction.activate(null);
/*
* An action that works similar to a function with a single string argument.
*/
const paramAction = new Gio.SimpleAction({
name: 'paramAction',
parameter_type: new GLib.VariantType('s'),
});
paramAction.connect('activate', (action, parameter) => {
console.log(`${action.name} activated: ${parameter.unpack()}`);
});
paramAction.activate(GLib.Variant.new_string('string'));
Stateful Actions
TIP
Depending on the implementation, stateful actions may also act on calls to Gio.Action.activate()
. Consult the documentation for details.
Stateful actions operate similar to object properties. Depending on how Gio.Action.change_state()
is implemented, the state of an action may be read-write, read-only, or conditional depending on its value. Implementations may use Gio.Action.get_state_hint()
to validate a state change, although there are no guarantees how the hint may be interpreted.
Gio.SimpleAction
implements state changes by emitting a change-state
signal to allow validating the new value. If the signal has a handler connected, it can decide whether to call Gio.SimpleAction.set_state()
, otherwise it will be called unconditionally.
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
/*
* The value type of a stateful action is set at construction from the initial
* value, and can't be changed afterwards.
*/
const stateAction = new Gio.SimpleAction({
name: 'stateAction',
state: GLib.Variant.new_int32(-1),
state_hint: new GLib.Variant('(ii)', [-1, GLib.MAXINT32]),
});
/*
* The state will only change once the handler has approved the request.
*/
stateAction.connect('notify::state', (action, _pspec) => {
console.log(`${action.name} state changed: ${action.state.print(true)}`);
});
/*
* The handler may check for equality, and use the hint to validate the request.
*/
stateAction.connect('change-state', (action, value) => {
console.log(`${action.name} change request: ${value.print(true)}`);
if (action.state.equal(value))
return;
const [min, max] = action.state_hint.deepUnpack();
const request = value.unpack();
if (request >= min && request <= max)
action.set_state(value);
});
Specialized Actions
Gio.PropertyAction
is a stateful action, which is created from and bound to a GObject Property. Only read-write properties with basic types are supported, including:
GObject.TYPE_BOOLEAN
GObject.TYPE_INT32
GObject.TYPE_UINT32
GObject.TYPE_DOUBLE
GObject.TYPE_FLOAT
GObject.TYPE_STRING
- Enumerations, which are available as a string
The property value is not stored in the Gio.Action
, but instead forwarded by property notifications as state changes:
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
const SomeObject = GObject.registerClass({
Properties: {
'example-property': GObject.ParamSpec.string(
'example-property',
'Example Property',
'A read-write string property',
GObject.ParamFlags.READWRITE,
null
),
},
}, class SomeObject extends GObject.Object {
});
const someInstance = new SomeObject({
example_property: 'initial value',
});
someInstance.connect('notify::example-property', (object, _pspec) => {
console.log(`GObject Property: ${object.example_property}`);
});
const propertyAction = new Gio.PropertyAction({
name: 'example',
object: someInstance,
property_name: 'example-property',
});
propertyAction.connect('notify::state', (action, _pspec) => {
console.log(`Action State: ${action.state.unpack()}`);
});
someInstance.example_property = 'new value';
propertyAction.change_state(GLib.Variant.new_string('newer value'));
GSettings also has a convenient method for creating actions bound to a settings value. Boolean settings (i.e. b
) will become an activatable action, which is toggled when activated, while all other types are stateful with the same type as the given key.
import Gio from 'gi://Gio';
const settings = new Gio.Settings({
schema_id: 'org.gnome.desktop.interface',
});
settings.connect('changed::enable-animations', (object, _key) => {
console.log(`GSettings Value: ${object.example_property}`);
});
const settingsAction = settings.create_action('enable-animations');
settingsAction.connect('notify::state', (action, _pspec) => {
console.log(`Action State: ${action.state.unpack()}`);
});
settings.set_boolean('enable-animations', false);
settingsAction.activate(null);
Action Groups
Actions are usually managed by objects that implement the Gio.ActionGroup
, and possibly Gio.ActionMap
interfaces. Gio.Application
implements both interfaces, as does Gio.SimpleActionGroup
.
When activated via a group, as opposed to calling Gio.Action.activate()
directly, parameters may be passed as a "detail". For string parameters with only alpha-numeric characters, periods and hyphens, this is as simple as actionName::string-value
. Other types may be passed in the form of a serialized GVariant such as actionName('string-!@#$%^&*')
and actionName(5)
. This will be very convenient when working with Gio.MenuModel
.
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
/*
* GSimpleActionGroup implements both GActionGroup and GActionMap
*/
const actionGroup = new Gio.SimpleActionGroup();
/*
* Using GActionMap, the writable interface for groups of actions.
*
* This is an override in GJS, necessary because the standard method is not
* introspectable.
*/
actionGroup.add_action_entries([
{
name: 'basicAction',
activate: (action, _parameter) => {
console.log(`${action.name} activated!`);
},
},
{
name: 'paramAction',
parameter_type: new GLib.VariantType('s'),
activate: (action, parameter) => {
console.log(`${action.name} activated: ${parameter.unpack()}`);
},
},
{
name: 'stateAction',
state: GLib.Variant.new_boolean(true),
change_state: (action, value) => {
console.log(`${action.name} change requested: ${value.print(true)}`);
},
},
]);
actionGroup.add_action(new Gio.SimpleAction({
name: 'removeAction',
activate: (action, _parameter) => {
console.log(`${action.name} activated!`);
},
}));
const removeAction = actionGroup.lookup_action('removeAction');
if (removeAction !== null)
removeAction.enabled = !removeAction.enabled;
actionGroup.remove_action('removeAction');
/*
* Using GActionGroup, the readable interface for groups of actions.
*
* Actions can be queried, activated and state changes requested, but can not be
* added, removed, enabled or disabled with this interface.
*/
actionGroup.connect('action-added', (action, name) => {
console.log(`${name} added`);
});
actionGroup.connect('action-enabled-changed', (action, name, enabled) => {
console.log(`${name} is now ${enabled ? 'enabled' : 'disabled'}`);
});
actionGroup.connect('action-removed', (action, name) => {
console.log(`${name} removed`);
});
actionGroup.connect('action-state-changed', (action, name, value) => {
console.log(`${name} state is now ${value.print(true)}`);
});
if (actionGroup.has_action('basicAction'))
actionGroup.activate_action('basicAction', null);
if (actionGroup.get_action_enabled('paramAction')) {
actionGroup.activate_action('paramAction', new GLib.Variant('s', 'string'));
actionGroup.activate_action('paramAction::string', null);
}
const [
exists,
enabled,
parameterType,
stateType,
stateHint,
state,
] = actionGroup.query_action('stateAction');
if (enabled && state.unpack() === true)
actionGroup.change_action_state('stateAction', GLib.Variant.new_boolean(false));
Gio.Application
, including subclasses like Gtk.Application
and Adw.Application
, is the preferred group for application-wide actions such as quit
and about
. All of the same methods and signals may be used on the application instance, just like Gio.SimpleActionGroup
.
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
const application = Gio.Application.new('guide.gjs.Example',
Gio.ApplicationFlags.DEFAULT_FLAGS);
application.connect('activate', () => {
console.log('The application has been activated');
});
application.connect('startup', () => {
console.log('The application will run until instructed to quit');
application.hold();
});
application.connect('shutdown', () => {
console.log('The application is shutting down');
application.hold();
});
/*
* If activated elsewhere in the application, the action name will be `app.quit`
*/
const quitAction = new Gio.SimpleAction({
name: 'quit',
});
quitAction.connect('activate', () => {
console.log('The application is being instructed to quit');
application.quit();
});
application.add_action(quitAction);
/*
* Activate the `quit` action, shortly after the application starts.
*/
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
application.activate_action('quit', null);
});
application.run([imports.system.programInvocationName].concat(ARGV));
GMenu
TIP
It's also possible to define menu models in Gtk.Builder
XML, but only the programmatic API will be demonstrated here.
Gio.MenuModel
is an abstract-base class (not an interface) for defining structured menus with items, sections and submenus. The provided implementation for the platform is Gio.Menu
.
Unlike Gio.Action
, menu models contain presentation information like labels and icon names. In most cases, actions provide the functionality for menus, while menus often act as the presenter of groups of actions.
Menu Items
NOTE
Gio.MenuItem
objects are immutable, meaning that once added to a menu any changes to the items will not change the item in the model.
Menu items can take several forms, aside from the standard item type which corresponds to a standard activatable GAction. In particular, activatable boolean types (i.e. b
) become checkbox menu items, and stateful string types and enumerations (i.e. s
) become radio buttons.
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
const menuModel = new Gio.Menu();
/*
* Items with parameterless GActions can be added very easily, while those with
* simple parameters can be set using a detailed action.
*/
menuModel.append('See full menu', 'pizza.full-menu');
menuModel.append('House Pizza', 'pizza.deal::today');
/*
* In other cases, you may want to build items manually and add an icon or
* custom attributes. Note that the consumer of the menu will decide if an icon
* is displayed.
*/
const allergyItem = new Gio.MenuItem();
allergyItem.set_label('Allergy Warning');
allergyItem.set_action_and_target_value('pizza.allergyWarning');
allergyItem.set_icon(Gio.Icon.new_for_string('dialog-warning-symbolic'));
allergyItem.set_attribute('disclaimer-url',
GLib.Variant.new_string('https://www.pizza.com/allergy-warning'));
menuModel.append_item(allergyItem);
/*
* Actions with a string state type (`s`) can be used for a group of radio
* buttons, by specifying the same action name with different target values.
*
* This works well with a GPropertyAction bound to a GObject property holding
* an enumeration, since they are stored as strings.
*/
menuModel.append('Cheese', 'pizza.style::cheese');
menuModel.append('Hawaiian', 'pizza.style::hawaiian');
menuModel.append('Pepperoni', 'pizza.style::pepperoni');
menuModel.append('Vegetarian', 'pizza.style::vegetarian');
/*
* Actions with a boolean state type (`b`) will have a checkbox.
*/
menuModel.append('Extra Cheese', 'pizza.extra-cheese');
Sections and Submenus
TIP
Submenus should be used conservatively, as they can result in a confusing user experience. See the GNOME Human Interface Guidelines for tips.
Menu sections are a way to logically and visually group items, while keeping them in the same menu level. A common set of menu sections might include one for Preferences, Help and About, with another for Quit.
Submenus are less common and often used to group items that follow logically from a parent item. A common pattern is an Open item, which might have sub-items such as Open In New Tab, Open In New Window and so on.
import Gio from 'gi://Gio';
const menuModel = new Gio.Menu();
/*
* Submenus should group related items, that follow logically from the parent.
*/
const menuSubmenu = new Gio.Menu();
menuSubmenu.append('Open', 'win.open');
menuSubmenu.append('Open In New Tab', 'win.open-tab');
menuSubmenu.append('Open In New Window', 'win.open-window');
menuModel.append_submenu('Open', menuSubmenu);
/*
* Menu sections should group related items, while other items should be in
* their own section, or stand-alone items.
*
* If a label is given when adding the section, it will usually be presented in
* way that associates it with the separator.
*/
const menuSection = new Gio.Menu();
menuSection.append('Preferences', 'app.preferences');
menuSection.append('Help', 'app.help');
menuSection.append('About GJS', 'app.about');
menuModel.append_section(null, menuSection);
const quitSection = new Gio.Menu();
menuSection.append('Quit', 'app.quit');
menuModel.append_section(null, quitSection);
Consuming Menu Models
Gio.MenuModel
emits Gio.MenuModel::items-changed
an efficient way to track the membership of items, sections and submenus, similar to Gio.ListModel
.
import Gio from 'gi://Gio';
const menuModel = new Gio.Menu();
menuModel.connect('items-changed', (menu, position, removed, added) => {
console.log(`position: ${position}, removed: ${removed}, added: ${added}`);
/* Items are added and removed from the same position, so the removals
* must be handled first.
*
* NOTE: remember that the items have already changed in the model when this
* signal is emitted, so you can not query removed items.
*/
while (removed--)
console.log('removed an item');
/* Once the removals have been processed, the additions must be inserted
* at the same position.
*/
for (let i = 0; i < added; i++)
console.log('added an item');
});
Because the entries in a Gio.MenuModel
may be nested, either as sections or submenus, it may be necessary to iterate items. This is not typically something you will want to do, but may help understand how menu models work.
import Gio from 'gi://Gio';
const menuModel = new Gio.Menu();
/*
* Get an attribute iterator for the item at index `0`
*/
const attrIter = menuModel.iterate_item_attributes(0);
while (attrIter.next()) {
const attributeName = attrIter.get_name();
const value = attrIter.get_value();
let icon = null;
switch (attributeName) {
/*
* This is the label of the menu item.
*/
case Gio.MENU_ATTRIBUTE_LABEL:
console.log(`${attributeName}: "${value.unpack()}"`);
break;
/*
* Icons must be deserialized from GVariant to GIcon.
*/
case Gio.MENU_ATTRIBUTE_ICON:
icon = Gio.Icon.deserialize(value);
console.log(`${attributeName}: ${icon.$gtype.name}`);
break;
/*
* This is the GAction name (e.g. `quit`), but does not include the
* namespace or scope (e.g. `app`). The full action name is something
* like `app.quit`, although action names may also contain periods.
*/
case Gio.MENU_ATTRIBUTE_ACTION:
console.log(`${attributeName}: "${value.unpack()}"`);
break;
/*
* This is the GAction namespace (e.g. `app`), which should combined
* with the GAction name (e.g. `${actionNamespace}.${actionName}`).
*/
case Gio.MENU_ATTRIBUTE_ACTION_NAMESPACE:
console.log(`${attributeName}: "${value.unpack()}"`);
break;
/*
* This is the activatable parameter, or stateful value of the action.
*/
case Gio.MENU_ATTRIBUTE_TARGET:
console.log(`${attributeName}: ${value.print(true)}`);
break;
/*
* Handling custom attributes will require understanding how they are
* intended to be used.
*/
case 'my-custom-attribute':
default:
console.log(`${attributeName}: ${value.print(true)}`);
break;
}
}
/*
* Get a link iterator for the item at index `0`.
*
* Links associate sections and submenus with a particular item.
*/
const linkIter = menuModel.iterate_item_links(0);
while (linkIter.next()) {
const linkName = linkIter.get_name();
const value = linkIter.get_value();
switch (linkIter) {
/*
* This is a menu section, an instance of GMenuModel. Sections take the
* place of a menu item, unlike submenus.
*/
case Gio.MENU_LINK_SECTION:
console.log(`${linkName}: ${value.$gtype.name}`);
break;
/*
* This is a submenu, an instance of GMenuModel. Submenus are associated
* with a menu item, unlike sections which are displayed in place of the
* item.
*/
case Gio.MENU_LINK_SUBMENU:
console.log(`${linkName}: ${value.$gtype.name}`);
break;
/*
* Handling custom link types will require understanding how they are
* intended to be used.
*/
case 'my-custom-link':
default:
console.log(`${linkName}: ${value.$gtype.name}`);
break;
}
}