keyboard shortcuts, menu items, and header zoom scaling
This commit is contained in:
parent
bb3cc40b51
commit
9209619473
|
|
@ -3,12 +3,22 @@ import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UniformTypeIdentifiers
|
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 {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
var window: NSWindow!
|
var window: NSWindow!
|
||||||
var appState: AppState!
|
var appState: AppState!
|
||||||
private var titleCancellable: AnyCancellable?
|
private var titleCancellable: AnyCancellable?
|
||||||
private var titleBarView: TitleBarView?
|
private var titleBarView: TitleBarView?
|
||||||
private var focusTitleObserver: NSObjectProtocol?
|
private var focusTitleObserver: NSObjectProtocol?
|
||||||
|
private var windowControllers: [WindowController] = []
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
_ = ConfigManager.shared
|
_ = ConfigManager.shared
|
||||||
|
|
@ -99,9 +109,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
let item = NSMenuItem()
|
let item = NSMenuItem()
|
||||||
let menu = NSMenu(title: "File")
|
let menu = NSMenu(title: "File")
|
||||||
|
|
||||||
let newItem = NSMenuItem(title: "New Note", action: #selector(newNote), keyEquivalent: "n")
|
let newWindowItem = NSMenuItem(title: "New Window", action: #selector(newWindow), keyEquivalent: "n")
|
||||||
newItem.target = self
|
newWindowItem.target = self
|
||||||
menu.addItem(newItem)
|
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")
|
let openItem = NSMenuItem(title: "Open...", action: #selector(openNote), keyEquivalent: "o")
|
||||||
openItem.target = self
|
openItem.target = self
|
||||||
|
|
@ -117,6 +132,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
saveAsItem.target = self
|
saveAsItem.target = self
|
||||||
menu.addItem(saveAsItem)
|
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
|
item.submenu = menu
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +154,26 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
menu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
menu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
||||||
menu.addItem(.separator())
|
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")
|
let findItem = NSMenuItem(title: "Find...", action: #selector(NSTextView.performFindPanelAction(_:)), keyEquivalent: "f")
|
||||||
findItem.tag = Int(NSTextFinder.Action.showFindInterface.rawValue)
|
findItem.tag = Int(NSTextFinder.Action.showFindInterface.rawValue)
|
||||||
menu.addItem(findItem)
|
menu.addItem(findItem)
|
||||||
|
|
@ -190,6 +231,51 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
appState.newNote()
|
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() {
|
@objc private func openNote() {
|
||||||
let panel = NSOpenPanel()
|
let panel = NSOpenPanel()
|
||||||
panel.allowedContentTypes = Self.supportedContentTypes
|
panel.allowedContentTypes = Self.supportedContentTypes
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,8 @@ extension Notification.Name {
|
||||||
static let focusEditor = Notification.Name("focusEditor")
|
static let focusEditor = Notification.Name("focusEditor")
|
||||||
static let focusTitle = Notification.Name("focusTitle")
|
static let focusTitle = Notification.Name("focusTitle")
|
||||||
static let formatDocument = Notification.Name("formatDocument")
|
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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1262,26 +1262,25 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
private var isUpdatingImages = false
|
private var isUpdatingImages = false
|
||||||
private var isUpdatingTables = false
|
private var isUpdatingTables = false
|
||||||
private var embeddedTableViews: [MarkdownTableView] = []
|
private var embeddedTableViews: [MarkdownTableView] = []
|
||||||
private var focusObserver: NSObjectProtocol?
|
private var observers: [NSObjectProtocol] = []
|
||||||
private var settingsObserver: NSObjectProtocol?
|
|
||||||
|
|
||||||
init(_ parent: EditorTextView) {
|
init(_ parent: EditorTextView) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
super.init()
|
super.init()
|
||||||
focusObserver = NotificationCenter.default.addObserver(
|
|
||||||
|
observers.append(NotificationCenter.default.addObserver(
|
||||||
forName: .focusEditor, object: nil, queue: .main
|
forName: .focusEditor, object: nil, queue: .main
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
guard let tv = self?.textView else { return }
|
guard let tv = self?.textView else { return }
|
||||||
tv.window?.makeFirstResponder(tv)
|
tv.window?.makeFirstResponder(tv)
|
||||||
tv.setSelectedRange(NSRange(location: 0, length: 0))
|
tv.setSelectedRange(NSRange(location: 0, length: 0))
|
||||||
}
|
})
|
||||||
NotificationCenter.default.addObserver(
|
observers.append(NotificationCenter.default.addObserver(
|
||||||
forName: .formatDocument, object: nil, queue: .main
|
forName: .formatDocument, object: nil, queue: .main
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
self?.formatCurrentDocument()
|
self?.formatCurrentDocument()
|
||||||
}
|
})
|
||||||
|
observers.append(NotificationCenter.default.addObserver(
|
||||||
settingsObserver = NotificationCenter.default.addObserver(
|
|
||||||
forName: .settingsChanged, object: nil, queue: .main
|
forName: .settingsChanged, object: nil, queue: .main
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
guard let tv = self?.textView, let ts = tv.textStorage else { return }
|
guard let tv = self?.textView, let ts = tv.textStorage else { return }
|
||||||
|
|
@ -1297,14 +1296,31 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
applySyntaxHighlighting(to: ts, format: parent.fileFormat)
|
applySyntaxHighlighting(to: ts, format: parent.fileFormat)
|
||||||
ts.endEditing()
|
ts.endEditing()
|
||||||
tv.needsDisplay = true
|
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 {
|
deinit {
|
||||||
if let obs = focusObserver {
|
for obs in observers {
|
||||||
NotificationCenter.default.removeObserver(obs)
|
|
||||||
}
|
|
||||||
if let obs = settingsObserver {
|
|
||||||
NotificationCenter.default.removeObserver(obs)
|
NotificationCenter.default.removeObserver(obs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1497,6 +1513,47 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
textView.insertText("\n" + indent, replacementRange: textView.selectedRange())
|
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)..<eqIdx]
|
||||||
|
.trimmingCharacters(in: .whitespaces)
|
||||||
|
let insertion = "\n/= \(afterLet)\n"
|
||||||
|
let endOfLine = NSMaxRange(lineRange)
|
||||||
|
tv.insertText(insertion, replacementRange: NSRange(location: endOfLine, length: 0))
|
||||||
|
}
|
||||||
|
} else if !trimmed.isEmpty {
|
||||||
|
let lineStart = lineRange.location
|
||||||
|
let whitespacePrefix = line.prefix(while: { $0 == " " || $0 == "\t" })
|
||||||
|
let insertLoc = lineStart + whitespacePrefix.count
|
||||||
|
tv.insertText("/= ", replacementRange: NSRange(location: insertLoc, length: 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func textView(_ textView: NSTextView, clickedOnLink link: Any, at charIndex: Int) -> Bool {
|
func textView(_ textView: NSTextView, clickedOnLink link: Any, at charIndex: Int) -> Bool {
|
||||||
var urlString: String?
|
var urlString: String?
|
||||||
if let url = link as? URL {
|
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 contentStart = hashRange.location + hashRange.length
|
||||||
let contentRange = NSRange(location: contentStart, length: NSMaxRange(lineRange) - contentStart)
|
let contentRange = NSRange(location: contentStart, length: NSMaxRange(lineRange) - contentStart)
|
||||||
if contentRange.length > 0 {
|
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(.font, value: h3Font, range: contentRange)
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.text, 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 contentStart = hashRange.location + hashRange.length
|
||||||
let contentRange = NSRange(location: contentStart, length: NSMaxRange(lineRange) - contentStart)
|
let contentRange = NSRange(location: contentStart, length: NSMaxRange(lineRange) - contentStart)
|
||||||
if contentRange.length > 0 {
|
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(.font, value: h2Font, range: contentRange)
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.text, 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 contentStart = hashRange.location + hashRange.length
|
||||||
let contentRange = NSRange(location: contentStart, length: NSMaxRange(lineRange) - contentStart)
|
let contentRange = NSRange(location: contentStart, length: NSMaxRange(lineRange) - contentStart)
|
||||||
if contentRange.length > 0 {
|
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(.font, value: h1Font, range: contentRange)
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange)
|
textStorage.addAttribute(.foregroundColor, value: palette.text, range: contentRange)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue