diff --git a/Cmdline/Action/Remove.cs b/Cmdline/Action/Remove.cs index 2f6e0128e3..3440712d5f 100644 --- a/Cmdline/Action/Remove.cs +++ b/Cmdline/Action/Remove.cs @@ -76,9 +76,10 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) { try { + HashSet possibleConfigOnlyDirs = null; var installer = ModuleInstaller.GetInstance(ksp, manager.Cache, user); Search.AdjustModulesCase(ksp, options.modules); - installer.UninstallList(options.modules); + installer.UninstallList(options.modules, ref possibleConfigOnlyDirs); } catch (ModNotInstalledKraken kraken) { diff --git a/Cmdline/Action/Replace.cs b/Cmdline/Action/Replace.cs index f7ed064ede..c8d21c2f24 100644 --- a/Cmdline/Action/Replace.cs +++ b/Cmdline/Action/Replace.cs @@ -155,7 +155,8 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) // TODO: These instances all need to go. try { - ModuleInstaller.GetInstance(ksp, manager.Cache, User).Replace(to_replace, replace_ops, new NetAsyncModulesDownloader(User, manager.Cache)); + HashSet possibleConfigOnlyDirs = null; + ModuleInstaller.GetInstance(ksp, manager.Cache, User).Replace(to_replace, replace_ops, new NetAsyncModulesDownloader(User, manager.Cache), ref possibleConfigOnlyDirs); User.RaiseMessage("\r\nDone!\r\n"); } catch (DependencyNotSatisfiedKraken ex) diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index 01955be06d..e501aa3a80 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -83,6 +83,7 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) try { + HashSet possibleConfigOnlyDirs = null; if (options.upgrade_all) { var registry = RegistryManager.Instance(ksp).registry; @@ -129,13 +130,13 @@ public int RunCommand(CKAN.KSP ksp, object raw_options) } - ModuleInstaller.GetInstance(ksp, manager.Cache, User).Upgrade(to_upgrade, new NetAsyncModulesDownloader(User, manager.Cache)); + ModuleInstaller.GetInstance(ksp, manager.Cache, User).Upgrade(to_upgrade, new NetAsyncModulesDownloader(User, manager.Cache), ref possibleConfigOnlyDirs); } else { // TODO: These instances all need to go. Search.AdjustModulesCase(ksp, options.modules); - ModuleInstaller.GetInstance(ksp, manager.Cache, User).Upgrade(options.modules, new NetAsyncModulesDownloader(User, manager.Cache)); + ModuleInstaller.GetInstance(ksp, manager.Cache, User).Upgrade(options.modules, new NetAsyncModulesDownloader(User, manager.Cache), ref possibleConfigOnlyDirs); } } catch (ModuleNotFoundKraken kraken) diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index e922771df9..4a4f4da0f3 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -54,16 +54,18 @@ public override void Run(Action process = null) } // FUTURE: BackgroundWorker + + HashSet possibleConfigOnlyDirs = null; ModuleInstaller inst = ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, this); inst.onReportModInstalled = OnModInstalled; if (plan.Remove.Count > 0) { - inst.UninstallList(plan.Remove); + inst.UninstallList(plan.Remove, ref possibleConfigOnlyDirs); plan.Remove.Clear(); } NetAsyncModulesDownloader dl = new NetAsyncModulesDownloader(this, manager.Cache); if (plan.Upgrade.Count > 0) { - inst.Upgrade(plan.Upgrade, dl); + inst.Upgrade(plan.Upgrade, dl, ref possibleConfigOnlyDirs); plan.Upgrade.Clear(); } if (plan.Install.Count > 0) { @@ -72,7 +74,7 @@ public override void Run(Action process = null) plan.Install.Clear(); } if (plan.Replace.Count > 0) { - inst.Replace(AllReplacements(plan.Replace), resolvOpts, dl, true); + inst.Replace(AllReplacements(plan.Replace), resolvOpts, dl, ref possibleConfigOnlyDirs, true); } trans.Complete(); diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 1e31529ab7..cc94f2a19c 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -732,7 +732,7 @@ internal static void CopyZipEntry(ZipFile zipfile, ZipEntry entry, string fullPa /// This *DOES* save the registry. /// Preferred over Uninstall. /// - public void UninstallList(IEnumerable mods, bool ConfirmPrompt = true, IEnumerable installing = null) + public void UninstallList(IEnumerable mods, ref HashSet possibleConfigOnlyDirs, bool ConfirmPrompt = true, IEnumerable installing = null) { mods = mods.Memoize(); // Pre-check, have they even asked for things which are installed? @@ -788,7 +788,7 @@ public void UninstallList(IEnumerable mods, bool ConfirmPrompt = true, I foreach (string mod in goners) { User.RaiseMessage("Removing {0}...", mod); - Uninstall(mod); + Uninstall(mod, ref possibleConfigOnlyDirs); } // Enforce consistency if we're not installing anything, @@ -801,10 +801,10 @@ public void UninstallList(IEnumerable mods, bool ConfirmPrompt = true, I User.RaiseMessage("Done!\r\n"); } - public void UninstallList(string mod) + public void UninstallList(string mod, ref HashSet possibleConfigOnlyDirs) { var list = new List { mod }; - UninstallList(list); + UninstallList(list, ref possibleConfigOnlyDirs); } /// @@ -812,8 +812,9 @@ public void UninstallList(string mod) /// Use UninstallList for user queries, it also does dependency handling. /// This does *NOT* save the registry. /// - - private void Uninstall(string modName) + /// Identifier of module to uninstall + /// Directories that the user might want to remove after uninstall + private void Uninstall(string modName, ref HashSet possibleConfigOnlyDirs) { TxFileManager file_transaction = new TxFileManager(); @@ -870,8 +871,6 @@ private void Uninstall(string modName) log.DebugFormat("Removing {0}", file); file_transaction.Delete(path); - - } } catch (Exception ex) @@ -893,14 +892,25 @@ private void Uninstall(string modName) // before parents. GH #78. foreach (string directory in directoriesToDelete.OrderBy(dir => dir.Length).Reverse()) { - if (!Directory.EnumerateFileSystemEntries(directory).Any()) + log.DebugFormat("Checking {0}...", directory); + // It is bad if any of this directories gets removed + // So we protect them + // A few string comparisons will be cheaper than hitting the disk, so do this first + if (IsReservedDirectory(directory)) + { + log.DebugFormat("Directory {0} is reserved, skipping", directory); + continue; + } + + var contents = Directory + .EnumerateFileSystemEntries(directory, "*", SearchOption.AllDirectories) + .Select(f => ksp.ToRelativeGameDir(f)) + .Memoize(); + log.DebugFormat("Got contents: {0}", string.Join(", ", contents)); + var owners = contents.Select(f => registry_manager.registry.FileOwner(f)); + log.DebugFormat("Got owners: {0}", string.Join(", ", owners)); + if (!contents.Any()) { - // It is bad if any of this directories gets removed - // So we protect them - if (IsReservedDirectory(directory)) - { - continue; - } // We *don't* use our file_transaction to delete files here, because // it fails if the system's temp directory is on a different device @@ -914,6 +924,15 @@ private void Uninstall(string modName) log.DebugFormat("Removing {0}", directory); Directory.Delete(directory); } + else if (contents.All(f => registry_manager.registry.FileOwner(f) == null)) + { + log.DebugFormat("Directory {0} contains only non-registered files, ask user about it later"); + if (possibleConfigOnlyDirs == null) + { + possibleConfigOnlyDirs = new HashSet(); + } + possibleConfigOnlyDirs.Add(directory); + } else { log.InfoFormat("Not removing directory {0}, it's not empty", directory); @@ -986,7 +1005,7 @@ public HashSet AddParentDirectories(HashSet directories) /// /// Add. /// Remove. - public void AddRemove(IEnumerable add = null, IEnumerable remove = null, bool enforceConsistency = true) + public void AddRemove(ref HashSet possibleConfigOnlyDirs, IEnumerable add = null, IEnumerable remove = null, bool enforceConsistency = true) { // TODO: We should do a consistency check up-front, rather than relying // upon our registry catching inconsistencies at the end. @@ -1003,7 +1022,7 @@ public void AddRemove(IEnumerable add = null, IEnumerable add = null, IEnumerable - public void Upgrade(IEnumerable identifiers, IDownloader netAsyncDownloader, bool enforceConsistency = true) + public void Upgrade(IEnumerable identifiers, IDownloader netAsyncDownloader, ref HashSet possibleConfigOnlyDirs, bool enforceConsistency = true) { var resolver = new RelationshipResolver(identifiers.ToList(), null, RelationshipResolver.DependsOnlyOpts(), registry_manager.registry, ksp.VersionCriteria()); - Upgrade(resolver.ModList(), netAsyncDownloader, enforceConsistency); + Upgrade(resolver.ModList(), netAsyncDownloader, ref possibleConfigOnlyDirs, enforceConsistency); } /// @@ -1040,7 +1059,7 @@ public void Upgrade(IEnumerable identifiers, IDownloader netAsyncDownloa /// Will *re-install* or *downgrade* (with a warning) as well as upgrade. /// Throws ModuleNotFoundKraken if a module is not installed. /// - public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloader, bool enforceConsistency = true) + public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloader, ref HashSet possibleConfigOnlyDirs, bool enforceConsistency = true) { modules = modules.Memoize(); User.RaiseMessage("About to upgrade...\r\n"); @@ -1092,6 +1111,7 @@ public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloa } AddRemove( + ref possibleConfigOnlyDirs, modules, to_remove, enforceConsistency @@ -1104,7 +1124,7 @@ public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloa /// Will *re-install* or *downgrade* (with a warning) as well as upgrade. /// Throws ModuleNotFoundKraken if a module is not installed. /// - public void Replace(IEnumerable replacements, RelationshipResolverOptions options, IDownloader netAsyncDownloader, bool enforceConsistency = true) + public void Replace(IEnumerable replacements, RelationshipResolverOptions options, IDownloader netAsyncDownloader, ref HashSet possibleConfigOnlyDirs, bool enforceConsistency = true) { replacements = replacements.Memoize(); log.Debug("Using Replace method"); @@ -1181,6 +1201,7 @@ public void Replace(IEnumerable replacements, RelationshipRes { var resolvedModsToInstall = resolver.ModList().ToList(); AddRemove( + ref possibleConfigOnlyDirs, resolvedModsToInstall, modsToRemove, enforceConsistency diff --git a/Core/Net/Repo.cs b/Core/Net/Repo.cs index e1faa890cb..c81cde36e4 100644 --- a/Core/Net/Repo.cs +++ b/Core/Net/Repo.cs @@ -209,9 +209,11 @@ private static void HandleModuleChanges(List metadataChanges, IUser { try { + HashSet possibleConfigOnlyDirs = null; installer.Upgrade( new[] { changedIdentifier }, new NetAsyncModulesDownloader(new NullUser(), cache), + ref possibleConfigOnlyDirs, enforceConsistency: false ); } diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj index 8677bccddc..a460475170 100644 --- a/GUI/CKAN-GUI.csproj +++ b/GUI/CKAN-GUI.csproj @@ -113,6 +113,12 @@ + + UserControl + + + DeleteDirectories.cs + Component @@ -170,6 +176,9 @@ Form + + Form + Form @@ -298,6 +307,12 @@ ..\..\CompatibleKspVersionsDialog.cs + + DeleteDirectories.cs + + + ..\..\DeleteDirectories.cs + EditLabelsDialog.cs diff --git a/GUI/DeleteDirectories.Designer.cs b/GUI/DeleteDirectories.Designer.cs new file mode 100644 index 0000000000..5ae322499f --- /dev/null +++ b/GUI/DeleteDirectories.Designer.cs @@ -0,0 +1,205 @@ +namespace CKAN +{ + partial class DeleteDirectories + { + /// + /// 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(DeleteDirectories)); + this.ExplanationLabel = new System.Windows.Forms.Label(); + this.Splitter = new System.Windows.Forms.SplitContainer(); + this.DirectoriesListView = new ThemedListView(); + this.DirectoryColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.ContentsListView = new ThemedListView(); + this.FileColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.SelectDirPrompt = new System.Windows.Forms.ListViewItem(); + this.BottomButtonPanel = new System.Windows.Forms.Panel(); + this.OpenDirectoryButton = new System.Windows.Forms.Button(); + this.KeepAllButton = new System.Windows.Forms.Button(); + this.DeleteButton = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.Splitter)).BeginInit(); + this.Splitter.Panel1.SuspendLayout(); + this.Splitter.Panel2.SuspendLayout(); + this.Splitter.SuspendLayout(); + this.SuspendLayout(); + // + // ExplanationLabel + // + this.ExplanationLabel.Dock = System.Windows.Forms.DockStyle.Top; + this.ExplanationLabel.Font = new System.Drawing.Font(System.Drawing.SystemFonts.DefaultFont.Name, 12, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel); + this.ExplanationLabel.Location = new System.Drawing.Point(5, 0); + this.ExplanationLabel.Name = "ExplanationLabel"; + this.ExplanationLabel.Padding = new System.Windows.Forms.Padding(5,5,5,5); + this.ExplanationLabel.Size = new System.Drawing.Size(490, 70); + resources.ApplyResources(this.ExplanationLabel, "ExplanationLabel"); + // + // Splitter + // + this.Splitter.Dock = System.Windows.Forms.DockStyle.Fill; + this.Splitter.Location = new System.Drawing.Point(5, 70); + this.Splitter.Margin = new System.Windows.Forms.Padding(0,0,0,0); + this.Splitter.Name = "Splitter"; + this.Splitter.Size = new System.Drawing.Size(490, 385); + this.Splitter.SplitterDistance = 250; + this.Splitter.SplitterWidth = 10; + this.Splitter.TabIndex = 0; + // + // Splitter.Panel1 + // + this.Splitter.Panel1.Controls.Add(this.DirectoriesListView); + this.Splitter.Panel1MinSize = 200; + // + // Splitter.Panel2 + // + this.Splitter.Panel2.Controls.Add(this.ContentsListView); + this.Splitter.Panel2MinSize = 200; + // + // DirectoriesListView + // + this.DirectoriesListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.DirectoryColumn}); + this.DirectoriesListView.CheckBoxes = true; + this.DirectoriesListView.FullRowSelect = true; + this.DirectoriesListView.HideSelection = false; + this.DirectoriesListView.Location = new System.Drawing.Point(0, 0); + this.DirectoriesListView.Name = "DirectoriesListView"; + this.DirectoriesListView.Size = new System.Drawing.Size(230, 455); + this.DirectoriesListView.TabIndex = 0; + this.DirectoriesListView.UseCompatibleStateImageBehavior = false; + this.DirectoriesListView.View = System.Windows.Forms.View.Details; + this.DirectoriesListView.Dock = System.Windows.Forms.DockStyle.Fill; + this.DirectoriesListView.ItemSelectionChanged += new System.Windows.Forms.ListViewItemSelectionChangedEventHandler(DirectoriesListView_ItemSelectionChanged); + // + // DirectoryColumn + // + this.DirectoryColumn.Width = -1; + resources.ApplyResources(this.DirectoryColumn, "DirectoryColumn"); + // + // ContentsListView + // + this.ContentsListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.FileColumn}); + this.ContentsListView.FullRowSelect = true; + this.ContentsListView.Location = new System.Drawing.Point(0, 0); + this.ContentsListView.Name = "ContentsListView"; + this.ContentsListView.Size = new System.Drawing.Size(230, 455); + this.ContentsListView.TabIndex = 1; + this.ContentsListView.UseCompatibleStateImageBehavior = false; + this.ContentsListView.View = System.Windows.Forms.View.Details; + this.ContentsListView.Dock = System.Windows.Forms.DockStyle.Fill; + // + // FileColumn + // + this.FileColumn.Width = -1; + resources.ApplyResources(this.FileColumn, "FileColumn"); + // + // SelectDirPrompt + // + resources.ApplyResources(this.SelectDirPrompt, "SelectDirPrompt"); + // + // BottomButtonPanel + // + this.BottomButtonPanel.Controls.Add(this.OpenDirectoryButton); + this.BottomButtonPanel.Controls.Add(this.KeepAllButton); + this.BottomButtonPanel.Controls.Add(this.DeleteButton); + this.BottomButtonPanel.Dock = System.Windows.Forms.DockStyle.Bottom; + this.BottomButtonPanel.Name = "BottomButtonPanel"; + this.BottomButtonPanel.Size = new System.Drawing.Size(500, 40); + // + // OpenDirectoryButton + // + this.OpenDirectoryButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom + | System.Windows.Forms.AnchorStyles.Left)); + this.OpenDirectoryButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.OpenDirectoryButton.Location = new System.Drawing.Point(5, 5); + this.OpenDirectoryButton.Name = "OpenDirectoryButton"; + this.OpenDirectoryButton.Size = new System.Drawing.Size(112, 30); + this.OpenDirectoryButton.TabIndex = 2; + this.OpenDirectoryButton.UseVisualStyleBackColor = true; + this.OpenDirectoryButton.Click += new System.EventHandler(this.OpenDirectoryButton_Click); + resources.ApplyResources(this.OpenDirectoryButton, "OpenDirectoryButton"); + // + // KeepAllButton + // + this.KeepAllButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom + | System.Windows.Forms.AnchorStyles.Right)); + this.KeepAllButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.KeepAllButton.Location = new System.Drawing.Point(266, 5); + this.KeepAllButton.Name = "KeepAllButton"; + this.KeepAllButton.Size = new System.Drawing.Size(112, 30); + this.KeepAllButton.TabIndex = 3; + this.KeepAllButton.UseVisualStyleBackColor = true; + this.KeepAllButton.Click += new System.EventHandler(this.KeepAllButton_Click); + resources.ApplyResources(this.KeepAllButton, "KeepAllButton"); + // + // DeleteButton + // + this.DeleteButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom + | System.Windows.Forms.AnchorStyles.Right)); + this.DeleteButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.DeleteButton.Location = new System.Drawing.Point(383, 5); + this.DeleteButton.Name = "DeleteButton"; + this.DeleteButton.Size = new System.Drawing.Size(112, 30); + this.DeleteButton.TabIndex = 4; + this.DeleteButton.UseVisualStyleBackColor = true; + this.DeleteButton.Click += new System.EventHandler(this.DeleteButton_Click); + resources.ApplyResources(this.DeleteButton, "DeleteButton"); + // + // DeleteDirectories + // + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; + this.Controls.Add(this.Splitter); + this.Controls.Add(this.ExplanationLabel); + this.Controls.Add(this.BottomButtonPanel); + this.Margin = new System.Windows.Forms.Padding(0,0,0,0); + this.Padding = new System.Windows.Forms.Padding(0,0,0,0); + this.Name = "DeleteDirectories"; + this.Size = new System.Drawing.Size(500, 500); + resources.ApplyResources(this, "$this"); + this.Splitter.Panel1.ResumeLayout(false); + this.Splitter.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.Splitter)).EndInit(); + this.Splitter.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + } + + #endregion + + private System.Windows.Forms.Label ExplanationLabel; + private System.Windows.Forms.SplitContainer Splitter; + private System.Windows.Forms.ListView DirectoriesListView; + private System.Windows.Forms.ColumnHeader DirectoryColumn; + private System.Windows.Forms.ListView ContentsListView; + private System.Windows.Forms.ColumnHeader FileColumn; + private System.Windows.Forms.ListViewItem SelectDirPrompt; + private System.Windows.Forms.Panel BottomButtonPanel; + private System.Windows.Forms.Button OpenDirectoryButton; + private System.Windows.Forms.Button KeepAllButton; + private System.Windows.Forms.Button DeleteButton; + } +} diff --git a/GUI/DeleteDirectories.cs b/GUI/DeleteDirectories.cs new file mode 100644 index 0000000000..64e793521d --- /dev/null +++ b/GUI/DeleteDirectories.cs @@ -0,0 +1,125 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Windows.Forms; +using CKAN.Extensions; + +namespace CKAN +{ + public partial class DeleteDirectories : UserControl + { + public DeleteDirectories() + { + InitializeComponent(); + } + + /// + /// Set up the display for interaction. + /// This is separate from Wait so we can set up + /// before the calling code switches to the tab. + /// + /// Directories that the user may want to delete + public void LoadDirs(KSP ksp, HashSet possibleConfigOnlyDirs) + { + instance = ksp; + var items = possibleConfigOnlyDirs + .OrderBy(d => d) + .Select(d => new ListViewItem(instance.ToRelativeGameDir(d).Replace('/', Path.DirectorySeparatorChar)) + { + Tag = d, + Checked = true + }) + .ToArray(); + Util.Invoke(this, () => + { + DirectoriesListView.Items.Clear(); + DirectoriesListView.Items.AddRange(items); + DirectoriesListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); + DirectoriesListView_ItemSelectionChanged(null, null); + }); + } + + /// + /// Allow the user to interact with the control, + /// return after they click one of the action buttons. + /// + /// The directories to delete if the return is true + /// + /// true if user chose to delete, false otherwise + /// + public bool Wait(out HashSet toDelete) + { + if (Platform.IsMono) + { + // Workaround: make sure the ListView headers are drawn + Util.Invoke(DirectoriesListView, () => + { + DirectoriesListView.EndUpdate(); + ContentsListView.EndUpdate(); + }); + } + + // Reset the task each time + task = new TaskCompletionSource(); + // This will block until one of the buttons calls SetResult + if (task.Task.Result) + { + toDelete = DirectoriesListView.CheckedItems.Cast() + .Select(lvi => lvi.Tag as string) + .Where(s => !string.IsNullOrEmpty(s)) + .ToHashSet(); + return true; + } + else + { + toDelete = null; + return false; + } + } + + private void DirectoriesListView_ItemSelectionChanged(Object sender, ListViewItemSelectionChangedEventArgs e) + { + ContentsListView.Items.Clear(); + ContentsListView.Items.AddRange( + DirectoriesListView.SelectedItems.Cast() + .SelectMany(lvi => Directory.EnumerateFileSystemEntries( + lvi.Tag as string, + "*", + SearchOption.AllDirectories) + .Select(f => new ListViewItem(instance.ToRelativeGameDir(f).Replace('/', Path.DirectorySeparatorChar)))) + .ToArray()); + if (DirectoriesListView.SelectedItems.Count == 0) + { + ContentsListView.Items.Add(SelectDirPrompt); + } + ContentsListView.AutoResizeColumns( + ContentsListView.Items.Count > 0 + ? ColumnHeaderAutoResizeStyle.ColumnContent + : ColumnHeaderAutoResizeStyle.HeaderSize); + OpenDirectoryButton.Enabled = DirectoriesListView.SelectedItems.Count > 0; + } + + private void OpenDirectoryButton_Click(object sender, EventArgs e) + { + foreach (ListViewItem lvi in DirectoriesListView.SelectedItems) + { + Utilities.ProcessStartURL(lvi.Tag as string); + } + } + + private void DeleteButton_Click(object sender, EventArgs e) + { + task?.SetResult(true); + } + + private void KeepAllButton_Click(object sender, EventArgs e) + { + task?.SetResult(false); + } + + private TaskCompletionSource task = null; + private KSP instance = null; + } +} diff --git a/GUI/DeleteDirectories.resx b/GUI/DeleteDirectories.resx new file mode 100644 index 0000000000..27c463582a --- /dev/null +++ b/GUI/DeleteDirectories.resx @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + The below directories are leftover after removing some mods. They contain files that were not installed by CKAN (probably either generated by a mod or manually installed). CKAN does not automatically delete files it did not install, but you can choose to remove them if it looks safe to do so (recommended). +Note that if you decide not to remove a directory, ModuleManager may incorrectly think that mod is still installed. + Directories + Directory Contents + Click a directory at the left to see its contents + Open Directory + Delete Checked + Keep All + diff --git a/GUI/EditLabelsDialog.resx b/GUI/EditLabelsDialog.resx index 63d193f16f..a140974a17 100644 --- a/GUI/EditLabelsDialog.resx +++ b/GUI/EditLabelsDialog.resx @@ -132,7 +132,7 @@ Remove on install Close Save - Cancel + Cancel Delete Export... Edit Labels diff --git a/GUI/Localization/de-DE/DeleteDirectories.de-DE.resx b/GUI/Localization/de-DE/DeleteDirectories.de-DE.resx new file mode 100644 index 0000000000..9cc67c76f2 --- /dev/null +++ b/GUI/Localization/de-DE/DeleteDirectories.de-DE.resx @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + Die folgenden Verzeichnisse sind nach dem Entfernen einiger Mods übrig geblieben. Sie enthalten Dateien, die nicht von CKAN installiert wurden (wahrscheinlich entweder durch eine Mod generiert oder manuell installiert). CKAN löscht Dateien, die es nicht installiert hat, nicht automatisch. Du kannst sie dennoch entfernen lassen, wenn es dir sicher erscheint (empfohlen). +Beachte, dass ModuleManager fälschlicherweise denken könnte, dass die Mod noch installiert ist, wenn ein Verzeichnis nicht gelöscht wird. + Verzeichnisse + Verzeichnisinhalt + Klicke auf der linken Seite auf ein Verzeichnis, um dessen Inhalt anzuzeigen + Verzeichnis öffnen + Markierte löschen + Alles behalten + diff --git a/GUI/Localization/de-DE/EditLabelsDialog.de-DE.resx b/GUI/Localization/de-DE/EditLabelsDialog.de-DE.resx index ca4c5adf88..cbdc336fb6 100644 --- a/GUI/Localization/de-DE/EditLabelsDialog.de-DE.resx +++ b/GUI/Localization/de-DE/EditLabelsDialog.de-DE.resx @@ -132,7 +132,7 @@ Label nach der Installation entfernen Schließen Speichern - Abbrechen + Abbrechen Löschen Labels bearbeiten diff --git a/GUI/Main.Designer.cs b/GUI/Main.Designer.cs index 8666a0ff77..d7197d2a6a 100644 --- a/GUI/Main.Designer.cs +++ b/GUI/Main.Designer.cs @@ -158,6 +158,8 @@ private void InitializeComponent() this.toolStripSeparator6 = new System.Windows.Forms.ToolStripSeparator(); this.toolStripSeparator7 = new System.Windows.Forms.ToolStripSeparator(); this.quitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.DeleteDirectoriesTabPage = new System.Windows.Forms.TabPage(); + this.DeleteDirectories = new CKAN.DeleteDirectories(); this.menuStrip1.SuspendLayout(); this.menuStrip2.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); @@ -175,6 +177,7 @@ private void InitializeComponent() this.ChooseRecommendedModsTabPage.SuspendLayout(); this.ChooseProvidedModsTabPage.SuspendLayout(); this.minimizedContextMenuStrip.SuspendLayout(); + this.DeleteDirectoriesTabPage.SuspendLayout(); this.SuspendLayout(); // // menuStrip1 @@ -835,6 +838,7 @@ private void InitializeComponent() this.MainTabControl.Controls.Add(this.WaitTabPage); this.MainTabControl.Controls.Add(this.ChooseRecommendedModsTabPage); this.MainTabControl.Controls.Add(this.ChooseProvidedModsTabPage); + this.MainTabControl.Controls.Add(this.DeleteDirectoriesTabPage); this.MainTabControl.Dock = System.Windows.Forms.DockStyle.Fill; this.MainTabControl.Location = new System.Drawing.Point(0, 35); this.MainTabControl.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); @@ -1294,6 +1298,28 @@ private void InitializeComponent() this.ChooseProvidedModsLabel.TabIndex = 30; resources.ApplyResources(this.ChooseProvidedModsLabel, "ChooseProvidedModsLabel"); // + // DeleteDirectoriesTabPage + // + this.DeleteDirectoriesTabPage.BackColor = System.Drawing.SystemColors.Control; + this.DeleteDirectoriesTabPage.Controls.Add(this.DeleteDirectories); + this.DeleteDirectoriesTabPage.Location = new System.Drawing.Point(0, 0); + this.DeleteDirectoriesTabPage.Margin = new System.Windows.Forms.Padding(0,0,0,0); + this.DeleteDirectoriesTabPage.Name = "DeleteDirectoriesTabPage"; + this.DeleteDirectoriesTabPage.Padding = new System.Windows.Forms.Padding(0,0,0,0); + this.DeleteDirectoriesTabPage.Size = new System.Drawing.Size(500, 500); + this.DeleteDirectoriesTabPage.TabIndex = 31; + resources.ApplyResources(this.DeleteDirectoriesTabPage, "DeleteDirectoriesTabPage"); + // + // DeleteDirectories + // + this.DeleteDirectories.Dock = System.Windows.Forms.DockStyle.Fill; + this.DeleteDirectories.Location = new System.Drawing.Point(0, 0); + this.DeleteDirectories.Margin = new System.Windows.Forms.Padding(0,0,0,0); + this.DeleteDirectories.Padding = new System.Windows.Forms.Padding(0,0,0,0); + this.DeleteDirectories.Name = "DeleteDirectories"; + this.DeleteDirectories.Size = new System.Drawing.Size(500, 500); + this.DeleteDirectories.TabIndex = 32; + // // minimizeNotifyIcon // this.minimizeNotifyIcon.ContextMenuStrip = this.minimizedContextMenuStrip; @@ -1433,6 +1459,8 @@ private void InitializeComponent() this.ChooseRecommendedModsTabPage.PerformLayout(); this.ChooseProvidedModsTabPage.ResumeLayout(false); this.ChooseProvidedModsTabPage.PerformLayout(); + this.DeleteDirectoriesTabPage.ResumeLayout(false); + this.DeleteDirectoriesTabPage.PerformLayout(); this.minimizedContextMenuStrip.ResumeLayout(false); this.ResumeLayout(false); this.PerformLayout(); @@ -1555,6 +1583,8 @@ private void InitializeComponent() private System.Windows.Forms.ColumnHeader columnHeader6; private System.Windows.Forms.ColumnHeader columnHeader8; private System.Windows.Forms.Label ChooseProvidedModsLabel; + private System.Windows.Forms.TabPage DeleteDirectoriesTabPage; + private CKAN.DeleteDirectories DeleteDirectories; private System.Windows.Forms.NotifyIcon minimizeNotifyIcon; private System.Windows.Forms.ContextMenuStrip minimizedContextMenuStrip; private System.Windows.Forms.ToolStripMenuItem updatesToolStripMenuItem; diff --git a/GUI/Main.resx b/GUI/Main.resx index 55e6982dcb..a8c2b945d5 100644 --- a/GUI/Main.resx +++ b/GUI/Main.resx @@ -254,6 +254,7 @@ Mod Mod description Several mods provide the virtual module Foo, choose one of the following mods: + Delete Directories CKAN N available updates Refresh diff --git a/GUI/MainDialogs.cs b/GUI/MainDialogs.cs index a9071e1fa2..9159d793dc 100644 --- a/GUI/MainDialogs.cs +++ b/GUI/MainDialogs.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Threading.Tasks; using System.Windows.Forms; @@ -46,45 +45,5 @@ public int SelectionDialog(string message, params object[] args) { return selectionDialog.ShowSelectionDialog(message, args); } - - // Ugly Hack. Possible fix is to alter the relationship provider so we can use a loop - // over reason for to find a user requested mod. Or, you know, pass in a handler to it. - private readonly ConcurrentStack last_mod_to_have_install_toggled = new ConcurrentStack(); - - private async Task TooManyModsProvideCore(TooManyModsProvideKraken kraken) - { - TaskCompletionSource task = new TaskCompletionSource(); - Util.Invoke(this, () => - { - UpdateProvidedModsDialog(kraken, task); - tabController.ShowTab("ChooseProvidedModsTabPage", 3); - tabController.SetTabLock(true); - }); - return await task.Task; - } - - public async Task TooManyModsProvide(TooManyModsProvideKraken kraken) - { - // We want LMtHIT to be the last user selection. If we alter this handling a too many provides - // it needs to be reset so a potential second too many provides doesn't use the wrong mod. - GUIMod mod; - - var module = await TooManyModsProvideCore(kraken); - - if (module == null - && last_mod_to_have_install_toggled.TryPeek(out mod)) - { - MarkModForInstall(mod.Identifier, true); - } - Util.Invoke(this, () => - { - tabController.SetTabLock(false); - tabController.HideTab("ChooseProvidedModsTabPage"); - tabController.ShowTab("ManageModsTabPage"); - }); - - last_mod_to_have_install_toggled.TryPop(out mod); - return module; - } } } diff --git a/GUI/MainInstall.cs b/GUI/MainInstall.cs index c602bef0c7..b25f8d2f09 100644 --- a/GUI/MainInstall.cs +++ b/GUI/MainInstall.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Diagnostics; using System.Collections.Generic; using System.ComponentModel; @@ -77,7 +78,7 @@ private void InstallMods(object sender, DoWorkEventArgs e) var opts = (KeyValuePair) e.Argument; - IRegistryQuerier registry = RegistryManager.Instance(manager.CurrentInstance).registry; + Registry registry = RegistryManager.Instance(manager.CurrentInstance).registry; ModuleInstaller installer = ModuleInstaller.GetInstance(CurrentInstance, Manager.Cache, GUI.user); // Avoid accumulating multiple event handlers installer.onReportModInstalled -= OnModInstalled; @@ -146,6 +147,8 @@ private void InstallMods(object sender, DoWorkEventArgs e) installCanceled = true; }; + HashSet possibleConfigOnlyDirs = null; + // checks if all actions were successfull bool processSuccessful = false; bool resolvedAllProvidedMods = false; @@ -161,7 +164,7 @@ private void InstallMods(object sender, DoWorkEventArgs e) processSuccessful = false; if (!installCanceled) { - installer.UninstallList(toUninstall, false, toInstall.Select(m => m.identifier)); + installer.UninstallList(toUninstall, ref possibleConfigOnlyDirs, false, toInstall.Select(m => m.identifier)); processSuccessful = true; } } @@ -170,7 +173,7 @@ private void InstallMods(object sender, DoWorkEventArgs e) processSuccessful = false; if (!installCanceled) { - installer.Upgrade(toUpgrade, downloader); + installer.Upgrade(toUpgrade, downloader, ref possibleConfigOnlyDirs); processSuccessful = true; } } @@ -183,6 +186,9 @@ private void InstallMods(object sender, DoWorkEventArgs e) processSuccessful = true; } } + + HandlePossibleConfigOnlyDirs(registry, possibleConfigOnlyDirs); + e.Result = new KeyValuePair(processSuccessful, opts.Key); if (installCanceled) { @@ -307,6 +313,46 @@ private void InstallMods(object sender, DoWorkEventArgs e) } } + private void HandlePossibleConfigOnlyDirs(Registry registry, HashSet possibleConfigOnlyDirs) + { + if (possibleConfigOnlyDirs != null) + { + // Check again for registered files, since we may + // just have installed or upgraded some + possibleConfigOnlyDirs.RemoveWhere( + d => Directory.EnumerateFileSystemEntries(d, "*", SearchOption.AllDirectories) + .Any(f => registry.FileOwner(CurrentInstance.ToRelativeGameDir(f)) != null)); + if (possibleConfigOnlyDirs.Count > 0) + { + AddStatusMessage(""); + tabController.ShowTab("DeleteDirectoriesTabPage", 4); + tabController.SetTabLock(true); + + DeleteDirectories.LoadDirs(CurrentInstance, possibleConfigOnlyDirs); + + // Wait here for the GUI process to finish dealing with the user + if (DeleteDirectories.Wait(out HashSet toDelete)) + { + foreach (string dir in toDelete) + { + try + { + Directory.Delete(dir, true); + } + catch + { + // Don't worry if it doesn't work, just keep going + } + } + } + + tabController.ShowTab("WaitTabPage"); + tabController.HideTab("DeleteDirectoriesTabPage"); + tabController.SetTabLock(false); + } + } + } + private void OnModInstalled(CkanModule mod) { AddStatusMessage(string.Format(Properties.Resources.MainInstallModSuccess, mod.name)); @@ -372,70 +418,5 @@ private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e) Util.Invoke(menuStrip1, () => menuStrip1.Enabled = true); } - private TaskCompletionSource toomany_source; - private void UpdateProvidedModsDialog(TooManyModsProvideKraken tooManyProvides, TaskCompletionSource task) - { - toomany_source = task; - ChooseProvidedModsLabel.Text = String.Format( - Properties.Resources.MainInstallProvidedBy, - tooManyProvides.requested - ); - - ChooseProvidedModsListView.Items.Clear(); - - ChooseProvidedModsListView.ItemChecked += ChooseProvidedModsListView_ItemChecked; - - foreach (CkanModule module in tooManyProvides.modules) - { - ChooseProvidedModsListView.Items.Add(new ListViewItem(new string[] - { - Manager.Cache.IsMaybeCachedZip(module) - ? string.Format(Properties.Resources.MainChangesetCached, module.name, module.version) - : string.Format(Properties.Resources.MainChangesetHostSize, module.name, module.version, module.download.Host ?? "", CkanModule.FmtSize(module.download_size)), - module.@abstract - }) - { - Tag = module, - Checked = false - }); - } - ChooseProvidedModsListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); - ChooseProvidedModsContinueButton.Enabled = false; - } - - - private void ChooseProvidedModsListView_ItemChecked(object sender, ItemCheckedEventArgs e) - { - var any_item_selected = ChooseProvidedModsListView.Items.Cast().Any(item => item.Checked); - ChooseProvidedModsContinueButton.Enabled = any_item_selected; - if (!e.Item.Checked) - { - return; - } - - foreach (ListViewItem item in ChooseProvidedModsListView.Items.Cast() - .Where(item => item != e.Item && item.Checked)) - { - item.Checked = false; - } - - } - - private void ChooseProvidedModsCancelButton_Click(object sender, EventArgs e) - { - toomany_source.SetResult(null); - } - - private void ChooseProvidedModsContinueButton_Click(object sender, EventArgs e) - { - foreach (ListViewItem item in ChooseProvidedModsListView.Items) - { - if (item.Checked) - { - toomany_source.SetResult((CkanModule)item.Tag); - } - } - } - } } diff --git a/GUI/MainProvides.cs b/GUI/MainProvides.cs new file mode 100644 index 0000000000..9dad8561ff --- /dev/null +++ b/GUI/MainProvides.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Concurrent; +using System.Windows.Forms; + +namespace CKAN +{ + public partial class Main + { + // Ugly Hack. Possible fix is to alter the relationship provider so we can use a loop + // over reason for to find a user requested mod. Or, you know, pass in a handler to it. + private readonly ConcurrentStack last_mod_to_have_install_toggled = new ConcurrentStack(); + + public async Task TooManyModsProvide(TooManyModsProvideKraken kraken) + { + // We want LMtHIT to be the last user selection. If we alter this handling a too many provides + // it needs to be reset so a potential second too many provides doesn't use the wrong mod. + GUIMod mod; + + var module = await TooManyModsProvideCore(kraken); + + if (module == null + && last_mod_to_have_install_toggled.TryPeek(out mod)) + { + MarkModForInstall(mod.Identifier, true); + } + Util.Invoke(this, () => + { + tabController.SetTabLock(false); + tabController.HideTab("ChooseProvidedModsTabPage"); + tabController.ShowTab("ManageModsTabPage"); + }); + + last_mod_to_have_install_toggled.TryPop(out mod); + return module; + } + + private async Task TooManyModsProvideCore(TooManyModsProvideKraken kraken) + { + TaskCompletionSource task = new TaskCompletionSource(); + Util.Invoke(this, () => + { + UpdateProvidedModsDialog(kraken, task); + tabController.ShowTab("ChooseProvidedModsTabPage", 3); + tabController.SetTabLock(true); + }); + return await task.Task; + } + + private TaskCompletionSource toomany_source; + + private void UpdateProvidedModsDialog(TooManyModsProvideKraken tooManyProvides, TaskCompletionSource task) + { + toomany_source = task; + ChooseProvidedModsLabel.Text = String.Format( + Properties.Resources.MainInstallProvidedBy, + tooManyProvides.requested + ); + + ChooseProvidedModsListView.Items.Clear(); + + ChooseProvidedModsListView.ItemChecked += ChooseProvidedModsListView_ItemChecked; + + foreach (CkanModule module in tooManyProvides.modules) + { + ChooseProvidedModsListView.Items.Add(new ListViewItem(new string[] + { + Manager.Cache.IsMaybeCachedZip(module) + ? string.Format(Properties.Resources.MainChangesetCached, module.name, module.version) + : string.Format(Properties.Resources.MainChangesetHostSize, module.name, module.version, module.download.Host ?? "", CkanModule.FmtSize(module.download_size)), + module.@abstract + }) + { + Tag = module, + Checked = false + }); + } + ChooseProvidedModsListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); + ChooseProvidedModsContinueButton.Enabled = false; + } + + private void ChooseProvidedModsListView_ItemChecked(object sender, ItemCheckedEventArgs e) + { + var any_item_selected = ChooseProvidedModsListView.Items.Cast().Any(item => item.Checked); + ChooseProvidedModsContinueButton.Enabled = any_item_selected; + if (!e.Item.Checked) + { + return; + } + + foreach (ListViewItem item in ChooseProvidedModsListView.Items.Cast() + .Where(item => item != e.Item && item.Checked)) + { + item.Checked = false; + } + } + + private void ChooseProvidedModsCancelButton_Click(object sender, EventArgs e) + { + toomany_source.SetResult(null); + } + + private void ChooseProvidedModsContinueButton_Click(object sender, EventArgs e) + { + foreach (ListViewItem item in ChooseProvidedModsListView.Items) + { + if (item.Checked) + { + toomany_source.SetResult((CkanModule)item.Tag); + } + } + } + + } +} diff --git a/GUI/ThemedTabControl.cs b/GUI/ThemedTabControl.cs index 755e017f41..f58cdfaf05 100644 --- a/GUI/ThemedTabControl.cs +++ b/GUI/ThemedTabControl.cs @@ -19,7 +19,7 @@ protected override void OnDrawItem(DrawItemEventArgs e) { // Background Rectangle bgRect = e.Bounds; - bgRect.Inflate(-2, 0); + bgRect.Inflate(-2, -1); bgRect.Offset(0, 1); e.Graphics.FillRectangle(new SolidBrush(BackColor), bgRect); // Text diff --git a/Tests/Core/ModuleInstaller.cs b/Tests/Core/ModuleInstaller.cs index 2707aea896..cc3ea26cd5 100644 --- a/Tests/Core/ModuleInstaller.cs +++ b/Tests/Core/ModuleInstaller.cs @@ -438,8 +438,9 @@ public void UninstallModNotFound() Assert.Throws(delegate { + HashSet possibleConfigOnlyDirs = null; // This should throw, as our tidy KSP has no mods installed. - CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).UninstallList("Foo"); + CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).UninstallList("Foo", ref possibleConfigOnlyDirs); }); manager.CurrentInstance = null; // I weep even more. @@ -560,7 +561,8 @@ public void CanUninstallMod() Assert.IsTrue(File.Exists(mod_file_path)); // Attempt to uninstall it. - CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).UninstallList(modules); + HashSet possibleConfigOnlyDirs = null; + CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).UninstallList(modules, ref possibleConfigOnlyDirs); // Check that the module is not installed. Assert.IsFalse(File.Exists(mod_file_path)); @@ -616,7 +618,8 @@ public void UninstallEmptyDirs() modules.Add(TestData.DogeCoinFlag_101_module().identifier); modules.Add(TestData.DogeCoinPlugin_module().identifier); - CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).UninstallList(modules); + HashSet possibleConfigOnlyDirs = null; + CKAN.ModuleInstaller.GetInstance(manager.CurrentInstance, manager.Cache, nullUser).UninstallList(modules, ref possibleConfigOnlyDirs); // Check that the directory has been deleted. Assert.IsFalse(Directory.Exists(directoryPath));