Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Menus and the tray

A menu bar in the window, menus that open as platform windows of their own, context menus named on any widget, and a tray icon the desktop draws from the same menu value.

A menu is a widget: a menu holds items and separators, and an item holding items is a submenu. Opening one is not a widget’s job. A menubar opens its own headings, and anything else goes through Menus.open(host, …), because opening needs a Host and a widget must not have one (ADR-0106).

A horizontal bar of headings, each an item whose children are its menu.

menubar id="app-menu" {
  item "File" {
    item press="app.new" accelerator="Ctrl+N" "New"
    item press="app.open" accelerator="Ctrl+O" "Open…"
    separator
    item press="app.quit" accelerator="Ctrl+Q" "Quit"
  }
  item "View" {
    item press="app.toggle-hud" accelerator="Ctrl+F" checked=#false "Frame rate"
    item "Theme" {
      item press="app.light" "Light"
      item press="app.dark" "Dark"
    }
  }
  item "Help" {
    item "Nothing here yet" disabled=#true
  }
}
new MenuBar(
        new Item("File").submenu(
                new Item("New", actions::create).accelerator("Ctrl+N"),
                new Item("Open…", actions::open).accelerator("Ctrl+O"),
                new Separator(),
                new Item("Quit", window::quit).accelerator("Ctrl+Q")
        ),
        new Item("View").submenu(
                new Item("Frame rate", window::toggleHud).accelerator("Ctrl+F").checked(hudShown),
                new Item("Theme").submenu(
                        new Item("Light", () -> actions.pick("light")),
                        new Item("Dark", () -> actions.pick("dark"))
                )
        ),
        new Item("Help").submenu(
                new Item("Nothing here yet").disabled(true)
        )
).id("app-menu");

The bar owns its menus. It opens a heading’s menu as a popup against the heading, swaps to the neighbour when the pointer runs along the bar with a menu down, and binds every accelerator its rows name while it is mounted (ADR-0163). A menu bar is a widget and not a property of the window, so a screen may have a second one.

Attributes

AttributeTypeDefaultWhat it does
id, classstringThe usual.

The children are items. One that has children is a heading. A child that is not an item is placed in the row unchanged.

Styling

The CSS type is menubar. Each heading is a menu-title, which matches :hover, :focus-visible, :active and :disabled, and carries the class open while its menu is down. The menus themselves are menu.

Keyboard

KeyDoes
F10, or a tap on Altopens the first heading, or closes the bar when a menu is open
Left, Rightwalk the headings, and swap menus while one is open
Enter, Space, Downopen the focused heading
Escapecloses one menu, then the bar

A bare Alt is a gesture rather than a shortcut, which is why it has a registration of its own (ADR-0223). There are no mnemonics.

Read more

A column of rows: the value a context menu, a tray and a heading all hold.

A menu of four rows: one with an icon and the accelerator Ctrl+T, two more with accelerators, a separator, and a row ending in a chevron for its submenu

Icons, accelerators, a separator and a submenu in one menu.

menu id="row-menu" {
  item press="app.rename" "Rename"
  item press="app.duplicate" "Duplicate"
  separator
  item press="app.delete" "Delete"
}
var rowMenu = new Menu(
        new Item("Rename", actions::rename),
        new Item("Duplicate", actions::duplicate),
        new Separator(),
        new Item("Delete", actions::delete)
);

Menus.open(host, "more-button", rowMenu).ifPresent(open -> this.menu = open);

A document declares a menu. Menus.open(host, anchorId, menu) measures it, places it under the node with that id, flips it above when it would run off the bottom of the screen, and opens it in a window of its own, so it may hang past the window’s edge (ADR-0104). The answer is empty when nothing with that id has been painted or the platform has no popup windows (ADR-0102). Choosing a command closes the whole stack. A menu too tall for the screen scrolls (ADR-0118).

Attributes

AttributeTypeDefaultWhat it does
id, classstringThe usual.

The children are items and separators.

Styling

The CSS type is menu. A row that has an open submenu carries the class open. When any row has an icon or is checkable, every row reserves an item-lead column so the labels line up. A row that leads to a submenu ends in an item-chevron. The row height is --gb-menu-item-height.

Keyboard

KeyDoes
Up, Downmove between rows
Enter, Spacerun the row, or open its submenu at once
Rightopens the submenu, or on a plain row moves to the next heading of a bar
Leftcloses a submenu, or moves to the previous heading of a bar
Escapecloses this menu and no more

The pointer opens a submenu after 150 ms of resting on its row, so a pointer crossing three rows on the way somewhere does not drop one out (ADR-0112).

Read more

item

One row: a label, an optional icon, an accelerator to show, a tick, and either a command or a submenu.

item icon="palette" press="app.toggle-theme" accelerator="Ctrl+T" checked=#true "Switch the light"
new Item("Switch the light", actions::toggleTheme)
        .icon(paletteIcon)
        .accelerator("Ctrl+T")
        .checked(true);

Attributes

AttributeTypeDefaultWhat it does
argumentstring""The label. A row with neither a label nor an icon is refused.
pressaction namenoneThe command.
iconicon namenoneAn icon in the lead column.
acceleratorstringnoneText shown right-aligned, and bound while a menubar holds the row.
checkedbooleanabsent#true shows a tick, #false is a checkable row that is off, and no attribute is a row that is not checkable.
disabledboolean#falseGreys the row and refuses it.
id, classstringThe usual.

A nested item is a submenu. That is the only thing an item can contain, and there is no submenu node. A row cannot be both a command and a heading.

In Java, new Item(label, onPress) is a command, new Item(label) has nothing behind it yet, and submenu(Widget...), icon, accelerator, checked(boolean), checkable() and disabled(boolean) set the rest.

Styling

The CSS type is item. It matches :hover, :focus-visible, :active and :disabled, and carries the class open while its submenu is. Its parts are item-lead and item-chevron. The accelerator text has no part of its own.

separator

A rule between groups of rows.

separator
new Separator();

It takes only id and class. The CSS type is separator, a 1px line in --gb-border.

Accelerators

An accelerator is written as text, Ctrl+S, Shift+Ctrl+Z, Primary+Q. The row shows the text as written. A menubar parses every accelerator under it with Shortcut.of and binds it on the window while the bar is mounted, so the key works with every menu shut. When the bar unmounts it gives back the keys it took and no others (ADR-0220).

Primary names the desktop’s own modifier, Cmd on macOS and Ctrl elsewhere, so one document fits both (ADR-0378). Ctrl, Control, Alt, Option, Meta, Cmd, Super and Win are the other spellings.

Warning

Only a menubar binds. A menu that is only ever opened, a context menu or a Menus.open call, shows its accelerators and registers none of them. Bind those with host.shortcut(…) yourself. A row that is disabled, has a submenu, or has no press is skipped.

Context menus

Any widget names its context menu, and the application says what the name means (ADR-0108).

panel context-menu="content" {
  text "Right-click anywhere in here."
}
@Override public void start(Host host) {
    Menus.contextMenus(host, Map.of("content", contentMenu()));
}

A right-click walks up from the widget under the pointer to the first ancestor with a context-menu and opens that menu at the pointer. The Menu key, and Shift+F10, open the same menu at the focused widget instead (ADR-0208). A name the application never registered is logged and ignored. A right-click on a list row selects it first (ADR-0224).

In Java the attribute is .contextMenu("content") on any widget, or .itemMenu(item -> "row-menu") on a list.

The tray icon

A tray icon is a menu somebody else draws. The application hands the desktop a tooltip, a picture and an ordinary Menu, and the desktop’s shell themes it, spaces it and clicks it (ADR-0191). There is no markup node for it.

private Optional<BackendTray> tray = Optional.empty();

@Override public void start(Host host) {
    var menu = new Menu(
            new Item("Switch the light", actions::toggleTheme),
            new Separator(),
            new Item("Screens").submenu(
                    new Item("Basic", () -> actions.pickScreen("basic")),
                    new Item("Panels", () -> actions.pickScreen("panels"))
            ),
            new Separator(),
            new Item("Quit", () -> host.window().close())
    );
    tray = Trays.show(host, TrayIcon.of("Goldberry — showcase", menu).icons(onLightShell, onDarkShell));
}

@Override public void stop() {
    tray.ifPresent(BackendTray::close);
}
CallWhat it does
TrayIcon.of(tooltip, menu)A tray with the platform’s default picture.
.icon(PixelBuffer)One picture, in physical pixels, 32×32 or 64×64.
.icons(forLightShell, forDarkShell)Two pictures, named for the panel each sits on. The tray follows the desktop’s theme while it is up, and shows the light-shell one where the desktop says nothing.
.tooltip(String)The hover text.
Trays.show(host, tray)Puts it up. Empty when this session has no notification area.
BackendTray.close()Takes it down. The tray outlives the process if you forget, so close it in stop().

Rows map as a desktop expects: a checkable item is a checkbox row, a nested item is a submenu, a disabled one is disabled. Icons and accelerators on rows are dropped with a warning, because the shell draws the rows and takes neither (ADR-0501). The menu cannot be changed in place. Close the tray and show a new one.

Note

The tray is SDL3’s. It has run for real on Linux under AppIndicator. The Windows and macOS paths are compiled and unverified (ADR-0191).

Read more