1 module pixelperfectengine.concrete.windowhandler;
2 
3 public import pixelperfectengine.concrete.interfaces;
4 public import pixelperfectengine.concrete.window;
5 public import pixelperfectengine.concrete.types;
6 public import pixelperfectengine.concrete.popup;
7 public import pixelperfectengine.concrete.dialogs;
8 
9 public import pixelperfectengine.system.input.interfaces;
10 
11 public import pixelperfectengine.graphics.layers : ISpriteLayer;
12 
13 import collections.linkedlist;
14 import pixelperfectengine.system.etc : cmpObjPtr;
15 
16 import sdl.mouse;
17 import std.math : nearbyint;
18 
19 /**
20  * Handles windows as well as PopUpElements.
21  */
22 public class WindowHandler : InputListener, MouseListener, PopUpHandler {
23 	alias WindowSet = LinkedList!(Window, false, "a is b");
24 	alias PopUpSet = LinkedList!(PopUpElement, false, "a is b");
25 	protected WindowSet windows;
26 	protected PopUpSet popUpElements;
27 	private int numOfPopUpElements;
28 	//private int[] priorities;
29 	protected int screenWidth, screenHeight, rasterWidth, rasterHeight, moveX, moveY, mouseX, mouseY;
30 	protected double mouseConvX, mouseConvY;
31 	//public Bitmap16Bit[wchar] basicFont, altFont, alarmFont;
32 	///Sets the default style for the windowhandler.
33 	///If null, the global default will be used instead.
34 	public StyleSheet defaultStyle;
35 	//public Bitmap16Bit[int] styleBrush;
36 	protected ABitmap background;
37 	///A window that is used for top-level stuff, like elements in the background, or an integrated window.
38 	protected Window baseWindow;
39 	///The type of the current cursor
40 	protected CursorType cursor;
41 	///SDL cursor pointer to operate it
42 	protected SDL_Cursor* sdlCursor;
43 	private ISpriteLayer spriteLayer;
44 	//private Window windowToMove;
45 	protected MouseEventReceptor dragEventSrc;
46 	private PopUpElement dragEventDestPopUp;
47 	//private ubyte lastMouseButton;
48 	/**
49 	 * Creates an instance of WindowHandler.
50 	 * Params:
51 	 *   sW = Screen width
52 	 *   sH = Screen height
53 	 *   rW = Raster width
54 	 *   rH = Raster height
55 	 *   sl = The spritelayer, that will display the windows as sprites.
56 	 */
57 	public this(int sW, int sH, int rW, int rH, ISpriteLayer sl) {
58 		screenWidth = sW;
59 		screenHeight = sH;
60 		rasterWidth = rW;
61 		rasterHeight = rH;
62 		spriteLayer = sl;
63 		mouseConvX = cast(double)screenWidth / rasterWidth;
64 		mouseConvY = cast(double)screenHeight / rasterHeight;
65 	}
66 	/**
67 	 * Sets the cursor to the given type.
68 	 */
69 	public CursorType setCursor(CursorType type) {
70 		cursor = type;
71 		sdlCursor = SDL_CreateSystemCursor(cast(SDL_SystemCursor)cursor);
72 		SDL_SetCursor(sdlCursor);
73 		return cursor;
74 	}
75 	/**
76 	 * Returns the current cursor type.
77 	 */
78 	public CursorType getCursor() @nogc @safe pure nothrow {
79 		return cursor;
80 	}
81 	/**
82 	 * Adds a window to the handler, then sets it to top and hands over the focus to it.
83 	 * Params:
84 	 *   w = The window to be added to the handler.
85 	 */
86 	public void addWindow(Window w) @trusted {
87 		windows.put(w);
88 		w.addHandler(this);
89 		w.draw();
90 		setWindowToTop(w);
91 	}
92 
93 	/**
94 	 * Adds a DefaultDialog as a message box.
95 	 * Params:
96 	 *   title = Title of the window.
97 	 *   message = The text that appears in the window.
98 	 *   width = The width of the dialog window. 256 pixels is default.
99 	 */
100 	public void message(dstring title, dstring message, int width = 256) {
101 		import pixelperfectengine.concrete.dialogs.defaultdialog;
102 		StyleSheet ss = getStyleSheet();
103 		dstring[] formattedMessage = ss.getChrFormatting("label").font.breakTextIntoMultipleLines(message, width -
104 				ss.drawParameters["WindowLeftPadding"] - ss.drawParameters["WindowRightPadding"]);
105 		int height = cast(int)(formattedMessage.length * (ss.getChrFormatting("label").font.size +
106 				ss.drawParameters["TextSpacingTop"] + ss.drawParameters["TextSpacingBottom"]));
107 		height += ss.drawParameters["WindowTopPadding"] + ss.drawParameters["WindowBottomPadding"] +
108 				ss.drawParameters["ComponentHeight"];
109 		Coordinate c = Coordinate(mouseX - width / 2, mouseY - height / 2, mouseX + width / 2, mouseY + height / 2);
110 		//Text title0 = new Text(title, ss.getChrFormatting("windowHeader"));
111 		addWindow(new DefaultDialog(c, null, title, formattedMessage));
112 	}
113 	/**
114 	 * Adds a background to the spritelayer without disrupting window priorities.
115 	 * Params:
116 	 *   b = The bitmap to become the background. Should match the raster's sizes, but can be of any bitdepth.
117 	 */
118 	public void addBackground(ABitmap b) {
119 		background = b;
120 		spriteLayer.addSprite(background, 65_536, 0, 0);
121 	}
122 	/**
123 	 * Returns the window priority or -1 if the window can't be found.
124 	 * Params:
125 	 *   w = The window of which priority must be checked.
126 	 */
127 	public int whichWindow(Window w) @safe pure nothrow {
128 		try
129 			return cast(int)windows.which(w);
130 		catch (Exception e)
131 			return -1;
132 	}
133 	/**
134 	 * Sets sender to be top priority, and hands focus to it.
135 	 * Params:
136 	 *   w = The window that needs to be set.
137 	 */
138 	public void setWindowToTop(Window w) {
139 		windows[0].focusTaken();
140 		sizediff_t pri = whichWindow(w);
141 		windows.setAsFirst(pri);
142 		updateSpriteOrder();
143 		windows[0].focusGiven();
144 	}
145 	/**
146 	 * Updates the sprite order by removing everything, then putting them back again.
147 	 */
148 	protected void updateSpriteOrder() {
149 		spriteLayer.clear();
150 		for (int i ; i < windows.length ; i++)
151 			spriteLayer.addSprite(windows[i].getOutput, i, windows[i].getPosition.left, windows[i].getPosition.top);
152 		if (background) spriteLayer.addSprite(background, 65_536, 0, 0);
153 		if (baseWindow) spriteLayer.addSprite(baseWindow.getOutput, 65_535, 0, 0);
154 	}
155 	/**
156 	 * Returns the default stylesheet, either one that has been set locally to this handler, or the global one.
157 	 */
158 	public StyleSheet getStyleSheet() {
159 		if (defaultStyle)
160 			return defaultStyle;
161 		else
162 			return globalDefaultStyle;
163 	}
164 	/**
165 	 * Removes the window from the list of windows, essentially closing it.
166 	 * 
167 	 * NOTE: The closed window should be dereferenced in other places in order to be deallocated by the GC. If not,
168 	 * then it can be used to restore the window without creating a new one, potentially saving it's states.
169 	 */
170 	public void closeWindow(Window sender) {
171 		const int p = whichWindow(sender);
172 		windows.remove(p);
173 
174 		updateSpriteOrder();
175 	}
176 	
177 	/**
178 	 * Initializes mouse drag event.
179 	 * Used to avoid issues from stray mouse release, etc.
180 	 * Params:
181 	 *   dragEventSrc = The receptor of mouse drag events.
182 	 */
183 	public void initDragEvent(MouseEventReceptor dragEventSrc) @safe nothrow {
184 		this.dragEventSrc = dragEventSrc;
185 	}
186 	/**
187 	 * Updates the window's coordinates.
188 	 * DUPLICATE FUNCTION OF `refreshWindow`! REMOVE IT BY RELEASE VERSION OF 0.10.0, AND REPLACE IT WITH AN ALIAS!
189 	 */
190 	public void updateWindowCoord(Window sender) @safe nothrow {
191 		const int n = whichWindow(sender);
192 		spriteLayer.replaceSprite(sender.getOutput(), n, sender.getPosition());
193 	}
194 	//implementation of the MouseListener interface starts here
195 	/**
196 	 * Called on mouse click events.
197 	 */
198 	public void mouseClickEvent(MouseEventCommons mec, MouseClickEvent mce) {
199 		mce.x = cast(int)(mce.x / mouseConvX);
200 		mce.y = cast(int)(mce.y / mouseConvY);
201 		if (!mce.state && dragEventSrc) {
202 			dragEventSrc.passMCE(mec, mce);
203 			dragEventSrc = null;
204 		}
205 		if (numOfPopUpElements < 0) {
206 			foreach (PopUpElement pe ; popUpElements) {
207 				if (pe.getPosition().isBetween(mce.x, mce. y)) {
208 					pe.passMCE(mec, mce);
209 					return;
210 				}
211 			}
212 			if (mce.state) {
213 				removeAllPopUps();
214 				return;
215 			}
216 		} else if (mce.state) {
217 			foreach (Window w ; windows) {
218 				const Box pos = w.getPosition();
219 				if (pos.isBetween(mce.x, mce.y)) {
220 					if (!w.active && mce.state) { //If window is not active, then the window order must be reset
221 						//windows[0].focusTaken();
222 						setWindowToTop(w);
223 					}
224 					w.passMCE(mec, mce);
225 					dragEventSrc = w;
226 					return;
227 				}
228 			}
229 		}
230 		if (baseWindow) baseWindow.passMCE(mec, mce);
231 	}
232 	/**
233 	 * Called on mouse wheel events.
234 	 */
235 	public void mouseWheelEvent(MouseEventCommons mec, MouseWheelEvent mwe) {
236 		if (numOfPopUpElements < 0) popUpElements[$ - 1].passMWE(mec, mwe);
237 		else if (windows.length) windows[0].passMWE(mec, mwe);
238 		else if (baseWindow) baseWindow.passMWE(mec, mwe);
239 	}
240 	/**
241 	 * Called on mouse motion events.
242 	 */
243 	public void mouseMotionEvent(MouseEventCommons mec, MouseMotionEvent mme) {
244 		mme.relX = cast(int)nearbyint(mme.relX / mouseConvX);
245 		mme.relY = cast(int)nearbyint(mme.relY / mouseConvY);
246 		mme.x = cast(int)nearbyint(mme.x / mouseConvX);
247 		mme.y = cast(int)nearbyint(mme.y / mouseConvY);
248 		mouseX = mme.x;
249 		mouseY = mme.y;
250 		if (dragEventSrc) {
251 			dragEventSrc.passMME(mec, mme);
252 			return;
253 		}
254 		if (numOfPopUpElements < 0) {
255 			popUpElements[$ - 1].passMME(mec, mme);
256 			return;
257 		}
258 		foreach (Window key; windows) {
259 			if (key.getPosition.isBetween(mme.x, mme.y)) {
260 				key.passMME(mec, mme);
261 				return;
262 			}
263 		}
264 		if (baseWindow) baseWindow.passMME(mec, mme);
265 	}
266 	/**
267 	 * Sets the BaseWindow to the given object.
268 	 *
269 	 * The base window has no priority and will reside forever in the background. Can be used for various ends.
270 	 */
271 	public Window setBaseWindow(Window w) @safe nothrow {
272 		import pixelperfectengine.graphics.layers.base : RenderingMode;
273 		w.addHandler(this);
274 		baseWindow = w;
275 		spriteLayer.addSprite(w.getOutput, 65_535, w.getPosition.left, w.getPosition.top);
276 		spriteLayer.setSpriteRenderingMode(65_535, RenderingMode.Blitter);
277 		return baseWindow;
278 	}
279 	
280 	
281 	/**
282 	 * Replaces the window's old sprite in the spritelayer's display list with the new one.
283 	 *
284 	 * Needed to be called each time the window's sprite is being replaced, or else the previous one will be continued to
285 	 * be displayed without any updates.
286 	 */
287 	public void refreshWindow(Window sender) @safe nothrow {
288 		const int n = whichWindow(sender);
289 		spriteLayer.replaceSprite(windows[n].getOutput, n, windows[n].getPosition);
290 	}
291 	/**
292 	 * Adds a popup element into the environment and moves it to the current cursor position.
293 	 * Params:
294 	 *   p = The pop-up element to be added.
295 	 */
296 	public void addPopUpElement(PopUpElement p) {
297 		popUpElements.put(p);
298 		p.addParent(this);
299 		p.draw;
300 		/+mouseX -= (p.getPosition.width/2);
301 		mouseY -= (p.getPosition.height/2);+/
302 		int posX = mouseX, posY = mouseY;
303 		if (posX + p.getPosition.width > rasterWidth)
304 			posX -= p.getPosition.width;
305 		if (posY + p.getPosition.height > rasterHeight)
306 			posY -= p.getPosition.height;
307 		p.move(posX, posY);
308 		numOfPopUpElements--;
309 		spriteLayer.addSprite(p.getOutput(), numOfPopUpElements, p.getPosition.left, p.getPosition.top);
310 
311 	}
312 	/**
313 	 * Adds a pop-up element into the environment and moves it to the given location.
314 	 * Params:
315 	 *   p = The pop-up element to be added.
316 	 *   x = The x coordinate on the raster.
317 	 *   y = The y coordinate on the raster.
318 	 */
319 	public void addPopUpElement(PopUpElement p, int x, int y){
320 		popUpElements.put(p);
321 		p.addParent(this);
322 		p.draw;
323 		p.move(x, y);
324 		numOfPopUpElements--;
325 		spriteLayer.addSprite(p.getOutput,numOfPopUpElements, x, y);
326 	}
327 	/**
328 	 * Removes all pop-up elements from the environment, effectively ending the pop-up session.
329 	 */
330 	private void removeAllPopUps(){
331 		for ( ; numOfPopUpElements < 0 ; numOfPopUpElements++){
332 			spriteLayer.removeSprite(numOfPopUpElements);
333 		}
334 		/+foreach (key ; popUpElements) {
335 			key.destroy;
336 		}+/
337 		///Why didn't I add a method to clear linked lists? (slams head into wall)
338 		popUpElements = PopUpSet(new PopUpElement[](0));
339 		/+while (popUpElements.length) {
340 			popUpElements.remove(0);
341 		}+/
342 	}
343 	/**
344 	 * Removes the pop-up element with the highest priority.
345 	 */
346 	private void removeTopPopUp(){
347 
348 		spriteLayer.removeSprite(numOfPopUpElements++);
349 
350 		popUpElements.remove(popUpElements.length - 1);
351 	}
352 	/**
353 	 * Returns the default stylesheet (popup).
354 	 */
355 	public StyleSheet getDefaultStyleSheet(){
356 		return defaultStyle;
357 	}
358 	/**
359 	 * Ends the current pop-up session.
360 	 * Params:
361 	 *   p = UNUSED
362 	 */
363 	public void endPopUpSession(PopUpElement p){
364 		removeAllPopUps();
365 	}
366 	/**
367 	 * Removes the given popup element.
368 	 */
369 	public void closePopUp(PopUpElement p){
370 		popUpElements.removeByElem(p);
371 	}
372 	
373 	
374 	/*public Coordinate getAbsolutePosition(PopUpElement sender){
375 		for(int i ; i < popUpElements.length ; i++){
376 			if(popUpElements[i] = sender){
377 
378 			}
379 		}
380 		return Coordinate();
381 	}*/
382 	//implementation of the `InputListener` interface
383 	/**
384 	 * Called when a keybinding event is generated.
385 	 * The `id` should be generated from a string, usually the name of the binding.
386 	 * `code` is a duplicate of the code used for fast lookup of the binding, which also contains other info (deviceID, etc).
387 	 * `timestamp` is the time lapsed since the start of the program, can be used to measure time between keypresses.
388 	 * NOTE: Hat events on joysticks don't generate keyReleased events, instead they generate keyPressed events on release.
389 	 */
390 	public void keyEvent(uint id, BindingCode code, uint timestamp, bool isPressed) {
391 		import pixelperfectengine.system.etc : hashCalc;
392 		if (isPressed) {
393 			switch (id) {
394 				case hashCalc("systab"):
395 					if (windows.length) {
396 						windows[0].cycleFocus(1);
397 					}
398 					break;
399 				case hashCalc("systabs"):
400 					if (windows.length) {
401 						windows[0].cycleFocus(-1);
402 					}
403 					break;
404 				case hashCalc("syctabc"):
405 					if (windows.length > 1) {
406 						windows = windows[1..$] ~ windows[0..1];
407 					}
408 					break;
409 				case hashCalc("syctabsc"):
410 					if (windows.length > 1) {
411 						windows = windows[$ - 1..$] ~ windows[0..$-1];
412 					}
413 					break;
414 				default:
415 					break;
416 			}
417 		}
418 	}
419 	/**
420 	 * Called when an axis is being operated.
421 	 * The `id` should be generated from a string, usually the name of the binding.
422 	 * `code` is a duplicate of the code used for fast lookup of the binding, which also contains other info (deviceID, etc).
423 	 * `timestamp` is the time lapsed since the start of the program, can be used to measure time between keypresses.
424 	 * `value` is the current position of the axis normalized between -1.0 and +1.0 for joysticks, and 0.0 and +1.0 for analog
425 	 * triggers.
426 	 */
427 	public void axisEvent(uint id, BindingCode code, uint timestamp, float value) {
428 
429 	}
430 	public int[2] getRasterSizes() {
431 		return [rasterWidth, rasterHeight];
432 	}
433 }
434 
435