|
|
|
@@ -61,6 +61,16 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|
|
|
|
else { self.moveSelection(by: offset) }
|
|
|
|
|
}
|
|
|
|
|
window.dismiss = { [weak self] in self?.dismiss() }
|
|
|
|
|
window.canUseNumberShortcuts = { [weak self] in
|
|
|
|
|
guard let self else { return false }
|
|
|
|
|
return !self.isLaunching && self.calculation == nil && !self.results.isEmpty
|
|
|
|
|
&& self.launcherFrameBeforeReview == nil && !self.actions.isShown
|
|
|
|
|
}
|
|
|
|
|
window.numberHintsChanged = { [weak self] in self?.updateNumberHints() }
|
|
|
|
|
window.launchNumber = { [weak self] index in
|
|
|
|
|
guard let self, self.results.indices.contains(index) else { return }
|
|
|
|
|
self.openApplication(self.results[index])
|
|
|
|
|
}
|
|
|
|
|
window.dragEnded = { [weak self] in
|
|
|
|
|
guard let self else { return }
|
|
|
|
|
self.alignmentGuides?.end(snapping: self.window)
|
|
|
|
@@ -171,6 +181,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func windowDidResignKey(_ notification: Notification) {
|
|
|
|
|
(window as? LauncherPanel)?.resetNumberHints()
|
|
|
|
|
guard !isLaunching, launcherFrameBeforeReview == nil, window?.attachedSheet == nil else { return }
|
|
|
|
|
dismiss(restoringFocus: false)
|
|
|
|
|
}
|
|
|
|
@@ -433,6 +444,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|
|
|
|
detail: "\(answer.detail) · \(cacheStatus)"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if calculation != nil { (window as? LauncherPanel)?.resetNumberHints() }
|
|
|
|
|
if calculation != nil {
|
|
|
|
|
results = []
|
|
|
|
|
} else if searchField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
|
|
@@ -535,6 +547,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|
|
|
|
cell.imageView?.image = NSWorkspace.shared.icon(forFile: application.url.path)
|
|
|
|
|
cell.toolTip = application.url.path
|
|
|
|
|
}
|
|
|
|
|
configureNumberHint(cell, row: row)
|
|
|
|
|
return cell
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -546,6 +559,25 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|
|
|
|
return view
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func configureNumberHint(_ cell: ApplicationCell, row: Int) {
|
|
|
|
|
let visible = (window as? LauncherPanel)?.numberHintsVisible == true
|
|
|
|
|
&& calculation == nil && row < 9 && results.indices.contains(row)
|
|
|
|
|
cell.numberHint.isHidden = !visible
|
|
|
|
|
cell.kindLabel.isHidden = visible
|
|
|
|
|
if visible { cell.numberHint.setSingleKey(String(row + 1)) }
|
|
|
|
|
cell.setAccessibilityHelp(visible ? "Command-\(row + 1) opens this application" : nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func updateNumberHints() {
|
|
|
|
|
let rows = tableView.rows(in: tableView.visibleRect)
|
|
|
|
|
guard rows.length > 0 else { return }
|
|
|
|
|
for row in rows.location..<(rows.location + rows.length) {
|
|
|
|
|
if let cell = tableView.view(atColumn: 0, row: row, makeIfNecessary: false) as? ApplicationCell {
|
|
|
|
|
configureNumberHint(cell, row: row)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func makeCell(identifier: NSUserInterfaceItemIdentifier) -> ApplicationCell {
|
|
|
|
|
let cell = ApplicationCell()
|
|
|
|
|
cell.identifier = identifier
|
|
|
|
@@ -573,7 +605,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|
|
|
|
kind.setAccessibilityElement(false)
|
|
|
|
|
cell.imageView = icon
|
|
|
|
|
cell.textField = label
|
|
|
|
|
for view in [icon, title, kind] {
|
|
|
|
|
cell.numberHint.isHidden = true
|
|
|
|
|
for view in [icon, title, kind, cell.numberHint] {
|
|
|
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
cell.addSubview(view)
|
|
|
|
|
}
|
|
|
|
@@ -587,12 +620,15 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|
|
|
|
title.trailingAnchor.constraint(lessThanOrEqualTo: kind.leadingAnchor, constant: -12),
|
|
|
|
|
kind.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
|
|
|
|
|
kind.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -8),
|
|
|
|
|
cell.numberHint.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -8),
|
|
|
|
|
cell.numberHint.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
|
|
|
|
|
])
|
|
|
|
|
return cell
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc private func showActions() {
|
|
|
|
|
guard !isLaunching, launcherFrameBeforeReview == nil, window?.attachedSheet == nil, results.indices.contains(tableView.selectedRow) else { return }
|
|
|
|
|
(window as? LauncherPanel)?.resetNumberHints()
|
|
|
|
|
let application = results[tableView.selectedRow]
|
|
|
|
|
actions.open = { [weak self] in self?.openApplication(application) }
|
|
|
|
|
actions.showInFinder = { [weak self] in
|
|
|
|
@@ -635,6 +671,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|
|
|
|
review.view.bottomAnchor.constraint(equalTo: content.bottomAnchor)
|
|
|
|
|
])
|
|
|
|
|
for view in launcherViews { view.setAccessibilityHidden(true) }
|
|
|
|
|
(window as? LauncherPanel)?.resetNumberHints()
|
|
|
|
|
openButton.keyEquivalent = ""
|
|
|
|
|
updateOpenButton()
|
|
|
|
|
var frame = window.frame
|
|
|
|
@@ -699,6 +736,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|
|
|
|
|
|
|
|
|
private func openApplication(_ application: Application) {
|
|
|
|
|
guard !isLaunching else { return }
|
|
|
|
|
(window as? LauncherPanel)?.resetNumberHints()
|
|
|
|
|
isLaunching = true
|
|
|
|
|
updateOpenButton()
|
|
|
|
|
Task {
|
|
|
|
@@ -723,6 +761,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|
|
|
|
private final class ApplicationCell: NSTableCellView {
|
|
|
|
|
let locationLabel = NSTextField(labelWithString: "")
|
|
|
|
|
let kindLabel = NSTextField(labelWithString: "Application")
|
|
|
|
|
let numberHint = ShortcutIndicator("1")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
@@ -750,14 +789,73 @@ private final class ApplicationTableView: NSTableView {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
|
private final class LauncherPanel: NSPanel {
|
|
|
|
|
final class LauncherPanel: NSPanel {
|
|
|
|
|
override var canBecomeKey: Bool { true }
|
|
|
|
|
override var canBecomeMain: Bool { true }
|
|
|
|
|
var navigateSelection: ((Int) -> Void)?
|
|
|
|
|
var dismiss: (() -> Void)?
|
|
|
|
|
var dragEnded: (() -> Void)?
|
|
|
|
|
var canUseNumberShortcuts: (() -> Bool)?
|
|
|
|
|
var numberHintsChanged: (() -> Void)?
|
|
|
|
|
var launchNumber: ((Int) -> Void)?
|
|
|
|
|
private var numberShortcuts = CommandNumberShortcuts()
|
|
|
|
|
private var commandHoldTask: Task<Void, Never>?
|
|
|
|
|
var numberHintsVisible: Bool { numberShortcuts.isVisible }
|
|
|
|
|
|
|
|
|
|
private func commandOnly(_ event: NSEvent) -> Bool {
|
|
|
|
|
event.modifierFlags.intersection([.command, .control, .option, .shift, .function]) == .command
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func resetNumberHints() {
|
|
|
|
|
commandHoldTask?.cancel()
|
|
|
|
|
commandHoldTask = nil
|
|
|
|
|
let wasVisible = numberShortcuts.isVisible
|
|
|
|
|
numberShortcuts.reset()
|
|
|
|
|
if wasVisible { numberHintsChanged?() }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func finishCommandHold(at time: TimeInterval) {
|
|
|
|
|
guard isKeyWindow, isVisible, attachedSheet == nil, canUseNumberShortcuts?() == true else {
|
|
|
|
|
resetNumberHints()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if numberShortcuts.reveal(at: time) {
|
|
|
|
|
commandHoldTask?.cancel()
|
|
|
|
|
commandHoldTask = nil
|
|
|
|
|
numberHintsChanged?()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func resignKey() {
|
|
|
|
|
resetNumberHints()
|
|
|
|
|
super.resignKey()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func orderOut(_ sender: Any?) {
|
|
|
|
|
resetNumberHints()
|
|
|
|
|
super.orderOut(sender)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func sendEvent(_ event: NSEvent) {
|
|
|
|
|
let eligible = isKeyWindow && isVisible && attachedSheet == nil && canUseNumberShortcuts?() == true
|
|
|
|
|
if event.type == .flagsChanged {
|
|
|
|
|
if !commandOnly(event) || !eligible {
|
|
|
|
|
resetNumberHints()
|
|
|
|
|
} else if numberShortcuts.update(commandOnly: true, eligible: true, at: event.timestamp) {
|
|
|
|
|
commandHoldTask = Task { [weak self] in
|
|
|
|
|
do { try await Task.sleep(for: .seconds(1)) } catch { return }
|
|
|
|
|
self?.finishCommandHold(at: ProcessInfo.processInfo.systemUptime)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if event.type == .keyDown {
|
|
|
|
|
if eligible, commandOnly(event),
|
|
|
|
|
let index = numberShortcuts.index(for: event.charactersIgnoringModifiers ?? "") {
|
|
|
|
|
resetNumberHints()
|
|
|
|
|
if !event.isARepeat { launchNumber?(index) }
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
resetNumberHints()
|
|
|
|
|
}
|
|
|
|
|
if event.type == .keyDown, attachedSheet == nil,
|
|
|
|
|
event.modifierFlags.intersection([.control, .command, .option, .shift]) == .control {
|
|
|
|
|
switch event.charactersIgnoringModifiers?.lowercased() {
|
|
|
|
|