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
285 						{
286 							argName = argName.unhyphen;
287 							switch (argName)
288 							{
289 								static foreach (idx, param; params[0 .. $-1])
290 								{
291 									case paramNames[idx]:
292 								}
293 									break;
294 								static if (is(typeof(Member.addParameter)))
295 								{
296 								default:
297 									member.addParameter(argName, argValue);
298 									break;
299 								}
300 								else
301 								{
302 									// TODO: add generic parameter setting here (iterate by UDA maybe)
303 								default:
304 									enforce(false, "Unknown parameter " ~ argName ~ " for Widget " ~ memberName);
305 									assert(false);
306 								}
307 							}
308 						}
309 					}
310 				}
311 				return ParseContinue.recurse;
312 			};
313 
314 			enum hasText = is(typeof(Member.text) == string) || is(typeof(Member.text()) == string);
315 			enum hasContent = is(typeof(Member.content) == string) || is(typeof(Member.content()) == string);
316 			enum hasLabel = is(typeof(Member.label) == string) || is(typeof(Member.label()) == string);
317 			static if (hasText || hasContent || hasLabel)
318 			{
319 				enum member = hasText ? "text" : hasContent ? "content" : hasLabel ? "label" : null;
320 				widgetTextHandlers[memberName] = (Widget widget, string text)
321 				{
322 					auto w = cast(Member)widget;
323 					assert(w, "Called widget text handler with widget of type "
324 						~ typeid(widget).name ~ " but it was registered for "
325 						~ memberName ~ " which is incompatible");
326 					mixin("w.", member, " = w.", member, " ~ text;");
327 				};
328 			}
329 
330 			// TODO: might want to check for child methods/structs that register as child nodes
331 		}
332 	}
333 }
334 
335 ///
336 Widget makeWidgetFromString(string xml, Widget parent)
337 {
338 	auto document = new Document(xml, true, true);
339 	auto r = document.root;
340 	return miniguiWidgetFromXml(r, parent);
341 }
342 
343 ///
344 Window createWindowFromXml(string xml)
345 {
346 	return createWindowFromXml(new Document(xml, true, true));
347 }
348 ///
349 Window createWindowFromXml(Document document)
350 {
351 	auto r = document.root;
352 	return cast(Window) miniguiWidgetFromXml(r, null);
353 }
354 ///
355 Widget miniguiWidgetFromXml(Element element, Widget parent)
356 {
357 	Widget w;
358 	miniguiWidgetFromXml(element, parent, w);
359 	return w;
360 }
361 ///
362 ParseContinue miniguiWidgetFromXml(Element element, Widget parent, out Widget w)
363 {
364 	assert(widgetFactoryFunctions !is null, "No widget factories have been registered, register them using initMinigui!(arsd.minigui); at startup");
365 
366 	if (auto factory = element.tagName in widgetFactoryFunctions)
367 	{
368 		auto c = (*factory)(parent, element, w);
369 
370 		if (c == ParseContinue.recurse)
371 		{
372 			c = ParseContinue.next;
373 			Widget dummy;
374 			foreach (child; element.children)
375 				if (miniguiWidgetFromXml(child, w, dummy) == ParseContinue.abort)
376 				{
377 					c = ParseContinue.abort;
378 					break;
379 				}
380 		}
381 		return c;
382 	}
383 	else if (element.tagName == "#text")
384 	{
385 		string text = element.nodeValue.strip;
386 		if (text.length)
387 		{
388 			assert(parent, "got xml text without parent, make sure you only pass elements!");
389 			if (auto factory = parent.elementName in widgetTextHandlers)
390 				(*factory)(parent, text);
391 			else
392 			{
393 				import std.stdio : stderr;
394 
395 				stderr.writeln("WARN: no text handler for widget ", parent.elementName, " ~= ", [text]);
396 			}
397 		}
398 		return ParseContinue.next;
399 	}
400 	else
401 	{
402 		enforce(false, "Unknown tag " ~ element.tagName);
403 		assert(false);
404 	}
405 }
406 
407 string elementName(Widget w)
408 {
409 	if (w is null)
410 		return null;
411 	auto name = typeid(w).name;
412 	foreach_reverse (i, char c; name)
413 		if (c == '.')
414 			return name[i + 1 .. $];
415 	return name;
416 }
417