dlangui/src/dlangui/widgets/lists.d

1548 lines
50 KiB
D

// Written in the D programming language.
/**
This module contains list widgets implementation.
Similar to lists implementation in Android UI API.
Synopsis:
----
import dlangui.widgets.lists;
----
Copyright: Vadim Lopatin, 2014
License: Boost License 1.0
Authors: Vadim Lopatin, coolreader.org@gmail.com
*/
module dlangui.widgets.lists;
import dlangui.widgets.widget;
import dlangui.widgets.controls;
import dlangui.widgets.scrollbar;
import dlangui.widgets.layouts;
import dlangui.core.signals;
/** interface - slot for onAdapterChangeListener */
interface OnAdapterChangeHandler {
void onAdapterChange(ListAdapter source);
}
/// list widget adapter provides items for list widgets
interface ListAdapter {
/// returns number of widgets in list
@property int itemCount() const;
/// return list item widget by item index
Widget itemWidget(int index);
/// return list item's state flags
uint itemState(int index) const;
/// set one or more list item's state flags, returns updated state
uint setItemState(int index, uint flags);
/// reset one or more list item's state flags, returns updated state
uint resetItemState(int index, uint flags);
/// returns integer item id by index (if supported)
int itemId(int index) const;
/// returns string item id by index (if supported)
string itemStringId(int index) const;
/// remove all items
void clear();
/// connect adapter change handler
ListAdapter connect(OnAdapterChangeHandler handler);
/// disconnect adapter change handler
ListAdapter disconnect(OnAdapterChangeHandler handler);
/// called when theme is changed
void onThemeChanged();
/// return true to receive mouse events
@property bool wantMouseEvents();
/// return true to receive keyboard events
@property bool wantKeyEvents();
}
/// List adapter for simple list of widget instances
class ListAdapterBase : ListAdapter {
/** Handle items change */
protected Signal!OnAdapterChangeHandler adapterChanged;
/// connect adapter change handler
override ListAdapter connect(OnAdapterChangeHandler handler) {
adapterChanged.connect(handler);
return this;
}
/// disconnect adapter change handler
override ListAdapter disconnect(OnAdapterChangeHandler handler) {
adapterChanged.disconnect(handler);
return this;
}
/// returns integer item id by index (if supported)
override int itemId(int index) const {
return 0;
}
/// returns string item id by index (if supported)
override string itemStringId(int index) const {
return null;
}
/// returns number of widgets in list
override @property int itemCount() const {
// override it
return 0;
}
/// return list item widget by item index
override Widget itemWidget(int index) {
// override it
return null;
}
/// return list item's state flags
override uint itemState(int index) const {
// override it
return State.Enabled;
}
/// set one or more list item's state flags, returns updated state
override uint setItemState(int index, uint flags) {
return 0;
}
/// reset one or more list item's state flags, returns updated state
override uint resetItemState(int index, uint flags) {
return 0;
}
/// remove all items
override void clear() {
}
/// notify listeners about list items changes
void updateViews() {
if (adapterChanged.assigned)
adapterChanged.emit(this);
}
/// called when theme is changed
void onThemeChanged() {
}
/// return true to receive mouse events
override @property bool wantMouseEvents() {
return false;
}
/// return true to receive keyboard events
override @property bool wantKeyEvents() {
return false;
}
}
/// List adapter for simple list of widget instances
class WidgetListAdapter : ListAdapterBase {
private WidgetList _widgets;
/// list of widgets to display
@property ref const(WidgetList) widgets() { return _widgets; }
/// returns number of widgets in list
@property override int itemCount() const {
return _widgets.count;
}
/// return list item widget by item index
override Widget itemWidget(int index) {
return _widgets.get(index);
}
/// return list item's state flags
override uint itemState(int index) const {
return _widgets.get(index).state;
}
/// set one or more list item's state flags, returns updated state
override uint setItemState(int index, uint flags) {
return _widgets.get(index).setState(flags).state;
}
/// reset one or more list item's state flags, returns updated state
override uint resetItemState(int index, uint flags) {
return _widgets.get(index).resetState(flags).state;
}
/// add item
WidgetListAdapter add(Widget item, int index = -1) {
_widgets.insert(item, index);
updateViews();
return this;
}
/// remove item
WidgetListAdapter remove(int index) {
auto item = _widgets.remove(index);
destroy(item);
updateViews();
return this;
}
/// remove all items
override void clear() {
_widgets.clear();
updateViews();
}
/// called when theme is changed
override void onThemeChanged() {
super.onThemeChanged();
foreach(w; _widgets)
w.onThemeChanged();
}
~this() {
//Log.d("Destroying WidgetListAdapter");
}
/// return true to receive mouse events
override @property bool wantMouseEvents() {
return true;
}
}
/** List adapter providing strings only. */
class StringListAdapterBase : ListAdapterBase {
protected UIStringCollection _items;
protected uint[] _states;
protected int[] _intIds;
protected string[] _stringIds;
protected string[] _iconIds;
protected int _lastItemIndex;
/** create empty string list adapter. */
this() {
_lastItemIndex = -1;
}
/** Init with array of string resource IDs. */
this(string[] items) {
_items.addAll(items);
_intIds.length = items.length;
_stringIds.length = items.length;
_iconIds.length = items.length;
_lastItemIndex = -1;
updateStatesLength();
}
/** Init with array of unicode strings. */
this(dstring[] items) {
_items.addAll(items);
_intIds.length = items.length;
_stringIds.length = items.length;
_iconIds.length = items.length;
_lastItemIndex = -1;
updateStatesLength();
}
/** Init with array of StringListValue. */
this(StringListValue[] items) {
_intIds.length = items.length;
_stringIds.length = items.length;
_iconIds.length = items.length;
for (int i = 0; i < items.length; i++) {
_items.add(items[i].label);
_intIds[i] = items[i].intId;
_stringIds[i] = items[i].stringId;
_iconIds[i] = items[i].iconId;
}
_lastItemIndex = -1;
updateStatesLength();
}
/// remove all items
override void clear() {
_items.clear();
updateStatesLength();
updateViews();
}
/// remove item by index
StringListAdapterBase remove(int index) {
if (index < 0 || index >= _items.length)
return this;
for (int i = 0; i < _items.length - 1; i++) {
_intIds[i] = _intIds[i + 1];
_stringIds[i] = _stringIds[i + 1];
_iconIds[i] = _iconIds[i + 1];
_states[i] = _states[i + 1];
}
_items.remove(index);
_intIds.length = items.length;
_states.length = _items.length;
_stringIds.length = items.length;
_iconIds.length = items.length;
updateViews();
return this;
}
/// add new item
StringListAdapterBase add(UIString item, int index = -1) {
if (index < 0 || index > _items.length)
index = _items.length;
_items.add(item, index);
_intIds.length = items.length;
_states.length = _items.length;
_stringIds.length = items.length;
_iconIds.length = items.length;
for (int i = _items.length - 1; i > index; i--) {
_intIds[i] = _intIds[i - 1];
_stringIds[i] = _stringIds[i - 1];
_iconIds[i] = _iconIds[i - 1];
_states[i] = _states[i - 1];
}
_intIds[index] = 0;
_stringIds[index] = null;
_iconIds[index] = null;
_states[index] = State.Enabled;
updateViews();
return this;
}
/// add new string resource item
StringListAdapterBase add(string item, int index = -1) {
return add(UIString.fromId(item), index);
}
/// add new raw dstring item
StringListAdapterBase add(dstring item, int index = -1) {
return add(UIString.fromRaw(item), index);
}
/** Access to items collection. */
@property ref const(UIStringCollection) items() { return _items; }
/** Replace items collection. */
@property StringListAdapterBase items(dstring[] values) {
_items = values;
_intIds.length = items.length;
_states.length = _items.length;
_stringIds.length = items.length;
_iconIds.length = items.length;
for (int i = 0; i < _items.length; i++) {
_intIds[i] = 0;
_stringIds[i] = null;
_iconIds[i] = null;
_states[i] = State.Enabled;
}
updateViews();
return this;
}
/** Replace items collection. */
@property StringListAdapterBase items(UIString[] values) {
_items = values;
_intIds.length = items.length;
_states.length = _items.length;
_stringIds.length = items.length;
_iconIds.length = items.length;
for (int i = 0; i < _items.length; i++) {
_intIds[i] = 0;
_stringIds[i] = null;
_iconIds[i] = null;
_states[i] = State.Enabled;
}
updateViews();
return this;
}
/** Replace items collection. */
@property StringListAdapterBase items(StringListValue[] values) {
_items = values;
_intIds.length = items.length;
_states.length = _items.length;
_stringIds.length = items.length;
_iconIds.length = items.length;
for (int i = 0; i < _items.length; i++) {
_intIds[i] = values[i].intId;
_stringIds[i] = values[i].stringId;
_iconIds[i] = values[i].iconId;
_states[i] = State.Enabled;
}
updateViews();
return this;
}
/// returns number of widgets in list
@property override int itemCount() const {
return _items.length;
}
/// returns integer item id by index (if supported)
override int itemId(int index) const {
return index >= 0 && index < _intIds.length ? _intIds[index] : 0;
}
/// returns string item id by index (if supported)
override string itemStringId(int index) const {
return index >= 0 && index < _stringIds.length ? _stringIds[index] : null;
}
protected void updateStatesLength() {
if (_states.length < _items.length) {
int oldlen = cast(int)_states.length;
_states.length = _items.length;
for (int i = oldlen; i < _items.length; i++)
_states[i] = State.Enabled;
}
if (_intIds.length < items.length)
_intIds.length = items.length;
if (_stringIds.length < items.length)
_stringIds.length = items.length;
if (_iconIds.length < items.length)
_iconIds.length = items.length;
}
/// return list item's state flags
override uint itemState(int index) const {
if (index < 0 || index >= _items.length)
return 0;
return _states[index];
}
/// set one or more list item's state flags, returns updated state
override uint setItemState(int index, uint flags) {
updateStatesLength();
_states[index] |= flags;
return _states[index];
}
/// reset one or more list item's state flags, returns updated state
override uint resetItemState(int index, uint flags) {
updateStatesLength();
_states[index] &= ~flags;
return _states[index];
}
~this() {
}
}
/** List adapter providing strings only. */
class StringListAdapter : StringListAdapterBase {
protected TextWidget _widget;
/** create empty string list adapter. */
this() {
super();
}
/** Init with array of string resource IDs. */
this(string[] items) {
super(items);
}
/** Init with array of unicode strings. */
this(dstring[] items) {
super(items);
}
/** Init with array of StringListValue. */
this(StringListValue[] items) {
super(items);
}
/// return list item widget by item index
override Widget itemWidget(int index) {
updateStatesLength();
if (_widget is null) {
_widget = new TextWidget("STRING_LIST_ITEM");
_widget.styleId = STYLE_LIST_ITEM;
} else {
if (index == _lastItemIndex)
return _widget;
}
// update widget
_widget.text = _items.get(index);
_widget.state = _states[index];
_lastItemIndex = index;
return _widget;
}
/// called when theme is changed
override void onThemeChanged() {
super.onThemeChanged();
if (_widget)
_widget.onThemeChanged();
}
/// set one or more list item's state flags, returns updated state
override uint setItemState(int index, uint flags) {
uint res = super.setItemState(index, flags);
if (_widget !is null && _lastItemIndex == index)
_widget.state = res;
return res;
}
/// reset one or more list item's state flags, returns updated state
override uint resetItemState(int index, uint flags) {
uint res = super.resetItemState(index, flags);
if (_widget !is null && _lastItemIndex == index)
_widget.state = res;
return res;
}
~this() {
if (_widget)
destroy(_widget);
}
}
/** List adapter providing strings with icons. */
class IconStringListAdapter : StringListAdapterBase {
protected HorizontalLayout _widget;
protected TextWidget _textWidget;
protected ImageWidget _iconWidget;
/** create empty string list adapter. */
this() {
super();
}
/** Init with array of StringListValue. */
this(StringListValue[] items) {
super(items);
}
/// return list item widget by item index
override Widget itemWidget(int index) {
updateStatesLength();
if (_widget is null) {
_widget = new HorizontalLayout("ICON_STRING_LIST_ITEM");
_widget.styleId = STYLE_LIST_ITEM;
_textWidget = new TextWidget("label");
_iconWidget = new ImageWidget("icon");
_widget.addChild(_iconWidget);
_widget.addChild(_textWidget);
} else {
if (index == _lastItemIndex)
return _widget;
}
// update widget
_textWidget.text = _items.get(index);
_textWidget.state = _states[index];
if (_iconIds[index]) {
_iconWidget.visibility = Visibility.Visible;
_iconWidget.drawableId = _iconIds[index];
} else {
_iconWidget.visibility = Visibility.Gone;
}
_lastItemIndex = index;
return _widget;
}
/// called when theme is changed
override void onThemeChanged() {
super.onThemeChanged();
if (_widget)
_widget.onThemeChanged();
}
/// set one or more list item's state flags, returns updated state
override uint setItemState(int index, uint flags) {
uint res = super.setItemState(index, flags);
if (_widget !is null && _lastItemIndex == index) {
_widget.state = res;
_textWidget.state = res;
}
return res;
}
/// reset one or more list item's state flags, returns updated state
override uint resetItemState(int index, uint flags) {
uint res = super.resetItemState(index, flags);
if (_widget !is null && _lastItemIndex == index) {
_widget.state = res;
_textWidget.state = res;
}
return res;
}
~this() {
if (_widget)
destroy(_widget);
}
}
/** interface - slot for onItemSelectedListener */
interface OnItemSelectedHandler {
bool onItemSelected(Widget source, int itemIndex);
}
/** interface - slot for onItemClickListener */
interface OnItemClickHandler {
bool onItemClick(Widget source, int itemIndex);
}
/** List widget - shows content as hori*/
class ListWidget : WidgetGroup, OnScrollHandler, OnAdapterChangeHandler {
/** Handle selection change. */
Signal!OnItemSelectedHandler itemSelected;
/** Handle item click / activation (e.g. Space or Enter key press and mouse double click) */
Signal!OnItemClickHandler itemClick;
protected Orientation _orientation = Orientation.Vertical;
/// returns linear layout orientation (Vertical, Horizontal)
@property Orientation orientation() { return _orientation; }
/// sets linear layout orientation
@property ListWidget orientation(Orientation value) {
_orientation = value;
_scrollbar.orientation = value;
requestLayout();
return this;
}
protected Rect[] _itemRects;
protected Point[] _itemSizes;
protected bool _needScrollbar;
protected Point _sbsz; // scrollbar size
protected ScrollBar _scrollbar;
protected int _lastMeasureWidth;
protected int _lastMeasureHeight;
/// first visible item index
protected int _firstVisibleItem;
/// scroll position - offset of scroll area
protected int _scrollPosition;
/// maximum scroll position
protected int _maxScrollPosition;
/// client area rectangle (counting padding, margins, and scrollbar)
protected Rect _clientRc;
/// total height of all items for Vertical orientation, or width for Horizontal
protected int _totalSize;
/// item with Hover state, -1 if no such item
protected int _hoverItemIndex;
/// item with Selected state, -1 if no such item
protected int _selectedItemIndex;
/// when true, mouse hover selects underlying item
protected bool _selectOnHover;
/// when true, mouse hover selects underlying item
@property bool selectOnHover() { return _selectOnHover; }
/// when true, mouse hover selects underlying item
@property ListWidget selectOnHover(bool select) { _selectOnHover = select; return this; }
/// if true, generate itemClicked on mouse down instead mouse up event
protected bool _clickOnButtonDown;
/// returns rectangle for item (not scrolled, first item starts at 0,0)
Rect itemRectNoScroll(int index) {
if (index < 0 || index >= _itemRects.length)
return Rect.init;
Rect res;
res = _itemRects[index];
return res;
}
/// returns rectangle for item (scrolled)
Rect itemRect(int index) {
if (index < 0 || index >= _itemRects.length)
return Rect.init;
Rect res = itemRectNoScroll(index);
if (_orientation == Orientation.Horizontal) {
res.left -= _scrollPosition;
res.right -= _scrollPosition;
} else {
res.top -= _scrollPosition;
res.bottom -= _scrollPosition;
}
return res;
}
/// returns item index by 0-based offset from top/left of list content
int itemByPosition(int pos) {
return 0;
}
protected ListAdapter _adapter;
/// when true, need to destroy adapter on list destroy
protected bool _ownAdapter;
/// get adapter
@property ListAdapter adapter() { return _adapter; }
/// set adapter
@property ListWidget adapter(ListAdapter adapter) {
if (_adapter is adapter)
return this; // no changes
if (_adapter)
_adapter.disconnect(this);
if (_adapter !is null && _ownAdapter)
destroy(_adapter);
_adapter = adapter;
if (_adapter)
_adapter.connect(this);
_ownAdapter = false;
onAdapterChange(_adapter);
return this;
}
/// set adapter, which will be owned by list (destroy will be called for adapter on widget destroy)
@property ListWidget ownAdapter(ListAdapter adapter) {
if (_adapter is adapter)
return this; // no changes
if (_adapter)
_adapter.disconnect(this);
if (_adapter !is null && _ownAdapter)
destroy(_adapter);
_adapter = adapter;
if (_adapter)
_adapter.connect(this);
_ownAdapter = true;
onAdapterChange(_adapter);
return this;
}
/// returns number of widgets in list
@property int itemCount() {
if (_adapter !is null)
return _adapter.itemCount;
return 0;
}
/// return list item widget by item index
Widget itemWidget(int index) {
if (_adapter !is null)
return _adapter.itemWidget(index);
return null;
}
/// returns true if item with corresponding index is enabled
bool itemEnabled(int index) {
if (_adapter !is null && index >= 0 && index < itemCount)
return (_adapter.itemState(index) & State.Enabled) != 0;
return false;
}
/// empty parameter list constructor - for usage by factory
this() {
this(null);
}
/// create with ID parameter
this(string ID, Orientation orientation = Orientation.Vertical) {
super(ID);
_orientation = orientation;
focusable = true;
_hoverItemIndex = -1;
_selectedItemIndex = -1;
_scrollbar = new ScrollBar("listscroll", orientation);
_scrollbar.visibility = Visibility.Gone;
_scrollbar.scrollEvent = &onScrollEvent;
addChild(_scrollbar);
}
protected void setHoverItem(int index) {
if (_hoverItemIndex == index)
return;
if (_hoverItemIndex != -1) {
_adapter.resetItemState(_hoverItemIndex, State.Hovered);
invalidate();
}
_hoverItemIndex = index;
if (_hoverItemIndex != -1) {
_adapter.setItemState(_hoverItemIndex, State.Hovered);
invalidate();
}
}
/// item list is changed
override void onAdapterChange(ListAdapter source) {
requestLayout();
}
/// override to handle change of selection
protected void selectionChanged(int index, int previouslySelectedItem = -1) {
if (itemSelected.assigned)
itemSelected(this, index);
}
/// override to handle mouse up on item
protected void itemClicked(int index) {
if (itemClick.assigned)
itemClick(this, index);
}
/// allow to override state for updating of items
// currently used to treat main menu items with opened submenu as focused
@property protected uint overrideStateForItem() {
return state;
}
protected void updateSelectedItemFocus() {
if (_selectedItemIndex != -1) {
if ((_adapter.itemState(_selectedItemIndex) & State.Focused) != (overrideStateForItem & State.Focused)) {
if (overrideStateForItem & State.Focused)
_adapter.setItemState(_selectedItemIndex, State.Focused);
else
_adapter.resetItemState(_selectedItemIndex, State.Focused);
invalidate();
}
}
}
/// override to handle focus changes
override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) {
updateSelectedItemFocus();
}
/// ensure selected item is visible (scroll if necessary)
void makeSelectionVisible() {
if (_selectedItemIndex < 0)
return; // no selection
if (needLayout) {
_makeSelectionVisibleOnNextLayout = true;
return;
}
makeItemVisible(_selectedItemIndex);
}
protected bool _makeSelectionVisibleOnNextLayout;
/// ensure item is visible
void makeItemVisible(int itemIndex) {
if (itemIndex < 0 || itemIndex >= itemCount)
return; // no selection
Rect viewrc = Rect(0, 0, _clientRc.width, _clientRc.height);
Rect scrolledrc = itemRect(itemIndex);
if (scrolledrc.isInsideOf(viewrc)) // completely visible
return;
int delta = 0;
if (_orientation == Orientation.Vertical) {
if (scrolledrc.top < viewrc.top)
delta = scrolledrc.top - viewrc.top;
else if (scrolledrc.bottom > viewrc.bottom)
delta = scrolledrc.bottom - viewrc.bottom;
} else {
if (scrolledrc.left < viewrc.left)
delta = scrolledrc.left - viewrc.left;
else if (scrolledrc.right > viewrc.right)
delta = scrolledrc.right - viewrc.right;
}
int newPosition = _scrollPosition + delta;
_scrollbar.position = newPosition;
_scrollPosition = newPosition;
invalidate();
}
/// move selection
bool moveSelection(int direction, bool wrapAround = true) {
if (itemCount <= 0)
return false;
int maxAttempts = itemCount - 1;
int index = _selectedItemIndex;
for (int i = 0; i < maxAttempts; i++) {
int newIndex = 0;
if (index < 0) {
// no previous selection
if (direction > 0)
newIndex = wrapAround ? 0 : itemCount - 1;
else
newIndex = wrapAround ? itemCount - 1 : 0;
} else {
// step
newIndex = index + direction;
}
if (newIndex < 0)
newIndex = wrapAround ? itemCount - 1 : 0;
else if (newIndex >= itemCount)
newIndex = wrapAround ? 0 : itemCount - 1;
if (newIndex != index) {
if (selectItem(newIndex)) {
selectionChanged(_selectedItemIndex, index);
return true;
}
index = newIndex;
}
}
return true;
}
bool selectItem(int index, int disabledItemsSkipDirection) {
//debug Log.d("selectItem ", index, " skipDirection=", disabledItemsSkipDirection);
if (index == -1 || disabledItemsSkipDirection == 0)
return selectItem(index);
int maxAttempts = itemCount;
for (int i = 0; i < maxAttempts; i++) {
if (selectItem(index))
return true;
index += disabledItemsSkipDirection > 0 ? 1 : -1;
if (index < 0)
index = itemCount - 1;
if (index >= itemCount)
index = 0;
}
return false;
}
/** Selected item index. */
@property int selectedItemIndex() {
return _selectedItemIndex;
}
@property void selectedItemIndex(int index) {
selectItem(index);
}
bool selectItem(int index) {
//debug Log.d("selectItem ", index);
if (_selectedItemIndex == index) {
updateSelectedItemFocus();
makeSelectionVisible();
return true;
}
if (index != -1 && !itemEnabled(index))
return false;
if (_selectedItemIndex != -1) {
_adapter.resetItemState(_selectedItemIndex, State.Selected | State.Focused);
invalidate();
}
_selectedItemIndex = index;
if (_selectedItemIndex != -1) {
makeSelectionVisible();
_adapter.setItemState(_selectedItemIndex, State.Selected | (overrideStateForItem & State.Focused));
invalidate();
}
return true;
}
~this() {
if (_adapter)
_adapter.disconnect(this);
//Log.d("Destroying List ", _id);
if (_adapter !is null && _ownAdapter)
destroy(_adapter);
_adapter = null;
}
/// handle scroll event
override bool onScrollEvent(AbstractSlider source, ScrollEvent event) {
int newPosition = _scrollPosition;
if (event.action == ScrollAction.SliderMoved) {
// scroll
newPosition = event.position;
} else {
// use default handler for page/line up/down events
newPosition = event.defaultUpdatePosition();
}
if (_scrollPosition != newPosition) {
_scrollPosition = newPosition;
if (_scrollPosition > _maxScrollPosition)
_scrollPosition = _maxScrollPosition;
if (_scrollPosition < 0)
_scrollPosition = 0;
invalidate();
}
return true;
}
/// handle theme change: e.g. reload some themed resources
override void onThemeChanged() {
super.onThemeChanged();
_scrollbar.onThemeChanged();
for (int i = 0; i < itemCount; i++) {
Widget w = itemWidget(i);
w.onThemeChanged();
}
if (_adapter)
_adapter.onThemeChanged();
}
/// sets minimum size for the list, override to change
Point minimumVisibleContentSize() {
if (_orientation == Orientation.Vertical)
return Point(measureMinChildrenSize().x, 100);
else
return Point(100, measureMinChildrenSize().y);
}
protected Point measureMinChildrenSize() {
// measure children
Point sz;
for (int i = 0; i < itemCount; i++) {
Widget w = itemWidget(i);
if (w is null || w.visibility == Visibility.Gone)
continue;
w.measure(SIZE_UNSPECIFIED, SIZE_UNSPECIFIED);
if (_orientation == Orientation.Vertical) {
// Vertical
if (sz.x < w.measuredWidth)
sz.x = w.measuredWidth;
sz.y += w.measuredHeight;
} else {
// Horizontal
if (sz.y < w.measuredHeight)
sz.y = w.measuredHeight;
sz.x += w.measuredWidth;
}
}
return sz;
}
/// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
override void measure(int parentWidth, int parentHeight) {
if (visibility == Visibility.Gone) {
_measuredWidth = _measuredHeight = 0;
return;
}
if (_itemSizes.length < itemCount)
_itemSizes.length = itemCount;
Rect m = margins;
Rect p = padding;
// set widget area to small when first measure
if (parentWidth == SIZE_UNSPECIFIED && parentHeight == SIZE_UNSPECIFIED)
{
Point sz = minimumVisibleContentSize;
measuredContent(parentWidth, parentHeight, sz.x, sz.y);
return;
}
// 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;
bool oldNeedLayout = _needLayout;
Visibility oldScrollbarVisibility = _scrollbar.visibility;
_scrollbar.visibility = Visibility.Visible;
_scrollbar.measure(pwidth, pheight);
_lastMeasureWidth = pwidth;
_lastMeasureHeight = pheight;
int sbsize = _orientation == Orientation.Vertical ? _scrollbar.measuredWidth : _scrollbar.measuredHeight;
// measure children
Point sz;
_sbsz.destroy();
for (int i = 0; i < itemCount; i++) {
Widget w = itemWidget(i);
if (w is null || w.visibility == Visibility.Gone) {
_itemSizes[i].x = _itemSizes[i].y = 0;
continue;
}
w.measure(pwidth, pheight);
_itemSizes[i].x = w.measuredWidth;
_itemSizes[i].y = w.measuredHeight;
if (_orientation == Orientation.Vertical) {
// Vertical
if (sz.x < w.measuredWidth)
sz.x = w.measuredWidth;
sz.y += w.measuredHeight;
} else {
// Horizontal
if (sz.y < w.measuredHeight)
sz.y = w.measuredHeight;
sz.x += w.measuredWidth;
}
}
_needScrollbar = false;
if (_orientation == Orientation.Vertical) {
if (pheight != SIZE_UNSPECIFIED && sz.y > pheight) {
// need scrollbar
if (pwidth != SIZE_UNSPECIFIED) {
pwidth -= sbsize;
_sbsz.x = sbsize;
_needScrollbar = true;
}
}
} else {
if (pwidth != SIZE_UNSPECIFIED && sz.x > pwidth) {
// need scrollbar
if (pheight != SIZE_UNSPECIFIED) {
pheight -= sbsize;
_sbsz.y = sbsize;
_needScrollbar = true;
}
}
}
if (_needScrollbar) {
// recalculate with scrollbar
sz.x = sz.y = 0;
for (int i = 0; i < itemCount; i++) {
Widget w = itemWidget(i);
if (w is null || w.visibility == Visibility.Gone)
continue;
w.measure(pwidth, pheight);
_itemSizes[i].x = w.measuredWidth;
_itemSizes[i].y = w.measuredHeight;
if (_orientation == Orientation.Vertical) {
// Vertical
if (sz.x < w.measuredWidth)
sz.x = w.measuredWidth;
sz.y += w.measuredHeight;
} else {
// Horizontal
w.measure(pwidth, pheight);
if (sz.y < w.measuredHeight)
sz.y = w.measuredHeight;
sz.x += w.measuredWidth;
}
}
}
measuredContent(parentWidth, parentHeight, sz.x + _sbsz.x, sz.y + _sbsz.y);
if (_scrollbar.visibility == oldScrollbarVisibility) {
_needLayout = oldNeedLayout;
_scrollbar.cancelLayout();
}
}
protected void updateItemPositions() {
Rect r;
int p = 0;
for (int i = 0; i < itemCount; i++) {
if (_itemSizes[i].x == 0 && _itemSizes[i].y == 0)
continue;
if (_orientation == Orientation.Vertical) {
// Vertical
int w = _clientRc.width;
int h = _itemSizes[i].y;
r.top = p;
r.bottom = p + h;
r.left = 0;
r.right = w;
_itemRects[i] = r;
p += h;
} else {
// Horizontal
int h = _clientRc.height;
int w = _itemSizes[i].x;
r.top = 0;
r.bottom = h;
r.left = p;
r.right = p + w;
_itemRects[i] = r;
p += w;
}
}
_totalSize = p;
if (_needScrollbar) {
if (_orientation == Orientation.Vertical) {
_scrollbar.setRange(0, p);
_scrollbar.pageSize = _clientRc.height;
_scrollbar.position = _scrollPosition;
} else {
_scrollbar.setRange(0, p);
_scrollbar.pageSize = _clientRc.width;
_scrollbar.position = _scrollPosition;
}
}
/// maximum scroll position
if (_orientation == Orientation.Vertical) {
_maxScrollPosition = _totalSize - _clientRc.height;
if (_maxScrollPosition < 0)
_maxScrollPosition = 0;
} else {
_maxScrollPosition = _totalSize - _clientRc.width;
if (_maxScrollPosition < 0)
_maxScrollPosition = 0;
}
if (_scrollPosition > _maxScrollPosition)
_scrollPosition = _maxScrollPosition;
if (_scrollPosition < 0)
_scrollPosition = 0;
if (_needScrollbar) {
if (_orientation == Orientation.Vertical) { // FIXME:
_scrollbar.position = _scrollPosition;
} else {
_scrollbar.position = _scrollPosition;
}
}
}
/// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
override void layout(Rect rc) {
_needLayout = false;
if (visibility == Visibility.Gone) {
return;
}
_pos = rc;
Rect parentrc = rc;
applyMargins(rc);
applyPadding(rc);
if (_itemRects.length < itemCount)
_itemRects.length = itemCount;
// measure again if client size has been changed
if (_lastMeasureWidth != rc.width || _lastMeasureHeight != rc.height)
measure(parentrc.width, parentrc.height);
// hide scrollbar or update rc for scrollbar
Rect sbrect = rc;
// layout scrollbar
if (_needScrollbar) {
rc.right -= _sbsz.x;
rc.bottom -= _sbsz.y;
} else {
_scrollbar.visibility = Visibility.Gone;
}
_clientRc = rc;
// calc item rectangles
updateItemPositions();
// layout scrollbar - must be under updateItemPositions()
if (_needScrollbar) {
_scrollbar.visibility = Visibility.Visible;
if (_orientation == Orientation.Vertical)
sbrect.left = sbrect.right - _sbsz.x;
else
sbrect.top = sbrect.bottom - _sbsz.y;
_scrollbar.layout(sbrect);
}
if (_makeSelectionVisibleOnNextLayout) {
makeSelectionVisible();
_makeSelectionVisibleOnNextLayout = false;
}
_needLayout = false;
_scrollbar.cancelLayout();
}
/// 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, alpha);
// draw scrollbar
if (_needScrollbar)
_scrollbar.onDraw(buf);
Point scrollOffset;
if (_orientation == Orientation.Vertical) {
scrollOffset.y = _scrollPosition;
} else {
scrollOffset.x = _scrollPosition;
}
// draw items
for (int i = 0; i < itemCount; i++) {
Rect itemrc = _itemRects[i];
itemrc.left += rc.left - scrollOffset.x;
itemrc.right += rc.left - scrollOffset.x;
itemrc.top += rc.top - scrollOffset.y;
itemrc.bottom += rc.top - scrollOffset.y;
if (itemrc.intersects(rc)) {
Widget w = itemWidget(i);
if (w is null || w.visibility != Visibility.Visible)
continue;
w.layout(itemrc);
w.onDraw(buf);
}
}
}
/// list navigation using keys
override bool onKeyEvent(KeyEvent event) {
if (itemCount == 0)
return false;
int navigationDelta = 0;
if (event.action == KeyAction.KeyDown) {
if (orientation == Orientation.Vertical) {
if (event.keyCode == KeyCode.DOWN)
navigationDelta = 1;
else if (event.keyCode == KeyCode.UP)
navigationDelta = -1;
} else {
if (event.keyCode == KeyCode.RIGHT)
navigationDelta = 1;
else if (event.keyCode == KeyCode.LEFT)
navigationDelta = -1;
}
}
if (navigationDelta != 0) {
moveSelection(navigationDelta);
return true;
}
if (event.action == KeyAction.KeyDown) {
if (event.keyCode == KeyCode.HOME) {
// select first enabled item on HOME key
selectItem(0, 1);
return true;
} else if (event.keyCode == KeyCode.END) {
// select last enabled item on END key
selectItem(itemCount - 1, -1);
return true;
} else if (event.keyCode == KeyCode.PAGEDOWN) {
// TODO
} else if (event.keyCode == KeyCode.PAGEUP) {
// TODO
}
}
if ((event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN)) {
if (event.action == KeyAction.KeyDown && enabled) {
if (itemEnabled(_selectedItemIndex)) {
itemClicked(_selectedItemIndex);
}
}
return true;
}
return super.onKeyEvent(event);
//if (_selectedItemIndex != -1 && event.action == KeyAction.KeyUp && (event.keyCode == KeyCode.SPACE || event.keyCode == KeyCode.RETURN)) {
// itemClicked(_selectedItemIndex);
// return true;
//}
//if (navigationDelta != 0) {
// int p = _selectedItemIndex;
// if (p < 0) {
// if (navigationDelta < 0)
// p = itemCount - 1;
// else
// p = 0;
// } else {
// p += navigationDelta;
// if (p < 0)
// p = itemCount - 1;
// else if (p >= itemCount)
// p = 0;
// }
// setHoverItem(-1);
// selectItem(p);
// return true;
//}
//return false;
}
/// process mouse event; return true if event is processed by widget.
override bool onMouseEvent(MouseEvent event) {
//Log.d("onMouseEvent ", id, " ", event.action, " (", event.x, ",", event.y, ")");
if (event.action == MouseAction.Leave || event.action == MouseAction.Cancel) {
setHoverItem(-1);
return true;
}
// delegate processing of mouse wheel to scrollbar widget
if (event.action == MouseAction.Wheel && _needScrollbar) {
return _scrollbar.onMouseEvent(event);
}
// support onClick
Rect rc = _pos;
applyMargins(rc);
applyPadding(rc);
Point scrollOffset;
if (_orientation == Orientation.Vertical) {
scrollOffset.y = _scrollPosition;
} else {
scrollOffset.x = _scrollPosition;
}
if (event.action == MouseAction.Wheel) {
if (_scrollbar)
_scrollbar.sendScrollEvent(event.wheelDelta > 0 ? ScrollAction.LineUp : ScrollAction.LineDown);
return true;
}
if (event.action == MouseAction.ButtonDown && (event.flags & (MouseFlag.LButton || MouseFlag.RButton)))
setFocus();
if (itemCount > _itemRects.length)
return true; // layout not yet called
for (int i = 0; i < itemCount; i++) {
Rect itemrc = _itemRects[i];
itemrc.left += rc.left - scrollOffset.x;
itemrc.right += rc.left - scrollOffset.x;
itemrc.top += rc.top - scrollOffset.y;
itemrc.bottom += rc.top - scrollOffset.y;
if (itemrc.isPointInside(Point(event.x, event.y))) {
if (_adapter && _adapter.wantMouseEvents) {
auto itemWidget = _adapter.itemWidget(i);
if (itemWidget) {
Widget oldParent = itemWidget.parent;
itemWidget.parent = this;
if (event.action == MouseAction.Move && event.noModifiers && itemWidget.hasTooltip) {
itemWidget.scheduleTooltip(200);
}
//itemWidget.onMouseEvent(event);
itemWidget.parent = oldParent;
}
}
//Log.d("mouse event action=", event.action, " button=", event.button, " flags=", event.flags);
if ((event.flags & (MouseFlag.LButton || MouseFlag.RButton)) || _selectOnHover) {
if (_selectedItemIndex != i && itemEnabled(i)) {
int prevSelection = _selectedItemIndex;
selectItem(i);
setHoverItem(-1);
selectionChanged(_selectedItemIndex, prevSelection);
}
} else {
if (itemEnabled(i))
setHoverItem(i);
}
if (event.button == MouseButton.Left || event.button == MouseButton.Right) {
if ((_clickOnButtonDown && event.action == MouseAction.ButtonDown) || (!_clickOnButtonDown && event.action == MouseAction.ButtonUp)) {
if (itemEnabled(i)) {
itemClicked(i);
if (_clickOnButtonDown)
event.doNotTrackButtonDown = true;
}
}
}
return true;
}
}
return true;
}
/// returns true if item is child of this widget (when deepSearch == true - returns true if item is this widget or one of children inside children tree).
override bool isChild(Widget item, bool deepSearch = true) {
if (_adapter && _adapter.wantMouseEvents) {
for (int i = 0; i < itemCount; i++) {
auto itemWidget = _adapter.itemWidget(i);
if (itemWidget is item)
return true;
}
}
return super.isChild(item, deepSearch);
}
}
class StringListWidget : ListWidget {
import std.conv : to;
import std.datetime.stopwatch : StopWatch;
import core.time : dur;
private dstring _searchString;
private StopWatch _stopWatch;
this(string ID = null) {
super(ID);
styleId = STYLE_EDIT_BOX;
}
this(string ID, string[] items) {
super(ID);
styleId = STYLE_EDIT_BOX;
ownAdapter = new StringListAdapter(items);
}
this(string ID, dstring[] items) {
super(ID);
styleId = STYLE_EDIT_BOX;
ownAdapter = new StringListAdapter(items);
}
this(string ID, StringListValue[] items) {
super(ID);
styleId = STYLE_EDIT_BOX;
ownAdapter = new StringListAdapter(items);
}
@property void items(string[] itemResourceIds) {
_selectedItemIndex = -1;
ownAdapter = new StringListAdapter(itemResourceIds);
if(itemResourceIds.length > 0) {
selectedItemIndex = 0;
}
requestLayout();
}
@property void items(dstring[] items) {
_selectedItemIndex = -1;
ownAdapter = new StringListAdapter(items);
if(items.length > 0) {
selectedItemIndex = 0;
}
requestLayout();
}
@property void items(StringListValue[] items) {
_selectedItemIndex = -1;
ownAdapter = new StringListAdapter(items);
if(items.length > 0) {
selectedItemIndex = 0;
}
requestLayout();
}
/// StringListValue list values
override bool setStringListValueListProperty(string propName, StringListValue[] values) {
if (propName == "items") {
items = values;
return true;
}
return false;
}
/// get selected item as text
@property dstring selectedItem() {
if (_selectedItemIndex < 0 || _selectedItemIndex >= _adapter.itemCount)
return "";
return (cast(StringListAdapter)adapter).items.get(_selectedItemIndex);
}
override bool onKeyEvent(KeyEvent event) {
if (itemCount == 0) return false;
// Accept user input and try to find a match in the list.
if (event.action == KeyAction.Text) {
if ( !_stopWatch.running) { _stopWatch.start; }
auto timePassed = _stopWatch.peek; //.to!("seconds", float)(); // dtop is std.datetime.to
if (timePassed > dur!"msecs"(500))
_searchString = ""d;
_searchString ~= to!dchar(event.text.toUTF8);
_stopWatch.reset;
if ( selectClosestMatch(_searchString) ) {
invalidate();
return true;
}
}
return super.onKeyEvent(event);
}
private bool selectClosestMatch(dstring term) {
import std.uni : toLower;
if (term.length == 0) return false;
auto myItems = (cast(StringListAdapter)adapter).items;
// Perfect match or best match
int[] indexes;
foreach(int itemIndex; 0 .. myItems.length) {
dstring item = myItems.get(itemIndex);
if (item == term) {
// Perfect match
indexes ~= itemIndex;
break;
} else {
// Term approximate to something
bool addItem = true;
foreach(int termIndex; 0 .. cast(int)term.length) {
if (termIndex < item.length) {
if ( toLower(term[termIndex]) != toLower(item[termIndex]) ) {
addItem = false;
break;
}
}
}
if (addItem) { indexes ~= itemIndex; }
}
}
// Return best match
if (indexes.length > 0) {
selectItem(indexes[0]);
itemSelected(this, indexes[0]);
return true;
}
return false; // Did not find term
}
}
//import dlangui.widgets.metadata;
//mixin(registerWidgets!(ListWidget, StringListWidget)());