source_utils_dialogKeys.bs

' Key models for the dialog family, as pure decisions.
'
' WHY THIS IS NOT JUST `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 deliberately, so no caller has to know
' which layout its translations produced. up/down are GATED on `stacked` just as
' deliberately: 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.
'
' @param {string} key - the key name from onKeyEvent
' @param {boolean} stacked - true when the row is laid out vertically
' @returns {string} - "cancel" | "resolve" | "stepBack" | "stepForward" | "consume"
function buttonDialogKeyAction(key as string, stacked as boolean) as string
  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