1 /*
2  * Copyright (C) 2015-2017, by Laszlo Szeremi under the Boost license.
3  *
4  * Pixel Perfect Engine, concrete.window module
5  */
6 
7 module PixelPerfectEngine.concrete.window;
8 
9 import PixelPerfectEngine.graphics.bitmap;
10 import PixelPerfectEngine.graphics.draw;
11 import PixelPerfectEngine.graphics.layers;
12 
13 public import PixelPerfectEngine.concrete.elements;
14 public import PixelPerfectEngine.concrete.types;
15 public import PixelPerfectEngine.concrete.interfaces;
16 public import PixelPerfectEngine.concrete.windowhandler;
17 
18 import PixelPerfectEngine.system.etc;
19 import PixelPerfectEngine.system.input.interfaces;
20 
21 import std.algorithm.mutation;
22 import std.stdio;
23 import std.conv;
24 import std.file;
25 import std.path;
26 import std.datetime;
27 
28 import collections.linkedlist;
29 
30 /**
31  * Basic window. All other windows are inherited from this class.
32  */
33 public class Window : ElementContainer, Focusable, MouseEventReceptor {
34 	alias FocusableSet = LinkedList!(Focusable, false, "a is b");//alias FocusableSet = LinkedList!(Focusable, false, "cmpObjPtr(a, b)");
35 	alias WESet = LinkedList!(WindowElement, false, "a is b");//alias WESet = LinkedList!(WindowElement, false, "cmpObjPtr(a, b)");
36 	alias SBSet = LinkedList!(ISmallButton, false, "a is b");//alias SBSet = LinkedList!(ISmallButton, false, "cmpObjPtr(a, b)");
37 	alias CWSet = LinkedList!(Window, false, "a is b");//alias CWSet = LinkedList!(Window, false, "cmpObjPtr(a, b)");
38 	protected FocusableSet			focusables;		///All focusable objects belonging to the window
39 	protected WESet			 		elements;		///Stores all window elements here
40 	protected Text					title;			///Title of the window
41 	protected WindowElement 		lastMouseEventTarget;	///Used for mouse move and wheel events
42 	protected sizediff_t			focusedElement; ///The index of the currently focused element, or -1 if none
43 	public WindowHandler 			handler;		///The handler of the window
44 	//public Bitmap16Bit[int] altStyleBrush;
45 	protected BitmapDrawer 			output;			///Graphics output of the window
46 	//public int header;//, sizeX, sizeY;
47 	protected int 					moveX, moveY;	///Relative x and y coordinates for drag events
48 	protected uint					flags;			///Stores various flags
49 	protected static enum IS_ACTIVE = 1 << 0;
50 	protected static enum NEEDS_FULL_UPDATE = 1 << 1;
51 	protected static enum HEADER_UPDATE = 1 << 2;
52 	protected static enum IS_MOVED = 1 << 3;
53 	protected static enum IS_RESIZED = 1 << 4;
54 	protected static enum IS_RESIZED_L = 1 << 5;
55 	protected static enum IS_RESIZED_T = 1 << 6;
56 	protected static enum IS_RESIZED_B = 1 << 7;
57 	protected static enum IS_RESIZED_R = 1 << 8;
58 	protected static enum IS_RESIZABLE_BY_MOUSE = 1 << 9;
59 	//protected bool 					fullUpdate;		///True if window needs full redraw
60 	//protected bool 					isActive;		///True if window is currently active
61 	//protected bool 					headerUpdate;	///True if needs header update
62 	protected Point					lastMousePos;	///Stores the last mouse position.
63 	protected SBSet					smallButtons;	///Contains the icons of the extra buttons. Might be replaced with a WindowElement in the future
64 	protected Box	 				position;		///Position of the window
65 	public StyleSheet 				customStyle;	///Custom stylesheet for this window
66 	protected CWSet					children;		///Stores child windows
67 	protected Window				parent;			///Stores reference to the parent
68 	public void delegate()			onClose;		///Called when the window is closed
69 	public static void delegate() 	onDrawUpdate;	///Called if not null after every draw update
70 	/**
71 	 * Custom constructor. "size" sets both the initial position and the size of the window.
72 	 * Buttons in the header can be set through the `smallButtons` parameter
73 	 */
74 	public this(Box size, Text title, ISmallButton[] smallButtons, StyleSheet customStyle = null) {
75 		position = size;
76 		output = new BitmapDrawer(position.width, position.height);
77 		this.title = title;
78 		this.customStyle = customStyle;
79 		foreach (key; smallButtons) {
80 			addHeaderButton(key);
81 		}
82 		focusedElement = -1;
83 	}
84 	///Ditto
85 	public this(Box size, dstring title, ISmallButton[] smallButtons, StyleSheet customStyle = null) {
86 		this(size, new Text(title, getStyleSheet().getChrFormatting("windowHeader")), smallButtons, customStyle);
87 	}
88 	/**
89 	 * Default constructor. "size" sets both the initial position and the size of the window.
90 	 * Adds a close button to the header.
91 	 */
92 	public this(Box size, Text title, StyleSheet customStyle = null) {
93 		position = size;
94 		output = new BitmapDrawer(position.width(), position.height());
95 		this.title = title;
96 		this.customStyle = customStyle;
97 		SmallButton closeButton = closeButton(customStyle is null ? globalDefaultStyle : customStyle);
98 		closeButton.onMouseLClick = &close;
99 		addHeaderButton(closeButton);
100 		focusedElement = -1;
101 	}
102 	///Ditto
103 	public this(Box size, dstring title, StyleSheet customStyle = null) {
104 		this(size, new Text(title, getStyleSheet().getChrFormatting("windowHeader")), customStyle);
105 	}
106 	/**
107 	 * Returns the window's position.
108 	 */
109 	public Box getPosition() @nogc @safe pure nothrow const {
110 		return position;
111 	}
112 	/**
113 	 * Sets the new position for the window.
114 	 */
115 	public Box setPosition(Box newPos) {
116 		position = newPos;
117 		if (output.output.width != position.width || output.output.height != position.height) {
118 			output = new BitmapDrawer(position.width, position.height);
119 			draw();
120 		}
121 		return position;
122 	}
123 	/**
124 	 * If the current window doesn't contain a custom StyleSheet, it gets from it's parent.
125 	 */
126 	public StyleSheet getStyleSheet() @safe {
127 		if (customStyle is null) {
128 			if (parent is null) return globalDefaultStyle;
129 			else return parent.getStyleSheet();
130 		} else {
131 			return customStyle;
132 		}
133 	}
134 	/**
135 	 * Adds an element to the window.
136 	 */
137 	public void addElement(WindowElement we) {
138 		we.setParent(this);
139 		elements.put(we);
140 		focusables.put(we);
141 		we.draw();
142 	}
143 	/**
144 	 * Removes the WindowElement if 'we' is found within its ranges, does nothing otherwise.
145 	 */
146 	public void removeElement(WindowElement we) {
147 		synchronized {
148 			//we.setParent(null);
149 			elements.removeByElem(we);
150 			focusables.removeByElem(we);
151 		}
152 		draw();
153 	}
154 	/**
155 	 * Adds a smallbutton to the header.
156 	 */
157 	public void addHeaderButton(ISmallButton sb) {
158 		const int headerHeight = getStyleSheet().drawParameters["WindowHeaderHeight"];
159 		if (!sb.isSmallButtonHeight(headerHeight)) throw new Exception("Wrong SmallButton height.");
160 		
161 		int left, right = position.width;
162 		foreach (ISmallButton key; smallButtons) {
163 			if (key.isLeftSide) left += headerHeight;
164 			else right -= headerHeight;
165 		}
166 		Box b;
167 		if (sb.isLeftSide) 
168 			b = Box(left, 0, left + headerHeight, headerHeight);
169 		else
170 			b = Box(right - headerHeight, 0, right, headerHeight);
171 		WindowElement we = cast(WindowElement)sb;
172 		we.setParent(this);
173 		we.setPosition(b);
174 
175 		smallButtons.put(sb);
176 	}
177 	/**
178 	 * Removes a smallbutton from the header.
179 	 */
180 	public void removeHeaderButton(ISmallButton sb) {
181 		smallButtons.removeByElem(sb);
182 		elements.removeByElem(cast(WindowElement)sb);
183 		drawHeader();
184 	}
185 	/**
186 	 * Draws the window. Intended to be used by the WindowHandler.
187 	 */
188 	public void draw(bool drawHeaderOnly = false) {
189 		if(output.output.width != position.width || output.output.height != position.height) {
190 			output = new BitmapDrawer(position.width(), position.height());
191 			handler.refreshWindow(this);
192 		}
193 		
194 		//drawing the header
195 		drawHeader();
196 		if(drawHeaderOnly)
197 			return;
198 		StyleSheet ss = getStyleSheet();
199 		const Box bodyarea = Box(0, ss.drawParameters["WindowHeaderHeight"], position.width - 1, position.height - 1);
200 		drawFilledBox(bodyarea, ss.getColor("window"));
201 		drawLine(bodyarea.cornerUL, bodyarea.cornerLL, ss.getColor("windowascent"));
202 		drawLine(bodyarea.cornerUL, bodyarea.cornerUR, ss.getColor("windowascent"));
203 		drawLine(bodyarea.cornerLL, bodyarea.cornerLR, ss.getColor("windowdescent"));
204 		drawLine(bodyarea.cornerUR, bodyarea.cornerLR, ss.getColor("windowdescent"));
205 
206 		foreach (WindowElement we; elements) {
207 			we.draw();
208 		}
209 		
210 	}
211 	/**
212 	 * Draws the header.
213 	 */
214 	protected void drawHeader() {
215 		StyleSheet ss = getStyleSheet();
216 		const int headerHeight = ss.drawParameters["WindowHeaderHeight"];
217 		Box headerArea = Box(0, 0, position.width - 1, headerHeight - 1);
218 
219 		foreach (ISmallButton sb; smallButtons) {
220 			if (sb.isLeftSide) headerArea.left += headerHeight;
221 			else headerArea.right -= headerHeight;
222 			WindowElement we = cast(WindowElement)sb;
223 			we.draw;
224 		}
225 		
226 		if (active) {
227 			drawFilledBox(headerArea, ss.getColor("WHAtop"));
228 			drawLine(headerArea.cornerUL, headerArea.cornerLL, ss.getColor("WHAascent"));
229 			drawLine(headerArea.cornerUL, headerArea.cornerUR, ss.getColor("WHAascent"));
230 			drawLine(headerArea.cornerLL, headerArea.cornerLR, ss.getColor("WHAdescent"));
231 			drawLine(headerArea.cornerUR, headerArea.cornerLR, ss.getColor("WHAdescent"));
232 		} else {
233 			drawFilledBox(headerArea, ss.getColor("window"));
234 			drawLine(headerArea.cornerUL, headerArea.cornerLL, ss.getColor("windowascent"));
235 			drawLine(headerArea.cornerUL, headerArea.cornerUR, ss.getColor("windowascent"));
236 			drawLine(headerArea.cornerLL, headerArea.cornerLR, ss.getColor("windowdescent"));
237 			drawLine(headerArea.cornerUR, headerArea.cornerLR, ss.getColor("windowdescent"));
238 		}
239 		drawTextSL(headerArea, title, Point(0,0));
240 	}
241 	///Returns true if the window is focused
242 	public @property bool active() @safe @nogc pure nothrow {
243 		return flags & IS_ACTIVE;
244 	}
245 	///Sets the IS_ACTIVE flag to the given value
246 	protected @property bool active(bool val) @safe {
247 		if (val) {
248 			flags |= IS_ACTIVE;
249 			title.formatting = getStyleSheet().getChrFormatting("windowHeader");
250 		} else {
251 			flags &= ~IS_ACTIVE;
252 			title.formatting = getStyleSheet().getChrFormatting("windowHeaderInactive");
253 		}
254 		return active();
255 	}
256 	///Returns whether the window is moved or not
257 	public @property bool isMoved() @safe @nogc pure nothrow {
258 		return flags & IS_MOVED ? true : false;
259 	}
260 	///Sets whether the window is moved or not
261 	public @property bool isMoved(bool val) @safe @nogc pure nothrow {
262 		if (val) flags |= IS_MOVED;
263 		else flags &= ~IS_MOVED;
264 		return isMoved();
265 	}
266 	///Sets the title of the window
267 	public void setTitle(Text s) @trusted {
268 		title = s;
269 		drawHeader();
270 	}
271 	///Ditto
272 	public void setTitle(dstring s) @trusted {
273 		title.text = s;
274 		drawHeader();
275 	}
276 	///Returns the title of the window
277 	public Text getTitle() @safe @nogc pure nothrow {
278 		return title;
279 	}
280 	/**
281 	 * Closes the window by calling the WindowHandler's closeWindow function.
282 	 */
283 	public void close(Event ev) {
284 		close();
285 	}
286 	///Ditto
287 	public void close() {
288 		if (onClose !is null) onClose();
289 		if (parent !is null) parent.removeChildWindow(this);
290 		handler.closeWindow(this);
291 	}
292 	/**
293 	 * Adds a WindowHandler to the window.
294 	 */
295 	public void addHandler(WindowHandler wh) @nogc @safe pure nothrow {
296 		handler = wh;
297 	}
298 	
299 	public Coordinate getAbsolutePosition(WindowElement sender) {
300 		Box p = sender.getPosition();
301 		p.relMove(position.left, position.top);
302 		return p;
303 	}
304 	/**
305 	 * Moves the window to the exact location.
306 	 */
307 	public void move(const int x, const int y) {
308 		position.move(x,y);
309 		handler.updateWindowCoord(this);
310 	}
311 	/**
312 	 * Moves the window by the given values.
313 	 */
314 	public void relMove(const int x, const int y) {
315 		position.relMove(x,y);
316 		handler.updateWindowCoord(this);
317 	}
318 	/**
319 	 * Sets the size of the window, also issues a redraw.
320 	 */
321 	public void resize(const int width, const int height) {
322 		position.right = position.left + width;
323 		position.bottom = position.top + height;
324 		draw();
325 		handler.refreshWindow(this);
326 	}
327 	/**
328 	 * Returns the outputted bitmap.
329 	 * Can be overridden for 32 bit outputs.
330 	 */
331 	public @property ABitmap getOutput() @nogc @safe pure nothrow {
332 		return output.output;
333 	}
334 	/**
335 	 * Gives focus to the windowelement requesting it.
336 	 */
337 	public void requestFocus(WindowElement sender) {
338 		if (focusables.has(sender)) {
339 			try {
340 				if (focusedElement != -1)
341 					focusables[focusedElement].focusTaken();
342 				Focusable f = cast(Focusable)(sender);
343 				focusedElement = focusables.which(f);
344 				focusables[focusedElement].focusGiven();
345 			} catch (Exception e) {
346 				debug writeln(e);
347 			}
348 		}
349 	}
350 	/**
351 	 * Sets the cursor to the given type on request.
352 	 */
353 	public void requestCursor(CursorType type) {
354 		handler.setCursor(type);
355 	}
356 	/**
357 	 * Adds a child window to the current window.
358 	 */
359 	public void addChildWindow(Window w) {
360 		children.put(w);
361 		w.parent = this;
362 		children.setAsFirst(children.length);
363 		handler.addWindow(w);
364 	}
365 	/**
366 	 * Removes a child window from the current window.
367 	 */
368 	public void removeChildWindow(Window w) {
369 		if (children.removeByElem(w)) {
370 			w.parent = null;
371 			handler.closeWindow(w);
372 		}
373 		
374 	}
375 	/**
376 	 * Returns the child windows.
377 	 */
378 	public CWSet getChildWindows() {
379 		return children;
380 	}
381 	//Implementation of the `Canvas` interface starts here.
382 	///Draws a line.
383 	public void drawLine(Point from, Point to, ubyte color) @trusted {
384 		output.drawLine(from, to, color);
385 	}
386 	///Draws a line pattern.
387 	public void drawLinePattern(Point from, Point to, ubyte[] pattern) @trusted {
388 		output.drawLinePattern(from, to, pattern);
389 	}
390 	///Draws an empty rectangle.
391 	public void drawBox(Coordinate target, ubyte color) @trusted {
392 		output.drawBox(target, color);
393 	}
394 	///Draws an empty rectangle with line patterns.
395 	public void drawBoxPattern(Coordinate target, ubyte[] pattern) @trusted {
396 		output.drawBox(target, pattern);
397 	}
398 	///Draws a filled rectangle with a specified color,
399 	public void drawFilledBox(Coordinate target, ubyte color) @trusted {
400 		output.drawFilledBox(target, color);
401 	}
402 	///Pastes a bitmap to the given point using blitter, which threats color #0 as transparency.
403 	public void bitBLT(Point target, ABitmap source) @trusted {
404 		output.bitBLT(target, cast(Bitmap8Bit)source);
405 	}
406 	///Pastes a slice of a bitmap to the given point using blitter, which threats color #0 as transparency.
407 	public void bitBLT(Point target, ABitmap source, Coordinate slice) @trusted {
408 		output.bitBLT(target, cast(Bitmap8Bit)source, slice);
409 	}
410 	///Pastes a repeated bitmap pattern over the specified area.
411 	public void bitBLTPattern(Coordinate target, ABitmap pattern) @trusted {
412 		output.bitBLTPattern(target, cast(Bitmap8Bit)pattern);
413 	}
414 	///XOR blits a repeated bitmap pattern over the specified area.
415 	public void xorBitBLT(Coordinate target, ABitmap pattern) @trusted {
416 		output.xorBitBLT(target, cast(Bitmap8Bit)pattern);
417 	}
418 	///XOR blits a color index over a specified area.
419 	public void xorBitBLT(Coordinate target, ubyte color) @trusted {
420 		output.xorBitBLT(target, color);
421 	}
422 	///Fills an area with the specified color.
423 	public void fill(Point target, ubyte color, ubyte background = 0) @trusted {
424 
425 	}
426 	///Draws a single line text within the given prelimiter.
427 	public void drawTextSL(Coordinate target, Text text, Point offset) @trusted {
428 		output.drawSingleLineText(target, text, offset.x, offset.y);
429 	}
430 	///Draws a multi line text within the given prelimiter.
431 	public void drawTextML(Coordinate target, Text text, Point offset) @trusted {
432 		output.drawMultiLineText(target, text, offset.x, offset.y);
433 	}
434 	///Clears the area within the target
435 	public void clearArea(Coordinate target) @trusted {
436 		output.drawFilledBox(target, getStyleSheet.getColor("window"));
437 	}
438 	//Implementation of the `Focusable` interface:
439 	///Called when an object receives focus.
440 	public void focusGiven() {
441 		active = true;
442 		draw;
443 	}
444 	///Called when an object loses focus.
445 	public void focusTaken() {
446 		if (focusedElement != -1) {
447 			focusables[focusedElement].focusTaken;
448 			focusedElement = -1;
449 		}
450 		if (lastMouseEventTarget) {
451 			lastMouseEventTarget.focusTaken();
452 			lastMouseEventTarget = null;
453 		}
454 		active = false;
455 		draw;
456 	}
457 	///Cycles the focus on a single element.
458 	///Returns -1 if end is reached, or the number of remaining elements that
459 	///are cycleable in the direction.
460 	public int cycleFocus(int direction) {
461 		if (focusedElement < focusables.length && focusedElement >= 0) {
462 			if (focusables[focusedElement].cycleFocus(direction) == -1) {
463 				focusables[focusedElement].focusTaken;
464 				focusedElement += direction;
465 				focusables[focusedElement].focusGiven;
466 			}
467 		} else if (focusedElement == -1) {
468 			focusedElement = 0;
469 			focusables[focusedElement].focusGiven;
470 		} else return -1;
471 		if (direction > 1) return cast(int)(focusables.length - focusedElement);
472 		else return cast(int)focusedElement;
473 	}
474 	///Passes key events to the focused element when not in text editing mode.
475 	public void passKey(uint keyCode, ubyte mod) {
476 		if (focusedElement != -1) {
477 			focusables[focusedElement].passKey(keyCode, mod);
478 		}
479 	}
480 	//Implementation of `MouseEventReceptor` interface starts here
481 	///Passes mouse click event
482 	public void passMCE(MouseEventCommons mec, MouseClickEvent mce) {
483 		if (!isMoved) {
484 			lastMousePos = Point(mce.x - position.left, mce.y - position.top);
485 			foreach (WindowElement we; elements) {
486 				if (we.getPosition.isBetween(lastMousePos)) {
487 					lastMouseEventTarget = we;
488 					mce.x = lastMousePos.x;
489 					mce.y = lastMousePos.y;
490 					we.passMCE(mec, mce);
491 					return;
492 				}
493 			}
494 			foreach (ISmallButton sb; smallButtons) {
495 				WindowElement we = cast(WindowElement)sb;
496 				if (we.getPosition.isBetween(lastMousePos)) {
497 					lastMouseEventTarget = we;
498 					mce.x = lastMousePos.x;
499 					mce.y = lastMousePos.y;
500 					we.passMCE(mec, mce);
501 					return;
502 				}
503 			}
504 			const int headerHeight = getStyleSheet().drawParameters["WindowHeaderHeight"];
505 			if (lastMousePos.y < headerHeight) {
506 				isMoved = true;
507 				handler.initDragEvent(this);
508 			}
509 			lastMouseEventTarget = null;
510 		} else if (!mce.state) {
511 			isMoved = false;
512 		}
513 	}
514 	///Passes mouse move event
515 	public void passMME(MouseEventCommons mec, MouseMotionEvent mme) {
516 		lastMousePos = Point(mme.x - position.left, mme.y - position.top);
517 		if (isMoved) {
518 			if (mme.buttonState)
519 				relMove(mme.relX, mme.relY);
520 			else
521 				isMoved = false;
522 		} else if (lastMouseEventTarget) {
523 			mme.x = lastMousePos.x;
524 			mme.y = lastMousePos.y;
525 			lastMouseEventTarget.passMME(mec, mme);
526 			if (!lastMouseEventTarget.getPosition.isBetween(mme.x, mme.y)) {
527 				lastMouseEventTarget = null;
528 			}
529 		} else {
530 			foreach (WindowElement we; elements) {
531 				if (we.getPosition.isBetween(lastMousePos)) {
532 					lastMouseEventTarget = we;
533 					mme.x = lastMousePos.x;
534 					mme.y = lastMousePos.y;
535 					we.passMME(mec, mme);
536 					return;
537 				}
538 			}
539 		}
540 	}
541 	///Passes mouse scroll event
542 	public void passMWE(MouseEventCommons mec, MouseWheelEvent mwe) {
543 		if (lastMouseEventTarget) {
544 			lastMouseEventTarget.passMWE(mec, mwe);
545 		}
546 	}
547 	/**
548 	 * Puts a PopUpElement on the GUI.
549 	 */
550 	public void addPopUpElement(PopUpElement p) {
551 		handler.addPopUpElement(p);
552 	}
553 	/**
554 	 * Puts a PopUpElement on the GUI at the given position.
555 	 */
556 	public void addPopUpElement(PopUpElement p, int x, int y) {
557 		handler.addPopUpElement(p, x, y);
558 	}
559 	/** 
560 	 * Ends the popup session and closes all popups.
561 	 */
562 	public void endPopUpSession(PopUpElement p) {
563 		handler.endPopUpSession(p);
564 	}
565 	/**
566 	 * Closes a single popup element.
567 	 */
568 	public void closePopUp(PopUpElement p) {
569 		handler.closePopUp(p);
570 	}
571 	///Generates a generic close button
572 	public static SmallButton closeButton(StyleSheet ss = globalDefaultStyle) {
573 		const int windowHeaderHeight = ss.drawParameters["WindowHeaderHeight"];
574 		SmallButton sb = new SmallButton("closeButtonB", "closeButtonA", "close", 
575 				Box(0,0, windowHeaderHeight - 1, windowHeaderHeight - 1));
576 		sb.isLeftSide = true;
577 		if (ss !is globalDefaultStyle)
578 			sb.customStyle = ss;
579 		return sb;
580 	}
581 }
582