fix: reload tint colors while running
This commit is contained in:
@@ -2,6 +2,7 @@ import AppKit
|
||||
|
||||
@MainActor
|
||||
final class ActionPill: NSView {
|
||||
private var themeObservation: ThemeObservation?
|
||||
init(primaryButton: NSButton, actionsButton: NSButton, primaryCommand: String? = nil) {
|
||||
super.init(frame: .zero)
|
||||
wantsLayer = true
|
||||
@@ -37,7 +38,46 @@ final class ActionPill: NSView {
|
||||
actionKeys.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
actionKeys.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
|
||||
])
|
||||
themeObservation = ThemeObservation { [weak self, weak primaryButton, weak actionsButton] in
|
||||
self?.layer?.borderColor = ThemePalette.accent.withAlphaComponent(0.35).cgColor
|
||||
self?.layer?.backgroundColor = ThemePalette.background.withAlphaComponent(0.25).cgColor
|
||||
primaryButton?.contentTintColor = ThemePalette.text
|
||||
actionsButton?.contentTintColor = ThemePalette.text
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates action pills programmatically") }
|
||||
}
|
||||
|
||||
/// Keeps palette subscriptions scoped to the existing control's lifetime.
|
||||
@MainActor
|
||||
final class ThemeObservation {
|
||||
nonisolated(unsafe) private let token: NSObjectProtocol
|
||||
|
||||
init(_ apply: @escaping @MainActor () -> Void) {
|
||||
apply()
|
||||
token = NotificationCenter.default.addObserver(
|
||||
forName: ThemePalette.didChangeNotification, object: nil, queue: .main
|
||||
) { _ in
|
||||
MainActor.assumeIsolated { apply() }
|
||||
}
|
||||
}
|
||||
|
||||
deinit { NotificationCenter.default.removeObserver(token) }
|
||||
}
|
||||
|
||||
extension NSSearchField {
|
||||
/// Recolors the current presentation without changing the query or field-editor selection.
|
||||
func applyTheme() {
|
||||
textColor = ThemePalette.text
|
||||
if let placeholderAttributedString {
|
||||
let placeholder = NSMutableAttributedString(attributedString: placeholderAttributedString)
|
||||
placeholder.addAttribute(.foregroundColor, value: ThemePalette.secondaryText,
|
||||
range: NSRange(location: 0, length: placeholder.length))
|
||||
self.placeholderAttributedString = placeholder
|
||||
}
|
||||
if let editor = currentEditor() as? NSTextView {
|
||||
editor.textColor = ThemePalette.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import ConjureKit
|
||||
@MainActor
|
||||
final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
|
||||
private var search: ActionsSearchField!
|
||||
private var themeObservation: ThemeObservation?
|
||||
private var background: LauncherBackground!
|
||||
private var entries: [ActionEntry] = []
|
||||
private var visibleEntries: [Int] = []
|
||||
@@ -74,23 +75,23 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
selectionScroll = nil
|
||||
selectionViewportHeight = nil
|
||||
entries = [
|
||||
ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: ThemePalette.text),
|
||||
ActionEntry(title: "Favorite", symbol: "star", color: ThemePalette.text),
|
||||
ActionEntry(title: "Show in Finder", symbol: "finder", color: ThemePalette.text),
|
||||
ActionEntry(title: "Reset ranking", symbol: "arrow.counterclockwise", color: ThemePalette.text),
|
||||
ActionEntry(title: "Quit application", symbol: "stop.circle", color: ThemePalette.text),
|
||||
ActionEntry(title: "Uninstall application", symbol: "trash", color: ThemePalette.error),
|
||||
ActionEntry(title: "Copy Emoji", symbol: "doc.on.doc", color: ThemePalette.text),
|
||||
ActionEntry(title: "Change source language…", symbol: "character.bubble", color: ThemePalette.text),
|
||||
ActionEntry(title: "Change target language…", symbol: "character.bubble", color: ThemePalette.text),
|
||||
ActionEntry(title: "Swap languages", symbol: "arrow.left.arrow.right", color: ThemePalette.text),
|
||||
ActionEntry(title: "MyMemory (no key)", symbol: "checkmark", color: ThemePalette.text),
|
||||
ActionEntry(title: "OpenAI · gpt-6-luna", symbol: "checkmark", color: ThemePalette.text),
|
||||
ActionEntry(title: "Browse Notes", symbol: "list.bullet", color: ThemePalette.text, commandKey: "P"),
|
||||
ActionEntry(title: "New Note", symbol: "square.and.pencil", color: ThemePalette.text, commandKey: "N"),
|
||||
ActionEntry(title: "Delete Note", symbol: "trash", color: ThemePalette.error),
|
||||
ActionEntry(title: "Copy as Markdown", symbol: "doc.on.doc", color: ThemePalette.text),
|
||||
ActionEntry(title: "Export as Markdown…", symbol: "square.and.arrow.up", color: ThemePalette.text)
|
||||
ActionEntry(title: "Open", symbol: "arrow.up.forward.app"),
|
||||
ActionEntry(title: "Favorite", symbol: "star"),
|
||||
ActionEntry(title: "Show in Finder", symbol: "finder"),
|
||||
ActionEntry(title: "Reset ranking", symbol: "arrow.counterclockwise"),
|
||||
ActionEntry(title: "Quit application", symbol: "stop.circle"),
|
||||
ActionEntry(title: "Uninstall application", symbol: "trash", destructive: true),
|
||||
ActionEntry(title: "Copy Emoji", symbol: "doc.on.doc"),
|
||||
ActionEntry(title: "Change source language…", symbol: "character.bubble"),
|
||||
ActionEntry(title: "Change target language…", symbol: "character.bubble"),
|
||||
ActionEntry(title: "Swap languages", symbol: "arrow.left.arrow.right"),
|
||||
ActionEntry(title: "MyMemory (no key)", symbol: "checkmark"),
|
||||
ActionEntry(title: "OpenAI · gpt-6-luna", symbol: "checkmark"),
|
||||
ActionEntry(title: "Browse Notes", symbol: "list.bullet", commandKey: "P"),
|
||||
ActionEntry(title: "New Note", symbol: "square.and.pencil", commandKey: "N"),
|
||||
ActionEntry(title: "Delete Note", symbol: "trash", destructive: true),
|
||||
ActionEntry(title: "Copy as Markdown", symbol: "doc.on.doc"),
|
||||
ActionEntry(title: "Export as Markdown…", symbol: "square.and.arrow.up")
|
||||
]
|
||||
view = ActionsPanelView()
|
||||
view.wantsLayer = true
|
||||
@@ -212,6 +213,13 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
selectionViewportHeight = viewportHeight
|
||||
}
|
||||
updateSelection()
|
||||
themeObservation = ThemeObservation { [weak self, weak content] in
|
||||
guard let self else { return }
|
||||
self.search.applyTheme()
|
||||
self.applicationLabel.textColor = ThemePalette.secondaryText
|
||||
self.empty.textColor = ThemePalette.secondaryText
|
||||
content?.layer?.borderColor = ThemePalette.secondaryText.withAlphaComponent(0.22).cgColor
|
||||
}
|
||||
}
|
||||
|
||||
func show(for application: Application, relativeTo anchor: NSView) {
|
||||
@@ -558,8 +566,10 @@ private struct ActionEntry {
|
||||
let icon: IconView
|
||||
let returnKey: ShortcutIndicator
|
||||
let hasCommandKey: Bool
|
||||
private var themeObservation: ThemeObservation?
|
||||
|
||||
init(title: String, symbol: String, color: NSColor, commandKey: String? = nil) {
|
||||
init(title: String, symbol: String, destructive: Bool = false, commandKey: String? = nil) {
|
||||
let color = destructive ? ThemePalette.error : ThemePalette.text
|
||||
hasCommandKey = commandKey != nil
|
||||
returnKey = commandKey.map { ShortcutIndicator("⌘", $0) } ?? ShortcutIndicator("↵")
|
||||
let font = NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
|
||||
@@ -590,12 +600,22 @@ private struct ActionEntry {
|
||||
returnKey.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),
|
||||
returnKey.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
||||
])
|
||||
themeObservation = ThemeObservation { [weak button, weak icon] in
|
||||
let color = destructive ? ThemePalette.error : ThemePalette.text
|
||||
button?.contentTintColor = color
|
||||
if let button {
|
||||
let title = NSMutableAttributedString(attributedString: button.attributedTitle)
|
||||
title.addAttribute(.foregroundColor, value: color, range: NSRange(location: 0, length: title.length))
|
||||
button.attributedTitle = title
|
||||
}
|
||||
icon?.contentTintColor = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class SelectionCell: NSView {
|
||||
let entry = ActionEntry(title: "", symbol: "checkmark", color: ThemePalette.text)
|
||||
let entry = ActionEntry(title: "", symbol: "checkmark")
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
@@ -6,6 +6,7 @@ import ConjureKit
|
||||
struct ConjureApp {
|
||||
static func main() {
|
||||
let application = NSApplication.shared
|
||||
ThemePalette.startObserving()
|
||||
let delegate = ApplicationDelegate()
|
||||
application.delegate = delegate
|
||||
application.setActivationPolicy(.accessory)
|
||||
|
||||
@@ -8,6 +8,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
private let category = NSPopUpButton()
|
||||
private let status = NSTextField(labelWithString: "")
|
||||
private let empty = NSTextField(labelWithString: "No matching emoji")
|
||||
private var themeObservation: ThemeObservation?
|
||||
private let copyButton = NSButton(title: "Copy and Paste", target: nil, action: nil)
|
||||
private let actionsButton = NSButton(title: "Actions", target: nil, action: nil)
|
||||
private let actions = ApplicationActionsController()
|
||||
@@ -93,6 +94,11 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
footer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8),
|
||||
footer.heightAnchor.constraint(equalToConstant: 36),
|
||||
])
|
||||
themeObservation = ThemeObservation { [weak self] in
|
||||
self?.category.contentTintColor = ThemePalette.secondaryText
|
||||
self?.empty.textColor = ThemePalette.secondaryText
|
||||
self?.status.textColor = ThemePalette.secondaryText
|
||||
}
|
||||
inputChanged()
|
||||
}
|
||||
|
||||
@@ -308,6 +314,9 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
@MainActor
|
||||
private final class EmojiSectionHeader: NSView {
|
||||
private let label = NSTextField(labelWithString: "")
|
||||
private var themeObservation: ThemeObservation?
|
||||
private var title = ""
|
||||
private var count = 0
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
@@ -317,11 +326,17 @@ private final class EmojiSectionHeader: NSView {
|
||||
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
||||
])
|
||||
themeObservation = ThemeObservation { [weak self] in
|
||||
guard let self else { return }
|
||||
self.setTitle(self.title, count: self.count)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates emoji headers programmatically") }
|
||||
|
||||
func setTitle(_ title: String, count: Int) {
|
||||
self.title = title
|
||||
self.count = count
|
||||
let text = NSMutableAttributedString(string: title, attributes: [
|
||||
.font: NSFont.systemFont(ofSize: 13), .foregroundColor: ThemePalette.secondaryText,
|
||||
])
|
||||
@@ -348,7 +363,6 @@ private final class EmojiItem: NSCollectionViewItem {
|
||||
}
|
||||
|
||||
func highlight(_ selected: Bool) {
|
||||
button.layer?.backgroundColor = (selected ? ThemePalette.selectionFill : ThemePalette.text.withAlphaComponent(0.045)).cgColor
|
||||
button.setAccessibilitySelected(selected)
|
||||
button.selected = selected
|
||||
}
|
||||
@@ -361,6 +375,14 @@ private final class EmojiButton: NSButton {
|
||||
var selected = false { didSet { updateBorder() } }
|
||||
private var hovered = false { didSet { updateBorder() } }
|
||||
private var hoverTracking: NSTrackingArea?
|
||||
private var themeObservation: ThemeObservation?
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
themeObservation = ThemeObservation { [weak self] in self?.updateBorder() }
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates emoji buttons programmatically") }
|
||||
|
||||
override func updateTrackingAreas() {
|
||||
super.updateTrackingAreas()
|
||||
@@ -377,6 +399,7 @@ private final class EmojiButton: NSButton {
|
||||
override func mouseExited(with event: NSEvent) { hovered = false }
|
||||
|
||||
private func updateBorder() {
|
||||
layer?.backgroundColor = (selected ? ThemePalette.selectionFill : ThemePalette.text.withAlphaComponent(0.045)).cgColor
|
||||
layer?.borderWidth = selected || hovered ? 0.5 : 0
|
||||
layer?.borderColor = (selected ? ThemePalette.accent.withAlphaComponent(0.8)
|
||||
: ThemePalette.secondaryText.withAlphaComponent(0.35)).cgColor
|
||||
|
||||
@@ -58,9 +58,21 @@ private final class AlignmentGuideView: NSView {
|
||||
var snapsHorizontally = false
|
||||
var snapsVertically = false
|
||||
private static let dash: [CGFloat] = [1, 5]
|
||||
private static let muted = ThemePalette.accent.withAlphaComponent(0.4).cgColor
|
||||
private static let aligned = ThemePalette.accent.withAlphaComponent(0.95).cgColor
|
||||
private var muted = ThemePalette.accent.withAlphaComponent(0.4).cgColor
|
||||
private var aligned = ThemePalette.accent.withAlphaComponent(0.95).cgColor
|
||||
private static let shadow = NSColor.black.withAlphaComponent(0.6).cgColor
|
||||
private var themeObservation: ThemeObservation?
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
themeObservation = ThemeObservation { [weak self] in
|
||||
self?.muted = ThemePalette.accent.withAlphaComponent(0.4).cgColor
|
||||
self?.aligned = ThemePalette.accent.withAlphaComponent(0.95).cgColor
|
||||
self?.needsDisplay = true
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates alignment guides programmatically") }
|
||||
|
||||
override func hitTest(_ point: NSPoint) -> NSView? { nil }
|
||||
|
||||
@@ -75,14 +87,14 @@ private final class AlignmentGuideView: NSView {
|
||||
context.setLineDash(phase: 0, lengths: Self.dash)
|
||||
context.setShadow(offset: .zero, blur: 2, color: Self.shadow)
|
||||
|
||||
context.setStrokeColor(snapsHorizontally ? Self.aligned : Self.muted)
|
||||
context.setStrokeColor(snapsHorizontally ? aligned : muted)
|
||||
context.move(to: NSPoint(x: targetFrame.minX, y: bounds.minY))
|
||||
context.addLine(to: NSPoint(x: targetFrame.minX, y: bounds.maxY))
|
||||
context.move(to: NSPoint(x: targetFrame.maxX, y: bounds.minY))
|
||||
context.addLine(to: NSPoint(x: targetFrame.maxX, y: bounds.maxY))
|
||||
context.strokePath()
|
||||
|
||||
context.setStrokeColor(snapsVertically ? Self.aligned : Self.muted)
|
||||
context.setStrokeColor(snapsVertically ? aligned : muted)
|
||||
context.move(to: NSPoint(x: bounds.minX, y: targetFrame.maxY))
|
||||
context.addLine(to: NSPoint(x: bounds.maxX, y: targetFrame.maxY))
|
||||
context.strokePath()
|
||||
|
||||
@@ -2,39 +2,131 @@ import AppKit
|
||||
|
||||
@MainActor
|
||||
enum ThemePalette {
|
||||
private static let loaded: (scheme: ColorScheme, error: String?) = {
|
||||
struct State {
|
||||
let scheme: ColorScheme
|
||||
var error: String?
|
||||
var usesFallback: Bool
|
||||
let background: NSColor
|
||||
let selectionFill: NSColor
|
||||
let selectionBorder: NSColor
|
||||
let keyBackground: NSColor
|
||||
|
||||
init(scheme: ColorScheme, error: String? = nil, usesFallback: Bool = false) {
|
||||
self.scheme = scheme
|
||||
self.error = error
|
||||
self.usesFallback = usesFallback
|
||||
background = scheme.background.withAlphaComponent(0.55)
|
||||
selectionFill = scheme.accent.withAlphaComponent(0.12)
|
||||
selectionBorder = scheme.accent.withAlphaComponent(0.30)
|
||||
keyBackground = scheme.keyBackground.withAlphaComponent(0.5)
|
||||
}
|
||||
}
|
||||
|
||||
static var loaded: State = {
|
||||
do {
|
||||
return (try ColorScheme.load(from: ColorScheme.configurationDirectory), nil)
|
||||
return State(scheme: try ColorScheme.load(from: ColorScheme.configurationDirectory))
|
||||
} catch {
|
||||
return (.sunset, "Color configuration: \(error.localizedDescription)\nUsing the built-in sunset palette. Fix the file and restart Conjure.")
|
||||
return State(scheme: .sunset,
|
||||
error: "Color configuration: \(error.localizedDescription)\nUsing the built-in sunset palette. Fix the file to apply changes.",
|
||||
usesFallback: true)
|
||||
}
|
||||
}()
|
||||
static let configurationError = loaded.error
|
||||
static let background = loaded.scheme.background.withAlphaComponent(0.55)
|
||||
static let text = loaded.scheme.text
|
||||
static let secondaryText = loaded.scheme.secondaryText
|
||||
static let accent = loaded.scheme.accent
|
||||
static let selectionFill = accent.withAlphaComponent(0.12)
|
||||
static let selectionBorder = accent.withAlphaComponent(0.30)
|
||||
static let glow = loaded.scheme.glow
|
||||
static let keyBackground = loaded.scheme.keyBackground.withAlphaComponent(0.5)
|
||||
static let error = loaded.scheme.error
|
||||
private static var observation: ColorSchemeObservation?
|
||||
|
||||
static let didChangeNotification = Notification.Name("ConjureThemePaletteDidChange")
|
||||
static var configurationError: String? { loaded.error }
|
||||
static var background: NSColor { loaded.background }
|
||||
static var text: NSColor { loaded.scheme.text }
|
||||
static var secondaryText: NSColor { loaded.scheme.secondaryText }
|
||||
static var accent: NSColor { loaded.scheme.accent }
|
||||
static var selectionFill: NSColor { loaded.selectionFill }
|
||||
static var selectionBorder: NSColor { loaded.selectionBorder }
|
||||
static var glow: NSColor { loaded.scheme.glow }
|
||||
static var keyBackground: NSColor { loaded.keyBackground }
|
||||
static var error: NSColor { loaded.scheme.error }
|
||||
|
||||
static func startObserving() {
|
||||
guard observation == nil else { return }
|
||||
let directory = ColorScheme.configurationDirectory
|
||||
observation = ColorSchemeObservation(directory: directory) {
|
||||
let previousError = configurationError
|
||||
reload(from: directory)
|
||||
if let diagnostic = configurationError, diagnostic != previousError {
|
||||
NSLog("%@", diagnostic)
|
||||
}
|
||||
}
|
||||
reload(from: directory)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func reload(from directory: URL = ColorScheme.configurationDirectory) -> Bool {
|
||||
do {
|
||||
let scheme = try ColorScheme.load(from: directory)
|
||||
let previous = loaded.scheme
|
||||
guard scheme.background != previous.background || scheme.text != previous.text
|
||||
|| scheme.secondaryText != previous.secondaryText || scheme.accent != previous.accent
|
||||
|| scheme.glow != previous.glow || scheme.keyBackground != previous.keyBackground
|
||||
|| scheme.error != previous.error else {
|
||||
loaded.error = nil
|
||||
loaded.usesFallback = false
|
||||
return false
|
||||
}
|
||||
loaded = State(scheme: scheme)
|
||||
NotificationCenter.default.post(name: didChangeNotification, object: nil)
|
||||
return true
|
||||
} catch {
|
||||
let recovery = loaded.usesFallback
|
||||
? "Using the built-in sunset palette."
|
||||
: "Keeping the last valid palette."
|
||||
loaded.error = "Color configuration: \(error.localizedDescription)\n\(recovery) Fix the file to apply changes."
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class LauncherBackground: NSView {
|
||||
weak var canvas: NSView?
|
||||
private let amber = NSGradient(colors: [
|
||||
private var amber = NSGradient(colors: [
|
||||
ThemePalette.accent.withAlphaComponent(0.18),
|
||||
ThemePalette.accent.withAlphaComponent(0.065),
|
||||
ThemePalette.accent.withAlphaComponent(0),
|
||||
])!
|
||||
private let ember = NSGradient(colors: [
|
||||
private var ember = NSGradient(colors: [
|
||||
ThemePalette.glow.withAlphaComponent(0.26),
|
||||
ThemePalette.glow.withAlphaComponent(0.10),
|
||||
ThemePalette.glow.withAlphaComponent(0),
|
||||
])!
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
observePalette()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
observePalette()
|
||||
}
|
||||
|
||||
private func observePalette() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(paletteChanged),
|
||||
name: ThemePalette.didChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
@objc private func paletteChanged() {
|
||||
amber = NSGradient(colors: [
|
||||
ThemePalette.accent.withAlphaComponent(0.18),
|
||||
ThemePalette.accent.withAlphaComponent(0.065),
|
||||
ThemePalette.accent.withAlphaComponent(0),
|
||||
])!
|
||||
ember = NSGradient(colors: [
|
||||
ThemePalette.glow.withAlphaComponent(0.26),
|
||||
ThemePalette.glow.withAlphaComponent(0.10),
|
||||
ThemePalette.glow.withAlphaComponent(0),
|
||||
])!
|
||||
needsDisplay = true
|
||||
}
|
||||
|
||||
override func hitTest(_ point: NSPoint) -> NSView? { nil }
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import ApplicationServices
|
||||
@MainActor
|
||||
final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
|
||||
private let searchField = NSSearchField()
|
||||
private var themeObservation: ThemeObservation?
|
||||
private let searchSymbol = IconView(font: .systemFont(ofSize: 20))
|
||||
private let commandBack = NSButton(title: "", target: nil, action: nil)
|
||||
private let translationHeader = NSStackView()
|
||||
@@ -400,6 +401,17 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
window.initialFirstResponder = searchField
|
||||
launcherViews = [header, sectionLabel, scrollView, emptyLabel, footer]
|
||||
themeObservation = ThemeObservation { [weak self, weak content] in
|
||||
guard let self else { return }
|
||||
self.searchField.applyTheme()
|
||||
(self.searchField.currentEditor() as? NSTextView)?.insertionPointColor = ThemePalette.accent
|
||||
self.searchSymbol.contentTintColor = ThemePalette.accent
|
||||
self.commandBack.contentTintColor = ThemePalette.secondaryText
|
||||
for label in [self.sectionLabel, self.statusLabel, self.emptyLabel] {
|
||||
label.textColor = ThemePalette.secondaryText
|
||||
}
|
||||
content?.layer?.borderColor = ThemePalette.secondaryText.withAlphaComponent(0.22).cgColor
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshApplications() {
|
||||
@@ -1015,12 +1027,28 @@ final class ApplicationCell: NSTableCellView {
|
||||
private static let opaqueMask = [NSColor.black.cgColor, NSColor.black.cgColor]
|
||||
private static let fadedMask = [NSColor.black.cgColor, NSColor.clear.cgColor]
|
||||
private static let timing = CAMediaTimingFunction(name: .easeOut)
|
||||
static let applicationKind = NSAttributedString(string: "Application", attributes: [
|
||||
private static var kindColor = ThemePalette.secondaryText
|
||||
private static var cachedApplicationKind = NSAttributedString(string: "Application", attributes: [
|
||||
.font: NSFont.systemFont(ofSize: LauncherRowView.textSize), .foregroundColor: ThemePalette.secondaryText
|
||||
])
|
||||
static let commandKind = NSAttributedString(string: "Command", attributes: applicationKind.attributes(at: 0, effectiveRange: nil))
|
||||
static let favoriteKind = favorited(applicationKind)
|
||||
static let favoriteCommandKind = favorited(commandKind)
|
||||
private static var cachedCommandKind = NSAttributedString(string: "Command", attributes: cachedApplicationKind.attributes(at: 0, effectiveRange: nil))
|
||||
private static var cachedFavoriteKind = favorited(cachedApplicationKind)
|
||||
private static var cachedFavoriteCommandKind = favorited(cachedCommandKind)
|
||||
static var applicationKind: NSAttributedString { refreshKinds(); return cachedApplicationKind }
|
||||
static var commandKind: NSAttributedString { refreshKinds(); return cachedCommandKind }
|
||||
static var favoriteKind: NSAttributedString { refreshKinds(); return cachedFavoriteKind }
|
||||
static var favoriteCommandKind: NSAttributedString { refreshKinds(); return cachedFavoriteCommandKind }
|
||||
|
||||
private static func refreshKinds() {
|
||||
guard kindColor != ThemePalette.secondaryText else { return }
|
||||
kindColor = ThemePalette.secondaryText
|
||||
cachedApplicationKind = NSAttributedString(string: "Application", attributes: [
|
||||
.font: NSFont.systemFont(ofSize: LauncherRowView.textSize), .foregroundColor: kindColor
|
||||
])
|
||||
cachedCommandKind = NSAttributedString(string: "Command", attributes: cachedApplicationKind.attributes(at: 0, effectiveRange: nil))
|
||||
cachedFavoriteKind = favorited(cachedApplicationKind)
|
||||
cachedFavoriteCommandKind = favorited(cachedCommandKind)
|
||||
}
|
||||
|
||||
private static func favorited(_ kind: NSAttributedString) -> NSAttributedString {
|
||||
let font = kind.attribute(.font, at: 0, effectiveRange: nil) as! NSFont
|
||||
@@ -1041,12 +1069,13 @@ final class ApplicationCell: NSTableCellView {
|
||||
let locationLabel = NSTextField(labelWithString: "")
|
||||
let kindLabel = NSTextField(labelWithString: "Application")
|
||||
let numberHint = ShortcutIndicator(
|
||||
"1", backgroundColor: ThemePalette.keyBackground,
|
||||
"1", hasBackground: true,
|
||||
fontSize: 10, keySize: 20
|
||||
)
|
||||
private let kindFade = CAGradientLayer()
|
||||
private var numberHintVisible = false
|
||||
private var hintGeneration = 0
|
||||
private var themeObservation: ThemeObservation?
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
@@ -1094,10 +1123,28 @@ final class ApplicationCell: NSTableCellView {
|
||||
numberHint.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||
numberHint.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
])
|
||||
themeObservation = ThemeObservation { [weak self] in self?.applyTheme() }
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates application cells programmatically") }
|
||||
|
||||
private func applyTheme() {
|
||||
Self.refreshKinds()
|
||||
textField?.textColor = ThemePalette.text
|
||||
locationLabel.textColor = ThemePalette.secondaryText
|
||||
let kind = kindLabel.stringValue
|
||||
let favorite = kindLabel.attributedStringValue.length > 0
|
||||
&& kindLabel.attributedStringValue.attribute(.attachment, at: 0, effectiveRange: nil) != nil
|
||||
if kind.hasSuffix("Application") {
|
||||
kindLabel.attributedStringValue = favorite ? Self.favoriteKind : Self.applicationKind
|
||||
} else if kind.hasSuffix("Command") {
|
||||
kindLabel.attributedStringValue = favorite ? Self.favoriteCommandKind : Self.commandKind
|
||||
} else {
|
||||
kindLabel.textColor = ThemePalette.secondaryText
|
||||
}
|
||||
if imageView?.contentTintColor != nil { imageView?.contentTintColor = ThemePalette.accent }
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
updateKindFadeGeometry()
|
||||
@@ -1400,6 +1447,14 @@ final class LauncherRowView: NSTableRowView {
|
||||
static let rowHeight: CGFloat = 38
|
||||
static let textSize: CGFloat = 14
|
||||
static let iconToTextSpacing: CGFloat = 12
|
||||
private var themeObservation: ThemeObservation?
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
themeObservation = ThemeObservation { [weak self] in self?.needsDisplay = true }
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates result rows programmatically") }
|
||||
var topSpacing: CGFloat = 0 {
|
||||
didSet { needsLayout = true; needsDisplay = true }
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
private let actions = ApplicationActionsController()
|
||||
private let editor = NotesTextView()
|
||||
private let status = NSTextField(labelWithString: "")
|
||||
private var themeObservation: ThemeObservation?
|
||||
private let noteTitle = NSTextField(labelWithString: "Untitled Note")
|
||||
private let vimButton = NSButton(title: "Vim Off", target: nil, action: nil)
|
||||
private let formatting = NSStackView()
|
||||
@@ -185,6 +186,15 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 80),
|
||||
footer.heightAnchor.constraint(equalToConstant: 36),
|
||||
])
|
||||
themeObservation = ThemeObservation { [weak self] in
|
||||
guard let self else { return }
|
||||
self.noteTitle.textColor = ThemePalette.secondaryText
|
||||
self.vimButton.contentTintColor = ThemePalette.secondaryText
|
||||
self.status.textColor = ThemePalette.error
|
||||
for case let button as NSButton in self.formatting.arrangedSubviews {
|
||||
button.contentTintColor = ThemePalette.secondaryText
|
||||
}
|
||||
}
|
||||
guard let store else {
|
||||
editor.isEditable = false
|
||||
for control in [browseButton, actionsButton] { control.isEnabled = false }
|
||||
@@ -371,11 +381,12 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
alert.informativeText = "This permanently deletes the note and its embedded images. This cannot be undone."
|
||||
alert.addButton(withTitle: "Cancel").keyEquivalent = "\u{1b}"
|
||||
let delete = alert.addButton(withTitle: "Delete Note")
|
||||
delete.contentTintColor = ThemePalette.error
|
||||
let theme = ThemeObservation { [weak delete] in delete?.contentTintColor = ThemePalette.error }
|
||||
delete.keyEquivalent = "\r"
|
||||
alert.window.initialFirstResponder = delete
|
||||
alert.window.defaultButtonCell = delete.cell as? NSButtonCell
|
||||
alert.beginSheetModal(for: window) { [weak self] response in
|
||||
alert.beginSheetModal(for: window) { [weak self, theme] response in
|
||||
defer { withExtendedLifetime(theme) {} }
|
||||
guard let self else { return }
|
||||
defer { self.focusEditor() }
|
||||
guard response == .alertSecondButtonReturn, self.selectedID == id,
|
||||
|
||||
@@ -17,6 +17,60 @@ final class NotesTextView: NSTextView {
|
||||
private var markerColumn: CGFloat = 22
|
||||
private var checkboxes: [NSButton] = []
|
||||
private var listParagraphs: [(range: NSRange, kind: String, depth: Int, number: Int, checked: Bool)] = []
|
||||
private var themeObservation: ThemeObservation?
|
||||
|
||||
override init(frame frameRect: NSRect = .zero) {
|
||||
super.init(frame: frameRect)
|
||||
}
|
||||
|
||||
override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) {
|
||||
super.init(frame: frameRect, textContainer: container)
|
||||
themeObservation = ThemeObservation { [weak self] in self?.applyTheme() }
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates note editors programmatically") }
|
||||
|
||||
/// Only visual attributes change here; semantic marks, selection and undo sessions stay intact.
|
||||
private func applyTheme() {
|
||||
insertionPointColor = ThemePalette.accent
|
||||
selectedTextAttributes = [.backgroundColor: ThemePalette.selectionFill]
|
||||
let typing = currentTypingAttributes
|
||||
if let storage = textStorage {
|
||||
let full = NSRange(location: 0, length: storage.length)
|
||||
edits.disableUndoRegistration()
|
||||
storage.beginEditing()
|
||||
storage.enumerateAttributes(in: full) { attributes, range, _ in
|
||||
let codeBlock = attributes[NoteAttributes.block] as? String == "code"
|
||||
let inlineCode = attributes[NoteAttributes.inlineCode] as? Bool == true
|
||||
storage.addAttribute(.foregroundColor,
|
||||
value: inlineCode || attributes[.link] != nil ? ThemePalette.accent : ThemePalette.text,
|
||||
range: range)
|
||||
if codeBlock || inlineCode {
|
||||
storage.addAttribute(.backgroundColor, value: ThemePalette.selectionFill, range: range)
|
||||
}
|
||||
}
|
||||
applyCodeColors(to: storage)
|
||||
storage.endEditing()
|
||||
edits.enableUndoRegistration()
|
||||
}
|
||||
setTypingAttributes(typing)
|
||||
for button in checkboxes { button.contentTintColor = ThemePalette.accent }
|
||||
needsDisplay = true
|
||||
}
|
||||
|
||||
private func applyCodeColors(to storage: NSTextStorage) {
|
||||
let source = storage.string as NSString
|
||||
storage.enumerateAttribute(NoteAttributes.block, in: NSRange(location: 0, length: storage.length)) { value, range, _ in
|
||||
guard value as? String == "code" else { return }
|
||||
let code = source.substring(with: range)
|
||||
let language = storage.attribute(NoteAttributes.language, at: range.location, effectiveRange: nil) as? String
|
||||
guard let highlighted = Self.highlighter.highlight(code, as: language), highlighted.string == code else { return }
|
||||
highlighted.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlighted.length)) { value, run, _ in
|
||||
if let value { storage.addAttribute(.foregroundColor, value: value,
|
||||
range: NSRange(location: range.location + run.location, length: run.length)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
var vimState = NotesVimState() {
|
||||
didSet { if oldValue.mode != vimState.mode { needsDisplay = true } }
|
||||
}
|
||||
@@ -517,16 +571,7 @@ final class NotesTextView: NSTextView {
|
||||
}
|
||||
index = NSMaxRange(row)
|
||||
}
|
||||
storage.enumerateAttribute(NoteAttributes.block, in: full) { value, range, _ in
|
||||
guard value as? String == "code" else { return }
|
||||
let code = source.substring(with: range)
|
||||
let language = storage.attribute(NoteAttributes.language, at: range.location, effectiveRange: nil) as? String
|
||||
guard let highlighted = Self.highlighter.highlight(code, as: language), highlighted.string == code else { return }
|
||||
highlighted.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlighted.length)) { value, run, _ in
|
||||
if let value { storage.addAttribute(.foregroundColor, value: value,
|
||||
range: NSRange(location: range.location + run.location, length: run.length)) }
|
||||
}
|
||||
}
|
||||
applyCodeColors(to: storage)
|
||||
storage.enumerateAttribute(.attachment, in: full) { value, _, _ in
|
||||
guard let attachment = value as? NSTextAttachment,
|
||||
let cell = attachment.attachmentCell as? NSTextAttachmentCell, let image = cell.image else { return }
|
||||
|
||||
@@ -19,6 +19,8 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
private let recoveryLabel = NSTextField(wrappingLabelWithString: "")
|
||||
private let recoveryButton = NSButton(title: "Retry uninstall recovery…", target: nil, action: nil)
|
||||
private var recoveryReport: UninstallReport?
|
||||
private var themeObservation: ThemeObservation?
|
||||
private var loginStatusIsError = false
|
||||
var retryUninstallRecovery: (() async -> UninstallReport)?
|
||||
|
||||
init(shortcuts: GlobalShortcuts) {
|
||||
@@ -197,6 +199,18 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
for label in [instructions, statusLabel, errorLabel, spotlight, loginStatus, recoveryLabel] {
|
||||
label.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
||||
}
|
||||
themeObservation = ThemeObservation { [weak self] in
|
||||
guard let self else { return }
|
||||
for label in [heading, loginHeading, translationHeading] { label.textColor = ThemePalette.text }
|
||||
for label in [instructions, self.statusLabel, spotlight] { label.textColor = ThemePalette.secondaryText }
|
||||
self.errorLabel.textColor = ThemePalette.error
|
||||
self.loginStatus.textColor = self.loginStatusIsError ? ThemePalette.error : ThemePalette.secondaryText
|
||||
self.recoveryLabel.textColor = self.recoveryReport?.needsRecovery == true ? ThemePalette.error : ThemePalette.secondaryText
|
||||
for button in [self.recorder, self.commandSpaceButton, self.clearButton, keyboardSettings,
|
||||
self.loginToggle, self.loginSettings, self.recoveryButton] {
|
||||
button.contentTintColor = ThemePalette.accent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setUninstallRecoveryReport(_ report: UninstallReport) {
|
||||
@@ -230,6 +244,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
private func refreshLogin() {
|
||||
let status = SMAppService.mainApp.status
|
||||
loginStatus.textColor = ThemePalette.secondaryText
|
||||
loginStatusIsError = false
|
||||
loginToggle.state = (status == .enabled || status == .requiresApproval) ? .on : .off
|
||||
loginSettings.isHidden = status != .requiresApproval
|
||||
switch status {
|
||||
@@ -244,6 +259,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
@unknown default:
|
||||
loginStatus.stringValue = "Launch at login status is unavailable."
|
||||
loginStatus.textColor = ThemePalette.error
|
||||
loginStatusIsError = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +276,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
refreshLogin()
|
||||
loginStatus.stringValue = "Could not change launch at login: \(error.localizedDescription)"
|
||||
loginStatus.textColor = ThemePalette.error
|
||||
loginStatusIsError = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import CoreText
|
||||
|
||||
@MainActor
|
||||
final class ShortcutIndicator: NSStackView {
|
||||
init(_ keys: String..., backgroundColor: NSColor? = nil, fontSize: CGFloat = 12, keySize: CGFloat = 18) {
|
||||
private var themeObservation: ThemeObservation?
|
||||
init(_ keys: String..., hasBackground: Bool = false, fontSize: CGFloat = 12, keySize: CGFloat = 18) {
|
||||
super.init(frame: .zero)
|
||||
orientation = .horizontal
|
||||
alignment = .centerY
|
||||
@@ -21,7 +22,7 @@ final class ShortcutIndicator: NSStackView {
|
||||
badge.layer?.cornerRadius = 5
|
||||
badge.layer?.borderWidth = 1
|
||||
badge.layer?.borderColor = ThemePalette.secondaryText.withAlphaComponent(0.35).cgColor
|
||||
badge.layer?.backgroundColor = backgroundColor?.cgColor
|
||||
badge.layer?.backgroundColor = hasBackground ? ThemePalette.keyBackground.cgColor : nil
|
||||
badge.setAccessibilityElement(false)
|
||||
badge.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
@@ -30,6 +31,15 @@ final class ShortcutIndicator: NSStackView {
|
||||
])
|
||||
addArrangedSubview(badge)
|
||||
}
|
||||
themeObservation = ThemeObservation { [weak self] in
|
||||
guard let self else { return }
|
||||
for case let badge as NSTextField in self.arrangedSubviews {
|
||||
badge.textColor = ThemePalette.secondaryText
|
||||
badge.layer?.borderColor = ThemePalette.secondaryText.withAlphaComponent(0.35).cgColor
|
||||
badge.layer?.backgroundColor = hasBackground ? ThemePalette.keyBackground.cgColor : nil
|
||||
badge.needsDisplay = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setSingleKey(_ key: String) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import AppKit
|
||||
import Darwin
|
||||
|
||||
@MainActor
|
||||
final class ColorSchemeObservation {
|
||||
private struct Watch: @unchecked Sendable {
|
||||
let source: any DispatchSourceFileSystemObject
|
||||
let device: dev_t
|
||||
let inode: ino_t
|
||||
}
|
||||
|
||||
private let directory: URL
|
||||
private let onChange: @MainActor () -> Void
|
||||
private var watches: [String: Watch] = [:]
|
||||
|
||||
init(directory: URL, onChange: @escaping @MainActor () -> Void) {
|
||||
self.directory = directory.standardizedFileURL
|
||||
self.onChange = onChange
|
||||
refreshWatches()
|
||||
}
|
||||
|
||||
deinit {
|
||||
for watch in watches.values { watch.source.cancel() }
|
||||
}
|
||||
|
||||
private func filesChanged() {
|
||||
refreshWatches()
|
||||
onChange()
|
||||
}
|
||||
|
||||
private func refreshWatches() {
|
||||
var ancestor = directory
|
||||
var isDirectory: ObjCBool = false
|
||||
while !FileManager.default.fileExists(atPath: ancestor.path, isDirectory: &isDirectory)
|
||||
|| !isDirectory.boolValue {
|
||||
let parent = ancestor.deletingLastPathComponent()
|
||||
guard parent.path != ancestor.path else { break }
|
||||
ancestor = parent
|
||||
}
|
||||
let urls = [ancestor, ancestor.deletingLastPathComponent(),
|
||||
directory.appendingPathComponent("config.toml"), directory.appendingPathComponent("theme.json")]
|
||||
var obsolete = Set(watches.keys)
|
||||
for url in urls {
|
||||
let path = url.path
|
||||
let descriptor = open(path, O_EVTONLY | O_CLOEXEC)
|
||||
guard descriptor >= 0 else { continue }
|
||||
var identity = stat()
|
||||
guard fstat(descriptor, &identity) == 0 else {
|
||||
close(descriptor)
|
||||
continue
|
||||
}
|
||||
obsolete.remove(path)
|
||||
if let existing = watches[path], existing.device == identity.st_dev, existing.inode == identity.st_ino {
|
||||
close(descriptor)
|
||||
continue
|
||||
}
|
||||
watches.removeValue(forKey: path)?.source.cancel()
|
||||
let source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor,
|
||||
eventMask: [.write, .extend, .delete, .rename, .attrib, .revoke], queue: .main)
|
||||
source.setEventHandler { [weak self] in
|
||||
Task { @MainActor [weak self] in self?.filesChanged() }
|
||||
}
|
||||
source.setCancelHandler { close(descriptor) }
|
||||
watches[path] = Watch(source: source, device: identity.st_dev, inode: identity.st_ino)
|
||||
source.resume()
|
||||
}
|
||||
for path in obsolete { watches.removeValue(forKey: path)?.source.cancel() }
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,9 @@ final class TranslationController: NSViewController {
|
||||
private let output = NSTextView()
|
||||
private let settingsButton = NSButton(title: "Open Translate Settings", target: nil, action: nil)
|
||||
private let status = NSTextField(wrappingLabelWithString: "")
|
||||
private var statusIsError = false
|
||||
private var outputSecondaryRange: NSRange?
|
||||
private var themeObservation: ThemeObservation?
|
||||
private let translateButton = NSButton(title: "Translate", target: nil, action: nil)
|
||||
private var task: Task<Void, Never>?
|
||||
var input: (() -> String)?
|
||||
@@ -179,6 +182,24 @@ final class TranslationController: NSViewController {
|
||||
footer.heightAnchor.constraint(equalToConstant: 36),
|
||||
scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 100),
|
||||
])
|
||||
themeObservation = ThemeObservation { [weak self, weak chevron] in
|
||||
guard let self else { return }
|
||||
self.providerButton.contentTintColor = ThemePalette.text
|
||||
self.languageButton.contentTintColor = ThemePalette.text
|
||||
chevron?.contentTintColor = ThemePalette.secondaryText
|
||||
self.stylePill(self.providerControl)
|
||||
self.sourcePreview.textColor = ThemePalette.secondaryText
|
||||
self.status.textColor = self.statusIsError ? ThemePalette.error : ThemePalette.secondaryText
|
||||
if let storage = self.output.textStorage {
|
||||
storage.beginEditing()
|
||||
storage.addAttribute(.foregroundColor, value: ThemePalette.text,
|
||||
range: NSRange(location: 0, length: storage.length))
|
||||
if let range = self.outputSecondaryRange {
|
||||
storage.addAttribute(.foregroundColor, value: ThemePalette.secondaryText, range: range)
|
||||
}
|
||||
storage.endEditing()
|
||||
}
|
||||
}
|
||||
inputChanged()
|
||||
}
|
||||
|
||||
@@ -266,6 +287,7 @@ final class TranslationController: NSViewController {
|
||||
func inputChanged() {
|
||||
cancelRequest()
|
||||
output.string = ""
|
||||
outputSecondaryRange = nil
|
||||
let isEmpty = (input?() ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
if textToTranslate != nil {
|
||||
translateButton.title = "Translate"
|
||||
@@ -307,6 +329,7 @@ final class TranslationController: NSViewController {
|
||||
}
|
||||
|
||||
private func setStatus(_ message: String, isError: Bool = false) {
|
||||
statusIsError = isError
|
||||
status.stringValue = message
|
||||
status.textColor = isError ? ThemePalette.error : ThemePalette.secondaryText
|
||||
}
|
||||
@@ -362,6 +385,7 @@ final class TranslationController: NSViewController {
|
||||
}
|
||||
}
|
||||
output.string = ""
|
||||
outputSecondaryRange = nil
|
||||
setStatus("Translating…")
|
||||
translateButton.isEnabled = false
|
||||
let myMemoryTranslate = self.myMemory
|
||||
@@ -379,6 +403,7 @@ final class TranslationController: NSViewController {
|
||||
.foregroundColor: ThemePalette.text,
|
||||
])
|
||||
if !myMemory, let separator = translation.range(of: "\n\n") {
|
||||
self.outputSecondaryRange = NSRange(separator.upperBound..<translation.endIndex, in: translation)
|
||||
rendered.addAttributes([
|
||||
.foregroundColor: ThemePalette.secondaryText,
|
||||
.font: NSFont.systemFont(ofSize: 14),
|
||||
|
||||
@@ -4,6 +4,8 @@ import AppKit
|
||||
final class TranslationSettingsController: NSViewController {
|
||||
private let keyField = NSSecureTextField()
|
||||
private let status = NSTextField(wrappingLabelWithString: "")
|
||||
private var statusIsError = false
|
||||
private var themeObservation: ThemeObservation?
|
||||
private let credentials: TranslationController.Credentials
|
||||
|
||||
init(credentials: TranslationController.Credentials = .keychain) {
|
||||
@@ -55,6 +57,14 @@ final class TranslationSettingsController: NSViewController {
|
||||
for label in [status, myMemory, openAI] {
|
||||
label.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
||||
}
|
||||
themeObservation = ThemeObservation { [weak self] in
|
||||
save.contentTintColor = ThemePalette.accent
|
||||
forget.contentTintColor = ThemePalette.accent
|
||||
myMemory.textColor = ThemePalette.secondaryText
|
||||
openAI.textColor = ThemePalette.secondaryText
|
||||
guard let self else { return }
|
||||
self.status.textColor = self.statusIsError ? ThemePalette.error : ThemePalette.secondaryText
|
||||
}
|
||||
do {
|
||||
setStatus(try credentials.load() == nil ? "No OpenAI API key saved." : "OpenAI API key saved in Keychain.")
|
||||
} catch { setStatus(error.localizedDescription, isError: true) }
|
||||
@@ -84,6 +94,7 @@ final class TranslationSettingsController: NSViewController {
|
||||
}
|
||||
|
||||
private func setStatus(_ message: String, isError: Bool = false) {
|
||||
statusIsError = isError
|
||||
status.stringValue = message
|
||||
status.textColor = isError ? ThemePalette.error : ThemePalette.secondaryText
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
private let status = NSTextField(wrappingLabelWithString: "Finding associated files…")
|
||||
private let remove = NSButton(title: "Uninstall application", target: nil, action: nil)
|
||||
private let back = NSButton(title: "Back", target: nil, action: nil)
|
||||
private var themeObservation: ThemeObservation?
|
||||
private var statusIsError = false
|
||||
private var candidates: [UninstallCandidate] = []
|
||||
private var visible: [UninstallCandidate] = []
|
||||
private var selected: Set<URL> = []
|
||||
@@ -176,6 +178,18 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
footer.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -8),
|
||||
footer.heightAnchor.constraint(equalToConstant: 36)
|
||||
])
|
||||
themeObservation = ThemeObservation { [weak self, weak context, weak guidance, weak pill] in
|
||||
guard let self else { return }
|
||||
context?.textColor = ThemePalette.text
|
||||
guidance?.textColor = ThemePalette.secondaryText
|
||||
self.search.applyTheme()
|
||||
(self.search.currentEditor() as? NSTextView)?.insertionPointColor = ThemePalette.accent
|
||||
self.status.textColor = self.statusIsError ? ThemePalette.error : ThemePalette.secondaryText
|
||||
self.back.contentTintColor = ThemePalette.secondaryText
|
||||
self.remove.contentTintColor = ThemePalette.error
|
||||
pill?.layer?.borderColor = ThemePalette.accent.withAlphaComponent(0.35).cgColor
|
||||
pill?.layer?.backgroundColor = ThemePalette.background.withAlphaComponent(0.25).cgColor
|
||||
}
|
||||
}
|
||||
|
||||
func applyDiscoveryResult(_ result: Result<[UninstallCandidate], Error>, preservingSelection: Bool = false) {
|
||||
@@ -205,6 +219,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
|
||||
private func updateStatus() {
|
||||
status.textColor = ThemePalette.secondaryText
|
||||
statusIsError = false
|
||||
remove.isEnabled = false
|
||||
switch discoveryState {
|
||||
case .loading:
|
||||
@@ -212,6 +227,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
case .failed(let message):
|
||||
status.stringValue = message
|
||||
status.textColor = ThemePalette.error
|
||||
statusIsError = true
|
||||
case .ready:
|
||||
let chosen = candidates.filter { selected.contains($0.url) }
|
||||
let size = chosen.compactMap(\.size).reduce(Int64(0), +)
|
||||
@@ -260,7 +276,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
cell.toggle.isEnabled = candidate.isSelectable && !busy
|
||||
cell.toggle.setAccessibilityLabel("Select \(candidate.url.lastPathComponent)")
|
||||
cell.name.stringValue = candidate.url.lastPathComponent
|
||||
cell.name.textColor = candidate.isSelectable ? ThemePalette.text : ThemePalette.secondaryText
|
||||
cell.selectable = candidate.isSelectable
|
||||
let path = (candidate.url.deletingLastPathComponent().path as NSString).abbreviatingWithTildeInPath
|
||||
cell.detail.stringValue = candidate.reason.map { "\(path) · \($0)" } ?? path
|
||||
cell.size.stringValue = candidate.size.map { ByteCountFormatter.string(fromByteCount: $0, countStyle: .file) } ?? "—"
|
||||
@@ -319,6 +335,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
}
|
||||
do {
|
||||
status.textColor = ThemePalette.secondaryText
|
||||
statusIsError = false
|
||||
status.stringValue = "Waiting for application to quit…"
|
||||
for app in running where !app.isTerminated {
|
||||
guard app.terminate() else { throw ReviewError("\(app.localizedName ?? application.name) could not quit. No files were moved.") }
|
||||
@@ -369,6 +386,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
if canRecover { alert.addButton(withTitle: "Retry recovery") }
|
||||
guard await alert.beginSheetModal(for: window) == .alertSecondButtonReturn, canRecover else { return }
|
||||
status.textColor = ThemePalette.secondaryText
|
||||
statusIsError = false
|
||||
status.stringValue = "Recovering pending uninstall…"
|
||||
let report = await uninstaller.recover()
|
||||
changed?()
|
||||
@@ -425,6 +443,14 @@ private final class UninstallFileCell: NSTableCellView {
|
||||
let detail = NSTextField(labelWithString: "")
|
||||
let size = NSTextField(labelWithString: "")
|
||||
let fileIcon: IconView
|
||||
private var themeObservation: ThemeObservation?
|
||||
var selectable = true { didSet { applyTheme() } }
|
||||
|
||||
private func applyTheme() {
|
||||
name.textColor = selectable ? ThemePalette.text : ThemePalette.secondaryText
|
||||
detail.textColor = ThemePalette.secondaryText
|
||||
size.textColor = ThemePalette.secondaryText
|
||||
}
|
||||
|
||||
init() {
|
||||
let font = NSFont.systemFont(ofSize: 14, weight: .medium)
|
||||
@@ -456,6 +482,7 @@ private final class UninstallFileCell: NSTableCellView {
|
||||
row.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
||||
row.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4)
|
||||
])
|
||||
themeObservation = ThemeObservation { [weak self] in self?.applyTheme() }
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates uninstall rows programmatically") }
|
||||
|
||||
Reference in New Issue
Block a user