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