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