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 applicationnv.MenuEntry.close_window()nv.MenuEntry.minimize()/nv.MenuEntry.maximize()nv.MenuEntry.full_screen()— enters full screen; pair it withrestore()to offer the way backnv.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
(
⌘Son macOS,Ctrl+Selsewhere). - 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:
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:
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:
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 mountedMenuBarAreacollapses to zero size. With several windows the bar follows the focused window's menu; a window withmenu=Noneshows the main window's. An application menu is synthesized automatically — aMenuEntry.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.