feat: add calculator and unit conversion
This commit is contained in:
@@ -7,10 +7,12 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
private let tableView = ApplicationTableView()
|
||||
private let statusLabel = NSTextField(labelWithString: "Finding applications…")
|
||||
private let emptyLabel = NSTextField(labelWithString: "Finding applications…")
|
||||
private let sectionLabel = NSTextField(labelWithString: "Applications")
|
||||
private let openButton = NSButton(title: "Open Application", target: nil, action: nil)
|
||||
private let history: LaunchHistory
|
||||
private var applications: [Application] = []
|
||||
private var results: [Application] = []
|
||||
private var calculation: CalculatorResult?
|
||||
private var locationLabels: [URL: String] = [:]
|
||||
private var discoveryTask: Task<Void, Never>?
|
||||
private var isLaunching = false
|
||||
@@ -179,7 +181,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
let searchFont = NSFont.systemFont(ofSize: 20)
|
||||
searchField.font = searchFont
|
||||
searchField.placeholderAttributedString = NSAttributedString(
|
||||
string: "Search for apps…",
|
||||
string: "Search apps, calculate, or convert units…",
|
||||
attributes: [.foregroundColor: SunsetPalette.secondaryText, .font: searchFont]
|
||||
)
|
||||
searchField.textColor = SunsetPalette.text
|
||||
@@ -193,7 +195,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
searchField.sendsSearchStringImmediately = true
|
||||
searchField.sendsWholeSearchString = false
|
||||
searchField.delegate = self
|
||||
searchField.setAccessibilityLabel("Search applications")
|
||||
searchField.setAccessibilityLabel("Search applications, calculate, or convert units")
|
||||
|
||||
let symbol = NSImageView()
|
||||
symbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
|
||||
@@ -204,7 +206,6 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
header.alignment = .centerY
|
||||
header.spacing = 12
|
||||
|
||||
let sectionLabel = NSTextField(labelWithString: "Applications")
|
||||
sectionLabel.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||
sectionLabel.textColor = SunsetPalette.secondaryText
|
||||
|
||||
@@ -372,19 +373,26 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
private func updateResults(preservingSelection: Bool = false) {
|
||||
let selectedURL = preservingSelection && results.indices.contains(tableView.selectedRow)
|
||||
? results[tableView.selectedRow].id : nil
|
||||
if searchField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
calculation = Calculator.evaluate(searchField.stringValue)
|
||||
if calculation != nil {
|
||||
results = []
|
||||
} else if searchField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
results = history.suggestions(in: applications)
|
||||
} else {
|
||||
results = history.results(for: searchField.stringValue, in: applications)
|
||||
}
|
||||
sectionLabel.stringValue = calculation == nil ? "Applications" : "Calculator"
|
||||
tableView.setAccessibilityLabel(calculation == nil ? "Applications" : "Calculator result")
|
||||
tableView.reloadData()
|
||||
if !results.isEmpty {
|
||||
if resultCount > 0 {
|
||||
let row = selectedURL.flatMap { url in results.firstIndex { $0.id == url } } ?? 0
|
||||
tableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
|
||||
tableView.scrollRowToVisible(row)
|
||||
}
|
||||
emptyLabel.isHidden = !results.isEmpty
|
||||
if discoveryTask != nil {
|
||||
emptyLabel.isHidden = resultCount > 0
|
||||
if let calculation {
|
||||
statusLabel.stringValue = calculation.detail
|
||||
} else if discoveryTask != nil {
|
||||
emptyLabel.stringValue = "Finding applications…"
|
||||
statusLabel.stringValue = "Finding applications…"
|
||||
} else {
|
||||
@@ -414,16 +422,24 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
return true
|
||||
}
|
||||
|
||||
private var resultCount: Int { calculation == nil ? results.count : 1 }
|
||||
|
||||
private func moveSelection(by offset: Int) {
|
||||
guard !results.isEmpty else { return }
|
||||
let row = min(max(tableView.selectedRow + offset, 0), results.count - 1)
|
||||
guard resultCount > 0 else { return }
|
||||
let row = min(max(tableView.selectedRow + offset, 0), resultCount - 1)
|
||||
tableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
|
||||
tableView.scrollRowToVisible(row)
|
||||
}
|
||||
|
||||
private func updateOpenButton() {
|
||||
openButton.isEnabled = !isLaunching && launcherFrameBeforeReview == nil && results.indices.contains(tableView.selectedRow)
|
||||
actionsButton.isEnabled = openButton.isEnabled
|
||||
let canAct = !isLaunching && launcherFrameBeforeReview == nil && tableView.selectedRow >= 0
|
||||
openButton.title = calculation == nil ? "Open Application" : "Copy Answer"
|
||||
openButton.toolTip = calculation == nil
|
||||
? "Open the selected application (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))
|
||||
actionsButton.isEnabled = canAct && calculation == nil && results.indices.contains(tableView.selectedRow)
|
||||
}
|
||||
|
||||
func tableViewSelectionDidChange(_ notification: Notification) {
|
||||
@@ -431,20 +447,32 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
|
||||
func numberOfRows(in tableView: NSTableView) -> Int {
|
||||
results.count
|
||||
resultCount
|
||||
}
|
||||
|
||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||
let identifier = NSUserInterfaceItemIdentifier("applicationCell")
|
||||
let cell = (tableView.makeView(withIdentifier: identifier, owner: self) as? ApplicationCell)
|
||||
?? makeCell(identifier: identifier)
|
||||
let application = results[row]
|
||||
cell.textField?.stringValue = application.name
|
||||
let location = locationLabels[application.id]
|
||||
cell.locationLabel.stringValue = location ?? ""
|
||||
cell.locationLabel.isHidden = location == nil
|
||||
cell.imageView?.image = NSWorkspace.shared.icon(forFile: application.url.path)
|
||||
cell.toolTip = application.url.path
|
||||
if let calculation {
|
||||
cell.textField?.stringValue = calculation.displayValue
|
||||
cell.locationLabel.isHidden = true
|
||||
cell.kindLabel.stringValue = calculation.copyValue == nil ? "Error" : "Calculator"
|
||||
cell.imageView?.image = NSImage(systemSymbolName: "equal", accessibilityDescription: nil)?
|
||||
.withSymbolConfiguration(.init(pointSize: LauncherRowView.iconSize, weight: .regular))
|
||||
cell.imageView?.contentTintColor = SunsetPalette.accent
|
||||
cell.toolTip = calculation.detail
|
||||
} else {
|
||||
let application = results[row]
|
||||
cell.textField?.stringValue = application.name
|
||||
let location = locationLabels[application.id]
|
||||
cell.locationLabel.stringValue = location ?? ""
|
||||
cell.locationLabel.isHidden = location == nil
|
||||
cell.kindLabel.stringValue = "Application"
|
||||
cell.imageView?.contentTintColor = nil
|
||||
cell.imageView?.image = NSWorkspace.shared.icon(forFile: application.url.path)
|
||||
cell.toolTip = application.url.path
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
@@ -476,7 +504,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
title.orientation = .horizontal
|
||||
title.alignment = .centerY
|
||||
title.spacing = 8
|
||||
let kind = NSTextField(labelWithString: "Application")
|
||||
let kind = cell.kindLabel
|
||||
kind.font = .systemFont(ofSize: 14)
|
||||
kind.textColor = SunsetPalette.secondaryText
|
||||
kind.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
@@ -582,6 +610,16 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
|
||||
@objc private func openSelection() {
|
||||
guard !isLaunching, launcherFrameBeforeReview == nil, window?.attachedSheet == nil else { return }
|
||||
if let calculation {
|
||||
guard tableView.selectedRow == 0, let value = calculation.copyValue else { return }
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
if pasteboard.setString(value, forType: .string) {
|
||||
dismiss()
|
||||
}
|
||||
return
|
||||
}
|
||||
guard results.indices.contains(tableView.selectedRow) else { return }
|
||||
openApplication(results[tableView.selectedRow])
|
||||
}
|
||||
@@ -622,6 +660,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
@MainActor
|
||||
private final class ApplicationCell: NSTableCellView {
|
||||
let locationLabel = NSTextField(labelWithString: "")
|
||||
let kindLabel = NSTextField(labelWithString: "Application")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
Reference in New Issue
Block a user