项目文件夹

文件
wehub-resource-sync 5b99bf6bca
CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:34:54 +08:00

290 行
11 KiB
Swift

import AppKit
import EdmundCore
import Sparkle
// Entry point for the app the user knows as "Edmund" (CFBundleName). The
// executable target — and so this binary at Edmund.app/Contents/MacOS/edmd — is
// named `edmd`, an expansion of "Editor for Markdown". The backronym is the
// app's original working name; it survives only here and in Package.swift, never
// in anything user-facing.
// --- App Delegate -----------------------------------------------------------
@MainActor
class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
var aboutWindowController: AboutWindowController?
var settingsWindowController: SettingsWindowController?
// startingUpdater: true kicks off the scheduled background check immediately;
// the "Check for Updates…" menu item targets this controller directly.
let updaterController = SPUStandardUpdaterController(
startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
// MARK: - Typewriter Mode (persisted)
static let typewriterModeKey = "EditorTypewriterMode"
/// Whether typewriter scrolling is enabled. Defaults to on (the historical
/// behavior) when nothing has been saved yet.
static func typewriterModeEnabled(_ defaults: UserDefaults = .standard) -> Bool {
defaults.object(forKey: typewriterModeKey) as? Bool ?? true
}
func applicationDidFinishLaunching(_ notification: Notification) {
AppSettings.applyLogging()
Log.info("Edmund launched", category: .app)
AppSettings.applyAppearance()
setupMenuBar()
// Opt-in (default off): upload any crash reports macOS wrote for us since
// last launch. Fire-and-forget; never blocks startup.
if AppSettings.sendCrashLogs {
CrashReporter.uploadPendingReports(
alreadySent: AppSettings.sentCrashReports,
onSent: { AppSettings.sentCrashReports.insert($0) })
}
// Open file from command-line argument. When a file is given,
// `applicationShouldOpenUntitledFile` suppresses the otherwise-automatic
// blank document, so we don't end up with two windows.
let args = CommandLine.arguments
if args.count > 1 {
let url = URL(fileURLWithPath: args[1])
Log.info("Opening file from launch argument: \(url.path)", category: .document)
NSDocumentController.shared.openDocument(withContentsOf: url, display: true) { _, _, _ in }
}
#if DEBUG
ReproScript.runIfRequested()
#endif
}
// Auto-open a blank document on launch only when no file was passed on the
// command line (otherwise the file arg + the blank doc make two windows).
func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool {
CommandLine.arguments.count <= 1 && AppSettings.startupAction == .createNewDocument
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
AppSettings.reopenWindows
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
true
}
// Reopen a new untitled document when the app is activated with no windows.
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !flag {
if AppSettings.startupAction == .createNewDocument {
NSDocumentController.shared.newDocument(nil)
}
}
return true
}
// MARK: - Settings
@MainActor @objc func showAbout(_ sender: Any?) {
if aboutWindowController == nil {
aboutWindowController = AboutWindowController()
}
aboutWindowController?.showWindow(nil)
aboutWindowController?.window?.makeKeyAndOrderFront(nil)
}
@MainActor @objc func showSettings(_ sender: Any?) {
if settingsWindowController == nil {
settingsWindowController = SettingsWindowController()
}
settingsWindowController?.showWindow(nil)
settingsWindowController?.window?.makeKeyAndOrderFront(nil)
}
// MARK: - View
/// Toggles typewriter scrolling, persists the choice, and applies it to every
/// open document immediately.
@MainActor @objc func toggleTypewriterMode(_ sender: Any?) {
let newValue = !AppDelegate.typewriterModeEnabled()
UserDefaults.standard.set(newValue, forKey: AppDelegate.typewriterModeKey)
for case let doc as Document in NSDocumentController.shared.documents {
doc.editor?.typewriterModeEnabled = newValue
}
}
@MainActor func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
if menuItem.action == #selector(toggleTypewriterMode(_:)) {
menuItem.state = AppDelegate.typewriterModeEnabled() ? .on : .off
}
return true
}
// MARK: - Open Document
/// Manual Open panel — bypasses NSDocumentController's type validation
/// which is broken without Info.plist.
@MainActor @objc func openDocumentManually(_ sender: Any?) {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = false
panel.canChooseDirectories = false
panel.canChooseFiles = true
let complete: (NSApplication.ModalResponse) -> Void = { response in
guard response == .OK, let url = panel.url else { return }
NSDocumentController.shared.openDocument(
withContentsOf: url, display: true
) { _, _, error in
if let error = error {
NSAlert(error: error).runModal()
}
}
}
// Attach the panel to the front window as a sheet so it's always visible
// — a free-floating panel can open off-screen or behind the window
// (the app's windows can launch off-screen), which looks like a hang.
NSApp.activate(ignoringOtherApps: true)
if let window = NSApp.keyWindow ?? NSApp.mainWindow {
panel.beginSheetModal(for: window, completionHandler: complete)
} else {
panel.begin(completionHandler: complete)
}
}
// MARK: - Menu Bar
@MainActor private func setupMenuBar() {
let mainMenu = NSMenu()
// App menu (required for Cmd+Q)
let appMenuItem = NSMenuItem()
let appMenu = NSMenu()
appMenu.addItem(withTitle: "About Edmund",
action: #selector(AppDelegate.showAbout(_:)),
keyEquivalent: "")
appMenu.addItem(NSMenuItem.separator())
appMenu.addItem(withTitle: "Settings\u{2026}",
action: #selector(AppDelegate.showSettings(_:)),
keyEquivalent: ",")
let updatesItem = appMenu.addItem(
withTitle: "Check for Updates\u{2026}",
action: #selector(SPUStandardUpdaterController.checkForUpdates(_:)),
keyEquivalent: "")
updatesItem.target = updaterController
appMenu.addItem(NSMenuItem.separator())
appMenu.addItem(withTitle: "Quit Edmund",
action: #selector(NSApplication.terminate(_:)),
keyEquivalent: "q")
appMenuItem.submenu = appMenu
mainMenu.addItem(appMenuItem)
// File menu — NSDocument provides the standard actions
let fileMenuItem = NSMenuItem()
let fileMenu = NSMenu(title: "File")
fileMenu.addItem(withTitle: "New",
action: #selector(NSDocumentController.newDocument(_:)),
keyEquivalent: "n")
fileMenu.addItem(withTitle: "Open\u{2026}",
action: #selector(AppDelegate.openDocumentManually(_:)),
keyEquivalent: "o")
// Recent documents submenu
let recentMenuItem = NSMenuItem(title: "Open Recent", action: nil, keyEquivalent: "")
let recentMenu = NSMenu(title: "Open Recent")
recentMenu.addItem(withTitle: "Clear Menu",
action: #selector(NSDocumentController.clearRecentDocuments(_:)),
keyEquivalent: "")
recentMenuItem.submenu = recentMenu
fileMenu.addItem(recentMenuItem)
fileMenu.addItem(NSMenuItem.separator())
fileMenu.addItem(withTitle: "Save",
action: #selector(NSDocument.save(_:)),
keyEquivalent: "s")
fileMenu.addItem(NSMenuItem.separator())
fileMenu.addItem(withTitle: "Rename\u{2026}",
action: #selector(Document.rename(_:)),
keyEquivalent: "")
fileMenu.addItem(withTitle: "Move To\u{2026}",
action: #selector(Document.move(_:)),
keyEquivalent: "")
fileMenu.addItem(NSMenuItem.separator())
fileMenu.addItem(withTitle: "Export as PDF\u{2026}",
action: #selector(Document.exportToPDF(_:)),
keyEquivalent: "")
fileMenu.addItem(withTitle: "Print\u{2026}",
action: #selector(Document.printDocument(_:)),
keyEquivalent: "p")
fileMenuItem.submenu = fileMenu
mainMenu.addItem(fileMenuItem)
// Edit menu (required for Cmd+C/V/X/A/Z)
let editMenuItem = NSMenuItem()
let editMenu = NSMenu(title: "Edit")
editMenu.addItem(withTitle: "Undo",
action: #selector(EditorTextView.undo(_:)),
keyEquivalent: "z")
let redoItem = editMenu.addItem(withTitle: "Redo",
action: #selector(EditorTextView.redo(_:)),
keyEquivalent: "z")
redoItem.keyEquivalentModifierMask = [.command, .shift]
editMenu.addItem(NSMenuItem.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")
editMenuItem.submenu = editMenu
mainMenu.addItem(editMenuItem)
// Format menu — built from the declarative command registry.
mainMenu.addItem(FormatMenu.build())
// View menu — built in its own file (ViewMenu.swift).
mainMenu.addItem(ViewMenu.build())
NSApplication.shared.mainMenu = mainMenu
}
}
// --- Launch -----------------------------------------------------------------
let app = NSApplication.shared
// Must be created before NSDocumentController.shared is first accessed.
let _ = DocumentController()
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps: true)
app.run()