import AppKit import SwiftUI final class AppDelegate: NSObject, NSApplicationDelegate { private var settingsWindow: NSWindow? func applicationWillFinishLaunching(_ notification: Notification) { NSApp.mainMenu = buildMainMenu() } func applicationDidFinishLaunching(_ notification: Notification) { NSApp.activate(ignoringOtherApps: true) } func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { true } // Hold termination until in-flight scp uploads finish (bounded), so a // Cmd-Q-then-save sync isn't abandoned or its failure swallowed. func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { guard SyncEngine.inFlightUploads > 0 else { return .terminateNow } SyncEngine.onIdle = { SyncEngine.onIdle = nil NSApp.reply(toApplicationShouldTerminate: true) } DispatchQueue.main.asyncAfter(deadline: .now() + 20) { if SyncEngine.onIdle != nil { SyncEngine.onIdle = nil NSApp.reply(toApplicationShouldTerminate: true) } } return .terminateLater } @objc func showSettings(_ sender: Any?) { if settingsWindow == nil { let host = NSHostingController(rootView: SettingsView()) let window = NSWindow(contentViewController: host) window.title = "TextPlus Settings" window.styleMask.remove([.resizable, .miniaturizable]) window.isReleasedWhenClosed = false window.center() settingsWindow = window } settingsWindow?.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } // MARK: - Menu private func buildMainMenu() -> NSMenu { let mainMenu = NSMenu() // App menu let appMenuItem = NSMenuItem() mainMenu.addItem(appMenuItem) let appMenu = NSMenu() appMenuItem.submenu = appMenu appMenu.addItem(withTitle: "About TextPlus", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "") appMenu.addItem(.separator()) appMenu.addItem(withTitle: "Settings…", action: #selector(AppDelegate.showSettings(_:)), keyEquivalent: ",") appMenu.addItem(.separator()) appMenu.addItem(withTitle: "Hide TextPlus", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h") let hideOthers = appMenu.addItem(withTitle: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h") hideOthers.keyEquivalentModifierMask = [.command, .option] appMenu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "") appMenu.addItem(.separator()) appMenu.addItem(withTitle: "Quit TextPlus", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") // File menu let fileMenuItem = NSMenuItem() mainMenu.addItem(fileMenuItem) let fileMenu = NSMenu(title: "File") fileMenuItem.submenu = fileMenu fileMenu.addItem(withTitle: "New", action: #selector(NSDocumentController.newDocument(_:)), keyEquivalent: "n") fileMenu.addItem(withTitle: "Open…", action: #selector(NSDocumentController.openDocument(_:)), keyEquivalent: "o") // AppKit auto-inserts a managed "Open Recent" submenu after the // item whose action is openDocument: — don't build one by hand. fileMenu.addItem(.separator()) fileMenu.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w") fileMenu.addItem(withTitle: "Save", action: #selector(NSDocument.save(_:)), keyEquivalent: "s") let saveAs = fileMenu.addItem(withTitle: "Save As…", action: #selector(NSDocument.saveAs(_:)), keyEquivalent: "S") saveAs.keyEquivalentModifierMask = [.command, .shift] fileMenu.addItem(withTitle: "Revert to Saved", action: #selector(NSDocument.revertToSaved(_:)), keyEquivalent: "") fileMenu.addItem(.separator()) let syncNow = fileMenu.addItem(withTitle: "Sync Now", action: #selector(Document.syncNow(_:)), keyEquivalent: "s") syncNow.keyEquivalentModifierMask = [.command, .option] // Edit menu let editMenuItem = NSMenuItem() mainMenu.addItem(editMenuItem) let editMenu = NSMenu(title: "Edit") editMenuItem.submenu = editMenu let undo = editMenu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z") undo.target = nil let redo = editMenu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z") redo.keyEquivalentModifierMask = [.command, .shift] editMenu.addItem(.separator()) editMenu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x") editMenu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c") editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v") editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a") editMenu.addItem(.separator()) let findItem = editMenu.addItem(withTitle: "Find…", action: #selector(NSTextView.performFindPanelAction(_:)), keyEquivalent: "f") findItem.tag = Int(NSTextFinder.Action.showFindInterface.rawValue) let findNext = editMenu.addItem(withTitle: "Find Next", action: #selector(NSTextView.performFindPanelAction(_:)), keyEquivalent: "g") findNext.tag = Int(NSTextFinder.Action.nextMatch.rawValue) let findPrev = editMenu.addItem(withTitle: "Find Previous", action: #selector(NSTextView.performFindPanelAction(_:)), keyEquivalent: "G") findPrev.keyEquivalentModifierMask = [.command, .shift] findPrev.tag = Int(NSTextFinder.Action.previousMatch.rawValue) let useSelection = editMenu.addItem(withTitle: "Use Selection for Find", action: #selector(NSTextView.performFindPanelAction(_:)), keyEquivalent: "e") useSelection.tag = Int(NSTextFinder.Action.setSearchString.rawValue) // View menu let viewMenuItem = NSMenuItem() mainMenu.addItem(viewMenuItem) let viewMenu = NSMenu(title: "View") viewMenuItem.submenu = viewMenu let editorMode = viewMenu.addItem(withTitle: "Editor", action: #selector(Document.showEditorMode(_:)), keyEquivalent: "1") editorMode.keyEquivalentModifierMask = [.command, .control] let splitMode = viewMenu.addItem(withTitle: "Editor + Preview", action: #selector(Document.showSplitMode(_:)), keyEquivalent: "2") splitMode.keyEquivalentModifierMask = [.command, .control] let boardMode = viewMenu.addItem(withTitle: "Board", action: #selector(Document.showBoardMode(_:)), keyEquivalent: "3") boardMode.keyEquivalentModifierMask = [.command, .control] viewMenu.addItem(.separator()) let bigger = viewMenu.addItem(withTitle: "Make Text Bigger", action: #selector(Document.makeTextBigger(_:)), keyEquivalent: "+") bigger.keyEquivalentModifierMask = [.command] // "+" is a shifted key, so that item only fires on ⌘⇧=. This hidden // alternate makes plain ⌘= work too, like TextEdit. let biggerAlternate = viewMenu.addItem(withTitle: "Make Text Bigger", action: #selector(Document.makeTextBigger(_:)), keyEquivalent: "=") biggerAlternate.keyEquivalentModifierMask = [.command] biggerAlternate.isAlternate = true viewMenu.addItem(withTitle: "Make Text Smaller", action: #selector(Document.makeTextSmaller(_:)), keyEquivalent: "-") viewMenu.addItem(.separator()) viewMenu.addItem(withTitle: "Toggle Soft Wrap", action: #selector(Document.toggleSoftWrap(_:)), keyEquivalent: "") viewMenu.addItem(withTitle: "Toggle Line Numbers", action: #selector(Document.toggleLineNumbers(_:)), keyEquivalent: "") let cycleTheme = viewMenu.addItem(withTitle: "Cycle Theme", action: #selector(Document.cycleTheme(_:)), keyEquivalent: "t") cycleTheme.keyEquivalentModifierMask = [.command, .control] // Format menu (Markdown markup) let formatMenuItem = NSMenuItem() mainMenu.addItem(formatMenuItem) let formatMenu = NSMenu(title: "Format") formatMenuItem.submenu = formatMenu let boldItem = formatMenu.addItem(withTitle: "Bold", action: #selector(Document.markupBold(_:)), keyEquivalent: "b") boldItem.keyEquivalentModifierMask = [.command] let italicItem = formatMenu.addItem(withTitle: "Italic", action: #selector(Document.markupItalic(_:)), keyEquivalent: "i") italicItem.keyEquivalentModifierMask = [.command] let codeItem = formatMenu.addItem(withTitle: "Inline Code", action: #selector(Document.markupCode(_:)), keyEquivalent: "k") codeItem.keyEquivalentModifierMask = [.command, .shift] formatMenu.addItem(withTitle: "Strikethrough", action: #selector(Document.markupStrikethrough(_:)), keyEquivalent: "") formatMenu.addItem(.separator()) formatMenu.addItem(withTitle: "Heading", action: #selector(Document.markupHeading(_:)), keyEquivalent: "") formatMenu.addItem(withTitle: "Bullet List", action: #selector(Document.markupList(_:)), keyEquivalent: "") formatMenu.addItem(withTitle: "Block Quote", action: #selector(Document.markupQuote(_:)), keyEquivalent: "") let linkItem = formatMenu.addItem(withTitle: "Link", action: #selector(Document.markupLink(_:)), keyEquivalent: "k") linkItem.keyEquivalentModifierMask = [.command] // Tools menu let toolsMenuItem = NSMenuItem() mainMenu.addItem(toolsMenuItem) let toolsMenu = NSMenu(title: "Tools") toolsMenuItem.submenu = toolsMenu let listSel = toolsMenu.addItem(withTitle: "List Div & Class Selectors", action: #selector(Document.listSelectors(_:)), keyEquivalent: "l") listSel.keyEquivalentModifierMask = [.command, .option] let extractSel = toolsMenu.addItem(withTitle: "Extract Text for Selector…", action: #selector(Document.extractForSelector(_:)), keyEquivalent: "e") extractSel.keyEquivalentModifierMask = [.command, .option] toolsMenu.addItem(.separator()) let listings = toolsMenu.addItem(withTitle: "Extract Discogs Listings", action: #selector(Document.extractListings(_:)), keyEquivalent: "d") listings.keyEquivalentModifierMask = [.command, .option] let sendPG = toolsMenu.addItem(withTitle: "Send Page to Postgres", action: #selector(Document.sendToPostgres(_:)), keyEquivalent: "p") sendPG.keyEquivalentModifierMask = [.command, .option] // Window menu let windowMenuItem = NSMenuItem() mainMenu.addItem(windowMenuItem) let windowMenu = NSMenu(title: "Window") windowMenuItem.submenu = windowMenu windowMenu.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m") windowMenu.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "") windowMenu.addItem(.separator()) windowMenu.addItem(withTitle: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: "") NSApp.windowsMenu = windowMenu return mainMenu } }