-
Notifications
You must be signed in to change notification settings - Fork 0
06 Canvas Dialog
This framework allows you to create Canvas windows in two ways:
- Transient – the window is created only when the user executes an open action, e.g. by menu item. When the window is closed, the window is destroyed (removed from memory).
-
Persistent – the window is created once, when the simulator starts, and is immediately hidden. When the user executes an open action, the window is simply shown (the
show()method). When the user closes the window, thehide()method is called.
| Advantages | Disadvantages | |
|---|---|---|
| Transient | Simple implementation. It's easier to destroy everything and recreate it. It only uses memory when the user wants to display the window. |
There's a noticeable 1-2 second delay in the window's content appearing because the window must first be created in memory. The window won't remember its last position and size because it's always recreated. |
| Persistent | The window's content is quickly displayed to the user because the window itself has already been created and is now just coming out of hiding. If the user resizes or moves the window, when it is reopened, the window will be as it was left. |
The window takes up memory, even if the user never opens it. It complicates the code somewhat because it's easier to delete everything and recreate it. |
Although the default behavior in FlightGear is to create and delete a window each time, I personally prefer windows created once – the memory is there to speed up the program, so I use it in my add-ons.
I ran a test for the Logbook add-on, which has 9 Persistent Canvas windows, some of them quite complex, and opened them all. RAM usage was ~150 MiB higher compared to the same add-on modified so that it did not create any Canvas windows. This averages out to ~17 MiB per window. As you can see, this is not a huge amount of memory usage.
This framework includes two base classes that you can inherit from to create the appropriate window type.
- The
/framework/nasal/Canvas/BaseDialogs/TransientDialog.nasclass – as you can see, not much happens there, other than adding support for the Esc key, as this type of window requires no additional handling. - The
/framework/nasal/Canvas/BaseDialogs/PersistentDialog.nasclass – as you can see, there's more code needed to properly handle such a window.
When creating a dialog that inherits from PersistentDialog, the following happens in the PersistentDialog.new() method:
- The window is hidden immediately after its creation, because we don't want it to automatically display without user action.
- The
del()method of theWindowobject is overridden by our function. FlightGear itself can call theWindow.del()method when the user clicks the X on the window bar. However, in the case of a Persistent window, we don't destroy the window, we hide it. You could pass thedestroy_on_closeflag with the valuefalsewhen creating the window, and FlightGear itself would call thehide()method instead ofdel(). However, FlightGear won't gain anything extra, and you might need to callhide()for additional actions in your dialog. For example, you have a dialog that needs to start its timer, but you need to stop the timer on the hide action, if only to prevent it from running in the background if it's not needed. In this case, your dialog must override thehide()method of thePersistentDialogclass and stop the timer there; it's logical. However, Nasal doesn't support polymorphism. That is, if the base class calls itshide()method, your dialog'shide()will not be called! Therefore, thePersistentDialogclass already includes implemented logic for calling its child methods. This is handled by the_callMethodByChild()method. However, for thePersistentDialogclass to know who its child is, you must tell it by calling thesetChild()method.
Additionally, the PersistentDialog class has additional logic for positioning the window in the center of the screen. By default, FlightGear opens Canvas windows in the center of the screen. However, a problem arises in Persistent dialog when the user changes the window size. For example, a user launched the simulator in an 800x600 window but later stretched it to the 1920x1080 resolution. This means that when the Persistent window was created, the resolution was 800x600, and the window calculated the center for that resolution. Therefore, when the user opens the Persistent window after changing the resolution to 1920x1080, the window will not be centered on the screen, but will instead be displayed in the upper-left corner. Therefore, the PersistentDialog class solves this problem by adding listeners (_addScreenSizeListeners()), which react to the FlightGear window's resolution change and recalculate the center of the screen.
In TransientDialog you don't need this logic because the Transient is always created anew, so it will always adjust to the current center of the screen.
var AboutDialog = {
#
# Constructor.
#
# @return hash
#
new: func {
var obj = {
parents: [
AboutDialog,
TransientDialog.new( # Inheriting from the TransientDialog class
width: 300,
height: 400,
title: "About",
),
],
};
# Create your stuff here ...
# Dialog already has a canvas.VBoxLayout prepared for adding more elements to the dialog:
# obj._vbox.addItem(...);
return obj;
},
#
# Destructor.
#
# @return void
# @override TransientDialog
#
del: func {
# Destroy your stuff here if needed...
call(TransientDialog.del, [], me);
},
};var AboutDialog = {
#
# Constructor.
#
# @return hash
#
new: func {
var obj = {
parents: [
AboutDialog,
PersistentDialog.new( # Inheriting from the PersistentDialog class
width: 300,
height: 400,
title: "About",
),
],
};
# Let the parent know who their child is.
call(PersistentDialog.setChild, [obj, AboutDialog], obj.parents[1]);
# Enable correct handling of window positioning in the center of the screen.
call(PersistentDialog.setPositionOnCenter, [], obj.parents[1]);
# Create your stuff here ...
# Dialog already has a canvas.VBoxLayout prepared for adding more elements to the dialog:
# obj._vbox.addItem(...);
return obj;
},
#
# Destructor.
#
# @return void
# @override PersistentDialog
#
del: func {
# Destroy your stuff here...
call(PersistentDialog.del, [], me);
},
#
# Show the dialog.
#
# @return void
# @override PersistentDialog
#
show: func {
# Add more stuff here on show the window if needed...
call(PersistentDialog.show, [], me);
},
#
# Hide the dialog.
#
# @return void
# @override PersistentDialog
#
hide: func {
# Add more stuff here on hide the window if needed, like stop timer, etc...
call(PersistentDialog.hide, [], me);
},
};Creating Canvas windows immediately when the simulator starts (PersistentDialog) has another drawback I haven't mentioned yet. Many aircraft developers assume that Canvas indices and textures will never change, and simply hardcode expectations like "the PFD texture is always at index 10." This can cause unintended side effects, such as your dialog boxes appearing on aircraft displays!
To avoid this, the add-on defers the creation of its PersistentDialog windows by 3 seconds (see timer in /framework/nasal/Bootstrap.nas file). This allows the aircraft's Canvas windows to be created first, and only then initializes the add-on's windows.
This approach also requires disabling any menu items that open Canvas windows until those windows have been created. Otherwise, clicking such a menu item could try to show a non-existent Canvas window and cause the add-on to crash. Therefore, the menu item that operates on the Persistent dialog should have the <name> tag set with some unique name (see the /addon-menubar-items.xml file).
The framework will first disable all menu items that contain the <name> tag, and then automatically re-enable them after the onInitCanvas hook is called, unless you've specified the names of menu items that you don't want to be automatically re-enabled in the excludedMenuNamesForEnabled hook. This can be useful if you need to manually control menu re-enablement due to other factors. In that case, you should call gui.menuEnable('your-name-of-menu-item', true); in the appropriate place in your code.
If aircraft implementations improve, or if FlightGear introduces a proper solution, this delay will no longer be necessary.
Of course, for simpler cases, you can also solve this differently, for example, by always creating a Persistent dialog for a menu action (if it hasn't been created yet). Then all this delay-loading logic might be unnecessary. But then you'll need more logic in the menu.