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