From 9fd29770a93e24c3cdc200d570c2c04a2ddb8a94 Mon Sep 17 00:00:00 2001
From: Vadim Lopatin <coolreader.org@gmail.com>
Date: Thu, 4 Dec 2014 11:53:36 +0300
Subject: [PATCH] tree widget - almost usable

---
 examples/example1/src/main.d        |  23 ++-
 res/mdpi/arrow_right_down_black.png | Bin 160 -> 154 bytes
 res/mdpi/arrow_right_hollow.png     | Bin 2873 -> 2877 bytes
 res/theme_default.xml               |  10 +-
 src/dlangui/widgets/controls.d      |   6 +-
 src/dlangui/widgets/tree.d          | 253 ++++++++++++++++++++++++----
 src/dlangui/widgets/widget.d        |   2 -
 7 files changed, 252 insertions(+), 42 deletions(-)

diff --git a/examples/example1/src/main.d b/examples/example1/src/main.d
index 0f600c50..74394a72 100644
--- a/examples/example1/src/main.d
+++ b/examples/example1/src/main.d
@@ -623,15 +623,28 @@ extern (C) int UIAppMain(string[] args) {
         // tree view example
 		TreeWidget tree = new TreeWidget("TREE1");
 		tree.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
-        TreeItem tree1 = tree.items.newChild("group1", "Group 1"d);
+        TreeItem tree1 = tree.items.newChild("group1", "Group 1"d, "document-open");
         tree1.newChild("g1_1", "Group 1 item 1"d);
         tree1.newChild("g1_2", "Group 1 item 2"d);
         tree1.newChild("g1_3", "Group 1 item 3"d);
-        TreeItem tree2 = tree.items.newChild("group2", "Group 2"d);
-        tree2.newChild("g2_1", "Group 2 item 1"d);
-        tree2.newChild("g2_2", "Group 2 item 2"d);
-        tree2.newChild("g2_3", "Group 2 item 3"d);
+        TreeItem tree2 = tree.items.newChild("group2", "Group 2"d, "document-save");
+        tree2.newChild("g2_1", "Group 2 item 1"d, "edit-copy");
+        tree2.newChild("g2_2", "Group 2 item 2"d, "edit-cut");
+        tree2.newChild("g2_3", "Group 2 item 3"d, "edit-paste");
         tree2.newChild("g2_4", "Group 2 item 4"d);
+        TreeItem tree3 = tree.items.newChild("group3", "Group 3"d);
+        tree3.newChild("g3_1", "Group 3 item 1"d);
+        tree3.newChild("g3_2", "Group 3 item 2"d);
+        TreeItem tree32 = tree3.newChild("g3_3", "Group 3 item 3"d);
+        tree3.newChild("g3_4", "Group 3 item 4"d);
+        tree32.newChild("group3_2_1", "Group 3 item 2 subitem 1"d);
+        tree32.newChild("group3_2_2", "Group 3 item 2 subitem 2"d);
+        tree32.newChild("group3_2_3", "Group 3 item 2 subitem 3"d);
+        tree32.newChild("group3_2_4", "Group 3 item 2 subitem 4"d);
+        tree32.newChild("group3_2_5", "Group 3 item 2 subitem 5"d);
+        tree3.newChild("g3_5", "Group 3 item 5"d);
+        tree3.newChild("g3_6", "Group 3 item 6"d);
+
         tree.items.selectItem(tree.items.child(0));
 		tabs.addTab(tree, "Tree"d);
 
diff --git a/res/mdpi/arrow_right_down_black.png b/res/mdpi/arrow_right_down_black.png
index e9e92bd81fdf4bd128e748c77ff7d5460b6542f8..e4adc299332bea75dd819e06c02383eb25ae813d 100644
GIT binary patch
delta 119
zcmZ3$IE!(DR>6M;AY8C_`D6wL2F?PH$YKTtZeb8+WSBKa0w~B{;_2(kevgNRL(eKV
zOQ#<wr0(hB7@~3h?WKi`3=A9%26o>|{v2?cx{s|UJa_?X6d3(Fe2^*WwL~g2Yo9hy
O3xlVtpUXO@geCxo(j|WY

delta 125
zcmbQmxPWnjR@p=bAk6!7<P-w~180FpWHAE+w=f7ZGR&GI0Tg5}@$_|NzsJMK&MWBr
zS9BpzNY~TFF+}71)k_<B84NfM7}&jk`A$$$;O5@cXF=;7ShWHgxk45&iLP*9y*2+2
ZbIT+f`;(?Gf`A$sJYD@<);T3K0RSsRC;I>Z

diff --git a/res/mdpi/arrow_right_hollow.png b/res/mdpi/arrow_right_hollow.png
index 8c0aab510e6a5424df2c88c1edf7f80d9056110c..1a30a31200d6fcb9a3554b15a272d99105d84bc7 100644
GIT binary patch
delta 126
zcmV-^0D=Fx7QGg*tO^PL0RI60puMM)leG#UXaoo|AK3zw)BpegNJ&INR4C8QkueSc
zAP57i|6j_7&xyo1n4k`JV!0~>>8se5B#vMp;jN%Q-yoX-Y*P}<a9OazCBd`i<D1Li
g^`9dm$EGhc0Fg;!04m)+VgLXD07*qoM6N<$f>9PVH~;_u

delta 122
zcmV-=0EPd(7P%I%tO^N{0FeNZZv4c`leG#UWB~^XH88(tw*UYDL`g(JR4C7_k}(PZ
zFbKr_dY|Bn)1?I|9rQHdXhc1;VK}v<W_Onb0J4CKA|O%~@KO=*BP*DjEblB|TmEw^
cVV!KJ**#sMw#JtuIsgCw07*qoM6N<$f{*?%S^xk5

diff --git a/res/theme_default.xml b/res/theme_default.xml
index 21cd1b64..1f735434 100644
--- a/res/theme_default.xml
+++ b/res/theme_default.xml
@@ -172,15 +172,19 @@
         <state state_hovered="true" backgroundColor="#C0FFFF00"/>
     </style>
     <style id="TREE_ITEM_EXPAND_ICON"
-        align="Center"
+        margins="2,0,2,0"
+        align="Left|VCenter"
         textFlags="Parent"
     />
     <style id="TREE_ITEM_ICON"
-        align="Center"
+        margins="2,0,2,0"
+        align="Left|VCenter"
         textFlags="Parent"
     />
     <style id="TREE_ITEM_LABEL"
-        align="Center"
+        layoutWidth="FILL_PARENT"
+        layoutHeight="WRAP_CONTENT"
+        align="Left|VCenter"
         textFlags="Parent"
     />
 </theme>
diff --git a/src/dlangui/widgets/controls.d b/src/dlangui/widgets/controls.d
index 2adb7e91..d7b381ed 100644
--- a/src/dlangui/widgets/controls.d
+++ b/src/dlangui/widgets/controls.d
@@ -406,7 +406,11 @@ class AbstractSlider : WidgetGroup {
         return this;
     }
 
-    protected bool sendScrollEvent(ScrollAction action, int position) {
+    bool sendScrollEvent(ScrollAction action) {
+        return sendScrollEvent(action, _position);
+    }
+
+    bool sendScrollEvent(ScrollAction action, int position) {
         if (!onScrollEventListener.assigned)
             return false;
         ScrollEvent event = new ScrollEvent(action, _minValue, _maxValue, _pageSize, position);
diff --git a/src/dlangui/widgets/tree.d b/src/dlangui/widgets/tree.d
index 26d28b0c..22dd5689 100644
--- a/src/dlangui/widgets/tree.d
+++ b/src/dlangui/widgets/tree.d
@@ -46,6 +46,13 @@ class TreeItem {
         return res;
     }
 
+    /// returns topmost item
+    @property TreeItems root() {
+        TreeItem p = this;
+        while (p._parent)
+            p = p._parent;
+        return cast(TreeItems)p;
+    }
 
     @property TreeItem parent() { return _parent; }
     @property protected TreeItem parent(TreeItem p) { _parent = p; return this; }
@@ -86,9 +93,7 @@ class TreeItem {
     }
 
     @property TreeItem selectedItem() {
-        if (_parent)
-            return _parent.selectedItem();
-        return null;
+        return root.selectedItem();
     }
 
     bool isSelected() {
@@ -156,19 +161,16 @@ class TreeItem {
     int childIndex(TreeItem item) { return _children.indexOf(item); }
     /// notify listeners
     protected void onUpdate(TreeItem item) {
-        if (_parent)
-            _parent.onUpdate(item);
+        root.onUpdate(item);
     }
-
     protected void toggleExpand(TreeItem item) {
-        
-        if (_parent)
-            _parent.toggleExpand(item);
+        root.toggleExpand(item);
     }
-
     protected void selectItem(TreeItem item) {
-        if (_parent)
-            _parent.selectItem(item);
+        root.selectItem(item);
+    }
+    protected void activateItem(TreeItem item) {
+        root.activateItem(item);
     }
 }
 
@@ -180,10 +182,16 @@ interface OnTreeStateChangeListener {
     void onTreeStateChange(TreeItems source);
 }
 
+interface OnTreeSelectionChangeListener {
+    void onTreeItemSelected(TreeItems source, TreeItem selectedItem, bool activated);
+}
+
 class TreeItems : TreeItem {
     // signal handler OnTreeContentChangeListener
-    Signal!OnTreeContentChangeListener contentListener;
-    Signal!OnTreeStateChangeListener stateListener;
+    Listener!OnTreeContentChangeListener contentListener;
+    Listener!OnTreeStateChangeListener stateListener;
+    Listener!OnTreeSelectionChangeListener selectionListener;
+
     protected TreeItem _selectedItem;
 
     this() {
@@ -211,6 +219,18 @@ class TreeItems : TreeItem {
         _selectedItem = item;
         if (stateListener.assigned)
             stateListener(this);
+        if (selectionListener.assigned)
+            selectionListener(this, _selectedItem, false);
+    }
+
+    override void activateItem(TreeItem item) {
+        if (!(_selectedItem is item)) {
+            _selectedItem = item;
+            if (stateListener.assigned)
+                stateListener(this);
+        }
+        if (selectionListener.assigned)
+            selectionListener(this, _selectedItem, true);
     }
 
     @property override TreeItem selectedItem() {
@@ -219,6 +239,77 @@ class TreeItems : TreeItem {
 
 }
 
+/// grid control action codes
+enum TreeActions : int {
+    /// no action
+	None = 0,
+    /// move selection up
+    Up = 2000,
+    /// move selection down
+    Down,
+    /// move selection left
+    Left,
+    /// move selection right
+    Right,
+
+    /// scroll up, w/o changing selection
+    ScrollUp,
+    /// scroll down, w/o changing selection
+    ScrollDown,
+    /// scroll left, w/o changing selection
+    ScrollLeft,
+    /// scroll right, w/o changing selection
+    ScrollRight,
+
+    /// scroll top w/o changing selection
+    ScrollTop,
+    /// scroll bottom, w/o changing selection
+    ScrollBottom,
+
+    /// scroll up, w/o changing selection
+    ScrollPageUp,
+    /// scroll down, w/o changing selection
+    ScrollPageDown,
+    /// scroll left, w/o changing selection
+    ScrollPageLeft,
+    /// scroll right, w/o changing selection
+    ScrollPageRight,
+
+	/// move cursor one page up
+	PageUp,
+	/// move cursor one page up with selection
+	SelectPageUp,
+	/// move cursor one page down
+	PageDown,
+	/// move cursor one page down with selection
+	SelectPageDown,
+	/// move cursor to the beginning of page
+	PageBegin, 
+	/// move cursor to the beginning of page with selection
+	SelectPageBegin, 
+	/// move cursor to the end of page
+	PageEnd,   
+	/// move cursor to the end of page with selection
+	SelectPageEnd,   
+	/// move cursor to the beginning of line
+	LineBegin,
+	/// move cursor to the beginning of line with selection
+	SelectLineBegin,
+	/// move cursor to the end of line
+	LineEnd,
+	/// move cursor to the end of line with selection
+	SelectLineEnd,
+	/// move cursor to the beginning of document
+	DocumentBegin,
+	/// move cursor to the beginning of document with selection
+	SelectDocumentBegin,
+	/// move cursor to the end of document
+	DocumentEnd,
+	/// move cursor to the end of document with selection
+	SelectDocumentEnd,
+}
+
+
 const int DOUBLE_CLICK_TIME_MS = 250;
 
 class TreeItemWidget : HorizontalLayout {
@@ -238,33 +329,40 @@ class TreeItemWidget : HorizontalLayout {
 
         _item = item;
         _tab = new TextWidget("tab");
-        dchar[] tabText;
-        dchar[] singleTab = [' ', ' ', ' ', ' '];
-        for (int i = 1; i < _item.level; i++)
-            tabText ~= singleTab;
-        _tab.text = cast(dstring)tabText;
+        //dchar[] tabText;
+        //dchar[] singleTab = [' ', ' ', ' ', ' '];
+        //for (int i = 1; i < _item.level; i++)
+        //    tabText ~= singleTab;
+        //_tab.text = cast(dstring)tabText;
+        int w = (_item.level - 1) * style.font.size * 2;
+        _tab.minWidth = w;
+        _tab.maxWidth = w;
         if (_item.hasChildren) {
             _expander = new ImageWidget("expander", _item.hasChildren && _item.expanded ? "arrow_right_down_black" : "arrow_right_hollow");
-            _expander.styleId = "TREE_ITEM_EXPANDER_ICON";
+            _expander.styleId = "TREE_ITEM_EXPAND_ICON";
             _expander.clickable = true;
             _expander.trackHover = true;
-
             //_expander.setState(State.Parent);
-            _expander.onClickListener.connect(delegate(Widget source) {
+
+            _expander.onClickListener = delegate(Widget source) {
                 _item.selectItem(_item);
                 _item.toggleExpand(_item);
                 return true;
-            });
+            };
         }
-        onClickListener.connect(delegate(Widget source) {
+        onClickListener = delegate(Widget source) {
             long ts = currentTimeMillis();
             _item.selectItem(_item);
-            if (_item.hasChildren && ts - lastClickTime < DOUBLE_CLICK_TIME_MS) {
-                _item.toggleExpand(_item);
+            if (ts - lastClickTime < DOUBLE_CLICK_TIME_MS) {
+                if (_item.hasChildren) {
+                    _item.toggleExpand(_item);
+                } else {
+                    _item.activateItem(_item);
+                }
             }
             lastClickTime = ts;
             return true;
-        });
+        };
         if (_item.iconRes.length > 0) {
             _icon = new ImageWidget("icon", _item.iconRes);
             _icon.styleId = "TREE_ITEM_ICON";
@@ -282,6 +380,28 @@ class TreeItemWidget : HorizontalLayout {
         addChild(_label);
     }
 
+    override bool onKeyEvent(KeyEvent event) {
+        if (onKeyListener.assigned && onKeyListener(this, event))
+            return true; // processed by external handler
+        if (!focused || !visible)
+            return false;
+        if (event.action != KeyAction.KeyDown)
+            return false;
+        int action = 0;
+        switch (event.keyCode) {
+            case KeyCode.SPACE:
+            case KeyCode.RETURN:
+                if (_item.hasChildren)
+                    _item.toggleExpand(_item);
+                else
+                    _item.activateItem(_item);
+                return true;
+            default:
+                break;
+        }
+        return false;
+    }
+
     void updateWidget() {
         if (_expander) {
             _expander.drawable = _item.expanded ? "arrow_right_down_black" : "arrow_right_hollow";
@@ -297,12 +417,14 @@ class TreeItemWidget : HorizontalLayout {
     }
 }
 
-class TreeWidgetBase :  ScrollWidget, OnTreeContentChangeListener, OnTreeStateChangeListener {
+class TreeWidgetBase :  ScrollWidget, OnTreeContentChangeListener, OnTreeStateChangeListener, OnTreeSelectionChangeListener, OnKeyHandler {
 
     protected TreeItems _tree;
 
     @property ref TreeItems items() { return _tree; }
 
+    Signal!OnTreeSelectionChangeListener selectionListener;
+
     protected bool _needUpdateWidgets;
     protected bool _needUpdateWidgetStates;
 
@@ -310,10 +432,30 @@ class TreeWidgetBase :  ScrollWidget, OnTreeContentChangeListener, OnTreeStateCh
 		super(ID, hscrollbarMode, vscrollbarMode);
         contentWidget = new VerticalLayout("TREE_CONTENT");
         _tree = new TreeItems();
-        _tree.contentListener.connect(this);
-        _tree.stateListener.connect(this);
+        _tree.contentListener = this;
+        _tree.stateListener = this;
         _needUpdateWidgets = true;
         _needUpdateWidgetStates = true;
+		acceleratorMap.add( [
+			new Action(TreeActions.Up, KeyCode.UP, 0),
+			new Action(TreeActions.Down, KeyCode.DOWN, 0),
+			new Action(TreeActions.Left, KeyCode.LEFT, 0),
+			new Action(TreeActions.Right, KeyCode.RIGHT, 0),
+			new Action(TreeActions.LineBegin, KeyCode.HOME, 0),
+			new Action(TreeActions.LineEnd, KeyCode.END, 0),
+			new Action(TreeActions.PageUp, KeyCode.PAGEUP, 0),
+			new Action(TreeActions.PageDown, KeyCode.PAGEDOWN, 0),
+			new Action(TreeActions.PageBegin, KeyCode.PAGEUP, KeyFlag.Control),
+			new Action(TreeActions.PageEnd, KeyCode.PAGEDOWN, KeyFlag.Control),
+			new Action(TreeActions.ScrollTop, KeyCode.HOME, KeyFlag.Control),
+			new Action(TreeActions.ScrollBottom, KeyCode.END, KeyFlag.Control),
+			new Action(TreeActions.ScrollPageUp, KeyCode.PAGEUP, KeyFlag.Control),
+			new Action(TreeActions.ScrollPageDown, KeyCode.PAGEDOWN, KeyFlag.Control),
+			new Action(TreeActions.ScrollUp, KeyCode.UP, KeyFlag.Control),
+			new Action(TreeActions.ScrollDown, KeyCode.DOWN, KeyFlag.Control),
+			new Action(TreeActions.ScrollLeft, KeyCode.LEFT, KeyFlag.Control),
+			new Action(TreeActions.ScrollRight, KeyCode.RIGHT, KeyFlag.Control),
+		]);
     }
 
     ~this() {
@@ -325,7 +467,19 @@ class TreeWidgetBase :  ScrollWidget, OnTreeContentChangeListener, OnTreeStateCh
 
     /** Override to use custom tree item widgets. */
     protected Widget createItemWidget(TreeItem item) {
-        return new TreeItemWidget(item);
+        Widget res = new TreeItemWidget(item);
+        res.onKeyListener = this;
+        return res;
+    }
+
+    override bool onKey(Widget source, KeyEvent event) {
+		if (event.action == KeyAction.KeyDown) {
+			Action action = findKeyAction(event.keyCode, event.flags & (KeyFlag.Shift | KeyFlag.Alt | KeyFlag.Control));
+			if (action !is null) {
+				return handleAction(action);
+			}
+		}
+        return false;
     }
 
     protected void addWidgets(TreeItem item) {
@@ -385,6 +539,43 @@ class TreeWidgetBase :  ScrollWidget, OnTreeContentChangeListener, OnTreeStateCh
         requestLayout();
     }
 
+    override void onTreeItemSelected(TreeItems source, TreeItem selectedItem, bool activated) {
+        if (selectionListener.assigned)
+            selectionListener(source, selectedItem, activated);
+    }
+
+	override protected bool handleAction(const Action a) {
+        Log.d("tree.handleAction ", a.id);
+        switch (a.id) {
+            case TreeActions.ScrollLeft:
+                if (_hscrollbar)
+                    _hscrollbar.sendScrollEvent(ScrollAction.LineUp);
+                break;
+            case TreeActions.ScrollRight:
+                if (_hscrollbar)
+                    _hscrollbar.sendScrollEvent(ScrollAction.LineDown);
+                break;
+            case TreeActions.ScrollUp:
+                if (_vscrollbar)
+                    _vscrollbar.sendScrollEvent(ScrollAction.LineUp);
+                break;
+            case TreeActions.ScrollPageUp:
+                if (_vscrollbar)
+                    _vscrollbar.sendScrollEvent(ScrollAction.PageUp);
+                break;
+            case TreeActions.ScrollDown:
+                if (_vscrollbar)
+                    _vscrollbar.sendScrollEvent(ScrollAction.LineDown);
+                break;
+            case TreeActions.ScrollPageDown:
+                if (_vscrollbar)
+                    _vscrollbar.sendScrollEvent(ScrollAction.PageDown);
+                break;
+            default:
+                return super.handleAction(a);
+        }
+        return true;
+    }
 }
 
 class TreeWidget :  TreeWidgetBase {
diff --git a/src/dlangui/widgets/widget.d b/src/dlangui/widgets/widget.d
index 7094db0f..f70de433 100644
--- a/src/dlangui/widgets/widget.d
+++ b/src/dlangui/widgets/widget.d
@@ -774,8 +774,6 @@ class Widget {
     bool handleMoveFocusUsingKeys(KeyEvent event) {
         if (!focused || !visible)
             return false;
-        if (!visible)
-            return false;
         if (event.action != KeyAction.KeyDown)
             return false;
         FocusMovement direction = FocusMovement.None;