122 lines
5.0 KiB
Swift
122 lines
5.0 KiB
Swift
import AppKit
|
|
import ConjureKit
|
|
|
|
@MainActor
|
|
final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate, NSPopoverDelegate {
|
|
private let popover = NSPopover()
|
|
private let search = ActionsSearchField()
|
|
private let action = NSButton(title: "Uninstall application", target: nil, action: nil)
|
|
private let empty = NSTextField(labelWithString: "No matching actions")
|
|
private let applicationLabel = NSTextField(labelWithString: "")
|
|
var uninstall: (() -> Void)?
|
|
var closed: (() -> Void)?
|
|
var isShown: Bool { popover.isShown }
|
|
|
|
init() {
|
|
super.init(nibName: nil, bundle: nil)
|
|
popover.contentViewController = self
|
|
popover.behavior = .semitransient
|
|
popover.delegate = self
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("Conjure creates actions programmatically") }
|
|
|
|
override func loadView() {
|
|
let content = NSVisualEffectView()
|
|
content.material = .popover
|
|
content.state = .active
|
|
content.appearance = NSAppearance(named: .darkAqua)
|
|
view = content
|
|
preferredContentSize = NSSize(width: 340, height: 150)
|
|
applicationLabel.textColor = SunsetPalette.secondaryText
|
|
applicationLabel.lineBreakMode = .byTruncatingTail
|
|
applicationLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
|
action.isBordered = false
|
|
action.alignment = .left
|
|
action.font = .systemFont(ofSize: 16, weight: .medium)
|
|
action.contentTintColor = .systemRed
|
|
action.attributedTitle = NSAttributedString(string: action.title, attributes: [
|
|
.foregroundColor: NSColor.systemRed, .font: NSFont.systemFont(ofSize: 16, weight: .medium)
|
|
])
|
|
action.image = NSImage(systemSymbolName: "trash", accessibilityDescription: nil)
|
|
action.imagePosition = .imageLeading
|
|
action.target = self
|
|
action.action = #selector(chooseAction)
|
|
action.setAccessibilityLabel("Uninstall application")
|
|
search.placeholderString = "Search actions…"
|
|
search.delegate = self
|
|
search.sendsSearchStringImmediately = true
|
|
search.setAccessibilityLabel("Search application actions")
|
|
search.closeActions = { [weak self] in self?.close() }
|
|
empty.textColor = SunsetPalette.secondaryText
|
|
empty.isHidden = true
|
|
let stack = NSStackView(views: [applicationLabel, action, empty, search])
|
|
stack.orientation = .vertical
|
|
stack.alignment = .leading
|
|
stack.spacing = 12
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
content.addSubview(stack)
|
|
NSLayoutConstraint.activate([
|
|
stack.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
|
|
stack.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
|
|
stack.topAnchor.constraint(equalTo: content.topAnchor, constant: 14),
|
|
search.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
action.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
applicationLabel.widthAnchor.constraint(equalTo: stack.widthAnchor)
|
|
])
|
|
}
|
|
|
|
func show(for application: Application, relativeTo anchor: NSView) {
|
|
if isShown { close(); return }
|
|
_ = view
|
|
applicationLabel.stringValue = application.name
|
|
search.stringValue = ""
|
|
controlTextDidChange(Notification(name: NSControl.textDidChangeNotification))
|
|
popover.show(relativeTo: anchor.bounds, of: anchor, preferredEdge: .maxY)
|
|
view.window?.makeFirstResponder(search)
|
|
}
|
|
|
|
func close() { popover.performClose(nil) }
|
|
|
|
func popoverDidClose(_ notification: Notification) { closed?() }
|
|
|
|
func controlTextDidChange(_ notification: Notification) {
|
|
let query = search.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let matches = query.isEmpty || action.title.range(of: query, options: [.caseInsensitive, .diacriticInsensitive]) != nil
|
|
action.isHidden = !matches
|
|
empty.isHidden = matches
|
|
}
|
|
|
|
func control(_ control: NSControl, textView: NSTextView, doCommandBy selector: Selector) -> Bool {
|
|
switch selector {
|
|
case #selector(NSResponder.insertNewline(_:)):
|
|
if !action.isHidden { chooseAction() }
|
|
case #selector(NSResponder.cancelOperation(_:)):
|
|
close()
|
|
case #selector(NSResponder.moveUp(_:)), #selector(NSResponder.moveDown(_:)):
|
|
return true // There is currently one contextual action.
|
|
default: return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
@objc private func chooseAction() {
|
|
close()
|
|
uninstall?()
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class ActionsSearchField: NSSearchField {
|
|
var closeActions: (() -> Void)?
|
|
|
|
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
|
if event.modifierFlags.intersection([.command, .control, .option, .shift]) == .command,
|
|
event.charactersIgnoringModifiers?.lowercased() == "k" {
|
|
closeActions?()
|
|
return true
|
|
}
|
|
return super.performKeyEquivalent(with: event)
|
|
}
|
|
}
|