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

Userobject | `u_pbt_listbar`
Item classes | `n_pbt_listbar_section` (section) → `n_pbt_listbar_item` (entry)
Used for | Replacing a side menu improvised out of buttons with structured, themed, collapsible navigation
Opt-in options | `ib_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
    n_pbt_listbar_section lnv_section

    lnv_section = uo_nav.of_add_header(/*id*/ "nav", /*title*/ "Navigation")
    lnv_section.of_add_item(/*id*/ "accueil",   /*label*/ "Home",   /*icon*/ "mono:img\home.svg")
    lnv_section.of_add_item(/*id*/ "documents", /*label*/ "Documents", /*icon*/ "mono:img\doc.svg")
    lnv_section.of_add_item(/*id*/ "recherche", /*label*/ "Search", /*icon*/ "mono:img\find.svg")

    uo_nav.of_add_header(/*id*/ "config", /*title*/ "Settings") &
          .of_add_item(/*id*/ "preferences", /*label*/ "Preferences", /*icon*/ "mono:img\gear.svg")

    // The selection is set on the ENTRY, through its full path
    uo_nav.of_select_item("nav", "accueil")

    // ue_selection_changed event of uo_nav : (string as_from_section, string as_from_id,
    //                                        string as_section, string as_id)
    choose case as_section + "/" + as_id
        case "nav/accueil"       ; of_ouvrir_accueil()
        case "nav/documents"     ; of_ouvrir_documents()
        case "config/preferences"; of_ouvrir_preferences()
    end choose

## Two levels, a mandatory path #

An entry identifier is only unique within its section, so there is no shortcut straight to an entry. Every access goes through the section, which makes the code unambiguous — see Hierarchies.

    // Component  ->  section        ->  entry       ->  property
    uo_nav        .of_section("nav") .of_item("docs") .is_text = "Documents"

The events also carry the full path, and the entry left with it: `ue_selection_changed(as_from_section, as_from_id, as_section, as_id)`.

## Properties #

Property | Type | Default | Purpose
--- | --- | --- | ---
`ib_collapsed` | boolean | `false` | `true` collapses the bar into an icon rail: the labels disappear, the icons stay clickable
`ib_auto_width` | boolean | `false` | Opt-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_reorderable` | boolean | `false` | Opt-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_selection` | boolean | `true` | Ask before the selection moves (raises `ue_selection_changing`, which can refuse). On by default: scripting nothing always lets the move happen. Set it to `false` to drop the round trip to PowerBuilder (~35 ms) where it would show — keyboard navigation, a selection moved in a loop
`is_theme_style` | string | `fluent` | Visual style of the component (`THEME_STYLE_*` constants)
`is_theme_mode` | string | `light` | Light or dark variant (`THEME_MODE_*` constants)
`il_theme_accent` | long | `-1` | Accent color of this component (`-1` = the theme accent)
`is_tooltip` | string | `""` | Simple tooltip shown when hovering the component
`is_super_tooltip_title` | string | `""` | Title of the rich tooltip (takes precedence over `is_tooltip`)
`is_super_tooltip_text` | string | `""` | Text of the rich tooltip (rich markup accepted)
`is_super_tooltip_image` | string | `""` | Image of the rich tooltip

### Properties of a section — `n_pbt_listbar_section` #

Property | Type | Default | Purpose
--- | --- | --- | ---
`is_title` | string | `""` | Section title. Accepts rich text markup. An empty title shows no header row at all: the section becomes a plain invisible grouping
`ib_collapsed` | boolean | `false` | Accordion: `true` collapses the entries of this section. The header stays visible and its chevron rotates

### Properties of an entry — `n_pbt_listbar_item` #

Property | Type | Default | Purpose
--- | --- | --- | ---
`is_text` | string | `""` | Entry label, which can be changed on the fly without rebuilding the bar. Accepts rich text markup
`is_image` | string | `""` | Icon, changeable on the fly (accepted forms: file path, `mono:`, `tint:`, DLL resource)
`ib_enabled` | boolean | `true` | `false` grays the entry out and blocks its click
`ib_visible` | boolean | `true` | `false` hides the entry without removing it from the bar
`is_tooltip` | string | `""` | Simple tooltip shown when hovering the item
`is_super_tooltip_title` | string | `""` | Title of the item rich tooltip (takes precedence over `is_tooltip`)
`is_super_tooltip_text` | string | `""` | Text of the item rich tooltip (rich markup accepted)
`is_super_tooltip_image` | string | `""` | Image of the item rich tooltip

## Methods #

### On the component #

Method | Purpose
--- | ---
`of_add_header (string as_id, string as_text)` | Adds a section and returns its `n_pbt_listbar_section` handle, on which entry additions can be chained
`of_section (string as_id)` | Handle of an existing section (created on first access)
`of_insert_item (string as_section, string as_id, string as_text, integer ai_index)` | Inserts an entry at position `ai_index` within its section
`of_insert_item (string as_section, string as_id, string as_text, string as_image, integer ai_index)` | Same, with the entry’s icon: `of_add_item` takes one, so inserting must be able to take one too
`of_move_item (string as_section, string as_id, integer ai_index)` | Moves an existing entry within its section, preserving its state
`of_remove_item (string as_section, string as_id)` | Removes one entry, designated by its section / identifier pair
`of_clear ( )` | Empties the bar: every section and every entry
`of_select_item (string as_section, string as_id)` | Selects an entry — strictly equivalent to the user clicking it: `ue_selection_changing` is asked first, then `ue_selection_changed` announces the move
`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
`of_clear_selection ( )` | Leaves no entry selected. Announced like any other move
`of_selected_key ( )` | Identifier of the selected entry, `""` if there is none. Always the current one: a click and `of_select_item` both come back through `ue_selection_changed`
`of_selected_section ( )` | Section holding the selected entry — an entry id is only unique inside its section, so the pair identifies the selection. `""` if there is none
`of_reset ( )` | Empties the bar, then brings the component back to its brand-new state
`of_set_redraw (boolean)` | Groups a burst of changes into a single render
`of_save_as_png (string)` · `of_save_as_jpg (string)` | Exports the rendering as an image

### On a section — `n_pbt_listbar_section` #

Method | Purpose
--- | ---
`of_add_item (string as_id, string as_label, string as_icon_file)` | Adds a normal entry to this section and returns its handle
`of_item (string as_id)` | Handle of an entry in this section (created on first access)

## Events #

Event | Raised when
--- | ---
`ue_selection_changed (string as_from_section, string as_from_id, string as_section, string as_id)` | The selection has moved — by a click or through `of_select_item`. 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_section, boolean ab_collapsed)` | The user collapses or expands a section from its header
`ue_item_reordered (string as_section, string as_id, integer ai_index)` | The user finished dragging an entry. `ai_index` is its new rank inside its section, starting from 1. Keep that order to give the user their bar back as they left it
`ue_layout_changed (string as_layout_json)` | The arrangement changed — the user rearranged something, or your own code did. Carries the whole layout, not just what moved: persisting it is one assignment
`ue_selection_changing (string as_from_section, string as_from_id, string as_section, string as_id)` → boolean | Cancelable, raised before the selection moves. Raised by default; `ib_veto_selection = false` removes it. Return `false` to keep the user where they are
`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 #

    n_pbt_listbar_section lnv_dossiers, lnv_outils

    uo_nav.of_set_redraw(false)

    lnv_dossiers = uo_nav.of_add_header("dossiers", "Folders")
    lnv_dossiers.of_add_item("recents",  "Recent",  "mono:img\clock.svg")
    lnv_dossiers.of_add_item("clients",  "Customers",  "mono:img\user.svg")
    lnv_dossiers.of_add_item("archives", "Archives", "mono:img\box.svg")

    lnv_outils = uo_nav.of_add_header("outils", "Tools")
    lnv_outils.of_add_item("import", "Import", "mono:img\import.svg")
    lnv_outils.of_add_item("export", "Export", "mono:img\export.svg")

    uo_nav.of_set_redraw(true)
    uo_nav.of_select_item("dossiers", "recents")

### 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_contenu.x     = uo_nav.x + al_width
    uo_contenu.width = parent.width - uo_contenu.x

### Accordion: collapsing a section #

    // Collapse the archives section, which is rarely used
    uo_nav.of_section("archives").ib_collapsed = true

    // ue_section_toggled event of uo_nav : (string as_section, boolean ab_collapsed)
    of_enregistrer_preference("section_" + as_section, String(ab_collapsed))

### Responding to navigation #

    // ue_selection_changed event of uo_nav : (string as_from_section, string as_from_id,
    //                                        string as_section, string as_id)
    // The section is part of the key : two sections may both have a "liste" entry.
    choose case as_section
        case "dossiers" ; of_ouvrir_dossier(as_id)
        case "outils"   ; of_lancer_outil(as_id)
    end choose

### Refusing a change of selection #

    // The question is asked by DEFAULT: nothing to enable. This line does the
    // opposite, dropping it where arbitration is useless and the cost would show.
    uo_nav.ib_veto_selection = false

    // ue_selection_changing event of uo_nav :
    //   (string as_from_section, string as_from_id, string as_section, string as_id)
    // Returning FALSE keeps the user on the entry being left.
    if of_saisie_en_cours(as_from_section, as_from_id) then
        MessageBox("Entry", "Finish the current record before navigating away.")
        return false
    end if
    return true

### Updating an entry on the fly #

    n_pbt_listbar_item lnv_entree

    // The full path is mandatory : component -> section -> entry
    lnv_entree = uo_nav.of_section("dossiers").of_item("recents")
    lnv_entree.is_text  = "Recent (12)"
    lnv_entree.is_image = "mono:img\clock_full.svg"

    // Gray out or hide according to permissions, without rebuilding the bar
    uo_nav.of_section("outils").of_item("import").ib_enabled = of_a_le_droit("import")
    uo_nav.of_section("outils").of_item("export").ib_visible = gb_mode_expert

### Selection driven from code #

    // Move the selection elsewhere : the accent band follows
    uo_nav.of_select_item("dossiers", "clients")

    // 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_construire_menu_selon_profil()

## Best practices #

- Keep the section handle returned by `of_add_header` in a local variable: it reads better than calling `of_section(...)` again for every entry.
- Use `mono:` for icons: they recolor with the theme, light and dark alike, and stay legible once the bar is collapsed into a rail.
- An empty section title works as a discreet grouping: the entries are spaced apart without any header row appearing.
- `ib_collapsed` shrinks the userobject down to the rail, but it does not move your other controls: handle `ue_auto_width` to take up the freed space, otherwise it just stays empty.
- In rail mode only the icons remain: set a tooltip on every entry so the labels stay available.
- The question is asked by default: a `ue_selection_changing` left empty always allows the move, you have nothing to do. Switch it off with `ib_veto_selection = false` where the click is repeated — keyboard navigation, a selection driven in a loop — since every question costs a round trip to PowerBuilder.
- Prefer `ib_enabled = false` over `ib_visible = false` when the entry will become available again: the menu does not change shape under the user's eyes.

← Component reference · Guide contents