diff --git a/Sources/Pesty/AppController.swift b/Sources/Pesty/AppController.swift index cb6a4a7..062ae64 100644 --- a/Sources/Pesty/AppController.swift +++ b/Sources/Pesty/AppController.swift @@ -595,7 +595,14 @@ final class AppController: NSObject, NSApplicationDelegate, NSWindowDelegate { guard let barWindow = barController?.window, event.window === barWindow else { return event } - if barWindow.firstResponder is BarResizeHandleResponder { return event } + // Only a handle that is actively dragging may own the bar's keys. + // A handle left holding first responder outside a drag (interrupted + // drag, focus moved onto it) used to swallow every key here, which made + // the bar look focused while arrows, Escape and typing all just beeped. + if let handle = barWindow.firstResponder as? BarResizeHandleResponder, + handle.isTrackingDrag { + return event + } if handleBarCommandShortcut(event) { return nil } diff --git a/Sources/Pesty/UI/BarResizeHandle.swift b/Sources/Pesty/UI/BarResizeHandle.swift index b05bb87..70283a5 100644 --- a/Sources/Pesty/UI/BarResizeHandle.swift +++ b/Sources/Pesty/UI/BarResizeHandle.swift @@ -4,7 +4,14 @@ import SwiftUI /// Lets the Paste Bar's local key monitor leave Escape and drag-time keys to /// the handle while it temporarily owns first-responder status. -protocol BarResizeHandleResponder: AnyObject {} +@MainActor +protocol BarResizeHandleResponder: AnyObject { + /// True only while a resize drag is in progress. The key monitor must not + /// hand the bar's keys to the handle outside a drag: if the handle ever + /// holds first responder without tracking, every key (arrows, Escape, + /// Return, letters) would be swallowed and only beep. + var isTrackingDrag: Bool { get } +} /// A compact drag target for resizing the Paste Bar vertically. /// @@ -82,11 +89,14 @@ private final class BarResizeHandleView: NSView, BarResizeHandleResponder { didSet { updateAccessibilityValue() } } - private var isTrackingDrag = false + var isTrackingDrag = false private var ownsResizeCursor = false private weak var previousFirstResponder: NSResponder? - override var acceptsFirstResponder: Bool { true } + // Being first responder means owning every key the Paste Bar receives, so + // only become one for the duration of a drag (mouseDown sets the flag + // before asking the window for focus). + override var acceptsFirstResponder: Bool { isTrackingDrag } override var intrinsicContentSize: NSSize { NSSize(width: 42, height: 14) } init(callbacks: Callbacks, accessibilityValue: CGFloat?) { @@ -202,10 +212,18 @@ private final class BarResizeHandleView: NSView, BarResizeHandleResponder { ownsResizeCursor = false } // If the previous responder refuses, the window itself takes over; - // the handle must never keep the bar's keys after a drag. - if let window, window.firstResponder === self, - !window.makeFirstResponder(previousFirstResponder) { - window.makeFirstResponder(nil) + // the handle must never keep the bar's keys after a drag. Restoring to + // the handle itself would re-trap the keys when the handle already + // owned focus before the drag, so that case also falls back to the + // window. + if let window, window.firstResponder === self { + let restored: Bool + if let previousFirstResponder, previousFirstResponder !== self { + restored = window.makeFirstResponder(previousFirstResponder) + } else { + restored = window.makeFirstResponder(nil) + } + if !restored { window.makeFirstResponder(nil) } } previousFirstResponder = nil } diff --git a/Sources/Pesty/UI/BarWindowController.swift b/Sources/Pesty/UI/BarWindowController.swift index 69eaa06..62a56b2 100644 --- a/Sources/Pesty/UI/BarWindowController.swift +++ b/Sources/Pesty/UI/BarWindowController.swift @@ -176,6 +176,14 @@ final class BarWindowController: NSWindowController, NSWindowDelegate { NSApp.activate(ignoringOtherApps: true) panel.makeKeyAndOrderFront(nil) + // The panel keeps its first responder across orderOut, so a resize + // drag that ended without returning focus would hand every key of the + // next presentation to the handle. Opening the bar always starts from + // the strip. + if panel.firstResponder is BarResizeHandleResponder { + panel.makeFirstResponder(nil) + } + let finish: @MainActor @Sendable () -> Void = { [weak self] in guard let self else { return } self.settle(token) {