diff --git a/DarkUI/Controls/DarkTreeView.cs b/DarkUI/Controls/DarkTreeView.cs new file mode 100644 index 0000000..3c8aef5 --- /dev/null +++ b/DarkUI/Controls/DarkTreeView.cs @@ -0,0 +1,1274 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +namespace DarkUI +{ + public class DarkTreeView : DarkScrollView + { + #region Event Region + + public event EventHandler SelectedNodesChanged; + public event EventHandler AfterNodeExpand; + public event EventHandler AfterNodeCollapse; + + #endregion + + #region Field Region + + private bool _disposed; + + private readonly int _expandAreaSize = 16; + private readonly int _iconSize = 16; + + private int _itemHeight = 20; + private int _indent = 20; + + private ObservableList _nodes; + private ObservableCollection _selectedNodes; + + private DarkTreeNode _anchoredNodeStart; + private DarkTreeNode _anchoredNodeEnd; + + private Bitmap _nodeClosed; + private Bitmap _nodeClosedHover; + private Bitmap _nodeClosedHoverSelected; + private Bitmap _nodeOpen; + private Bitmap _nodeOpenHover; + private Bitmap _nodeOpenHoverSelected; + + private bool _isDragging; + private DarkTreeNode _provisionalNode; + private DarkTreeNode _dropNode; + private bool _provisionalDragging; + private List _dragNodes; + private Timer _dragTimer; + private Point _dragPos; + + #endregion + + #region Property Region + + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ObservableList Nodes + { + get { return _nodes; } + set + { + if (_nodes != null) + { + _nodes.ItemsAdded -= Nodes_ItemsAdded; + _nodes.ItemsRemoved -= Nodes_ItemsRemoved; + + foreach (var node in _nodes) + UnhookNodeEvents(node); + } + + _nodes = value; + + _nodes.ItemsAdded += Nodes_ItemsAdded; + _nodes.ItemsRemoved += Nodes_ItemsRemoved; + + foreach (var node in _nodes) + HookNodeEvents(node); + + UpdateNodes(); + } + } + + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ObservableCollection SelectedNodes + { + get { return _selectedNodes; } + } + + [Category("Appearance")] + [Description("Determines the height of tree nodes.")] + [DefaultValue(20)] + public int ItemHeight + { + get { return _itemHeight; } + set + { + _itemHeight = value; + UpdateNodes(); + } + } + + [Category("Appearance")] + [Description("Determines the amount of horizontal space given by parent node.")] + [DefaultValue(20)] + public int Indent + { + get { return _indent; } + set + { + _indent = value; + UpdateNodes(); + } + } + + [Category("Behavior")] + [Description("Determines whether multiple tree nodes can be selected at once.")] + [DefaultValue(false)] + public bool MultiSelect { get; set; } + + [Category("Behavior")] + [Description("Determines whether nodes can be moved within this tree view.")] + [DefaultValue(false)] + public bool AllowMoveNodes { get; set; } + + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int VisibleNodeCount { get; private set; } + + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IComparer TreeViewNodeSorter { get; set; } + + #endregion + + #region Constructor Region + + public DarkTreeView() + { + Nodes = new ObservableList(); + _selectedNodes = new ObservableCollection(); + _selectedNodes.CollectionChanged += SelectedNodes_CollectionChanged; + + _dragTimer = new Timer(); + _dragTimer.Interval = 1; + _dragTimer.Tick += DragTimer_Tick; + + LoadIcons(); + } + + #endregion + + #region Dispose Region + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Release managed resources + } + + // Release unmanaged resources. + // Set large fields to null. + DisposeIcons(); + + if (SelectedNodesChanged != null) + SelectedNodesChanged = null; + + if (AfterNodeExpand != null) + AfterNodeExpand = null; + + if (AfterNodeCollapse != null) + AfterNodeExpand = null; + + if (_nodes != null) + _nodes.Dispose(); + + if (_selectedNodes != null) + _selectedNodes.CollectionChanged -= SelectedNodes_CollectionChanged; + + // Set disposed flag + _disposed = true; + } + + // Call Dispose on your base class. + base.Dispose(disposing); + } + + #endregion + + #region Event Handler Region + + private void Nodes_ItemsAdded(object sender, ObservableListModified e) + { + foreach (var node in e.Items) + { + node.ParentTree = this; + node.IsRoot = true; + + HookNodeEvents(node); + } + + if (TreeViewNodeSorter != null) + Nodes.Sort(TreeViewNodeSorter); + + UpdateNodes(); + } + + private void Nodes_ItemsRemoved(object sender, ObservableListModified e) + { + foreach (var node in e.Items) + { + node.ParentTree = this; + node.IsRoot = true; + + HookNodeEvents(node); + } + + UpdateNodes(); + } + + private void ChildNodes_ItemsAdded(object sender, ObservableListModified e) + { + foreach (var node in e.Items) + HookNodeEvents(node); + + UpdateNodes(); + } + + private void ChildNodes_ItemsRemoved(object sender, ObservableListModified e) + { + foreach (var node in e.Items) + { + if (SelectedNodes.Contains(node)) + SelectedNodes.Remove(node); + + UnhookNodeEvents(node); + } + + UpdateNodes(); + } + + private void SelectedNodes_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (SelectedNodesChanged != null) + SelectedNodesChanged(this, null); + } + + private void Nodes_TextChanged(object sender, EventArgs e) + { + UpdateNodes(); + } + + private void Nodes_NodeExpanded(object sender, EventArgs e) + { + UpdateNodes(); + + if (AfterNodeExpand != null) + AfterNodeExpand(this, null); + } + + private void Nodes_NodeCollapsed(object sender, EventArgs e) + { + UpdateNodes(); + + if (AfterNodeCollapse != null) + AfterNodeCollapse(this, null); + } + + protected override void OnMouseMove(MouseEventArgs e) + { + if (_provisionalDragging) + { + if (OffsetMousePosition != _dragPos) + { + StartDrag(); + HandleDrag(); + return; + } + } + + if (_isDragging) + { + if (_dropNode != null) + { + var rect = GetNodeFullRowArea(_dropNode); + if (!rect.Contains(OffsetMousePosition)) + { + _dropNode = null; + Invalidate(); + } + } + } + + CheckHover(); + + if (_isDragging) + { + HandleDrag(); + } + + base.OnMouseMove(e); + } + + protected override void OnMouseWheel(MouseEventArgs e) + { + CheckHover(); + + base.OnMouseWheel(e); + } + + protected override void OnMouseDown(MouseEventArgs e) + { + if (e.Button == MouseButtons.Left || e.Button == MouseButtons.Right) + { + foreach (var node in Nodes) + CheckNodeClick(node, OffsetMousePosition, e.Button); + } + + base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseEventArgs e) + { + if (_isDragging) + { + HandleDrop(); + } + + if (_provisionalDragging) + { + + if (_provisionalNode != null) + { + var pos = _dragPos; + if (OffsetMousePosition == pos) + SelectNode(_provisionalNode); + } + + _provisionalDragging = false; + } + + base.OnMouseUp(e); + } + + protected override void OnMouseDoubleClick(MouseEventArgs e) + { + if (ModifierKeys == Keys.Control) + return; + + if (e.Button == MouseButtons.Left) + { + foreach (var node in Nodes) + CheckNodeDoubleClick(node, OffsetMousePosition); + } + + base.OnMouseDoubleClick(e); + } + + protected override void OnMouseLeave(EventArgs e) + { + base.OnMouseLeave(e); + + foreach (var node in Nodes) + NodeMouseLeave(node); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (_isDragging) + return; + + if (Nodes.Count == 0) + return; + + if (e.KeyCode != Keys.Down && e.KeyCode != Keys.Up && e.KeyCode != Keys.Left && e.KeyCode != Keys.Right) + return; + + if (_anchoredNodeEnd == null) + { + if (Nodes.Count > 0) + SelectNode(Nodes[0]); + return; + } + + if (e.KeyCode == Keys.Down || e.KeyCode == Keys.Up) + { + if (MultiSelect && ModifierKeys == Keys.Shift) + { + if (e.KeyCode == Keys.Up) + { + if (_anchoredNodeEnd.PrevVisibleNode != null) + { + SelectAnchoredRange(_anchoredNodeEnd.PrevVisibleNode); + EnsureVisible(); + } + } + else if (e.KeyCode == Keys.Down) + { + if (_anchoredNodeEnd.NextVisibleNode != null) + { + SelectAnchoredRange(_anchoredNodeEnd.NextVisibleNode); + EnsureVisible(); + } + } + } + else + { + if (e.KeyCode == Keys.Up) + { + if (_anchoredNodeEnd.PrevVisibleNode != null) + { + SelectNode(_anchoredNodeEnd.PrevVisibleNode); + EnsureVisible(); + } + } + else if (e.KeyCode == Keys.Down) + { + if (_anchoredNodeEnd.NextVisibleNode != null) + { + SelectNode(_anchoredNodeEnd.NextVisibleNode); + EnsureVisible(); + } + } + } + } + + if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right) + { + if (e.KeyCode == Keys.Left) + { + if (_anchoredNodeEnd.Expanded && _anchoredNodeEnd.Nodes.Count > 0) + { + _anchoredNodeEnd.Expanded = false; + } + else + { + if (_anchoredNodeEnd.ParentNode != null) + { + SelectNode(_anchoredNodeEnd.ParentNode); + EnsureVisible(); + } + } + } + else if (e.KeyCode == Keys.Right) + { + if (!_anchoredNodeEnd.Expanded) + { + _anchoredNodeEnd.Expanded = true; + } + else + { + if (_anchoredNodeEnd.Nodes.Count > 0) + { + SelectNode(_anchoredNodeEnd.Nodes[0]); + EnsureVisible(); + } + } + } + } + } + + private void DragTimer_Tick(object sender, EventArgs e) + { + if (!_isDragging) + { + StopDrag(); + return; + } + + if (MouseButtons != MouseButtons.Left) + { + StopDrag(); + return; + } + + var pos = PointToClient(MousePosition); + + if (_vScrollBar.Visible) + { + // Scroll up + if (pos.Y < ClientRectangle.Top) + { + var difference = (pos.Y - ClientRectangle.Top) * -1; + + if (difference > ItemHeight) + difference = ItemHeight; + + _vScrollBar.Value = _vScrollBar.Value - difference; + } + + // Scroll down + if (pos.Y > ClientRectangle.Bottom) + { + var difference = pos.Y - ClientRectangle.Bottom; + + if (difference > ItemHeight) + difference = ItemHeight; + + _vScrollBar.Value = _vScrollBar.Value + difference; + } + } + + if (_hScrollBar.Visible) + { + // Scroll left + if (pos.X < ClientRectangle.Left) + { + var difference = (pos.X - ClientRectangle.Left) * -1; + + if (difference > ItemHeight) + difference = ItemHeight; + + _hScrollBar.Value = _hScrollBar.Value - difference; + } + + // Scroll right + if (pos.X > ClientRectangle.Right) + { + var difference = pos.X - ClientRectangle.Right; + + if (difference > ItemHeight) + difference = ItemHeight; + + _hScrollBar.Value = _hScrollBar.Value + difference; + } + } + } + + #endregion + + #region Method Region + + private void HookNodeEvents(DarkTreeNode node) + { + node.Nodes.ItemsAdded += ChildNodes_ItemsAdded; + node.Nodes.ItemsRemoved += ChildNodes_ItemsRemoved; + + node.TextChanged += Nodes_TextChanged; + node.NodeExpanded += Nodes_NodeExpanded; + node.NodeCollapsed += Nodes_NodeCollapsed; + + foreach (var childNode in node.Nodes) + HookNodeEvents(childNode); + } + + private void UnhookNodeEvents(DarkTreeNode node) + { + node.Nodes.ItemsAdded -= ChildNodes_ItemsAdded; + node.Nodes.ItemsRemoved -= ChildNodes_ItemsRemoved; + + node.TextChanged -= Nodes_TextChanged; + node.NodeExpanded -= Nodes_NodeExpanded; + node.NodeCollapsed -= Nodes_NodeCollapsed; + + foreach (var childNode in node.Nodes) + UnhookNodeEvents(childNode); + } + + private void UpdateNodes() + { + if (_isDragging) + return; + + if (Nodes.Count == 0) + return; + + var yOffset = 0; + var isOdd = false; + var index = 0; + DarkTreeNode prevNode = null; + + ContentSize = new Size(0, 0); + + for (var i = 0; i <= Nodes.Count - 1; i++) + { + var node = Nodes[i]; + UpdateNode(node, ref prevNode, 0, ref yOffset, ref isOdd, ref index); + } + + ContentSize = new Size(ContentSize.Width, yOffset); + + VisibleNodeCount = index; + + Invalidate(); + } + + private void UpdateNode(DarkTreeNode node, ref DarkTreeNode prevNode, int indent, ref int yOffset, + ref bool isOdd, ref int index) + { + UpdateNodeBounds(node, yOffset, indent); + + yOffset += ItemHeight; + + node.Odd = isOdd; + isOdd = !isOdd; + + node.VisibleIndex = index; + index++; + + node.PrevVisibleNode = prevNode; + + if (prevNode != null) + prevNode.NextVisibleNode = node; + + prevNode = node; + + if (node.Expanded) + { + foreach (var childNode in node.Nodes) + UpdateNode(childNode, ref prevNode, indent + Indent, ref yOffset, ref isOdd, ref index); + } + } + + private void UpdateNodeBounds(DarkTreeNode node, int yOffset, int indent) + { + var expandTop = yOffset + (ItemHeight / 2) - (_expandAreaSize / 2); + node.ExpandArea = new Rectangle(indent + 3, expandTop, _expandAreaSize, _expandAreaSize); + + var iconTop = yOffset + (ItemHeight / 2) - (_iconSize / 2); + node.IconArea = new Rectangle(node.ExpandArea.Right + 2, iconTop, _iconSize, _iconSize); + + using (var g = CreateGraphics()) + { + var textSize = (int)(g.MeasureString(node.Text, Font).Width); + node.TextArea = new Rectangle(node.IconArea.Right + 2, yOffset, textSize + 1, ItemHeight); + } + + node.FullArea = new Rectangle(indent, yOffset, (node.TextArea.Right - indent), ItemHeight); + + if (ContentSize.Width < node.TextArea.Right + 2) + ContentSize = new Size(node.TextArea.Right + 2, ContentSize.Height); + } + + private void LoadIcons() + { + DisposeIcons(); + + _nodeClosed = TreeViewIcons.node_closed_empty.SetColor(Colors.LightText); + _nodeClosedHover = TreeViewIcons.node_closed_empty.SetColor(Colors.BlueHighlight); + _nodeClosedHoverSelected = TreeViewIcons.node_closed_full.SetColor(Colors.LightText); + _nodeOpen = TreeViewIcons.node_open.SetColor(Colors.LightText); + _nodeOpenHover = TreeViewIcons.node_open.SetColor(Colors.BlueHighlight); + _nodeOpenHoverSelected = TreeViewIcons.node_open_empty.SetColor(Colors.LightText); + } + + private void DisposeIcons() + { + if (_nodeClosed != null) + _nodeClosed.Dispose(); + + if (_nodeClosedHover != null) + _nodeClosedHover.Dispose(); + + if (_nodeClosedHoverSelected != null) + _nodeClosedHoverSelected.Dispose(); + + if (_nodeOpen != null) + _nodeOpen.Dispose(); + + if (_nodeOpenHover != null) + _nodeOpenHover.Dispose(); + + if (_nodeOpenHoverSelected != null) + _nodeOpenHoverSelected.Dispose(); + } + + private void CheckHover() + { + if (!ClientRectangle.Contains(PointToClient(MousePosition))) + { + if (_isDragging) + { + if (_dropNode != null) + { + _dropNode = null; + Invalidate(); + } + } + + return; + } + + foreach (var node in Nodes) + CheckNodeHover(node, OffsetMousePosition); + } + + private void NodeMouseLeave(DarkTreeNode node) + { + node.ExpandAreaHot = false; + + foreach (var childNode in node.Nodes) + NodeMouseLeave(childNode); + + Invalidate(); + } + + private void CheckNodeHover(DarkTreeNode node, Point location) + { + if (_isDragging) + { + var rect = GetNodeFullRowArea(node); + if (rect.Contains(OffsetMousePosition)) + { + var newDropNode = _dragNodes.Contains(node) ? null : node; + + if (_dropNode != newDropNode) + { + _dropNode = newDropNode; + Invalidate(); + } + } + } + else + { + var hot = node.ExpandArea.Contains(location); + if (node.ExpandAreaHot != hot) + { + node.ExpandAreaHot = hot; + Invalidate(); + } + } + + foreach (var childNode in node.Nodes) + CheckNodeHover(childNode, location); + } + + private void CheckNodeClick(DarkTreeNode node, Point location, MouseButtons button) + { + var rect = GetNodeFullRowArea(node); + if (rect.Contains(location)) + { + if (node.ExpandArea.Contains(location)) + { + if (button == MouseButtons.Left) + node.Expanded = !node.Expanded; + } + else + { + if (button == MouseButtons.Left) + { + if (MultiSelect && ModifierKeys == Keys.Shift) + { + SelectAnchoredRange(node); + } + else if (MultiSelect && ModifierKeys == Keys.Control) + { + ToggleNode(node); + } + else + { + if (!SelectedNodes.Contains(node)) + SelectNode(node); + + _dragPos = OffsetMousePosition; + _provisionalDragging = true; + _provisionalNode = node; + } + + return; + } + else if (button == MouseButtons.Right) + { + if (MultiSelect && ModifierKeys == Keys.Shift) + return; + + if (MultiSelect && ModifierKeys == Keys.Control) + return; + + if (!SelectedNodes.Contains(node)) + SelectNode(node); + + return; + } + } + } + + if (node.Expanded) + { + foreach (var childNode in node.Nodes) + CheckNodeClick(childNode, location, button); + } + } + + private void CheckNodeDoubleClick(DarkTreeNode node, Point location) + { + var rect = GetNodeFullRowArea(node); + if (rect.Contains(location)) + { + if (!node.ExpandArea.Contains(location)) + node.Expanded = !node.Expanded; + + return; + } + + if (node.Expanded) + { + foreach (var childNode in node.Nodes) + CheckNodeDoubleClick(childNode, location); + } + } + + public void SelectNode(DarkTreeNode node) + { + _selectedNodes.Clear(); + _selectedNodes.Add(node); + + _anchoredNodeStart = node; + _anchoredNodeEnd = node; + + Invalidate(); + } + + public void SelectNodes(DarkTreeNode startNode, DarkTreeNode endNode) + { + var nodes = new List(); + + if (startNode == endNode) + nodes.Add(startNode); + + if (startNode.VisibleIndex < endNode.VisibleIndex) + { + var node = startNode; + nodes.Add(node); + while (node != endNode && node != null) + { + node = node.NextVisibleNode; + nodes.Add(node); + } + } + else if (startNode.VisibleIndex > endNode.VisibleIndex) + { + var node = startNode; + nodes.Add(node); + while (node != endNode && node != null) + { + node = node.PrevVisibleNode; + nodes.Add(node); + } + } + + SelectNodes(nodes, false); + } + + public void SelectNodes(List nodes, bool updateAnchors = true) + { + _selectedNodes.Clear(); + + foreach (var node in nodes) + _selectedNodes.Add(node); + + if (updateAnchors && _selectedNodes.Count > 0) + { + _anchoredNodeStart = _selectedNodes[_selectedNodes.Count - 1]; + _anchoredNodeEnd = _selectedNodes[_selectedNodes.Count - 1]; + } + + Invalidate(); + } + + private void SelectAnchoredRange(DarkTreeNode node) + { + _anchoredNodeEnd = node; + SelectNodes(_anchoredNodeStart, _anchoredNodeEnd); + } + + public void ToggleNode(DarkTreeNode node) + { + if (_selectedNodes.Contains(node)) + { + _selectedNodes.Remove(node); + + // If we just removed both the anchor start AND end then reset them + if (_anchoredNodeStart == node && _anchoredNodeEnd == node) + { + if (_selectedNodes.Count > 0) + { + _anchoredNodeStart = _selectedNodes[0]; + _anchoredNodeEnd = _selectedNodes[0]; + } + else + { + _anchoredNodeStart = null; + _anchoredNodeEnd = null; + } + } + + // If we just removed the anchor start then update it accordingly + if (_anchoredNodeStart == node) + { + if (_anchoredNodeEnd.VisibleIndex < node.VisibleIndex) + _anchoredNodeStart = node.PrevVisibleNode; + else if (_anchoredNodeEnd.VisibleIndex > node.VisibleIndex) + _anchoredNodeStart = node.NextVisibleNode; + else + _anchoredNodeStart = _anchoredNodeEnd; + } + + // If we just removed the anchor end then update it accordingly + if (_anchoredNodeEnd == node) + { + if (_anchoredNodeStart.VisibleIndex < node.VisibleIndex) + _anchoredNodeEnd = node.PrevVisibleNode; + else if (_anchoredNodeStart.VisibleIndex > node.VisibleIndex) + _anchoredNodeEnd = node.NextVisibleNode; + else + _anchoredNodeEnd = _anchoredNodeStart; + } + } + else + { + _selectedNodes.Add(node); + + _anchoredNodeStart = node; + _anchoredNodeEnd = node; + } + + Invalidate(); + } + + public Rectangle GetNodeFullRowArea(DarkTreeNode node) + { + if (node.ParentNode != null && !node.ParentNode.Expanded) + return new Rectangle(-1, -1, -1, -1); + + var width = Math.Max(ContentSize.Width, Viewport.Width); + var rect = new Rectangle(0, node.FullArea.Top, width, ItemHeight); + return rect; + } + + public void EnsureVisible() + { + if (SelectedNodes.Count == 0) + return; + + foreach (var node in SelectedNodes) + node.EnsureVisible(); + + var itemTop = -1; + + if (!MultiSelect) + itemTop = SelectedNodes[0].FullArea.Top; + else + itemTop = _anchoredNodeEnd.FullArea.Top; + + var itemBottom = itemTop + ItemHeight; + + if (itemTop < Viewport.Top) + VScrollTo(itemTop); + + if (itemBottom > Viewport.Bottom) + VScrollTo((itemBottom - Viewport.Height)); + } + + public void Sort() + { + if (TreeViewNodeSorter == null) + return; + + Nodes.Sort(TreeViewNodeSorter); + + foreach (var node in Nodes) + SortChildNodes(node); + } + + private void SortChildNodes(DarkTreeNode node) + { + node.Nodes.Sort(TreeViewNodeSorter); + + foreach (var childNode in node.Nodes) + SortChildNodes(childNode); + } + + public DarkTreeNode FindNode(string path) + { + foreach (var node in Nodes) + { + var compNode = FindNode(node, path); + if (compNode != null) + return compNode; + } + + return null; + } + + private DarkTreeNode FindNode(DarkTreeNode parentNode, string path, bool recursive = true) + { + if (parentNode.FullPath == path) + return parentNode; + + foreach (var node in parentNode.Nodes) + { + if (node.FullPath == path) + return node; + + if (recursive) + { + var compNode = FindNode(node, path); + if (compNode != null) + return compNode; + } + } + + return null; + } + + #endregion + + #region Drag & Drop Region + + private void StartDrag() + { + if (!AllowMoveNodes) + { + _provisionalDragging = false; + return; + } + + // Create initial list of nodes to drag + _dragNodes = new List(); + foreach (var node in SelectedNodes) + _dragNodes.Add(node); + + // Clear out any nodes with a parent that is being dragged + foreach (var node in _dragNodes.ToList()) + { + if (node.ParentNode == null) + continue; + + if (_dragNodes.Contains(node.ParentNode)) + _dragNodes.Remove(node); + } + + _provisionalDragging = false; + _isDragging = true; + + _dragTimer.Start(); + + Cursor = Cursors.SizeAll; + } + + private void HandleDrag() + { + if (!AllowMoveNodes) + return; + + var dropNode = _dropNode; + + if (dropNode == null) + { + if (Cursor != Cursors.No) + Cursor = Cursors.No; + + return; + } + + if (ForceDropToParent(dropNode)) + dropNode = dropNode.ParentNode; + + if (!CanMoveNodes(_dragNodes, dropNode)) + { + if (Cursor != Cursors.No) + Cursor = Cursors.No; + + return; + } + + if (Cursor != Cursors.SizeAll) + Cursor = Cursors.SizeAll; + } + + private void HandleDrop() + { + if (!AllowMoveNodes) + return; + + var dropNode = _dropNode; + + if (dropNode == null) + { + StopDrag(); + return; + } + + if (ForceDropToParent(dropNode)) + dropNode = dropNode.ParentNode; + + if (CanMoveNodes(_dragNodes, dropNode, true)) + { + var cachedSelectedNodes = SelectedNodes.ToList(); + + MoveNodes(_dragNodes, dropNode); + + foreach (var node in _dragNodes) + { + if (node.ParentNode == null) + Nodes.Remove(node); + else + node.ParentNode.Nodes.Remove(node); + + dropNode.Nodes.Add(node); + } + + if (TreeViewNodeSorter != null) + dropNode.Nodes.Sort(TreeViewNodeSorter); + + dropNode.Expanded = true; + + NodesMoved(_dragNodes); + + foreach (var node in cachedSelectedNodes) + _selectedNodes.Add(node); + } + + StopDrag(); + UpdateNodes(); + } + + private void StopDrag() + { + _isDragging = false; + _dragNodes = null; + _dropNode = null; + + _dragTimer.Stop(); + + Cursor = Cursors.Default; + + Invalidate(); + } + + protected virtual bool ForceDropToParent(DarkTreeNode node) + { + return false; + } + + protected virtual bool CanMoveNodes(List dragNodes, DarkTreeNode dropNode, bool isMoving = false) + { + if (dropNode == null) + return false; + + foreach (var node in dragNodes) + { + if (node == dropNode) + { + if (isMoving) + DarkMessageBox.ShowError(String.Format(@"Cannot move {0}. The destination folder is the same as the source folder.", node.Text), Application.ProductName); + + return false; + } + + if (node.ParentNode != null && node.ParentNode == dropNode) + { + if (isMoving) + DarkMessageBox.ShowError(String.Format(@"Cannot move {0}. The destination folder is the same as the source folder.", node.Text), Application.ProductName); + + return false; + } + + var parentNode = dropNode.ParentNode; + while (parentNode != null) + { + if (node == parentNode) + { + if (isMoving) + DarkMessageBox.ShowError(String.Format(@"Cannot move {0}. The destination folder is a subfolder of the source folder.", node.Text), Application.ProductName); + + return false; + } + + parentNode = parentNode.ParentNode; + } + } + + return true; + } + + protected virtual void MoveNodes(List dragNodes, DarkTreeNode dropNode) + { } + + protected virtual void NodesMoved(List nodesMoved) + { } + + #endregion + + #region Paint Region + + protected override void PaintContent(Graphics g) + { + foreach (var node in Nodes) + { + DrawNode(node, g); + } + } + + private void DrawNode(DarkTreeNode node, Graphics g) + { + var rect = GetNodeFullRowArea(node); + + // 1. Draw background + var bgColor = node.Odd ? Colors.HeaderBackground : Colors.GreyBackground; + + if (SelectedNodes.Count > 0 && SelectedNodes.Contains(node)) + bgColor = Focused ? Colors.BlueSelection : Colors.GreySelection; + + if (_isDragging && _dropNode == node) + bgColor = Focused ? Colors.BlueSelection : Colors.GreySelection; + + using (var b = new SolidBrush(bgColor)) + { + g.FillRectangle(b, rect); + } + + // 2. Draw plus/minus icon + if (node.Nodes.Count > 0) + { + var pos = new Point(node.ExpandArea.Location.X - 1, node.ExpandArea.Location.Y - 1); + + var icon = _nodeOpen; + + if (node.Expanded && !node.ExpandAreaHot) + icon = _nodeOpen; + else if (node.Expanded && node.ExpandAreaHot && !SelectedNodes.Contains(node)) + icon = _nodeOpenHover; + else if (node.Expanded && node.ExpandAreaHot && SelectedNodes.Contains(node)) + icon = _nodeOpenHoverSelected; + else if (!node.Expanded && !node.ExpandAreaHot) + icon = _nodeClosed; + else if (!node.Expanded && node.ExpandAreaHot && !SelectedNodes.Contains(node)) + icon = _nodeClosedHover; + else if (!node.Expanded && node.ExpandAreaHot && SelectedNodes.Contains(node)) + icon = _nodeClosedHoverSelected; + + g.DrawImageUnscaled(icon, pos); + } + + // 3. Draw icon + if (node.Icon != null) + { + if (node.Expanded && node.ExpandedIcon != null) + g.DrawImageUnscaled(node.ExpandedIcon, node.IconArea.Location); + else + g.DrawImageUnscaled(node.Icon, node.IconArea.Location); + } + + // 4. Draw text + using (var b = new SolidBrush(Colors.LightText)) + { + var stringFormat = new StringFormat + { + Alignment = StringAlignment.Near, + LineAlignment = StringAlignment.Center + }; + + g.DrawString(node.Text, Font, b, node.TextArea, stringFormat); + } + + // 5. Draw child nodes + if (node.Expanded) + { + foreach (var childNode in node.Nodes) + DrawNode(childNode, g); + } + } + + #endregion + } +} diff --git a/DarkUI/Controls/Items/DarkTreeNode.cs b/DarkUI/Controls/Items/DarkTreeNode.cs new file mode 100644 index 0000000..ae1b931 --- /dev/null +++ b/DarkUI/Controls/Items/DarkTreeNode.cs @@ -0,0 +1,254 @@ +using System; +using System.Drawing; + +namespace DarkUI +{ + public class DarkTreeNode + { + #region Event Region + + public event EventHandler> ItemsAdded; + public event EventHandler> ItemsRemoved; + + public event EventHandler TextChanged; + public event EventHandler NodeExpanded; + public event EventHandler NodeCollapsed; + + #endregion + + #region Field Region + + private string _text; + private bool _isRoot; + private DarkTreeView _parentTree; + private DarkTreeNode _parentNode; + + private ObservableList _nodes; + + private bool _expanded; + + #endregion + + #region Property Region + + public string Text + { + get { return _text; } + set + { + if (_text == value) + return; + + _text = value; + + OnTextChanged(); + } + } + + public Rectangle ExpandArea { get; set; } + public Rectangle IconArea { get; set; } + public Rectangle TextArea { get; set; } + public Rectangle FullArea { get; set; } + + public bool ExpandAreaHot { get; set; } + + public Bitmap Icon { get; set; } + public Bitmap ExpandedIcon { get; set; } + + public bool Expanded + { + get { return _expanded; } + set + { + if (_expanded == value) + return; + + if (value == true && Nodes.Count == 0) + return; + + _expanded = value; + + if (_expanded) + { + if (NodeExpanded != null) + NodeExpanded(this, null); + } + else + { + if (NodeCollapsed != null) + NodeCollapsed(this, null); + } + } + } + + public ObservableList Nodes + { + get { return _nodes; } + set + { + if (_nodes != null) + { + _nodes.ItemsAdded -= Nodes_ItemsAdded; + _nodes.ItemsRemoved -= Nodes_ItemsRemoved; + } + + _nodes = value; + + _nodes.ItemsAdded += Nodes_ItemsAdded; + _nodes.ItemsRemoved += Nodes_ItemsRemoved; + } + } + + public bool IsRoot + { + get { return _isRoot; } + set { _isRoot = value; } + } + + public DarkTreeView ParentTree + { + get { return _parentTree; } + set + { + if (_parentTree == value) + return; + + _parentTree = value; + + foreach (var node in Nodes) + node.ParentTree = _parentTree; + } + } + + public DarkTreeNode ParentNode + { + get { return _parentNode; } + set { _parentNode = value; } + } + + public bool Odd { get; set; } + + public object NodeType { get; set; } + + public object Tag { get; set; } + + public string FullPath + { + get + { + var parent = ParentNode; + var path = Text; + + while (parent != null) + { + path = string.Format("{0}{1}{2}", parent.Text, "\\", path); + parent = parent.ParentNode; + } + + return path; + } + } + + public DarkTreeNode PrevVisibleNode { get; set; } + + public DarkTreeNode NextVisibleNode { get; set; } + + public int VisibleIndex { get; set; } + + public bool IsNodeAncestor(DarkTreeNode node) + { + var parent = ParentNode; + while (parent != null) + { + if (parent == node) + return true; + + parent = parent.ParentNode; + } + + return false; + } + + #endregion + + #region Constructor Region + + public DarkTreeNode() + { + Nodes = new ObservableList(); + } + + public DarkTreeNode(string text) + : this() + { + Text = text; + } + + #endregion + + #region Method Region + + public void Remove() + { + if (ParentNode != null) + ParentNode.Nodes.Remove(this); + else + ParentTree.Nodes.Remove(this); + } + + public void EnsureVisible() + { + var parent = ParentNode; + + while (parent != null) + { + parent.Expanded = true; + parent = parent.ParentNode; + } + } + + #endregion + + #region Event Handler Region + + private void OnTextChanged() + { + if (ParentTree != null && ParentTree.TreeViewNodeSorter != null) + { + if (ParentNode != null) + ParentNode.Nodes.Sort(ParentTree.TreeViewNodeSorter); + else + ParentTree.Nodes.Sort(ParentTree.TreeViewNodeSorter); + } + + if (TextChanged != null) + TextChanged(this, null); + } + + private void Nodes_ItemsAdded(object sender, ObservableListModified e) + { + foreach (var node in e.Items) + { + node.ParentNode = this; + node.ParentTree = ParentTree; + } + + if (ParentTree != null && ParentTree.TreeViewNodeSorter != null) + Nodes.Sort(ParentTree.TreeViewNodeSorter); + + if (ItemsAdded != null) + ItemsAdded(this, e); + } + + private void Nodes_ItemsRemoved(object sender, ObservableListModified e) + { + if (Nodes.Count == 0) + Expanded = false; + + if (ItemsRemoved != null) + ItemsRemoved(this, e); + } + + #endregion + } +} diff --git a/DarkUI/DarkUI.csproj b/DarkUI/DarkUI.csproj index f4bee5f..de062b4 100644 --- a/DarkUI/DarkUI.csproj +++ b/DarkUI/DarkUI.csproj @@ -43,7 +43,10 @@ - + + Component + + Component @@ -84,6 +87,7 @@ Component + Form @@ -115,6 +119,11 @@ True ScrollIcons.resx + + True + True + TreeViewIcons.resx + @@ -141,6 +150,11 @@ ScrollIcons.Designer.cs DarkUI + + ResXFileCodeGenerator + TreeViewIcons.Designer.cs + DarkUI + @@ -169,6 +183,18 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\node_closed_empty.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\node_closed_full.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\node_open.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\node_open_empty.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/DarkUI/Resources/node_closed_empty.png b/DarkUI/Resources/node_closed_empty.png new file mode 100644 index 0000000..5da8192 Binary files /dev/null and b/DarkUI/Resources/node_closed_empty.png differ diff --git a/DarkUI/Resources/node_closed_full.png b/DarkUI/Resources/node_closed_full.png new file mode 100644 index 0000000..21a7062 Binary files /dev/null and b/DarkUI/Resources/node_closed_full.png differ diff --git a/DarkUI/Resources/node_open.png b/DarkUI/Resources/node_open.png new file mode 100644 index 0000000..a9addef Binary files /dev/null and b/DarkUI/Resources/node_open.png differ diff --git a/DarkUI/Resources/node_open_empty.png b/DarkUI/Resources/node_open_empty.png new file mode 100644 index 0000000..b73c8b3 Binary files /dev/null and b/DarkUI/Resources/node_open_empty.png differ diff --git a/Example/Example.csproj b/Example/Example.csproj index 23408d1..e99a27b 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -45,11 +45,20 @@ MainForm.cs + + True + True + Icons.resx + MainForm.cs + + ResXFileCodeGenerator + Icons.Designer.cs + ResXFileCodeGenerator Resources.Designer.cs @@ -76,6 +85,15 @@ DarkUI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Resources\Files_7954.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Resources\folder_Closed_16xLG.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Resources\folder_16x.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/Example/Resources/Files_7954.png b/Example/Resources/Files_7954.png new file mode 100644 index 0000000..009dde9 Binary files /dev/null and b/Example/Resources/Files_7954.png differ diff --git a/Example/Resources/folder_16x.png b/Example/Resources/folder_16x.png new file mode 100644 index 0000000..bf1195d Binary files /dev/null and b/Example/Resources/folder_16x.png differ diff --git a/Example/Resources/folder_Closed_16xLG.png b/Example/Resources/folder_Closed_16xLG.png new file mode 100644 index 0000000..ffa4210 Binary files /dev/null and b/Example/Resources/folder_Closed_16xLG.png differ