/++ A small extension module to [arsd.minigui] that adds functions for creating widgets and windows from short XML descriptions. If you choose to use this, it will require [arsd.dom] to be compiled into your project too. --- import arsd.minigui_xml; Window window = createWindowFromXml(` <MainWindow> <Button label="Hi!" /> </MainWindow> `); --- To add custom widgets to the minigui_xml factory, you need to register them with FIXME. You can attach some events right in the XML using attributes. The attribute names are `onEVENTNAME` or `ondirectEVENTNAME` and the values are one of the following three value types: $(LIST * If it starts with `&`, it is a delegate you need to register using the FIXME function. * If it starts with `(`, it is a string passed to the [arsd.dom.querySelector] function to get an element reference * Otherwise, it tries to call a script function (if scripting is available). ) Keep in mind For example, to make a page widget that changes based on a drop down selection, you may: ```xml <DropDownSelection onchange="$(+PageWidget).setCurrentTab"> <option>Foo</option> <option>Bar</option> </DropDownSelection> <PageWidget name="mypage"> <!-- contents elided --> </PageWidget> ``` That will create a select widget that when it changes, it will look for the next PageWidget sibling (that's the meaning of `+PageWidget`, see css selector syntax for more) and call its `setCurrentTab` method. Since the function knows `setCurrentTab` takes an integer, it will automatically pull the `intValue` member out of the event and pass it to the method. The given XML is the same as the following D: --- auto select = new DropDownSelection(parent); select.addOption("Foo"); select.addOption("Bar"); auto page = new PageWidget(parent); page.name = "mypage"; select.addEventListener("change", (Event event) { page.setCurrentTab(event.intValue); }); --- +/ module arsd.minigui_xml; public import arsd.minigui; public import arsd.minigui : Event; import arsd.textlayouter; import arsd.dom; import std.conv; import std.exception; import std.functional : toDelegate; import std.string : strip; import std.traits; private template ident(T...) { static if(is(T[0])) alias ident = T[0]; else alias ident = void; } enum ParseContinue { recurse, next, abort } alias WidgetFactory = ParseContinue delegate(Widget parent, Element element, out Widget result); alias WidgetTextHandler = void delegate(Widget widget, string text); WidgetFactory[string] widgetFactoryFunctions; WidgetTextHandler[string] widgetTextHandlers; void delegate(string eventName, Widget, Event, string content) xmlScriptEventHandler; static this() { xmlScriptEventHandler = toDelegate(&nullScriptEventHandler); } void nullScriptEventHandler(string eventName, Widget w, Event e, string) { import std.stdio : stderr; stderr.writeln("Ignoring event ", eventName, " ", e, " on widget ", w.elementName, " because xmlScriptEventHandler is not set"); } private bool startsWith(T)(T[] doesThis, T[] startWithThis) { return doesThis.length >= startWithThis.length && doesThis[0 .. startWithThis.length] == startWithThis; } private bool isLower(char c) { return c >= 'a' && c <= 'z'; } private bool isUpper(char c) { return c >= 'A' && c <= 'Z'; } private char assumeLowerToUpper(char c) { return cast(char)(c - 'a' + 'A'); } private char assumeUpperToLower(char c) { return cast(char)(c - 'A' + 'a'); } string hyphenate(string argname) { int hyphen; foreach (i, char c; argname) if (c.isUpper && (i == 0 || !argname[i - 1].isUpper)) hyphen++; if (hyphen == 0) return argname; char[] ret = new char[argname.length + hyphen]; int i; bool prevUpper; foreach (char c; argname) { bool upper = c.isUpper; if (upper) { if (!prevUpper) ret[i++] = '-'; ret[i++] = c.assumeUpperToLower; } else { ret[i++] = c; } prevUpper = upper; } assert(i == ret.length); return cast(string) ret; } string unhyphen(string argname) { int hyphen; foreach (i, char c; argname) if (c == '-' && (i == 0 || argname[i - 1] != '-')) hyphen++; if (hyphen == 0) return argname; char[] ret = new char[argname.length - hyphen]; int i; char prev; foreach (char c; argname) { if (c != '-') { if (prev == '-' && c.isLower) ret[i++] = c.assumeLowerToUpper; else ret[i++] = c; } prev = c; } assert(i == ret.length); return cast(string) ret; } void initMinigui(Modules...)() { import std.traits; import std.conv; static foreach (alias Module; Modules) { pragma(msg, Module.stringof); appendMiniguiModule!Module; } } void appendMiniguiModule(alias Module, string prefix = null)() { foreach(memberName; __traits(allMembers, Module)) static if(!__traits(isDeprecated, __traits(getMember, Module, memberName))) { alias Member = ident!(__traits(getMember, Module, memberName)); static if(is(Member == class) && !isAbstractClass!Member && is(Member : Widget) && __traits(getProtection, Member) != "private") { widgetFactoryFunctions[prefix ~ memberName] = (Widget parent, Element element, out Widget widget) { static if(is(Member : Dialog)) { widget = new Member(); } else static if(is(Member : Menu)) { widget = new Menu(null, null); } else static if(is(Member : Window)) { widget = new Member("test"); } else { string[string] args = element.attributes; enum paramNames = ParameterIdentifierTuple!(__traits(getMember, Member, "__ctor")); Parameters!(__traits(getMember, Member, "__ctor")) params; static assert(paramNames.length, Member); bool[cast(int)paramNames.length - 1] requiredParams; static foreach (idx, param; params[0 .. $-1]) {{ enum hyphenated = paramNames[idx].hyphenate; if (auto arg = hyphenated in args) { enforce(!requiredParams[idx], "May pass required parameter " ~ hyphenated ~ " only exactly once"); requiredParams[idx] = true; static if(is(typeof(param) == MemoryImage)) { } else static if(is(typeof(param) == Color)) { params[idx] = Color.fromString(*arg); } else static if(is(typeof(param) == TextLayouter)) params[idx] = null; else params[idx] = to!(typeof(param))(*arg); } else { enforce(false, "Missing required parameter " ~ hyphenated ~ " for Widget " ~ memberName); assert(false); } }} params[$-1] = cast(typeof(params[$-1])) parent; auto member = new Member(params); widget = member; foreach (argName, argValue; args) { if (argName.startsWith("on-")) { auto eventName = argName[3 .. $].unhyphen; widget.addEventListener(eventName, (event) { xmlScriptEventHandler(eventName, member, event, argValue); }); } else if (argName == "name") member.name = argValue; else if (argName == "statusTip") member.statusTip = argValue; else { argName = argName.unhyphen; switch (argName) { static foreach (idx, param; params[0 .. $-1]) { case paramNames[idx]: } break; static if (is(typeof(Member.addParameter))) { default: member.addParameter(argName, argValue); break; } else { // TODO: add generic parameter setting here (iterate by UDA maybe) default: enforce(false, "Unknown parameter " ~ argName ~ " for Widget " ~ memberName); assert(false); } } } } } return ParseContinue.recurse; }; enum hasText = is(typeof(Member.text) == string) || is(typeof(Member.text()) == string); enum hasContent = is(typeof(Member.content) == string) || is(typeof(Member.content()) == string); enum hasLabel = is(typeof(Member.label) == string) || is(typeof(Member.label()) == string); static if (hasText || hasContent || hasLabel) { enum member = hasText ? "text" : hasContent ? "content" : hasLabel ? "label" : null; widgetTextHandlers[memberName] = (Widget widget, string text) { auto w = cast(Member)widget; assert(w, "Called widget text handler with widget of type " ~ typeid(widget).name ~ " but it was registered for " ~ memberName ~ " which is incompatible"); mixin("w.", member, " = w.", member, " ~ text;"); }; } // TODO: might want to check for child methods/structs that register as child nodes } } } /// Widget makeWidgetFromString(string xml, Widget parent) { auto document = new Document(xml, true, true); auto r = document.root; return miniguiWidgetFromXml(r, parent); } /// Window createWindowFromXml(string xml) { return createWindowFromXml(new Document(xml, true, true)); } /// Window createWindowFromXml(Document document) { auto r = document.root; return cast(Window) miniguiWidgetFromXml(r, null); } /// Widget miniguiWidgetFromXml(Element element, Widget parent) { Widget w; miniguiWidgetFromXml(element, parent, w); return w; } /// ParseContinue miniguiWidgetFromXml(Element element, Widget parent, out Widget w) { assert(widgetFactoryFunctions !is null, "No widget factories have been registered, register them using initMinigui!(arsd.minigui); at startup"); if (auto factory = element.tagName in widgetFactoryFunctions) { auto c = (*factory)(parent, element, w); if (c == ParseContinue.recurse) { c = ParseContinue.next; Widget dummy; foreach (child; element.children) if (miniguiWidgetFromXml(child, w, dummy) == ParseContinue.abort) { c = ParseContinue.abort; break; } } return c; } else if (element.tagName == "#text") { string text = element.nodeValue.strip; if (text.length) { assert(parent, "got xml text without parent, make sure you only pass elements!"); if (auto factory = parent.elementName in widgetTextHandlers) (*factory)(parent, text); else { import std.stdio : stderr; stderr.writeln("WARN: no text handler for widget ", parent.elementName, " ~= ", [text]); } } return ParseContinue.next; } else { enforce(false, "Unknown tag " ~ element.tagName); assert(false); } } string elementName(Widget w) { if (w is null) return null; auto name = typeid(w).name; foreach_reverse (i, char c; name) if (c == '.') return name[i + 1 .. $]; return name; }