diff --git a/Core/GameInstance.cs b/Core/GameInstance.cs index 8cf0ef2e8b..bf316c7d79 100644 --- a/Core/GameInstance.cs +++ b/Core/GameInstance.cs @@ -180,14 +180,10 @@ private void LoadCompatibleVersions() } private string CompatibleGameVersionsFile() - { - return Path.Combine(CkanDir(), game.CompatibleVersionsFile); - } + => Path.Combine(CkanDir(), game.CompatibleVersionsFile); public List GetCompatibleVersions() - { - return new List(this._compatibleVersions); - } + => new List(this._compatibleVersions); public HashSet GetSuppressedCompatWarningIdentifiers => SuppressedCompatWarningIdentifiers.LoadFrom(Version(), SuppressedCompatWarningIdentifiersFile).Identifiers; @@ -204,12 +200,9 @@ public void AddSuppressedCompatWarningIdentifiers(HashSet idents) public string[] InstallFilters { - get - { - return File.Exists(InstallFiltersFile) + get => File.Exists(InstallFiltersFile) ? JsonConvert.DeserializeObject(File.ReadAllText(InstallFiltersFile)) : new string[] { }; - } set { @@ -322,9 +315,7 @@ private GameVersion DetectVersion(string directory) #region Things which would be better as Properties public string GameDir() - { - return gameDir; - } + => gameDir; public string CkanDir() { @@ -338,24 +329,13 @@ public string CkanDir() } public string DownloadCacheDir() - { - return CKANPathUtils.NormalizePath( - Path.Combine(CkanDir(), "downloads")); - } + => CKANPathUtils.NormalizePath(Path.Combine(CkanDir(), "downloads")); public string InstallHistoryDir() - { - return CKANPathUtils.NormalizePath( - Path.Combine(CkanDir(), "history") - ); - } + => CKANPathUtils.NormalizePath(Path.Combine(CkanDir(), "history")); public string TempDir() - { - return CKANPathUtils.NormalizePath( - Path.Combine(CkanDir(), "temp") - ); - } + => CKANPathUtils.NormalizePath(Path.Combine(CkanDir(), "temp")); public GameVersion Version() { @@ -367,9 +347,7 @@ public GameVersion Version() } public GameVersionCriteria VersionCriteria() - { - return new GameVersionCriteria(Version(), _compatibleVersions); - } + => new GameVersionCriteria(Version(), _compatibleVersions); #endregion @@ -439,18 +417,14 @@ public bool Scan() /// Returns path relative to this KSP's GameDir. /// public string ToRelativeGameDir(string path) - { - return CKANPathUtils.ToRelative(path, GameDir()); - } + => CKANPathUtils.ToRelative(path, GameDir()); /// /// Given a path relative to this KSP's GameDir, returns the /// absolute path on the system. /// public string ToAbsoluteGameDir(string path) - { - return CKANPathUtils.ToAbsolute(path, GameDir()); - } + => CKANPathUtils.ToAbsolute(path, GameDir()); /// /// https://xkcd.com/208/ @@ -485,25 +459,42 @@ public string DllPathToIdentifier(string relative_path) : null; } + /// + /// Generate a sequence of files in the game folder that weren't installed by CKAN + /// + /// A Registry object that knows which files CKAN installed in this folder + /// Relative file paths as strings + public IEnumerable UnmanagedFiles(Registry registry) + => Directory.EnumerateFiles(gameDir, "*", SearchOption.AllDirectories) + .Select(CKANPathUtils.NormalizePath) + .Where(absPath => !absPath.StartsWith(CkanDir())) + .Select(ToRelativeGameDir) + .Where(relPath => + !game.StockFolders.Any(f => relPath.StartsWith($"{f}/")) + && registry.FileOwner(relPath) == null); + + /// + /// Check whether a given path contains any files or folders installed by CKAN + /// + /// A Registry object that knows which files CKAN installed in this folder + /// Absolute path to a folder to check + /// true if any descendants of given path were installed by CKAN, false otherwise + public bool HasManagedFiles(Registry registry, string absPath) + => registry.FileOwner(ToRelativeGameDir(absPath)) != null + || Directory.EnumerateFileSystemEntries(absPath, "*", SearchOption.AllDirectories) + .Any(f => registry.FileOwner(ToRelativeGameDir(f)) != null); + public override string ToString() - { - return string.Format(Properties.Resources.GameInstanceToString, game.ShortName, gameDir); - } + => string.Format(Properties.Resources.GameInstanceToString, game.ShortName, gameDir); public bool Equals(GameInstance other) - { - return other != null && gameDir.Equals(other.GameDir()); - } + => other != null && gameDir.Equals(other.GameDir()); public override bool Equals(object obj) - { - return Equals(obj as GameInstance); - } + => Equals(obj as GameInstance); public override int GetHashCode() - { - return gameDir.GetHashCode(); - } + => gameDir.GetHashCode(); } } diff --git a/Core/Games/KerbalSpaceProgram.cs b/Core/Games/KerbalSpaceProgram.cs index 0cf8dbbc29..2ea2b5c660 100644 --- a/Core/Games/KerbalSpaceProgram.cs +++ b/Core/Games/KerbalSpaceProgram.cs @@ -14,9 +14,7 @@ public class KerbalSpaceProgram : IGame public string ShortName => "KSP"; public bool GameInFolder(DirectoryInfo where) - { - return Directory.Exists(Path.Combine(where.FullName, "GameData")); - } + => Directory.Exists(Path.Combine(where.FullName, "GameData")); /// /// Finds the Steam KSP path. Returns null if the folder cannot be located. @@ -96,15 +94,19 @@ public string MacPath() public string PrimaryModDirectoryRelative => "GameData"; public string PrimaryModDirectory(GameInstance inst) - { - return CKANPathUtils.NormalizePath( + => CKANPathUtils.NormalizePath( Path.Combine(inst.GameDir(), PrimaryModDirectoryRelative)); - } public string[] StockFolders => new string[] { "GameData/Squad", - "GameData/SquadExpansion" + "GameData/SquadExpansion", + "KSP_Data", + "KSP_x64_Data", + "KSPLauncher_Data", + "Launcher_Data", + "MonoBleedingEdge", + "PDLauncher", }; public string[] ReservedPaths => new string[] @@ -123,21 +125,17 @@ public string PrimaryModDirectory(GameInstance inst) /// /// public bool IsReservedDirectory(GameInstance inst, string path) - { - return path == inst.GameDir() || path == inst.CkanDir() - || path == PrimaryModDirectory(inst) - || path == Missions(inst) - || path == Scenarios(inst) || path == Tutorial(inst) - || path == Ships(inst) || path == ShipsThumbs(inst) - || path == ShipsVab(inst) || path == ShipsThumbsVAB(inst) - || path == ShipsSph(inst) || path == ShipsThumbsSPH(inst) - || path == ShipsScript(inst); - } + => path == inst.GameDir() || path == inst.CkanDir() + || path == PrimaryModDirectory(inst) + || path == Missions(inst) + || path == Scenarios(inst) || path == Tutorial(inst) + || path == Ships(inst) || path == ShipsThumbs(inst) + || path == ShipsVab(inst) || path == ShipsThumbsVAB(inst) + || path == ShipsSph(inst) || path == ShipsThumbsSPH(inst) + || path == ShipsScript(inst); public bool AllowInstallationIn(string name, out string path) - { - return allowedFolders.TryGetValue(name, out path); - } + => allowedFolders.TryGetValue(name, out path); public void RebuildSubdirectories(string absGameRoot) { @@ -218,64 +216,34 @@ public GameVersion DetectVersion(DirectoryInfo where) public Uri RepositoryListURL => new Uri("https://raw.githubusercontent.com/KSP-CKAN/CKAN-meta/master/repositories.json"); private string Missions(GameInstance inst) - { - return CKANPathUtils.NormalizePath( - Path.Combine(inst.GameDir(), "Missions")); - } + => CKANPathUtils.NormalizePath(Path.Combine(inst.GameDir(), "Missions")); private string Ships(GameInstance inst) - { - return CKANPathUtils.NormalizePath( - Path.Combine(inst.GameDir(), "Ships")); - } + => CKANPathUtils.NormalizePath(Path.Combine(inst.GameDir(), "Ships")); private string ShipsVab(GameInstance inst) - { - return CKANPathUtils.NormalizePath( - Path.Combine(Ships(inst), "VAB")); - } + => CKANPathUtils.NormalizePath(Path.Combine(Ships(inst), "VAB")); private string ShipsSph(GameInstance inst) - { - return CKANPathUtils.NormalizePath( - Path.Combine(Ships(inst), "SPH")); - } + => CKANPathUtils.NormalizePath(Path.Combine(Ships(inst), "SPH")); private string ShipsThumbs(GameInstance inst) - { - return CKANPathUtils.NormalizePath( - Path.Combine(Ships(inst), "@thumbs")); - } + => CKANPathUtils.NormalizePath(Path.Combine(Ships(inst), "@thumbs")); private string ShipsThumbsSPH(GameInstance inst) - { - return CKANPathUtils.NormalizePath( - Path.Combine(ShipsThumbs(inst), "SPH")); - } + => CKANPathUtils.NormalizePath(Path.Combine(ShipsThumbs(inst), "SPH")); private string ShipsThumbsVAB(GameInstance inst) - { - return CKANPathUtils.NormalizePath( - Path.Combine(ShipsThumbs(inst), "VAB")); - } + => CKANPathUtils.NormalizePath(Path.Combine(ShipsThumbs(inst), "VAB")); private string ShipsScript(GameInstance inst) - { - return CKANPathUtils.NormalizePath( - Path.Combine(Ships(inst), "Script")); - } + => CKANPathUtils.NormalizePath(Path.Combine(Ships(inst), "Script")); private string Tutorial(GameInstance inst) - { - return CKANPathUtils.NormalizePath( - Path.Combine(inst.GameDir(), "saves", "training")); - } + => CKANPathUtils.NormalizePath(Path.Combine(inst.GameDir(), "saves", "training")); private string Scenarios(GameInstance inst) - { - return CKANPathUtils.NormalizePath( - Path.Combine(inst.GameDir(), "saves", "scenarios")); - } + => CKANPathUtils.NormalizePath(Path.Combine(inst.GameDir(), "saves", "scenarios")); private readonly Dictionary allowedFolders = new Dictionary { diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj index 81cf1eaef9..82a63f21d1 100644 --- a/GUI/CKAN-GUI.csproj +++ b/GUI/CKAN-GUI.csproj @@ -231,6 +231,12 @@ ModInfo.cs + + UserControl + + + UnmanagedFiles.cs + UserControl @@ -331,6 +337,9 @@ Form + + Form + Form @@ -868,6 +877,9 @@ ModInfo.cs + + UnmanagedFiles.cs + PlayTime.cs diff --git a/GUI/Controls/ManageMods.Designer.cs b/GUI/Controls/ManageMods.Designer.cs index 224a9ed994..085c651d51 100644 --- a/GUI/Controls/ManageMods.Designer.cs +++ b/GUI/Controls/ManageMods.Designer.cs @@ -1,5 +1,3 @@ -using System.Windows.Forms; - namespace CKAN.GUI { partial class ManageMods @@ -124,7 +122,7 @@ private void InitializeComponent() this.launchGameToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; this.launchGameToolStripMenuItem.Name = "launchGameToolStripMenuItem"; this.launchGameToolStripMenuItem.Size = new System.Drawing.Size(146, 56); - this.launchGameToolStripMenuItem.Overflow = ToolStripItemOverflow.AsNeeded; + this.launchGameToolStripMenuItem.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; this.launchGameToolStripMenuItem.Click += new System.EventHandler(this.launchGameToolStripMenuItem_Click); resources.ApplyResources(this.launchGameToolStripMenuItem, "launchGameToolStripMenuItem"); // @@ -134,7 +132,7 @@ private void InitializeComponent() this.RefreshToolButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; this.RefreshToolButton.Name = "RefreshToolButton"; this.RefreshToolButton.Size = new System.Drawing.Size(114, 56); - this.RefreshToolButton.Overflow = ToolStripItemOverflow.AsNeeded; + this.RefreshToolButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; this.RefreshToolButton.Click += new System.EventHandler(this.RefreshToolButton_Click); resources.ApplyResources(this.RefreshToolButton, "RefreshToolButton"); // @@ -144,7 +142,7 @@ private void InitializeComponent() this.UpdateAllToolButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; this.UpdateAllToolButton.Name = "UpdateAllToolButton"; this.UpdateAllToolButton.Size = new System.Drawing.Size(232, 56); - this.UpdateAllToolButton.Overflow = ToolStripItemOverflow.AsNeeded; + this.UpdateAllToolButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; this.UpdateAllToolButton.Click += new System.EventHandler(this.MarkAllUpdatesToolButton_Click); resources.ApplyResources(this.UpdateAllToolButton, "UpdateAllToolButton"); // @@ -155,7 +153,7 @@ private void InitializeComponent() this.ApplyToolButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; this.ApplyToolButton.Name = "ApplyToolButton"; this.ApplyToolButton.Size = new System.Drawing.Size(173, 56); - this.ApplyToolButton.Overflow = ToolStripItemOverflow.AsNeeded; + this.ApplyToolButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; this.ApplyToolButton.Click += new System.EventHandler(this.ApplyToolButton_Click); resources.ApplyResources(this.ApplyToolButton, "ApplyToolButton"); // @@ -180,7 +178,7 @@ private void InitializeComponent() this.FilterToolButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; this.FilterToolButton.Name = "FilterToolButton"; this.FilterToolButton.Size = new System.Drawing.Size(201, 56); - this.FilterToolButton.Overflow = ToolStripItemOverflow.AsNeeded; + this.FilterToolButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; resources.ApplyResources(this.FilterToolButton, "FilterToolButton"); // // FilterCompatibleButton @@ -273,7 +271,7 @@ private void InitializeComponent() this.NavBackwardToolButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; this.NavBackwardToolButton.Name = "NavBackwardToolButton"; this.NavBackwardToolButton.Size = new System.Drawing.Size(44, 56); - this.NavBackwardToolButton.Overflow = ToolStripItemOverflow.AsNeeded; + this.NavBackwardToolButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; this.NavBackwardToolButton.Click += new System.EventHandler(this.NavBackwardToolButton_Click); resources.ApplyResources(this.NavBackwardToolButton, "NavBackwardToolButton"); // @@ -283,7 +281,7 @@ private void InitializeComponent() this.NavForwardToolButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; this.NavForwardToolButton.Name = "NavForwardToolButton"; this.NavForwardToolButton.Size = new System.Drawing.Size(44, 56); - this.NavForwardToolButton.Overflow = ToolStripItemOverflow.AsNeeded; + this.NavForwardToolButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; this.NavForwardToolButton.Click += new System.EventHandler(this.NavForwardToolButton_Click); resources.ApplyResources(this.NavForwardToolButton, "NavForwardToolButton"); // diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 49e108a06c..aab0d4e0c3 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -1647,7 +1647,10 @@ public async Task UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerie new_conflicts = tuple.Item2.ToDictionary( item => new GUIMod(item.Key, registry, gameVersion), item => item.Value); - Main.Instance.AddStatusMessage(string.Join(";", new_conflicts.Values)); + if (new_conflicts.Count > 0) + { + Main.Instance.AddStatusMessage(string.Join(";", new_conflicts.Values)); + } } catch (DependencyNotSatisfiedKraken k) { diff --git a/GUI/Controls/UnmanagedFiles.Designer.cs b/GUI/Controls/UnmanagedFiles.Designer.cs new file mode 100644 index 0000000000..7c68e31348 --- /dev/null +++ b/GUI/Controls/UnmanagedFiles.Designer.cs @@ -0,0 +1,198 @@ +namespace CKAN.GUI +{ + partial class UnmanagedFiles + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(UnmanagedFiles)); + this.ToolTip = new System.Windows.Forms.ToolTip(); + this.Toolbar = new System.Windows.Forms.MenuStrip(); + this.RefreshButton = new System.Windows.Forms.ToolStripMenuItem(); + this.ExpandCollapseSeparator = new System.Windows.Forms.ToolStripSeparator(); + this.ExpandAllButton = new System.Windows.Forms.ToolStripMenuItem(); + this.CollapseAllButton = new System.Windows.Forms.ToolStripMenuItem(); + this.ResetCollapseButton = new System.Windows.Forms.ToolStripMenuItem(); + this.ShowDeleteSeparator = new System.Windows.Forms.ToolStripSeparator(); + this.ShowInFolderButton = new System.Windows.Forms.ToolStripMenuItem(); + this.DeleteButton = new System.Windows.Forms.ToolStripMenuItem(); + this.GameFolderTree = new System.Windows.Forms.TreeView(); + this.OKButton = new System.Windows.Forms.Button(); + this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel.SuspendLayout(); + this.SuspendLayout(); + // + // Toolbar + // + this.Toolbar.AutoSize = false; + this.Toolbar.Dock = System.Windows.Forms.DockStyle.Top; + this.Toolbar.ImageScalingSize = new System.Drawing.Size(24, 24); + this.Toolbar.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.RefreshButton, + this.ExpandCollapseSeparator, + this.ExpandAllButton, + this.CollapseAllButton, + this.ResetCollapseButton, + this.ShowDeleteSeparator, + this.ShowInFolderButton, + this.DeleteButton}); + this.Toolbar.CanOverflow = true; + this.Toolbar.Location = new System.Drawing.Point(0, 0); + this.Toolbar.Name = "Toolbar"; + this.Toolbar.ShowItemToolTips = true; + this.Toolbar.Size = new System.Drawing.Size(5876, 48); + this.Toolbar.TabStop = true; + this.Toolbar.TabIndex = 0; + this.Toolbar.Text = "Toolbar"; + // + // RefreshButton + // + this.RefreshButton.Image = global::CKAN.GUI.Properties.Resources.refresh; + this.RefreshButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; + this.RefreshButton.Name = "RefreshButton"; + this.RefreshButton.Size = new System.Drawing.Size(114, 56); + this.RefreshButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; + this.RefreshButton.Click += new System.EventHandler(this.RefreshButton_Click); + resources.ApplyResources(this.RefreshButton, "RefreshButton"); + // + // ExpandAllButton + // + this.ExpandAllButton.Image = global::CKAN.GUI.Properties.Resources.expandAll; + this.ExpandAllButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; + this.ExpandAllButton.Name = "ExpandAllButton"; + this.ExpandAllButton.Size = new System.Drawing.Size(114, 56); + this.ExpandAllButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; + this.ExpandAllButton.Click += new System.EventHandler(this.ExpandAllButton_Click); + resources.ApplyResources(this.ExpandAllButton, "ExpandAllButton"); + // + // CollapseAllButton + // + this.CollapseAllButton.Image = global::CKAN.GUI.Properties.Resources.collapseAll; + this.CollapseAllButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; + this.CollapseAllButton.Name = "CollapseAllButton"; + this.CollapseAllButton.Size = new System.Drawing.Size(114, 56); + this.CollapseAllButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; + this.CollapseAllButton.Click += new System.EventHandler(this.CollapseAllButton_Click); + resources.ApplyResources(this.CollapseAllButton, "CollapseAllButton"); + // + // ResetCollapseButton + // + this.ResetCollapseButton.Image = global::CKAN.GUI.Properties.Resources.resetCollapse; + this.ResetCollapseButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; + this.ResetCollapseButton.Name = "ResetCollapseButton"; + this.ResetCollapseButton.Size = new System.Drawing.Size(114, 56); + this.ResetCollapseButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; + this.ResetCollapseButton.Click += new System.EventHandler(this.ResetCollapseButton_Click); + resources.ApplyResources(this.ResetCollapseButton, "ResetCollapseButton"); + // + // ShowInFolderButton + // + this.ShowInFolderButton.Image = global::CKAN.GUI.Properties.Resources.search; + this.ShowInFolderButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; + this.ShowInFolderButton.Name = "ShowInFolderButton"; + this.ShowInFolderButton.Size = new System.Drawing.Size(114, 56); + this.ShowInFolderButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; + this.ShowInFolderButton.Click += new System.EventHandler(this.ShowInFolderButton_Click); + resources.ApplyResources(this.ShowInFolderButton, "ShowInFolderButton"); + // + // DeleteButton + // + this.DeleteButton.Image = global::CKAN.GUI.Properties.Resources.delete; + this.DeleteButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; + this.DeleteButton.Name = "DeleteButton"; + this.DeleteButton.Size = new System.Drawing.Size(114, 56); + this.DeleteButton.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; + this.DeleteButton.Click += new System.EventHandler(this.DeleteButton_Click); + resources.ApplyResources(this.DeleteButton, "DeleteButton"); + // + // GameFolderTree + // + this.GameFolderTree.Dock = System.Windows.Forms.DockStyle.Fill; + this.GameFolderTree.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.GameFolderTree.ImageList = new System.Windows.Forms.ImageList(this.components) + { + // ImageList's default makes icons look like garbage + ColorDepth = System.Windows.Forms.ColorDepth.Depth32Bit + }; + this.GameFolderTree.ImageList.Images.Add("folder", global::CKAN.GUI.Properties.Resources.folder); + this.GameFolderTree.ImageList.Images.Add("file", global::CKAN.GUI.Properties.Resources.file); + this.GameFolderTree.ShowPlusMinus = true; + this.GameFolderTree.ShowRootLines = false; + this.GameFolderTree.Location = new System.Drawing.Point(3, 3); + this.GameFolderTree.Name = "GameFolderTree"; + this.GameFolderTree.Size = new System.Drawing.Size(494, 494); + this.GameFolderTree.TabIndex = 1; + this.GameFolderTree.NodeMouseDoubleClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.GameFolderTree_NodeMouseDoubleClick); + // + // BottomButtonPanel + // + this.BottomButtonPanel.RightControls.Add(this.OKButton); + this.BottomButtonPanel.Dock = System.Windows.Forms.DockStyle.Bottom; + this.BottomButtonPanel.Name = "BottomButtonPanel"; + // + // OKButton + // + this.OKButton.AutoSize = true; + this.OKButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.OKButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.OKButton.Name = "OKButton"; + this.OKButton.Size = new System.Drawing.Size(112, 30); + this.OKButton.TabIndex = 2; + this.OKButton.Click += new System.EventHandler(this.OKButton_Click); + resources.ApplyResources(this.OKButton, "OKButton"); + // + // UnmanagedFiles + // + this.Controls.Add(this.GameFolderTree); + this.Controls.Add(this.Toolbar); + this.Controls.Add(this.BottomButtonPanel); + this.Name = "UnmanagedFiles"; + this.Size = new System.Drawing.Size(500, 500); + resources.ApplyResources(this, "$this"); + this.BottomButtonPanel.ResumeLayout(false); + this.BottomButtonPanel.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + } + + #endregion + + private System.Windows.Forms.ToolTip ToolTip; + private System.Windows.Forms.MenuStrip Toolbar; + private System.Windows.Forms.ToolStripMenuItem RefreshButton; + private System.Windows.Forms.ToolStripSeparator ExpandCollapseSeparator; + private System.Windows.Forms.ToolStripMenuItem ExpandAllButton; + private System.Windows.Forms.ToolStripMenuItem CollapseAllButton; + private System.Windows.Forms.ToolStripMenuItem ResetCollapseButton; + private System.Windows.Forms.ToolStripSeparator ShowDeleteSeparator; + private System.Windows.Forms.ToolStripMenuItem ShowInFolderButton; + private System.Windows.Forms.ToolStripMenuItem DeleteButton; + private System.Windows.Forms.TreeView GameFolderTree; + private LeftRightRowPanel BottomButtonPanel; + private System.Windows.Forms.Button OKButton; + } +} diff --git a/GUI/Controls/UnmanagedFiles.cs b/GUI/Controls/UnmanagedFiles.cs new file mode 100644 index 0000000000..bea989cfab --- /dev/null +++ b/GUI/Controls/UnmanagedFiles.cs @@ -0,0 +1,222 @@ +using System; +using System.Linq; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.Windows.Forms; +using System.IO; +using System.Threading.Tasks; + +namespace CKAN.GUI +{ + public partial class UnmanagedFiles : UserControl + { + public UnmanagedFiles() + { + InitializeComponent(); + GameFolderTree.TreeViewNodeSorter = new DirsFirstSorter(); + } + + public void Refresh() + { + Util.Invoke(GameFolderTree, _UpdateGameFolderTree); + } + + /// + /// Invoked when the user clicks OK + /// + public event Action Done; + + private GameInstanceManager manager => Main.Instance.Manager; + + private void _UpdateGameFolderTree() + { + GameFolderTree.BackColor = SystemColors.Window; + GameFolderTree.LineColor = SystemColors.WindowText; + + GameFolderTree.Nodes.Clear(); + var rootNode = GameFolderTree.Nodes.Add( + "", + manager.CurrentInstance.GameDir().Replace('/', Path.DirectorySeparatorChar), + "folder", "folder"); + + UseWaitCursor = true; + Task.Factory.StartNew(() => + { + var registry = RegistryManager.Instance(manager.CurrentInstance).registry; + var paths = manager.CurrentInstance?.UnmanagedFiles(registry).ToArray() + ?? new string[] { }; + Util.Invoke(this, () => + { + GameFolderTree.BeginUpdate(); + foreach (string path in paths) + { + AddContentPieces(rootNode, path.Split(new char[] {'/'})); + } + rootNode.Expand(); + rootNode.EnsureVisible(); + ExpandDefaultModDir(); + // The nodes don't have children at first, so the sort needs to be re-applied after they're added + GameFolderTree.Sort(); + GameFolderTree.EndUpdate(); + UseWaitCursor = false; + }); + }); + } + + private IEnumerable ParentPaths(string[] pathPieces) + => Enumerable.Range(1, pathPieces.Length) + .Select(numPieces => string.Join("/", pathPieces.Take(numPieces))); + + private void ExpandDefaultModDir() + { + foreach (string path in ParentPaths(manager.CurrentInstance.game.PrimaryModDirectoryRelative.Split(new char[] {'/'}))) + { + foreach (var node in GameFolderTree.Nodes.Find(path, true)) + { + node.Expand(); + } + } + } + + private void AddContentPieces(TreeNode parent, IEnumerable pieces) + { + var firstPiece = pieces.FirstOrDefault(); + if (firstPiece != null) + { + if (parent.ImageKey == "file") + { + parent.SelectedImageKey = parent.ImageKey = "folder"; + } + // Key/Name needs to be the full relative path for double click to work + var key = string.IsNullOrEmpty(parent.Name) + ? firstPiece + : $"{parent.Name}/{firstPiece}"; + var node = parent.Nodes[key] + ?? parent.Nodes.Add(key, firstPiece, "file", "file"); + AddContentPieces(node, pieces.Skip(1)); + } + } + + private void RefreshButton_Click(object sender, EventArgs e) + { + Refresh(); + } + + private void ExpandAllButton_Click(object sender, EventArgs e) + { + GameFolderTree.BeginUpdate(); + GameFolderTree.ExpandAll(); + GameFolderTree.Nodes[0].EnsureVisible(); + GameFolderTree.EndUpdate(); + GameFolderTree.Focus(); + } + + private void CollapseAllButton_Click(object sender, EventArgs e) + { + GameFolderTree.BeginUpdate(); + GameFolderTree.CollapseAll(); + GameFolderTree.Nodes[0].Expand(); + GameFolderTree.EndUpdate(); + GameFolderTree.Focus(); + } + + private void ResetCollapseButton_Click(object sender, EventArgs e) + { + GameFolderTree.BeginUpdate(); + GameFolderTree.CollapseAll(); + GameFolderTree.Nodes[0].Expand(); + ExpandDefaultModDir(); + GameFolderTree.Nodes[0].EnsureVisible(); + GameFolderTree.EndUpdate(); + GameFolderTree.Focus(); + } + + private void ShowInFolderButton_Click(object sender, EventArgs e) + { + OpenFileBrowser(GameFolderTree.SelectedNode); + GameFolderTree.Focus(); + } + + private void DeleteButton_Click(object sender, EventArgs e) + { + var relPath = GameFolderTree.SelectedNode?.Name; + var registry = RegistryManager.Instance(manager.CurrentInstance).registry; + var absPath = manager.CurrentInstance.ToAbsoluteGameDir(relPath); + if (manager.CurrentInstance.HasManagedFiles(registry, absPath)) + { + Main.Instance.ErrorDialog(Properties.Resources.FolderContainsManagedFiles, relPath); + } + else if (!string.IsNullOrEmpty(relPath) && Main.Instance.YesNoDialog( + string.Format(Properties.Resources.DeleteUnmanagedFileConfirmation, + relPath.Replace('/', Path.DirectorySeparatorChar)), + Properties.Resources.DeleteUnmanagedFileDelete, + Properties.Resources.DeleteUnmanagedFileCancel)) + { + if (File.Exists(absPath)) + { + File.Delete(absPath); + } + else if (Directory.Exists(absPath)) + { + Directory.Delete(absPath, true); + } + GameFolderTree.Nodes.Remove(GameFolderTree.SelectedNode); + } + } + + private void GameFolderTree_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) + { + OpenFileBrowser(e.Node); + } + + private void OKButton_Click(object sender, EventArgs e) + { + Done?.Invoke(); + } + + /// + /// Opens the folder of the double-clicked node + /// in the file browser of the user's system + /// + /// A node of the GameFolderTree + private void OpenFileBrowser(TreeNode node) + { + if (node != null) + { + string location = manager.CurrentInstance.ToAbsoluteGameDir(node.Name); + + if (File.Exists(location)) + { + // We need the Folder of the file + // Otherwise the OS would try to open the file in its default application + location = Path.GetDirectoryName(location); + } + + if (!Directory.Exists(location)) + { + // User either selected the parent node + // or he clicked on the tree node of a cached, but not installed mod + return; + } + + Utilities.ProcessStartURL(location); + } + } + } + + internal class DirsFirstSorter : IComparer, IComparer + { + public int Compare(object a, object b) + => Compare(a as TreeNode, b as TreeNode); + + public int Compare(TreeNode a, TreeNode b) + => a.Nodes.Count > 0 + ? b.Nodes.Count > 0 + ? string.Compare(a.Text, b.Text) + : -1 + : b.Nodes.Count > 0 + ? 1 + : string.Compare(a.Text, b.Text); + } +} diff --git a/GUI/Controls/UnmanagedFiles.resx b/GUI/Controls/UnmanagedFiles.resx new file mode 100644 index 0000000000..7210acfb8d --- /dev/null +++ b/GUI/Controls/UnmanagedFiles.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + OK + Refresh + Expand All + Collapse All + Reset + Show in Folder + Delete + diff --git a/GUI/Main/Main.Designer.cs b/GUI/Main/Main.Designer.cs index f35e4dbb2a..7b64caf578 100644 --- a/GUI/Main/Main.Designer.cs +++ b/GUI/Main/Main.Designer.cs @@ -78,6 +78,8 @@ private void InitializeComponent() this.ChooseRecommendedMods = new CKAN.GUI.ChooseRecommendedMods(); this.PlayTimeTabPage = new System.Windows.Forms.TabPage(); this.PlayTime = new CKAN.GUI.PlayTime(); + this.UnmanagedFilesTabPage = new System.Windows.Forms.TabPage(); + this.UnmanagedFiles = new CKAN.GUI.UnmanagedFiles(); this.ChooseProvidedModsTabPage = new System.Windows.Forms.TabPage(); this.ChooseProvidedMods = new CKAN.GUI.ChooseProvidedMods(); this.minimizeNotifyIcon = new System.Windows.Forms.NotifyIcon(this.components); @@ -96,6 +98,7 @@ private void InitializeComponent() this.toolStripSeparator8 = new System.Windows.Forms.ToolStripSeparator(); this.quitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.viewPlayTimeStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.viewUnmanagedFilesStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.DeleteDirectoriesTabPage = new System.Windows.Forms.TabPage(); this.DeleteDirectories = new CKAN.GUI.DeleteDirectories(); this.EditModpackTabPage = new System.Windows.Forms.TabPage(); @@ -114,6 +117,8 @@ private void InitializeComponent() this.ChooseProvidedModsTabPage.SuspendLayout(); this.PlayTimeTabPage.SuspendLayout(); this.PlayTime.SuspendLayout(); + this.UnmanagedFilesTabPage.SuspendLayout(); + this.UnmanagedFiles.SuspendLayout(); this.minimizedContextMenuStrip.SuspendLayout(); this.DeleteDirectoriesTabPage.SuspendLayout(); this.EditModpackTabPage.SuspendLayout(); @@ -148,6 +153,7 @@ private void InitializeComponent() this.auditRecommendationsMenuItem, this.toolStripSeparator7, this.viewPlayTimeStripMenuItem, + this.viewUnmanagedFilesStripMenuItem, this.toolStripSeparator8, this.ExitToolButton}); this.fileToolStripMenuItem.Name = "fileToolStripMenuItem"; @@ -419,6 +425,7 @@ private void InitializeComponent() this.MainTabControl.Controls.Add(this.WaitTabPage); this.MainTabControl.Controls.Add(this.ChooseRecommendedModsTabPage); this.MainTabControl.Controls.Add(this.PlayTimeTabPage); + this.MainTabControl.Controls.Add(this.UnmanagedFilesTabPage); this.MainTabControl.Controls.Add(this.ChooseProvidedModsTabPage); this.MainTabControl.Controls.Add(this.DeleteDirectoriesTabPage); this.MainTabControl.Controls.Add(this.EditModpackTabPage); @@ -554,6 +561,29 @@ private void InitializeComponent() this.PlayTime.TabIndex = 32; this.PlayTime.Done += PlayTime_Done; // + // UnmanagedFilesTabPage + // + this.UnmanagedFilesTabPage.BackColor = System.Drawing.SystemColors.Control; + this.UnmanagedFilesTabPage.Controls.Add(this.UnmanagedFiles); + this.UnmanagedFilesTabPage.Location = new System.Drawing.Point(4, 29); + this.UnmanagedFilesTabPage.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.UnmanagedFilesTabPage.Name = "UnmanagedFilesTabPage"; + this.UnmanagedFilesTabPage.Padding = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.UnmanagedFilesTabPage.Size = new System.Drawing.Size(1536, 948); + this.UnmanagedFilesTabPage.TabIndex = 24; + resources.ApplyResources(this.UnmanagedFilesTabPage, "UnmanagedFilesTabPage"); + // + // UnmanagedFiles + // + this.UnmanagedFiles.Dock = System.Windows.Forms.DockStyle.Fill; + this.UnmanagedFiles.Location = new System.Drawing.Point(0, 0); + this.UnmanagedFiles.Margin = new System.Windows.Forms.Padding(0,0,0,0); + this.UnmanagedFiles.Padding = new System.Windows.Forms.Padding(0,0,0,0); + this.UnmanagedFiles.Name = "UnmanagedFiles"; + this.UnmanagedFiles.Size = new System.Drawing.Size(500, 500); + this.UnmanagedFiles.TabIndex = 32; + this.UnmanagedFiles.Done += UnmanagedFiles_Done; + // // ChooseProvidedModsTabPage // this.ChooseProvidedModsTabPage.BackColor = System.Drawing.SystemColors.Control; @@ -726,6 +756,13 @@ private void InitializeComponent() this.viewPlayTimeStripMenuItem.Click += new System.EventHandler(this.viewPlayTimeStripMenuItem_Click); resources.ApplyResources(this.viewPlayTimeStripMenuItem, "viewPlayTimeStripMenuItem"); // + // viewUnmanagedFilesStripMenuItem + // + this.viewUnmanagedFilesStripMenuItem.Name = "viewUnmanagedFilesStripMenuItem"; + this.viewUnmanagedFilesStripMenuItem.Size = new System.Drawing.Size(180, 22); + this.viewUnmanagedFilesStripMenuItem.Click += new System.EventHandler(this.viewUnmanagedFilesStripMenuItem_Click); + resources.ApplyResources(this.viewUnmanagedFilesStripMenuItem, "viewUnmanagedFilesStripMenuItem"); + // // Main // this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); @@ -766,6 +803,10 @@ private void InitializeComponent() this.PlayTimeTabPage.PerformLayout(); this.PlayTime.ResumeLayout(false); this.PlayTime.PerformLayout(); + this.UnmanagedFilesTabPage.ResumeLayout(false); + this.UnmanagedFilesTabPage.PerformLayout(); + this.UnmanagedFiles.ResumeLayout(false); + this.UnmanagedFiles.PerformLayout(); this.DeleteDirectoriesTabPage.ResumeLayout(false); this.DeleteDirectoriesTabPage.PerformLayout(); this.EditModpackTabPage.ResumeLayout(false); @@ -826,6 +867,8 @@ private void InitializeComponent() private CKAN.GUI.ChooseRecommendedMods ChooseRecommendedMods; private System.Windows.Forms.TabPage PlayTimeTabPage; private CKAN.GUI.PlayTime PlayTime; + private System.Windows.Forms.TabPage UnmanagedFilesTabPage; + private CKAN.GUI.UnmanagedFiles UnmanagedFiles; private System.Windows.Forms.TabPage ChooseProvidedModsTabPage; private CKAN.GUI.ChooseProvidedMods ChooseProvidedMods; private System.Windows.Forms.TabPage DeleteDirectoriesTabPage; @@ -848,5 +891,6 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripSeparator toolStripSeparator8; private System.Windows.Forms.ToolStripMenuItem quitToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem viewPlayTimeStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem viewUnmanagedFilesStripMenuItem; } } diff --git a/GUI/Main/Main.resx b/GUI/Main/Main.resx index 460094007a..67a5397b7f 100644 --- a/GUI/Main/Main.resx +++ b/GUI/Main/Main.resx @@ -144,14 +144,15 @@ Report an issue with the CKAN &client Report an issue with mod &metadata &About - Manage mods + Manage Mods Changeset - Status log - Choose mods - Choose recommended, suggested, or supporting mods + Status Log + Choose Mods + Choose Recommended, Suggested, or Supporting Mods Play Time Delete Directories Edit Modpack + Unmanaged Files CKAN N available updates Refresh @@ -162,5 +163,6 @@ CKAN Settings Quit Play &time... + &Unmanaged files... CKAN diff --git a/GUI/Main/MainTime.cs b/GUI/Main/MainTime.cs index 59539da69c..0a5f42a13f 100644 --- a/GUI/Main/MainTime.cs +++ b/GUI/Main/MainTime.cs @@ -1,6 +1,4 @@ using System; -using System.Diagnostics; -using System.Windows.Forms; // Don't warn if we use our own obsolete properties #pragma warning disable 0618 diff --git a/GUI/Main/MainUnmanaged.cs b/GUI/Main/MainUnmanaged.cs new file mode 100644 index 0000000000..dc9f94e4ef --- /dev/null +++ b/GUI/Main/MainUnmanaged.cs @@ -0,0 +1,25 @@ +using System; + +// Don't warn if we use our own obsolete properties +#pragma warning disable 0618 + +namespace CKAN.GUI +{ + public partial class Main + { + private void viewUnmanagedFilesStripMenuItem_Click(object sender, EventArgs e) + { + UnmanagedFiles.Refresh(); + tabController.ShowTab("UnmanagedFilesTabPage", 2); + DisableMainWindow(); + } + + private void UnmanagedFiles_Done() + { + UpdateStatusBar(); + tabController.ShowTab("ManageModsTabPage"); + tabController.HideTab("UnmanagedFilesTabPage"); + EnableMainWindow(); + } + } +} diff --git a/GUI/Properties/Resources.Designer.cs b/GUI/Properties/Resources.Designer.cs index 0c02c1f262..a08845ca26 100644 --- a/GUI/Properties/Resources.Designer.cs +++ b/GUI/Properties/Resources.Designer.cs @@ -234,6 +234,46 @@ internal static System.Drawing.Bitmap settings { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap expandAll { + get { + object obj = ResourceManager.GetObject("expandAll", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap collapseAll { + get { + object obj = ResourceManager.GetObject("collapseAll", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap resetCollapse { + get { + object obj = ResourceManager.GetObject("resetCollapse", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap delete { + get { + object obj = ResourceManager.GetObject("delete", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -1159,5 +1199,18 @@ public static string AuthorSearchName { public static string FilterLinkToolTip { get { return (string)(ResourceManager.GetObject("FilterLinkToolTip", resourceCulture)); } } + + public static string FolderContainsManagedFiles { + get { return (string)(ResourceManager.GetObject("FolderContainsManagedFiles", resourceCulture)); } + } + public static string DeleteUnmanagedFileConfirmation { + get { return (string)(ResourceManager.GetObject("DeleteUnmanagedFileConfirmation", resourceCulture)); } + } + internal static string DeleteUnmanagedFileDelete { + get { return (string)(ResourceManager.GetObject("DeleteUnmanagedFileDelete", resourceCulture)); } + } + internal static string DeleteUnmanagedFileCancel { + get { return (string)(ResourceManager.GetObject("DeleteUnmanagedFileCancel", resourceCulture)); } + } } } diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index 313e758510..7eb696222b 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -130,6 +130,10 @@ ..\Resources\update.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\backward.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\filter.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\expandAll.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\collapseAll.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\resetCollapse.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\delete.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\forward.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\textClear.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\star.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a @@ -466,4 +470,10 @@ Do you want to allow CKAN to do this? If you click no you won't see this message Mods by {0} Click to search Ctrl-click or Shift-click to combine with current search + Can't delete {0} because it contains files that were installed by CKAN. +If you wish to remove this folder, uninstall these mods normally. + Are you sure you want to delete {0}? +This action cannot be undone! + Delete + Cancel diff --git a/GUI/Resources/collapseAll.png b/GUI/Resources/collapseAll.png new file mode 100644 index 0000000000..756aa86dc3 Binary files /dev/null and b/GUI/Resources/collapseAll.png differ diff --git a/GUI/Resources/delete.png b/GUI/Resources/delete.png new file mode 100644 index 0000000000..cc79c25510 Binary files /dev/null and b/GUI/Resources/delete.png differ diff --git a/GUI/Resources/expandAll.png b/GUI/Resources/expandAll.png new file mode 100644 index 0000000000..1dc1b1a73b Binary files /dev/null and b/GUI/Resources/expandAll.png differ diff --git a/GUI/Resources/resetCollapse.png b/GUI/Resources/resetCollapse.png new file mode 100644 index 0000000000..49f7f1f06c Binary files /dev/null and b/GUI/Resources/resetCollapse.png differ