2022-06-30 09:30:50 +00:00
|
|
|
# XML Dialog - XML dialog object without using PUI
|
2022-09-26 07:58:24 +00:00
|
|
|
# SPDX-FileCopyrightText: (C) 2022 James Turner <james@flightgear.org>
|
2022-06-30 09:30:50 +00:00
|
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
|
|
|
|
# alias this module to keep things somewhat readable
|
|
|
|
var cwidgets = canvas.gui.widgets;
|
|
|
|
|
|
|
|
# This is the Nasal peer of a dialog defined by XML
|
|
|
|
# it manages creating some top level Canvas (eg a Window) according
|
|
|
|
# to the XML data
|
|
|
|
var XMLDialog = {
|
|
|
|
|
|
|
|
init: func(dialogProps)
|
|
|
|
{
|
|
|
|
var d = me.dialog();
|
|
|
|
var sz = [d.width, d.height];
|
|
|
|
me._window = canvas.Window.new(sz, "dialog", d.name);
|
|
|
|
|
|
|
|
# FIXME: lookup translated dialog name, this is the
|
|
|
|
# key string here
|
|
|
|
me._window.setTitle(d.name);
|
|
|
|
|
|
|
|
var m = me;
|
|
|
|
me._window.del = func {
|
|
|
|
m.onWindowClosed();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
didBuild: func()
|
|
|
|
{
|
|
|
|
var ourCanvas = me._window.getCanvas(1); # create, probably
|
|
|
|
ourCanvas.set("background", canvas.style.getColor("bg_color"));
|
|
|
|
var rootCanvasGroup = ourCanvas.createGroup();
|
|
|
|
|
|
|
|
# get the root XMLObject, almost certainly a container of
|
|
|
|
# some kind. (eg frame / group / scroll area)
|
|
|
|
var rootObject = me.dialog().root;
|
|
|
|
|
|
|
|
# show the root object inside our Canvas group. We could delay this
|
|
|
|
# until we are made visible to make things more efficient/lazy
|
|
|
|
rootObject.show(rootCanvasGroup);
|
|
|
|
ourCanvas.setLayout(rootObject.layoutItem());
|
|
|
|
},
|
|
|
|
|
|
|
|
# this is the callback from the canvas.Window: we request a close of
|
|
|
|
# the dialog, which will end up with us in 'onClosed'
|
|
|
|
onWindowClosed: func
|
|
|
|
{
|
|
|
|
logprint(LOG_WARN, "XMLDialog window was requested to closed");
|
|
|
|
me.dialog().close();
|
|
|
|
},
|
|
|
|
|
|
|
|
onBringToFront: func()
|
|
|
|
{
|
|
|
|
me._window.raise();
|
|
|
|
},
|
|
|
|
|
|
|
|
onGeometryChanged: func()
|
|
|
|
{
|
|
|
|
var d = me.dialog();
|
|
|
|
logprint(LOG_INFO, "Dialog geometry is now:" ~ d.x ~ ", " ~ d.y ~ " w:" ~ d.width ~ ", h:" ~ d.height);
|
|
|
|
me._window.setPosition(d.x, d.y);
|
|
|
|
me._window.setSize(d.width, d.height);
|
|
|
|
},
|
|
|
|
|
|
|
|
onClose: func
|
|
|
|
{
|
|
|
|
logprint(LOG_WARN, "XMLDialog closed");
|
|
|
|
|
|
|
|
# call the base canvas.Window delete method, not
|
|
|
|
# our wrapper above.
|
|
|
|
call(canvas.Window.del, [], me._window);
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
# this is the callback function which C++ invokes, to create a peer
|
|
|
|
# for an XML dialog (and its C++ class). In the future, different kinds
|
|
|
|
# of dialog could be created if necessary (eg, with different window frame
|
|
|
|
# styles or frameless / non-draggable)
|
|
|
|
var _createDialogPeer = func(type)
|
|
|
|
{
|
|
|
|
logprint(LOG_INFO, "Creating dialog of type:" ~ type);
|
|
|
|
var z = gui.xml.Dialog.new({
|
|
|
|
parents: [XMLDialog]
|
|
|
|
});
|
|
|
|
|
|
|
|
return z;
|
|
|
|
};
|
|
|
|
|
|
|
|
var _getProp = func(objectProps, name, def)
|
|
|
|
{
|
|
|
|
var node = objectProps.getNode(name);
|
|
|
|
if (node == nil)
|
|
|
|
return def;
|
|
|
|
return node.getValue();
|
|
|
|
}
|
|
|
|
|
|
|
|
var _getBoolProp = func(objectProps, name, def)
|
|
|
|
{
|
|
|
|
var node = objectProps.getNode(name);
|
|
|
|
if (node == nil)
|
|
|
|
return def;
|
|
|
|
return node.getBoolValue();
|
|
|
|
}
|
|
|
|
|
|
|
|
var _getDoubleProp = func(objectProps, name, def)
|
|
|
|
{
|
|
|
|
var node = objectProps.getNode(name);
|
|
|
|
if (node == nil)
|
|
|
|
return def;
|
|
|
|
return node.getDoubleValue();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#################################################################################################
|
|
|
|
|
|
|
|
var XMLObjectBase =
|
|
|
|
{
|
|
|
|
_configValue: func(name, def = nil)
|
|
|
|
{
|
|
|
|
var node = me.config.getNode(name);
|
|
|
|
if (node == nil)
|
|
|
|
return def;
|
|
|
|
return node.getValue();
|
|
|
|
},
|
|
|
|
|
|
|
|
_configDouble: func(name, def = 0.0)
|
|
|
|
{
|
|
|
|
var node = me.config.getNode(name);
|
|
|
|
if (node == nil)
|
|
|
|
return def;
|
|
|
|
return node.getDoubleValue();
|
|
|
|
},
|
|
|
|
|
|
|
|
_configBool: func(name, def = 0)
|
|
|
|
{
|
|
|
|
var node = me.config.getNode(name);
|
|
|
|
if (node == nil)
|
|
|
|
return def;
|
|
|
|
return node.getBoolValue();
|
|
|
|
},
|
|
|
|
|
|
|
|
_applyLayoutConfig: func()
|
|
|
|
{
|
|
|
|
var halign = me._configValue("halign");
|
|
|
|
var valign = me._configValue("valign");
|
|
|
|
var l = me.layoutItem();
|
|
|
|
if (!l)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (halign or valign) {
|
|
|
|
var ha = 0;
|
|
|
|
if (halign == "left") {
|
|
|
|
ha = canvas.AlignLeft;
|
|
|
|
} elsif (halign == "center") {
|
|
|
|
ha = canvas.AlignHCenter;
|
|
|
|
} elsif (halign == "right") {
|
|
|
|
ha = canvas.AlignRight;
|
|
|
|
}
|
|
|
|
|
|
|
|
var va = 0;
|
|
|
|
if (valign == "top") {
|
|
|
|
va = canvas.AlignTop;
|
|
|
|
} elsif (valign == "center") {
|
|
|
|
va = canvas.AlignVCenter;
|
|
|
|
} elsif (valign == "bottom") {
|
|
|
|
va = canvas.AlignBottom;
|
|
|
|
}
|
|
|
|
|
|
|
|
l.setAlignment(ha + va);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ghosttype(l) == "canvas.Widget") {
|
|
|
|
var fixedWidth = me._configValue("width");
|
|
|
|
var fixedHeight = me._configValue("height");
|
|
|
|
if (fixedWidth or fixedHeight) {
|
|
|
|
# query existing values in case we only write to
|
|
|
|
# one axis, instead of both
|
|
|
|
var minSize = l.minimumSize();
|
|
|
|
var maxSize = l.maximumSize();
|
|
|
|
if (fixedWidth) {
|
|
|
|
minSize[0] = fixedWidth;
|
|
|
|
maxSize[0] = fixedWidth;
|
|
|
|
}
|
|
|
|
if (fixedHeight) {
|
|
|
|
minSize[1] = fixedHeight;
|
|
|
|
maxSize[1] = fixedHeight;
|
|
|
|
}
|
|
|
|
l.setMaximumSize(maxSize);
|
|
|
|
l.setMinimumSize(minSize);
|
|
|
|
}
|
|
|
|
|
|
|
|
var prefWidth = me._configValue("pref-width");
|
|
|
|
var prefHeight = me._configValue("pref-height");
|
|
|
|
if (prefWidth or prefHeight) {
|
|
|
|
var hint = l.sizeHint();
|
|
|
|
if (prefWidth) {
|
|
|
|
hint[0] = prefWidth;
|
|
|
|
}
|
|
|
|
if (prefHeight) {
|
|
|
|
hint[1] = prefHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
l.setSizeHint(hint);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
# layout item is not a NasalWidget, so lacks public
|
|
|
|
# setters for these. Could extend the API if we need to
|
|
|
|
# cover these cases where XML dialogs specify sizes on
|
|
|
|
# layouts
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
view: func { return me._view; },
|
|
|
|
layoutItem : func { return me._layout; }
|
|
|
|
};
|
|
|
|
|
|
|
|
#################################################################################################
|
|
|
|
# XML object peers
|
|
|
|
#
|
|
|
|
# For each object defined in XML, we create a C++ object (PUICompatObject) and invoke
|
|
|
|
# the callback below (_createCompatObject) to create a corresponding Nasal peer. There is
|
|
|
|
# no requirement for a 1:1 mapping from XML types to the classes below.
|
|
|
|
#
|
|
|
|
# The Nasal peer class responds to callbacks from C++, especially showing and hiding, to
|
|
|
|
# create some visual representation of the object. It's important that this view of the
|
|
|
|
# object is only created on demand, so that invisible GUI elements do not create Canvas
|
|
|
|
# objects, which have a rendering cost.
|
|
|
|
|
|
|
|
var XMLButton =
|
|
|
|
{
|
|
|
|
init: func(objectProps)
|
|
|
|
{
|
|
|
|
me._isDefault = me._configBool("default");
|
|
|
|
me._label = me.configValue("legend");
|
|
|
|
},
|
|
|
|
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
|
|
|
me._view = cwidgets.Button.new(viewParent, canvas.style, {});
|
|
|
|
me._view.setText(me._label);
|
|
|
|
me._layout = me._view;
|
|
|
|
|
|
|
|
if (me._isDefault) {
|
|
|
|
me._view.setDefault(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
# hook up the button to our bindings
|
|
|
|
me._view.listen("clicked", func me.activateBindings(); );
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
var XMLCheckbox =
|
|
|
|
{
|
|
|
|
init: func(objectProps)
|
|
|
|
{
|
|
|
|
me._label = me.configValue("label");
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
|
|
|
me._view = cwidgets.CheckBox.new(viewParent, canvas.style, {});
|
|
|
|
me._view.setText(me._label);
|
|
|
|
me._layout = me._view;
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
me.update();
|
|
|
|
return me._view;
|
|
|
|
},
|
|
|
|
|
|
|
|
update: func()
|
|
|
|
{
|
|
|
|
if (me._view == nil)
|
|
|
|
return;
|
|
|
|
|
|
|
|
var v = me.value;
|
|
|
|
if (v) {
|
|
|
|
me._view.setText(str(v));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
var XMLLabel =
|
|
|
|
{
|
|
|
|
init: func(objectProps)
|
|
|
|
{
|
|
|
|
me._label = me.configValue("label");
|
|
|
|
},
|
|
|
|
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
|
|
|
me._view = cwidgets.Label.new(viewParent, canvas.style, {});
|
|
|
|
me._view.setText(me._label);
|
|
|
|
me._layout = me._view;
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
me.update();
|
|
|
|
return me._view;
|
|
|
|
},
|
|
|
|
|
|
|
|
update: func()
|
|
|
|
{
|
|
|
|
if (me._view == nil)
|
|
|
|
return;
|
|
|
|
|
|
|
|
var v = me.value;
|
|
|
|
if (v) {
|
|
|
|
me._view.setText(str(v));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var XMLGroup =
|
|
|
|
{
|
|
|
|
init: func(objectProps)
|
|
|
|
{
|
|
|
|
me._layoutType = me.configValue("layout");
|
|
|
|
me._padding = me._configDouble("default-padding");
|
|
|
|
},
|
|
|
|
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
|
|
|
me._view = viewParent.createChild("group");
|
|
|
|
|
|
|
|
# check for layout on us
|
|
|
|
var layout = nil;
|
|
|
|
if (me._layoutType == "hbox") {
|
|
|
|
layout = canvas.HBoxLayout.new();
|
|
|
|
} elsif (me._layoutType == "vbox") {
|
|
|
|
layout = canvas.VBoxLayout.new();
|
|
|
|
} elsif (me._layoutType == "table") {
|
|
|
|
layout = canvas.GridLayout.new();
|
|
|
|
} else {
|
|
|
|
logprint(LOG_WARN, "Unknown layout type:" ~ me._layoutType);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (layout != nil) {
|
|
|
|
me._layout = layout;
|
|
|
|
layout.setSpacing(me._padding);
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (var c; me.children) {
|
|
|
|
# create view for each child
|
|
|
|
c.show(me._view);
|
|
|
|
if (layout != nil) {
|
|
|
|
var childItem = c.layoutItem();
|
|
|
|
|
|
|
|
if (me._layoutType == "table") {
|
|
|
|
var gpos = c.gridLocation();
|
|
|
|
layout.addItem(childItem, gpos.column, gpos.row, gpos.columnSpan, gpos.rowSpan);
|
|
|
|
} else {
|
|
|
|
layout.addItem(childItem);
|
|
|
|
|
|
|
|
# TODO: support 'equal' configBool here, set
|
|
|
|
# stretch factor as well?
|
|
|
|
|
|
|
|
# old layout.cxx code only implements stretch on
|
|
|
|
# hbox and vbox, so this is the correct equivalent place
|
|
|
|
# for compatability
|
|
|
|
if (c._configBool("stretch")) {
|
|
|
|
layout.setStretchFactor(childItem, 1.0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} # of children show+layout iteration
|
|
|
|
# record the childView elsewhere?
|
|
|
|
}
|
|
|
|
|
|
|
|
return me._view;
|
|
|
|
},
|
|
|
|
|
|
|
|
update: func()
|
|
|
|
{
|
|
|
|
# re-create children is we are visible?
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var XMLSlider =
|
|
|
|
{
|
|
|
|
init: func(objectProps)
|
|
|
|
{
|
|
|
|
# TODO: support vertical sliders
|
|
|
|
},
|
|
|
|
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
|
|
|
me._view = cwidgets.Slider.new(viewParent, canvas.style, {});
|
|
|
|
me._layout = me._view;
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
me.update();
|
|
|
|
return me._view;
|
|
|
|
},
|
|
|
|
|
|
|
|
update: func()
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-09-26 07:58:24 +00:00
|
|
|
var XMLDial =
|
|
|
|
{
|
|
|
|
init: func(objectProps)
|
|
|
|
{
|
|
|
|
logprint(LOG_INFO, "Init of XMLDial");
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
|
|
|
me._view = cwidgets.Dial.new(viewParent, canvas.style, {});
|
|
|
|
me._layout = me._view;
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
me.update();
|
|
|
|
return me._view;
|
|
|
|
},
|
|
|
|
|
|
|
|
update: func()
|
|
|
|
{
|
|
|
|
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-06-30 09:30:50 +00:00
|
|
|
var XMLTextEdit =
|
|
|
|
{
|
|
|
|
init: func(objectProps)
|
|
|
|
{
|
|
|
|
# TODO: config to restrict to numerical / decimal input, etc
|
|
|
|
},
|
|
|
|
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
|
|
|
me._view = cwidgets.LineEdit.new(viewParent, canvas.style, {});
|
|
|
|
me._layout = me._view;
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
me.update();
|
|
|
|
return me._view;
|
|
|
|
},
|
|
|
|
|
|
|
|
update: func()
|
|
|
|
{
|
|
|
|
# don't overwrite if it has focus
|
|
|
|
if (me._view and !me._view.hasActiveFocus()) {
|
|
|
|
me._view.setText(str(me.value));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var XMLEmpty =
|
|
|
|
{
|
|
|
|
init: func(objectProps)
|
|
|
|
{
|
|
|
|
},
|
|
|
|
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
|
|
|
# TODO: add Nasal-Canvas API to create spacer items
|
|
|
|
# explicitly
|
|
|
|
me._view = canvas.createChild("empty", "group");
|
|
|
|
me._layout = canvas.Spacer.new();
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
|
|
|
|
return me._view;
|
|
|
|
},
|
|
|
|
|
|
|
|
update: func()
|
|
|
|
{
|
|
|
|
me._view.setSize(me._size);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var XMLHRule =
|
|
|
|
{
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
2023-01-11 10:53:52 +00:00
|
|
|
var label = me.configValue("label");
|
|
|
|
|
2022-06-30 09:30:50 +00:00
|
|
|
me._view = cwidgets.HorizontalRule.new(viewParent, canvas.style, {});
|
2023-01-11 10:53:52 +00:00
|
|
|
if (label) {
|
|
|
|
me._view.setText(label);
|
|
|
|
}
|
|
|
|
|
2022-06-30 09:30:50 +00:00
|
|
|
me._layout = me._view;
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
return me._view;
|
2023-01-11 10:53:52 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
update: func()
|
|
|
|
{
|
|
|
|
# allow label to be updated live
|
|
|
|
var l = me.value();
|
|
|
|
if (l) {
|
|
|
|
me._view.setText(l);
|
|
|
|
}
|
|
|
|
|
|
|
|
me._view.setSize(me._size);
|
2022-06-30 09:30:50 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var XMLVRule =
|
|
|
|
{
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
|
|
|
me._view = cwidgets.VerticalRule.new(viewParent, canvas.style, {});
|
|
|
|
me._layout = me._view;
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
return me._view;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-01-11 10:53:52 +00:00
|
|
|
|
2023-01-20 16:11:49 +00:00
|
|
|
var XMLComboBox =
|
2023-01-11 10:53:52 +00:00
|
|
|
{
|
|
|
|
init: func(objectProps)
|
|
|
|
{
|
|
|
|
},
|
|
|
|
|
|
|
|
show: func(viewParent)
|
|
|
|
{
|
|
|
|
# do we support a label or is that a seperate widget?
|
|
|
|
|
2023-01-20 16:11:49 +00:00
|
|
|
me._view = cwidgets.ComboBox.new(viewParent, canvas.style, {});
|
2023-01-11 10:53:52 +00:00
|
|
|
me._layout = me._view;
|
|
|
|
me._applyLayoutConfig();
|
|
|
|
me.update();
|
|
|
|
return me._view;
|
|
|
|
},
|
|
|
|
|
|
|
|
update: func()
|
|
|
|
{
|
|
|
|
if (me._view and !me._view.hasActiveFocus()) {
|
|
|
|
me._view.setCurrentByValue(me.value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-06-30 09:30:50 +00:00
|
|
|
# this is the callback function invoked by C++ to build Nasal peers
|
|
|
|
# for the C++ objects defined by XML (PUICompatObject). It's primarly
|
|
|
|
# a factory method: once the peer object is created, all other behaviour
|
|
|
|
# is passed to it.
|
|
|
|
var _createCompatObject = func(type)
|
|
|
|
{
|
|
|
|
# default to a label
|
|
|
|
var widget = XMLLabel;
|
|
|
|
|
|
|
|
# FIXME use a hash for these :)
|
|
|
|
if (type == "button") {
|
|
|
|
widget = XMLButton;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type == "checkbox") {
|
|
|
|
widget = XMLCheckbox;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type == "slider") {
|
|
|
|
widget = XMLSlider;
|
|
|
|
}
|
|
|
|
|
2022-09-26 07:58:24 +00:00
|
|
|
if (type == "dial") {
|
|
|
|
widget = XMLDial;
|
|
|
|
}
|
|
|
|
|
2022-06-30 09:30:50 +00:00
|
|
|
if (type == "group") {
|
|
|
|
widget = XMLGroup;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type == "input") {
|
|
|
|
widget = XMLTextEdit;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type == "empty") {
|
|
|
|
widget = XMLEmptyWidget;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type == "hrule") {
|
|
|
|
widget = XMLHRule;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type == "vrule") {
|
|
|
|
widget = XMLVRule;
|
|
|
|
}
|
|
|
|
|
2023-01-11 10:53:52 +00:00
|
|
|
if (type == "combo") {
|
2023-01-20 16:11:49 +00:00
|
|
|
widget = XMLComboBox;
|
2023-01-11 10:53:52 +00:00
|
|
|
}
|
|
|
|
|
2022-06-30 09:30:50 +00:00
|
|
|
return gui.xml.Object.new({
|
|
|
|
parents: [widget, XMLObjectBase]
|
|
|
|
}, type);
|
|
|
|
};
|
|
|
|
|
|
|
|
logprint(LOG_INFO, "Loaded gui.XMLDialog");
|
|
|
|
|