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