You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
728 lines
16 KiB
728 lines
16 KiB
// Copyright (c) 2020 AlphaSierraPapa for the SharpDevelop Team
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
// software and associated documentation files (the "Software"), to deal in the Software
|
|
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|
// to whom the Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all copies or
|
|
// substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
|
|
namespace ICSharpCode.TreeView
|
|
{
|
|
public partial class SharpTreeNode : INotifyPropertyChanged
|
|
{
|
|
SharpTreeNodeCollection modelChildren;
|
|
internal SharpTreeNode modelParent;
|
|
bool isVisible = true;
|
|
|
|
void UpdateIsVisible(bool parentIsVisible, bool updateFlattener)
|
|
{
|
|
bool newIsVisible = parentIsVisible && !isHidden;
|
|
if (isVisible != newIsVisible)
|
|
{
|
|
isVisible = newIsVisible;
|
|
|
|
// invalidate the augmented data
|
|
SharpTreeNode node = this;
|
|
while (node != null && node.totalListLength >= 0)
|
|
{
|
|
node.totalListLength = -1;
|
|
node = node.listParent;
|
|
}
|
|
// Remember the removed nodes:
|
|
List<SharpTreeNode> removedNodes = null;
|
|
if (updateFlattener && !newIsVisible)
|
|
{
|
|
removedNodes = VisibleDescendantsAndSelf().ToList();
|
|
}
|
|
// also update the model children:
|
|
UpdateChildIsVisible(false);
|
|
|
|
// Validate our invariants:
|
|
if (updateFlattener)
|
|
CheckRootInvariants();
|
|
|
|
// Tell the flattener about the removed nodes:
|
|
if (removedNodes != null)
|
|
{
|
|
var flattener = GetListRoot().treeFlattener;
|
|
if (flattener != null)
|
|
{
|
|
flattener.NodesRemoved(GetVisibleIndexForNode(this), removedNodes);
|
|
foreach (var n in removedNodes)
|
|
n.OnIsVisibleChanged();
|
|
}
|
|
}
|
|
// Tell the flattener about the new nodes:
|
|
if (updateFlattener && newIsVisible)
|
|
{
|
|
var flattener = GetListRoot().treeFlattener;
|
|
if (flattener != null)
|
|
{
|
|
flattener.NodesInserted(GetVisibleIndexForNode(this), VisibleDescendantsAndSelf());
|
|
foreach (var n in VisibleDescendantsAndSelf())
|
|
n.OnIsVisibleChanged();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual void OnIsVisibleChanged() { }
|
|
|
|
void UpdateChildIsVisible(bool updateFlattener)
|
|
{
|
|
if (modelChildren != null && modelChildren.Count > 0)
|
|
{
|
|
bool showChildren = isVisible && isExpanded;
|
|
foreach (SharpTreeNode child in modelChildren)
|
|
{
|
|
child.UpdateIsVisible(showChildren, updateFlattener);
|
|
}
|
|
}
|
|
}
|
|
|
|
#region Main
|
|
|
|
public SharpTreeNode()
|
|
{
|
|
}
|
|
|
|
public SharpTreeNodeCollection Children {
|
|
get {
|
|
if (modelChildren == null)
|
|
modelChildren = new SharpTreeNodeCollection(this);
|
|
return modelChildren;
|
|
}
|
|
}
|
|
|
|
public SharpTreeNode Parent {
|
|
get { return modelParent; }
|
|
}
|
|
|
|
public virtual object Text {
|
|
get { return null; }
|
|
}
|
|
|
|
public virtual Brush Foreground {
|
|
get { return SystemColors.WindowTextBrush; }
|
|
}
|
|
|
|
public virtual object Icon {
|
|
get { return null; }
|
|
}
|
|
|
|
public virtual object ToolTip {
|
|
get { return null; }
|
|
}
|
|
|
|
public int Level {
|
|
get { return Parent != null ? Parent.Level + 1 : 0; }
|
|
}
|
|
|
|
public bool IsRoot {
|
|
get { return Parent == null; }
|
|
}
|
|
|
|
bool isHidden;
|
|
|
|
public bool IsHidden {
|
|
get { return isHidden; }
|
|
set {
|
|
if (isHidden != value)
|
|
{
|
|
isHidden = value;
|
|
if (modelParent != null)
|
|
UpdateIsVisible(modelParent.isVisible && modelParent.isExpanded, true);
|
|
RaisePropertyChanged(nameof(IsHidden));
|
|
if (Parent != null)
|
|
Parent.RaisePropertyChanged(nameof(ShowExpander));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true when this node is not hidden and when all parent nodes are expanded and not hidden.
|
|
/// </summary>
|
|
public bool IsVisible {
|
|
get { return isVisible; }
|
|
}
|
|
|
|
bool isSelected;
|
|
|
|
public bool IsSelected {
|
|
get { return isSelected; }
|
|
set {
|
|
if (isSelected != value)
|
|
{
|
|
isSelected = value;
|
|
RaisePropertyChanged(nameof(IsSelected));
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region OnChildrenChanged
|
|
internal protected virtual void OnChildrenChanged(NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (e.OldItems != null)
|
|
{
|
|
foreach (SharpTreeNode node in e.OldItems)
|
|
{
|
|
Debug.Assert(node.modelParent == this);
|
|
node.modelParent = null;
|
|
Debug.WriteLine("Removing {0} from {1}", node, this);
|
|
SharpTreeNode removeEnd = node;
|
|
while (removeEnd.modelChildren != null && removeEnd.modelChildren.Count > 0)
|
|
removeEnd = removeEnd.modelChildren.Last();
|
|
|
|
List<SharpTreeNode> removedNodes = null;
|
|
int visibleIndexOfRemoval = 0;
|
|
if (node.isVisible)
|
|
{
|
|
visibleIndexOfRemoval = GetVisibleIndexForNode(node);
|
|
removedNodes = node.VisibleDescendantsAndSelf().ToList();
|
|
}
|
|
|
|
RemoveNodes(node, removeEnd);
|
|
|
|
if (removedNodes != null)
|
|
{
|
|
var flattener = GetListRoot().treeFlattener;
|
|
if (flattener != null)
|
|
{
|
|
flattener.NodesRemoved(visibleIndexOfRemoval, removedNodes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (e.NewItems != null)
|
|
{
|
|
SharpTreeNode insertionPos;
|
|
if (e.NewStartingIndex == 0)
|
|
insertionPos = null;
|
|
else
|
|
insertionPos = modelChildren[e.NewStartingIndex - 1];
|
|
|
|
foreach (SharpTreeNode node in e.NewItems)
|
|
{
|
|
Debug.Assert(node.modelParent == null);
|
|
node.modelParent = this;
|
|
node.UpdateIsVisible(isVisible && isExpanded, false);
|
|
//Debug.WriteLine("Inserting {0} after {1}", node, insertionPos);
|
|
|
|
while (insertionPos != null && insertionPos.modelChildren != null && insertionPos.modelChildren.Count > 0)
|
|
{
|
|
insertionPos = insertionPos.modelChildren.Last();
|
|
}
|
|
InsertNodeAfter(insertionPos ?? this, node);
|
|
|
|
insertionPos = node;
|
|
if (node.isVisible)
|
|
{
|
|
var flattener = GetListRoot().treeFlattener;
|
|
if (flattener != null)
|
|
{
|
|
flattener.NodesInserted(GetVisibleIndexForNode(node), node.VisibleDescendantsAndSelf());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RaisePropertyChanged(nameof(ShowExpander));
|
|
RaiseIsLastChangedIfNeeded(e);
|
|
}
|
|
#endregion
|
|
|
|
#region Expanding / LazyLoading
|
|
|
|
public virtual object ExpandedIcon {
|
|
get { return Icon; }
|
|
}
|
|
|
|
public virtual bool ShowExpander {
|
|
get { return LazyLoading || Children.Any(c => !c.isHidden); }
|
|
}
|
|
|
|
bool isExpanded;
|
|
|
|
public bool IsExpanded {
|
|
get { return isExpanded; }
|
|
set {
|
|
if (isExpanded != value)
|
|
{
|
|
isExpanded = value;
|
|
if (isExpanded)
|
|
{
|
|
EnsureLazyChildren();
|
|
OnExpanding();
|
|
}
|
|
else
|
|
{
|
|
OnCollapsing();
|
|
}
|
|
UpdateChildIsVisible(true);
|
|
RaisePropertyChanged(nameof(IsExpanded));
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual void OnExpanding() { }
|
|
protected virtual void OnCollapsing() { }
|
|
|
|
bool lazyLoading;
|
|
|
|
public bool LazyLoading {
|
|
get { return lazyLoading; }
|
|
set {
|
|
lazyLoading = value;
|
|
if (lazyLoading)
|
|
{
|
|
IsExpanded = false;
|
|
if (canExpandRecursively)
|
|
{
|
|
canExpandRecursively = false;
|
|
RaisePropertyChanged(nameof(CanExpandRecursively));
|
|
}
|
|
}
|
|
RaisePropertyChanged(nameof(LazyLoading));
|
|
RaisePropertyChanged(nameof(ShowExpander));
|
|
}
|
|
}
|
|
|
|
bool canExpandRecursively = true;
|
|
|
|
/// <summary>
|
|
/// Gets whether this node can be expanded recursively.
|
|
/// If not overridden, this property returns false if the node is using lazy-loading, and true otherwise.
|
|
/// </summary>
|
|
public virtual bool CanExpandRecursively {
|
|
get { return canExpandRecursively; }
|
|
}
|
|
|
|
public virtual bool ShowIcon {
|
|
get { return Icon != null; }
|
|
}
|
|
|
|
protected virtual void LoadChildren()
|
|
{
|
|
throw new NotSupportedException(GetType().Name + " does not support lazy loading");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the children were initialized (loads children if lazy loading is enabled)
|
|
/// </summary>
|
|
public void EnsureLazyChildren()
|
|
{
|
|
if (LazyLoading)
|
|
{
|
|
LazyLoading = false;
|
|
LoadChildren();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Ancestors / Descendants
|
|
|
|
public IEnumerable<SharpTreeNode> Descendants()
|
|
{
|
|
return TreeTraversal.PreOrder(this.Children, n => n.Children);
|
|
}
|
|
|
|
public IEnumerable<SharpTreeNode> DescendantsAndSelf()
|
|
{
|
|
return TreeTraversal.PreOrder(this, n => n.Children);
|
|
}
|
|
|
|
internal IEnumerable<SharpTreeNode> VisibleDescendants()
|
|
{
|
|
return TreeTraversal.PreOrder(this.Children.Where(c => c.isVisible), n => n.Children.Where(c => c.isVisible));
|
|
}
|
|
|
|
internal IEnumerable<SharpTreeNode> VisibleDescendantsAndSelf()
|
|
{
|
|
return TreeTraversal.PreOrder(this, n => n.Children.Where(c => c.isVisible));
|
|
}
|
|
|
|
public IEnumerable<SharpTreeNode> Ancestors()
|
|
{
|
|
for (SharpTreeNode n = this.Parent; n != null; n = n.Parent)
|
|
yield return n;
|
|
}
|
|
|
|
public IEnumerable<SharpTreeNode> AncestorsAndSelf()
|
|
{
|
|
for (SharpTreeNode n = this; n != null; n = n.Parent)
|
|
yield return n;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Editing
|
|
|
|
public virtual bool IsEditable {
|
|
get { return false; }
|
|
}
|
|
|
|
bool isEditing;
|
|
|
|
public bool IsEditing {
|
|
get { return isEditing; }
|
|
set {
|
|
if (isEditing != value)
|
|
{
|
|
isEditing = value;
|
|
RaisePropertyChanged(nameof(IsEditing));
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual string LoadEditText()
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public virtual bool SaveEditText(string value)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Checkboxes
|
|
|
|
public virtual bool IsCheckable {
|
|
get { return false; }
|
|
}
|
|
|
|
bool? isChecked;
|
|
|
|
public bool? IsChecked {
|
|
get { return isChecked; }
|
|
set {
|
|
SetIsChecked(value, true);
|
|
}
|
|
}
|
|
|
|
void SetIsChecked(bool? value, bool update)
|
|
{
|
|
if (isChecked != value)
|
|
{
|
|
isChecked = value;
|
|
|
|
if (update)
|
|
{
|
|
if (IsChecked != null)
|
|
{
|
|
foreach (var child in Descendants())
|
|
{
|
|
if (child.IsCheckable)
|
|
{
|
|
child.SetIsChecked(IsChecked, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var parent in Ancestors())
|
|
{
|
|
if (parent.IsCheckable)
|
|
{
|
|
if (!parent.TryValueForIsChecked(true))
|
|
{
|
|
if (!parent.TryValueForIsChecked(false))
|
|
{
|
|
parent.SetIsChecked(null, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RaisePropertyChanged(nameof(IsChecked));
|
|
}
|
|
}
|
|
|
|
bool TryValueForIsChecked(bool? value)
|
|
{
|
|
if (Children.Where(n => n.IsCheckable).All(n => n.IsChecked == value))
|
|
{
|
|
SetIsChecked(value, false);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cut / Copy / Paste / Delete
|
|
|
|
public bool IsCut { get { return false; } }
|
|
/*
|
|
static List<SharpTreeNode> cuttedNodes = new List<SharpTreeNode>();
|
|
static IDataObject cuttedData;
|
|
static EventHandler requerySuggestedHandler; // for weak event
|
|
|
|
static void StartCuttedDataWatcher()
|
|
{
|
|
requerySuggestedHandler = new EventHandler(CommandManager_RequerySuggested);
|
|
CommandManager.RequerySuggested += requerySuggestedHandler;
|
|
}
|
|
|
|
static void CommandManager_RequerySuggested(object sender, EventArgs e)
|
|
{
|
|
if (cuttedData != null && !Clipboard.IsCurrent(cuttedData)) {
|
|
ClearCuttedData();
|
|
}
|
|
}
|
|
|
|
static void ClearCuttedData()
|
|
{
|
|
foreach (var node in cuttedNodes) {
|
|
node.IsCut = false;
|
|
}
|
|
cuttedNodes.Clear();
|
|
cuttedData = null;
|
|
}
|
|
|
|
//static public IEnumerable<SharpTreeNode> PurifyNodes(IEnumerable<SharpTreeNode> nodes)
|
|
//{
|
|
// var list = nodes.ToList();
|
|
// var array = list.ToArray();
|
|
// foreach (var node1 in array) {
|
|
// foreach (var node2 in array) {
|
|
// if (node1.Descendants().Contains(node2)) {
|
|
// list.Remove(node2);
|
|
// }
|
|
// }
|
|
// }
|
|
// return list;
|
|
//}
|
|
|
|
bool isCut;
|
|
|
|
public bool IsCut
|
|
{
|
|
get { return isCut; }
|
|
private set
|
|
{
|
|
isCut = value;
|
|
RaisePropertyChanged("IsCut");
|
|
}
|
|
}
|
|
|
|
internal bool InternalCanCut()
|
|
{
|
|
return InternalCanCopy() && InternalCanDelete();
|
|
}
|
|
|
|
internal void InternalCut()
|
|
{
|
|
ClearCuttedData();
|
|
cuttedData = Copy(ActiveNodesArray);
|
|
Clipboard.SetDataObject(cuttedData);
|
|
|
|
foreach (var node in ActiveNodes) {
|
|
node.IsCut = true;
|
|
cuttedNodes.Add(node);
|
|
}
|
|
}
|
|
|
|
internal bool InternalCanCopy()
|
|
{
|
|
return CanCopy(ActiveNodesArray);
|
|
}
|
|
|
|
internal void InternalCopy()
|
|
{
|
|
Clipboard.SetDataObject(Copy(ActiveNodesArray));
|
|
}
|
|
|
|
internal bool InternalCanPaste()
|
|
{
|
|
return CanPaste(Clipboard.GetDataObject());
|
|
}
|
|
|
|
internal void InternalPaste()
|
|
{
|
|
Paste(Clipboard.GetDataObject());
|
|
|
|
if (cuttedData != null) {
|
|
DeleteCore(cuttedNodes.ToArray());
|
|
ClearCuttedData();
|
|
}
|
|
}
|
|
*/
|
|
|
|
public virtual bool CanDelete()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public virtual void Delete()
|
|
{
|
|
throw new NotSupportedException(GetType().Name + " does not support deletion");
|
|
}
|
|
|
|
public virtual void DeleteCore()
|
|
{
|
|
throw new NotSupportedException(GetType().Name + " does not support deletion");
|
|
}
|
|
|
|
public virtual IDataObject Copy(SharpTreeNode[] nodes)
|
|
{
|
|
throw new NotSupportedException(GetType().Name + " does not support copy/paste or drag'n'drop");
|
|
}
|
|
|
|
/*
|
|
public virtual bool CanCopy(SharpTreeNode[] nodes)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public virtual bool CanPaste(IDataObject data)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public virtual void Paste(IDataObject data)
|
|
{
|
|
EnsureLazyChildren();
|
|
Drop(data, Children.Count, DropEffect.Copy);
|
|
}
|
|
*/
|
|
#endregion
|
|
|
|
#region Drag and Drop
|
|
public virtual bool CanDrag(SharpTreeNode[] nodes)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public virtual void StartDrag(DependencyObject dragSource, SharpTreeNode[] nodes)
|
|
{
|
|
DragDropEffects effects = DragDropEffects.All;
|
|
if (!nodes.All(n => n.CanDelete()))
|
|
effects &= ~DragDropEffects.Move;
|
|
DragDropEffects result = DragDrop.DoDragDrop(dragSource, Copy(nodes), effects);
|
|
if (result == DragDropEffects.Move)
|
|
{
|
|
foreach (SharpTreeNode node in nodes)
|
|
node.DeleteCore();
|
|
}
|
|
}
|
|
|
|
public virtual bool CanDrop(DragEventArgs e, int index)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
internal void InternalDrop(DragEventArgs e, int index)
|
|
{
|
|
if (LazyLoading)
|
|
{
|
|
EnsureLazyChildren();
|
|
index = Children.Count;
|
|
}
|
|
|
|
Drop(e, index);
|
|
}
|
|
|
|
public virtual void Drop(DragEventArgs e, int index)
|
|
{
|
|
throw new NotSupportedException(GetType().Name + " does not support Drop()");
|
|
}
|
|
#endregion
|
|
|
|
#region IsLast (for TreeView lines)
|
|
|
|
public bool IsLast {
|
|
get {
|
|
return Parent == null ||
|
|
Parent.Children[Parent.Children.Count - 1] == this;
|
|
}
|
|
}
|
|
|
|
void RaiseIsLastChangedIfNeeded(NotifyCollectionChangedEventArgs e)
|
|
{
|
|
switch (e.Action)
|
|
{
|
|
case NotifyCollectionChangedAction.Add:
|
|
if (e.NewStartingIndex == Children.Count - 1)
|
|
{
|
|
if (Children.Count > 1)
|
|
{
|
|
Children[Children.Count - 2].RaisePropertyChanged(nameof(IsLast));
|
|
}
|
|
Children[Children.Count - 1].RaisePropertyChanged(nameof(IsLast));
|
|
}
|
|
break;
|
|
case NotifyCollectionChangedAction.Remove:
|
|
if (e.OldStartingIndex == Children.Count)
|
|
{
|
|
if (Children.Count > 0)
|
|
{
|
|
Children[Children.Count - 1].RaisePropertyChanged(nameof(IsLast));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region INotifyPropertyChanged Members
|
|
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
|
|
public void RaisePropertyChanged(string name)
|
|
{
|
|
if (PropertyChanged != null)
|
|
{
|
|
PropertyChanged(this, new PropertyChangedEventArgs(name));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Gets called when the item is double-clicked.
|
|
/// </summary>
|
|
public virtual void ActivateItem(RoutedEventArgs e)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets called when the item is clicked with the middle mouse button.
|
|
/// </summary>
|
|
public virtual void ActivateItemSecondary(RoutedEventArgs e)
|
|
{
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
// used for keyboard navigation
|
|
object text = this.Text;
|
|
return text != null ? text.ToString() : string.Empty;
|
|
}
|
|
}
|
|
}
|