232 lines
8.2 KiB
Swift
232 lines
8.2 KiB
Swift
import Cocoa
|
|
import Combine
|
|
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
|
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
var window: NSWindow!
|
|
var appState: AppState!
|
|
private var titleCancellable: AnyCancellable?
|
|
private var titleBarView: TitleBarView?
|
|
private var focusTitleObserver: NSObjectProtocol?
|
|
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
_ = ConfigManager.shared
|
|
appState = AppState()
|
|
|
|
let contentView = ContentView(state: appState)
|
|
let hostingView = NSHostingView(rootView: contentView)
|
|
|
|
window = NSWindow(
|
|
contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800),
|
|
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
window.titlebarAppearsTransparent = true
|
|
window.titleVisibility = .hidden
|
|
window.backgroundColor = Theme.current.base
|
|
window.title = "Swiftly"
|
|
window.contentView = hostingView
|
|
window.center()
|
|
window.setFrameAutosaveName("SwiftlyMainWindow")
|
|
window.makeKeyAndOrderFront(nil)
|
|
|
|
applyThemeAppearance()
|
|
setupTitleBar()
|
|
setupMenuBar()
|
|
observeDocumentTitle()
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self, selector: #selector(settingsDidChange),
|
|
name: .settingsChanged, object: nil
|
|
)
|
|
}
|
|
|
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
|
return true
|
|
}
|
|
|
|
func applicationWillTerminate(_ notification: Notification) {
|
|
appState.saveNote()
|
|
}
|
|
|
|
// MARK: - Menu bar
|
|
|
|
private func setupMenuBar() {
|
|
let mainMenu = NSMenu()
|
|
|
|
mainMenu.addItem(buildAppMenu())
|
|
mainMenu.addItem(buildFileMenu())
|
|
mainMenu.addItem(buildEditMenu())
|
|
mainMenu.addItem(buildViewMenu())
|
|
mainMenu.addItem(buildWindowMenu())
|
|
|
|
NSApp.mainMenu = mainMenu
|
|
}
|
|
|
|
private func buildAppMenu() -> NSMenuItem {
|
|
let item = NSMenuItem()
|
|
let menu = NSMenu()
|
|
menu.addItem(withTitle: "About Swiftly", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
|
|
menu.addItem(.separator())
|
|
let settingsItem = NSMenuItem(title: "Settings...", action: #selector(openSettings), keyEquivalent: ",")
|
|
settingsItem.target = self
|
|
menu.addItem(settingsItem)
|
|
menu.addItem(.separator())
|
|
menu.addItem(withTitle: "Quit Swiftly", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
|
item.submenu = menu
|
|
return item
|
|
}
|
|
|
|
private func buildFileMenu() -> NSMenuItem {
|
|
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 openItem = NSMenuItem(title: "Open...", action: #selector(openNote), keyEquivalent: "o")
|
|
openItem.target = self
|
|
menu.addItem(openItem)
|
|
|
|
menu.addItem(.separator())
|
|
|
|
let saveItem = NSMenuItem(title: "Save", action: #selector(saveNote), keyEquivalent: "s")
|
|
saveItem.target = self
|
|
menu.addItem(saveItem)
|
|
|
|
let saveAsItem = NSMenuItem(title: "Save As...", action: #selector(saveNoteAs), keyEquivalent: "S")
|
|
saveAsItem.target = self
|
|
menu.addItem(saveAsItem)
|
|
|
|
item.submenu = menu
|
|
return item
|
|
}
|
|
|
|
private func buildEditMenu() -> NSMenuItem {
|
|
let item = NSMenuItem()
|
|
let menu = NSMenu(title: "Edit")
|
|
menu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z")
|
|
menu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z")
|
|
menu.addItem(.separator())
|
|
menu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
|
|
menu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
|
|
menu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
|
|
menu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
|
menu.addItem(.separator())
|
|
|
|
let findItem = NSMenuItem(title: "Find...", action: #selector(NSTextView.performFindPanelAction(_:)), keyEquivalent: "f")
|
|
findItem.tag = Int(NSTextFinder.Action.showFindInterface.rawValue)
|
|
menu.addItem(findItem)
|
|
|
|
item.submenu = menu
|
|
return item
|
|
}
|
|
|
|
private func buildViewMenu() -> NSMenuItem {
|
|
let item = NSMenuItem()
|
|
let menu = NSMenu(title: "View")
|
|
let toggleItem = NSMenuItem(title: "Toggle Sidebar", action: #selector(toggleSidebar), keyEquivalent: "b")
|
|
toggleItem.keyEquivalentModifierMask = .control
|
|
toggleItem.target = self
|
|
menu.addItem(toggleItem)
|
|
item.submenu = menu
|
|
return item
|
|
}
|
|
|
|
private func buildWindowMenu() -> NSMenuItem {
|
|
let item = NSMenuItem()
|
|
let menu = NSMenu(title: "Window")
|
|
menu.addItem(withTitle: "Minimize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m")
|
|
menu.addItem(withTitle: "Zoom", action: #selector(NSWindow.zoom(_:)), keyEquivalent: "")
|
|
item.submenu = menu
|
|
NSApp.windowsMenu = menu
|
|
return item
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
@objc private func newNote() {
|
|
appState.newNote()
|
|
}
|
|
|
|
@objc private func openNote() {
|
|
let panel = NSOpenPanel()
|
|
panel.allowedContentTypes = [UTType(filenameExtension: "md")!, .plainText]
|
|
panel.canChooseFiles = true
|
|
panel.canChooseDirectories = false
|
|
panel.allowsMultipleSelection = false
|
|
panel.beginSheetModal(for: window) { [weak self] response in
|
|
guard response == .OK, let url = panel.url else { return }
|
|
self?.appState.loadNoteFromFile(url)
|
|
}
|
|
}
|
|
|
|
@objc private func saveNote() {
|
|
appState.saveNote()
|
|
}
|
|
|
|
@objc private func saveNoteAs() {
|
|
let panel = NSSavePanel()
|
|
panel.allowedContentTypes = [UTType(filenameExtension: "md")!]
|
|
panel.nameFieldStringValue = "note.md"
|
|
panel.beginSheetModal(for: window) { [weak self] response in
|
|
guard response == .OK, let url = panel.url else { return }
|
|
self?.appState.saveNoteToFile(url)
|
|
}
|
|
}
|
|
|
|
@objc private func openSettings() {
|
|
SettingsWindowController.show()
|
|
}
|
|
|
|
@objc private func settingsDidChange() {
|
|
window.backgroundColor = Theme.current.base
|
|
window.contentView?.needsDisplay = true
|
|
}
|
|
|
|
@objc private func toggleSidebar() {
|
|
NotificationCenter.default.post(name: .toggleSidebar, object: nil)
|
|
}
|
|
|
|
private func setupTitleBar() {
|
|
let accessory = TitleBarAccessoryController()
|
|
window.addTitlebarAccessoryViewController(accessory)
|
|
|
|
let tbv = accessory.titleView
|
|
tbv.onCommit = { [weak self] rawTitle in
|
|
guard let self = self else { return }
|
|
let lines = self.appState.documentText.components(separatedBy: "\n")
|
|
var rest = Array(lines.dropFirst())
|
|
if rest.isEmpty && self.appState.documentText.isEmpty { rest = [] }
|
|
self.appState.documentText = ([rawTitle] + rest).joined(separator: "\n")
|
|
}
|
|
|
|
titleBarView = tbv
|
|
|
|
focusTitleObserver = NotificationCenter.default.addObserver(
|
|
forName: .focusTitle, object: nil, queue: .main
|
|
) { [weak self] _ in
|
|
self?.titleBarView?.beginEditing()
|
|
}
|
|
}
|
|
|
|
private func observeDocumentTitle() {
|
|
titleCancellable = appState.$documentText
|
|
.receive(on: RunLoop.main)
|
|
.sink { [weak self] text in
|
|
guard let self = self else { return }
|
|
let firstLine = text.components(separatedBy: "\n").first?
|
|
.trimmingCharacters(in: .whitespaces) ?? ""
|
|
let clean = firstLine.replacingOccurrences(
|
|
of: "^#+\\s*", with: "", options: .regularExpression
|
|
)
|
|
let displayTitle = clean.isEmpty ? "Swiftly" : String(clean.prefix(60))
|
|
self.window.title = displayTitle
|
|
self.titleBarView?.title = firstLine
|
|
}
|
|
}
|
|
}
|