Howto talk:Processing legacy PUI dialogs using Canvas

From FlightGear wiki
Revision as of 19:17, 19 June 2016 by Hooray (talk | contribs) (→‎Table parser: new section)
Jump to navigation Jump to search

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