Howto talk:Processing legacy PUI dialogs using Canvas: Difference between revisions
Jump to navigation
Jump to search
(→pastebin ...: new section) |
|||
Line 66: | Line 66: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
== pastebin ... == | |||
<pre># beginning of PUI namespace | |||
var PUI = { | |||
# static | |||
unsupported: func(tag, what="Widget") { | |||
print("PUI/XML ",what," not yet supported:", tag); | |||
}, | |||
## | |||
# call with exception handling (stub) | |||
# | |||
safeCall: func(codeObj, namespace=nil) { | |||
var retval = call(codeObj, [], nil, namespace, var err=[]); | |||
if (size(err)) { | |||
debug.dump(err); | |||
} | |||
return retval; | |||
}, | |||
## | |||
# generator for creating callbacks used for all Nasal compilation | |||
# TODO: this should take a closure for the <open> and <load> blocks ... | |||
makeTagCompiler: func(codeTag, description=nil) { | |||
if (description==nil) description = codeTag ~" block"; | |||
return func(node, namespace=nil) { | |||
var codeSrc = node.getNode(codeTag).getValue(); | |||
return func PUI.safeCall(compile(codeSrc), namespace); | |||
}; | |||
}, # makeTagCompiler | |||
setupCommandBinding: func(window, node) { | |||
var command = node.getNode("command").getValue(); | |||
if (command=="nasal") return PUI.setupScriptBinding(node); | |||
# TODO: fgcommands relating to PUI must be dynamically patched/overridden (e.g. dialog-apply/dialog-close) | |||
if (contains(PUI.fgcommandPatches, command)) { | |||
print("Most PUI related fgcommand patching not yet implemented"); | |||
# returns a patched fgcommand (Nasal callback) for the window/node | |||
return PUI.fgcommandPatches[command](window, node); | |||
} | |||
# else print("Returning non-PUI fgcommand"); | |||
# standard fgcommand, wrapped in a Nasal func | |||
# | |||
return func() { | |||
fgcommand(command, node); | |||
}; | |||
}, # setupCommandBinding | |||
setupScriptBinding: func(node) { | |||
# TODO: needs to be bound to the dialog's open block | |||
return PUI.nasalBindingCompiler(node); | |||
}, # setupScriptBinding | |||
# returns a vector of callback bindings (fgcommands and nasal scripts) | |||
setupBindings: func(window, node) { | |||
var callbacks=[]; | |||
var b=node.getChildren("binding"); | |||
foreach(var binding; b) { | |||
append(callbacks, PUI.setupCommandBinding(window, binding)); | |||
} | |||
return callbacks; | |||
}, # setupBindings | |||
makeBindingsCallback: func(bindingsVec) { | |||
return func() { | |||
foreach(var b; bindingsVec) { | |||
# bindings are invoked via call to ensure that | |||
# runtime errors don't block other bindings | |||
# i.e. if one binding fails, the other ones will still be executed | |||
# PUI.safeCall(b); | |||
b(); | |||
} # foreach binding | |||
} # return anonymous func | |||
}, # makeBindingsCallback | |||
### | |||
# TODO remove default PUI close button based on heuristic ? | |||
### | |||
# TODO: Some PUI fgcommands may need to be patched/replaced dynamically | |||
# and mapped to their Canvas equivalents - e.g. to call a window.del() destructor | |||
fgcommandPatches: { | |||
'dialog-close': func(window, node) { | |||
return func window.del(); | |||
}, | |||
'dialog-apply': func(window, node) { | |||
print("dialog-apply is not yet implemented ..."); | |||
return func; | |||
}, | |||
'dialog-update': func(window, node) { | |||
print("patched dialog-update is not yet implemented"); | |||
return func; | |||
}, | |||
'dialog-show': func(window, node) { | |||
return func; | |||
}, | |||
}, # fgcommandPatches | |||
### | |||
# TODO: unify node checking (vector], callback to be invoked if found | |||
applyPUIAttributes: func(widget, layout, node) { | |||
var stretch = node.getNode('stretch'); | |||
if (stretch != nil) { | |||
print("stretch not yet implemented"); | |||
} | |||
var halign = node.getNode('halign'); | |||
if (halign != nil) { | |||
print("halign not yet supported"); | |||
} | |||
var width = node.getNode('pref-width'); | |||
var height = node.getNode('pref-height'); | |||
height = height != nil ? height.getValue() : 32; | |||
if (width!=nil and height!=nil) | |||
widget.setFixedSize(width.getValue(), height); | |||
# TODO: visible & enable, live | |||
var visible = node.getNode("visible"); | |||
if (visible != nil) print("visible tag not yet supported"); | |||
var live = node.getNode("live"); | |||
if (live != nil) { | |||
# TODO: listeners should be stored and freed properly | |||
print("live properties not yet supported:", node.getNode('property').getValue()); | |||
} | |||
var format = node.getNode('format'); | |||
if (format !=nil) { | |||
var property=node.getNode('property').getValue(); | |||
var legend=sprintf(format.getValue(), getprop(property)); | |||
widget.setText(legend); | |||
} | |||
}, | |||
# checks if a tag is a known widget (i.e. listed in the WidgetFactory hash) | |||
isWidget: func(tag) { | |||
return contains(PUI.WidgetFactory,tag); | |||
}, | |||
getOrCreateTableCell: func(layout, row, col) { | |||
}, | |||
# pre-allocate a table data structure using ROW/COLS as per README.layout | |||
# and Canvas vbox/hbox layouts | |||
createTable: func(layout, node) { | |||
var nodeHash = node.getValues(); | |||
var topLevelLayout = layout; | |||
foreach(var child; node.getChildren()) { | |||
var tagType = child.getName(); | |||
if (PUI.isWidget(tagType)) { | |||
print("widget found in table:", tagType); | |||
} # known widget | |||
} # foreach | |||
return []; | |||
}, # createTable() | |||
## | |||
# deals with different layouts (group/frame) | |||
# and creates a table layout using Canvas vbox/hbox layouts | |||
LayoutFactory: { | |||
'layout': func(window, root, layout, node) { | |||
debug.dump(node); | |||
var mode = node.getNode('layout').getValue(); #getValues().layout; | |||
var myLayout = PUI.LayoutFactory[mode](window, root, layout, node); | |||
#layout.addItem(myLayout); | |||
}, | |||
'hbox': func(window, root, layout, node) { | |||
var myLayout = canvas.HBoxLayout.new(); | |||
PUI.parseWidgets(window, root, myLayout, node); | |||
layout.addItem(myLayout); | |||
}, # hbox | |||
'vbox': func(window, root, layout, node) { | |||
var myLayout = canvas.VBoxLayout.new(); | |||
PUI.parseWidgets(window, root, myLayout, node); | |||
layout.addItem(myLayout); | |||
}, #vbox | |||
'table': func(window, root, layout, node) { | |||
var layouts = PUI.createTable(layout, node); | |||
}, #table | |||
}, # LayoutFactory | |||
## | |||
# maps PUI tags ($FG_ROOT/Docs/README.gui) to callbacks/widgets implementing the Canvas equivalent | |||
WidgetFactory: { | |||
'frame': func(window, root, layout, node) { | |||
# groups and frames are equivalent | |||
PUI.WidgetFactory.group(window, root, layout, node); | |||
}, # frame | |||
'group': func(window, root, layout, node) { | |||
var myLayout = PUI.LayoutFactory.layout(window, root, layout, node); | |||
}, # group | |||
'dummy': func(window, root, layout, node) { | |||
PUI.WidgetFactory.button(window, root, layout, node); | |||
}, # dummy widget, adds a button | |||
'text': func(window, root, layout, node) { | |||
var legend = node.getNode('label').getValue(); | |||
var label = canvas.gui.widgets.Label.new(root, canvas.style, {wordWrap: 0}); | |||
label.setText(legend); | |||
PUI.applyPUIAttributes(label, layout, node); | |||
layout.addItem(label); | |||
}, # text | |||
'button': func(window, root, layout, node) { | |||
var legend = node.getNode('legend').getValue(); | |||
var button = canvas.gui.widgets.Button.new(root, canvas.style, {}) | |||
.setText(legend); | |||
layout.addItem(button); | |||
PUI.applyPUIAttributes(button, layout, node); | |||
# set up bindings: (should be moved to applyPUI*) | |||
var bindings = PUI.setupBindings(window, node); | |||
button.listen("clicked", PUI.makeBindingsCallback(bindings)); | |||
}, # button | |||
'input': func(window, root, layout, node) { | |||
var input = canvas.gui.widgets.LineEdit.new(root, canvas.style, {}); | |||
PUI.applyPUIAttributes(input, layout, node); | |||
layout.addItem(input); | |||
}, # input | |||
'checkbox': func(window, root, layout, node) { | |||
var hbox=canvas.HBoxLayout.new(); | |||
var checkbox = canvas.gui.widgets.CheckBox.new(root, canvas.style, {}); | |||
hbox.addItem(checkbox); | |||
# add a label | |||
PUI.WidgetFactory.text(window, root, hbox, node); | |||
layout.addItem(hbox); | |||
}, # checkbox | |||
'radio': func(window, root, layout, node) { | |||
}, # radio | |||
'combo': func(window, root, layout, node) { | |||
PUI.unsupported("combo"); | |||
}, # combo | |||
# this one is special in that it is actually shared between different list implementations: | |||
'list': func(window, root, layout, node) { | |||
var vbox = canvas.VBoxLayout.new(); | |||
layout.addItem(vbox); | |||
var scroll = canvas.gui.widgets.ScrollArea.new(root, canvas.style, {size: [96, 128]}).move(20, 100); | |||
vbox.addItem(scroll, 1); | |||
var scrollContent = | |||
scroll.getContent() | |||
.set("font", "LiberationFonts/LiberationSans-Bold.ttf") | |||
.set("character-size", 16) | |||
.set("alignment", "left"); | |||
var list = canvas.VBoxLayout.new(); | |||
scroll.setLayout(list); | |||
# returns the created list to the caller, which means we can reuse the code for other types of lists (airports, waypoints, property browser) | |||
PUI.applyPUIAttributes(scroll, layout, node); | |||
return {layout:list, root:scrollContent}; | |||
}, # list | |||
'airport-list': func(window, root, layout, node) { | |||
var list = PUI.WidgetFactory.list(window, root, layout, node); | |||
}, # airport-list | |||
'property-list': func(window, root, layout, node) { | |||
var myList = PUI.WidgetFactory.list(window, root, layout, node); | |||
var list = myList.layout; | |||
var scrollContent = myList.root; | |||
# we will be using this callback to populate/update the property browser | |||
var updateList = func(node) { | |||
var upButton = canvas.gui.widgets.Button.new(scrollContent, canvas.style, {}) | |||
.setText('..' ) | |||
.setFixedSize(200, 25); | |||
upButton._view._root.addEventListener("dblclick", func() { | |||
list.clear(); | |||
var parent = node.getParent(); | |||
if (parent == nil) parent = node; | |||
updateList( parent ); | |||
}); | |||
# add the navigation button (..) | |||
list.addItem( upButton ); | |||
# next, add all child nodes | |||
# TODO: this should probbably be sorted ... | |||
foreach(var n; node.getChildren() ) { | |||
# TODO: should probably use label instead here ? | |||
var PropertyButton = canvas.gui.widgets.Button.new(scrollContent, canvas.style, {}) | |||
.setText(n.getPath() ) | |||
.setFixedSize(200, 25); | |||
# save the node in a closure for later use | |||
(func() { | |||
var n = n; | |||
var bindings = PUI.setupBindings(window, node); | |||
# FIXME: this needs to use the patched fgcommand bindings ... for dialog-apply etc | |||
PropertyButton.listen("clicked", PUI.makeBindingsCallback(bindings)); | |||
PropertyButton._view._root.addEventListener("click", func() { | |||
print("click event:",n.getPath() ); | |||
}); | |||
# this will clear the list and call the function with the selected new node | |||
PropertyButton._view._root.addEventListener("dblclick", func() { | |||
print("double click event",n.getPath() ); | |||
# clear list | |||
list.clear(); | |||
updateList( n ); | |||
}); | |||
}()); # call the anonymous function | |||
list.addItem(PropertyButton); | |||
} # foreach child | |||
} # updateList() | |||
# invoke the updateList() function with thhe root node | |||
updateList( props.getNode("") ); | |||
}, # property-list | |||
'waypointlist': func(window, root, layout, node) { | |||
var list = PUI.WidgetFactory.list(window, root, layout, node); | |||
}, # waypointlist | |||
'select': func(window, root, layout, node) { | |||
}, # select | |||
'slider':func(window, root, layout, node) { | |||
}, # slider | |||
'dial': func(window, root, layout, node) { | |||
}, # dial | |||
'textbox':func(window, root, layout, node) { | |||
# TODO: should be list wrapping a label ... | |||
var myList = PUI.WidgetFactory.list(window, root, layout, node); | |||
var list = myList.layout; | |||
var scrollContent = myList.root; | |||
var input = canvas.gui.widgets.LineEdit.new(scrollContent, canvas.style, {}); | |||
#PUI.applyPUIAttributes(input, layout, node); | |||
list.addItem(input); | |||
}, # textbox | |||
'hrule': func(window, root, layout, node) { | |||
PUI.unsupported("hrule"); | |||
}, #hrule | |||
'vrule': func(window, root, layout, node) { | |||
PUI.unsupported("vrule"); | |||
}, # vrule | |||
'canvas': func(window, root, layout, node) { | |||
}, # canvas | |||
'map': func(window, root, layout, node) { | |||
# must be mapped to a Canvas that instantiates a MapStructure map | |||
}, # map | |||
'loglist': func(window, root, layout, node) { | |||
#die("loglist widget cannot be reimplemented due to missing C++ hooks: logstream"); | |||
}, # loglist | |||
'empty': func(window, root, layout, node) { | |||
layout.addStretch(1); | |||
}, # empty | |||
}, # WidgetFactory | |||
parseWidgets: func(window, root, layout, node) { | |||
foreach(var tag; node.getChildren()) { | |||
var t=tag.getName() ; | |||
if(contains(PUI.WidgetFactory, t)) { | |||
PUI.WidgetFactory[t] (window, root, layout, tag); | |||
# TODO: applyPUIAttributes | |||
# TODO: addItem/layout here | |||
# TODO: process bindings | |||
} | |||
# TODO: check if Nasal tag, layout tag, styling tag | |||
elsif (!contains(PUI.LayoutFactory, t) ) | |||
PUI.unsupported(t, "Tag"); | |||
} # foreach tag | |||
}, | |||
## | |||
# this is basically the entrypoint of the whole parser, it's what will be called by the fgcommand | |||
showDialog: func(node) { | |||
var filename = node.getNode('dialog-name').getValue() or die("Missing dialog file name"); | |||
# append .xml suffix: | |||
filename ~= ".xml"; | |||
# build path relative to $FG_ROOT | |||
# TODO: this may need to be fixed for aircraft-level dialogs | |||
var path = getprop("/sim/fg-root")~"/gui/dialogs/"~filename; | |||
print("Custom PUI/Canvas parser for dialog:", filename); | |||
# load the dialog from the base package | |||
# FIXME: should be using this to support aircraft dialogs: http://wiki.flightgear.org/Nasal_library#resolvepath.28.29 | |||
var dlgNode = io.read_properties(path); | |||
# turn the property node into a hash | |||
dlgHash = dlgNode.getValues(); | |||
# create a Canvas window | |||
var window = canvas.Window.new([640,480],"dialog").set('title', dlgHash.name); | |||
# adding a canvas to the new window and setting up background colors/transparency | |||
var myCanvas = window.createCanvas().set("background", canvas.style.getColor("bg_color")); | |||
# window.setCanvas(myCanvas); | |||
# creating the top-level/root group which will contain all other elements/group | |||
var root = myCanvas.createGroup(); | |||
# determine/set top-level layout | |||
var layoutMode = dlgHash.layout; | |||
var topLevelLayout = (layoutMode=='vbox')?canvas.VBoxLayout.new() : canvas.HBoxLayout.new(); | |||
myCanvas.setLayout(topLevelLayout); | |||
var namespace = {cmdarg: func dlgNode}; | |||
if(dlgNode.getNode("nasal/open")!=nil) { | |||
print("Executing nasal open block"); | |||
var cb=PUI.nasalOpenCompiler(dlgNode, namespace); # compile | |||
cb(); # and call | |||
} | |||
window.del = func() { | |||
var namespace = closure(cb); | |||
if(dlgNode.getNode("nasal/close")!=nil) { | |||
var close=PUI.nasalCloseCompiler(dlgNode, namespace); # invoke Nasal/close block | |||
#close=bind(close, namespace); | |||
close(); | |||
} | |||
# call the original dtor | |||
call(canvas.Window.del, [], window, var err=[]); | |||
} | |||
# http://wiki.flightgear.org/Built-in_Profiler#Nasal | |||
fgcommand("profiler-start", props.Node.new({filename:"pui2canvas-"~filename})); | |||
PUI.parseWidgets(window:window, root:root, layout:topLevelLayout, node:dlgNode); | |||
fgcommand("profiler-stop"); | |||
}, # showDialog | |||
}; # end of PUI namespace | |||
### | |||
# create a handful of tag compiler instances for different purposes | |||
# TODO: these should be moved to function scope for the closure handling ... | |||
# helper to compile bindings | |||
PUI.nasalBindingCompiler = PUI.makeTagCompiler(codeTag: "script"); | |||
# helper to compile & call embedded blocks | |||
PUI.nasalOpenCompiler = PUI.makeTagCompiler(codeTag:"nasal/open"); | |||
PUI.nasalCloseCompiler = PUI.makeTagCompiler(codeTag:"nasal/close"); | |||
PUI.canvasLoadCompiler = PUI.makeTagCompiler(codeTag:"canvas/load"); | |||
## | |||
# finally, register a new fgcommand for turning PUI dialogs into Canvas widgets | |||
# NOTE: Once/if the parser is sufficiently complete and works for most dialogs | |||
# this could actually override the built-in dialog-show fgcommand | |||
var fgcommand_name = getprop('/sim/gui/override-pui-fgcommands',0) == 1 ? 'dialog-show' : 'canvas-dialog-show'; | |||
addcommand(fgcommand_name, PUI.showDialog); | |||
print("pui2canvas module loaded, fgcommand registered using name:", fgcommand_name); | |||
</pre> |
Revision as of 09:06, 18 June 2016
Prioritizing tag/widget support
Note The following Python script is WIP, intended to determine the most important PUI/XML related tags (widgets and layouting, and expects $FG_ROOT to be set up correctly |
#!/bin/python
import os
import fnmatch
import string
import getopt, sys
from xml.dom.minidom import parseString
def usage():
print "Usage info ..."
def main():
try:
opts, args = getopt.getopt(sys.argv[1:], "ho:v", ["help", "output="])
except getopt.GetoptError as err:
# print help information and exit:
print(err) # will print something like "option -a not recognized"
usage()
sys.exit(2)
output = None
verbose = False
for o, a in opts:
if o == "-v":
verbose = True
elif o in ("-h", "--help"):
usage()
sys.exit()
elif o in ("-o", "--output"):
output = a
else:
assert False, "unhandled option"
# ...
fgroot = os.environ['FG_ROOT']
dialogs = os.path.join(fgroot, "gui/dialogs")
os.chdir(dialogs)
xml_dialogs = []
for file in os.listdir('.'):
if fnmatch.fnmatch(file, '*.xml'):
xml_dialogs.append(file)
print "xml dialogs found:", len(xml_dialogs)
file = open('autopilot.xml','r')
data = file.read()
file.close()
dom = parseString(data)
layouting_directives = ['group','frame']
# https://sourceforge.net/p/flightgear/flightgear/ci/next/tree/src/GUI/FGPUIDialog.cxx#l853
pui_tags = ['hrule','vrule','list','airport-list','property-list','input','text','checkbox','radio','button','map','canvas','combo','slider','dial','textbox','select','waypointlist','loglist','label']
for tag in pui_tags:
print tag + " occurrences: ", len(dom.getElementsByTagName(tag))
if __name__ == "__main__":
main()
pastebin ...
# beginning of PUI namespace var PUI = { # static unsupported: func(tag, what="Widget") { print("PUI/XML ",what," not yet supported:", tag); }, ## # call with exception handling (stub) # safeCall: func(codeObj, namespace=nil) { var retval = call(codeObj, [], nil, namespace, var err=[]); if (size(err)) { debug.dump(err); } return retval; }, ## # generator for creating callbacks used for all Nasal compilation # TODO: this should take a closure for the <open> and <load> blocks ... makeTagCompiler: func(codeTag, description=nil) { if (description==nil) description = codeTag ~" block"; return func(node, namespace=nil) { var codeSrc = node.getNode(codeTag).getValue(); return func PUI.safeCall(compile(codeSrc), namespace); }; }, # makeTagCompiler setupCommandBinding: func(window, node) { var command = node.getNode("command").getValue(); if (command=="nasal") return PUI.setupScriptBinding(node); # TODO: fgcommands relating to PUI must be dynamically patched/overridden (e.g. dialog-apply/dialog-close) if (contains(PUI.fgcommandPatches, command)) { print("Most PUI related fgcommand patching not yet implemented"); # returns a patched fgcommand (Nasal callback) for the window/node return PUI.fgcommandPatches[command](window, node); } # else print("Returning non-PUI fgcommand"); # standard fgcommand, wrapped in a Nasal func # return func() { fgcommand(command, node); }; }, # setupCommandBinding setupScriptBinding: func(node) { # TODO: needs to be bound to the dialog's open block return PUI.nasalBindingCompiler(node); }, # setupScriptBinding # returns a vector of callback bindings (fgcommands and nasal scripts) setupBindings: func(window, node) { var callbacks=[]; var b=node.getChildren("binding"); foreach(var binding; b) { append(callbacks, PUI.setupCommandBinding(window, binding)); } return callbacks; }, # setupBindings makeBindingsCallback: func(bindingsVec) { return func() { foreach(var b; bindingsVec) { # bindings are invoked via call to ensure that # runtime errors don't block other bindings # i.e. if one binding fails, the other ones will still be executed # PUI.safeCall(b); b(); } # foreach binding } # return anonymous func }, # makeBindingsCallback ### # TODO remove default PUI close button based on heuristic ? ### # TODO: Some PUI fgcommands may need to be patched/replaced dynamically # and mapped to their Canvas equivalents - e.g. to call a window.del() destructor fgcommandPatches: { 'dialog-close': func(window, node) { return func window.del(); }, 'dialog-apply': func(window, node) { print("dialog-apply is not yet implemented ..."); return func; }, 'dialog-update': func(window, node) { print("patched dialog-update is not yet implemented"); return func; }, 'dialog-show': func(window, node) { return func; }, }, # fgcommandPatches ### # TODO: unify node checking (vector], callback to be invoked if found applyPUIAttributes: func(widget, layout, node) { var stretch = node.getNode('stretch'); if (stretch != nil) { print("stretch not yet implemented"); } var halign = node.getNode('halign'); if (halign != nil) { print("halign not yet supported"); } var width = node.getNode('pref-width'); var height = node.getNode('pref-height'); height = height != nil ? height.getValue() : 32; if (width!=nil and height!=nil) widget.setFixedSize(width.getValue(), height); # TODO: visible & enable, live var visible = node.getNode("visible"); if (visible != nil) print("visible tag not yet supported"); var live = node.getNode("live"); if (live != nil) { # TODO: listeners should be stored and freed properly print("live properties not yet supported:", node.getNode('property').getValue()); } var format = node.getNode('format'); if (format !=nil) { var property=node.getNode('property').getValue(); var legend=sprintf(format.getValue(), getprop(property)); widget.setText(legend); } }, # checks if a tag is a known widget (i.e. listed in the WidgetFactory hash) isWidget: func(tag) { return contains(PUI.WidgetFactory,tag); }, getOrCreateTableCell: func(layout, row, col) { }, # pre-allocate a table data structure using ROW/COLS as per README.layout # and Canvas vbox/hbox layouts createTable: func(layout, node) { var nodeHash = node.getValues(); var topLevelLayout = layout; foreach(var child; node.getChildren()) { var tagType = child.getName(); if (PUI.isWidget(tagType)) { print("widget found in table:", tagType); } # known widget } # foreach return []; }, # createTable() ## # deals with different layouts (group/frame) # and creates a table layout using Canvas vbox/hbox layouts LayoutFactory: { 'layout': func(window, root, layout, node) { debug.dump(node); var mode = node.getNode('layout').getValue(); #getValues().layout; var myLayout = PUI.LayoutFactory[mode](window, root, layout, node); #layout.addItem(myLayout); }, 'hbox': func(window, root, layout, node) { var myLayout = canvas.HBoxLayout.new(); PUI.parseWidgets(window, root, myLayout, node); layout.addItem(myLayout); }, # hbox 'vbox': func(window, root, layout, node) { var myLayout = canvas.VBoxLayout.new(); PUI.parseWidgets(window, root, myLayout, node); layout.addItem(myLayout); }, #vbox 'table': func(window, root, layout, node) { var layouts = PUI.createTable(layout, node); }, #table }, # LayoutFactory ## # maps PUI tags ($FG_ROOT/Docs/README.gui) to callbacks/widgets implementing the Canvas equivalent WidgetFactory: { 'frame': func(window, root, layout, node) { # groups and frames are equivalent PUI.WidgetFactory.group(window, root, layout, node); }, # frame 'group': func(window, root, layout, node) { var myLayout = PUI.LayoutFactory.layout(window, root, layout, node); }, # group 'dummy': func(window, root, layout, node) { PUI.WidgetFactory.button(window, root, layout, node); }, # dummy widget, adds a button 'text': func(window, root, layout, node) { var legend = node.getNode('label').getValue(); var label = canvas.gui.widgets.Label.new(root, canvas.style, {wordWrap: 0}); label.setText(legend); PUI.applyPUIAttributes(label, layout, node); layout.addItem(label); }, # text 'button': func(window, root, layout, node) { var legend = node.getNode('legend').getValue(); var button = canvas.gui.widgets.Button.new(root, canvas.style, {}) .setText(legend); layout.addItem(button); PUI.applyPUIAttributes(button, layout, node); # set up bindings: (should be moved to applyPUI*) var bindings = PUI.setupBindings(window, node); button.listen("clicked", PUI.makeBindingsCallback(bindings)); }, # button 'input': func(window, root, layout, node) { var input = canvas.gui.widgets.LineEdit.new(root, canvas.style, {}); PUI.applyPUIAttributes(input, layout, node); layout.addItem(input); }, # input 'checkbox': func(window, root, layout, node) { var hbox=canvas.HBoxLayout.new(); var checkbox = canvas.gui.widgets.CheckBox.new(root, canvas.style, {}); hbox.addItem(checkbox); # add a label PUI.WidgetFactory.text(window, root, hbox, node); layout.addItem(hbox); }, # checkbox 'radio': func(window, root, layout, node) { }, # radio 'combo': func(window, root, layout, node) { PUI.unsupported("combo"); }, # combo # this one is special in that it is actually shared between different list implementations: 'list': func(window, root, layout, node) { var vbox = canvas.VBoxLayout.new(); layout.addItem(vbox); var scroll = canvas.gui.widgets.ScrollArea.new(root, canvas.style, {size: [96, 128]}).move(20, 100); vbox.addItem(scroll, 1); var scrollContent = scroll.getContent() .set("font", "LiberationFonts/LiberationSans-Bold.ttf") .set("character-size", 16) .set("alignment", "left"); var list = canvas.VBoxLayout.new(); scroll.setLayout(list); # returns the created list to the caller, which means we can reuse the code for other types of lists (airports, waypoints, property browser) PUI.applyPUIAttributes(scroll, layout, node); return {layout:list, root:scrollContent}; }, # list 'airport-list': func(window, root, layout, node) { var list = PUI.WidgetFactory.list(window, root, layout, node); }, # airport-list 'property-list': func(window, root, layout, node) { var myList = PUI.WidgetFactory.list(window, root, layout, node); var list = myList.layout; var scrollContent = myList.root; # we will be using this callback to populate/update the property browser var updateList = func(node) { var upButton = canvas.gui.widgets.Button.new(scrollContent, canvas.style, {}) .setText('..' ) .setFixedSize(200, 25); upButton._view._root.addEventListener("dblclick", func() { list.clear(); var parent = node.getParent(); if (parent == nil) parent = node; updateList( parent ); }); # add the navigation button (..) list.addItem( upButton ); # next, add all child nodes # TODO: this should probbably be sorted ... foreach(var n; node.getChildren() ) { # TODO: should probably use label instead here ? var PropertyButton = canvas.gui.widgets.Button.new(scrollContent, canvas.style, {}) .setText(n.getPath() ) .setFixedSize(200, 25); # save the node in a closure for later use (func() { var n = n; var bindings = PUI.setupBindings(window, node); # FIXME: this needs to use the patched fgcommand bindings ... for dialog-apply etc PropertyButton.listen("clicked", PUI.makeBindingsCallback(bindings)); PropertyButton._view._root.addEventListener("click", func() { print("click event:",n.getPath() ); }); # this will clear the list and call the function with the selected new node PropertyButton._view._root.addEventListener("dblclick", func() { print("double click event",n.getPath() ); # clear list list.clear(); updateList( n ); }); }()); # call the anonymous function list.addItem(PropertyButton); } # foreach child } # updateList() # invoke the updateList() function with thhe root node updateList( props.getNode("") ); }, # property-list 'waypointlist': func(window, root, layout, node) { var list = PUI.WidgetFactory.list(window, root, layout, node); }, # waypointlist 'select': func(window, root, layout, node) { }, # select 'slider':func(window, root, layout, node) { }, # slider 'dial': func(window, root, layout, node) { }, # dial 'textbox':func(window, root, layout, node) { # TODO: should be list wrapping a label ... var myList = PUI.WidgetFactory.list(window, root, layout, node); var list = myList.layout; var scrollContent = myList.root; var input = canvas.gui.widgets.LineEdit.new(scrollContent, canvas.style, {}); #PUI.applyPUIAttributes(input, layout, node); list.addItem(input); }, # textbox 'hrule': func(window, root, layout, node) { PUI.unsupported("hrule"); }, #hrule 'vrule': func(window, root, layout, node) { PUI.unsupported("vrule"); }, # vrule 'canvas': func(window, root, layout, node) { }, # canvas 'map': func(window, root, layout, node) { # must be mapped to a Canvas that instantiates a MapStructure map }, # map 'loglist': func(window, root, layout, node) { #die("loglist widget cannot be reimplemented due to missing C++ hooks: logstream"); }, # loglist 'empty': func(window, root, layout, node) { layout.addStretch(1); }, # empty }, # WidgetFactory parseWidgets: func(window, root, layout, node) { foreach(var tag; node.getChildren()) { var t=tag.getName() ; if(contains(PUI.WidgetFactory, t)) { PUI.WidgetFactory[t] (window, root, layout, tag); # TODO: applyPUIAttributes # TODO: addItem/layout here # TODO: process bindings } # TODO: check if Nasal tag, layout tag, styling tag elsif (!contains(PUI.LayoutFactory, t) ) PUI.unsupported(t, "Tag"); } # foreach tag }, ## # this is basically the entrypoint of the whole parser, it's what will be called by the fgcommand showDialog: func(node) { var filename = node.getNode('dialog-name').getValue() or die("Missing dialog file name"); # append .xml suffix: filename ~= ".xml"; # build path relative to $FG_ROOT # TODO: this may need to be fixed for aircraft-level dialogs var path = getprop("/sim/fg-root")~"/gui/dialogs/"~filename; print("Custom PUI/Canvas parser for dialog:", filename); # load the dialog from the base package # FIXME: should be using this to support aircraft dialogs: http://wiki.flightgear.org/Nasal_library#resolvepath.28.29 var dlgNode = io.read_properties(path); # turn the property node into a hash dlgHash = dlgNode.getValues(); # create a Canvas window var window = canvas.Window.new([640,480],"dialog").set('title', dlgHash.name); # adding a canvas to the new window and setting up background colors/transparency var myCanvas = window.createCanvas().set("background", canvas.style.getColor("bg_color")); # window.setCanvas(myCanvas); # creating the top-level/root group which will contain all other elements/group var root = myCanvas.createGroup(); # determine/set top-level layout var layoutMode = dlgHash.layout; var topLevelLayout = (layoutMode=='vbox')?canvas.VBoxLayout.new() : canvas.HBoxLayout.new(); myCanvas.setLayout(topLevelLayout); var namespace = {cmdarg: func dlgNode}; if(dlgNode.getNode("nasal/open")!=nil) { print("Executing nasal open block"); var cb=PUI.nasalOpenCompiler(dlgNode, namespace); # compile cb(); # and call } window.del = func() { var namespace = closure(cb); if(dlgNode.getNode("nasal/close")!=nil) { var close=PUI.nasalCloseCompiler(dlgNode, namespace); # invoke Nasal/close block #close=bind(close, namespace); close(); } # call the original dtor call(canvas.Window.del, [], window, var err=[]); } # http://wiki.flightgear.org/Built-in_Profiler#Nasal fgcommand("profiler-start", props.Node.new({filename:"pui2canvas-"~filename})); PUI.parseWidgets(window:window, root:root, layout:topLevelLayout, node:dlgNode); fgcommand("profiler-stop"); }, # showDialog }; # end of PUI namespace ### # create a handful of tag compiler instances for different purposes # TODO: these should be moved to function scope for the closure handling ... # helper to compile bindings PUI.nasalBindingCompiler = PUI.makeTagCompiler(codeTag: "script"); # helper to compile & call embedded blocks PUI.nasalOpenCompiler = PUI.makeTagCompiler(codeTag:"nasal/open"); PUI.nasalCloseCompiler = PUI.makeTagCompiler(codeTag:"nasal/close"); PUI.canvasLoadCompiler = PUI.makeTagCompiler(codeTag:"canvas/load"); ## # finally, register a new fgcommand for turning PUI dialogs into Canvas widgets # NOTE: Once/if the parser is sufficiently complete and works for most dialogs # this could actually override the built-in dialog-show fgcommand var fgcommand_name = getprop('/sim/gui/override-pui-fgcommands',0) == 1 ? 'dialog-show' : 'canvas-dialog-show'; addcommand(fgcommand_name, PUI.showDialog); print("pui2canvas module loaded, fgcommand registered using name:", fgcommand_name);