fix: do not activate a disabled toggle from a tray icon click [patch] - #9
Merged
matt-edmondson merged 2 commits intoSep 24, 2026
Merged
Conversation
OnTrayIconClicked found the first TrayToggleItem in the menu and called Activate unconditionally, never reading model.IsEnabled. The native menu path is gated for free — the toolkit greys a disabled NativeMenuItem out and will not raise Click for it — so the tray icon was the one path from a click to the tool's setter with nothing in the way. A tool registering a toggle with an isEnabled predicate therefore saw its setter run on a left click while the menu showed the same action greyed out, invoking logic the tool had explicitly gated off. The click handler now skips disabled toggles. Activate also returns early for a disabled item, which covers the gap the native gating leaves: an item is greyed out as of the last refresh, and a tool's state can change between painting the menu and the user clicking it. IsEnabled is the value the paint used, so this agrees with what the user saw. OnTrayIconClicked becomes internal so it can be driven with no display, as the rest of this class's tests already are. Fixes #7 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VSTy8Ye7JXqeFqvhrmnRt
SonarCloud failed the quality gate at 66.7% coverage on new code. The uncovered line was the early return in Activate: every test drove OnTrayIconClicked, which skips a disabled toggle before Activate is reached, so the backstop behind the native menu was never executed. The honest fix is to test it rather than delete it. The guard covers the gap native greying cannot - an item greyed out as of the last refresh, with a click already on its way - and dropping a correct check to raise a coverage number would trade the fix for the metric. NativeMenuItem exposes Click with no way to raise it, so the native path cannot be driven from a test at all. Activate becomes internal, as OnTrayIconClicked already is and for the same reason, and the new test calls it with an item the tool has since disabled. Verified the test fails without the guard (setter runs once against an expected zero), and against a local cobertura report that every executable line this PR adds is now hit. The two remaining uncovered lines in the file, OnQuitRequested and OnExit, are pre-existing and untouched by this diff. 113 tests: 112 pass, 1 pre-existing skip. Solution builds with 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VSTy8Ye7JXqeFqvhrmnRt
|
matt-edmondson
deleted the
claude/trayapp-7-disabled-toggle-icon-click
branch
September 24, 2026 00:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #7.
OnTrayIconClickedtook the firstTrayToggleItemin the menu and calledActivateon it without readingIsEnabled:The menu path never needed that check because the toolkit supplies it:
BuildNativeMenuwiresnative.Click += …, and a greyed-outNativeMenuItemdoes not raiseClick. The tray icon is the one path from a click to the tool's setter with nothing in the way, so a tool'sisEnabledpredicate was honoured in the menu and silently ignored on the icon — the entry showed unavailable while a left-click ran it anyway.Measured: with the guards reverted,
TrayIconClick_WithTheToggleDisabled_DoesNotRunTheToolsSetterrecords the setter running once against an expected zero.The change
Two guards, both small:
OnTrayIconClicked—if (model is TrayToggleItem { IsEnabled: true }), exactly as the issue proposes.Activate— returns early for a disabled item. This is the defence-in-depth the issue raises as optional, and it earns its place on a case the toolkit gating cannot cover: a native item is greyed out as of the last refresh, and nothing repaints a menu the user already has open, so a tool whose state changes between the paint and the click still reachesActivate.IsEnabledis the value the paint used, so the guard agrees with what the user saw rather than re-reading the predicate and disagreeing with the screen.Both
OnTrayIconClickedandActivateareinternal, joiningNativeMenuandHasStarted, so they can be driven with no display — which is how the rest of this class is already tested. Neither is public, so the package surface is unchanged.TrayMenu.Activateis deliberately not touched: itsboolreturn documents "the item's action completed without throwing", and making it also mean "was disabled" would conflate two different answers in a public API.One thing worth your call
The guard sits inside the loop, per the issue's suggested line, so the click now activates the first enabled toggle. With two toggles where the first is disabled, that means the icon flips the second one rather than doing nothing. For the single-toggle case they are identical. If you would rather the fast path stay pinned to "the first switch" and do nothing when it is unavailable, that is moving the
returnout one level — say the word and I will change it.Quit is unaffected
Worth confirming, since the
Activateguard applies to commands too:TrayMenuconstructsQuitItemwithgetEnabled: null, so itsIsEnabledis alwaystrue.TrayIconClick_WithNoToggleAtAll_DoesNothingpins that a toggle-free menu has nothing picked for it.Tests
Five added to
TrayApplicationTests, which previously only covered menu projection and disposal. Verified by reverting each guard in turn and re-running: three fail against the old implementation.TrayIconClick_WithTheToggleDisabled_DoesNotRunTheToolsSetterTrayIconClick_BecomingEnabledAgain_RunsTheToolsSetterActivate_WithTheItemDisabled_DoesNotRunTheToolsSetterActivateguard — setter ran once, expected zeroTrayIconClick_WithTheToggleEnabled_RunsTheToolsSetterTrayIconClick_WithNoToggleAtAll_DoesNothingEach test calls
menu.Refresh()before clicking.IsEnabledis a cache of the last refresh and defaults totrue, so without it the disabled test would have passed for the wrong reason.Activate_WithTheItemDisabled_…was added after SonarCloud failed the gate at 66.7% coverage on new code: every other test drivesOnTrayIconClicked, which skips a disabled toggle beforeActivateis reached, so the backstop was never executed. The alternative was deleting the guard to raise the number, which would have traded the fix for the metric.NativeMenuItemexposesClickwith no way to raise it, so the native path cannot be driven from a test — hence callingActivatedirectly. Checked against a local cobertura report that every executable line this PR adds is now hit; the two uncovered lines remaining in the file (OnQuitRequested,OnExit) are pre-existing and untouched by this diff.Full suite: 112 passed, 0 failed, 1 skipped (the pre-existing non-Linux
DesktopSessioncase). Solution builds with 0 warnings, 0 errors.🤖 Generated with Claude Code
https://claude.ai/code/session_018VSTy8Ye7JXqeFqvhrmnRt