1 /**
2  * Window module.
3  *
4  * License:
5  *     MIT. See LICENSE for full details.
6  */
7 module tkd.window.window;
8 
9 /**
10  * Private imports.
11  */
12 import std.conv;
13 import std..string;
14 import tkd.element.element : CommandCallback;
15 import tkd.element.uielement;
16 import tkd.widget.common.color;
17 import tkd.image.image;
18 import tkd.interpreter;
19 
20 /**
21  * The Window class creates a new toplevel window.
22  *
23  * A window is similar to a frame except that it is created as a top-level 
24  * widget. The primary purpose of a toplevel is to serve as a container for 
25  * dialog boxes and other collections of widgets.
26  *
27  * Example:
28  * ---
29  * auto window = new Window("New window")
30  * 	.setGeometry(640, 480, 10, 10)
31  * 	.setDefaultIcon(new Png!("icon.png"))
32  * 	.setMaxSize(1024, 768)
33  * 	.setProtocolCommand(WindowProtocol.deleteWindow, delegate(CommandArgs args){ ... });
34  * ---
35  *
36  * Common_Commands:
37  *     These are injected common commands that can also be used with this widget.
38  *     $(P
39  *         $(LINK2 ../widget/common/color.html, Color) $(BR)
40  *     )
41  *
42  * Additional_Events:
43  *     Additional events that can also be bound to using the $(LINK2 ../element/uielement.html#UiElement.bind, bind) method.
44  *     $(P
45  *         <<PrevWindow>>,
46  *         <Alt-Key>,
47  *         <Key-Tab>,
48  *         <Key-F10>,
49  *     )
50  *
51  * See_Also:
52  *     $(LINK2 ../element/uielement.html, tkd.element.uielement)
53  */
54 class Window : UiElement
55 {
56 	/**
57 	 * The main window of the application.
58 	 */
59 	static private Window _mainWindow;
60 
61 	/*
62 	 * Constructor.
63 	 *
64 	 * This constructor is mainly for internal use. When creating a window 
65 	 * using this constructor what you are really doing is creating a new 
66 	 * reference to the main application window.
67 	 */
68 	private this()
69 	{
70 		super();
71 		this.overrideGeneratedId(".");
72 	}
73 
74 	/**
75 	 * Constructor.
76 	 *
77 	 * It's important to understand that the window will be drawn immediately 
78 	 * by default. This means that the window will display before any other 
79 	 * actions take place, including drawing or managing other widgets. This is 
80 	 * so other methods (such as platformId) that rely on the window being 
81 	 * drawn don't fail if immediately used afterwards.
82 	 * 
83 	 * This behaviour can be overridden by passing false as the waitForWindow 
84 	 * argument which is useful if you want the entire UI belonging to the new 
85 	 * window to be drawn before actaully showing it.
86 	 *
87 	 * The parent window is responsible for the life of this window. If the 
88 	 * parent window is destroyed, this one will be too.
89 	 *
90 	 * Params:
91 	 *     parent = The window to act as a parent.
92 	 *     title = The title of the window.
93 	 *     waitForWindow = Whether to wait for the window to be drawn before continuing.
94 	 */
95 	public this(Window parent, string title, bool waitForWindow = true)
96 	{
97 		super(parent);
98 		this._elementId = "window";
99 		this._tk.eval("toplevel %s", this.id);
100 
101 		if (waitForWindow)
102 		{
103 			// This tip was gathered from the following post, if it proves not 
104 			// to be cross-platform there are other tips in this post that 
105 			// could be used.
106 			// http://stackoverflow.com/questions/8929031/grabbing-a-new-window-in-tcl-tk
107 			this._tk.eval("tkwait visibility %s", this.id);
108 		}
109 
110 		this.setTitle(title);
111 	}
112 
113 	/**
114 	 * Constructor.
115 	 *
116 	 * It's important to understand that the window will be drawn immediately 
117 	 * by default. This means that the window will display before any other 
118 	 * actions take place, including drawing or managing other widgets. This is 
119 	 * so other methods (such as platformId) that rely on the window being 
120 	 * drawn don't fail if immediately used afterwards.
121 	 * 
122 	 * This behaviour can be overridden by passing false as the waitForWindow 
123 	 * argument which is useful if you want the entire UI belonging to the new 
124 	 * window to be drawn before actaully showing it.
125 	 *
126 	 * If no parent is specified the new window with be a child of the main 
127 	 * window.
128 	 *
129 	 * Params:
130 	 *     title = The title of the window.
131 	 *     waitForWindow = Whether to wait for the window to be drawn before continuing.
132 	 */
133 	public this(string title, bool waitForWindow = true)
134 	{
135 		this(new Window(), title, waitForWindow);
136 	}
137 
138 	/**
139 	 * Set the title of the window.
140 	 *
141 	 * Params:
142 	 *     title = The title of the window.
143 	 *
144 	 * Returns:
145 	 *     This window to aid method chaining.
146 	 */
147 	public auto setTitle(this T)(string title)
148 	{
149 		this._tk.eval("wm title %s {%s}", this.id, title);
150 
151 		return cast(T) this;
152 	}
153 
154 	/**
155 	 * Set the opacity of the window.
156 	 *
157 	 * Params:
158 	 *     opacity = A number between 0.0 and 1.0 specifying the transparency.
159 	 *
160 	 * Returns:
161 	 *     This window to aid method chaining.
162 	 */
163 	public auto setOpacity(this T)(double opacity)
164 	{
165 		assert(opacity >= 0 && opacity <= 1, "Opacity must be between 0.0 and 1.0.");
166 
167 		this._tk.eval("wm attributes %s -alpha %s", this.id, opacity);
168 
169 		return cast(T) this;
170 	}
171 
172 	/**
173 	 * Handle setting the window to fullscreen.
174 	 *
175 	 * Params:
176 	 *     fullscreen = A boolean specifying if the window should be fullscreen or not.
177 	 *
178 	 * Returns:
179 	 *     This window to aid method chaining.
180 	 */
181 	public auto setFullscreen(this T)(bool fullscreen)
182 	{
183 		this._tk.eval("wm attributes %s -fullscreen %s", this.id, fullscreen);
184 
185 		return cast(T) this;
186 	}
187 
188 	/**
189 	 * Handle setting the window to be the top-most. This makes the window not 
190 	 * able to be lowered behind any others.
191 	 *
192 	 * Params:
193 	 *     topmost = A boolean specifying if the window should be top-most or not.
194 	 *
195 	 * Returns:
196 	 *     This window to aid method chaining.
197 	 */
198 	public auto setTopmost(this T)(bool topmost)
199 	{
200 		this._tk.eval("wm attributes %s -topmost %s", this.id, topmost);
201 
202 		return cast(T) this;
203 	}
204 
205 	version (Windows)
206 	{
207 		/**
208 		 * Handle disabling the window. Windows only.
209 		 *
210 		 * Params:
211 		 *     disabled = A boolean specifying if the window should be disabled or not.
212 		 *
213 		 * Returns:
214 		 *     This window to aid method chaining.
215 		 */
216 		public auto setDisabled(this T)(bool disabled)
217 		{
218 			this._tk.eval("wm attributes %s -disabled %s", this.id, disabled);
219 
220 			return cast(T) this;
221 		}
222 
223 		/**
224 		 * Handle changing the window to a tool window. Windows only.
225 		 *
226 		 * Params:
227 		 *     toolWindow = A boolean specifying if the window should be a tool window or not.
228 		 *
229 		 * Returns:
230 		 *     This window to aid method chaining.
231 		 */
232 		public auto setToolWindow(this T)(bool toolWindow)
233 		{
234 			this._tk.eval("wm attributes %s -toolwindow %s", this.id, toolWindow);
235 
236 			return cast(T) this;
237 		}
238 	}
239 
240 	version (OSX)
241 	{
242 		/**
243 		 * Set the modified state of the window. Mac OSX only.
244 		 *
245 		 * Params:
246 		 *     modified = A boolean specifying if the window should show it's been modified or not.
247 		 *
248 		 * Returns:
249 		 *     This window to aid method chaining.
250 		 */
251 		public auto setModified(this T)(bool modified)
252 		{
253 			this._tk.eval("wm attributes %s -modified %s", this.id, modified);
254 
255 			return cast(T) this;
256 		}
257 
258 		/**
259 		 * Set the notify state of the window. On Mac OS it usually bounces the 
260 		 * dock icon. Mac OSX only.
261 		 *
262 		 * Params:
263 		 *     modified = A boolean specifying if the window should show a notification or not.
264 		 *
265 		 * Returns:
266 		 *     This window to aid method chaining.
267 		 */
268 		public auto setNotify(this T)(bool modified)
269 		{
270 			this._tk.eval("wm attributes %s -notify %s", this.id, modified);
271 
272 			return cast(T) this;
273 		}
274 	}
275 
276 	/**
277 	 * Restore the window's state to before it was minimised or withdrawn.
278 	 *
279 	 * Returns:
280 	 *     This window to aid method chaining.
281 	 */
282 	public auto deiconify(this T)()
283 	{
284 		this._tk.eval("wm deiconify %s", this.id);
285 
286 		return cast(T) this;
287 	}
288 
289 	/**
290 	 * Minimise the window.
291 	 *
292 	 * Returns:
293 	 *     This window to aid method chaining.
294 	 */
295 	public auto iconify(this T)()
296 	{
297 		this._tk.eval("wm iconify %s", this.id);
298 
299 		return cast(T) this;
300 	}
301 
302 	/**
303 	 * Set the size and postition of the window.
304 	 *
305 	 * Params:
306 	 *     width = The width of the window.
307 	 *     height = The height of the window.
308 	 *     xPos = The horizontal position of the window.
309 	 *     yPos = The vertical position of the window.
310 	 *
311 	 * Returns:
312 	 *     This window to aid method chaining.
313 	 */
314 	public auto setGeometry(this T)(int width, int height, int xPos, int yPos)
315 	{
316 		this._tk.eval("wm geometry %s %sx%s+%s+%s", this.id, width, height, xPos, yPos);
317 		
318 		return cast(T) this;
319 	}
320 
321 	/**
322 	 * Set the default icon for this window. This is applied to all future 
323 	 * child windows as well.
324 	 *
325 	 * The data in the images is taken as a snapshot at the time of invocation. 
326 	 * If the images are later changed, this is not reflected to the titlebar 
327 	 * icons. Multiple images are accepted to allow different images sizes 
328 	 * (e.g., 16x16 and 32x32) to be provided. The window manager may scale 
329 	 * provided icons to an appropriate size.
330 	 *
331 	 * Params:
332 	 *     images = A variadic list of images.
333 	 *
334 	 * Returns:
335 	 *     This window to aid method chaining.
336 	 */
337 	public auto setDefaultIcon(this T)(Image[] images ...)
338 	{
339 		string defaultImages;
340 
341 		foreach (image; images)
342 		{
343 			defaultImages ~= format("%s ", image.id);
344 		}
345 
346 		this._tk.eval("wm iconphoto %s -default %s", this.id, defaultImages);
347 		
348 		return cast(T) this;
349 	}
350 
351 	/**
352 	 * Set the icon for this window, this overrides the default icon.
353 	 *
354 	 * Params:
355 	 *     images = A variadic list of images.
356 	 *
357 	 * Returns:
358 	 *     This window to aid method chaining.
359 	 */
360 	public auto setIcon(this T)(Image[] images ...)
361 	{
362 		string defaultImages;
363 
364 		foreach (image; images)
365 		{
366 			defaultImages ~= format("%s ", image.id);
367 		}
368 
369 		this._tk.eval("wm iconphoto %s %s", this.id, defaultImages);
370 		
371 		return cast(T) this;
372 	}
373 
374 	/**
375 	 * Set the maximum size of the window.
376 	 *
377 	 * Params:
378 	 *     width = The maximum width of the window.
379 	 *     height = The maximum height of the window.
380 	 *
381 	 * Returns:
382 	 *     This window to aid method chaining.
383 	 */
384 	public auto setMaxSize(this T)(int width, int height)
385 	{
386 		this._tk.eval("wm maxsize %s %s %s", this.id, width, height);
387 
388 		return cast(T) this;
389 	}
390 
391 	/**
392 	 * Set the minimum size of the window.
393 	 *
394 	 * Params:
395 	 *     width = The minimum width of the window.
396 	 *     height = The minimum height of the window.
397 	 *
398 	 * Returns:
399 	 *     This window to aid method chaining.
400 	 */
401 	public auto setMinSize(this T)(int width, int height)
402 	{
403 		this._tk.eval("wm minsize %s %s %s", this.id, width, height);
404 
405 		return cast(T) this;
406 	}
407 
408 	/**
409 	 * This command is used to manage window manager protocols such as 
410 	 * WM_DELETE_WINDOW. Protocol is the name of an atom corresponding to a 
411 	 * window manager protocol, such as WM_DELETE_WINDOW or WM_SAVE_YOURSELF or 
412 	 * WM_TAKE_FOCUS.
413 	 *
414 	 * Params:
415 	 *     protocol = The protocol to respond to.
416 	 *     callback = The callback to invoke when the protocol is encountered.
417 	 *
418 	 * Returns:
419 	 *     This window to aid method chaining.
420 	 *
421 	 * Callback_Arguments:
422 	 *     These are the fields within the callback's $(LINK2 
423 	 *     ../element/element.html#CommandArgs, CommandArgs) parameter which 
424 	 *     are populated by this method when the callback is executed. 
425 	 *     $(P
426 	 *         $(PARAM_TABLE
427 	 *             $(PARAM_ROW CommandArgs.element, The window that executed the callback.)
428 	 *             $(PARAM_ROW CommandArgs.uniqueData, The protocol that was responded to.)
429 	 *             $(PARAM_ROW CommandArgs.callback, The callback which was executed.)
430 	 *         )
431 	 *     )
432 	 *
433 	 * See_Also:
434 	 *     $(LINK2 ../element/element.html#CommandCallback, tkd.element.element.CommandCallback) $(BR)
435 	 *     $(LINK2 ../tkdapplication.html#WindowProtocol, tkd.tkdapplication.WindowProtocol) $(BR)
436 	 */
437 	public auto setProtocolCommand(this T)(string protocol, CommandCallback callback)
438 	{
439 		string command = this.createCommand(callback, protocol);
440 		this._tk.eval("wm protocol %s %s %s", this.id, protocol, command);
441 
442 		return cast(T) this;
443 	}
444 
445 	/**
446 	 * Remove a previously set protocol command.
447 	 *
448 	 * Params:
449 	 *     protocol = The protocol which will have the command removed.
450 	 *
451 	 * Returns:
452 	 *     This window to aid method chaining.
453 	 *
454 	 * See_Also:
455 	 *     $(LINK2 ../tkdapplication.html#WindowProtocol, tkd.tkdapplication.WindowProtocol)
456 	 */
457 	public auto removeProtocolCommand(this T)(string protocol)
458 	{
459 		this._tk.deleteCommand(this.getCommandName(protocol));
460 		this._tk.eval("wm protocol %s %s {}", this.id, protocol);
461 
462 		return cast(T) this;
463 	}
464 
465 	/**
466 	 * Set if the width and height can be resized.
467 	 *
468 	 * Params:
469 	 *     resizeWidth = True to allow width resizing, false to disable.
470 	 *     resizeHeight = True to allow height resizing, false to disable.
471 	 *
472 	 * Returns:
473 	 *     This window to aid method chaining.
474 	 */
475 	public auto setResizable(this T)(bool resizeWidth, bool resizeHeight)
476 	{
477 		this._tk.eval("wm resizable %s %s %s", this.id, resizeWidth, resizeHeight);
478 
479 		return cast(T) this;
480 	}
481 
482 	/**
483 	 * Determine if this window is above another.
484 	 *
485 	 * Params:
486 	 *     other = The other window to check this one is above.
487 	 *
488 	 * Returns:
489 	 *     true if this window is above other, false if not.
490 	 */
491 	public bool isAbove(Window other)
492 	{
493 		this._tk.eval("wm stackorder %s isabove %s", this.id, other.id);
494 
495 		return this._tk.getResult!(int) == 1;
496 	}
497 
498 	/**
499 	 * Determine if this window is below another.
500 	 *
501 	 * Params:
502 	 *     other = The other window to check this one is below.
503 	 *
504 	 * Returns:
505 	 *     true if this window is below other, false if not.
506 	 */
507 	public bool isBelow(Window other)
508 	{
509 		this._tk.eval("wm stackorder %s isbelow %s", this.id, other.id);
510 
511 		return this._tk.getResult!(int) == 1;
512 	}
513 
514 	/**
515 	 * Withdraw a window from being displayed/mapped by the window manager.
516 	 *
517 	 * Returns:
518 	 *     This window to aid method chaining.
519 	 */
520 	public auto withdraw(this T)()
521 	{
522 		this._tk.eval("wm withdraw %s", this.id);
523 
524 		return cast(T) this;
525 	}
526 
527 	/**
528 	 * Wait until this window has been destroyed.
529 	 *
530 	 * Returns:
531 	 *     This window to aid method chaining.
532 	 */
533 	public auto wait(this T)()
534 	{
535 		this._tk.eval("tkwait window %s", this.id);
536 
537 		return cast(T) this;
538 	}
539 
540 	/**
541 	 * Set a callback to be executed after a delay and after processing all 
542 	 * other events. The callback is executed only once and discarded. This is 
543 	 * useful for refreshing the GUI at regular intervals when monitoring 
544 	 * something or to schedule a future action.
545 	 *
546 	 * Params:
547 	 *     callback = The delegate function to be executed on idle.
548 	 *     msDelay  = The delay in millisecond before executing the given callback.
549 	 *
550 	 * Returns:
551 	 *     This window to aid method chaining.
552 	 *
553 	 * Example:
554 	 * ----
555 	 * InputStream stream = ...; // A data provider.
556 	 *
557 	 * this.mainWindow.setIdleCommand(delegate(CommandArgs args){
558 	 *
559 	 * 	// Use data.
560 	 * 	if (!stream.empty && stream.finished)
561 	 * 	{
562 	 * 		performStep(stream.next);
563 	 * 	}
564 	 *
565 	 * 	// Re-arm this callback and wait for more data.
566 	 * 	this.mainWindow.setIdleCommand(args.callback, 10_000);
567 	 *
568 	 * }, 10_000);
569 	 *
570 	 * ----
571 	 * Caveats:
572 	 *     The callback executed by this method is not asynchronous and could 
573 	 *     halt the GUI from processing events if it takes a long time to 
574 	 *     finish.
575 	 */
576 	public auto setIdleCommand(this T)(CommandCallback callback, int msDelay = 1)
577 	{
578 		assert(msDelay > 0, "The delay in milliseconds should be greater than zero.");
579 
580 		string command = this.createCommand(callback, "idle");
581 		this._tk.eval("after idle [list after %s %s]", msDelay.to!(string), command);
582 
583 		return cast(T) this;
584 	}
585 
586 	/**
587 	 * Get the main window of the application. This is just a convenience 
588 	 * method and shouldn't really be used. Instead use the $(LINK2 
589 	 * ../tkdapplication.html#TkdApplication.mainWindow, mainWindow) property 
590 	 * of the $(LINK2 ../tkdapplication.html, TkdApplication) class.
591 	 *
592 	 * Returns:
593 	 *     A reference to the main window of the application.
594 	 *
595 	 * See_Also:
596 	 *     $(LINK2 ../tkdapplication.html, tkd.tkdapplication)
597 	 */
598 	static public Window getMainWindow()
599 	{
600 		if (this._mainWindow is null)
601 		{
602 			this._mainWindow = new Window();
603 		}
604 
605 		return this._mainWindow;
606 	}
607 
608 	/**
609 	 * Mixin common commands.
610 	 */
611 	mixin Color;
612 }
613 
614 /**
615  * Window manager protocols.
616  *
617  * Caveats:
618  *     This list is incomplete.
619  */
620 enum WindowProtocol : string
621 {
622 	deleteWindow = "WM_DELETE_WINDOW", /// Issued when the window is to be deleted.
623 	saveYourself = "WM_SAVE_YOURSELF", /// Issued when the window is required to save itself.
624 	takeFocus    = "WM_TAKE_FOCUS",    /// Issued then the window is focused.
625 }