diff --git a/CHANGELOG.md b/CHANGELOG.md index ce45ff78c2..f907b2d549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file. - [GUI] Restore window position without default instance (#3878 by: HebaruSan; reviewed: techman83) - [CLI] Correctly print cmdline errors with braces (#3880 by: HebaruSan; reviewed: techman83) - [Multiple] Caching and changeset fixes (#3881 by: HebaruSan; reviewed: techman83) +- [GUI] Mod list fixes and improvements (#3883 by: HebaruSan; reviewed: techman83) ### Internal diff --git a/GUI/Controls/ManageMods.Designer.cs b/GUI/Controls/ManageMods.Designer.cs index 085c651d51..44772b35b0 100644 --- a/GUI/Controls/ManageMods.Designer.cs +++ b/GUI/Controls/ManageMods.Designer.cs @@ -346,6 +346,7 @@ private void InitializeComponent() // Installed // this.Installed.Name = "Installed"; + this.Installed.Frozen = true; this.Installed.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.Installed.DefaultCellStyle.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleCenter; this.Installed.Width = 50; @@ -354,6 +355,7 @@ private void InitializeComponent() // AutoInstalled // this.AutoInstalled.Name = "AutoInstalled"; + this.AutoInstalled.Frozen = true; this.AutoInstalled.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.AutoInstalled.DefaultCellStyle.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleCenter; this.AutoInstalled.Width = 50; @@ -362,6 +364,7 @@ private void InitializeComponent() // UpdateCol // this.UpdateCol.Name = "UpdateCol"; + this.UpdateCol.Frozen = true; this.UpdateCol.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.UpdateCol.DefaultCellStyle.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleCenter; this.UpdateCol.Width = 46; @@ -370,6 +373,7 @@ private void InitializeComponent() // ReplaceCol // this.ReplaceCol.Name = "ReplaceCol"; + this.ReplaceCol.Frozen = true; this.ReplaceCol.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic; this.ReplaceCol.DefaultCellStyle.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleCenter; this.ReplaceCol.Width = 46; diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 64c2863c63..99eb69d1e3 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -33,7 +33,6 @@ public ManageMods() mainModList = new ModList(); mainModList.ModFiltersUpdated += UpdateFilters; - UpdateFilters(); FilterToolButton.MouseHover += (sender, args) => FilterToolButton.ShowDropDown(); launchGameToolStripMenuItem.MouseHover += (sender, args) => launchGameToolStripMenuItem.ShowDropDown(); ApplyToolButton.MouseHover += (sender, args) => ApplyToolButton.ShowDropDown(); @@ -63,6 +62,7 @@ public ManageMods() private DateTime lastSearchTime; private string lastSearchKey; private NavigationHistory navHistory; + private static readonly Font uninstallingFont = new Font(SystemFonts.DefaultFont, FontStyle.Strikeout); private List currentChangeSet; private Dictionary conflicts; @@ -121,6 +121,24 @@ private void ChangeSetUpdated() InstallAllCheckbox.Checked = true; } OnChangeSetChanged?.Invoke(ChangeSet, Conflicts); + + var removing = (currentChangeSet ?? Enumerable.Empty()) + .Where(ch => ch?.ChangeType == GUIModChangeType.Remove) + .Select(ch => ch.Mod.identifier) + .ToHashSet(); + foreach (var kvp in mainModList.full_list_of_mod_rows) + { + if (removing.Contains(kvp.Key)) + { + // Set strikeout font for rows being uninstalled + kvp.Value.DefaultCellStyle.Font = uninstallingFont; + } + else if (kvp.Value.DefaultCellStyle.Font != null) + { + // Clear strikeout font for rows not being uninstalled + kvp.Value.DefaultCellStyle.Font = null; + } + } }); } @@ -395,19 +413,7 @@ public void Filter(SavedSearch search, bool merge) { EditModSearches.SetSearches(searches); } - - // Ask the configuration which columns to show. - foreach (DataGridViewColumn col in ModGrid.Columns) - { - // Some columns are always shown, and others are handled by UpdateModsList() - if (col.Name != "Installed" && col.Name != "UpdateCol" && col.Name != "ReplaceCol") - { - col.Visible = !Main.Instance.configuration.HiddenColumnNames.Contains(col.Name); - } - } - - // If these columns aren't hidden by the user, show them if the search includes installed modules - setInstalledColumnsVisible(!SearchesExcludeInstalled(searches)); + ShowHideColumns(searches); }); } @@ -417,19 +423,27 @@ public void SetSearches(List searches) { mainModList.SetSearches(searches); EditModSearches.SetSearches(searches); + ShowHideColumns(searches); + }); + } - // Ask the configuration which columns to show. - foreach (DataGridViewColumn col in ModGrid.Columns) + private void ShowHideColumns(List searches) + { + // Ask the configuration which columns to show. + foreach (DataGridViewColumn col in ModGrid.Columns) + { + // Some columns are always shown, and others are handled by UpdateModsList() + if (col.Name != "Installed" && col.Name != "UpdateCol" && col.Name != "ReplaceCol" + && !installedColumnNames.Contains(col.Name)) { - // Some columns are always shown, and others are handled by UpdateModsList() - if (col.Name != "Installed" && col.Name != "UpdateCol" && col.Name != "ReplaceCol") - { - col.Visible = !Main.Instance.configuration.HiddenColumnNames.Contains(col.Name); - } + col.Visible = !Main.Instance.configuration.HiddenColumnNames.Contains(col.Name); } + } - setInstalledColumnsVisible(!SearchesExcludeInstalled(searches)); - }); + // If these columns aren't hidden by the user, show them if the search includes installed modules + setInstalledColumnsVisible(mainModList.HasAnyInstalled + && !SearchesExcludeInstalled(searches) + && mainModList.HasVisibleInstalled()); } private static readonly string[] installedColumnNames = new string[] @@ -447,9 +461,7 @@ private void setInstalledColumnsVisible(bool visible) } private static bool SearchesExcludeInstalled(List searches) - { - return searches?.All(s => s != null && s.Installed == false) ?? false; - } + => searches?.All(s => s != null && s.Installed == false) ?? false; public void MarkAllUpdates() { @@ -976,9 +988,10 @@ private bool ShowModContextMenu() if (guiMod != null) { ModListContextMenuStrip.Show(Cursor.Position); + var isDownloadable = !guiMod.ToModule()?.IsMetapackage ?? false; // Set the menu options - downloadContentsToolStripMenuItem.Enabled = !guiMod.ToModule().IsMetapackage && !guiMod.IsCached; - purgeContentsToolStripMenuItem.Enabled = !guiMod.ToModule().IsMetapackage && guiMod.IsCached; + downloadContentsToolStripMenuItem.Enabled = isDownloadable && !guiMod.IsCached; + purgeContentsToolStripMenuItem.Enabled = isDownloadable && guiMod.IsCached; reinstallToolStripMenuItem.Enabled = guiMod.IsInstalled && !guiMod.IsAutodetected; return true; } @@ -1035,11 +1048,8 @@ private void reinstallToolStripMenuItem_Click(object sender, EventArgs e) } public Dictionary AllGUIMods() - => ModGrid.Rows.Cast() - .Select(row => row.Tag as GUIMod) - .Where(guiMod => guiMod != null) - .ToDictionary(guiMod => guiMod.Identifier, - guiMod => guiMod); + => mainModList.Modules.ToDictionary(guiMod => guiMod.Identifier, + guiMod => guiMod); private void purgeContentsToolStripMenuItem_Click(object sender, EventArgs e) { @@ -1058,7 +1068,8 @@ private void purgeContentsToolStripMenuItem_Click(object sender, EventArgs e) // Update all mods that share the same ZIP var allGuiMods = AllGUIMods(); foreach (var otherMod in selected.ToModule().GetDownloadsGroup( - allGuiMods.Values.Select(guiMod => guiMod.ToModule()))) + allGuiMods.Values.Select(guiMod => guiMod.ToModule()) + .Where(mod => mod != null))) { allGuiMods[otherMod.identifier].UpdateIsCached(); } @@ -1080,7 +1091,9 @@ private void EditModSearches_ApplySearches(List searches) mainModList.SetSearches(searches); // If these columns aren't hidden by the user, show them if the search includes installed modules - setInstalledColumnsVisible(!SearchesExcludeInstalled(searches)); + setInstalledColumnsVisible(mainModList.HasAnyInstalled + && !SearchesExcludeInstalled(searches) + && mainModList.HasVisibleInstalled()); } private void EditModSearches_SurrenderFocus() @@ -1208,20 +1221,14 @@ private void _UpdateModsList(Dictionary old_modules = null) Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListPopulatingList); // Update our mod listing - mainModList.ConstructModList(gui_mods.ToList(), Main.Instance.CurrentInstance.Name, ChangeSet); - mainModList.Modules = new ReadOnlyCollection( - mainModList.full_list_of_mod_rows.Values.Select(row => row.Tag as GUIMod).ToList()); + mainModList.ConstructModList(gui_mods, Main.Instance.CurrentInstance.Name, ChangeSet); // C# 7.0: Executes the task and discards it _ = UpdateChangeSetAndConflicts(Main.Instance.CurrentInstance, registry); Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListUpdatingFilters); - var has_any_updates = gui_mods.Any(mod => mod.HasUpdate); - var has_unheld_updates = gui_mods.Any(mod => mod.HasUpdate && !Main.Instance.LabelsHeld(mod.Identifier)); - var has_any_installed = gui_mods.Any(mod => mod.IsInstalled); - var has_any_replacements = gui_mods.Any(mod => mod.IsInstalled && mod.HasReplacement); - + var has_unheld_updates = mainModList.Modules.Any(mod => mod.HasUpdate && !Main.Instance.LabelsHeld(mod.Identifier)); Util.Invoke(menuStrip2, () => { FilterCompatibleButton.Text = String.Format(Properties.Resources.MainModListCompatible, @@ -1257,9 +1264,8 @@ private void _UpdateModsList(Dictionary old_modules = null) // After the update / replacement, they are hidden again. Util.Invoke(ModGrid, () => { - ModGrid.Columns["UpdateCol"].Visible = has_any_updates; - ModGrid.Columns["AutoInstalled"].Visible = has_any_installed && !Main.Instance.configuration.HiddenColumnNames.Contains("AutoInstalled"); - ModGrid.Columns["ReplaceCol"].Visible = has_any_replacements; + UpdateCol.Visible = mainModList.Modules.Any(mod => mod.HasUpdate); + ReplaceCol.Visible = mainModList.Modules.Any(mod => mod.IsInstalled && mod.HasReplacement); }); Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListUpdatingTray); diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index c91d0988b2..86fbf4ad27 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -420,7 +420,6 @@ private void CurrentInstanceUpdated(bool allowRepoUpdate) } else { - SetupDefaultSearch(); RefreshModList(); } } diff --git a/GUI/Main/MainDownload.cs b/GUI/Main/MainDownload.cs index 16ef6d7922..dd56cbe15d 100644 --- a/GUI/Main/MainDownload.cs +++ b/GUI/Main/MainDownload.cs @@ -88,7 +88,8 @@ private void UpdateCachedByDownloads(GUIMod module) // Update all mods that share the same ZIP var allGuiMods = ManageMods.AllGUIMods(); foreach (var otherMod in module.ToModule().GetDownloadsGroup( - allGuiMods.Values.Select(guiMod => guiMod.ToModule()))) + allGuiMods.Values.Select(guiMod => guiMod.ToModule()) + .Where(mod => mod != null))) { allGuiMods[otherMod.identifier].UpdateIsCached(); } diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index 3819e57d58..469db2a9d8 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -31,21 +31,24 @@ public enum GUIModFilter Tag = 11, } + /// + /// The holder of the list of mods to be shown. + /// Should be a pure data model and avoid UI stuff, but it's not there yet. + /// public class ModList { //identifier, row internal Dictionary full_list_of_mod_rows; - public ModList() - { - Modules = new ReadOnlyCollection(new List()); - } - - //TODO Move to relationship resolver and have it use this. - public delegate Task HandleTooManyProvides(TooManyModsProvideKraken kraken); - public event Action ModFiltersUpdated; - public ReadOnlyCollection Modules { get; set; } + public ReadOnlyCollection Modules { get; private set; } = + new ReadOnlyCollection(new List()); + public bool HasAnyInstalled { get; private set; } + + // Unlike GUIMod.IsInstalled, DataGridViewRow.Visible can change on the fly without notifying us + public bool HasVisibleInstalled() + => full_list_of_mod_rows.Values.Any(row => ((row.Tag as GUIMod)?.IsInstalled ?? false) + && row.Visible); public readonly ModuleLabelList ModuleLabels = ModuleLabelList.Load(ModuleLabelList.DefaultPath) ?? ModuleLabelList.GetDefaultLabels(); @@ -69,11 +72,9 @@ public void SetSearches(List newSearches) } private static bool SearchesEqual(List a, List b) - { - return a == null ? b == null + => a == null ? b == null : b == null ? false : a.SequenceEqual(b); - } private static string FilterName(GUIModFilter filter, ModuleTag tag = null, ModuleLabel label = null) { @@ -99,13 +100,11 @@ private static string FilterName(GUIModFilter filter, ModuleTag tag = null, Modu } public static SavedSearch FilterToSavedSearch(GUIModFilter filter, ModuleTag tag = null, ModuleLabel label = null) - { - return new SavedSearch() + => new SavedSearch() { Name = FilterName(filter, tag, label), Values = new List() { new ModSearch(filter, tag, label).Combined }, }; - } private static readonly RelationshipResolverOptions conflictOptions = new RelationshipResolverOptions() { @@ -169,9 +168,7 @@ public Tuple, Dictionary> ComputeFull .ToList(), modules_to_install)) { - //TODO This would be a good place to have an event that alters the row's graphics to show it will be removed - CkanModule depMod; - if (installed_modules.TryGetValue(dependent, out depMod)) + if (installed_modules.TryGetValue(dependent, out CkanModule depMod)) { CkanModule module_by_version = registry.GetModuleByVersion(depMod.identifier, depMod.version) @@ -234,50 +231,31 @@ private IEnumerable InstalledAfterChanges( } public bool IsVisible(GUIMod mod, string instanceName) - { - return (activeSearches?.Any(s => s?.Matches(mod) ?? true) ?? true) + => (activeSearches?.Any(s => s?.Matches(mod) ?? true) ?? true) && !HiddenByTagsOrLabels(mod, instanceName); - } private bool TagInSearches(ModuleTag tag) - { - return activeSearches?.Any(s => s?.TagNames.Contains(tag.Name) ?? false) ?? false; - } + => activeSearches?.Any(s => s?.TagNames.Contains(tag.Name) ?? false) ?? false; private bool LabelInSearches(ModuleLabel label) - { - return activeSearches?.Any(s => s?.Labels.Contains(label) ?? false) ?? false; - } + => activeSearches?.Any(s => s?.Labels.Contains(label) ?? false) ?? false; private bool HiddenByTagsOrLabels(GUIMod m, string instanceName) - { // "Hide" labels apply to all non-custom filters - if (ModuleLabels?.LabelsFor(instanceName) - .Where(l => !LabelInSearches(l) && l.Hide) - .Any(l => l.ModuleIdentifiers.Contains(m.Identifier)) - ?? false) - { - return true; - } - if (ModuleTags?.Tags?.Values - .Where(t => !TagInSearches(t) && t.Visible == false) - .Any(t => t.ModuleIdentifiers.Contains(m.Identifier)) + => (ModuleLabels?.LabelsFor(instanceName) + .Where(l => !LabelInSearches(l) && l.Hide) + .Any(l => l.ModuleIdentifiers.Contains(m.Identifier)) ?? false) - { - return true; - } - return false; - } + || (ModuleTags?.Tags?.Values + .Where(t => !TagInSearches(t) && t.Visible == false) + .Any(t => t.ModuleIdentifiers.Contains(m.Identifier)) + ?? false); public int CountModsBySearches(List searches) - { - return Modules.Count(mod => searches?.Any(s => s?.Matches(mod) ?? true) ?? true); - } + => Modules.Count(mod => searches?.Any(s => s?.Matches(mod) ?? true) ?? true); public int CountModsByFilter(GUIModFilter filter) - { - return CountModsBySearches(new List() { new ModSearch(filter, null, null) }); - } + => CountModsBySearches(new List() { new ModSearch(filter, null, null) }); /// /// Constructs the mod list suitable for display to the user. @@ -289,11 +267,12 @@ public int CountModsByFilter(GUIModFilter filter) public IEnumerable ConstructModList( IEnumerable modules, string instanceName, IEnumerable mc = null) { - List changes = mc?.ToList(); - full_list_of_mod_rows = modules.ToDictionary( + Modules = new ReadOnlyCollection(modules.ToList()); + var changes = mc?.ToList(); + full_list_of_mod_rows = Modules.ToDictionary( gm => gm.Identifier, - gm => MakeRow(gm, changes, instanceName) - ); + gm => MakeRow(gm, changes, instanceName)); + HasAnyInstalled = Modules.Any(m => m.IsInstalled); return full_list_of_mod_rows.Values; } @@ -398,24 +377,13 @@ private static string ToGridText(string text) => Platform.IsMono ? text.Replace("&", "&&") : text; public Color GetRowBackground(GUIMod mod, bool conflicted, string instanceName) - { - if (conflicted) - { - return Color.LightCoral; - } - DataGridViewRow row; - if (full_list_of_mod_rows.TryGetValue(mod.Identifier, out row)) - { - Color? myColor = ModuleLabels.LabelsFor(instanceName) - .FirstOrDefault(l => l.ModuleIdentifiers.Contains(mod.Identifier)) - ?.Color; - if (myColor.HasValue) - { - return myColor.Value; - } - } - return Color.Empty; - } + => conflicted ? Color.LightCoral + : full_list_of_mod_rows.ContainsKey(mod.Identifier) + ? ModuleLabels.LabelsFor(instanceName) + .FirstOrDefault(l => l.ModuleIdentifiers.Contains(mod.Identifier)) + ?.Color + ?? Color.Empty + : Color.Empty; /// /// Update the color and visible state of the given row @@ -436,13 +404,10 @@ public void ReapplyLabels(GUIMod mod, bool conflicted, string instanceName) /// Returns a version string shorn of any leading epoch as delimited by a single colon /// public string StripEpoch(string version) - { // If our version number starts with a string of digits, followed by // a colon, and then has no more colons, we're probably safe to assume // the first string of digits is an epoch - //return Regex.IsMatch(version, @"^[0-9][0-9]*:[^:]+$") ? Regex.Replace(version, @"^([^:]+):([^:]+)$", @"$2") : version; - return ContainsEpoch.IsMatch(version) ? RemoveEpoch.Replace(version, @"$2") : version; - } + => ContainsEpoch.IsMatch(version) ? RemoveEpoch.Replace(version, @"$2") : version; private static readonly Regex ContainsEpoch = new Regex(@"^[0-9][0-9]*:[^:]+$", RegexOptions.Compiled); private static readonly Regex RemoveEpoch = new Regex(@"^([^:]+):([^:]+)$", RegexOptions.Compiled);