20,741
edits
(→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> |