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.textlayouter; 82 83 import arsd.dom; 84 85 import std.conv; 86 import std.exception; 87 import std.functional : toDelegate; 88 import std.string : strip; 89 import std.traits; 90 91 private template ident(T...) 92 { 93 static if(is(T[0])) 94 alias ident = T[0]; 95 else 96 alias ident = void; 97 } 98 99 enum ParseContinue { recurse, next, abort } 100 101 alias WidgetFactory = ParseContinue delegate(Widget parent, Element element, out Widget result); 102 alias WidgetTextHandler = void delegate(Widget widget, string text); 103 104 WidgetFactory[string] widgetFactoryFunctions; 105 WidgetTextHandler[string] widgetTextHandlers; 106 107 void delegate(string eventName, Widget, Event, string content) xmlScriptEventHandler; 108 static this() 109 { 110 xmlScriptEventHandler = toDelegate(&nullScriptEventHandler); 111 } 112 113 void nullScriptEventHandler(string eventName, Widget w, Event e, string) 114 { 115 import std.stdio : stderr; 116 117 stderr.writeln("Ignoring event ", eventName, " ", e, " on widget ", w.elementName, " because xmlScriptEventHandler is not set"); 118 } 119 120 private bool startsWith(T)(T[] doesThis, T[] startWithThis) 121 { 122 return doesThis.length >= startWithThis.length && doesThis[0 .. startWithThis.length] == startWithThis; 123 } 124 125 private bool isLower(char c) 126 { 127 return c >= 'a' && c <= 'z'; 128 } 129 130 private bool isUpper(char c) 131 { 132 return c >= 'A' && c <= 'Z'; 133 } 134 135 private char assumeLowerToUpper(char c) 136 { 137 return cast(char)(c - 'a' + 'A'); 138 } 139 140 private char assumeUpperToLower(char c) 141 { 142 return cast(char)(c - 'A' + 'a'); 143 } 144 145 string hyphenate(string argname) 146 { 147 int hyphen; 148 foreach (i, char c; argname) 149 if (c.isUpper && (i == 0 || !argname[i - 1].isUpper)) 150 hyphen++; 151 152 if (hyphen == 0) 153 return argname; 154 char[] ret = new char[argname.length + hyphen]; 155 int i; 156 bool prevUpper; 157 foreach (char c; argname) 158 { 159 bool upper = c.isUpper; 160 if (upper) 161 { 162 if (!prevUpper) 163 ret[i++] = '-'; 164 ret[i++] = c.assumeUpperToLower; 165 } 166 else 167 { 168 ret[i++] = c; 169 } 170 prevUpper = upper; 171 } 172 assert(i == ret.length); 173 return cast(string) ret; 174 } 175 176 string unhyphen(string argname) 177 { 178 int hyphen; 179 foreach (i, char c; argname) 180 if (c == '-' && (i == 0 || argname[i - 1] != '-')) 181 hyphen++; 182 183 if (hyphen == 0) 184 return argname; 185 char[] ret = new char[argname.length - hyphen]; 186 int i; 187 char prev; 188 foreach (char c; argname) 189 { 190 if (c != '-') 191 { 192 if (prev == '-' && c.isLower) 193 ret[i++] = c.assumeLowerToUpper; 194 else 195 ret[i++] = c; 196 } 197 prev = c; 198 } 199 assert(i == ret.length); 200 return cast(string) ret; 201 } 202 203 void initMinigui(Modules...)() 204 { 205 import std.traits; 206 import std.conv; 207 208 static foreach (alias Module; Modules) 209 { 210 //pragma(msg, Module.stringof); 211 appendMiniguiModule!Module; 212 } 213 } 214 215 void appendMiniguiModule(alias Module, string prefix = null)() 216 { 217 foreach(memberName; __traits(allMembers, Module)) static if(!__traits(isDeprecated, __traits(getMember, Module, memberName))) 218 static if(memberName != "seperator") 219 { 220 alias Member = ident!(__traits(getMember, Module, memberName)); 221 static if(is(Member == class) && !isAbstractClass!Member && is(Member : Widget) && __traits(getProtection, Member) != "private") 222 { 223 widgetFactoryFunctions[prefix ~ memberName] = (Widget parent, Element element, out Widget widget) 224 { 225 static if(is(Member : MessageBox)) 226 { 227 widget = new MessageBox(""); 228 } 229 else static if(is(Member : Dialog)) 230 { 231 widget = new Member(null, 0, 0); // FIXME 232 } 233 else static if(is(Member : Menu)) 234 { 235 widget = new Menu(null, null); 236 } 237 else static if(is(Member : TooltipWindow)) 238 { 239 widget = null; 240 } 241 else static if(is(Member : Window)) 242 { 243 widget = new Member("test"); 244 } 245 else 246 { 247 string[string] args; 248 foreach(k, v; element.attributes) 249 args[k] = v; 250 251 enum paramNames = ParameterIdentifierTuple!(__traits(getMember, Member, "__ctor")); 252 Parameters!(__traits(getMember, Member, "__ctor")) params; 253 static assert(paramNames.length, Member); 254 bool[cast(int)paramNames.length - 1] requiredParams; 255 256 static foreach (idx, param; params[0 .. $-1]) 257 {{ 258 enum hyphenated = paramNames[idx].hyphenate; 259 if (auto arg = hyphenated in args) 260 { 261 enforce(!requiredParams[idx], "May pass required parameter " ~ hyphenated ~ " only exactly once"); 262 requiredParams[idx] = true; 263 static if(is(typeof(param) == MemoryImage)) 264 { 265 266 } 267 else static if(is(typeof(param) == Color)) 268 { 269 params[idx] = Color.fromString(*arg); 270 } 271 else static if(is(typeof(param) == TextLayouter)) 272 params[idx] = null; 273 else static if(is(typeof(param) == class)) 274 params[idx] = null; 275 else static if(is(typeof(param) == delegate)) 276 params[idx] = null; 277 else 278 params[idx] = to!(typeof(param))(*arg); 279 } 280 else 281 { 282 enforce(false, "Missing required parameter " ~ hyphenated ~ " for Widget " ~ memberName); 283 assert(false); 284 } 285 }} 286 287 params[$-1] = cast(typeof(params[$-1])) parent; 288 289 auto member = new Member(params); 290 widget = member; 291 292 foreach (argName, argValue; args) 293 { 294 if (argName.startsWith("on-")) 295 { 296 auto eventName = argName[3 .. $].unhyphen; 297 widget.addEventListener(eventName, (event) { xmlScriptEventHandler(eventName, member, event, argValue); }); 298 } 299 else if (argName == "name") 300 member.name = argValue; 301 else if (argName == "statusTip") 302 member.statusTip = argValue; 303 else 304 { 305 argName = argName.unhyphen; 306 switch (argName) 307 { 308 static foreach (idx, param; params[0 .. $-1]) 309 { 310 case paramNames[idx]: 311 } 312 break; 313 static if (is(typeof(Member.addParameter))) 314 { 315 default: 316 member.addParameter(argName, argValue); 317 break; 318 } 319 else 320 { 321 // TODO: add generic parameter setting here (iterate by UDA maybe) 322 default: 323 enforce(false, "Unknown parameter " ~ argName ~ " for Widget " ~ memberName); 324 assert(false); 325 } 326 } 327 } 328 } 329 } 330 return ParseContinue.recurse; 331 }; 332 333 enum hasText = is(typeof(Member.text) == string) || is(typeof(Member.text()) == string); 334 enum hasContent = is(typeof(Member.content) == string) || is(typeof(Member.content()) == string); 335 enum hasLabel = is(typeof(Member.label) == string) || is(typeof(Member.label()) == string); 336 static if (hasText || hasContent || hasLabel) 337 { 338 enum member = hasText ? "text" : hasContent ? "content" : hasLabel ? "label" : null; 339 widgetTextHandlers[memberName] = (Widget widget, string text) 340 { 341 auto w = cast(Member)widget; 342 assert(w, "Called widget text handler with widget of type " 343 ~ typeid(widget).name ~ " but it was registered for " 344 ~ memberName ~ " which is incompatible"); 345 mixin("w.", member, " = w.", member, " ~ text;"); 346 }; 347 } 348 349 // TODO: might want to check for child methods/structs that register as child nodes 350 } 351 } 352 } 353 354 /// 355 Widget makeWidgetFromString(string xml, Widget parent) 356 { 357 auto document = new Document(xml, true, true); 358 auto r = document.root; 359 return miniguiWidgetFromXml(r, parent); 360 } 361 362 /// 363 Window createWindowFromXml(string xml) 364 { 365 return createWindowFromXml(new Document(xml, true, true)); 366 } 367 /// 368 Window createWindowFromXml(Document document) 369 { 370 auto r = document.root; 371 return cast(Window) miniguiWidgetFromXml(r, null); 372 } 373 /// 374 Widget miniguiWidgetFromXml(Element element, Widget parent) 375 { 376 Widget w; 377 miniguiWidgetFromXml(element, parent, w); 378 return w; 379 } 380 /// 381 ParseContinue miniguiWidgetFromXml(Element element, Widget parent, out Widget w) 382 { 383 assert(widgetFactoryFunctions !is null, "No widget factories have been registered, register them using initMinigui!(arsd.minigui); at startup"); 384 385 if (auto factory = element.tagName in widgetFactoryFunctions) 386 { 387 auto c = (*factory)(parent, element, w); 388 389 if (c == ParseContinue.recurse) 390 { 391 c = ParseContinue.next; 392 Widget dummy; 393 foreach (child; element.children) 394 if (miniguiWidgetFromXml(child, w, dummy) == ParseContinue.abort) 395 { 396 c = ParseContinue.abort; 397 break; 398 } 399 } 400 return c; 401 } 402 else if (element.tagName == "#text") 403 { 404 string text = element.nodeValue.strip; 405 if (text.length) 406 { 407 assert(parent, "got xml text without parent, make sure you only pass elements!"); 408 if (auto factory = parent.elementName in widgetTextHandlers) 409 (*factory)(parent, text); 410 else 411 { 412 import std.stdio : stderr; 413 414 stderr.writeln("WARN: no text handler for widget ", parent.elementName, " ~= ", [text]); 415 } 416 } 417 return ParseContinue.next; 418 } 419 else 420 { 421 enforce(false, "Unknown tag " ~ element.tagName); 422 assert(false); 423 } 424 } 425 426 string elementName(Widget w) 427 { 428 if (w is null) 429 return null; 430 auto name = typeid(w).name; 431 foreach_reverse (i, char c; name) 432 if (c == '.') 433 return name[i + 1 .. $]; 434 return name; 435 } 436