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)());