From 2fc3df74bd803c8a88b2e241460630dcd872d02d Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Wed, 25 Sep 2024 13:44:02 -0500 Subject: [PATCH 1/2] Apply filters to Contents tab, red highlight if missing --- Core/ModuleInstaller.cs | 18 ++--- GUI/Controls/ModInfoTabs/Contents.Designer.cs | 1 + GUI/Controls/ModInfoTabs/Contents.cs | 79 +++++++++++++------ GUI/Properties/Resources.resx | 2 + 4 files changed, 68 insertions(+), 32 deletions(-) diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 6163b2c248..de8d2047f8 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -584,21 +584,21 @@ public static List FindInstallableFiles(CkanModule module, stri /// /// Returns the module contents if and only if we have it - /// available in our cache. Returns null, otherwise. + /// available in our cache, empty sequence otherwise. /// /// Intended for previews. /// - public static IEnumerable GetModuleContentsList(NetModuleCache Cache, - GameInstance instance, - - CkanModule module) + public static IEnumerable GetModuleContents(NetModuleCache Cache, + GameInstance instance, + CkanModule module, + HashSet filters) => (Cache.GetCachedFilename(module) is string filename ? Utilities.DefaultIfThrows(() => FindInstallableFiles(module, filename, instance) - // Skip folders - .Where(f => !f.source.IsDirectory) - .Select(f => instance.ToRelativeGameDir(f.destination))) + .Where(instF => !filters.Any(filt => + instF.destination != null + && instF.destination.Contains(filt)))) : null) - ?? Enumerable.Empty(); + ?? Enumerable.Empty(); #endregion diff --git a/GUI/Controls/ModInfoTabs/Contents.Designer.cs b/GUI/Controls/ModInfoTabs/Contents.Designer.cs index 2b9edfa93d..c7c79aa1ed 100644 --- a/GUI/Controls/ModInfoTabs/Contents.Designer.cs +++ b/GUI/Controls/ModInfoTabs/Contents.Designer.cs @@ -85,6 +85,7 @@ private void InitializeComponent() this.ContentsPreviewTree.ImageList.Images.Add("file", global::CKAN.GUI.EmbeddedImages.file); this.ContentsPreviewTree.ShowPlusMinus = true; this.ContentsPreviewTree.ShowRootLines = false; + this.ContentsPreviewTree.ShowNodeToolTips = true; this.ContentsPreviewTree.Location = new System.Drawing.Point(3, 65); this.ContentsPreviewTree.Name = "ContentsPreviewTree"; this.ContentsPreviewTree.Size = new System.Drawing.Size(494, 431); diff --git a/GUI/Controls/ModInfoTabs/Contents.cs b/GUI/Controls/ModInfoTabs/Contents.cs index 59e787f790..61c29eb23e 100644 --- a/GUI/Controls/ModInfoTabs/Contents.cs +++ b/GUI/Controls/ModInfoTabs/Contents.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; using System.IO; @@ -9,6 +8,9 @@ using System.Runtime.Versioning; #endif +using Autofac; + +using CKAN.Configuration; using CKAN.GUI.Attributes; namespace CKAN.GUI @@ -53,9 +55,9 @@ public void RefreshModContentsTree() private void ContentsPreviewTree_NodeMouseDoubleClick(object? sender, TreeNodeMouseClickEventArgs? e) { - if (e != null) + if (e != null && manager?.CurrentInstance is GameInstance inst) { - Utilities.OpenFileBrowser(e.Node.Name); + Utilities.OpenFileBrowser(inst.ToAbsoluteGameDir(e.Node.Name)); } } @@ -119,9 +121,9 @@ private void _UpdateModContentsTree(CkanModule? module, bool force = false) ContentsOpenButton.Enabled = false; ContentsPreviewTree.Enabled = false; } - else if (manager != null - && manager?.CurrentInstance != null + else if (manager?.CurrentInstance is GameInstance inst && manager?.Cache != null + && selectedModule != null && Main.Instance?.currentUser != null) { rootNode.Text = Path.GetFileName( @@ -134,23 +136,33 @@ private void _UpdateModContentsTree(CkanModule? module, bool force = false) UseWaitCursor = true; Task.Factory.StartNew(() => { - var paths = ModuleInstaller.GetModuleContentsList(manager.Cache, manager.CurrentInstance, module) - // Load fully in bg - .ToArray(); + var filters = ServiceLocator.Container.Resolve().GlobalInstallFilters + .Concat(inst.InstallFilters) + .ToHashSet(); + var tuples = ModuleInstaller.GetModuleContents(manager.Cache, inst, module, filters) + .Select(f => (path: inst.ToRelativeGameDir(f.destination), + dir: f.source.IsDirectory, + exists: !selectedModule.IsInstalled + || File.Exists(f.destination) + || Directory.Exists(f.destination))) + .ToArray(); // Stop if user switched to another mod if (rootNode.TreeView != null) { Util.Invoke(this, () => { ContentsPreviewTree.BeginUpdate(); - foreach (string path in paths) + foreach ((string path, bool dir, bool exists) in tuples) { - AddContentPieces( - rootNode, - path.Split(new char[] {'/'})); + AddContentPieces(inst, rootNode, + path.Split(new char[] {'/'}), + dir, exists); } rootNode.ExpandAll(); - rootNode.EnsureVisible(); + var initialFocus = FirstMatching(rootNode, + n => n.ForeColor == Color.Red) + ?? rootNode; + initialFocus.EnsureVisible(); ContentsPreviewTree.EndUpdate(); UseWaitCursor = false; }); @@ -161,24 +173,45 @@ private void _UpdateModContentsTree(CkanModule? module, bool force = false) } } - private static void AddContentPieces(TreeNode parent, IEnumerable pieces) + private static void AddContentPieces(GameInstance inst, + TreeNode parent, + string[] pieces, + bool dir, + bool exists) { var firstPiece = pieces.FirstOrDefault(); if (firstPiece != null) { - if (parent.ImageKey == "file") - { - parent.SelectedImageKey = parent.ImageKey = "folder"; - } // Key/Name needs to be the full relative path for double click to work var key = string.IsNullOrEmpty(parent.Name) - ? firstPiece - : $"{parent.Name}/{firstPiece}"; - var node = parent.Nodes[key] - ?? parent.Nodes.Add(key, firstPiece, "file", "file"); - AddContentPieces(node, pieces.Skip(1)); + ? firstPiece + : $"{parent.Name}/{firstPiece}"; + var node = parent.Nodes[key]; + if (node == null) + { + var iconKey = dir || pieces.Length > 1 ? "folder" : "file"; + node = parent.Nodes.Add(key, firstPiece, iconKey, iconKey); + if (!exists && (pieces.Length == 1 || !Directory.Exists(inst.ToAbsoluteGameDir(key)))) + { + node.ForeColor = Color.Red; + node.ToolTipText = iconKey == "folder" + ? Properties.Resources.ModInfoFolderNotFound + : Properties.Resources.ModInfoFileNotFound; + } + } + if (pieces.Length > 1) + { + AddContentPieces(inst, node, pieces.Skip(1).ToArray(), dir, exists); + } } } + private static TreeNode? FirstMatching(TreeNode root, Func predicate) + => predicate(root) ? root + : root.Nodes.OfType() + .Select(n => FirstMatching(n, predicate)) + .OfType() + .FirstOrDefault(); + } } diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index 4e225eb1fd..aedf7f0084 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -280,6 +280,8 @@ If you suspect a bug in the client: https://github.com/KSP-CKAN/CKAN/issues/new/ This mod is not in the cache, click 'Download' to preview contents Module is cached, preview available Module has no download + Folder not found! + File not found! Home page: SpaceDock: Curse: From b1609dd3363d291fbb59171b8529db5ed3cf35c0 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Wed, 25 Sep 2024 14:48:45 -0500 Subject: [PATCH 2/2] Separate change reason for missing files or folders --- Core/Registry/IRegistryQuerier.cs | 2 +- GUI/Controls/ManageMods.cs | 2 +- GUI/Model/GUIMod.cs | 8 +++++--- GUI/Model/ModChange.cs | 14 +++++++++----- GUI/Model/ModList.cs | 18 ++++++++++-------- GUI/Properties/Resources.de-DE.resx | 2 +- GUI/Properties/Resources.fr-FR.resx | 4 ++-- GUI/Properties/Resources.it-IT.resx | 2 +- GUI/Properties/Resources.ja-JP.resx | 2 +- GUI/Properties/Resources.ko-KR.resx | 2 +- GUI/Properties/Resources.pl-PL.resx | 2 +- GUI/Properties/Resources.resx | 5 +++-- GUI/Properties/Resources.ru-RU.resx | 2 +- GUI/Properties/Resources.zh-CN.resx | 4 ++-- Tests/GUI/Model/ModList.cs | 4 ++-- 15 files changed, 41 insertions(+), 32 deletions(-) diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index db4398b96f..c8e34ad6f5 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -274,7 +274,7 @@ public static Dictionary> CheckUpgradeable(this IRegistry }; } - private static bool MetadataChanged(this IRegistryQuerier querier, string identifier) + public static bool MetadataChanged(this IRegistryQuerier querier, string identifier) { try { diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 61a3df7184..80cb6207c4 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -1291,7 +1291,7 @@ private void reinstallToolStripMenuItem_Click(object? sender, EventArgs? e) registry.GetModuleByVersion(module.identifier, module.version) ?? module, - true) + true, false) }, null); } } diff --git a/GUI/Model/GUIMod.cs b/GUI/Model/GUIMod.cs index 9474948550..8bae0914b6 100644 --- a/GUI/Model/GUIMod.cs +++ b/GUI/Model/GUIMod.cs @@ -291,7 +291,9 @@ public CkanModule ToCkanModule() /// The CkanModule associated with this GUIMod or null if there is none public CkanModule ToModule() => Mod; - public IEnumerable GetModChanges(bool upgradeChecked, bool replaceChecked) + public IEnumerable GetModChanges(bool upgradeChecked, + bool replaceChecked, + bool metadataChanged) { if (replaceChecked) { @@ -309,7 +311,7 @@ public IEnumerable GetModChanges(bool upgradeChecked, bool replaceChe yield return new ModUpgrade(Mod, GUIModChangeType.Update, SelectedMod, - false); + false, false); } else { @@ -329,7 +331,7 @@ public IEnumerable GetModChanges(bool upgradeChecked, bool replaceChe yield return new ModUpgrade(Mod, GUIModChangeType.Update, SelectedMod, - false); + false, metadataChanged); } } diff --git a/GUI/Model/ModChange.cs b/GUI/Model/ModChange.cs index 9229e563f6..4129908244 100644 --- a/GUI/Model/ModChange.cs +++ b/GUI/Model/ModChange.cs @@ -138,11 +138,13 @@ public class ModUpgrade : ModChange public ModUpgrade(CkanModule mod, GUIModChangeType changeType, CkanModule targetMod, - bool userReinstall) + bool userReinstall, + bool metadataChanged) : base(mod, changeType) { - this.targetMod = targetMod; - this.userReinstall = userReinstall; + this.targetMod = targetMod; + this.userReinstall = userReinstall; + this.metadataChanged = metadataChanged; } public override string? NameAndStatus @@ -150,8 +152,9 @@ public override string? NameAndStatus public override string Description => IsReinstall - ? userReinstall ? Properties.Resources.MainChangesetUserReinstall - : Properties.Resources.MainChangesetReinstall + ? userReinstall ? Properties.Resources.MainChangesetReinstallUser + : metadataChanged ? Properties.Resources.MainChangesetReinstallMetadataChanged + : Properties.Resources.MainChangesetReinstallMissing : string.Format(Properties.Resources.MainChangesetUpdateSelected, targetMod.version); @@ -165,5 +168,6 @@ private bool IsReinstall && targetMod.version == Mod.version; private readonly bool userReinstall; + private readonly bool metadataChanged; } } diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index a2cf963fcb..5265829441 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -434,19 +434,21 @@ public string StripEpoch(string version) private static readonly Regex ContainsEpoch = new Regex(@"^[0-9][0-9]*:[^:]+$", RegexOptions.Compiled); private static readonly Regex RemoveEpoch = new Regex(@"^([^:]+):([^:]+)$", RegexOptions.Compiled); - private static IEnumerable rowChanges(DataGridViewRow row, - DataGridViewColumn? upgradeCol, - DataGridViewColumn? replaceCol) - => (row.Tag as GUIMod)?.GetModChanges( + private static IEnumerable rowChanges(IRegistryQuerier registry, + DataGridViewRow row, + DataGridViewColumn? upgradeCol, + DataGridViewColumn? replaceCol) + => row.Tag is GUIMod gmod ? gmod.GetModChanges( upgradeCol != null && upgradeCol.Visible && row.Cells[upgradeCol.Index] is DataGridViewCheckBoxCell upgradeCell && (bool)upgradeCell.Value, replaceCol != null && replaceCol.Visible && row.Cells[replaceCol.Index] is DataGridViewCheckBoxCell replaceCell - && (bool)replaceCell.Value) - ?? Enumerable.Empty(); + && (bool)replaceCell.Value, + registry.MetadataChanged(gmod.Identifier)) + : Enumerable.Empty(); - public HashSet ComputeUserChangeSet(IRegistryQuerier? registry, + public HashSet ComputeUserChangeSet(IRegistryQuerier registry, GameVersionCriteria? crit, GameInstance? instance, DataGridViewColumn? upgradeCol, @@ -454,7 +456,7 @@ public HashSet ComputeUserChangeSet(IRegistryQuerier? registry, { log.Debug("Computing user changeset"); var modChanges = full_list_of_mod_rows?.Values - .SelectMany(row => rowChanges(row, upgradeCol, replaceCol)) + .SelectMany(row => rowChanges(registry, row, upgradeCol, replaceCol)) .ToList() ?? new List(); diff --git a/GUI/Properties/Resources.de-DE.resx b/GUI/Properties/Resources.de-DE.resx index c431083ee7..d088d2b4b4 100644 --- a/GUI/Properties/Resources.de-DE.resx +++ b/GUI/Properties/Resources.de-DE.resx @@ -184,7 +184,7 @@ Möchten Sie es wirklich installieren? Installieren Abbrechen Mod-Update ausgewählt vom Nutzer {0}. - Neu installieren (Metadaten geändert) + Neu installieren (Metadaten geändert) Mod-Import Statuslog Statuslog diff --git a/GUI/Properties/Resources.fr-FR.resx b/GUI/Properties/Resources.fr-FR.resx index 4e6ff07856..04ec0f89d3 100644 --- a/GUI/Properties/Resources.fr-FR.resx +++ b/GUI/Properties/Resources.fr-FR.resx @@ -359,10 +359,10 @@ Voulez-vous vraiment les installer ? L'annulation annulera toute l'installation. Mise à jour demandée vers la version {0}. - + Réinstaller (métadonnées modifiées) - + Réinstallation (à la demande de l'utilisateur) diff --git a/GUI/Properties/Resources.it-IT.resx b/GUI/Properties/Resources.it-IT.resx index bce1450282..58437f9f7e 100644 --- a/GUI/Properties/Resources.it-IT.resx +++ b/GUI/Properties/Resources.it-IT.resx @@ -323,7 +323,7 @@ Sei sicuro di volerli installare? L'annullamento interromperà l'intera installa Aggiorna la versione selezionata dall'utente alla versione {0}. - + Reinstallazione (Metadati cambiati) diff --git a/GUI/Properties/Resources.ja-JP.resx b/GUI/Properties/Resources.ja-JP.resx index 4bdafdb381..0c473a191f 100644 --- a/GUI/Properties/Resources.ja-JP.resx +++ b/GUI/Properties/Resources.ja-JP.resx @@ -187,7 +187,7 @@ Try to move {2} out of {3} and restart CKAN. インストール 取消 選択されたものをバージョン{0}にアップデートする。 - () + () Modインポート Mod (*.zip)|*.zip ステータスログ diff --git a/GUI/Properties/Resources.ko-KR.resx b/GUI/Properties/Resources.ko-KR.resx index 93373400a2..5f8df15d39 100644 --- a/GUI/Properties/Resources.ko-KR.resx +++ b/GUI/Properties/Resources.ko-KR.resx @@ -188,7 +188,7 @@ 설치 취소하기 유저가 선택한 것을 버전 {0}으로 업데이트 하기. - () + () 유저가 선택한 모드를 설치하기. 모드들 가져오기 모드들 (*.zip)|*.zip diff --git a/GUI/Properties/Resources.pl-PL.resx b/GUI/Properties/Resources.pl-PL.resx index 2db8273492..07008f2199 100644 --- a/GUI/Properties/Resources.pl-PL.resx +++ b/GUI/Properties/Resources.pl-PL.resx @@ -330,7 +330,7 @@ Na pewno chcesz je zainstalować? Anulowanie przerwie całą instalację. Aktualizacja wybrana przez użytkownika do wersji {0}. - + Zainstaluj ponownie (Metadane zmienione) diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index aedf7f0084..4a496cb432 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -215,8 +215,9 @@ Do you want to add the additional versions to this game instance's compatibility Are you sure you want to install them? Cancelling will abort the entire installation. Update selected by user to version {0}. - Re-install (metadata changed) - Re-install (user requested) + Re-install (metadata changed) + Re-install (missing folders or files) + Re-install (user requested) Import Mods Mods (*.zip)|*.zip Status log diff --git a/GUI/Properties/Resources.ru-RU.resx b/GUI/Properties/Resources.ru-RU.resx index 878aba48aa..523b2e5d1f 100644 --- a/GUI/Properties/Resources.ru-RU.resx +++ b/GUI/Properties/Resources.ru-RU.resx @@ -187,7 +187,7 @@ Установить Отмена Обновить выбранное пользователем до версии {0}. - Переустановка (Метаданные изменены) + Переустановка (Метаданные изменены) Импортировать модификации Модификации (*.zip)|*.zip Журнал diff --git a/GUI/Properties/Resources.zh-CN.resx b/GUI/Properties/Resources.zh-CN.resx index a0b627e71f..738f49d3a3 100644 --- a/GUI/Properties/Resources.zh-CN.resx +++ b/GUI/Properties/Resources.zh-CN.resx @@ -354,10 +354,10 @@ 用户选择更新到 {0} 版本. - + 重新安装 (元数据已更改) - + 重新安装 (用户请求) diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index e01cd86774..36b6a1e9bf 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -27,7 +27,7 @@ public class ModListTests public void ComputeFullChangeSetFromUserChangeSet_WithEmptyList_HasEmptyChangeSet() { var item = new ModList(); - Assert.That(item.ComputeUserChangeSet(null, null, null, null, null), Is.Empty); + Assert.That(item.ComputeUserChangeSet(Registry.Empty(), null, null, null, null), Is.Empty); } [Test] @@ -211,7 +211,7 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() { // Install the "other" module installer.InstallList( - modList.ComputeUserChangeSet(null, null, null, null, null).Select(change => change.Mod).ToList(), + modList.ComputeUserChangeSet(Registry.Empty(), null, null, null, null).Select(change => change.Mod).ToList(), new RelationshipResolverOptions(), registryManager, ref possibleConfigOnlyDirs,