fix: morph launcher into uninstall review and align its filter
This commit is contained in:
@@ -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 opens a review sheet with file checkboxes, paths, sizes, and a selected-item count. Filtering files by name or path does not change file selections. Up/Down highlights files; Space toggles the highlighted eligible item when the file list has focus. Back or Escape returns to the launcher. Return or the uninstall button opens a final confirmation; nothing is moved merely by choosing the action.
|
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 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.
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
private let actionsButton = NSButton(title: "Actions ⌘K", target: nil, action: nil)
|
private let actionsButton = NSButton(title: "Actions ⌘K", target: nil, action: nil)
|
||||||
private let actions = ApplicationActionsController()
|
private let actions = ApplicationActionsController()
|
||||||
private var uninstallReview: UninstallReviewController?
|
private var uninstallReview: UninstallReviewController?
|
||||||
|
private var launcherViews: [NSView] = []
|
||||||
|
private var launcherFrameBeforeReview: NSRect?
|
||||||
|
|
||||||
init(history: LaunchHistory = LaunchHistory(), uninstaller: ApplicationUninstaller = ApplicationUninstaller()) {
|
init(history: LaunchHistory = LaunchHistory(), uninstaller: ApplicationUninstaller = ApplicationUninstaller()) {
|
||||||
self.history = history
|
self.history = history
|
||||||
@@ -46,7 +48,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
window.delegate = self
|
window.delegate = self
|
||||||
window.navigateSelection = { [weak self] offset in
|
window.navigateSelection = { [weak self] offset in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
if self.actions.isShown { self.actions.moveSelection(by: offset) }
|
if let review = self.uninstallReview { review.moveSelection(offset) }
|
||||||
|
else if self.actions.isShown { self.actions.moveSelection(by: offset) }
|
||||||
else { self.moveSelection(by: offset) }
|
else { self.moveSelection(by: offset) }
|
||||||
}
|
}
|
||||||
window.dismiss = { [weak self] in self?.dismiss() }
|
window.dismiss = { [weak self] in self?.dismiss() }
|
||||||
@@ -69,6 +72,12 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func present() {
|
func present() {
|
||||||
|
if launcherFrameBeforeReview != nil {
|
||||||
|
showWindow(nil)
|
||||||
|
NSApp.activate()
|
||||||
|
window?.makeKeyAndOrderFront(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
let frontmost = NSWorkspace.shared.frontmostApplication
|
let frontmost = NSWorkspace.shared.frontmostApplication
|
||||||
if frontmost?.processIdentifier != ProcessInfo.processInfo.processIdentifier {
|
if frontmost?.processIdentifier != ProcessInfo.processInfo.processIdentifier {
|
||||||
previousApplication = frontmost
|
previousApplication = frontmost
|
||||||
@@ -96,6 +105,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
|
|
||||||
func dismiss(restoringFocus: Bool = true) {
|
func dismiss(restoringFocus: Bool = true) {
|
||||||
guard !restoringFocus || (!isLaunching && window?.attachedSheet == nil) else { return }
|
guard !restoringFocus || (!isLaunching && window?.attachedSheet == nil) else { return }
|
||||||
|
if let review = uninstallReview { review.goBack(); return }
|
||||||
|
guard launcherFrameBeforeReview == nil else { return }
|
||||||
actions.close()
|
actions.close()
|
||||||
alignmentGuides?.end()
|
alignmentGuides?.end()
|
||||||
let previous = previousApplication
|
let previous = previousApplication
|
||||||
@@ -108,7 +119,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func savePosition() {
|
private func savePosition() {
|
||||||
guard let window, let screen = window.screen, let identifier = displayIdentifier(screen) else { return }
|
guard launcherFrameBeforeReview == nil, let window, let screen = window.screen, let identifier = displayIdentifier(screen) else { return }
|
||||||
UserDefaults.standard.set([
|
UserDefaults.standard.set([
|
||||||
"display": identifier, "x": window.frame.minX, "y": window.frame.minY
|
"display": identifier, "x": window.frame.minX, "y": window.frame.minY
|
||||||
], forKey: Self.positionKey)
|
], forKey: Self.positionKey)
|
||||||
@@ -148,7 +159,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func windowDidResignKey(_ notification: Notification) {
|
func windowDidResignKey(_ notification: Notification) {
|
||||||
guard !isLaunching, window?.attachedSheet == nil else { return }
|
guard !isLaunching, launcherFrameBeforeReview == nil, window?.attachedSheet == nil else { return }
|
||||||
dismiss(restoringFocus: false)
|
dismiss(restoringFocus: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +334,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
actionPill.layer?.backgroundColor = SunsetPalette.accent.withAlphaComponent(0.08).cgColor
|
actionPill.layer?.backgroundColor = SunsetPalette.accent.withAlphaComponent(0.08).cgColor
|
||||||
}
|
}
|
||||||
window.initialFirstResponder = searchField
|
window.initialFirstResponder = searchField
|
||||||
|
launcherViews = [header, sectionLabel, scrollView, emptyLabel, footer]
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshApplications() {
|
private func refreshApplications() {
|
||||||
@@ -416,7 +428,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateOpenButton() {
|
private func updateOpenButton() {
|
||||||
openButton.isEnabled = !isLaunching && results.indices.contains(tableView.selectedRow)
|
openButton.isEnabled = !isLaunching && launcherFrameBeforeReview == nil && results.indices.contains(tableView.selectedRow)
|
||||||
actionsButton.isEnabled = openButton.isEnabled
|
actionsButton.isEnabled = openButton.isEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +508,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func showActions() {
|
@objc private func showActions() {
|
||||||
guard !isLaunching, window?.attachedSheet == nil, results.indices.contains(tableView.selectedRow) else { return }
|
guard !isLaunching, launcherFrameBeforeReview == nil, window?.attachedSheet == nil, results.indices.contains(tableView.selectedRow) else { return }
|
||||||
let application = results[tableView.selectedRow]
|
let application = results[tableView.selectedRow]
|
||||||
actions.open = { [weak self] in self?.openApplication(application) }
|
actions.open = { [weak self] in self?.openApplication(application) }
|
||||||
actions.uninstall = { [weak self] in self?.reviewUninstall(application) }
|
actions.uninstall = { [weak self] in self?.reviewUninstall(application) }
|
||||||
@@ -505,12 +517,13 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func reviewUninstall(_ application: Application) {
|
private func reviewUninstall(_ application: Application) {
|
||||||
guard let window, window.attachedSheet == nil else { return }
|
guard let window, let content = window.contentView, launcherFrameBeforeReview == nil, window.attachedSheet == nil else { return }
|
||||||
let review = UninstallReviewController(application: application, uninstaller: uninstaller)
|
let review = UninstallReviewController(application: application, uninstaller: uninstaller)
|
||||||
uninstallReview = review
|
uninstallReview = review
|
||||||
review.closed = { [weak self] in
|
launcherFrameBeforeReview = window.frame
|
||||||
self?.uninstallReview = nil
|
review.closed = { [weak self, weak review] in
|
||||||
self?.window?.makeFirstResponder(self?.searchField)
|
guard let self, let review else { return }
|
||||||
|
self.restoreLauncher(from: review)
|
||||||
}
|
}
|
||||||
review.changed = { [weak self] in
|
review.changed = { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
@@ -518,7 +531,50 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
self.discoveryTask = nil
|
self.discoveryTask = nil
|
||||||
self.refreshApplications()
|
self.refreshApplications()
|
||||||
}
|
}
|
||||||
review.present(on: window)
|
review.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
review.view.alphaValue = 0
|
||||||
|
content.addSubview(review.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
review.view.leadingAnchor.constraint(equalTo: content.leadingAnchor),
|
||||||
|
review.view.trailingAnchor.constraint(equalTo: content.trailingAnchor),
|
||||||
|
review.view.topAnchor.constraint(equalTo: content.topAnchor),
|
||||||
|
review.view.bottomAnchor.constraint(equalTo: content.bottomAnchor)
|
||||||
|
])
|
||||||
|
for view in launcherViews { view.setAccessibilityHidden(true) }
|
||||||
|
openButton.keyEquivalent = ""
|
||||||
|
updateOpenButton()
|
||||||
|
var frame = window.frame
|
||||||
|
frame.size.height = min(540, window.screen?.visibleFrame.height ?? 540)
|
||||||
|
frame.origin.y = window.frame.maxY - frame.height
|
||||||
|
if let screen = window.screen { frame.origin.y = max(frame.origin.y, screen.visibleFrame.minY) }
|
||||||
|
NSAnimationContext.runAnimationGroup { context in
|
||||||
|
context.duration = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? 0 : 0.18
|
||||||
|
window.animator().setFrame(frame, display: true)
|
||||||
|
for view in launcherViews { view.animator().alphaValue = 0 }
|
||||||
|
review.view.animator().alphaValue = 1
|
||||||
|
}
|
||||||
|
review.present()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restoreLauncher(from review: UninstallReviewController) {
|
||||||
|
guard let window, let frame = launcherFrameBeforeReview, uninstallReview === review else { return }
|
||||||
|
uninstallReview = nil
|
||||||
|
for view in launcherViews { view.setAccessibilityHidden(false) }
|
||||||
|
NSAnimationContext.runAnimationGroup { context in
|
||||||
|
context.duration = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? 0 : 0.18
|
||||||
|
window.animator().setFrame(frame, display: true)
|
||||||
|
for view in launcherViews { view.animator().alphaValue = 1 }
|
||||||
|
review.view.animator().alphaValue = 0
|
||||||
|
} completionHandler: { [weak self] in
|
||||||
|
MainActor.assumeIsolated {
|
||||||
|
review.view.removeFromSuperview()
|
||||||
|
guard let self else { return }
|
||||||
|
self.launcherFrameBeforeReview = nil
|
||||||
|
self.openButton.keyEquivalent = "\r"
|
||||||
|
self.updateOpenButton()
|
||||||
|
self.window?.makeFirstResponder(self.searchField)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func openSelection() {
|
@objc private func openSelection() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import AppKit
|
|||||||
import ConjureKit
|
import ConjureKit
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
|
final class UninstallReviewController: NSViewController, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
|
||||||
private let application: Application
|
private let application: Application
|
||||||
private let uninstaller: ApplicationUninstaller
|
private let uninstaller: ApplicationUninstaller
|
||||||
private let search = NSSearchField()
|
private let search = NSSearchField()
|
||||||
@@ -15,31 +15,23 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
|
|||||||
private var selected: Set<URL> = []
|
private var selected: Set<URL> = []
|
||||||
private var busy = false
|
private var busy = false
|
||||||
private var scanTask: Task<Void, Never>?
|
private var scanTask: Task<Void, Never>?
|
||||||
|
private var window: NSWindow? { view.window }
|
||||||
var closed: (() -> Void)?
|
var closed: (() -> Void)?
|
||||||
var changed: (() -> Void)?
|
var changed: (() -> Void)?
|
||||||
|
|
||||||
init(application: Application, uninstaller: ApplicationUninstaller) {
|
init(application: Application, uninstaller: ApplicationUninstaller) {
|
||||||
self.application = application
|
self.application = application
|
||||||
self.uninstaller = uninstaller
|
self.uninstaller = uninstaller
|
||||||
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 540),
|
super.init(nibName: nil, bundle: nil)
|
||||||
styleMask: [.titled, .closable, .fullSizeContentView], backing: .buffered, defer: false)
|
configure()
|
||||||
window.title = "Uninstall \(application.name)"
|
|
||||||
window.appearance = NSAppearance(named: .darkAqua)
|
|
||||||
window.titlebarAppearsTransparent = true
|
|
||||||
window.backgroundColor = .clear
|
|
||||||
window.isOpaque = false
|
|
||||||
window.isReleasedWhenClosed = false
|
|
||||||
super.init(window: window)
|
|
||||||
window.delegate = self
|
|
||||||
configure(window)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) { fatalError("Conjure creates uninstall review programmatically") }
|
required init?(coder: NSCoder) { fatalError("Conjure creates uninstall review programmatically") }
|
||||||
|
|
||||||
func present(on parent: NSWindow) {
|
func present() {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
parent.beginSheet(window)
|
|
||||||
window.makeFirstResponder(search)
|
window.makeFirstResponder(search)
|
||||||
|
(search.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
|
||||||
scanTask = Task { [weak self, application] in
|
scanTask = Task { [weak self, application] in
|
||||||
do {
|
do {
|
||||||
let found = try await Task.detached(priority: .userInitiated) {
|
let found = try await Task.detached(priority: .userInitiated) {
|
||||||
@@ -56,21 +48,22 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configure(_ window: NSWindow) {
|
private func configure() {
|
||||||
let content = NSVisualEffectView()
|
let content = UninstallReviewContent()
|
||||||
content.material = .hudWindow
|
content.wantsLayer = true
|
||||||
content.blendingMode = .behindWindow
|
view = content
|
||||||
content.state = .active
|
search.placeholderAttributedString = NSAttributedString(
|
||||||
window.contentView = content
|
string: "Filter files and folders by name or path…",
|
||||||
let background = LauncherBackground(frame: content.bounds)
|
attributes: [.foregroundColor: SunsetPalette.secondaryText, .font: NSFont.systemFont(ofSize: 20)]
|
||||||
background.autoresizingMask = [.width, .height]
|
)
|
||||||
background.setAccessibilityElement(false)
|
if let cell = search.cell as? NSSearchFieldCell {
|
||||||
content.addSubview(background)
|
cell.searchButtonCell = nil
|
||||||
search.placeholderString = "Filter files and folders by name or path…"
|
cell.cancelButtonCell = nil
|
||||||
|
}
|
||||||
search.delegate = self
|
search.delegate = self
|
||||||
search.sendsSearchStringImmediately = true
|
search.sendsSearchStringImmediately = true
|
||||||
search.setAccessibilityLabel("Filter uninstall files")
|
search.setAccessibilityLabel("Filter uninstall files")
|
||||||
search.font = .systemFont(ofSize: 18)
|
search.font = .systemFont(ofSize: 20)
|
||||||
search.textColor = SunsetPalette.text
|
search.textColor = SunsetPalette.text
|
||||||
search.isBordered = false
|
search.isBordered = false
|
||||||
search.drawsBackground = false
|
search.drawsBackground = false
|
||||||
@@ -80,7 +73,16 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
|
|||||||
status.setAccessibilityLabel("Uninstall selection and progress")
|
status.setAccessibilityLabel("Uninstall selection and progress")
|
||||||
back.target = self
|
back.target = self
|
||||||
back.action = #selector(goBack)
|
back.action = #selector(goBack)
|
||||||
back.bezelStyle = .rounded
|
back.title = ""
|
||||||
|
back.image = NSImage(systemSymbolName: "chevron.left", accessibilityDescription: nil)
|
||||||
|
back.imageScaling = .scaleProportionallyDown
|
||||||
|
back.isBordered = false
|
||||||
|
back.contentTintColor = SunsetPalette.secondaryText
|
||||||
|
back.setAccessibilityLabel("Back to applications")
|
||||||
|
let header = NSStackView(views: [back, search])
|
||||||
|
header.orientation = .horizontal
|
||||||
|
header.alignment = .centerY
|
||||||
|
header.spacing = 12
|
||||||
remove.target = self
|
remove.target = self
|
||||||
remove.action = #selector(confirmUninstall)
|
remove.action = #selector(confirmUninstall)
|
||||||
remove.contentTintColor = .systemRed
|
remove.contentTintColor = .systemRed
|
||||||
@@ -105,18 +107,21 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
|
|||||||
scroll.autohidesScrollers = true
|
scroll.autohidesScrollers = true
|
||||||
scroll.drawsBackground = false
|
scroll.drawsBackground = false
|
||||||
scroll.contentView.drawsBackground = false
|
scroll.contentView.drawsBackground = false
|
||||||
let footer = NSStackView(views: [back, NSView(), remove])
|
let footer = NSStackView(views: [NSView(), remove])
|
||||||
footer.orientation = .horizontal
|
footer.orientation = .horizontal
|
||||||
footer.spacing = 12
|
footer.spacing = 12
|
||||||
for view in [search, status, scroll, footer] {
|
for view in [header, status, scroll, footer] {
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
content.addSubview(view)
|
content.addSubview(view)
|
||||||
}
|
}
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
search.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
|
header.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
|
||||||
search.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
|
header.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -24),
|
||||||
search.topAnchor.constraint(equalTo: (window.contentLayoutGuide as! NSLayoutGuide).topAnchor, constant: 16),
|
header.topAnchor.constraint(equalTo: content.topAnchor, constant: 20),
|
||||||
search.heightAnchor.constraint(equalToConstant: 32),
|
header.heightAnchor.constraint(equalToConstant: 32),
|
||||||
|
back.widthAnchor.constraint(equalToConstant: 22),
|
||||||
|
back.heightAnchor.constraint(equalToConstant: 22),
|
||||||
|
search.widthAnchor.constraint(equalTo: header.widthAnchor, constant: -34),
|
||||||
status.leadingAnchor.constraint(equalTo: search.leadingAnchor),
|
status.leadingAnchor.constraint(equalTo: search.leadingAnchor),
|
||||||
status.trailingAnchor.constraint(equalTo: search.trailingAnchor),
|
status.trailingAnchor.constraint(equalTo: search.trailingAnchor),
|
||||||
status.topAnchor.constraint(equalTo: search.bottomAnchor, constant: 12),
|
status.topAnchor.constraint(equalTo: search.bottomAnchor, constant: 12),
|
||||||
@@ -161,7 +166,7 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func moveSelection(_ offset: Int) {
|
func moveSelection(_ offset: Int) {
|
||||||
guard !visible.isEmpty else { return }
|
guard !visible.isEmpty else { return }
|
||||||
let row = min(max(table.selectedRow + offset, 0), visible.count - 1)
|
let row = min(max(table.selectedRow + offset, 0), visible.count - 1)
|
||||||
table.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
|
table.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
|
||||||
@@ -300,23 +305,32 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
|
|||||||
back.isEnabled = !value
|
back.isEnabled = !value
|
||||||
remove.isEnabled = !value && !selected.isEmpty
|
remove.isEnabled = !value && !selected.isEmpty
|
||||||
table.reloadData()
|
table.reloadData()
|
||||||
if !value { updateStatus() }
|
if !value {
|
||||||
|
updateStatus()
|
||||||
|
window?.makeFirstResponder(search)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
override func cancelOperation(_ sender: Any?) { goBack() }
|
||||||
if !busy { goBack() }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func goBack() {
|
@objc func goBack() {
|
||||||
guard !busy, let window, let parent = window.sheetParent else { return }
|
guard !busy else { return }
|
||||||
scanTask?.cancel()
|
scanTask?.cancel()
|
||||||
parent.endSheet(window)
|
(view as! UninstallReviewContent).acceptsInteraction = false
|
||||||
window.orderOut(nil)
|
view.setAccessibilityHidden(true)
|
||||||
closed?()
|
closed?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class UninstallReviewContent: NSView {
|
||||||
|
var acceptsInteraction = true
|
||||||
|
|
||||||
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
||||||
|
acceptsInteraction ? super.hitTest(point) : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct ReviewError: LocalizedError {
|
private struct ReviewError: LocalizedError {
|
||||||
let errorDescription: String?
|
let errorDescription: String?
|
||||||
init(_ message: String) { errorDescription = message }
|
init(_ message: String) { errorDescription = message }
|
||||||
|
|||||||
Reference in New Issue
Block a user