Members
(static, constant) BUTTON_SPACING
Layout is DERIVED, not a stack of fixed offsets: every block is measured and the next one flows below it. Fixed offsets could not survive a title that wraps to two lines, a message of any length, or a button row whose width depends on the translation.
The flow itself — the order of the blocks and the gaps between them — lives in source/utils/dialogLayout.bs, shared with JRListDialog and OverviewDialog and unit-tested there. What stays here is what is specific to THIS dialog: a panel that grows to fit its button row, and the row that made it grow.
- Default Value
- 24
(static, constant) MAX_BUTTONS
- Default Value
- 3
(static, constant) OPENING_ANNOUNCEMENT_DELAY
The ONE piece of narration timing that cannot be event-driven.
Established on-device, one behavior at a time:
- The platform announces the focused button ONLY when focus first enters the dialog. Moving between buttons afterwards is silent (TextButton extends Group, not Roku's Button, so the "text of button is spoken only if focused" rule never applies). Those moves are ours alone, and need no timing.
- That one initial announcement FLUSHES ours. Speaking from the focus observer, which is as early as any event lets us react, still loses: the platform speaks after us and we are cut off. Speaking before focus loses the same way. To survive we must speak AFTER the platform's announcement, and SceneGraph exposes no event for "the platform has finished dispatching speech" — the only observable speech events are for utterances the app itself starts. So the opening announcement is the one place a delay is unavoidable.
Keep it short: long enough to land second, short enough to flush the platform's word before it is audible. At 0.6s the clipped word was clearly audible. Changing this needs an ear, not reasoning — there is no test that can catch it.
- Default Value
- 0.15
(static, constant) PANEL_MAX_WIDTH
- Default Value
- 1200
(static, constant) PANEL_WIDTH
Width starts here and GROWS to fit the button row (long translations), up to PANEL_MAX_WIDTH. Past that the row stacks vertically instead of overflowing.
Both on the 6px scale so the panel's left and right edges land on the 720p output grid: (1920 - w) / 2 = 960 - w/2, so a width divisible by 6 gives a panelX divisible by 3. 760 did not, which put both vertical edges off it.
- Default Value
- 762
Methods
(static) applyButtonLayout(contentWidth) → {void}
Horizontal row normally; vertical, full-width buttons when they cannot fit.
| Name | Type | Description |
|---|---|---|
contentWidth | integer |
- Type:
- void
(static) applyLayout() → {void}
Position the chrome. Called on any content change; safe to run repeatedly.
Everything is measured then flowed, so a two-line title, a long message and a wide button row each push what follows instead of colliding with it.
- Type:
- void
(static) buttonRowHeight() → {integer}
Rendered height of the button row, or a computed fallback before it lays out (localBoundingRect is 0 until the TextButtons size themselves).
- Type:
- integer
(static) cancelDialog() → {void}
Cancel this dialog from OUTSIDE — see cancelOpenDialog() in source/utils/dialogs.bs. Routed through resolveDialog so the once-only guard and the standard cancelled result both apply: a third party clearing the screen must be indistinguishable from the user pressing Back, or the owner waiting on the result is stranded.
- Type:
- void
(static) centerButtonRow() → {void}
Centre the row on the PANEL, not the screen — they only coincided while the panel was a fixed width centred on screen.
- Type:
- void
(static) closeDialog() → {void}
Dismiss this overlay. Appended directly to the scene by dialogs.bs, so closing is a removeChild from our parent + restoring focus to the opener (returnFocusTo). Mirrors OverviewDialog.closeDialog().
- Type:
- void
(static) focusButton(index) → {void}
| Name | Type | Description |
|---|---|---|
index | integer |
- Type:
- void
(static) init() → {void}
- Type:
- void
(static) moveButtonFocus(delta) → {void}
Step button focus, WRAPPING at the ends to match JRButtonGroup — the app's other horizontal button row wraps, so a dialog that dead-ends feels broken.
Narration of the newly focused button is handled by the focusedChild observer, so it covers remote-driven moves and programmatic ones alike.
| Name | Type | Description |
|---|---|---|
delta | integer |
- Type:
- void
(static) naturalButtonRowWidth() → {integer}
Width the row wants at its natural size, before any panel growth.
- Type:
- integer
(static) onButtonFocusChanged() → {void}
Screen-reader announcement, driven by the focus event itself.
The platform announces the focused button the moment focus lands, and app speech and platform speech interrupt each other rather than queueing — so we have to speak second to be the one that survives. The focus change IS that moment, so reacting to it needs no guess about timing (an earlier version used a tuned delay; the value was a guess that would rot on different hardware).
First focus = the dialog opening: announce the whole thing, folding in the button so it is one utterance in the built-in order. Later focus changes announce just the button the user moved to — nothing else has changed.
- Type:
- void
(static) onButtonReady() → {void}
A button finished sizing itself. Re-run the whole layout, not just the centring: the panel's width and height and the row's position are all derived from the buttons' real measurements. Laying out once from an estimate left the bottom padding short, and left a stacked row positioned off the panel.
- Type:
- void
(static) onButtonsChanged() → {void}
- Type:
- void
(static) onKeyEvent(key, press) → {boolean}
The decision itself is buttonDialogKeyAction in source/utils/dialogKeys.bs, which is pure and unit-tested. Everything here is the doing.
| Name | Type | Description |
|---|---|---|
key | string | |
press | boolean |
- Type:
- boolean
(static) onMessageChanged() → {void}
- Type:
- void
(static) onMessageRendered() → {void}
- Type:
- void
(static) onOpeningAnnouncementDue() → {void}
Announce the whole dialog, once the platform's own focus announcement is out of the way. Folds in the focused button so this is a single utterance in the built-in order: title, message, button.
- Type:
- void
(static) onSubheadingChanged() → {void}
SET THE TEXT FIELDS BEFORE PRESENTING, never after. renderTracking fires on the none -> full transition and never again (the same property that made the button row use TextButton's isReady instead), so once the dialog has rendered no later text change re-runs applyLayout — the new text draws on top of the old layout. True of title and message too; the helpers in source/utils/dialogs.bs set every field before presentOverlayDialog for exactly this reason.
- Type:
- void
(static) onSubheadingRendered() → {void}
- Type:
- void
(static) onTitleChanged() → {void}
- Type:
- void
(static) onTitleMeasured() → {void}
- Type:
- void
(static) resolveDialog(index) → {void}
Resolve the dialog exactly once, then close. index = -1 means cancelled (back).
| Name | Type | Description |
|---|---|---|
index | integer |
- Type:
- void
(static) settleLayout() → {void}
Lay out NOW, called by presentOverlayDialog once this dialog is attached.
The dialog is hidden until this returns, so there is no renderTracking to wait on — and none is needed, because localBoundingRect() answers while hidden. See dialogReveal.bs.
- Type:
- void