Add command right click functionality for window resizing like in i3wm#1618
Add command right click functionality for window resizing like in i3wm#1618tymscar wants to merge 8 commits intonikitabobko:mainfrom
Conversation
|
Hello! Sorry for bothering @nikitabobko, I just wanted to know if theres anything else needed from me for this PR? |
nikitabobko
left a comment
There was a problem hiding this comment.
Thanks for the PR. I do want to implement this feature at some point.
I was thinking of more generic mouse events where users could assign command for whatever mouse + keyboard events (as described here: #371), but we can start small and implement the mouse dragging/moving with the modifier the way you propose it in this PR, and the way it's done in i3
I have left my first iteration review comments. The most major problem is logic duplication. Please share the logic with existing func resizeWithMouse. Once/If you fix the comments, we can discuss the PR further
| (1 << CGEventType.mouseMoved.rawValue) | | ||
| (1 << CGEventType.flagsChanged.rawValue), | ||
| ) | ||
| if cmdRightTap == nil, |
There was a problem hiding this comment.
GlobalObserver.initObserver is called only once at startup. Why do you need cmdRightTap == nil check?
There was a problem hiding this comment.
Sorry about that. I usually try to code more defensively. I didn't look to deep into how we reload the config, so I was thinking the whole thing might get reinitialised.
| @MainActor | ||
| private func scheduleThrottledRefresh() { | ||
| if pendingDragRefreshTask != nil { return } | ||
| pendingDragRefreshTask = Task { @MainActor in | ||
| try? await Task.sleep(for: preferredFrameDuration()) | ||
| runRefreshSession(.globalObserver("cmdRightMouseDragged"), optimisticallyPreLayoutWorkspaces: true) | ||
| pendingDragRefreshTask = nil | ||
| } | ||
| } | ||
|
|
||
| @MainActor | ||
| private func preferredFrameDuration() -> Duration { | ||
| let maxFps = NSScreen.screens.map { $0.maximumFramesPerSecond }.max() ?? 60 | ||
| let fps = max(maxFps, 1) | ||
| let nanosPerFrame = 1_000_000_000 / fps | ||
| return .nanoseconds(nanosPerFrame) | ||
| } |
There was a problem hiding this comment.
Please take a look at how throttling is implemented in func movedObs and do the same.
I think there is no need for Task.sleep, and there is no need for complicated frames math
There was a problem hiding this comment.
Oh yeah, that's so much cleaner!
| func onCmdRightMouseDragged() async { | ||
| guard let session = cmdRightResizeSession else { return } | ||
| guard let window = Window.get(byId: session.windowId) else { return } | ||
|
|
||
| let point = mouseLocation | ||
| let direction = edgeToDirection(session.edge) | ||
| let delta: CGFloat = (direction.orientation == .h) ? (point.x - session.startPoint.x) : (point.y - session.startPoint.y) | ||
| let diff: CGFloat = direction.isPositive ? delta : -delta | ||
|
|
||
| guard let (parent, _, neighborIndex, orientation) = resolveParentAndNeighbor(window, direction) else { return } | ||
| if abs(diff) < 1 { return } | ||
|
|
||
| window.parentsWithSelf.lazy | ||
| .prefix(while: { $0 !== parent }) | ||
| .compactMap { node -> TreeNode? in | ||
| let p = node.parent as? TilingContainer | ||
| return (p?.orientation == orientation && p?.layout == .tiles) ? node : nil | ||
| } | ||
| .forEach { $0.setWeight(orientation, $0.getWeightBeforeResize(orientation) + diff) } | ||
|
|
||
| let sibling = parent.children[neighborIndex] | ||
| sibling.setWeight(orientation, sibling.getWeightBeforeResize(orientation) - diff) | ||
|
|
||
| currentlyManipulatedWithMouseWindowId = window.windowId |
There was a problem hiding this comment.
I read this code diagonally, so maybe I'm missing something, but my intuition says that the existing func resizeWithMouse can be reused
There was a problem hiding this comment.
You might need to refactor func resizeWithMouse to make it more generic to handle both cases, but I don't want these obviously related logics being implemented two times in two different ways
There was a problem hiding this comment.
Your custom logic doesn't handle more complicated cases:
Screen.Recording.2025-10-02.at.13.47.28.mov
| callback: { _, type, event, _ in | ||
| if !TrayMenuModel.shared.isEnabled { return Unmanaged.passUnretained(event) } | ||
| let flags = event.flags | ||
| let isCmd = flags.contains(.maskCommand) |
There was a problem hiding this comment.
The modifier must be configurable in the config
Smth like:
mouse-resize-modifier = 'alt' # Possible values: cmd, alt, ctrl, shiftAeroSpace's default MOD key is alt, so that should be the default for resizing as well
There was a problem hiding this comment.
That makes sense. I personally always prefer command for this sort of things. Thanks for bringing this up!
| import Common | ||
|
|
||
|
|
||
| private enum ResizeEdge { case left, right, up, down } |
There was a problem hiding this comment.
Can't CardinalDirection just be reused?
|
Thanks so much for the review @nikitabobko ! So I spent quite a while trying all sorts of things out and I think I got something that reduces the duplication as much as I can. You might know this better, but as far as I can tell, I think the way we resize with the mouse normally and how right-click drag should work is totally different. In the case of the mouse resize, that's just normal macOS resizing, and then the accessibility framework updates us on what happened so we can sync up the weights. Whereas for the right-click drag one, that's fully custom, which means we have to handle the actual resizing logic. So what I ended up doing was I factored out the weights calculation and cleaned up the logic a bit, and now both methods use that, but I am not married to the idea. I am all ears if you think there's something cleaner. Thanks for giving me the opportunity to learn more! |
f324d2e to
7aff813
Compare
…ify drag refresh handling
…nd simplifying CmdRightResizeSession
f9cb42a to
8f31af7
Compare
33d72d5 to
98358ff
Compare
|
Hey @nikitabobko! I have been using this fork now for four months daily, and I find it super helpful. So I spent some time fixing merge conflicts, rebased onto main, as well as fixed the issue with resizing diagonals you have told me about using that GIF. |
f0bfc64 to
df97f49
Compare
|
@tymscar Thanks a lot for the PR! I’ve been running your branch for a week with no major issues. Just one thing: Would it be possible to add support for resizing floating windows as well? |
|
Thank you @jthomaschewski ! I don't really use floating windows, and I will be busy starting from tomorrow until middle of December, but I might take a look at it after that! |
# Conflicts: # Sources/AppBundle/GlobalObserver.swift # Sources/AppBundle/config/Config.swift # Sources/AppBundle/config/parseConfig.swift # Sources/AppBundle/mouse/mouse.swift
Phase 4 tab detection fixes (5 blocking, 7 non-blocking): - Fix index out-of-bounds crash on tab promotion (clamp saved index) - Fix tryOnWindowDetected skipped for slot-restored tabs - Fix suspendedWindowSlots cross-reference leak in tryRestoreTabSlot - Fix getWindowLevel mid-cycle cache rebuild causing state inconsistency - Fix floating-point group keys (round bounds to integers) - Add O(1) reverse lookups for isBackgroundTab and tabGroupKey - Replace force-cast with guard-let in validateStillPopups - Make TabGroup struct private, add access-level documentation - Add explanatory comments for layoutReason and false-positive risks Phase 1-3 pre-existing bug fixes: - Fix dead code in GlobalObserver flagsChanged handler (PR nikitabobko#1618) The guard-isModifier prevented modifier-release events from reaching the switch, so releasing the resize modifier mid-drag never terminated the resize session. Moved flagsChanged before the guard. - Fix swapWindows using window1.ownIndex instead of window2.ownIndex in moveWithMouse.swift
This PR adds the right click drag feature from i3wm.
The user should be able to command right-click and drag on any window, and it will resize in the correct direction. If you drag to the left from the left side, you make it bigger, for example.
I have gotten it quite smooth, and as far as I can tell, it actually circumvents the issue that arises when you manually resize a window over another one, and they switch spaces. I can't replicate that with this method. This method runs at the refresh rate of your fastest monitor, or if that fails, I default it to 60fps.
I have also made it so when you hold Command, it doesn't pass through the right-click to the window under it. That was annoying because while dragging, I would by mistake click on Back or something.
Fixes issue #206