From 342359625c31e78128616931f264c58f419f88c2 Mon Sep 17 00:00:00 2001 From: focustense Date: Mon, 28 Jun 2021 23:14:42 -0400 Subject: [PATCH] Jump to profile row from build warning. The implementation of this is a hack, since pages can't really talk to each other, and especially since the row isn't guaranteed to be visible through the filters. The hack forces it to be visible until the filters are changed, which actually requires multiple property change notifications and cycles to work correctly, but doesn't seem to be too noticeable in the UI, performance-wise. Only really noticeable effect is that the row will sometimes appear at the top and sometimes in the middle, but is still always within the visible area. Should be an acceptable tradeoff. Jump is activated by double-click. For now there's no additional link in the info panel, but it could be added easily if there's a need. Introduction of `RecordKey` wasn't strictly necessary here, but it's a first step toward refactoring something that's very repetitive. Fixes #14. --- Focus.Apps.EasyNpc/Build/BuildChecker.cs | 11 ++++ Focus.Apps.EasyNpc/Build/BuildPage.xaml | 8 ++- Focus.Apps.EasyNpc/Build/BuildPage.xaml.cs | 10 +++- Focus.Apps.EasyNpc/Build/BuildViewModel.cs | 10 +++- Focus.Apps.EasyNpc/Build/BuildWarnings.cs | 15 ++++- Focus.Apps.EasyNpc/GameData/Records/Npc.cs | 4 +- .../GameData/Records/RecordKey.cs | 20 +++++++ Focus.Apps.EasyNpc/Main/MainViewModel.cs | 9 +++ Focus.Apps.EasyNpc/Main/MainWindow.xaml.cs | 12 ++++ .../Profile/NpcConfiguration.cs | 2 +- .../Profile/ProfileViewModel.cs | 58 ++++++++++++++++++- 11 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 Focus.Apps.EasyNpc/GameData/Records/RecordKey.cs diff --git a/Focus.Apps.EasyNpc/Build/BuildChecker.cs b/Focus.Apps.EasyNpc/Build/BuildChecker.cs index bb09afc..7a7b22b 100644 --- a/Focus.Apps.EasyNpc/Build/BuildChecker.cs +++ b/Focus.Apps.EasyNpc/Build/BuildChecker.cs @@ -1,5 +1,6 @@ using Focus.Apps.EasyNpc.Configuration; using Focus.Apps.EasyNpc.GameData.Files; +using Focus.Apps.EasyNpc.GameData.Records; using Focus.Apps.EasyNpc.Profile; using System; using System.Collections.Generic; @@ -80,6 +81,8 @@ private IEnumerable CheckMissingPlugins(IEnumerable .Select(x => npcConfigs.TryGetValue(Tuple.Create(x.BasePluginName, x.LocalFormIdHex), out var npc) ? new { + npc.BasePluginName, + npc.LocalFormIdHex, npc.EditorId, npc.Name, FieldName = x.Field == NpcProfileField.FacePlugin ? "face" : "default", @@ -87,6 +90,7 @@ private IEnumerable CheckMissingPlugins(IEnumerable } : null) .Where(x => x != null) .Select(x => new BuildWarning( + new RecordKey(x.BasePluginName, x.LocalFormIdHex), BuildWarningId.SelectedPluginRemoved, WarningMessages.SelectedPluginRemoved(x.EditorId, x.Name, x.FieldName, x.PluginName))); } @@ -111,6 +115,7 @@ private IEnumerable CheckModPluginConsistency( { if (npc.RequiresFacegenData()) yield return new BuildWarning( + new RecordKey(npc), BuildWarningId.FaceModNotSpecified, WarningMessages.FaceModNotSpecified(npc.EditorId, npc.Name)); yield break; @@ -120,6 +125,7 @@ private IEnumerable CheckModPluginConsistency( if (!modPluginMap.IsModInstalled(npc.FaceModName)) { yield return new BuildWarning( + new RecordKey(npc), BuildWarningId.FaceModNotInstalled, WarningMessages.FaceModNotInstalled(npc.EditorId, npc.Name, npc.FaceModName)); yield break; @@ -127,6 +133,7 @@ private IEnumerable CheckModPluginConsistency( if (!modsProvidingFacePlugin.Contains(npc.FaceModName)) yield return new BuildWarning( npc.FacePluginName, + new RecordKey(npc), BuildWarningId.FaceModPluginMismatch, WarningMessages.FaceModPluginMismatch(npc.EditorId, npc.Name, npc.FaceModName, npc.FacePluginName)); var faceMeshFileName = FileStructure.GetFaceMeshFileName(npc.BasePluginName, npc.LocalFormIdHex); @@ -142,16 +149,19 @@ private IEnumerable CheckModPluginConsistency( // include it isn't loaded, i.e. due to the mod or plugin being disabled. yield return new BuildWarning( npc.FacePluginName, + new RecordKey(npc), BuildWarningId.FaceModMissingFaceGen, WarningMessages.FaceModMissingFaceGen(npc.EditorId, npc.Name, npc.FaceModName)); else if (!npc.RequiresFacegenData() && (hasLooseFacegen || hasArchiveFacegen)) yield return new BuildWarning( npc.FacePluginName, + new RecordKey(npc), BuildWarningId.FaceModExtraFaceGen, WarningMessages.FaceModExtraFaceGen(npc.EditorId, npc.Name, npc.FaceModName)); else if (hasLooseFacegen && hasArchiveFacegen) yield return new BuildWarning( npc.FacePluginName, + new RecordKey(npc), BuildWarningId.FaceModMultipleFaceGen, WarningMessages.FaceModMultipleFaceGen(npc.EditorId, npc.Name, npc.FaceModName)); } @@ -196,6 +206,7 @@ private static IEnumerable CheckWigs( .Where(x => x.Wig != null && (!enableDewiggify || !matchedWigKeys.Contains(x.Wig.Key))) .Select(x => enableDewiggify ? new BuildWarning( + new RecordKey(x.Npc), x.Wig.IsBald ? BuildWarningId.FaceModWigNotMatchedBald : BuildWarningId.FaceModWigNotMatched, x.Wig.IsBald ? WarningMessages.FaceModWigNotMatchedBald( diff --git a/Focus.Apps.EasyNpc/Build/BuildPage.xaml b/Focus.Apps.EasyNpc/Build/BuildPage.xaml index acff991..d67ac3d 100644 --- a/Focus.Apps.EasyNpc/Build/BuildPage.xaml +++ b/Focus.Apps.EasyNpc/Build/BuildPage.xaml @@ -113,7 +113,13 @@ - + diff --git a/Focus.Apps.EasyNpc/Build/BuildPage.xaml.cs b/Focus.Apps.EasyNpc/Build/BuildPage.xaml.cs index 8a71611..104ca5c 100644 --- a/Focus.Apps.EasyNpc/Build/BuildPage.xaml.cs +++ b/Focus.Apps.EasyNpc/Build/BuildPage.xaml.cs @@ -1,6 +1,6 @@ using System; using System.Windows; - +using System.Windows.Input; using TKey = Mutagen.Bethesda.Plugins.FormKey; namespace Focus.Apps.EasyNpc.Build @@ -56,5 +56,13 @@ private void SkipProblemsButton_Click(object sender, RoutedEventArgs e) { Model.IsProblemCheckerVisible = false; } + + private void WarningsListBox_MouseDoubleClick(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton != MouseButton.Left) + return; + if (WarningsListBox.SelectedItem is BuildWarning buildWarning && buildWarning.RecordKey != null) + Model.ExpandWarning(buildWarning); + } } } diff --git a/Focus.Apps.EasyNpc/Build/BuildViewModel.cs b/Focus.Apps.EasyNpc/Build/BuildViewModel.cs index 6fef15f..e9c9aa6 100644 --- a/Focus.Apps.EasyNpc/Build/BuildViewModel.cs +++ b/Focus.Apps.EasyNpc/Build/BuildViewModel.cs @@ -10,8 +10,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; -using System.Threading; using System.Threading.Tasks; namespace Focus.Apps.EasyNpc.Build @@ -20,6 +18,7 @@ public class BuildViewModel : INotifyPropertyChanged where TKey : struct { public event PropertyChangedEventHandler PropertyChanged; + public event EventHandler WarningExpanded; public bool EnableDewiggify { get; set; } = true; [DependsOn("Problems")] @@ -126,6 +125,13 @@ public void DismissProblems() IsReadyToBuild = true; } + public void ExpandWarning(BuildWarning warning) + { + // There isn't actually any notion of expansion in this context, aside from the info panel which is more of + // a "select" action. This just serves as a signal for an external component to handle the request. + WarningExpanded?.Invoke(this, warning); + } + public void OpenBuildOutput() { if (!Directory.Exists(OutputDirectory)) // In case user moved/deleted after the build diff --git a/Focus.Apps.EasyNpc/Build/BuildWarnings.cs b/Focus.Apps.EasyNpc/Build/BuildWarnings.cs index cee5bcf..d75802a 100644 --- a/Focus.Apps.EasyNpc/Build/BuildWarnings.cs +++ b/Focus.Apps.EasyNpc/Build/BuildWarnings.cs @@ -1,4 +1,5 @@ -using System; +using Focus.Apps.EasyNpc.GameData.Records; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -29,6 +30,7 @@ public class BuildWarning // Used for help links, if provided. public BuildWarningId? Id { get; init; } public string Message { get; init; } + public RecordKey RecordKey { get; init; } public string PluginName { get; init; } public BuildWarning() { } @@ -43,11 +45,22 @@ public BuildWarning(BuildWarningId id, string message) : this(message) Id = id; } + public BuildWarning(RecordKey key, BuildWarningId id, string message) : this(id, message) + { + RecordKey = key; + } + public BuildWarning(string pluginName, BuildWarningId id, string message) : this(id, message) { PluginName = pluginName; } + + public BuildWarning(string pluginName, RecordKey key, BuildWarningId id, string message) + : this(pluginName, id, message) + { + RecordKey = key; + } } public class BuildWarningIdsToTextConverter : IValueConverter diff --git a/Focus.Apps.EasyNpc/GameData/Records/Npc.cs b/Focus.Apps.EasyNpc/GameData/Records/Npc.cs index 0a2acb1..9e6988a 100644 --- a/Focus.Apps.EasyNpc/GameData/Records/Npc.cs +++ b/Focus.Apps.EasyNpc/GameData/Records/Npc.cs @@ -4,14 +4,12 @@ namespace Focus.Apps.EasyNpc.GameData.Records { - public interface INpc + public interface INpc : IRecordKey where TKey : struct { - string BasePluginName { get; } string EditorId { get; } bool IsFemale { get; } TKey Key { get; } - string LocalFormIdHex { get; } string Name { get; } IReadOnlyList> Overrides { get; } } diff --git a/Focus.Apps.EasyNpc/GameData/Records/RecordKey.cs b/Focus.Apps.EasyNpc/GameData/Records/RecordKey.cs new file mode 100644 index 0000000..0185036 --- /dev/null +++ b/Focus.Apps.EasyNpc/GameData/Records/RecordKey.cs @@ -0,0 +1,20 @@ +using System; + +namespace Focus.Apps.EasyNpc.GameData.Records +{ + public interface IRecordKey + { + string BasePluginName { get; } + string LocalFormIdHex { get; } + } + + public record RecordKey(string BasePluginName, string LocalFormIdHex) : IRecordKey + { + public RecordKey(IRecordKey key) : this(key.BasePluginName, key.LocalFormIdHex) { } + + public bool Equals(IRecordKey key) + { + return key.BasePluginName == BasePluginName && key.LocalFormIdHex == LocalFormIdHex; + } + } +} \ No newline at end of file diff --git a/Focus.Apps.EasyNpc/Main/MainViewModel.cs b/Focus.Apps.EasyNpc/Main/MainViewModel.cs index ecd0113..cbe36ce 100644 --- a/Focus.Apps.EasyNpc/Main/MainViewModel.cs +++ b/Focus.Apps.EasyNpc/Main/MainViewModel.cs @@ -21,6 +21,7 @@ public abstract class MainViewModel : IProfileContainer, ISettingsContainer where TKey : struct { + public event EventHandler PageNavigationRequested; public event PropertyChangedEventHandler PropertyChanged; public BuildViewModel Build { get; private set; } @@ -88,11 +89,19 @@ public MainViewModel(bool isFirstLaunch, bool debugMode) Build = new BuildViewModel( gameDataEditor.ArchiveProvider, buildChecker, gameDataEditor.MergedPluginBuilder, Loader.ModPluginMapFactory, Profile.GetAllNpcConfigurations(), wigResolver, faceGenEditor, Logger); + Build.WarningExpanded += BuildViewModel_WarningExpanded; IsLoaded = true; }; } protected abstract IGameDataEditor CreateEditor(); + + private void BuildViewModel_WarningExpanded(object sender, BuildWarning e) + { + var found = Profile.SelectNpc(e.RecordKey); + if (found) + PageNavigationRequested?.Invoke(this, "profile"); + } } public class MainViewModel : MainViewModel diff --git a/Focus.Apps.EasyNpc/Main/MainWindow.xaml.cs b/Focus.Apps.EasyNpc/Main/MainWindow.xaml.cs index c9c146f..f401fcf 100644 --- a/Focus.Apps.EasyNpc/Main/MainWindow.xaml.cs +++ b/Focus.Apps.EasyNpc/Main/MainWindow.xaml.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Windows; namespace Focus.Apps.EasyNpc.Main @@ -55,6 +56,7 @@ public MainWindow(MainViewModel model) model.Settings.WelcomeAcked += (sender, e) => (MainNavigationView.MenuItems[0] as NavigationViewItem).IsSelected = true; } + model.PageNavigationRequested += (sender, pageName) => SelectPage(pageName); } private void Navigate(string pageName) @@ -73,6 +75,16 @@ private void NavigationView_SelectionChanged(NavigationView sender, NavigationVi else Navigate((string)args.SelectedItemContainer.Tag); } + + private void SelectPage(string pageName) + { + var matchingItem = MainNavigationView.MenuItems + .OfType() + .Where(x => string.Equals(x.Tag?.ToString(), pageName, StringComparison.OrdinalIgnoreCase)) + .SingleOrDefault(); + if (matchingItem != null) + MainNavigationView.SelectedItem = matchingItem; + } } record NavLink(string Title, Type PageType, Func ViewModelSelector); diff --git a/Focus.Apps.EasyNpc/Profile/NpcConfiguration.cs b/Focus.Apps.EasyNpc/Profile/NpcConfiguration.cs index 9aecdef..8fac160 100644 --- a/Focus.Apps.EasyNpc/Profile/NpcConfiguration.cs +++ b/Focus.Apps.EasyNpc/Profile/NpcConfiguration.cs @@ -7,7 +7,7 @@ namespace Focus.Apps.EasyNpc.Profile { - public class NpcConfiguration : INotifyPropertyChanged + public class NpcConfiguration : INotifyPropertyChanged, IRecordKey where TKey : struct { private static readonly HashSet DlcPluginNames = new HashSet( diff --git a/Focus.Apps.EasyNpc/Profile/ProfileViewModel.cs b/Focus.Apps.EasyNpc/Profile/ProfileViewModel.cs index 7c0942a..983c761 100644 --- a/Focus.Apps.EasyNpc/Profile/ProfileViewModel.cs +++ b/Focus.Apps.EasyNpc/Profile/ProfileViewModel.cs @@ -24,7 +24,13 @@ public class ProfileViewModel : INotifyPropertyChanged [DependsOn("NpcConfigurations", "OnlyFaceOverrides", "ShowSinglePluginOverrides", "DisplayedNpcsSentinel")] public IEnumerable> DisplayedNpcs { - get { return ApplyFilters(GetAllNpcConfigurations()); } + get + { + var filteredNpcs = ApplyFilters(GetAllNpcConfigurations()); + return filterBypassNpc != null ? + new[] { filterBypassNpc }.Union(filteredNpcs) : + filteredNpcs; + } } public NpcFilters Filters { get; private init; } = new NpcFilters(); @@ -44,11 +50,12 @@ public IEnumerable> DisplayedNpcs // internal handler to force the DisplayedNpcs to update (just invert the value). protected bool DisplayedNpcsSentinel { get; private set; } + private NpcConfiguration filterBypassNpc; // One-time "ignore filter", used when jumping to an NPC private readonly IReadOnlySet loadedPluginNamesSet; - private readonly ProfileEventLog profileEventLog; private readonly IModPluginMapFactory modPluginMapFactory; private readonly Dictionary> npcConfigurations = new(); private readonly IReadOnlyList npcOrder; + private readonly ProfileEventLog profileEventLog; public ProfileViewModel( IEnumerable> npcs, IModPluginMapFactory modPluginMapFactory, @@ -158,12 +165,32 @@ public void SaveToFile(Window dialogOwner) savedProfile.SaveToFile(dialog.FileName); } + public bool SelectNpc(RecordKey key) + { + // At the moment, this doesn't get called often enough to justify having another dictionary with "our" key + // instead of the game editor's (e.g. Mutagen's) key. + var npcConfig = npcConfigurations.Values.SingleOrDefault(x => key.Equals(x)); + if (npcConfig != null) + { + SelectedNpc = npcConfig; + RefreshDisplayedNpcs(); + // Have to do this AFTER the refresh, so the refresh doesn't reset it. + filterBypassNpc = npcConfig; + } + return npcConfig != null; + } + public void SetFaceOverride(Mugshot mugshot, bool detectPlugin = false) { SelectedNpc?.SetFaceMod(mugshot?.ProvidingMod, detectPlugin); SyncMugshotMod(); } + protected void OnDisplayedNpcsSentinelChanged() + { + ClearFilterBypass(); + } + protected void OnFocusedMugshotChanged() { foreach (var overrideConfig in SelectedNpcOverrides ?? Enumerable.Empty>()) @@ -193,6 +220,16 @@ protected void OnFocusedNpcOverrideChanged() } } + protected void OnNpcConfigurationsChanged() + { + ClearFilterBypass(); + } + + protected void OnOnlyFaceOverridesChanged() + { + ClearFilterBypass(); + } + protected void OnSelectedNpcChanged(object before, object after) { var next = after as NpcConfiguration; @@ -211,6 +248,11 @@ protected void OnSelectedNpcChanged(object before, object after) next.FaceModChanged += OnNpcFaceModChanged; } + protected void OnShowSinglePluginOverridesChanged() + { + ClearFilterBypass(); + } + private static void ApplyColumnFilter( ref IEnumerable> npcs, DataGridColumn column, Func, string> propertySelector) @@ -262,8 +304,17 @@ private IEnumerable> ApplyFilters(IEnumerable x.GetOverrideCount(!Filters.NonDlc, !OnlyFaceOverrides) >= minOverrideCount); + return filterBypassNpc != null ? displayedNpcs.Union(new[] { filterBypassNpc }) : displayedNpcs; + } + + private void ClearFilterBypass() + { + if (filterBypassNpc == null) + return; + filterBypassNpc = null; + DisplayedNpcsSentinel = !DisplayedNpcsSentinel; } private void ClearNpcHighlights() @@ -292,6 +343,7 @@ private void OnNpcProfilePropertyChanged(object sender, ProfileEvent e) private void RefreshDisplayedNpcs() { + filterBypassNpc = null; DisplayedNpcsSentinel = !DisplayedNpcsSentinel; }