diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f6170f..7bc1606d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Programa is a fork of [cmux](https://github.com/manaflow-ai/cmux); for history p - The `programa codex install-hooks` and `uninstall-hooks` legacy aliases. Use `programa codex install-integration` and `uninstall-integration`. ### Fixed +- Dragging the sidebar edge resizes the sidebar again instead of moving the whole window. - The Window menu's Fill, Center, Move & Resize, Full Screen Tile and Move to Display items, and their shortcuts, work again. The main window had been marked not movable, which makes macOS disable all of them. - The notifications popover now opens in minimal mode, the default layout. Show Notifications (⌘I), the sidebar bell and the menu bar icon's Show Notifications did nothing there. - Port chips in the sidebar now show up again for terminals restored after relaunching Programa. The restored terminals kept their running shell, but port scanning never learned about them. diff --git a/Sources/WindowSwizzles.swift b/Sources/WindowSwizzles.swift index a186a097..819a6079 100644 --- a/Sources/WindowSwizzles.swift +++ b/Sources/WindowSwizzles.swift @@ -165,8 +165,14 @@ extension NSWindow { return } - guard shouldSuppressWindowMoveForFolderDrag(window: self, event: event), - let contentView = self.contentView else { + // The window stays isMovable so AppKit validates the Window menu tiling + // items, but no click may start a native window drag: one on the SwiftUI + // sidebar resizer band moved the window instead of resizing the sidebar, + // and folder-icon drags dragged the window along. Every deliberate drag + // surface (WindowDragHandleView, the titlebar background above) re-enables + // movability around its own performDrag via withTemporaryWindowMovableEnabled. + guard event.type == .leftMouseDown, isMovable, + contentView is MainWindowHostingViewMarker else { #if DEBUG if event.type == .keyDown { folderGuardMs = (ProcessInfo.processInfo.systemUptime - folderGuardStart) * 1000.0 @@ -179,39 +185,10 @@ extension NSWindow { programa_sendEvent(event) return } -#if DEBUG - if event.type == .keyDown { - folderGuardMs = (ProcessInfo.processInfo.systemUptime - folderGuardStart) * 1000.0 - } - let originalDispatchStart = event.type == .keyDown ? ProcessInfo.processInfo.systemUptime : 0 -#endif - - let contentPoint = contentView.convert(event.locationInWindow, from: nil) - let hitView = contentView.hitTest(contentPoint) - let previousMovableState = isMovable - if previousMovableState { - isMovable = false - } - - #if DEBUG - let hitDesc = hitView.map { String(describing: type(of: $0)) } ?? "nil" - dlog("window.sendEvent.folderDown suppress=1 hit=\(hitDesc) wasMovable=\(previousMovableState)") - #endif + isMovable = false programa_sendEvent(event) -#if DEBUG - if event.type == .keyDown { - originalDispatchMs = (ProcessInfo.processInfo.systemUptime - originalDispatchStart) * 1000.0 - } -#endif - - if previousMovableState { - isMovable = previousMovableState - } - - #if DEBUG - dlog("window.sendEvent.folderDown restore nowMovable=\(isMovable)") - #endif + isMovable = true } @objc func programa_performKeyEquivalent(with event: NSEvent) -> Bool { diff --git a/programaTests/WindowAndDragTests.swift b/programaTests/WindowAndDragTests.swift index 229c95f7..93351adb 100644 --- a/programaTests/WindowAndDragTests.swift +++ b/programaTests/WindowAndDragTests.swift @@ -1571,7 +1571,17 @@ final class WindowMoveSuppressionHitPathTests: XCTestCase { } } - private func movabilityDuringMouseDown(contentView: NSView) -> (during: Bool?, after: Bool) { + private struct MovabilityRecordingRepresentable: NSViewRepresentable { + let view: MovabilityRecordingView + func makeNSView(context: Context) -> MovabilityRecordingView { view } + func updateNSView(_ nsView: MovabilityRecordingView, context: Context) {} + } + + /// The click only reaches the recorder once the window is on screen and key; + /// an unshown window drops it before hit-testing. + private func movabilityDuringMouseDown( + contentView: (MovabilityRecordingView) -> NSView + ) -> (during: Bool?, after: Bool) { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 320, height: 180), styleMask: [.titled, .closable], @@ -1579,25 +1589,33 @@ final class WindowMoveSuppressionHitPathTests: XCTestCase { defer: false ) defer { window.orderOut(nil) } - window.contentView = contentView + let recorder = MovabilityRecordingView(frame: NSRect(x: 0, y: 0, width: 320, height: 180)) + window.contentView = contentView(recorder) window.isMovable = true - let view = MovabilityRecordingView(frame: NSRect(x: 0, y: 0, width: 80, height: 60)) - contentView.addSubview(view) - window.sendEvent(makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 40, y: 30), window: window)) - return (view.movableDuringMouseDown, window.isMovable) + window.makeKeyAndOrderFront(nil) + window.displayIfNeeded() + window.contentView?.layoutSubtreeIfNeeded() + window.sendEvent(makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 160, y: 90), window: window)) + return (recorder.movableDuringMouseDown, window.isMovable) } /// A movable main window must not let a plain click start a native window drag: /// the sidebar resizer band moved the window instead of resizing the sidebar. func testMainWindowMouseDownDispatchKeepsWindowImmovableThenRestoresMovable() { - let result = movabilityDuringMouseDown(contentView: MainWindowHostingView(rootView: EmptyView())) + let result = movabilityDuringMouseDown { recorder in + MainWindowHostingView(rootView: MovabilityRecordingRepresentable(view: recorder)) + } XCTAssertEqual(result.during, false) XCTAssertTrue(result.after) } /// Other windows (Settings, Debug windows) keep native window dragging. func testNonMainWindowMouseDownDispatchLeavesWindowMovable() { - let result = movabilityDuringMouseDown(contentView: NSView()) + let result = movabilityDuringMouseDown { recorder in + let container = NSView(frame: recorder.frame) + container.addSubview(recorder) + return container + } XCTAssertEqual(result.during, true) XCTAssertTrue(result.after) }