diff --git a/README.md b/README.md index 303729e..d6840d7 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ The launcher, emoji picker, notes, and translation tool open immediately without Right-click the launcher background for Settings and Quit. Actions include favorites, resetting usage ranking, revealing applications in Finder, quitting them, and uninstalling them. -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 creates a fresh native visual-effect backdrop; closing releases it instead of moving cached material between the footer and header. 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. ### Global shortcuts and launch at login diff --git a/Sources/Conjure/ApplicationActionsController.swift b/Sources/Conjure/ApplicationActionsController.swift index 5cb18fd..5695a09 100644 --- a/Sources/Conjure/ApplicationActionsController.swift +++ b/Sources/Conjure/ApplicationActionsController.swift @@ -3,30 +3,16 @@ import ConjureKit @MainActor final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate { - private let search = ActionsSearchField() - private let background = LauncherBackground(frame: .zero) - private var material: NSVisualEffectView? - private let entries = [ - ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: SunsetPalette.text), - ActionEntry(title: "Favorite", symbol: "star", color: SunsetPalette.text), - ActionEntry(title: "Show in Finder", symbol: "finder", color: SunsetPalette.text), - ActionEntry(title: "Reset ranking", symbol: "arrow.counterclockwise", color: SunsetPalette.text), - ActionEntry(title: "Quit application", symbol: "stop.circle", color: SunsetPalette.text), - ActionEntry(title: "Uninstall application", symbol: "trash", color: .systemRed), - ActionEntry(title: "Copy Emoji", symbol: "doc.on.doc", color: SunsetPalette.text), - ActionEntry(title: "Change source language…", symbol: "character.bubble", color: SunsetPalette.text), - ActionEntry(title: "Change target language…", symbol: "character.bubble", color: SunsetPalette.text), - ActionEntry(title: "Swap languages", symbol: "arrow.left.arrow.right", color: SunsetPalette.text), - ActionEntry(title: "MyMemory (no key)", symbol: "checkmark", color: SunsetPalette.text), - ActionEntry(title: "OpenAI · gpt-6-luna", symbol: "checkmark", color: SunsetPalette.text) - ] + private var search: ActionsSearchField! + private var background: LauncherBackground! + private var entries: [ActionEntry] = [] private var visibleEntries: [Int] = [] private var availableEntries = Array(0..<6) private var selectedEntry: Int? = 0 private var rowTopConstraints: [NSLayoutConstraint] = [] private var heightConstraint: NSLayoutConstraint? - private let empty = NSTextField(labelWithString: "No matching actions") - private let applicationLabel = NSTextField(labelWithString: "") + private var empty: NSTextField! + private var applicationLabel: NSTextField! private var mouseMonitor: Any? private weak var coveredPill: NSView? private var coveredPillAlpha: CGFloat = 1 @@ -52,17 +38,39 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat init() { super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("Conjure creates actions programmatically") } - override func loadView() { + override func loadView() { preparePresentation() } + + private func preparePresentation() { + search = ActionsSearchField() + background = LauncherBackground(frame: .zero) + empty = NSTextField(labelWithString: "No matching actions") + applicationLabel = NSTextField(labelWithString: "") + rowTopConstraints = [] + entries = [ + ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: SunsetPalette.text), + ActionEntry(title: "Favorite", symbol: "star", color: SunsetPalette.text), + ActionEntry(title: "Show in Finder", symbol: "finder", color: SunsetPalette.text), + ActionEntry(title: "Reset ranking", symbol: "arrow.counterclockwise", color: SunsetPalette.text), + ActionEntry(title: "Quit application", symbol: "stop.circle", color: SunsetPalette.text), + ActionEntry(title: "Uninstall application", symbol: "trash", color: .systemRed), + ActionEntry(title: "Copy Emoji", symbol: "doc.on.doc", color: SunsetPalette.text), + ActionEntry(title: "Change source language…", symbol: "character.bubble", color: SunsetPalette.text), + ActionEntry(title: "Change target language…", symbol: "character.bubble", color: SunsetPalette.text), + ActionEntry(title: "Swap languages", symbol: "arrow.left.arrow.right", color: SunsetPalette.text), + ActionEntry(title: "MyMemory (no key)", symbol: "checkmark", color: SunsetPalette.text), + ActionEntry(title: "OpenAI · gpt-6-luna", symbol: "checkmark", color: SunsetPalette.text) + ] view = ActionsPanelView() view.wantsLayer = true view.layer?.shadowColor = NSColor.black.cgColor view.layer?.shadowOpacity = 0.35 view.layer?.shadowRadius = 14 view.layer?.shadowOffset = NSSize(width: 0, height: -4) - let content = NSView() - content.wantsLayer = true - content.layer?.cornerRadius = 18 - content.layer?.masksToBounds = true + let content = NSVisualEffectView() + content.material = .hudWindow + content.blendingMode = .behindWindow + content.state = .active + roundMaterialCorners(content, radius: 18) content.layer?.borderWidth = 0.5 background.setAccessibilityElement(false) content.addSubview(background) @@ -90,7 +98,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat empty.isHidden = true content.translatesAutoresizingMaskIntoConstraints = false view.addSubview(content) - for child in [background, applicationLabel, empty, search] { + for child in [background!, applicationLabel!, empty!, search!] { child.translatesAutoresizingMaskIntoConstraints = false if child !== background { content.addSubview(child) } } @@ -169,7 +177,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat } guard let window = anchor.window, let host = window.contentView else { return } let pill = anchor.superview ?? anchor - _ = view + // Reuse behavior, not native drawing state from a differently sized popup. + if isViewLoaded { preparePresentation() } else { _ = view } applicationLabel.stringValue = title availableEntries = available let openTitle = emoji ? "Copy and Paste" : "Open" @@ -213,24 +222,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat coveredPillAlpha = pill.alphaValue } coveredPill?.setAccessibilityHidden(true) - // A backdrop belongs to one presentation; do not move a cached native material - // between the footer Actions popup and the header provider popup. - material?.removeFromSuperview() - let backdrop = NSVisualEffectView() - backdrop.material = .hudWindow - backdrop.blendingMode = .behindWindow - backdrop.state = .active - roundMaterialCorners(backdrop, radius: 18) - backdrop.translatesAutoresizingMaskIntoConstraints = false - let content = background.superview! - content.addSubview(backdrop, positioned: .below, relativeTo: background) - NSLayoutConstraint.activate([ - backdrop.leadingAnchor.constraint(equalTo: content.leadingAnchor), - backdrop.trailingAnchor.constraint(equalTo: content.trailingAnchor), - backdrop.topAnchor.constraint(equalTo: content.topAnchor), - backdrop.bottomAnchor.constraint(equalTo: content.bottomAnchor), - ]) - material = backdrop host.layoutSubtreeIfNeeded() background.needsDisplay = true NSAnimationContext.runAnimationGroup { context in @@ -265,8 +256,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat coveredPill?.setAccessibilityHidden(false) if !animated { view.removeFromSuperview() - material?.removeFromSuperview() - material = nil coveredPill?.layer?.removeAllAnimations() coveredPill?.alphaValue = coveredPillAlpha coveredPill = nil @@ -283,8 +272,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat MainActor.assumeIsolated { guard self.transitionGeneration == generation, self.isClosing else { return } self.view.removeFromSuperview() - self.material?.removeFromSuperview() - self.material = nil self.coveredPill = nil self.isClosing = false }