Skip to content

Menu Bar

nv.Window(menu=...) gives a window a menu bar. The menu is a declarative model registered on the window — not widgets in the tree — and renders as a bar at the top of the content area, below the window chrome:

import nuiitivet.material as nv


class EditorState:
    """The menu binds to this, so it lives outside the widget tree."""

    def __init__(self) -> None:
        self.can_save = nv.Observable(False)
        self.word_wrap = nv.Observable(False)

    ...


class Editor(nv.ComposableWidget):
    def __init__(self, state: EditorState) -> None:
        super().__init__()
        self.state = state

    ...


state = EditorState()
app = nv.App(
    nv.Window(
        content=lambda: Editor(state),
        title="Editor",
        menu=nv.MenuBar([
            nv.MenuEntry("File", submenu=[
                nv.MenuEntry("Open...", shortcut="Accel+O", on_select=state.open),
                nv.MenuEntry("Save", shortcut="Accel+S",
                               on_select=state.save, enabled=state.can_save),
                nv.MenuEntry.separator(),
                nv.MenuEntry.quit(),
            ]),
            nv.MenuEntry("View", submenu=[
                nv.MenuEntry("Word Wrap", on_select=state.wrap_changed,
                               checked=state.word_wrap),
            ]),
        ]),
    ),
)
app.run()

A runnable demo is at samples/window/menu_bar.py.

Items

One type, nv.MenuEntry, covers every role:

Role Construction
Action nv.MenuEntry("Open...", on_select=..., shortcut="Accel+O")
Submenu nv.MenuEntry("File", submenu=[...]) — nesting is unlimited
Separator nv.MenuEntry.separator()
Standard item nv.MenuEntry.quit() and friends

An item is exactly one of these; the constructor raises on any other combination (e.g. on_select together with submenu). on_select is called with no arguments and may be sync or async.

Standard items

Standard items are prebuilt commands — no on_select needed — whose labels and accelerators follow platform conventions (quit() is "Quit ⌘Q" on macOS and "Exit" elsewhere):

  • nv.MenuEntry.quit() — exit the application
  • nv.MenuEntry.close_window()
  • nv.MenuEntry.minimize() / nv.MenuEntry.maximize()
  • nv.MenuEntry.full_screen() — enters full screen; pair it with restore() to offer the way back
  • nv.MenuEntry.restore() — exit full screen / restore the pre-maximize size / bring a minimized window back

label, shortcut and enabled are overridable on each factory.

Shortcuts

shortcut takes the same spec strings as key_shortcut()"Accel+S", "Ctrl+Shift+Z" — or a Shortcut value. The one declaration does both jobs:

  • The accelerator is displayed next to the item, in the platform's form (⌘S on macOS, Ctrl+S elsewhere).
  • The gesture fires the item app-wide, without opening the menu. A disabled item does not fire.

Do not also register the same gesture with key_shortcut() — the menu item is the registration.

Reactive properties

label and enabled accept an Observable and update the rendered menu live. checked makes the item checkable and must be a writable Observable[bool]: activating the item toggles the value first, then calls on_select, and the check mark follows the observable from anywhere.

Structure is not observable. To add or remove items, assign a whole new model — item properties keep updating live in between:

app.menu = nv.MenuBar([...])   # wholesale replacement

Acting on the focused pane

A shared entry — one File > Save over several open documents — must act on whichever pane the user is working in. Hold that in app state: an observable each pane writes when it gains focus, read by the entry's on_select:

class State:
    def __init__(self) -> None:
        self.notes = Document("notes.txt")
        self.draft = Document("draft.txt")
        self.active = nv.Observable(self.notes)

    def save(self) -> None:
        document = self.active.value
        ...


class Pane(nv.ComposableWidget):
    def __init__(self, state: State, document: Document) -> None:
        super().__init__()
        self.state = state
        self.document = document

    def build(self) -> nv.Widget:
        return nv.TextField(
            value=self.document.text,
            label=self.document.name,
            on_focus_change=self._on_focus_change,
        )

    def _on_focus_change(self, focused: bool, source: nv.FocusSource) -> None:
        if focused:
            self.state.active.value = self.document


menu = nv.MenuBar([
    nv.MenuEntry("File", submenu=[
        nv.MenuEntry(
            state.active.map(lambda d: f"Save {d.name}"),  # the label names its target
            shortcut="Accel+S",
            on_select=state.save,
        ),
    ]),
])

Only focused=True writes: focus moving to the menu itself — or anywhere else — leaves active on the last pane, which is exactly what Save should hit. The entry's shortcut rides along, so Accel+S saves the focused pane with no per-pane binding.

A runnable demo is at samples/window/menu_bar_active_pane.py.

Placement

By default the bar appears at the top of the content area, under either OSChrome or a CustomChrome header. To render it somewhere else — say inside a custom title bar, VS Code-style — mount nv.MenuBarArea there:

chrome = nv.CustomChrome(
    header=nv.Row(children=[
        nv.Text("Editor"),
        nv.MenuBarArea(),
    ]),
)

A mounted MenuBarArea suppresses the automatic insertion; the model stays on the window either way, so menus, callbacks and shortcuts are unaffected by placement. An area with no registered menu renders nothing.

Keyboard

With a menu open: Up/Down rove the items, Left/Right walk into and out of a submenu — and, at the top level, switch to the neighboring menu — Enter activates, Escape closes. A focused bar title opens its menu with Down or Enter.

Styling

Geometry and per-instance colors live in nv.MenuBarStyle, attached to the model:

nv.MenuBar(items, style=nv.MenuBarStyle(bar_height=40))

Colors come from the active theme, so the bar and its popups follow light/dark switching automatically. To override individual colors for one menu bar, set the corresponding MenuBarStyle fields (bar_background, popup_background, ...); a field left None follows the theme.

Platform notes

  • Windows / Linux — the bar is drawn in-app, below the chrome (or at a MenuBarArea), as described above.
  • macOS — the same model goes to the global menu bar (NSMenu); nothing is drawn in the window and a mounted MenuBarArea collapses to zero size. With several windows the bar follows the focused window's menu; a window with menu=None shows the main window's. An application menu is synthesized automatically — a MenuEntry.quit() found in one of your menus is relocated into it, and one is added if you have none. Accelerators become native key equivalents (⌘S). No platform branching is needed in app code.