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