feat: add Command-number launch hints and Finder icon
This commit is contained in:
@@ -6,7 +6,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
private let search = ActionsSearchField()
|
||||
private let entries = [
|
||||
ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Show in Finder", symbol: "folder", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Show in Finder", symbol: "finder", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Reset ranking", symbol: "arrow.counterclockwise", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Quit application", symbol: "stop.circle", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Uninstall application", symbol: "trash", color: .systemRed)
|
||||
@@ -269,6 +269,7 @@ private struct ActionEntry {
|
||||
button.setAccessibilityLabel(title)
|
||||
let icon = ActionIconView()
|
||||
icon.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)
|
||||
?? (symbol == "finder" ? NSWorkspace.shared.icon(forFile: "/System/Library/CoreServices/Finder.app") : nil)
|
||||
icon.contentTintColor = color
|
||||
icon.imageScaling = .scaleProportionallyDown
|
||||
icon.setAccessibilityElement(false)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -31,6 +31,11 @@ final class ShortcutIndicator: NSStackView {
|
||||
}
|
||||
}
|
||||
|
||||
func setSingleKey(_ key: String) {
|
||||
precondition(arrangedSubviews.count == 1)
|
||||
(arrangedSubviews[0] as? NSTextField)?.stringValue = key
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates shortcut indicators programmatically") }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user