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 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 303 p.move(mouseX, mouseY); 304 numOfPopUpElements--; 305 spriteLayer.addSprite(p.getOutput(), numOfPopUpElements, p.getPosition.left, p.getPosition.top); 306 307 } 308 /** 309 * Adds a pop-up element into the environment and moves it to the given location. 310 * Params: 311 * p = The pop-up element to be added. 312 * x = The x coordinate on the raster. 313 * y = The y coordinate on the raster. 314 */ 315 public void addPopUpElement(PopUpElement p, int x, int y){ 316 popUpElements.put(p); 317 p.addParent(this); 318 p.draw; 319 p.move(x, y); 320 numOfPopUpElements--; 321 spriteLayer.addSprite(p.getOutput,numOfPopUpElements, x, y); 322 } 323 /** 324 * Removes all pop-up elements from the environment, effectively ending the pop-up session. 325 */ 326 private void removeAllPopUps(){ 327 for ( ; numOfPopUpElements < 0 ; numOfPopUpElements++){ 328 spriteLayer.removeSprite(numOfPopUpElements); 329 } 330 /+foreach (key ; popUpElements) { 331 key.destroy; 332 }+/ 333 ///Why didn't I add a method to clear linked lists? (slams head into wall) 334 popUpElements = PopUpSet(new PopUpElement[](0)); 335 /+while (popUpElements.length) { 336 popUpElements.remove(0); 337 }+/ 338 } 339 /** 340 * Removes the pop-up element with the highest priority. 341 */ 342 private void removeTopPopUp(){ 343 344 spriteLayer.removeSprite(numOfPopUpElements++); 345 346 popUpElements.remove(popUpElements.length - 1); 347 } 348 /** 349 * Returns the default stylesheet (popup). 350 */ 351 public StyleSheet getDefaultStyleSheet(){ 352 return defaultStyle; 353 } 354 /** 355 * Ends the current pop-up session. 356 * Params: 357 * p = UNUSED 358 */ 359 public void endPopUpSession(PopUpElement p){ 360 removeAllPopUps(); 361 } 362 /** 363 * Removes the given popup element. 364 */ 365 public void closePopUp(PopUpElement p){ 366 popUpElements.removeByElem(p); 367 } 368 369 370 /*public Coordinate getAbsolutePosition(PopUpElement sender){ 371 for(int i ; i < popUpElements.length ; i++){ 372 if(popUpElements[i] = sender){ 373 374 } 375 } 376 return Coordinate(); 377 }*/ 378 //implementation of the `InputListener` interface 379 /** 380 * Called when a keybinding event is generated. 381 * The `id` should be generated from a string, usually the name of the binding. 382 * `code` is a duplicate of the code used for fast lookup of the binding, which also contains other info (deviceID, etc). 383 * `timestamp` is the time lapsed since the start of the program, can be used to measure time between keypresses. 384 * NOTE: Hat events on joysticks don't generate keyReleased events, instead they generate keyPressed events on release. 385 */ 386 public void keyEvent(uint id, BindingCode code, uint timestamp, bool isPressed) { 387 import pixelperfectengine.system.etc : hashCalc; 388 if (isPressed) { 389 switch (id) { 390 case hashCalc("systab"): 391 if (windows.length) { 392 windows[0].cycleFocus(1); 393 } 394 break; 395 case hashCalc("systabs"): 396 if (windows.length) { 397 windows[0].cycleFocus(-1); 398 } 399 break; 400 case hashCalc("syctabc"): 401 if (windows.length > 1) { 402 windows = windows[1..$] ~ windows[0..1]; 403 } 404 break; 405 case hashCalc("syctabsc"): 406 if (windows.length > 1) { 407 windows = windows[$ - 1..$] ~ windows[0..$-1]; 408 } 409 break; 410 default: 411 break; 412 } 413 } 414 } 415 /** 416 * Called when an axis is being operated. 417 * The `id` should be generated from a string, usually the name of the binding. 418 * `code` is a duplicate of the code used for fast lookup of the binding, which also contains other info (deviceID, etc). 419 * `timestamp` is the time lapsed since the start of the program, can be used to measure time between keypresses. 420 * `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 421 * triggers. 422 */ 423 public void axisEvent(uint id, BindingCode code, uint timestamp, float value) { 424 425 } 426 } 427 428