Add keyboard navigation to the stacked-window switcher - #1808
Conversation
c900192 to
3e94718
Compare
Indeed we'll want to take this a different route. There are two options:
I'm fine with either approach, and both have their own pros & cons. |
3e94718 to
d251b39
Compare
Arrow keys move the highlight, Return raises the selected window and leaves the list up so the stack can be walked, Escape closes. Rectangle is an accessory app, so its panels can never become key and ordinary focus cannot deliver these keys - the list uses a consuming event tap instead, scoped as tightly as it can be. Only bare navigation keys are claimed: any held modifier goes to the app underneath, the tap is not installed at all while VoiceOver is running, since Quick Nav owns the arrow keys, and an untouched list expires after five seconds so it can never sit holding them. macOS reports every arrow keystroke as carrying .function and .numericPad. Those are properties of the key rather than modifiers being held, so only the four keys a user can actually hold are treated as a reason to pass a keystroke through. The badge also stops counting stacks that are not there: a window covering the screen joins a stack the tiled windows already form, but never forms one on its own. Rows that would fall off the bottom of the screen are not built, since arrow keys could otherwise select - and Return raise - a window with no visible row. The toggle shortcut follows the app's existing shortcut conventions, and the view code moves into StackBadgeWindow, StackBadgeListPanel and StackBadgeRowView, one view per file.
d251b39 to
5e506d8
Compare
|
I was thinking about this a little more and figured I'd elaborate a bit. I think it's ok to remove the badge number entirely, and only show the list of window titles that can be selected. This lends itself to the NSMenu approach quite well. The NSPanel is certainly the better choice for the user experience, but it's also a lot more involved to get right. My initial thought is perhaps simply go with the NSMenu for the first cut, since that should be pretty easy and can be converted over to the NSPanel later. In the meantime, I'm going to go ahead and push out a release and will roll this change into the next one once it's ready. |
|
Happy to go whichever way you prefer. One data point before you decide: I had a go at the NSPanel route and it's further along than you might expect - The one thing that pushed me toward the panel: an NSMenu closes on selection, so you lose being able to walk a stack - down, Return, look, down, Return - which turns out to be the thing I use this for most. With the panel the list stays up and keeps the keyboard. If you'd still rather have NSMenu as the first cut I'll do that instead, no argument. And no rush on my side given you're mid-release. On dropping the count: I can see the case, though the badge is what tells you a stack is there in the first place. Would you want the list on hover with no indicator at all beforehand? |
|
Oh, nice! Let's continue with the NSPanel route for sure. In my testing, the badge and the list are shown at the same time on hover, and that's how I ended up thinking down that path. Is the intent to show the badge even when not hovering, or is it to first show the badge, then the list? |
|
Provided we're not going the NSMenu route, I'm actually good with however you'd like to take it - I was just looking to simplify in case there were road bumps going with the NSPanel. |
|
Good question, and I think you spotted something. Today both appear together on dwell, which does make the count redundant - if you can see three rows, a pill saying 3 is not earning its place. That is the version you were testing, and it is a fair reason to wonder why the badge is there at all. The intent is the two-stage one: dwell on a stack corner shows just the badge, and the list opens when you move onto it. That makes the badge the affordance you act on rather than a label duplicating the list, and passing your cursor over a stack on the way to somewhere else costs you a small pill instead of a full popup. There is a second reason that only applies now that we are on the panel: showing the list means taking key status, since that is how it gets the arrow keys. If the list opens on dwell alone, resting the cursor near a stack corner quietly takes the keyboard from whatever you were typing in. Gating it behind a deliberate move onto the badge means we only take the keyboard when someone has actually opted in. I will implement that and update the PR. If you would rather it stayed one stage, say so and I will drop it back - it is a small change either way. |
|
I like the two stage concept. 👍 |
Follow-on to the hover badge from #1795. Arrow keys move the highlight, Return raises the selected window and leaves the list up so you can walk the stack, Escape closes.
How the keys are delivered, since this is the part worth your judgement. Rectangle is an accessory app, so its panels can never become key, and a focus-based approach genuinely doesn't work — I built it that way first and it required a click before the arrows did anything, then leaked arrow keys into the window it had just raised. The list uses a consuming
CGEventtap instead, scoped as narrowly as I could make it:I'd rather ask than assume: is a key-consuming event tap acceptable here at all? You know Rectangle's accessibility bar and its users far better than I do, and if the answer is no, the list works fine mouse-only and I'll drop this half.
Known limitation, stated plainly. With a CJK input method composing, bare arrows and Return are candidate-selection keys, and there's no reliable way to detect that an IME owns the keystroke. While the list is open — at most five seconds without interaction — those keys would go to the list instead of the IME. Mouse interaction is unaffected. I couldn't find an honest fix for this; flagging it rather than papering over it.
Also here, because they're the same subsystem:
ActiveEventMonitorstart/stop is serialized and tap-timeout recovery re-enables the port in place instead of tearing the monitor down from inside its own callback. Snapping shares that class, so the force-unwrap that recovery path could hit wasn't scoped to this feature.StackBadgeWindow,StackBadgeListPanelandStackBadgeRowView, one view per file, matchingFootprintWindow.This builds on #1810 for
OverlapOffsetGeometry.coversScreen, which the covering-window rule uses. The diff here will shrink to the badge changes alone once that merges; happy to reorder if you'd rather take them the other way round.Everything stays behind the existing badge preference, off by default.