1 // Written in the D programming language. 2 3 /** 4 DLANGUI library. 5 6 This module contains declaration of tabbed view controls. 7 8 9 10 Synopsis: 11 12 ---- 13 import dlangui.widgets.tabs; 14 15 ---- 16 17 Copyright: Vadim Lopatin, 2014 18 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 19 Authors: $(WEB coolreader.org, Vadim Lopatin) 20 */ 21 module dlangui.widgets.tabs; 22 23 import dlangui.core.signals; 24 import dlangui.widgets.layouts; 25 import dlangui.widgets.controls; 26 27 interface TabHandler { 28 void onTabChanged(string newActiveTabId, string previousTabId); 29 } 30 31 class TabItem { 32 private string _iconRes; 33 private string _id; 34 private UIString _label; 35 private long _lastAccessTs; 36 this(string id, string labelRes, string iconRes = null) { 37 _id = id; 38 _label = labelRes; 39 _iconRes = iconRes; 40 } 41 this(string id, dstring labelRes, string iconRes = null) { 42 _id = id; 43 _label = labelRes; 44 _iconRes = iconRes; 45 _lastAccessTs = std.datetime.Clock.currStdTime; 46 } 47 @property string iconId() const { return _iconRes; } 48 @property string id() const { return _id; } 49 @property ref UIString text() { return _label; } 50 @property TabItem iconId(string id) { _iconRes = id; return this; } 51 @property TabItem id(string id) { _id = id; return this; } 52 @property long lastAccessTs() { return _lastAccessTs; } 53 @property void lastAccessTs(long ts) { _lastAccessTs = ts; } 54 void updateAccessTs() { _lastAccessTs = std.datetime.Clock.currStdTime; } 55 } 56 57 class TabItemWidget : HorizontalLayout { 58 private ImageWidget _icon; 59 private TextWidget _label; 60 private ImageButton _closeButton; 61 private TabItem _item; 62 private bool _enableCloseButton; 63 @property TabItem tabItem() { return _item; } 64 @property TabControl tabControl() { return cast(TabControl)parent; } 65 this(TabItem item, bool enableCloseButton = true) { 66 styleId = "TAB_UP_BUTTON"; 67 _enableCloseButton = enableCloseButton; 68 _icon = new ImageWidget(); 69 _label = new TextWidget(); 70 _label.styleId = "TAB_UP_BUTTON_TEXT"; 71 _label.state = State.Parent; 72 _closeButton = new ImageButton("CLOSE"); 73 _closeButton.styleId = "BUTTON_TRANSPARENT"; 74 _closeButton.drawableId = "close"; 75 _closeButton.trackHover = true; 76 _closeButton.onClickListener = &onClick; 77 if (_enableCloseButton) { 78 _closeButton.visibility = Visibility.Gone; 79 } else { 80 _closeButton.visibility = Visibility.Visible; 81 } 82 addChild(_icon); 83 addChild(_label); 84 addChild(_closeButton); 85 setItem(item); 86 clickable = true; 87 trackHover = true; 88 } 89 protected bool onClick(Widget source) { 90 if (source.compareId("CLOSE")) { 91 Log.d("tab close button pressed"); 92 } 93 return true; 94 } 95 protected void setItem(TabItem item) { 96 _item = item; 97 if (item.iconId !is null) { 98 _icon.visibility = Visibility.Visible; 99 _icon.drawableId = item.iconId; 100 } else { 101 _icon.visibility = Visibility.Gone; 102 } 103 _label.text = item.text; 104 id = item.id; 105 } 106 } 107 108 /// tab item list helper class 109 class TabItemList { 110 private TabItem[] _list; 111 private int _len; 112 113 this() { 114 } 115 116 /// get item by index 117 TabItem get(int index) { 118 if (index < 0 || index >= _len) 119 return null; 120 return _list[index]; 121 } 122 /// get item by id 123 TabItem get(string id) { 124 int idx = indexById(id); 125 if (idx < 0) 126 return null; 127 return _list[idx]; 128 } 129 @property int length() const { return _len; } 130 /// append new item 131 TabItemList add(TabItem item) { 132 return insert(item, -1); 133 } 134 /// insert new item to specified position 135 TabItemList insert(TabItem item, int index) { 136 if (index > _len || index < 0) 137 index = _len; 138 if (_list.length <= _len) 139 _list.length = _len + 4; 140 for (int i = _len; i > index; i--) 141 _list[i] = _list[i - 1]; 142 _list[index] = item; 143 _len++; 144 return this; 145 } 146 /// remove item by index 147 TabItem remove(int index) { 148 TabItem res = _list[index]; 149 for (int i = index; i < _len - 1; i++) 150 _list[i] = _list[i + 1]; 151 _len--; 152 return res; 153 } 154 /// find tab index by id 155 int indexById(string id) { 156 import std.algorithm; 157 for (int i = 0; i < _len; i++) { 158 if (_list[i].id.equal(id)) 159 return i; 160 } 161 return -1; 162 } 163 } 164 165 class TabControl : WidgetGroup { 166 protected TabItemList _items; 167 protected ImageButton _moreButton; 168 protected bool _enableCloseButton; 169 protected TabItemWidget[] _sortedItems; 170 171 protected void delegate(string newActiveTabId, string previousTabId) _onTabChanged; 172 @property void delegate(string newActiveTabId, string previousTabId) onTabChangedListener() { return _onTabChanged; } 173 @property TabControl onTabChangedListener(void delegate(string newActiveTabId, string previousTabId) listener) { _onTabChanged = listener; return this; } 174 175 this(string ID) { 176 super(ID); 177 _items = new TabItemList(); 178 _moreButton = new ImageButton("MORE", "tab_more"); 179 _moreButton.styleId = "BUTTON_TRANSPARENT"; 180 _moreButton.onClickListener = &onClick; 181 _moreButton.margins(Rect(3,3,3,6)); 182 _enableCloseButton = true; 183 styleId = "TAB_UP"; 184 addChild(_moreButton); // first child is always MORE button, the rest corresponds to tab list 185 } 186 /// returns tab count 187 @property int tabCount() const { 188 return _items.length; 189 } 190 /// returns tab item by id (null if index out of range) 191 TabItem tab(int index) { 192 return _items.get(index); 193 } 194 /// returns tab item by id (null if not found) 195 TabItem tab(string id) { 196 return _items.get(id); 197 } 198 /// get tab index by tab id (-1 if not found) 199 int tabIndex(string id) { 200 return _items.indexById(id); 201 } 202 protected void updateTabs() { 203 // TODO: 204 } 205 static bool accessTimeComparator(TabItemWidget a, TabItemWidget b) { 206 return (a.tabItem.lastAccessTs > b.tabItem.lastAccessTs); 207 } 208 protected TabItemWidget[] sortedItems() { 209 _sortedItems.length = _items.length; 210 for (int i = 0; i < _items.length; i++) 211 _sortedItems[i] = cast(TabItemWidget)_children.get(i + 1); 212 std.algorithm.sort!(accessTimeComparator)(_sortedItems); 213 return _sortedItems; 214 } 215 /// remove tab 216 TabControl removeTab(string id) { 217 int index = _items.indexById(id); 218 if (index >= 0) { 219 _children.remove(index + 1); 220 _items.remove(index); 221 requestLayout(); 222 } 223 return this; 224 } 225 /// add new tab 226 TabControl addTab(TabItem item, int index = -1, bool enableCloseButton = false) { 227 _items.insert(item, index); 228 TabItemWidget widget = new TabItemWidget(item, enableCloseButton); 229 widget.parent = this; 230 widget.onClickListener = &onClick; 231 _children.insert(widget, index); 232 updateTabs(); 233 requestLayout(); 234 return this; 235 } 236 /// add new tab by id and label string 237 TabControl addTab(string id, dstring label, string iconId = null, bool enableCloseButton = false) { 238 TabItem item = new TabItem(id, label, iconId); 239 return addTab(item, -1, enableCloseButton); 240 } 241 /// add new tab by id and label string resource id 242 TabControl addTab(string id, string labelResourceId, string iconId = null, bool enableCloseButton = false) { 243 TabItem item = new TabItem(id, labelResourceId, iconId); 244 return addTab(item, -1, enableCloseButton); 245 } 246 protected bool onClick(Widget source) { 247 if (source.compareId("MORE")) { 248 Log.d("tab MORE button pressed"); 249 return true; 250 } 251 string id = source.id; 252 int index = tabIndex(id); 253 if (index >= 0) { 254 selectTab(index); 255 } 256 return true; 257 } 258 /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 259 override void measure(int parentWidth, int parentHeight) { 260 //Log.d("tabControl.measure enter"); 261 Rect m = margins; 262 Rect p = padding; 263 // calc size constraints for children 264 int pwidth = parentWidth; 265 int pheight = parentHeight; 266 if (parentWidth != SIZE_UNSPECIFIED) 267 pwidth -= m.left + m.right + p.left + p.right; 268 if (parentHeight != SIZE_UNSPECIFIED) 269 pheight -= m.top + m.bottom + p.top + p.bottom; 270 // measure children 271 Point sz; 272 _moreButton.measure(pwidth, pheight); 273 sz.x = _moreButton.measuredWidth; 274 sz.y = _moreButton.measuredHeight; 275 pwidth -= sz.x; 276 for (int i = 1; i < _children.count; i++) { 277 Widget tab = _children.get(i); 278 tab.visibility = Visibility.Visible; 279 tab.measure(pwidth, pheight); 280 if (sz.y < tab.measuredHeight) 281 sz.y = tab.measuredHeight; 282 if (sz.x + tab.measuredWidth > pwidth) 283 break; 284 sz.x += tab.measuredWidth; 285 } 286 measuredContent(parentWidth, parentHeight, sz.x, sz.y); 287 //Log.d("tabControl.measure exit"); 288 } 289 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 290 override void layout(Rect rc) { 291 //Log.d("tabControl.layout enter"); 292 _needLayout = false; 293 if (visibility == Visibility.Gone) { 294 return; 295 } 296 _pos = rc; 297 applyMargins(rc); 298 applyPadding(rc); 299 // more button 300 Rect moreRc = rc; 301 moreRc.left = rc.right - _moreButton.measuredWidth; 302 _moreButton.layout(moreRc); 303 rc.right -= _moreButton.measuredWidth; 304 // tabs 305 int maxw = rc.width; 306 // measure and update visibility 307 TabItemWidget[] sorted = sortedItems(); 308 int w = 0; 309 for (int i = 0; i < sorted.length; i++) { 310 TabItemWidget widget = sorted[i]; 311 widget.visibility = Visibility.Visible; 312 widget.measure(rc.width, rc.height); 313 if (w + widget.measuredWidth < maxw) { 314 w += widget.measuredWidth; 315 } else { 316 widget.visibility = Visibility.Gone; 317 } 318 } 319 // layout visible items 320 for (int i = 1; i < _children.count; i++) { 321 TabItemWidget widget = cast(TabItemWidget)_children.get(i); 322 if (widget.visibility != Visibility.Visible) 323 continue; 324 w = widget.measuredWidth; 325 rc.right = rc.left + w; 326 widget.layout(rc); 327 rc.left += w; 328 } 329 //Log.d("tabControl.layout exit"); 330 } 331 /// Draw widget at its position to buffer 332 override void onDraw(DrawBuf buf) { 333 //Log.d("tabControl.onDraw enter"); 334 if (visibility != Visibility.Visible) 335 return; 336 super.onDraw(buf); 337 Rect rc = _pos; 338 applyMargins(rc); 339 applyPadding(rc); 340 auto saver = ClipRectSaver(buf, rc); 341 for (int i = 0; i < _children.count; i++) { 342 Widget item = _children.get(i); 343 if (item.visibility != Visibility.Visible) 344 continue; 345 item.onDraw(buf); 346 } 347 //Log.d("tabControl.onDraw exit"); 348 } 349 350 protected string _selectedTabId; 351 352 void selectTab(int index) { 353 if (_children.get(index + 1).compareId(_selectedTabId)) 354 return; // already selected 355 string previousSelectedTab = _selectedTabId; 356 for (int i = 1; i < _children.count; i++) { 357 if (index == i - 1) { 358 _children.get(i).state = State.Selected; 359 _selectedTabId = _children.get(i).id; 360 } else { 361 _children.get(i).state = State.Normal; 362 } 363 } 364 if (_onTabChanged !is null) 365 _onTabChanged(_selectedTabId, previousSelectedTab); 366 } 367 } 368 369 /// container for widgets controlled by TabControl 370 class TabHost : FrameLayout, TabHandler { 371 this(string ID, TabControl tabControl = null) { 372 super(ID); 373 _tabControl = tabControl; 374 if (_tabControl !is null) 375 _tabControl.onTabChangedListener = &onTabChanged; 376 styleId = "TAB_HOST"; 377 } 378 protected TabControl _tabControl; 379 /// get currently set control widget 380 @property TabControl tabControl() { return _tabControl; } 381 /// set new control widget 382 @property TabHost tabControl(TabControl newWidget) { 383 _tabControl = newWidget; 384 if (_tabControl !is null) 385 _tabControl.onTabChangedListener = &onTabChanged; 386 return this; 387 } 388 389 protected void delegate(string newActiveTabId, string previousTabId) _onTabChanged; 390 @property void delegate(string newActiveTabId, string previousTabId) onTabChangedListener() { return _onTabChanged; } 391 @property TabHost onTabChangedListener(void delegate(string newActiveTabId, string previousTabId) listener) { _onTabChanged = listener; return this; } 392 393 protected override void onTabChanged(string newActiveTabId, string previousTabId) { 394 if (newActiveTabId !is null) { 395 showChild(newActiveTabId, Visibility.Invisible, true); 396 } 397 if (_onTabChanged !is null) 398 _onTabChanged(newActiveTabId, previousTabId); 399 } 400 401 /// remove tab 402 TabHost removeTab(string id) { 403 assert(_tabControl !is null, "No TabControl set for TabHost"); 404 Widget child = removeChild(id); 405 if (child !is null) { 406 destroy(child); 407 } 408 _tabControl.removeTab(id); 409 requestLayout(); 410 return this; 411 } 412 /// add new tab by id and label string 413 TabHost addTab(Widget widget, dstring label, string iconId = null, bool enableCloseButton = false) { 414 assert(_tabControl !is null, "No TabControl set for TabHost"); 415 assert(widget.id !is null, "ID for tab host page is mandatory"); 416 assert(_children.indexOf(id) == -1, "duplicate ID for tab host page"); 417 _tabControl.addTab(widget.id, label, iconId, enableCloseButton); 418 addChild(widget); 419 return this; 420 } 421 /// add new tab by id and label string resource id 422 TabHost addTab(Widget widget, string labelResourceId, string iconId = null, bool enableCloseButton = false) { 423 assert(_tabControl !is null, "No TabControl set for TabHost"); 424 assert(widget.id !is null, "ID for tab host page is mandatory"); 425 assert(_children.indexOf(id) == -1, "duplicate ID for tab host page"); 426 _tabControl.addTab(widget.id, labelResourceId, iconId, enableCloseButton); 427 addChild(widget); 428 return this; 429 } 430 /// select tab 431 void selectTab(string ID) { 432 int index = _tabControl.tabIndex(ID); 433 if (index != -1) { 434 _tabControl.selectTab(index); 435 } 436 } 437 // /// request relayout of widget and its children 438 // override void requestLayout() { 439 // Log.d("TabHost.requestLayout called"); 440 // super.requestLayout(); 441 // //_needLayout = true; 442 // } 443 // /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 444 // override void layout(Rect rc) { 445 // Log.d("TabHost.layout() called"); 446 // super.layout(rc); 447 // Log.d("after layout(): needLayout = ", needLayout); 448 // } 449 450 } 451 452 /// compound widget - contains from TabControl widget (tabs header) and TabHost (content pages) 453 class TabWidget : VerticalLayout, TabHandler { 454 protected TabControl _tabControl; 455 protected TabHost _tabHost; 456 this(string ID) { 457 super(ID); 458 _tabControl = new TabControl("TAB_CONTROL"); 459 _tabHost = new TabHost("TAB_HOST", _tabControl); 460 styleId = "TAB_WIDGET"; 461 addChild(_tabControl); 462 addChild(_tabHost); 463 } 464 465 protected void delegate(string newActiveTabId, string previousTabId) _onTabChanged; 466 @property void delegate(string newActiveTabId, string previousTabId) onTabChangedListener() { return _onTabChanged; } 467 @property TabWidget onTabChangedListener(void delegate(string newActiveTabId, string previousTabId) listener) { _onTabChanged = listener; return this; } 468 469 protected override void onTabChanged(string newActiveTabId, string previousTabId) { 470 // forward to listener 471 if (_onTabChanged !is null) 472 _onTabChanged(newActiveTabId, previousTabId); 473 } 474 475 /// add new tab by id and label string resource id 476 TabWidget addTab(Widget widget, string labelResourceId, string iconId = null, bool enableCloseButton = false) { 477 _tabHost.addTab(widget, labelResourceId, iconId, enableCloseButton); 478 return this; 479 } 480 /// add new tab by id and label (raw value) 481 TabWidget addTab(Widget widget, dstring label, string iconId = null, bool enableCloseButton = false) { 482 _tabHost.addTab(widget, label, iconId, enableCloseButton); 483 return this; 484 } 485 /// remove tab by id 486 TabWidget removeTab(string id) { 487 _tabHost.removeTab(id); 488 requestLayout(); 489 return this; 490 } 491 /// select tab 492 void selectTab(string ID) { 493 _tabHost.selectTab(ID); 494 } 495 // /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 496 // override void layout(Rect rc) { 497 // Log.d("TabWidget.layout() called"); 498 // super.layout(rc); 499 // Log.d("after layout(): tabhost.needLayout = ", _tabHost.needLayout); 500 // } 501 }