fix: rebuild complete popup presentation between openings
This commit is contained in:
@@ -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.
|
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
|
### Global shortcuts and launch at login
|
||||||
|
|
||||||
|
|||||||
@@ -3,30 +3,16 @@ import ConjureKit
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate {
|
final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate {
|
||||||
private let search = ActionsSearchField()
|
private var search: ActionsSearchField!
|
||||||
private let background = LauncherBackground(frame: .zero)
|
private var background: LauncherBackground!
|
||||||
private var material: NSVisualEffectView?
|
private var entries: [ActionEntry] = []
|
||||||
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 visibleEntries: [Int] = []
|
private var visibleEntries: [Int] = []
|
||||||
private var availableEntries = Array(0..<6)
|
private var availableEntries = Array(0..<6)
|
||||||
private var selectedEntry: Int? = 0
|
private var selectedEntry: Int? = 0
|
||||||
private var rowTopConstraints: [NSLayoutConstraint] = []
|
private var rowTopConstraints: [NSLayoutConstraint] = []
|
||||||
private var heightConstraint: NSLayoutConstraint?
|
private var heightConstraint: NSLayoutConstraint?
|
||||||
private let empty = NSTextField(labelWithString: "No matching actions")
|
private var empty: NSTextField!
|
||||||
private let applicationLabel = NSTextField(labelWithString: "")
|
private var applicationLabel: NSTextField!
|
||||||
private var mouseMonitor: Any?
|
private var mouseMonitor: Any?
|
||||||
private weak var coveredPill: NSView?
|
private weak var coveredPill: NSView?
|
||||||
private var coveredPillAlpha: CGFloat = 1
|
private var coveredPillAlpha: CGFloat = 1
|
||||||
@@ -52,17 +38,39 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
init() { super.init(nibName: nil, bundle: nil) }
|
init() { super.init(nibName: nil, bundle: nil) }
|
||||||
required init?(coder: NSCoder) { fatalError("Conjure creates actions programmatically") }
|
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 = ActionsPanelView()
|
||||||
view.wantsLayer = true
|
view.wantsLayer = true
|
||||||
view.layer?.shadowColor = NSColor.black.cgColor
|
view.layer?.shadowColor = NSColor.black.cgColor
|
||||||
view.layer?.shadowOpacity = 0.35
|
view.layer?.shadowOpacity = 0.35
|
||||||
view.layer?.shadowRadius = 14
|
view.layer?.shadowRadius = 14
|
||||||
view.layer?.shadowOffset = NSSize(width: 0, height: -4)
|
view.layer?.shadowOffset = NSSize(width: 0, height: -4)
|
||||||
let content = NSView()
|
let content = NSVisualEffectView()
|
||||||
content.wantsLayer = true
|
content.material = .hudWindow
|
||||||
content.layer?.cornerRadius = 18
|
content.blendingMode = .behindWindow
|
||||||
content.layer?.masksToBounds = true
|
content.state = .active
|
||||||
|
roundMaterialCorners(content, radius: 18)
|
||||||
content.layer?.borderWidth = 0.5
|
content.layer?.borderWidth = 0.5
|
||||||
background.setAccessibilityElement(false)
|
background.setAccessibilityElement(false)
|
||||||
content.addSubview(background)
|
content.addSubview(background)
|
||||||
@@ -90,7 +98,7 @@ 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, 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) }
|
||||||
}
|
}
|
||||||
@@ -169,7 +177,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
}
|
}
|
||||||
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
|
// Reuse behavior, not native drawing state from a differently sized popup.
|
||||||
|
if isViewLoaded { preparePresentation() } else { _ = view }
|
||||||
applicationLabel.stringValue = title
|
applicationLabel.stringValue = title
|
||||||
availableEntries = available
|
availableEntries = available
|
||||||
let openTitle = emoji ? "Copy and Paste" : "Open"
|
let openTitle = emoji ? "Copy and Paste" : "Open"
|
||||||
@@ -213,24 +222,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
coveredPillAlpha = pill.alphaValue
|
coveredPillAlpha = pill.alphaValue
|
||||||
}
|
}
|
||||||
coveredPill?.setAccessibilityHidden(true)
|
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()
|
host.layoutSubtreeIfNeeded()
|
||||||
background.needsDisplay = true
|
background.needsDisplay = true
|
||||||
NSAnimationContext.runAnimationGroup { context in
|
NSAnimationContext.runAnimationGroup { context in
|
||||||
@@ -265,8 +256,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
coveredPill?.setAccessibilityHidden(false)
|
coveredPill?.setAccessibilityHidden(false)
|
||||||
if !animated {
|
if !animated {
|
||||||
view.removeFromSuperview()
|
view.removeFromSuperview()
|
||||||
material?.removeFromSuperview()
|
|
||||||
material = nil
|
|
||||||
coveredPill?.layer?.removeAllAnimations()
|
coveredPill?.layer?.removeAllAnimations()
|
||||||
coveredPill?.alphaValue = coveredPillAlpha
|
coveredPill?.alphaValue = coveredPillAlpha
|
||||||
coveredPill = nil
|
coveredPill = nil
|
||||||
@@ -283,8 +272,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
MainActor.assumeIsolated {
|
MainActor.assumeIsolated {
|
||||||
guard self.transitionGeneration == generation, self.isClosing else { return }
|
guard self.transitionGeneration == generation, self.isClosing else { return }
|
||||||
self.view.removeFromSuperview()
|
self.view.removeFromSuperview()
|
||||||
self.material?.removeFromSuperview()
|
|
||||||
self.material = nil
|
|
||||||
self.coveredPill = nil
|
self.coveredPill = nil
|
||||||
self.isClosing = false
|
self.isClosing = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user