feat(cycle): add Toggle Window mode for last-active window - #10
Conversation
Keep existing Hide (Z-order first). Toggle Window always shows or hides the app's last active window so multi-window apps stick to one. Based on upstream v1.10.3 without local passthrough changes.
| var matchingHandles = new List<HWND>(matchingWindows.Count); | ||
| foreach (var window in matchingWindows) | ||
| { | ||
| matchingHandles.Add(window.Handle); | ||
| } |
There was a problem hiding this comment.
Why not just this:
var matchingHandles = matchingWindows.Select(w => w.Handle).ToList();?
| } | ||
|
|
||
| // Prefer BCL IndexOf when the caller passed a List (common case). | ||
| if (handles is List<HWND> list) |
There was a problem hiding this comment.
Seems like this is always true, because there's only one call to ToggleWindowTargetResolver.Resolve and it's always passing List<HWND> there.
I think this could be greatly simplified then. Let's just pass List<HWND> directly. We don't need this generalization today so I would not complicate it for the sake of potential future uses.
|
|
||
| ## Toggle Window | ||
|
|
||
| **Best for:** Multi-window apps where you always want the *same* window (the last one you used). |
There was a problem hiding this comment.
Actually this is really good idea. I think I was thinking about the same but not as separate cycle mode, but just make this a default behavior for Hide mode. Z-order is a bit confusing on windows and this was pissing me off as well :)
I think the only reason I did not do that is that I only use Hide mode for apps where I only have a single window so it didn't really matter.
I'll pull these changes and spend some time using that to get a better opinion myself.
|
|
||
| private static int IndexOf(IReadOnlyList<HWND> handles, HWND? handle) | ||
| { | ||
| if (handle is not { } h) |
There was a problem hiding this comment.
Honestly this is so non-obvious with that negative pattern that I had to look it up what it does to be sure.
Given the other comment simplifies that function we can go back to something more straightforward like:
if (handle is null)
{
return -1;
}| /// <param name="currentHandle">Foreground window handle, or null if unknown.</param> | ||
| /// <param name="rememberedHandle">Last toggled handle for this app, or null.</param> | ||
| /// <returns>Null when <paramref name="matchingHandles"/> is empty.</returns> | ||
| public static Result? Resolve( |
There was a problem hiding this comment.
Any reason why you went with static API in this case?
| return null; | ||
| } | ||
|
|
||
| var target = matchingWindows[resolution.Value.TargetIndex]; |
There was a problem hiding this comment.
I wonder if it wouldn't make more sense to actually pass collection of ApplicationWindow to the resolver and then we can avoid all that playing around with indexes which seems like extra complexity that is not necessary here.
There was a problem hiding this comment.
Pull request overview
This PR adds a fourth cycle mode, Toggle Window, which always shows/hides an app's last-active window instead of following Z-order like Hide. It introduces a pure ToggleWindowTargetResolver (focused match → remembered handle → first match) and wires it into Switcher, which now remembers per-app target handles in a _lastToggleWindows dictionary. The Switcher.Execute control flow is refactored into ActivateFirst/ExecuteToggleWindow helpers, and the off-app gate is broadened to match any window of the target process. The mode is surfaced through the enum, label converter, cycle-mode selector UI, docs, and tests.
Changes:
- New
ToggleWindowcycle mode with a dedicated, unit-tested target resolver and remembered-window logic inSwitcher. - Refactored
Switcher.Executedispatch and broadened the "already on this app" detection to consider all matching windows. - Updated UI (selector button, label converter), configuration enum, docs (README,
cycle-modes.mdx), and round-trip/resolver tests.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
AppSwitcher/ToggleWindowTargetResolver.cs |
New pure resolver deciding target window index and Activate/Hide action. |
AppSwitcher/Switcher.cs |
Adds ExecuteToggleWindow/ActivateFirst, remembered-handle map, and refactors mode dispatch/off-app gate. |
AppSwitcher/Configuration/Configuration.cs |
Appends ToggleWindow to the CycleMode enum. |
AppSwitcher/UI/Converters/CycleModeToLabelConverter.cs |
Maps ToggleWindow to the "Toggle Window" label. |
AppSwitcher/UI/Controls/CycleModeSelector.xaml |
Adds the Toggle Window selector button and tooltip. |
AppSwitcher/UI/Controls/CycleModeSelector.xaml.cs |
Updates button appearance state for the new button. |
AppSwitcher.Tests/ToggleWindowTargetResolverTests.cs |
Unit tests covering resolver priority/action cases. |
AppSwitcher.Tests/Configuration/Storage/ApplicationConfigurationDocumentTests.cs |
Adds ToggleWindow to the config round-trip theory. |
docs/src/content/docs/configuration/cycle-modes.mdx |
Documents the new mode and clarifies Hide behavior. |
README.md |
Updates feature list to four cycle modes including Toggle Window. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Summary
Details
In multi-window apps, Hide mode can surface different windows based on Z-order. Toggle Window remembers and always targets the app's last active window, so repeated hotkey presses reliably show/hide the same window.
Changes include:
ToggleWindowTargetResolverfor resolving the last-active window targetSwitcherandConfiguration/ cycle mode UIcycle-modes.mdx, README)Based on upstream v1.10.3.
Test plan
dotnet test)