1 // Written in the D programming language.
2 
3 /**
4 This module contains Combo Box widgets implementation.
5 
6 
7 
8 Synopsis:
9 
10 ----
11 import dlangui.widgets.combobox;
12 
13 // creation of simple strings list
14 ComboBox box = new ComboBox("combo1", ["value 1"d, "value 2"d, "value 3"d]);
15 
16 // select first item
17 box.selectedItemIndex = 0;
18 
19 // get selected item text
20 println(box.text);
21 
22 ----
23 
24 Copyright: Vadim Lopatin, 2014
25 License:   Boost License 1.0
26 Authors:   Vadim Lopatin, coolreader.org@gmail.com
27 */
28 module dlangui.widgets.combobox;
29 
30 import dlangui.widgets.widget;
31 import dlangui.widgets.layouts;
32 import dlangui.widgets.editors;
33 import dlangui.widgets.lists;
34 import dlangui.widgets.controls;
35 import dlangui.widgets.popup;
36 
37 private import std.algorithm;
38 
39 /** Abstract ComboBox. */
40 class ComboBoxBase : HorizontalLayout, OnClickHandler {
41     protected Widget _body;
42     protected ImageButton _button;
43     protected ListAdapter _adapter;
44     protected bool _ownAdapter;
45     protected int _selectedItemIndex;
46 
47     /** Handle item click. */
48     Signal!OnItemSelectedHandler itemClick;
49 
50     protected Widget createSelectedItemWidget() {
51         Widget res;
52         if (_adapter && _selectedItemIndex < _adapter.itemCount) {
53             res = _adapter.itemWidget(_selectedItemIndex);
54             res.id = "COMBOBOX_BODY";
55         } else {
56             res = new Widget("COMBOBOX_BODY");
57         }
58         res.layoutWidth = WRAP_CONTENT;
59         res.layoutHeight = WRAP_CONTENT;
60         return res;
61     }
62 
63     /** Selected item index. */
64     @property int selectedItemIndex() {
65         return _selectedItemIndex;
66     }
67 
68     @property ComboBoxBase selectedItemIndex(int index) {
69         if (_selectedItemIndex == index)
70             return this;
71         if (_selectedItemIndex != -1 && _adapter.itemCount > _selectedItemIndex) {
72             _adapter.resetItemState(_selectedItemIndex, State.Selected | State.Focused | State.Hovered);
73         }
74         _selectedItemIndex = index;
75         if (itemClick.assigned)
76             itemClick(this, index);
77         return this;
78     }
79 
80     /// change enabled state
81     override @property Widget enabled(bool flg) { 
82         super.enabled(flg);
83         _button.enabled = flg;
84         return this; 
85     }
86     /// return true if state has State.Enabled flag set
87     override @property bool enabled() { return super.enabled; }
88 
89     override bool onClick(Widget source) {
90         if (enabled)
91             showPopup();
92         return true;
93     }
94 
95     protected ImageButton createButton() {
96         ImageButton res = new ImageButton("COMBOBOX_BUTTON", ATTR_SCROLLBAR_BUTTON_DOWN);
97         res.styleId = STYLE_COMBO_BOX_BUTTON;
98         res.layoutWeight = 0;
99         res.click = this;
100         res.alignment = Align.VCenter | Align.Right;
101         return res;
102     }
103 
104     protected ListWidget createPopup() {
105         ListWidget list = new ListWidget("POPUP_LIST");
106         list.adapter = _adapter;
107         list.selectedItemIndex = _selectedItemIndex;
108         return list;
109     }
110 
111     protected PopupWidget _popup;
112     protected ListWidget _popupList;
113 
114     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
115     override void layout(Rect rc) {
116         super.layout(rc);
117         _pos = rc;
118         applyMargins(rc);
119         applyPadding(rc);
120         rc.left = rc.right - _button.measuredWidth;
121         _button.layout(rc);
122     }
123 
124     protected void popupClosed() {
125     }
126 
127     protected void showPopup() {
128         if (!_adapter || !_adapter.itemCount)
129             return; // don't show empty popup
130         _popupList = createPopup();
131         _popup = window.showPopup(_popupList, this, PopupAlign.Below | PopupAlign.FitAnchorSize);
132         _popup.flags = PopupFlags.CloseOnClickOutside;
133         _popup.styleId = STYLE_POPUP_MENU;
134         _popup.popupClosed = delegate (PopupWidget source) {
135             _popup = null;
136             _popupList = null;
137         };
138         _popupList.itemSelected = delegate(Widget source, int index) {
139             //selectedItemIndex = index;
140             return true;
141         };
142         _popupList.itemClick = delegate(Widget source, int index) {
143             selectedItemIndex = index;
144             if (_popup !is null) {
145                 _popup.close();
146                 _popup = null;
147                 popupClosed();
148             }
149             return true;
150         };
151         _popupList.setFocus();
152     }
153 
154     this(string ID, ListAdapter adapter, bool ownAdapter = true) {
155         super(ID);
156         _adapter = adapter;
157         _ownAdapter = ownAdapter;
158         styleId = STYLE_COMBO_BOX;
159         trackHover = true;
160         initialize();
161     }
162 
163     void setAdapter(ListAdapter adapter, bool ownAdapter = true) {
164         if (_adapter) {
165             if (_ownAdapter)
166                 destroy(_adapter);
167             removeAllChildren();
168         }
169         _adapter = adapter;
170         _ownAdapter = ownAdapter;
171         initialize();
172     }
173 
174     override void onThemeChanged() {
175         super.onThemeChanged();
176         if (_body)
177             _body.onThemeChanged();
178         if (_adapter)
179             _adapter.onThemeChanged();
180         if (_button)
181             _button.onThemeChanged();
182     }
183 
184     protected void initialize() {
185         _body = createSelectedItemWidget();
186         _body.click = this;
187         _button = createButton();
188         //_body.state = State.Parent;
189         //focusable = true;
190         _button.focusable = false;
191         _body.focusable = false;
192         focusable = true;
193         //_body.focusable = true;
194         addChild(_body);
195         addChild(_button);
196     }
197 
198     ~this() {
199     }
200 }
201 
202 
203 /** ComboBox with list of strings. */
204 class ComboBox : ComboBoxBase {
205     
206     /// empty parameter list constructor - for usage by factory
207     this() {
208         this(null);
209     }
210     /// create with ID parameter
211     this(string ID) {
212         super(ID, new StringListAdapter(), true);
213     }
214 
215     this(string ID, string[] items) {
216         super(ID, new StringListAdapter(items), true);
217     }
218 
219     this(string ID, dstring[] items) {
220         super(ID, new StringListAdapter(items), true);
221     }
222 
223     this(string ID, StringListValue[] items) {
224         super(ID, new StringListAdapter(items), true);
225     }
226 
227     @property void items(string[] itemResourceIds) {
228         setAdapter(new StringListAdapter(itemResourceIds));
229         if(itemResourceIds.length > 0) {
230            selectedItemIndex = 0;
231         }
232         requestLayout();
233     }
234 
235     @property void items(dstring[] items) {
236         setAdapter(new StringListAdapter(items));
237         if(items.length > 0) {
238             if (selectedItemIndex == -1 || selectedItemIndex > items.length)
239                 selectedItemIndex = 0;
240         }
241         requestLayout();
242     }
243 
244     @property void items(StringListValue[] items) {
245         if (auto a = cast(StringListAdapter)_adapter)
246             a.items = items;
247         else
248             setAdapter(new StringListAdapter(items));
249         if(items.length > 0) {
250            selectedItemIndex = 0;
251         }
252         requestLayout();
253     }
254 
255     /// StringListValue list values
256     override bool setStringListValueListProperty(string propName, StringListValue[] values) {
257         if (propName == "items") {
258             items = values;
259             return true;
260         }
261         return false;
262     }
263 
264     /// get selected item as text
265     @property dstring selectedItem() {
266         if (_selectedItemIndex < 0 || _selectedItemIndex >= _adapter.itemCount)
267             return "";
268         return adapter.items.get(_selectedItemIndex);
269     }
270 
271     /// returns list of items
272     @property ref const(UIStringCollection) items() {
273         return (cast(StringListAdapter)_adapter).items;
274     }
275 
276     @property StringListAdapter adapter() {
277         return cast(StringListAdapter)_adapter;
278     }
279 
280     @property override dstring text() {
281         return _body.text;
282     }
283 
284     @property override Widget text(dstring txt) {
285         int idx = adapter.items.indexOf(txt);
286         if (idx >= 0) {
287             selectedItemIndex = idx;
288         } else {
289             // not found
290             _selectedItemIndex = -1;
291             _body.text = txt;
292         }
293         return this;
294     }
295 
296     @property override Widget text(UIString txt) {
297         int idx = adapter.items.indexOf(txt);
298         if (idx >= 0) {
299             selectedItemIndex = idx;
300         } else {
301             // not found
302             _selectedItemIndex = -1;
303             _body.text = txt;
304         }
305         return this;
306     }
307 
308     override @property ComboBoxBase selectedItemIndex(int index) {
309         _body.text = adapter.items[index];
310         return super.selectedItemIndex(index);
311     }
312 
313     /** Selected item index. */
314     override @property int selectedItemIndex() {
315         return super.selectedItemIndex;
316     }
317 
318     override void initialize() {
319         super.initialize();
320         _body.focusable = false;
321         _body.clickable = true;
322         focusable = true;
323         clickable = true;
324         click = this;
325     }
326 
327     override protected Widget createSelectedItemWidget() {
328         TextWidget res = new TextWidget("COMBO_BOX_BODY");
329         res.styleId = STYLE_COMBO_BOX_BODY;
330         res.clickable = true;
331         res.layoutWidth = FILL_PARENT;
332         res.layoutHeight = WRAP_CONTENT;
333         int maxItemWidth = 0;
334         for(int i = 0; i < _adapter.itemCount; i++) {
335             Widget item = _adapter.itemWidget(i);
336             item.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
337             if (maxItemWidth < item.measuredWidth)
338                 maxItemWidth = item.measuredWidth;
339         }
340         res.minWidth = maxItemWidth;
341         return res;
342     }
343 
344     ~this() {
345         if (_adapter) {
346             destroy(_adapter);
347             _adapter = null;
348         }
349     }
350 
351 }
352 
353 
354 
355 /** ComboBox with list of strings. */
356 class IconTextComboBox : ComboBoxBase {
357 
358     /// empty parameter list constructor - for usage by factory
359     this() {
360         this(null);
361     }
362     /// create with ID parameter
363     this(string ID) {
364         super(ID, new IconStringListAdapter(), true);
365     }
366 
367     this(string ID, StringListValue[] items) {
368         super(ID, new IconStringListAdapter(items), true);
369     }
370 
371     @property void items(StringListValue[] items) {
372         if (auto a = cast(IconStringListAdapter)_adapter)
373             a.items = items;
374         else
375             setAdapter(new IconStringListAdapter(items));
376         if(items.length > 0) {
377             selectedItemIndex = 0;
378         }
379         requestLayout();
380     }
381 
382     /// get selected item as text
383     @property dstring selectedItem() {
384         if (_selectedItemIndex < 0 || _selectedItemIndex >= _adapter.itemCount)
385             return "";
386         return adapter.items.get(_selectedItemIndex);
387     }
388 
389     /// returns list of items
390     @property ref const(UIStringCollection) items() {
391         return (cast(StringListAdapter)_adapter).items;
392     }
393 
394     @property StringListAdapter adapter() {
395         return cast(StringListAdapter)_adapter;
396     }
397 
398     @property override dstring text() {
399         return _body.text;
400     }
401 
402     @property override Widget text(dstring txt) {
403         int idx = adapter.items.indexOf(txt);
404         if (idx >= 0) {
405             selectedItemIndex = idx;
406         } else {
407             // not found
408             _selectedItemIndex = -1;
409             _body.text = txt;
410         }
411         return this;
412     }
413 
414     @property override Widget text(UIString txt) {
415         int idx = adapter.items.indexOf(txt);
416         if (idx >= 0) {
417             selectedItemIndex = idx;
418         } else {
419             // not found
420             _selectedItemIndex = -1;
421             _body.text = txt;
422         }
423         return this;
424     }
425 
426     override @property ComboBoxBase selectedItemIndex(int index) {
427         _body.text = adapter.items[index];
428         return super.selectedItemIndex(index);
429     }
430 
431     /** Selected item index. */
432     override @property int selectedItemIndex() {
433         return super.selectedItemIndex;
434     }
435 
436     override void initialize() {
437         super.initialize();
438         _body.focusable = false;
439         _body.clickable = true;
440         focusable = true;
441         clickable = true;
442         click = this;
443     }
444 
445     override protected Widget createSelectedItemWidget() {
446         TextWidget res = new TextWidget("COMBO_BOX_BODY");
447         res.styleId = STYLE_COMBO_BOX_BODY;
448         res.clickable = true;
449         res.layoutWidth = FILL_PARENT;
450         res.layoutHeight = WRAP_CONTENT;
451         int maxItemWidth = 0;
452         for(int i = 0; i < _adapter.itemCount; i++) {
453             Widget item = _adapter.itemWidget(i);
454             item.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
455             if (maxItemWidth < item.measuredWidth)
456                 maxItemWidth = item.measuredWidth;
457         }
458         res.minWidth = maxItemWidth;
459         return res;
460     }
461 
462     ~this() {
463         if (_adapter) {
464             destroy(_adapter);
465             _adapter = null;
466         }
467     }
468 }
469 
470 /** Editable ComboBox with list of strings. */
471 class ComboEdit : ComboBox {
472 
473     protected EditLine _edit;
474 
475     /// empty parameter list constructor - for usage by factory
476     this() {
477         this(null);
478         postInit();
479     }
480     /// create with ID parameter
481     this(string ID) {
482         super(ID);
483         postInit();
484     }
485 
486     this(string ID, string[] items) {
487         super(ID, items);
488         postInit();
489     }
490 
491     this(string ID, dstring[] items) {
492         super(ID, items);
493         postInit();
494     }
495 
496     protected void postInit() {
497         focusable = false;
498         clickable = false;
499         _edit.focusable = true;
500     }
501 
502     /// process key event, return true if event is processed.
503     override bool onKeyEvent(KeyEvent event) {
504         if (event.keyCode == KeyCode.DOWN && enabled) {
505             if (event.action == KeyAction.KeyDown) {
506                 showPopup();
507             }
508             return true;
509         }
510         if ((event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN) && readOnly && enabled) {
511             if (event.action == KeyAction.KeyDown) {
512                 showPopup();
513             }
514             return true;
515         }
516         if (_edit.onKeyEvent(event))
517             return true;
518         return super.onKeyEvent(event);
519     }
520 
521     override protected void popupClosed() {
522         _edit.setFocus();
523     }
524 
525     // called to process click and notify listeners
526     override protected bool handleClick() {
527         _edit.setFocus();
528         return true;
529     }
530 
531     @property bool readOnly() {
532         return _edit.readOnly;
533     }
534 
535     @property ComboBox readOnly(bool ro) {
536         _edit.readOnly = ro;
537         return this;
538     }
539 
540     /// set bool property value, for ML loaders
541     mixin(generatePropertySettersMethodOverride("setBoolProperty", "bool",
542                                                 "readOnly"));
543 
544     override protected Widget createSelectedItemWidget() {
545         EditLine res = new EditLine("COMBOBOX_BODY");
546         res.layoutWidth = FILL_PARENT;
547         res.layoutHeight = WRAP_CONTENT;
548         res.readOnly = false;
549         _edit = res;
550         postInit();
551         //_edit.focusable = true;
552         return res;
553     }
554 
555     override void initialize() {
556         super.initialize();
557         //focusable = false;
558         //_body.focusable = true;
559     }
560 
561 }
562 
563 //import dlangui.widgets.metadata;
564 //mixin(registerWidgets!(ComboBox)());