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