style: use shared notes browsing and actions panels
This commit is contained in:
@@ -33,17 +33,26 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
var selectProvider: ((Bool) -> Void)?
|
||||
private var currentProviderEntry: Int?
|
||||
var selectLanguage: ((String) -> Void)?
|
||||
private struct LanguageChoice {
|
||||
let code: String
|
||||
let name: String
|
||||
var selectNote: ((UUID) -> Void)?
|
||||
var browseNotes: (() -> Void)?
|
||||
var newNote: (() -> Void)?
|
||||
private enum ChoiceID: Equatable {
|
||||
case language(String)
|
||||
case note(UUID)
|
||||
}
|
||||
private struct SelectionChoice {
|
||||
let id: ChoiceID
|
||||
let title: String
|
||||
let searchText: FuzzySearchText
|
||||
}
|
||||
private var languages: [LanguageChoice]?
|
||||
private var matchingLanguages: [LanguageChoice] = []
|
||||
private var currentLanguage: String?
|
||||
private var languageTable: NSTableView?
|
||||
private var languageScroll: NSScrollView?
|
||||
private var languageViewportHeight: NSLayoutConstraint?
|
||||
private var choices: [SelectionChoice]?
|
||||
private var matchingChoices: [SelectionChoice] = []
|
||||
private var currentChoice: ChoiceID?
|
||||
private var selectionTable: NSTableView?
|
||||
private var selectionScroll: NSScrollView?
|
||||
private var selectionViewportHeight: NSLayoutConstraint?
|
||||
private var selectionAccessibilityLabel = ""
|
||||
private var currentChoiceAccessibilityLabel = ""
|
||||
var closed: (() -> Void)?
|
||||
var isShown: Bool { viewIfLoaded?.superview != nil && !isClosing }
|
||||
|
||||
@@ -58,9 +67,9 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
empty = NSTextField(labelWithString: "No matching actions")
|
||||
applicationLabel = NSTextField(labelWithString: "")
|
||||
rowTopConstraints = []
|
||||
languageTable = nil
|
||||
languageScroll = nil
|
||||
languageViewportHeight = nil
|
||||
selectionTable = nil
|
||||
selectionScroll = nil
|
||||
selectionViewportHeight = nil
|
||||
entries = [
|
||||
ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Favorite", symbol: "star", color: SunsetPalette.text),
|
||||
@@ -73,7 +82,9 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
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)
|
||||
ActionEntry(title: "OpenAI · gpt-6-luna", symbol: "checkmark", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Browse Notes", symbol: "list.bullet", color: SunsetPalette.text, commandKey: "P"),
|
||||
ActionEntry(title: "New Note", symbol: "square.and.pencil", color: SunsetPalette.text, commandKey: "N")
|
||||
]
|
||||
view = ActionsPanelView()
|
||||
view.wantsLayer = true
|
||||
@@ -108,6 +119,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
search.sendsSearchStringImmediately = true
|
||||
search.setAccessibilityLabel("Search application actions")
|
||||
search.closeActions = { [weak self] in self?.close() }
|
||||
search.commandKey = { [weak self] key in self?.handleCommandKey(key) ?? false }
|
||||
empty.textColor = SunsetPalette.secondaryText
|
||||
empty.font = .systemFont(ofSize: 14)
|
||||
empty.isHidden = true
|
||||
@@ -156,11 +168,11 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
search.heightAnchor.constraint(equalToConstant: 24)
|
||||
])
|
||||
content.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor
|
||||
if languages != nil {
|
||||
if choices != nil {
|
||||
height.isActive = false
|
||||
let table = NSTableView()
|
||||
table.style = .plain
|
||||
table.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Language")))
|
||||
table.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Selection")))
|
||||
table.headerView = nil
|
||||
table.backgroundColor = .clear
|
||||
table.selectionHighlightStyle = .none
|
||||
@@ -172,8 +184,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.target = self
|
||||
table.action = #selector(chooseLanguageRow)
|
||||
table.setAccessibilityLabel("Translation languages")
|
||||
table.action = #selector(chooseSelectionRow)
|
||||
table.setAccessibilityLabel(selectionAccessibilityLabel)
|
||||
let scroll = NSScrollView()
|
||||
scroll.documentView = table
|
||||
scroll.hasVerticalScroller = true
|
||||
@@ -189,9 +201,9 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
scroll.bottomAnchor.constraint(equalTo: search.topAnchor, constant: -8),
|
||||
viewportHeight,
|
||||
])
|
||||
languageTable = table
|
||||
languageScroll = scroll
|
||||
languageViewportHeight = viewportHeight
|
||||
selectionTable = table
|
||||
selectionScroll = scroll
|
||||
selectionViewportHeight = viewportHeight
|
||||
}
|
||||
updateSelection()
|
||||
}
|
||||
@@ -223,22 +235,42 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
|
||||
func showLanguages(_ choices: [(code: String, name: String)], selected: String,
|
||||
source: Bool, relativeTo anchor: NSView) {
|
||||
currentLanguage = selected
|
||||
currentChoice = .language(selected)
|
||||
selectionAccessibilityLabel = "Translation languages"
|
||||
currentChoiceAccessibilityLabel = "Current language"
|
||||
show(title: source ? "Source language" : "Target language", relativeTo: anchor, available: [],
|
||||
searchAccessibilityLabel: source ? "Search source languages" : "Search target languages",
|
||||
languageChoices: choices.map { LanguageChoice(code: $0.code, name: $0.name, searchText: FuzzySearchText($0.name)) })
|
||||
selectionChoices: choices.map {
|
||||
SelectionChoice(id: .language($0.code), title: $0.name, searchText: FuzzySearchText($0.name))
|
||||
}, selectionName: "languages")
|
||||
}
|
||||
|
||||
func showNotes(_ notes: [(id: UUID, title: String)], selected: UUID?, relativeTo anchor: NSView) {
|
||||
currentChoice = selected.map { .note($0) }
|
||||
selectionAccessibilityLabel = "Notes"
|
||||
currentChoiceAccessibilityLabel = "Current note"
|
||||
show(title: "Browse Notes", relativeTo: anchor, available: [],
|
||||
searchAccessibilityLabel: "Search notes",
|
||||
selectionChoices: notes.map {
|
||||
SelectionChoice(id: .note($0.id), title: $0.title, searchText: FuzzySearchText($0.title))
|
||||
}, selectionName: "notes")
|
||||
}
|
||||
|
||||
func showNotesActions(relativeTo anchor: NSView) {
|
||||
show(title: "Notes", relativeTo: anchor, available: [12, 13],
|
||||
searchAccessibilityLabel: "Search notes actions")
|
||||
}
|
||||
|
||||
private func show(title: String, relativeTo anchor: NSView, available: [Int], emoji: Bool = false,
|
||||
searchAccessibilityLabel: String = "Search application actions", belowAnchor: Bool = false,
|
||||
languageChoices: [LanguageChoice]? = nil) {
|
||||
selectionChoices: [SelectionChoice]? = nil, selectionName: String = "") {
|
||||
if viewIfLoaded?.superview != nil {
|
||||
if isShown, availableEntries == available, applicationLabel.stringValue == title { close(); return }
|
||||
close(animated: false)
|
||||
}
|
||||
guard let window = anchor.window, let host = window.contentView else { return }
|
||||
let pill = anchor.superview ?? anchor
|
||||
languages = languageChoices
|
||||
choices = selectionChoices
|
||||
// Reuse behavior, not native drawing state from a differently sized popup.
|
||||
if isViewLoaded { preparePresentation() } else { _ = view }
|
||||
applicationLabel.stringValue = title
|
||||
@@ -248,7 +280,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
entries[0].button.setAccessibilityLabel(openTitle)
|
||||
entries[0].icon.image = NSImage(systemSymbolName: emoji ? "doc.on.clipboard" : "arrow.up.forward.app", accessibilityDescription: nil)
|
||||
search.setAccessibilityLabel(searchAccessibilityLabel)
|
||||
empty.stringValue = languages == nil ? "No matching actions" : "No matching languages"
|
||||
empty.stringValue = choices == nil ? "No matching actions" : "No matching \(selectionName)"
|
||||
let favorite = entries[1]
|
||||
let favoriteTitle = isFavorite ? "Remove Favorite" : "Favorite"
|
||||
favorite.button.attributedTitle = NSAttributedString(string: favoriteTitle, attributes: [
|
||||
@@ -260,7 +292,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
entries[index].icon.isHidden = currentProviderEntry != index
|
||||
}
|
||||
search.placeholderAttributedString = NSAttributedString(
|
||||
string: languages != nil ? "Search for languages…" : belowAnchor ? "Search for providers…" : "Search for actions…",
|
||||
string: choices != nil ? "Search for \(selectionName)…" : belowAnchor ? "Search for providers…" : "Search for actions…",
|
||||
attributes: [.foregroundColor: SunsetPalette.secondaryText,
|
||||
.font: NSFont.systemFont(ofSize: LauncherRowView.textSize)])
|
||||
search.stringValue = ""
|
||||
@@ -287,7 +319,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
coveredPill?.setAccessibilityHidden(true)
|
||||
host.layoutSubtreeIfNeeded()
|
||||
background.needsDisplay = true
|
||||
if let selectedEntry { languageTable?.scrollRowToVisible(selectedEntry) }
|
||||
if let selectedEntry { selectionTable?.scrollRowToVisible(selectedEntry) }
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? 0 : 0.16
|
||||
view.animator().alphaValue = 1
|
||||
@@ -345,16 +377,16 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
|
||||
func controlTextDidChange(_ notification: Notification) {
|
||||
let query = search.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let languages, let table = languageTable {
|
||||
matchingLanguages = FuzzySearchText.results(for: query, in: languages, text: \.searchText) {
|
||||
$0.name.localizedStandardCompare($1.name) == .orderedAscending
|
||||
if let choices, let table = selectionTable {
|
||||
matchingChoices = FuzzySearchText.results(for: query, in: choices, text: \.searchText) {
|
||||
$0.title.localizedStandardCompare($1.title) == .orderedAscending
|
||||
}
|
||||
let selection = (query.isEmpty ? matchingLanguages.firstIndex(where: { $0.code == currentLanguage }) : nil)
|
||||
?? (matchingLanguages.isEmpty ? nil : 0)
|
||||
let selection = (query.isEmpty ? matchingChoices.firstIndex(where: { $0.id == currentChoice }) : nil)
|
||||
?? (matchingChoices.isEmpty ? nil : 0)
|
||||
for entry in entries { entry.view.isHidden = true }
|
||||
languageViewportHeight?.constant = CGFloat(max(min(matchingLanguages.count, 6), 1)) * table.rowHeight
|
||||
empty.isHidden = !matchingLanguages.isEmpty
|
||||
languageScroll?.isHidden = matchingLanguages.isEmpty
|
||||
selectionViewportHeight?.constant = CGFloat(max(min(matchingChoices.count, 6), 1)) * table.rowHeight
|
||||
empty.isHidden = !matchingChoices.isEmpty
|
||||
selectionScroll?.isHidden = matchingChoices.isEmpty
|
||||
view.superview?.layoutSubtreeIfNeeded()
|
||||
table.reloadData()
|
||||
selectedEntry = selection
|
||||
@@ -380,9 +412,9 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
}
|
||||
|
||||
func moveSelection(by offset: Int) {
|
||||
if let table = languageTable {
|
||||
guard !matchingLanguages.isEmpty else { return }
|
||||
let next = min(max((selectedEntry ?? 0) + offset, 0), matchingLanguages.count - 1)
|
||||
if let table = selectionTable {
|
||||
guard !matchingChoices.isEmpty else { return }
|
||||
let next = min(max((selectedEntry ?? 0) + offset, 0), matchingChoices.count - 1)
|
||||
table.selectRowIndexes(IndexSet(integer: next), byExtendingSelection: false)
|
||||
table.scrollRowToVisible(next)
|
||||
return
|
||||
@@ -394,10 +426,10 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
}
|
||||
|
||||
private func updateSelection() {
|
||||
if let table = languageTable {
|
||||
if let table = selectionTable {
|
||||
let visible = table.rows(in: table.visibleRect)
|
||||
for row in visible.location..<NSMaxRange(visible) {
|
||||
if let cell = table.view(atColumn: 0, row: row, makeIfNecessary: false) as? LanguageCell {
|
||||
if let cell = table.view(atColumn: 0, row: row, makeIfNecessary: false) as? SelectionCell {
|
||||
cell.entry.view.isSelected = selectedEntry == row
|
||||
cell.entry.returnKey.isHidden = selectedEntry != row
|
||||
}
|
||||
@@ -407,7 +439,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
for index in entries.indices {
|
||||
let selected = selectedEntry == index
|
||||
entries[index].view.isSelected = selected
|
||||
entries[index].returnKey.isHidden = !selected
|
||||
entries[index].returnKey.isHidden = !selected && !entries[index].hasCommandKey
|
||||
let current = currentProviderEntry == index ? ", Current provider" : ""
|
||||
entries[index].button.setAccessibilityValue((selected ? "Selected" : "Not selected") + current)
|
||||
}
|
||||
@@ -431,32 +463,42 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
return true
|
||||
}
|
||||
|
||||
func numberOfRows(in tableView: NSTableView) -> Int { matchingLanguages.count }
|
||||
private func handleCommandKey(_ key: String) -> Bool {
|
||||
guard isShown, availableEntries == [12, 13] else { return false }
|
||||
switch key {
|
||||
case "p": close(); browseNotes?()
|
||||
case "n": close(); newNote?()
|
||||
default: return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func numberOfRows(in tableView: NSTableView) -> Int { matchingChoices.count }
|
||||
|
||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||
let identifier = NSUserInterfaceItemIdentifier("LanguageCell")
|
||||
let cell = tableView.makeView(withIdentifier: identifier, owner: self) as? LanguageCell ?? LanguageCell()
|
||||
let identifier = NSUserInterfaceItemIdentifier("SelectionCell")
|
||||
let cell = tableView.makeView(withIdentifier: identifier, owner: self) as? SelectionCell ?? SelectionCell()
|
||||
cell.identifier = identifier
|
||||
let choice = matchingLanguages[row]
|
||||
cell.entry.button.title = choice.name
|
||||
cell.entry.button.setAccessibilityLabel(choice.name)
|
||||
cell.entry.button.setAccessibilityValue(choice.code == currentLanguage ? "Current language" : "")
|
||||
let choice = matchingChoices[row]
|
||||
cell.entry.button.title = choice.title
|
||||
cell.entry.button.setAccessibilityLabel(choice.title)
|
||||
cell.entry.button.setAccessibilityValue(choice.id == currentChoice ? currentChoiceAccessibilityLabel : "")
|
||||
cell.entry.button.target = self
|
||||
cell.entry.button.action = #selector(chooseButton(_:))
|
||||
cell.entry.button.tag = row
|
||||
cell.entry.icon.isHidden = choice.code != currentLanguage
|
||||
cell.entry.icon.isHidden = choice.id != currentChoice
|
||||
cell.entry.view.isSelected = selectedEntry == row
|
||||
cell.entry.returnKey.isHidden = selectedEntry != row
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableViewSelectionDidChange(_ notification: Notification) {
|
||||
guard let table = languageTable else { return }
|
||||
guard let table = selectionTable else { return }
|
||||
selectedEntry = table.selectedRow >= 0 ? table.selectedRow : nil
|
||||
updateSelection()
|
||||
}
|
||||
|
||||
@objc private func chooseLanguageRow() {
|
||||
@objc private func chooseSelectionRow() {
|
||||
if let selectedEntry { chooseAction(at: selectedEntry) }
|
||||
}
|
||||
|
||||
@@ -468,11 +510,14 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
|
||||
private func chooseAction(at index: Int) {
|
||||
guard isShown else { return }
|
||||
if languages != nil {
|
||||
guard matchingLanguages.indices.contains(index) else { return }
|
||||
let code = matchingLanguages[index].code
|
||||
if choices != nil {
|
||||
guard matchingChoices.indices.contains(index) else { return }
|
||||
let id = matchingChoices[index].id
|
||||
close()
|
||||
selectLanguage?(code)
|
||||
switch id {
|
||||
case .language(let code): selectLanguage?(code)
|
||||
case .note(let id): selectNote?(id)
|
||||
}
|
||||
return
|
||||
}
|
||||
guard visibleEntries.contains(index) else { return }
|
||||
@@ -490,6 +535,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
case 9: swapLanguages?()
|
||||
case 10: selectProvider?(false)
|
||||
case 11: selectProvider?(true)
|
||||
case 12: browseNotes?()
|
||||
case 13: newNote?()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
@@ -500,9 +547,12 @@ private struct ActionEntry {
|
||||
let view = LauncherRowView()
|
||||
let button: NSButton
|
||||
let icon: IconView
|
||||
let returnKey = ShortcutIndicator("↵")
|
||||
let returnKey: ShortcutIndicator
|
||||
let hasCommandKey: Bool
|
||||
|
||||
init(title: String, symbol: String, color: NSColor) {
|
||||
init(title: String, symbol: String, color: NSColor, commandKey: String? = nil) {
|
||||
hasCommandKey = commandKey != nil
|
||||
returnKey = commandKey.map { ShortcutIndicator("⌘", $0) } ?? ShortcutIndicator("↵")
|
||||
let font = NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
|
||||
icon = IconView(font: font)
|
||||
button = NSButton(title: title, target: nil, action: nil)
|
||||
@@ -535,7 +585,7 @@ private struct ActionEntry {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class LanguageCell: NSView {
|
||||
private final class SelectionCell: NSView {
|
||||
let entry = ActionEntry(title: "", symbol: "checkmark", color: SunsetPalette.text)
|
||||
|
||||
init() {
|
||||
@@ -550,7 +600,7 @@ private final class LanguageCell: NSView {
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates language rows programmatically") }
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates selection rows programmatically") }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -565,12 +615,16 @@ private final class ActionsPanelView: NSView {
|
||||
@MainActor
|
||||
private final class ActionsSearchField: NSSearchField {
|
||||
var closeActions: (() -> Void)?
|
||||
var commandKey: ((String) -> Bool)?
|
||||
|
||||
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||||
if event.modifierFlags.intersection([.command, .control, .option, .shift]) == .command,
|
||||
event.charactersIgnoringModifiers?.lowercased() == "k" {
|
||||
closeActions?()
|
||||
return true
|
||||
let key = event.charactersIgnoringModifiers?.lowercased() {
|
||||
if key == "k" {
|
||||
closeActions?()
|
||||
return true
|
||||
}
|
||||
if commandKey?(key) == true { return true }
|
||||
}
|
||||
return super.performKeyEquivalent(with: event)
|
||||
}
|
||||
|
||||
@@ -98,8 +98,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
else { self.moveSelection(by: offset) }
|
||||
}
|
||||
window.commandNavigation = { [weak self] key in
|
||||
self?.translation?.handleControlKey(key) ?? self?.emojiPicker?.handleControlKey(key) ?? false
|
||||
self?.notes?.handleControlKey(key) ?? self?.translation?.handleControlKey(key)
|
||||
?? self?.emojiPicker?.handleControlKey(key) ?? false
|
||||
}
|
||||
window.commandShortcut = { [weak self] key in self?.notes?.handleCommandKey(key) ?? false }
|
||||
window.usesTextNavigation = { [weak self] in self?.notes != nil }
|
||||
window.dismiss = { [weak self] in self?.dismiss() }
|
||||
window.escape = { [weak self] in self?.handleEscape() }
|
||||
@@ -476,7 +478,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
|
||||
private func handleEscape() {
|
||||
let selector = #selector(NSResponder.cancelOperation(_:))
|
||||
if actions.handle(selector) || translation?.handle(selector) == true || emojiPicker?.handle(selector) == true { return }
|
||||
if actions.handle(selector) || notes?.handle(selector) == true
|
||||
|| translation?.handle(selector) == true || emojiPicker?.handle(selector) == true { return }
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -485,6 +488,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
handleEscape()
|
||||
return true
|
||||
}
|
||||
if let notes { return notes.handle(commandSelector) }
|
||||
if let emojiPicker { return emojiPicker.handle(commandSelector) }
|
||||
if let translation { return translation.handle(commandSelector) }
|
||||
switch commandSelector {
|
||||
@@ -807,8 +811,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
|
||||
@discardableResult
|
||||
private func closeCommand() -> Bool {
|
||||
guard saveNotes() else { return false }
|
||||
notes?.closed?()
|
||||
guard notes?.close() != false else { return false }
|
||||
translation?.close()
|
||||
emojiPicker?.close()
|
||||
return true
|
||||
@@ -1200,6 +1203,7 @@ final class LauncherPanel: NSPanel {
|
||||
var numberHintsChanged: (() -> Void)?
|
||||
var launchNumber: ((Int) -> Void)?
|
||||
var commandNavigation: ((String) -> Bool)?
|
||||
var commandShortcut: ((String) -> Bool)?
|
||||
var usesTextNavigation: (() -> Bool)?
|
||||
private var numberShortcuts = CommandNumberShortcuts()
|
||||
private var commandHoldTask: Task<Void, Never>?
|
||||
@@ -1304,18 +1308,20 @@ final class LauncherPanel: NSPanel {
|
||||
}
|
||||
resetNumberHints(modifierFlags: event.modifierFlags)
|
||||
}
|
||||
if event.type == .keyDown, attachedSheet == nil, usesTextNavigation?() != true,
|
||||
if event.type == .keyDown, attachedSheet == nil,
|
||||
event.modifierFlags.intersection([.control, .command, .option, .shift]) == .control {
|
||||
if commandNavigation?(event.charactersIgnoringModifiers?.lowercased() ?? "") == true { return }
|
||||
switch event.charactersIgnoringModifiers?.lowercased() {
|
||||
case "j":
|
||||
navigateSelection?(1)
|
||||
return
|
||||
case "k":
|
||||
navigateSelection?(-1)
|
||||
return
|
||||
default:
|
||||
break
|
||||
if usesTextNavigation?() != true {
|
||||
switch event.charactersIgnoringModifiers?.lowercased() {
|
||||
case "j":
|
||||
navigateSelection?(1)
|
||||
return
|
||||
case "k":
|
||||
navigateSelection?(-1)
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
super.sendEvent(event)
|
||||
@@ -1324,6 +1330,15 @@ final class LauncherPanel: NSPanel {
|
||||
}
|
||||
}
|
||||
|
||||
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||||
if attachedSheet == nil,
|
||||
event.modifierFlags.intersection([.command, .control, .option, .shift]) == .command,
|
||||
commandShortcut?(event.charactersIgnoringModifiers?.lowercased() ?? "") == true {
|
||||
return true
|
||||
}
|
||||
return super.performKeyEquivalent(with: event)
|
||||
}
|
||||
|
||||
override func cancelOperation(_ sender: Any?) {
|
||||
if let escape { escape() } else { dismiss?() }
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import Highlighter
|
||||
|
||||
@MainActor
|
||||
final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
private let createButton = NSButton(title: "New Note", target: nil, action: nil)
|
||||
private let browseButton = NSButton(title: "Browse Notes", target: nil, action: nil)
|
||||
private let actionsButton = NSButton(title: "Actions", target: nil, action: nil)
|
||||
private let noteMenu = NSMenu(title: "Notes")
|
||||
private let actions = ApplicationActionsController()
|
||||
private let editor = NSTextView()
|
||||
private let status = NSTextField(labelWithString: "")
|
||||
private let defaults: UserDefaults
|
||||
@@ -29,16 +29,22 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates Notes programmatically") }
|
||||
|
||||
override func loadView() {
|
||||
view = NSView()
|
||||
createButton.target = self
|
||||
createButton.action = #selector(newNote)
|
||||
createButton.toolTip = "Create a note (Command-N)"
|
||||
let content = NotesView()
|
||||
content.commandShortcut = { [weak self] in self?.handleCommandKey($0) ?? false }
|
||||
view = content
|
||||
browseButton.target = self
|
||||
browseButton.action = #selector(browseNotes)
|
||||
browseButton.setAccessibilityLabel("Browse notes")
|
||||
browseButton.toolTip = "Browse notes (Command-P)"
|
||||
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")
|
||||
actionsButton.setAccessibilityLabel("Notes actions")
|
||||
actionsButton.toolTip = "Notes actions (Command-K)"
|
||||
actions.closed = { [weak self] in self?.focusEditor() }
|
||||
actions.selectNote = { [weak self] in self?.switchNote($0) }
|
||||
actions.newNote = { [weak self] in self?.newNote() }
|
||||
actions.browseNotes = { [weak self] in self?.browseNotes() }
|
||||
let pill = ActionPill(primaryButton: browseButton, actionsButton: actionsButton, primaryCommand: "p")
|
||||
let footer = NSStackView(views: [status, NSView(), pill])
|
||||
footer.orientation = .horizontal
|
||||
footer.alignment = .centerY
|
||||
@@ -91,7 +97,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
])
|
||||
guard let store else {
|
||||
editor.isEditable = false
|
||||
for control in [createButton, actionsButton] { control.isEnabled = false }
|
||||
for control in [browseButton, actionsButton] { control.isEnabled = false }
|
||||
showError(loadError!)
|
||||
return
|
||||
}
|
||||
@@ -119,7 +125,6 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
do {
|
||||
try store.update(id: selectedID, contents: editor.attributedString())
|
||||
isDirty = false
|
||||
updateActions()
|
||||
status.stringValue = "Saved locally · Markdown · Paste or drop images"
|
||||
status.textColor = SunsetPalette.secondaryText
|
||||
return true
|
||||
@@ -129,20 +134,50 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func goBack() {
|
||||
guard save() else { return }
|
||||
func handle(_ selector: Selector) -> Bool {
|
||||
actions.isShown && actions.handle(selector)
|
||||
}
|
||||
|
||||
func handleControlKey(_ key: String) -> Bool {
|
||||
guard actions.isShown, key == "j" || key == "k" else { return false }
|
||||
actions.moveSelection(by: key == "j" ? 1 : -1)
|
||||
return true
|
||||
}
|
||||
|
||||
func handleCommandKey(_ key: String) -> Bool {
|
||||
guard store != nil else { return false }
|
||||
switch key {
|
||||
case "p": browseNotes()
|
||||
case "k": showActions()
|
||||
case "n": newNote()
|
||||
default: return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func close() -> Bool {
|
||||
guard save() else { return false }
|
||||
actions.close(animated: false)
|
||||
closed?()
|
||||
return true
|
||||
}
|
||||
|
||||
func goBack() {
|
||||
if actions.isShown { actions.close() }
|
||||
else { _ = close() }
|
||||
}
|
||||
|
||||
@objc private func newNote() {
|
||||
actions.close(animated: false)
|
||||
guard save(), let store else { return }
|
||||
do { select(try store.create()); focusEditor() }
|
||||
catch { showError(error) }
|
||||
}
|
||||
|
||||
@objc private func switchNote(_ sender: NSMenuItem) {
|
||||
guard let value = sender.representedObject as? UUID, save() else { return }
|
||||
select(value)
|
||||
private func switchNote(_ id: UUID) {
|
||||
guard save() else { return }
|
||||
select(id)
|
||||
focusEditor()
|
||||
}
|
||||
|
||||
@@ -155,20 +190,10 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
editor.isEditable = true
|
||||
editor.setSelectedRange(NSRange(location: editor.string.utf16.count, length: 0))
|
||||
MarkdownHighlighting.apply(to: editor)
|
||||
updateActions()
|
||||
status.stringValue = "Saved locally · Markdown · Paste or drop images"
|
||||
status.textColor = SunsetPalette.secondaryText
|
||||
}
|
||||
|
||||
private func updateActions() {
|
||||
noteMenu.removeAllItems()
|
||||
for note in store?.notes ?? [] {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private func showError(_ error: Error) {
|
||||
status.stringValue = "Couldn’t save or load notes: \(error.localizedDescription)"
|
||||
@@ -176,9 +201,28 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
status.textColor = .systemRed
|
||||
}
|
||||
|
||||
@objc private func showActions() {
|
||||
noteMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: actionsButton.bounds.maxY + 4), in: actionsButton)
|
||||
focusEditor()
|
||||
@objc func browseNotes() {
|
||||
guard let store else { return }
|
||||
actions.showNotes(store.notes.map { (id: $0.id, title: $0.title) },
|
||||
selected: selectedID, relativeTo: browseButton)
|
||||
}
|
||||
|
||||
@objc func showActions() {
|
||||
guard store != nil else { return }
|
||||
actions.showNotesActions(relativeTo: actionsButton)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class NotesView: NSView {
|
||||
var commandShortcut: ((String) -> Bool)?
|
||||
|
||||
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||||
if event.modifierFlags.intersection([.command, .control, .option, .shift]) == .command,
|
||||
commandShortcut?(event.charactersIgnoringModifiers?.lowercased() ?? "") == true {
|
||||
return true
|
||||
}
|
||||
return super.performKeyEquivalent(with: event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user