feat: refine footer key badges and uninstall shortcuts

This commit is contained in:
2026-10-12 23:47:41 +02:00
parent 47aa757526
commit f0ab87e797
3 changed files with 62 additions and 16 deletions
+1 -1
View File
@@ -79,7 +79,7 @@ Applications with identical display names show a muted containing-folder hint be
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. 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 red **Uninstall application** action morphs the existing launcher into a file-review screen with checkboxes, paths, sizes, and a selected-item count; it does not create another window or review sheet. A native 180-millisecond resize and content crossfade runs in both directions, or switches immediately with Reduce Motion. The filter uses a centered, intrinsic-height text field without built-in search/cancel icons, beside an explicit back chevron. Filtering files by name or path does not change file selections. Up/Down or Control-K/Control-J highlights files; Space toggles the highlighted eligible item when the file list has focus. Back or Escape restores the original launcher frame, query, and selection. Temporary review geometry is not saved as the launcher position. Leaving is blocked during confirmation or an uninstall operation. Return or the uninstall button still opens a native final confirmation dialog; nothing is moved merely by entering review. The red **Uninstall application** action morphs the existing launcher into a file-review screen with checkboxes, paths, sizes, and a selected-item count; it does not create another window or review sheet. A native 180-millisecond resize and content crossfade runs in both directions, or switches immediately with Reduce Motion. The filter uses a centered, intrinsic-height text field without built-in search/cancel icons, beside an explicit back chevron. Filtering files by name or path does not change file selections. Up/Down or Control-K/Control-J highlights files; Return toggles the highlighted eligible file, including from the filter input, and Space also toggles when the list has focus. Back or Escape restores the original launcher frame, query, and selection. Temporary review geometry is not saved as the launcher position. Leaving is blocked during confirmation or an uninstall operation. Command-Return or the uninstall button opens a native final confirmation dialog; nothing is moved merely by entering review. The confirmation control uses the launcher's frosted rounded pill with separate Command and Return key badges. The launcher Actions control likewise shows separate Command and K badges matching its Open Return badge.
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. 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.
+29 -10
View File
@@ -256,6 +256,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
openButton.action = #selector(openSelection) openButton.action = #selector(openSelection)
openButton.isEnabled = false openButton.isEnabled = false
actionsButton.isBordered = false actionsButton.isBordered = false
actionsButton.title = "Actions"
actionsButton.font = .systemFont(ofSize: 14, weight: .medium) actionsButton.font = .systemFont(ofSize: 14, weight: .medium)
actionsButton.contentTintColor = SunsetPalette.text actionsButton.contentTintColor = SunsetPalette.text
actionsButton.keyEquivalent = "k" actionsButton.keyEquivalent = "k"
@@ -266,14 +267,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
actionsButton.toolTip = "Actions for the selected application (Command-K)" actionsButton.toolTip = "Actions for the selected application (Command-K)"
actionsButton.isEnabled = false actionsButton.isEnabled = false
openButton.toolTip = "Open the selected application (Return). Use ↑ and ↓ to select." openButton.toolTip = "Open the selected application (Return). Use ↑ and ↓ to select."
let returnKey = NSTextField(labelWithString: "↵") let returnKey = shortcutBadge("↵")
returnKey.font = .systemFont(ofSize: 14) let commandKey = shortcutBadge("⌘")
returnKey.textColor = SunsetPalette.secondaryText let kKey = shortcutBadge("K")
returnKey.alignment = .center
returnKey.wantsLayer = true
returnKey.layer?.cornerRadius = 5
returnKey.layer?.borderWidth = 1
returnKey.setAccessibilityElement(false)
let actionPill = NSVisualEffectView() let actionPill = NSVisualEffectView()
actionPill.material = .popover actionPill.material = .popover
@@ -281,7 +277,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
actionPill.state = .active actionPill.state = .active
roundMaterialCorners(actionPill, radius: 18) roundMaterialCorners(actionPill, radius: 18)
actionPill.layer?.borderWidth = 0.5 actionPill.layer?.borderWidth = 0.5
for view in [openButton, returnKey, actionsButton] { for view in [openButton, returnKey, actionsButton, commandKey, kKey] {
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
actionPill.addSubview(view) actionPill.addSubview(view)
} }
@@ -320,7 +316,11 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
openButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), openButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
returnKey.leadingAnchor.constraint(equalTo: openButton.trailingAnchor, constant: 8), returnKey.leadingAnchor.constraint(equalTo: openButton.trailingAnchor, constant: 8),
actionsButton.leadingAnchor.constraint(equalTo: returnKey.trailingAnchor, constant: 14), actionsButton.leadingAnchor.constraint(equalTo: returnKey.trailingAnchor, constant: 14),
actionsButton.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -14), commandKey.leadingAnchor.constraint(equalTo: actionsButton.trailingAnchor, constant: 8),
commandKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
kKey.leadingAnchor.constraint(equalTo: commandKey.trailingAnchor, constant: 4),
kKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
kKey.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12),
actionsButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), actionsButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
returnKey.widthAnchor.constraint(equalToConstant: 18), returnKey.widthAnchor.constraint(equalToConstant: 18),
@@ -699,3 +699,22 @@ func roundMaterialCorners(_ view: NSVisualEffectView, radius: CGFloat) {
view.layer?.masksToBounds = true view.layer?.masksToBounds = true
view.maskImage = mask view.maskImage = mask
} }
@MainActor
func shortcutBadge(_ title: String) -> NSTextField {
let badge = NSTextField(labelWithString: title)
badge.font = .systemFont(ofSize: 14)
badge.textColor = SunsetPalette.secondaryText
badge.alignment = .center
badge.wantsLayer = true
badge.layer?.cornerRadius = 5
badge.layer?.borderWidth = 1
badge.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor
badge.setAccessibilityElement(false)
badge.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
badge.widthAnchor.constraint(equalToConstant: 18),
badge.heightAnchor.constraint(equalToConstant: 18)
])
return badge
}
@@ -86,8 +86,11 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
remove.target = self remove.target = self
remove.action = #selector(confirmUninstall) remove.action = #selector(confirmUninstall)
remove.contentTintColor = .systemRed remove.contentTintColor = .systemRed
remove.bezelStyle = .rounded remove.isBordered = false
remove.font = .systemFont(ofSize: 14, weight: .medium)
remove.keyEquivalent = "\r" remove.keyEquivalent = "\r"
remove.keyEquivalentModifierMask = .command
remove.toolTip = "Confirm uninstall (Command-Return). Return toggles the highlighted file."
remove.isEnabled = false remove.isEnabled = false
table.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("file"))) table.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("file")))
table.headerView = nil table.headerView = nil
@@ -107,7 +110,31 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
scroll.autohidesScrollers = true scroll.autohidesScrollers = true
scroll.drawsBackground = false scroll.drawsBackground = false
scroll.contentView.drawsBackground = false scroll.contentView.drawsBackground = false
let footer = NSStackView(views: [NSView(), remove]) let pill = NSVisualEffectView()
pill.material = .popover
pill.blendingMode = .withinWindow
pill.state = .active
roundMaterialCorners(pill, radius: 18)
pill.layer?.borderWidth = 0.5
pill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor
pill.layer?.backgroundColor = SunsetPalette.accent.withAlphaComponent(0.08).cgColor
let commandKey = shortcutBadge("⌘")
let returnKey = shortcutBadge("↵")
for child in [remove, commandKey, returnKey] {
child.translatesAutoresizingMaskIntoConstraints = false
pill.addSubview(child)
}
NSLayoutConstraint.activate([
pill.heightAnchor.constraint(equalToConstant: 36),
remove.leadingAnchor.constraint(equalTo: pill.leadingAnchor, constant: 14),
remove.centerYAnchor.constraint(equalTo: pill.centerYAnchor),
commandKey.leadingAnchor.constraint(equalTo: remove.trailingAnchor, constant: 8),
commandKey.centerYAnchor.constraint(equalTo: pill.centerYAnchor),
returnKey.leadingAnchor.constraint(equalTo: commandKey.trailingAnchor, constant: 4),
returnKey.centerYAnchor.constraint(equalTo: pill.centerYAnchor),
returnKey.trailingAnchor.constraint(equalTo: pill.trailingAnchor, constant: -12)
])
let footer = NSStackView(views: [NSView(), pill])
footer.orientation = .horizontal footer.orientation = .horizontal
footer.spacing = 12 footer.spacing = 12
for view in [header, status, scroll, footer] { for view in [header, status, scroll, footer] {
@@ -132,7 +159,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
footer.leadingAnchor.constraint(equalTo: search.leadingAnchor), footer.leadingAnchor.constraint(equalTo: search.leadingAnchor),
footer.trailingAnchor.constraint(equalTo: search.trailingAnchor), footer.trailingAnchor.constraint(equalTo: search.trailingAnchor),
footer.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -16), footer.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -16),
footer.heightAnchor.constraint(equalToConstant: 32) footer.heightAnchor.constraint(equalToConstant: 36)
]) ])
} }
@@ -159,7 +186,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
switch selector { switch selector {
case #selector(NSResponder.moveDown(_:)): moveSelection(1) case #selector(NSResponder.moveDown(_:)): moveSelection(1)
case #selector(NSResponder.moveUp(_:)): moveSelection(-1) case #selector(NSResponder.moveUp(_:)): moveSelection(-1)
case #selector(NSResponder.insertNewline(_:)): confirmUninstall() case #selector(NSResponder.insertNewline(_:)): toggleHighlighted()
case #selector(NSResponder.cancelOperation(_:)): goBack() case #selector(NSResponder.cancelOperation(_:)): goBack()
default: return false default: return false
} }
@@ -390,7 +417,7 @@ private final class UninstallFilesTable: NSTableView {
var toggle: (() -> Void)? var toggle: (() -> Void)?
var dismiss: (() -> Void)? var dismiss: (() -> Void)?
override func keyDown(with event: NSEvent) { override func keyDown(with event: NSEvent) {
if event.keyCode == 49 { toggle?() } if [36, 76, 49].contains(event.keyCode), event.modifierFlags.intersection([.command, .option, .control]).isEmpty { toggle?() }
else if event.keyCode == 53 { dismiss?() } else if event.keyCode == 53 { dismiss?() }
else { super.keyDown(with: event) } else { super.keyDown(with: event) }
} }