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