feat: add Open as the default application action
This commit is contained in:
@@ -4,8 +4,14 @@ import ConjureKit
|
||||
@MainActor
|
||||
final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate {
|
||||
private let search = ActionsSearchField()
|
||||
private let action = NSButton(title: "Uninstall application", target: nil, action: nil)
|
||||
private let row = LauncherRowView()
|
||||
private let entries = [
|
||||
ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Uninstall application", symbol: "trash", color: .systemRed)
|
||||
]
|
||||
private var visibleEntries = [0, 1]
|
||||
private var selectedEntry: Int? = 0
|
||||
private var rowTopConstraints: [NSLayoutConstraint] = []
|
||||
private var heightConstraint: NSLayoutConstraint?
|
||||
private let empty = NSTextField(labelWithString: "No matching actions")
|
||||
private let applicationLabel = NSTextField(labelWithString: "")
|
||||
private var mouseMonitor: Any?
|
||||
@@ -14,6 +20,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
private var isClosing = false
|
||||
private var transitionGeneration = 0
|
||||
private var panel: ActionsPanelView { view as! ActionsPanelView }
|
||||
var open: (() -> Void)?
|
||||
var uninstall: (() -> Void)?
|
||||
var closed: (() -> Void)?
|
||||
var isShown: Bool { viewIfLoaded?.superview != nil && !isClosing }
|
||||
@@ -40,31 +47,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
applicationLabel.textColor = SunsetPalette.secondaryText
|
||||
applicationLabel.lineBreakMode = .byTruncatingTail
|
||||
applicationLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
row.isSelected = true
|
||||
row.addGestureRecognizer(NSClickGestureRecognizer(target: self, action: #selector(chooseAction)))
|
||||
action.isBordered = false
|
||||
action.alignment = .left
|
||||
action.font = .systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
|
||||
action.contentTintColor = .systemRed
|
||||
action.attributedTitle = NSAttributedString(string: action.title, attributes: [
|
||||
.foregroundColor: NSColor.systemRed, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
|
||||
])
|
||||
let icon = ActionIconView()
|
||||
icon.image = NSImage(systemSymbolName: "trash", accessibilityDescription: nil)
|
||||
icon.contentTintColor = .systemRed
|
||||
icon.imageScaling = .scaleProportionallyDown
|
||||
icon.setAccessibilityElement(false)
|
||||
action.target = self
|
||||
action.action = #selector(chooseAction)
|
||||
action.setAccessibilityLabel("Uninstall application")
|
||||
let returnKey = NSTextField(labelWithString: "↵")
|
||||
returnKey.font = .systemFont(ofSize: 14)
|
||||
returnKey.textColor = SunsetPalette.secondaryText
|
||||
returnKey.alignment = .center
|
||||
returnKey.wantsLayer = true
|
||||
returnKey.layer?.cornerRadius = 5
|
||||
returnKey.layer?.borderWidth = 1
|
||||
returnKey.setAccessibilityElement(false)
|
||||
search.font = .systemFont(ofSize: LauncherRowView.textSize)
|
||||
search.textColor = SunsetPalette.text
|
||||
search.placeholderAttributedString = NSAttributedString(string: "Search for actions…", attributes: [
|
||||
@@ -86,17 +68,30 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
empty.isHidden = true
|
||||
content.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(content)
|
||||
for child in [background, applicationLabel, row, empty, search] {
|
||||
for child in [background, applicationLabel, empty, search] {
|
||||
child.translatesAutoresizingMaskIntoConstraints = false
|
||||
if child !== background { content.addSubview(child) }
|
||||
}
|
||||
for child in [icon, action, returnKey] {
|
||||
child.translatesAutoresizingMaskIntoConstraints = false
|
||||
row.addSubview(child)
|
||||
for (index, entry) in entries.enumerated() {
|
||||
entry.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
content.addSubview(entry.view)
|
||||
entry.button.tag = index
|
||||
entry.button.target = self
|
||||
entry.button.action = #selector(chooseButton(_:))
|
||||
entry.view.addGestureRecognizer(NSClickGestureRecognizer(target: self, action: #selector(chooseRow(_:))))
|
||||
let top = entry.view.topAnchor.constraint(equalTo: applicationLabel.bottomAnchor,
|
||||
constant: 10 + CGFloat(index) * LauncherRowView.rowHeight)
|
||||
rowTopConstraints.append(top)
|
||||
NSLayoutConstraint.activate([
|
||||
entry.view.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 8),
|
||||
entry.view.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8),
|
||||
top
|
||||
])
|
||||
}
|
||||
let height = view.heightAnchor.constraint(equalToConstant: 116 + LauncherRowView.rowHeight)
|
||||
heightConstraint = height
|
||||
NSLayoutConstraint.activate([
|
||||
view.widthAnchor.constraint(equalToConstant: 360),
|
||||
view.heightAnchor.constraint(equalToConstant: 116),
|
||||
view.widthAnchor.constraint(equalToConstant: 360), height,
|
||||
content.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
content.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
content.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
@@ -108,36 +103,22 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
applicationLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
|
||||
applicationLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
|
||||
applicationLabel.topAnchor.constraint(equalTo: content.topAnchor, constant: 14),
|
||||
row.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 8),
|
||||
row.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8),
|
||||
row.topAnchor.constraint(equalTo: applicationLabel.bottomAnchor, constant: 10),
|
||||
row.heightAnchor.constraint(equalToConstant: LauncherRowView.rowHeight),
|
||||
icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 8),
|
||||
icon.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
||||
icon.widthAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
|
||||
icon.heightAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
|
||||
action.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: LauncherRowView.iconToTextSpacing),
|
||||
action.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
||||
action.trailingAnchor.constraint(equalTo: returnKey.leadingAnchor, constant: -12),
|
||||
returnKey.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -12),
|
||||
returnKey.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
||||
returnKey.widthAnchor.constraint(equalToConstant: 18),
|
||||
returnKey.heightAnchor.constraint(equalToConstant: 18),
|
||||
empty.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
|
||||
empty.centerYAnchor.constraint(equalTo: row.centerYAnchor),
|
||||
empty.centerYAnchor.constraint(equalTo: entries[0].view.centerYAnchor),
|
||||
search.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
|
||||
search.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
|
||||
search.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -8),
|
||||
search.heightAnchor.constraint(equalToConstant: 24)
|
||||
])
|
||||
content.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor
|
||||
returnKey.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor
|
||||
updateSelection()
|
||||
}
|
||||
|
||||
func show(for application: Application, relativeTo anchor: NSView) {
|
||||
if isShown { close(); return }
|
||||
guard let window = anchor.window, let host = window.contentView else { return }
|
||||
let pill = anchor.superview ?? anchor
|
||||
_ = view
|
||||
applicationLabel.stringValue = application.name
|
||||
search.stringValue = ""
|
||||
controlTextDidChange(Notification(name: NSControl.textDidChangeNotification))
|
||||
@@ -204,26 +185,106 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
|
||||
func controlTextDidChange(_ notification: Notification) {
|
||||
let query = search.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let matches = query.isEmpty || action.title.range(of: query, options: [.caseInsensitive, .diacriticInsensitive]) != nil
|
||||
row.isHidden = !matches
|
||||
empty.isHidden = matches
|
||||
visibleEntries = entries.indices.filter { query.isEmpty || entries[$0].button.title.range(of: query, options: [.caseInsensitive, .diacriticInsensitive]) != nil }
|
||||
selectedEntry = visibleEntries.first
|
||||
for index in entries.indices {
|
||||
let position = visibleEntries.firstIndex(of: index)
|
||||
entries[index].view.isHidden = position == nil
|
||||
rowTopConstraints[index].constant = 10 + CGFloat(position ?? 0) * LauncherRowView.rowHeight
|
||||
}
|
||||
heightConstraint?.constant = 116 + CGFloat(max(visibleEntries.count, 1) - 1) * LauncherRowView.rowHeight
|
||||
empty.isHidden = !visibleEntries.isEmpty
|
||||
updateSelection()
|
||||
}
|
||||
|
||||
func moveSelection(by offset: Int) {
|
||||
guard !visibleEntries.isEmpty else { return }
|
||||
let current = selectedEntry.flatMap { visibleEntries.firstIndex(of: $0) } ?? 0
|
||||
selectedEntry = visibleEntries[min(max(current + offset, 0), visibleEntries.count - 1)]
|
||||
updateSelection()
|
||||
}
|
||||
|
||||
private func updateSelection() {
|
||||
for index in entries.indices {
|
||||
let selected = selectedEntry == index
|
||||
entries[index].view.isSelected = selected
|
||||
entries[index].returnKey.isHidden = !selected
|
||||
entries[index].button.setAccessibilityValue(selected ? "Selected" : "Not selected")
|
||||
}
|
||||
}
|
||||
|
||||
func control(_ control: NSControl, textView: NSTextView, doCommandBy selector: Selector) -> Bool {
|
||||
switch selector {
|
||||
case #selector(NSResponder.insertNewline(_:)):
|
||||
if !row.isHidden { chooseAction() }
|
||||
if let selectedEntry { chooseAction(at: selectedEntry) }
|
||||
case #selector(NSResponder.cancelOperation(_:)): close()
|
||||
case #selector(NSResponder.moveUp(_:)), #selector(NSResponder.moveDown(_:)): return true
|
||||
case #selector(NSResponder.moveUp(_:)): moveSelection(by: -1)
|
||||
case #selector(NSResponder.moveDown(_:)): moveSelection(by: 1)
|
||||
default: return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@objc private func chooseAction() {
|
||||
guard isShown else { return }
|
||||
@objc private func chooseButton(_ sender: NSButton) { chooseAction(at: sender.tag) }
|
||||
|
||||
@objc private func chooseRow(_ sender: NSGestureRecognizer) {
|
||||
if let index = entries.firstIndex(where: { $0.view === sender.view }) { chooseAction(at: index) }
|
||||
}
|
||||
|
||||
private func chooseAction(at index: Int) {
|
||||
guard isShown, visibleEntries.contains(index) else { return }
|
||||
close()
|
||||
uninstall?()
|
||||
if index == 0 { open?() } else { uninstall?() }
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private struct ActionEntry {
|
||||
let view = LauncherRowView()
|
||||
let button: NSButton
|
||||
let returnKey = NSTextField(labelWithString: "↵")
|
||||
|
||||
init(title: String, symbol: String, color: NSColor) {
|
||||
button = NSButton(title: title, target: nil, action: nil)
|
||||
button.isBordered = false
|
||||
button.alignment = .left
|
||||
button.font = .systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
|
||||
button.contentTintColor = color
|
||||
button.attributedTitle = NSAttributedString(string: title, attributes: [
|
||||
.foregroundColor: color, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
|
||||
])
|
||||
button.setAccessibilityLabel(title)
|
||||
let icon = ActionIconView()
|
||||
icon.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)
|
||||
icon.contentTintColor = color
|
||||
icon.imageScaling = .scaleProportionallyDown
|
||||
icon.setAccessibilityElement(false)
|
||||
returnKey.font = .systemFont(ofSize: 14)
|
||||
returnKey.textColor = SunsetPalette.secondaryText
|
||||
returnKey.alignment = .center
|
||||
returnKey.wantsLayer = true
|
||||
returnKey.layer?.cornerRadius = 5
|
||||
returnKey.layer?.borderWidth = 1
|
||||
returnKey.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor
|
||||
returnKey.setAccessibilityElement(false)
|
||||
for child in [icon, button, returnKey] {
|
||||
child.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(child)
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
view.heightAnchor.constraint(equalToConstant: LauncherRowView.rowHeight),
|
||||
icon.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
|
||||
icon.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
icon.widthAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
|
||||
icon.heightAnchor.constraint(equalToConstant: LauncherRowView.iconSize),
|
||||
button.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: LauncherRowView.iconToTextSpacing),
|
||||
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
button.trailingAnchor.constraint(equalTo: returnKey.leadingAnchor, constant: -12),
|
||||
returnKey.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),
|
||||
returnKey.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
returnKey.widthAnchor.constraint(equalToConstant: 18),
|
||||
returnKey.heightAnchor.constraint(equalToConstant: 18)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user