From 920961947392591f66096527966a14f4c57e599d Mon Sep 17 00:00:00 2001 From: jess Date: Mon, 6 Apr 2026 15:38:11 -0700 Subject: [PATCH] keyboard shortcuts, menu items, and header zoom scaling --- src/AppDelegate.swift | 92 +++++++++++++++++++++++++++++++++++++++++-- src/ContentView.swift | 4 ++ src/EditorView.swift | 89 +++++++++++++++++++++++++++++++++-------- 3 files changed, 166 insertions(+), 19 deletions(-) diff --git a/src/AppDelegate.swift b/src/AppDelegate.swift index c37a226..5e380f4 100644 --- a/src/AppDelegate.swift +++ b/src/AppDelegate.swift @@ -3,12 +3,22 @@ import Combine import SwiftUI import UniformTypeIdentifiers +class WindowController { + let window: NSWindow + let appState: AppState + init(window: NSWindow, appState: AppState) { + self.window = window + self.appState = appState + } +} + class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! var appState: AppState! private var titleCancellable: AnyCancellable? private var titleBarView: TitleBarView? private var focusTitleObserver: NSObjectProtocol? + private var windowControllers: [WindowController] = [] func applicationDidFinishLaunching(_ notification: Notification) { _ = ConfigManager.shared @@ -99,9 +109,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { let item = NSMenuItem() let menu = NSMenu(title: "File") - let newItem = NSMenuItem(title: "New Note", action: #selector(newNote), keyEquivalent: "n") - newItem.target = self - menu.addItem(newItem) + let newWindowItem = NSMenuItem(title: "New Window", action: #selector(newWindow), keyEquivalent: "n") + newWindowItem.target = self + menu.addItem(newWindowItem) + + let newNoteItem = NSMenuItem(title: "New Note", action: #selector(newNote), keyEquivalent: "N") + newNoteItem.keyEquivalentModifierMask = [.command, .shift] + newNoteItem.target = self + menu.addItem(newNoteItem) let openItem = NSMenuItem(title: "Open...", action: #selector(openNote), keyEquivalent: "o") openItem.target = self @@ -117,6 +132,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { saveAsItem.target = self menu.addItem(saveAsItem) + menu.addItem(.separator()) + + let openStorageItem = NSMenuItem(title: "Open Storage Directory", action: #selector(openStorageDirectory), keyEquivalent: "") + openStorageItem.target = self + menu.addItem(openStorageItem) + item.submenu = menu return item } @@ -133,6 +154,26 @@ class AppDelegate: NSObject, NSApplicationDelegate { menu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a") menu.addItem(.separator()) + let boldItem = NSMenuItem(title: "Bold", action: #selector(boldSelection), keyEquivalent: "b") + boldItem.target = self + menu.addItem(boldItem) + + let italicItem = NSMenuItem(title: "Italic", action: #selector(italicizeSelection), keyEquivalent: "i") + italicItem.target = self + menu.addItem(italicItem) + + menu.addItem(.separator()) + + let tableItem = NSMenuItem(title: "Insert Table", action: #selector(insertTable), keyEquivalent: "t") + tableItem.target = self + menu.addItem(tableItem) + + let evalItem = NSMenuItem(title: "Smart Eval", action: #selector(smartEval), keyEquivalent: "e") + evalItem.target = self + menu.addItem(evalItem) + + menu.addItem(.separator()) + let findItem = NSMenuItem(title: "Find...", action: #selector(NSTextView.performFindPanelAction(_:)), keyEquivalent: "f") findItem.tag = Int(NSTextFinder.Action.showFindInterface.rawValue) menu.addItem(findItem) @@ -190,6 +231,51 @@ class AppDelegate: NSObject, NSApplicationDelegate { appState.newNote() } + @objc private func newWindow() { + let state = AppState() + let contentView = ContentView(state: state) + let hostingView = NSHostingView(rootView: contentView) + + let win = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + win.titlebarAppearsTransparent = true + win.titleVisibility = .hidden + win.backgroundColor = Theme.current.base + win.title = "Swiftly" + win.contentView = hostingView + win.center() + win.makeKeyAndOrderFront(nil) + + let controller = WindowController(window: win, appState: state) + windowControllers.append(controller) + } + + @objc private func openStorageDirectory() { + let dir = ConfigManager.shared.autoSaveDirectory + let url = URL(fileURLWithPath: dir, isDirectory: true) + NSWorkspace.shared.open(url) + } + + @objc private func boldSelection() { + NotificationCenter.default.post(name: .boldSelection, object: nil) + } + + @objc private func italicizeSelection() { + NotificationCenter.default.post(name: .italicizeSelection, object: nil) + } + + @objc private func insertTable() { + NotificationCenter.default.post(name: .insertTable, object: nil) + } + + @objc private func smartEval() { + NotificationCenter.default.post(name: .smartEval, object: nil) + } + @objc private func openNote() { let panel = NSOpenPanel() panel.allowedContentTypes = Self.supportedContentTypes diff --git a/src/ContentView.swift b/src/ContentView.swift index a9d9170..c8e57ca 100644 --- a/src/ContentView.swift +++ b/src/ContentView.swift @@ -31,4 +31,8 @@ extension Notification.Name { static let focusEditor = Notification.Name("focusEditor") static let focusTitle = Notification.Name("focusTitle") static let formatDocument = Notification.Name("formatDocument") + static let insertTable = Notification.Name("insertTable") + static let boldSelection = Notification.Name("boldSelection") + static let italicizeSelection = Notification.Name("italicizeSelection") + static let smartEval = Notification.Name("smartEval") } diff --git a/src/EditorView.swift b/src/EditorView.swift index 94798e6..200ff5d 100644 --- a/src/EditorView.swift +++ b/src/EditorView.swift @@ -1262,26 +1262,25 @@ struct EditorTextView: NSViewRepresentable { private var isUpdatingImages = false private var isUpdatingTables = false private var embeddedTableViews: [MarkdownTableView] = [] - private var focusObserver: NSObjectProtocol? - private var settingsObserver: NSObjectProtocol? + private var observers: [NSObjectProtocol] = [] init(_ parent: EditorTextView) { self.parent = parent super.init() - focusObserver = NotificationCenter.default.addObserver( + + observers.append(NotificationCenter.default.addObserver( forName: .focusEditor, object: nil, queue: .main ) { [weak self] _ in guard let tv = self?.textView else { return } tv.window?.makeFirstResponder(tv) tv.setSelectedRange(NSRange(location: 0, length: 0)) - } - NotificationCenter.default.addObserver( + }) + observers.append(NotificationCenter.default.addObserver( forName: .formatDocument, object: nil, queue: .main ) { [weak self] _ in self?.formatCurrentDocument() - } - - settingsObserver = NotificationCenter.default.addObserver( + }) + observers.append(NotificationCenter.default.addObserver( forName: .settingsChanged, object: nil, queue: .main ) { [weak self] _ in guard let tv = self?.textView, let ts = tv.textStorage else { return } @@ -1297,14 +1296,31 @@ struct EditorTextView: NSViewRepresentable { applySyntaxHighlighting(to: ts, format: parent.fileFormat) ts.endEditing() tv.needsDisplay = true - } + }) + observers.append(NotificationCenter.default.addObserver( + forName: .boldSelection, object: nil, queue: .main + ) { [weak self] _ in + self?.wrapSelection(with: "**") + }) + observers.append(NotificationCenter.default.addObserver( + forName: .italicizeSelection, object: nil, queue: .main + ) { [weak self] _ in + self?.wrapSelection(with: "*") + }) + observers.append(NotificationCenter.default.addObserver( + forName: .insertTable, object: nil, queue: .main + ) { [weak self] _ in + self?.insertBlankTable() + }) + observers.append(NotificationCenter.default.addObserver( + forName: .smartEval, object: nil, queue: .main + ) { [weak self] _ in + self?.performSmartEval() + }) } deinit { - if let obs = focusObserver { - NotificationCenter.default.removeObserver(obs) - } - if let obs = settingsObserver { + for obs in observers { NotificationCenter.default.removeObserver(obs) } } @@ -1497,6 +1513,47 @@ struct EditorTextView: NSViewRepresentable { textView.insertText("\n" + indent, replacementRange: textView.selectedRange()) } + private func wrapSelection(with wrapper: String) { + guard let tv = textView else { return } + let sel = tv.selectedRange() + guard sel.length > 0 else { return } + let str = tv.string as NSString + let selected = str.substring(with: sel) + let wrapped = wrapper + selected + wrapper + tv.insertText(wrapped, replacementRange: sel) + tv.setSelectedRange(NSRange(location: sel.location + wrapper.count, length: selected.count)) + } + + private func insertBlankTable() { + guard let tv = textView else { return } + let table = "| Column 1 | Column 2 | Column 3 |\n| --- | --- | --- |\n| | | |\n" + tv.insertText(table, replacementRange: tv.selectedRange()) + } + + private func performSmartEval() { + guard let tv = textView else { return } + let str = tv.string as NSString + let cursor = tv.selectedRange().location + let lineRange = str.lineRange(for: NSRange(location: cursor, length: 0)) + let line = str.substring(with: lineRange).trimmingCharacters(in: .newlines) + let trimmed = line.trimmingCharacters(in: .whitespaces) + + if trimmed.hasPrefix("let ") { + if let eqIdx = trimmed.firstIndex(of: "="), eqIdx > trimmed.startIndex { + let afterLet = trimmed[trimmed.index(trimmed.startIndex, offsetBy: 4).. Bool { var urlString: String? if let url = link as? URL { @@ -1946,7 +2003,7 @@ private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: N let contentStart = hashRange.location + hashRange.length let contentRange = NSRange(location: contentStart, length: NSMaxRange(lineRange) - contentStart) if contentRange.length > 0 { - let h3Font = NSFont.systemFont(ofSize: 15, weight: .bold) + let h3Font = NSFont.systemFont(ofSize: round(baseFont.pointSize * 1.15), weight: .bold) textStorage.addAttribute(.font, value: h3Font, range: contentRange) textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange) } @@ -1961,7 +2018,7 @@ private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: N let contentStart = hashRange.location + hashRange.length let contentRange = NSRange(location: contentStart, length: NSMaxRange(lineRange) - contentStart) if contentRange.length > 0 { - let h2Font = NSFont.systemFont(ofSize: 18, weight: .bold) + let h2Font = NSFont.systemFont(ofSize: round(baseFont.pointSize * 1.38), weight: .bold) textStorage.addAttribute(.font, value: h2Font, range: contentRange) textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange) } @@ -1976,7 +2033,7 @@ private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: N let contentStart = hashRange.location + hashRange.length let contentRange = NSRange(location: contentStart, length: NSMaxRange(lineRange) - contentStart) if contentRange.length > 0 { - let h1Font = NSFont.systemFont(ofSize: 22, weight: .bold) + let h1Font = NSFont.systemFont(ofSize: round(baseFont.pointSize * 1.69), weight: .bold) textStorage.addAttribute(.font, value: h1Font, range: contentRange) textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange) }