Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 10 additions & 33 deletions Sources/WindowSwizzles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
34 changes: 26 additions & 8 deletions programaTests/WindowAndDragTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1571,33 +1571,51 @@ 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],
backing: .buffered,
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)
}
Expand Down
Loading