PBToolboxAI v4 ← Site

listbar — u_pbt_listbar #

← Component reference · Guide contents

Side navigation bar: collapsible accordion sections holding entries with icons, an accent band on the current entry, and collapsing into an icon rail.

▶ See it live — Demo application, Listbar tile: the preview, the code behind it and this page, side by side.


At a glance #

Userobjectu_pbt_listbar
Item classesn_pbt_listbar_section (section) → n_pbt_listbar_item (entry)
Used forReplacing a side menu improvised out of buttons with structured, themed, collapsible navigation
Opt-in optionsib_auto_width, ib_reorderable

This is the only component in the library that publishes ib_auto_width: its natural width actually means something, since the collapsed rail is far narrower than the expanded bar. The common case is in fact already covered without enabling anything — ib_collapsed = true shrinks the bar down to the rail, and gives its width back when it expands.


Quick start #

// window open event

// A section, then its entries : each entry is addressed by section/entry
uo_nav.of_add_section(/*key*/ "nav", /*text*/ "Navigation")
uo_nav.of_add_item(/*keys*/ "nav/home",   /*text*/ "Home",      /*image*/ "mono:img\home.svg")
uo_nav.of_add_item(/*keys*/ "nav/docs",   /*text*/ "Documents", /*image*/ "mono:img\doc.svg")
uo_nav.of_add_item(/*keys*/ "nav/search", /*text*/ "Search",    /*image*/ "mono:img\find.svg")

// A second section, with a single entry
uo_nav.of_add_section(/*key*/ "settings", /*text*/ "Settings")
uo_nav.of_add_item(/*keys*/ "settings/prefs", /*text*/ "Preferences", /*image*/ "mono:img\gear.svg")

// The selection is set on the ENTRY, by its address : the section, then the entry
uo_nav.of_select_item(/*keys*/ "nav/home")

// of_select_item raises ue_selection_changed, like a click : the first page opens there
// ue_selection_changed event of uo_nav : (string as_from_keys, string as_keys)
choose case as_keys
    case "nav/home"       ; of_open_home()
    case "nav/docs"       ; of_open_documents()
    case "settings/prefs" ; of_open_preferences()
end choose

Two levels, one address #

An entry identifier is only unique within its section, so an entry is named by its address: the section, then the entry, joined by / — "nav/docs". The component is what takes it (of_item, of_select_item, of_remove_item…); a section is named by its key alone (of_section("nav")). See Hierarchies.

// A section by its key, an entry by its address
uo_nav.of_section(/*key*/ "nav").is_title = "Navigation"
uo_nav.of_item(/*keys*/ "nav/docs").is_text = "Documents"

The events also carry the full path, and the entry left with it: ue_selection_changed(as_from_keys, as_keys).


Properties #

PropertyTypeDefaultPurpose
ib_collapsedbooleanfalsetrue collapses the bar into an icon rail: section titles and labels give way to a thin rule between sections, and every entry stays visible, even in a folded section (the fold comes back on the way out). An entry with no icon shows its initial, and each entry tells its label on hover
ib_auto_widthbooleanfalseOpt-in: same for the width, including when expanded (the bar sizes itself to the longest label). Collapsing into the rail already shrinks by itself; ue_auto_width follows in both cases
ib_reorderablebooleanfalseOpt-in: the user can drag an entry to another position. The move stays inside its section — an entry id is only unique there, so crossing would risk two identical keys (raises ue_item_reordered)
ib_veto_selectionbooleanfalseOpt-in: ask before the selection moves — a gesture of the user and of_select_item / of_clear_selection (raises ue_selection_changing, which can refuse; a refused order returns -4). Off by default, like every cancelable event of the library: set it to true when you need to refuse a move (unsaved changes). Each question of a gesture costs a round trip to PowerBuilder (~35 ms)
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""Simple tooltip shown when hovering the component
is_super_tooltip_titlestring""Title of the rich tooltip (takes precedence over is_tooltip)
is_super_tooltip_textstring""Text of the rich tooltip (rich markup accepted)
is_super_tooltip_imagestring""Image of the rich tooltip

Properties of a section — n_pbt_listbar_section #

PropertyTypeDefaultPurpose
is_titlestring""Section title. Accepts rich text markup. An empty title shows no header row at all: the section becomes a plain invisible grouping; a gap separates it from the section above (a thin rule in the rail), and it can no longer be folded — nobody could open it again
ib_collapsedbooleanfalseAccordion: true collapses the entries of this section. The header stays visible and its chevron rotates; ue_section_toggled follows, as for a click on the header
ib_pinnedbooleanfalsetrue pins the section at the bottom of the bar (Settings, Help): a footer that stays in view while the rest of the bar scrolls, pushed to the bottom when there is room to spare, and at the bottom of the rail too. Several pinned sections stack in their order. Only the drawing changes: the layout (of_get_layout) keeps the order of the sections, and the Down arrow goes from the last entry at the top to the first one of the footer
is_imagestring""Icon before the section title (accepted forms: path, mono:, tint:, DLL resource). An empty string removes it
il_accentlong-1Accent of this section, i.e. of its title and nothing else: its entries keep their own (-1 = the component's)
il_back_colorlong-1Background of the section title, as RGB(r,g,b) (-1 = the component's)
il_text_colorlong-1Text colour of the section title (-1 = the component's)
il_back_color_hoverlong-1Background of the title under the pointer (-1 = the component's)
il_text_color_hoverlong-1Text colour of the title under the pointer (-1 = the component's)

Properties of an entry — n_pbt_listbar_item #

PropertyTypeDefaultPurpose
is_textstring""Entry label, which can be changed on the fly without rebuilding the bar. Accepts rich text markup
is_imagestring""Icon, changeable on the fly (accepted forms: file path, mono:, tint:, DLL resource)
ib_enabledbooleantruefalse greys the entry: it stays in place and answers nothing — no click, no Enter, no right click, and the arrows skip it
ib_visiblebooleantruefalse hides the entry without removing it from the bar. A hidden entry cannot become the selection (of_select_item returns -5); hiding the one that is selected keeps it selected, and showing it again gives it back as it was
is_badgestring""Count pill at the end of the entry ("12" unread), in the theme's badge colours; in the rail, on the corner of the icon. Any short text, markup accepted. An empty string removes it
of_is_selected ( ) → boolean——Is this entry the selected one? Read only, and deliberately: the selection is one state of the whole bar, not a flag per entry. To set it, of_select_item on the component
is_tooltipstring""Simple tooltip shown when hovering the item
is_super_tooltip_titlestring""Title of the item rich tooltip (takes precedence over is_tooltip)
is_super_tooltip_textstring""Text of the item rich tooltip (rich markup accepted)
is_super_tooltip_imagestring""Image of the item rich tooltip
il_accentlong-1Accent of this entry: the bar that marks it when it is selected takes it (-1 = the component's). For "this entry in red", il_back_color / il_text_color
il_back_colorlong-1The entry's own background, as RGB(r,g,b) (-1 = what the theme gives)
il_text_colorlong-1Text colour of the entry (-1 = what the theme gives)
il_back_color_hoverlong-1Background of the entry under the pointer (-1 = what the theme gives)
il_text_color_hoverlong-1Text colour of the entry under the pointer (-1 = what the theme gives)

Methods #

On the component #

MethodPurpose
of_add_section (string as_key, string as_text)Adds a section at the end of the bar. An empty text draws no title line. The handle comes from of_section("nav") when you want to set a property. Returns 0 once applied, -5 when the key is empty, already taken or holds a / or a `, -2` when the component is not created
of_insert_section (string as_key, string as_text, integer ai_index)Adds a section at the rank you choose (first position = 1) rather than at the end. The index counts sections, not rows. A name already taken is refused: two sections under the same name would make the address of each of their entries ambiguous. Returns 0 once applied, -5 when the key is empty, already taken or holds a / or a `, -2` when the component is not created
of_move_section (string as_key, integer ai_index)Moves a section to rank ai_index, carrying its entries with it. Moving the header alone would drop its entries into whatever section then stands above them. Returns 0 once applied, -5 for a section this bar never added, -2 when the component is not created
of_remove_section (string as_key)Removes a section and everything it holds. Emptying it while keeping its entries would orphan them: they would carry a section identifier that names nothing. If the selection was in it, ue_selection_changed says there is none any more. Returns 0 once applied, -5 for a section this bar never added, -2 when the component is not created
of_section (string as_key)Handle of one section, by its key: its title, icon, fold and colours are set through it (created on first access). An entry is reached from the component instead, by its address (of_item)
of_add_item (string as_keys, string as_text, string as_image)Adds an entry at its address, "nav/home": the section it lands in, then its own identifier. An overload omits the icon. Returns 0 once added, -5 when the section was not added by this bar, when the address is already taken or when the key holds a `, -2` when the component is not created
of_item (string as_keys)Handle of one entry, by its address (created on first access)
of_insert_item (string as_keys, string as_text, integer ai_index)Inserts an entry at position ai_index within its section (first position = 1). Returns 0 once added, -5 when the section was not added by this bar, when the address is already taken or when the key holds a `, -2` when the component is not created
of_insert_item (string as_keys, string as_text, string as_image, integer ai_index)Same, with the entry's icon: of_add_item takes one, so inserting must too. Returns 0 once added, -5 when the section was not added by this bar, when the address is already taken or when the key holds a `, -2` when the component is not created
of_move_item (string as_keys, integer ai_index)Moves an existing entry within its section, preserving its state. Returns 0 once applied, -5 for an address this bar never added, -2 when the component is not created
of_remove_item (string as_keys)Removes one entry, designated by its address. If it was the selection, ue_selection_changed says there is none any more. Returns 0 once applied, -5 for an address this bar never added, -2 when the component is not created
of_clear ( )Empties the bar: every section and every entry. If an entry was selected, ue_selection_changed says there is none any more (an empty address); ue_layout_changed is not raised, you are rebuilding. Returns 0 once applied, -2 when the component is not created
of_select_item (string as_keys)Selects an entry, like a click on it: with ib_veto_selection on, ue_selection_changing is asked first; then ue_selection_changed follows, as for a click (right after your script, from the event queue), and of_selected_key() read right after already answers the new address. The entry already selected: nothing is asked nor raised. Returns 0 once selected (or already selected), -4 when your ue_selection_changing refused (nothing moves), -5 for an address this bar never added, a hidden entry or an entry the demo cap does not draw, -2 when the component is not created
of_get_layout ( )Reads the current arrangement back as JSON: the sections in order, each with its entries in order and whether it is folded. Store it (file, database, registry) and hand it back with of_set_layout at the next start. The same pair carries the same names on every component that can be rearranged
of_set_layout (string as_layout_json)Restores an arrangement read with of_get_layout or received with ue_layout_changed. What the layout does not name keeps its place at the end: a layout saved yesterday must not make what was added since disappear. Applying it raises no event — you supplied it. Returns 0 once applied, -5 for an empty text or one that is not a JSON object (a truncated file): nothing is sent; the fold of a section that is not there is ignored, -2 when the component is not created
of_clear_selection ( )Leaves no entry selected, like any move: with ib_veto_selection on, ue_selection_changing is asked first, then ue_selection_changed follows with an empty address. Nothing selected already: nothing is asked nor raised. Returns 0 once applied, -4 when your ue_selection_changing refused (the entry stays selected), -2 when the component is not created
of_selected_key ( )Address of the selected entry — "nav/docs", "" if there is none. It is exactly what ue_selection_changed hands you: a comparison is a comparison, not a reassembly. Always the current one, read from the component: after a click, and right after of_select_item too
of_reset ( )Empties the bar, then brings the component back to its brand-new state. If an entry was selected, ue_selection_changed goes with an empty address; ue_layout_changed is not raised. Returns 0 once applied, -2 when the component is not created
of_set_redraw (boolean)Groups a burst of changes into a single render. Returns 0 once applied, -2 when the component is not created
of_save_as_png (string) · of_save_as_jpg (string)Exports the rendering as an image. Returns 0 once the image is written, -4 when writing fails, -2 when the component is not created

On a section — n_pbt_listbar_section #

MethodPurpose
of_count ( )Number of entries in the section, hidden ones included
of_keys_at (long al_index)Address of the entry at rank al_index (from 1) — "nav/docs", ready to pass back to of_item
of_has (string as_key)Does the section hold this entry?

Events #

EventRaised when
ue_selection_changed (string as_from_keys, string as_keys)The selection has moved: the user picked an entry (click, Enter, Space), your code moved it (of_select_item, of_clear_selection — an order raises it too, like SelectItem raises SelectionChanged), or the selected entry left (of_remove_item, of_remove_section, of_clear, of_reset — as_keys is then empty). Same arguments as ue_selection_changing: the question and its outcome read the same way, and the as_from_* pair designates the entry left (empty if there is none)
ue_section_toggled (string as_key, boolean ab_collapsed)The user folds or opens a section: a click on its title, Enter on the title, or the Left / Right arrows (the other way round right to left), or your code setting a section's ib_collapsed
ue_item_reordered (string as_keys, integer ai_index)The user finished dragging an entry. ai_index is its new rank inside its section, starting from 1, like of_move_item. Keep that order to give the user their bar back as they left it
ue_layout_changed (string as_layout_json)The arrangement to store has changed: an entry or a section moved, a section was folded or opened, something was removed — by the user or by your code. Carries the whole layout, not just what moved: persisting it is one assignment. Adds, the rail, of_set_layout, of_clear and of_reset do not raise it: they are construction, not arrangement
ue_item_rclicked (string as_keys)The user right-clicked an entry: its address, "nav/docs". One event per right click, and the entry is not selected. Open your own menu there, at the pointer: m_menu.PopMenu(PointerX(), PointerY()). A greyed entry raises nothing
ue_selection_changing (string as_from_keys, string as_keys) → booleanCancelable, asked before the selection moves — a gesture of the user, of_select_item or of_clear_selection — and only when ib_veto_selection = true (off by default). Return false to stay where you are: nothing moves, and the order of your code returns -4
ue_auto_width (long al_width)The component has recomputed its ideal width — requires ib_auto_width = true
ue_ready ( )The component has finished loading; everything sent beforehand has been replayed
ue_runtime_missing ( )The WebView2 runtime is missing: the component stays empty
ue_bg_color (long al_color)The component has computed its theme background color; the userobject has already adopted it (backcolor)

Examples #

A complete side menu #

// The whole bar in a single render
uo_nav.of_set_redraw(/*on*/ false)

// First section and its three entries
uo_nav.of_add_section(/*key*/ "folders", /*text*/ "Folders")
uo_nav.of_add_item(/*keys*/ "folders/recent",    /*text*/ "Recent",    /*image*/ "mono:img\clock.svg")
uo_nav.of_add_item(/*keys*/ "folders/customers", /*text*/ "Customers", /*image*/ "mono:img\user.svg")
uo_nav.of_add_item(/*keys*/ "folders/archives",  /*text*/ "Archives",  /*image*/ "mono:img\box.svg")

// Second section and its two entries
uo_nav.of_add_section(/*key*/ "tools", /*text*/ "Tools")
uo_nav.of_add_item(/*keys*/ "tools/import", /*text*/ "Import", /*image*/ "mono:img\import.svg")
uo_nav.of_add_item(/*keys*/ "tools/export", /*text*/ "Export", /*image*/ "mono:img\export.svg")

// One render for everything, then the first entry is selected
uo_nav.of_set_redraw(/*on*/ true)
uo_nav.of_select_item(/*keys*/ "folders/recent")

A collapsible rail that frees up space #

// Collapse into an icon rail : the bar shrinks by itself (and gets its width
// back when you expand it).
uo_nav.ib_collapsed = true
// ue_auto_width event of uo_nav : (long al_width)
// The bar has just adopted its ideal width : realign whatever sits to its right.
uo_content.x     = uo_nav.x + al_width
uo_content.width = parent.width - uo_content.x

Accordion: collapsing a section #

// Collapse the archives section, which is rarely used
uo_nav.of_section(/*key*/ "folders").ib_collapsed = true
// ue_section_toggled event of uo_nav : (string as_key, boolean ab_collapsed)
// Remember the fold the user chose
of_save_preference("section_" + as_key, String(ab_collapsed))

Settings and Help pinned at the bottom #

A section without a title, pinned: no header line, two entries that stay at the foot of the bar even when the navigation, longer than the window, scrolls. A click there selects as anywhere else: ue_selection_changed receives foot/settings.

// A section WITHOUT a title (no header line), pinned at the bottom of the bar :
// it stays in view while the entries above it scroll
uo_nav.of_add_section(/*key*/ "foot", /*text*/ "")
uo_nav.of_add_item(/*keys*/ "foot/settings", /*text*/ "Settings", /*image*/ "mono:img\\gear.svg")
uo_nav.of_add_item(/*keys*/ "foot/help",     /*text*/ "Help",     /*image*/ "mono:img\\help.svg")
uo_nav.of_section(/*key*/ "foot").ib_pinned = true

Responding to navigation #

// ue_selection_changed event of uo_nav : (string as_from_keys, string as_keys)
// The address carries both levels : two sections may both have a "list" entry.
// Local variables
n_pbt_utils lnv_utils   // autoinstantiate : nothing to create, nothing to destroy
string ls_keys[]

// Split the address : [1] = the section, [2] = the entry
if lnv_utils.of_split_path(/*keys*/ as_keys, /*out*/ ls_keys) < 2 then return
choose case ls_keys[1]
    case "folders" ; of_open_folder(ls_keys[2])
    case "tools"   ; of_run_tool(ls_keys[2])
end choose

Refusing a change of selection #

// The question is off by DEFAULT : switch it on to be able to refuse a move
uo_nav.ib_veto_selection = true
// ue_selection_changing event of uo_nav :
//   (string as_from_keys, string as_keys)
// Returning FALSE keeps the user on the entry being left.
if of_has_unsaved_changes(as_from_keys) then
    MessageBox("Entry", "Finish the current record before navigating away.")
    return false
end if
return true

Updating an entry on the fly #

// Local variables
n_pbt_listbar_item lnv_entry

// The entry is named by its address : the section, then the entry
lnv_entry = uo_nav.of_item(/*keys*/ "folders/recent")
lnv_entry.is_text  = "Recent (12)"
lnv_entry.is_image = "mono:img\clock_full.svg"
// Gray out or hide according to permissions, without rebuilding the bar
uo_nav.of_item(/*keys*/ "tools/import").ib_enabled = of_is_allowed("import")
uo_nav.of_item(/*keys*/ "tools/export").ib_visible = gb_expert_mode

Selection driven from code #

// Move the selection elsewhere : the accent band follows, and
// ue_selection_changed goes as for a click
uo_nav.of_select_item(/*keys*/ "folders/customers")

// Or clear it completely
uo_nav.of_clear_selection()

Rebuilding the bar #

// of_clear empties sections and entries ; of_reset also restores the component defaults
uo_nav.of_clear()
of_build_menu_for_profile()

Best practices #

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