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