Skip to content

06 Canvas Dialog

PlayeRom edited this page Nov 17, 2025 · 1 revision

This framework allows you to create Canvas windows in two ways:

  1. 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).
  2. 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, the hide() 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.

  1. The /framework/nasal/Canvas/BaseDialogs/TransientDialog.nas class – 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.
  2. The /framework/nasal/Canvas/BaseDialogs/PersistentDialog.nas class – as you can see, there's more code needed to properly handle such a window.

PersistentDialog Class

When creating a dialog that inherits from PersistentDialog, the following happens in the PersistentDialog.new() method:

  1. The window is hidden immediately after its creation, because we don't want it to automatically display without user action.
  2. The del() method of the Window object is overridden by our function. FlightGear itself can call the Window.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 the destroy_on_close flag with the value false when creating the window, and FlightGear itself would call the hide() method instead of del(). However, FlightGear won't gain anything extra, and you might need to call hide() 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 the hide() method of the PersistentDialog class and stop the timer there; it's logical. However, Nasal doesn't support polymorphism. That is, if the base class calls its hide() method, your dialog's hide() will not be called! Therefore, the PersistentDialog class already includes implemented logic for calling its child methods. This is handled by the _callMethodByChild() method. However, for the PersistentDialog class to know who its child is, you must tell it by calling the setChild() method.

Center position

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.

Minimal example of creating a Transient dialog

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);
    },
};

Minimal example of creating a Persistent dialog

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);
    },
};

Deferring Canvas loading

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.

Clone this wiki locally