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