From d6360800d146b89262b26def4a9058080748ce98 Mon Sep 17 00:00:00 2001 From: Tyler Porter Date: Wed, 10 Jun 2026 14:50:28 -0400 Subject: [PATCH] Refresh UI when changing layout/tracking options --- EmoTracker.Data/ApplicationSettings.cs | 33 ++++++++++++ EmoTracker.Data/Sessions/SessionSettings.cs | 58 +++++++++++++++++++++ EmoTracker.Data/Sessions/TrackerState.cs | 26 ++++++--- 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/EmoTracker.Data/ApplicationSettings.cs b/EmoTracker.Data/ApplicationSettings.cs index c3bf92f..6c2ad13 100644 --- a/EmoTracker.Data/ApplicationSettings.cs +++ b/EmoTracker.Data/ApplicationSettings.cs @@ -97,6 +97,39 @@ internal void SeedIntoSession(Sessions.SessionSettings target) target.PinLocationsOnItemCapture = mbSeedPinLocationsOnItemCapture; } + /// + /// Companion to , called by + /// 's OnChanged hooks when a + /// forwarded setting is written directly on a state (menu bindings, + /// dev terminal, MCP tools, Lua). If the source is the active + /// state's settings, updates the persisted seed — unless the write + /// came from pack-load (init.lua), per + /// — and raises + /// PropertyChanged so XAML bound to this singleton's forwarder + /// properties updates even though the write bypassed the forwarder + /// setter. Writes on non-active states (forks mid-pipeline, + /// definitional states) are ignored. + /// + internal void SyncSeedsFromSession(Sessions.SessionSettings source, string propertyName) + { + if (source == null || !ReferenceEquals(source, ActiveSessionSettings)) + return; + + if (!Sessions.PackageLoader.IsLoading) + { + switch (propertyName) + { + case nameof(IgnoreAllLogic): mbSeedIgnoreAllLogic = source.IgnoreAllLogic; break; + case nameof(DisplayAllLocations): mbSeedDisplayAllLocations = source.DisplayAllLocations; break; + case nameof(AlwaysAllowClearing): mbSeedAlwaysAllowClearing = source.AlwaysAllowClearing; break; + case nameof(AutoUnpinLocationsOnClear): mbSeedAutoUnpinLocationsOnClear = source.AutoUnpinLocationsOnClear; break; + case nameof(PinLocationsOnItemCapture): mbSeedPinLocationsOnItemCapture = source.PinLocationsOnItemCapture; break; + } + } + + NotifyPropertyChanged(propertyName); + } + public bool IgnoreAllLogic { get diff --git a/EmoTracker.Data/Sessions/SessionSettings.cs b/EmoTracker.Data/Sessions/SessionSettings.cs index 72b648e..32fd268 100644 --- a/EmoTracker.Data/Sessions/SessionSettings.cs +++ b/EmoTracker.Data/Sessions/SessionSettings.cs @@ -54,21 +54,27 @@ public partial class SessionSettings : TransactableModelTypeBase public partial bool IgnoreAllLogic { get; set; } [KVMutable] + [OnChanged(nameof(OnDisplayAllLocationsChanged))] public partial bool DisplayAllLocations { get; set; } [KVMutable] + [OnChanged(nameof(OnAlwaysAllowClearingChanged))] public partial bool AlwaysAllowClearing { get; set; } [KVMutable] + [OnChanged(nameof(OnAutoUnpinLocationsOnClearChanged))] public partial bool AutoUnpinLocationsOnClear { get; set; } [KVMutable] + [OnChanged(nameof(OnPinLocationsOnItemCaptureChanged))] public partial bool PinLocationsOnItemCapture { get; set; } [KVMutable] + [OnChanged(nameof(OnMapEnabledChanged))] public partial bool MapEnabled { get; set; } [KVMutable] + [OnChanged(nameof(OnSwapLeftRightChanged))] public partial bool SwapLeftRight { get; set; } // ----------- Construction --------------------------------------------- @@ -103,6 +109,22 @@ public SessionSettings() // ----------- OnChanged hooks ------------------------------------------ + /// + /// Set by around its settings + /// bulk-copy so propagating values into a fork doesn't fire + /// side-effectful OnChanged hooks (e.g. a pack reload) against the + /// half-built fork state. + /// + internal bool SuppressOnChangedHooks; + + protected void OnSwapLeftRightChanged() => ReloadOwnerState(); + protected void OnMapEnabledChanged() => ReloadOwnerState(); + + protected void OnDisplayAllLocationsChanged() => SyncForwarder(nameof(DisplayAllLocations)); + protected void OnAlwaysAllowClearingChanged() => SyncForwarder(nameof(AlwaysAllowClearing)); + protected void OnAutoUnpinLocationsOnClearChanged() => SyncForwarder(nameof(AutoUnpinLocationsOnClear)); + protected void OnPinLocationsOnItemCaptureChanged() => SyncForwarder(nameof(PinLocationsOnItemCapture)); + protected void OnIgnoreAllLogicChanged() { // Drive a refresh on the owning state's LocationDatabase so the @@ -111,6 +133,42 @@ protected void OnIgnoreAllLogicChanged() // ApplicationSettings.IgnoreAllLogic's setter. var state = OwnerState as TrackerState; state?.Locations.RefreshAccessibility(); + SyncForwarder(nameof(IgnoreAllLogic)); + } + + /// + /// SwapLeftRight and MapEnabled are consumed at pack parse time + /// (LayoutItem.TryParse mirrors dock/margin definition + /// values; MapPanel.TryParseInternal bails when the map is + /// disabled), so a new value only takes effect after re-parsing + /// the pack — the legacy Tracker setters called + /// Reload() for the same reason. No pack loaded means + /// nothing to re-parse, so skip rather than pointlessly resetting + /// empty catalogs. + /// + void ReloadOwnerState() + { + if (SuppressOnChangedHooks) return; + var state = OwnerState as TrackerState; + if (state?.PackageInstance?.GamePackage == null) return; + state.Reload(); + } + + /// + /// keeps forwarder properties + /// (plus seeds persisted to ApplicationSettings.json) for code and + /// XAML bound to the process-wide singleton rather than a state's + /// Settings (e.g. LocationMapControl's DisplayAllLocations + /// multibinding). Writes through this state's setters must fan out + /// to the singleton or those bindings go stale — that asymmetry is + /// why the "Show All Locations" menu toggle (bound per-state) had + /// no visible effect while the F11 hotkey (routed through the + /// forwarder) worked. + /// + void SyncForwarder(string propertyName) + { + if (SuppressOnChangedHooks) return; + ApplicationSettings.Instance.SyncSeedsFromSession(this, propertyName); } // ----------- Fork support --------------------------------------------- diff --git a/EmoTracker.Data/Sessions/TrackerState.cs b/EmoTracker.Data/Sessions/TrackerState.cs index 085b6b4..1e5c54e 100644 --- a/EmoTracker.Data/Sessions/TrackerState.cs +++ b/EmoTracker.Data/Sessions/TrackerState.cs @@ -505,13 +505,25 @@ public TrackerState Fork(string name = null) // store via InitializeAsForkOf — but tying the lifecycle to // ItemBase.Fork's machinery here is more boilerplate than just // copying the seven scalar bools, so we bulk-copy directly. - copy.Settings.IgnoreAllLogic = this.Settings.IgnoreAllLogic; - copy.Settings.DisplayAllLocations = this.Settings.DisplayAllLocations; - copy.Settings.AlwaysAllowClearing = this.Settings.AlwaysAllowClearing; - copy.Settings.AutoUnpinLocationsOnClear = this.Settings.AutoUnpinLocationsOnClear; - copy.Settings.PinLocationsOnItemCapture = this.Settings.PinLocationsOnItemCapture; - copy.Settings.MapEnabled = this.Settings.MapEnabled; - copy.Settings.SwapLeftRight = this.Settings.SwapLeftRight; + // Hooks are suppressed for the copy: the fork's layout tree was + // forked above from the source's already-mirrored shape, so + // SwapLeftRight's reload-on-change hook must not re-run pack + // load against the half-built fork. + copy.Settings.SuppressOnChangedHooks = true; + try + { + copy.Settings.IgnoreAllLogic = this.Settings.IgnoreAllLogic; + copy.Settings.DisplayAllLocations = this.Settings.DisplayAllLocations; + copy.Settings.AlwaysAllowClearing = this.Settings.AlwaysAllowClearing; + copy.Settings.AutoUnpinLocationsOnClear = this.Settings.AutoUnpinLocationsOnClear; + copy.Settings.PinLocationsOnItemCapture = this.Settings.PinLocationsOnItemCapture; + copy.Settings.MapEnabled = this.Settings.MapEnabled; + copy.Settings.SwapLeftRight = this.Settings.SwapLeftRight; + } + finally + { + copy.Settings.SuppressOnChangedHooks = false; + } // ---- Per-state pack-driven scalar settings --------------------- // AllowResize / DisabledImageFilterSpec are populated on the