style: align notes controls with action pills

This commit is contained in:
Valdemar Lorenzen
2026-10-19 09:51:00 +02:00
parent fcd2d1229f
commit 0b5d27752c
7 changed files with 96 additions and 107 deletions
+4 -4
View File
@@ -2,7 +2,7 @@ import AppKit
@MainActor
final class ActionPill: NSView {
init(primaryButton: NSButton, actionsButton: NSButton) {
init(primaryButton: NSButton, actionsButton: NSButton, primaryCommand: String? = nil) {
super.init(frame: .zero)
wantsLayer = true
layer?.cornerRadius = 18
@@ -14,12 +14,12 @@ final class ActionPill: NSView {
button.font = .systemFont(ofSize: 12, weight: .medium)
button.contentTintColor = SunsetPalette.text
}
primaryButton.keyEquivalent = "\r"
primaryButton.keyEquivalentModifierMask = []
primaryButton.keyEquivalent = primaryCommand ?? "\r"
primaryButton.keyEquivalentModifierMask = primaryCommand == nil ? [] : .command
actionsButton.title = "Actions"
actionsButton.keyEquivalent = "k"
actionsButton.keyEquivalentModifierMask = .command
let returnKey = ShortcutIndicator("↵")
let returnKey = primaryCommand.map { ShortcutIndicator("⌘", $0.uppercased()) } ?? ShortcutIndicator("↵")
let actionKeys = ShortcutIndicator("⌘", "K")
for control in [primaryButton, returnKey, actionsButton, actionKeys] {
control.translatesAutoresizingMaskIntoConstraints = false
+10 -5
View File
@@ -6,6 +6,7 @@ import ApplicationServices
@MainActor
final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
private let searchField = NSSearchField()
private let commandTitle = NSTextField(labelWithString: "Conjure Notes")
private let searchSymbol = IconView(font: .systemFont(ofSize: 20))
private let commandBack = NSButton(title: "", target: nil, action: nil)
private let translationHeader = NSStackView()
@@ -262,6 +263,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
attributes: [.foregroundColor: SunsetPalette.secondaryText, .font: searchFont]
)
searchField.textColor = SunsetPalette.text
commandTitle.font = searchFont
commandTitle.textColor = SunsetPalette.text
commandTitle.isHidden = true
searchField.isBordered = false
searchField.drawsBackground = false
searchField.focusRingType = .none
@@ -288,7 +292,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
commandBack.heightAnchor.constraint(equalToConstant: IconStyle.applicationSize(for: searchFont)),
])
translationHeader.isHidden = true
let header = NSStackView(views: [searchSymbol, commandBack, searchField, translationHeader])
let header = NSStackView(views: [searchSymbol, commandBack, searchField, commandTitle, translationHeader])
header.orientation = .horizontal
header.alignment = .centerY
header.spacing = 12
@@ -899,7 +903,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
self.translation = nil
self.emojiPicker = nil
self.notes = nil
self.searchField.isEnabled = true
self.searchField.isHidden = false
self.commandTitle.isHidden = true
self.activeCommand = nil
self.commandBack.isHidden = true
self.commandBack.target = nil
@@ -935,8 +940,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
)
searchField.setAccessibilityLabel(accessibilityLabel)
if command == .notes {
searchField.stringValue = "Conjure Notes"
searchField.isEnabled = false
searchField.isHidden = true
commandTitle.isHidden = false
}
for view in launcherViews.dropFirst() {
view.isHidden = true
@@ -951,7 +956,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: content.leadingAnchor),
controller.view.trailingAnchor.constraint(equalTo: content.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 16),
controller.view.topAnchor.constraint(equalTo: command == .notes ? commandTitle.bottomAnchor : searchField.bottomAnchor, constant: 16),
controller.view.bottomAnchor.constraint(equalTo: content.bottomAnchor),
])
if let notes { notes.focusEditor() }
+43 -62
View File
@@ -1,12 +1,13 @@
import AppKit
import ConjureKit
import UniformTypeIdentifiers
import Highlighter
@MainActor
final class NotesController: NSViewController, NSTextViewDelegate {
private let notePicker = NSPopUpButton()
private let editor = NotesTextView()
private let createButton = NSButton(title: "New Note", target: nil, action: nil)
private let actionsButton = NSButton(title: "Actions", target: nil, action: nil)
private let noteMenu = NSMenu(title: "Notes")
private let editor = NSTextView()
private let status = NSTextField(labelWithString: "")
private let defaults: UserDefaults
private var store: NotesStore?
@@ -29,16 +30,19 @@ final class NotesController: NSViewController, NSTextViewDelegate {
override func loadView() {
view = NSView()
notePicker.target = self
notePicker.action = #selector(switchNote)
notePicker.setAccessibilityLabel("Choose a note")
let create = NSButton(title: "New Note", target: self, action: #selector(newNote))
create.keyEquivalent = "n"
create.keyEquivalentModifierMask = .command
let image = NSButton(title: "Insert Image…", target: self, action: #selector(insertImage))
let header = NSStackView(views: [notePicker, create, image])
header.spacing = 8
notePicker.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
createButton.target = self
createButton.action = #selector(newNote)
createButton.toolTip = "Create a note (Command-N)"
actionsButton.target = self
actionsButton.action = #selector(showActions)
actionsButton.setAccessibilityLabel("Choose a note")
actionsButton.toolTip = "Choose a note (Command-K)"
actionsButton.menu = noteMenu
let pill = ActionPill(primaryButton: createButton, actionsButton: actionsButton, primaryCommand: "n")
let footer = NSStackView(views: [status, NSView(), pill])
footer.orientation = .horizontal
footer.alignment = .centerY
footer.spacing = 12
editor.delegate = self
editor.isRichText = true
@@ -70,26 +74,24 @@ final class NotesController: NSViewController, NSTextViewDelegate {
status.textColor = SunsetPalette.secondaryText
status.setAccessibilityLabel("Notes save status")
status.lineBreakMode = .byTruncatingTail
for control in [header, scroll, status] {
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
for control in [scroll, footer] {
control.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(control)
}
NSLayoutConstraint.activate([
header.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
header.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
header.topAnchor.constraint(equalTo: view.topAnchor),
notePicker.widthAnchor.constraint(greaterThanOrEqualToConstant: 180),
scroll.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 10),
scroll.leadingAnchor.constraint(equalTo: header.leadingAnchor),
scroll.trailingAnchor.constraint(equalTo: header.trailingAnchor),
scroll.bottomAnchor.constraint(equalTo: status.topAnchor, constant: -8),
status.leadingAnchor.constraint(equalTo: header.leadingAnchor),
status.trailingAnchor.constraint(equalTo: header.trailingAnchor),
status.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -12),
scroll.topAnchor.constraint(equalTo: view.topAnchor),
scroll.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
scroll.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
scroll.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -8),
footer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
footer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8),
footer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8),
footer.heightAnchor.constraint(equalToConstant: 36),
])
guard let store else {
editor.isEditable = false
for control in [notePicker, create, image] { control.isEnabled = false }
for control in [createButton, actionsButton] { control.isEnabled = false }
showError(loadError!)
return
}
@@ -117,8 +119,8 @@ final class NotesController: NSViewController, NSTextViewDelegate {
do {
try store.update(id: selectedID, contents: editor.attributedString())
isDirty = false
updatePicker()
status.stringValue = "Saved locally · Markdown · Paste images or use Insert Image"
updateActions()
status.stringValue = "Saved locally · Markdown · Paste or drop images"
status.textColor = SunsetPalette.secondaryText
return true
} catch {
@@ -138,9 +140,8 @@ final class NotesController: NSViewController, NSTextViewDelegate {
catch { showError(error) }
}
@objc private func switchNote() {
guard let value = notePicker.selectedItem?.representedObject as? UUID else { return }
guard save() else { updatePicker(); return }
@objc private func switchNote(_ sender: NSMenuItem) {
guard let value = sender.representedObject as? UUID, save() else { return }
select(value)
focusEditor()
}
@@ -154,17 +155,18 @@ final class NotesController: NSViewController, NSTextViewDelegate {
editor.isEditable = true
editor.setSelectedRange(NSRange(location: editor.string.utf16.count, length: 0))
MarkdownHighlighting.apply(to: editor)
updatePicker()
status.stringValue = "Saved locally · Markdown · Paste images or use Insert Image"
updateActions()
status.stringValue = "Saved locally · Markdown · Paste or drop images"
status.textColor = SunsetPalette.secondaryText
}
private func updatePicker() {
notePicker.removeAllItems()
private func updateActions() {
noteMenu.removeAllItems()
for note in store?.notes ?? [] {
notePicker.menu?.addItem(withTitle: note.title, action: nil, keyEquivalent: "")
notePicker.lastItem?.representedObject = note.id
if note.id == selectedID { notePicker.select(notePicker.lastItem) }
let item = noteMenu.addItem(withTitle: note.title, action: #selector(switchNote(_:)), keyEquivalent: "")
item.target = self
item.representedObject = note.id
item.state = note.id == selectedID ? .on : .off
}
}
@@ -174,30 +176,9 @@ final class NotesController: NSViewController, NSTextViewDelegate {
status.textColor = .systemRed
}
@objc private func insertImage() {
guard let window = view.window, editor.isEditable else { return }
let panel = NSOpenPanel()
panel.allowedContentTypes = [.image]
panel.canChooseDirectories = false
panel.beginSheetModal(for: window) { [weak self] response in
guard response == .OK, let self, let url = panel.url else { return }
do { try self.editor.insertImage(at: url); self.focusEditor() }
catch { self.showError(error) }
}
}
}
@MainActor
final class NotesTextView: NSTextView {
func insertImage(at url: URL) throws {
guard let image = NSImage(contentsOf: url) else {
throw CocoaError(.fileReadCorruptFile)
}
let attachment = NSTextAttachment()
attachment.attachmentCell = NSTextAttachmentCell(imageCell: image)
attachment.fileWrapper = try FileWrapper(url: url, options: .immediate)
let contents = NSAttributedString(attachment: attachment)
insertText(contents, replacementRange: selectedRange())
@objc private func showActions() {
noteMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: actionsButton.bounds.maxY + 4), in: actionsButton)
focusEditor()
}
}