G3X Touch UI Framework
UI Views and the View Stack
The G3X Touch's UI framework is centered around the concept of views and the view stack.
Views render content to be displayed on the screen and handle user interactions with the touchscreen and bezel controls (knobs and buttons). All views are FS components that implement the interface UiView
. There are two types of views: pages and popups. Only one page can be open at a time. Meanwhile, multiple popups can be open simultaneously.
Open views are stored on the view stack. The open page is located at the bottom of the stack, and popups are pushed onto the stack in the order in which they were opened. The view at the top of the stack is considered the active view. Typically, all user interactions are routed to the active view.
Each view stack has two layers: the main layer and the overlay layer. As the name implies, views rendered in the overlay layer always appear on top of any views in the main layer. The main layer contains the open page and may also contain popups. The overlay layer only contains popups.
The view stack also maintains a history. Each time a view is pushed to the view stack, a new snapshot of the view stack state - a history state - is created and pushed onto the history stack. This allows previous history states to be restored via a 'Back' operation. The current state of the view stack is the active history state.
Every view stack maintains the same "original" history state: the open page is a special view called the empty view with no open popups. This history state, also referred to as the empty page state, is always maintained by the view stack and cannot be discarded; attempting to execute a Back operation while the empty page state is active has no effect.
View Lifecycle
UI views have a complicated lifecycle, since they can move between many different states. Each view stack has its own instance of any particular view. Therefore, views of the same type in different stacks do not implicitly share state.
A view is considered in-use when it appears in any history state that is currently in the history stack (including the active history state) and out-of-use otherwise.
A view is considered open when it appears in the active history state and closed otherwise. The onOpen()
and onClose()
lifecycle methods are called when a view switches between these two states.
Finally, a view is considered active or resumed when it appears at the top of the active history state, and inactive or paused otherwise. The onResume()
and onPause()
lifecycle methods are called when a view switches between these two states. Each view stack only has one active view at a time, and it is with this view that the user primarily interacts.
Views are always created initially in an out of use state. Views can also have different lifespans, determined by their lifecycle policy. The different possible lifecycle policies are included as members of the UiViewLifecyclePolicy
enum:
- Static: The view is created immediately when it is registered (see below) and is never destroyed. Should only be used by views that have persistent state and need to be created when the G3X Touch instrument is first initialized.
- Persistent: The view is created immediately before the first time it transitions to an in-use state and is never destroyed. Should be used by views that have persistent state.
- Transient: The view is created immediately before the first time it transitions to an in-use state and is destroyed when it transitions back to an out-of-use state. Should be used by views that do not have persistent state.
Views have a non-negligible memory footprint when created. Therefore, it is recommended that the transient lifecycle policy be used whenever possible to reduce memory load and increase stability in memory-limited scenarios oftentimes seen in low-spec PCs and the XBox consoles.
Ensure that transient views properly release all resources they use when they are destroyed to avoid memory leaks. Cleanup code should be included in the view's destroy()
method.
It is also good practice to ensure all views contain cleanup code, regardless of their lifecycle policy. This is because a mod or plugin can theoretically replace any registered view with another one (see Registering Views).
Panes
Each GDU has two panes: the PFD pane and the MFD pane. Each pane maintains its own independent view stack. One of these panes is always visible, while the other can be either visible or hidden depending on whether the GDU is in Fullscreen or Splitscreen mode. The always-visible pane is called the main pane while the other is called the splitscreen pane. For PFD GDUs, the PFD pane is the main pane. For MFD GDUs, the MFD pane is the main pane.
When a pane is hidden, its view stack reverts to the empty page state.
The UI Service
Each GDU contains one instance of UiService
, which is the class that controls all view-related state and logic. Switching between Fullscreen and Splitscreen modes, opening and closing views - all of these actions and more are handled by UiService
. All views are passed a reference to their parent UiService
instance as a prop.
Registering Views
Views must be registered with UiService
before they can be used. Each view is registered under a unique string key, which is then used to open and retrieve the view from UiService
. Views are registered on a per-view stack layer basis; different views may be registered under the same key to different view stacks and even to different layers within the same view stack. Finally, views are registered as factories - functions that create and return the registered view as a VNode
- instead of directly as instances of the view. This is to allow UiService
to handle the process of creating views in accordance with their lifecycle policies.
If a view is registered to a view stack layer under a key that is already registered to that layer, then the new view will replace the existing view registered under that key. If an instance of the existing view has already been created, it will be destroyed when it is replaced.
The UiViewKeys
enum contains all UI view keys defined and used by the base G3X Touch.
UI views can be registered by plugins using the registerUiViews()
method.
Interaction Events
The user interacts with the GDU in two ways: (1) through "touch" (mouse) events, and (2) through manipulating the GDU's "physical" knobs and buttons. Touch events are handled by various components rendered on the GDU screen that simulate touchscreen buttons, sliders, touchpads, etc. Meanwhile, the "physical" events are handled using the UiInteractionEvent
API.
When a user manipulates a physical knob or button, the interaction generates a UiInteractionEvent
. The event is then routed to a series of handlers implementing the UiInteractionHandler
interface. As each handler is notified of the event, it can either choose to handle the event or do nothing. If the event is handled, then no further handlers will be notified of the event. If the event is not handled, then the next handler is notified and the process repeats.
The event routing path depends on whether the event originated from a knob or a button. For buttons, the path is always the same:
- The active UI view in the MFD pane.
- The active UI view in the PFD pane.
- The default interaction handler defined in the UI service.
For knobs, the path depends on which knob was manipulated. If the single knob was manipulated (only found on GDU 470 units, which are not currently supported), then the path is the same as that for buttons (above). If the left or right knob was manipulated (only found on GDU 460 units), then the path is as follows:
- The active UI view in the pane displayed on the same side as the manipulated knob (this can be either the PFD or MFD pane for either knob depending on how the user has configured the GDU). If the GDU is in Fullscreen mode, then the visible pane receives events from both left and right knobs.
- The default interaction handler defined in the UI service.
Because left and right knob events are only routed to the pane that is "supposed to" handle them, views and other interaction handlers invoked by views need not concern themselves with whether they are in the left or right pane when receiving a left/right knob event. They can simply handle (or not) each knob event "as-is" when it is received.
The UiInteractionUtils
and UiKnobUtils
classes contain utility methods for working with the interaction events.
Bezel Rotary Knob Labels
Each GDU displays a contextual label for each bezel rotary knob in the status bar on the bottom of the screen. These labels are meant to describe the current functionality of the knobs.
Views can control what appears for each label using the knobLabelState
property defined by UiView
. Each knob has three actions that can each have its own label text: outer rotate, inner rotate, and inner push. The knobLabelState
property is a map of knob actions to label text requested by the view. Knob actions are represented by members of the UiKnobId
enum. Each status bar knob label is automatically formatted based on the requested label text for each of the knob's actions.
The requested label state defined by a view is automatically applied to the appropriate label when the view becomes the active view of its view stack. Therefore, just as with handling knob interaction events, views need not worry about whether they are the active view or are in the left or right pane when requesting a specific knob label state. They can request label state as if they have control of all knobs at all times, and the UI service will ensure the requested label states are routed only to the appropriate labels.
MFD Main Pages
The MFD pane has a special UI view called the main view. This view is opened as a page in the MFD view stack and displays the MFD main pages. MFD main pages are components that implement the interface MfdPage
. While the main view is open and active, the user can select one of several MFD main pages to be displayed, such as the Map page or the Active Flight Plan page.
Here is an example of the main view with the Map page selected and displayed.
MFD main pages have a lifecycle logic that is similar to that of UI views. A main page is staged when it is selected by a user and unstaged otherwise. After a page becomes staged and its parent main view is open, there is a short delay before the page is displayed. The page is open when it is displayed, and closed when it is not displayed. A main page can only be open when its parent main view is open. Finally, the open main page is resumed when its parent main view is resumed and paused when its parent main view is paused. Like with UI views, lifecycle methods are called when a main page transitions between any of these lifecycle states.
MFD main pages also handle UI interaction events and can request knob label states. While the main view is the active view, it will route all interaction events to the open and resumed main page before attempting to handle the events itself. The main view also passes the requested label state of its open main page through to the UI service. If the main view and its open main page both request a label for the same knob action, the label requested by the main page takes priority.
MFD main pages must be registered before they can be selected and displayed. Registration is handled through a registrar (MfdMainPageRegistrar
). Like with views, main pages are registered using factories. Unlike views, however, main pages all use one lifecycle policy: main pages are created immediately before the first time they are staged and are never destroyed.
If an MFD main page is registered under a key that is already registered, then the new page will replace the existing page registered under that key.
The MfdMainPageKeys
enum contains all MFD main page keys defined and used by the base G3X Touch.
MFD main pages can be registered by plugins using the registerMfdMainPages()
method.
PFD Pages
The default page on the PFD pane is the PFD instruments page, whose key is UiViewKeys.PfdInstruments
. On an MFD GDU, the user can configure the PFD pane to display other pages. These pages implement the UiView
interface. However, instead of being directly registered with UiService
, PFD pages are registered through a registrar (PfdPageRegistrar
). Once a PFD page is registered, users will be able to select the page via the Display Setup menu on MFD GDUs:
If a PFD page is registered under a key that is already registered, then the new page will replace the existing page registered under that key. If an instance of the existing page has already been created, then it will be destroyed when it is replaced.
PFD pages can be registered by plugins using the registerPfdPages()
method.