diff --git a/GUI/Controls/EditModSearch.cs b/GUI/Controls/EditModSearch.cs index ebf245e2f..c141cdb03 100644 --- a/GUI/Controls/EditModSearch.cs +++ b/GUI/Controls/EditModSearch.cs @@ -135,10 +135,10 @@ private void ImmediateHandler(object? sender, EventArgs? e) } break; } - if (Main.Instance?.CurrentInstance != null) + if (Main.Instance?.CurrentInstance is GameInstance inst) { // Sync the search boxes immediately - currentSearch = ModSearch.Parse(FilterCombinedTextBox.Text); + currentSearch = ModSearch.Parse(inst, FilterCombinedTextBox.Text); } SearchToEditor(); } diff --git a/GUI/Controls/EditModSearchDetails.cs b/GUI/Controls/EditModSearchDetails.cs index c6a3737cf..0231c787a 100644 --- a/GUI/Controls/EditModSearchDetails.cs +++ b/GUI/Controls/EditModSearchDetails.cs @@ -45,6 +45,7 @@ public void SetFocus() public ModSearch CurrentSearch() => new ModSearch( + Main.Instance!.CurrentInstance!, FilterByNameTextBox.Text, FilterByAuthorTextBox.Text.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries).ToList(), FilterByDescriptionTextBox.Text, diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index f83e6b9e3..d4c70639e 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -294,16 +294,18 @@ private void FilterLabelsToolButton_DropDown_Opening(object? sender, CancelEvent if (currentInstance != null) { FilterLabelsToolButton.DropDownItems.Clear(); - foreach (ModuleLabel mlbl in ModuleLabelList.ModuleLabels.LabelsFor(currentInstance.Name)) - { - FilterLabelsToolButton.DropDownItems.Add(new ToolStripMenuItem( - $"{mlbl.Name} ({mlbl.ModuleCount(currentInstance.game)})", - null, customFilterButton_Click) - { - Tag = mlbl, - ToolTipText = Properties.Resources.FilterLinkToolTip, - }); - } + FilterLabelsToolButton.DropDownItems.AddRange( + ModuleLabelList.ModuleLabels + .LabelsFor(currentInstance.Name) + .Select(mlbl => new ToolStripMenuItem( + $"{mlbl.Name} ({mlbl.ModuleCount(currentInstance.game)})", + null, customFilterButton_Click) + { + Tag = mlbl, + BackColor = mlbl.Color ?? Color.Transparent, + ToolTipText = Properties.Resources.FilterLinkToolTip, + }) + .ToArray()); } } @@ -389,73 +391,73 @@ private void tagFilterButton_Click(object? sender, EventArgs? e) { var clicked = sender as ToolStripMenuItem; var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.Tag, clicked?.Tag as ModuleTag, null), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.Tag, clicked?.Tag as ModuleTag, null), merge); } private void customFilterButton_Click(object? sender, EventArgs? e) { var clicked = sender as ToolStripMenuItem; var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.CustomLabel, null, clicked?.Tag as ModuleLabel), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.CustomLabel, null, clicked?.Tag as ModuleLabel), merge); } private void FilterCompatibleButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.Compatible), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.Compatible), merge); } private void FilterInstalledButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.Installed), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.Installed), merge); } private void FilterInstalledUpdateButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.InstalledUpdateAvailable), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.InstalledUpdateAvailable), merge); } private void FilterReplaceableButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.Replaceable), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.Replaceable), merge); } private void FilterCachedButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.Cached), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.Cached), merge); } private void FilterUncachedButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.Uncached), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.Uncached), merge); } private void FilterNewButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.NewInRepository), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.NewInRepository), merge); } private void FilterNotInstalledButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.NotInstalled), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.NotInstalled), merge); } private void FilterIncompatibleButton_Click(object? sender, EventArgs? e) { var merge = ModifierKeys.HasAnyFlag(Keys.Control, Keys.Shift); - Filter(ModList.FilterToSavedSearch(GUIModFilter.Incompatible), merge); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.Incompatible), merge); } private void FilterAllButton_Click(object? sender, EventArgs? e) { - Filter(ModList.FilterToSavedSearch(GUIModFilter.All), false); + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.All), false); } /// @@ -468,7 +470,7 @@ public void Filter(SavedSearch search, bool merge) if (currentInstance != null) { var searches = search.Values - .Select(s => ModSearch.Parse(s)) + .Select(s => ModSearch.Parse(currentInstance!, s)) .OfType() .ToList(); @@ -1501,25 +1503,25 @@ private bool _UpdateModsList(Dictionary? old_modules = null) Util.Invoke(menuStrip2, () => { FilterCompatibleButton.Text = string.Format(Properties.Resources.MainModListCompatible, - mainModList.CountModsByFilter(GUIModFilter.Compatible)); + mainModList.CountModsByFilter(currentInstance, GUIModFilter.Compatible)); FilterInstalledButton.Text = string.Format(Properties.Resources.MainModListInstalled, - mainModList.CountModsByFilter(GUIModFilter.Installed)); + mainModList.CountModsByFilter(currentInstance, GUIModFilter.Installed)); FilterInstalledUpdateButton.Text = string.Format(Properties.Resources.MainModListUpgradeable, - mainModList.CountModsByFilter(GUIModFilter.InstalledUpdateAvailable)); + mainModList.CountModsByFilter(currentInstance, GUIModFilter.InstalledUpdateAvailable)); FilterReplaceableButton.Text = string.Format(Properties.Resources.MainModListReplaceable, - mainModList.CountModsByFilter(GUIModFilter.Replaceable)); + mainModList.CountModsByFilter(currentInstance, GUIModFilter.Replaceable)); FilterCachedButton.Text = string.Format(Properties.Resources.MainModListCached, - mainModList.CountModsByFilter(GUIModFilter.Cached)); + mainModList.CountModsByFilter(currentInstance, GUIModFilter.Cached)); FilterUncachedButton.Text = string.Format(Properties.Resources.MainModListUncached, - mainModList.CountModsByFilter(GUIModFilter.Uncached)); + mainModList.CountModsByFilter(currentInstance, GUIModFilter.Uncached)); FilterNewButton.Text = string.Format(Properties.Resources.MainModListNewlyCompatible, - mainModList.CountModsByFilter(GUIModFilter.NewInRepository)); + mainModList.CountModsByFilter(currentInstance, GUIModFilter.NewInRepository)); FilterNotInstalledButton.Text = string.Format(Properties.Resources.MainModListNotInstalled, - mainModList.CountModsByFilter(GUIModFilter.NotInstalled)); + mainModList.CountModsByFilter(currentInstance, GUIModFilter.NotInstalled)); FilterIncompatibleButton.Text = string.Format(Properties.Resources.MainModListIncompatible, - mainModList.CountModsByFilter(GUIModFilter.Incompatible)); + mainModList.CountModsByFilter(currentInstance, GUIModFilter.Incompatible)); FilterAllButton.Text = string.Format(Properties.Resources.MainModListAll, - mainModList.CountModsByFilter(GUIModFilter.All)); + mainModList.CountModsByFilter(currentInstance, GUIModFilter.All)); UpdateAllToolButton.Enabled = has_unheld_updates; }); @@ -1851,7 +1853,7 @@ private void hiddenTagsLabelsLinkList_TagClicked(ModuleTag tag, bool merge) private void hiddenTagsLabelsLinkList_LabelClicked(ModuleLabel label, bool merge) { - Filter(ModList.FilterToSavedSearch(GUIModFilter.CustomLabel, null, label), + Filter(ModList.FilterToSavedSearch(currentInstance!, GUIModFilter.CustomLabel, null, label), merge); } diff --git a/GUI/Controls/ModInfo.cs b/GUI/Controls/ModInfo.cs index 25be5c85f..f860af21f 100644 --- a/GUI/Controls/ModInfo.cs +++ b/GUI/Controls/ModInfo.cs @@ -185,12 +185,14 @@ private void UpdateTagsAndLabels(CkanModule mod) private void tagsLabelsLinkList_TagClicked(ModuleTag tag, bool merge) - => OnChangeFilter?.Invoke(ModList.FilterToSavedSearch(GUIModFilter.Tag, + => OnChangeFilter?.Invoke(ModList.FilterToSavedSearch(Main.Instance!.CurrentInstance!, + GUIModFilter.Tag, tag, null), merge); private void tagsLabelsLinkList_LabelClicked(ModuleLabel label, bool merge) - => OnChangeFilter?.Invoke(ModList.FilterToSavedSearch(GUIModFilter.CustomLabel, + => OnChangeFilter?.Invoke(ModList.FilterToSavedSearch(Main.Instance!.CurrentInstance!, + GUIModFilter.CustomLabel, null, label), merge); diff --git a/GUI/Controls/ModInfoTabs/Metadata.cs b/GUI/Controls/ModInfoTabs/Metadata.cs index 72e54d7b7..43171a8a7 100644 --- a/GUI/Controls/ModInfoTabs/Metadata.cs +++ b/GUI/Controls/ModInfoTabs/Metadata.cs @@ -132,7 +132,8 @@ private void OnAuthorClick(object? sender, LinkLabelLinkClickedEventArgs? e) new SavedSearch() { Name = string.Format(Properties.Resources.AuthorSearchName, author), - Values = Enumerable.Repeat(ModSearch.FromAuthors(Enumerable.Repeat(author, 1)).Combined, 1) + Values = Enumerable.Repeat(ModSearch.FromAuthors(Main.Instance!.CurrentInstance!, + Enumerable.Repeat(author, 1)).Combined, 1) .OfType() .ToList(), }, diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index c502e1b47..d2d731114 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -666,6 +666,7 @@ private void SetupDefaultSearch() // Fall back to old setting ManageMods.Filter( ModList.FilterToSavedSearch( + CurrentInstance, (GUIModFilter)configuration.ActiveFilter, configuration.TagFilter == null ? null @@ -678,7 +679,7 @@ private void SetupDefaultSearch() } else { - var searches = def.Select(s => ModSearch.Parse(s)) + var searches = def.Select(s => ModSearch.Parse(CurrentInstance, s)) .OfType() .ToList(); ManageMods.SetSearches(searches); diff --git a/GUI/Main/MainTrayIcon.cs b/GUI/Main/MainTrayIcon.cs index 1961b51af..9255759d0 100644 --- a/GUI/Main/MainTrayIcon.cs +++ b/GUI/Main/MainTrayIcon.cs @@ -55,17 +55,20 @@ private void UpdateTrayState() private void UpdateTrayInfo() { - var count = ManageMods.mainModList.CountModsByFilter(GUIModFilter.InstalledUpdateAvailable); - - if (count == 0) - { - updatesToolStripMenuItem.Enabled = false; - updatesToolStripMenuItem.Text = Properties.Resources.MainTrayNoUpdates; - } - else + if (CurrentInstance != null) { - updatesToolStripMenuItem.Enabled = true; - updatesToolStripMenuItem.Text = string.Format(Properties.Resources.MainTrayUpdatesAvailable, count); + var count = ManageMods.mainModList.CountModsByFilter(CurrentInstance, + GUIModFilter.InstalledUpdateAvailable); + if (count == 0) + { + updatesToolStripMenuItem.Enabled = false; + updatesToolStripMenuItem.Text = Properties.Resources.MainTrayNoUpdates; + } + else + { + updatesToolStripMenuItem.Enabled = true; + updatesToolStripMenuItem.Text = string.Format(Properties.Resources.MainTrayUpdatesAvailable, count); + } } toolStripSeparator4.Visible = true; updatesToolStripMenuItem.Visible = true; diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index 85ea02003..1ec5328c9 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -103,13 +103,14 @@ private static string FilterName(GUIModFilter filter, return ""; } - public static SavedSearch FilterToSavedSearch(GUIModFilter filter, + public static SavedSearch FilterToSavedSearch(GameInstance instance, + GUIModFilter filter, ModuleTag? tag = null, ModuleLabel? label = null) => new SavedSearch() { Name = FilterName(filter, tag, label), - Values = new List() { new ModSearch(filter, tag, label).Combined ?? "" }, + Values = new List() { new ModSearch(instance, filter, tag, label).Combined ?? "" }, }; private static RelationshipResolverOptions conflictOptions(StabilityToleranceConfig stabilityTolerance) @@ -277,8 +278,8 @@ private bool HiddenByTagsOrLabels(GUIMod m, string instanceName, IGame game, Reg public int CountModsBySearches(List searches) => Modules.Count(mod => searches?.Any(s => s?.Matches(mod) ?? true) ?? true); - public int CountModsByFilter(GUIModFilter filter) - => CountModsBySearches(new List() { new ModSearch(filter, null, null) }); + public int CountModsByFilter(GameInstance inst, GUIModFilter filter) + => CountModsBySearches(new List() { new ModSearch(inst, filter, null, null) }); /// /// Constructs the mod list suitable for display to the user. diff --git a/GUI/Model/ModSearch.cs b/GUI/Model/ModSearch.cs index 6cd81735d..c5527b5ca 100644 --- a/GUI/Model/ModSearch.cs +++ b/GUI/Model/ModSearch.cs @@ -30,6 +30,7 @@ public sealed class ModSearch : IEquatable /// Identifier prefix to find in mod conflicts relationships /// Full formatted search string if known, will be auto generated otherwise public ModSearch( + GameInstance instance, string byName, List byAuthors, string byDescription, List? licenses, List? localizations, List? depends, List? recommends, List? suggests, List? conflicts, @@ -39,6 +40,7 @@ public ModSearch( bool? upgradeable, bool? replaceable, string? combined = null) { + Instance = instance; Name = (ShouldNegateTerm(byName, out string subName) ? "-" : "") + CkanModule.nonAlphaNums.Replace(subName, ""); initStringList(Authors, byAuthors); @@ -55,6 +57,7 @@ public ModSearch( initStringList(TagNames, tagNames); initStringList(LabelNames, labelNames); + LabelsByNegation = FindLabels(instance, LabelNames); Compatible = compatible; Installed = installed; @@ -74,10 +77,12 @@ private static void initStringList(List dest, List? source) } } - public ModSearch(GUIModFilter filter, + public ModSearch(GameInstance instance, + GUIModFilter filter, ModuleTag? tag = null, ModuleLabel? label = null) { + Instance = instance; switch (filter) { case GUIModFilter.Compatible: Compatible = true; break; @@ -95,9 +100,12 @@ public ModSearch(GUIModFilter filter, case GUIModFilter.All: break; } + LabelsByNegation = FindLabels(instance, LabelNames); Combined = getCombined(); } + private readonly GameInstance Instance; + /// /// String to search for in mod names, identifiers, and abbreviations /// @@ -155,6 +163,7 @@ public ModSearch(GUIModFilter filter, public readonly List TagNames = new List(); public readonly List LabelNames = new List(); + private readonly IDictionary<(bool negate, bool exclude), ModuleLabel[]> LabelsByNegation; public readonly bool? Compatible; public readonly bool? Installed; @@ -168,8 +177,9 @@ public ModSearch(GUIModFilter filter, /// /// The authors for the search /// A search for the authors - public static ModSearch FromAuthors(IEnumerable authors) + public static ModSearch FromAuthors(GameInstance instance, IEnumerable authors) => new ModSearch( + instance, // Can't search for spaces, so massage them like SearchableAuthors "", authors.Select(a => CkanModule.nonAlphaNums.Replace(a, "")).ToList(), "", null, null, @@ -185,6 +195,7 @@ public static ModSearch FromAuthors(IEnumerable authors) /// A search containing all the search terms public ModSearch MergedWith(ModSearch other) => new ModSearch( + Instance, Name + other.Name, Authors.Concat(other.Authors).Distinct().ToList(), Description + other.Description, @@ -308,7 +319,7 @@ private static string triStateString(bool val, string suffix) /// /// New search object, or null if no search terms defined /// - public static ModSearch? Parse(string combined) + public static ModSearch? Parse(GameInstance instance, string combined) { if (string.IsNullOrWhiteSpace(combined)) { @@ -444,6 +455,7 @@ private static string triStateString(bool val, string suffix) } } return new ModSearch( + instance, byName, byAuthors, byDescription, byLicenses, byLocalizations, depends, recommends, suggests, conflicts, supports, @@ -590,20 +602,14 @@ private bool MatchesTags(GUIMod mod) : mod.ToModule().Tags?.Contains(subTag) ?? false)); private bool MatchesLabels(GUIMod mod) - => LabelNames.Count < 1 - || (Main.Instance?.CurrentInstance is GameInstance inst - && ModuleLabelList.ModuleLabels.LabelsFor(inst.Name) - .ToArray() - is ModuleLabel[] instanceLabels - && LabelNames.All(ln => - ShouldNegateTerm(ln, out string subLabel) ^ ( - string.IsNullOrEmpty(subLabel) - ? instanceLabels.All(lbl => !lbl.ContainsModule(inst.game, mod.Identifier)) - : instanceLabels.Where(lbl => lbl.Name == subLabel) - .ToArray() - is ModuleLabel[] myLabels - && myLabels.Length > 0 - && myLabels.All(lbl => lbl.ContainsModule(inst.game, mod.Identifier))))); + => LabelsByNegation.All(kvp => + kvp.Key.negate ^ ( + kvp.Key.exclude + ? kvp.Value.All(lbl => !lbl.ContainsModule(Instance.game, + mod.Identifier)) + : kvp.Value.Length > 0 + && kvp.Value.All(lbl => lbl.ContainsModule(Instance.game, + mod.Identifier)))); private bool MatchesCompatible(GUIMod mod) => !Compatible.HasValue || Compatible.Value == !mod.IsIncompatible; @@ -623,6 +629,37 @@ private bool MatchesUpgradeable(GUIMod mod) private bool MatchesReplaceable(GUIMod mod) => !Replaceable.HasValue || Replaceable.Value == (mod.IsInstalled && mod.HasReplacement); + private static IDictionary<(bool negate, bool exclude), ModuleLabel[]> FindLabels( + GameInstance inst, + IEnumerable names) + => FindLabels(ModuleLabelList.ModuleLabels + .LabelsFor(inst.Name) + .ToArray(), + names); + + private static IDictionary<(bool negate, bool exclude), ModuleLabel[]> FindLabels( + ModuleLabel[] instLabels, + IEnumerable names) + => names.Select(ln => (negate: ShouldNegateTerm(ln.Replace(" ", ""), + out string subLabel), + exclude: string.IsNullOrEmpty(subLabel), + labels: string.IsNullOrEmpty(subLabel) + ? instLabels + : instLabels.Where(lbl => lbl.Name + .Replace(" ", "") + .Equals(subLabel, + StringComparison.OrdinalIgnoreCase)) + .ToArray())) + .GroupBy(tuple => (tuple.negate, tuple.exclude), + tuple => tuple.labels) + .ToDictionary(grp => grp.Key, + grp => grp.Any(labels => labels.Length < 1) + // A name that matches no labels makes nothing match + ? Array.Empty() + : grp.SelectMany(labels => labels) + .Distinct() + .ToArray()); + public bool Equals(ModSearch? other) => other != null && Name == other.Name diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index 58aaeb61e..d4f6ac83f 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -69,8 +69,11 @@ public static Array GetFilters() [TestCaseSource("GetFilters")] public void CountModsByFilter_EmptyModList_ReturnsZero(GUIModFilter filter) { - var item = new ModList(); - Assert.That(item.CountModsByFilter(filter), Is.EqualTo(0)); + using (var tidy = new DisposableKSP()) + { + var item = new ModList(); + Assert.That(item.CountModsByFilter(tidy.KSP, filter), Is.EqualTo(0)); + } } [Test]