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