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