Howto:Creating a Canvas GUI Widget

From FlightGear wiki
Jump to navigation Jump to search

As the integrated, and fully script-able, Canvas GUI develops, new widgets This is a link to a Wikipedia article need to be added [1]. This article shows how to create a Canvas GUI widget.

Background

Originally, suggested and discussed back in early 2014 by a few FlightGear core developers, the pui2canvas approach to parsing a subset of PUI/XML and mapping that to Canvas widgets/properties, has turned out to be a suitable fasttrack solution to get rid of the legacy PUI engine, without causing major regressions or tons of work. It seems, that probably nobody would object against removing PUI and replacing it by CUI, keeping the old PUI/XML syntax. At least for a while. Dropping PUI would be a huge step forward.[2]

For the time being, this approach has a number of major advantages, i.e. it has proven to be able to deal with existing UI resources (dialogs, and the Menubar), without the original legacy UI dialogs (or any C++ code) having to be touched/modified, so that the primary thing missing to parse/process these dialogs are a handful of missing Canvas widgets, which can also be implemented in scripting space usually.

Besides, this approach also is the only effort that can easily support procedurally created and updated dialogs like Aircraft Checklists, Tutorials, Joystick Configuration (as well as a number of custom aircraft dialogs).

Also, the pui2canvas approach can help address a number of long-standing PUI related issues, such as rendering arttfacts on some ATI/AMD GPUs, but also PUI/Canvas inter-operability issues related to Canvas Event Handling.

Status

1rightarrow.png See Canvas Widget Matrix for the main article about this subject.

Back in June 2016, there were just five widgets in the Canvas GUI:

In contrast, the PUI subsystem in FlightGear supports ~15 documented widgets (see $FG_ROOT/Docs/README.gui#l219 for a list of widgets made available to FlightGear via PUI/XML). For a complete list (including custom/undocumented widgets), refer to FGPUIDialog::makeObject() in flightgear/src/GUI/FGPUIDialog.cxx (line 850)

In addition, FlightGear introduces a handful of custom PUI widgets implemented in C++ space, that not even PUI itself supports directly:

However, many PUI widgets can be emulated/approximated by using a combination of existing widgets, and/or functionality found in existing widgets, i.e. by referring to their source code.

For instance, all the data needed to obtain a list of airports or waypoints can be queried using the Navdb APIs exposed via Nasal/CppBind, specifically:

  • findAirportsWithinRange()
  • findNavaidsWithinRange()

At this point, we can map arbitrary navdb calls to help populate a ScrollArea with entries.

Thus, what is primarily needed to implement support for arbitrary -list types is a ScrollArea that uses buttons (or labels) for each entry.

As of late June 2016, the following widgets are primarily needed to help getting rid of PUI (listed in ascending complexity):

The key ones really being the last 4 in that list (radio, slider, dial and combo/dropdown)

The <canvas> (README.gui | C++ code) widget is particularly easy to support, because it's just an embedded canvas that can be treated as a Canvas Image with its own Nasal code running to update the whole thing.


Most other widgets currently missing can be implemented by using a combination of these widgets, e.g. a ScrollArea with buttons can be used to implement any type of list (airportlist, waypointlist, property-browser etc), just by adding event handling callbacks to each entry/button that invoke the corresponding bindings:

This ScrollArea-based approach has the added benefit that improvements to the ScrollArea widget will automatically benefit widgets using it internally.

However, once these basic widgets are supported, additional FlightGear/flight simulation specific widgets may be implemented sooner or later, e.g. for picking a frequency, heading/course etc (see UI_Unification#Widget_tags).

Widget.nas

This is the base class for all widgets, all widgets must inherit from it, or from any of its child classes - it can be found in $FG_ROOT/Nasal/canvas/gui/Widget.nas, its main methods are:

  • new() (constructor)
  • setFixedSize()
  • setEnabled()
  • move()
  • setSize()
  • setFocus()
  • clearFocus()
  • onRemove()
  • ...

Widget.nas is a scripting space wrapper for the lower-level NasalWidget module (see Nasal/CppBind) in simgear/layout: https://sourceforge.net/p/flightgear/simgear/ci/next/tree/simgear/canvas/layout/NasalWidget.cxx The wrapper is registered and set up in $FG_SRC/Scripting/NasalCanvas.cxx: https://sourceforge.net/p/flightgear/flightgear/ci/next/tree/src/Scripting/NasalCanvas.cxx#l543

Part one

A new Canvas widget will have two parts to it.

The first provides the background code and the APIs for that widget. The second defines the appearance of the widget, i.e. its style.

The first part is a Nasal file in $FG_ROOT/Nasal/canvas/gui/widgets, e.g., . They must follow the basic structure of the example below, replacing

 gui.widgets.myWidget = {
    # our constructor
    new: func(parent, style, cfg){
        var m = gui.Widget.new(gui.widgets.myWidget);
        m._cfg = Config.new(cfg);
        m._focus_policy = m.NoFocus;
        m._setView(style.createWidget(parent, "myWidget", m._cfg));
 
        return m;
    }, # new() constructor
    # widget-specific APIs come below, e.g., setText()
 };


Notes:

  • the file name, and class name, are by convention using a capital letter
  • All widgets inherit from canvas.gui.Widget (or any of its child classes).
  • The configuration hash can be used to alter the function of the widget.
  • The string "mywidget" corresponds to the name of the widget below.
  • In order the access methods in the code in next section, use me._view.<API>. MVC This is a link to a Wikipedia article
  • In order to access the top-level root group, use me._view._root, to see what else is available, use debug.dump(me._view)

Part two

The second part is a section contained in $FG_ROOT/Nasal/canvas/gui/styles/DefaultStyle.nas.

The example below shows the minimum code needed, replace mywidget with your widget's name:

 DefaultStyle.widgets.mywidget = {
    new: func(parent, cfg){
        me._root = parent.createChild("group", "mywidget");
    },
    setSize: func(model, w, h){
        # ...
        # code for changing size
        # ...

        return me;
    },
    update: func(model){}
    # other APIs below, e.g., setBackground()
};


Notes:

  • the setSize() and update() methods must exist, although update() may be empty.
  • Some APIs from Part 1, especially ones that change the widget's appearance/size, will need to fall to this code, see note above.
  • In particular, the layout logic (e.g. hbox, vbox) need a way to notify a widget that it needs to be updated/resized, which is accomplished through these methods

Accessing colors

The colors of some parts of the Canvas GUI are loaded from $FG_ROOT/gui/styles/AmbianceClassic/style.xml (note that AmbianceClassic is, as of June 2016, the only style). You can define colors for your new widget there and access them in from the code above using, for example, me._style.getColor("<color name>");.

Background images

If your widget needs background images (e.g., the button widget), they must be placed in $FG_ROOT/gui/styles/AmbianceClassic/widgets (again, AmbianceClassic is the only style as of June 2016). You can then find their folder using me._style._dir_widgets. See the fgdata/Nasal/canvas/gui/widgets/Button.nas widget for an example.

In general, Canvas widgets will need some custom artwork - for the time being, Tom simply reused existing artwork, e.g. from Lubuntu/gtk (see the CheckBox implementation for reference) The corresponding artwork resides in $FG_ROOT/styles/AmbianceClassic: fgdata/gui/styles/AmbianceClassic/style.xml


Part three

All that remains is registering the new widget. To do this, open $FG_ROOT/Nasal/canvas/gui.nas and add a new loadWidget("myWidget"); to the section where the others are loaded (line 23 onwards), for example:

loadWidget("myWidget");

Notes:

Examples

This article is a stub. You can help the wiki by expanding it.

Implementing a slider widget

We can approximate a simple slider by using three buttons, with the middle button having a registered drag/drop listener, so that it can be repositioned accordingly

 gui.widgets.slider = {
    # our constructor
    new: func(parent, style, cfg){
        var m = gui.Widget.new(gui.widgets.slider);
        m._cfg = Config.new(cfg);
        m._focus_policy = m.NoFocus;
        m._setView(style.createWidget(parent, "slider", m._cfg));
 
        return m;
    }, # new() constructor
    # widget-specific APIs come below, e.g., setText()
 };


To support both, vertical and horizontal, sliders, it makes sense to use the config hash and add a mode attribute to it, which may contain either horizontal or vertical

For an example, we can refer to the ScrollArea.nas code which is doing this sort of thing already.

Implementing a radio button

To implement a radio buttno widget, it makes sense to look at Button.nas and CheckBox.nas - i.e. we need a button that changes its background image based on being selected/hovered - which is basically what the checkbox is doing, too:

Implementing a dropdown menu

To implement a simple dropdown/combo widget, we mainly need the following functionality:

  • a ScrollArea
  • a vbox layout
  • buttons for each entry added to the ScrollArea

Thus, instead of inheriting from gui.Widgets, we can directly inherit our combo widget from ScrollArea:

 gui.widgets.combo = {
    # our constructor
    new: func(parent, style, cfg){
        var m = gui.widgets.ScrollArea.new(gui.widgets.combo);
        m._cfg = Config.new(cfg);
        m._focus_policy = m.NoFocus;
        m._setView(style.createWidget(parent, "combo", m._cfg));
 
        return m;
    }, # new() constructor
    # widget-specific APIs come below, e.g., setText()
 };


Due to inheriting directly from the ScrollArea widget, we now have access to all its functionality:

  • setLayout()
  • getContent()
  • setColorBackgrond()
  • setSize()
  • scrollTo()
  • ...

Implementing a graph widget

Openvg-via-canvas.png
 gui.widgets.graph = {
    # our constructor
    new: func(parent, style, cfg){
        var m = gui.Widget.new(gui.widgets.graph);
        m._cfg = Config.new(cfg);
        m._focus_policy = m.NoFocus;
        m._setView(style.createWidget(parent, "graph", m._cfg));
 
        return m;
    }, # new() constructor
    # widget-specific APIs come below, e.g., setText()
 };


Implementing a Map widget

1rightarrow.png See MapStructure for the main article about this subject.

WIP.png Work in progress
This article or section will be worked on in the upcoming hours or days.
See history for the latest developments.
MapStructure Canvas widget prototyping

Assuming, we have access to a root group, the most minimal snippet of code to display a layered MapStructure map is the following:

var TestMap = root.createChild("map");
TestMap.setController("Aircraft position");
TestMap.setRange(25);

TestMap.setTranslation(width, height);
     var r = func(name,vis=1,zindex=nil) return caller(0)[0];
     foreach(var type; [r('VOR'),r('APT'), r('APS') ] )
            TestMap.addLayer(factory: canvas.SymbolLayer, type_arg: type.name, visible: type.vis, priority: type.zindex,);

Next, we need to wrap this in a Widget.nas child class and add the corresponding boilerplate code to DefaultStyle.nas:

diff --git a/Nasal/canvas/gui/widgets/Map.nas b/Nasal/canvas/gui/widgets/Map.nas
new file mode 100644
index 0000000..d26a902
--- /dev/null
+++ b/Nasal/canvas/gui/widgets/Map.nas
@@ -0,0 +1,25 @@
+gui.widgets.Map = {
+  new: func(parent, style, cfg)
+  {
+    var m = gui.Widget.new(gui.widgets.Map);
+    m._cfg = Config.new(cfg);
+    m._focus_policy = m.NoFocus;
+    m._setView( style.createWidget(parent, "map", m._cfg) );
+
+    var TestMap = m._view._root.createChild("map");
+    TestMap.setController("Aircraft position");
+    TestMap.setRange(35);
+     
+     TestMap.setTranslation(640/2, 480/2);
+     var r = func(name,vis=1,zindex=nil) return caller(0)[0];
+     foreach(var type; [r('VOR'),r('APT'), r('APS') ] )
+            TestMap.addLayer(factory: canvas.SymbolLayer, type_arg: type.name, visible: type.vis, priority: type.zindex,);
+
+    return m;
+  },
+  setBackground: func(bg)
+  {
+    me._view.setBackground(me, bg);
+    return me;
+  }
+};
diff --git a/Nasal/canvas/gui/styles/DefaultStyle.nas b/Nasal/canvas/gui/styles/DefaultStyle.nas
index 4d3de27..1f26dd4 100644
--- a/Nasal/canvas/gui/styles/DefaultStyle.nas
+++ b/Nasal/canvas/gui/styles/DefaultStyle.nas
@@ -451,3 +451,20 @@ DefaultStyle.widgets["scroll-area"] = {
     model._scroller_delta[dir] = model._size[dir] - model._scroller_size[dir];
   }
 };
+
+DefaultStyle.widgets.map = {
+    new: func(parent, cfg){
+        me._root = parent.createChild("group", "map");
+    },
+    setSize: func(model, w, h){
+        # ...
+        # code for changing size
+        # ...
+
+        return me;
+    },
+    update: func(model){
+       }
+    # other APIs below, e.g., setBackground()
+};
+

To actually instantiate the widget, use something along these lines (via the Nasal Console):

var (width,height) = (640,480);
var title = 'MapStructure widget test';

# create a new window, dimensions are WIDTH x HEIGHT, using the dialog decoration (i.e. titlebar)
var window = canvas.Window.new([width,height],"dialog").set('title',title);

# adding a canvas to the new window and setting up background colors/transparency
var myCanvas = window.createCanvas();

# Using specific css colors would also be possible:
myCanvas.set("background", "#ffaac0");

# creating the top-level/root group which will contain all other elements/group
var root = myCanvas.createGroup();


# create a new layout
var myLayout = canvas.HBoxLayout.new();

# assign it to the Canvas
myCanvas.setLayout(myLayout);

var map = canvas.gui.widgets.Map.new(root, canvas.style, {});

myLayout.addItem(map);
MapStructure map wrapped in a Canvas widget

Everything that we'd like to be able to customize at the widget level, needs to be exposed via a corresponding method added to the subclass inheriting from Widget.nas, for example:

  • setController()
  • setRange()
  • setLayers()
  • ...

In addition, most MapStructure symbology, appearance (scaling, styling etc) and behavior can be customized by overriding the defaults (which are otherwise still based on the original 744 ND implementation). For an example, see flightgear/fgdata/next/gui/dialogs/map-canvas.xml#l362:

Note  The Styles/Options hashes must have one entry for each added layer matching its name
                        ##
                        # Styling: This is a bit crude at the moment, i.e. no dedicated APIs yet - but it's
                        # just there to prototype things for now
                        var Styles = {};
                        Styles.get = func(type) return Styles[type];
                        var Options = {};
                        Options.get = func(type) return Options[type];

                        ## set up a few keys supported by the DME.symbol file to customize appearance:
                        Styles.DME = {};
                        Styles.DME.debug = 1; # HACK for benchmarking/debugging purposes
                        Styles.DME.animation_test = 0; # for prototyping animated symbols

                        Styles.DME.scale_factor = 0.4; # 40% (applied to whole group)
                        Styles.DME.line_width = 3.0;
                        Styles.DME.color_tuned = [0,1,0]; #rgb
                        Styles.DME.color_default = [1,1,0];  #rgb

                        Styles.APT = {};
                        Styles.APT.scale_factor = 0.4; # 40% (applied to whole group)
                        Styles.APT.line_width = 3.0;
                        Styles.APT.color_default = [0,0.6,0.85];  #rgb
                        Styles.APT.label_font_color = Styles.APT.color_default;
                        Styles.APT.label_font_size=28;


To approximate the appearance of the legacy Map dialog, we will be using a hbox layout with two columns, with the left column containing a vbox layout with checkboxes added for each layer to control visibility of the layer, and the right hbox cell containing the actual MapStructure map:

Once we are finished, we can check if the widget is working properly by adding a handful of identical widgets to the same dialog, all of which should work independently, i.e. having their own checkbox/range widgets to control the corresponding map widget.

References

References
  1. bugman  (Jun 21st, 2016).  Re: Aircraft Center | pui2canvas .
  2. Torsten Dreyer  (Jun 14th, 2016).  Re: [Flightgear-devel] GUI options (Was: Aircraft center) .