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