From b325b8cfcdf9ca0ca84c0713e96e0fc1e3ee326b Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 18 Oct 2026 02:08:36 +0200 Subject: [PATCH] refactor: share footer action pill across launcher tools --- README.md | 2 + Sources/Conjure/ActionPill.swift | 43 +++++++++++++++++++ Sources/Conjure/EmojiPickerController.swift | 25 +++-------- .../Conjure/LauncherWindowController.swift | 34 +-------------- Sources/Conjure/TranslationController.swift | 30 +------------ 5 files changed, 52 insertions(+), 82 deletions(-) create mode 100644 Sources/Conjure/ActionPill.swift diff --git a/README.md b/README.md index 8c38945..b8770ae 100644 --- a/README.md +++ b/README.md @@ -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. +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 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. diff --git a/Sources/Conjure/ActionPill.swift b/Sources/Conjure/ActionPill.swift new file mode 100644 index 0000000..c1ec855 --- /dev/null +++ b/Sources/Conjure/ActionPill.swift @@ -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") } +} diff --git a/Sources/Conjure/EmojiPickerController.swift b/Sources/Conjure/EmojiPickerController.swift index b4a3206..aaa4382 100644 --- a/Sources/Conjure/EmojiPickerController.swift +++ b/Sources/Conjure/EmojiPickerController.swift @@ -61,32 +61,17 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource, status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) copyButton.target = self 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.action = #selector(showActions) - actionsButton.keyEquivalent = "k" - 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 pill = ActionPill(primaryButton: copyButton, actionsButton: actionsButton) let footer = NSStackView(views: [status, NSView(), pill]) - pill.heightAnchor.constraint(equalToConstant: 36).isActive = true actions.closed = { [weak self] in self?.copyButton.keyEquivalent = "\r" self?.focusSearch?() } - footer.spacing = 8 + footer.orientation = .horizontal + footer.alignment = .centerY + footer.spacing = 12 for child in [category, scroll, empty, footer] { child.translatesAutoresizingMaskIntoConstraints = false view.addSubview(child) @@ -101,7 +86,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource, empty.centerXAnchor.constraint(equalTo: scroll.centerXAnchor), empty.centerYAnchor.constraint(equalTo: scroll.centerYAnchor), 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.heightAnchor.constraint(equalToConstant: 36), ]) diff --git a/Sources/Conjure/LauncherWindowController.swift b/Sources/Conjure/LauncherWindowController.swift index 33c2098..ead35d9 100644 --- a/Sources/Conjure/LauncherWindowController.swift +++ b/Sources/Conjure/LauncherWindowController.swift @@ -330,36 +330,16 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe emptyLabel.textColor = SunsetPalette.secondaryText emptyLabel.alignment = .center - openButton.isBordered = false - openButton.font = .systemFont(ofSize: 12, weight: .medium) - openButton.contentTintColor = SunsetPalette.text - openButton.keyEquivalent = "\r" openButton.target = self openButton.action = #selector(openSelection) 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.action = #selector(showActions) actionsButton.setAccessibilityLabel("Entry actions") actionsButton.toolTip = "Actions for the selected application or command (Command-K)" actionsButton.isEnabled = false openButton.toolTip = "Open the selected application (Return). Use ↑ and ↓ to select." - let returnKey = ShortcutIndicator("↵") - 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 actionPill = ActionPill(primaryButton: openButton, actionsButton: actionsButton) let footer = NSStackView(views: [statusLabel, NSView(), actionPill]) footer.orientation = .horizontal @@ -388,22 +368,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe footer.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8), footer.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -8), 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 { let border = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor 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 launcherViews = [header, sectionLabel, scrollView, emptyLabel, footer] diff --git a/Sources/Conjure/TranslationController.swift b/Sources/Conjure/TranslationController.swift index d4c34d3..80a2b98 100644 --- a/Sources/Conjure/TranslationController.swift +++ b/Sources/Conjure/TranslationController.swift @@ -102,11 +102,6 @@ final class TranslationController: NSViewController { languageButton.setAccessibilityLabel("Translation languages") actionsButton.target = self 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 self?.translateButton.keyEquivalent = "\r" self?.focusSearch?() @@ -122,31 +117,8 @@ final class TranslationController: NSViewController { name: Notification.Name("ConjureTranslationCredentialsChanged"), object: nil) translateButton.target = self 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." - let returnKey = ShortcutIndicator("↵") - 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 actionPill = ActionPill(primaryButton: translateButton, actionsButton: actionsButton) let footer = NSStackView(views: [languageButton, status, settingsButton, NSView(), actionPill]) footer.orientation = .horizontal footer.alignment = .centerY