Skip to content
Open
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
9 changes: 8 additions & 1 deletion Sources/Pesty/AppController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
32 changes: 25 additions & 7 deletions Sources/Pesty/UI/BarResizeHandle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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?) {
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/Pesty/UI/BarWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down