feat: add contextual actions and recoverable application uninstall
This commit is contained in:
@@ -17,9 +17,14 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
private var previousApplication: NSRunningApplication?
|
||||
private var alignmentGuides: LauncherAlignmentGuides?
|
||||
private static let positionKey = "launcherPosition"
|
||||
private let uninstaller: ApplicationUninstaller
|
||||
private let actionsButton = NSButton(title: "Actions ⌘K", target: nil, action: nil)
|
||||
private let actions = ApplicationActionsController()
|
||||
private var uninstallReview: UninstallReviewController?
|
||||
|
||||
init(history: LaunchHistory = LaunchHistory()) {
|
||||
init(history: LaunchHistory = LaunchHistory(), uninstaller: ApplicationUninstaller = ApplicationUninstaller()) {
|
||||
self.history = history
|
||||
self.uninstaller = uninstaller
|
||||
let window = LauncherPanel(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 750, height: 474),
|
||||
styleMask: [.borderless],
|
||||
@@ -46,6 +51,11 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
self.alignmentGuides?.end(snapping: self.window)
|
||||
self.savePosition()
|
||||
}
|
||||
actions.closed = { [weak self] in
|
||||
guard let self else { return }
|
||||
if NSApp.isActive { self.window?.makeFirstResponder(self.searchField) }
|
||||
else { self.dismiss(restoringFocus: false) }
|
||||
}
|
||||
restorePosition()
|
||||
}
|
||||
|
||||
@@ -81,6 +91,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
|
||||
func dismiss(restoringFocus: Bool = true) {
|
||||
guard !restoringFocus || (!isLaunching && window?.attachedSheet == nil) else { return }
|
||||
actions.close()
|
||||
alignmentGuides?.end()
|
||||
let previous = previousApplication
|
||||
previousApplication = nil
|
||||
@@ -132,7 +143,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
|
||||
func windowDidResignKey(_ notification: Notification) {
|
||||
guard !isLaunching, window?.attachedSheet == nil else { return }
|
||||
guard !isLaunching, !actions.isShown, window?.attachedSheet == nil else { return }
|
||||
dismiss(restoringFocus: false)
|
||||
}
|
||||
|
||||
@@ -228,6 +239,16 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
openButton.target = self
|
||||
openButton.action = #selector(openSelection)
|
||||
openButton.isEnabled = false
|
||||
actionsButton.isBordered = false
|
||||
actionsButton.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
actionsButton.contentTintColor = SunsetPalette.text
|
||||
actionsButton.keyEquivalent = "k"
|
||||
actionsButton.keyEquivalentModifierMask = .command
|
||||
actionsButton.target = self
|
||||
actionsButton.action = #selector(showActions)
|
||||
actionsButton.setAccessibilityLabel("Application actions")
|
||||
actionsButton.toolTip = "Actions for the selected application (Command-K)"
|
||||
actionsButton.isEnabled = false
|
||||
openButton.toolTip = "Open the selected application (Return). Use ↑ and ↓ to select."
|
||||
let returnKey = NSTextField(labelWithString: "↵")
|
||||
returnKey.font = .systemFont(ofSize: 14)
|
||||
@@ -244,7 +265,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
actionPill.state = .active
|
||||
roundMaterialCorners(actionPill, radius: 18)
|
||||
actionPill.layer?.borderWidth = 0.5
|
||||
for view in [openButton, returnKey] {
|
||||
for view in [openButton, returnKey, actionsButton] {
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
actionPill.addSubview(view)
|
||||
}
|
||||
@@ -282,7 +303,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
openButton.leadingAnchor.constraint(equalTo: actionPill.leadingAnchor, constant: 14),
|
||||
openButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
|
||||
returnKey.leadingAnchor.constraint(equalTo: openButton.trailingAnchor, constant: 8),
|
||||
returnKey.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12),
|
||||
actionsButton.leadingAnchor.constraint(equalTo: returnKey.trailingAnchor, constant: 14),
|
||||
actionsButton.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -14),
|
||||
actionsButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
|
||||
returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
|
||||
returnKey.widthAnchor.constraint(equalToConstant: 18),
|
||||
returnKey.heightAnchor.constraint(equalToConstant: 18),
|
||||
@@ -305,6 +328,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
ApplicationCatalog.discover()
|
||||
}.value
|
||||
guard let self else { return }
|
||||
guard !Task.isCancelled else { return }
|
||||
self.applications = applications
|
||||
self.updateLocationLabels()
|
||||
self.discoveryTask = nil
|
||||
@@ -388,6 +412,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
|
||||
private func updateOpenButton() {
|
||||
openButton.isEnabled = !isLaunching && results.indices.contains(tableView.selectedRow)
|
||||
actionsButton.isEnabled = openButton.isEnabled
|
||||
}
|
||||
|
||||
func tableViewSelectionDidChange(_ notification: Notification) {
|
||||
@@ -465,6 +490,30 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
return cell
|
||||
}
|
||||
|
||||
@objc private func showActions() {
|
||||
guard !isLaunching, window?.attachedSheet == nil, results.indices.contains(tableView.selectedRow) else { return }
|
||||
let application = results[tableView.selectedRow]
|
||||
actions.uninstall = { [weak self] in self?.reviewUninstall(application) }
|
||||
actions.show(for: application, relativeTo: actionsButton)
|
||||
}
|
||||
|
||||
private func reviewUninstall(_ application: Application) {
|
||||
guard let window, window.attachedSheet == nil else { return }
|
||||
let review = UninstallReviewController(application: application, uninstaller: uninstaller)
|
||||
uninstallReview = review
|
||||
review.closed = { [weak self] in
|
||||
self?.uninstallReview = nil
|
||||
self?.window?.makeFirstResponder(self?.searchField)
|
||||
}
|
||||
review.changed = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.discoveryTask?.cancel()
|
||||
self.discoveryTask = nil
|
||||
self.refreshApplications()
|
||||
}
|
||||
review.present(on: window)
|
||||
}
|
||||
|
||||
@objc private func openSelection() {
|
||||
guard !isLaunching, results.indices.contains(tableView.selectedRow) else { return }
|
||||
let application = results[tableView.selectedRow]
|
||||
@@ -551,7 +600,7 @@ private final class LauncherPanel: NSPanel {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class LauncherRowView: NSTableRowView {
|
||||
final class LauncherRowView: NSTableRowView {
|
||||
override func drawSelection(in dirtyRect: NSRect) {
|
||||
let selection = NSBezierPath(roundedRect: bounds.insetBy(dx: 0.75, dy: 0.75), xRadius: 10, yRadius: 10)
|
||||
SunsetPalette.selectionFill.setFill()
|
||||
|
||||
Reference in New Issue
Block a user