radialmenu — u_pbt_radialmenu #
← Component reference · Guide contents
Radial context menu: the commands in a ring around the cursor, picked by direction rather than read down a list.
▶ See it live — Demo application, Radial menu tile: the preview, the code behind it and this page, side by side.
In brief #
| Userobject | u_pbt_radialmenu |
| Item class | n_pbt_radialmenu_item (of_item(keys)) |
| Used for | Offering a handful of frequent commands where the hand already is |
| Principle | You describe the branches; the wheel, the shape and the navigation are ours |
Quick start #
// A wheel built for whatever is under the cursor
uo_wheel.of_add_item(/*keys*/ "cut", /*text*/ "Cut")
uo_wheel.of_add_item(/*keys*/ "copy", /*text*/ "Copy")
uo_wheel.of_add_item(/*keys*/ "paste", /*text*/ "Paste")
uo_wheel.of_add_item(/*keys*/ "delete", /*text*/ "Delete")
// It opens when the right button is released: see ue_rclicked
Open, then choose #
The wheel opens when the right button is released, and a second click is what chooses. That is not an implementation detail: were it to open on the press, the release of that very click would immediately pick whatever sector had landed under the cursor, and the user would fire a command without ever seeing it.
That is why of_show() is called from the right-click event of the control the user made the gesture on (a statictext, a button, a grid…): by then the button is already up, and the next click really is the one that chooses. The radial menu itself has no surface — so no mouse event of its own.
The component itself is invisible: it takes no room in the window. Drop it anywhere, give it zero width and height — it only exists while the wheel is up.
The pointed branch — by the mouse or the keyboard — shows its whole label, above its neighbours: a sector keeps two lines at most (one past eight branches), and that is what lets it carry a short label without lying about what it does. The hub in the middle is the way out: clicking it closes the wheel; inside a sub-wheel, it climbs one level back. While the wheel is open the application keeps running: a branch removed, greyed or hidden in the meantime can no longer be chosen (ue_dismissed instead), and of_clear, of_remove_item or of_reset close the open wheel.
// It opens when the right button is released: see ue_rclicked
uo_wheel.of_show()
From the keyboard, the arrows walk the branches, stepping over greyed ones, Enter or Space chooses, ← and Backspace climb back one level, and Escape climbs one level out of a sub-wheel — it only closes the wheel at the root. None of these keys reaches the application while the wheel is open. Read right to left, the ring turns the other way and ← and → trade places: → climbs back one level.
Sub-wheels #
An address hangs branches under another one: export/pdf. Choosing the parent branch chooses nothing: the wheel is replaced by a wheel of its children, and the hub becomes the way back.
Why replace rather than add a second ring? Because an outer ring would halve the sectors at every level. Eight branches is already the readable maximum; there is no room to show two levels at once.
The ue_item_selected event reports the full address (export/pdf), not the bare leaf key. Two sub-wheels are therefore free to name their branches alike.
// A branch, then two entries at its address
uo_wheel.of_add_item(/*keys*/ "export", /*text*/ "Export")
uo_wheel.of_add_item(/*keys*/ "export/pdf", /*text*/ "PDF")
uo_wheel.of_add_item(/*keys*/ "export/csv", /*text*/ "CSV")
To remove one branch — its sub-wheel with it — without rebuilding the whole wheel: of_remove_item("export/csv"). The other branches stay in place.
When there are too many branches #
A wheel is read by direction, and past eight sectors the slices stop being distinguishable. ii_max_sectors sets that ceiling (3 to 12, 8 by default).
Branches beyond it are not lost: the last place in the ring becomes a branch holding all of them, which opens as a sub-wheel. A menu that dropped its tail would be a menu lying about what it offers.
Tightening the ring is often a gain: four wide branches are quicker to aim at than eight narrow ones.
Properties #
| Property | Type | Default | Role |
|---|---|---|---|
ii_max_sectors | integer | 8 | How many branches one ring may carry (3 to 12). Whatever exceeds it moves under a last branch that opens as a sub-wheel. Past eight, the wheel tightens (smaller icon, one-line label) so the labels do not overlap |
is_theme_style | string | "" | Visual style of the component (THEME_STYLE_* constants); empty = the application's, followed at every change |
is_theme_mode | string | "" | Light or dark variant (THEME_MODE_* constants); empty = the application's, followed at every change |
il_theme_accent | long | -1 | Accent colour of this component (-1 = the application accent, or the theme's) |
is_tooltip | string | "" | Plain tooltip. Kept for compatibility: the launcher is invisible, it has no surface to hover, and nothing ever shows it |
is_super_tooltip_title | string | "" | Title of the rich tooltip. Kept for compatibility, never shown — see is_tooltip |
is_super_tooltip_text | string | "" | Text of the rich tooltip. Kept for compatibility, never shown — see is_tooltip |
is_super_tooltip_image | string | "" | Image of the rich tooltip. Kept for compatibility, never shown — see is_tooltip |
Methods #
| Method | Role | |
|---|---|---|
of_add_item (string as_keys, string as_text) | Adds a branch at its address: format/strike hangs under Format, whose parent must already exist. That same address comes back in ue_item_selected when it is chosen. Returns 0 once added, -5 when the address is refused (parent not found, address already taken, an empty level such as export/ or /pdf, a key holding ` | or starting with __), -2` when the component is not created |
of_add_item (string as_keys, string as_text, string as_image, boolean ab_checked) | Same, with the icon and the mark — the icon third, as everywhere else in the library. A branch is greyed through its handle: of_item(keys).ib_enabled = false. Returns 0 once added, -5 when the address is refused (parent not found, address already taken, an empty level such as export/ or /pdf, a key holding ` | or starting with __), -2` when the component is not created |
of_clear ( ) | Empties the wheel and frees the handles obtained through of_item (each one designated a branch that no longer exists). Returns 0 once applied, -2 when the component is not created | |
long of_show ( ) | Opens the wheel centred on the cursor. Call it from the right-click of the control that receives the gesture. A wheel with no visible branch does not open: ue_dismissed is raised. Returns 0 once asked, -2 when the component is not created | |
long of_show (long al_x, long al_y) | Same, centred on a screen position in pixels. Negative coordinates are a real point, (-1, -1) included: a screen placed to the left of or above the main one. Returns 0 once asked, -2 when the component is not created | |
of_item (string as_keys) | Returns a branch handle, by its address, so you can change it later (label, state, colours). A bare key is resolved into the full address of the one branch that carries it: of_item("csv") and of_item("import/csv") then return the same handle. Ambiguous, or carried by no branch, it returns a handle that changes nothing rather than the wrong branch. The handle's of_key() returns the key of the last level | |
of_remove_item (string as_keys) | Removes one branch and its sub-wheel; the others stay. Returns 0 once removed, -5 when no branch lives at that address, -2 when the component is not created | |
of_reset ( ) | Empties the wheel and puts every property back to its default. Returns 0 once applied, -2 when the component is not created |
Events #
| Event | Fired when |
|---|---|
ue_item_selected (string as_keys) | The user chose a branch. as_keys is its full address (export/pdf), not the bare leaf key. One event per gesture: a double click chooses only once |
ue_dismissed ( ) | The wheel closed without any branch being chosen: a click on the hub, a click outside the wheel, Escape at the root — and also when of_show had nothing to show |
A click in a corner of the window, outside the disc, counts as a click outside: the wheel closes (
ue_dismissed) and the click goes through to the application behind, instead of being swallowed by an invisible square.
Item properties #
| Property | Type | Default | Role |
|---|---|---|---|
is_text | string | "" | Branch label. Keep it short: a sector is a slice, not a row, and shows two lines at most — the pointed branch shows its whole label. Empty, the key is shown |
ib_enabled | boolean | true | Set to false and the branch is greyed, its sector ignoring every click |
ib_visible | boolean | true | Set to false and the branch leaves the wheel — sub-wheel included — without being removed; the sectors close up, and it comes back as it was. A branch whose sub-entries are all hidden stays on the wheel, greyed: it has nothing left to open and cannot be chosen as a leaf |
ib_checked | boolean | false | Set to true and a thin arc under the band marks the branch as being on |
il_accent | long | -1 | Accent of this branch: its band when pointed at, and its check mark (-1 = the component's) |
il_back_color | long | -1 | Band of this branch at rest (-1 = the theme's) |
il_text_color | long | -1 | Label colour of this branch (-1 = the theme's) |
il_back_color_hover | long | -1 | Slice of this branch while it is pointed at (-1 = the theme's) |
il_text_color_hover | long | -1 | Label colour of this branch while it is pointed at (-1 = the theme's) |
A branch shows no tooltip: the round window has no room outside its disc. The is_tooltip the handle inherits is kept and reads back, but it is never shown — the pointed branch is what shows its whole label.
Examples #
One wheel per context #
// A wheel built for whatever is under the cursor
uo_wheel.of_clear()
uo_wheel.of_add_item(/*keys*/ "bold", /*text*/ "Bold", /*image*/ "mono:img\packimages.dll:svg/samples/bold", /*checked*/ true)
uo_wheel.of_add_item(/*keys*/ "find", /*text*/ "Find", /*image*/ "mono:img\packimages.dll:svg/samples/find", /*checked*/ false)
Acting on the chosen path #
// ue_item_selected event of the radial menu
// as_keys carries the full path, e.g. "export/pdf"
choose case as_keys
case "export/pdf"
of_export_pdf()
case "delete"
of_delete_selection()
end choose
Mark, grey out, tighten #
// Mark a branch as being on
uo_wheel.of_item(/*keys*/ "bold").ib_checked = true
// Grey out the one that makes no sense here
uo_wheel.of_item(/*keys*/ "paste").ib_enabled = false
// Four branches at most on one ring
uo_wheel.ii_max_sectors = 4
Good practice #
- Open on the control's RELEASED right-click, never on the press. That is what separates opening from choosing, and the user needs both.
- Short labels. One or two words. The sector is there for the direction; the whole text only reads once the branch is pointed at.
- Four to six branches beat eight. A wheel is remembered by position; the fewer positions, the faster they are learnt.
- Put the most frequent commands at the top and the bottom. Those are the two directions the hand reaches without thinking.
- Keep the order stable from one opening to the next: the whole point of a wheel is that the gesture ends up preceding the reading.
- A radial menu does not replace a list menu. Twenty rare commands read better in a list; keep the wheel for the handful used constantly.
Inherited from the common base #
These members exist on every visual component — they are not specific to this one. They are detailed once, in the transverse chapters; this table only says where to read them.
| Members | Role | Detailed in |
|---|---|---|
of_count · of_keys_at · of_has | Walk what the component holds | 3.2 Items |
of_reset | Put the component back to zero | 3.6 Resetting a component: of_reset() |
of_register_shortcut · of_clear_shortcuts | The component's keyboard chords | 3.5 Keyboard shortcuts |
of_is_created · of_is_ready · of_get_last_error | Whether it was born, whether it is ready, what failed | 3.7 Diagnostics |
of_save_as_png · of_save_as_jpg | Export the rendering as an image | 3.8 Exporting the rendering as an image |
of_set_redraw | Group changes into a single repaint | 3.10 Best practices |
of_preload_icons | Icons shown with no delay | Instant display: of_icon |
of_set_translation | Translate one of the component's labels | 5.2 Adapting a label: of_set_translation |
of_focus_webview | Give the component the focus | 6.4 Keyboard and focus |
of_print · of_print_to_pdf | Print, or write a PDF | 6.9 Printing |
of_set_property · of_get_property · of_component_name | Driving a property by its name | 3.1 The property engine |
Two helpers are not inherited: of_icon and of_escape_markup live on n_pbt_utils. Declare one — n_pbt_utils lnv_utils, nothing to create — and call them on it.