refactor: share footer action pill across launcher tools

This commit is contained in:
2026-10-18 02:08:36 +02:00
parent b13976c516
commit b325b8cfcd
5 changed files with 52 additions and 82 deletions
+2
View File
@@ -65,6 +65,8 @@ Right-click the launcher background for Settings and Quit. Actions include favor
Action and provider popups paint a popup-sized background using the launcher window’s gradient coordinates, keeping the glows continuous without showing the results underneath. Each opening rebuilds the complete native view and layer tree, rather than resizing a retained presentation between the footer and header. Callbacks and selection behavior stay in the controller. Layout finishes before the opening fade, including when switching popup types. Action and provider popups paint a popup-sized background using the launcher window’s gradient coordinates, keeping the glows continuous without showing the results underneath. Each opening rebuilds the complete native view and layer tree, rather than resizing a retained presentation between the footer and header. Callbacks and selection behavior stay in the controller. Layout finishes before the opening fade, including when switching popup types.
The launcher, emoji picker, and Translate share `ActionPill` for footer styling, alignment, spacing, and keyboard indicators: primary action with Return first, then Actions with separate Command and K keycaps. Controllers supply the buttons and retain their targets, enabled state, and popup behavior.
### Global shortcuts and launch at login ### Global shortcuts and launch at login
In Settings, choose a **Global shortcut destination**: **Applications**, **Conjure Emoji**, **Conjure Notes**, or **Conjure Translate**. Choose **Record Shortcut…** and press a combination containing Command, Option, or Control. For example, select **Conjure Emoji** and record Command-Shift-Space to open the emoji picker directly. **Clear** disables only the selected destination's shortcut. No shortcuts are assigned by default; existing launcher bindings are preserved. In Settings, choose a **Global shortcut destination**: **Applications**, **Conjure Emoji**, **Conjure Notes**, or **Conjure Translate**. Choose **Record Shortcut…** and press a combination containing Command, Option, or Control. For example, select **Conjure Emoji** and record Command-Shift-Space to open the emoji picker directly. **Clear** disables only the selected destination's shortcut. No shortcuts are assigned by default; existing launcher bindings are preserved.
+43
View File
@@ -0,0 +1,43 @@
import AppKit
@MainActor
final class ActionPill: NSView {
init(primaryButton: NSButton, actionsButton: NSButton) {
super.init(frame: .zero)
wantsLayer = true
layer?.cornerRadius = 18
layer?.borderWidth = 0.5
layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor
layer?.backgroundColor = SunsetPalette.background.withAlphaComponent(0.25).cgColor
for button in [primaryButton, actionsButton] {
button.isBordered = false
button.font = .systemFont(ofSize: 12, weight: .medium)
button.contentTintColor = SunsetPalette.text
}
primaryButton.keyEquivalent = "\r"
primaryButton.keyEquivalentModifierMask = []
actionsButton.title = "Actions"
actionsButton.keyEquivalent = "k"
actionsButton.keyEquivalentModifierMask = .command
let returnKey = ShortcutIndicator("↵")
let actionKeys = ShortcutIndicator("⌘", "K")
for control in [primaryButton, returnKey, actionsButton, actionKeys] {
control.translatesAutoresizingMaskIntoConstraints = false
addSubview(control)
}
NSLayoutConstraint.activate([
heightAnchor.constraint(equalToConstant: 36),
primaryButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
primaryButton.centerYAnchor.constraint(equalTo: centerYAnchor),
returnKey.leadingAnchor.constraint(equalTo: primaryButton.trailingAnchor, constant: 8),
returnKey.centerYAnchor.constraint(equalTo: centerYAnchor),
actionsButton.leadingAnchor.constraint(equalTo: returnKey.trailingAnchor, constant: 14),
actionsButton.centerYAnchor.constraint(equalTo: centerYAnchor),
actionKeys.leadingAnchor.constraint(equalTo: actionsButton.trailingAnchor, constant: 8),
actionKeys.centerYAnchor.constraint(equalTo: centerYAnchor),
actionKeys.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
])
}
required init?(coder: NSCoder) { fatalError("Conjure creates action pills programmatically") }
}
+5 -20
View File
@@ -61,32 +61,17 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
copyButton.target = self copyButton.target = self
copyButton.action = #selector(copyAndPasteSelection) copyButton.action = #selector(copyAndPasteSelection)
copyButton.keyEquivalent = "\r"
copyButton.isBordered = false
copyButton.font = .systemFont(ofSize: 12, weight: .medium)
copyButton.contentTintColor = SunsetPalette.text
actionsButton.target = self actionsButton.target = self
actionsButton.action = #selector(showActions) actionsButton.action = #selector(showActions)
actionsButton.keyEquivalent = "k" let pill = ActionPill(primaryButton: copyButton, actionsButton: actionsButton)
actionsButton.keyEquivalentModifierMask = .command
actionsButton.isBordered = false
actionsButton.font = copyButton.font
actionsButton.contentTintColor = SunsetPalette.text
let pill = NSStackView(views: [copyButton, ShortcutIndicator("↵"), actionsButton, ShortcutIndicator("⌘", "K")])
pill.spacing = 8
pill.edgeInsets = NSEdgeInsets(top: 0, left: 14, bottom: 0, right: 12)
pill.wantsLayer = true
pill.layer?.cornerRadius = 18
pill.layer?.borderWidth = 0.5
pill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor
pill.layer?.backgroundColor = SunsetPalette.background.withAlphaComponent(0.25).cgColor
let footer = NSStackView(views: [status, NSView(), pill]) let footer = NSStackView(views: [status, NSView(), pill])
pill.heightAnchor.constraint(equalToConstant: 36).isActive = true
actions.closed = { [weak self] in actions.closed = { [weak self] in
self?.copyButton.keyEquivalent = "\r" self?.copyButton.keyEquivalent = "\r"
self?.focusSearch?() self?.focusSearch?()
} }
footer.spacing = 8 footer.orientation = .horizontal
footer.alignment = .centerY
footer.spacing = 12
for child in [category, scroll, empty, footer] { for child in [category, scroll, empty, footer] {
child.translatesAutoresizingMaskIntoConstraints = false child.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(child) view.addSubview(child)
@@ -101,7 +86,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
empty.centerXAnchor.constraint(equalTo: scroll.centerXAnchor), empty.centerXAnchor.constraint(equalTo: scroll.centerXAnchor),
empty.centerYAnchor.constraint(equalTo: scroll.centerYAnchor), empty.centerYAnchor.constraint(equalTo: scroll.centerYAnchor),
footer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), footer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
footer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), footer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8),
footer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8), footer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8),
footer.heightAnchor.constraint(equalToConstant: 36), footer.heightAnchor.constraint(equalToConstant: 36),
]) ])
+1 -33
View File
@@ -330,36 +330,16 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
emptyLabel.textColor = SunsetPalette.secondaryText emptyLabel.textColor = SunsetPalette.secondaryText
emptyLabel.alignment = .center emptyLabel.alignment = .center
openButton.isBordered = false
openButton.font = .systemFont(ofSize: 12, weight: .medium)
openButton.contentTintColor = SunsetPalette.text
openButton.keyEquivalent = "\r"
openButton.target = self openButton.target = self
openButton.action = #selector(openSelection) openButton.action = #selector(openSelection)
openButton.isEnabled = false openButton.isEnabled = false
actionsButton.isBordered = false
actionsButton.title = "Actions"
actionsButton.font = .systemFont(ofSize: 12, weight: .medium)
actionsButton.contentTintColor = SunsetPalette.text
actionsButton.keyEquivalent = "k"
actionsButton.keyEquivalentModifierMask = .command
actionsButton.target = self actionsButton.target = self
actionsButton.action = #selector(showActions) actionsButton.action = #selector(showActions)
actionsButton.setAccessibilityLabel("Entry actions") actionsButton.setAccessibilityLabel("Entry actions")
actionsButton.toolTip = "Actions for the selected application or command (Command-K)" actionsButton.toolTip = "Actions for the selected application or command (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 = ShortcutIndicator("↵") let actionPill = ActionPill(primaryButton: openButton, actionsButton: actionsButton)
let actionKeys = ShortcutIndicator("⌘", "K")
let actionPill = NSView()
actionPill.wantsLayer = true
actionPill.layer?.cornerRadius = 18
actionPill.layer?.borderWidth = 0.5
for view in [openButton, returnKey, actionsButton, actionKeys] {
view.translatesAutoresizingMaskIntoConstraints = false
actionPill.addSubview(view)
}
let footer = NSStackView(views: [statusLabel, NSView(), actionPill]) let footer = NSStackView(views: [statusLabel, NSView(), actionPill])
footer.orientation = .horizontal footer.orientation = .horizontal
@@ -388,22 +368,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
footer.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8), footer.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8),
footer.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -8), footer.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -8),
footer.heightAnchor.constraint(equalToConstant: 36), footer.heightAnchor.constraint(equalToConstant: 36),
actionPill.heightAnchor.constraint(equalToConstant: 36),
openButton.leadingAnchor.constraint(equalTo: actionPill.leadingAnchor, constant: 14),
openButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
returnKey.leadingAnchor.constraint(equalTo: openButton.trailingAnchor, constant: 8),
actionsButton.leadingAnchor.constraint(equalTo: returnKey.trailingAnchor, constant: 14),
actionKeys.leadingAnchor.constraint(equalTo: actionsButton.trailingAnchor, constant: 8),
actionKeys.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
actionKeys.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12),
actionsButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
]) ])
window.effectiveAppearance.performAsCurrentDrawingAppearance { window.effectiveAppearance.performAsCurrentDrawingAppearance {
let border = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor let border = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor
content.layer?.borderColor = border content.layer?.borderColor = border
actionPill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor
actionPill.layer?.backgroundColor = SunsetPalette.background.withAlphaComponent(0.25).cgColor
} }
window.initialFirstResponder = searchField window.initialFirstResponder = searchField
launcherViews = [header, sectionLabel, scrollView, emptyLabel, footer] launcherViews = [header, sectionLabel, scrollView, emptyLabel, footer]
+1 -29
View File
@@ -102,11 +102,6 @@ final class TranslationController: NSViewController {
languageButton.setAccessibilityLabel("Translation languages") languageButton.setAccessibilityLabel("Translation languages")
actionsButton.target = self actionsButton.target = self
actionsButton.action = #selector(showActions) actionsButton.action = #selector(showActions)
actionsButton.keyEquivalent = "k"
actionsButton.keyEquivalentModifierMask = .command
actionsButton.isBordered = false
actionsButton.font = .systemFont(ofSize: 12, weight: .medium)
actionsButton.contentTintColor = SunsetPalette.text
actions.closed = { [weak self] in actions.closed = { [weak self] in
self?.translateButton.keyEquivalent = "\r" self?.translateButton.keyEquivalent = "\r"
self?.focusSearch?() self?.focusSearch?()
@@ -122,31 +117,8 @@ final class TranslationController: NSViewController {
name: Notification.Name("ConjureTranslationCredentialsChanged"), object: nil) name: Notification.Name("ConjureTranslationCredentialsChanged"), object: nil)
translateButton.target = self translateButton.target = self
translateButton.action = #selector(performPrimaryAction) translateButton.action = #selector(performPrimaryAction)
translateButton.keyEquivalent = "\r"
translateButton.isBordered = false
translateButton.font = .systemFont(ofSize: 12, weight: .medium)
translateButton.contentTintColor = SunsetPalette.text
translateButton.toolTip = "Translate the input (Return). Escape dismisses Conjure." translateButton.toolTip = "Translate the input (Return). Escape dismisses Conjure."
let returnKey = ShortcutIndicator("↵") let actionPill = ActionPill(primaryButton: translateButton, actionsButton: actionsButton)
let actionPill = NSView()
stylePill(actionPill)
let actionsKey = ShortcutIndicator("⌘", "K")
for control in [translateButton, returnKey, actionsButton, actionsKey] {
control.translatesAutoresizingMaskIntoConstraints = false
actionPill.addSubview(control)
}
NSLayoutConstraint.activate([
actionPill.heightAnchor.constraint(equalToConstant: 36),
translateButton.leadingAnchor.constraint(equalTo: actionPill.leadingAnchor, constant: 14),
translateButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
returnKey.leadingAnchor.constraint(equalTo: translateButton.trailingAnchor, constant: 8),
returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
actionsButton.leadingAnchor.constraint(equalTo: returnKey.trailingAnchor, constant: 14),
actionsButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
actionsKey.leadingAnchor.constraint(equalTo: actionsButton.trailingAnchor, constant: 8),
actionsKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
actionsKey.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12),
])
let footer = NSStackView(views: [languageButton, status, settingsButton, NSView(), actionPill]) let footer = NSStackView(views: [languageButton, status, settingsButton, NSView(), actionPill])
footer.orientation = .horizontal footer.orientation = .horizontal
footer.alignment = .centerY footer.alignment = .centerY