1 // Written in the D programming language. 2 3 /** 4 This module contains list widgets implementation. 5 6 Similar to lists implementation in Android UI API. 7 8 Synopsis: 9 10 ---- 11 import dlangui.widgets.lists; 12 13 ---- 14 15 Copyright: Vadim Lopatin, 2014 16 License: Boost License 1.0 17 Authors: Vadim Lopatin, coolreader.org@gmail.com 18 */ 19 module dlangui.widgets.lists; 20 21 import dlangui.widgets.widget; 22 import dlangui.widgets.controls; 23 import dlangui.widgets.scrollbar; 24 import dlangui.widgets.layouts; 25 import dlangui.core.signals; 26 27 28 /** interface - slot for onAdapterChangeListener */ 29 interface OnAdapterChangeHandler { 30 void onAdapterChange(ListAdapter source); 31 } 32 33 34 /// list widget adapter provides items for list widgets 35 interface ListAdapter { 36 /// returns number of widgets in list 37 @property int itemCount() const; 38 /// return list item widget by item index 39 Widget itemWidget(int index); 40 /// return list item's state flags 41 uint itemState(int index) const; 42 /// set one or more list item's state flags, returns updated state 43 uint setItemState(int index, uint flags); 44 /// reset one or more list item's state flags, returns updated state 45 uint resetItemState(int index, uint flags); 46 /// returns integer item id by index (if supported) 47 int itemId(int index) const; 48 /// returns string item id by index (if supported) 49 string itemStringId(int index) const; 50 51 /// remove all items 52 void clear(); 53 54 /// connect adapter change handler 55 ListAdapter connect(OnAdapterChangeHandler handler); 56 /// disconnect adapter change handler 57 ListAdapter disconnect(OnAdapterChangeHandler handler); 58 59 /// called when theme is changed 60 void onThemeChanged(); 61 62 /// return true to receive mouse events 63 @property bool wantMouseEvents(); 64 /// return true to receive keyboard events 65 @property bool wantKeyEvents(); 66 } 67 68 /// List adapter for simple list of widget instances 69 class ListAdapterBase : ListAdapter { 70 /** Handle items change */ 71 protected Signal!OnAdapterChangeHandler adapterChanged; 72 73 /// connect adapter change handler 74 override ListAdapter connect(OnAdapterChangeHandler handler) { 75 adapterChanged.connect(handler); 76 return this; 77 } 78 /// disconnect adapter change handler 79 override ListAdapter disconnect(OnAdapterChangeHandler handler) { 80 adapterChanged.disconnect(handler); 81 return this; 82 } 83 /// returns integer item id by index (if supported) 84 override int itemId(int index) const { 85 return 0; 86 } 87 /// returns string item id by index (if supported) 88 override string itemStringId(int index) const { 89 return null; 90 } 91 92 /// returns number of widgets in list 93 override @property int itemCount() const { 94 // override it 95 return 0; 96 } 97 98 /// return list item widget by item index 99 override Widget itemWidget(int index) { 100 // override it 101 return null; 102 } 103 104 /// return list item's state flags 105 override uint itemState(int index) const { 106 // override it 107 return State.Enabled; 108 } 109 /// set one or more list item's state flags, returns updated state 110 override uint setItemState(int index, uint flags) { 111 return 0; 112 } 113 /// reset one or more list item's state flags, returns updated state 114 override uint resetItemState(int index, uint flags) { 115 return 0; 116 } 117 118 /// remove all items 119 override void clear() { 120 } 121 122 /// notify listeners about list items changes 123 void updateViews() { 124 if (adapterChanged.assigned) 125 adapterChanged.emit(this); 126 } 127 128 /// called when theme is changed 129 void onThemeChanged() { 130 } 131 132 /// return true to receive mouse events 133 override @property bool wantMouseEvents() { 134 return false; 135 } 136 137 /// return true to receive keyboard events 138 override @property bool wantKeyEvents() { 139 return false; 140 } 141 } 142 143 /// List adapter for simple list of widget instances 144 class WidgetListAdapter : ListAdapterBase { 145 private WidgetList _widgets; 146 /// list of widgets to display 147 @property ref const(WidgetList) widgets() { return _widgets; } 148 /// returns number of widgets in list 149 @property override int itemCount() const { 150 return _widgets.count; 151 } 152 /// return list item widget by item index 153 override Widget itemWidget(int index) { 154 return _widgets.get(index); 155 } 156 /// return list item's state flags 157 override uint itemState(int index) const { 158 return _widgets.get(index).state; 159 } 160 /// set one or more list item's state flags, returns updated state 161 override uint setItemState(int index, uint flags) { 162 return _widgets.get(index).setState(flags).state; 163 } 164 /// reset one or more list item's state flags, returns updated state 165 override uint resetItemState(int index, uint flags) { 166 return _widgets.get(index).resetState(flags).state; 167 } 168 /// add item 169 WidgetListAdapter add(Widget item, int index = -1) { 170 _widgets.insert(item, index); 171 updateViews(); 172 return this; 173 } 174 /// remove item 175 WidgetListAdapter remove(int index) { 176 auto item = _widgets.remove(index); 177 destroy(item); 178 updateViews(); 179 return this; 180 } 181 /// remove all items 182 override void clear() { 183 _widgets.clear(); 184 updateViews(); 185 } 186 /// called when theme is changed 187 override void onThemeChanged() { 188 super.onThemeChanged(); 189 foreach(w; _widgets) 190 w.onThemeChanged(); 191 } 192 ~this() { 193 //Log.d("Destroying WidgetListAdapter"); 194 } 195 196 /// return true to receive mouse events 197 override @property bool wantMouseEvents() { 198 return true; 199 } 200 } 201 202 /** List adapter providing strings only. */ 203 class StringListAdapterBase : ListAdapterBase { 204 protected UIStringCollection _items; 205 protected uint[] _states; 206 protected int[] _intIds; 207 protected string[] _stringIds; 208 protected string[] _iconIds; 209 protected int _lastItemIndex; 210 211 /** create empty string list adapter. */ 212 this() { 213 _lastItemIndex = -1; 214 } 215 216 /** Init with array of string resource IDs. */ 217 this(string[] items) { 218 _items.addAll(items); 219 _intIds.length = items.length; 220 _stringIds.length = items.length; 221 _iconIds.length = items.length; 222 _lastItemIndex = -1; 223 updateStatesLength(); 224 } 225 226 /** Init with array of unicode strings. */ 227 this(dstring[] items) { 228 _items.addAll(items); 229 _intIds.length = items.length; 230 _stringIds.length = items.length; 231 _iconIds.length = items.length; 232 _lastItemIndex = -1; 233 updateStatesLength(); 234 } 235 236 /** Init with array of StringListValue. */ 237 this(StringListValue[] items) { 238 _intIds.length = items.length; 239 _stringIds.length = items.length; 240 _iconIds.length = items.length; 241 for (int i = 0; i < items.length; i++) { 242 _items.add(items[i].label); 243 _intIds[i] = items[i].intId; 244 _stringIds[i] = items[i].stringId; 245 _iconIds[i] = items[i].iconId; 246 } 247 _lastItemIndex = -1; 248 updateStatesLength(); 249 } 250 251 /// remove all items 252 override void clear() { 253 _items.clear(); 254 updateStatesLength(); 255 updateViews(); 256 } 257 258 /// remove item by index 259 StringListAdapterBase remove(int index) { 260 if (index < 0 || index >= _items.length) 261 return this; 262 for (int i = 0; i < _items.length - 1; i++) { 263 _intIds[i] = _intIds[i + 1]; 264 _stringIds[i] = _stringIds[i + 1]; 265 _iconIds[i] = _iconIds[i + 1]; 266 _states[i] = _states[i + 1]; 267 } 268 _items.remove(index); 269 _intIds.length = items.length; 270 _states.length = _items.length; 271 _stringIds.length = items.length; 272 _iconIds.length = items.length; 273 updateViews(); 274 return this; 275 } 276 277 /// add new item 278 StringListAdapterBase add(UIString item, int index = -1) { 279 if (index < 0 || index > _items.length) 280 index = _items.length; 281 _items.add(item, index); 282 _intIds.length = items.length; 283 _states.length = _items.length; 284 _stringIds.length = items.length; 285 _iconIds.length = items.length; 286 for (int i = _items.length - 1; i > index; i--) { 287 _intIds[i] = _intIds[i - 1]; 288 _stringIds[i] = _stringIds[i - 1]; 289 _iconIds[i] = _iconIds[i - 1]; 290 _states[i] = _states[i - 1]; 291 } 292 _intIds[index] = 0; 293 _stringIds[index] = null; 294 _iconIds[index] = null; 295 _states[index] = State.Enabled; 296 updateViews(); 297 return this; 298 } 299 /// add new string resource item 300 StringListAdapterBase add(string item, int index = -1) { 301 return add(UIString.fromId(item), index); 302 } 303 /// add new raw dstring item 304 StringListAdapterBase add(dstring item, int index = -1) { 305 return add(UIString.fromRaw(item), index); 306 } 307 308 /** Access to items collection. */ 309 @property ref const(UIStringCollection) items() { return _items; } 310 311 /** Replace items collection. */ 312 @property StringListAdapterBase items(dstring[] values) { 313 _items = values; 314 _intIds.length = items.length; 315 _states.length = _items.length; 316 _stringIds.length = items.length; 317 _iconIds.length = items.length; 318 for (int i = 0; i < _items.length; i++) { 319 _intIds[i] = 0; 320 _stringIds[i] = null; 321 _iconIds[i] = null; 322 _states[i] = State.Enabled; 323 } 324 updateViews(); 325 return this; 326 } 327 328 /** Replace items collection. */ 329 @property StringListAdapterBase items(UIString[] values) { 330 _items = values; 331 _intIds.length = items.length; 332 _states.length = _items.length; 333 _stringIds.length = items.length; 334 _iconIds.length = items.length; 335 for (int i = 0; i < _items.length; i++) { 336 _intIds[i] = 0; 337 _stringIds[i] = null; 338 _iconIds[i] = null; 339 _states[i] = State.Enabled; 340 } 341 updateViews(); 342 return this; 343 } 344 345 /** Replace items collection. */ 346 @property StringListAdapterBase items(StringListValue[] values) { 347 _items = values; 348 _intIds.length = items.length; 349 _states.length = _items.length; 350 _stringIds.length = items.length; 351 _iconIds.length = items.length; 352 for (int i = 0; i < _items.length; i++) { 353 _intIds[i] = values[i].intId; 354 _stringIds[i] = values[i].stringId; 355 _iconIds[i] = values[i].iconId; 356 _states[i] = State.Enabled; 357 } 358 updateViews(); 359 return this; 360 } 361 362 /// returns number of widgets in list 363 @property override int itemCount() const { 364 return _items.length; 365 } 366 367 /// returns integer item id by index (if supported) 368 override int itemId(int index) const { 369 return index >= 0 && index < _intIds.length ? _intIds[index] : 0; 370 } 371 372 /// returns string item id by index (if supported) 373 override string itemStringId(int index) const { 374 return index >= 0 && index < _stringIds.length ? _stringIds[index] : null; 375 } 376 377 protected void updateStatesLength() { 378 if (_states.length < _items.length) { 379 int oldlen = cast(int)_states.length; 380 _states.length = _items.length; 381 for (int i = oldlen; i < _items.length; i++) 382 _states[i] = State.Enabled; 383 } 384 if (_intIds.length < items.length) 385 _intIds.length = items.length; 386 if (_stringIds.length < items.length) 387 _stringIds.length = items.length; 388 if (_iconIds.length < items.length) 389 _iconIds.length = items.length; 390 } 391 392 /// return list item's state flags 393 override uint itemState(int index) const { 394 if (index < 0 || index >= _items.length) 395 return 0; 396 return _states[index]; 397 } 398 399 /// set one or more list item's state flags, returns updated state 400 override uint setItemState(int index, uint flags) { 401 updateStatesLength(); 402 _states[index] |= flags; 403 return _states[index]; 404 } 405 /// reset one or more list item's state flags, returns updated state 406 override uint resetItemState(int index, uint flags) { 407 updateStatesLength(); 408 _states[index] &= ~flags; 409 return _states[index]; 410 } 411 412 ~this() { 413 } 414 } 415 416 /** List adapter providing strings only. */ 417 class StringListAdapter : StringListAdapterBase { 418 protected TextWidget _widget; 419 420 /** create empty string list adapter. */ 421 this() { 422 super(); 423 } 424 425 /** Init with array of string resource IDs. */ 426 this(string[] items) { 427 super(items); 428 } 429 430 /** Init with array of unicode strings. */ 431 this(dstring[] items) { 432 super(items); 433 } 434 435 /** Init with array of StringListValue. */ 436 this(StringListValue[] items) { 437 super(items); 438 } 439 440 /// return list item widget by item index 441 override Widget itemWidget(int index) { 442 updateStatesLength(); 443 if (_widget is null) { 444 _widget = new TextWidget("STRING_LIST_ITEM"); 445 _widget.styleId = STYLE_LIST_ITEM; 446 } else { 447 if (index == _lastItemIndex) 448 return _widget; 449 } 450 // update widget 451 _widget.text = _items.get(index); 452 _widget.state = _states[index]; 453 _lastItemIndex = index; 454 return _widget; 455 } 456 457 /// called when theme is changed 458 override void onThemeChanged() { 459 super.onThemeChanged(); 460 if (_widget) 461 _widget.onThemeChanged(); 462 } 463 464 /// set one or more list item's state flags, returns updated state 465 override uint setItemState(int index, uint flags) { 466 uint res = super.setItemState(index, flags); 467 if (_widget !is null && _lastItemIndex == index) 468 _widget.state = res; 469 return res; 470 } 471 472 473 474 /// reset one or more list item's state flags, returns updated state 475 override uint resetItemState(int index, uint flags) { 476 uint res = super.resetItemState(index, flags); 477 if (_widget !is null && _lastItemIndex == index) 478 _widget.state = res; 479 return res; 480 } 481 482 ~this() { 483 if (_widget) 484 destroy(_widget); 485 } 486 } 487 488 /** List adapter providing strings with icons. */ 489 class IconStringListAdapter : StringListAdapterBase { 490 protected HorizontalLayout _widget; 491 protected TextWidget _textWidget; 492 protected ImageWidget _iconWidget; 493 494 /** create empty string list adapter. */ 495 this() { 496 super(); 497 } 498 499 /** Init with array of StringListValue. */ 500 this(StringListValue[] items) { 501 super(items); 502 } 503 504 /// return list item widget by item index 505 override Widget itemWidget(int index) { 506 updateStatesLength(); 507 if (_widget is null) { 508 _widget = new HorizontalLayout("ICON_STRING_LIST_ITEM"); 509 _widget.styleId = STYLE_LIST_ITEM; 510 _textWidget = new TextWidget("label"); 511 _iconWidget = new ImageWidget("icon"); 512 _widget.addChild(_iconWidget); 513 _widget.addChild(_textWidget); 514 } else { 515 if (index == _lastItemIndex) 516 return _widget; 517 } 518 // update widget 519 _textWidget.text = _items.get(index); 520 _textWidget.state = _states[index]; 521 if (_iconIds[index]) { 522 _iconWidget.visibility = Visibility.Visible; 523 _iconWidget.drawableId = _iconIds[index]; 524 } else { 525 _iconWidget.visibility = Visibility.Gone; 526 } 527 _lastItemIndex = index; 528 return _widget; 529 } 530 531 /// called when theme is changed 532 override void onThemeChanged() { 533 super.onThemeChanged(); 534 if (_widget) 535 _widget.onThemeChanged(); 536 } 537 538 /// set one or more list item's state flags, returns updated state 539 override uint setItemState(int index, uint flags) { 540 uint res = super.setItemState(index, flags); 541 if (_widget !is null && _lastItemIndex == index) { 542 _widget.state = res; 543 _textWidget.state = res; 544 } 545 return res; 546 } 547 548 /// reset one or more list item's state flags, returns updated state 549 override uint resetItemState(int index, uint flags) { 550 uint res = super.resetItemState(index, flags); 551 if (_widget !is null && _lastItemIndex == index) { 552 _widget.state = res; 553 _textWidget.state = res; 554 } 555 return res; 556 } 557 558 ~this() { 559 if (_widget) 560 destroy(_widget); 561 } 562 } 563 564 /** interface - slot for onItemSelectedListener */ 565 interface OnItemSelectedHandler { 566 bool onItemSelected(Widget source, int itemIndex); 567 } 568 569 /** interface - slot for onItemClickListener */ 570 interface OnItemClickHandler { 571 bool onItemClick(Widget source, int itemIndex); 572 } 573 574 575 /** List widget - shows content as hori*/ 576 class ListWidget : WidgetGroup, OnScrollHandler, OnAdapterChangeHandler { 577 578 /** Handle selection change. */ 579 Signal!OnItemSelectedHandler itemSelected; 580 /** Handle item click / activation (e.g. Space or Enter key press and mouse double click) */ 581 Signal!OnItemClickHandler itemClick; 582 583 protected Orientation _orientation = Orientation.Vertical; 584 /// returns linear layout orientation (Vertical, Horizontal) 585 @property Orientation orientation() { return _orientation; } 586 /// sets linear layout orientation 587 @property ListWidget orientation(Orientation value) { 588 _orientation = value; 589 _scrollbar.orientation = value; 590 requestLayout(); 591 return this; 592 } 593 594 protected Rect[] _itemRects; 595 protected Point[] _itemSizes; 596 protected bool _needScrollbar; 597 protected Point _sbsz; // scrollbar size 598 protected ScrollBar _scrollbar; 599 protected int _lastMeasureWidth; 600 protected int _lastMeasureHeight; 601 602 /// first visible item index 603 protected int _firstVisibleItem; 604 /// scroll position - offset of scroll area 605 protected int _scrollPosition; 606 /// maximum scroll position 607 protected int _maxScrollPosition; 608 /// client area rectangle (counting padding, margins, and scrollbar) 609 protected Rect _clientRc; 610 /// total height of all items for Vertical orientation, or width for Horizontal 611 protected int _totalSize; 612 /// item with Hover state, -1 if no such item 613 protected int _hoverItemIndex; 614 /// item with Selected state, -1 if no such item 615 protected int _selectedItemIndex; 616 617 /// when true, mouse hover selects underlying item 618 protected bool _selectOnHover; 619 /// when true, mouse hover selects underlying item 620 @property bool selectOnHover() { return _selectOnHover; } 621 /// when true, mouse hover selects underlying item 622 @property ListWidget selectOnHover(bool select) { _selectOnHover = select; return this; } 623 624 /// if true, generate itemClicked on mouse down instead mouse up event 625 protected bool _clickOnButtonDown; 626 627 /// returns rectangle for item (not scrolled, first item starts at 0,0) 628 Rect itemRectNoScroll(int index) { 629 if (index < 0 || index >= _itemRects.length) 630 return Rect.init; 631 Rect res; 632 res = _itemRects[index]; 633 return res; 634 } 635 636 /// returns rectangle for item (scrolled) 637 Rect itemRect(int index) { 638 if (index < 0 || index >= _itemRects.length) 639 return Rect.init; 640 Rect res = itemRectNoScroll(index); 641 if (_orientation == Orientation.Horizontal) { 642 res.left -= _scrollPosition; 643 res.right -= _scrollPosition; 644 } else { 645 res.top -= _scrollPosition; 646 res.bottom -= _scrollPosition; 647 } 648 return res; 649 } 650 651 /// returns item index by 0-based offset from top/left of list content 652 int itemByPosition(int pos) { 653 return 0; 654 } 655 656 protected ListAdapter _adapter; 657 /// when true, need to destroy adapter on list destroy 658 protected bool _ownAdapter; 659 660 /// get adapter 661 @property ListAdapter adapter() { return _adapter; } 662 /// set adapter 663 @property ListWidget adapter(ListAdapter adapter) { 664 if (_adapter is adapter) 665 return this; // no changes 666 if (_adapter) 667 _adapter.disconnect(this); 668 if (_adapter !is null && _ownAdapter) 669 destroy(_adapter); 670 _adapter = adapter; 671 if (_adapter) 672 _adapter.connect(this); 673 _ownAdapter = false; 674 onAdapterChange(_adapter); 675 return this; 676 } 677 /// set adapter, which will be owned by list (destroy will be called for adapter on widget destroy) 678 @property ListWidget ownAdapter(ListAdapter adapter) { 679 if (_adapter is adapter) 680 return this; // no changes 681 if (_adapter) 682 _adapter.disconnect(this); 683 if (_adapter !is null && _ownAdapter) 684 destroy(_adapter); 685 _adapter = adapter; 686 if (_adapter) 687 _adapter.connect(this); 688 _ownAdapter = true; 689 onAdapterChange(_adapter); 690 return this; 691 } 692 693 /// returns number of widgets in list 694 @property int itemCount() { 695 if (_adapter !is null) 696 return _adapter.itemCount; 697 return 0; 698 } 699 700 /// return list item widget by item index 701 Widget itemWidget(int index) { 702 if (_adapter !is null) 703 return _adapter.itemWidget(index); 704 return null; 705 } 706 707 /// returns true if item with corresponding index is enabled 708 bool itemEnabled(int index) { 709 if (_adapter !is null && index >= 0 && index < itemCount) 710 return (_adapter.itemState(index) & State.Enabled) != 0; 711 return false; 712 } 713 714 /// empty parameter list constructor - for usage by factory 715 this() { 716 this(null); 717 } 718 /// create with ID parameter 719 this(string ID, Orientation orientation = Orientation.Vertical) { 720 super(ID); 721 _orientation = orientation; 722 focusable = true; 723 _hoverItemIndex = -1; 724 _selectedItemIndex = -1; 725 _scrollbar = new ScrollBar("listscroll", orientation); 726 _scrollbar.visibility = Visibility.Gone; 727 _scrollbar.scrollEvent = &onScrollEvent; 728 addChild(_scrollbar); 729 } 730 731 protected void setHoverItem(int index) { 732 if (_hoverItemIndex == index) 733 return; 734 if (_hoverItemIndex != -1) { 735 _adapter.resetItemState(_hoverItemIndex, State.Hovered); 736 invalidate(); 737 } 738 _hoverItemIndex = index; 739 if (_hoverItemIndex != -1) { 740 _adapter.setItemState(_hoverItemIndex, State.Hovered); 741 invalidate(); 742 } 743 } 744 745 /// item list is changed 746 override void onAdapterChange(ListAdapter source) { 747 requestLayout(); 748 } 749 750 /// override to handle change of selection 751 protected void selectionChanged(int index, int previouslySelectedItem = -1) { 752 if (itemSelected.assigned) 753 itemSelected(this, index); 754 } 755 756 /// override to handle mouse up on item 757 protected void itemClicked(int index) { 758 if (itemClick.assigned) 759 itemClick(this, index); 760 } 761 762 /// allow to override state for updating of items 763 // currently used to treat main menu items with opened submenu as focused 764 @property protected uint overrideStateForItem() { 765 return state; 766 } 767 768 protected void updateSelectedItemFocus() { 769 if (_selectedItemIndex != -1) { 770 if ((_adapter.itemState(_selectedItemIndex) & State.Focused) != (overrideStateForItem & State.Focused)) { 771 if (overrideStateForItem & State.Focused) 772 _adapter.setItemState(_selectedItemIndex, State.Focused); 773 else 774 _adapter.resetItemState(_selectedItemIndex, State.Focused); 775 invalidate(); 776 } 777 } 778 } 779 780 /// override to handle focus changes 781 override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) { 782 updateSelectedItemFocus(); 783 } 784 785 /// ensure selected item is visible (scroll if necessary) 786 void makeSelectionVisible() { 787 if (_selectedItemIndex < 0) 788 return; // no selection 789 if (needLayout) { 790 _makeSelectionVisibleOnNextLayout = true; 791 return; 792 } 793 makeItemVisible(_selectedItemIndex); 794 } 795 796 protected bool _makeSelectionVisibleOnNextLayout; 797 /// ensure item is visible 798 void makeItemVisible(int itemIndex) { 799 if (itemIndex < 0 || itemIndex >= itemCount) 800 return; // no selection 801 802 Rect viewrc = Rect(0, 0, _clientRc.width, _clientRc.height); 803 Rect scrolledrc = itemRect(itemIndex); 804 if (scrolledrc.isInsideOf(viewrc)) // completely visible 805 return; 806 int delta = 0; 807 if (_orientation == Orientation.Vertical) { 808 if (scrolledrc.top < viewrc.top) 809 delta = scrolledrc.top - viewrc.top; 810 else if (scrolledrc.bottom > viewrc.bottom) 811 delta = scrolledrc.bottom - viewrc.bottom; 812 } else { 813 if (scrolledrc.left < viewrc.left) 814 delta = scrolledrc.left - viewrc.left; 815 else if (scrolledrc.right > viewrc.right) 816 delta = scrolledrc.right - viewrc.right; 817 } 818 int newPosition = _scrollPosition + delta; 819 _scrollbar.position = newPosition; 820 _scrollPosition = newPosition; 821 invalidate(); 822 } 823 824 /// move selection 825 bool moveSelection(int direction, bool wrapAround = true) { 826 if (itemCount <= 0) 827 return false; 828 int maxAttempts = itemCount - 1; 829 int index = _selectedItemIndex; 830 for (int i = 0; i < maxAttempts; i++) { 831 int newIndex = 0; 832 if (index < 0) { 833 // no previous selection 834 if (direction > 0) 835 newIndex = wrapAround ? 0 : itemCount - 1; 836 else 837 newIndex = wrapAround ? itemCount - 1 : 0; 838 } else { 839 // step 840 newIndex = index + direction; 841 } 842 if (newIndex < 0) 843 newIndex = wrapAround ? itemCount - 1 : 0; 844 else if (newIndex >= itemCount) 845 newIndex = wrapAround ? 0 : itemCount - 1; 846 if (newIndex != index) { 847 if (selectItem(newIndex)) { 848 selectionChanged(_selectedItemIndex, index); 849 return true; 850 } 851 index = newIndex; 852 } 853 } 854 return true; 855 } 856 857 bool selectItem(int index, int disabledItemsSkipDirection) { 858 //debug Log.d("selectItem ", index, " skipDirection=", disabledItemsSkipDirection); 859 if (index == -1 || disabledItemsSkipDirection == 0) 860 return selectItem(index); 861 int maxAttempts = itemCount; 862 for (int i = 0; i < maxAttempts; i++) { 863 if (selectItem(index)) 864 return true; 865 index += disabledItemsSkipDirection > 0 ? 1 : -1; 866 if (index < 0) 867 index = itemCount - 1; 868 if (index >= itemCount) 869 index = 0; 870 } 871 return false; 872 } 873 874 /** Selected item index. */ 875 @property int selectedItemIndex() { 876 return _selectedItemIndex; 877 } 878 879 @property void selectedItemIndex(int index) { 880 selectItem(index); 881 } 882 883 bool selectItem(int index) { 884 //debug Log.d("selectItem ", index); 885 if (_selectedItemIndex == index) { 886 updateSelectedItemFocus(); 887 makeSelectionVisible(); 888 return true; 889 } 890 if (index != -1 && !itemEnabled(index)) 891 return false; 892 if (_selectedItemIndex != -1) { 893 _adapter.resetItemState(_selectedItemIndex, State.Selected | State.Focused); 894 invalidate(); 895 } 896 _selectedItemIndex = index; 897 if (_selectedItemIndex != -1) { 898 makeSelectionVisible(); 899 _adapter.setItemState(_selectedItemIndex, State.Selected | (overrideStateForItem & State.Focused)); 900 invalidate(); 901 } 902 return true; 903 } 904 905 ~this() { 906 if (_adapter) 907 _adapter.disconnect(this); 908 //Log.d("Destroying List ", _id); 909 if (_adapter !is null && _ownAdapter) 910 destroy(_adapter); 911 _adapter = null; 912 } 913 914 /// handle scroll event 915 override bool onScrollEvent(AbstractSlider source, ScrollEvent event) { 916 int newPosition = _scrollPosition; 917 if (event.action == ScrollAction.SliderMoved) { 918 // scroll 919 newPosition = event.position; 920 } else { 921 // use default handler for page/line up/down events 922 newPosition = event.defaultUpdatePosition(); 923 } 924 if (_scrollPosition != newPosition) { 925 _scrollPosition = newPosition; 926 if (_scrollPosition > _maxScrollPosition) 927 _scrollPosition = _maxScrollPosition; 928 if (_scrollPosition < 0) 929 _scrollPosition = 0; 930 invalidate(); 931 } 932 return true; 933 } 934 935 /// handle theme change: e.g. reload some themed resources 936 override void onThemeChanged() { 937 super.onThemeChanged(); 938 _scrollbar.onThemeChanged(); 939 for (int i = 0; i < itemCount; i++) { 940 Widget w = itemWidget(i); 941 w.onThemeChanged(); 942 } 943 if (_adapter) 944 _adapter.onThemeChanged(); 945 } 946 947 /// sets minimum size for the list, override to change 948 Point minimumVisibleContentSize() { 949 if (_orientation == Orientation.Vertical) 950 return Point(measureMinChildrenSize().x, 100); 951 else 952 return Point(100, measureMinChildrenSize().y); 953 } 954 955 protected Point measureMinChildrenSize() { 956 // measure children 957 Point sz; 958 for (int i = 0; i < itemCount; i++) { 959 Widget w = itemWidget(i); 960 if (w is null || w.visibility == Visibility.Gone) 961 continue; 962 963 w.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED); 964 if (_orientation == Orientation.Vertical) { 965 // Vertical 966 if (sz.x < w.measuredWidth) 967 sz.x = w.measuredWidth; 968 sz.y += w.measuredHeight; 969 } else { 970 // Horizontal 971 if (sz.y < w.measuredHeight) 972 sz.y = w.measuredHeight; 973 sz.x += w.measuredWidth; 974 } 975 } 976 return sz; 977 } 978 979 /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 980 override void measure(int parentWidth, int parentHeight) { 981 if (visibility == Visibility.Gone) { 982 _measuredWidth = _measuredHeight = 0; 983 return; 984 } 985 if (_itemSizes.length < itemCount) 986 _itemSizes.length = itemCount; 987 Rect m = margins; 988 Rect p = padding; 989 990 // set widget area to small when first measure 991 if (parentWidth == SIZE_UNSPECIFIED && parentHeight == SIZE_UNSPECIFIED) 992 { 993 Point sz = minimumVisibleContentSize; 994 measuredContent(parentWidth, parentHeight, sz.x, sz.y); 995 return; 996 } 997 998 // calc size constraints for children 999 int pwidth = parentWidth; 1000 int pheight = parentHeight; 1001 if (parentWidth != SIZE_UNSPECIFIED) 1002 pwidth -= m.left + m.right + p.left + p.right; 1003 if (parentHeight != SIZE_UNSPECIFIED) 1004 pheight -= m.top + m.bottom + p.top + p.bottom; 1005 1006 bool oldNeedLayout = _needLayout; 1007 Visibility oldScrollbarVisibility = _scrollbar.visibility; 1008 1009 _scrollbar.visibility = Visibility.Visible; 1010 _scrollbar.measure(pwidth, pheight); 1011 1012 _lastMeasureWidth = pwidth; 1013 _lastMeasureHeight = pheight; 1014 1015 int sbsize = _orientation == Orientation.Vertical ? _scrollbar.measuredWidth : _scrollbar.measuredHeight; 1016 // measure children 1017 Point sz; 1018 _sbsz.destroy(); 1019 for (int i = 0; i < itemCount; i++) { 1020 Widget w = itemWidget(i); 1021 if (w is null || w.visibility == Visibility.Gone) { 1022 _itemSizes[i].x = _itemSizes[i].y = 0; 1023 continue; 1024 } 1025 w.measure(pwidth, pheight); 1026 _itemSizes[i].x = w.measuredWidth; 1027 _itemSizes[i].y = w.measuredHeight; 1028 if (_orientation == Orientation.Vertical) { 1029 // Vertical 1030 if (sz.x < w.measuredWidth) 1031 sz.x = w.measuredWidth; 1032 sz.y += w.measuredHeight; 1033 } else { 1034 // Horizontal 1035 if (sz.y < w.measuredHeight) 1036 sz.y = w.measuredHeight; 1037 sz.x += w.measuredWidth; 1038 } 1039 } 1040 _needScrollbar = false; 1041 if (_orientation == Orientation.Vertical) { 1042 if (pheight != SIZE_UNSPECIFIED && sz.y > pheight) { 1043 // need scrollbar 1044 if (pwidth != SIZE_UNSPECIFIED) { 1045 pwidth -= sbsize; 1046 _sbsz.x = sbsize; 1047 _needScrollbar = true; 1048 } 1049 } 1050 } else { 1051 if (pwidth != SIZE_UNSPECIFIED && sz.x > pwidth) { 1052 // need scrollbar 1053 if (pheight != SIZE_UNSPECIFIED) { 1054 pheight -= sbsize; 1055 _sbsz.y = sbsize; 1056 _needScrollbar = true; 1057 } 1058 } 1059 } 1060 if (_needScrollbar) { 1061 // recalculate with scrollbar 1062 sz.x = sz.y = 0; 1063 for (int i = 0; i < itemCount; i++) { 1064 Widget w = itemWidget(i); 1065 if (w is null || w.visibility == Visibility.Gone) 1066 continue; 1067 w.measure(pwidth, pheight); 1068 _itemSizes[i].x = w.measuredWidth; 1069 _itemSizes[i].y = w.measuredHeight; 1070 if (_orientation == Orientation.Vertical) { 1071 // Vertical 1072 if (sz.x < w.measuredWidth) 1073 sz.x = w.measuredWidth; 1074 sz.y += w.measuredHeight; 1075 } else { 1076 // Horizontal 1077 w.measure(pwidth, pheight); 1078 if (sz.y < w.measuredHeight) 1079 sz.y = w.measuredHeight; 1080 sz.x += w.measuredWidth; 1081 } 1082 } 1083 } 1084 measuredContent(parentWidth, parentHeight, sz.x + _sbsz.x, sz.y + _sbsz.y); 1085 1086 if (_scrollbar.visibility == oldScrollbarVisibility) { 1087 _needLayout = oldNeedLayout; 1088 _scrollbar.cancelLayout(); 1089 } 1090 } 1091 1092 1093 protected void updateItemPositions() { 1094 Rect r; 1095 int p = 0; 1096 for (int i = 0; i < itemCount; i++) { 1097 if (_itemSizes[i].x == 0 && _itemSizes[i].y == 0) 1098 continue; 1099 if (_orientation == Orientation.Vertical) { 1100 // Vertical 1101 int w = _clientRc.width; 1102 int h = _itemSizes[i].y; 1103 r.top = p; 1104 r.bottom = p + h; 1105 r.left = 0; 1106 r.right = w; 1107 _itemRects[i] = r; 1108 p += h; 1109 } else { 1110 // Horizontal 1111 int h = _clientRc.height; 1112 int w = _itemSizes[i].x; 1113 r.top = 0; 1114 r.bottom = h; 1115 r.left = p; 1116 r.right = p + w; 1117 _itemRects[i] = r; 1118 p += w; 1119 } 1120 } 1121 _totalSize = p; 1122 if (_needScrollbar) { 1123 if (_orientation == Orientation.Vertical) { 1124 _scrollbar.setRange(0, p); 1125 _scrollbar.pageSize = _clientRc.height; 1126 _scrollbar.position = _scrollPosition; 1127 } else { 1128 _scrollbar.setRange(0, p); 1129 _scrollbar.pageSize = _clientRc.width; 1130 _scrollbar.position = _scrollPosition; 1131 } 1132 } 1133 /// maximum scroll position 1134 if (_orientation == Orientation.Vertical) { 1135 _maxScrollPosition = _totalSize - _clientRc.height; 1136 if (_maxScrollPosition < 0) 1137 _maxScrollPosition = 0; 1138 } else { 1139 _maxScrollPosition = _totalSize - _clientRc.width; 1140 if (_maxScrollPosition < 0) 1141 _maxScrollPosition = 0; 1142 } 1143 if (_scrollPosition > _maxScrollPosition) 1144 _scrollPosition = _maxScrollPosition; 1145 if (_scrollPosition < 0) 1146 _scrollPosition = 0; 1147 if (_needScrollbar) { 1148 if (_orientation == Orientation.Vertical) { // FIXME: 1149 _scrollbar.position = _scrollPosition; 1150 } else { 1151 _scrollbar.position = _scrollPosition; 1152 } 1153 } 1154 } 1155 1156 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 1157 override void layout(Rect rc) { 1158 _needLayout = false; 1159 if (visibility == Visibility.Gone) { 1160 return; 1161 } 1162 _pos = rc; 1163 1164 Rect parentrc = rc; 1165 applyMargins(rc); 1166 applyPadding(rc); 1167 1168 if (_itemRects.length < itemCount) 1169 _itemRects.length = itemCount; 1170 1171 // measure again if client size has been changed 1172 if (_lastMeasureWidth != rc.width || _lastMeasureHeight != rc.height) 1173 measure(parentrc.width, parentrc.height); 1174 1175 // hide scrollbar or update rc for scrollbar 1176 Rect sbrect = rc; 1177 // layout scrollbar 1178 if (_needScrollbar) { 1179 rc.right -= _sbsz.x; 1180 rc.bottom -= _sbsz.y; 1181 } else { 1182 _scrollbar.visibility = Visibility.Gone; 1183 } 1184 1185 _clientRc = rc; 1186 1187 // calc item rectangles 1188 updateItemPositions(); 1189 1190 // layout scrollbar - must be under updateItemPositions() 1191 if (_needScrollbar) { 1192 _scrollbar.visibility = Visibility.Visible; 1193 if (_orientation == Orientation.Vertical) 1194 sbrect.left = sbrect.right - _sbsz.x; 1195 else 1196 sbrect.top = sbrect.bottom - _sbsz.y; 1197 _scrollbar.layout(sbrect); 1198 } 1199 1200 if (_makeSelectionVisibleOnNextLayout) { 1201 makeSelectionVisible(); 1202 _makeSelectionVisibleOnNextLayout = false; 1203 } 1204 _needLayout = false; 1205 _scrollbar.cancelLayout(); 1206 } 1207 1208 /// Draw widget at its position to buffer 1209 override void onDraw(DrawBuf buf) { 1210 if (visibility != Visibility.Visible) 1211 return; 1212 super.onDraw(buf); 1213 Rect rc = _pos; 1214 applyMargins(rc); 1215 applyPadding(rc); 1216 auto saver = ClipRectSaver(buf, rc, alpha); 1217 // draw scrollbar 1218 if (_needScrollbar) 1219 _scrollbar.onDraw(buf); 1220 1221 Point scrollOffset; 1222 if (_orientation == Orientation.Vertical) { 1223 scrollOffset.y = _scrollPosition; 1224 } else { 1225 scrollOffset.x = _scrollPosition; 1226 } 1227 // draw items 1228 for (int i = 0; i < itemCount; i++) { 1229 Rect itemrc = _itemRects[i]; 1230 itemrc.left += rc.left - scrollOffset.x; 1231 itemrc.right += rc.left - scrollOffset.x; 1232 itemrc.top += rc.top - scrollOffset.y; 1233 itemrc.bottom += rc.top - scrollOffset.y; 1234 if (itemrc.intersects(rc)) { 1235 Widget w = itemWidget(i); 1236 if (w is null || w.visibility != Visibility.Visible) 1237 continue; 1238 w.layout(itemrc); 1239 w.onDraw(buf); 1240 } 1241 } 1242 } 1243 1244 /// list navigation using keys 1245 override bool onKeyEvent(KeyEvent event) { 1246 if (itemCount == 0) 1247 return false; 1248 int navigationDelta = 0; 1249 if (event.action == KeyAction.KeyDown) { 1250 if (orientation == Orientation.Vertical) { 1251 if (event.keyCode == KeyCode.DOWN) 1252 navigationDelta = 1; 1253 else if (event.keyCode == KeyCode.UP) 1254 navigationDelta = -1; 1255 } else { 1256 if (event.keyCode == KeyCode.RIGHT) 1257 navigationDelta = 1; 1258 else if (event.keyCode == KeyCode.LEFT) 1259 navigationDelta = -1; 1260 } 1261 } 1262 if (navigationDelta != 0) { 1263 moveSelection(navigationDelta); 1264 return true; 1265 } 1266 if (event.action == KeyAction.KeyDown) { 1267 if (event.keyCode == KeyCode.HOME) { 1268 // select first enabled item on HOME key 1269 selectItem(0, 1); 1270 return true; 1271 } else if (event.keyCode == KeyCode.END) { 1272 // select last enabled item on END key 1273 selectItem(itemCount - 1, -1); 1274 return true; 1275 } else if (event.keyCode == KeyCode.PAGEDOWN) { 1276 // TODO 1277 } else if (event.keyCode == KeyCode.PAGEUP) { 1278 // TODO 1279 } 1280 } 1281 if ((event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN)) { 1282 if (event.action == KeyAction.KeyDown && enabled) { 1283 if (itemEnabled(_selectedItemIndex)) { 1284 itemClicked(_selectedItemIndex); 1285 } 1286 } 1287 return true; 1288 } 1289 return super.onKeyEvent(event); 1290 //if (_selectedItemIndex != -1 && event.action == KeyAction.KeyUp && (event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN)) { 1291 // itemClicked(_selectedItemIndex); 1292 // return true; 1293 //} 1294 //if (navigationDelta != 0) { 1295 // int p = _selectedItemIndex; 1296 // if (p < 0) { 1297 // if (navigationDelta < 0) 1298 // p = itemCount - 1; 1299 // else 1300 // p = 0; 1301 // } else { 1302 // p += navigationDelta; 1303 // if (p < 0) 1304 // p = itemCount - 1; 1305 // else if (p >= itemCount) 1306 // p = 0; 1307 // } 1308 // setHoverItem(-1); 1309 // selectItem(p); 1310 // return true; 1311 //} 1312 //return false; 1313 } 1314 1315 /// process mouse event; return true if event is processed by widget. 1316 override bool onMouseEvent(MouseEvent event) { 1317 //Log.d("onMouseEvent ", id, " ", event.action, " (", event.x, ",", event.y, ")"); 1318 super.onMouseEvent(event); 1319 if (event.action == MouseAction.Leave || event.action == MouseAction.Cancel) { 1320 setHoverItem(-1); 1321 return true; 1322 } 1323 // delegate processing of mouse wheel to scrollbar widget 1324 if (event.action == MouseAction.Wheel && _needScrollbar) { 1325 return _scrollbar.onMouseEvent(event); 1326 } 1327 // support onClick 1328 Rect rc = _pos; 1329 applyMargins(rc); 1330 applyPadding(rc); 1331 Point scrollOffset; 1332 if (_orientation == Orientation.Vertical) { 1333 scrollOffset.y = _scrollPosition; 1334 } else { 1335 scrollOffset.x = _scrollPosition; 1336 } 1337 if (event.action == MouseAction.Wheel) { 1338 if (_scrollbar) 1339 _scrollbar.sendScrollEvent(event.wheelDelta > 0 ? ScrollAction.LineUp : ScrollAction.LineDown); 1340 return true; 1341 } 1342 if (event.action == MouseAction.ButtonDown && (event.flags & (MouseFlag.LButton || MouseFlag.RButton))) 1343 setFocus(); 1344 if (itemCount > _itemRects.length) 1345 return true; // layout not yet called 1346 for (int i = 0; i < itemCount; i++) { 1347 Rect itemrc = _itemRects[i]; 1348 itemrc.left += rc.left - scrollOffset.x; 1349 itemrc.right += rc.left - scrollOffset.x; 1350 itemrc.top += rc.top - scrollOffset.y; 1351 itemrc.bottom += rc.top - scrollOffset.y; 1352 if (itemrc.isPointInside(Point(event.x, event.y))) { 1353 if (_adapter && _adapter.wantMouseEvents) { 1354 auto itemWidget = _adapter.itemWidget(i); 1355 if (itemWidget) { 1356 Widget oldParent = itemWidget.parent; 1357 itemWidget.parent = this; 1358 if (event.action == MouseAction.Move && event.noModifiers && itemWidget.hasTooltip) { 1359 itemWidget.scheduleTooltip(200); 1360 } 1361 //itemWidget.onMouseEvent(event); 1362 itemWidget.parent = oldParent; 1363 } 1364 } 1365 //Log.d("mouse event action=", event.action, " button=", event.button, " flags=", event.flags); 1366 if ((event.flags & (MouseFlag.LButton || MouseFlag.RButton)) || _selectOnHover) { 1367 if (_selectedItemIndex != i && itemEnabled(i)) { 1368 int prevSelection = _selectedItemIndex; 1369 selectItem(i); 1370 setHoverItem(-1); 1371 selectionChanged(_selectedItemIndex, prevSelection); 1372 } 1373 } else { 1374 if (itemEnabled(i)) 1375 setHoverItem(i); 1376 } 1377 if (event.button == MouseButton.Left || event.button == MouseButton.Right) { 1378 if ((_clickOnButtonDown && event.action == MouseAction.ButtonDown) || (!_clickOnButtonDown && event.action == MouseAction.ButtonUp)) { 1379 if (itemEnabled(i)) { 1380 itemClicked(i); 1381 if (_clickOnButtonDown) 1382 event.doNotTrackButtonDown = true; 1383 } 1384 } 1385 } 1386 return true; 1387 } 1388 } 1389 return true; 1390 } 1391 /// returns true if item is child of this widget (when deepSearch == true - returns true if item is this widget or one of children inside children tree). 1392 override bool isChild(Widget item, bool deepSearch = true) { 1393 if (_adapter && _adapter.wantMouseEvents) { 1394 for (int i = 0; i < itemCount; i++) { 1395 auto itemWidget = _adapter.itemWidget(i); 1396 if (itemWidget is item) 1397 return true; 1398 } 1399 } 1400 return super.isChild(item, deepSearch); 1401 } 1402 } 1403 1404 class StringListWidget : ListWidget { 1405 import std.conv : to; 1406 import std.datetime.stopwatch : StopWatch; 1407 import core.time : dur; 1408 private dstring _searchString; 1409 private StopWatch _stopWatch; 1410 1411 this(string ID = null) { 1412 super(ID); 1413 styleId = STYLE_EDIT_BOX; 1414 clickable = true; 1415 } 1416 1417 this(string ID, string[] items) { 1418 super(ID); 1419 styleId = STYLE_EDIT_BOX; 1420 ownAdapter = new StringListAdapter(items); 1421 clickable = true; 1422 } 1423 1424 this(string ID, dstring[] items) { 1425 super(ID); 1426 styleId = STYLE_EDIT_BOX; 1427 ownAdapter = new StringListAdapter(items); 1428 clickable = true; 1429 } 1430 1431 this(string ID, StringListValue[] items) { 1432 super(ID); 1433 styleId = STYLE_EDIT_BOX; 1434 ownAdapter = new StringListAdapter(items); 1435 clickable = true; 1436 } 1437 1438 @property void items(string[] itemResourceIds) { 1439 _selectedItemIndex = -1; 1440 ownAdapter = new StringListAdapter(itemResourceIds); 1441 if(itemResourceIds.length > 0) { 1442 selectedItemIndex = 0; 1443 } 1444 requestLayout(); 1445 } 1446 1447 @property void items(dstring[] items) { 1448 _selectedItemIndex = -1; 1449 ownAdapter = new StringListAdapter(items); 1450 if(items.length > 0) { 1451 selectedItemIndex = 0; 1452 } 1453 requestLayout(); 1454 } 1455 1456 @property void items(StringListValue[] items) { 1457 _selectedItemIndex = -1; 1458 ownAdapter = new StringListAdapter(items); 1459 if(items.length > 0) { 1460 selectedItemIndex = 0; 1461 } 1462 requestLayout(); 1463 } 1464 1465 /// StringListValue list values 1466 override bool setStringListValueListProperty(string propName, StringListValue[] values) { 1467 if (propName == "items") { 1468 items = values; 1469 return true; 1470 } 1471 return false; 1472 } 1473 1474 /// get selected item as text 1475 @property dstring selectedItem() { 1476 if (_selectedItemIndex < 0 || _selectedItemIndex >= _adapter.itemCount) 1477 return ""; 1478 return (cast(StringListAdapter)adapter).items.get(_selectedItemIndex); 1479 } 1480 1481 override bool onKeyEvent(KeyEvent event) { 1482 if (itemCount == 0) return false; 1483 1484 // Accept user input and try to find a match in the list. 1485 if (event.action == KeyAction.Text) { 1486 if ( !_stopWatch.running) { _stopWatch.start; } 1487 1488 version(DigitalMars) { 1489 auto timePassed = _stopWatch.peek; //.to!("seconds", float)(); // dtop is std.datetime.to 1490 1491 if (timePassed > dur!"msecs"(500)) _searchString = ""d; 1492 } else { 1493 auto timePassed = (cast(float)_stopWatch.peek.total!"msecs")/1000.0f; 1494 1495 if (timePassed > 0.5) _searchString = ""d; // TODO_GRIM: make everything look-alike 1496 } 1497 _searchString ~= to!dchar(event.text.toUTF8); 1498 _stopWatch.reset; 1499 1500 if ( selectClosestMatch(_searchString) ) { 1501 invalidate(); 1502 return true; 1503 } 1504 } 1505 1506 return super.onKeyEvent(event); 1507 } 1508 1509 1510 private bool selectClosestMatch(dstring term) { 1511 import std.uni : toLower; 1512 if (term.length == 0) return false; 1513 auto myItems = (cast(StringListAdapter)adapter).items; 1514 1515 // Perfect match or best match 1516 int[] indexes; 1517 foreach(int itemIndex; 0 .. myItems.length) { 1518 dstring item = myItems.get(itemIndex); 1519 1520 if (item == term) { 1521 // Perfect match 1522 indexes ~= itemIndex; 1523 break; 1524 } else { 1525 // Term approximate to something 1526 bool addItem = true; 1527 foreach(int termIndex; 0 .. cast(int)term.length) { 1528 if (termIndex < item.length) { 1529 if ( toLower(term[termIndex]) != toLower(item[termIndex]) ) { 1530 addItem = false; 1531 break; 1532 } 1533 } 1534 } 1535 1536 if (addItem) { indexes ~= itemIndex; } 1537 1538 } 1539 } 1540 1541 // Return best match 1542 if (indexes.length > 0) { 1543 selectItem(indexes[0]); 1544 itemSelected(this, indexes[0]); 1545 return true; 1546 } 1547 1548 return false; // Did not find term 1549 1550 } 1551 1552 1553 1554 } 1555 1556 //import dlangui.widgets.metadata; 1557 //mixin(registerWidgets!(ListWidget, StringListWidget)());