Menus
TL;DR: three surfaces, three mechanisms. The app menu bar takes
contributions from a plugin's configure(), guarded by isAbstractMenuManager;
a track menu and a right-click menu are the display model's trackMenuItems()
and contextMenuItems(), captured from the super method where the display is
defined and added to with addDisplayMenuItems from anywhere else. All three
carry the same MenuItem[].
Adding a top-level menu
JBrowse Web, Desktop and React App define File, Add and Tools. The menus
plugin adds Help with appendToMenu, which creates the menu when it is
absent. The embeddable single-view components have no menu bar at all, so guard
with isAbstractMenuManager.
Contribute from configure: the spreadsheet-view plugin registers its view type
in install and offers the way to open one in configure.
export default class SpreadsheetViewPlugin extends Plugin {
name = 'SpreadsheetViewPlugin'
install(pluginManager: PluginManager) {
SpreadsheetViewF(pluginManager)
LaunchSpreadsheetViewF(pluginManager)
}
configure(pluginManager: PluginManager) {
const { rootModel } = pluginManager
// configure also runs in the web worker, which has no rootModel — the
// guard is what keeps a menu contribution from throwing there
if (isAbstractMenuManager(rootModel)) {
rootModel.appendToMenu('Add', {
label: 'Spreadsheet view',
icon: ViewComfyIcon,
onClick: (session: AbstractViewContainer) => {
session.addView('SpreadsheetView', {})
},
})
}
}
}
Adding track menu items
A track's menu is self.displays.flatMap(d => d.trackMenuItems()), so a display
contributes by redefining trackMenuItems. It is a method, not a getter, so
capture the super version first — the same
super-capture pattern
as any extended MST view. This is the shape for a display's own menu, built
where the display is defined; to add to one belonging to another plugin, use
addDisplayMenuItems below.
.views(self => {
const { trackMenuItems: superTrackMenuItems } = self
return {
/**
* #method
*/
trackMenuItems() {
return [
...superTrackMenuItems(),
...buildMafTrackMenuItems(self),
...mafLaunchMenuItems({
session: getSession(self),
model: self,
view: getContainingView(self) as LinearGenomeViewModel,
}),
]
},
}
})
Adding track context-menu items
Right-clicking a linear track opens contextMenuItems, which can vary by
whether the click hit a feature and by which one.
To add items to a menu on a display you do not own, use addDisplayMenuItems
(addViewMenuItems for a view):
- Resolves the display type by name, and appends what your callback returns to what is already there.
- An array of names contributes the same items to several types from one
call. Your callback is then handed the union of their models, so it may only
use a menu they all have — a menu only one of them has is a compile error
rather than a
self[menu]that is undefined on the rest. groupcollects several plugins' entries into one submenu. Without it, each contribution becomes its own top-level row.undefinedfrom the callback adds nothing, which is how an item scoped to some state opts out.
export default function DotplotReadVsRefMenuItem(pluginManager: PluginManager) {
addDisplayMenuItems(pluginManager, 'LinearAlignmentsDisplay', {
menu: 'contextMenuItems',
group: LAUNCH_LABEL,
// Offered from the read id, which the hit test carries, so the item is
// there when the menu opens rather than a fetch later; the feature it needs
// is resolved in the onClick (normally already in hand, since the fetch
// rebuilds this menu).
items: self => {
const featureId = self.contextMenuFeatureId
const feature = self.contextMenuFeature
const track = getContainingTrack(self)
return featureId === undefined
? undefined
: {
label: 'Dotplot of read vs ref',
icon: AddIcon,
onClick: () => {
withContextMenuFeature(self, featureId, feature, feat => {
queueReadVsRefDialog({
node: self,
track,
feature: feat,
onSubmit: launchDotplotReadVsRef,
})
})
},
}
},
})
}
MenuItem
MenuItem is a discriminated union. Every menu-producing view returns
MenuItem[].
type | Own fields | Description |
|---|---|---|
checkbox | checked, onClick | a setting row with a checkbox; leaves the menu open |
custom | render | renders arbitrary React content inline, e.g. a slider |
divider | priority | a horizontal rule; not clickable |
normal | onClick | an action row; the default when type is omitted |
radio | checked, onClick | a setting row with a radio button; leaves the menu open |
subHeader | priority, label | a text label for a section of a menu; not clickable |
subMenu | subMenu | nests another MenuItem[], to any depth |
Every variant except divider and subHeader also takes these:
| Field | Type | Description |
|---|---|---|
id | string | stable identifier, for tests and for finding a row again |
label (required) | React.ReactNode | the row's text |
priority | number | sort weight within the menu; higher sorts earlier |
subLabel | string | secondary text under the label; prefer withHint in the label |
icon | React.ElementType | any MUI icon component |
disabled | boolean | renders the row unclickable |
helpText | string | tooltip shown from a help icon at the trailing edge |
disabledHelpText | string | tooltip shown when the item is disabled, in place of helpText |
keepMenuOpen | boolean | override the dismiss-on-click rule; see staysOpenOnClick |
endAdornment | React.ReactNode | arbitrary trailing content; prefer pin |
pin | MenuItemPin | the "make this the default for all tracks of this type" pin; set it with a promotable builder |
Clicking a row dismisses the menu unless it is a checkbox or radio, which
are settings and stay open. staysOpenOnClick is that rule, exported so a test
can assert the behavior.
Builders
Build rows with these. Import them from @jbrowse/core/ui/menuItems, a
React-free entry, so a state model or plugin menuItems module does not pull
the Material UI barrel into every host that installs the plugin.
| Builder | Description |
|---|---|
checkboxItem | one checkbox setting row |
makeRadioSubMenu | a radio group wrapped in a submenu row |
promotableRadioItem | radioItem plus a promote-to-default pin |
promotableRadioItems | radioItems plus a pin per option, from a factory over the value |
promotableToggleItem | checkboxItem plus a promote-to-default pin |
radioItem | one radio setting row; the singular of radioItems |
radioItems | a radio group, one row per option |
showLegendCheckboxItem | the shared "Show legend" checkbox |
toggleItem | a checkbox row whose setter takes the new value |
withHint | a row label carrying an aside that is only sometimes there |
withSubHeader | a section heading, present only if the section is |
Prefer the plural promotableRadioItems for a whole radio group: it takes the
pin as a factory over the option's value, so a group cannot end up one pin
short.
Root model menu API
Called from configure(), guarded by isAbstractMenuManager. A contribution is
recorded and merged in each time the menu opens, so none of these return
anything and one that throws costs your item rather than the app. A negative
position counts from the end.
| Action | Description |
|---|---|
setMenus(newMenus) | Replace the menu bar wholesale. Item contributions recorded before this one are dropped along with the menus they targeted, so a plugin adding to the existing bar wants appendToMenu instead. |
appendMenu(menuName) | Add a top-level menu, if the app bar does not already have one with this name. |
insertMenu(menuName, position) | Insert a top-level menu, if the app bar does not already have one with this name. |
appendToMenu(menuName, menuItem) | Add a menu item to a top-level menu, creating the menu if it does not exist. |
insertInMenu(menuName, menuItem, position) | Insert a menu item into a top-level menu, creating the menu if it does not exist. |
appendToSubMenu(menuPath, menuItem) | Add a menu item to a sub-menu, creating any part of the path that does not exist. |
insertInSubMenu(menuPath, menuItem, position) | Insert a menu item into a sub-menu, creating any part of the path that does not exist. |