From 574142393c5973d7956601ab6d20eff77c92c2d3 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Sun, 11 Mar 2018 01:46:14 -0600 Subject: [PATCH] Fix slow response to checkbox changes --- Cmdline/Action/Import.cs | 2 +- ConsoleUI/DependencyScreen.cs | 105 ++++++--------- ConsoleUI/DownloadImportDialog.cs | 2 +- ConsoleUI/InstallScreen.cs | 16 +-- ConsoleUI/ModListScreen.cs | 71 ++++++---- Core/ModuleInstaller.cs | 14 +- Core/Registry/Registry.cs | 135 +++++++++---------- GUI/CKAN-GUI.csproj | 1 + GUI/GUIMod.cs | 11 +- GUI/GUIUser.cs | 39 ++++++ GUI/Main.cs | 214 ------------------------------ GUI/MainChangeset.cs | 7 +- GUI/MainImport.cs | 2 +- GUI/MainInstall.cs | 115 +++++++--------- GUI/MainModList.cs | 184 +++++++++++++++++++++++++ 15 files changed, 449 insertions(+), 469 deletions(-) create mode 100644 GUI/GUIUser.cs diff --git a/Cmdline/Action/Import.cs b/Cmdline/Action/Import.cs index 8d0819d5cc..ad72eef6e8 100644 --- a/Cmdline/Action/Import.cs +++ b/Cmdline/Action/Import.cs @@ -46,7 +46,7 @@ public int RunCommand(CKAN.KSP ksp, object options) log.InfoFormat("Importing {0} files", toImport.Count); List toInstall = new List(); ModuleInstaller inst = ModuleInstaller.GetInstance(ksp, user); - inst.ImportFiles(toImport, user, id => toInstall.Add(id), !opts.Headless); + inst.ImportFiles(toImport, user, mod => toInstall.Add(mod.identifier), !opts.Headless); if (toInstall.Count > 0) { inst.InstallList( diff --git a/ConsoleUI/DependencyScreen.cs b/ConsoleUI/DependencyScreen.cs index 254e45b435..0ee8df1108 100644 --- a/ConsoleUI/DependencyScreen.cs +++ b/ConsoleUI/DependencyScreen.cs @@ -39,12 +39,12 @@ public DependencyScreen(KSPManager mgr, ChangePlan cp, HashSet rej, bool new ConsoleListBoxColumn() { Header = "Install", Width = 7, - Renderer = (Dependency d) => StatusSymbol(d.identifier), + Renderer = (Dependency d) => StatusSymbol(d.module) }, new ConsoleListBoxColumn() { Header = "Name", - Width = 24, - Renderer = (Dependency d) => d.identifier, + Width = 36, + Renderer = (Dependency d) => d.module.ToString() }, new ConsoleListBoxColumn() { Header = "Sources", @@ -56,7 +56,7 @@ public DependencyScreen(KSPManager mgr, ChangePlan cp, HashSet rej, bool ); dependencyList.AddTip("+", "Toggle"); dependencyList.AddBinding(Keys.Plus, (object sender) => { - ChangePlan.toggleContains(accepted, dependencyList.Selection.identifier); + ChangePlan.toggleContains(accepted, dependencyList.Selection.module); return true; }); @@ -81,7 +81,7 @@ public DependencyScreen(KSPManager mgr, ChangePlan cp, HashSet rej, bool if (dependencyList.Selection != null) { LaunchSubScreen(new ModInfoScreen( manager, plan, - registry.LatestAvailable(dependencyList.Selection.identifier, manager.CurrentInstance.VersionCriteria()), + dependencyList.Selection.module, debug )); } @@ -94,20 +94,20 @@ public DependencyScreen(KSPManager mgr, ChangePlan cp, HashSet rej, bool AddBinding(Keys.Escape, (object sender) => { // Add everything to rejected foreach (var kvp in dependencies) { - rejected.Add(kvp.Key); + rejected.Add(kvp.Key.identifier); } return false; }); AddTip("F9", "Accept"); AddBinding(Keys.F9, (object sender) => { - foreach (string name in accepted) { - plan.Install.Add(name); + foreach (CkanModule mod in accepted) { + plan.Install.Add(mod); } // Add the rest to rejected foreach (var kvp in dependencies) { if (!accepted.Contains(kvp.Key)) { - rejected.Add(kvp.Key); + rejected.Add(kvp.Key.identifier); } } return false; @@ -126,85 +126,66 @@ public bool HaveOptions() return dependencies.Count > 0; } - private void generateList(HashSet inst) + private void generateList(HashSet inst) { - foreach (string mod in inst) { - CkanModule m = registry.LatestAvailable(mod, manager.CurrentInstance.VersionCriteria()); - if (m != null) { - AddDependencies(inst, mod, m.recommends, true); - AddDependencies(inst, mod, m.suggests, false); - } + foreach (CkanModule mod in inst) { + AddDependencies(inst, mod, mod.recommends, true); + AddDependencies(inst, mod, mod.suggests, false); } } - private void AddDependencies(HashSet alreadyInstalling, string identifier, List source, bool installByDefault) + private void AddDependencies(HashSet alreadyInstalling, CkanModule dependent, List source, bool installByDefault) { if (source != null) { foreach (RelationshipDescriptor dependency in source) { if (!rejected.Contains(dependency.name)) { - try { - if (registry.LatestAvailable( - dependency.name, - manager.CurrentInstance.VersionCriteria(), - dependency - ) != null - && !registry.IsInstalled(dependency.name) - && !alreadyInstalling.Contains(dependency.name)) { - - AddDep(dependency.name, installByDefault, identifier); - } - } catch (ModuleNotFoundKraken) { - // LatestAvailable throws if you recommend a "provides" name, - // so ask the registry again for provides-based choices - List opts = registry.LatestAvailableWithProvides( - dependency.name, - manager.CurrentInstance.VersionCriteria(), - dependency - ); - foreach (CkanModule provider in opts) { - if (!registry.IsInstalled(provider.identifier) - && !alreadyInstalling.Contains(provider.identifier)) { - - // Default to not installing because these probably conflict with each other - AddDep(provider.identifier, false, identifier); - } + // LatestAvailable throws if you recommend a "provides" name, + // so ask the registry again for provides-based choices + List opts = registry.LatestAvailableWithProvides( + dependency.name, + manager.CurrentInstance.VersionCriteria(), + dependency + ); + foreach (CkanModule provider in opts) { + if (!registry.IsInstalled(provider.identifier) + && !alreadyInstalling.Contains(provider)) { + + // Default to not installing because these probably conflict with each other + AddDep(provider, installByDefault, dependent); } - } catch (Kraken) { - // GUI/MainInstall.cs::AddMod just ignores all exceptions, - // so that's baked into the infrastructure } } } } } - private void AddDep(string identifier, bool defaultInstall, string dependent) + private void AddDep(CkanModule mod, bool defaultInstall, CkanModule dependent) { - if (dependencies.ContainsKey(identifier)) { - dependencies[identifier].defaultInstall |= defaultInstall; - dependencies[identifier].dependents.Add(dependent); + if (dependencies.ContainsKey(mod)) { + dependencies[mod].defaultInstall |= defaultInstall; + dependencies[mod].dependents.Add(dependent); } else { - dependencies.Add(identifier, new Dependency() { - identifier = identifier, + dependencies.Add(mod, new Dependency() { + module = mod, defaultInstall = defaultInstall, - dependents = new List() {dependent} + dependents = new List() { dependent } }); } if (defaultInstall) { - accepted.Add(identifier); + accepted.Add(mod); } } - private string StatusSymbol(string identifier) + private string StatusSymbol(CkanModule mod) { - if (accepted.Contains(identifier)) { + if (accepted.Contains(mod)) { return installing; } else { return notinstalled; } } - private HashSet accepted = new HashSet(); + private HashSet accepted = new HashSet(); private HashSet rejected; private IRegistryQuerier registry; @@ -212,8 +193,8 @@ private string StatusSymbol(string identifier) private ChangePlan plan; private bool debug; - private Dictionary dependencies = new Dictionary(); - private ConsoleListBox dependencyList; + private Dictionary dependencies = new Dictionary(); + private ConsoleListBox dependencyList; private static readonly string notinstalled = " "; private static readonly string installing = "+"; @@ -227,17 +208,17 @@ public class Dependency { /// /// Identifier of mod /// - public string identifier; + public CkanModule module; /// /// True if we default to installing, false otherwise /// - public bool defaultInstall; + public bool defaultInstall; /// /// List of mods that recommended or suggested this mod /// - public List dependents = new List(); + public List dependents = new List(); } } diff --git a/ConsoleUI/DownloadImportDialog.cs b/ConsoleUI/DownloadImportDialog.cs index b2aa7f3468..7c602d43f4 100644 --- a/ConsoleUI/DownloadImportDialog.cs +++ b/ConsoleUI/DownloadImportDialog.cs @@ -30,7 +30,7 @@ public static void ImportDownloads(KSP gameInst, ChangePlan cp) ProgressScreen ps = new ProgressScreen("Importing Downloads", "Calculating..."); ModuleInstaller inst = ModuleInstaller.GetInstance(gameInst, ps); ps.Run(() => inst.ImportFiles(files, ps, - (string identifier) => cp.Install.Add(identifier))); + (CkanModule mod) => cp.Install.Add(mod))); // Don't let the installer re-use old screen references inst.User = null; } diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index f0b28f8384..4d42bfcf72 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -67,7 +67,7 @@ public override void Run(Action process = null) plan.Upgrade.Clear(); } if (plan.Install.Count > 0) { - List iList = new List(plan.Install); + List iList = new List(plan.Install); inst.InstallList(iList, resolvOpts, dl); plan.Install.Clear(); } @@ -109,19 +109,15 @@ public override void Run(Action process = null) RaiseError(ex.InconsistenciesPretty); } catch (TooManyModsProvideKraken ex) { - List opts = new List(); - foreach (CkanModule opt in ex.modules) { - opts.Add(opt.identifier); - } - ConsoleChoiceDialog ch = new ConsoleChoiceDialog( + ConsoleChoiceDialog ch = new ConsoleChoiceDialog( $"Module {ex.requested} is provided by multiple modules. Which would you like to install?", "Name", - opts, - (string s) => s + ex.modules, + (CkanModule mod) => mod.ToString() ); - string chosen = ch.Run(); + CkanModule chosen = ch.Run(); DrawBackground(); - if (!string.IsNullOrEmpty(chosen)) { + if (chosen != null) { // Use chosen to continue installing plan.Install.Add(chosen); retry = true; diff --git a/ConsoleUI/ModListScreen.cs b/ConsoleUI/ModListScreen.cs index 60018f2c5c..c535aaa459 100644 --- a/ConsoleUI/ModListScreen.cs +++ b/ConsoleUI/ModListScreen.cs @@ -174,10 +174,10 @@ public ModListScreen(KSPManager mgr, bool dbg) moduleList.AddBinding(Keys.Plus, (object sender) => { if (moduleList.Selection != null) { if (!registry.IsInstalled(moduleList.Selection.identifier, false)) { - plan.ToggleInstall(moduleList.Selection.identifier); + plan.ToggleInstall(moduleList.Selection); } else if (registry.IsInstalled(moduleList.Selection.identifier, false) && registry.HasUpdate(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria())) { - plan.ToggleUpgrade(moduleList.Selection.identifier); + plan.ToggleUpgrade(moduleList.Selection); } } return true; @@ -189,7 +189,7 @@ public ModListScreen(KSPManager mgr, bool dbg) ); moduleList.AddBinding(Keys.Minus, (object sender) => { if (moduleList.Selection != null && registry.IsInstalled(moduleList.Selection.identifier, false)) { - plan.ToggleRemove(moduleList.Selection.identifier); + plan.ToggleRemove(moduleList.Selection); } return true; }); @@ -325,7 +325,7 @@ private bool ViewSuggestions() // Only check mods that are still available try { if (registry.LatestAvailable(im.identifier, manager.CurrentInstance.VersionCriteria()) != null) { - reinstall.Install.Add(im.identifier); + reinstall.Install.Add(im.Module); } } catch { // The registry object badly needs an IsAvailable check @@ -337,8 +337,8 @@ private bool ViewSuggestions() LaunchSubScreen(ds); bool needRefresh = false; // Copy the right ones into our real plan - foreach (string mod in reinstall.Install) { - if (!registry.IsInstalled(mod, false)) { + foreach (CkanModule mod in reinstall.Install) { + if (!registry.IsInstalled(mod.identifier, false)) { plan.Install.Add(mod); needRefresh = true; } @@ -582,34 +582,34 @@ public ChangePlan() { } /// /// Add or remove a mod from the remove list /// - /// The mod to add or remove - public void ToggleRemove(string identifier) + /// The mod to add or remove + public void ToggleRemove(CkanModule mod) { - Install.Remove(identifier); - Upgrade.Remove(identifier); - toggleContains(Remove, identifier); + Install.Remove(mod); + Upgrade.Remove(mod.identifier); + toggleContains(Remove, mod.identifier); } /// /// Add or remove a mod from the install list /// - /// The mod to add or remove - public void ToggleInstall(string identifier) + /// The mod to add or remove + public void ToggleInstall(CkanModule mod) { - Upgrade.Remove(identifier); - Remove.Remove(identifier); - toggleContains(Install, identifier); + Upgrade.Remove(mod.identifier); + Remove.Remove(mod.identifier); + toggleContains(Install, mod); } /// /// Add or remove a mod from the upgrade list /// - /// The mod to add or remove - public void ToggleUpgrade(string identifier) + /// The mod to add or remove + public void ToggleUpgrade(CkanModule mod) { - Install.Remove(identifier); - Remove.Remove(identifier); - toggleContains(Upgrade, identifier); + Install.Remove(mod); + Remove.Remove(mod.identifier); + toggleContains(Upgrade, mod.identifier); } /// @@ -659,11 +659,14 @@ public InstallStatus GetModStatus(KSPManager manager, IRegistryQuerier registry, return InstallStatus.Installed; } } else { - if (Install.Contains(identifier)) { - return InstallStatus.Installing; - } else { - return InstallStatus.NotInstalled; + foreach (CkanModule m in Install) + { + if (m.identifier == identifier) + { + return InstallStatus.Installing; + } } + return InstallStatus.NotInstalled; } } @@ -701,10 +704,26 @@ public static void toggleContains(HashSet list, string identifier) } } + /// + /// Add or remove a value from a HashSet + /// + /// HashSet to manipulate + /// The value + public static void toggleContains(HashSet list, CkanModule mod) + { + if (list != null && mod != null) { + if (list.Contains(mod)) { + list.Remove(mod); + } else { + list.Add(mod); + } + } + } + /// /// Mods we're planning to install /// - public readonly HashSet Install = new HashSet(); + public readonly HashSet Install = new HashSet(); /// /// Mods we're planning to upgrade diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 82700f8e8f..952bd6c236 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -1098,11 +1098,11 @@ private void DownloadModules(IEnumerable mods, IDownloader downloade /// Object for user interaction /// Function to call to mark a mod for installation /// True to ask user whether to delete imported files, false to leave the files as is - public void ImportFiles(HashSet files, IUser user, Action installMod, bool allowDelete = true) + public void ImportFiles(HashSet files, IUser user, Action installMod, bool allowDelete = true) { - Registry registry = registry_manager.registry; - HashSet installable = new HashSet(); - List deletable = new List(); + Registry registry = registry_manager.registry; + HashSet installable = new HashSet(); + List deletable = new List(); // Get the mapping of known hashes to modules Dictionary> index = registry.GetSha1Index(); int i = 0; @@ -1121,7 +1121,7 @@ public void ImportFiles(HashSet files, IUser user, Action inst { if (mod.IsCompatibleKSP(ksp.VersionCriteria())) { - installable.Add(mod.identifier); + installable.Add(mod); } if (Cache.IsMaybeCachedZip(mod)) { @@ -1143,9 +1143,9 @@ public void ImportFiles(HashSet files, IUser user, Action inst if (installable.Count > 0 && user.RaiseYesNoDialog($"Install {installable.Count} compatible imported mods in game instance {ksp.Name} ({ksp.GameDir()})?")) { // Install the imported mods - foreach (string identifier in installable) + foreach (CkanModule mod in installable) { - installMod(identifier); + installMod(mod); } } if (allowDelete && deletable.Count > 0 && user.RaiseYesNoDialog($"Import complete. Delete {deletable.Count} old files?")) diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index 3ff0e6bc82..0e04bd95e0 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -37,6 +37,12 @@ public class Registry : IEnlistmentNotification, IRegistryQuerier [JsonProperty] private Dictionary installed_modules; [JsonProperty] private Dictionary installed_files; // filename => module + // Index of which mods provide what, format: + // providers[provided] = { provider1, provider2, ... } + // Built by BuildProvidesIndex, makes LatestAvailableWithProvides much faster. + [JsonIgnore] private Dictionary> providers + = new Dictionary>(); + [JsonIgnore] private string transaction_backup; /// @@ -72,8 +78,7 @@ [JsonIgnore] public IEnumerable InstalledDlls private void DeSerialisationFixes(StreamingContext context) { // Our context is our KSP install. - KSP ksp = (KSP) context.Context; - + KSP ksp = (KSP)context.Context; // Older registries didn't have the installed_files list, so we create one // if absent. @@ -90,7 +95,7 @@ private void DeSerialisationFixes(StreamingContext context) { log.Warn("Older registry format detected, normalising paths..."); - var normalised_installed_files = new Dictionary(); + var normalised_installed_files = new Dictionary(); foreach (KeyValuePair tuple in installed_files) { @@ -177,6 +182,7 @@ private void DeSerialisationFixes(StreamingContext context) } registry_version = LATEST_REGISTRY_VERSION; + BuildProvidesIndex(); } /// @@ -210,20 +216,20 @@ public void Repair() #region Constructors public Registry( - Dictionary installed_modules, - Dictionary installed_dlls, - Dictionary available_modules, - Dictionary installed_files, - SortedDictionary repositories - ) + Dictionary installed_modules, + Dictionary installed_dlls, + Dictionary available_modules, + Dictionary installed_files, + SortedDictionary repositories) { // Is there a better way of writing constructors than this? Srsly? this.installed_modules = installed_modules; - this.installed_dlls = installed_dlls; + this.installed_dlls = installed_dlls; this.available_modules = available_modules; - this.installed_files = installed_files; - this.repositories = repositories; - registry_version = LATEST_REGISTRY_VERSION; + this.installed_files = installed_files; + this.repositories = repositories; + registry_version = LATEST_REGISTRY_VERSION; + BuildProvidesIndex(); } // If deserialsing, we don't want everything put back directly, @@ -352,6 +358,7 @@ public void SetAllAvailable(IEnumerable newAvail) SealionTransaction(); // Clear current modules available_modules = new Dictionary(); + providers.Clear(); // Add the new modules foreach (CkanModule module in newAvail) { @@ -390,6 +397,7 @@ public void AddAvailable(CkanModule module) log.DebugFormat("Available: {0} version {1}", identifier, module.version); available_modules[identifier].Add(module); + BuildProvidesIndexFor(available_modules[identifier]); } /// @@ -425,15 +433,13 @@ public List Available(KspVersionCriteria ksp_version) // It's nice to see things in alphabetical order, so sort our keys first. candidates.Sort(); - //Cache - CkanModule[] modules_for_current_version = available_modules.Values.Select(pair => pair.Latest(ksp_version)).Where(mod => mod != null).ToArray(); // Now find what we can give our user. foreach (string candidate in candidates) { CkanModule available = LatestAvailable(candidate, ksp_version); if (available != null - && allDependenciesCompatible(available, ksp_version, modules_for_current_version)) + && allDependenciesCompatible(available, ksp_version)) { compatible.Add(available); } @@ -449,11 +455,6 @@ public List Incompatible(KspVersionCriteria ksp_version) var candidates = new List(available_modules.Keys); var incompatible = new List(); - CkanModule[] modules_for_current_version = available_modules.Values - .Select(pair => pair.Latest(ksp_version)) - .Where(mod => mod != null) - .ToArray(); - // It's nice to see things in alphabetical order, so sort our keys first. candidates.Sort(); @@ -464,7 +465,7 @@ public List Incompatible(KspVersionCriteria ksp_version) // If a mod is available, it might still have incompatible dependencies. if (available == null - || !allDependenciesCompatible(available, ksp_version, modules_for_current_version)) + || !allDependenciesCompatible(available, ksp_version)) { incompatible.Add(LatestAvailable(candidate, null)); } @@ -473,7 +474,7 @@ public List Incompatible(KspVersionCriteria ksp_version) return incompatible; } - private bool allDependenciesCompatible(CkanModule mod, KspVersionCriteria ksp_version, CkanModule[] modules_for_current_version) + private bool allDependenciesCompatible(CkanModule mod, KspVersionCriteria ksp_version) { // we need to check that we can get everything we depend on if (mod.depends != null) @@ -482,7 +483,7 @@ private bool allDependenciesCompatible(CkanModule mod, KspVersionCriteria ksp_ve { try { - if (!LatestAvailableWithProvides(dependency.name, ksp_version, modules_for_current_version).Any()) + if (!LatestAvailableWithProvides(dependency.name, ksp_version).Any()) { return false; } @@ -518,7 +519,7 @@ public CkanModule LatestAvailable( try { - return available_modules[module].Latest(ksp_version,relationship_descriptor); + return available_modules[module].Latest(ksp_version, relationship_descriptor); } catch (KeyNotFoundException) { @@ -604,66 +605,56 @@ public static void GetMinMaxVersions(IEnumerable modVersions, } /// - /// + /// Generate the providers index so we can find providing modules quicker /// - public List LatestAvailableWithProvides(string module, KspVersionCriteria ksp_version, RelationshipDescriptor relationship_descriptor = null) + private void BuildProvidesIndex() { - // Calculates a cache of modules which - // are compatible with the current version of KSP, and then - // calls the private version below for heavy lifting. - return LatestAvailableWithProvides(module, ksp_version, - available_modules.Values.Select(pair => pair.Latest(ksp_version)).Where(mod => mod != null).ToArray(), - relationship_descriptor); + providers.Clear(); + foreach (AvailableModule am in available_modules.Values) + { + BuildProvidesIndexFor(am); + } } /// - /// Returns the latest version of a module that can be installed for - /// the given KSP version. This is a *private* method that assumes - /// the `available_for_current_version` list has been correctly - /// calculated. Not for direct public consumption. ;) + /// Ensure one AvailableModule is present in the right spots in the providers index /// - private List LatestAvailableWithProvides(string module, KspVersionCriteria ksp_version, - IEnumerable available_for_current_version, RelationshipDescriptor relationship_descriptor=null) + private void BuildProvidesIndexFor(AvailableModule am) { - log.DebugFormat("Finding latest available with provides for {0}", module); - - // TODO: Check user's stability tolerance (stable, unstable, testing, etc) - - var modules = new List(); - - try + foreach (CkanModule m in am.AllAvailable()) { - // If we can find the module requested for our KSP, use that. - CkanModule mod = LatestAvailable(module, ksp_version, relationship_descriptor); - if (mod != null) + foreach (string provided in m.ProvidesList) { - modules.Add(mod); + HashSet provs = null; + if (providers.TryGetValue(provided, out provs)) + provs.Add(am); + else + providers.Add(provided, new HashSet() { am }); } } - catch (ModuleNotFoundKraken) + } + + /// + /// + /// + public List LatestAvailableWithProvides( + string module, + KspVersionCriteria ksp_version, + RelationshipDescriptor relationship_descriptor = null) + { + HashSet provs; + if (providers.TryGetValue(module, out provs)) { - // It's cool if we can't find it, though. + // For each AvailableModule, we want the latest one matching our constraints + return provs.Select(am => am.Latest(ksp_version, relationship_descriptor)) + .Where(m => m?.ProvidesList?.Contains(module) ?? false) + .ToList(); } - - // Walk through all our available modules, and see if anything - // provides what we need. - - // Get our candidate module. We can assume this is non-null, as - // if it *is* null then available_for_current_version is corrupted, - // and something is terribly wrong. - foreach (CkanModule candidate in available_for_current_version) + else { - // Find everything this module provides (for our version of KSP) - List provides = candidate.provides; - - // If the module has provides, and any of them are what we're looking - // for, the add it to our list. - if (provides != null && provides.Any(provided => provided == module)) - { - modules.Add(candidate); - } + // Nothing provides this, return empty list + return new List(); } - return modules; } /// @@ -948,7 +939,9 @@ public Version InstalledVersion(string modIdentifier, bool with_provides=true) public CkanModule GetInstalledVersion(string mod_identifer) { InstalledModule installedModule; - return installed_modules.TryGetValue(mod_identifer, out installedModule) ? installedModule.Module : null; + return installed_modules.TryGetValue(mod_identifer, out installedModule) + ? installedModule.Module + : null; } /// diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj index 10ef052d00..246ed1b163 100644 --- a/GUI/CKAN-GUI.csproj +++ b/GUI/CKAN-GUI.csproj @@ -113,6 +113,7 @@ Form + Component diff --git a/GUI/GUIMod.cs b/GUI/GUIMod.cs index 9abd948f61..ba4d64ea8a 100644 --- a/GUI/GUIMod.cs +++ b/GUI/GUIMod.cs @@ -242,18 +242,19 @@ public void SetInstallChecked(DataGridViewRow row, bool? set_value_to = null) if (install_cell != null) { bool changeTo = set_value_to ?? (bool)install_cell.Value; - //Need to do this check here to prevent an infinite loop - //which is at least happening on Linux - //TODO: Eliminate the cause - if (changeTo != IsInstallChecked) + if (IsInstallChecked != changeTo) { IsInstallChecked = changeTo; + } + // Setting this property causes ModList_CellValueChanged to be called, + // which calls SetInstallChecked again. Treat it conservatively. + if ((bool)install_cell.Value != IsInstallChecked) + { install_cell.Value = IsInstallChecked; } } } - private bool Equals(GUIMod other) { return Equals(Name, other.Name); diff --git a/GUI/GUIUser.cs b/GUI/GUIUser.cs new file mode 100644 index 0000000000..5adb407ef8 --- /dev/null +++ b/GUI/GUIUser.cs @@ -0,0 +1,39 @@ +using System; + +namespace CKAN +{ + + public class GUIUser : NullUser + { + public delegate bool DisplayYesNo(string message); + + public Action displayMessage; + public Action displayError; + public DisplayYesNo displayYesNo; + + protected override bool DisplayYesNoDialog(string message) + { + if (displayYesNo == null) + return true; + + return displayYesNo(message); + } + + protected override void DisplayMessage(string message, params object[] args) + { + displayMessage(message, args); + } + + protected override void DisplayError(string message, params object[] args) + { + displayError(message, args); + } + + protected override void ReportProgress(string format, int percent) + { + Main.Instance.SetDescription($"{format} - {percent}%"); + Main.Instance.SetProgress(percent); + } + } + +} diff --git a/GUI/Main.cs b/GUI/Main.cs index 7b25644414..c0fb96db3b 100644 --- a/GUI/Main.cs +++ b/GUI/Main.cs @@ -480,19 +480,6 @@ private void MarkAllUpdatesToolButton_Click(object sender, EventArgs e) ModList.Refresh(); } - private void ModList_SelectedIndexChanged(object sender, EventArgs e) - { - var module = GetSelectedModule(); - - AddStatusMessage(string.Empty); - - ModInfoTabControl.SelectedModule = module; - if (module == null) - return; - - NavSelectMod(module); - } - public void UpdateModContentsTree(CkanModule module, bool force = false) { ModInfoTabControl.UpdateModContentsTree(module, force); @@ -574,174 +561,6 @@ private void OnFilterUpdateTimer(object source, EventArgs e) filterTimer.Stop(); } - /// - /// Programmatic implementation of row sorting by columns. - /// - private void ModList_HeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) - { - var new_sort_column = ModList.Columns[e.ColumnIndex]; - var current_sort_column = ModList.Columns[configuration.SortByColumnIndex]; - - // Reverse the sort order if the current sorting column is clicked again. - configuration.SortDescending = new_sort_column == current_sort_column && !configuration.SortDescending; - - // Reset the glyph. - current_sort_column.HeaderCell.SortGlyphDirection = SortOrder.None; - configuration.SortByColumnIndex = new_sort_column.Index; - UpdateFilters(this); - } - - /// - /// Called on key down when the mod list is focused. - /// Makes the Home/End keys go to the top/bottom of the list respectively. - /// - private void ModList_KeyDown(object sender, KeyEventArgs e) - { - DataGridViewCell cell = null; - switch (e.KeyCode) - { - case Keys.Home: - // First row. - cell = ModList.Rows[0].Cells[2]; - break; - - case Keys.End: - // Last row. - cell = ModList.Rows[ModList.Rows.Count - 1].Cells[2]; - break; - } - - if (cell != null) - { - e.Handled = true; - - // Selects the top/bottom row and scrolls the list to it. - ModList.CurrentCell = cell; - } - } - - /// - /// Called on key press when the mod is focused. Scrolls to the first mod with name - /// beginning with the key pressed. If more than one unique keys are pressed in under - /// a second, it searches for the combination of the keys pressed. If the same key is - /// being pressed repeatedly, it cycles through mods names beginning with that key. - /// If space is pressed, the checkbox at the current row is toggled. - /// - private void ModList_KeyPress(object sender, KeyPressEventArgs e) - { - var current_row = ModList.CurrentRow; - var key = e.KeyChar.ToString(); - - // Check the key. If it is space and the current row is selected, mark the current mod as selected. - if (key == " ") - { - if (current_row != null && current_row.Selected) - { - var gui_mod = (GUIMod)current_row.Tag; - if (gui_mod.IsInstallable()) - MarkModForInstall(gui_mod.Identifier, gui_mod.IsInstallChecked); - } - - e.Handled = true; - return; - } - - if (e.KeyChar == (char)Keys.Enter) - { - // Don't try to search for newlines. - return; - } - - // Determine time passed since last key press. - TimeSpan interval = DateTime.Now - lastSearchTime; - if (interval.TotalSeconds < 1) - { - // Last keypress was < 1 sec ago, so combine the last and current keys. - key = lastSearchKey + key; - } - - // Remember the current time and key. - lastSearchTime = DateTime.Now; - lastSearchKey = key; - - if (key.Distinct().Count() == 1) - { - // Treat repeating and single keypresses the same. - key = key.Substring(0, 1); - } - - FocusMod(key, false); - e.Handled = true; - } - - /// - /// I'm pretty sure this is what gets called when the user clicks on a ticky in the mod list. - /// - private void ModList_CellContentClick(object sender, DataGridViewCellEventArgs e) - { - ModList.CommitEdit(DataGridViewDataErrorContexts.Commit); - } - - private void ModList_CellMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e) - { - if (e.Button != MouseButtons.Left) - return; - - if (e.RowIndex < 0) - return; - - DataGridViewRow row = ModList.Rows[e.RowIndex]; - if (!(row.Cells[0] is DataGridViewCheckBoxCell)) - return; - - // Need to change the state here, because the user hasn't clicked on a checkbox. - row.Cells[0].Value = !(bool)row.Cells[0].Value; - ModList.CommitEdit(DataGridViewDataErrorContexts.Commit); - } - - private async void ModList_CellValueChanged(object sender, DataGridViewCellEventArgs e) - { - int row_index = e.RowIndex; - int column_index = e.ColumnIndex; - - if (row_index < 0 || column_index < 0) - return; - - DataGridView grid = sender as DataGridView; - DataGridViewRow row = grid?.Rows[row_index]; - DataGridViewCell gridCell = row?.Cells[column_index]; - - if (gridCell is DataGridViewLinkCell) - { - // Launch URLs if found in grid - DataGridViewLinkCell cell = gridCell as DataGridViewLinkCell; - string cmd = cell?.Value.ToString(); - if (!string.IsNullOrEmpty(cmd)) - Process.Start(cmd); - } - else if (column_index < 2) - { - GUIMod gui_mod = row?.Tag as GUIMod; - if (gui_mod != null) - { - switch (column_index) - { - case 0: - gui_mod.SetInstallChecked(row); - if (gui_mod.IsInstallChecked) - last_mod_to_have_install_toggled.Push(gui_mod); - break; - case 1: - gui_mod.SetUpgradeChecked(row); - break; - } - await UpdateChangeSetAndConflicts( - RegistryManager.Instance(CurrentInstance).registry - ); - } - } - } - private async Task UpdateChangeSetAndConflicts(IRegistryQuerier registry) { IEnumerable full_change_set = null; @@ -1282,37 +1101,4 @@ private void NavForwardToolButton_Click(object sender, EventArgs e) #endregion } - - public class GUIUser : NullUser - { - public delegate bool DisplayYesNo(string message); - - public Action displayMessage; - public Action displayError; - public DisplayYesNo displayYesNo; - - protected override bool DisplayYesNoDialog(string message) - { - if (displayYesNo == null) - return true; - - return displayYesNo(message); - } - - protected override void DisplayMessage(string message, params object[] args) - { - displayMessage(message, args); - } - - protected override void DisplayError(string message, params object[] args) - { - displayError(message, args); - } - - protected override void ReportProgress(string format, int percent) - { - Main.Instance.SetDescription($"{format} - {percent}%"); - Main.Instance.SetProgress(percent); - } - } } diff --git a/GUI/MainChangeset.cs b/GUI/MainChangeset.cs index 323f2c5389..b626c807fa 100644 --- a/GUI/MainChangeset.cs +++ b/GUI/MainChangeset.cs @@ -46,11 +46,12 @@ public void UpdateChangesDialog(List changeset, BackgroundWorker inst continue; } + CkanModule m = change.Mod.ToModule(); ListViewItem item = new ListViewItem() { - Text = CurrentInstance.Cache.IsCachedZip(change.Mod.ToModule()) - ? $"{change.Mod.Name} {change.Mod.Version} (cached)" - : $"{change.Mod.Name} {change.Mod.Version} ({change.Mod.ToModule()?.download.Host ?? ""}, {change.Mod.DownloadSize})" + Text = CurrentInstance.Cache.IsMaybeCachedZip(m) + ? $"{m.name} {m.version} (cached)" + : $"{m.name} {m.version} ({m.download.Host ?? ""}, {CkanModule.FmtSize(m.download_size)})" }; var sub_change_type = new ListViewItem.ListViewSubItem {Text = change.ChangeType.ToString()}; diff --git a/GUI/MainImport.cs b/GUI/MainImport.cs index 54aaeb3a12..7c26f5ddf5 100644 --- a/GUI/MainImport.cs +++ b/GUI/MainImport.cs @@ -44,7 +44,7 @@ private void ImportModules() ModuleInstaller.GetInstance(CurrentInstance, currentUser).ImportFiles( GetFiles(dlg.FileNames), currentUser, - (string id) => MarkModForInstall(id, false) + (CkanModule mod) => MarkModForInstall(mod.identifier, false) ); } finally diff --git a/GUI/MainInstall.cs b/GUI/MainInstall.cs index ed1d2f6f60..7e53ad5877 100644 --- a/GUI/MainInstall.cs +++ b/GUI/MainInstall.cs @@ -60,8 +60,8 @@ private void InstallMods(object sender, DoWorkEventArgs e) // this probably need // Now work on satisifying dependencies. - var recommended = new Dictionary>(); - var suggested = new Dictionary>(); + var recommended = new Dictionary>(); + var suggested = new Dictionary>(); foreach (var change in opts.Key) { @@ -231,69 +231,56 @@ private void InstallMods(object sender, DoWorkEventArgs e) // this probably need } } - private void AddMod(IEnumerable relations, Dictionary> chooseAble, - string identifier, IRegistryQuerier registry) + private void AddMod( + IEnumerable relations, + Dictionary> chooseAble, + string identifier, + IRegistryQuerier registry) { if (relations == null) return; - foreach (RelationshipDescriptor mod in relations) + foreach (RelationshipDescriptor rel in relations) { - try + List providers = registry.LatestAvailableWithProvides( + rel.name, + CurrentInstance.VersionCriteria(), + rel + ); + foreach (CkanModule provider in providers) { - // if the mod is available for the current KSP version _and_ - // the mod is not installed _and_ - // the mod is not already in the install list - if (registry.LatestAvailable(mod.name, CurrentInstance.VersionCriteria()) != null - && !registry.IsInstalled(mod.name) - && !toInstall.Any(m => m.identifier == mod.name)) + if (!registry.IsInstalled(provider.identifier) + && !toInstall.Any(m => m.identifier == provider.identifier)) { - // add it to the list of chooseAble mods we display to the user - if (!chooseAble.ContainsKey(mod.name)) + // We want to show this mod to the user. Add it. + List dependers; + if (chooseAble.TryGetValue(provider, out dependers)) { - chooseAble.Add(mod.name, new List()); + // Add the dependent mod to the list of reasons this dependency is shown. + dependers.Add(identifier); } - chooseAble[mod.name].Add(identifier); - } - } - catch (ModuleNotFoundKraken) - { - List providers = registry.LatestAvailableWithProvides( - mod.name, - CurrentInstance.VersionCriteria(), - mod - ); - foreach (CkanModule provider in providers) - { - if (!registry.IsInstalled(provider.identifier) - && !toInstall.Any(m => m.identifier == provider.identifier)) + else { - // We want to show this mod to the user. Add it. - if (!chooseAble.ContainsKey(provider.identifier)) - { - // Add a new entry if this provider isn't listed yet. - chooseAble.Add(provider.identifier, new List()); - } - // Add the dependent mod to the list of reasons this dependency is shown. - chooseAble[provider.identifier].Add(identifier); + // Add a new entry if this provider isn't listed yet. + chooseAble.Add(provider, new List() { identifier }); } } } - catch (Kraken) - { - } } } - private void ShowSelection(Dictionary> selectable, bool suggest = false) + private void ShowSelection(Dictionary> selectable, bool suggest = false) { if (installCanceled) return; // If we're going to install something anyway, then don't list it in the // recommended list, since they can't de-select it anyway. - foreach (var item in toInstall) + foreach (var kvp in selectable) { - selectable.Remove(item.identifier); + if (toInstall.Any(m => m.identifier == kvp.Key.identifier)) + { + selectable.Remove(kvp.Key); + } } Dictionary mods = GetShowableMods(selectable); @@ -432,45 +419,37 @@ private void ChooseProvidedModsContinueButton_Click(object sender, EventArgs e) /// /// /// - private Dictionary GetShowableMods(Dictionary> mods) + private Dictionary GetShowableMods(Dictionary> mods) { Dictionary modules = new Dictionary(); var opts = new RelationshipResolverOptions { - with_all_suggests = false, - with_recommends = false, - with_suggests = false, - without_enforce_consistency = false, + with_all_suggests = false, + with_recommends = false, + with_suggests = false, + without_enforce_consistency = false, without_toomanyprovides_kraken = true }; foreach (var pair in mods) { - CkanModule module; - try { - var resolver = new RelationshipResolver(new List { pair.Key }, opts, - RegistryManager.Instance(manager.CurrentInstance).registry, CurrentInstance.VersionCriteria()); - if (!resolver.ModList().Any()) + RelationshipResolver resolver = new RelationshipResolver( + new List { pair.Key }, + opts, + RegistryManager.Instance(manager.CurrentInstance).registry, + CurrentInstance.VersionCriteria() + ); + + if (resolver.ModList().Any()) { - continue; + // Resolver was able to find a way to install, so show it to the user + modules.Add(pair.Key, String.Join(",", pair.Value.ToArray())); } - - module = RegistryManager.Instance(manager.CurrentInstance) - .registry.LatestAvailable(pair.Key, CurrentInstance.VersionCriteria()); - } - catch - { - continue; - } - - if (module == null) - { - continue; } - modules.Add(module, String.Join(",", pair.Value.ToArray())); + catch { } } return modules; } @@ -504,7 +483,7 @@ private void UpdateRecommendedDialog(Dictionary mods, bool s { Tag = module, Checked = !suggested, - Text = CurrentInstance.Cache.IsCachedZip(pair.Key) + Text = CurrentInstance.Cache.IsMaybeCachedZip(pair.Key) ? $"{pair.Key.name} {pair.Key.version} (cached)" : $"{pair.Key.name} {pair.Key.version} ({pair.Key.download.Host ?? ""}, {CkanModule.FmtSize(pair.Key.download_size)})" }; diff --git a/GUI/MainModList.cs b/GUI/MainModList.cs index 9466bf8df9..f64086dc57 100644 --- a/GUI/MainModList.cs +++ b/GUI/MainModList.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -7,6 +8,7 @@ using System.Text.RegularExpressions; using System.Windows.Forms; using CKAN.Versioning; +using log4net; namespace CKAN { @@ -235,6 +237,188 @@ public void _MarkModForUpdate(string identifier) } } } + + private void ModList_SelectedIndexChanged(object sender, EventArgs e) + { + var module = GetSelectedModule(); + + AddStatusMessage(string.Empty); + + ModInfoTabControl.SelectedModule = module; + if (module == null) + return; + + NavSelectMod(module); + } + + /// + /// Programmatic implementation of row sorting by columns. + /// + private void ModList_HeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) + { + var new_sort_column = ModList.Columns[e.ColumnIndex]; + var current_sort_column = ModList.Columns[configuration.SortByColumnIndex]; + + // Reverse the sort order if the current sorting column is clicked again. + configuration.SortDescending = new_sort_column == current_sort_column && !configuration.SortDescending; + + // Reset the glyph. + current_sort_column.HeaderCell.SortGlyphDirection = SortOrder.None; + configuration.SortByColumnIndex = new_sort_column.Index; + UpdateFilters(this); + } + + /// + /// Called on key down when the mod list is focused. + /// Makes the Home/End keys go to the top/bottom of the list respectively. + /// + private void ModList_KeyDown(object sender, KeyEventArgs e) + { + DataGridViewCell cell = null; + switch (e.KeyCode) + { + case Keys.Home: + // First row. + cell = ModList.Rows[0].Cells[2]; + break; + + case Keys.End: + // Last row. + cell = ModList.Rows[ModList.Rows.Count - 1].Cells[2]; + break; + } + + if (cell != null) + { + e.Handled = true; + + // Selects the top/bottom row and scrolls the list to it. + ModList.CurrentCell = cell; + } + } + + /// + /// Called on key press when the mod is focused. Scrolls to the first mod with name + /// beginning with the key pressed. If more than one unique keys are pressed in under + /// a second, it searches for the combination of the keys pressed. If the same key is + /// being pressed repeatedly, it cycles through mods names beginning with that key. + /// If space is pressed, the checkbox at the current row is toggled. + /// + private void ModList_KeyPress(object sender, KeyPressEventArgs e) + { + var current_row = ModList.CurrentRow; + var key = e.KeyChar.ToString(); + + // Check the key. If it is space and the current row is selected, mark the current mod as selected. + if (key == " ") + { + if (current_row != null && current_row.Selected) + { + var gui_mod = (GUIMod)current_row.Tag; + if (gui_mod.IsInstallable()) + MarkModForInstall(gui_mod.Identifier, gui_mod.IsInstallChecked); + } + + e.Handled = true; + return; + } + + if (e.KeyChar == (char)Keys.Enter) + { + // Don't try to search for newlines. + return; + } + + // Determine time passed since last key press. + TimeSpan interval = DateTime.Now - lastSearchTime; + if (interval.TotalSeconds < 1) + { + // Last keypress was < 1 sec ago, so combine the last and current keys. + key = lastSearchKey + key; + } + + // Remember the current time and key. + lastSearchTime = DateTime.Now; + lastSearchKey = key; + + if (key.Distinct().Count() == 1) + { + // Treat repeating and single keypresses the same. + key = key.Substring(0, 1); + } + + FocusMod(key, false); + e.Handled = true; + } + + /// + /// I'm pretty sure this is what gets called when the user clicks on a ticky in the mod list. + /// + private void ModList_CellContentClick(object sender, DataGridViewCellEventArgs e) + { + ModList.CommitEdit(DataGridViewDataErrorContexts.Commit); + } + + private void ModList_CellMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e) + { + if (e.Button != MouseButtons.Left) + return; + + if (e.RowIndex < 0) + return; + + DataGridViewRow row = ModList.Rows[e.RowIndex]; + if (!(row.Cells[0] is DataGridViewCheckBoxCell)) + return; + + // Need to change the state here, because the user hasn't clicked on a checkbox. + row.Cells[0].Value = !(bool)row.Cells[0].Value; + ModList.CommitEdit(DataGridViewDataErrorContexts.Commit); + } + + private async void ModList_CellValueChanged(object sender, DataGridViewCellEventArgs e) + { + int row_index = e.RowIndex; + int column_index = e.ColumnIndex; + + if (row_index < 0 || column_index < 0) + return; + + DataGridView grid = sender as DataGridView; + DataGridViewRow row = grid?.Rows[row_index]; + DataGridViewCell gridCell = row?.Cells[column_index]; + + if (gridCell is DataGridViewLinkCell) + { + // Launch URLs if found in grid + DataGridViewLinkCell cell = gridCell as DataGridViewLinkCell; + string cmd = cell?.Value.ToString(); + if (!string.IsNullOrEmpty(cmd)) + Process.Start(cmd); + } + else if (column_index < 2) + { + GUIMod gui_mod = row?.Tag as GUIMod; + if (gui_mod != null) + { + switch (column_index) + { + case 0: + gui_mod.SetInstallChecked(row); + if (gui_mod.IsInstallChecked) + last_mod_to_have_install_toggled.Push(gui_mod); + break; + case 1: + gui_mod.SetUpgradeChecked(row); + break; + } + await UpdateChangeSetAndConflicts( + RegistryManager.Instance(CurrentInstance).registry + ); + } + } + } + } ///