using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Data; using System.Linq; using System.Text; using System.Windows.Forms; namespace Duplicati.Scheduler.Utility { /// /// A folder tree with 3-state checkboxes /// public partial class FolderTreeControl : UserControl { /// /// A file tree with 3-state checkboxes /// public FolderTreeControl() { InitializeComponent(); InitializeTree(); this.treeView.BackColor = this.BackColor; } /// /// Make the tree, only populate root nodes /// private void InitializeTree() { this.treeView.Nodes.Clear(); TreeNode Computer = UpdateNode(this.treeView.Nodes, "Computer", false); foreach (System.IO.DriveInfo Drive in System.IO.DriveInfo.GetDrives()) if (Drive.IsReady) UpdateNode(Computer.Nodes, Drive.RootDirectory.Name, false); Computer.ForeColor = this.ForeColor; } private bool InHere = false; // No re-entry /// /// Expanding, make the children nodes /// private void treeView_BeforeExpand(object sender, TreeViewCancelEventArgs e) { if (!InHere) { InHere = true; this.treeView.BeginUpdate(); Populate((string)e.Node.Tag, e.Node, e.Node.Checked && e.Node.ImageIndex != 1); this.treeView.Sort(); e.Node.Expand(); // Sort collapses all, so we gotta force expand, which is why we need that InHere thing this.treeView.EndUpdate(); InHere = false; } } public string[] itsSelectedFolders = new string[0]; /// /// List of selected folders /// public string[] SelectedFolders { get { return Selected(this.treeView.Nodes[0]); } } /// /// Sets the list as selected /// /// Folders to select public void SetSelectedFolders(string[] aFolderList) { itsSelectedFolders = aFolderList; AddSelections(); } /// /// Get list of selected nodes [recursive] /// /// Node to search /// List of selected nodes private string[] Selected(TreeNode aNode) { if (aNode.Checked && aNode.ImageIndex != 1) return new string[] { (string)aNode.Tag }; List Result = new List(); foreach (TreeNode Node in aNode.Nodes) Result.AddRange(Selected(Node)); return Result.ToArray(); } /// /// Returns true if node is selected /// /// Node /// true if node is selected private bool IsChecked(TreeNode aNode) { return aNode.Checked && aNode.ImageIndex == 2; } /// /// A handy look up /// private Dictionary NodesByPath = new Dictionary(); /// /// Gets a list of sub directories, returns exception if encountered /// /// Path to list /// null of OK, otherwise error /// List of subs private string[] GetDirectories(string aRoot, out Exception outError) { string[] Result = new string[0]; outError = Duplicati.Scheduler.Utility.Tools.TryCatch((Action)delegate() { Result = System.IO.Directory.GetDirectories(aRoot); }); return Result; } /// /// Fill a node with child nodes if any /// /// Path to fill /// Parent node /// Check the nodes or not private void Populate(string aRoot, TreeNode aParent, bool aChecked) { Exception Ignored; foreach (string Folder in GetDirectories(aRoot, out Ignored)) UpdateNode(aParent.Nodes, Folder, aChecked); } /// /// Gets the name of the node, special case for Recycle Bin /// /// Folder to get name /// Name private string NodeName(string aFolder) { string Result = System.IO.Path.GetFileName(aFolder); if (string.IsNullOrEmpty(Result)) Result = aFolder; return Result.Replace("$Recycle.Bin", " Recycle Bin"); } /// /// Add/Change a node /// /// Nodes for this one to join /// Folder to use /// Checked or not /// The new node private TreeNode UpdateNode(TreeNodeCollection aNodes, string aFolder, bool aChecked) { if (aNodes.Count > 0 && string.IsNullOrEmpty(aNodes[0].Text)) aNodes.RemoveAt(0); // Remove the fake TreeNode NewNode = null; if (NodesByPath.ContainsKey(aFolder)) NewNode = NodesByPath[aFolder]; // We already got one... else NewNode = aNodes[ aNodes.Add(new TreeNode(NodeName( aFolder )) { Tag = aFolder, Checked = aChecked, SelectedImageIndex = aChecked ? 2 : 0, ImageIndex = aChecked ? 2 : 0, })]; NodesByPath[aFolder] = NewNode; Exception HasErrs; bool HasSubs = GetDirectories(aFolder, out HasErrs).Length > 0; if (HasErrs != null) { NewNode.Tag = null; NewNode.ForeColor = Color.DarkGray; NewNode.ToolTipText = HasErrs.Message; } // Add a fake node so that the little '+' will show if (HasSubs) NewNode.Nodes.Add(string.Empty); return NewNode; } /// /// Breaks the path into its parts /// /// Path to parse /// Components of the path private string[] PathParts(string aPath) { List Result = new List(); for (string Part = System.IO.Path.GetDirectoryName(aPath); !string.IsNullOrEmpty(Part); Part = System.IO.Path.GetDirectoryName(Part) ) Result.Add(Part); Result.Reverse(); return Result.ToArray(); } /// /// This adds a list of nodes to the tree /// private void AddSelections() { foreach (string Entry in itsSelectedFolders) { TreeNode Parent = this.treeView.Nodes[0]; foreach (string Part in PathParts(Entry)) { Parent = UpdateNode(Parent.Nodes, Part, true); Parent.SelectedImageIndex = Parent.ImageIndex = 1; } UpdateNode(Parent.Nodes, Entry, true); } } /// /// This goes up the tree setting any partial checks /// /// Node to start private void Propagate(TreeNode aNode) { if (aNode.Parent == null) return; for (TreeNode Up = aNode.Parent; Up != null; Up = Up.Parent) { // Sets partial check if some of the nodes are not checked int CheckCount = (from TreeNode qN in Up.Nodes where qN.Checked select qN).Count(); Up.Checked = CheckCount != 0; Up.SelectedImageIndex = Up.ImageIndex = CheckCount == 0 ? 0 : (CheckCount == aNode.Parent.Nodes.Count ? 2 : 1); } } /// /// Set a node checked or not /// /// Node to set /// Set up-tree nodes private void CheckNodes(TreeNode aNode, bool aPropagate) { if (aPropagate) Propagate(aNode); SetChecked(aNode, aNode.Checked); } /// /// Sets a node and children checked or not [recursive] /// /// Node to set /// Checked or not private void SetChecked(TreeNode aNode, bool aValue) { aNode.SelectedImageIndex = aNode.ImageIndex = aValue ? 2 : 0; aNode.Checked = aValue; foreach (TreeNode Node in aNode.Nodes) SetChecked(Node, aValue); } private TreeNode itsContextNode; /// /// Pressed NODE - Determine if User has tried to check a node /// private void treeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) { itsContextNode = e.Node; if (e.Button != MouseButtons.Left) return; // See if clicked inside checked zone (36 pixels left of node bounds) Rectangle ExpandedBounds = Rectangle.FromLTRB(e.Node.Bounds.Left - 36, e.Node.Bounds.Top, e.Node.Bounds.Right, e.Node.Bounds.Bottom); if (ExpandedBounds.Contains(e.Location) && e.Node.Tag != null) { e.Node.Checked = !e.Node.Checked; CheckNodes(e.Node, true); } } /// /// Context menu allows explorer to open /// private void openToolStripMenuItem_Click(object sender, EventArgs e) { if (itsContextNode != null) System.Diagnostics.Process.Start((string)itsContextNode.Tag); } } }