Howto talk:Processing legacy PUI dialogs using Canvas: Difference between revisions
Jump to navigation
Jump to search
(→pastebin ...: new section) |
(→Table parser: new section) |
||
Line 531: | Line 531: | ||
</pre> | </pre> | ||
== Table parser == | |||
<syntaxhighlight lang="nasal"> | |||
var filename = "/timeofday.xml"; | |||
var path = getprop('/sim/fg-root') ~ filename; | |||
var parseTable = func(path) { | |||
var dialog = io.read_properties(path); | |||
} | |||
var createTable = func() { | |||
} | |||
parseTable(path: path); | |||
</syntaxhighlight> |
Revision as of 19:17, 19 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);
Table parser
var filename = "/timeofday.xml";
var path = getprop('/sim/fg-root') ~ filename;
var parseTable = func(path) {
var dialog = io.read_properties(path);
}
var createTable = func() {
}
parseTable(path: path);