diff --git a/src/GitHub.Api/UI/TreeBase.cs b/src/GitHub.Api/UI/TreeBase.cs index 915abe89f..4804558ce 100644 --- a/src/GitHub.Api/UI/TreeBase.cs +++ b/src/GitHub.Api/UI/TreeBase.cs @@ -183,23 +183,9 @@ protected bool PromoteNode(TNode previouslyAddedNode, string nextLabel) public void SetCheckStateOnAll(bool isChecked) { - var nodeCheckState = isChecked ? CheckState.Checked : CheckState.Empty; foreach (var node in Nodes) { - var wasChecked = node.CheckState == CheckState.Checked; - node.CheckState = nodeCheckState; - - if (!node.IsFolder) - { - if (isChecked && !wasChecked) - { - AddCheckedNode(node); - } - else if (!isChecked && wasChecked) - { - RemoveCheckedNode(node); - } - } + SetCheckStateOnNode(node, isChecked); } } @@ -250,39 +236,32 @@ protected void ToggleNodeVisibility(int idx, TNode node) protected void ToggleNodeChecked(int idx, TNode node) { + CheckState checkState; var isChecked = false; - switch (node.CheckState) { case CheckState.Mixed: case CheckState.Empty: - node.CheckState = CheckState.Checked; + checkState = CheckState.Checked; isChecked = true; break; case CheckState.Checked: - node.CheckState = CheckState.Empty; + checkState = CheckState.Empty; break; - } - if (!node.IsFolder) - { - if (isChecked) - { - AddCheckedNode(node); - } - else - { - RemoveCheckedNode(node); - } + default: + throw new ArgumentOutOfRangeException("Unknown CheckState"); } + SetCheckStateOnNode(node, checkState); + if (node.IsFolderOrContainer) { ToggleChildrenChecked(idx, node, isChecked); } - ToggleParentFoldersChecked(idx, node, isChecked); + ToggleParentFolderAndContainersChecked(idx, node, checkState); } private void ToggleChildrenChecked(int idx, TNode node, bool isChecked) @@ -290,20 +269,8 @@ private void ToggleChildrenChecked(int idx, TNode node, bool isChecked) for (var i = idx + 1; i < Nodes.Count && node.Level < Nodes[i].Level; i++) { var childNode = Nodes[i]; - var wasChecked = childNode.CheckState == CheckState.Checked; - childNode.CheckState = isChecked ? CheckState.Checked : CheckState.Empty; - if (!childNode.IsFolder) - { - if (isChecked && !wasChecked) - { - AddCheckedNode(childNode); - } - else if (!isChecked && wasChecked) - { - RemoveCheckedNode(childNode); - } - } + SetCheckStateOnNode(childNode, isChecked); if (childNode.IsFolderOrContainer) { @@ -332,9 +299,36 @@ private List GetLeafNodes(TNode node, int idx) return results; } + private void SetCheckStateOnNode(TNode node, bool setChecked) + { + SetCheckStateOnNode(node, setChecked ? CheckState.Checked : CheckState.Empty); + } + + private void SetCheckStateOnNode(TNode node, CheckState setCheckState) + { + var isChecked = setCheckState == CheckState.Checked + || setCheckState == CheckState.Mixed; + + var wasChecked = node.CheckState == CheckState.Checked; + + node.CheckState = setCheckState; + + if (!node.IsFolder) + { + if (isChecked && !wasChecked) + { + AddCheckedNode(node); + } + else if (!isChecked && wasChecked) + { + RemoveCheckedNode(node); + } + } + } - private void ToggleParentFoldersChecked(int idx, TNode node, bool isChecked) + private void ToggleParentFolderAndContainersChecked(int idx, TNode node, CheckState checkState) { + var isChecked = checkState != CheckState.Empty; while (true) { if (node.Level > 0) @@ -384,14 +378,13 @@ private void ToggleParentFoldersChecked(int idx, TNode node, bool isChecked) var parentIndex = firstSiblingIndex - 1; var parentNode = Nodes[parentIndex]; - if (siblingsInSameState) - { - parentNode.CheckState = isChecked ? CheckState.Checked : CheckState.Empty; - } - else - { - parentNode.CheckState = CheckState.Mixed; - } + + var parentNodeState = + siblingsInSameState + ? node.CheckState + : CheckState.Mixed; + + SetCheckStateOnNode(parentNode, parentNodeState); idx = parentIndex; node = parentNode; diff --git a/src/tests/UnitTests/UI/TreeBaseTests.cs b/src/tests/UnitTests/UI/TreeBaseTests.cs index ec0c03467..e2e5e07ed 100644 --- a/src/tests/UnitTests/UI/TreeBaseTests.cs +++ b/src/tests/UnitTests/UI/TreeBaseTests.cs @@ -185,6 +185,11 @@ public override TestTreeNode SelectedNode } } + public new void ToggleNodeChecked(int idx, TestTreeNode node) + { + base.ToggleNodeChecked(idx, node); + } + protected override List Nodes { get @@ -606,6 +611,211 @@ public void ShouldPopulateTreeWithSingleEntryWithMetaInPath() } }); } + + [Test] + public void ShouldCheckParentOfMetaFile() + { + var testTree = new TestTree(true); + var testTreeListener = testTree.TestTreeListener; + + testTreeListener.GetCollapsedFolders().Returns(new string[0]); + testTreeListener.SelectedNode.Returns((TestTreeNode)null); + testTreeListener.GetCheckedFiles().Returns(new string[0]); + testTreeListener.Nodes.Returns(new List()); + testTreeListener.PathSeparator.Returns(@"\"); + testTreeListener.DisplayRootNode.Returns(true); + testTreeListener.IsSelectable.Returns(false); + testTreeListener.Title.Returns("Test Tree"); + testTreeListener.PromoteMetaFiles.Returns(true); + + var testTreeData = new[] { + new TestTreeData { + Path = "Folder\\Default Scene.unity" + }, + new TestTreeData { + Path = "Folder\\Default Scene.unity.meta" + } + }; + testTree.Load(testTreeData); + + testTree.CreatedTreeNodes.ShouldAllBeEquivalentTo(new[] { + new TestTreeNode { + Path = "Test Tree", + Label = "Test Tree", + IsFolder = true + }, + new TestTreeNode { + Path = "Folder", + Label = "Folder", + Level = 1, + IsFolder = true + }, + new TestTreeNode { + Path = "Folder\\Default Scene.unity", + Label = "Default Scene.unity", + Level = 2, + TreeData = testTreeData[0], + IsContainer = true + }, + new TestTreeNode { + Path = "Folder\\Default Scene.unity.meta", + Label = "Default Scene.unity.meta", + Level = 3, + TreeData = testTreeData[1] + } + }); + + var sceneNode = testTree.CreatedTreeNodes[2]; + var sceneMetaNode = testTree.CreatedTreeNodes[3]; + + Assert.AreEqual(CheckState.Empty, sceneNode.CheckState); + Assert.AreEqual(CheckState.Empty, sceneMetaNode.CheckState); + + testTree.ToggleNodeChecked(3, sceneMetaNode); + + Assert.AreEqual(CheckState.Checked, sceneNode.CheckState); + Assert.AreEqual(CheckState.Checked, sceneMetaNode.CheckState); + + testTreeListener.Received(2).AddCheckedNode(Arg.Any()); + } + + [Test] + public void ShouldRippleChecksCorrectly() + { + var testTree = new TestTree(true); + var testTreeListener = testTree.TestTreeListener; + + testTreeListener.GetCollapsedFolders().Returns(new string[0]); + testTreeListener.SelectedNode.Returns((TestTreeNode)null); + testTreeListener.GetCheckedFiles().Returns(new string[0]); + testTreeListener.Nodes.Returns(new List()); + testTreeListener.PathSeparator.Returns(@"\"); + testTreeListener.DisplayRootNode.Returns(true); + testTreeListener.IsSelectable.Returns(false); + testTreeListener.Title.Returns("Test Tree"); + testTreeListener.PromoteMetaFiles.Returns(true); + + var testTreeData = new[] { + new TestTreeData { + Path = "Root\\Parent\\A.txt" + }, + new TestTreeData { + Path = "Root\\Parent\\B.txt" + }, + new TestTreeData { + Path = "Root\\Parent\\C.txt" + } + }; + + testTree.Load(testTreeData); + + testTree.CreatedTreeNodes.ShouldAllBeEquivalentTo(new[] { + new TestTreeNode { + Path = "Test Tree", + Label = "Test Tree", + IsFolder = true + }, + new TestTreeNode { + Path = "Root", + Label = "Root", + Level = 1, + IsFolder = true + }, + new TestTreeNode { + Path = "Root\\Parent", + Label = "Parent", + Level = 2, + IsFolder = true + }, + new TestTreeNode { + Path = "Root\\Parent\\A.txt", + Label = "A.txt", + Level = 3, + TreeData = testTreeData[0], + }, + new TestTreeNode { + Path = "Root\\Parent\\B.txt", + Label = "B.txt", + Level = 3, + TreeData = testTreeData[1], + }, + new TestTreeNode { + Path = "Root\\Parent\\C.txt", + Label = "C.txt", + Level = 3, + TreeData = testTreeData[2], + } + }); + + var rootNode = testTree.CreatedTreeNodes[1]; + var parentNode = testTree.CreatedTreeNodes[2]; + var aNode = testTree.CreatedTreeNodes[3]; + var bNode = testTree.CreatedTreeNodes[4]; + var cNode = testTree.CreatedTreeNodes[5]; + + // Initial state, everything unchecked + + Assert.AreEqual(CheckState.Empty, rootNode.CheckState); + Assert.AreEqual(CheckState.Empty, parentNode.CheckState); + Assert.AreEqual(CheckState.Empty, aNode.CheckState); + Assert.AreEqual(CheckState.Empty, bNode.CheckState); + Assert.AreEqual(CheckState.Empty, cNode.CheckState); + + testTree.ToggleNodeChecked(1, rootNode); + + // Checked the root node, everything checked + + Assert.AreEqual(CheckState.Checked, rootNode.CheckState); + Assert.AreEqual(CheckState.Checked, parentNode.CheckState); + Assert.AreEqual(CheckState.Checked, aNode.CheckState); + Assert.AreEqual(CheckState.Checked, bNode.CheckState); + Assert.AreEqual(CheckState.Checked, cNode.CheckState); + + testTreeListener.Received(3).AddCheckedNode(Arg.Any()); + testTreeListener.ClearReceivedCalls(); + + // Unchecked c.txt, c.txt unchecked, parents mixed + + testTree.ToggleNodeChecked(5, cNode); + + Assert.AreEqual(CheckState.Mixed, rootNode.CheckState); + Assert.AreEqual(CheckState.Mixed, parentNode.CheckState); + Assert.AreEqual(CheckState.Checked, aNode.CheckState); + Assert.AreEqual(CheckState.Checked, bNode.CheckState); + Assert.AreEqual(CheckState.Empty, cNode.CheckState); + + testTreeListener.Received(1).RemoveCheckedNode(Arg.Any()); + testTreeListener.ClearReceivedCalls(); + + testTree.ToggleNodeChecked(5, cNode); + + // Checked c.txt, everything checked + + Assert.AreEqual(CheckState.Checked, rootNode.CheckState); + Assert.AreEqual(CheckState.Checked, parentNode.CheckState); + Assert.AreEqual(CheckState.Checked, aNode.CheckState); + Assert.AreEqual(CheckState.Checked, bNode.CheckState); + Assert.AreEqual(CheckState.Checked, cNode.CheckState); + + testTreeListener.Received(1).AddCheckedNode(Arg.Any()); + testTreeListener.ClearReceivedCalls(); + + // Unchecked a.txt b.txt and c.txt, everything checked + + testTree.ToggleNodeChecked(3, aNode); + testTree.ToggleNodeChecked(4, bNode); + testTree.ToggleNodeChecked(5, cNode); + + Assert.AreEqual(CheckState.Empty, rootNode.CheckState); + Assert.AreEqual(CheckState.Empty, parentNode.CheckState); + Assert.AreEqual(CheckState.Empty, aNode.CheckState); + Assert.AreEqual(CheckState.Empty, bNode.CheckState); + Assert.AreEqual(CheckState.Empty, cNode.CheckState); + + testTreeListener.Received(3).RemoveCheckedNode(Arg.Any()); + testTreeListener.ClearReceivedCalls(); + } + [Test] public void ShouldPopulateTreeWithSingleEntryWithNonPromotedMetaInPath() {