// Copyright (C) Stichting Deltares 2016. All rights reserved.
//
// This file is part of Ringtoets.
//
// Ringtoets is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see .
//
// All names, logos, and references to "Deltares" are registered trademarks of
// Stichting Deltares and remain full property of Stichting Deltares at all times.
// All rights reserved.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;
using Core.Common.Base;
using Core.Common.Controls.Forms;
using Core.Common.Controls.TreeView.Properties;
using Core.Common.Utils.Events;
using BaseResources = Core.Common.Base.Properties.Resources;
namespace Core.Common.Controls.TreeView
{
///
/// A that encapulates a
/// in such a way that objects can be registered for configuring
/// the way tree nodes are drawn and can be interacted with. A general implementation is
/// provided when it comes to:
///
///
/// drawing tree nodes (including images, checkboxes and forecolors);
///
///
/// selecting tree nodes;
///
///
/// expanding/collapsing tree nodes;
///
///
/// renaming tree nodes (including feedback popups);
///
///
/// removing tree nodes (including feedback popups);
///
///
/// dragging and dropping tree nodes (including visual feedback);
///
///
/// opening tree node context menus;
///
///
/// short keys for tree node interaction via the keyboard.
///
///
///
///
/// The current implementation assumes that the data hierarchy,
/// defined by the combination of registered objects and the provided
/// , only contains uniquely identifiable data objects. Additionally, only one
/// object can be registered per .
///
public partial class TreeViewControl : UserControl
{
public event EventHandler DataDoubleClick;
public event EventHandler SelectedDataChanged;
public event EventHandler> DataDeleted; // TODO; Way to explicit!
private const int maximumTextLength = 259;
private const string stateImageLocationString = "StateImage";
private const int uncheckedCheckBoxStateImageIndex = 0;
private const int checkedCheckBoxStateImageIndex = 1;
private readonly DragDropHandler dragDropHandler = new DragDropHandler();
private readonly Dictionary tagTypeTreeNodeInfoLookup = new Dictionary();
private readonly Dictionary treeNodeObserverLookup = new Dictionary();
private object data;
///
/// Creates a new instance of .
///
public TreeViewControl()
{
InitializeComponent();
treeView.ImageList = new ImageList
{
ColorDepth = ColorDepth.Depth32Bit
};
treeView.StateImageList = new ImageList();
treeView.StateImageList.Images.Add(CreateCheckBoxGlyph(CheckBoxState.UncheckedNormal));
treeView.StateImageList.Images.Add(CreateCheckBoxGlyph(CheckBoxState.CheckedNormal));
treeView.DrawMode = TreeViewDrawMode.Normal;
treeView.AllowDrop = true;
treeView.LabelEdit = true;
treeView.HideSelection = false;
treeView.BeforeLabelEdit += TreeViewBeforeLabelEdit;
treeView.AfterLabelEdit += TreeViewAfterLabelEdit;
treeView.AfterCheck += TreeViewAfterCheck;
treeView.KeyDown += TreeViewKeyDown;
treeView.MouseClick += TreeViewMouseClick;
treeView.DoubleClick += TreeViewDoubleClick;
treeView.DragDrop += TreeViewDragDrop;
treeView.DragOver += TreeViewDragOver;
treeView.ItemDrag += TreeViewItemDrag;
treeView.DragLeave += TreeViewDragLeave;
treeView.AfterSelect += TreeViewAfterSelect;
}
///
/// Gets or sets the data to show in the .
///
///
/// Ensure all necessary objects are registered in order to prevent
/// an . Take notice of the fact that these kind of
/// exceptions might not directly occur after setting the data.
///
public object Data
{
get
{
return data;
}
set
{
RemoveAllNodes();
data = value;
if (data == null)
{
return;
}
AddRootNode();
treeView.SelectedNode = treeView.Nodes.Count > 0 ? treeView.Nodes[0] : null;
}
}
///
/// Gets the data of the selected tree node in the .
///
/// null is returned when no tree node is selected.
public object SelectedData
{
get
{
return treeView.SelectedNode != null ? treeView.SelectedNode.Tag : null;
}
}
///
/// This method registers a object.
///
/// The to register.
/// Only one object can be registered per .
public void RegisterTreeNodeInfo(TreeNodeInfo treeNodeInfo)
{
tagTypeTreeNodeInfoLookup[treeNodeInfo.TagType] = treeNodeInfo;
}
///
/// This method returns whether or not the tree node corresponding to the
/// can be renamed.
///
/// The data object to obtain the corresponding tree node for.
/// Whether or not the tree node can be renamed or false when no corresponding tree node is found.
public bool CanRenameNodeForData(object dataObject)
{
var treeNode = GetNodeByTag(dataObject);
return treeNode != null && CanRename(treeNode);
}
///
/// This method tries to start a rename action for the tree node corresponding to the
/// .
///
/// The data object to obtain the corresponding tree node for.
///
/// When a tree node is found that cannot be renamed, a popup is shown for notifying the end user.
/// The renaming logic will be skipped when no corresponding tree node is found.
///
public void TryRenameNodeForData(object dataObject)
{
var treeNode = GetNodeByTag(dataObject);
if (treeNode != null)
{
Rename(treeNode);
}
}
///
/// This method returns whether or not the tree node corresponding to the
/// can be removed.
///
/// The data object to obtain the corresponding tree node for.
/// Whether or not the tree node can be removed or false when no corresponding tree node is found.
public bool CanRemoveNodeForData(object dataObject)
{
var treeNode = GetNodeByTag(dataObject);
return treeNode != null && CanRemove(treeNode);
}
///
/// This method tries to remove the tree node corresponding to the .
///
/// The data object to obtain the corresponding tree node for.
///
/// When a tree node is found that can be removed, a popup is shown for confirmation by the end user.
/// When a tree node is found that cannot be removed, a popup is shown for notifying the end user.
/// The removing logic will be skipped when no corresponding tree node is found.
///
public void TryRemoveNodeForData(object dataObject)
{
var treeNode = GetNodeByTag(dataObject);
if (treeNode != null)
{
Remove(treeNode);
}
}
///
/// This method returns whether or not the tree node corresponding to the
/// can be collapsed/expanded.
///
/// The data object to obtain the corresponding tree node for.
/// Whether or not the tree node can be collapsed/expanded or false when no corresponding tree node is found.
public bool CanExpandOrCollapseForData(object dataObject)
{
var treeNode = GetNodeByTag(dataObject);
return treeNode != null && treeNode.Nodes.Count > 0;
}
///
/// This method tries to collapse all nodes of the tree node corresponding to the
/// (child nodes are taken into account recursively).
///
///
/// The collapsing logic will be skipped when no corresponding tree node is found.
///
public void TryCollapseAllNodesForData(object dataObject)
{
var treeNode = GetNodeByTag(dataObject);
if (treeNode != null)
{
CollapseAll(treeNode);
}
}
///
/// This method tries to expand all nodes of the tree node corresponding to the
/// (child nodes are taken into account recursively).
///
///
/// The expanding logic will be skipped when no corresponding tree node is found.
///
public void TryExpandAllNodesForData(object dataObject)
{
var treeNode = GetNodeByTag(dataObject);
if (treeNode != null)
{
ExpandAll(treeNode);
}
}
///
/// This method tries to select the tree node corresponding to the .
///
/// The data object to obtain the corresponding tree node for.
///
/// The tree node selection is set to null when no corresponding tree node is found.
///
public void TrySelectNodeForData(object dataObject)
{
treeView.SelectedNode = GetNodeByTag(dataObject);
}
///
/// This method tries to return the path of the tree node corresponding to the .
///
/// The data object to obtain the corresponding tree node for.
/// The path of the tree node or null when no corresponding tree node is found.
public string TryGetPathForData(object dataObject)
{
var treeNode = GetNodeByTag(dataObject);
return treeNode != null ? treeNode.FullPath : null;
}
private bool CanRename(TreeNode treeNode)
{
var treeNodeInfo = TryGetTreeNodeInfoForData(treeNode.Tag);
var parentTag = GetParentTag(treeNode);
return treeNodeInfo.CanRename != null && treeNodeInfo.CanRename(treeNode.Tag, parentTag);
}
private void Rename(TreeNode treeNode)
{
if (!CanRename(treeNode))
{
MessageBox.Show(Resources.TreeViewControl_The_selected_item_cannot_be_renamed, BaseResources.Confirm, MessageBoxButtons.OK);
return;
}
treeNode.BeginEdit();
}
private bool CanRemove(TreeNode treeNode)
{
var treeNodeInfo = TryGetTreeNodeInfoForData(treeNode.Tag);
var parentTag = GetParentTag(treeNode);
return treeNodeInfo.CanRemove != null && treeNodeInfo.CanRemove(treeNode.Tag, parentTag);
}
private void Remove(TreeNode treeNode)
{
if (!CanRemove(treeNode))
{
MessageBox.Show(Resources.TreeViewControl_The_selected_item_cannot_be_removed, BaseResources.Confirm, MessageBoxButtons.OK);
return;
}
var message = string.Format(Resources.TreeViewControl_Are_you_sure_you_want_to_remove_the_selected_item);
if (MessageBox.Show(message, BaseResources.Confirm, MessageBoxButtons.OKCancel) != DialogResult.OK)
{
return;
}
var treeNodeInfo = TryGetTreeNodeInfoForData(treeNode.Tag);
if (treeNodeInfo.OnNodeRemoved != null)
{
var parentTag = GetParentTag(treeNode);
treeNodeInfo.OnNodeRemoved(treeNode.Tag, parentTag);
}
OnNodeDataDeleted(treeNode);
}
private static void CollapseAll(TreeNode treeNode)
{
treeNode.Collapse();
foreach (TreeNode childNode in treeNode.Nodes)
{
CollapseAll(childNode);
}
}
private static void ExpandAll(TreeNode treeNode)
{
treeNode.Expand();
foreach (TreeNode childNode in treeNode.Nodes)
{
ExpandAll(childNode);
}
}
///
/// This method searches all nodes in the tree view for a node with a matching tag.
///
/// The node data to search the corresponding for.
/// The corresponding the provided node data or null if not found.
private TreeNode GetNodeByTag(object nodeData)
{
return treeView.Nodes.Count > 0 ? GetNodeByTag(treeView.Nodes[0], nodeData) : null;
}
private static TreeNode GetNodeByTag(TreeNode rootNode, object tag)
{
if (Equals(rootNode.Tag, tag))
{
return rootNode;
}
return rootNode.Nodes
.Cast()
.Select(n => GetNodeByTag(n, tag))
.FirstOrDefault(node => node != null);
}
private void AddRootNode()
{
var rootNode = new TreeNode
{
Tag = data
};
UpdateNode(rootNode);
if (rootNode.Nodes.Count > 0)
{
rootNode.Expand();
}
treeView.Nodes.Add(rootNode);
treeNodeObserverLookup.Add(rootNode, new TreeNodeObserver(rootNode, this));
}
private TreeNode CreateTreeNode(TreeNode parentNode, object nodeData)
{
var newNode = new TreeNode
{
Tag = nodeData
};
if (treeView.CheckBoxes)
{
newNode.Checked = parentNode.Checked;
}
UpdateNode(newNode);
treeNodeObserverLookup.Add(newNode, new TreeNodeObserver(newNode, this));
return newNode;
}
///
/// This method updates the provided .
///
/// The to update.
/// Thrown when no corresponding can be found for the provided .
private void UpdateNode(TreeNode treeNode)
{
var treeNodeInfo = TryGetTreeNodeInfoForData(treeNode.Tag);
if (treeNodeInfo == null)
{
throw new InvalidOperationException("No tree node info registered");
}
// First of all refresh the child nodes as the other logic below might depend on the presence of child nodes
RefreshChildNodes(treeNode, treeNodeInfo);
if (treeNodeInfo.Text != null)
{
var text = treeNodeInfo.Text(treeNode.Tag);
// Having very big strings causes rendering problems in the tree view
treeNode.Text = text.Length > maximumTextLength
? text.Substring(0, maximumTextLength)
: text;
}
else
{
treeNode.Text = "";
}
treeNode.ForeColor = treeNodeInfo.ForeColor != null
? treeNodeInfo.ForeColor(treeNode.Tag)
: Color.FromKnownColor(KnownColor.ControlText);
SetTreeNodeImageKey(treeNode, treeNodeInfo);
if (treeNodeInfo.CanCheck != null && treeNodeInfo.CanCheck(treeNode.Tag)
&& treeNodeInfo.IsChecked != null)
{
if (treeNode.Checked != treeNodeInfo.IsChecked(treeNode.Tag))
{
treeView.AfterCheck -= TreeViewAfterCheck;
treeNode.Checked = !treeNode.Checked;
treeView.AfterCheck += TreeViewAfterCheck;
}
treeNode.StateImageIndex = treeNode.Checked ? checkedCheckBoxStateImageIndex : uncheckedCheckBoxStateImageIndex;
}
}
private void RefreshChildNodes(TreeNode treeNode, TreeNodeInfo treeNodeInfo)
{
var newTreeNodes = new Dictionary();
List outdatedTreeNodes = treeNode.Nodes.Cast().ToList();
Dictionary