feat: add Conjure Translate command
This commit is contained in:
@@ -31,6 +31,11 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
private var uninstallReview: UninstallReviewController?
|
||||
private var launcherViews: [NSView] = []
|
||||
private var launcherFrameBeforeReview: NSRect?
|
||||
private var translation: TranslationController?
|
||||
private var showsTranslationCommand = true
|
||||
private var translationCommandRow: Int? {
|
||||
calculation == nil && showsTranslationCommand ? results.count : nil
|
||||
}
|
||||
|
||||
init(history: LaunchHistory = LaunchHistory(), uninstaller: ApplicationUninstaller = ApplicationUninstaller()) {
|
||||
self.history = history
|
||||
@@ -63,13 +68,14 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
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
|
||||
return !self.isLaunching && self.calculation == nil && self.resultCount > 0
|
||||
&& self.launcherFrameBeforeReview == nil && !self.actions.isShown && self.translation == nil
|
||||
}
|
||||
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])
|
||||
guard let self else { return }
|
||||
if index == self.translationCommandRow { self.showTranslation() }
|
||||
else if self.results.indices.contains(index) { self.openApplication(self.results[index]) }
|
||||
}
|
||||
window.dragEnded = { [weak self] in
|
||||
guard let self else { return }
|
||||
@@ -95,7 +101,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
|
||||
func present() {
|
||||
(window as? LauncherPanel)?.beginNumberHintSession()
|
||||
if launcherFrameBeforeReview != nil {
|
||||
if launcherFrameBeforeReview != nil || translation != nil {
|
||||
showWindow(nil)
|
||||
NSApp.activate()
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
@@ -131,6 +137,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
func dismiss(restoringFocus: Bool = true) {
|
||||
guard !restoringFocus || (!isLaunching && window?.attachedSheet == nil) else { return }
|
||||
if let review = uninstallReview { review.goBack(); return }
|
||||
if let translation {
|
||||
translation.goBack()
|
||||
if restoringFocus { return }
|
||||
}
|
||||
guard launcherFrameBeforeReview == nil else { return }
|
||||
actions.close()
|
||||
alignmentGuides?.end()
|
||||
@@ -417,6 +427,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
|
||||
private func updateResults(preservingSelection: Bool = false, allowingCurrencyFetch: Bool = true) {
|
||||
guard translation == nil else { return }
|
||||
let selectedURL = preservingSelection && results.indices.contains(tableView.selectedRow)
|
||||
? results[tableView.selectedRow].id : nil
|
||||
let query = searchField.stringValue
|
||||
@@ -447,6 +458,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
detail: "\(answer.detail) · \(cacheStatus)"
|
||||
)
|
||||
}
|
||||
showsTranslationCommand = query.split(whereSeparator: \.isWhitespace).allSatisfy {
|
||||
"Conjure Translate".localizedStandardContains(String($0))
|
||||
}
|
||||
if calculation != nil { (window as? LauncherPanel)?.resetNumberHints() }
|
||||
if calculation != nil {
|
||||
results = []
|
||||
@@ -456,7 +470,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
results = history.results(for: searchField.stringValue, in: applications)
|
||||
}
|
||||
sectionLabel.stringValue = isCurrencyCalculation ? "Daily currency rates"
|
||||
: calculation == nil ? "Applications" : "Calculator"
|
||||
: calculation == nil ? "Applications & Commands" : "Calculator"
|
||||
tableView.setAccessibilityLabel(isCurrencyCalculation ? "Daily currency conversion result"
|
||||
: calculation == nil ? "Applications" : "Calculator result")
|
||||
tableView.reloadData()
|
||||
@@ -473,17 +487,23 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
statusLabel.stringValue = "Finding applications…"
|
||||
} else {
|
||||
emptyLabel.stringValue = applications.isEmpty ? "No applications found" : "No matching applications"
|
||||
statusLabel.stringValue = results.count == 1 ? "1 application" : "\(results.count) applications"
|
||||
statusLabel.stringValue = "\(results.count) applications" + (showsTranslationCommand ? " · 1 command" : "")
|
||||
}
|
||||
updateOpenButton()
|
||||
(window as? LauncherPanel)?.sampleNumberHints()
|
||||
}
|
||||
|
||||
func controlTextDidChange(_ notification: Notification) {
|
||||
if let translation { translation.inputChanged(); return }
|
||||
updateResults()
|
||||
}
|
||||
|
||||
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
||||
if let translation {
|
||||
if commandSelector == #selector(NSResponder.insertNewline(_:)) { translation.translate(); return true }
|
||||
if commandSelector == #selector(NSResponder.cancelOperation(_:)) { translation.goBack(); return true }
|
||||
return false
|
||||
}
|
||||
switch commandSelector {
|
||||
case #selector(NSResponder.moveDown(_:)):
|
||||
moveSelection(by: 1)
|
||||
@@ -499,9 +519,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
return true
|
||||
}
|
||||
|
||||
private var resultCount: Int { calculation == nil ? results.count : 1 }
|
||||
private var resultCount: Int { calculation == nil ? results.count + (showsTranslationCommand ? 1 : 0) : 1 }
|
||||
|
||||
private func moveSelection(by offset: Int) {
|
||||
guard translation == nil else { return }
|
||||
guard resultCount > 0 else { return }
|
||||
let row = min(max(tableView.selectedRow + offset, 0), resultCount - 1)
|
||||
tableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
|
||||
@@ -509,13 +530,13 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
|
||||
private func updateOpenButton() {
|
||||
let canAct = !isLaunching && launcherFrameBeforeReview == nil && tableView.selectedRow >= 0
|
||||
openButton.title = calculation == nil ? "Open Application" : "Copy Answer"
|
||||
let canAct = !isLaunching && launcherFrameBeforeReview == nil && translation == nil && tableView.selectedRow >= 0
|
||||
openButton.title = calculation == nil ? (tableView.selectedRow == translationCommandRow ? "Open Command" : "Open Application") : "Copy Answer"
|
||||
openButton.toolTip = calculation == nil
|
||||
? "Open the selected application (Return). Use ↑ and ↓ to select."
|
||||
? "Open the selected entry (Return). Use ↑ and ↓ to select."
|
||||
: "Copy the answer to the clipboard (Return)."
|
||||
openButton.isEnabled = canAct && (calculation.map { $0.copyValue != nil }
|
||||
?? results.indices.contains(tableView.selectedRow))
|
||||
?? (results.indices.contains(tableView.selectedRow) || tableView.selectedRow == translationCommandRow))
|
||||
actionsButton.isEnabled = canAct && calculation == nil && results.indices.contains(tableView.selectedRow)
|
||||
}
|
||||
|
||||
@@ -553,6 +574,14 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
.withSymbolConfiguration(.init(pointSize: LauncherRowView.iconSize, weight: .regular))
|
||||
cell.imageView?.contentTintColor = SunsetPalette.accent
|
||||
cell.toolTip = calculation.detail
|
||||
} else if row == translationCommandRow {
|
||||
cell.textField?.stringValue = "Conjure Translate"
|
||||
cell.textField?.setAccessibilityLabel(nil)
|
||||
cell.locationLabel.isHidden = true
|
||||
cell.kindLabel.stringValue = "Command"
|
||||
cell.imageView?.image = NSImage(systemSymbolName: "character.bubble", accessibilityDescription: nil)
|
||||
cell.imageView?.contentTintColor = SunsetPalette.accent
|
||||
cell.toolTip = "Translate English to Japanese"
|
||||
} else {
|
||||
let application = results[row]
|
||||
cell.textField?.stringValue = application.name
|
||||
@@ -582,9 +611,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
|
||||
private func configureNumberHint(_ cell: ApplicationCell, row: Int, animated: Bool = false) {
|
||||
let visible = (window as? LauncherPanel)?.numberHintsVisible == true
|
||||
&& calculation == nil && row < 9 && results.indices.contains(row)
|
||||
&& calculation == nil && row < 9 && row < resultCount && translation == nil
|
||||
cell.setNumberHint(visible ? String(row + 1) : nil, animated: animated)
|
||||
cell.setAccessibilityHelp(visible ? "Command-\(row + 1) opens this application" : nil)
|
||||
cell.setAccessibilityHelp(visible ? "Command-\(row + 1) opens this entry" : nil)
|
||||
}
|
||||
|
||||
private func updateNumberHints() {
|
||||
@@ -605,6 +634,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
|
||||
@objc private func showActions() {
|
||||
guard !isLaunching, launcherFrameBeforeReview == nil, window?.attachedSheet == nil, results.indices.contains(tableView.selectedRow) else { return }
|
||||
guard translation == nil else { return }
|
||||
(window as? LauncherPanel)?.resetNumberHints()
|
||||
let application = results[tableView.selectedRow]
|
||||
actions.open = { [weak self] in self?.openApplication(application) }
|
||||
@@ -692,7 +722,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
|
||||
@objc private func openSelection() {
|
||||
guard !isLaunching, launcherFrameBeforeReview == nil, window?.attachedSheet == nil else { return }
|
||||
guard !isLaunching, translation == nil, launcherFrameBeforeReview == nil, window?.attachedSheet == nil else { return }
|
||||
if tableView.selectedRow == translationCommandRow { showTranslation(); return }
|
||||
if let calculation {
|
||||
guard tableView.selectedRow == 0, let value = calculation.copyValue else { return }
|
||||
let pasteboard = NSPasteboard.general
|
||||
@@ -738,6 +769,54 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
updateOpenButton()
|
||||
}
|
||||
}
|
||||
|
||||
private func showTranslation() {
|
||||
guard translation == nil, let content = window?.contentView else { return }
|
||||
let controller = TranslationController()
|
||||
translation = controller
|
||||
controller.input = { [weak self] in self?.searchField.stringValue ?? "" }
|
||||
let previousQuery = searchField.stringValue
|
||||
controller.closed = { [weak self, weak controller] in
|
||||
guard let self, let controller else { return }
|
||||
controller.view.removeFromSuperview()
|
||||
self.translation = nil
|
||||
for view in self.launcherViews.dropFirst() {
|
||||
view.isHidden = false
|
||||
view.setAccessibilityHidden(false)
|
||||
view.alphaValue = 1
|
||||
}
|
||||
self.searchField.placeholderString = "Search apps, calculate, or convert…"
|
||||
self.searchField.setAccessibilityLabel("Search applications, calculate, or convert units and currencies")
|
||||
self.searchField.stringValue = previousQuery
|
||||
self.openButton.keyEquivalent = "\r"
|
||||
self.updateResults()
|
||||
self.window?.makeFirstResponder(self.searchField)
|
||||
}
|
||||
actions.close()
|
||||
(window as? LauncherPanel)?.resetNumberHints()
|
||||
openButton.keyEquivalent = ""
|
||||
searchField.stringValue = ""
|
||||
searchField.placeholderString = "Enter English text to translate…"
|
||||
searchField.setAccessibilityLabel("English text to translate into Japanese")
|
||||
for view in launcherViews.dropFirst() {
|
||||
view.isHidden = true
|
||||
view.setAccessibilityHidden(true)
|
||||
}
|
||||
controller.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
controller.view.alphaValue = 0
|
||||
content.addSubview(controller.view)
|
||||
NSLayoutConstraint.activate([
|
||||
controller.view.leadingAnchor.constraint(equalTo: content.leadingAnchor),
|
||||
controller.view.trailingAnchor.constraint(equalTo: content.trailingAnchor),
|
||||
controller.view.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 16),
|
||||
controller.view.bottomAnchor.constraint(equalTo: content.bottomAnchor),
|
||||
])
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? 0 : 0.18
|
||||
controller.view.animator().alphaValue = 1
|
||||
}
|
||||
window?.makeFirstResponder(searchField)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
Reference in New Issue
Block a user