Skip to content

fix(profile): button-initiated DPI/SmartShift changes update the UI and persist - #148

Merged
mmaher88 merged 1 commit into
masterfrom
fix-button-persists-to-profile
Jul 28, 2026
Merged

fix(profile): button-initiated DPI/SmartShift changes update the UI and persist#148
mmaher88 merged 1 commit into
masterfrom
fix-button-persists-to-profile

Conversation

@mmaher88

Copy link
Copy Markdown
Owner

Symptom

Bind a button to DPI cycle or SmartShift toggle, press it: the hardware changes but the UI keeps showing the old value. This one has been chased repeatedly.

Root cause

Traced with instrumentation on real hardware:

smartShiftChanged relay fired      → clears DeviceModel's display cache
smartShiftEnabled() → hasDisplayCache=false, live=false   ← correct, briefly
onDisplayProfileChanged ... smartShift=true               ← re-pushes the UNCHANGED profile
smartShiftEnabled() → hasDisplayCache=true, display=true, live=false   ← UI stale again

The button wrote straight to DeviceSession, bypassing the profile. DeviceModel has two sources of truth — a display cache (fed by profile loads / UI edits) and the live session — and the getters short-circuit to the cache. The relay correctly cleared the cache, but onDisplayProfileChanged then re-armed it from the unchanged profile, clobbering the live value.

That's why cache-invalidation patches kept regressing: the bug is a sequencing race (clear → re-arm), so any new caller of setDisplayValues reintroduces it. The value also snapped back on the next profile apply.

Fix

Persist button-initiated changes into the active profile, so there's one source of truth.

  • ProfileOrchestrator::applyHardwareChange — mirror of applyDisplayedChange, but targets the hardware profile (the button acted on the device, not on whichever profile you're viewing), and skips the hardware forward since the device is already in the new state. UI refreshes only when the viewed profile is the active one.
  • Dispatcher notifies explicitly (dpiChangedByButton / smartShiftChangedByButton) rather than having the orchestrator listen to DeviceSession's change signals — this is what stops a profile application from echoing back and rewriting the profile it just applied.
  • AppRoot wires the two signals, honoring the "zero connect() calls in the .cpp" convention.

Behavior change

A DPI cycled by button now survives the next profile apply instead of snapping back, and is written to the profile on disk. That is the intended semantics for this change. Saves are synchronous, matching the existing applyDisplayedChange path (which already saves per UI change, including slider drags).

Test fixture fix

AppRootFixture seeded a default profile with SmartShift on but left the mock session's SmartShift at its zero-initialised false, so a toggle computed from the session was a silent no-op. Seeded the session to match, as real enumeration + applyProfileToHardware do in production.

Tests

  • 3 orchestrator tests, incl. writing the hardware profile while viewing a different one (viewed profile and UI must stay untouched)
  • 2 end-to-end tests driving a real button press through dispatcher → AppRoot wiring → orchestrator → UI + profile

Full suite green: 736 C++. Smoke-tested on an MX Master 3S — SmartShift toggle and DPI cycle both reflect in the UI immediately.

@mmaher88
mmaher88 force-pushed the fix-button-persists-to-profile branch 2 times, most recently from 6a13026 to b6f3069 Compare July 28, 2026 06:05
…nd persist

A diverted button that cycles DPI or toggles SmartShift wrote straight to
DeviceSession, bypassing the profile entirely. The UI kept showing the old
value: DeviceModel's display cache is re-armed by onDisplayProfileChanged
with the unchanged profile's values, clobbering the live state the relay had
just exposed. The value also snapped back on the next profile apply.

Persist the change into the active profile instead, so there is one source of
truth. applyHardwareChange mirrors applyDisplayedChange but targets the
HARDWARE profile — the button acted on the device, not on whichever profile
is being viewed — and skips the hardware forward because the device is
already in the new state. The UI is refreshed only when the viewed profile is
the active one.

The dispatcher notifies explicitly rather than having the orchestrator listen
to DeviceSession's change signals; that keeps a profile *application* from
echoing back and rewriting the profile it just applied.

AppRootFixture seeded a default profile with SmartShift on but left the mock
session's SmartShift at its zero-initialised default, so a toggle computed
from the session was a no-op. Seed the session to match, as real enumeration
would.

Tests: 3 orchestrator (incl. writing the hardware profile while viewing
another) + 2 end-to-end button-press tests. Verified on an MX Master 3S.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant