PBToolboxAI v5 ← Site

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 #

Userobjectu_pbt_radialmenu
Item classn_pbt_radialmenu_item (of_item(keys))
Used forOffering a handful of frequent commands where the hand already is
PrincipleYou 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); of_clear, of_reset, of_close and a second of_show close the open wheel, and so does of_remove_item when the wheel showed the branch removed — ue_dismissed is raised at once, and of_is_open answers false as soon as the call returns.

// 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.

To open it from the keyboard — the Menu key, Shift+F10 —, call of_show_over from the control that has the focus, with ab_keyboard true: the wheel is centred on that control, wherever the mouse is, and its first branch that answers is lit — Enter chooses it, the arrows go on from there. of_show_over(control) without that second argument also serves a toolbar button: the wheel opens over it.


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 #

PropertyTypeDefaultRole
ii_max_sectorsinteger8How 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_stylestring""Visual style of the component (THEME_STYLE_* constants); empty = the application's, followed at every change
is_theme_modestring""Light or dark variant (THEME_MODE_* constants); empty = the application's, followed at every change
il_theme_accentlong-1Accent colour of this component (-1 = the application accent, or the theme's)

Methods #

MethodRole
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
of_close ( )Closes the wheel of the component if it is on screen, without a choice: ue_dismissed is raised at once, as for a click outside — even for a wheel asked for a moment ago and not drawn yet — and of_is_open answers false as soon as it returns. Nothing open, nothing happens. Returns 0 once done, -2 when the component is not created
of_is_open ( )true while a wheel of the component is on screen — opened, and not answered yet by a choice or a dismissal. of_show and of_close are applied before they return: it answers true right after of_show, false right after of_close
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. A wheel still on screen is replaced: it raises ue_dismissed first. 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
long of_show_over (dragobject ado_control)Opens the wheel centred on a control — a picture, a field, a toolbar button —, wherever the mouse is. Returns 0 once asked, -5 when the control is not valid, -2 when the component is not created
long of_show_over (dragobject ado_control, boolean ab_keyboard)Same, saying how the wheel was asked for: ab_keyboard true when the user opened it from the keyboard (Menu key, Shift+F10) — its first branch that answers is lit, Enter chooses it. Returns 0 once asked, -5 when the control is not valid, -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. A bare key is resolved as of_item does it. The wheel on screen closes (ue_dismissed) only when it showed that branch: removing a hidden one leaves it open. 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 #

EventFired 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 — or by the code: of_close, of_clear, of_reset, of_remove_item of a branch it showed, a second of_show that replaces it, an of_show that had nothing to show. Once per opening, at once: of_is_open already answers false

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 #

PropertyTypeDefaultRole
is_textstring""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_enabledbooleantrueSet to false and the branch is greyed, its sector ignoring every click. Reads back what you set: a branch greyed because none of its sub-entries is visible still reads true
ib_visiblebooleantrueSet 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_checkedbooleanfalseSet to true and a thin arc under the band marks the branch as being on
is_imagestring""The icon of the branch, changed in place — a Play that becomes a Pause. "" takes it away. Same paths as of_add_item (mono:, tint:, file.dll:NAME)
is_tonestring""The tone of the command: its label coloured by the theme, light or dark, at rest — u_pbt_radialmenu.TONE_DANGER for the one that destroys, TONE_WARN, TONE_SUCCESS, TONE_INFO. "" or TONE_NEUTRAL = the text of the theme; a colour of your own (il_text_color) wins
is_groupstring""A radio group: the branches of one group under the same parent are exclusive — checking one (ib_checked = true, or the user choosing it, before ue_item_selected) unchecks the others. "" = an ordinary branch
il_accentlong-1Accent of this branch: its band when pointed at, and its check mark (-1 = the component's)
il_back_colorlong-1Band of this branch at rest (-1 = the theme's)
il_text_colorlong-1Label colour of this branch (-1 = the theme's)
il_back_color_hoverlong-1Slice of this branch while it is pointed at (-1 = the theme's). Given without il_text_color_hover, it gets a text readable on it
il_text_color_hoverlong-1Label 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

From the keyboard, and closed by the code #

// Menu key on the grid: the wheel opens centred on it, first branch lit
uo_wheel.of_show_over(/*control*/ dw_orders, /*keyboard*/ true)

// Later, from a timer: the wheel closes without a choice (ue_dismissed)
if uo_wheel.of_is_open() then uo_wheel.of_close()

A radio group, a command that destroys #

// Three exclusive branches: choosing one unchecks the other two
uo_wheel.of_item(/*keys*/ "align/left").is_group = "align"
uo_wheel.of_item(/*keys*/ "align/center").is_group = "align"
uo_wheel.of_item(/*keys*/ "align/right").is_group = "align"

// Delete takes the tone of danger, light or dark
uo_wheel.of_item(/*keys*/ "delete").is_tone = u_pbt_radialmenu.TONE_DANGER

Good practice #

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.

MembersRoleDetailed in
of_count · of_keys_at · of_hasWalk what the component holds3.2 Items
of_resetPut the component back to zero3.6 Resetting a component: of_reset()
of_register_shortcut · of_clear_shortcutsThe component's keyboard chords3.5 Keyboard shortcuts
of_is_created · of_is_ready · of_get_last_errorWhether it was born, whether it is ready, what failed3.7 Diagnostics
of_save_as_png · of_save_as_jpgExport the rendering as an image3.8 Exporting the rendering as an image
of_set_redrawGroup changes into a single repaint3.10 Best practices
of_preload_iconsIcons shown with no delayInstant display: of_icon
of_set_translationTranslate one of the component's labels5.2 Adapting a label: of_set_translation
of_focus_webviewGive the component the focus6.4 Keyboard and focus
of_print · of_print_to_pdfPrint, or write a PDF6.9 Printing
of_set_property · of_get_property · of_component_nameDriving a property by its name3.1 The property engine
is_accessible_name · is_accessible_descriptionThe name and description a screen reader says3.4 Optional properties and events

On this invisible launcher, of_count and of_keys_at walk what you added, hidden branches included — not what the wheel will draw. Exporting as an image, printing, of_focus_webview and of_set_redraw have no useful effect here: the component draws nothing of its own, the wheel lives in its own window.

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.


← Component reference · Guide contents