612 lines
No EOL
15 KiB
C#
612 lines
No EOL
15 KiB
C#
/*
|
|
Copyright (c) 2018, Lars Brubaker, John Lewin
|
|
All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
modification, are permitted provided that the following conditions are met:
|
|
|
|
1. Redistributions of source code must retain the above copyright notice, this
|
|
list of conditions and the following disclaimer.
|
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
this list of conditions and the following disclaimer in the documentation
|
|
and/or other materials provided with the distribution.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
The views and conclusions contained in the software and documentation are those
|
|
of the authors and should not be interpreted as representing official policies,
|
|
either expressed or implied, of the FreeBSD Project.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using MatterHackers.Agg;
|
|
using MatterHackers.Agg.Font;
|
|
using MatterHackers.Agg.Image;
|
|
using MatterHackers.Agg.Platform;
|
|
using MatterHackers.Agg.UI;
|
|
using MatterHackers.ImageProcessing;
|
|
using MatterHackers.VectorMath;
|
|
|
|
namespace MatterHackers.MatterControl.CustomWidgets
|
|
{
|
|
public class TreeNode : FlowLayoutWidget, ICheckbox
|
|
{
|
|
private readonly GuiWidget content;
|
|
private TreeView _treeView;
|
|
private ImageBuffer _image = null;
|
|
private readonly TextWidget textWidget;
|
|
private readonly TreeExpandWidget expandWidget;
|
|
private readonly ImageWidget imageWidget;
|
|
private bool isDirty;
|
|
|
|
public TreeNode(ThemeConfig theme, bool useIcon = true, TreeNode nodeParent = null)
|
|
: base(FlowDirection.TopToBottom)
|
|
{
|
|
this.HAnchor = HAnchor.Fit | HAnchor.Left;
|
|
this.VAnchor = VAnchor.Fit;
|
|
|
|
this.NodeParent = nodeParent;
|
|
|
|
this.TitleBar = new FlowLayoutWidget();
|
|
this.TitleBar.Click += (s, e) =>
|
|
{
|
|
if (TreeView != null)
|
|
{
|
|
TreeView.SelectedNode = this;
|
|
TreeView.NotifyItemClicked(TitleBar, e);
|
|
}
|
|
};
|
|
|
|
TreeNode hitNode = null;
|
|
this.TitleBar.MouseDown += (s, e) =>
|
|
{
|
|
if (TreeView != null && e.Button == MouseButtons.Left)
|
|
{
|
|
if (e.Clicks == 1)
|
|
{
|
|
hitNode = this;
|
|
}
|
|
else if (e.Clicks == 2)
|
|
{
|
|
// Nodes can move around in the tree between clicks.
|
|
// Make sure we're hitting the same node twice.
|
|
if (this != hitNode)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TreeView.SelectedNode = this;
|
|
|
|
if (this.Nodes.Count > 0)
|
|
{
|
|
this.Expanded = !this.Expanded;
|
|
}
|
|
else
|
|
{
|
|
this.TreeView.NotifyItemDoubleClicked(TitleBar, e);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this.AddChild(this.TitleBar);
|
|
|
|
// add a check box
|
|
expandWidget = new TreeExpandWidget(theme)
|
|
{
|
|
Expandable = GetNodeCount(false) != 0,
|
|
VAnchor = VAnchor.Fit | VAnchor.Center,
|
|
Height = 16,
|
|
Width = 16
|
|
};
|
|
|
|
expandWidget.Click += (s, e) =>
|
|
{
|
|
this.Expanded = !this.Expanded;
|
|
expandWidget.Expanded = this.Expanded;
|
|
};
|
|
|
|
this.TitleBar.AddChild(expandWidget);
|
|
|
|
this.HighlightRegion = new FlowLayoutWidget()
|
|
{
|
|
VAnchor = VAnchor.Fit,
|
|
HAnchor = HAnchor.Fit,
|
|
Padding = useIcon ? new BorderDouble(2) : new BorderDouble(4, 2),
|
|
Selectable = false
|
|
};
|
|
this.TitleBar.AddChild(this.HighlightRegion);
|
|
|
|
// add a check box
|
|
if (useIcon)
|
|
{
|
|
_image = new ImageBuffer(16, 16);
|
|
|
|
this.HighlightRegion.AddChild(imageWidget = new ImageWidget(this.Image, listenForImageChanged: false)
|
|
{
|
|
VAnchor = VAnchor.Center,
|
|
Margin = new BorderDouble(right: 4),
|
|
Selectable = false
|
|
});
|
|
}
|
|
|
|
this.HighlightRegion.AddChild(textWidget = new TextWidget(this.Text, pointSize: theme.DefaultFontSize, textColor: theme.TextColor)
|
|
{
|
|
Selectable = false,
|
|
AutoExpandBoundsToText = true,
|
|
VAnchor = VAnchor.Center
|
|
});
|
|
|
|
content = new FlowLayoutWidget(FlowDirection.TopToBottom)
|
|
{
|
|
HAnchor = HAnchor.Fit | HAnchor.Left,
|
|
Visible = false, // content starts out not visible
|
|
Name = "content",
|
|
Margin = new BorderDouble(12, 3),
|
|
};
|
|
this.AddChild(content);
|
|
|
|
// Register listeners
|
|
this.Nodes.CollectionChanged += this.Nodes_CollectionChanged;
|
|
}
|
|
|
|
public override void OnKeyDown(KeyEventArgs keyEvent)
|
|
{
|
|
base.OnKeyDown(keyEvent);
|
|
|
|
var restoreFocus = Focused;
|
|
|
|
if (!keyEvent.Handled)
|
|
{
|
|
switch (keyEvent.KeyCode)
|
|
{
|
|
case Keys.Right:
|
|
this.Expanded = true;
|
|
keyEvent.Handled = true;
|
|
break;
|
|
|
|
case Keys.Left:
|
|
if (!this.Expanded)
|
|
{
|
|
if (this.NodeParent != null)
|
|
{
|
|
// navigate back up to the parent of this node
|
|
TreeView.SelectedNode = this.NodeParent;
|
|
TreeView.NotifyItemClicked(TreeView, new MouseEventArgs(MouseButtons.Left, 1, 0, 0, 0));
|
|
}
|
|
|
|
restoreFocus = false;
|
|
}
|
|
else
|
|
{
|
|
this.Expanded = false;
|
|
}
|
|
|
|
keyEvent.Handled = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (restoreFocus && !Focused)
|
|
{
|
|
Focus();
|
|
}
|
|
}
|
|
|
|
private void Nodes_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
|
|
{
|
|
// Assign NodeParent when items are added
|
|
foreach (var item in e.NewItems)
|
|
{
|
|
if (item is TreeNode treeNode)
|
|
{
|
|
treeNode.NodeParent = this;
|
|
}
|
|
}
|
|
}
|
|
|
|
isDirty = true;
|
|
}
|
|
|
|
public FlowLayoutWidget TitleBar { get; }
|
|
|
|
public FlowLayoutWidget HighlightRegion { get; }
|
|
|
|
// **** Not implemented ****
|
|
public void BeginEdit() => throw new NotImplementedException();
|
|
|
|
public void Collapse(bool collapseChildren) => throw new NotImplementedException();
|
|
|
|
public void Collapse() => throw new NotImplementedException();
|
|
|
|
public void EndEdit(bool cancel) => throw new NotImplementedException();
|
|
|
|
public void EnsureVisible() => throw new NotImplementedException();
|
|
|
|
public void ExpandAll() => throw new NotImplementedException();
|
|
|
|
public void Remove() => throw new NotImplementedException();
|
|
|
|
public int GetNodeCount(bool includeSubTrees)
|
|
{
|
|
if (includeSubTrees)
|
|
{
|
|
return this.Descendants<TreeNode>().Count();
|
|
}
|
|
|
|
return content?.Children.Where((c) => c is TreeNode).Count() ?? 0;
|
|
}
|
|
|
|
public bool AlwaysExpandable
|
|
{
|
|
get => expandWidget.AlwaysExpandable;
|
|
set => expandWidget.AlwaysExpandable = value;
|
|
}
|
|
|
|
public override void OnDraw(Graphics2D graphics2D)
|
|
{
|
|
if (isDirty)
|
|
{
|
|
// doing this during draw will often result in a enumeration changed
|
|
RebuildContentSection();
|
|
}
|
|
|
|
base.OnDraw(graphics2D);
|
|
}
|
|
|
|
public override void OnTextChanged(EventArgs e)
|
|
{
|
|
if (textWidget != null)
|
|
{
|
|
textWidget.Text = this.Text;
|
|
}
|
|
|
|
base.OnTextChanged(e);
|
|
}
|
|
|
|
public override void OnClosed(EventArgs e)
|
|
{
|
|
// Unregister listeners
|
|
this.Nodes.CollectionChanged -= this.Nodes_CollectionChanged;
|
|
|
|
base.OnClosed(e);
|
|
}
|
|
|
|
public void Toggle()
|
|
{
|
|
content.Visible = !content.Visible;
|
|
}
|
|
|
|
public IEnumerable<TreeNode> Ancestors()
|
|
{
|
|
var context = this.NodeParent;
|
|
while (context != null)
|
|
{
|
|
yield return context;
|
|
|
|
context = context.NodeParent;
|
|
}
|
|
}
|
|
|
|
private void RebuildContentSection()
|
|
{
|
|
// Remove but don't close all the current nodes
|
|
content.RemoveChildren();
|
|
|
|
using (content.LayoutLock())
|
|
{
|
|
// Then add them back in (after the change)
|
|
foreach (var node in Nodes)
|
|
{
|
|
node.NodeParent = this;
|
|
node.ClearRemovedFlag();
|
|
content.AddChild(node);
|
|
}
|
|
}
|
|
|
|
content.PerformLayout();
|
|
|
|
// If the node count is ending at 0 we removed content and need to rebuild the title bar so it will net have a + in it
|
|
expandWidget.Expandable = GetNodeCount(false) != 0;
|
|
|
|
isDirty = false;
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return textWidget?.Text ?? "";
|
|
}
|
|
|
|
public bool Checked { get; set; }
|
|
|
|
public bool Editing { get; }
|
|
|
|
public bool Expandable
|
|
{
|
|
get => expandWidget.Expandable;
|
|
set => expandWidget.Expandable = value;
|
|
}
|
|
|
|
public bool ReserveIconSpace
|
|
{
|
|
get => expandWidget.ReserveIconSpace;
|
|
set => expandWidget.ReserveIconSpace = value;
|
|
}
|
|
|
|
private bool _expanded;
|
|
|
|
public bool Expanded
|
|
{
|
|
get => _expanded;
|
|
set
|
|
{
|
|
if (_expanded != value || content.Visible != value)
|
|
{
|
|
_expanded = value;
|
|
expandWidget.Expanded = _expanded;
|
|
|
|
content.Visible = _expanded && this.Nodes.Count > 0;
|
|
ExpandedChanged?.Invoke(this, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public TreeNode FirstNode { get; }
|
|
|
|
public ImageBuffer Image
|
|
{
|
|
get
|
|
{
|
|
return _image;
|
|
}
|
|
|
|
set
|
|
{
|
|
if (_image != value)
|
|
{
|
|
_image = value;
|
|
|
|
if (imageWidget != null)
|
|
{
|
|
imageWidget.Image = _image;
|
|
}
|
|
|
|
OnImageChanged(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public TreeNode LastNode { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the zero-based depth of the tree node in the TreeView control.
|
|
/// </summary>
|
|
public int Level { get; }
|
|
|
|
// Summary:
|
|
// Gets the next sibling tree node.
|
|
//
|
|
// Returns:
|
|
// A TreeNode that represents the next sibling tree node.
|
|
public TreeNode NextNode { get; }
|
|
|
|
// Summary:
|
|
// Gets the next visible tree node.
|
|
//
|
|
// Returns:
|
|
// A TreeNode that represents the next visible tree node.
|
|
public TreeNode NextVisibleNode { get; }
|
|
|
|
// Summary:
|
|
// Gets or sets the font that is used to display the text on the tree node label.
|
|
//
|
|
// Returns:
|
|
// The StyledTypeFace that is used to display the text on the tree node label.
|
|
public StyledTypeFace NodeFont { get; set; }
|
|
|
|
// Summary:
|
|
// Gets the parent tree node of the current tree node.
|
|
//
|
|
// Returns:
|
|
// A TreeNode that represents the parent of the current tree
|
|
// node.
|
|
public TreeNode NodeParent { get; protected set; }
|
|
|
|
public ObservableCollection<TreeNode> Nodes { get; } = new ObservableCollection<TreeNode>();
|
|
|
|
public int PointSize { get; set; }
|
|
|
|
// Summary:
|
|
// Gets the previous sibling tree node.
|
|
//
|
|
// Returns:
|
|
// A TreeNode that represents the previous sibling tree node.
|
|
public TreeNode PrevNode { get; }
|
|
|
|
// Summary:
|
|
// Gets the previous visible tree node.
|
|
//
|
|
// Returns:
|
|
// A TreeNode that represents the previous visible tree node.
|
|
public TreeNode PrevVisibleNode { get; }
|
|
|
|
// Summary:
|
|
// Gets a value indicating whether the tree node is in the selected state.
|
|
//
|
|
// Returns:
|
|
// true if the tree node is in the selected state; otherwise, false.
|
|
public bool Selected
|
|
{
|
|
get
|
|
{
|
|
if (TreeView != null)
|
|
{
|
|
return TreeView.SelectedNode == this;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Summary:
|
|
// Gets or sets the image list index value of the image that is displayed when the
|
|
// tree node is in the selected state.
|
|
//
|
|
// Returns:
|
|
// A zero-based index value that represents the image position in an ImageList.
|
|
public ImageBuffer SelectedImage { get; set; }
|
|
|
|
// Summary:
|
|
// Gets or sets the index of the image that is used to indicate the state of the
|
|
// TreeNode when the parent TreeView has
|
|
// its TreeView.CheckBoxes property set to false.
|
|
//
|
|
// Returns:
|
|
// The index of the image that is used to indicate the state of the TreeNode.
|
|
//
|
|
// Exceptions:
|
|
// T:System.ArgumentOutOfRangeException:
|
|
// The specified index is less than -1 or greater than 14.
|
|
public ImageBuffer StateImage { get; set; }
|
|
|
|
// Summary:
|
|
// Gets or sets the object that contains data about the tree node.
|
|
//
|
|
// Returns:
|
|
// An System.Object that contains data about the tree node. The default is null.
|
|
public object Tag { get; set; }
|
|
|
|
public Color TextColor { get; set; }
|
|
|
|
// Summary:
|
|
// Gets the parent tree view that the tree node is assigned to.
|
|
//
|
|
// Returns:
|
|
// A TreeView that represents the parent tree view that the
|
|
// tree node is assigned to, or null if the node has not been assigned to a tree
|
|
// view.
|
|
|
|
public virtual TreeView TreeView
|
|
{
|
|
get => _treeView ?? NodeParent.TreeView;
|
|
set => _treeView = value;
|
|
}
|
|
|
|
private void OnImageChanged(EventArgs args)
|
|
{
|
|
ImageChanged?.Invoke(this, null);
|
|
}
|
|
|
|
public event EventHandler CheckedStateChanged;
|
|
|
|
public event EventHandler ExpandedChanged;
|
|
|
|
public event EventHandler ImageChanged;
|
|
|
|
private class TreeExpandWidget : FlowLayoutWidget
|
|
{
|
|
private readonly ImageBuffer arrowRight;
|
|
private readonly ImageBuffer arrowDown;
|
|
private readonly ImageBuffer placeholder;
|
|
private readonly IconButton imageButton = null;
|
|
|
|
public TreeExpandWidget(ThemeConfig theme)
|
|
{
|
|
arrowRight = StaticData.Instance.LoadIcon("fa-angle-right_12.png", 12, 12).SetToColor(theme.TextColor);
|
|
arrowDown = StaticData.Instance.LoadIcon("fa-angle-down_12.png", 12, 12).SetToColor(theme.TextColor);
|
|
placeholder = new ImageBuffer(16, 16);
|
|
|
|
this.Margin = new BorderDouble(right: 4);
|
|
|
|
imageButton = new IconButton(placeholder, theme)
|
|
{
|
|
MinimumSize = new Vector2(16 * DeviceScale, 16 * DeviceScale),
|
|
VAnchor = VAnchor.Center,
|
|
Selectable = false,
|
|
Width = 16 * DeviceScale,
|
|
Height = 16 * DeviceScale
|
|
};
|
|
|
|
this.AddChild(imageButton);
|
|
}
|
|
|
|
private bool _alwaysExpandable;
|
|
|
|
public bool AlwaysExpandable
|
|
{
|
|
get => _alwaysExpandable;
|
|
set
|
|
{
|
|
imageButton.SetIcon(_expanded ? arrowDown : arrowRight);
|
|
_alwaysExpandable = value;
|
|
}
|
|
}
|
|
|
|
private bool? _expandable = null;
|
|
|
|
public bool Expandable
|
|
{
|
|
get => _expandable == true || this.AlwaysExpandable;
|
|
set
|
|
{
|
|
if (_expandable != value)
|
|
{
|
|
_expandable = value;
|
|
}
|
|
|
|
this.EnsureExpansionState();
|
|
}
|
|
}
|
|
|
|
private bool _expanded;
|
|
|
|
public bool Expanded
|
|
{
|
|
get => _expanded;
|
|
set
|
|
{
|
|
if (_expanded != value)
|
|
{
|
|
_expanded = value;
|
|
|
|
this.EnsureExpansionState();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void EnsureExpansionState()
|
|
{
|
|
if (!this.Expandable)
|
|
{
|
|
if (this.ReserveIconSpace)
|
|
{
|
|
imageButton.SetIcon(placeholder);
|
|
}
|
|
|
|
imageButton.Visible = this.ReserveIconSpace;
|
|
}
|
|
else
|
|
{
|
|
imageButton.Visible = true;
|
|
imageButton.SetIcon(_expanded ? arrowDown : arrowRight);
|
|
}
|
|
}
|
|
|
|
public bool ReserveIconSpace { get; set; } = true;
|
|
}
|
|
}
|
|
} |