From 7df511866d6ba4a9ca4a6866d374b8d684f77fe5 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Sun, 5 May 2019 20:28:59 +0000 Subject: [PATCH] Auto-uninstall auto-installed modules --- Core/ModuleInstaller.cs | 65 ++++++++-------------- Core/Registry/IRegistryQuerier.cs | 11 ++++ Core/Registry/InstalledModule.cs | 11 +++- Core/Registry/Registry.cs | 45 ++++++++++++++- Core/Relationships/RelationshipResolver.cs | 8 +++ GUI/GUIMod.cs | 30 ++++++++-- GUI/Main.Designer.cs | 11 ++++ GUI/Main.cs | 28 +++++++--- GUI/MainModList.cs | 56 ++++++++++++++----- Tests/GUI/GH1866.cs | 2 +- Tests/GUI/GUIMod.cs | 2 +- Tests/GUI/MainModList.cs | 2 +- 12 files changed, 195 insertions(+), 76 deletions(-) diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 0142081cd4..0de428fafd 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -136,7 +136,8 @@ public static string CachedOrDownload(CkanModule module, NetModuleCache cache, s public void InstallList(List modules, RelationshipResolverOptions options, IDownloader downloader = null) { var resolver = new RelationshipResolver(modules, null, options, registry_manager.registry, ksp.VersionCriteria()); - InstallList(resolver.ModList().ToList(), options, downloader); + // Only pass the CkanModules of the parameters, so we can tell which are auto + InstallList(resolver.ModList().Where(m => modules.Contains(m.identifier)).ToList(), options, downloader); } /// @@ -214,7 +215,7 @@ public void InstallList(ICollection modules, RelationshipResolverOpt User.RaiseProgress(String.Format("Installing mod \"{0}\"", modsToInstall[i]), percent_complete); - Install(modsToInstall[i]); + Install(modsToInstall[i], resolver.ReasonFor(modsToInstall[i]) is SelectionReason.Depends); } User.RaiseProgress("Updating registry", 70); @@ -243,31 +244,6 @@ public void InstallList(ICollection modules, RelationshipResolverOpt User.RaiseProgress("Done!", 100); } - public void InstallList(ModuleResolution modules, RelationshipResolverOptions options) - { - // We're about to install all our mods; so begin our transaction. - using (TransactionScope transaction = CkanTransaction.CreateTransactionScope()) - { - var enumeratedMods = modules.Select((m, i) => new { Idx = i, Module = m }); - foreach (var item in enumeratedMods) - { - var percentComplete = (item.Idx * 100) / modules.Count; - User.RaiseProgress(string.Format("Installing mod \"{0}\"", item.Module), percentComplete); - Install(item.Module); - } - - User.RaiseProgress("Updating registry", 70); - - registry_manager.Save(!options.without_enforce_consistency); - - User.RaiseProgress("Committing filesystem changes", 80); - - transaction.Complete(); - - EnforceCacheSizeLimit(); - } - } - /// /// Returns the module contents if and only if we have it /// available in our cache. Returns null, otherwise. @@ -291,7 +267,7 @@ public IEnumerable GetModuleContentsList(CkanModule module) /// /// Install our mod from the filename supplied. /// If no file is supplied, we will check the cache or throw FileNotFoundKraken. - /// Does *not* resolve dependencies; this actually does the heavy listing. + /// Does *not* resolve dependencies; this does the heavy lifting. /// Does *not* save the registry. /// Do *not* call this directly, use InstallList() instead. /// @@ -299,11 +275,9 @@ public IEnumerable GetModuleContentsList(CkanModule module) /// Propagates a FileExistsKraken if we were going to overwrite a file. /// Throws a FileNotFoundKraken if we can't find the downloaded module. /// + /// TODO: The name of this and InstallModule() need to be made more distinctive. /// - // - // TODO: The name of this and InstallModule() need to be made more distinctive. - - private void Install(CkanModule module, string filename = null) + private void Install(CkanModule module, bool autoInstalled, string filename = null) { CheckMetapackageInstallationKraken(module); @@ -337,7 +311,7 @@ private void Install(CkanModule module, string filename = null) IEnumerable files = InstallModule(module, filename); // Register our module and its files. - registry.RegisterModule(module, files, ksp); + registry.RegisterModule(module, files, ksp, autoInstalled); // Finish our transaction, but *don't* save the registry; we may be in an // intermediate, inconsistent state. @@ -762,9 +736,13 @@ public void UninstallList(IEnumerable mods, bool ConfirmPrompt = true, I } // Find all the things which need uninstalling. - IEnumerable goners = mods.Union( + IEnumerable revdep = mods.Union( registry_manager.registry.FindReverseDependencies( mods.Except(installing ?? new string[] {}))); + IEnumerable goners = revdep.Union( + registry_manager.registry.FindRemovableAutoInstalled( + registry_manager.registry.InstalledModules.Where(im => !revdep.Contains(im.identifier)) + ).Select(im => im.identifier)); // If there us nothing to uninstall, skip out. if (!goners.Any()) @@ -999,7 +977,7 @@ public HashSet AddParentDirectories(HashSet directories) /// /// Add. /// Remove. - public void AddRemove(IEnumerable add = null, IEnumerable remove = null, bool enforceConsistency = true) + public void AddRemove(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. @@ -1007,14 +985,15 @@ public void AddRemove(IEnumerable add = null, IEnumerable re using (var tx = CkanTransaction.CreateTransactionScope()) { - foreach (string identifier in remove) + foreach (InstalledModule instMod in remove) { - Uninstall(identifier); + Uninstall(instMod.Module.identifier); } foreach (CkanModule module in add) { - Install(module); + var previous = remove?.FirstOrDefault(im => im.Module.identifier == module.identifier); + Install(module, previous?.AutoInstalled ?? false); } registry_manager.Save(enforceConsistency); @@ -1050,7 +1029,7 @@ public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloa // adding everything that needs installing (which may involve new mods to // satisfy dependencies). We always know the list passed in is what we need to // install, but we need to calculate what needs to be removed. - var to_remove = new List(); + var to_remove = new List(); // Let's discover what we need to do with each module! foreach (CkanModule module in modules) @@ -1071,7 +1050,7 @@ public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloa else { // Module already installed. We'll need to remove it first. - to_remove.Add(module.identifier); + to_remove.Add(installed_mod); CkanModule installed = installed_mod.Module; if (installed.version.IsEqualTo(module.version)) @@ -1105,7 +1084,7 @@ public void Replace(IEnumerable replacements, RelationshipRes { log.Debug("Using Replace method"); List modsToInstall = new List(); - var modsToRemove = new List(); + var modsToRemove = new List(); foreach (ModuleReplacement repl in replacements) { modsToInstall.Add(repl.ReplaceWith); @@ -1139,7 +1118,7 @@ public void Replace(IEnumerable replacements, RelationshipRes else { // Obviously, we need to remove the mod we are replacing - modsToRemove.Add(repl.ToReplace.identifier); + modsToRemove.Add(installedMod); log.DebugFormat("Ok, we are removing {0}", repl.ToReplace.identifier); //Check whether our Replacement target is already installed @@ -1150,7 +1129,7 @@ public void Replace(IEnumerable replacements, RelationshipRes { //Module already installed. We'll need to treat it as an upgrade. log.DebugFormat("It turns out {0} is already installed, we'll upgrade it.", installed_replacement.identifier); - modsToRemove.Add(installed_replacement.identifier); + modsToRemove.Add(installed_replacement); CkanModule installed = installed_replacement.Module; if (installed.version.IsEqualTo(repl.ReplaceWith.version)) diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 86804cf452..22128911df 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -77,6 +77,17 @@ List LatestAvailableWithProvides( /// HashSet FindReverseDependencies(IEnumerable modules); + /// + /// Find auto-installed modules that have no depending modules + /// or only auto-installed depending modules. + /// installedModules is a parameter so we can experiment with + /// changes that have not yet been made, such as removing other modules. + /// + /// The modules currently installed + /// + /// Sequence of removable auto-installed modules, if any + /// + IEnumerable FindRemovableAutoInstalled(IEnumerable installedModules); /// /// Gets the installed version of a mod. Does not check for provided or autodetected mods. diff --git a/Core/Registry/InstalledModule.cs b/Core/Registry/InstalledModule.cs index 2c692013ba..23c1170052 100644 --- a/Core/Registry/InstalledModule.cs +++ b/Core/Registry/InstalledModule.cs @@ -83,7 +83,7 @@ public class InstalledModule [JsonProperty] private CkanModule source_module; -// private static readonly ILog log = LogManager.GetLogger(typeof(InstalledModule)); + [JsonProperty] private bool auto_installed; // TODO: Our InstalledModuleFile already knows its path, so this could just // be a list. However we've left it as a dictionary for now to maintain @@ -110,15 +110,22 @@ public DateTime InstallTime get { return install_time; } } + public bool AutoInstalled + { + get { return auto_installed; } + set { auto_installed = value; } + } + #endregion #region Constructors - public InstalledModule(KSP ksp, CkanModule module, IEnumerable relative_files) + public InstalledModule(KSP ksp, CkanModule module, IEnumerable relative_files, bool autoInstalled) { install_time = DateTime.Now; source_module = module; installed_files = new Dictionary(); + auto_installed = autoInstalled; foreach (string file in relative_files) { diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index 29dc11174f..fa865c332f 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -209,7 +209,8 @@ private void DeSerialisationFixes(StreamingContext context) var new_control_lock_installed = new InstalledModule( ksp, control_lock_mod, - control_lock_entry.Files + control_lock_entry.Files, + control_lock_entry.AutoInstalled ); // Re-insert into registry. @@ -743,7 +744,7 @@ public CkanModule GetModuleByVersion(string ident, ModuleVersion version) /// Register the supplied module as having been installed, thereby keeping /// track of its metadata and files. /// - public void RegisterModule(CkanModule mod, IEnumerable absolute_files, KSP ksp) + public void RegisterModule(CkanModule mod, IEnumerable absolute_files, KSP ksp, bool autoInstalled) { SealionTransaction(); @@ -791,7 +792,7 @@ public void RegisterModule(CkanModule mod, IEnumerable absolute_files, K } // Finally, register our module proper. - var installed = new InstalledModule(ksp, mod, relative_files); + var installed = new InstalledModule(ksp, mod, relative_files, autoInstalled); installed_modules.Add(mod.identifier, installed); } @@ -1118,6 +1119,44 @@ public HashSet FindReverseDependencies(IEnumerable modules_to_re return FindReverseDependencies(modules_to_remove, installed, new HashSet(installed_dlls.Keys), _installedDlcModules); } + /// + /// Find auto-installed modules that have no depending modules + /// or only auto-installed depending modules. + /// + /// The modules currently installed + /// The DLLs that are manually installed + /// The DLCs that are installed + /// + /// Sequence of removable auto-installed modules, if any + /// + private static IEnumerable FindRemovableAutoInstalled( + IEnumerable installedModules, + IEnumerable dlls, + IDictionary dlc + ) + { + var instCkanMods = installedModules.Select(im => im.Module); + // ToList ensures that the collection isn't modified while the enumeration operation is executing + return installedModules.ToList().Where( + im => FindReverseDependencies(new List { im.identifier }, instCkanMods, dlls, dlc) + .All(id => installedModules.First(orig => orig.identifier == id).AutoInstalled)); + } + + /// + /// Find auto-installed modules that have no depending modules + /// or only auto-installed depending modules. + /// installedModules is a parameter so we can experiment with + /// changes that have not yet been made, such as removing other modules. + /// + /// The modules currently installed + /// + /// Sequence of removable auto-installed modules, if any + /// + public IEnumerable FindRemovableAutoInstalled(IEnumerable installedModules) + { + return FindRemovableAutoInstalled(installedModules, InstalledDlls, InstalledDlc); + } + /// /// Get a dictionary of all mod versions indexed by their downloads' SHA-1 hash. /// Useful for finding the mods for a group of files without repeatedly searching the entire registry. diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index ae597a3820..c4083e563e 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -673,6 +673,14 @@ public override string Reason get { return " Requested by user.\r\n"; } } } + + public class NoLongerUsed: SelectionReason + { + public override string Reason + { + get { return " Auto-installed, depending modules removed.\r\n"; } + } + } public class Replacement : SelectionReason { diff --git a/GUI/GUIMod.cs b/GUI/GUIMod.cs index 2f28e4f96c..1fcb094578 100644 --- a/GUI/GUIMod.cs +++ b/GUI/GUIMod.cs @@ -8,10 +8,12 @@ namespace CKAN { public sealed class GUIMod { - private CkanModule Mod { get; set; } + private CkanModule Mod { get; set; } + private InstalledModule InstalledMod { get; set; } public string Name { get; private set; } public bool IsInstalled { get; private set; } + public bool IsAutoInstalled { get; private set; } public bool HasUpdate { get; private set; } public bool HasReplacement { get; private set; } public bool IsIncompatible { get; private set; } @@ -82,6 +84,8 @@ public GUIMod(InstalledModule instMod, IRegistryQuerier registry, KspVersionCrit { IsInstalled = true; IsInstallChecked = true; + InstalledMod = instMod; + IsAutoInstalled = instMod.AutoInstalled; InstallDate = instMod.InstallTime; InstalledVersion = instMod.Module.version.ToString(); if (LatestVersion == null || LatestVersion.Equals("-")) @@ -285,7 +289,7 @@ public static implicit operator CkanModule(GUIMod mod) public void SetUpgradeChecked(DataGridViewRow row, bool? set_value_to = null) { //Contract.Requires(row.Cells[1] is DataGridViewCheckBoxCell); - var update_cell = row.Cells[1] as DataGridViewCheckBoxCell; + var update_cell = row.Cells["UpdateCol"] as DataGridViewCheckBoxCell; if (update_cell != null) { var old_value = (bool) update_cell.Value; @@ -300,7 +304,7 @@ public void SetUpgradeChecked(DataGridViewRow row, bool? set_value_to = null) public void SetInstallChecked(DataGridViewRow row, bool? set_value_to = null) { //Contract.Requires(row.Cells[0] is DataGridViewCheckBoxCell); - var install_cell = row.Cells[0] as DataGridViewCheckBoxCell; + var install_cell = row.Cells["Installed"] as DataGridViewCheckBoxCell; if (install_cell != null) { bool changeTo = set_value_to ?? (bool)install_cell.Value; @@ -326,7 +330,7 @@ public void SetInstallChecked(DataGridViewRow row, bool? set_value_to = null) public void SetReplaceChecked(DataGridViewRow row, bool? set_value_to = null) { - var replace_cell = row.Cells[2] as DataGridViewCheckBoxCell; + var replace_cell = row.Cells["ReplaceCol"] as DataGridViewCheckBoxCell; if (replace_cell != null) { var old_value = (bool) replace_cell.Value; @@ -337,6 +341,24 @@ public void SetReplaceChecked(DataGridViewRow row, bool? set_value_to = null) replace_cell.Value = value; } } + + public void SetAutoInstallChecked(DataGridViewRow row, bool? set_value_to = null) + { + var auto_cell = row.Cells["AutoInstalled"] as DataGridViewCheckBoxCell; + if (auto_cell != null) + { + var old_value = (bool) auto_cell.Value; + + bool value = set_value_to ?? old_value; + IsAutoInstalled = value; + InstalledMod.AutoInstalled = value; + + if (old_value != value) + { + auto_cell.Value = value; + } + } + } private bool Equals(GUIMod other) { diff --git a/GUI/Main.Designer.cs b/GUI/Main.Designer.cs index 242eab799c..e8d01f30a5 100644 --- a/GUI/Main.Designer.cs +++ b/GUI/Main.Designer.cs @@ -72,6 +72,7 @@ private void InitializeComponent() this.ModList = new CKAN.MainModListGUI(); this.InstallAllCheckbox = new System.Windows.Forms.CheckBox(); this.Installed = new System.Windows.Forms.DataGridViewCheckBoxColumn(); + this.AutoInstalled = new System.Windows.Forms.DataGridViewCheckBoxColumn(); this.UpdateCol = new System.Windows.Forms.DataGridViewCheckBoxColumn(); this.ReplaceCol = new System.Windows.Forms.DataGridViewCheckBoxColumn(); this.ModName = new System.Windows.Forms.DataGridViewTextBoxColumn(); @@ -536,6 +537,7 @@ private void InitializeComponent() this.ModList.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.ModList.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { this.Installed, + this.AutoInstalled, this.UpdateCol, this.ReplaceCol, this.ModName, @@ -571,6 +573,14 @@ private void InitializeComponent() this.Installed.DefaultCellStyle.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleCenter; this.Installed.Width = 50; // + // AutoInstalled + // + this.AutoInstalled.HeaderText = "Auto-installed"; + this.AutoInstalled.Name = "AutoInstalled"; + this.AutoInstalled.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; + this.AutoInstalled.DefaultCellStyle.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleCenter; + this.AutoInstalled.Width = 50; + // // UpdateCol // this.UpdateCol.HeaderText = "Update"; @@ -1373,6 +1383,7 @@ private void InitializeComponent() public CKAN.MainModListGUI ModList; private System.Windows.Forms.CheckBox InstallAllCheckbox; private System.Windows.Forms.DataGridViewCheckBoxColumn Installed; + private System.Windows.Forms.DataGridViewCheckBoxColumn AutoInstalled; private System.Windows.Forms.DataGridViewCheckBoxColumn UpdateCol; private System.Windows.Forms.DataGridViewCheckBoxColumn ReplaceCol; private System.Windows.Forms.DataGridViewTextBoxColumn ModName; diff --git a/GUI/Main.cs b/GUI/Main.cs index 37b4d0d71b..05e8600218 100644 --- a/GUI/Main.cs +++ b/GUI/Main.cs @@ -45,6 +45,8 @@ public KSPManager Manager get { return manager; } set { manager = value; } } + + private bool needRegistrySave = false; public MainModList mainModList { get; } @@ -502,6 +504,13 @@ protected override void OnFormClosing(FormClosingEventArgs e) // Save settings. configuration.Save(); + + if (needRegistrySave) + { + // Save registry + RegistryManager.Instance(CurrentInstance).Save(false); + } + base.OnFormClosing(e); } @@ -780,9 +789,8 @@ private void Filter(GUIModFilter filter) // Ask the configuration which columns to show. foreach (DataGridViewColumn col in ModList.Columns) { - // Start with the third column, because the first one is always shown - // and the 2nd/3rd are handled by UpdateModsList(). - if (col.Index > 2) + // Some columns are always shown, and others are handled by UpdateModsList() + if (col.Name != "Installed" && col.Name != "UpdateCol" && col.Name != "ReplaceCol") { col.Visible = !configuration.HiddenColumnNames.Contains(col.Name); } @@ -800,9 +808,10 @@ private void Filter(GUIModFilter filter) case GUIModFilter.Replaceable: FilterToolButton.Text = "Filter (Replaceable)"; break; case GUIModFilter.Cached: FilterToolButton.Text = "Filter (Cached)"; break; case GUIModFilter.NewInRepository: FilterToolButton.Text = "Filter (New)"; break; - case GUIModFilter.NotInstalled: FilterToolButton.Text = "Filter (Not installed)"; - ModList.Columns[5].Visible = false; - ModList.Columns[9].Visible = false; break; + case GUIModFilter.NotInstalled: ModList.Columns["InstalledVersion"].Visible = false; + ModList.Columns["InstallDate"].Visible = false; + ModList.Columns["AutoInstalled"].Visible = false; + FilterToolButton.Text = "Filter (Not installed)"; break; default: FilterToolButton.Text = "Filter (Compatible)"; break; } } @@ -1186,8 +1195,11 @@ private void reinstallToolStripMenuItem_Click(object sender, EventArgs e) new ModChange(module, GUIModChangeType.Remove, null) }; // Then everything we need to re-install: - HashSet goners = registry.FindReverseDependencies( - new List() { module.Identifier } + var revdep = registry.FindReverseDependencies(new List() { module.Identifier }); + var goners = revdep.Union( + registry.FindRemovableAutoInstalled( + registry.InstalledModules.Where(im => !revdep.Contains(im.identifier)) + ).Select(im => im.Module.identifier) ); foreach (string id in goners) { diff --git a/GUI/MainModList.cs b/GUI/MainModList.cs index 63f7dcad2a..863874c563 100644 --- a/GUI/MainModList.cs +++ b/GUI/MainModList.cs @@ -249,7 +249,8 @@ private void _UpdateModsList(IEnumerable mc, Dictionary AddLogMessage("Updating filters..."); - var has_any_updates = gui_mods.Any(mod => mod.HasUpdate); + var has_any_updates = gui_mods.Any(mod => mod.HasUpdate); + var has_any_installed = gui_mods.Any(mod => mod.IsInstalled); var has_any_replacements = gui_mods.Any(mod => mod.IsInstalled && mod.HasReplacement); //TODO Consider using smart enumeration pattern so stuff like this is easier @@ -282,8 +283,9 @@ private void _UpdateModsList(IEnumerable mc, Dictionary // Hide update and replacement columns if not needed. // Write it to the configuration, else they are hidden agian after a filter change. // After the update / replacement, they are hidden again. - ModList.Columns[1].Visible = has_any_updates; - ModList.Columns[2].Visible = has_any_replacements; + ModList.Columns["UpdateCol"].Visible = has_any_updates; + ModList.Columns["AutoInstalled"].Visible = has_any_installed && !configuration.HiddenColumnNames.Contains("AutoInstalled"); + ModList.Columns["ReplaceCol"].Visible = has_any_replacements; AddLogMessage("Updating tray..."); UpdateTrayInfo(); @@ -373,7 +375,7 @@ private void ModList_HeaderMouseClick(object sender, DataGridViewCellMouseEventA ModListHeaderContextMenuStrip.Items.AddRange( ModList.Columns.Cast() - .Where(col => col.Index > 2) + .Where(col => col.Name != "Installed" && col.Name != "UpdateCol" && col.Name != "ReplaceCol") .Select(col => new ToolStripMenuItem() { Name = col.Name, @@ -431,7 +433,7 @@ private void ModList_KeyDown(object sender, KeyEventArgs e) case Keys.Space: // If they've focused one of the checkbox columns, don't intercept - if (ModList.CurrentCell.ColumnIndex > 2) + if (ModList.CurrentCell.ColumnIndex > 3) { DataGridViewRow row = ModList.CurrentRow; // Toggle Update column if enabled, otherwise Install @@ -553,22 +555,26 @@ private async void ModList_CellValueChanged(object sender, DataGridViewCellEvent if (!string.IsNullOrEmpty(cmd)) Process.Start(cmd); } - else if (column_index <= 2) + else { GUIMod gui_mod = row?.Tag as GUIMod; if (gui_mod != null) { - switch (column_index) + switch (ModList.Columns[column_index].Name) { - case 0: + case "Installed": gui_mod.SetInstallChecked(row); if (gui_mod.IsInstallChecked) last_mod_to_have_install_toggled.Push(gui_mod); break; - case 1: + case "AutoInstalled": + gui_mod.SetAutoInstallChecked(row); + needRegistrySave = true; + break; + case "UpdateCol": gui_mod.SetUpgradeChecked(row); break; - case 2: + case "ReplaceCol": gui_mod.SetReplaceChecked(row); break; } @@ -775,6 +781,13 @@ public async Task> ComputeChangeSetFromModList( changeSet.Add(new ModChange(new GUIMod(module_by_version, registry, version), GUIModChangeType.Remove, null)); modules_to_remove.Add(module_by_version); } + foreach (var im in registry.FindRemovableAutoInstalled( + registry.InstalledModules.Where(im => !modules_to_remove.Any(m => m.identifier == im.identifier)) + )) + { + changeSet.Add(new ModChange(new GUIMod(im.Module, registry, version), GUIModChangeType.Remove, null)); + modules_to_remove.Add(im.Module); + } bool handled_all_too_many_provides = false; while (!handled_all_too_many_provides) @@ -879,6 +892,16 @@ private DataGridViewRow MakeRow(GUIMod mod, List changes, bool hideEp Value = mod.IsAutodetected ? "AD" : "-" }; + var autoInstalled = mod.IsInstalled + ? (DataGridViewCell) new DataGridViewCheckBoxCell() + { + Value = mod.IsAutoInstalled + } + : new DataGridViewTextBoxCell() + { + Value = "-" + }; + var updating = mod.IsInstallable() && mod.HasUpdate ? (DataGridViewCell) new DataGridViewCheckBoxCell() { @@ -933,10 +956,11 @@ private DataGridViewRow MakeRow(GUIMod mod, List changes, bool hideEp var installDate = new DataGridViewTextBoxCell() { Value = mod.InstallDate }; var desc = new DataGridViewTextBoxCell() { Value = mod.Abstract }; - item.Cells.AddRange(selecting, updating, replacing, name, author, installVersion, latestVersion, compat, size, installDate, downloadCount, desc); + item.Cells.AddRange(selecting, autoInstalled, updating, replacing, name, author, installVersion, latestVersion, compat, size, installDate, downloadCount, desc); - selecting.ReadOnly = selecting is DataGridViewTextBoxCell; - updating.ReadOnly = updating is DataGridViewTextBoxCell; + selecting.ReadOnly = selecting is DataGridViewTextBoxCell; + autoInstalled.ReadOnly = autoInstalled is DataGridViewTextBoxCell; + updating.ReadOnly = updating is DataGridViewTextBoxCell; return item; } @@ -1057,6 +1081,8 @@ public static Dictionary ComputeConflictsFromModList(IRegistryQu public HashSet ComputeUserChangeSet() { + var registry = RegistryManager.Instance(Main.Instance.CurrentInstance).registry; + var removableAuto = registry.FindRemovableAutoInstalled(registry.InstalledModules); return new HashSet( Modules .Where(mod => mod.IsInstallable()) @@ -1064,6 +1090,10 @@ public HashSet ComputeUserChangeSet() .Where(change => change.HasValue) .Select(change => change.Value) .Select(change => new ModChange(change.Key, change.Value, null)) + .Union(removableAuto.Select(im => new ModChange( + new GUIMod(im, registry, Main.Instance.CurrentInstance.VersionCriteria()), + GUIModChangeType.Remove, + new SelectionReason.NoLongerUsed()))) ); } } diff --git a/Tests/GUI/GH1866.cs b/Tests/GUI/GH1866.cs index b28b71ad2d..44f9524d21 100644 --- a/Tests/GUI/GH1866.cs +++ b/Tests/GUI/GH1866.cs @@ -64,7 +64,7 @@ public void Up() // install it and set it as pre-installed _manager.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip()); - _registry.RegisterModule(_anyVersionModule, new string[] { }, _instance.KSP); + _registry.RegisterModule(_anyVersionModule, new string[] { }, _instance.KSP, false); _registry.AddAvailable(_anyVersionModule); ModuleInstaller.GetInstance(_instance.KSP, _manager.Cache, _manager.User).InstallList( diff --git a/Tests/GUI/GUIMod.cs b/Tests/GUI/GUIMod.cs index 1153b4d5c8..ef7d0b7b32 100644 --- a/Tests/GUI/GUIMod.cs +++ b/Tests/GUI/GUIMod.cs @@ -44,7 +44,7 @@ public void HasUpdateReturnsTrueWhenUpdateAvailible() var new_version = generatror.GeneratorRandomModule(version: new ModuleVersion("0.25"), ksp_version: tidy.KSP.Version(), identifier:old_version.identifier); var registry = Registry.Empty(); - registry.RegisterModule(old_version, Enumerable.Empty(), null); + registry.RegisterModule(old_version, Enumerable.Empty(), null, false); registry.AddAvailable(new_version); var mod = new GUIMod(old_version, registry, tidy.KSP.VersionCriteria()); diff --git a/Tests/GUI/MainModList.cs b/Tests/GUI/MainModList.cs index 472fdb90a9..ed87dca7f7 100644 --- a/Tests/GUI/MainModList.cs +++ b/Tests/GUI/MainModList.cs @@ -64,7 +64,7 @@ public async Task ComputeChangeSetFromModList_WithConflictingMods_ThrowsInconsis module.conflicts = new List { new ModuleRelationshipDescriptor { name = "kOS" } }; registry.AddAvailable(module); registry.AddAvailable(TestData.kOS_014_module()); - registry.RegisterModule(module, Enumerable.Empty(), tidy.KSP); + registry.RegisterModule(module, Enumerable.Empty(), tidy.KSP, false); var mainList = new MainModList(null, null, new GUIUser()); var mod = new GUIMod(module, registry, tidy.KSP.VersionCriteria());