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