Howto:Creating a Canvas GUI Widget

From FlightGear wiki
Revision as of 11:19, 3 July 2016 by Hooray (talk | contribs)
Jump to navigation Jump to search

Originally, suggested and discussed back in early 2014 by a few FlightGear core developers, the pui2canvas approach to parsing a subset of PUI/XML 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.[1]

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 and Tutorials (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.

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

As of June 2016, there are 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. 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.


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:

  • 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

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 hrule widget

The following example is based on adapting the line-drawing example taken from Canvas_Snippets#Adding_OpenVG_Paths. For instance, consider the following snippet (assuming there is already a root group):

var width = 640;
var height = 320;
var group = root.createChild("group");
var line = graph.createChild("path", "x-axis")
.moveTo(10, height/2)
.lineTo(width-10, height/2)
.setColor(1,0,0) # red 
.setStrokeLineWidth(3); # thickness

To adapt this to work as a widget, we need to get rid of the width/height variables and instead refer to the position vector in Widget.nas:

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


Map

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:

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()
  • ...

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. Torsten Dreyer  (Jun 14th, 2016).  Re: [Flightgear-devel] GUI options (Was: Aircraft center) .
  2. bugman  (Jun 21st, 2016).  Re: Aircraft Center | pui2canvas .