dlangui/src/dlangui/widgets/tabs.d

847 lines
27 KiB
D

// Written in the D programming language.
/**
This module contains declaration of tabbed view controls.
TabItemWidget - single tab header in tab control
TabWidget
TabHost
TabControl
Synopsis:
----
import dlangui.widgets.tabs;
----
Copyright: Vadim Lopatin, 2014
License: Boost License 1.0
Authors: Vadim Lopatin, coolreader.org@gmail.com
*/
module dlangui.widgets.tabs;
import dlangui.core.signals;
import dlangui.widgets.layouts;
import dlangui.widgets.controls;
import std.algorithm;
/// current tab is changed handler
interface TabHandler {
void onTabChanged(string newActiveTabId, string previousTabId);
}
/// tab close button pressed handler
interface TabCloseHandler {
void onTabClose(string tabId);
}
/// tab item metadata
class TabItem {
private static __gshared long _lastAccessCounter;
private string _iconRes;
private string _id;
private UIString _label;
private long _lastAccessTs;
this(string id, string labelRes, string iconRes = null) {
_id = id;
_label = labelRes;
_iconRes = iconRes;
}
this(string id, dstring labelRes, string iconRes = null) {
_id = id;
_label = labelRes;
_iconRes = iconRes;
_lastAccessTs = _lastAccessCounter++;
}
@property string iconId() const { return _iconRes; }
@property string id() const { return _id; }
@property ref UIString text() { return _label; }
@property TabItem iconId(string id) { _iconRes = id; return this; }
@property TabItem id(string id) { _id = id; return this; }
@property long lastAccessTs() { return _lastAccessTs; }
@property void lastAccessTs(long ts) { _lastAccessTs = ts; }
void updateAccessTs() {
_lastAccessTs = _lastAccessCounter++; //std.datetime.Clock.currStdTime;
}
protected Object _objectParam;
@property Object objectParam() {
return _objectParam;
}
@property TabItem objectParam(Object value) {
_objectParam = value;
return this;
}
protected int _intParam;
@property int intParam() {
return _intParam;
}
@property TabItem intParam(int value) {
_intParam = value;
return this;
}
}
/// tab item widget - to show tab header
class TabItemWidget : HorizontalLayout {
private ImageWidget _icon;
private TextWidget _label;
private ImageButton _closeButton;
private TabItem _item;
private bool _enableCloseButton;
Signal!TabCloseHandler tabClose;
@property TabItem tabItem() { return _item; }
@property TabControl tabControl() { return cast(TabControl)parent; }
this(TabItem item, bool enableCloseButton = true) {
styleId = STYLE_TAB_UP_BUTTON;
_enableCloseButton = enableCloseButton;
_icon = new ImageWidget();
_label = new TextWidget();
_label.styleId = STYLE_TAB_UP_BUTTON_TEXT;
_label.state = State.Parent;
_closeButton = new ImageButton("CLOSE");
_closeButton.styleId = STYLE_BUTTON_TRANSPARENT;
_closeButton.drawableId = "close";
_closeButton.trackHover = true;
_closeButton.click = &onClick;
if (!_enableCloseButton) {
_closeButton.visibility = Visibility.Gone;
} else {
_closeButton.visibility = Visibility.Visible;
}
addChild(_icon);
addChild(_label);
addChild(_closeButton);
setItem(item);
clickable = true;
trackHover = true;
}
void setStyles(string tabButtonStyle, string tabButtonTextStyle) {
styleId = tabButtonStyle;
_label.styleId = tabButtonTextStyle;
}
protected bool onClick(Widget source) {
if (source.compareId("CLOSE")) {
Log.d("tab close button pressed");
if (tabClose.assigned)
tabClose(_item.id);
}
return true;
}
@property TabItem item() {
return _item;
}
@property void setItem(TabItem item) {
_item = item;
if (item.iconId !is null) {
_icon.visibility = Visibility.Visible;
_icon.drawableId = item.iconId;
} else {
_icon.visibility = Visibility.Gone;
}
_label.text = item.text;
id = item.id;
}
}
/// tab item list helper class
class TabItemList {
private TabItem[] _list;
private int _len;
this() {
}
/// get item by index
TabItem get(int index) {
if (index < 0 || index >= _len)
return null;
return _list[index];
}
/// get item by index
const (TabItem) get(int index) const {
if (index < 0 || index >= _len)
return null;
return _list[index];
}
/// get item by index
TabItem opIndex(int index) {
return get(index);
}
/// get item by index
const (TabItem) opIndex(int index) const {
return get(index);
}
/// get item by id
TabItem get(string id) {
int idx = indexById(id);
if (idx < 0)
return null;
return _list[idx];
}
/// get item by id
const (TabItem) get(string id) const {
int idx = indexById(id);
if (idx < 0)
return null;
return _list[idx];
}
/// get item by id
TabItem opIndex(string id) {
return get(id);
}
@property int length() const { return _len; }
/// append new item
TabItemList add(TabItem item) {
return insert(item, -1);
}
/// insert new item to specified position
TabItemList insert(TabItem item, int index) {
if (index > _len || index < 0)
index = _len;
if (_list.length <= _len)
_list.length = _len + 4;
for (int i = _len; i > index; i--)
_list[i] = _list[i - 1];
_list[index] = item;
_len++;
return this;
}
/// remove item by index
TabItem remove(int index) {
TabItem res = _list[index];
for (int i = index; i < _len - 1; i++)
_list[i] = _list[i + 1];
_len--;
return res;
}
/// find tab index by id
int indexById(string id) const {
for (int i = 0; i < _len; i++) {
if (_list[i].id.equal(id))
return i;
}
return -1;
}
}
/// tab header - tab labels, with optional More button
class TabControl : WidgetGroupDefaultDrawing {
protected TabItemList _items;
protected ImageButton _moreButton;
protected bool _enableCloseButton;
protected TabItemWidget[] _sortedItems;
protected int _buttonOverlap;
protected string _tabStyle;
protected string _tabButtonStyle;
protected string _tabButtonTextStyle;
/// signal of tab change (e.g. by clicking on tab header)
Signal!TabHandler tabChanged;
/// signal on tab close button
Signal!TabCloseHandler tabClose;
protected Align _tabAlignment;
@property Align tabAlignment() { return _tabAlignment; }
@property void tabAlignment(Align a) { _tabAlignment = a; }
/// empty parameter list constructor - for usage by factory
this() {
this(null);
}
/// create with ID parameter
this(string ID, Align tabAlignment = Align.Top) {
super(ID);
_tabAlignment = tabAlignment;
setStyles(STYLE_TAB_UP, STYLE_TAB_UP_BUTTON, STYLE_TAB_UP_BUTTON_TEXT);
_items = new TabItemList();
_moreButton = new ImageButton("MORE", "tab_more");
_moreButton.styleId = STYLE_BUTTON_TRANSPARENT;
_moreButton.mouseEvent = &onMouse;
_moreButton.margins(Rect(3,3,3,6));
_enableCloseButton = true;
styleId = _tabStyle;
addChild(_moreButton); // first child is always MORE button, the rest corresponds to tab list
}
void setStyles(string tabStyle, string tabButtonStyle, string tabButtonTextStyle) {
_tabStyle = tabStyle;
_tabButtonStyle = tabButtonStyle;
_tabButtonTextStyle = tabButtonTextStyle;
styleId = _tabStyle;
for (int i = 1; i < _children.count; i++) {
TabItemWidget w = cast(TabItemWidget)_children[i];
if (w) {
w.setStyles(_tabButtonStyle, _tabButtonTextStyle);
}
}
_buttonOverlap = currentTheme.get(tabButtonStyle).customLength("overlap", 0);
}
/// when true, shows close buttons in tabs
@property bool enableCloseButton() { return _enableCloseButton; }
/// ditto
@property void enableCloseButton(bool enabled) {
_enableCloseButton = enabled;
}
/// returns tab count
@property int tabCount() const {
return _items.length;
}
/// returns tab item by id (null if index out of range)
TabItem tab(int index) {
return _items.get(index);
}
/// returns tab item by id (null if not found)
TabItem tab(string id) {
return _items.get(id);
}
/// returns tab item by id (null if not found)
const(TabItem) tab(string id) const {
return _items.get(id);
}
/// get tab index by tab id (-1 if not found)
int tabIndex(string id) {
return _items.indexById(id);
}
protected void updateTabs() {
// TODO:
}
static bool accessTimeComparator(TabItemWidget a, TabItemWidget b) {
return (a.tabItem.lastAccessTs > b.tabItem.lastAccessTs);
}
protected TabItemWidget[] sortedItems() {
_sortedItems.length = _items.length;
for (int i = 0; i < _items.length; i++)
_sortedItems[i] = cast(TabItemWidget)_children.get(i + 1);
std.algorithm.sort!(accessTimeComparator)(_sortedItems);
return _sortedItems;
}
/// find next or previous tab index, based on access time
int getNextItemIndex(int direction) {
if (_items.length == 0)
return -1;
if (_items.length == 1)
return 0;
TabItemWidget[] items = sortedItems();
for (int i = 0; i < items.length; i++) {
if (items[i].id == _selectedTabId) {
int next = i + direction;
if (next < 0)
next = cast(int)(items.length - 1);
if (next >= items.length)
next = 0;
return _items.indexById(items[next].id);
}
}
return -1;
}
/// remove tab
TabControl removeTab(string id) {
string nextId;
if (id.equal(_selectedTabId)) {
// current tab is being closed: remember next tab id
int nextIndex = getNextItemIndex(1);
if (nextIndex < 0)
nextIndex = getNextItemIndex(-1);
if (nextIndex >= 0)
nextId = _items[nextIndex].id;
}
int index = _items.indexById(id);
if (index >= 0) {
Widget w = _children.remove(index + 1);
if (w)
destroy(w);
_items.remove(index);
if (id.equal(_selectedTabId))
_selectedTabId = null;
requestLayout();
}
if (nextId) {
index = _items.indexById(nextId);
if (index >= 0) {
selectTab(index, true);
}
}
return this;
}
/// change name of tab
void renameTab(string ID, dstring name) {
int index = _items.indexById(id);
if (index >= 0) {
renameTab(index, name);
}
}
/// change name of tab
void renameTab(int index, dstring name) {
_items[index].text = name;
for (int i = 0; i < _children.count; i++) {
TabItemWidget widget = cast (TabItemWidget)_children[i];
if (widget && widget.item is _items[index]) {
widget.setItem(_items[index]);
requestLayout();
break;
}
}
}
protected void onTabClose(string tabId) {
if (tabClose.assigned)
tabClose(tabId);
}
/// add new tab
TabControl addTab(TabItem item, int index = -1, bool enableCloseButton = false) {
_items.insert(item, index);
TabItemWidget widget = new TabItemWidget(item, enableCloseButton);
widget.parent = this;
widget.mouseEvent = &onMouse;
widget.setStyles(_tabButtonStyle, _tabButtonTextStyle);
widget.tabClose = &onTabClose;
_children.insert(widget, index);
updateTabs();
requestLayout();
return this;
}
/// add new tab by id and label string
TabControl addTab(string id, dstring label, string iconId = null, bool enableCloseButton = false) {
TabItem item = new TabItem(id, label, iconId);
return addTab(item, -1, enableCloseButton);
}
/// add new tab by id and label string resource id
TabControl addTab(string id, string labelResourceId, string iconId = null, bool enableCloseButton = false) {
TabItem item = new TabItem(id, labelResourceId, iconId);
return addTab(item, -1, enableCloseButton);
}
protected bool onMouse(Widget source, MouseEvent event) {
if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
if (source.compareId("MORE")) {
Log.d("tab MORE button pressed");
return true;
}
string id = source.id;
int index = tabIndex(id);
if (index >= 0) {
selectTab(index, true);
}
}
return true;
}
/// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
override void measure(int parentWidth, int parentHeight) {
//Log.d("tabControl.measure enter");
Rect m = margins;
Rect p = padding;
// calc size constraints for children
int pwidth = parentWidth;
int pheight = parentHeight;
if (parentWidth != SIZE_UNSPECIFIED)
pwidth -= m.left + m.right + p.left + p.right;
if (parentHeight != SIZE_UNSPECIFIED)
pheight -= m.top + m.bottom + p.top + p.bottom;
// measure children
Point sz;
if (_moreButton.visibility == Visibility.Visible) {
_moreButton.measure(pwidth, pheight);
sz.x = _moreButton.measuredWidth;
sz.y = _moreButton.measuredHeight;
}
pwidth -= sz.x;
for (int i = 1; i < _children.count; i++) {
Widget tab = _children.get(i);
tab.visibility = Visibility.Visible;
tab.measure(pwidth, pheight);
if (sz.y < tab.measuredHeight)
sz.y = tab.measuredHeight;
if (sz.x + tab.measuredWidth > pwidth)
break;
sz.x += tab.measuredWidth - _buttonOverlap;
}
measuredContent(parentWidth, parentHeight, sz.x, sz.y);
//Log.d("tabControl.measure exit");
}
/// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
override void layout(Rect rc) {
//Log.d("tabControl.layout enter");
_needLayout = false;
if (visibility == Visibility.Gone) {
return;
}
_pos = rc;
applyMargins(rc);
applyPadding(rc);
// more button
Rect moreRc = rc;
if (_moreButton.visibility == Visibility.Visible) {
moreRc.left = rc.right - _moreButton.measuredWidth;
_moreButton.layout(moreRc);
rc.right -= _moreButton.measuredWidth;
}
// tabs
int maxw = rc.width;
// measure and update visibility
TabItemWidget[] sorted = sortedItems();
int w = 0;
for (int i = 0; i < sorted.length; i++) {
TabItemWidget widget = sorted[i];
widget.visibility = Visibility.Visible;
widget.measure(rc.width, rc.height);
if (w + widget.measuredWidth < maxw) {
w += widget.measuredWidth - _buttonOverlap;
} else {
widget.visibility = Visibility.Gone;
}
}
// layout visible items
for (int i = 1; i < _children.count; i++) {
TabItemWidget widget = cast(TabItemWidget)_children.get(i);
if (widget.visibility != Visibility.Visible)
continue;
w = widget.measuredWidth;
rc.right = rc.left + w;
widget.layout(rc);
rc.left += w - _buttonOverlap;
}
//Log.d("tabControl.layout exit");
}
/// Draw widget at its position to buffer
override void onDraw(DrawBuf buf) {
if (visibility != Visibility.Visible)
return;
super.onDraw(buf);
Rect rc = _pos;
applyMargins(rc);
applyPadding(rc);
auto saver = ClipRectSaver(buf, rc);
for (int i = _children.count - 1; i >= 0; i--) {
Widget item = _children.get(i);
if (item.visibility != Visibility.Visible)
continue;
if (item.id.equal(_selectedTabId))
continue;
item.onDraw(buf);
}
for (int i = 0; i < _children.count; i++) {
Widget item = _children.get(i);
if (item.visibility != Visibility.Visible)
continue;
if (!item.id.equal(_selectedTabId))
continue;
item.onDraw(buf);
}
}
protected string _selectedTabId;
@property string selectedTabId() const {
return _selectedTabId;
}
void updateAccessTs() {
int index = _items.indexById(_selectedTabId);
if (index >= 0)
_items[index].updateAccessTs();
}
void selectTab(int index, bool updateAccess) {
if (_children.get(index + 1).compareId(_selectedTabId))
return; // already selected
string previousSelectedTab = _selectedTabId;
for (int i = 1; i < _children.count; i++) {
if (index == i - 1) {
_children.get(i).state = State.Selected;
_selectedTabId = _children.get(i).id;
if (updateAccess)
updateAccessTs();
} else {
_children.get(i).state = State.Normal;
}
}
if (tabChanged.assigned)
tabChanged(_selectedTabId, previousSelectedTab);
}
}
/// container for widgets controlled by TabControl
class TabHost : FrameLayout, TabHandler {
/// empty parameter list constructor - for usage by factory
this() {
this(null);
}
/// create with ID parameter
this(string ID, TabControl tabControl = null) {
super(ID);
_tabControl = tabControl;
if (_tabControl !is null)
_tabControl.tabChanged = &onTabChanged;
styleId = STYLE_TAB_HOST;
}
protected TabControl _tabControl;
/// get currently set control widget
@property TabControl tabControl() { return _tabControl; }
/// set new control widget
@property TabHost tabControl(TabControl newWidget) {
_tabControl = newWidget;
if (_tabControl !is null)
_tabControl.tabChanged = &onTabChanged;
return this;
}
protected Visibility _hiddenTabsVisibility = Visibility.Invisible;
@property Visibility hiddenTabsVisibility() { return _hiddenTabsVisibility; }
@property void hiddenTabsVisibility(Visibility v) { _hiddenTabsVisibility = v; }
/// signal of tab change (e.g. by clicking on tab header)
Signal!TabHandler tabChanged;
protected override void onTabChanged(string newActiveTabId, string previousTabId) {
if (newActiveTabId !is null) {
showChild(newActiveTabId, _hiddenTabsVisibility, true);
}
if (tabChanged.assigned)
tabChanged(newActiveTabId, previousTabId);
}
/// get tab content widget by id
Widget tabBody(string id) {
for (int i = 0; i < _children.count; i++) {
if (_children[i].compareId(id))
return _children[i];
}
return null;
}
/// remove tab
TabHost removeTab(string id) {
assert(_tabControl !is null, "No TabControl set for TabHost");
Widget child = removeChild(id);
if (child !is null) {
destroy(child);
}
_tabControl.removeTab(id);
requestLayout();
return this;
}
/// add new tab by id and label string
TabHost addTab(Widget widget, dstring label, string iconId = null, bool enableCloseButton = false) {
assert(_tabControl !is null, "No TabControl set for TabHost");
assert(widget.id !is null, "ID for tab host page is mandatory");
assert(_children.indexOf(id) == -1, "duplicate ID for tab host page");
_tabControl.addTab(widget.id, label, iconId, enableCloseButton);
//widget.focusGroup = true; // doesn't allow move focus outside of tab content
addChild(widget);
return this;
}
/// add new tab by id and label string resource id
TabHost addTab(Widget widget, string labelResourceId, string iconId = null, bool enableCloseButton = false) {
assert(_tabControl !is null, "No TabControl set for TabHost");
assert(widget.id !is null, "ID for tab host page is mandatory");
assert(_children.indexOf(id) == -1, "duplicate ID for tab host page");
_tabControl.addTab(widget.id, labelResourceId, iconId, enableCloseButton);
addChild(widget);
return this;
}
/// select tab
void selectTab(string ID, bool updateAccess) {
int index = _tabControl.tabIndex(ID);
if (index != -1) {
_tabControl.selectTab(index, updateAccess);
}
}
// /// request relayout of widget and its children
// override void requestLayout() {
// Log.d("TabHost.requestLayout called");
// super.requestLayout();
// //_needLayout = true;
// }
// /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
// override void layout(Rect rc) {
// Log.d("TabHost.layout() called");
// super.layout(rc);
// Log.d("after layout(): needLayout = ", needLayout);
// }
}
/// compound widget - contains from TabControl widget (tabs header) and TabHost (content pages)
class TabWidget : VerticalLayout, TabHandler, TabCloseHandler {
protected TabControl _tabControl;
protected TabHost _tabHost;
/// empty parameter list constructor - for usage by factory
this() {
this(null);
}
/// create with ID parameter
this(string ID, Align tabAlignment = Align.Top) {
super(ID);
_tabControl = new TabControl("TAB_CONTROL", tabAlignment);
_tabHost = new TabHost("TAB_HOST", _tabControl);
_tabControl.tabChanged.connect(this);
_tabControl.tabClose.connect(this);
styleId = STYLE_TAB_WIDGET;
if (tabAlignment == Align.Top) {
addChild(_tabControl);
addChild(_tabHost);
} else {
addChild(_tabHost);
addChild(_tabControl);
}
focusGroup = true;
}
TabControl tabControl() { return _tabControl; }
TabHost tabHost() { return _tabHost; }
/// signal of tab change (e.g. by clicking on tab header)
Signal!TabHandler tabChanged;
/// signal on tab close button
Signal!TabCloseHandler tabClose;
protected override void onTabClose(string tabId) {
if (tabClose.assigned)
tabClose(tabId);
}
protected override void onTabChanged(string newActiveTabId, string previousTabId) {
// forward to listener
if (tabChanged.assigned)
tabChanged(newActiveTabId, previousTabId);
}
/// add new tab by id and label string resource id
TabWidget addTab(Widget widget, string labelResourceId, string iconId = null, bool enableCloseButton = false) {
_tabHost.addTab(widget, labelResourceId, iconId, enableCloseButton);
return this;
}
/// add new tab by id and label (raw value)
TabWidget addTab(Widget widget, dstring label, string iconId = null, bool enableCloseButton = false) {
_tabHost.addTab(widget, label, iconId, enableCloseButton);
return this;
}
/// remove tab by id
TabWidget removeTab(string id) {
_tabHost.removeTab(id);
requestLayout();
return this;
}
/// change name of tab
void renameTab(string ID, dstring name) {
_tabControl.renameTab(ID, name);
}
/// change name of tab
void renameTab(int index, dstring name) {
_tabControl.renameTab(index, name);
}
@property Visibility hiddenTabsVisibility() { return _tabHost.hiddenTabsVisibility; }
@property void hiddenTabsVisibility(Visibility v) { _tabHost.hiddenTabsVisibility = v; }
/// select tab
void selectTab(string ID, bool updateAccess = true) {
_tabHost.selectTab(ID, updateAccess);
}
/// select tab
void selectTab(int index, bool updateAccess = true) {
_tabControl.selectTab(index, updateAccess);
}
/// get tab content widget by id
Widget tabBody(string id) {
return _tabHost.tabBody(id);
}
/// get tab content widget by id
Widget tabBody(int index) {
string id = _tabControl.tab(index).id;
return _tabHost.tabBody(id);
}
/// returns tab item by id (null if index out of range)
TabItem tab(int index) {
return _tabControl.tab(index);
}
/// returns tab item by id (null if not found)
TabItem tab(string id) {
return _tabControl.tab(id);
}
/// returns tab count
@property int tabCount() const {
return _tabControl.tabCount;
}
/// get tab index by tab id (-1 if not found)
int tabIndex(string id) {
return _tabControl.tabIndex(id);
}
/// change style ids
void setStyles(string tabWidgetStyle, string tabStyle, string tabButtonStyle, string tabButtonTextStyle) {
styleId = tabWidgetStyle;
_tabControl.setStyles(tabStyle, tabButtonStyle, tabButtonTextStyle);
}
private bool _tabNavigationInProgress;
/// process key event, return true if event is processed.
override bool onKeyEvent(KeyEvent event) {
if (_tabNavigationInProgress) {
if (event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) {
if (!(event.flags & KeyFlag.Control)) {
_tabNavigationInProgress = false;
_tabControl.updateAccessTs();
}
}
}
if (event.action == KeyAction.KeyDown) {
if (event.keyCode == KeyCode.TAB && (event.flags & KeyFlag.Control)) {
// support Ctrl+Tab and Ctrl+Shift+Tab for navigation
_tabNavigationInProgress = true;
int direction = (event.flags & KeyFlag.Shift) ? - 1 : 1;
int index = _tabControl.getNextItemIndex(direction);
if (index >= 0)
selectTab(index, false);
return true;
}
}
return super.onKeyEvent(event);
}
@property const(TabItem) selectedTab() const {
return _tabControl.tab(selectedTabId);
}
@property TabItem selectedTab() {
return _tabControl.tab(selectedTabId);
}
@property string selectedTabId() const {
return _tabControl._selectedTabId;
}
/// get tab content widget by id
Widget selectedTabBody() {
return _tabHost.tabBody(_tabControl._selectedTabId);
}
}