PBToolboxAI v4 ← 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), 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 #

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)
is_tooltipstring""Plain tooltip. Kept for compatibility: the launcher is invisible, it has no surface to hover, and nothing ever shows it
is_super_tooltip_titlestring""Title of the rich tooltip. Kept for compatibility, never shown — see is_tooltip
is_super_tooltip_textstring""Text of the rich tooltip. Kept for compatibility, never shown — see is_tooltip
is_super_tooltip_imagestring""Image of the rich tooltip. Kept for compatibility, never shown — see is_tooltip

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
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 #

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 — 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 #

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
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
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)
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

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

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