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