From c8d9783b1bbe9810f353eaff6dea8e941de8c6bf Mon Sep 17 00:00:00 2001 From: maximdx Date: Sat, 7 Nov 2026 21:18:18 +0800 Subject: [PATCH] Only hand the bar's keys to a resize handle that is dragging The resize handle could hold first responder outside a drag. The bar's key monitor then handed every key to the handle, which handled none of them, so arrows, Escape, Return and typing all just beeped while the bar looked focused. Hiding the bar did not help because the panel keeps its first responder across orderOut; only relaunching the app cleared it. Gate the key monitor's early return on isTrackingDrag, make the handle focusable only while a drag is in progress, never restore focus to the handle itself in finishTracking, and clear a stale handle first responder when the bar is shown. Fixes #112. --- Sources/Pesty/AppController.swift | 9 +++++- Sources/Pesty/UI/BarResizeHandle.swift | 32 +++++++++++++++++----- Sources/Pesty/UI/BarWindowController.swift | 8 ++++++ 3 files changed, 41 insertions(+), 8 deletions(-) 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) {