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