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