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 { 219 alias Member = ident!(__traits(getMember, Module, memberName)); 220 static if(is(Member == class) && !isAbstractClass!Member && is(Member : Widget) && __traits(getProtection, Member) != "private") 221 { 222 widgetFactoryFunctions[prefix ~ memberName] = (Widget parent, Element element, out Widget widget) 223 { 224 static if(is(Member : Dialog)) 225 { 226 widget = new Member(); 227 } 228 else static if(is(Member : Menu)) 229 { 230 widget = new Menu(null, null); 231 } 232 else static if(is(Member : Window)) 233 { 234 widget = new Member("test"); 235 } 236 else 237 { 238 string[string] args = element.attributes; 239 240 enum paramNames = ParameterIdentifierTuple!(__traits(getMember, Member, "__ctor")); 241 Parameters!(__traits(getMember, Member, "__ctor")) params; 242 static assert(paramNames.length, Member); 243 bool[cast(int)paramNames.length - 1] requiredParams; 244 245 static foreach (idx, param; params[0 .. $-1]) 246 {{ 247 enum hyphenated = paramNames[idx].hyphenate; 248 if (auto arg = hyphenated in args) 249 { 250 enforce(!requiredParams[idx], "May pass required parameter " ~ hyphenated ~ " only exactly once"); 251 requiredParams[idx] = true; 252 static if(is(typeof(param) == MemoryImage)) 253 { 254 255 } 256 else static if(is(typeof(param) == Color)) 257 { 258 params[idx] = Color.fromString(*arg); 259 } 260 else static if(is(typeof(param) == TextLayouter)) 261 params[idx] = null; 262 else 263 params[idx] = to!(typeof(param))(*arg); 264 } 265 else 266 { 267 enforce(false, "Missing required parameter " ~ hyphenated ~ " for Widget " ~ memberName); 268 assert(false); 269 } 270 }} 271 272 params[$-1] = cast(typeof(params[$-1])) parent; 273 274 auto member = new Member(params); 275 widget = member; 276 277 foreach (argName, argValue; args) 278 { 279 if (argName.startsWith("on-")) 280 { 281 auto eventName = argName[3 .. $].unhyphen; 282 widget.addEventListener(eventName, (event) { xmlScriptEventHandler(eventName, member, event, argValue); }); 283 } 284 else if (argName == "name") 285 member.name = argValue; 286 else if (argName == "statusTip") 287 member.statusTip = argValue; 288 else 289 { 290 argName = argName.unhyphen; 291 switch (argName) 292 { 293 static foreach (idx, param; params[0 .. $-1]) 294 { 295 case paramNames[idx]: 296 } 297 break; 298 static if (is(typeof(Member.addParameter))) 299 { 300 default: 301 member.addParameter(argName, argValue); 302 break; 303 } 304 else 305 { 306 // TODO: add generic parameter setting here (iterate by UDA maybe) 307 default: 308 enforce(false, "Unknown parameter " ~ argName ~ " for Widget " ~ memberName); 309 assert(false); 310 } 311 } 312 } 313 } 314 } 315 return ParseContinue.recurse; 316 }; 317 318 enum hasText = is(typeof(Member.text) == string) || is(typeof(Member.text()) == string); 319 enum hasContent = is(typeof(Member.content) == string) || is(typeof(Member.content()) == string); 320 enum hasLabel = is(typeof(Member.label) == string) || is(typeof(Member.label()) == string); 321 static if (hasText || hasContent || hasLabel) 322 { 323 enum member = hasText ? "text" : hasContent ? "content" : hasLabel ? "label" : null; 324 widgetTextHandlers[memberName] = (Widget widget, string text) 325 { 326 auto w = cast(Member)widget; 327 assert(w, "Called widget text handler with widget of type " 328 ~ typeid(widget).name ~ " but it was registered for " 329 ~ memberName ~ " which is incompatible"); 330 mixin("w.", member, " = w.", member, " ~ text;"); 331 }; 332 } 333 334 // TODO: might want to check for child methods/structs that register as child nodes 335 } 336 } 337 } 338 339 /// 340 Widget makeWidgetFromString(string xml, Widget parent) 341 { 342 auto document = new Document(xml, true, true); 343 auto r = document.root; 344 return miniguiWidgetFromXml(r, parent); 345 } 346 347 /// 348 Window createWindowFromXml(string xml) 349 { 350 return createWindowFromXml(new Document(xml, true, true)); 351 } 352 /// 353 Window createWindowFromXml(Document document) 354 { 355 auto r = document.root; 356 return cast(Window) miniguiWidgetFromXml(r, null); 357 } 358 /// 359 Widget miniguiWidgetFromXml(Element element, Widget parent) 360 { 361 Widget w; 362 miniguiWidgetFromXml(element, parent, w); 363 return w; 364 } 365 /// 366 ParseContinue miniguiWidgetFromXml(Element element, Widget parent, out Widget w) 367 { 368 assert(widgetFactoryFunctions !is null, "No widget factories have been registered, register them using initMinigui!(arsd.minigui); at startup"); 369 370 if (auto factory = element.tagName in widgetFactoryFunctions) 371 { 372 auto c = (*factory)(parent, element, w); 373 374 if (c == ParseContinue.recurse) 375 { 376 c = ParseContinue.next; 377 Widget dummy; 378 foreach (child; element.children) 379 if (miniguiWidgetFromXml(child, w, dummy) == ParseContinue.abort) 380 { 381 c = ParseContinue.abort; 382 break; 383 } 384 } 385 return c; 386 } 387 else if (element.tagName == "#text") 388 { 389 string text = element.nodeValue.strip; 390 if (text.length) 391 { 392 assert(parent, "got xml text without parent, make sure you only pass elements!"); 393 if (auto factory = parent.elementName in widgetTextHandlers) 394 (*factory)(parent, text); 395 else 396 { 397 import std.stdio : stderr; 398 399 stderr.writeln("WARN: no text handler for widget ", parent.elementName, " ~= ", [text]); 400 } 401 } 402 return ParseContinue.next; 403 } 404 else 405 { 406 enforce(false, "Unknown tag " ~ element.tagName); 407 assert(false); 408 } 409 } 410 411 string elementName(Widget w) 412 { 413 if (w is null) 414 return null; 415 auto name = typeid(w).name; 416 foreach_reverse (i, char c; name) 417 if (c == '.') 418 return name[i + 1 .. $]; 419 return name; 420 } 421