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 		if (onDrawUpdate !is null)
122 			onDrawUpdate();
123 		return position;
124 	}
125 	/**
126 	 * If the current window doesn't contain a custom StyleSheet, it gets from it's parent.
127 	 */
128 	public StyleSheet getStyleSheet() @safe {
129 		if (customStyle is null) {
130 			if (parent is null) return globalDefaultStyle;
131 			else return parent.getStyleSheet();
132 		} else {
133 			return customStyle;
134 		}
135 	}
136 	/**
137 	 * Adds an element to the window.
138 	 */
139 	public void addElement(WindowElement we) {
140 		we.setParent(this);
141 		elements.put(we);
142 		focusables.put(we);
143 		we.draw();
144 	}
145 	/**
146 	 * Removes the WindowElement if 'we' is found within its ranges, does nothing otherwise.
147 	 */
148 	public void removeElement(WindowElement we) {
149 		synchronized {
150 			//we.setParent(null);
151 			elements.removeByElem(we);
152 			focusables.removeByElem(we);
153 		}
154 		draw();
155 	}
156 	/**
157 	 * Adds a smallbutton to the header.
158 	 */
159 	public void addHeaderButton(ISmallButton sb) {
160 		const int headerHeight = getStyleSheet().drawParameters["WindowHeaderHeight"];
161 		if (!sb.isSmallButtonHeight(headerHeight)) throw new Exception("Wrong SmallButton height.");
162 		
163 		int left, right = position.width;
164 		foreach (ISmallButton key; smallButtons) {
165 			if (key.isLeftSide) left += headerHeight;
166 			else right -= headerHeight;
167 		}
168 		Box b;
169 		if (sb.isLeftSide) 
170 			b = Box(left, 0, left + headerHeight, headerHeight);
171 		else
172 			b = Box(right - headerHeight, 0, right, headerHeight);
173 		WindowElement we = cast(WindowElement)sb;
174 		we.setParent(this);
175 		we.setPosition(b);
176 
177 		smallButtons.put(sb);
178 	}
179 	/**
180 	 * Removes a smallbutton from the header.
181 	 */
182 	public void removeHeaderButton(ISmallButton sb) {
183 		smallButtons.removeByElem(sb);
184 		elements.removeByElem(cast(WindowElement)sb);
185 		drawHeader();
186 	}
187 	/**
188 	 * Draws the window. Intended to be used by the WindowHandler.
189 	 */
190 	public void draw(bool drawHeaderOnly = false) {
191 		if(output.output.width != position.width || output.output.height != position.height) {
192 			output = new BitmapDrawer(position.width(), position.height());
193 			handler.refreshWindow(this);
194 		}
195 		
196 		//drawing the header
197 		drawHeader();
198 		if(drawHeaderOnly)
199 			return;
200 		StyleSheet ss = getStyleSheet();
201 		const Box bodyarea = Box(0, ss.drawParameters["WindowHeaderHeight"], position.width - 1, position.height - 1);
202 		drawFilledBox(bodyarea, ss.getColor("window"));
203 		drawLine(bodyarea.cornerUL, bodyarea.cornerLL, ss.getColor("windowascent"));
204 		drawLine(bodyarea.cornerUL, bodyarea.cornerUR, ss.getColor("windowascent"));
205 		drawLine(bodyarea.cornerLL, bodyarea.cornerLR, ss.getColor("windowdescent"));
206 		drawLine(bodyarea.cornerUR, bodyarea.cornerLR, ss.getColor("windowdescent"));
207 
208 		foreach (WindowElement we; elements) {
209 			we.draw();
210 		}
211 		if (onDrawUpdate !is null)
212 			onDrawUpdate();
213 	}
214 	/**
215 	 * Draws the header.
216 	 */
217 	protected void drawHeader() {
218 		StyleSheet ss = getStyleSheet();
219 		const int headerHeight = ss.drawParameters["WindowHeaderHeight"];
220 		Box headerArea = Box(0, 0, position.width - 1, headerHeight - 1);
221 
222 		foreach (ISmallButton sb; smallButtons) {
223 			if (sb.isLeftSide) headerArea.left += headerHeight;
224 			else headerArea.right -= headerHeight;
225 			WindowElement we = cast(WindowElement)sb;
226 			we.draw;
227 		}
228 		
229 		if (active) {
230 			drawFilledBox(headerArea, ss.getColor("WHAtop"));
231 			drawLine(headerArea.cornerUL, headerArea.cornerLL, ss.getColor("WHAascent"));
232 			drawLine(headerArea.cornerUL, headerArea.cornerUR, ss.getColor("WHAascent"));
233 			drawLine(headerArea.cornerLL, headerArea.cornerLR, ss.getColor("WHAdescent"));
234 			drawLine(headerArea.cornerUR, headerArea.cornerLR, ss.getColor("WHAdescent"));
235 		} else {
236 			drawFilledBox(headerArea, ss.getColor("window"));
237 			drawLine(headerArea.cornerUL, headerArea.cornerLL, ss.getColor("windowascent"));
238 			drawLine(headerArea.cornerUL, headerArea.cornerUR, ss.getColor("windowascent"));
239 			drawLine(headerArea.cornerLL, headerArea.cornerLR, ss.getColor("windowdescent"));
240 			drawLine(headerArea.cornerUR, headerArea.cornerLR, ss.getColor("windowdescent"));
241 		}
242 		drawTextSL(headerArea, title, Point(0,0));
243 	}
244 	///Returns true if the window is focused
245 	public @property bool active() @safe @nogc pure nothrow {
246 		return flags & IS_ACTIVE;
247 	}
248 	///Sets the IS_ACTIVE flag to the given value
249 	protected @property bool active(bool val) @safe {
250 		if (val) {
251 			flags |= IS_ACTIVE;
252 			title.formatting = getStyleSheet().getChrFormatting("windowHeader");
253 		} else {
254 			flags &= ~IS_ACTIVE;
255 			title.formatting = getStyleSheet().getChrFormatting("windowHeaderInactive");
256 		}
257 		return active();
258 	}
259 	///Returns whether the window is moved or not
260 	public @property bool isMoved() @safe @nogc pure nothrow {
261 		return flags & IS_MOVED ? true : false;
262 	}
263 	///Sets whether the window is moved or not
264 	public @property bool isMoved(bool val) @safe @nogc pure nothrow {
265 		if (val) flags |= IS_MOVED;
266 		else flags &= ~IS_MOVED;
267 		return isMoved();
268 	}
269 	///Sets the title of the window
270 	public void setTitle(Text s) @trusted {
271 		title = s;
272 		drawHeader();
273 	}
274 	///Ditto
275 	public void setTitle(dstring s) @trusted {
276 		title.text = s;
277 		drawHeader();
278 	}
279 	///Returns the title of the window
280 	public Text getTitle() @safe @nogc pure nothrow {
281 		return title;
282 	}
283 	/**
284 	 * Closes the window by calling the WindowHandler's closeWindow function.
285 	 */
286 	public void close(Event ev) {
287 		close();
288 	}
289 	///Ditto
290 	public void close() {
291 		if (onClose !is null) onClose();
292 		if (parent !is null) parent.removeChildWindow(this);
293 		if (onDrawUpdate !is null)
294 			onDrawUpdate();
295 		handler.closeWindow(this);
296 	}
297 	/**
298 	 * Adds a WindowHandler to the window.
299 	 */
300 	public void addHandler(WindowHandler wh) @nogc @safe pure nothrow {
301 		handler = wh;
302 	}
303 	
304 	public Coordinate getAbsolutePosition(WindowElement sender) {
305 		Box p = sender.getPosition();
306 		p.relMove(position.left, position.top);
307 		return p;
308 	}
309 	/**
310 	 * Moves the window to the exact location.
311 	 */
312 	public void move(const int x, const int y) {
313 		position.move(x,y);
314 		handler.updateWindowCoord(this);
315 		if (onDrawUpdate !is null)
316 			onDrawUpdate();
317 	}
318 	/**
319 	 * Moves the window by the given values.
320 	 */
321 	public void relMove(const int x, const int y) {
322 		position.relMove(x,y);
323 		handler.updateWindowCoord(this);
324 		if (onDrawUpdate !is null)
325 			onDrawUpdate();
326 	}
327 	/**
328 	 * Sets the size of the window, also issues a redraw.
329 	 */
330 	public void resize(const int width, const int height) {
331 		position.right = position.left + width;
332 		position.bottom = position.top + height;
333 		draw();
334 		handler.refreshWindow(this);
335 	}
336 	/**
337 	 * Returns the outputted bitmap.
338 	 * Can be overridden for 32 bit outputs.
339 	 */
340 	public @property ABitmap getOutput() @nogc @safe pure nothrow {
341 		return output.output;
342 	}
343 	/**
344 	 * Gives focus to the windowelement requesting it.
345 	 */
346 	public void requestFocus(WindowElement sender) {
347 		if (focusables.has(sender)) {
348 			try {
349 				if (focusedElement != -1)
350 					focusables[focusedElement].focusTaken();
351 				Focusable f = cast(Focusable)(sender);
352 				focusedElement = focusables.which(f);
353 				focusables[focusedElement].focusGiven();
354 			} catch (Exception e) {
355 				debug writeln(e);
356 			}
357 		}
358 	}
359 	/**
360 	 * Sets the cursor to the given type on request.
361 	 */
362 	public void requestCursor(CursorType type) {
363 		handler.setCursor(type);
364 	}
365 	/**
366 	 * Adds a child window to the current window.
367 	 */
368 	public void addChildWindow(Window w) {
369 		children.put(w);
370 		w.parent = this;
371 		children.setAsFirst(children.length);
372 		handler.addWindow(w);
373 	}
374 	/**
375 	 * Removes a child window from the current window.
376 	 */
377 	public void removeChildWindow(Window w) {
378 		if (children.removeByElem(w)) {
379 			w.parent = null;
380 			handler.closeWindow(w);
381 		}
382 		
383 	}
384 	/**
385 	 * Returns the child windows.
386 	 */
387 	public CWSet getChildWindows() {
388 		return children;
389 	}
390 	//Implementation of the `Canvas` interface starts here.
391 	///Draws a line.
392 	public void drawLine(Point from, Point to, ubyte color) @trusted {
393 		output.drawLine(from, to, color);
394 	}
395 	///Draws a line pattern.
396 	public void drawLinePattern(Point from, Point to, ubyte[] pattern) @trusted {
397 		output.drawLinePattern(from, to, pattern);
398 	}
399 	///Draws an empty rectangle.
400 	public void drawBox(Coordinate target, ubyte color) @trusted {
401 		output.drawBox(target, color);
402 	}
403 	///Draws an empty rectangle with line patterns.
404 	public void drawBoxPattern(Coordinate target, ubyte[] pattern) @trusted {
405 		output.drawBox(target, pattern);
406 	}
407 	///Draws a filled rectangle with a specified color,
408 	public void drawFilledBox(Coordinate target, ubyte color) @trusted {
409 		output.drawFilledBox(target, color);
410 	}
411 	///Pastes a bitmap to the given point using blitter, which threats color #0 as transparency.
412 	public void bitBLT(Point target, ABitmap source) @trusted {
413 		output.bitBLT(target, cast(Bitmap8Bit)source);
414 	}
415 	///Pastes a slice of a bitmap to the given point using blitter, which threats color #0 as transparency.
416 	public void bitBLT(Point target, ABitmap source, Coordinate slice) @trusted {
417 		output.bitBLT(target, cast(Bitmap8Bit)source, slice);
418 	}
419 	///Pastes a repeated bitmap pattern over the specified area.
420 	public void bitBLTPattern(Coordinate target, ABitmap pattern) @trusted {
421 		output.bitBLTPattern(target, cast(Bitmap8Bit)pattern);
422 	}
423 	///XOR blits a repeated bitmap pattern over the specified area.
424 	public void xorBitBLT(Coordinate target, ABitmap pattern) @trusted {
425 		output.xorBitBLT(target, cast(Bitmap8Bit)pattern);
426 	}
427 	///XOR blits a color index over a specified area.
428 	public void xorBitBLT(Coordinate target, ubyte color) @trusted {
429 		output.xorBitBLT(target, color);
430 	}
431 	///Fills an area with the specified color.
432 	public void fill(Point target, ubyte color, ubyte background = 0) @trusted {
433 
434 	}
435 	///Draws a single line text within the given prelimiter.
436 	public void drawTextSL(Coordinate target, Text text, Point offset) @trusted {
437 		output.drawSingleLineText(target, text, offset.x, offset.y);
438 	}
439 	///Draws a multi line text within the given prelimiter.
440 	public void drawTextML(Coordinate target, Text text, Point offset) @trusted {
441 		output.drawMultiLineText(target, text, offset.x, offset.y);
442 	}
443 	///Clears the area within the target
444 	public void clearArea(Coordinate target) @trusted {
445 		output.drawFilledBox(target, getStyleSheet.getColor("window"));
446 	}
447 	//Implementation of the `Focusable` interface:
448 	///Called when an object receives focus.
449 	public void focusGiven() {
450 		active = true;
451 		draw;
452 	}
453 	///Called when an object loses focus.
454 	public void focusTaken() {
455 		if (focusedElement != -1) {
456 			focusables[focusedElement].focusTaken;
457 			focusedElement = -1;
458 		}
459 		if (lastMouseEventTarget) {
460 			lastMouseEventTarget.focusTaken();
461 			lastMouseEventTarget = null;
462 		}
463 		active = false;
464 		draw;
465 	}
466 	///Cycles the focus on a single element.
467 	///Returns -1 if end is reached, or the number of remaining elements that
468 	///are cycleable in the direction.
469 	public int cycleFocus(int direction) {
470 		if (focusedElement < focusables.length && focusedElement >= 0) {
471 			if (focusables[focusedElement].cycleFocus(direction) == -1) {
472 				focusables[focusedElement].focusTaken;
473 				focusedElement += direction;
474 				focusables[focusedElement].focusGiven;
475 			}
476 		} else if (focusedElement == -1) {
477 			focusedElement = 0;
478 			focusables[focusedElement].focusGiven;
479 		} else return -1;
480 		if (direction > 1) return cast(int)(focusables.length - focusedElement);
481 		else return cast(int)focusedElement;
482 	}
483 	///Passes key events to the focused element when not in text editing mode.
484 	public void passKey(uint keyCode, ubyte mod) {
485 		if (focusedElement != -1) {
486 			focusables[focusedElement].passKey(keyCode, mod);
487 		}
488 	}
489 	//Implementation of `MouseEventReceptor` interface starts here
490 	///Passes mouse click event
491 	public void passMCE(MouseEventCommons mec, MouseClickEvent mce) {
492 		if (!isMoved) {
493 			lastMousePos = Point(mce.x - position.left, mce.y - position.top);
494 			foreach (WindowElement we; elements) {
495 				if (we.getPosition.isBetween(lastMousePos)) {
496 					lastMouseEventTarget = we;
497 					mce.x = lastMousePos.x;
498 					mce.y = lastMousePos.y;
499 					we.passMCE(mec, mce);
500 					return;
501 				}
502 			}
503 			foreach (ISmallButton sb; smallButtons) {
504 				WindowElement we = cast(WindowElement)sb;
505 				if (we.getPosition.isBetween(lastMousePos)) {
506 					lastMouseEventTarget = we;
507 					mce.x = lastMousePos.x;
508 					mce.y = lastMousePos.y;
509 					we.passMCE(mec, mce);
510 					return;
511 				}
512 			}
513 			const int headerHeight = getStyleSheet().drawParameters["WindowHeaderHeight"];
514 			if (lastMousePos.y < headerHeight) {
515 				isMoved = true;
516 				handler.initDragEvent(this);
517 			}
518 			lastMouseEventTarget = null;
519 		} else if (!mce.state) {
520 			isMoved = false;
521 		}
522 	}
523 	///Passes mouse move event
524 	public void passMME(MouseEventCommons mec, MouseMotionEvent mme) {
525 		lastMousePos = Point(mme.x - position.left, mme.y - position.top);
526 		if (isMoved) {
527 			if (mme.buttonState)
528 				relMove(mme.relX, mme.relY);
529 			else
530 				isMoved = false;
531 		} else if (lastMouseEventTarget) {
532 			mme.x = lastMousePos.x;
533 			mme.y = lastMousePos.y;
534 			lastMouseEventTarget.passMME(mec, mme);
535 			if (!lastMouseEventTarget.getPosition.isBetween(mme.x, mme.y)) {
536 				lastMouseEventTarget = null;
537 			}
538 		} else {
539 			foreach (WindowElement we; elements) {
540 				if (we.getPosition.isBetween(lastMousePos)) {
541 					lastMouseEventTarget = we;
542 					mme.x = lastMousePos.x;
543 					mme.y = lastMousePos.y;
544 					we.passMME(mec, mme);
545 					return;
546 				}
547 			}
548 		}
549 	}
550 	///Passes mouse scroll event
551 	public void passMWE(MouseEventCommons mec, MouseWheelEvent mwe) {
552 		if (lastMouseEventTarget) {
553 			lastMouseEventTarget.passMWE(mec, mwe);
554 		}
555 	}
556 	/**
557 	 * Puts a PopUpElement on the GUI.
558 	 */
559 	public void addPopUpElement(PopUpElement p) {
560 		handler.addPopUpElement(p);
561 	}
562 	/**
563 	 * Puts a PopUpElement on the GUI at the given position.
564 	 */
565 	public void addPopUpElement(PopUpElement p, int x, int y) {
566 		handler.addPopUpElement(p, x, y);
567 	}
568 	/** 
569 	 * Ends the popup session and closes all popups.
570 	 */
571 	public void endPopUpSession(PopUpElement p) {
572 		handler.endPopUpSession(p);
573 	}
574 	/**
575 	 * Closes a single popup element.
576 	 */
577 	public void closePopUp(PopUpElement p) {
578 		handler.closePopUp(p);
579 	}
580 	///Generates a generic close button
581 	public static SmallButton closeButton(StyleSheet ss = globalDefaultStyle) {
582 		const int windowHeaderHeight = ss.drawParameters["WindowHeaderHeight"];
583 		SmallButton sb = new SmallButton("closeButtonB", "closeButtonA", "close", 
584 				Box(0,0, windowHeaderHeight - 1, windowHeaderHeight - 1));
585 		sb.isLeftSide = true;
586 		if (ss !is globalDefaultStyle)
587 			sb.customStyle = ss;
588 		return sb;
589 	}
590 }
591