1167 lines
38 KiB
C#
1167 lines
38 KiB
C#
/// Project : Kopano OL Extension
|
|
///
|
|
/// Copyright 2016 Kopano b.v.
|
|
///
|
|
/// This program is free software: you can redistribute it and/or modify
|
|
/// it under the terms of the GNU Affero General Public License, version 3,
|
|
/// as published by the Free Software Foundation.
|
|
///
|
|
/// This program is distributed in the hope that it will be useful,
|
|
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
|
|
/// GNU Affero General Public License for more details.
|
|
///
|
|
/// You should have received a copy of the GNU Affero General Public License
|
|
/// along with this program.If not, see<http://www.gnu.org/licenses/>.
|
|
///
|
|
/// Consult LICENSE file for details
|
|
|
|
using Acacia.Native;
|
|
using Acacia.Utils;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
|
|
namespace Acacia.Controls
|
|
{
|
|
//[Designer(typeof(KopanoTreeViewDesigner))]
|
|
public class KTree : UserControl
|
|
{
|
|
#region Checkboxes
|
|
|
|
public class CheckStateChangedEventArgs : EventArgs
|
|
{
|
|
public readonly KTreeNode Node;
|
|
|
|
public CheckStateChangedEventArgs(KTreeNode node)
|
|
{
|
|
this.Node = node;
|
|
}
|
|
}
|
|
|
|
public delegate void CheckStateChangedHandler(object sender, CheckStateChangedEventArgs e);
|
|
public event CheckStateChangedHandler CheckStateChanged;
|
|
|
|
internal void OnCheckStateChanged(KTreeNode node)
|
|
{
|
|
if (CheckStateChanged != null)
|
|
{
|
|
CheckStateChanged(this, new CheckStateChangedEventArgs(node));
|
|
}
|
|
}
|
|
|
|
private KCheckManager _checkManager;
|
|
[Browsable(false)]
|
|
public KCheckManager CheckManager
|
|
{
|
|
get { return _checkManager; }
|
|
set { _checkManager = value; Rerender(); }
|
|
}
|
|
|
|
public KCheckStyle CheckStyle
|
|
{
|
|
get
|
|
{
|
|
return _checkManager == null ? KCheckStyle.None : _checkManager.CheckStyle;
|
|
}
|
|
|
|
set
|
|
{
|
|
switch(value)
|
|
{
|
|
case KCheckStyle.TwoState:
|
|
_checkManager = new KCheckManager.TwoState();
|
|
break;
|
|
case KCheckStyle.ThreeState:
|
|
_checkManager = new KCheckManager.ThreeState();
|
|
break;
|
|
case KCheckStyle.Recursive:
|
|
_checkManager = new KCheckManager.Recursive();
|
|
break;
|
|
case KCheckStyle.RecursiveThreeState:
|
|
_checkManager = new KCheckManager.RecursiveThreeState();
|
|
break;
|
|
default:
|
|
_checkManager = null;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ToggleCheck(KTreeNode node)
|
|
{
|
|
if (_checkManager == null || node == null)
|
|
return;
|
|
|
|
if (!SelectedNodes.Contains(node) || SelectedNodes.Count == 1)
|
|
{
|
|
// Update the single node if it's not part of the selection, or it's the only selection
|
|
_checkManager.ToggleCheck(node);
|
|
}
|
|
else
|
|
{
|
|
// Update all selected nodes
|
|
BeginUpdate();
|
|
try
|
|
{
|
|
_checkManager.ToggleCheck(SelectedNodes);
|
|
}
|
|
finally
|
|
{
|
|
EndUpdate();
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
private Padding _nodePadding = new Padding(2, 4, 2, 4);
|
|
public Padding NodePadding
|
|
{
|
|
get { return _nodePadding; }
|
|
set { _nodePadding = value; Rerender(); }
|
|
}
|
|
|
|
private int _nodeIdent = 8;
|
|
public int NodeIndent
|
|
{
|
|
get { return _nodeIdent; }
|
|
set { _nodeIdent = value; Rerender(); }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Images
|
|
|
|
private ImageList _images;
|
|
|
|
public ImageList Images
|
|
{
|
|
get { return _images; }
|
|
set { _images = value; Rerender(); }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Nodes
|
|
|
|
private readonly KTreeNodes _rootNodes;
|
|
|
|
[Browsable(false)]
|
|
public KTreeNodes RootNodes
|
|
{
|
|
get { return _rootNodes; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Creation
|
|
|
|
public KTree()
|
|
{
|
|
SetStyle(ControlStyles.ResizeRedraw, true);
|
|
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
|
|
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
|
|
SetStyle(ControlStyles.Selectable, true);
|
|
BackColor = SystemColors.Window;
|
|
|
|
_rootNodes = new KTreeNodes(this);
|
|
SetupRenderer();
|
|
InitScrollBars();
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region Selection
|
|
|
|
private bool _fullRowSelect = true;
|
|
public bool FullRowSelect
|
|
{
|
|
get { return _fullRowSelect; }
|
|
set { _fullRowSelect = value; Rerender(); }
|
|
}
|
|
|
|
internal KTreeNode ActiveNode { get; private set; }
|
|
|
|
private int ActiveNodeIndex
|
|
{
|
|
get { return ActiveNode == null ? -1 : _presentNodes.IndexOf(ActiveNode); }
|
|
}
|
|
|
|
private KSelectionManager _selectionManager = new KSelectionManager.Multiple();
|
|
public bool MultipleSelection
|
|
{
|
|
get { return _selectionManager is KSelectionManager.Multiple; }
|
|
set
|
|
{
|
|
_selectionManager = value ? new KSelectionManager.Multiple() : (KSelectionManager)new KSelectionManager.Single();
|
|
}
|
|
}
|
|
|
|
[Browsable(false)]
|
|
public IReadOnlyCollection<KTreeNode> SelectedNodes
|
|
{
|
|
get { return _selectionManager.CurrentSelection; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selects a single node and makes it the active node.
|
|
/// </summary>
|
|
/// <param name="node">The node. Pass null to deselect any nodes</param>
|
|
/// <param name="scroll">Set to a specific mode to scroll the node into view</param>
|
|
public void SelectNode(KTreeNode node, ScrollMode scroll = ScrollMode.None)
|
|
{
|
|
DoSelectNode(node, SelectAction.Set, scroll);
|
|
}
|
|
|
|
private enum SelectAction
|
|
{
|
|
Set,
|
|
Toggle,
|
|
Range,
|
|
AddRange,
|
|
Activate
|
|
}
|
|
|
|
private KTreeNode _selectRangeAnchor;
|
|
|
|
private void DoSelectNode(KTreeNode node, SelectAction action, ScrollMode scroll)
|
|
{
|
|
if (action == SelectAction.Set || action == SelectAction.Range)
|
|
_selectionManager.Clear();
|
|
|
|
if (node != null)
|
|
{
|
|
switch(action)
|
|
{
|
|
|
|
case SelectAction.Range:
|
|
case SelectAction.AddRange:
|
|
if (_selectRangeAnchor == null)
|
|
{
|
|
_selectRangeAnchor = node;
|
|
_selectionManager.Add(node);
|
|
break;
|
|
}
|
|
|
|
// Select any nodes from the anchor to the current node
|
|
int activeIndex = Math.Max(0, _presentNodes.IndexOf(_selectRangeAnchor));
|
|
int nodeIndex = _presentNodes.IndexOf(node);
|
|
// Keep to order just in case the selection manager wants it
|
|
if (activeIndex > nodeIndex)
|
|
{
|
|
for (int i = nodeIndex; i <= activeIndex; ++i)
|
|
if (_presentNodes[i].IsSelectable)
|
|
_selectionManager.Add(_presentNodes[i]);
|
|
}
|
|
else
|
|
{
|
|
for (int i = activeIndex; i <= nodeIndex; ++i)
|
|
if (_presentNodes[i].IsSelectable)
|
|
_selectionManager.Add(_presentNodes[i]);
|
|
}
|
|
break;
|
|
case SelectAction.Set:
|
|
_selectRangeAnchor = node;
|
|
_selectionManager.Add(node);
|
|
break;
|
|
case SelectAction.Toggle:
|
|
_selectRangeAnchor = node;
|
|
_selectionManager.Toggle(node);
|
|
break;
|
|
}
|
|
|
|
if (scroll != ScrollMode.None)
|
|
ScrollIntoView(node, scroll);
|
|
}
|
|
|
|
ActiveNode = node;
|
|
|
|
// Must rerender
|
|
// TODO: affected nodes only
|
|
Rerender();
|
|
|
|
// Raise event if needed
|
|
CheckSelectionChanged();
|
|
}
|
|
|
|
public class SelectionChangedEventArgs : EventArgs
|
|
{
|
|
public readonly KTreeNode[] SelectedNodes;
|
|
|
|
public SelectionChangedEventArgs(KTreeNode[] selectedNodes)
|
|
{
|
|
this.SelectedNodes = selectedNodes;
|
|
}
|
|
}
|
|
|
|
public delegate void SelectionChangedDelegate(object sender, SelectionChangedEventArgs e);
|
|
|
|
public event SelectionChangedDelegate SelectionChanged;
|
|
|
|
private readonly List<KTreeNode> _previousSelection = new List<KTreeNode>();
|
|
|
|
private void CheckSelectionChanged()
|
|
{
|
|
if (_updateCount != 0)
|
|
return;
|
|
|
|
IReadOnlyCollection<KTreeNode> selection = _selectionManager.CurrentSelection;
|
|
if (!selection.SameElements(_previousSelection))
|
|
{
|
|
_previousSelection.Clear();
|
|
_previousSelection.AddRange(selection);
|
|
|
|
OnSelectionChanged(new SelectionChangedEventArgs(selection.ToArray()));
|
|
}
|
|
}
|
|
|
|
virtual protected void OnSelectionChanged(SelectionChangedEventArgs e)
|
|
{
|
|
if (SelectionChanged != null)
|
|
SelectionChanged(this, e);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Mouse handling
|
|
|
|
protected override void OnMouseDown(MouseEventArgs e)
|
|
{
|
|
base.OnMouseDown(e);
|
|
|
|
if ((e.Button & MouseButtons.Left) != 0)
|
|
{
|
|
HitTestResult? hit = HitTest(e.Location);
|
|
if (hit != null && hit.Value.Node.IsSelectable)
|
|
{
|
|
switch(hit.Value.Part)
|
|
{
|
|
case KTreeNodeMeasurements.Part.Expander:
|
|
hit.Value.Node.ToggleExpanded();
|
|
break;
|
|
case KTreeNodeMeasurements.Part.CheckBox:
|
|
ToggleCheck(hit.Value.Node);
|
|
break;
|
|
case KTreeNodeMeasurements.Part.Text:
|
|
case KTreeNodeMeasurements.Part.None:
|
|
case KTreeNodeMeasurements.Part.Image:
|
|
DoSelectNode(hit.Value.Node, ActionFromModifiers(false), ScrollMode.Auto);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private SelectAction ActionFromModifiers(bool isKeyboard)
|
|
{
|
|
if (ModifierKeys == (Keys.Shift | Keys.Control))
|
|
return SelectAction.AddRange;
|
|
else if (ModifierKeys == Keys.Shift)
|
|
return SelectAction.Range;
|
|
else if (ModifierKeys == Keys.Control)
|
|
{
|
|
if (isKeyboard)
|
|
return SelectAction.Activate;
|
|
else
|
|
return SelectAction.Toggle;
|
|
}
|
|
else
|
|
return SelectAction.Set;
|
|
}
|
|
|
|
protected override void OnMouseDoubleClick(MouseEventArgs e)
|
|
{
|
|
if ((e.Button & MouseButtons.Left) != 0)
|
|
{
|
|
HitTestResult? hit = HitTest(e.Location, KTreeNodeMeasurements.Part.Text, KTreeNodeMeasurements.Part.Image);
|
|
if (hit != null && hit.Value.Node.IsSelectable)
|
|
{
|
|
hit.Value.Node.ToggleExpanded();
|
|
}
|
|
}
|
|
}
|
|
|
|
private KTreeNode _highlightNode;
|
|
private KTreeNodeMeasurements.Part? _highlightPart;
|
|
|
|
protected override void OnMouseMove(MouseEventArgs e)
|
|
{
|
|
CheckMouseHighlight();
|
|
}
|
|
|
|
protected override void OnMouseLeave(EventArgs e)
|
|
{
|
|
// The mouse might be over a control hosted in a node, so check highlighting again
|
|
CheckMouseHighlight();
|
|
}
|
|
|
|
private void CheckMouseHighlight()
|
|
{
|
|
HitTestResult? hit = HitTest(PointToClient(MousePosition));
|
|
HighlightNode(hit?.Node, hit?.Part);
|
|
}
|
|
|
|
private void HighlightNode(KTreeNode newHighlight, KTreeNodeMeasurements.Part? newPart)
|
|
{
|
|
if (newHighlight != _highlightNode || _highlightPart != newPart)
|
|
{
|
|
KTreeNode old = _highlightNode;
|
|
|
|
if (newHighlight != null && !newHighlight.IsSelectable)
|
|
{
|
|
_highlightNode = null;
|
|
_highlightPart = null;
|
|
}
|
|
else
|
|
{
|
|
_highlightNode = newHighlight;
|
|
_highlightPart = newPart;
|
|
}
|
|
|
|
// Render old node without highlight
|
|
if (old != null)
|
|
Rerender(old);
|
|
|
|
// Render new node
|
|
if (_highlightNode != null)
|
|
Rerender(_highlightNode);
|
|
}
|
|
}
|
|
|
|
protected override void OnMouseWheel(MouseEventArgs e)
|
|
{
|
|
// Let the scrollbar handle the scrolling
|
|
if (HaveVerticalScrollBar)
|
|
_verticalScrollBar.ForwardMouseWheel(e);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Hit testing
|
|
|
|
private struct HitTestResult
|
|
{
|
|
public KTreeNode Node;
|
|
public KTreeNodeMeasurements.Part Part;
|
|
}
|
|
|
|
private HitTestResult? HitTest(Point location, params KTreeNodeMeasurements.Part[] wanted)
|
|
{
|
|
if (location.X < 0 || location.X >= ViewRectangle.Width)
|
|
return null;
|
|
|
|
KTreeNode node = NodeAtY(location.Y);
|
|
if (node == null)
|
|
return null;
|
|
|
|
KTreeNodeMeasurements.Part? part = node.EffectiveDimension.HitTest(location.X + _horizontalScrollBar.Value);
|
|
if (part != null)
|
|
{
|
|
// Check if it's the part we're interested in
|
|
if (wanted.Length > 0 && !wanted.Contains(part.Value))
|
|
return null;
|
|
|
|
// Part.None is valid only if full row selection is enabled
|
|
if (!FullRowSelect && part.Value == KTreeNodeMeasurements.Part.None)
|
|
return null;
|
|
|
|
// Success
|
|
return new HitTestResult() { Node = node, Part = part.Value };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private KTreeNode NodeAtY(int y)
|
|
{
|
|
int index = NodeIndexAtY(y);
|
|
if (index < 0 || index >= _presentNodes.Count)
|
|
return null;
|
|
return _presentNodes[index];
|
|
}
|
|
|
|
private int NodeIndexAtY(int y)
|
|
{
|
|
// TODO: use a secondary index, or assume all rows are the same height?
|
|
y += _verticalScrollBar.Value;
|
|
if (y < 0)
|
|
return -1;
|
|
for(int i = 0; i < _presentNodes.Count; ++i)
|
|
{
|
|
if (_presentNodes[i].EffectiveDimension.NodeRect.ContainsY(y))
|
|
return i;
|
|
}
|
|
return _presentNodes.Count;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Keyboard handling
|
|
|
|
protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e)
|
|
{
|
|
switch (e.KeyCode)
|
|
{
|
|
case Keys.Up:
|
|
KeySelect(-1);
|
|
break;
|
|
case Keys.Down:
|
|
KeySelect(1);
|
|
break;
|
|
case Keys.PageUp:
|
|
KeySelect(-PageSize);
|
|
break;
|
|
case Keys.PageDown:
|
|
KeySelect(PageSize);
|
|
break;
|
|
case Keys.Home:
|
|
KeySelect(-_presentNodes.Count);
|
|
break;
|
|
case Keys.End:
|
|
KeySelect(_presentNodes.Count);
|
|
break;
|
|
case Keys.Left:
|
|
KeyExpand(false);
|
|
break;
|
|
case Keys.Right:
|
|
KeyExpand(true);
|
|
break;
|
|
case Keys.Space:
|
|
if (ModifierKeys == Keys.Control || _checkManager == null)
|
|
{
|
|
DoSelectNode(ActiveNode, SelectAction.Toggle, ScrollMode.Auto);
|
|
}
|
|
else if (_checkManager != null)
|
|
{
|
|
ToggleCheck(ActiveNode);
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
e.IsInputKey = true;
|
|
}
|
|
|
|
private int PageSize
|
|
{
|
|
get
|
|
{
|
|
int firstVisible = NodeIndexAtY(0);
|
|
int count = 0;
|
|
for (int i = 0; i < _presentNodes.Count; ++i)
|
|
{
|
|
Rectangle nodeRect = _presentNodes[i].EffectiveDimension.NodeRect;
|
|
if (nodeRect.Bottom > ViewRectangle.Bottom)
|
|
break;
|
|
else if (nodeRect.Top >= ViewRectangle.Top)
|
|
++count;
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
|
|
private void KeyExpand(bool expand)
|
|
{
|
|
if (ActiveNode == null)
|
|
return;
|
|
|
|
if (expand)
|
|
{
|
|
if (ActiveNode.ChildLoader.NeedsExpander)
|
|
{
|
|
if (!ActiveNode.IsExpanded)
|
|
ActiveNode.IsExpanded = true;
|
|
else
|
|
DoSelectNode(ActiveNode.Children.First(), ActionFromModifiers(true), ScrollMode.Auto);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (ActiveNode.IsExpanded)
|
|
ActiveNode.IsExpanded = false;
|
|
else if (ActiveNode.Parent != null)
|
|
DoSelectNode(ActiveNode.Parent, ActionFromModifiers(true), ScrollMode.Auto);
|
|
}
|
|
}
|
|
|
|
private void KeySelect(int dir)
|
|
{
|
|
int currentIndex = ActiveNodeIndex;
|
|
for (;;)
|
|
{
|
|
currentIndex = currentIndex + dir;
|
|
currentIndex = Math.Max(Math.Min(currentIndex, _presentNodes.Count - 1), 0);
|
|
KTreeNode node = currentIndex < _presentNodes.Count ? _presentNodes[currentIndex] : null;
|
|
if (node != null && !node.IsSelectable)
|
|
continue;
|
|
DoSelectNode(node, ActionFromModifiers(true), dir > 0 ? ScrollMode.Bottom : ScrollMode.Top);
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Columns
|
|
// TODO
|
|
/*
|
|
private readonly TreeViewColumnCollection _columns;
|
|
|
|
[Category("Columns")]
|
|
[Browsable(true)]
|
|
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
|
|
public TreeViewColumnCollection Columns
|
|
{
|
|
get { return _columns; }
|
|
}*/
|
|
|
|
#endregion
|
|
|
|
#region Rendering
|
|
|
|
private KTreeRenderer _renderer;
|
|
|
|
public void SetupRenderer(bool enableVisualStyles = true)
|
|
{
|
|
if (enableVisualStyles && Application.RenderWithVisualStyles)
|
|
_renderer = new KTreeRendererVisualStyles();
|
|
else
|
|
_renderer = new KTreeRendererDefault();
|
|
Rerender();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The nodes that are currently present, i.e. their parents are expanded.
|
|
/// </summary>
|
|
private readonly List<KTreeNode> _presentNodes = new List<KTreeNode>();
|
|
|
|
internal void OnNodeExpandedChanged(KTreeNode node)
|
|
{
|
|
BeginUpdate();
|
|
try
|
|
{
|
|
if (node.IsExpanded)
|
|
{
|
|
int index = _presentNodes.IndexOf(node);
|
|
if (index < 0)
|
|
return;
|
|
|
|
// Add the child nodes
|
|
InsertChildNodes(node, index + 1);
|
|
}
|
|
else
|
|
{
|
|
RemoveChildNodes(node);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
EndUpdate();
|
|
}
|
|
}
|
|
|
|
private void RemoveChildNodes(KTreeNode parent)
|
|
{
|
|
int index = _presentNodes.IndexOf(parent);
|
|
if (index < 0)
|
|
return;
|
|
|
|
// Remove any node that's deeper than the current node
|
|
int depth = parent.Depth;
|
|
int first = index + 1;
|
|
int past = first;
|
|
while (past < _presentNodes.Count)
|
|
{
|
|
if (_presentNodes[past].Depth > depth)
|
|
++past;
|
|
else break;
|
|
}
|
|
if (past > first)
|
|
_presentNodes.RemoveRange(first, past - first);
|
|
}
|
|
|
|
internal void OnNodeChildrenChanged(KTreeNode node)
|
|
{
|
|
Rerender(node);
|
|
}
|
|
|
|
internal void OnNodeAdded(KTreeNode parent, KTreeNode node)
|
|
{
|
|
BeginUpdate();
|
|
try
|
|
{
|
|
if (parent == null)
|
|
{
|
|
// TODO: this probably leads to wrong order
|
|
_presentNodes.Add(node);
|
|
}
|
|
else
|
|
{
|
|
if (!parent.IsExpanded)
|
|
return;
|
|
|
|
int index = _presentNodes.IndexOf(parent);
|
|
if (index < 0)
|
|
return;
|
|
++index;
|
|
int depth = parent.Depth;
|
|
while (index < _presentNodes.Count)
|
|
{
|
|
if (_presentNodes[index].Depth <= depth)
|
|
break;
|
|
++index;
|
|
}
|
|
_presentNodes.Insert(index, node);
|
|
}
|
|
|
|
if (node.IsExpanded)
|
|
{
|
|
OnNodeExpandedChanged(node);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
EndUpdate();
|
|
}
|
|
}
|
|
|
|
internal void OnNodeRemoved(KTreeNode parent, KTreeNode node)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
internal void OnNodeCleared(KTreeNode parent)
|
|
{
|
|
BeginUpdate();
|
|
try
|
|
{
|
|
if (parent == null)
|
|
{
|
|
// Root node cleared, means no more nodes
|
|
_presentNodes.Clear();
|
|
}
|
|
else
|
|
{
|
|
RemoveChildNodes(parent);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
EndUpdate();
|
|
}
|
|
}
|
|
|
|
private int InsertChildNodes(KTreeNode parent, int index)
|
|
{
|
|
if (parent.IsExpanded)
|
|
{
|
|
foreach (KTreeNode node in parent.Children)
|
|
{
|
|
_presentNodes.Insert(index, node);
|
|
index = InsertChildNodes(node, index + 1);
|
|
}
|
|
}
|
|
return index;
|
|
}
|
|
|
|
private int _updateCount;
|
|
|
|
public void BeginUpdate()
|
|
{
|
|
++_updateCount;
|
|
}
|
|
|
|
public void EndUpdate()
|
|
{
|
|
--_updateCount;
|
|
if (_updateCount == 0)
|
|
{
|
|
Rerender();
|
|
CheckSelectionChanged();
|
|
}
|
|
}
|
|
|
|
internal void Rerender(KTreeNode node = null)
|
|
{
|
|
if (_updateCount != 0)
|
|
return;
|
|
|
|
// TODO: use node
|
|
MeasureNodes();
|
|
UpdateScrollBars();
|
|
CheckMouseHighlight();
|
|
Invalidate();
|
|
}
|
|
|
|
private void MeasureNodes()
|
|
{
|
|
_renderer.Init(ViewRectangle, this);
|
|
using (Graphics graphics = CreateGraphics())
|
|
{
|
|
foreach (KTreeNode node in _presentNodes)
|
|
{
|
|
_renderer.MeasureNode(graphics, node);
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly List<Control> _nodeControls = new List<Control>();
|
|
|
|
protected override void OnPaint(PaintEventArgs e)
|
|
{
|
|
int firstVisibleNode = NodeIndexAtY(0);
|
|
List<Control> visibleControls = new List<Control>();
|
|
|
|
for (int i = firstVisibleNode; i < _presentNodes.Count; ++i)
|
|
{
|
|
KTreeNode node = _presentNodes[i];
|
|
// Stop rendering when we're out of view
|
|
if (node.EffectiveDimension.NodeRect.Y - _verticalScrollBar.Value >= ViewRectangle.Bottom)
|
|
break;
|
|
|
|
// Render the node
|
|
_renderer.RenderNode(e.Graphics, node, new Point(_horizontalScrollBar.Value, _verticalScrollBar.Value),
|
|
node == _highlightNode ? _highlightPart : null);
|
|
|
|
// May have to add the control
|
|
if (node.Control != null)
|
|
{
|
|
if (node.Control.Parent == null)
|
|
{
|
|
_nodeControls.Add(node.Control);
|
|
node.Control.Parent = this;
|
|
}
|
|
visibleControls.Add(node.Control);
|
|
}
|
|
}
|
|
|
|
// Check if any controls became invisible
|
|
for(int i = 0; i < _nodeControls.Count;)
|
|
{
|
|
if (!visibleControls.Contains(_nodeControls[i]))
|
|
{
|
|
_nodeControls[i].Parent = null;
|
|
_nodeControls.RemoveAt(i);
|
|
}
|
|
else
|
|
{
|
|
++i;
|
|
}
|
|
}
|
|
|
|
// Fill in a rectangle below the scrollbars, as that may be rendered. If they are not visible, the width or height
|
|
// automatically becomes 0
|
|
e.Graphics.FillRectangle(SystemBrushes.Control,
|
|
ClientSize.Width - VerticalScrollBarWidth, ClientSize.Height - HorizontalScrollBarHeight,
|
|
VerticalScrollBarWidth, HorizontalScrollBarHeight);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Scrollbars
|
|
|
|
private class VScrollBar2 : VScrollBar
|
|
{
|
|
public VScrollBar2()
|
|
{
|
|
}
|
|
|
|
internal void ForwardMouseWheel(MouseEventArgs e)
|
|
{
|
|
OnMouseWheel(e);
|
|
}
|
|
}
|
|
|
|
private class HScrollBar2 : HScrollBar
|
|
{
|
|
internal void ForwardMouseWheel(MouseEventArgs e)
|
|
{
|
|
OnMouseWheel(e);
|
|
}
|
|
}
|
|
|
|
private readonly VScrollBar2 _verticalScrollBar = new VScrollBar2();
|
|
private readonly HScrollBar2 _horizontalScrollBar = new HScrollBar2();
|
|
|
|
private void InitScrollBars()
|
|
{
|
|
_verticalScrollBar.Scroll += _scrollBar_Scroll;
|
|
_verticalScrollBar.PreviewKeyDown += _verticalScrollBar_PreviewKeyDown;
|
|
Controls.Add(_verticalScrollBar);
|
|
_horizontalScrollBar.Scroll += _scrollBar_Scroll;
|
|
_horizontalScrollBar.PreviewKeyDown += _verticalScrollBar_PreviewKeyDown;
|
|
Controls.Add(_horizontalScrollBar);
|
|
}
|
|
|
|
private void _verticalScrollBar_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
|
|
{
|
|
|
|
}
|
|
|
|
private void _scrollBar_Scroll(object sender, ScrollEventArgs e)
|
|
{
|
|
// Mouse might be over different node now
|
|
CheckMouseHighlight();
|
|
|
|
// Repaint
|
|
Invalidate();
|
|
}
|
|
|
|
protected override void OnSizeChanged(EventArgs e)
|
|
{
|
|
base.OnSizeChanged(e);
|
|
MeasureNodes();
|
|
UpdateScrollBars();
|
|
}
|
|
|
|
// TODO: this goes wrong if resizing makes a scrollbar appear/disappear
|
|
private void UpdateScrollBars()
|
|
{
|
|
// May happen during init
|
|
if (ViewRectangle.Height < 0)
|
|
return;
|
|
|
|
// Update scrollbar ranges
|
|
_verticalScrollBar.Minimum = 0;
|
|
_verticalScrollBar.Maximum = _renderer.TotalRect.Height;
|
|
_horizontalScrollBar.Minimum = 0;
|
|
_horizontalScrollBar.Maximum = _renderer.TotalRect.Width;
|
|
|
|
// Set change sizes
|
|
if (_presentNodes.Count > 0)
|
|
_verticalScrollBar.SmallChange = _presentNodes.First().EffectiveDimension.NodeRect.Height;
|
|
_verticalScrollBar.LargeChange = ViewRectangle.Height;
|
|
_horizontalScrollBar.SmallChange = NodeIndent;
|
|
_horizontalScrollBar.LargeChange = Math.Max(0, ViewRectangle.Width); // Negative on miminize
|
|
if (_verticalScrollBar.LargeChange >= _verticalScrollBar.Maximum)
|
|
_verticalScrollBar.Value = 0;
|
|
|
|
// Set the positions, make them size 0 if not required
|
|
_verticalScrollBar.SetBounds(ClientSize.Width - VerticalScrollBarWidth, 0, VerticalScrollBarWidth, ClientSize.Height - HorizontalScrollBarHeight);
|
|
_horizontalScrollBar.SetBounds(0, ClientSize.Height - HorizontalScrollBarHeight, ClientSize.Width - VerticalScrollBarWidth, HorizontalScrollBarHeight);
|
|
}
|
|
|
|
private bool HaveVerticalScrollBar
|
|
{
|
|
get { return _verticalScrollBar.LargeChange < _verticalScrollBar.Maximum; }
|
|
}
|
|
private int VerticalScrollBarWidth
|
|
{
|
|
get { return HaveVerticalScrollBar ? SystemInformation.VerticalScrollBarWidth : 0; }
|
|
}
|
|
private bool HaveHorizontalScrollBar
|
|
{
|
|
get { return _horizontalScrollBar.LargeChange < _horizontalScrollBar.Maximum; }
|
|
}
|
|
private int HorizontalScrollBarHeight
|
|
{
|
|
get { return HaveHorizontalScrollBar ? SystemInformation.HorizontalScrollBarHeight : 0; }
|
|
}
|
|
|
|
private Rectangle ViewRectangle
|
|
{
|
|
get
|
|
{
|
|
Rectangle r = ClientRectangle;
|
|
r.Width -= VerticalScrollBarWidth;
|
|
r.Height -= HorizontalScrollBarHeight;
|
|
return r;
|
|
}
|
|
}
|
|
|
|
private Rectangle ScrolledRectangle
|
|
{
|
|
get
|
|
{
|
|
Rectangle r = ClientRectangle;
|
|
r.Width -= VerticalScrollBarWidth;
|
|
r.Height -= HorizontalScrollBarHeight;
|
|
r.X += _horizontalScrollBar.Value;
|
|
r.Y += _verticalScrollBar.Value;
|
|
return r;
|
|
}
|
|
}
|
|
|
|
public enum ScrollMode
|
|
{
|
|
None,
|
|
Auto,
|
|
Top,
|
|
Middle,
|
|
Bottom
|
|
}
|
|
|
|
public void ScrollIntoView(KTreeNode node, ScrollMode mode)
|
|
{
|
|
if (mode == ScrollMode.None)
|
|
return;
|
|
|
|
if (!node.IsVisible)
|
|
{
|
|
//return;
|
|
foreach (KTreeNode parent in node.Ancestors)
|
|
{
|
|
if (!parent.IsExpanded)
|
|
{
|
|
parent.IsExpanded = true;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Number of pixels from edge to keep node in Y direction.
|
|
// TODO: this assumes all nodes are the same height
|
|
int scrollBorderY = node.EffectiveDimension.NodeRect.Height;
|
|
|
|
// Vertical
|
|
// Do nothing if the node is already fully visible
|
|
if (!ScrolledRectangle.ContainsY(node.EffectiveDimension.NodeRect.Top - scrollBorderY) ||
|
|
!ScrolledRectangle.ContainsY(node.EffectiveDimension.NodeRect.Bottom + scrollBorderY))
|
|
{
|
|
if (mode == ScrollMode.Auto)
|
|
{
|
|
if (node.EffectiveDimension.NodeRect.Top + scrollBorderY < ScrolledRectangle.Y)
|
|
mode = ScrollMode.Top;
|
|
else
|
|
mode = ScrollMode.Bottom;
|
|
}
|
|
|
|
switch (mode)
|
|
{
|
|
case ScrollMode.Top:
|
|
SetVScroll(node.EffectiveDimension.NodeRect.Top - ViewRectangle.Top - scrollBorderY);
|
|
break;
|
|
case ScrollMode.Middle:
|
|
SetVScroll((node.EffectiveDimension.NodeRect.Top + node.EffectiveDimension.NodeRect.Height / 2) - (ViewRectangle.Top + ViewRectangle.Height / 2));
|
|
break;
|
|
case ScrollMode.Bottom:
|
|
SetVScroll(node.EffectiveDimension.NodeRect.Bottom - ViewRectangle.Bottom + scrollBorderY);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Horizontal
|
|
if (!ScrolledRectangle.ContainsX(node.EffectiveDimension.NodeRect.Left) || !ScrolledRectangle.ContainsX(node.EffectiveDimension.NodeRect.Right))
|
|
{
|
|
// Align left or right, depending on which is the smallest change
|
|
int alignLeft = node.EffectiveDimension.NodeRect.Left - ViewRectangle.Left;
|
|
int alignRight = node.EffectiveDimension.NodeRect.Right - ViewRectangle.Right;
|
|
if (Math.Abs(alignLeft - _horizontalScrollBar.Value) < Math.Abs(alignRight - _horizontalScrollBar.Value))
|
|
SetHScroll(alignLeft);
|
|
else
|
|
SetHScroll(alignRight);
|
|
}
|
|
|
|
// Check current highlight
|
|
CheckMouseHighlight();
|
|
}
|
|
|
|
private void SetVScroll(int value)
|
|
{
|
|
_verticalScrollBar.Value = Math.Max(_verticalScrollBar.Minimum, Math.Min(value, _verticalScrollBar.Maximum - _verticalScrollBar.LargeChange + 1));
|
|
}
|
|
|
|
private void SetHScroll(int value)
|
|
{
|
|
_horizontalScrollBar.Value = Math.Max(_horizontalScrollBar.Minimum, Math.Min(value, _horizontalScrollBar.Maximum - _horizontalScrollBar.LargeChange + 1));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Focus
|
|
|
|
protected override void OnGotFocus(EventArgs e)
|
|
{
|
|
base.OnGotFocus(e);
|
|
Invalidate();
|
|
}
|
|
|
|
protected override void OnLostFocus(EventArgs e)
|
|
{
|
|
base.OnLostFocus(e);
|
|
Invalidate();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Winforms Autogenerated
|
|
|
|
private void InitializeComponent()
|
|
{
|
|
this.SuspendLayout();
|
|
//
|
|
// KTree
|
|
//
|
|
this.Name = "KTree";
|
|
this.ResumeLayout(false);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Disabled state
|
|
|
|
protected override void OnEnabledChanged(EventArgs e)
|
|
{
|
|
base.OnEnabledChanged(e);
|
|
RedrawBorder();
|
|
}
|
|
|
|
protected override void WndProc(ref Message m)
|
|
{
|
|
if (m.Msg == (int)WM.NCPAINT)
|
|
{
|
|
WmNcPaint(ref m);
|
|
return;
|
|
}
|
|
base.WndProc(ref m);
|
|
}
|
|
|
|
private void WmNcPaint(ref Message m)
|
|
{
|
|
if (BorderStyle == BorderStyle.None)
|
|
return;
|
|
|
|
IntPtr hDC = User32.GetWindowDC(m.HWnd);
|
|
try
|
|
{
|
|
using (Graphics g = Graphics.FromHdc(hDC))
|
|
{
|
|
_renderer.RenderControlBorder(g, new Rectangle(0, 0, Width, Height));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
User32.ReleaseDC(m.HWnd, hDC);
|
|
}
|
|
}
|
|
|
|
protected override void OnResize(EventArgs e)
|
|
{
|
|
base.OnResize(e);
|
|
RedrawBorder();
|
|
}
|
|
|
|
private void RedrawBorder()
|
|
{
|
|
// Force NCPaint update
|
|
User32.RedrawWindow(this.Handle, IntPtr.Zero, IntPtr.Zero,
|
|
User32.RedrawWindowFlags.Frame | User32.RedrawWindowFlags.Invalidate /*| User32.RedrawWindowFlags.UpdateNow*/);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|