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 bindbc.sdl.bind.sdlmouse;
17 
18 /**
19  * Handles windows as well as PopUpElements.
20  */
21 public class WindowHandler : InputListener, MouseListener, PopUpHandler {
22 	alias WindowSet = LinkedList!(Window, false, "a is b");
23 	alias PopUpSet = LinkedList!(PopUpElement, false, "a is b");
24 	protected WindowSet windows;
25 	protected PopUpSet popUpElements;
26 	private int numOfPopUpElements;
27 	//private int[] priorities;
28 	protected int screenWidth, screenHeight, rasterWidth, rasterHeight, moveX, moveY, mouseX, mouseY;
29 	protected double mouseConvX, mouseConvY;
30 	//public Bitmap16Bit[wchar] basicFont, altFont, alarmFont;
31 	///Sets the default style for the windowhandler.
32 	///If null, the global default will be used instead.
33 	public StyleSheet defaultStyle;
34 	//public Bitmap16Bit[int] styleBrush;
35 	protected ABitmap background;
36 	///A window that is used for top-level stuff, like elements in the background, or an integrated window.
37 	protected Window baseWindow;
38 	///The type of the current cursor
39 	protected CursorType cursor;
40 	///SDL cursor pointer to operate it
41 	protected SDL_Cursor* sdlCursor;
42 	private ISpriteLayer spriteLayer;
43 	//private Window windowToMove;
44 	protected MouseEventReceptor dragEventSrc;
45 	private PopUpElement dragEventDestPopUp;
46 	//private ubyte lastMouseButton;
47 	/**
48 	 * Default CTOR.
49 	 * sW and sH set the screen width and height.
50 	 * rW and rH set the raster width and height.
51 	 * ISpriteLayer sets the SpriteLayer, that will display the windows and popups as sprites.
52 	 */
53 	public this(int sW, int sH, int rW, int rH, ISpriteLayer sl) {
54 		screenWidth = sW;
55 		screenHeight = sH;
56 		rasterWidth = rW;
57 		rasterHeight = rH;
58 		spriteLayer = sl;
59 		mouseConvX = cast(double)screenWidth / rasterWidth;
60 		mouseConvY = cast(double)screenHeight / rasterHeight;
61 	}
62 	/**
63 	 * Sets the cursor to the given type.
64 	 */
65 	public CursorType setCursor(CursorType type) {
66 		cursor = type;
67 		sdlCursor = SDL_CreateSystemCursor(cast(SDL_SystemCursor)cursor);
68 		SDL_SetCursor(sdlCursor);
69 		return cursor;
70 	}
71 	/**
72 	 * Returns the current cursor type.
73 	 */
74 	public CursorType getCursor() @nogc @safe pure nothrow {
75 		return cursor;
76 	}
77 	/**
78 	 * Adds a window to the handler.
79 	 */
80 	public void addWindow(Window w) @trusted {
81 		windows.put(w);
82 		w.addHandler(this);
83 		w.draw();
84 		setWindowToTop(w);
85 	}
86 
87 	/**
88 	 * Adds a DefaultDialog as a message box
89 	 */
90 	public void message(dstring title, dstring message, int width = 256) {
91 		import PixelPerfectEngine.concrete.dialogs.defaultdialog;
92 		StyleSheet ss = getStyleSheet();
93 		dstring[] formattedMessage = ss.getChrFormatting("label").font.breakTextIntoMultipleLines(message, width -
94 				ss.drawParameters["WindowLeftPadding"] - ss.drawParameters["WindowRightPadding"]);
95 		int height = cast(int)(formattedMessage.length * (ss.getChrFormatting("label").font.size +
96 				ss.drawParameters["TextSpacingTop"] + ss.drawParameters["TextSpacingBottom"]));
97 		height += ss.drawParameters["WindowTopPadding"] + ss.drawParameters["WindowBottomPadding"] +
98 				ss.drawParameters["ComponentHeight"];
99 		Coordinate c = Coordinate(mouseX - width / 2, mouseY - height / 2, mouseX + width / 2, mouseY + height / 2);
100 		//Text title0 = new Text(title, ss.getChrFormatting("windowHeader"));
101 		addWindow(new DefaultDialog(c, null, title, formattedMessage));
102 	}
103 	/**
104 	 * Adds a background.
105 	 */
106 	public void addBackground(ABitmap b) {
107 		background = b;
108 		spriteLayer.addSprite(background, 65_536, 0, 0);
109 	}
110 	/**
111 	 * Returns the window priority or -1 if the window can't be found.
112 	 */
113 	private int whichWindow(Window w) @safe pure nothrow {
114 		try
115 			return cast(int)windows.which(w);
116 		catch (Exception e)
117 			return -1;
118 	}
119 	/**
120 	 * Sets sender to be top priority.
121 	 */
122 	public void setWindowToTop(Window w) {
123 		windows[0].focusTaken();
124 		sizediff_t pri = whichWindow(w);
125 		windows.setAsFirst(pri);
126 		updateSpriteOrder();
127 		windows[0].focusGiven();
128 	}
129 	/**
130 	 * Updates the sprite order by removing everything, then putting them back again.
131 	 */
132 	protected void updateSpriteOrder() {
133 		spriteLayer.clear();
134 		for (int i ; i < windows.length ; i++)
135 			spriteLayer.addSprite(windows[i].getOutput, i, windows[i].getPosition);
136 		if (background) spriteLayer.addSprite(background, 65_536, 0, 0);
137 		if (baseWindow) spriteLayer.addSprite(baseWindow.getOutput, 65_535, 0, 0);
138 	}
139 	/**
140 	 * Returns the default stylesheet.
141 	 */
142 	public StyleSheet getStyleSheet() {
143 		if (defaultStyle)
144 			return defaultStyle;
145 		else
146 			return globalDefaultStyle;
147 	}
148 	/**
149 	 * Closes the given window.
150 	 *
151 	 * NOTE: The closed window should be dereferenced in other places in order to be deallocated by the GC. If not,
152 	 * then it can be used to restore the window without creating a new one, potentially saving it's states.
153 	 */
154 	public void closeWindow(Window sender) {
155 		const int p = whichWindow(sender);
156 		windows.remove(p);
157 
158 		updateSpriteOrder();
159 	}
160 	
161 	/**
162 	 * Initializes drag event.
163 	 * Used to avoid issues from stray mouse release, etc.
164 	 */
165 	public void initDragEvent(MouseEventReceptor dragEventSrc) @safe nothrow {
166 		this.dragEventSrc = dragEventSrc;
167 	}
168 	/**
169 	 * Updates the sender's coordinates.
170 	 */
171 	public void updateWindowCoord(Window sender) @safe nothrow {
172 		const int n = whichWindow(sender);
173 		spriteLayer.replaceSprite(sender.getOutput(), n, sender.getPosition());
174 	}
175 	//implementation of the MouseListener interface starts here
176 	/**
177 	 * Called on mouse click events.
178 	 */
179 	public void mouseClickEvent(MouseEventCommons mec, MouseClickEvent mce) {
180 		mce.x = cast(int)(mce.x / mouseConvX);
181 		mce.y = cast(int)(mce.y / mouseConvY);
182 		if (!mce.state && dragEventSrc) {
183 			dragEventSrc.passMCE(mec, mce);
184 			dragEventSrc = null;
185 		}
186 		if (numOfPopUpElements < 0) {
187 			foreach (PopUpElement pe ; popUpElements) {
188 				if (pe.getPosition().isBetween(mce.x, mce. y)) {
189 					pe.passMCE(mec, mce);
190 					return;
191 				}
192 			}
193 			if (mce.state) {
194 				removeAllPopUps();
195 				return;
196 			}
197 		} else if (mce.state) {
198 			foreach (Window w ; windows) {
199 				const Box pos = w.getPosition();
200 				if (pos.isBetween(mce.x, mce.y)) {
201 					if (!w.active && mce.state) { //If window is not active, then the window order must be reset
202 						//windows[0].focusTaken();
203 						setWindowToTop(w);
204 					}
205 					w.passMCE(mec, mce);
206 					dragEventSrc = w;
207 					return;
208 				}
209 			}
210 		}
211 		if (baseWindow) baseWindow.passMCE(mec, mce);
212 	}
213 	/**
214 	 * Called on mouse wheel events.
215 	 */
216 	public void mouseWheelEvent(MouseEventCommons mec, MouseWheelEvent mwe) {
217 		if (numOfPopUpElements < 0) popUpElements[$ - 1].passMWE(mec, mwe);
218 		else if (windows.length) windows[0].passMWE(mec, mwe);
219 		else if (baseWindow) baseWindow.passMWE(mec, mwe);
220 	}
221 	/**
222 	 * Called on mouse motion events.
223 	 */
224 	public void mouseMotionEvent(MouseEventCommons mec, MouseMotionEvent mme) {
225 		mme.relX = cast(int)(mme.relX / mouseConvX);
226 		mme.relY = cast(int)(mme.relY / mouseConvY);
227 		mme.x = cast(int)(mme.x / mouseConvX);
228 		mme.y = cast(int)(mme.y / mouseConvY);
229 		mouseX = mme.x;
230 		mouseY = mme.y;
231 		if (dragEventSrc) {
232 			dragEventSrc.passMME(mec, mme);
233 			return;
234 		}
235 		if (numOfPopUpElements < 0) {
236 			popUpElements[$ - 1].passMME(mec, mme);
237 			return;
238 		}
239 		foreach (Window key; windows) {
240 			if (key.getPosition.isBetween(mme.x, mme.y)) {
241 				key.passMME(mec, mme);
242 				return;
243 			}
244 		}
245 		if (baseWindow) baseWindow.passMME(mec, mme);
246 	}
247 	/**
248 	 * Sets the BaseWindow to the given object.
249 	 *
250 	 * The base window has no priority and will reside forever in the background. Can be used for various ends.
251 	 */
252 	public Window setBaseWindow(Window w) @safe nothrow {
253 		import PixelPerfectEngine.graphics.layers.base : RenderingMode;
254 		w.addHandler(this);
255 		baseWindow = w;
256 		spriteLayer.addSprite(w.getOutput, 65_535, w.getPosition);
257 		spriteLayer.setSpriteRenderingMode(65_535, RenderingMode.Blitter);
258 		return baseWindow;
259 	}
260 	
261 	
262 	/**
263 	 * Replaces the window's old sprite in the spritelayer's display list with the new one.
264 	 *
265 	 * Needed to be called each time the window's sprite is being replaced, or else the previous one will be continued to
266 	 * be displayed without any updates.
267 	 */
268 	public void refreshWindow(Window sender) @safe {
269 		const int n = whichWindow(sender);
270 		spriteLayer.replaceSprite(windows[n].getOutput, n, windows[n].getPosition);
271 	}
272 	/**
273 	 * Adds a popup element and moves it to the current cursor position.
274 	 */
275 	public void addPopUpElement(PopUpElement p){
276 		popUpElements.put(p);
277 		p.addParent(this);
278 		p.draw;
279 		/+mouseX -= (p.getPosition.width/2);
280 		mouseY -= (p.getPosition.height/2);+/
281 		p.move(mouseX - p.getPosition.width/2, mouseY - p.getPosition.height/2);
282 		numOfPopUpElements--;
283 		spriteLayer.addSprite(p.getOutput,numOfPopUpElements,mouseX,mouseY);
284 
285 	}
286 	public void addPopUpElement(PopUpElement p, int x, int y){
287 		popUpElements.put(p);
288 		p.addParent(this);
289 		p.draw;
290 		p.move(x, y);
291 		numOfPopUpElements--;
292 		spriteLayer.addSprite(p.getOutput,numOfPopUpElements, x, y);
293 	}
294 	private void removeAllPopUps(){
295 		for ( ; numOfPopUpElements < 0 ; numOfPopUpElements++){
296 			spriteLayer.removeSprite(numOfPopUpElements);
297 		}
298 		/+foreach (key ; popUpElements) {
299 			key.destroy;
300 		}+/
301 		///Why didn't I add a method to clear linked lists? (slams head into wall)
302 		popUpElements = PopUpSet(new PopUpElement[](0));
303 		/+while (popUpElements.length) {
304 			popUpElements.remove(0);
305 		}+/
306 	}
307 	private void removeTopPopUp(){
308 
309 		spriteLayer.removeSprite(numOfPopUpElements++);
310 
311 		popUpElements.remove(popUpElements.length - 1);
312 	}
313 	public StyleSheet getDefaultStyleSheet(){
314 		return defaultStyle;
315 	}
316 	public void endPopUpSession(PopUpElement p){
317 		removeAllPopUps();
318 	}
319 	public void closePopUp(PopUpElement p){
320 		popUpElements.removeByElem(p);
321 	}
322 	
323 	
324 	/*public Coordinate getAbsolutePosition(PopUpElement sender){
325 		for(int i ; i < popUpElements.length ; i++){
326 			if(popUpElements[i] = sender){
327 
328 			}
329 		}
330 		return Coordinate();
331 	}*/
332 	//implementation of the `InputListener` interface
333 	/**
334 	 * Called when a keybinding event is generated.
335 	 * The `id` should be generated from a string, usually the name of the binding.
336 	 * `code` is a duplicate of the code used for fast lookup of the binding, which also contains other info (deviceID, etc).
337 	 * `timestamp` is the time lapsed since the start of the program, can be used to measure time between keypresses.
338 	 * NOTE: Hat events on joysticks don't generate keyReleased events, instead they generate keyPressed events on release.
339 	 */
340 	public void keyEvent(uint id, BindingCode code, uint timestamp, bool isPressed) {
341 		if (isPressed) {
342 
343 		}
344 	}
345 	/**
346 	 * Called when an axis is being operated.
347 	 * The `id` should be generated from a string, usually the name of the binding.
348 	 * `code` is a duplicate of the code used for fast lookup of the binding, which also contains other info (deviceID, etc).
349 	 * `timestamp` is the time lapsed since the start of the program, can be used to measure time between keypresses.
350 	 * `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
351 	 * triggers.
352 	 */
353 	public void axisEvent(uint id, BindingCode code, uint timestamp, float value) {
354 
355 	}
356 }
357 
358