Cancel recording when the user clicks outside the input

Fixes #4
This commit is contained in:
Sindre Sorhus 2020-05-10 02:00:24 +08:00
parent 4dac019386
commit 8e7291f441
3 changed files with 64 additions and 5 deletions

View File

@ -14,12 +14,14 @@ struct ContentView: View {
VStack { VStack {
HStack { HStack {
KeyboardShortcuts.Recorder(for: .testShortcut1) KeyboardShortcuts.Recorder(for: .testShortcut1)
Text("Is pressed? \(isPressed1 ? "Yes" : "No")") .padding(.trailing, 10)
Text("Pressed? \(isPressed1 ? "👍" : "👎")")
.frame(width: 100, alignment: .leading) .frame(width: 100, alignment: .leading)
} }
HStack { HStack {
KeyboardShortcuts.Recorder(for: .testShortcut2) KeyboardShortcuts.Recorder(for: .testShortcut2)
Text("Is pressed? \(isPressed2 ? "Yes" : "No")") .padding(.trailing, 10)
Text("Pressed? \(isPressed2 ? "👍" : "👎")")
.frame(width: 100, alignment: .leading) .frame(width: 100, alignment: .leading)
} }
} }

View File

@ -40,12 +40,21 @@ extension KeyboardShortcuts {
return size return size
} }
private var cancelButton: NSButtonCell?
private var showsCancelButton: Bool {
get { (cell as? NSSearchFieldCell)?.cancelButtonCell != nil }
set {
(cell as? NSSearchFieldCell)?.cancelButtonCell = newValue ? cancelButton : nil
}
}
public required init(for name: Name) { public required init(for name: Name) {
self.shortcutName = name self.shortcutName = name
super.init(frame: .zero) super.init(frame: .zero)
self.delegate = self self.delegate = self
self.placeholderString = "Click to Record" self.placeholderString = "Record Shortcut"
self.centersPlaceholder = true self.centersPlaceholder = true
self.alignment = .center self.alignment = .center
(self.cell as? NSSearchFieldCell)?.searchButtonCell = nil (self.cell as? NSSearchFieldCell)?.searchButtonCell = nil
@ -59,6 +68,10 @@ extension KeyboardShortcuts {
self.setContentHuggingPriority(.defaultHigh, for: .vertical) self.setContentHuggingPriority(.defaultHigh, for: .vertical)
self.setContentHuggingPriority(.defaultHigh, for: .horizontal) self.setContentHuggingPriority(.defaultHigh, for: .horizontal)
self.widthAnchor.constraint(greaterThanOrEqualToConstant: CGFloat(minimumWidth)).isActive = true self.widthAnchor.constraint(greaterThanOrEqualToConstant: CGFloat(minimumWidth)).isActive = true
// Hide the cancel button when not showing the shortcut so the placeholder text is properly centered. Must be last.
self.cancelButton = (self.cell as? NSSearchFieldCell)?.cancelButtonCell
self.showsCancelButton = !stringValue.isEmpty
} }
@available(*, unavailable) @available(*, unavailable)
@ -71,12 +84,20 @@ extension KeyboardShortcuts {
if stringValue.isEmpty { if stringValue.isEmpty {
userDefaultsRemove(name: shortcutName) userDefaultsRemove(name: shortcutName)
} }
showsCancelButton = !stringValue.isEmpty
if stringValue.isEmpty {
// Hack to ensure that the placeholder centers after the above `showsCancelButton` setter.
focus()
}
} }
/// :nodoc: /// :nodoc:
public func controlTextDidEndEditing(_ object: Notification) { public func controlTextDidEndEditing(_ object: Notification) {
eventMonitor = nil eventMonitor = nil
placeholderString = "Click to Record" placeholderString = "Record Shortcut"
showsCancelButton = !stringValue.isEmpty
} }
/// :nodoc: /// :nodoc:
@ -88,17 +109,36 @@ extension KeyboardShortcuts {
} }
placeholderString = "Press Shortcut" placeholderString = "Press Shortcut"
showsCancelButton = !stringValue.isEmpty
hideCaret() hideCaret()
eventMonitor = LocalEventMonitor(events: [.keyDown]) { [weak self] event in eventMonitor = LocalEventMonitor(events: [.keyDown, .leftMouseUp, .rightMouseUp]) { [weak self] event in
guard let self = self else { guard let self = self else {
return nil return nil
} }
let clickPoint = self.convert(event.locationInWindow, from: nil)
let clickMargin: CGFloat = 3
if
(event.type == .leftMouseUp || event.type == .rightMouseUp),
!self.frame.insetBy(dx: -clickMargin, dy: -clickMargin).contains(clickPoint)
{
self.blur()
return nil
}
guard event.isKeyEvent else {
return nil
}
if if
event.modifiers.isEmpty, event.modifiers.isEmpty,
event.specialKey == .tab event.specialKey == .tab
{ {
self.blur()
// We intentionally bubble up the event so it can focus the next responder.
return event return event
} }
@ -134,24 +174,37 @@ extension KeyboardShortcuts {
} }
if let menuItem = shortcut.takenByMainMenu { if let menuItem = shortcut.takenByMainMenu {
// TODO: Find a better way to make it possible to dismiss the alert by pressing "Enter". How can we make the input automatically temporarily lose focus while the alert is open?
self.blur()
NSAlert.showModal( NSAlert.showModal(
for: self.window, for: self.window,
message: "This keyboard shortcut cannot be used as it's already used by the “\(menuItem.title)” menu item." message: "This keyboard shortcut cannot be used as it's already used by the “\(menuItem.title)” menu item."
) )
self.focus()
return nil return nil
} }
guard !shortcut.isTakenBySystem else { guard !shortcut.isTakenBySystem else {
self.blur()
NSAlert.showModal( NSAlert.showModal(
for: self.window, for: self.window,
message: "This keyboard shortcut cannot be used as it's already a system-wide keyboard shortcut.", message: "This keyboard shortcut cannot be used as it's already a system-wide keyboard shortcut.",
// TODO: Add button to offer to open the relevant system preference pane for the user. // TODO: Add button to offer to open the relevant system preference pane for the user.
informativeText: "Most system-wide keyboard shortcuts can be changed in “System Preferences Keyboard Shortcuts“." informativeText: "Most system-wide keyboard shortcuts can be changed in “System Preferences Keyboard Shortcuts“."
) )
self.focus()
return nil return nil
} }
self.stringValue = "\(shortcut)" self.stringValue = "\(shortcut)"
self.showsCancelButton = true
userDefaultsSet(name: self.shortcutName, shortcut: shortcut) userDefaultsSet(name: self.shortcutName, shortcut: shortcut)
self.blur() self.blur()
@ -161,6 +214,10 @@ extension KeyboardShortcuts {
return shouldBecomeFirstResponder return shouldBecomeFirstResponder
} }
private func focus() {
window?.makeFirstResponder(self)
}
private func blur() { private func blur() {
window?.makeFirstResponder(nil) window?.makeFirstResponder(nil)
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 82 KiB