Menus and tray
Menus are built in the main process with @owear/core and dispatched back to
the main process. Tray icons reuse the same Menu template.
Building a menu
import { Menu } from '@owear/core'
const menu = Menu.buildFromTemplate([
{
label: 'File',
submenu: [
{ label: 'New', accelerator: 'CmdOrCtrl+N', click: () => createNote() },
{ type: 'separator' },
{ role: 'quit' },
],
},
{
label: 'View',
submenu: [
{ label: 'Sidebar', type: 'checkbox', checked: true, click: (mi) => toggle(mi.checked) },
{ type: 'separator' },
{ role: 'toggleDevTools' },
{ role: 'reload' },
],
},
])
Item options
interface MenuItemConstructorOptions {
id?: string
label?: string
role?: MenuItemRole
type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'
checked?: boolean
enabled?: boolean
visible?: boolean
accelerator?: string
sublabel?: string
toolTip?: string
submenu?: Array<MenuItemConstructorOptions | MenuItem> | Menu
click?: (menuItem: MenuItem, window: BrowserWindow | undefined) => void
}
Roles
undo redo cut copy paste pasteAndMatchStyle selectAll delete reload forceReload toggleDevTools resetZoom zoomIn zoomOut togglefullscreen minimize close quit about hide hideOthers unhide
Roles are handled in the main process (e.g. quit calls app.quit(), copy
runs document.execCommand('copy') in the focused window). If you set click,
it takes precedence over role.
Accelerators
accelerator accepts Electron-style strings. CmdOrCtrl maps to Ctrl on
Linux and Windows.
{ label: 'Save', accelerator: 'CmdOrCtrl+S', click: save }
Application menu
Menu.setApplicationMenu(menu) // menubar
Menu.setApplicationMenu(null) // remove
Menu.getApplicationMenu()
Linux: the application menubar is a no-op by design. GNOME does not use a global menubar, so apps draw their own in web code. Accelerators still work.
Context menu (popup)
app.handle('show.context', async () => {
Menu.buildFromTemplate([
{ label: 'Rename', click: () => rename() },
{ type: 'separator' },
{ role: 'copy' },
]).popup({ window: win })
return null
})
popup({ window?, x?, y?, items? }) can also take items directly, so you do
not have to build a Menu first:
menu.popup({ window: win, items: [{ label: 'Copy', role: 'copy' }] })
On Linux the popup is positioned at the current pointer because the click arrives asynchronously from the renderer (there is no native trigger event).
Click routing
Clicks from a popup are delivered to the main process (the handler runs) and also
broadcast as menu.click. Popup items created from a template are registered by
id, so the main-process dispatcher can find the right MenuItem.
Tray
import { Tray, Menu, nativeImage } from '@owear/core'
const tray = new Tray(nativeImage.createFromPath('icon.png'))
tray.setToolTip('My App')
tray.setTitle('My App')
tray.setContextMenu(Menu.buildFromTemplate([
{ label: 'Show window', click: () => win.show() },
{ type: 'separator' },
{ role: 'quit' },
]))
tray.on('click', () => win.show())
tray.on('right-click', () => console.log('right'))
tray.on('double-click', () => console.log('double'))
Tray API
new Tray(image?: NativeImage | string)
tray.setImage(image)
tray.setPressedImage(image)
tray.setToolTip(text)
tray.setTitle(text)
tray.setContextMenu(menu | null)
tray.popupContextMenu(menu?)
tray.destroy()
tray.id
There is one tray per app; creating a second Tray replaces the current one.
Platform notes
- Linux: implemented with the
org.kde.StatusNotifierItem+ dbusmenu standard, hand-rolled over GDBus (no libappindicator). It works on GNOME (with the appindicator extension), KDE, and XFCE with the SNI plugin. If no StatusNotifierWatcher is present, tray creation fails with a clear error. - Windows:
Shell_NotifyIcon(verify in CI).
The tray module is marked optional: if its system dependencies are missing it
is simply omitted from the build.
Next steps
menumodule reference andtray.MenuSDK reference andTray.