11. Migrating from 3.0 to 4.0 #
← Migrating from 2.0 to 3.0 · Contents · Release notes →
4.0 removes two methods from the thirty-two components of 3.0 — of_set_data of the crosstab, renamed of_from_datastore (11.33), and of_transform of n_pbt_xml, which goes with XSLT (11.31) — and changes signatures the compiler shows you line by line: three properties renamed (id_minimum and id_maximum of the progress bar, 11.6; il_doc_line of the code editor, 11.28), event arguments added or renamed (ue_tile_moved, 11.6; ue_failed of the REST client, 11.6, and of the video, 11.26; ue_error of the picture, 11.25; ue_cell_double_clicked, 11.33; ue_closed of the command palette, 11.36; the toaster's ue_toast_clicked, ue_toast_action and ue_toast_dismissed, 11.38), of_count and of_keys_at in long (11.34) and methods that now return a code (of_refresh, of_select, of_show, of_scroll_to_end, of_set_header…). 11.6 also aligns two counts, one event and the path of the JSON tree with the rest of the library: that is where, with the sections cited, code that compiled needs touching. What changes is concentrated on the ribbon (ribbon), which went through a complete review. Two changes ask you to read your code again: the questions asked before a ribbon gallery tile and before a side bar (listbar) selection (11.1) and the handles freed by a removal (11.2). The others fix behaviours you may have worked around (11.3) — among them the right-click on an item, which now produces a single event in every component, and the breadcrumb (breadcrumb), whose addresses and refusals were reviewed. Then come the n_pbt_menu2ribbon generator (11.4) and what was added (11.5). The tabs (tab) follow the same rule: their two questions, before a tab change and before a close, are now optional (11.1). Finally, themes follow the application (11.7): the application accent survives a change of theme, and a theme axis left empty follows the application's. The security review of crypto (11.8) closes a JWT algorithm confusion, replaces several silences with a refusal that states its reason and sets the rule for the relative paths of files.
The complete review of 28/09/2026 read every shipped component again; what it changes has one section per component: restclient (11.9), speechout (11.10), statictext and rich text (11.11), breadcrumb (11.12), buttonbar and the window keyboard (11.13), button (11.14), menubar (11.15), ribbon (11.16), toolbar (11.17), statusbar (11.18), dockcontainer (11.19), tab (11.20), listbar (11.21), tilesbox (11.22), stepbar (11.23), progressbar (11.24), picture (11.25), video (11.26), pdfviewer (11.27), codeeditor (11.28), jsontree and xmltree (11.29, 11.30), xml (11.31), json (11.32), crosstab (11.33), shellexplorer (11.35), commandpalette (11.36), radialmenu (11.37), toaster (11.38) and messagebox (11.39); the common base has two sections, 11.34 and 11.40. One rule runs through them, PowerBuilder's own (SelectTab raises SelectionChanged): a change requested by your code raises the same event as a user gesture, and asks the veto question if you switched it on; the method returns -4 when you refuse. It holds for a selection (of_select_*), an opening or a folding (of_expand, of_collapse, of_open), a zoom, a full screen, a chapter; nothing is raised when nothing changes, and of_reset stays silent. Setting a VALUE (a check mark, a text, a volume, a filter) stays silent, like SetItem. ue_layout_changed is raised in both cases: it says that a layout to store has changed.
11.1 The questions before a change are now opt-in #
In 3.0 the ribbon asked the ue_gallery_selection_changing question by default before keeping a tile: ib_veto_gallery was true. In 4.0 it is false, like every cancelable event of the library: each question costs a round trip to PowerBuilder, and only an application that answers it should pay for it. An application that did not script the event sees no difference. New: of_select_gallery_item raises ue_gallery_selection_changed like a click and, with the question switched on, asks it too; a refusal makes it return -4, the tile does not change.
// 4.0 : the question is only asked on request. If your ue_gallery_selection_changing
// returns false for some tiles, switch it on where the ribbon is built.
uo_ribbon.ib_veto_gallery = true
The recipe: search your ribbon descendants for ue_gallery_selection_changing; wherever it is scripted, add ib_veto_gallery = true to the build. Wherever you wrote ib_veto_gallery = false to switch the question off, the line is now useless and can go.
The side bar (listbar): ib_veto_selection #
Same rule: in 3.0 the listbar asked ue_selection_changing by default before every selection change (ib_veto_selection was true). In 4.0 it is false. If you scripted ue_selection_changing to refuse a move, set ib_veto_selection = true: without it the event is no longer raised and the selection moves without asking you. When switched on, the question is asked for a selection requested by your code too (of_select_item, of_clear_selection), as in 3.0; a refusal now makes them return -4, and the selection does not move. ue_selection_changed fires as before.
// 4.0 : the listbar only asks on request. If your ue_selection_changing
// returns false (unsaved changes), switch the question on where the bar is built.
uo_nav.ib_veto_selection = true
The recipe: search your u_pbt_listbar descendants for ue_selection_changing; wherever it returns false, add ib_veto_selection = true to the build. Wherever you wrote ib_veto_selection = false to switch the question off, the line is now useless and can go.
The tabs (tab): ib_veto_selection and ib_veto_close #
Same rule, two questions: in 3.0 the tab component asked ue_selection_changing by default before every tab change and ue_tab_closing before every close (ib_veto_selection and ib_veto_close were true). In 4.0 both are false. If you scripted ue_selection_changing or ue_tab_closing to refuse, set ib_veto_selection = true or ib_veto_close = true: without it the event is no longer raised and the tab changes or closes without asking you. When switched on, the question is asked for of_select_page too, as in 3.0, but before the tab changes: a refusal makes it return -4 and nothing moves; an agreement changes the tab at once, ue_selection_changed fires as before, and of_selected_key() read right after already returns the new key.
// 4.0 : the tab only asks on request. If your ue_selection_changing or
// ue_tab_closing returns false (unsaved changes), switch the questions on.
uo_tabs.ib_veto_selection = true
uo_tabs.ib_veto_close = true
The recipe: search your u_pbt_tab descendants for ue_selection_changing and ue_tab_closing; wherever one returns false, add the matching line to the build. Wherever you wrote = false to switch a question off, the line is now useless and can go.
11.2 Adds say what they refuse, removals free their handles #
In 3.0, an add the ribbon ignored — a key already taken, a menu entry under a plain button — still returned 0, and nothing appeared. In 4.0 it returns -5, and the code of the send (-2 when the component is not created) instead of a fixed 0. And a removal frees the handles of what leaves: a variable holding the handle of a removed control is no longer valid.
| What | 3.0 | 4.0 |
|---|---|---|
| Adding a key already taken (tab, group, control) | 0, ignored | -5 |
| Menu entry, combo choice or tile under a control of another kind | 0, ignored | -5 |
of_add_tab, of_insert_tab, of_add_group, of_add_contextual_*, of_add_qat on a component not created | 0 | -2 |
A handle kept after of_remove_tab, of_remove_group, of_remove_item or of_clear | stayed in memory until the ribbon was destroyed | freed: IsValid() returns false |
// 4.0 : after a removal or of_clear, ask the component for the handle again
uo_ribbon.of_clear()
uo_ribbon.of_add_tab(/*key*/ "home", /*title*/ "Home")
uo_ribbon.of_add_group(/*keys*/ "home/clip", /*title*/ "Clipboard")
uo_ribbon.of_add_button(/*keys*/ "home/clip/paste", /*label*/ "Paste", /*image*/ "")
uo_ribbon.of_item(/*keys*/ "home/clip/paste").ib_enabled = false
The recipe: do not keep a handle beyond a removal or an of_clear — ask for it again with of_tab, of_group, of_item or of_menu_item, which costs nothing. Where a build error matters to you, test the code the add returns.
11.3 Other behaviours you may notice #
When the window is narrow, the ribbon folds its groups into a button that opens a panel. That panel is where the most visible differences come from:
| What | 3.0 | 4.0 | |
|---|---|---|---|
| Button, or main part of a split, clicked in the panel of a folded group | ue_menu_selected("", false) | ue_clicked(address) | |
| Menu entry chosen in that panel | incomplete address | full address, cascade included | |
| Greyed control, or hidden entry, in that panel | answered / was shown | does not answer / is not shown | |
| Big split shrunk for lack of room | a click only opened the menu | two parts: the command and the menu arrow | |
Menu entry carrying the key of its control (home/clip/paste/paste) | ue_clicked("home/clip/paste") | ue_menu_selected("home/clip/paste/paste", …) | |
of_menu_item("…/exp/pdf") when a cascade also holds pdf | could aim at …/exp/more/pdf | aims at the entry the address names | |
as_from_keys of ue_gallery_selection_changed with no previous tile | the gallery's address | "" | |
Reading back a Quick Access Toolbar handle, il_color of a colour picker, ii_visible_items, is_keytip of a tab, il_color of a contextual group, of_count() of a gallery | empty, -1 or 0 | the value on screen | |
of_keys_at of a combo | "home/font/cb/cb:Arial" | "home/font/cb/Arial": a choice's key is its label | |
of_ctx_group(k).il_color when a tab also carries the key k | coloured the tab | colours the banner only | |
Item colours (il_back_color…) and is_tooltip of a tab or a group | no effect | applied; a group's tooltip shows on its caption | |
of_remove_item("save") on a Quick Access Toolbar button | -5 | the button is removed | |
of_add_app_menu_separator("saveas/s1") | a line at the root of the menu | the line in the submenu of saveas; a key without / does not change | |
of_clear() while a tab was active | no event | ue_selection_changed("") | |
Spinner with a decimal step (0.1) | 0.30000000000000004 | 0.3 | |
| Alt shortcut (keytip) on a greyed control, ribbon minimized | the ribbon stayed open | it closes, nothing runs | |
| toolbar: greyed combo, spinner or gallery in the chevron panel | answered | do not answer | |
All: right-click on an item (ue_item_rclicked, ue_panel_rclicked, ue_row_rclicked…) | the page also sent the component's event, which no item component passed on to PowerBuilder | one event, the item's; a component's ue_rclicked, where there is one, fires outside the items only | |
All: a file drop (ib_allow_drop) | ue_drag_leave then ue_drop_files; paths cut at 260 characters | ue_drop_files alone, whole paths; of_reset removes the drop target | |
breadcrumb: of_path() with a hidden segment, or in the demo version | the displayed keys: the hidden one skipped, the first segments cut | the full address of where you are: of_truncate(of_path()) lands; the typed field starts from the same | |
breadcrumb: of_keys_at(n); of_has("computer/C:") | the bare key; looked among the menu branches | the segment's address; looks among the segments | |
breadcrumb: reading a deep address handle, of_item("a/b/c").is_text | read the last level alone: empty on a repeated key | the segment at that address | |
breadcrumb: of_truncate, of_remove_item, of_clear_children, of_remove_child on an unknown address or a repeated bare key; of_add_child under a missing segment or on a branch already taken; of_add_item of an address that would land elsewhere | 0, ignored (or the last key alone kept) | -5 | |
breadcrumb: a handle kept after of_clear, of_remove_item, of_truncate, of_clear_children or of_remove_child | stayed in memory | freed: IsValid() returns false | |
| breadcrumb: typed field left unchanged, or window switched (Alt+Tab) while typing | ue_path_entered raised | nothing; the typing stays when the window changes | |
breadcrumb: greyed segment; Enter on a segment | its chevron opened and it took a drop; Enter also pressed the window's default button | entirely inert; Enter clicks the segment only | |
buttonbar: Enter while another button of the bar has the keyboard focus | fired the default button (is_default) | fires the focused button, as Windows does; the default fires when no button of the bar has the focus | |
buttonbar: greyed or hidden default or cancel button; is_shortcut of a greyed or hidden button | Enter, Escape or Alt+letter swallowed with no effect, even in a PowerBuilder control of the window | the key stays with the window | |
buttonbar: multi-letter is_shortcut ("Save") | Alt+S only worked while the bar had the focus | Alt+S works from anywhere in the window (the first letter, as documented) | |
buttonbar: of_add_button / of_insert_button on a key already taken or holding / or a vertical bar; of_remove_button / of_move_button on an unknown key | 0, ignored | -5 | |
buttonbar: of_remove_button, of_clear_buttons | the button's Alt+letter shortcut, colours and tooltip stayed (a button re-created under the same key took them back); the handle stayed in memory | they leave with the button; the handle is freed: IsValid() returns false | |
codeeditor: reading back is_text of a CRLF text | in LF (in CRLF only while the editor was folded): is_text = ls_original was false | with the line endings it was given, folded or not | |
codeeditor: reading back is_syntax after SYNTAX_CSHARP, SYNTAX_CPP, SYNTAX_JAVA, SYNTAX_XML | the grammar in use ("c", "html") | the value written | |
codeeditor: of_insert_text while read-only or folded | inserted while read-only; refused while folded, but 0 | -4, nothing is inserted | |
codeeditor: ii_doc_line | a band only, often out of view, absent in wrap mode; an integer | the line comes to the middle of the view (a folded region hiding it opens), band in wrap mode too; caret and selection untouched; a long — renamed il_doc_line in 4.0 (section 11.28) | |
codeeditor: #regionopen with ib_folding | rewritten as #region in the text (is_text, and after folding) | never touched; only the folded display shows #region | |
codeeditor: ue_find_result | twice per of_find, on every keystroke, and (0, 0) when the bar closed; raised even with ib_search_enabled = false | once per search or step; while typing when the count changes; never on closing; of_find without effect when the search is disabled | |
codeeditor: ue_caret_changed | silent while typing; folded, the DISPLAYED line | raised while typing too; folded, the gutter's line | |
codeeditor: Ctrl+Z after Enter or Tab; Enter / Tab while folded | the undo history was wiped; folded, the key changed the display and was lost afterwards | every keystroke can be undone; folded, the key has no effect | |
codeeditor: ib_folding = true with ib_line_numbers = false | the regions stayed closed, with no marker to open them | a narrow gutter keeps the markers | |
commandpalette: of_add_command on a key already taken or holding / or a vertical bar; of_remove_command on an unknown key | 0 (a key taken gave two twin commands) | -5 | |
commandpalette: of_open / of_register_shortcut with neither ipo_owner nor ipo_receiver | 0, and no event ever arrived | -5 | |
| commandpalette: the chord pressed while the palette is open | no effect | closes the palette | |
| commandpalette: a palette object destroyed (page or window closed) | its chord stayed registered and swallowed the key in the window, with no event at all | the chord leaves with the object, and a palette it had opened closes | |
commandpalette: chord shown on a row, on a non-Latin keyboard, with a digit and Shift, Ctrl+Space, Ctrl+Up, Ctrl+Del, or an F key alone | did not answer; Win + the letter ran the command whose chord was that single letter | answers: the physical key, read the way the DLL reads it; Win is never a modifier | |
commandpalette: a command whose chord is Ctrl+C, Ctrl+V, Ctrl+Z or Ctrl+A, while typing | the command ran instead of the copy, the paste… | the search box keeps these four keys | |
commandpalette: of_reset | kept is_recent, an open palette and the chord already registered | empties them, closes it, and brings the registered chord back to SHORTCUT_DEFAULT | |
commandpalette: pbm_custom02 of the ipo_owner window | triggered at every palette event | never: only an ipo_receiver that is set still receives it | |
All: reading back a long text value (is_text of a large script) | slower and slower with the size (several seconds); cut without warning beyond a million characters | proportional to the size; whole | |
n_pbt_utils.of_dir_has_subfolders | counted hidden and system folders and junctions; unreadable beyond 260 characters | ignores them, as the Explorer does (the ab_include_hidden overload counts them); reads long paths | |
crosstab: thousands separator of of_set_value_format | applied to ALL the measures; "," not recognised ($1 234 in French) | a setting of THIS measure; ",", ".", " " recognised; "" = default of the Options menu | |
crosstab: of_set_member_filter(field, "") | no member kept: empty table | removes the filter, like of_set_member_order("") | |
crosstab: is_show, is_conditional_formatting, of_set_value_format before of_add_value_field | lost without a word | kept, read back as set, applied when the field enters Values | |
| crosstab: sorting a column or folding a group with the mouse | no event | ue_layout_changed, like everything of_get_layout saves | |
crosstab: dates of a DataStore on a non-French machine (3/15/2025, 15.03.2025) | grouped into a wrong or empty month; dates sorted on the year alone | the right month; the column read back as yyyy-mm-dd, sorted in time order | |
| crosstab: numbers shown in German, Italian, Spanish, Portuguese | the American way (1,234.5) | in the display language (1.234,5 in German) | |
crosstab: of_export_csv | numbers with a dot (1436.5), read as text by a French Excel; a =… label ran when the file was opened | decimal separator of the display language; an apostrophe before = + - @ | |
crosstab: of_export_xlsx | hidden totals and subtotals exported anyway; top grand total ignored; a symbol « before » moved after | the table as on screen | |
| crosstab: « Add to values » from the menu of a field already in Values | replaced its values | adds a value, like a drag | |
| crosstab: « Remove » from the menu of a filtered field | the filter stayed active, unseen | the filter leaves with the field | |
crosstab: AGG_DISTINCT_COUNT on a column with empty cells | empty counted as a value | ignored, as in Excel | |
| crosstab: formula naming a calculated measure | accepted, was worth 0 | refused through ue_calc_field_error; its messages are in English | |
crosstab: ue_layout_changed with a brace in a member; ad_value of ue_cell_double_clicked | layout cut short; value 0 when a row field is named value, exponent lost | read whole | |
crosstab: ib_enabled = false, from the keyboard | Enter opened the menu of a chip, Ctrl+C copied | silent | |
| crosstab: keyboard | the focus fell back to the top of the page after each action; no way to move a field | the focus stays on the element used; the menu of a chip moves it (another zone, left, right) | |
| crosstab: dark theme | negatives and icon arrows hard to read; selected colour-scale cell unreadable | a palette of its own in dark mode | |
crypto: of_jwt_sign with JWT_EDDSA | the header said "alg":"EDDSA", which other libraries (jose, PyJWT, jjwt) refuse | "alg":"EdDSA", the registered name; a 3.0 token still verifies | |
crypto: of_generate_keypair of an unknown kind or size ("ec_p256", 1024) | 0 and an RSA 2048 pair, without a word | -5, both PEM empty, is_last_error names the accepted KEY_* | |
crypto: of_hash_file("SHA-256", …); unknown algorithm | empty, and is_last_error said "file cannot be read" of a perfect file | the name is read as of_hash reads it (SHA-256 = HASH_SHA256); an unknown algorithm is named as such, apart from an unreadable file | |
crypto: of_base64_encode_file of a file too large (over 64 MB in 32 bits, 512 MB in 64 bits); of a missing file in demo | "file cannot be read"; "demo limit" | "too large for this process"; "file cannot be read" — is_last_error gives the real reason | |
crypto: is_last_error after a successful native call (of_crc32, of_hash in MD5, of_hash_file…) | kept the error of the previous call | empty | |
crypto: empty HMAC key; non-numeric exp/nbf of a JWT; of_password_verify of a value over 10 000 000 rounds; of_random_password of an unknown set | an obscure engine error; a token valid forever; minutes of computing; CHARSET_ALL taken without a word | refused at once, is_last_error says why | |
crypto: of_base64_decode_to_file("", path) | -5 | 0, an empty file | |
dockcontainer: of_add_panel with an unknown as_relative_to, a control already hosted, a key holding / or a vertical bar, or POSITION_STACK on the main panel | 0: the panel docked against the root, the control was hosted twice, or the panel stacked on the main one stayed invisible | -5, and the control is not touched | |
dockcontainer: of_move_panel to an unknown target, or POSITION_STACK on the main panel | 0: the panel went against the root, or nothing moved | -5 | |
dockcontainer: of_remove_panel, of_reset | the control came back to its window hidden, at the dock's position and size | it comes back in its place, at its size and with the visibility it had before of_add_panel | |
dockcontainer: is_main set before of_add_panel; is_main = ""; is_main of a stacked panel | silently ignored; nothing was sent; its stack neighbours became invisible | kept and applied when the panel is added; the main panel is removed; it leaves its stack, the others stay visible | |
dockcontainer: of_set_layout of a layout that knows none of the panels, or of a text that is not a layout | the dock was emptied, or the whole message was rejected; 0 | the panels stay, placed next to the others (never on the main one); -5 for a text that is not a layout | |
dockcontainer: ue_layout_changed | raised on drag, resize and docking only: a layout saved on this event forgot a closed panel or the active tab | also raised when a panel is closed, hidden, selected or renamed, and when the main panel changes | |
| dockcontainer: click on the tab (or header) of the panel already active | ue_panel_selected on every click | nothing: the event reports a CHANGE of active panel | |
dockcontainer: unpinned panel (ib_pinned = false) | its strip only opened on a click (the doc said "on hover") | it opens on hover (400 ms) or on a click; opened on hover, it folds back when the mouse leaves it | |
| dockcontainer: standard PB control (MultiLineEdit, button…) in a floating panel | its notifications (modified, clicked, colours) no longer arrived | they arrive as when it is docked | |
dockcontainer: hiding a floating panel (ib_visible = false) | its window stayed open | it closes; shown again, the panel comes back docked in its place | |
| dockcontainer: floating window | its useful area was smaller than the panel (title bar taken from it), it could leave the screen, and a title with markup showed the tags | the useful area has the size of the panel, the window stays on screen, the title is plain text | |
| dockcontainer: splitter from the keyboard | the first arrow made it jump to 50 % | it starts from its displayed position | |
json: null value given to of_set_number, of_add_number, of_set_boolean, of_add_boolean, or null or empty value given to of_set_raw / of_add_raw | the whole document became null (of_text returned null) or stopped being JSON; a null boolean was written false | null is written, the rest of the document is intact | |
json: writing into a LOADED document whose key is written "k" : v or with an escaped character, followed by a line break, or whose root is an array; of_set_members on such a document | the key was added a second time (reading it back gave the old value), the write returned -4 wrongly, or the document was replaced by the fragment | the value is replaced or added as on a built document; of_set_members returns -4 when the root is not an object | |
json: of_set_*("items/1/qty", …) — a NEW key in an object held in an array | -4 | 0: the key is added to the first element; an array is crossed by an index that exists, only an object key is created | |
json: path with an empty segment (/name, a//b, a/) | keys named "" were created; of_remove could remove another value | -5, nothing is written; a reader answers "absent" | |
json: of_load of a badly written number (-, 01, +1, .5, 1., 1e) | 0, and of_is_valid true | -4, like any invalid JSON | |
json: of_get_number of a number with an exponent (1e-7, 2.5E+3) or more than 9 decimals | exponent ignored (2.5E+3 read as 2.5), decimals cut at 9 | exponent resolved, 17 significant digits kept | |
| json: reading or editing a big document (an 80 KB API response, 2000 rows) | seconds per call (20 s for of_count), the application looked frozen | a time proportional to the size; reading the rows one by one (rows/1, rows/2…) or walking the keys with of_name no longer starts over from the beginning | |
listbar: of_add_section / of_insert_section on a taken key or one holding / or a vertical bar; of_add_item / of_insert_item on a taken address or under a section the bar did not add; of_remove_item, of_move_item, of_select_item, of_remove_section, of_move_section on an unknown address | 0, ignored; an entry under an unknown section showed under the last section | -5; of_add_section also returns -2 when the component is not created | |
listbar: a handle held after of_remove_item, of_remove_section or of_clear | stayed in memory | freed: IsValid() returns false | |
listbar: removing or emptying what holds the selection (of_remove_item, of_remove_section, of_clear) | the selection left without an event; after of_clear, of_selected_key still returned the old one | ue_selection_changed(old, "") | |
listbar: ue_layout_changed | silent when a section folds and when an entry is removed; raised on entering the rail | raised on a fold (click or a section's ib_collapsed) and on a removal; no longer for the rail, which is not part of the layout | |
listbar: rail (ib_collapsed = true) | the entries of a folded section disappeared; an entry without an icon showed an empty square; of_reset left the bar narrow | every entry, a thin rule between sections, the initial of an entry without an icon, the label on hover; of_reset gives the bar its width back | |
listbar: a section's is_image; a colour set on section k when an entry also has the key k | no effect; the entry took the colour too | the icon is drawn before the title; only the section takes the colour | |
| listbar: keyboard | after a label change or a selection, the arrows stopped answering; with no selection the bar was out of the Tab order; section titles only folded with the mouse | the focus stays on the entry; one Tab stop; the titles are reached with the arrows and fold with Enter or Space | |
listbar: disabled entry (ib_enabled = false); ib_auto_width on an expanded bar | faded label, hard to read in light themes; reported the width it already had | the theme's disabled colour; the width its longest line asks for | |
listbar: dragging an entry (ib_reorderable) | button released outside the window: the entry still followed the mouse; the closing click could select the dropped entry | the gesture ends when the mouse comes back; the end of a drag selects nothing | |
menubar: ue_menu_opening | raised beside the dropdown: what it changed (greying Paste…) only showed at the next opening | the dropdown waits for the event to return: the change shows in this opening | |
menubar: of_item, of_remove_item on an item whose key also exists in another cascade (file/export/pdf, file/print/pdf) | could reach the other item (the first one found); the shortcut shown on one fired the other | reach the item the address names; a key alone no longer searches the cascades | |
menubar: of_add_menu, of_add_item, of_add_separator on a key already taken, holding / or a vertical bar, or under an unknown menu or parent; of_remove_item / of_remove_menu of an unknown address | 0; an item with a mistyped parent landed at the root of the menu, a key already taken made twin items | -5 | |
menubar: a handle or a shortcut after of_remove_item, of_remove_menu or of_clear | the handle stayed in memory; the shortcut stayed armed and swallowed the key, which no longer reached the application | handle freed: IsValid() returns false; shortcut disarmed | |
| menubar: item greyed, hidden or removed while its dropdown is open; shortcut of an item under a greyed cascade | the click or the key still raised ue_item_selected | nothing | |
menubar: item colours (il_back_color…) of a menu or an item; is_tooltip of a menu | no effect; a colour read back -1 | applied: on the menu title, on the item in the dropdown; the tooltip of a menu shows on its title, an item of the dropdown shows none | |
| menubar: keyboard | the arrows got stuck on a greyed menu; in RTL, left and right were swapped; Alt+letter of a hidden menu hid the same letter of another menu; after Alt+letter and a pick, the next Alt switched off | greyed menus are skipped; the arrows follow the writing direction; only a shown menu answers its letter; the next Alt lights the bar again | |
menubar: a cascade whose items are all hidden; of_count("file"), of_keys_at | was picked like a command (file/export); separators were counted (__sep0) | greyed; only items are counted | |
menubar: of_item("file") — an address of one level | read the menu back, and wrote to a "file" item that does not exist | names nothing: use of_menu("file") | |
Any text read from a component or a JSON: \b and \f escapes | stayed written as they were | decoded (backspace, form feed) | |
| messagebox: a dialog that could not open (WebView2 runtime missing, engine not ready) | of_show and the shortcuts (of_info…) returned 0, like a dismissal: of_info let the caller believe a dialog had been shown | -4 (-6 when the runtime is missing), the reason through of_get_last_error(); of_choose and of_prompt return "" and of_get_last_error says so. A = 1 test for "confirmed" stays right | |
messagebox: Esc or Alt+F4 while a cancel button still counts down (of_add_button_timed) | closed the dialog (0) | do nothing until the delay is over: it is there to make the user read | |
| messagebox: the text of the message and of the instruction | could not be selected; pressing on it moved the dialog | can be selected and copied (Ctrl+C copies the selection, or the whole dialog with nothing selected); the dialog moves by its title, its icon, its margins | |
messagebox: an input longer than about 900 characters (of_prompt, ib_input) | came back empty, like a cancel | comes back whole | |
messagebox: al_hwnd = 0, a dialog opened from another one, a dialog moved then enlarged | the whole desktop was disabled; the window came back to life while the first dialog was still waiting; the dialog jumped back to the centre | nothing is disabled without an owner; the window stays disabled while any dialog waits on it; the dialog keeps its place | |
progressbar: empty or inverted range (id_maximum = 0 then id_value = 0) | 0 % whatever the value | 100 % once the value reaches the maximum (an empty batch is finished), 0 % before | |
| progressbar: the percentage shown, the ring | rounded to the nearest (99.5 % showed "100%"); ring fixed at 128 px; while waiting, the ring spun with the arc of the value (empty at 0: nothing moved) | rounded down: 100 % means finished; the ring follows the size of the control; while waiting, a fixed arc spins; fewer animations asked by Windows: the waiting state pulses | |
Any double property read back (id_value…) on a machine with a decimal comma | the decimal part was misread (0.5 did not read back as 0.5) | read back exactly, whatever the separator of the machine | |
speechout: a new of_speak (or of_close) during a reading | the cut reading raised no ue_stopped | every reading that started ends on a ue_stopped | |
speechout: il_timeout_ms = 0 | cut the synchronous reading after 5 s | no limit (24 hours at most) | |
| speechout: a voice that could not be created (WebView2 runtime missing, page refused) | of_speak, of_speak_from, of_pause, of_resume, of_enqueue, of_clear_queue returned -1, of_open the raw DLL code | -2 (component not created, as everywhere), is_last_error says why; -4 when the page did not answer | |
speechout: of_speak_sync called from an event raised during another of_speak_sync | cut the outer call's reading, which still returned 0 | refused: -4, is_last_error = "already speaking synchronously" | |
speechout: of_speak_from past the last sentence | 0, nothing was read | -5 | |
speechout: of_is_speaking() right after of_speak | false until the voice list had arrived | true from of_speak on | |
speechout: of_pick_voice("", gender), or the voice list unreadable | picked in the page's language; a failure emptied is_voice | "" = is_lang; a failure keeps is_voice and fills is_last_error | |
speechout: of_voice_used, of_duration, of_spoken_text right after changing is_lang or the dictionary | could answer with the previous settings | answer with the settings just set | |
| speechout: script error in the voice's page | silent | ue_error("script error: …") | |
statictext: il_back_color with ii_corner_radius | the background filled the whole rectangle, only the border line was rounded | the background follows the corners; outside them, the color of the object the block sits on, which the userobject takes too | |
statictext: is_marquee_direction with a right-to-left reading direction | reversed: MARQUEE_START scrolled towards the end, "left" towards the right | MARQUEE_START scrolls towards the start of reading; "left" / "right" stay physical | |
statictext: reading back il_back_color, ii_marquee_speed, is_marquee_direction, ib_marquee_overflow_only, ib_marquee_pause_on_hover, is_font_family | -1, 0, "", false whatever was set; a family with a space in quotes | the value that was set | |
statictext: ii_marquee_speed = 0 | kept the previous speed | default speed (60) | |
statictext: of_reset after a border | color and thickness kept: the next border inherited them | cleared | |
statictext: ib_track_mouse, component disabled while hovered | ue_mouse_leave never raised | ue_mouse_leave raised on leaving | |
statictext: scrolling (ib_marquee) | a [foldarea] no longer folded afterwards; with ib_marquee_overflow_only, a HEIGHT change restarted from the beginning; with ib_wrap = false, a wrong ue_text_overflow(true) | fixed: the text is no longer copied, only a real new WIDTH is re-measured, and the scrolling shows the whole text | |
statictext: ib_hover_effect and hovered links on a dark theme | darkened (lower contrast) | lightened | |
Rich text: [symbol=clock] | shown as it is | a monochrome glyph; an unknown name stays text | |
statusbar: of_add_panel / of_insert_panel on a taken key or one holding / or a vertical bar; of_move_panel, of_remove_panel, of_clear_menu, of_add_menu_separator on an unknown panel; of_add_menu_item / of_insert_menu_item under an unknown panel or on a taken entry; of_remove_menu_item, of_move_menu_item on an unknown address | 0, ignored | -5 | |
statusbar: of_insert_panel(…, 0) | appended at the end of the bar | first, like every position (0 or less = first) | |
statusbar: a separator (of_add_sep) in of_count, of_keys_at and the positions of of_insert_panel / of_move_panel | counted as a panel with an empty key | ignored: a position counts panels | |
statusbar: a separator between two ALIGN_END panels | drawn at the end of the start group | between the two: it opens the group of the panel after it | |
| statusbar: right-click on a non-clickable panel | no event | ue_panel_rclicked (a disabled panel stays mute) | |
statusbar: of_panel(k).ii_progress read on a panel without a bar | 0, like a bar at 0 % | -1 | |
statusbar: of_menu_item("k"), a one-level address | wrote to the panel k (ib_enabled greyed it) | designates nothing: writes nowhere, reads empty | |
statusbar: colours and tooltip of a panel after of_clear, then a panel added under the same key | taken over by the new panel | forgotten: the panel starts clean | |
statusbar: is_state in a dark theme; disabled panel | light-theme colours, hard to read; greyed by transparency, background lost on hover | a readable dark palette; greyed by the theme colour, background kept | |
| statusbar: a panel whose list entries are all hidden | hand cursor and chevron, click without effect | a plain panel, clickable or not as ib_clickable says | |
| statusbar, toolbar: Left / Right arrows in right-to-left reading (RTL) | the wrong way round: Right went to the left of the screen | the way the screen goes, like the tab and the ribbon | |
stepbar: setting ii_current | nothing was raised | ue_step_changed when the bar moves, as the documentation promised | |
| stepbar: removing, inserting or moving a step before the current step | the current step changed silently (it was a rank) | the same step stays current; if that step is removed, the bar moves on to the next one it can land on and raises ue_step_changed | |
stepbar: ii_current set before the steps; of_clear_steps | brought back to the first step; the current step survived the clear | kept and applied as soon as that step exists; clearing starts again on the first step | |
stepbar: of_add_step / of_insert_step on a taken key or one holding / or a vertical bar; of_move_step, of_remove_step on an unknown key | 0, ignored | -5 | |
stepbar: of_insert_step(…, 0) | appended at the end of the bar | inserted first, like every position in the library (of_add_step appends) | |
| stepbar: left / right arrows in a right-to-left language | the right arrow went to the next step, drawn on the left | swapped: the left arrow goes to the next step | |
tab: of_add_page / of_insert_page with a key holding / or a vertical bar; a send that fails | 0; the page stayed hosted and its key taken | -5; a failed send returns its code, the page goes back to its window and the key stays free | |
tab: of_select_page of a hidden or disabled page, or one past the demo cap | 0, and the page showed under a band with no active tab | -5, nothing moves | |
tab: cross, shortcut (is_shortcut), “Close all / others / to the right” on a disabled or hidden tab | the tab was activated or closed, and PB released its page | no effect: a tab one can neither see nor reach is neither activated nor closed | |
| tab: the active tab is closed, removed or hidden | the next or the first took over, even hidden or disabled; the as_from_key of ue_selection_changed was empty | the selectable neighbour takes over (right, otherwise left); as_from_key names the tab really left, even gone | |
| tab: no visible tab left (all hidden, or a layout that hides them) | the page of the old tab stayed on screen, without an event | the page is hidden and ue_selection_changed(old, "") is raised; showing a tab again selects it | |
tab: a handle held after of_remove_page or a close; its shortcut | stayed; a page reopened under the same key got back its colour, tooltip and old shortcut | freed: IsValid() returns false, the page comes back fresh | |
tab: of_set_layout with a text that is not JSON | 0, and the message went out broken | -5, nothing is sent | |
| tab: keyboard in the tab strip | Enter lost the focus; the arrows stopped on disabled or overflowed tabs; no Up/Down on a vertical band and no swap in a right-to-left language | the focus stays on the tab; the arrows follow the axis of the band and skip what does not answer; Delete closes a closable tab | |
| tab: text of a disabled tab; tooltip of a tab with a truncated title | faded as a whole (unreadable on hover in office2007 dark); the native full name hid your tooltip | text in the theme's disabled colour, readable; your tooltip comes first | |
tilesbox: of_get_layout | always returned "": a saved layout was never restored | the whole layout, the same text as ue_layout_changed; it also keeps the size and visibility of each tile and the visibility of each group | |
tilesbox: of_set_layout naming a tile under another group than its own; a text that is not JSON | the first tile with that key, in any group, was moved there; 0 | a tile is found back by its group and its key: the entry is ignored; -5 on a text that is not JSON | |
tilesbox: of_add_group on a key already taken; of_add_tile / of_insert_tile on a taken key or under a group never added; an empty key, or one with / or ` | ` | 0; a group added again was renamed, a tile under an unknown group created it under its key | -5, nothing is added: add the group first with of_add_group |
tilesbox: of_move_tile, of_remove_tile, of_add_live_item, of_clear_live_items on a tile that does not exist; of_remove_group on a group that does not exist | 0 | -5 | |
tilesbox: a handle held after of_remove_tile, of_remove_group, of_clear, or a tile dragged into another group | stayed in memory; a tile re-created at the same address took back the old one's colors and tooltip, a dragged tile lost them | the handle is released (ask again with of_tile / of_group); colors and tooltip follow the dragged tile and leave with what is removed | |
statusbar, stepbar, crosstab: a handle held after of_remove_panel, of_remove_menu_item, of_clear_menu, of_clear (statusbar), of_remove_step, of_clear_steps (stepbar), of_remove_calc_field or of_remove_calc_measure (crosstab) | stayed in memory until the component was destroyed | freed: IsValid() returns false; ask again with of_panel, of_menu_item, of_step or of_field | |
Every component with items: reading back il_back_color_hover / il_text_color_hover of an item never coloured | 0, black | -1, "the theme's colour", like the three other item colours | |
tilesbox: reading back ib_enabled / ib_visible of a group, or ii_badge_size | false, false, 0 whatever was set | the value in force | |
tilesbox: disabled group (ib_enabled = false) | its header still folded it (click, Enter) and raised ue_group_toggled; a dragged tile could land in it | inert: no fold, no event, no drop | |
tilesbox: item colors (il_back_color, il_text_color, il_accent…) set on a group | no effect; the color of a group a could paint a tile a of another group | they paint the group header (background, title, hover, accent rule), never its tiles | |
tilesbox: of_tile(""), or an address with one or three levels | setting a property stopped the application ("Array boundary exceeded") or wrote on the group | an inert handle: it writes nothing and reads empty | |
| tilesbox: keyboard and screen reader | the focus was lost on every update coming from PB (a badge) and the size menu closed; size menu by mouse only; in RTL, arrows the wrong way; labels read with their markup | focus and menu kept; size menu by keyboard (arrows, Enter, Esc); arrows in reading order; plain text | |
toaster: il_max_visible | default 5, sent by EVERY toaster: a toaster that had set 10 fell back to 5 as soon as another one showed a toast | default 0 = nothing is sent; the cap is SHARED by the application and the last value set wins (5 until someone sets it) | |
toaster: of_show of a toast waiting for room | returned 0: the toast could not be closed by of_close, and its events carried an id never received; full waiting line (200): 0 too, toast lost without a word | a real identifier, at once; of_close takes it out of the line; full waiting line: -4 | |
toaster: two toasts without is_key carrying a button with the same key | the second REPLACED the first (the button key was taken for the toast key) and of_show returned the id of the first | two toasts, two identifiers; only is_key updates in place | |
| toaster: events of several toasters | filed by WINDOW: two toasters anchored to the same window took each other's events, and those of a destroyed toaster went to the next one | each toaster receives those of ITS toasts; a destroyed toaster receives nothing more, its toasts stay on screen, silent | |
toaster: pbm_custom02 of the ipo_owner window | the anchor window received a pbm_custom02 at each toast event, even without ipo_receiver | only a window set in ipo_receiver gets it; if you had mapped your window's pbm_custom02 for the toasts, set ipo_receiver, or better, remove that code: the events arrive on their own | |
| toaster: display | a toast stayed put when its window moved; a toast taller than 800 px (a long error trace) NEVER appeared; width not scaled (180 px at 200 %); updating a hovered toast restarted the whole stack | it follows its window; the text scrolls inside the capped toast; width scaled to the screen; the stack stays paused under the pointer | |
toolbar: adds, of_add_separator, of_move_item, of_remove_* on a taken key, one holding / or ` | `, an unknown bar or an unknown address | 0, ignored — a typo in a bar name created a phantom bar | -5; only main needs no of_add_bar |
toolbar: of_clear, of_remove_bar, of_remove_item | handles stayed, shortcuts stayed armed, and a tool recreated under the same key got the old one's colours back | handles freed, shortcuts disarmed, colours and tooltips forgotten; ask for a handle again after a removal | |
toolbar: of_item on a three-level address (a menu entry) | acted on the bar's TOOL that bore the entry's name | designates nothing; an entry is driven through of_menu_item(address) | |
| toolbar: entry chosen in a drop-down folded into the chevron | ue_menu_selected without the cascade's parent, under another drop-down, or empty | the full address, as in the bar | |
toolbar: ii_index of a bar | never moved a bar to the right | the bar takes the requested rank | |
toolbar: of_add_bar(key, band, rank) on a rank already held, or a band beyond the others | undefined order; the band read back as given (5 for the second row) | it is inserted before the bar that held the rank; the bands close ranks | |
toolbar: of_count, of_keys_at of a bar, rank of of_move_item | counted the separators, under an empty key | the tools only: a separator has no key and holds no rank | |
| toolbar: Enter in a text box | validated the field AND pressed the window's default button | validates the field only; Escape gives it back its validated text | |
toolbar: is_shortcut read back; ii_band / ii_index of an unknown bar | ctrl+s (normalised); 1 | Ctrl+S, as written; 0 | |
toolbar: colour or tooltip set on a BAR (of_bar) | no effect, or painted the tool bearing the same name | applied to the bar, and to it alone | |
| toolbar: shortcut of a hidden tool or one past the demo cap | swallowed the key | the key goes to the next tool holding the same chord | |
webbrowser: a page that does not load (unknown host, network, certificate, of_stop) | raised ue_load_completed, like a success | raises ue_load_failed(as_url, al_status) instead | |
webbrowser: words in is_address ("invoice 2026") | went out as https://invoice 2026 and no navigation happened after that | refused with ue_error, what follows loads; a search when is_search_url is set | |
webbrowser: a disk path (C:\temp\x.html) or //host/x in is_address | prefixed with https://, so not found | file:///C:/temp/x.html; https://host/x; javascript: refused | |
webbrowser: is_address read back after a followed link or Back | the last address set | the page on screen | |
webbrowser: of_go_back, of_go_forward, of_stop right after is_address | could run BEFORE the address; of_stop let the waiting pages start again | play in the order written; of_stop also drops the waiting pages | |
webbrowser: of_save_as_png, of_save_as_jpg, of_print_to_pdf, of_print | captured or printed the address bar | the site on screen | |
webbrowser: target=_blank link, window.open after a click | nothing happened | the page opens in the view, ue_new_window says so | |
webbrowser: of_execute_javascript answer longer than 4,095 characters | cut without a word | whole | |
| webbrowser: F5, Ctrl+R, Ctrl+wheel in the address bar | reloaded or zoomed the bar itself | no effect in the bar; active in the site | |
webbrowser: right-click in a text field with ib_context_menu | Back / Forward / Reload | Cut / Copy / Paste | |
webbrowser: Tab or of_focus_webview without an address bar; file dropped on the site with ib_allow_drop | focus lost in a 0 px bar; file opened by the browser | the focus goes to the site, Tab leaves it for your window; ue_drop_files | |
pdfviewer: is_source read back, and as_source of ue_load_completed | the internal address /_file/?p=… | what you wrote (doc\notice.pdf) | |
| pdfviewer: missing file, not a PDF, site unreachable or refusing the frame | ue_load_completed on an error page, or no event at all | ue_load_failed(as_source, as_reason) (REASON_* constants), the viewer stays empty | |
pdfviewer: http://, data: of a type other than PDF, https:// web page | shown (or an empty frame for http://) | refused: ue_load_failed with REASON_INSECURE or REASON_NOT_PDF | |
pdfviewer: a # in is_source | cut the path (Quote #12.pdf not found); the fragment of a URL got mixed with ii_page / ii_zoom | a # is part of the file name; the fragment of a URL is ignored, page and zoom go through the properties | |
pdfviewer: ii_page set before is_source | lost: the document opened at page 1 | kept for that document; with no page set since the last is_source, a new document opens at page 1 | |
pdfviewer: PDF regenerated at the same path then of_refresh; PDF of 32 MB or more; path longer than 260 characters | the old version for 5 minutes; nothing; not found | the file as it is on disk; shown (served in chunks); shown | |
pdfviewer: link followed in the document; of_print | the site replaced the document, with no way back; printed the page framing the document | the document stays, ue_link_clicked(as_url) gives you the address; of_print prints the document (-4 without a document) | |
picture, button, tilesbox: il_badge_color | 0 = the theme's color; black could not be reached; unset, read back 0 | -1 = not set (the theme's color), like every other color; 0 is black. Replace il_badge_color = 0 with -1 | |
picture: id_zoom set before ib_zoomable | read back as set, while the picture stayed fitted | ignored: id_zoom stays 1.0 while ib_zoomable is false; set ib_zoomable first | |
picture: ib_mirror on a picture turned a quarter; is_align / is_valign with a rotation | flipped vertically on screen; the placement turned with the picture | mirror and placement are read on screen, whatever the rotation | |
| picture: dragging the zoomed picture; wheel; disabled picture (disabled statictext too) | ue_clicked on every move; zoom towards the centre, the picture could leave its frame; file drops were still accepted | no click after a move; zoom towards the pointer, the picture always covers its frame; disabled, it refuses every drop; ue_error names the source | |
xml: a read (of_get_value, of_count, of_is_valid…) or a change after a build | acted on the last LOADED document, or on nothing | the built document is loaded and becomes THE document; the next of_start_element starts a new one | |
xml: a failed of_load (empty, malformed), of_start_element after a load | the previous document stayed readable and editable (of_is_valid true); a half-built document stayed in of_xml | nothing stays | |
xml: of_xml, of_pretty, of_get_values of a large document | cut without a word beyond a few MB | whole | |
xml: of_get_value of a value XPath (count(//line), sum(//@qty)) | empty | the value as text (3, true) | |
| xml: an XPath whose prefix is declared below the root (a SOAP answer) | empty without of_register_namespace | resolved | |
xml: is_last_error after an invalid XPath, a page that does not answer, a refusal; codes of of_register_namespace / of_clear_namespaces | empty, or "no node matched" whatever the cause; changes returned -4 when the page was missing; always 0; a page without an answer returned the text null | the real reason; -2 when the page does not answer; an empty URI is refused (-5); empty rather than null | |
xml: of_pretty | lost CDATA, comments, processing instructions and the XML declaration, and reshaped mixed text | everything is kept; an element holding text stays on one line | |
| xml: building — a line break or tab in an attribute, a CR in a text, a control character, an invalid name | read back altered; malformed document; name written as is | read back identical; control character dropped; name refused (is_last_error), the element left out | |
xml: a document holding a <parsererror> element; message of a malformed XML | refused as malformed; message framed by "This page contains the following errors" | loaded; the error line alone | |
xml: of_remove of nested nodes, of the root, of an attribute; of_set_value("/"); of_add_child under an attribute | counted twice; emptied the document; the attribute stayed; 0 with no effect; "parent not found" | the real count; the root stays (0 and is_last_error); the attribute is removed; -4; "the node cannot have children" | |
xml: of_close then a read; ipo_owner set after the first read | the loaded document was lost; ignored until of_close | the document is kept and loaded back; passed on at the next read | |
metro-light and metro-dark themes: text on the accent | white on the #25a0da blue in both themes (panel headers, active tab, ribbon application button, tiles): hard to read | metro-light: the accent becomes the deeper blue #1b76a1, text still white; metro-dark: the accent stays #25a0da, the text on it becomes near-black (#1b1b1b) | |
office-dark, office2003-dark, office2003-light, office2007-light themes: accent and header colours | in dark mode, the accent was hard to read on the page (step bullet, badge), and so was white text on the accent hover and the crosstab column heads; in light mode, the top of the gradients of the application button and panel headers was too pale for white text | office-dark: accent #1178d1, hover #0f6cbd; office2003-dark: accent #4574cb, hover #2f5aa8; crosstab heads darker on hover and on totals; office2003-light and office2007-light: deeper gradient top (#3e73cf, #4275c5), office2007-light panel headers more opaque. Text still white, at least 4.5:1 | |
text on a colour chosen by the application (il_accent, an item colour, a panel header, a badge) | white up to a fairly light colour: a medium blue such as #25a0da got white text at 2.95:1 | white or near-black (#1b1b1b), whichever reads better (WCAG contrast ratio): #25a0da now gets dark text | |
jsontree: of_clear | emptied the tree AND put ib_wrap and ib_search_enabled back to their defaults | empties the tree only; the settings stay (of_reset is the one that puts everything back) | |
jsontree: is_json = "" | raised ue_error "Invalid JSON" | empties the tree without an error, as the documentation promised | |
jsontree: numbers and keys shown, is_json read back | rewritten by the engine: 12345678901234567890 shown 12345678901234567000, 1.10 → 1.1, the first of two identical keys lost; is_json read back compacted, and empty after invalid JSON | as received, both keys included; is_json reads back exactly as it was set | |
jsontree: of_match_count, al_count of ue_search_result | counted LINES; two occurrences on one line were both "current" | count OCCURRENCES, only one is current and of_search_next moves from one to the other | |
| jsontree: blocks opened by a search | ALL those holding a match, for good: searching "e" opened the whole document | only those hiding the CURRENT occurrence; of_clear_search gives back the folds from before the search | |
jsontree: of_select_path | a line hidden in a folded block stayed invisible; an unknown path returned 0 | the blocks hiding it open; an unknown path returns -5 and the selection does not move | |
| jsontree: folding the block that holds the selection | the selection became invisible, and the next arrow started over from the top | the selection moves onto the folded block; on a user gesture, ue_selection_changed reports it | |
xmltree: of_clear | emptied the tree AND put ib_wrap and ib_search_enabled back to their defaults | empties the tree only; the settings stay (of_reset is the one that puts everything back) | |
xmltree: is_xml = "" | raised ue_error "Invalid XML" and showed a red error | empties the tree without an error, as the documentation promised | |
xmltree: is_xml relu | re-serialised by the engine: quotes changed, line breaks lost, empty after an invalid XML | reads back exactly as it was set, an invalid text included | |
| xmltree: ce qui s'affiche | a CDATA was invisible (the element showed empty), comments, processing instructions and the declaration were missing, the text of mixed content was glued together before the children, a text was trimmed of its blanks, an attribute value holding a quote was shown as it was | every node in its place, in document order; the line stays XML (quote chosen or ", , &, <); significant blanks stay | |
xmltree: of_match_count, al_count de ue_search_result | counted LINES; a match straddling two pieces (id="4152") was counted without being highlighted | count OCCURRENCES, each highlighted, only one is current | |
| xmltree: éléments ouverts par une recherche | ALL those holding a match, for good | only those hiding the CURRENT occurrence; of_clear_search gives back the folds of before the search | |
xmltree: of_select_path | a line hidden in a folded element stayed invisible; an unknown path returned 0 | the elements hiding it open; an unknown path returns -5 and the selection does not move | |
xmltree: ue_selection_changed | raised also without a change (End on the last line); folding the element that held the selection made it invisible, silently | raised only when the selection changes; on a fold, the selection moves onto the folded element and the event says so | |
xmltree: ue_error | the raw text of the engine's error page; a <parsererror> element of the document passed for an error | the error line alone ("error on line N at column M: …"); a <parsererror> of the document is an element like any other | |
| restclient: cookies of a synchronous request | none kept: each of_get / of_post opened its own session; ib_keep_cookies only applied to async calls | one session per object, shared by synchronous and async calls: a synchronous login keeps you logged in for the calls that follow | |
restclient: ib_keep_cookies changed after the first request | no effect until of_close | applied at once; at false, the cookies kept so far are forgotten | |
| restclient: redirection to another host or port | only Authorization, Cookie and the Basic authentication were dropped: an API key (X-Api-Key…) went to the third party | none of the headers set by of_set_header follows, only Content-Type stays | |
restclient: of_set_bearer("") after of_set_basic; of_set_basic then of_set_bearer; of_set_basic("", "") | the Basic password kept leaving; two Authorization at once; Basic Og== sent | one authentication at a time: each replaces the other, an empty token or user removes it | |
restclient: HEAD, or a 204 / 304 answer announcing a size | -4 "connection lost", and replayed when async | the status (200, 304…) | |
restclient: a body with one invalid byte; charset=iso-8859-1 or utf-16 | the whole body re-read as Windows-1252; charset ignored | only the invalid byte becomes a replacement character; the announced charset is read | |
| restclient: a text body of several hundred MB | kept three times in memory: a 32-bit process could die | -4 "body too large : use of_download" beyond 64 MB | |
| restclient: a hundred async requests sent at once | a hundred threads in parallel | six at most at the same time, the next ones wait their turn | |
| restclient: an async request sent more often than every 50 ms (a telemetry timer) | no answer delivered while the stream lasted | the answers arrive as they come | |
restclient: of_cancel of an unknown id; of a request whose answer has arrived | -1; 0 (but ue_response was what arrived) | -5; -4, too late | |
restclient: of_response_text() after of_download; after an of_download(url, "") | the number of bytes written; the previous answer | empty: that body is in the file; empty | |
restclient: reading an async answer during ue_progress | memory being written: a possible crash | empty, of_status(id) = 0 while the request runs | |
video: changing film (is_source, of_play(source)) | the segment, subtitles, markers and chapters of the old film stayed | they go with it (set before any film, they wait for the first one); a playlist film keeps its subtitles. Set them after is_source | |
video: of_go_chapter, of_next_chapter, of_previous_chapter | raised ue_seeked; of_go_chapter past the number of chapters returned 0 and did nothing | ue_seeked and ue_chapter_changed, like a click on the bar; -5 | |
video: of_add_source while an is_source is set | the playlist never started, of_next went to the first film | the first entry loads at once (unless the film set is PLAYING: the playlist follows it when it ends) | |
video: ue_started after of_stop or the end of the film, then of_play | never: once per source | at every new playback (not when a pause resumes) | |
video: of_play on a film in error; of_reset; ii_rate at the next film | "playing" forever, no event; the old film stayed loaded (of_duration, of_capture); back to normal speed | ue_failed again; the film is unloaded; the speed holds | |
video: of_capture to a file that cannot be written; to "\x" or "C:x" | -4 (the doc said -5); written according to the current folder | -4, and no truncated file is left; -5: give a full path | |
| video: full screen then Alt+Tab to another application | the film stayed above everything | the other application comes in front; coming back to yours puts the film above everything again | |
| pdfviewer: a plain click on a link of the PDF | nothing: only Ctrl+click raised ue_link_clicked | ue_link_clicked(url), the document stays on screen | |
webbrowser: a data: page (is_address = "data:text/html,…") | shown, but no ue_load_completed | ue_load_completed with the requested address | |
webbrowser: a new window asked for by the site towards file:, data: or mailto: | opened in the view, ue_new_window raised | refused: ue_error, no ue_new_window; only http and https open | |
| webbrowser: a site asking for the camera, the microphone, the position, notifications | browser prompt to the user, answer remembered | refused unless ue_permission_requested returns true; nothing is remembered | |
| webbrowser: cookies, sessions and storage of the sites | in the profile shared with the components | in a browsing profile of their own: a session signed in with 3.0 is not carried over (sign in once more); of_clear_browsing_data clears it | |
webbrowser: is_search_url without %s, without http(s):// or in javascript: | accepted: every search failed or lost the typed text | refused: ue_error, the engine in force stays | |
webbrowser: gestion/ or [::1]/x in is_address | "not an address" (or a search) | https://gestion/, https://[::1]/x; the word alone stays text | |
webbrowser: right-click on a selection, a link, an image with ib_context_menu | Back / Forward / Reload only | in addition: Copy, Open link, Copy link address, Copy image | |
tab: closable tab disabled (ib_enabled = false) | the cross vanished but its room stayed empty | no cross and no empty room: the tab is as wide as a non-closable one | |
video: of_chapters on a title holding ` | ` or a tab | split into two chapters | read as it is |
video: a .srt in Windows-1252 or UTF-16 | unreadable accents; no subtitle at all | read correctly | |
commandpalette: of_open without the WebView2 runtime; toaster: of_show without the runtime | -1 | -6, like the message box | |
crypto, xml, restclient: of_open when the page or the client cannot be created | the raw code of the DLL (-1, -3…) | -2, and is_last_error says why |
If you had worked around one of these defects — for instance by handling ue_menu_selected with an empty address to catch the clicks of a folded group — remove the workaround: the documented event now arrives.
11.4 The n_pbt_menu2ribbon generator #
In 3.0, the code written by n_pbt_menu2ribbon used handle methods that no longer existed: it did not compile without edits. In 4.0 it writes the current API — every add by its address, through the component —, carries submenus over at any depth, keeps greyed and checked items as they are, and its router covers ue_clicked, ue_toggled and ue_menu_selected. The keys change: each one is now the ClassName of the menu item (m_fichier/g1/m_ouvrir) instead of the dotted path (m_principal.m_fichier.m_ouvrir).
The recipe: a ribbon already built by hand, or from generated code you then fixed, has nothing to change. To start again from the generator, run it again and paste the new router too: the old one read dotted keys.
11.5 What was added #
None of the following asks you to change your code. The new components of 4.0 are presented in the release notes.
| Object | Added |
|---|---|
| ribbon | of_add_menu_separator accepts the address of an entry: the line goes into the cascade under it |
| ribbon | of_add_menu_item and of_add_menu_check accept a cascade of any depth, and every level is driven through of_menu_item(address) |
| menubar | of_add_separator takes the address of an item (file/export): the line goes into its cascade; of_menu(...) carries the colours (il_accent, il_back_color…) and the tooltip of the title |
| crosstab | of_set_label_filter(as_field, as_type, as_a, as_b) on text (LABEL_CONTAINS, LABEL_BEGINS, LABEL_ENDS) and the constants LABEL_GT, LABEL_LT, LABEL_BETWEEN: the form with two double could only pass a number |
| crypto | KEYFORMAT_TEXT, KEYFORMAT_HEX, KEYFORMAT_BASE64 and an overload of of_hmac, of_jwt_sign and of_jwt_verify that takes them (a key as BYTES: a secret handed out encoded, AWS SigV4); an optional entropy for of_protect and of_unprotect |
| crypto | of_hmac_verify (a received HMAC — a webhook — compared in constant time); of_jwt_header and an of_jwt_sign overload that adds members to the header (kid); of_totp_verify(…, ref al_step), the accepted step, to refuse a code already used; il_password_hash_rounds |
| messagebox | of_get_last_error(): why the last dialog could not open (an empty string when it did) |
| n_pbt_utils | of_dir_has_subfolders(as_dir, ab_include_hidden): the same question, counting hidden and system folders and junctions too |
| n_pbt_utils | of_dw_date_order(): the order in which a DataWindow writes a date on this workstation (dmy, mdy, ymd) |
| speechout | of_is_paused(): true while the reading is paused |
| Rich text | the [symbol=name] tag: eighteen monochrome glyphs that follow the text colour (chapter 4) |
| toolbar | of_menu_item(address) → n_pbt_toolbar_menu_item: every drop-down menu entry has its own state (ib_enabled, ib_visible, ib_checked, is_text, is_shortcut), designated by its full address |
| toolbar | is_date on a date picker; of_clear_menu; of_remove_item of a menu entry; of_add_menu_separator on an entry (cascade); cascades of any depth |
| pdfviewer | ue_load_failed(as_source, as_reason) and the REASON_* constants; ue_link_clicked(as_url); file:/// addresses; ib_allow_save and ib_allow_print: the Save and Print commands of the reader's toolbar |
| picture | is_alt_text (text alternative read by a screen reader); a keyboard for the zoom: +, -, 0, and the arrows to move around; n_pbt_utils.of_json_get_str_array |
| webbrowser | is_search_url (search engine for words that are not an address); ib_veto_new_window and ue_new_window; ib_veto_downloads and ue_download_starting; ue_load_failed; ue_error; of_show_html; of_clear_browsing_data and ib_private; ib_veto_navigation and ue_navigating; is_title and ue_title_changed; ue_permission_requested and the PERMISSION_* constants; the LOADSTATUS_* constants of ue_load_failed; F5, Ctrl+R, Alt+arrows and Esc in the address bar |
| jsontree | of_selected_text returns the DECODED value of the selected leaf (a string without quotes or escapes); of_has_selection tells the root, whose path is "", from "nothing selected"; the keyboard skips the closing braces (they are no longer selected) |
| xmltree | every attribute is a target: a click gives its path (/order/@id), of_select_path selects it, of_selected_value returns its value; a large document draws only the lines on screen (8,000 sibling elements: 12 s → 0.15 s, an arrow key in a few ms) |
| video | ue_muted_changed(ab_on): the user muted or unmuted the sound (the bar's speaker, the M key), as volume and speed already had their event |
| ribbon | of_remove_menu_item, of_clear_menu, of_remove_combo_item, of_clear_combo, of_remove_gallery_item, of_remove_app_menu_item, of_remove_contextual_group: the removals under a control — a “Recent files” list is updated without recreating the control |
| ribbon | of_app_menu_item(address): the handle of an application menu entry (is_label, is_image, ib_enabled, ib_checked, ib_visible); of_app_menu_item("") walks the menu |
| ribbon | key tips of several letters (is_keytip = "FP"), typed one after the other; accessible names (Quick Access Toolbar, tiles) for screen readers |
| toolbar | of_add_split_button: a split button (action + menu); is_group: exclusive toggles (alignment); is_image on a tool and on a menu entry; ue_item_rclicked(address): the right click of a tool |
11.6 Names and counts aligned with the rest of the library #
Seven places spoke differently from the rest of the library. 4.0 aligns them rather than keeping the gap: two names change — the compiler shows you each line to fix —, two counts now start at 1, like every position in the product, the path the JSON tree hands back becomes the path of n_pbt_json, the XML tree's under a default namespace an XPath that works, and an ambient event becomes opt-in. These are the only changes in 4.0 that ask you to touch code that used to compile.
| What | 3.0 | 4.0 |
|---|---|---|
| progressbar: bounds of the scale | il_minimum, il_maximum, longs | id_minimum, id_maximum, doubles: a 0..1 scale or a byte count over 2 GB is valid; the prefix says the type |
| json: array index in a path | from 0: lines/0 is the first line | from 1: lines/1 is the first line; lines/0 addresses nothing — a read finds it absent, a write returns -5 |
jsontree: path of a node (ue_node_clicked, ue_selection_changed, of_selected_path, of_select_path) | levels joined by a dot, array index from 0: lines.0.sku — a dialect n_pbt_json did not read | the path of n_pbt_json: levels joined by /, index from 1 (lines/1/sku), "" for the root — to hand as it is to of_get_string on the same text |
xmltree: path of an element under a DEFAULT namespace (xmlns="urn:…") | /Envelope/Body — an XPath that finds nothing | /*[local-name()='Envelope']/*[local-name()='Body'] — to hand as it is to n_pbt_xml.of_get_value; an element without a namespace or with a prefix keeps its path |
toolbar: band and index of the layout (of_get_layout, of_set_layout, ue_layout_changed) | from 0, a string called “opaque” while ii_band, ii_index and ue_bar_reordered counted from 1 | from 1, like ii_band, ii_index and ue_bar_reordered; a read → restore round trip gives the same string |
statictext: ue_cycle | raised on every scrolling loop, with no subscription — and also by an animation inside the text | an ambient event, hence opt-in: raised only when ib_track_cycle = true, like ue_mouse_enter with ib_track_mouse; only on each full pass of the text |
tilesbox: 2nd argument of ue_tile_moved | as_to_keys, while it carries ONE key: the destination group | as_to_key: the name says what it carries, as everywhere else (as_key = one level, as_keys = an address) |
restclient: ue_failed | ue_failed (long al_id, string as_error) | ue_failed (long al_id, long al_code, string as_error): -4 failure, -5 URL or file — no need to parse the text any more |
restclient: of_json_value | a key, first occurrence at any depth; a text value only — a number returned the name of the next key | an n_pbt_json path ("json/customer", "items/1/qty"), any value: "4152", "true". A nested key is named with its parents |
The recipe: (1) recompile — the compiler flags il_minimum, il_maximum and as_to_keys: rename them id_minimum, id_maximum and as_to_key; (2) look for json paths that address an array and add 1 to each index — a loop for i = 0 to n - 1 over "lines/" + String(i) becomes for i = 1 to n; (3) a toolbar layout saved by 3.0 counts from 0: add 1 to the band and index of each bar before handing it to of_set_layout, or let the user rearrange once — read back as it is, it would put its first two bands together; (4) where ue_cycle is scripted, set ib_track_cycle = true at construction; (5) where you read a path from the JSON tree (ue_node_clicked, of_selected_path) or gave one (of_select_path), replace the dots with / and add 1 to each array index — lines.0.sku becomes lines/1/sku, which goes as it is to n_pbt_json.of_get_string.
11.7 Themes follow the application #
The theme review made every component hold the rule chapter 4 already announced: the application decides the theme and the accent, a component departs from them only as far as it is asked to. No method is renamed; what changes shows on screen, when reading the properties back, and in two return codes.
| What | 3.0 | 4.0 |
|---|---|---|
PBT_SetDefaultTheme | changing theme wiped the accent set by PBT_SetDefaultThemeAccent — even when setting the same theme again — and flattened the gradient background of office2007 and office2003 | the application accent stays from one theme to the next; each theme keeps its background |
PBT_GetDefaultThemeAccent | the theme accent when the application had set none | -1 as long as the application has set no accent |
| native return codes | PBT_SetDefaultTheme with an unknown name returned 0 and left the next components without a theme; PBT_SetDefaultThemeAccent with a PowerBuilder system colour painted it black | -5 in both cases; the previous theme or accent stays |
| one theme axis set alone | froze the other axis on the application theme of the moment | the empty axis follows the application theme at every change of it; both axes empty = the application theme, entirely |
is_theme_style · is_theme_mode · il_theme_accent | read back "", "" and -1, whatever was set | the style and mode shown, and the component's own accent (-1 when it has none) |
an unknown axis value ("office2010") or a badly written one (" Metro ") | the component lost its theme and stopped listening to the application's until of_reset() | spaces and case ignored; an unknown value is ignored and the axis keeps its value |
| accent of a component with a local theme | it kept its theme's accent, even when the application set one | it follows the application accent as long as it has none of its own |
| a custom accent | only the accent changed: hover and pressed accent buttons, tinted selections, checked ribbon buttons kept the theme accent | everything derived from the accent follows it |
tilesbox : il_badge_color | 0 and -1 = the theme colour; read back 0 with no colour set; black was unreachable | -1 = the theme colour (read back -1); 0 is black, as for every colour |
a PowerBuilder system colour (ButtonFace…) in il_theme_accent | painted black | counted as -1: the application accent |
webbrowser · pdfviewer | third-party content followed the Windows light/dark mode, and a white rectangle came before its first paint | it follows the application default theme, over the theme background (4.11) |
| accent of office-dark and office2003-dark | a dark blue, read at 3:1 when it coloured text (links, [accent]) | a lighter blue (#479ef5, #7ea1e8), with dark text on it |
datagrid: rules between rows | missing in eight themes out of ten | present in all ten themes, in the theme's grid colour |
| Windows high contrast | monochrome icons and rings drawn as shadows disappeared | supported (4.10) |
The recipe: (1) if you set the accent again after each PBT_SetDefaultTheme, it is no longer needed — and if you relied on a change of theme to clear the accent, call PBT_SetDefaultThemeAccent(-1); (2) test the return code of PBT_SetDefaultTheme when the name comes from user input or a file; (3) where you set both axes on every component to follow the application, leave them empty; (4) a tile whose badge went back "to the theme" with il_badge_color = 0 is now written -1.
11.8 crypto: what security now refuses, and where relative paths go #
The security review of crypto shuts a real door — a JWT forged with the PUBLIC key passed when the algorithm was missing — and replaces several silences with a refusal that states its reason. No method is removed; an ECDSA signature changes form, and the relative paths of files follow one rule.
| What | 3.0 | 4.0 |
|---|---|---|
of_jwt_verify, of_jwt_sign | an EMPTY algorithm meant JWT_HS256, and a PEM key was accepted as the HS256 secret: verified with the PUBLIC key as the secret, a forged token passed; the header's algorithm was compared without case, a crit header was ignored | the algorithm is REQUIRED and compared EXACTLY; a PEM key is never an HMAC secret (of_hmac too); crit is refused — empty string and is_last_error |
of_jwt_sign | an exp given in the claims was silently replaced by the al_expires_seconds duration | both at once are refused: keep one or the other |
of_encrypt, of_decrypt | an EMPTY password encrypted — anyone read the value back | refused, as of_encrypt_file already did; of_password_hash("") stays allowed |
il_pbkdf2_rounds | a value outside 1,000 to 10,000,000 fell back to 100,000 in silence | the call is REFUSED: empty string for a text, -5 for a file, and is_last_error states the bounds |
of_password_hash | il_pbkdf2_rounds (100,000 rounds) | il_password_hash_rounds, 600,000 by default (the OWASP figure); the values already stored carry their rounds and still verify |
of_base64_decode, of_base64url_decode, of_hex_decode, of_decrypt… | bytes that are not a text (a PDF, a picture) came back as replacement characters, without an error | empty string and is_last_error, which points to of_base64_decode_to_file or of_decrypt_file |
of_sign (EC key) | an IEEE-P1363 signature (64 bytes), which openssl dgst and Java refuse | a DER signature; of_verify reads both forms, a signature already stored still verifies; the ES256 JWT stays in P1363 |
of_totp_code, of_totp_verify | a non-base32 character in the secret (a 0 typed for an O) was dropped in silence: ANOTHER secret, codes that never matched | refused (spaces and = tolerated) |
of_encrypt_file | "whatever its size" | up to 64 GB, the limit of one AES-GCM container: beyond, -7 |
relative paths: n_pbt_crypto, u_pbt_zip, of_capture of the video player | read in the CURRENT folder (which moves after a DirList or a file dialog), written next to the EXE (in the IDE: the PowerBuilder folder) | READ in the current folder, then in the folder the application started in, then next to the EXE; WRITTEN in the folder the application started in — the same in the IDE and compiled |
The recipe: (1) ALWAYS pass a JWT's algorithm as a constant (n_pbt_crypto.JWT_RS256), never read from the token nor from an INI that may lack it; (2) if a server verified your EC signatures in P1363 (.NET VerifyData by default), convert them or verify them in DER (DSASignatureFormat.Rfc3279DerSequence); (3) an il_pbkdf2_rounds set out of bounds must come back between 1,000 and 10,000,000; (4) a file a relative path wrote next to the EXE is now in the application's start folder — give a full path if you want another one.
11.9 restclient: headers that state their refusal, redirections that keep the request #
The review of restclient replaces two silent losses — a change never made behind a redirection, an authentication never sent behind a malformed header — with the right behaviour or a refusal that states its reason. No method is removed; three become functions, two properties arrive, and Windows authentication leaves only when you ask for it. One point holds for the five nonvisual components that raise events: an error in your event code now shows.
| What | 3.0 | 4.0 |
|---|---|---|
of_set_header, of_set_bearer, of_set_basic | subroutines: nothing was returned, a malformed value was kept as it was | long functions: 0, or -5 and nothing changes — empty name, a name with a space, a colon or a control character, a value with a line break; an empty value removes the header |
| a request with a malformed header | left WITHOUT ANY header — Authorization included —, with no error | -5, and is_last_error = "invalid header: <name>"; nothing leaves |
| a 301 / 302 redirection of a PUT, PATCH or DELETE | went on as a GET without a body: the change was never made, and the call returned 200 | method and body kept; only a POST becomes a GET; a 303 goes on as a GET without a body (except HEAD) |
| a reused connection (the one of a redirection) that the server closes at the same moment | -4 and "request failed: 12152", now and then | the request is sent again ONCE, body included, on a new connection: the server had answered nothing to it |
of_response_text ( ), of_response_headers ( ), of_response_header, of_json_value | the last synchronous response of the PROCESS: a second n_pbt_restclient overwrote it | that of the OBJECT; of_close forgets it |
| body read as text, 32-bit application | capped at 64 MB | capped at 16 MB (64 MB in 64 bits): beyond, -4 "body too large" — read it with of_download |
of_get_async, of_request_async… on an invalid URL or header | a request id, then a ue_failed | -5 at once, no event follows; is_last_error says why ("invalid URL: …") |
| Windows authentication (Negotiate / NTLM) | the system answered an intranet server's challenge on its own with the session's credentials | off by default: the credentials go to no server without ib_windows_auth = true (new property); is_client_certificate (new) sets a client certificate |
of_cancel (long al_id), of_status (long al_id), of_response_text (long al_id)… | the id of a request of ANOTHER object reached that request | only the object's requests: of_cancel returns -5 and cancels nothing, the readers return 0 / "" |
of_status (long al_id) in ue_failed | could return 200: the status of an answer received then lost | 0 for any failed request |
ue_failed | -4 failure, -5 URL or file | -4 failure, -5 file or client certificate, -6 range request refused in demo |
runtime error in an event of the application — restclient, speechout, soundplayer, toaster, commandpalette | swallowed without a word, and the next events were no longer delivered | reaches the application's SystemError event like any script error; the next events are delivered |
il_max_retries | also replayed a failure that repeats identically (too many redirections, an invalid or HTTPS → HTTP redirection, TLS, a file that cannot be written) | those failures are no longer replayed; 429, 503 and a network failure still are |
| URLs and paths | the #… fragment went to the server; a relative Location ?page=2 lost the last segment; a relative path of of_download followed the current folder of the moment | the fragment is no longer sent; Location resolved the way a browser does (RFC 3986); a relative path is resolved at the call, in the application's current folder when the library was loaded |
The recipe: (1) test what of_set_header, of_set_bearer and of_set_basic return when the value comes from user input, an INI or a database — a call that ignores the return still compiles, but the -5 is now the only trace of a refused header; (2) an intranet API with Windows authentication that answered 200 now answers 401: set ib_windows_auth = true on THAT client; (3) a 32-bit application reading a body over 16 MB as text moves to of_download; (4) if you shared a request id between two objects, read the answer on the object that sent it; (5) an error that now shows in SystemError from ue_response or ue_failed already existed: it was swallowed.
11.10 speechout: live settings, a silent destruction #
The review of speechout keeps two promises the documentation made and the code did not — a setting changed during a reading applies to the next sentence, of_speak_from past the text returns -5 — and removes an event raised at the wrong moment. No method is removed. Added: of_add_abbreviation, of_clear_abbreviations, the ue_mark event and two tags, [mark=name] and [rate=150]…[/rate].
| What | 3.0 | 4.0 |
|---|---|---|
of_speak_from (al_index) | past the text, the reading under way was CUT, nothing started, and the return was 0 | -5, and the reading under way goes on |
ii_rate, ii_pitch, ii_volume, is_lang, is_voice | changed during a reading, taken only at the NEXT reading (the documentation promised the next sentence) | live properties: the next sentence takes them |
ue_voice_fallback | silent when only the REGION was missing (fr-CA asked, fr-FR voice) | raised; language tags compare without case, and fr_FR is fr-FR |
destroy (n_pbt_speechout) | during a reading, raised ue_stopped — on a window already half destroyed | no event; of_close() still raises ue_stopped |
of_sentence_count, of_count | the count of the text read the PREVIOUS time (0 before any reading) | the count of is_text as it is |
of_pause, of_resume, of_clear_queue | created the voice when it did not exist; a pause during the wait for the voices was lost | 0 without creating anything, -4 when the page did not answer; the pause holds until of_resume |
ue_sentence (al_index) | an abbreviation (Mr., Dr., e.g.), a spelled address or a version cut the sentence; a text without a full stop was one sentence | they no longer cut; a sentence longer than 250 characters is cut: the sentence numbers may change |
[pause=ms] | ignored at the END of the text | a silence after the last sentence, before ue_stopped |
The recipe: (1) test the -5 of of_speak_from when the index comes from a click; (2) if your ue_stopped did something when the window closed, call of_close() yourself in its close — the destruction no longer raises it; (3) do not keep sentence numbers from one version to the next: take them from ue_sentence, as the documentation always said.
11.11 statictext and rich text: markup read from left to right #
The review of statictext mostly fixed the shared markup text (4.7): these lines apply to the label of every component — tab title, status bar panel, toast, dialog. No method is removed. Added: of_refresh_parent_color(), and the [action], [hyperlink] areas and [foldarea] headers of a statictext are reachable with the keyboard (Tab, then Enter or Space).
| What | 3.0 | 4.0 |
|---|---|---|
of_escape_markup (une donnée collée à une balise) | an escaped ] right after a tag paired with the ] of the tag: "[b]" + of_escape_markup("] end") + "[/b]" showed [b]] end without bold | the data shows as written, and the formatting around it holds |
[/b], [/i]… | a closing tag closed the LAST open tag, whatever it was: [b]A[i]B[/b]C left C bold; [/br] closed something too | a closing tag closes its own tag only (the styles opened inside go on); a closing tag without an opener, or that of a self-closing tag, is ignored |
[picture=…] | cut at the first comma: a data URI (what of_icon() returns) or a file name with a comma did not show | the dimensions are read at the end of the value: both show |
[picture=\\server\share\x.png] | the image was read from the network share | refused: a markup image never reaches a network share. Go through of_icon(), which embeds it, or a picture |
[hyperlink=HTTPS://…] | a scheme in capitals was not opened | opened: the scheme is read case-insensitively; the list stays http, https, mailto |
ii_padding | 0 = the theme's margin: a label could never line up with the edge of a neighbouring field | -1 = the theme's margin (new default), 0 = no margin |
ue_clicked, ue_double_clicked | raised when folding a [foldarea] (three events for a double click), and when releasing the mouse after selecting text | no longer raised: folding is a reading matter |
[foldarea:… [action=…]] | raised ue_action AND folded the block | ue_action only, the block stays as it is |
ii_line_spacing, is_marquee_direction | a value outside 50-400 was ignored and read back 0; an unknown direction ("UP") scrolled towards the start but read back as written | brought back to the nearest bound, read back as applied; an unknown direction is and reads back MARQUEE_START |
of_scroll_to_end | returned nothing | returns 0, or -2 when the component is not created |
The recipe: (1) an ii_padding = 0 written to mean "the theme's margin" becomes -1 — or goes; (2) a markup image stored on a network share goes through of_icon(); (3) a ue_clicked that reacted to a section being folded will no longer be called: folding is not scripted.
11.12 breadcrumb: what gives when the path does not fit, and leaving the field #
The second review of the breadcrumb (breadcrumb) fixed the fold by width, the OVERFLOW_SHRINK mode and what a segment takes along with it. No method is removed. One line applies to every native menu of the library: type-ahead.
| What | 3.0 | 4.0 |
|---|---|---|
of_edit() sans / without ib_editable | returns 0, nothing opens | returns -4 |
| leaving the path field for another control of the application | the field stayed open, text half typed, and nothing was reported | a changed text is validated: ue_path_entered; switching to another application still keeps the typing |
OVERFLOW_SHRINK | every label capped at 12 characters, even with room to spare; the current place cut clean at the edge of the bar | nothing is cut while there is room; the middle gives ground first, down to a letter and …, the first segment and the current place last |
OVERFLOW_COLLAPSE | the label of the current place could be elided before the middle folded | the middle folds first; the current place is elided only as a last resort |
of_insert_item(…, ai_index) | the handles of the shifted segments designated another segment, their colours and tooltips stayed at the old address | their handles are released, their colours and tooltips follow them; a removed segment (of_clear, of_truncate, of_remove_item) takes its own along |
of_item("b").il_back_color / of_item("a/b").il_back_color | two values for the same segment | one: the unique bare key and the address designate the same segment |
OVERFLOW_SCROLL, mouse wheel | no effect | slides the strip |
| native menus (trail branches, ribbon, menu bar, toolbar, context menu): a letter typed | did nothing, mnemonics aside | the letters typed within a second jump to the first entry that starts that way ("Win"), as in the explorer; a menu of more than 500 entries draws only what can be seen |
The recipe: (1) an of_edit() whose 0 you tested without setting ib_editable now returns -4; (2) a ue_path_entered may arrive when the user clicks elsewhere in the window — it is the same text as with Enter; (3) after an of_insert_item in the middle of the trail, ask again for the handles of the segments that follow.
11.13 buttonbar and window keyboard: the key goes to the component of the focus #
The second review of the button bar (buttonbar) touched the window keyboard, shared by every component that answers Enter, Escape, an Alt+letter or a registered shortcut (button, buttonbar, and the of_register_shortcut shortcuts of every component). No method is removed; one is added, of_focus_button.
| What | 3.0 | 4.0 |
|---|---|---|
| two components of the same window answer Enter, Escape or the same shortcut (two MDI sheets, two panels) | the first one created received it, even in the other sheet | the one closest to the focus (same sheet, same panel); on a tie, none: the key stays with the window |
Enter on a PB CommandButton holding the focus, or in an open DropDownListBox; Escape in an open list | fired the default / cancel button of the component | stays with the control, as in a Windows dialog box |
of_register_shortcut(chord, key) on a button of the bar, then the button greyed and re-enabled, or its is_shortcut letter changed | the shortcut was erased | kept: the Alt+letter and the application shortcut no longer touch each other |
| a button's Alt+letter against an explicit shortcut on the same key | the first registered won | the explicit shortcut wins; the letters are consulted only after it |
of_clear_shortcuts() | also erased the is_shortcut of a button | erases only the application's shortcuts |
buttonbar: of_register_shortcut(chord) without a key, or with a key that names no button | returned 0 and swallowed the key without firing anything | returns -5 |
buttonbar: removing the is_default / is_cancel button, or clearing the bar | the property kept the key; a button re-added under that key became the default again | emptied: the role leaves with its button |
| buttonbar: another button of the bar has the focus | the accent stayed on is_default while Enter fired the focused button | the focused button wears the default look; is_default reads back unchanged |
buttonbar: a negative ii_padding_x | came back as 12 | the theme's inset, like ii_padding_y and ii_gap |
| buttonbar: the separator line between buttons | drawn in the middle of the gap, even when the buttons did not touch | only between joined buttons (ii_gap = 0), and on the right side in right-to-left reading |
The recipe: (1) on a bar, give a shortcut to ONE button — of_register_shortcut("Ctrl+S", "save") — the keyless form returns -5; (2) after removing and re-adding the default button, set is_default again; (3) to put the focus on a given button ("No" rather than "Yes"), call of_focus_button("no").
11.14 button: a grayed-out button lets go, and Alt+letter answers #
The review of the button (button, deprecated in favor of buttonbar but still delivered and fixed) changes what it does on the keyboard and what its properties read back. No method is removed or added.
| What | 3.0 | 4.0 |
|---|---|---|
a grayed-out button with ib_default, ib_cancel or is_shortcut (ib_enabled = false) | held Enter, Escape or its shortcut for the whole window: the key was lost | gives the key back to the window (a default PB CommandButton, a SingleLineEdit receive it); enabled again, it takes it back |
the & of is_text (&Save) | underlined the letter, nothing more | Alt + the letter fires the button wherever the focus is in the window, after the explicit shortcuts, never while grayed out; a PB menu bar using the same letter loses it |
| Enter or Space held on the focused button | a burst of ue_clicked; Space fired on press | a single ue_clicked; Space fires on release, as in Windows |
an is_shortcut the window cannot fire ("Win+S", "Ctrl+Entrée", "Ctrl+Pause") | read back as written; "Win+S" fired the button on a plain S | refused: it reads back "". Key names are written in English (Enter, Esc, Del) |
is_shortcut with a tooltip | the chord appeared nowhere | added to the tooltip (to the title of a rich one): "Save (Ctrl+S)" |
of_register_shortcut("F5") on the button, then ib_default, ib_cancel or is_shortcut changed | F5 was erased | F5 and is_shortcut both answer |
is_text, is_image, is_style, ii_badge, il_badge_color, ii_badge_size, ii_image_size read after of_reset() | the old value, while the screen was reset | their default ("", "standard", 0, -1) |
| a grayed-out button and the Tab key | received the focus | is skipped |
an image-only caption ([picture=…]), or an & inside a tag | the caption vanished, or the tag was broken | shown as written |
The recipe: write the key names of is_shortcut in English ("Ctrl+Enter") and read the property back when in doubt: "" means refused. If you grayed out a default button to "disable Enter", nothing to change: that is now what happens.
11.15 menubar: a menu bar that gives the focus back, and Alt+letter from anywhere #
The review of the menu bar (menubar) changes what it does with the focus and the keyboard, and what every native menu does in right-to-left writing. No method is removed nor changes its signature.
| What | 3.0 | 4.0 |
|---|---|---|
a click in the bar, then a pick (Edit > Paste) | the bar kept the focus: GetFocus() in ue_item_selected no longer named the field, the paste went nowhere | the focus goes back to the previous control before ue_item_selected is raised; same on Escape and on a click outside |
Alt+F held, or F10, with the focus in a field | nothing: only Alt tapped then F opened File | Alt+F opens File from any control, F10 does what Alt does; an Alt+F your application registered keeps the priority, and a native PowerBuilder menu on the same letter loses it |
Ctrl++, Ctrl+-, Ctrl+,, Alt+Left, Ctrl+Space, Ctrl+Tab, Ctrl+Backspace (every component with shortcuts) | shown, but nothing fired from a PB control; the button refused them ("") | fired wherever the focus is; these keys need Ctrl or Alt (alone, they stay navigation) |
in RTL, the dropdowns of every native menu (menu bar, ribbon, toolbar, breadcrumb, context menu…) | aligned on the left, cascades on the right, a › chevron, Left closed | the dropdown hangs right-aligned under its title, a cascade opens to the left (a ‹ chevron): Left opens, Right closes |
| a click on the title of a menu already open | closed it then opened it again (a second ue_menu_opening) | closes it, as in Windows |
an is_shortcut set on an item that opens a cascade | shown, and raised ue_item_selected("file/export"), an address no click produces | neither shown nor fired: a shortcut belongs on a leaf |
| a bar whose menus are all hidden or greyed, beside a ribbon | took Alt for nothing: the ribbon keytips no longer came up | gives Alt back to the ribbon while no title can be reached |
two titles on the same letter (&File, &Favorites) | Alt+F always opened the first one | in the bar, the band walks from one to the other, Enter opens |
| an item removed then added again at the same address (a recent files list) | took back the colours and tooltip of the one that left | is born without colour (il_back_color reads back -1); same after of_remove_menu and of_clear |
an accented or non-Latin letter in a dropdown (&Édition) | underlined, but no key reached it | is typed as on the keyboard |
| the Tab key on the bar | one stop per title | one stop for the whole bar; the arrows walk from title to title |
| new | — | is_image (an item's icon, changed live), is_group (radio items), ue_menu_closed(as_key) (a dropdown closed without a choice) |
The recipe: in ue_item_selected, work on GetFocus() or on the DataWindow being edited as with a PowerBuilder menu; put an & in every title; if your application used Alt+letter for something else, register it (of_register_shortcut): it keeps the priority.
11.16 ribbon: a choice that holds, menus that stay open, refusals that say so #
The second review of the ribbon (ribbon) fixes what the user saw — a font choice undone, a menu closing under the pointer — and applies the library's rule to the ribbon: an add or a removal that cannot happen returns -5. No method is removed nor changes its signature.
| What | 3.0 | 4.0 |
|---|---|---|
| ribbon: a choice in the list of a combo box, then leaving the field | ue_combo_changed twice — the choice, then the text typed before it, which overwrote it; is_text read back the typed text | a single ue_combo_changed, the choice; the field shows it at once, even for a read-only combo |
ribbon: Enter or Escape in a combo box, a spinner, or on a focused ribbon button | went also to the window's default or Cancel button | stay in the ribbon; Escape in a field gives back the text from before the typing |
| ribbon: a menu open while the application updates the ribbon (another control hidden, a tab renamed, a tile added) | closed, and the choice made in it raised no event | stays open; it closes only when its own control goes, is hidden or greyed |
ribbon: is_app_button rewritten (“File” → “Fichier”); is_app_button = "" | the application menu was emptied; an empty string showed “File” | the menu stays; an empty string removes the button and its menu |
ribbon: of_select_tab of an unknown or hidden tab | 0 | -5, nothing changes |
ribbon: adding a menu entry, a tile, a combo choice, a Quick Access Toolbar button, an application menu entry or a contextual group on a key already taken; a key holding a vertical bar or starting with __; an unknown parent (separator, header, choice, entry without an application button, tab under a missing contextual group) | 0: a duplicate, or nothing at all (a contextual tab without a banner) | -5 |
ribbon: of_remove_tab, of_remove_group, of_remove_item, of_select_gallery_item on an unknown address; of_open on a Quick Access Toolbar button | 0, no effect | -5. of_open("tab/group") opens the panel of a folded group |
ribbon: an of_qat_item handle kept after of_remove_item of its button | stayed valid and designated a button that was gone | freed: IsValid() returns false |
| ribbon: key tips — two controls with the same letter, one of them hidden; a control of a folded group | the hidden one stole the letter and nothing ran; the letter of a folded group did nothing; a two-letter key tip never answered | only a visible, enabled control answers; the letter opens the folded group's panel; "FP" is typed letter after letter |
| ribbon: arrow keys on a greyed split button, colour picker, combo box or gallery | stopped on them, with nothing to do there | skip them; Down on a drop-down or split button opens its menu |
The recipe: (1) ue_selection_changed and ue_minimized are raised when your code calls of_select_tab or sets ib_minimized too, as for a click: if you called your handling yourself right behind it, remove that call, it would run twice; (2) test the -5 of the adds if your build may repeat a key; (3) an is_app_button = "" that meant “File” is now written is_app_button = "File". The removals under a control and the handle of an application menu entry are in table 11.5.
11.17 toolbar: one shortcut rule, a single tooltip, refusals that say so #
The second review of the toolbar (toolbar) aligns its shortcuts with the window's and makes visible what was written to no effect. No method is removed nor changes its signature; two return codes appear (-5).
| What | 3.0 | 4.0 |
|---|---|---|
a tool with is_shortcut = "Delete" (a bare key) and a field in the bar | Delete in the field fired the tool and erased nothing; outside the bar, nothing | a bare key is never a shortcut: it needs Ctrl or Alt, or F1 to F24 — the window's rule |
| a shortcut key held down (every component) | the action repeated: a toggle flickered | one action per press |
the inherited of_register_shortcut(chord, key), on a toolbar | fired nothing, and the key was swallowed | takes the address of the tool or entry ("main/save"), -5 otherwise; the one-argument form returns -5; of_clear_shortcuts empties is_shortcut too |
| the shortcut of a text box, a date picker, a label | swallowed the key and did nothing | the field takes the caret (Ctrl+F on a search box), the picker opens its calendar; a label takes none |
the tooltip given at the add (of_add_button(…, tooltip)) | tags shown as typed; two bubbles at once when is_tooltip was set afterwards | a single themed tooltip, rich text, the shortcut appended; is_tooltip replaces it and reads back |
of_add_bar(MAIN_BAR) after a first tool on main | returned 0 and did nothing | -5: to place it, add main before any tool |
of_set_layout of a text that is not JSON | returned 0 | -5 |
ue_layout_changed | missing when adding a bar pushed another aside; raised when showing a bar already visible | raised in the first case, no longer in the second |
| separators, a drop-down or cascade whose entries are all hidden | orphan rules; the drop-down and the cascade stayed live and silent | no rule leading, trailing or doubled; they are drawn greyed |
| a right click in the bar | no event, ue_rclicked included | ue_item_rclicked(address) on a tool, ue_rclicked outside any tool — never both |
the colours of a menu entry (of_menu_item(…).il_back_color) | written and read back, never drawn | drawn in the menu |
toolbar: ue_rclicked, ue_mouse_enter, ue_mouse_leave | documented, but never raised | raised: a right click on the tray outside any tool, the mouse entering and leaving |
The recipe: (1) a tool shortcut on a bare key (other than F1 to F24) takes Ctrl or Alt; (2) on a toolbar, of_register_shortcut receives the address of the tool or entry; (3) to place the main bar, call of_add_bar(MAIN_BAR, band, rank) before its first tool.
11.18 statusbar: a grip that resizes, a message that does not replace the text #
The second review of the status bar (statusbar) gives the resize grip its real role and turns every empty or unknown key into a refusal. No method is removed nor changes its signature; of_flash_panel gains a return code (-5).
| What | 3.0 | 4.0 |
|---|---|---|
ib_show_resize_grip = true, then drag the grip | a drawing: nothing moves | the window is resized from its corner (bottom-left in RTL); the grip is not drawn on a maximized window or one without a sizing border |
of_flash_panel on a key the bar was never given, or on an address | 0, nothing is shown | -5 |
of_panel(k).is_text read during an of_flash_panel | the message | the panel's text (the message is shown over it) |
of_panel(""), of_panel("a/b"): write then read | wrote into the keyless panels and the separators | designates nothing: writes nowhere, reads back empty |
is_state outside the STATE_* constants ("Error", "very bad") | read back as is, with no colour; a value holding a space raised a script error | case is ignored ("Error" = error); any other value means no state and reads back empty |
ii_progress = 150 then read back | 150 (drawn at 100 %) | 100, what is drawn |
| a disabled panel that carries a state | kept the state colour: it looked active | greyed, state mark included |
a panel removed, emptied (of_clear, of_clear_menu), disabled or hidden while its list is open | the list stayed open, and a pick raised ue_panel_menu_clicked | the list closes, nothing is raised (likewise for an entry that became greyed or hidden) |
| double-click on a panel carrying a drop-down list | ue_panel_double_clicked on top of the opened list | the list alone |
| Up / Down arrow on a focused panel carrying a drop-down list | moved to the next panel | opens the list (Left / Right arrows still move) |
| a drop-down list made of separators alone | chevron and an empty list | no list: the panel stays what ib_clickable says |
The recipe: (1) set ib_show_resize_grip on a status bar placed at the bottom of a resizable window; (2) test the return of of_flash_panel like that of the other keyed methods; (3) give a key to every panel you want to read back or change.
11.19 dockcontainer: an of_select_panel that finds the panel, floating windows that remember #
The second review of the panel container (dockcontainer) gives of_select_panel an effect on a collapsed or floating panel, and makes the layout remember the floating windows. No method is removed nor changes its signature.
| What | 3.0 | 4.0 |
|---|---|---|
of_select_panel of a collapsed, floating or hidden panel | 0, nothing shows | collapsed: its sliding panel opens; floating: its window comes to the front; hidden: -5 |
of_float_panel of the main panel or of a hidden panel | 0, nothing happens | -5 |
of_move_panel that stacks the MAIN panel | 0, and its neighbour vanishes without a tab | -5, nothing moves |
unknown as_position (of_add_panel, of_move_panel) | 0, the panel docks at the bottom | -5, the control is not touched |
layout (of_get_layout, ue_layout_changed, of_set_layout) and FLOATING panels | forgotten: a detached panel came back docked; moving its window raised nothing | kept with the place of their window, reopened on restore (brought back onto a screen); moving or resizing the window raises ue_layout_changed |
of_set_layout of a layout without a main panel | the current main panel is removed | it is kept |
is_title of a panel added without a title | its key; a title given later to the main panel did not make its header appear | "" (the tab still shows the key); the main panel's header follows its title |
The recipe: (1) ue_panel_selected, ue_panel_floated and ue_panel_pinned are raised after of_select_panel, of_float_panel and ib_pinned too, as for a gesture: if you called your handling yourself right behind them, remove that call, it would run twice; (2) test the -5 of of_move_panel, of_float_panel and of_select_panel; (3) a layout saved before 4.0 restores as it is, its panels stay docked.
11.20 tab: honest return codes, a right-click that goes back to the application #
The second review of the tabs (tab) makes of_add_page and of_select_page say what they refuse, announces the layout when your code hides a tab, and hands the right-click back to the application when the built-in menu is off. No method is removed nor changes its signature; one event is added.
| What | 3.0 | 4.0 |
|---|---|---|
of_add_page of a control already hosted (by this tab under another key, or by another component) | 0, and a tab with nothing behind it | -5, nothing is added |
of_select_page of a tab the component refuses while no tab is active (all hidden) | 0 | -5 |
Hiding or showing a tab from your code (ib_visible) | no ue_layout_changed | ue_layout_changed raised with the whole layout, as for of_move_page: it is the notice that the layout to store has changed |
Right-click on a tab with ib_context_menu = false | nothing | the tab is selected then ue_tab_rclicked(as_key) (new) is raised: open your menu there |
Coming back to the current tab while ue_selection_changing awaits its answer | a "yes" answer still activated the other tab | the question is cancelled: the user's last gesture wins |
A tab disabled or hidden while ue_tab_closing awaits its answer | closed anyway | stays open |
| Closing the focused tab with the keyboard (Delete) | the focus left the strip, the arrows no longer answered | the focus goes to the tab that became active |
The recipe: (1) test the -5 of of_add_page (a page is hosted only once) and of of_select_page; (2) if you store the layout in ue_layout_changed, nothing to do — it now follows your ib_visible too; (3) where ib_context_menu = false left the right-click unanswered, script ue_tab_rclicked.
11.21 listbar: a hidden entry never selected, a checked layout #
The second review of the navigation bar (listbar) makes of_select_item refuse a hidden entry and of_set_layout a text that is not a layout, and adds a count pill, the Left / Right keys and the right click of an entry. No method is removed or changes its signature.
| What | 3.0 | 4.0 |
|---|---|---|
of_select_item of a hidden entry (ib_visible = false) | 0, an invisible selection | -5, the selection does not move; hiding the entry already selected keeps it selected |
of_set_layout of a text that is not a JSON object (truncated file) | 0, nothing is applied | -5, nothing is sent |
| Section folded in a restored layout, added later | born folded | born open: the fold only applies to a section that is there |
| Section with an empty title | its entries glued to the ones above; foldable, with nothing left to open it | a gap (a thin rule in the rail) separates it; it never folds |
New: is_badge (entry), ue_item_rclicked(as_keys), Left / Right and type-ahead | — | a count pill, the right click of an entry (one event, the entry is not selected), folding from the keyboard |
The recipe: (1) ue_selection_changed is raised after an of_select_item of your code too (opening the home page at start-up, for instance), as for a click: if you opened the page yourself right behind it, leave that to the event, otherwise it would open twice; (2) test the -5 of of_select_item if you hide entries, and that of of_set_layout if the layout comes from a file; (3) a section without a title no longer folds: give it a title if it must fold.
11.22 tilesbox: a title that no longer creates a group, rearranging from the keyboard #
The second review of the tile panel (tilesbox) stops a group's title from creating that group, makes of_add_live_item refuse an empty text, gives one Tab stop per group and adds rearranging from the keyboard. No method is removed or changes its signature. il_badge_color now follows the rule of every colour (see the picture, button, tilesbox row above).
| What | 3.0 | 4.0 | |
|---|---|---|---|
of_group(key).is_title on a group never added | created a visible group that could be neither filled nor removed (except by of_clear) | writes nothing: only of_add_group creates a group; an empty key, or one with / or ` | `, gives an inert handle |
of_add_live_item with an empty text | 0, and the fixed text is_live_text was wiped | -5, nothing changes | |
of_tile(address).of_count() | 0 | the number of faces set by of_add_live_item | |
Rearranging (ib_reorderable = true) | with the mouse only | from the keyboard too: Ctrl+Left/Right within the group (reading order), Ctrl+Up/Down to the neighbour group; same ue_tile_moved and ue_layout_changed | |
| Tab key | stopped on every tile; an arrow towards an edge with no neighbour jumped to the previous or next tile of the document | one stop per group (the last tile focused), the arrows walk the tiles; at an edge, the focus stays | |
| Tile disabled or hidden while it is dragged | moved on release, ue_tile_moved raised | the drag is abandoned, nothing moves | |
a group's ib_collapsed set by your code | no event | ue_group_toggled, like a click on the header (nothing when the group already is in that state) |
The recipe: (1) create a group with of_add_group before changing its title; (2) test the -5 of of_add_live_item if the text of a face can be empty; (3) what you did in ue_tile_moved now also applies to a move from the keyboard.
11.23 stepbar: "everything done" anchored on the last step, removed steps that no longer leave anything behind #
The second review of the step bar (stepbar) makes "everything done" consistent with the rule of the current step (an identity, not a rank), makes a removed step take what belonged to it, and fixes four display defects. No signature changes.
| What | 3.0 | 4.0 |
|---|---|---|
of_add_step while the bar stands on "everything done" | the added step showed as completed (check mark); ii_current went to n + 2 with no event | added after the last step of that moment, it becomes the current step and ue_step_changed is raised; inserted before it, nothing changes |
of_step(key).is_state on a key that names no step | kept, then applied to the step added later under that name | ignored; an added step starts on its computed state |
Colours and tooltip of a removed step (of_remove_step, of_clear_steps) | came back on the next step added under the same key | leave with the step |
Compacted bar (OVERFLOW_AUTO) whose current step is forced to error | no label left; a step forced to STATE_CURRENT gained one | the label stays with the current step (the rank of ii_current), whatever its state |
OVERFLOW_SHRINK | labels on several lines, long words cut with no "..." | one line per label, ending in "..." |
POSITION_BOTTOM with a second line (is_description) | above the label, glued to it | under the label, above the bullet |
Hovering a disabled or out-of-reach step (il_back_color_hover, il_text_color_hover) | the hover colour lit up | nothing lights up: the step will not answer the click |
The recipe: if your code adds a step after setting "everything done" (ii_current = of_count() + 1) and wants the bar to stay so, set ii_current = of_count() + 1 again after the add. If you rebuild the bar with the same keys, set the colours and tooltips you want again: they no longer survive the removal.
11.24 progressbar: a bar that fits its control, a label that follows the language #
The second review of the progress bar (progressbar) makes it fit where an HProgressBar stood, sends the component only the values that change the drawing, writes the percentage in the display language and adds is_label_format (a label of your own) and is_state (paused, error). No signature changes.
| What | 3.0 | 4.0 |
|---|---|---|
| Linear bar in a low or wide control | 12 px margin, 640 px wide at most: below 24 px high, a scroll bar appeared and the bar was cut | 2 px margin, the whole width; the track (14 px) and the label shrink with the height |
Percentage label (ib_label) | "50%" in every language | in the display language: "50 %" in French |
of_set_property("value", "12,5"), or a text that is not a number | sent as it stands: the command was lost without a word; an invalid minimum blocked the scale until the next of_reset | "12,5" is read 12.5; a text that is not a number (nor true/false for a boolean) is ignored |
id_value read back | the value the component had received; every assignment was a round trip with it | the last value the application set; the component receives only those that change the drawing (a tenth of a percent) |
of_reset() on a filled bar | the bar slid back down to zero | it goes back to zero at once |
The recipe: (1) if you computed a percentage by hand, set id_maximum and assign the raw value, on every row if you like; (2) if an automated test read the label, expect the format of the display language; (3) to show a pause or an error, prefer is_state to a colour chosen with il_color.
11.25 picture: an assignment reads the file again, a failure says why #
The second review of the image (picture) makes it read the disk at every assignment, like the native Picture, gives ue_error a second argument that says why the image is missing, refuses an http:// address instead of failing with no reason, and raises only one ue_clicked on a double click. ue_error is the only signature that changes.
| What | 3.0 | 4.0 |
|---|---|---|
| picture: double click on the image | two ue_clicked, then ue_double_clicked | one ue_clicked, then ue_double_clicked — like the native Picture |
picture: ue_error | ue_error (string as_message): the message only | ue_error (string as_message, string as_reason): as_reason is one of the REASON_* constants (not found, too large, http refused, format not shown, other). Your event script receives the argument by itself; an event ue_error(…) call written in your code now passes two |
picture: is_source = "http://…" | tried over https by the engine; on a server without TLS, a failure with no reason | refused up front: error glyph and ue_error with REASON_INSECURE. Download the image first (n_pbt_restclient.of_download) and show the file |
picture: the same path set again after rewriting the file; mono: or tint: on is_source; an .avif image | the old image; the error glyph; not found | the file read again, like the native Picture; the glyph recolored by the theme; shown |
| picture: zoomed picture smaller than its frame (bands); horizontal wheel, touchpad; right button held down | it left its frame when dragged; one zoom notch per event, zoom out on a horizontal scroll; the picture moved | it stays where is_align and is_valign put it; zoom in proportion to the gesture, nothing on a horizontal scroll; only the left button drags |
The recipe: (1) if a script opened a record on ue_clicked and another action on ue_double_clicked, nothing to do — the double click no longer opens the record again; (2) replace any parsing of the text of as_message with a test of as_reason; (3) an image on an intranet server over http is downloaded first (n_pbt_restclient.of_download into a temporary folder), then shown by its path; (4) if you added a ?v= to the path to force re-reading a rewritten image, remove it: assigning again is enough.
11.26 video: subtitles and formats that play, a failure that says why #
The second review of the player (video) gives ue_failed a second argument that says why a film does not play, refuses an http:// address, plays .mkv and .mov, loads subtitles given by URL, and pauses the film when the component is hidden. ue_failed is the only signature that changes.
| What | 3.0 | 4.0 | |
|---|---|---|---|
video: ue_failed | ue_failed (string as_message) | ue_failed (string as_message, string as_reason) — as_reason is a REASON_* constant | |
video: an http:// source | failed as "cannot be found" | refused at once, REASON_INSECURE; only https plays | |
video: a .mkv, a .mov | .mkv "cannot be found", .mov refused by its name | served and played when the engine decodes them; .avi, .wmv, .flv stay refused (REASON_FORMAT) | |
| video: a subtitle given by URL | never showed, saying nothing | downloaded by the component (the server must allow CORS); a refusal raises ue_subtitles_failed | |
video: of_add_subtitles twice for the same language | replaced the track silently, 0 | -5 (also for a language holding / or ` | ); of_remove_subtitles` first |
| video: the component hidden while playing | the film went on, with its sound | paused, ue_paused, and waits for of_play (ib_pause_when_hidden, true by default) | |
| video: dragging the volume on the bar | one ue_volume_changed per pixel | one, when released | |
video: of_capture to a .jpg | a PNG under a .jpg name | a JPEG; the overload of_capture(as_path, al_max_width) scales the picture down | |
video: of_get_last_error after an of_capture failure | the library's previous error | the reason of THIS failure (missing folder, empty path…) | |
video: Escape in full screen, ib_enabled = false | no effect (only Alt+F4 left) | leaves full screen |
The recipe: (1) add as_reason to your ue_failed script and test it rather than the message text; (2) a film or a subtitle from an http intranet server is downloaded first (n_pbt_restclient.of_download to a temporary folder), then played by its path; (3) ue_fullscreen_changed, ue_pip_changed, ue_source_changed and ue_chapter_changed are raised after your own calls too (of_fullscreen, of_pip, of_next, of_seek…), as for a gesture: if your code called its handling itself right behind them, remove that call, it would run twice; (4) a wallboard that must play hidden sets ib_pause_when_hidden = false; (5) to replace the track of a language, of_remove_subtitles then of_add_subtitles.
11.27 pdfviewer: of_refresh says what it does, nothing downloads behind your back #
The second review of the viewer (pdfviewer) gives of_refresh a return code — and, to keep the same name with the same meaning, the ones of webbrowser and shellexplorer too —, reports every link of the document, and refuses what will never show instead of leaving you waiting. No event signature changes.
| What | 3.0 | 4.0 |
|---|---|---|
pdfviewer · webbrowser · shellexplorer: of_refresh | subroutine | long: 0 once asked, -4 without a document or a page (pdfviewer, webbrowser), -2 if the component is not created |
pdfviewer: of_refresh after a refusal (http://…) | did nothing, without an event | the refusal is said again: ue_load_failed |
pdfviewer: a data: address of more than 2 MB of characters | never shown, no event | ue_load_failed with REASON_TOO_LARGE, at once |
| pdfviewer: a remote PDF served "as a download" | ue_load_failed, but the file went to Downloads | ue_load_failed with REASON_REFUSED, nothing is downloaded |
pdfviewer: ue_link_clicked on a mailto:, file: or relative link | never raised (only http and https); a relative link arrived as an internal address | raised for every link; as_url is a mailto:… as is, or a disk path for a local file |
pdfviewer: of_print_to_pdf | wrote the page around the reader, not the document | -4, nothing is written: copy the file of is_source |
pdfviewer: file://localhost/C:/…, file:////server/share/… | a wrong path, not found | C:\… and \\server\share\… |
pdfviewer: the focus given to the component (Tab, of_focus_webview) | stayed on the page: the keys did not scroll the document | goes to the document: PageDown, arrows, Home / End scroll it |
The recipe: (1) a call uo_pdf.of_refresh() compiles as is; test its return if you want to know there was nothing to read again; (2) a document of more than 1.5 MB held in memory is written to a temporary file, then shown by its path; (3) in ue_link_clicked, expect a mailto: and a disk path too, not only a web address; (4) to copy the document on screen, copy the file of is_source rather than of_print_to_pdf.
11.28 codeeditor: a search you can see, a “go to” that moves the caret, replace #
The second review of the code editor (codeeditor) brings on screen what the search finds, keeps the user's caret during a search, renames ii_doc_line and adds what a script editor is expected to do: replace, drive the caret, know whether to save, show an error in the gutter. No event signature changes.
| What | 3.0 | 4.0 |
|---|---|---|
codeeditor: ii_doc_line | a long with an integer prefix | il_doc_line: rename it; a line past the end no longer marks anything (it reads back as written) |
codeeditor: of_find with ib_search_enabled = false | 0, without searching anything | -4 |
codeeditor: of_find("") | ran the previous search again | empties the box and clears the highlights, without ue_find_result |
codeeditor: of_find, Enter in the bar | the match often stayed out of view; the caret went onto it | the match is brought on screen; the search starts from the caret, which only moves when the bar is closed (Escape) |
codeeditor: of_insert_text with the search bar open | replaced the match found, out of view, and emptied the Ctrl+Z history | inserts at the user's caret, brought on screen; Ctrl+Z undoes it |
codeeditor: a new is_text | kept the scroll position of the previous document; a CR-only text read back in LF | shows from its first line and clears the markers; CR alone reads back in CR (a mix in CRLF) |
codeeditor: ib_current_line with ib_wrap | no band | the band covers the whole line of the caret |
| codeeditor: additions | — | replace (of_replace, of_replace_all, Ctrl+H) and search options (ib_find_match_case, ib_find_whole_word, ib_find_regex, F3); caret and selection (il_caret_line, il_caret_column, of_select_range, of_selected_text, of_line_count); ib_modified; gutter markers (of_add_marker, of_remove_marker, of_clear_markers, of_marker_count) |
The recipe: (1) replace ii_doc_line with il_doc_line; (2) test the -4 of of_find if the search can be switched off; (3) to “go to the line” of an error while moving the caret, set il_caret_line (and of_add_marker to show it) — il_doc_line only marks; (4) if you followed ue_caret_changed to learn where a snippet was inserted, read il_caret_line after of_insert_text; (5) a Save button can follow ib_modified, which you set back to false after saving.
11.29 jsontree and xmltree: the keyboard stays with the tree, a search that no longer moves the selection #
The second review of the JSON tree (jsontree) covered the engine it shares with the XML tree (xmltree): what follows applies to both unless stated. No signature changes; the JSON tree gains of_expand_path, of_collapse_path, of_is_expanded, of_copy and the ue_node_toggled event, raised by a user gesture as by of_expand_path and of_collapse_path.
| What | 3.0 | 4.0 |
|---|---|---|
| jsontree, xmltree: Enter on a leaf | passed to the window: its default button fired | kept by the tree, raises ue_node_clicked like a click; with no selection, nothing |
| jsontree, xmltree: Escape with the search box open | the window's Cancel button fired | closes the box first; box closed, Escape goes to the window |
jsontree, xmltree: closing lines (}, ], </x>) | reached by the arrows (ue_selection_changed for a block already reported); a click scrolled back to the opening line | skipped by the keyboard; a click selects the block without moving the view |
| jsontree, xmltree: Right arrow on a leaf | moved down one line | does nothing, as in a tree |
| jsontree, xmltree: a search during a selection | of_search_next or Escape could fold the chosen line and move the selection without an event | the selected line stays visible and selected: of_selected_path does not change |
jsontree: the message of ue_error | half translated, half English ("JSON invalide : Unexpected end of JSON (line 1, column 6)") | entirely in the display language, line and column included: do not compare its text |
| jsontree, xmltree: a document of more than 1,500,000 lines | shown, but its end out of reach, without a word | refused: a message in the tree and ue_error |
| jsontree: Ctrl+C in the tree | copied nothing, and went on as a window shortcut | copies the selection (like of_copy); with no selection, the shortcut stays the window's |
The recipe: (1) if a window relied on Enter in the tree to trigger its default button, add that step to ue_node_clicked; (2) no longer look for an empty key in ue_selection_changed (a closing line is no longer selected); (3) do not parse the text of ue_error: it follows the language; (4) to restore a user's folds, read of_is_expanded and set them again with of_expand_path / of_collapse_path.
11.30 xmltree: every line has its XPath, any XPath selects #
The second review of the XML tree (xmltree) takes up everything the previous section says about the shared engine. It adds what is specific to XML: a path that ALWAYS designates the node of the line, readable as it is by n_pbt_xml. No signature changes; the XML tree gains of_selected_xml, of_copy, of_expand_path, of_collapse_path, of_is_expanded and the ue_node_toggled event.
| What | 3.0 | 4.0 |
|---|---|---|
| xmltree: path of a text, comment or processing-instruction line | its element's (while of_selected_value gave its own text); "" before the root | its own: /r/text()[2], /r/comment()[1], /r/processing-instruction('x')[1], /comment()[1] — n_pbt_xml.of_get_value reads the same value there |
| xmltree: element whose prefix is declared again with another URI, or with a sibling of the same name in another namespace | a path that found nothing, or ANOTHER node | *[local-name()='x' and namespace-uri()='urn:…'], which designates that very node |
xmltree : of_select_path | only the exact form the tree writes, -5 otherwise | any XPath (//line[@sku='A'], /r/a[1]…): the first node it matches is selected, of_selected_path then gives the tree's own path |
xmltree: the message of ue_error | “Invalid XML : error on line 1 at column 11: …”; the tree stayed empty | “Invalid XML : line 1, column 11 (…)”, in the display language; the tree shows the source, the faulty line marked |
xmltree: <?xml-stylesheet?> without an XML declaration | shown twice | one line |
n_pbt_xml: an XPath with @xml:lang | refused (unknown xml prefix), empty value | read: the xml prefix is bound by default, as the standard requires |
| xmltree: Right arrow on a tag with attributes | attributes could only be reached with the mouse | walks the attributes one by one (Left goes back), one ue_selection_changed per step, then goes down |
| xmltree: Ctrl+C, folding by path | nothing; Ctrl+C went to the window as a shortcut | of_selected_xml, of_copy and Ctrl+C (the XML of the selection), of_expand_path, of_collapse_path, of_is_expanded (any XPath), ue_node_toggled (a gesture, or of_expand_path / of_collapse_path) |
The recipe: (1) if you went up from a text or comment line to its element through of_selected_path, take the parent of the path (what comes before the last /); (2) do not parse the text of ue_error: it follows the language; (3) a path saved in 3.0 under mixed namespaces is found again by handing it to of_select_path, which now accepts any XPath, then reading of_selected_path back.
11.31 xml: refusals that say so, namespaces that hold #
The second review of n_pbt_xml changes no signature. It makes of_remove follow the library's codes, refuses a read while a built document has an element open, and makes namespaces hold: what a mutator writes reads back in the right namespace, or is refused with a reason.
| What | 3.0 | 4.0 |
|---|---|---|
xml: of_remove(""), of_remove with nothing loaded, a page that does not answer | 0, like "nothing removed" | -5; -4; -2 — the count (>= 0) only when the removal was tried |
xml: a read (of_exists, of_count, of_get_value, of_is_valid…) or a change while a built element is still open | the document was loaded as it stood, and the rest of the build started a NEW document: what came before was lost without an error | refused: -4, empty or false, and is_last_error = "the document is still being built : close its elements first"; nothing is lost (of_xml and of_pretty stay available) |
xml: of_add_xml under an element with a default namespace, or with the target's prefix (<s:Item/> under s:Body) | the fragment stayed outside the namespace (xmlns=''), invisible to the XPath that had been used to add it; the target's prefix was refused (-4) | the fragment inherits the namespaces in force at its target, like of_add_child |
xml: of_set_attr / of_add_child with a prefixed name (xsi:type, p:item) | 0, an attribute outside the namespace that the XPath did not find; with an undeclared prefix, a document that no longer loaded | in the namespace of the prefix (declared at the node, or bound by of_register_namespace); -4 and "undeclared prefix : xsi" when it is bound nowhere |
xml: of_pretty after an of_add_child under a prefixed parent (s:Body) | <Item> without a prefix: read back, outside the namespace | <s:Item>, as in of_xml |
xml: of_element_attr twice with the same name; a name :x, x: or a:b:c | two attributes (malformed document); name accepted (a document that does not load) | one attribute, with the second value; name refused (is_last_error), the element left out |
xml: of_transform | applies an XSLT stylesheet to the loaded document | removed: XSLT no longer exists in the engine (Chromium is removing it); transform on the server side or in PowerScript |
The recipe: (1) an of_remove tested for = 0 (nothing removed) still works; test < 0 (nothing could be tried); (2) if you read a document WHILE building it (an of_exists to check, an of_count to number), close its elements first, or keep the count yourself; (3) an of_add_xml fragment no longer has to declare its target's namespace again: one that wrote xmlns='' to leave it still can; (4) test the return of of_set_attr / of_add_child with a prefixed name, and declare the prefix (or bind it with of_register_namespace).
11.32 json: an array that builds quickly, numbers that keep their digits, an empty text that says so #
The second review of n_pbt_json removes and changes no signature. It adds the decimal and longlong overloads of of_set_number and of_add_number, and the readers of_get_decimal and of_get_longlong. Building an array with of_add_* no longer rescans the whole array on each add: 2,000 rows build in a fraction of a second instead of several minutes. Two rules are now written in the documentation, unchanged: a null string is written "" (for a JSON null, of_set_raw(path, "null")), and a key written twice is read at its FIRST occurrence.
| What | 3.0 | 4.0 |
|---|---|---|
json: of_load of a text made of blanks (line break, tab) | -4, like a malformed text | -5, like an empty text |
json: of_load of a text preceded by a BOM (UTF-8 file with BOM) | -4 | 0, the BOM is skipped |
json: of_load of a document nested more than 512 levels deep | 0 — and a very deep text could stop the application | -4, the application stays alive |
json: of_set_number / of_add_number of a decimal (or of a literal such as 690.50) | converted to a double: about fifteen digits, a cent could be lost | decimal overload: every digit, trailing zeros dropped (690.5); a longlong has its own too |
json: of_get_number of an exponent with leading zeros (1E-0000005) | 0, or a huge value | the right value (0.00001) |
every component: \u followed by four characters that are not hexadecimal, in a string read back | the four characters disappeared | kept as they are, like an unknown escape |
The recipe: (1) if you tested of_load(...) = -4 to recognise an empty file, test < 0; (2) a decimal amount given to of_set_number is now written with every digit, with no change to your code; (3) read an id of more than 15 digits with of_get_longlong, an amount with of_get_decimal, no longer with of_get_number.
11.33 crosstab: of_from_datastore, refusals that say so, the displayed value on a double-click #
The second review of the crosstab renames of_set_data to of_from_datastore, the name of the datagrid and the scheduler, which read a DataStore the same way (same arguments, same codes). Any add, filter or format that names an unknown field now returns -5 instead of 0. The table only draws the columns that can be seen: a customer code or a daily date posed in Columns no longer freezes the screen. New: is_thousands, CF_ICONS, the VALUEFILTER_* and DATE_* constants.
| What | 3.0 | 4.0 |
|---|---|---|
| crosstab: feeding from a DataStore | of_set_data(ds) | of_from_datastore(ds), same codes |
| crosstab: an unknown field (a typo) given to an add, a filter, a format, an order, a date grouping | 0, nothing happens | -5 |
crosstab: a filter type, an aggregate, a date part not in the list; a calculated measure in Rows; of_set_value_filter at a position with no measure | 0; the position slid onto the last measure | -5 |
crosstab: of_set_layout of a text that is not JSON; of_remove_calc_field / of_remove_calc_measure of an unknown name | 0 | -5 |
crosstab: ad_value of ue_cell_double_clicked | the raw aggregate (400 under "80.0 %"), 0 for an empty cell | the displayed value (0.8), NULL for an empty cell; an empty member is null in the tuples |
| crosstab: the same field in Rows (or Columns, Filters) AND in Values | depended on the call order: the value vanished; Remove on a dimension removed everything | allowed everywhere; adding or moving a dimension keeps the values, Remove removes the chip aimed at only |
| crosstab: an empty member (NULL, empty text); a text with a tab or a line break in the DataStore | "null" or a row with no label; phantom rows, shifted columns | one (blank) member, last; the text stays in its row |
crosstab: is_label after a new of_from_datastore; is_label = "" | lost; a chip with no text | kept; "" gives the DataWindow header back |
| crosstab: Ctrl+C in demo mode | ue_copy raised, clipboard written | refused like an export, the grid says so |
crosstab: of_collapse_all; the grid's member filter; of_export_xlsx past 16,384 columns; exports in VALUES_ROWS | rows only; applied at each box; an unreadable file; the measures side by side | rows and columns; applied on OK (10,000 members listed at most); refused, ue_xlsx_saved says why; as on screen |
The recipe: (1) replace of_set_data with of_from_datastore; (2) if you ignored the code returned by of_add_*_field or a filter, check < 0 — a typo finally shows; (3) in ue_cell_double_clicked, test IsNull(ad_value) before computing, and read ad_value as the displayed value (a share, not a sum, in a "%" mode).
11.34 Common base: of_count and of_keys_at count in long #
of_count returned an integer and of_keys_at took an integer rank — on every component, on their handles and on the non-visual objects that carry them (commandpalette, messagebox, speechout, toaster). Past 32,767 elements the count overflowed: a calendar (scheduler) fed from a 40,000-row DataStore could no longer be counted, and its last rows could not be walked. The four signatures are now in long: long of_count ( ), long of_count ( string as_keys ), string of_keys_at ( long al_index ), string of_keys_at ( string as_keys, long al_index ).
The recipe: a call does not change. An integer variable that receives of_count() stays right up to 32,767: declare it long. A descendant that overrode of_count or of_keys_at takes the new signature — the compiler shows you the line.
11.35 shellexplorer: a refresh that keeps the state, each row for itself #
The review of the shell explorer keeps its events: of_select, of_expand and of_collapse raise them like a click, as in 3.0. of_refresh and ib_show_files no longer close the tree. New: is_file_filter (the files shown), ib_show_hidden (hidden items, by default the Explorer setting), ib_enabled and of_refresh(path), which reads one branch again.
| What | 3.0 | 4.0 |
|---|---|---|
shellexplorer: of_refresh(), or ib_show_files changed | the tree closed, the selection vanished without an event | the open branches and the selection come back, found by their paths; a selection that no longer exists raises ue_selected("", "") |
shellexplorer: of_expand, of_select, of_collapse of an empty path | 0 | -5 |
shellexplorer: of_expand of a file or of a folder without children | nothing | ue_path_not_found(…, "expand", "not a branch") |
shellexplorer: of_keys_at(path, i) | path/child, which nothing could read back | the child's path, whole: it feeds back into of_has, of_select, of_expand |
| shellexplorer: the same folder shown at two levels (the Music of the Desktop and that of the user folder) | a click, a chevron or a double-click on one acted on the other | each row answers for itself |
| shellexplorer: hidden files and folders | never shown | shown according to the Explorer setting of the workstation, or ib_show_hidden |
| shellexplorer: a branch on a share that does not answer | "…" for ever, even closed and opened again | ue_error "… : timeout" after 30 seconds; the branch is asked again when it reopens |
The recipe: (1) ue_selected and ue_expanded are raised after an of_select or an of_expand of your code too, once the folder is reached (a path still to be read arrives a little later): do your work in the event, not right behind the call, or read of_selected_key() once the selection is there; (2) in ue_selected, handle as_path = "" (the selection is gone); (3) test -5 on an empty path; (4) set ib_show_hidden = false if your users must never see hidden items, whatever their Explorer says.
11.36 commandpalette: each object only touches its own palette, a close that always says so, the application's language #
There is only one palette on screen at a time: when a second palette object opens its own, it replaces the first one. An object now answers only for its palette and its chord. ue_opened and ue_closed are still raised on of_open / of_close: they tell the life cycle of the window, not a gesture (same rule as the audio and video players). New: of_insert_command (a command at a chosen rank), of_anchor_under (the palette under a control, in screen pixels), and on the handle is_group, is_hint, is_keywords, is_icon.
| What | 3.0 | 4.0 |
|---|---|---|
commandpalette: ue_closed | no argument; nothing at all when the palette died before showing (closed or replaced at once, window refused) | ue_closed(string as_reason) follows every of_open that returned 0: "", cancelled, failed or blocked |
commandpalette: of_is_open, of_close, of_reset, destroying an object whose palette was replaced by another one's | answered TRUE for the other's palette, and closed it | answer only for their palette: FALSE, and nothing is closed |
| commandpalette: destroying (or resetting) an object whose chord another palette object of the same window has taken over | removed the other's chord: the key no longer answered anywhere | the other's chord stays |
commandpalette: of_register_shortcut of a key the hook does not name (ctrl+ù, win+k, space without a modifier) | 0 ("set"), and the key never did anything | -5; the previous chord is kept |
commandpalette: of_register_shortcut with an empty is_shortcut and no chord set | 0 | 2 |
commandpalette: of_add_command of a key holding a comma | 0 — and is_recent read it back as two commands | -5 |
| commandpalette: a group declared in several goes (File, Edit, then File) | two "File" headers | one only, where it first appeared; the order of the commands is kept within the group |
| commandpalette: the search and accents | preferences did not find « Préférences » | accents do not count |
| commandpalette: the language | the palette spoke English whatever PBT_SetLanguage said | the application's language; an is_placeholder you set is not translated |
| commandpalette: the shortcut shown on a row, AZERTY keyboard; AltGr in the search box | Ctrl+A read as Ctrl+Q; typing € (AltGr+E) ran the command shown as Ctrl+Alt+E | the key is read as the DLL names it; a character typed is never a chord |
commandpalette: a chord set without ipo_owner (the whole application) before a window's chord, on the same key | won in that window too | the window's chord comes first |
commandpalette: POSITION_ABSOLUTE | a command keyed x or y sent the palette to the edge of the screen; on two screens it went to the window's screen | il_x / il_y hold, on the screen of the requested point |
commandpalette: inv_pump | internal variable left public | protected: it is not part of the API, nothing to read or write |
commandpalette: a command whose text quotes __license | the palette did not open (-5) | opens: only a reserved word written as a key is still refused |
The recipe: (1) if your code triggers ue_closed() itself, give it a reason (""); a script that only receives it compiles as it is; (2) if you waited for ue_opened after of_open, also handle ue_closed("cancelled"), "failed", "blocked"; (3) test 2 and -5 on the return of of_register_shortcut; (4) rename a command key that holds a comma.
11.37 radialmenu: a bare key that aims true, an open wheel that follows the application, a label that can be read #
The wheel is drawn once, and the application keeps running while it is on screen: what you change in the meantime now counts. The hub spells nothing out (it had not done so since 3.0, whatever the documentation said): the pointed branch is what shows its whole label.
| What | 3.0 | 4.0 |
|---|---|---|
radialmenu: of_item with a bare key (of_item("csv")) | changed the label and state of the one branch carrying it, but its colours were not drawn, and the handle outlived the removal of the parent | is resolved into the full address (import/csv): same handle, colours included, freed with the parent. Ambiguous or unknown: the handle changes nothing |
| radialmenu: a handle on an address not added yet | a colour set through it coloured the branch added later at that address, even after of_clear | writes nothing; of_clear forgets every colour |
radialmenu: of_show | returned nothing | returns a long: 0, or -2 when the component is not created |
radialmenu: of_show(-1, -1) | opened the wheel at the cursor | opens it at the point (-1, -1), like any negative coordinate; at the cursor is of_show() |
| radialmenu: a branch removed, cleared, greyed or hidden while the wheel is open | could still be chosen: ue_item_selected for a branch that was gone | can no longer be chosen: ue_dismissed; of_clear, of_remove_item and of_reset close the open wheel |
| radialmenu: right-click elsewhere while the wheel is open | the new wheel replaced the old one without ue_dismissed | ue_dismissed for the old one, then the new wheel |
| radialmenu: long or empty label | cut at two lines and readable nowhere; empty, a slice with nothing on it | the pointed branch shows it whole; empty, the key is shown |
| radialmenu: right-to-left reading | the ring turned clockwise, ← climbed back | the ring turns the other way, ← and → trade places |
The recipe: (1) of_show returns a long: a bare call compiles as it is; (2) replace of_show(-1, -1) with of_show(); (3) if you undid in ue_dismissed what you set up on opening, nothing to do: it now also comes when a wheel replaces another; (4) a handle obtained by a key two branches carry no longer changes anything: give the address.
11.38 toaster: events that say which toast, refusals that say so, a toast that waits to be seen #
The three toast events change signature, of_add_button returns a code, and a toast is no longer lost when the application is minimized.
| What | 3.0 | 4.0 |
|---|---|---|
toaster: ue_toast_clicked, ue_toast_action, ue_toast_dismissed | (string as_key …): as_key carried the identifier of the toast, as text | (long al_id, string as_key …): al_id is the number of_show returned, as_key the is_key of the toast (empty without one) |
toaster: of_add_button | returned the number of buttons; a key already taken, an empty key and a fourth button went through without a word | returns 0; -5 with nothing added for an empty key, a key already taken, one holding / or a vertical bar, or a fourth button. The number is read with of_count() |
toaster: of_show with a setting out of range | showed it anyway: an unknown kind as an information, an unknown corner top left | returns -5 and shows nothing: is_kind, is_position or is_sound outside their constants, il_max_visible outside 0 to 20, il_timeout below TIMEOUT_AUTO |
toaster: the same is_key shown by two toasters | the second replaced the first one's toast and took its events; an update kept the original corner | each toaster has its own; an update also takes the corner, the screen anchoring and the anchor window |
| toaster: the sound | a system sound for every toast, information and success included; none for a toast coming out of the waiting line | is_sound: SOUND_AUTO (default) = error and warning only, SOUND_ALWAYS, SOUND_NEVER; played when the toast is asked for, even if it waits for room |
toaster: toasts anchored to the screen from two windows, or without ipo_owner | landed exactly on top of each other; the cap was counted per window | one stack per monitor and per corner, and il_max_visible counts per monitor |
| toaster: a toast anchored to a minimized window | went to -32,000 px and expired without being seen | waits: it is not shown and its countdown does not run until the window comes back |
| toaster: a toast arriving while the stack is hovered | counted down and could leave during the reading | is born paused, like the others |
toaster: PBT_SetLanguage, PBT_SetDefaultTheme, PBT_SetDefaultThemeAccent, PBT_SetDefaultFont, PBT_SetFlowDirection while a toast is shown | did not reach it; the font size stayed 11 pt | follow at once, font size included |
| toaster: the text of a toast | could not be selected; grabbing the scrollbar of a long text could answer and close | can be selected and copied (right-click, Copy); the selection and the scrollbar no longer close the toast |
toaster: il_button_count | public variable | private: read the number of buttons with of_count() |
toaster: a toast whose text quotes __license | was not shown | is shown: only a reserved word written as a key is still refused |
toaster: of_close of a toast on screen | no event | ue_toast_dismissed, like its cross; a toast still waiting, never seen, raises nothing |
The recipe: (1) add long al_id as the first argument of your ue_toast_clicked, ue_toast_action and ue_toast_dismissed, and replace Long(as_key) with al_id; as_key is now the key of the toast; (2) if you read the return of of_add_button as a number of buttons, read of_count(); (3) to keep a sound on an information or a success, set is_sound = SOUND_ALWAYS.
11.39 messagebox: an empty required field is never accepted, choices with unique keys #
Five behaviours change; no signature does.
| What | 3.0 | 4.0 | |
|---|---|---|---|
messagebox: ib_input_required and a default button with a countdown (of_add_button_timed) | the automatic click accepted the box with the field empty (of_show returned the button, of_input_value an empty string) | the countdown runs out without clicking; the button waits for the field to be filled | |
| messagebox: Esc or Alt+F4 on a button that is both default and cancel, required field empty | returned 1, like a click on that button | returns 0: closed without a choice | |
messagebox: of_add_choice | accepted a key already taken, or holding / or ` | ` | returns -5 and adds nothing |
messagebox: of_choose without any choice | showed a box with a single Cancel button | shows nothing: an empty string, and of_get_last_error says "no choice to show" | |
messagebox: the buttons of the ready-made boxes (of_info, of_confirm, of_yes_no, of_prompt…) when the application set no language | "Ok" | "OK", the word of the English catalogue ("Cancel", "Yes", "No" for the others) |
The recipe: (1) a required field is now accepted by hand — if you relied on the automatic click to move on, give the field a default value (is_input_value); (2) give each choice a unique key, with no / or |, and test for -5 when your keys come from data.
11.40 Common base: the language announced by default, the decimal numbers of every machine #
Two fixes of the common base, valid for every component; no signature changes.
| What | 3.0 | 4.0 |
|---|---|---|
the language a component announces (<html lang>) | French, as long as the application chose no language | English, the default language of the components; the language chosen by the application always replaces it |
a decimal number given to a property (id_*) | only a decimal comma was converted: on a machine whose decimal separator is another character, the value did not arrive | the decimal separator of the machine, whatever it is, is converted |
The recipe: nothing to change. An application that chooses its language still sees it applied; a screen reader that read a component of an English application in French now reads it in English.
11.41 Keyboard, focus and room: what the 4.0 tests put right #
Behaviours seen while playing the components in a real application; no signature changes, one method is added.
| What | 3.0 | 4.0 |
|---|---|---|
Enter on a focused PowerBuilder CommandButton | nothing (the key was lost) | the button is pressed (clicked), as in a Windows dialog box; a bar's default button is not |
A disabled component (ib_enabled = false) | Tab stopped on it | it leaves the Tab order; enabled again (or of_reset), it takes its place back |
A Ctrl shortcut on a character without a key of its own (AZERTY: - on the 6 key, + with Shift) | Ctrl+- never found its shortcut | after the key, the character of the active layout: Ctrl+-, Ctrl++, Ctrl+,, Ctrl+. fire on any keyboard |
| Typing in an open menu (search, underlined letters) | a top-row key gave its digit (AZERTY: - arrived as 6) | the character of the layout; an underlined &1 still answers the 1 key |
The calendar of the date picker (toolbar) | mouse only | at the keyboard too: arrows (day, week), Page Up/Down (month), Home/End, Enter picks, Escape closes |
| Alt with two ribbons (or menu bars) in the same window, no sub-window between them | the first one created | the one nearest the focus on screen |
breadcrumb in a very narrow bar | the first segment never gave ground: the current place fell to nothing, the trail overflowed | after the middle, the current place gives ground down to a letter and “…”, then the first segment; nothing leaves the bar |
menubar: a choice made with the mouse in the drop-down | the focus stayed on the bar (Edit > Paste pasted nowhere) | the focus goes back to the control that had it |
ribbon: typing in a combo box then choosing in its list | two ue_combo_changed: the typed text, then the choice | one, the choice |
toolbar: is_text = "" set in ue_text_changed, the focus in the field | the old text came back, with a second event | the field stays empty |
| The focus given back to a component after a window of the library (command palette) | the page, with no element: the code editor's caret was gone | the element that had it |
A label with an underlined letter (&File) in right-to-left | letters out of order (“ileF”) | the word reads in order |
n_pbt_utils.of_dir_folders(as_dir, ref as_names[]) | DirList into a ListBox: 4.5 s for 30,000 folders, and the application's current directory changed | new: the sub-folders as the Explorer shows them, sorted, in one pass; -4 unreadable, -5 empty path |
breadcrumb: of_add_children(as_keys, as_child_keys[], as_texts[]) | one branch at a time (of_add_child): 30,000 sub-folders opened the menu in over 20 s | new: a whole level in one call |
The recipe: nothing to change. An application whose focused CommandButton had to ignore Enter disables it (Enabled = false) or moves the focus away; for a large folder, replace the loop of of_add_child with one of_add_children.
11.42 Data and keyboard: what the second batch of 4.0 tests put right #
Behaviours seen while playing the data components in a real application; no signature changes.
| What | 3.0 | 4.0 |
|---|---|---|
| Enter or Escape pressed in a component that does not take them | lost: the Default or Cancel PowerBuilder CommandButton did not fire | the key goes back to the PowerBuilder window, as chapter 6.4 promises: Escape presses the Cancel button, Enter the Default one |
PBT_SetLanguage followed at once by an order to the component | the text of an event raised by that order (ue_error of xmltree) stayed in the old language | the language holds from the next order on |
jsontree, xmltree: the focus given to the component (of_focus_webview, the Tab key) | stayed on the page: the arrows and Enter went nowhere | the tree takes it; F3 and Shift+F3 go to the next or previous occurrence of the search |
xmltree: an error reported AFTER the last character of its line | no column underlined | the end of the line is marked (↵) |
codeeditor with line wrapping (ib_wrap) | the il_doc_line band could sit one row too high, on the end of the previous line | the bands cover every row of their line and follow the resizing |
codeeditor: "Replace all" and of_replace_all on a long text | several seconds when the focus was in the editor; beyond that, of_replace_all could return 0 | instant, and the number of replacements is returned; Ctrl+H with an empty search puts the caret in "Find" |
crosstab: "Add to columns" (or rows, filters) from a chip's menu, by keyboard | the focus was lost; only Enter opened a chip's menu | the focus follows the chip to its new zone; Shift+F10 and the Menu key open its menu too |
json: thousands of of_add_* in a row | nearly a second for 2,000 rows | a few tens of milliseconds; the document is the same |
The recipe: nothing to change. An application that handled Escape pressed in a component itself (a shortcut registered by of_register_shortcut) keeps it: a shortcut comes before the window.
11.43 Content and media: what the third batch of 4.0 tests put right #
Behaviours seen while playing the content and media components in a real application; no signature changes.
| What | 3.0 | 4.0 |
|---|---|---|
speechout: of_speak then of_pause in the same script | ue_started and the first ue_sentence arrived before ue_paused: the voice was announced as started | ue_started is raised when the voice really starts speaking; a pause set before that holds it: nothing is said or announced until of_resume |
speechout: a language with no exact voice on the machine (fr-CA where there is only fr-FR) | ue_started announced the language asked for (fr-CA) | ue_started announces the language really read (fr-FR), after ue_voice_fallback |
webbrowser: Tab after the last control of the address bar | the focus left the component for the PowerBuilder window | the focus enters the site, as in a browser; Shift+Tab from the site goes back to the bar |
webbrowser: ib_private = true set while browsing is already private | no effect: the site found again what it had kept in the private session | a new, empty private session every time |
video in full screen: a modal box of the application (a MessageBox in ue_ended) | hidden under the film, which took the keyboard back from it: the application looked frozen | the film gives way: the box comes in front and gets the keyboard; once it is closed, the film takes the screen back and Escape leaves it |
tilesbox: an arrow key at an edge, next to a smaller tile | Up arrow on a large tile of the top row jumped sideways, to the small neighbour | only a tile entirely past the edge counts: at an edge, the focus stays where it is |
dockcontainer: il_back_color / il_text_color of a stacked panel | lost as soon as its tab was active (it took the theme's accent) | the active tab keeps the panel's colours, like its button on the rail |
progressbar: id_value set at every turn of a long loop | about 2.5 s for 20,000 rows | well under a second; the bar still moves while the loop runs |
The recipe: nothing to change. An application that counted on ue_started to know that of_speak had been accepted reads its return code (0) instead; one that compares the language of ue_started with is_lang now learns about the fallback from that difference too, besides ue_voice_fallback.
11.44 Data: read a cell, sort and search more finely (json, jsontree, xmltree, crosstab) #
These 4.0 additions remove no signature. Two change what you see without you writing anything: the crosstab's Ctrl+C now copies the headers, and ue_cell_double_clicked receives one more argument.
| What | 3.0 | 4.0 |
|---|---|---|
crosstab: ue_cell_double_clicked | ( string as_row_tuple_json, string as_col_tuple_json, double ad_value ) | ( string as_row_tuple_json, string as_col_tuple_json, integer ai_measure, double ad_value ): ai_measure is the position of the double-clicked value in Values, from 1; with the two tuples, it is the address of_get_cell_value reads back |
crosstab: Ctrl+C on cells | the figures alone | the headers with them: the names of the columns on a first line, the member of each row (its path, "North / Boston") in a first column; ib_copy_headers = false gives the figures alone |
crosstab: of_remove_field, of_get_cell_value | — | new: take a field out of every zone without rebuilding the layout; read the value of a cell by its tuples and the position of its measure |
json: of_get_array, of_pretty (long al_indent) | — | new: every element of an array in one call; an indent in spaces, or the document compact on one line with 0 |
jsontree, xmltree: the search | always case-insensitive, and inside words | ib_find_match_case and ib_find_whole_word, like the codeeditor, with the Aa and ab toggles of the search box; by default the search stays that of 3.0 |
The recipe: a script of ue_cell_double_clicked has nothing to change, the argument comes before the value. An application that pastes the result of Ctrl+C into a process that expects figures only sets ib_copy_headers = false.
11.45 Navigation and containers: tabs from the keyboard, panels on a double click (tab, dockcontainer, ribbon, stepbar, listbar) #
These 4.0 additions remove no signature. Three gestures change what a user sees without you writing anything: the middle click and Ctrl+Tab on a tab, and the double click on a dockcontainer panel or on the title bar of its floating window.
| What | 3.0 | 4.0 |
|---|---|---|
tab: middle click on a tab | nothing | closes a closable tab, by the same path as its cross: ue_tab_closing if ib_veto_close = true, then ue_tab_closed; a tab that cannot close, or a disabled one, does nothing |
tab: Ctrl+Tab, Ctrl+Shift+Tab, Ctrl+Page Down / Page Up | nothing | the next or previous tab that can become active, wrapping round, whether the keyboard is in the band or in a hosted page (a PowerBuilder control, another component); it is a gesture: ue_selection_changing if ib_veto_selection = true, then ue_selection_changed. From a control outside the tab, the key stays with the window |
dockcontainer: double click on a header or a tab; on the title bar of a floating window | nothing; the floating window was maximised | detaches the panel into a floating window (ue_panel_floated); on that window's title bar, docks it back in its place (ue_panel_docked), like its cross |
dockcontainer: right click on a header or a tab | nothing | a menu Float, Close, Close others (the other closable panels of the same group); each close asks its own question |
dockcontainer: ib_veto_close, ue_panel_closing, of_close_panel, n_pbt_dock_panel.ib_closable | — | new: the question before a panel closes (opt-in, false by default), closing from code (-4 if you refuse it, -5 for the main panel or a panel that cannot close), and whether a panel can close, changed live |
ribbon: n_pbt_ribbon_item.is_image, id_min, id_max, id_step | set at the add | a control's image and a spinner's range change live and read back; a spinner's value is brought back into its new range without raising ue_value_changed, like id_value |
stepbar: STATE_WARNING, STATE_SKIPPED, NAV_VISITED | — | new: a step passed with a warning (amber bullet), a skipped step (dashed ring), and a mode where every step already reached stays clickable, ahead too after going back |
listbar: n_pbt_listbar_section.ib_pinned | — | new: a section pinned at the bottom of the bar (Settings, Help), which stays in view while the bar scrolls, in the rail too; the saved layout keeps the order of the sections |
The recipe: nothing to change. In a tab page, Ctrl+Tab now changes tab even when the focus is in a control of the page, as in a Windows property sheet. To maximise a dockcontainer floating window, its Maximise button is still there.
11.46 Menus: the menu bar's chevron, a drop-down list in the toolbar, ten thousand commands (menubar, toolbar, commandpalette) #
These 4.0 additions remove no signature. One default behaviour changes: a menu bar that is too narrow no longer wraps, it folds its last titles into a chevron. And of_open of a command palette no longer refuses to open without ipo_owner or ipo_receiver.
| What | 3.0 | 4.0 |
|---|---|---|
menubar: a bar too narrow for its titles | wrapped: its height followed the rows, ue_auto_height at every change of width | stays on one line: the titles that do not fit, from the end, go into the dropdown of a chevron (their entries as cascades, Alt + their letter opens them from the chevron, the arrows stop on it like on a title, on the left in RTL); the height no longer changes. ib_wrap = true gives back the former behaviour |
menubar: ib_track_hover, ue_item_hover, of_add_header, n_pbt_menubar_menu.is_align | — | new: the pointed entry of a dropdown, for the help text of a status bar (opt-in, false by default; an empty address on closing), section headers in a dropdown, and a menu set at the end of the bar (ALIGN_END, Help) |
toolbar: of_add_combo, of_add_combo_item, of_remove_combo_item, of_clear_combo, ue_combo_changed, n_pbt_toolbar_item.ib_editable | — | new: a drop-down list in the bar, editable or not, that stays a working list folded into the chevron; its event has the name and the meaning of the ribbon's |
commandpalette: two palettes on the same ipo_owner | shared their queue of events (one could receive the other's ue_closed), and the second took the chord of the first | each its own events and chord; the same chord registered by the second goes to it (1). of_open and of_register_shortcut no longer return -5 without ipo_owner or ipo_receiver: the events always arrive |
commandpalette: thousands of commands | every line drawn: opening and typing slowed down | only the visible lines are drawn: 10,000 commands open and filter without a wait |
| Shortcuts of the whole library: AltGr + a key that types a character (the euro of a French keyboard) | fired the Ctrl+Alt shortcut of that key, in a PowerBuilder field too | the character is typed, no shortcut fires; AltGr alone no longer hands the keyboard to a menu bar or a ribbon |
The recipe: a menu bar whose variable height was wanted sets ib_wrap = true; an application that laid out its controls in ue_auto_height at every resize has nothing more to do after the first one. An application shortcut on Ctrl+Alt + a letter that the user's keyboard types with AltGr no longer fires while he types: choose another key.
11.47 Content and media: a log line by line, the actual size of a picture, a film that waits (statictext, picture, video) #
These 4.0 additions remove no signature and change no default. Two behaviours become more precise: the zoom of a picture may go past its ceiling to reach the actual size, and a film from a URL played without a network says so.
| What | 3.0 | 4.0 |
|---|---|---|
statictext: of_append_text, ib_follow_end | — | new: a log written line by line — only the line added travels to the component — and, as an option, the view that stays at the bottom as long as the reader does not scroll up |
statictext: ib_auto_width, ue_auto_width | — | new: the userobject takes the width of its text (opt-in, false by default), as ib_auto_height does for the height |
picture: id_max_zoom, the 1 key | the zoom stopped at 8.0 times the fitted view, whatever happened: a large scan shown small was never readable pixel for pixel | the ceiling is set (id_max_zoom, 8.0 by default); the 1 key shows the actual size, and the ceiling rises to it when it asks for more |
video: ue_buffering, of_is_buffering | a film waiting for its data stayed frozen without a word | a ring in the theme's accent turns on the picture, and ue_buffering(true) then ue_buffering(false) frame the wait |
video: a film from a URL while the workstation has no network | "this file cannot be found or played", ue_failed with REASON_UNSUPPORTED | "no network connection" on the picture, ue_failed with REASON_NETWORK |
The recipe: nothing to change. A log that rewrote is_text at every line moves to of_append_text; code that tested REASON_UNSUPPORTED for a film from a URL without a network tests REASON_NETWORK.
11.48 Review of 03/10/2026: a layout put back is announced, an export to a relative path lands in the right place (toolbar, crosstab) #
No signature changes. Two behaviours line up with the rule of the library: every change of the layout is announced, and a relative path to write to is resolved as for the other exports.
| What | 3.0 | 4.0 |
|---|---|---|
toolbar: of_set_layout | no event | raises ue_layout_changed, as the dockcontainer and the datagrid do: every change of the layout is announced, whether it comes from the user or from your code |
crosstab: of_export_csv, of_export_xlsx to a relative path | written in the current folder of the moment — which a DirList or a file dialog may have moved; ue_csv_saved / ue_xlsx_saved gave the path as given | written in the folder the application started in, as the other native exports; the event gives the full path; a path that depends on the current folder of a drive (\x, C:x) is refused |
The recipe: an application that saves the layout in ue_layout_changed saves the same value again after of_set_layout — harmless. An export that relied on the current folder of the moment gives a full path.