import "pkg:/source/api/apiPromise.bs"
import "pkg:/source/utils/misc.bs"
' JRScreen has no init() on purpose. The roku-log manager is owned by JRScene.init():
' see docs/architecture/logging.md. A duplicate call here was a no-op in the app
' (`addFields` ignores an existing field) and in Rooibos (BaseTestSuite provisions `rLog`
' before any screen mounts), while allocating a throwaway log_Log + Timer node on every
' screen mount.
' Function called when the screen is displayed by the screen manager
' It is expected that screens override this function to handle focus
' managmenet and any other actions required on screen shown
sub onScreenShown()
restoreFocus(m.top)
end sub
' Function called when the screen is hidden by the screen manager
' It is expected that screens override this function if required,
' to handle focus any actions required on the screen being hidden
sub onScreenHidden()
end sub
' Function called when the screen is being permanently removed by SceneManager
' Override this to clean up resources: task nodes, observers, large data structures
' Parent components are responsible for calling onDestroy() on children that need it
'
' Floor for screens that DON'T override onDestroy(): abandon any pending
' fetchAsync promises so a late pool response can't fire into the destroyed
' node. Screens that override onDestroy() get this via the auto-abandon-promises
' BSC plugin (it injects abandonApiPromises() into their own onDestroy). SG
' component onDestroy does not chain to this base.
sub onDestroy()
abandonApiPromises()
end sub
' ---------------------------------------------------------------------------
' sgRouter view-lifecycle bridge
'
' sgRouter drives the views it mounts in its outlet through a promise-native
' lifecycle (onViewOpen / onViewSuspend / onViewResume / beforeViewClose) and
' asks them to take focus via handleFocus(). JellyRock screens instead
' implement onScreenShown / onScreenHidden / onDestroy and own their focus
' restoration via lastFocus. These overrides bridge the two contracts so every
' screen works under the router with NO per-screen changes.
'
' The router is the only thing that mounts/suspends/resumes/destroys screens:
' there is no scene stack.
'
' These names override sgrouter_View's no-op defaults; the interface
' declarations are inherited from sgrouter_View (via JRGroup), so no <function>
' entries are needed in JRScreen.xml.
' ---------------------------------------------------------------------------
' Router: this view has become active for the first time. Publish it as the
' active routed view BEFORE onScreenShown so JRScene's overhang controller and
' main.bs's playback-event observer-attach react to the right node. Screens that
' need route data read m.top.route (routeParams / queryParams / context) inside
' their own onScreenShown / onRouteUpdate overrides.
function onViewOpen(_params = {} as object) as dynamic
m.global.activeRoutedView = m.top
m.isShowFocusPending = true
onScreenShown()
return promises.resolve(invalid)
end function
' Router: a previously-suspended view has returned to the top of the stack (goBack
' uncovered it). Re-publish it as the active routed view, then restore via onScreenShown.
function onViewResume(_params = {} as object) as dynamic
m.isCoverPending = false
m.global.activeRoutedView = m.top
m.isShowFocusPending = true
onScreenShown()
return promises.resolve(invalid)
end function
' Router: a new view is being pushed on top of this one (still alive).
' Save focus the same way SceneManager.pushScene does so it can be restored.
'
' The view is still on screen here: every route suspends with "show", and the incoming view
' opens several message-loop turns later. So onScreenHidden waits for hideCovered, which
' JRScene calls in the step the incoming view opens; anything it changes on screen then
' changes in the same render pass as the swap.
function onViewSuspend(_params = {} as object) as dynamic
saveLastFocus()
m.isCoverPending = true
return promises.resolve(invalid)
end function
' JRScene's screen swap: the view that covers this one has opened. Hide this view and, if the
' router suspended it, run onScreenHidden. A view the router CLOSED instead (back, a cleared
' stack) already ran onScreenHidden in beforeViewClose, so it is only hidden.
sub hideCovered()
m.top.visible = false
if m.isCoverPending <> true then return
m.isCoverPending = false
onScreenHidden()
end sub
' Router: this view is about to be permanently destroyed.
function beforeViewClose(_params = {} as object) as dynamic
onScreenHidden()
onDestroy()
return promises.resolve(invalid)
end function
' Router: take remote focus. The router also calls this with routerFocused = false when focus
' LEFT its outlet (to the overhang, a dialog or AppWaitHost). Taking focus then would steal it
' back, so that notice is ignored while another node holds focus. The call right after the view
' is shown always takes focus, and a stranded focus (on the bare scene) is still recovered.
' Screens override restoreScreenFocus, not this.
function handleFocus(data = {} as object) as boolean
wasShowPending = m.isShowFocusPending = true
m.isShowFocusPending = false
if not wasShowPending and isValid(data) and data.routerFocused = false and isFocusHeldElsewhere()
return false
end if
restoreScreenFocus()
return true
end function
' Where this screen takes focus. Mirrors onScreenShown's focus rule (restore the saved
' descendant, else focus the view root). Screens that restore focus differently override this.
sub restoreScreenFocus()
restoreFocus(m.top)
end sub
' True when a node outside the bare scene holds focus. The scene itself holding it (or nothing)
' means focus is stranded.
function isFocusHeldElsewhere() as boolean
scene = m.top.getScene()
focused = scene
while isValid(focused) and focused.hasFocus() = false
focused = focused.focusedChild
end while
return isValid(focused) and not focused.isSameNode(scene)
end function
' Walk the focus chain to the deepest focused descendant and remember it in
' m.top.lastFocus. Lifted from SceneManager.pushScene's focus-save loop.
sub saveLastFocus()
if isValid(m.top.focusedChild)
focused = m.top.focusedChild
while isValid(focused) and focused.hasFocus() = false
focused = focused.focusedChild
end while
if isValid(focused)
m.top.lastFocus = focused
end if
end if
end sub