source_utils_dialogKeys.bs
' Key models for the dialog family, as pure decisions.
'
' WHY THIS LIVES OUTSIDE `onKeyEvent`. JRListDialog's first version reached its
' Cancel button by catching a `down` that BUBBLED OUT of the list, which works
' only while the list declines to handle it. Above 8 rows the list switched to
' `fixedFocusWrap`, wrapped forever, and never let a `down` past; Cancel became
' unreachable and `up` died with it. Both halves were correct in isolation and
' contradicted each other, and no test could see it because the decision lived
' inside a component's onKeyEvent where nothing can call it.
'
' So the decision moved here, where a test can ask "what does DOWN do on the
' last row of a 30-item list?" without a device, a scene, or a render.
import "pkg:/source/utils/misc.bs"
' What a key press means to a selection-list dialog.
'
' The list WRAPS IN BOTH DIRECTIONS and Back is the way out:
'
' OK on a row commits (the list's own business, not ours)
' DOWN past the last row wraps to the top
' UP past the first row wraps to the bottom
' BACK dismisses without choosing
' anything else consumed (a modal lets nothing reach the screen behind it)
'
' Symmetric on purpose. Wrapping is only worth having because it makes a long
' list cheap to navigate with a remote (one UP to reach the end of thirty
' tracks), and an asymmetric wrap does not deliver that; it only cycles forward.
'
' ItemDetails' TrackDropdown, which this otherwise mirrors, dismisses on UP-at-top
' instead. That is right for a dropdown: UP returns you to the trigger you opened
' it from. A centred modal has no trigger above it, so the gesture would be
' arbitrary here, and it would cost the one ergonomic win wrapping exists for.
'
' `focusedIndex` is checked EXPLICITLY rather than inferred from the fact that
' the key reached us. Inferring position from bubbling is precisely the
' assumption that broke: it holds only for a list configuration nobody promised
' would stay.
'
' @param {string} key - the key name from onKeyEvent
' @param {integer} focusedIndex - the list's currently focused row
' @param {integer} itemCount - number of rows in the list
' @returns {string} - "dismiss" | "wrapToTop" | "wrapToBottom" | "consume"
function listDialogKeyAction(key as string, focusedIndex as integer, itemCount as integer) as string
if key = "back" then return "dismiss"
' A list with fewer than two rows has nowhere to wrap to, so both directions
' are inert rather than a no-op jump onto the row you are already on.
if itemCount > 1
if key = "down" and focusedIndex >= itemCount - 1 then return "wrapToTop"
if key = "up" and focusedIndex <= 0 then return "wrapToBottom"
end if
return "consume"
end function
' What a key press means to a BUTTON-ROW dialog: JRDialog, and
' QuickConnectDialog, whose row happens to hold exactly one button.
'
' Here for the same reason listDialogKeyAction is: the decision was in a
' component's onKeyEvent, where nothing can call it, and it was wrong there.
' `applyButtonLayout` stacks the row vertically when the labels are too wide to
' fit across the panel, and onKeyEvent moved focus only on left/right. So a row
' the user could SEE was stacked ignored up/down entirely and was navigated along
' an axis that was not on screen. Nothing caught it: the only evidence was a
' `Down` press in an RTA take that had been a silent no-op since it was written.
'
' The model:
'
' BACK cancels
' OK resolves with the focused button
' LEFT / RIGHT step the row, in BOTH orientations
' UP / DOWN step the row only when it is stacked
' anything else consumed (a modal lets nothing reach the screen behind it)
'
' left/right work in both orientations on purpose, so no caller has to know
' which layout its translations produced. up/down are GATED on `stacked` on
' purpose too: moving focus along an axis the user cannot see is its own bug, so
' a horizontal row consumes them under modal containment rather than acting.
'
' Stepping WRAPS at the ends rather than clamping, matching JRButtonGroup, the
' app's other horizontal row. A dialog that dead-ends feels broken. The caller
' owns the wrap arithmetic; this function only says which direction to step.
'
' `anyKeyResolves` replaces all of that with one rule: every key, Back included,
' resolves with the focused button. It is for a dialog whose only question is
' "is anyone there?" (the still-watching prompt), where any press is the answer
' and making the viewer find a particular button would defeat it.
'
' @param {string} key - the key name from onKeyEvent
' @param {boolean} stacked - true when the row is laid out vertically
' @param {boolean} anyKeyResolves - true when any key answers the dialog
' @returns {string} - "cancel" | "resolve" | "stepBack" | "stepForward" | "consume"
function buttonDialogKeyAction(key as string, stacked as boolean, anyKeyResolves = false as boolean) as string
if anyKeyResolves then return "resolve"
if key = "back" then return "cancel"
if key = "OK" then return "resolve"
if key = "left" then return "stepBack"
if key = "right" then return "stepForward"
if stacked
if key = "up" then return "stepBack"
if key = "down" then return "stepForward"
end if
return "consume"
end function
' What a key press means to the overview dialog: a scrolling body above one OK
' button. The dialog is appended to the scene, so a key it does not consume
' bubbles into JRScene and acts on the screen behind it. The model therefore
' names every outcome, and "consume" is the answer to everything it does not act on.
'
' BACK closes
' OK on the button closes
' OK on the text moves focus to the button
' DOWN on the button consumed
' DOWN with text left below scrolls down
' DOWN at the end of the text moves focus to the button
' UP on the button moves focus to the text, when there is text to scroll
' UP with text left above scrolls up
' anything else consumed
'
' @param {string} key - the key name from onKeyEvent
' @param {boolean} okFocused - true when the OK button holds focus
' @param {integer} scrollPosition - how far the text is scrolled
' @param {integer} maxScroll - the furthest the text can scroll; 0 when it fits
' @returns {string} - "close" | "focusOk" | "focusText" | "scrollDown" | "scrollUp" | "consume"
function overviewDialogKeyAction(key as string, okFocused as boolean, scrollPosition as integer, maxScroll as integer) as string
if key = "back" then return "close"
if key = "OK"
if okFocused then return "close"
return "focusOk"
end if
if key = "down"
if okFocused then return "consume"
if maxScroll > 0 and scrollPosition < maxScroll then return "scrollDown"
return "focusOk"
end if
if key = "up"
if okFocused
if maxScroll > 0 then return "focusText"
return "consume"
end if
if scrollPosition > 0 then return "scrollUp"
return "consume"
end if
return "consume"
end function