1 module pixelperfectengine.concrete.elements.listview; 2 3 import pixelperfectengine.concrete.elements.base; 4 import pixelperfectengine.concrete.elements.scrollbar; 5 6 import pixelperfectengine.system.etc : clamp, min, max; 7 8 //import pixelperfectengine.system.input.types : TextInputFieldType; 9 10 /** 11 * Defines a single item in the listview. 12 * Draws directly onto the canvas to avoit using multiple framebuffers. 13 * Can be inherited from, and that's when non-alphabetical ordering can be implemented. 14 */ 15 public class ListViewItem { 16 /** 17 * Defines a single field (cell) in a listview. 18 */ 19 public struct Field { 20 private uint flags; ///Stores various flags (constraints, etc.) 21 public Text text; ///Stores the text of this field if there's any. 22 public ABitmap bitmap; ///Custom bitmap, can be 32 bit if target enables it. 23 static enum IS_EDITABLE = 1 << 0;///Set if field is text editable. 24 static enum INTEGER = 1 << 1;///Set if field only accepts integer numbers. 25 static enum NUMERIC = 1 << 3;///Set if field only accepts numeric values (integer or floating-point). 26 static enum POSITIVE_ONLY = 1 << 4;///Set if field only accepts positive numeric values (integer or floating-point). 27 28 /** 29 * Default constructor. 30 */ 31 this(Text text, ABitmap bitmap, bool editable = false, bool numOnly = false, bool fp = false, 32 bool posOnly = false) @nogc @safe pure nothrow { 33 this.text = text; 34 this.bitmap = bitmap; 35 if (editable) flags |= IS_EDITABLE; 36 if (numOnly) { 37 if (fp) { 38 flags |= NUMERIC; 39 } else { 40 flags |= INTEGER; 41 } 42 if (posOnly) { 43 flags |= POSITIVE_ONLY; 44 } 45 } 46 } 47 ///Returns whether the field is editable. 48 public @property bool editable() @nogc @safe pure nothrow const { 49 return flags & IS_EDITABLE ? true : false; 50 } 51 ///Sets whether the field is editable. Returns the new value. 52 public @property bool editable(bool val) @nogc @safe pure nothrow { 53 if (val) flags |= IS_EDITABLE; 54 else flags &= ~IS_EDITABLE; 55 return flags & IS_EDITABLE ? true : false; 56 } 57 ///Returns whether the field is integer only. 58 public @property bool integer() @nogc @safe pure nothrow const { 59 return flags & INTEGER ? true : false; 60 } 61 ///Sets whether the field is integer only. Returns the new value. 62 public @property bool integer(bool val) @nogc @safe pure nothrow { 63 if (val) flags |= INTEGER; 64 else flags &= ~INTEGER; 65 return flags & INTEGER ? true : false; 66 } 67 ///Returns whether the field is numeric only. 68 public @property bool numeric() @nogc @safe pure nothrow const { 69 return flags & NUMERIC ? true : false; 70 } 71 ///Sets whether the field is numeric only. Returns the new value. 72 public @property bool numeric(bool val) @nogc @safe pure nothrow { 73 if (val) flags |= NUMERIC; 74 else flags &= ~NUMERIC; 75 return flags & NUMERIC ? true : false; 76 } 77 ///Returns whether the field is positive only. 78 public @property bool positiveOnly() @nogc @safe pure nothrow const { 79 return flags & POSITIVE_ONLY ? true : false; 80 } 81 ///Sets whether the field is positive only. Returns the new value. 82 public @property bool positiveOnly(bool val) @nogc @safe pure nothrow { 83 if (val) flags |= POSITIVE_ONLY; 84 else flags &= ~POSITIVE_ONLY; 85 return flags & POSITIVE_ONLY ? true : false; 86 } 87 } 88 /** 89 * Stores the list of items to be displayed. 90 */ 91 public Field[] fields; 92 ///Height of this item. 93 public int height; 94 /** 95 * Creates a list view item from texts. 96 */ 97 this (int height, Text[] fields) @safe pure nothrow { 98 this.height = height; 99 this.fields.reserve = fields.length; 100 foreach (Text key; fields) { 101 this.fields ~= Field(key, null); 102 } 103 } 104 /** 105 * Creates a ListViewItem from fields directly. 106 */ 107 this (int height, Field[] fields) @nogc @safe pure nothrow { 108 this.height = height; 109 this.fields = fields; 110 } 111 /** 112 * Creates a ListViewItem with default text formatting. 113 */ 114 this (int height, dstring[] ds) @safe nothrow { 115 this.height = height; 116 fields.reserve = ds.length; 117 foreach (dstring key ; ds) { 118 this.fields ~= Field(new Text(key, globalDefaultStyle.getChrFormatting("ListViewHeader")), null); 119 } 120 } 121 /** 122 * Creates a ListViewItem with default text formatting and input type. 123 */ 124 this (int height, dstring[] ds, TextInputFieldType[] inputTypes) @safe nothrow { 125 this.height = height; 126 fields.reserve = ds.length; 127 assert (ds.length == inputTypes.length, "Mismatch in inputTypes and text length"); 128 for (size_t i ; i < ds.length ; i++) { 129 Field f = Field(new Text(ds[i], globalDefaultStyle.getChrFormatting("ListViewHeader")), null); 130 fields ~= f; 131 } 132 } 133 /** 134 * Accesses fields like an array. 135 */ 136 public ref Field opIndex(size_t index) @nogc @safe pure nothrow { 137 return fields[index]; 138 } 139 /** 140 * Accesses fields like an array. 141 */ 142 public Field opIndexAssign(Field value, size_t index) @nogc @safe pure nothrow { 143 fields[index] = value; 144 return value; 145 } 146 ///Returns the amount of fields in this item. 147 public size_t length() @nogc @safe pure nothrow { 148 return fields.length; 149 } 150 /** 151 * Draws the ListViewItem. Draw parameters are supplied via a nested class found in ListView. 152 */ 153 public void draw(ListView parent) { 154 StyleSheet ss = parent.drawParams.ss; 155 Box target = parent.drawParams.target; 156 Box t = Box(target.left, target.top, target.left, target.bottom); 157 Point offset = Point(parent.drawParams.offsetP, parent.drawParams.offsetFR); 158 for (int i = parent.drawParams.offsetC ; i <= parent.drawParams.targetC ; i++) { 159 t.right = min(t.left + parent.drawParams.columnWidths[i] - offset.x, target.right); 160 parent.drawTextSL(t.pad(ss.drawParameters["ListViewColPadding"], ss.drawParameters["ListViewRowPadding"]), 161 fields[i].text, offset); 162 t.left = t.right; 163 offset.x = 0; 164 } 165 parent.drawParams.target.relMove(0, height - parent.drawParams.offsetFR); 166 parent.drawParams.offsetFR = 0; 167 } 168 } 169 /** 170 * Defines the header of a ListView. 171 * Extended from a ListViewItem. 172 */ 173 public class ListViewHeader : ListViewItem { 174 public int[] columnWidths; ///Width of each columns 175 ///Default CTOR 176 this(int height, int[] columnWidths, Text[] fields) @safe pure nothrow { 177 assert (columnWidths.length == fields.length, "Lenght mismatch between the two arrays!"); 178 this.columnWidths = columnWidths; 179 super(height, fields); 180 } 181 ///CTOR for creating fields with default text formatting 182 this(int height, int[] columnWidths, dstring[] ds) @safe nothrow { 183 Text[] fields; 184 fields.reserve = ds.length; 185 foreach (dstring key; ds) { 186 fields ~= new Text(key, globalDefaultStyle.getChrFormatting("ListViewHeader")); 187 } 188 this(height, columnWidths, fields); 189 } 190 /** 191 * Draws the header. Draw parameters are supplied via a nested class found in ListView. 192 */ 193 public override void draw(ListView parent) { 194 if (!height) return; 195 StyleSheet ss = parent.drawParams.ss; 196 Box target = parent.drawParams.target; 197 Box t = Box(target.left, target.top, target.left, target.bottom); 198 Point offset = Point(parent.drawParams.offsetP, 0); 199 for (int i = parent.drawParams.offsetC ; i <= parent.drawParams.targetC ; i++) { 200 t.right = min(t.left + parent.drawParams.columnWidths[i] - offset.x, target.right); 201 if (!offset.x) { 202 parent.drawLine(t.cornerUL, t.cornerLL, ss.getColor("windowascent")); 203 } 204 if (t.left + parent.drawParams.columnWidths[i] < target.right) { 205 Point from = t.cornerUR, to = t.cornerLR; 206 from.x = from.x - 1; 207 to.x = to.x - 1; 208 parent.drawLine(from, to, ss.getColor("windowdescent")); 209 } 210 with (parent) { 211 drawLine(t.cornerUL, t.cornerUR, ss.getColor("windowascent")); 212 drawLine(t.cornerLL, t.cornerLR, ss.getColor("windowdescent")); 213 drawTextSL(t.pad(ss.drawParameters["ListViewColPadding"], ss.drawParameters["ListViewRowPadding"]), fields[i].text, 214 offset); 215 } 216 t.left = t.right; 217 offset.x = 0; 218 } 219 parent.drawParams.target.relMove(0, height); 220 } 221 } 222 /** 223 * Implements a basic ListView 224 */ 225 public class ListView : WindowElement, ElementContainer, TextInputListener { 226 ///Supplies draw parameters to the items 227 public class DrawParameters { 228 ///StyleSheet that is being used currently 229 StyleSheet ss; 230 ///Contains the reference to the header's columnWidth attribute 231 int[] columnWidths; 232 ///The first column to be drawn 233 const int offsetC; 234 ///The last column to be drawn 235 const int targetC; 236 ///Offset in pixels for the first column 237 const int offsetP; 238 ///Offset of the first row. Should be set to zero after the first row has been drawn. 239 int offsetFR; 240 ///The prelimiter where the item should be drawn. 241 Box target; 242 ///CTOR 243 this (StyleSheet ss, int[] columnWidths, const int offsetC, const int targetC, const int offsetP, 244 int offsetFR) @safe @nogc pure nothrow { 245 this.ss = ss; 246 this.columnWidths = columnWidths; 247 this.offsetC = offsetC; 248 this.targetC = targetC; 249 this.offsetP = offsetP; 250 this.offsetFR = offsetFR; 251 } 252 } 253 protected HorizScrollBar horizSlider; ///Horizontal scroll bar. 254 protected VertScrollBar vertSlider; ///Vertical scroll bar. 255 ///The header of the ListView. 256 ///Accessed in a safe manner to ensure it's being updated on the output raster. 257 protected ListViewHeader _header; 258 ///Entries in the ListView. 259 ///Accessed in a safe manner to ensure it's being updated on the output raster and that the number of columns match. 260 protected ListViewItem[] entries; 261 protected int selection; ///Selected item's number, or -1 if none selected. 262 protected int hSelection; ///Horizontal selection for text editing. 263 protected int tselect; ///Lenght of selected characters. 264 protected int cursorPos; ///Position of cursor. 265 protected int horizTextOffset;///Horizontal text offset if text cannot fit the cell. 266 ///Text editing area. 267 protected Box textArea; 268 ///Filters the input to the cell if not null. 269 protected InputFilter filter; 270 ///Holds shared draw parameters that are used when the element is being drawn. 271 ///Should be set to null otherwise. 272 public DrawParameters drawParams; 273 ///Called when an item is selected 274 public EventDeleg onItemSelect; 275 ///Called when text input is finished and accepted 276 public EventDeleg onTextInput; 277 protected static enum EDIT_EN = 1<<9; 278 protected static enum MULTICELL_EDIT_EN = 1<<10; 279 protected static enum TEXTINPUT_EN = 1<<11; 280 protected static enum INSERT = 1<<12; 281 ///Standard CTOR 282 public this(ListViewHeader header, ListViewItem[] entries, string source, Box position) { 283 _header = header; 284 this.entries = entries; 285 this.source = source; 286 this.position = position; 287 recalculateTotalSizes(); 288 } 289 /** 290 * Accesses data entries in a safe manner. 291 */ 292 public ListViewItem opIndex(size_t index) @nogc @safe pure nothrow { 293 return entries[index]; 294 /+scope(exit) { 295 assert(entries[index].length == _header.length, "Column number mismatch error!"); 296 }+/ 297 } 298 /** 299 * Accesses data entries in a safe manner. 300 */ 301 public ListViewItem opIndexAssign(ListViewItem value, size_t index) @safe pure { 302 if (value.length == _header.length) { 303 if (entries.length == index) { 304 entries ~= value; 305 } else { 306 entries[index] = value; 307 } 308 } else throw new Exception("Column number mismatch!"); 309 return value; 310 } 311 /** 312 * Allows to append a single element to the entry list. 313 */ 314 public ListViewItem opOpAssign(string op)(ListViewItem value) { 315 static if (op == "~" || op == "+") { 316 if (value.length == _header.length) { 317 entries ~= value; 318 } else throw new Exception("Column number mismatch!"); 319 } else static assert (0, "Unsupported operator!"); 320 return value; 321 } 322 /** 323 * Allows to append multiple elements to the entry list. 324 */ 325 public ListViewItem[] opOpAssign(string op)(ListViewItem[] value) { 326 static if (op == "~" || op == "+") { 327 foreach (ListViewItem key; value) { 328 if (key.length == _header.length) { 329 entries ~= key; 330 } else throw new Exception("Column number mismatch!"); 331 } 332 } else static assert (0, "Unsupported operator!"); 333 return value; 334 } 335 override public void draw() { 336 StyleSheet ss = getStyleSheet; 337 if (flags & TEXTINPUT_EN) { //only redraw the editing cell in this case 338 const int textPadding = ss.drawParameters["TextSpacingSides"]; 339 340 clearArea(textArea); 341 //drawBox(position, ss.getColor("windowascent")); 342 343 //draw cursor 344 //if (flags & ENABLE_TEXT_EDIT) { 345 //calculate cursor first 346 Box cursor = Box(textArea.left + textPadding, textArea.top + textPadding, textArea.left + textPadding, 347 textArea.bottom - textPadding); 348 cursor.left += text.getWidth(0, cursorPos) - horizTextOffset; 349 //cursor must be at least single pixel wide 350 cursor.right = cursor.left; 351 if (tselect) { 352 cursor.right += text.getWidth(cursorPos, cursorPos + tselect); 353 } else if (flags & INSERT) { 354 if (cursorPos < text.charLength) cursor.right += text.getWidth(cursorPos, cursorPos+1); 355 else cursor.right += text.font.chars(' ').xadvance; 356 } else { 357 cursor.right++; 358 } 359 //Clamp down if cursor is wider than the text editing area 360 cursor.right = cursor.right <= textArea.right - textPadding ? cursor.right : textArea.right - textPadding; 361 //Draw cursor 362 parent.drawFilledBox(cursor, ss.getColor("selection")); 363 364 //} 365 //draw text 366 parent.drawTextSL(textArea - textPadding, text, Point(horizTextOffset, 0)); 367 } else { 368 parent.clearArea(position); 369 370 parent.drawBox(position, ss.getColor("windowascent")); 371 Point upper = Point(0, position.top + _header.height); 372 Point lower = Point(0, position.bottom); 373 { ///Calculate first column stuff 374 int offsetP, offsetC, targetC, targetP; 375 if (horizSlider) { 376 offsetP = horizSlider.value(); 377 //int offsetC; 378 for (; _header.columnWidths[offsetC] < offsetP ; offsetC++) { 379 offsetP -= _header.columnWidths[offsetC]; 380 } 381 ///Calculate last column number 382 //int targetC; 383 targetP = horizSlider.value() + position.width; 384 for (; _header.columnWidths[targetC] < targetP ; targetC++) { 385 targetP -= _header.columnWidths[targetC]; 386 } 387 //targetP = _header.columnWidths[targetC] - targetP; 388 lower.y -= horizSlider.getPosition().height; 389 } else { 390 targetC = cast(int)_header.columnWidths.length - 1; 391 } 392 drawParams = new DrawParameters(ss, _header.columnWidths, offsetC, targetC, offsetP, 0); 393 } 394 395 drawParams.target = Box(position.left, position.top, position.right, position.top + _header.height); 396 397 if (vertSlider) { 398 drawParams.target.right -= vertSlider.getPosition.width; 399 400 } 401 402 _header.draw(this); 403 /+Point upper = Point(drawParams.columnWidths[drawParams.offsetC] + position.left, position.top + _header.height); 404 Point lower = Point(upper.x, position.bottom);+/ 405 int firstRow, lastRow; 406 if (vertSlider) { 407 int pixelsTotal = vertSlider.value(); 408 for (; entries[firstRow].height < pixelsTotal ; firstRow++) { 409 pixelsTotal -= entries[firstRow].height; 410 } 411 drawParams.offsetFR = pixelsTotal; 412 pixelsTotal += position.height; 413 pixelsTotal -= _header.height; 414 if (horizSlider) pixelsTotal -= horizSlider.getPosition().height; 415 lastRow = firstRow; 416 for (; entries[lastRow].height < pixelsTotal ; lastRow++) { 417 pixelsTotal -= entries[lastRow].height; 418 } 419 } else { 420 lastRow = cast(int)entries.length - 1; 421 } 422 423 for (int i = firstRow ; i <= lastRow ; i++) { 424 if (ss.getColor("ListViewHSep") && i != lastRow) { 425 parent.drawLine(drawParams.target.cornerLL, drawParams.target.cornerLR, ss.getColor("ListViewHSep")); 426 } 427 if (selection == i) { 428 Box target = drawParams.target - 1; 429 target.bottom -= drawParams.offsetFR; 430 parent.drawFilledBox(target, ss.getColor("selection")); 431 } 432 entries[i].draw(this); 433 } 434 435 if (ss.getColor("ListViewVSep")) { 436 for (int i = drawParams.offsetC ; i <= drawParams.targetC ; i++) { 437 upper.x = drawParams.columnWidths[i]; 438 lower.x = drawParams.columnWidths[i]; 439 parent.drawLine(upper, lower, ss.getColor("ListViewVSep")); 440 } 441 } 442 if (horizSlider) horizSlider.draw; 443 if (vertSlider) vertSlider.draw; 444 445 drawParams = null; 446 } 447 if (onDraw !is null) { 448 onDraw(); 449 } 450 } 451 /** 452 * Returns the number of the selected item. 453 */ 454 public @property int value() @nogc @safe pure nothrow const { 455 return selection; 456 } 457 /** 458 * Sets the selected item and then does a redraw. 459 * -1 sets selection to none. 460 */ 461 public @property int value(int val) { 462 selection = val; 463 clamp(val, -1, cast(int)(entries.length) - 1); 464 draw; 465 return selection; 466 } 467 /** 468 * Enables or disables the text editing of this element. 469 */ 470 public @property bool editEnable(bool val) @nogc @safe pure nothrow { 471 if (val) flags |= EDIT_EN; 472 else flags &= ~EDIT_EN; 473 return flags & EDIT_EN ? true : false; 474 } 475 /** 476 * Returns true if text editing is enabled. 477 */ 478 public @property bool editEnable() @nogc @safe pure nothrow const { 479 return flags & EDIT_EN ? true : false; 480 } 481 /** 482 * Enables or disables editing for multiple cells. 483 * If disabled, the first cell with editing enabled will be able to be edited. 484 */ 485 public @property bool multicellEditEnable(bool val) @nogc @safe pure nothrow { 486 if (val) flags |= MULTICELL_EDIT_EN; 487 else flags &= ~MULTICELL_EDIT_EN; 488 return flags & MULTICELL_EDIT_EN ? true : false; 489 } 490 /** 491 * Returns true if text editing for multiple cells is enabled. 492 */ 493 public @property bool multicellEditEnable() @nogc @safe pure nothrow const { 494 return flags & MULTICELL_EDIT_EN ? true : false; 495 } 496 /** 497 * Sets a new header, also able to supply new entries. 498 */ 499 public void setHeader(ListViewHeader header, ListViewItem[] entries) { 500 _header = header; 501 foreach (ListViewItem key; entries) { 502 assert(key.length == header.length); 503 } 504 this.entries = entries; 505 refresh(); 506 draw(); 507 } 508 /** 509 * Removes an item from the entries. 510 * Returns the removed entry. 511 */ 512 public ListViewItem removeEntry(size_t index) { 513 import std.algorithm.mutation : remove; 514 ListViewItem result = entries[index]; 515 entries = entries.remove(index); 516 if (selection >= entries.length) selection--; 517 //draw; 518 return result; 519 } 520 /** 521 * Removes all entries in the list. 522 */ 523 public void clear() @safe { 524 entries.length = 0; 525 } 526 /** 527 * Refreshes the list view. 528 * Must be called every time when adding new items is finished. 529 */ 530 public void refresh() { 531 selection = -1; 532 recalculateTotalSizes; 533 draw; 534 } 535 /** 536 * Recalculates the total width and height of the list view's field, also generates scrollbars if needed. 537 */ 538 protected void recalculateTotalSizes() { 539 int totalWidth, totalHeight; 540 foreach (i ; _header.columnWidths) { 541 totalWidth += i; 542 } 543 foreach (ListViewItem key; entries) { 544 totalHeight += key.height; 545 } 546 totalHeight += _header.height; 547 StyleSheet ss = getStyleSheet(); 548 bool needsVSB, needsHSB; 549 if (totalWidth > position.width) 550 needsHSB = true; 551 if (totalHeight > position.height) 552 needsVSB = true; 553 if (needsVSB && totalWidth > position.width - ss.drawParameters["VertScrollBarSize"]) 554 needsHSB = true; 555 if (needsHSB && totalHeight > position.height - ss.drawParameters["HorizScrollBarSize"]) 556 needsVSB = true; 557 totalHeight -= _header.height; 558 if (needsVSB) { 559 const int maxvalue = needsHSB ? totalHeight - position.height - ss.drawParameters["HorizScrollBarSize"] : 560 totalHeight - position.height; 561 562 const Box target = Box(position.right - ss.drawParameters["HorizScrollBarSize"] + 2, position.top, 563 position.right, needsVSB ? position.bottom - ss.drawParameters["VertScrollBarSize"] : position.bottom); 564 vertSlider = new VertScrollBar(maxvalue, source ~ "VSB", target); 565 vertSlider.setParent(this); 566 vertSlider.onScrolling = &scrollBarEventOut; 567 } else vertSlider = null; 568 if (needsHSB){ 569 const int maxvalue = needsVSB ? totalWidth - position.width - ss.drawParameters["VertScrollBarSize"] : 570 totalWidth - position.width; 571 const Box target = Box(position.left, position.bottom - ss.drawParameters["VertScrollBarSize"] + 2, 572 needsVSB ? position.right - ss.drawParameters["HorizScrollBarSize"] : position.right, 573 position.bottom); 574 horizSlider = new HorizScrollBar(maxvalue, source ~ "VSB", target); 575 horizSlider.setParent(this); 576 horizSlider.onScrolling = &scrollBarEventOut; 577 } else horizSlider = null; 578 } 579 protected void scrollBarEventOut(Event ev) { 580 draw; 581 } 582 /** 583 * Returns the absolute position of the element. 584 */ 585 public Box getAbsolutePosition(WindowElement sender) { 586 return parent.getAbsolutePosition(sender); 587 } 588 /** 589 * Gives focus to the element if applicable 590 */ 591 public void requestFocus(WindowElement sender) { 592 593 } 594 /** 595 * Sets the cursor to the given type on request. 596 */ 597 public void requestCursor(CursorType type) { 598 599 } 600 ///Draws a line. 601 public void drawLine(Point from, Point to, ubyte color) @trusted { 602 parent.drawLine(from, to, color); 603 } 604 ///Draws a line pattern. 605 public void drawLinePattern(Point from, Point to, ubyte[] pattern) @trusted { 606 parent.drawLinePattern(from, to, pattern); 607 } 608 ///Draws an empty rectangle. 609 public void drawBox(Box target, ubyte color) @trusted { 610 parent.drawBox(target, color); 611 } 612 ///Draws an empty rectangle with line patterns. 613 public void drawBoxPattern(Box target, ubyte[] pattern) @trusted { 614 parent.drawBoxPattern(target, pattern); 615 } 616 ///Draws a filled rectangle with a specified color. 617 public void drawFilledBox(Box target, ubyte color) @trusted { 618 parent.drawFilledBox(target, color); 619 } 620 ///Pastes a bitmap to the given point using blitter, which threats color #0 as transparency. 621 public void bitBLT(Point target, ABitmap source) @trusted { 622 parent.bitBLT(target, source); 623 } 624 ///Pastes a slice of a bitmap to the given point using blitter, which threats color #0 as transparency. 625 public void bitBLT(Point target, ABitmap source, Box slice) @trusted { 626 parent.bitBLT(target, source, slice); 627 } 628 ///Pastes a repeated bitmap pattern over the specified area. 629 public void bitBLTPattern(Box target, ABitmap pattern) @trusted { 630 parent.bitBLTPattern(target, pattern); 631 } 632 ///XOR blits a repeated bitmap pattern over the specified area. 633 public void xorBitBLT(Box target, ABitmap pattern) @trusted { 634 parent.xorBitBLT(target, pattern); 635 } 636 ///XOR blits a color index over a specified area. 637 public void xorBitBLT(Box target, ubyte color) @trusted { 638 parent.xorBitBLT(target, color); 639 } 640 ///Fills an area with the specified color. 641 public void fill(Point target, ubyte color, ubyte background = 0) @trusted { 642 parent.fill(target, color, background); 643 } 644 ///Draws a single line text within the given prelimiter. 645 public void drawTextSL(Box target, Text text, Point offset) @trusted { 646 parent.drawTextSL(target, text, offset); 647 } 648 ///Draws a multi line text within the given prelimiter. 649 public void drawTextML(Box target, Text text, Point offset) @trusted { 650 parent.drawTextML(target, text, offset); 651 } 652 ///Clears the area within the target 653 public void clearArea(Box target) @trusted { 654 parent.clearArea(target); 655 } 656 ///Passes mouse click event 657 public override void passMCE(MouseEventCommons mec, MouseClickEvent mce) { 658 ///TODO: Handle mouse click when in text editing mode 659 if (state != ElementState.Enabled) return; 660 if (vertSlider) { 661 const Box p = vertSlider.getPosition(); 662 if (p.isBetween(mce.x, mce.y)) { 663 mce.x -= p.left; 664 mce.y -= p.top; 665 vertSlider.passMCE(mec, mce); 666 return; 667 } 668 } 669 if (horizSlider) { 670 const Box p = horizSlider.getPosition(); 671 if (p.isBetween(mce.x, mce.y)) { 672 mce.x -= p.left; 673 mce.y -= p.top; 674 horizSlider.passMCE(mec, mce); 675 return; 676 } 677 } 678 679 //if (mce.button != MouseButton.Left && !mce.state) return; 680 681 mce.x -= position.left; 682 mce.y -= position.top; 683 if (entries.length && mce.y > _header.height && mce.button == MouseButton.Left && mce.state) { 684 textArea.top = position.top; 685 textArea.left = position.left; 686 mce.y -= _header.height; 687 int pixelsTotal = mce.y, pos; 688 if (vertSlider) ///calculate outscrolled area 689 pixelsTotal += vertSlider.value; 690 while (pos < entries.length) { 691 if (pixelsTotal > entries[pos].height) { 692 pixelsTotal -= entries[pos].height; 693 textArea.top += entries[pos].height; 694 if (pos + 1 < entries.length) 695 pos++; 696 } else { 697 break; 698 } 699 } 700 if (pos >= entries.length) { 701 selection = -1; 702 } else if (selection == pos && (flags & EDIT_EN)) { 703 //Calculate horizontal selection for Multicell editing if needed 704 /+if (flags & MULTICELL_EDIT_EN) { 705 706 } else {+/ 707 textArea.top += _header.height; 708 foreach (size_t i, ListViewItem.Field f ; entries[selection].fields) { 709 if (f.editable) { 710 hSelection = cast(int)i; 711 if (vertSlider) textArea.top -= vertSlider.value; 712 if (horizSlider) textArea.left -= horizSlider.value; 713 714 with (textArea) { 715 bottom = entries[selection].height + textArea.top; 716 right = _header.columnWidths[i] + textArea.left; 717 left = max(textArea.left, position.left); 718 top = max(textArea.top, position.top); 719 right = min(textArea.right, position.right); 720 bottom = min(textArea.bottom, position.bottom); 721 } 722 text = new Text(entries[selection][hSelection].text); 723 cursorPos = 0; 724 tselect = cast(int)text.charLength; 725 //oldText = text; 726 if (flags & MULTICELL_EDIT_EN) { 727 if (textArea.left < mce.x && textArea.right > mce.x) { 728 inputHandler.startTextInput(this); 729 break; 730 } 731 } else { 732 inputHandler.startTextInput(this); 733 break; 734 } 735 736 } 737 textArea.left += _header.columnWidths[i]; 738 } 739 //} 740 selection = pos; 741 } else 742 selection = pos; 743 744 if (onItemSelect !is null && selection != -1) 745 onItemSelect(new Event(this, entries[selection], EventType.Selection, SourceType.WindowElement)); 746 } else if (!entries.length) 747 selection = -1; 748 749 draw(); 750 } 751 ///Passes mouse move event 752 public override void passMME(MouseEventCommons mec, MouseMotionEvent mme) { 753 if (state != ElementState.Enabled) return; 754 if (vertSlider) { 755 const Box p = vertSlider.getPosition(); 756 if (p.isBetween(mme.x, mme.y)) { 757 mme.x -= p.left - position.left; 758 mme.y -= p.top - position.top; 759 vertSlider.passMME(mec, mme); 760 return; 761 } 762 } 763 if (horizSlider) { 764 const Box p = horizSlider.getPosition(); 765 if (p.isBetween(mme.x, mme.y)) { 766 mme.x -= p.left - position.left; 767 mme.y -= p.top - position.top; 768 horizSlider.passMME(mec, mme); 769 return; 770 } 771 } 772 } 773 ///Passes mouse scroll event 774 public override void passMWE(MouseEventCommons mec, MouseWheelEvent mwe) { 775 if (state != ElementState.Enabled) return; 776 if (horizSlider) horizSlider.passMWE(mec, mwe); 777 if (vertSlider) vertSlider.passMWE(mec, mwe); 778 } 779 /** 780 * Puts a PopUpElement on the GUI. 781 */ 782 public void addPopUpElement(PopUpElement p) { 783 parent.addPopUpElement(p); 784 } 785 /** 786 * Puts a PopUpElement on the GUI at the given position. 787 */ 788 public void addPopUpElement(PopUpElement p, int x, int y) { 789 parent.addPopUpElement(p, x, y); 790 } 791 /** 792 * Ends the popup session and closes all popups. 793 */ 794 public void endPopUpSession(PopUpElement p) { 795 parent.endPopUpSession(p); 796 } 797 /** 798 * Closes a single popup element. 799 */ 800 public void closePopUp(PopUpElement p) { 801 parent.closePopUp(p); 802 } 803 //Interface `TextInputListener` starts here 804 /** 805 * Passes the inputted text to the target, alongside with a window ID and a timestamp. 806 */ 807 public void textInputEvent(uint timestamp, uint windowID, dstring text) { 808 import pixelperfectengine.system.etc : removeUnallowedSymbols; 809 /+if (allowedChars.length) { 810 text = removeUnallowedSymbols(text, allowedChars); 811 if (!text.length) return; 812 }+/ 813 if (filter) { 814 filter.use(text); 815 if (!text.length) return; 816 } 817 if (tselect) { 818 this.text.removeChar(cursorPos, tselect); 819 tselect = 0; 820 for(int j ; j < text.length ; j++){ 821 this.text.insertChar(cursorPos++, text[j]); 822 } 823 } else if (flags & INSERT) { 824 for(int j ; j < text.length ; j++){ 825 this.text.overwriteChar(cursorPos++, text[j]); 826 } 827 } else { 828 for(int j ; j < text.length ; j++){ 829 this.text.insertChar(cursorPos++, text[j]); 830 } 831 } 832 const int textPadding = getStyleSheet.drawParameters["TextSpacingSides"]; 833 const Coordinate textPos = Coordinate(textPadding,(position.height / 2) - (this.text.font.size / 2) , 834 position.width,position.height - textPadding); 835 const int x = this.text.getWidth(), cursorPixelPos = this.text.getWidth(0, cursorPos); 836 if(x > textPos.width) { 837 if(cursorPos == this.text.text.length) { 838 horizTextOffset = x - textPos.width; 839 } else if(cursorPixelPos < horizTextOffset) { //Test for whether the cursor would fall out from the current text area 840 horizTextOffset = cursorPixelPos; 841 } else if(cursorPixelPos > horizTextOffset + textPos.width) { 842 horizTextOffset = horizTextOffset + textPos.width; 843 } 844 } 845 draw(); 846 } 847 /** 848 * Passes text editing events to the target, alongside with a window ID and a timestamp. 849 */ 850 public void textEditingEvent(uint timestamp, uint windowID, dstring text, int start, int length) { 851 for (int i ; i < length ; i++) { 852 this.text.overwriteChar(start + i, text[i]); 853 } 854 cursorPos = start + length; 855 } 856 /** 857 * Passes text input key events to the target, e.g. cursor keys. 858 */ 859 public void textInputKeyEvent(uint timestamp, uint windowID, TextInputKey key, ushort modifier) { 860 switch(key) { 861 case TextInputKey.Enter: 862 entries[selection][hSelection].text = text; 863 inputHandler.stopTextInput(); 864 if(onTextInput !is null) 865 onTextInput(new CellEditEvent(this, entries[selection], selection, hSelection)); 866 //onTextInput(new Event(source, null, null, null, text, 0, EventType.T, null, this)); 867 break; 868 case TextInputKey.Escape: 869 //text = oldText; 870 inputHandler.stopTextInput(); 871 872 873 break; 874 case TextInputKey.Backspace: 875 if(cursorPos > 0){ 876 deleteCharacter(cursorPos - 1); 877 cursorPos--; 878 draw(); 879 } 880 break; 881 case TextInputKey.Delete: 882 if (tselect) { 883 for (int i ; i < tselect ; i++) { 884 deleteCharacter(cursorPos); 885 } 886 tselect = 0; 887 } else { 888 deleteCharacter(cursorPos); 889 } 890 draw(); 891 break; 892 case TextInputKey.CursorLeft: 893 if (modifier != KeyModifier.Shift) { 894 tselect = 0; 895 if(cursorPos > 0){ 896 --cursorPos; 897 draw(); 898 } 899 } 900 break; 901 case TextInputKey.CursorRight: 902 if (modifier != KeyModifier.Shift) { 903 tselect = 0; 904 if(cursorPos < text.charLength){ 905 ++cursorPos; 906 draw(); 907 } 908 } 909 break; 910 case TextInputKey.Home: 911 if (modifier != KeyModifier.Shift) { 912 tselect = 0; 913 cursorPos = 0; 914 draw(); 915 } 916 break; 917 case TextInputKey.End: 918 if (modifier != KeyModifier.Shift) { 919 tselect = 0; 920 cursorPos = cast(int)text.charLength; 921 draw(); 922 } 923 break; 924 case TextInputKey.Insert: 925 flags ^= INSERT; 926 draw(); 927 break; 928 default: 929 break; 930 } 931 } 932 /** 933 * When called, the listener should drop all text input. 934 */ 935 public void dropTextInput() { 936 flags &= ~TEXTINPUT_EN; 937 draw; 938 } 939 /** 940 * Called if text input should be initialized. 941 */ 942 public void initTextInput() { 943 flags |= TEXTINPUT_EN; 944 ListViewItem.Field f = opIndex(selection)[hSelection]; 945 if (f.numeric) { 946 if (f.positiveOnly) 947 filter = new DecimalFilter!false(f.text); 948 else 949 filter = new DecimalFilter!true(f.text); 950 } else if (f.integer) { 951 if (f.positiveOnly) 952 filter = new IntegerFilter!false(f.text); 953 else 954 filter = new IntegerFilter!true(f.text); 955 } else { 956 filter = null; 957 } 958 } 959 private void deleteCharacter(size_t n){ 960 text.removeChar(n); 961 } 962 }