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 if (parent is null) return; 327 StyleSheet ss = getStyleSheet; 328 if (flags & TEXTINPUT_EN) { //only redraw the editing cell in this case 329 const int textPadding = ss.drawParameters["TextSpacingSides"]; 330 331 clearArea(textArea); 332 //drawBox(position, ss.getColor("windowascent")); 333 334 //draw cursor 335 //if (flags & ENABLE_TEXT_EDIT) { 336 //calculate cursor first 337 Box cursor = Box(textArea.left + textPadding, textArea.top + textPadding, textArea.left + textPadding, 338 textArea.bottom - textPadding); 339 cursor.left += text.getWidth(0, cursorPos) - horizTextOffset; 340 //cursor must be at least single pixel wide 341 cursor.right = cursor.left; 342 if (tselect) { 343 cursor.right += text.getWidth(cursorPos, cursorPos + tselect); 344 } else if (flags & INSERT) { 345 if (cursorPos < text.charLength) cursor.right += text.getWidth(cursorPos, cursorPos+1); 346 else cursor.right += text.font.chars(' ').xadvance; 347 } else { 348 cursor.right++; 349 } 350 //Clamp down if cursor is wider than the text editing area 351 cursor.right = cursor.right <= textArea.right - textPadding ? cursor.right : textArea.right - textPadding; 352 //Draw cursor if it doesn't fall out of bounds 353 if (cursor.left < position.right && cursor.right < position.right) 354 parent.drawFilledBox(cursor, ss.getColor("selection")); 355 356 //} 357 //draw text 358 parent.drawTextSL(textArea - textPadding, text, Point(horizTextOffset, 0)); 359 } else { 360 parent.clearArea(position); 361 362 parent.drawBox(position, ss.getColor("windowascent")); 363 Point upper = Point(0, position.top + _header.height); 364 Point lower = Point(0, position.bottom); 365 { ///Calculate first column stuff 366 int offsetP, offsetC, targetC, targetP; 367 if (horizSlider) { 368 offsetP = horizSlider.value(); 369 for (; _header.columnWidths[offsetC] < offsetP ; offsetC++) { 370 offsetP -= _header.columnWidths[offsetC]; 371 } 372 offsetP = max(0, offsetP); 373 ///Calculate last column number 374 targetP = horizSlider.value() + position.width; 375 for (; _header.columnWidths.length > targetC && _header.columnWidths[targetC] < targetP ; targetC++) { 376 targetP -= _header.columnWidths[targetC]; 377 } 378 targetC = min(cast(int)(_header.columnWidths.length) - 1, targetC); 379 lower.y -= horizSlider.getPosition().height; 380 } else { 381 targetC = cast(int)_header.columnWidths.length - 1; 382 } 383 drawParams = new DrawParameters(ss, _header.columnWidths, offsetC, targetC, offsetP, 0); 384 } 385 386 drawParams.target = Box(position.left, position.top, position.right, position.top + _header.height); 387 388 if (vertSlider) { 389 drawParams.target.right -= vertSlider.getPosition.width; 390 391 } 392 393 _header.draw(this); 394 /+Point upper = Point(drawParams.columnWidths[drawParams.offsetC] + position.left, position.top + _header.height); 395 Point lower = Point(upper.x, position.bottom);+/ 396 int firstRow, lastRow; 397 if (vertSlider) { 398 int pixelsTotal = vertSlider.value(); 399 for (; entries[firstRow].height < pixelsTotal ; firstRow++) { 400 pixelsTotal -= entries[firstRow].height; 401 } 402 drawParams.offsetFR = pixelsTotal; 403 pixelsTotal += position.height; 404 pixelsTotal -= _header.height; 405 if (horizSlider) pixelsTotal -= horizSlider.getPosition().height; 406 lastRow = firstRow; 407 for (; entries.length > lastRow && entries[lastRow].height < pixelsTotal ; lastRow++) { 408 pixelsTotal -= entries[lastRow].height; 409 } 410 lastRow = min(cast(int)(entries.length) - 1, lastRow); 411 } else { 412 lastRow = cast(int)entries.length - 1; 413 } 414 415 for (int i = firstRow ; i <= lastRow ; i++) { 416 if (drawParams.target.bottom > position.bottom) 417 drawParams.target.bottom = position.bottom; 418 if (ss.getColor("ListViewHSep") && i != lastRow) { 419 parent.drawLine(drawParams.target.cornerLL, drawParams.target.cornerLR, ss.getColor("ListViewHSep")); 420 } 421 if (selection == i) { 422 Box target = drawParams.target - 1; 423 target.bottom -= drawParams.offsetFR; 424 parent.drawFilledBox(target, ss.getColor("selection")); 425 } 426 entries[i].draw(this); 427 } 428 429 if (ss.getColor("ListViewVSep")) { 430 for (int i = drawParams.offsetC ; i <= drawParams.targetC ; i++) { 431 upper.x = drawParams.columnWidths[i]; 432 lower.x = drawParams.columnWidths[i]; 433 parent.drawLine(upper, lower, ss.getColor("ListViewVSep")); 434 } 435 } 436 if (horizSlider) horizSlider.draw; 437 if (vertSlider) vertSlider.draw; 438 439 drawParams = null; 440 } 441 if (onDraw !is null) { 442 onDraw(); 443 } 444 } 445 /** 446 * Returns the numfer of entries the ListView has. 447 */ 448 public @property size_t numEntries() @nogc @safe pure nothrow const { 449 return entries.length; 450 } 451 /** 452 * Returns the number of the selected item, or minus one if nothing is selected. 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 * Returns the currently selected element, or null if none is selected. 469 */ 470 public @property ListViewItem selectedElement() @nogc @safe pure nothrow { 471 if (selection >= 0) 472 return entries[selection]; 473 else 474 return null; 475 } 476 /** 477 * Enables or disables the text editing of this element. 478 */ 479 public @property bool editEnable(bool val) @nogc @safe pure nothrow { 480 if (val) flags |= EDIT_EN; 481 else flags &= ~EDIT_EN; 482 return flags & EDIT_EN ? true : false; 483 } 484 /** 485 * Returns true if text editing is enabled. 486 */ 487 public @property bool editEnable() @nogc @safe pure nothrow const { 488 return flags & EDIT_EN ? true : false; 489 } 490 /** 491 * Enables or disables editing for multiple cells. 492 * If disabled, the first cell with editing enabled will be able to be edited. 493 */ 494 public @property bool multicellEditEnable(bool val) @nogc @safe pure nothrow { 495 if (val) flags |= MULTICELL_EDIT_EN; 496 else flags &= ~MULTICELL_EDIT_EN; 497 return flags & MULTICELL_EDIT_EN ? true : false; 498 } 499 /** 500 * Returns true if text editing for multiple cells is enabled. 501 */ 502 public @property bool multicellEditEnable() @nogc @safe pure nothrow const { 503 return flags & MULTICELL_EDIT_EN ? true : false; 504 } 505 /** 506 * Sets a new header, also able to supply new entries. 507 */ 508 public void setHeader(ListViewHeader header, ListViewItem[] entries) { 509 _header = header; 510 foreach (ListViewItem key; entries) { 511 assert(key.length == header.length); 512 } 513 this.entries = entries; 514 refresh(); 515 draw(); 516 } 517 /** 518 * Removes an item from the entries. 519 * Returns the removed entry. 520 */ 521 public ListViewItem removeEntry(size_t index) { 522 import std.algorithm.mutation : remove; 523 ListViewItem result = entries[index]; 524 entries = entries.remove(index); 525 if (selection >= entries.length) selection--; 526 //draw; 527 return result; 528 } 529 /** 530 * Inserts an element at the given index. 531 * Params: 532 * index = Where the new element should be inserted. 533 * item = The item to be inserted. 534 * Returns: The inserted element, or null if out of bounds. 535 */ 536 public ListViewItem insertAt(size_t index, ListViewItem item) { 537 if (!index) 538 entries = item ~ entries; 539 else if (entries.length > index) 540 entries = entries[0..index] ~ item ~ entries[index..$]; 541 else if (entries.length == index) 542 entries ~= item; 543 else 544 return null; 545 return item; 546 } 547 /** 548 * Moves the entry to the given position. 549 * Params: 550 * index: the entry to be moved. 551 * target: the position to be moved to. 552 */ 553 public void moveEntry(size_t index, size_t target) { 554 ListViewItem[] backup = entries[0..index - 1] ~ entries[index..$]; 555 entries = backup[0..target] ~ entries[index] ~ backup[target..$]; 556 } 557 /** 558 * Returns the current x and y scroll positions. 559 */ 560 public int[2] scroll() { 561 int[2] result; 562 if (horizSlider !is null) 563 result[0] = horizSlider.value; 564 if (vertSlider !is null) 565 result[1] = vertSlider.value; 566 return result; 567 } 568 /** 569 * Sets the scrolling to `pos`, then returns the new scrolling positions. 570 */ 571 public int[2] scroll(int[2] pos) { 572 if (horizSlider !is null) 573 horizSlider.value = pos[0]; 574 if (vertSlider !is null) 575 vertSlider.value = pos[1]; 576 return scroll(); 577 } 578 /** 579 * Removes all entries in the list. 580 */ 581 public void clear() @safe { 582 entries.length = 0; 583 } 584 /** 585 * Refreshes the list view. 586 * Must be called every time when adding new items is finished. 587 */ 588 public void refresh() { 589 if (selection >= entries.length) 590 selection = cast(int)entries.length - 1; 591 recalculateTotalSizes; 592 draw; 593 } 594 /** 595 * Recalculates the total width and height of the list view's field, also generates scrollbars if needed. 596 */ 597 protected void recalculateTotalSizes() { 598 int totalWidth, totalHeight; 599 foreach (i ; _header.columnWidths) { 600 totalWidth += i; 601 } 602 foreach (ListViewItem key; entries) { 603 totalHeight += key.height; 604 } 605 if (_header) 606 totalHeight += _header.height; 607 StyleSheet ss = getStyleSheet(); 608 bool needsVSB, needsHSB; 609 if (totalWidth > position.width) 610 needsHSB = true; 611 if (totalHeight > position.height) 612 needsVSB = true; 613 if (needsVSB && totalWidth > position.width - ss.drawParameters["VertScrollBarSize"]) 614 needsHSB = true; 615 if (needsHSB && totalHeight > position.height - ss.drawParameters["HorizScrollBarSize"]) 616 needsVSB = true; 617 totalHeight -= _header.height; 618 int hPos, vPos; 619 if (horizSlider) 620 hPos = horizSlider.value; 621 if (vertSlider) 622 vPos = vertSlider.value; 623 if (needsVSB) { 624 const int maxvalue = needsHSB ? totalHeight - (position.height - ss.drawParameters["HorizScrollBarSize"] 625 - _header.height) : totalHeight - (position.height - _header.height); 626 627 const Box target = Box(position.right - ss.drawParameters["HorizScrollBarSize"] + 2, position.top, 628 position.right, needsHSB ? position.bottom - ss.drawParameters["VertScrollBarSize"] : position.bottom); 629 vertSlider = new VertScrollBar(maxvalue, source ~ "VSB", target); 630 vertSlider.setParent(this); 631 vertSlider.value = vPos; 632 vertSlider.onScrolling = &scrollBarEventOut; 633 } else vertSlider = null; 634 if (needsHSB) { 635 const int maxvalue = needsVSB ? totalWidth - (position.width - ss.drawParameters["VertScrollBarSize"]) : 636 totalWidth - position.width; 637 const Box target = Box(position.left, position.bottom - ss.drawParameters["VertScrollBarSize"] + 2, 638 needsVSB ? position.right - ss.drawParameters["HorizScrollBarSize"] : position.right, 639 position.bottom); 640 horizSlider = new HorizScrollBar(maxvalue, source ~ "VSB", target); 641 horizSlider.setParent(this); 642 horizSlider.value = hPos; 643 horizSlider.onScrolling = &scrollBarEventOut; 644 } else horizSlider = null; 645 } 646 protected void scrollBarEventOut(Event ev) { 647 draw; 648 } 649 /** 650 * Returns the absolute position of the element. 651 */ 652 public Box getAbsolutePosition(WindowElement sender) { 653 return parent.getAbsolutePosition(sender); 654 } 655 /** 656 * Gives focus to the element if applicable 657 */ 658 public void requestFocus(WindowElement sender) { 659 660 } 661 /** 662 * Sets the cursor to the given type on request. 663 */ 664 public void requestCursor(CursorType type) { 665 666 } 667 ///Draws a line. 668 public void drawLine(Point from, Point to, ubyte color) @trusted { 669 if (parent !is null) parent.drawLine(from, to, color); 670 } 671 ///Draws a line pattern. 672 public void drawLinePattern(Point from, Point to, ubyte[] pattern) @trusted { 673 if (parent !is null) parent.drawLinePattern(from, to, pattern); 674 } 675 ///Draws an empty rectangle. 676 public void drawBox(Box target, ubyte color) @trusted { 677 if (parent !is null) parent.drawBox(target, color); 678 } 679 ///Draws an empty rectangle with line patterns. 680 public void drawBoxPattern(Box target, ubyte[] pattern) @trusted { 681 if (parent !is null) parent.drawBoxPattern(target, pattern); 682 } 683 ///Draws a filled rectangle with a specified color. 684 public void drawFilledBox(Box target, ubyte color) @trusted { 685 if (parent !is null) parent.drawFilledBox(target, color); 686 } 687 ///Pastes a bitmap to the given point using blitter, which threats color #0 as transparency. 688 public void bitBLT(Point target, ABitmap source) @trusted { 689 if (parent !is null) parent.bitBLT(target, source); 690 } 691 ///Pastes a slice of a bitmap to the given point using blitter, which threats color #0 as transparency. 692 public void bitBLT(Point target, ABitmap source, Box slice) @trusted { 693 if (parent !is null) parent.bitBLT(target, source, slice); 694 } 695 ///Pastes a repeated bitmap pattern over the specified area. 696 public void bitBLTPattern(Box target, ABitmap pattern) @trusted { 697 if (parent !is null) parent.bitBLTPattern(target, pattern); 698 } 699 ///XOR blits a repeated bitmap pattern over the specified area. 700 public void xorBitBLT(Box target, ABitmap pattern) @trusted { 701 if (parent !is null) parent.xorBitBLT(target, pattern); 702 } 703 ///XOR blits a color index over a specified area. 704 public void xorBitBLT(Box target, ubyte color) @trusted { 705 if (parent !is null) parent.xorBitBLT(target, color); 706 } 707 ///Fills an area with the specified color. 708 public void fill(Point target, ubyte color, ubyte background = 0) @trusted { 709 if (parent !is null) parent.fill(target, color, background); 710 } 711 ///Draws a single line text within the given prelimiter. 712 public void drawTextSL(Box target, Text text, Point offset) @trusted { 713 if (parent !is null) parent.drawTextSL(target, text, offset); 714 } 715 ///Draws a multi line text within the given prelimiter. 716 public void drawTextML(Box target, Text text, Point offset) @trusted { 717 if (parent !is null) parent.drawTextML(target, text, offset); 718 } 719 ///Clears the area within the target 720 public void clearArea(Box target) @trusted { 721 if (parent !is null) parent.clearArea(target); 722 } 723 ///Passes mouse click event 724 public override void passMCE(MouseEventCommons mec, MouseClickEvent mce) { 725 ///TODO: Handle mouse click when in text editing mode 726 if (state != ElementState.Enabled) return; 727 if (vertSlider) { 728 const Box p = vertSlider.getPosition(); 729 if (p.isBetween(mce.x, mce.y)) { 730 vertSlider.passMCE(mec, mce); 731 return; 732 } 733 } 734 if (horizSlider) { 735 const Box p = horizSlider.getPosition(); 736 if (p.isBetween(mce.x, mce.y)) { 737 horizSlider.passMCE(mec, mce); 738 return; 739 } 740 } 741 if (!textArea.isBetween(mce.x, mce.y) && (flags & TEXTINPUT_EN)){ 742 inputHandler.stopTextInput(); 743 return; 744 } 745 746 //if (mce.button != MouseButton.Left && !mce.state) return; 747 748 mce.x -= position.left; 749 mce.y -= position.top; 750 if (entries.length && mce.y > _header.height && mce.button == MouseButton.Left && mce.state) { 751 textArea.top = position.top; 752 textArea.left = position.left; 753 mce.y -= _header.height; 754 int pixelsTotal = mce.y, pos; 755 if (vertSlider) ///calculate outscrolled area 756 pixelsTotal += vertSlider.value; 757 while (pos < entries.length) { 758 if (pixelsTotal > entries[pos].height) { 759 pixelsTotal -= entries[pos].height; 760 textArea.top += entries[pos].height; 761 if (pos + 1 < entries.length) 762 pos++; 763 } else { 764 break; 765 } 766 } 767 if (pos >= entries.length) { 768 selection = -1; 769 } else if (selection == pos && (flags & EDIT_EN)) { 770 //Calculate horizontal selection for Multicell editing if needed 771 /+if (flags & MULTICELL_EDIT_EN) { 772 773 } else {+/ 774 textArea.top += _header.height; 775 foreach (size_t i, ListViewItem.Field f ; entries[selection].fields) { 776 if (f.editable) { 777 hSelection = cast(int)i; 778 779 780 with (textArea) { 781 bottom = entries[selection].height + textArea.top; 782 right = _header.columnWidths[i] + textArea.left; 783 left = max(textArea.left, position.left); 784 top = max(textArea.top, position.top); 785 right = min(textArea.right, position.right); 786 bottom = min(textArea.bottom, position.bottom); 787 } 788 text = new Text(entries[selection][hSelection].text); 789 cursorPos = 0; 790 tselect = cast(int)text.charLength; 791 //oldText = text; 792 if (flags & MULTICELL_EDIT_EN) { 793 if (textArea.left < mce.x && textArea.right > mce.x) { 794 if (vertSlider) { 795 textArea.top -= vertSlider.value; 796 textArea.bottom = entries[selection].height + textArea.top; 797 } 798 if (horizSlider) textArea.left -= horizSlider.value; 799 inputHandler.startTextInput(this); 800 break; 801 } 802 } else { 803 if (vertSlider) { 804 textArea.top -= vertSlider.value; 805 textArea.bottom = entries[selection].height + textArea.top; 806 } 807 if (horizSlider) textArea.left -= horizSlider.value; 808 inputHandler.startTextInput(this); 809 break; 810 } 811 812 } 813 textArea.left += _header.columnWidths[i]; 814 } 815 //} 816 selection = pos; 817 } else 818 selection = pos; 819 820 if (onItemSelect !is null && selection != -1) 821 onItemSelect(new Event(this, entries[selection], EventType.Selection, SourceType.WindowElement)); 822 } else if (!entries.length) { 823 selection = -1; 824 } else { 825 super.passMCE(mec, mce); 826 } 827 828 draw(); 829 } 830 ///Passes mouse move event 831 public override void passMME(MouseEventCommons mec, MouseMotionEvent mme) { 832 if (state != ElementState.Enabled) return; 833 if (vertSlider) { 834 const Box p = vertSlider.getPosition(); 835 if (p.isBetween(mme.x, mme.y)) { 836 mme.x -= p.left - position.left; 837 mme.y -= p.top - position.top; 838 vertSlider.passMME(mec, mme); 839 return; 840 } 841 } 842 if (horizSlider) { 843 const Box p = horizSlider.getPosition(); 844 if (p.isBetween(mme.x, mme.y)) { 845 mme.x -= p.left - position.left; 846 mme.y -= p.top - position.top; 847 horizSlider.passMME(mec, mme); 848 return; 849 } 850 } 851 } 852 ///Passes mouse scroll event 853 public override void passMWE(MouseEventCommons mec, MouseWheelEvent mwe) { 854 mwe.x *= hScrollSpeed; 855 mwe.y *= vScrollSpeed; 856 if (state != ElementState.Enabled) return; 857 if (horizSlider) horizSlider.passMWE(mec, mwe); 858 if (vertSlider) vertSlider.passMWE(mec, mwe); 859 } 860 /** 861 * Puts a PopUpElement on the GUI. 862 */ 863 public void addPopUpElement(PopUpElement p) { 864 parent.addPopUpElement(p); 865 } 866 /** 867 * Puts a PopUpElement on the GUI at the given position. 868 */ 869 public void addPopUpElement(PopUpElement p, int x, int y) { 870 parent.addPopUpElement(p, x, y); 871 } 872 /** 873 * Ends the popup session and closes all popups. 874 */ 875 public void endPopUpSession(PopUpElement p) { 876 parent.endPopUpSession(p); 877 } 878 /** 879 * Closes a single popup element. 880 */ 881 public void closePopUp(PopUpElement p) { 882 parent.closePopUp(p); 883 } 884 //Interface `TextInputListener` starts here 885 /** 886 * Passes the inputted text to the target, alongside with a window ID and a timestamp. 887 */ 888 public void textInputEvent(uint timestamp, uint windowID, dstring text) { 889 import pixelperfectengine.system.etc : removeUnallowedSymbols; 890 /+if (allowedChars.length) { 891 text = removeUnallowedSymbols(text, allowedChars); 892 if (!text.length) return; 893 }+/ 894 if (tselect) { 895 this.text.removeChar(cursorPos, tselect); 896 tselect = 0; 897 for(int j ; j < text.length ; j++){ 898 this.text.insertChar(cursorPos++, text[j]); 899 } 900 } else if (flags & INSERT) { 901 for(int j ; j < text.length ; j++){ 902 this.text.overwriteChar(cursorPos++, text[j]); 903 } 904 } else { 905 for(int j ; j < text.length ; j++){ 906 this.text.insertChar(cursorPos++, text[j]); 907 } 908 } 909 if (filter) { 910 dstring s = this.text.text; 911 filter.use(s); 912 this.text.text = s; 913 cursorPos = min(cursorPos, cast(uint)this.text.charLength); 914 } 915 const int textPadding = getStyleSheet.drawParameters["TextSpacingSides"]; 916 const Coordinate textPos = Coordinate(textPadding,(position.height / 2) - (this.text.font.size / 2) , 917 position.width,position.height - textPadding); 918 const int x = this.text.getWidth(), cursorPixelPos = this.text.getWidth(0, cursorPos); 919 if(x > textPos.width) { 920 if(cursorPos == this.text.text.length) { 921 horizTextOffset = x - textPos.width; 922 } else if(cursorPixelPos < horizTextOffset) { //Test for whether the cursor would fall out from the current text area 923 horizTextOffset = cursorPixelPos; 924 } else if(cursorPixelPos > horizTextOffset + textPos.width) { 925 horizTextOffset = horizTextOffset + textPos.width; 926 } 927 } 928 draw(); 929 } 930 /** 931 * Passes text editing events to the target, alongside with a window ID and a timestamp. 932 */ 933 public void textEditingEvent(uint timestamp, uint windowID, dstring text, int start, int length) { 934 for (int i ; i < length ; i++) { 935 this.text.overwriteChar(start + i, text[i]); 936 } 937 cursorPos = start + length; 938 } 939 /** 940 * Passes text input key events to the target, e.g. cursor keys. 941 */ 942 public void textInputKeyEvent(uint timestamp, uint windowID, TextInputKey key, ushort modifier) { 943 switch(key) { 944 case TextInputKey.Enter: 945 entries[selection][hSelection].text = text; 946 inputHandler.stopTextInput(); 947 if(onTextInput !is null) 948 onTextInput(new CellEditEvent(this, entries[selection], selection, hSelection)); 949 //onTextInput(new Event(source, null, null, null, text, 0, EventType.T, null, this)); 950 break; 951 case TextInputKey.Escape: 952 //text = oldText; 953 inputHandler.stopTextInput(); 954 955 956 break; 957 case TextInputKey.Backspace: 958 if(cursorPos > 0){ 959 deleteCharacter(cursorPos - 1); 960 cursorPos--; 961 draw(); 962 } 963 break; 964 case TextInputKey.Delete: 965 if (tselect) { 966 for (int i ; i < tselect ; i++) { 967 deleteCharacter(cursorPos); 968 } 969 tselect = 0; 970 } else { 971 deleteCharacter(cursorPos); 972 } 973 draw(); 974 break; 975 case TextInputKey.CursorLeft: 976 if (modifier != KeyModifier.Shift) { 977 tselect = 0; 978 if(cursorPos > 0){ 979 --cursorPos; 980 draw(); 981 } 982 } 983 break; 984 case TextInputKey.CursorRight: 985 if (modifier != KeyModifier.Shift) { 986 tselect = 0; 987 if(cursorPos < text.charLength){ 988 ++cursorPos; 989 draw(); 990 } 991 } 992 break; 993 case TextInputKey.Home: 994 if (modifier != KeyModifier.Shift) { 995 tselect = 0; 996 cursorPos = 0; 997 draw(); 998 } 999 break; 1000 case TextInputKey.End: 1001 if (modifier != KeyModifier.Shift) { 1002 tselect = 0; 1003 cursorPos = cast(int)text.charLength; 1004 draw(); 1005 } 1006 break; 1007 case TextInputKey.Insert: 1008 flags ^= INSERT; 1009 draw(); 1010 break; 1011 default: 1012 break; 1013 } 1014 } 1015 /** 1016 * When called, the listener should drop all text input. 1017 */ 1018 public void dropTextInput() { 1019 flags &= ~TEXTINPUT_EN; 1020 draw; 1021 } 1022 /** 1023 * Called if text input should be initialized. 1024 */ 1025 public void initTextInput() { 1026 flags |= TEXTINPUT_EN; 1027 ListViewItem.Field f = opIndex(selection)[hSelection]; 1028 switch(f.textInputType) { 1029 default: 1030 filter = null; 1031 break; 1032 case TextInputFieldType.ASCIIText: 1033 filter = new ASCIITextFilter(); 1034 break; 1035 case TextInputFieldType.Decimal: 1036 filter = new DecimalFilter!true(); 1037 break; 1038 case TextInputFieldType.Integer: 1039 filter = new IntegerFilter!true(); 1040 break; 1041 case TextInputFieldType.DecimalP: 1042 filter = new DecimalFilter!false(); 1043 break; 1044 case TextInputFieldType.IntegerP: 1045 filter = new IntegerFilter!false(); 1046 break; 1047 case TextInputFieldType.Hex: 1048 filter = new HexadecimalFilter(); 1049 break; 1050 case TextInputFieldType.Oct: 1051 break; 1052 case TextInputFieldType.Bin: 1053 break; 1054 } 1055 } 1056 private void deleteCharacter(size_t n){ 1057 text.removeChar(n); 1058 } 1059 public int[2] getRasterSizes() { 1060 return parent.getRasterSizes(); 1061 } 1062 }