feat: add Open as the default application action
This commit is contained in:
@@ -33,7 +33,7 @@ Open `Package.swift` in Xcode to edit the project. Use the bundled app above to
|
|||||||
- Type to filter applications; the first match is selected automatically.
|
- Type to filter applications; the first match is selected automatically.
|
||||||
- Up/Down or Control-K/Control-J changes the selection without leaving search or modifying its text.
|
- Up/Down or Control-K/Control-J changes the selection without leaving search or modifying its text.
|
||||||
- Return, keypad Enter, the Open Application button, or a double-click opens the selection.
|
- Return, keypad Enter, the Open Application button, or a double-click opens the selection.
|
||||||
- Command-K or the bottom-right Actions button opens a searchable selector for the selected application. Return chooses **Uninstall application**; Escape closes the selector and returns focus to search.
|
- Command-K or the bottom-right Actions button opens a searchable selector for the selected application. **Open** is first and selected by default. Up/Down or Control-K/Control-J selects an action; Return executes it. Escape closes the selector and returns focus to search.
|
||||||
- Escape or Command-W dismisses the launcher and restores the previously active application. Switching focus away dismisses it without stealing focus back.
|
- Escape or Command-W dismisses the launcher and restores the previously active application. Switching focus away dismisses it without stealing focus back.
|
||||||
- Reopen from Finder to show it again with an empty search. Dismissal leaves the process running; Command-Q quits it.
|
- Reopen from Finder to show it again with an empty search. Dismissal leaves the process running; Command-Q quits it.
|
||||||
- Every activation brings the launcher to the currently active macOS Space, including full-screen Spaces, without switching desktops.
|
- Every activation brings the launcher to the currently active macOS Space, including full-screen Spaces, without switching desktops.
|
||||||
@@ -77,9 +77,11 @@ Applications with identical display names show a muted containing-folder hint be
|
|||||||
|
|
||||||
## Application actions and uninstall
|
## Application actions and uninstall
|
||||||
|
|
||||||
Select an application and open **Actions ⌘K**. The red **Uninstall application** action opens a review sheet with file checkboxes, paths, sizes, and a selected-item count. Filtering by name or path does not change selections. Up/Down highlights rows; Space toggles the highlighted eligible item when the file list has focus. Back or Escape returns to the launcher. Return or the uninstall button opens a final confirmation; nothing is moved merely by choosing the action.
|
Select an application and open **Actions ⌘K**. **Open** is the first, default-selected action and uses the same launch path, successful-launch history tracking, dismissal, and error reporting as Return in the application list. Filtering resets selection to the first matching action; an empty result does nothing on Return. Up/Down or Control-K/Control-J navigates actions without changing the selected application.
|
||||||
|
|
||||||
The actions selector is an arrowless panel inside the launcher, aligned to the bottom and right edges of the footer action pill, replacing it visually while open rather than floating above it. It shares the launcher's frosted Sunset background, 38-point selection row height, 22×22-point icon boxes with 12-point icon-to-text spacing, 14-point action and search text, rounded selection styling, and Return key badge; selection width follows the panel. Application and command rows use the same sizing constants. SF Symbol optical alignment insets are removed so command icons render inside the same bounds as application icons. The application name appears at the top and borderless action search sits eight points from the bottom, without a divider. Native animations crossfade the panel and footer pill: 160 milliseconds to open and 120 milliseconds to close, with unchanged geometry. Reduce Motion disables the animation. Rapid reopening invalidates stale dismissal completions, and closing panels immediately stop intercepting input. Escape, Command-K, an outside click, or switching away closes it and restores the pill. Closing while Conjure remains active restores launcher search focus; switching away does not steal focus back. Return in action search chooses the visible action rather than launching the underlying application.
|
The red **Uninstall application** action opens a review sheet with file checkboxes, paths, sizes, and a selected-item count. Filtering files by name or path does not change file selections. Up/Down highlights files; Space toggles the highlighted eligible item when the file list has focus. Back or Escape returns to the launcher. Return or the uninstall button opens a final confirmation; nothing is moved merely by choosing the action.
|
||||||
|
|
||||||
|
The actions selector is an arrowless panel inside the launcher, aligned to the bottom and right edges of the footer action pill, replacing it visually while open rather than floating above it. It shares the launcher's frosted Sunset background, 38-point selection row height, 22×22-point icon boxes with 12-point icon-to-text spacing, 14-point action and search text, rounded selection styling, and Return key badge; selection width follows the panel. Application and command rows use the same sizing constants. SF Symbol optical alignment insets are removed so command icons render inside the same bounds as application icons. The application name appears at the top and borderless action search sits eight points from the bottom, without a divider. Native animations crossfade the panel and footer pill: 160 milliseconds to open and 120 milliseconds to close, with unchanged geometry. Reduce Motion disables the animation. Rapid reopening invalidates stale dismissal completions, and closing panels immediately stop intercepting input. Escape, Command-K, an outside click, or switching away closes it and restores the pill. Closing while Conjure remains active restores launcher search focus; switching away does not steal focus back. Return in action search executes the highlighted action: Open launches the application, while Uninstall opens its review.
|
||||||
|
|
||||||
Discovery includes the application bundle and exact bundle-ID paths in your Library: Preferences, Caches, Application Support, Logs, Saved Application State, Application Scripts, and Containers. Embedded extension identifiers are included. App-group entitlements are read from validated native code signatures; related shared Group Containers and Application Scripts remain unchecked because other applications may use them. Conjure does not guess name-based leftovers or include package receipts. This is conservative discovery, not a guarantee that every associated file is found.
|
Discovery includes the application bundle and exact bundle-ID paths in your Library: Preferences, Caches, Application Support, Logs, Saved Application State, Application Scripts, and Containers. Embedded extension identifiers are included. App-group entitlements are read from validated native code signatures; related shared Group Containers and Application Scripts remain unchecked because other applications may use them. Conjure does not guess name-based leftovers or include package receipts. This is conservative discovery, not a guarantee that every associated file is found.
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,14 @@ import ConjureKit
|
|||||||
@MainActor
|
@MainActor
|
||||||
final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate {
|
final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate {
|
||||||
private let search = ActionsSearchField()
|
private let search = ActionsSearchField()
|
||||||
private let action = NSButton(title: "Uninstall application", target: nil, action: nil)
|
private let entries = [
|
||||||
private let row = LauncherRowView()
|
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 empty = NSTextField(labelWithString: "No matching actions")
|
||||||
private let applicationLabel = NSTextField(labelWithString: "")
|
private let applicationLabel = NSTextField(labelWithString: "")
|
||||||
private var mouseMonitor: Any?
|
private var mouseMonitor: Any?
|
||||||
@@ -14,6 +20,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
private var isClosing = false
|
private var isClosing = false
|
||||||
private var transitionGeneration = 0
|
private var transitionGeneration = 0
|
||||||
private var panel: ActionsPanelView { view as! ActionsPanelView }
|
private var panel: ActionsPanelView { view as! ActionsPanelView }
|
||||||
|
var open: (() -> Void)?
|
||||||
var uninstall: (() -> Void)?
|
var uninstall: (() -> Void)?
|
||||||
var closed: (() -> Void)?
|
var closed: (() -> Void)?
|
||||||
var isShown: Bool { viewIfLoaded?.superview != nil && !isClosing }
|
var isShown: Bool { viewIfLoaded?.superview != nil && !isClosing }
|
||||||
@@ -40,31 +47,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
applicationLabel.textColor = SunsetPalette.secondaryText
|
applicationLabel.textColor = SunsetPalette.secondaryText
|
||||||
applicationLabel.lineBreakMode = .byTruncatingTail
|
applicationLabel.lineBreakMode = .byTruncatingTail
|
||||||
applicationLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
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.font = .systemFont(ofSize: LauncherRowView.textSize)
|
||||||
search.textColor = SunsetPalette.text
|
search.textColor = SunsetPalette.text
|
||||||
search.placeholderAttributedString = NSAttributedString(string: "Search for actions…", attributes: [
|
search.placeholderAttributedString = NSAttributedString(string: "Search for actions…", attributes: [
|
||||||
@@ -86,17 +68,30 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
empty.isHidden = true
|
empty.isHidden = true
|
||||||
content.translatesAutoresizingMaskIntoConstraints = false
|
content.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(content)
|
view.addSubview(content)
|
||||||
for child in [background, applicationLabel, row, empty, search] {
|
for child in [background, applicationLabel, empty, search] {
|
||||||
child.translatesAutoresizingMaskIntoConstraints = false
|
child.translatesAutoresizingMaskIntoConstraints = false
|
||||||
if child !== background { content.addSubview(child) }
|
if child !== background { content.addSubview(child) }
|
||||||
}
|
}
|
||||||
for child in [icon, action, returnKey] {
|
for (index, entry) in entries.enumerated() {
|
||||||
child.translatesAutoresizingMaskIntoConstraints = false
|
entry.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
row.addSubview(child)
|
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([
|
NSLayoutConstraint.activate([
|
||||||
view.widthAnchor.constraint(equalToConstant: 360),
|
view.widthAnchor.constraint(equalToConstant: 360), height,
|
||||||
view.heightAnchor.constraint(equalToConstant: 116),
|
|
||||||
content.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
content.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
content.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
content.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
content.topAnchor.constraint(equalTo: view.topAnchor),
|
content.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
@@ -108,36 +103,22 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
applicationLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
|
applicationLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
|
||||||
applicationLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
|
applicationLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
|
||||||
applicationLabel.topAnchor.constraint(equalTo: content.topAnchor, constant: 14),
|
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.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.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
|
||||||
search.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
|
search.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
|
||||||
search.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -8),
|
search.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -8),
|
||||||
search.heightAnchor.constraint(equalToConstant: 24)
|
search.heightAnchor.constraint(equalToConstant: 24)
|
||||||
])
|
])
|
||||||
content.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor
|
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) {
|
func show(for application: Application, relativeTo anchor: NSView) {
|
||||||
if isShown { close(); return }
|
if isShown { close(); return }
|
||||||
guard let window = anchor.window, let host = window.contentView else { return }
|
guard let window = anchor.window, let host = window.contentView else { return }
|
||||||
let pill = anchor.superview ?? anchor
|
let pill = anchor.superview ?? anchor
|
||||||
|
_ = view
|
||||||
applicationLabel.stringValue = application.name
|
applicationLabel.stringValue = application.name
|
||||||
search.stringValue = ""
|
search.stringValue = ""
|
||||||
controlTextDidChange(Notification(name: NSControl.textDidChangeNotification))
|
controlTextDidChange(Notification(name: NSControl.textDidChangeNotification))
|
||||||
@@ -204,26 +185,106 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
|
|
||||||
func controlTextDidChange(_ notification: Notification) {
|
func controlTextDidChange(_ notification: Notification) {
|
||||||
let query = search.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
let query = search.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let matches = query.isEmpty || action.title.range(of: query, options: [.caseInsensitive, .diacriticInsensitive]) != nil
|
visibleEntries = entries.indices.filter { query.isEmpty || entries[$0].button.title.range(of: query, options: [.caseInsensitive, .diacriticInsensitive]) != nil }
|
||||||
row.isHidden = !matches
|
selectedEntry = visibleEntries.first
|
||||||
empty.isHidden = matches
|
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 {
|
func control(_ control: NSControl, textView: NSTextView, doCommandBy selector: Selector) -> Bool {
|
||||||
switch selector {
|
switch selector {
|
||||||
case #selector(NSResponder.insertNewline(_:)):
|
case #selector(NSResponder.insertNewline(_:)):
|
||||||
if !row.isHidden { chooseAction() }
|
if let selectedEntry { chooseAction(at: selectedEntry) }
|
||||||
case #selector(NSResponder.cancelOperation(_:)): close()
|
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
|
default: return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func chooseAction() {
|
@objc private func chooseButton(_ sender: NSButton) { chooseAction(at: sender.tag) }
|
||||||
guard isShown else { return }
|
|
||||||
|
@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()
|
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)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
super.init(window: window)
|
super.init(window: window)
|
||||||
configureContent(in: window)
|
configureContent(in: window)
|
||||||
window.delegate = self
|
window.delegate = self
|
||||||
window.navigateSelection = { [weak self] offset in self?.moveSelection(by: offset) }
|
window.navigateSelection = { [weak self] offset in
|
||||||
|
guard let self else { return }
|
||||||
|
if self.actions.isShown { self.actions.moveSelection(by: offset) }
|
||||||
|
else { self.moveSelection(by: offset) }
|
||||||
|
}
|
||||||
window.dismiss = { [weak self] in self?.dismiss() }
|
window.dismiss = { [weak self] in self?.dismiss() }
|
||||||
window.dragEnded = { [weak self] in
|
window.dragEnded = { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
@@ -494,6 +498,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
@objc private func showActions() {
|
@objc private func showActions() {
|
||||||
guard !isLaunching, window?.attachedSheet == nil, results.indices.contains(tableView.selectedRow) else { return }
|
guard !isLaunching, window?.attachedSheet == nil, results.indices.contains(tableView.selectedRow) else { return }
|
||||||
let application = results[tableView.selectedRow]
|
let application = results[tableView.selectedRow]
|
||||||
|
actions.open = { [weak self] in self?.openApplication(application) }
|
||||||
actions.uninstall = { [weak self] in self?.reviewUninstall(application) }
|
actions.uninstall = { [weak self] in self?.reviewUninstall(application) }
|
||||||
actions.show(for: application, relativeTo: actionsButton)
|
actions.show(for: application, relativeTo: actionsButton)
|
||||||
openButton.keyEquivalent = actions.isShown ? "" : "\r"
|
openButton.keyEquivalent = actions.isShown ? "" : "\r"
|
||||||
@@ -517,8 +522,12 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func openSelection() {
|
@objc private func openSelection() {
|
||||||
guard !isLaunching, results.indices.contains(tableView.selectedRow) else { return }
|
guard results.indices.contains(tableView.selectedRow) else { return }
|
||||||
let application = results[tableView.selectedRow]
|
openApplication(results[tableView.selectedRow])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openApplication(_ application: Application) {
|
||||||
|
guard !isLaunching else { return }
|
||||||
isLaunching = true
|
isLaunching = true
|
||||||
updateOpenButton()
|
updateOpenButton()
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
Reference in New Issue
Block a user