1 /++ 2 A small extension module to [arsd.minigui] that adds 3 functions for creating widgets and windows from short 4 XML descriptions. 5 6 If you choose to use this, it will require [arsd.dom] 7 to be compiled into your project too. 8 9 --- 10 import arsd.minigui_xml; 11 Window window = createWindowFromXml(` 12 <MainWindow> 13 <Button label="Hi!" /> 14 </MainWindow> 15 `); 16 --- 17 18 19 To add custom widgets to the minigui_xml factory, you need 20 to register them with FIXME. 21 22 You can attach some events right in the XML using attributes. 23 The attribute names are `onEVENTNAME` or `ondirectEVENTNAME` 24 and the values are one of the following three value types: 25 26 $(LIST 27 * If it starts with `&`, it is a delegate you need 28 to register using the FIXME function. 29 30 * If it starts with `(`, it is a string passed to 31 the [arsd.dom.querySelector] function to get an 32 element reference 33 34 * Otherwise, it tries to call a script function (if 35 scripting is available). 36 ) 37 38 Keep in mind 39 For example, to make a page widget that changes based on a 40 drop down selection, you may: 41 42 ```xml 43 <DropDownSelection onchange="$(+PageWidget).setCurrentTab"> 44 <option>Foo</option> 45 <option>Bar</option> 46 </DropDownSelection> 47 <PageWidget name="mypage"> 48 <!-- contents elided --> 49 </PageWidget> 50 ``` 51 52 That will create a select widget that when it changes, it will 53 look for the next PageWidget sibling (that's the meaning of `+PageWidget`, 54 see css selector syntax for more) and call its `setCurrentTab` 55 method. 56 57 Since the function knows `setCurrentTab` takes an integer, it will 58 automatically pull the `intValue` member out of the event and pass 59 it to the method. 60 61 The given XML is the same as the following D: 62 63 --- 64 auto select = new DropDownSelection(parent); 65 select.addOption("Foo"); 66 select.addOption("Bar"); 67 auto page = new PageWidget(parent); 68 page.name = "mypage"; 69 70 select.addEventListener("change", (Event event) 71 { 72 page.setCurrentTab(event.intValue); 73 }); 74 --- 75 +/ 76 module arsd.minigui_xml; 77 78 public import arsd.minigui; 79 public import arsd.minigui : Event; 80 81 import arsd.dom; 82 83 import std.conv; 84 import std.exception; 85 import std.functional : toDelegate; 86 import std.string : strip; 87 import std.traits; 88 89 private template ident(T...) 90 { 91 static if(is(T[0])) 92 alias ident = T[0]; 93 else 94 alias ident = void; 95 } 96 97 enum ParseContinue { recurse, next, abort } 98 99 alias WidgetFactory = ParseContinue delegate(Widget parent, Element element, out Widget result); 100 alias WidgetTextHandler = void delegate(Widget widget, string text); 101 102 WidgetFactory[string] widgetFactoryFunctions; 103 WidgetTextHandler[string] widgetTextHandlers; 104 105 void delegate(string eventName, Widget, Event, string content) xmlScriptEventHandler; 106 static this() 107 { 108 xmlScriptEventHandler = toDelegate(&nullScriptEventHandler); 109 } 110 111 void nullScriptEventHandler(string eventName, Widget w, Event e, string) 112 { 113 import std.stdio : stderr; 114 115 stderr.writeln("Ignoring event ", eventName, " ", e, " on widget ", w.elementName, " because xmlScriptEventHandler is not set"); 116 } 117 118 private bool startsWith(T)(T[] doesThis, T[] startWithThis) 119 { 120 return doesThis.length >= startWithThis.length && doesThis[0 .. startWithThis.length] == startWithThis; 121 } 122 123 private bool isLower(char c) 124 { 125 return c >= 'a' && c <= 'z'; 126 } 127 128 private bool isUpper(char c) 129 { 130 return c >= 'A' && c <= 'Z'; 131 } 132 133 private char assumeLowerToUpper(char c) 134 { 135 return cast(char)(c - 'a' + 'A'); 136 } 137 138 private char assumeUpperToLower(char c) 139 { 140 return cast(char)(c - 'A' + 'a'); 141 } 142 143 string hyphenate(string argname) 144 { 145 int hyphen; 146 foreach (i, char c; argname) 147 if (c.isUpper && (i == 0 || !argname[i - 1].isUpper)) 148 hyphen++; 149 150 if (hyphen == 0) 151 return argname; 152 char[] ret = new char[argname.length + hyphen]; 153 int i; 154 bool prevUpper; 155 foreach (char c; argname) 156 { 157 bool upper = c.isUpper; 158 if (upper) 159 { 160 if (!prevUpper) 161 ret[i++] = '-'; 162 ret[i++] = c.assumeUpperToLower; 163 } 164 else 165 { 166 ret[i++] = c; 167 } 168 prevUpper = upper; 169 } 170 assert(i == ret.length); 171 return cast(string) ret; 172 } 173 174 string unhyphen(string argname) 175 { 176 int hyphen; 177 foreach (i, char c; argname) 178 if (c == '-' && (i == 0 || argname[i - 1] != '-')) 179 hyphen++; 180 181 if (hyphen == 0) 182 return argname; 183 char[] ret = new char[argname.length - hyphen]; 184 int i; 185 char prev; 186 foreach (char c; argname) 187 { 188 if (c != '-') 189 { 190 if (prev == '-' && c.isLower) 191 ret[i++] = c.assumeLowerToUpper; 192 else 193 ret[i++] = c; 194 } 195 prev = c; 196 } 197 assert(i == ret.length); 198 return cast(string) ret; 199 } 200 201 void initMinigui(Modules...)() 202 { 203 import std.traits; 204 import std.conv; 205 206 static foreach (alias Module; Modules) 207 { 208 pragma(msg, Module.stringof); 209 appendMiniguiModule!Module; 210 } 211 } 212 213 void appendMiniguiModule(alias Module, string prefix = null)() 214 { 215 foreach(memberName; __traits(allMembers, Module)) static if(!__traits(isDeprecated, __traits(getMember, Module, memberName))) 216 { 217 alias Member = ident!(__traits(getMember, Module, memberName)); 218 static if(is(Member == class) && !isAbstractClass!Member && is(Member : Widget) && __traits(getProtection, Member) != "private") 219 { 220 widgetFactoryFunctions[prefix ~ memberName] = (Widget parent, Element element, out Widget widget) 221 { 222 static if(is(Member : Dialog)) 223 { 224 widget = new Member(); 225 } 226 else static if(is(Member : Menu)) 227 { 228 widget = new Menu(null, null); 229 } 230 else static if(is(Member : Window)) 231 { 232 widget = new Member("test"); 233 } 234 else 235 { 236 string[string] args = element.attributes; 237 238 enum paramNames = ParameterIdentifierTuple!(__traits(getMember, Member, "__ctor")); 239 Parameters!(__traits(getMember, Member, "__ctor")) params; 240 static assert(paramNames.length, Member); 241 bool[cast(int)paramNames.length - 1] requiredParams; 242 243 static foreach (idx, param; params[0 .. $-1]) 244 {{ 245 enum hyphenated = paramNames[idx].hyphenate; 246 if (auto arg = hyphenated in args) 247 { 248 enforce(!requiredParams[idx], "May pass required parameter " ~ hyphenated ~ " only exactly once"); 249 requiredParams[idx] = true; 250 static if(is(typeof(param) == MemoryImage)) 251 { 252 253 } 254 else static if(is(typeof(param) == Color)) 255 { 256 params[idx] = Color.fromString(*arg); 257 } 258 else 259 params[idx] = to!(typeof(param))(*arg); 260 } 261 else 262 { 263 enforce(false, "Missing required parameter " ~ hyphenated ~ " for Widget " ~ memberName); 264 assert(false); 265 } 266 }} 267 268 params[$-1] = parent; 269 270 auto member = new Member(params); 271 widget = member; 272 273 foreach (argName, argValue; args) 274 { 275 if (argName.startsWith("on-")) 276 { 277 auto eventName = argName[3 .. $].unhyphen; 278 widget.addEventListener(eventName, (event) { xmlScriptEventHandler(eventName, member, event, argValue); }); 279 } 280 else 281 { 282 argName = argName.unhyphen; 283 switch (argName) 284 { 285 static foreach (idx, param; params[0 .. $-1]) 286 { 287 case paramNames[idx]: 288 } 289 break; 290 static if (is(typeof(Member.addParameter))) 291 { 292 default: 293 member.addParameter(argName, argValue); 294 break; 295 } 296 else 297 { 298 // TODO: add generic parameter setting here (iterate by UDA maybe) 299 default: 300 enforce(false, "Unknown parameter " ~ argName ~ " for Widget " ~ memberName); 301 assert(false); 302 } 303 } 304 } 305 } 306 } 307 return ParseContinue.recurse; 308 }; 309 310 enum hasText = is(typeof(Member.text) == string) || is(typeof(Member.text()) == string); 311 enum hasContent = is(typeof(Member.content) == string) || is(typeof(Member.content()) == string); 312 enum hasLabel = is(typeof(Member.label) == string) || is(typeof(Member.label()) == string); 313 static if (hasText || hasContent || hasLabel) 314 { 315 enum member = hasText ? "text" : hasContent ? "content" : hasLabel ? "label" : null; 316 widgetTextHandlers[memberName] = (Widget widget, string text) 317 { 318 auto w = cast(Member)widget; 319 assert(w, "Called widget text handler with widget of type " 320 ~ typeid(widget).name ~ " but it was registered for " 321 ~ memberName ~ " which is incompatible"); 322 mixin("w.", member, " = w.", member, " ~ text;"); 323 }; 324 } 325 326 // TODO: might want to check for child methods/structs that register as child nodes 327 } 328 } 329 } 330 331 /// 332 Widget makeWidgetFromString(string xml, Widget parent) 333 { 334 auto document = new Document(xml, true, true); 335 auto r = document.root; 336 return miniguiWidgetFromXml(r, parent); 337 } 338 339 /// 340 Window createWindowFromXml(string xml) 341 { 342 return createWindowFromXml(new Document(xml, true, true)); 343 } 344 /// 345 Window createWindowFromXml(Document document) 346 { 347 auto r = document.root; 348 return cast(Window) miniguiWidgetFromXml(r, null); 349 } 350 /// 351 Widget miniguiWidgetFromXml(Element element, Widget parent) 352 { 353 Widget w; 354 miniguiWidgetFromXml(element, parent, w); 355 return w; 356 } 357 /// 358 ParseContinue miniguiWidgetFromXml(Element element, Widget parent, out Widget w) 359 { 360 assert(widgetFactoryFunctions !is null, "No widget factories have been registered, register them using initMinigui!(arsd.minigui); at startup"); 361 362 if (auto factory = element.tagName in widgetFactoryFunctions) 363 { 364 auto c = (*factory)(parent, element, w); 365 366 if (c == ParseContinue.recurse) 367 { 368 c = ParseContinue.next; 369 Widget dummy; 370 foreach (child; element.children) 371 if (miniguiWidgetFromXml(child, w, dummy) == ParseContinue.abort) 372 { 373 c = ParseContinue.abort; 374 break; 375 } 376 } 377 return c; 378 } 379 else if (element.tagName == "#text") 380 { 381 string text = element.nodeValue.strip; 382 if (text.length) 383 { 384 assert(parent, "got xml text without parent, make sure you only pass elements!"); 385 if (auto factory = parent.elementName in widgetTextHandlers) 386 (*factory)(parent, text); 387 else 388 { 389 import std.stdio : stderr; 390 391 stderr.writeln("WARN: no text handler for widget ", parent.elementName, " ~= ", [text]); 392 } 393 } 394 return ParseContinue.next; 395 } 396 else 397 { 398 enforce(false, "Unknown tag " ~ element.tagName); 399 assert(false); 400 } 401 } 402 403 string elementName(Widget w) 404 { 405 if (w is null) 406 return null; 407 auto name = typeid(w).name; 408 foreach_reverse (i, char c; name) 409 if (c == '.') 410 return name[i + 1 .. $]; 411 return name; 412 } 413