import SwiftUI import AppKit import OnyxCore import OnyxDesign import OnyxNotch import OnyxMenuBar import OnyxWidgetKit import CalendarProvider @main struct OnyxApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate var body: some Scene { // Onyx hat kein Hauptfenster — alles lebt in der Notch und in der // Menüleiste. Die Einstellungen sind die einzige Ausnahme; im Panel // wäre dafür zu wenig Platz. Settings { if let model = delegate.model { SettingsView(model: model) } else { // Nur sichtbar, wenn die Einzelinstanz-Prüfung uns abgewiesen // hat und die App gerade wieder verschwindet. ProgressView().frame(width: 520, height: 420) } } } } @MainActor final class AppDelegate: NSObject, NSApplicationDelegate { private(set) var model: AppModel? private var coordinator: NotchCoordinator? private var menuBar: MenuBarController? private var onyxItem: NSStatusItem? private var calendarModel: CalendarModel? func applicationDidFinishLaunching(_ notification: Notification) { NSApp.setActivationPolicy(.accessory) // Vor allem anderen: sicherstellen, dass wir allein sind. Zwei Instanzen // bringen zwei Zustandsmaschinen und zwei Animationen auf dieselbe // Notch — das sieht aus wie ein Ruckler und ist keiner. if case .blocked(let pids) = SingleInstance.claim() { SingleInstance.explainBlocked(by: pids) NSApp.terminate(nil) return } registerWidgets() let model = AppModel() self.model = model let coordinator = NotchCoordinator(policy: model.displayPolicy) { _, presentation in NotchHostingView( rootView: NotchContainer(presentation: presentation) { NotchPanelContent(model: model) }, presentation: presentation) } coordinator.start() self.coordinator = coordinator applyContentSize() // Einstellungen wirken sofort. Eine Änderung, die erst nach einem // Neustart greift, ist in einer App ohne Fenster praktisch unauffindbar. model.onLayoutChanged = { [weak self] _ in self?.applyContentSize() } model.onDisplayPolicyChanged = { [weak self] policy in self?.coordinator?.policy = policy self?.applyContentSize() } let menuBar = MenuBarController() menuBar.start() self.menuBar = menuBar installOnyxStatusItem() observeMenuBarPreference() } func applicationWillTerminate(_ notification: Notification) { coordinator?.stop() menuBar?.stop() } /// Echte Widgets zuerst, danach die Platzhalter für alles, was noch fehlt. private func registerWidgets() { let calendarModel = CalendarModel(source: EventKitSource()) self.calendarModel = calendarModel WidgetRegistry.shared.register(CalendarWidget(model: calendarModel)) PlaceholderWidgets.registerAll() } private func applyContentSize() { guard let model, let coordinator else { return } let size = model.panelContentSize coordinator.activeControllers.forEach { $0.contentSize = size } } // MARK: - Menüleiste /// Das feste Onyx-Element: Panel öffnen, Einstellungen, Beenden. Es ist auch /// das Sicherheitsnetz, falls die Notch einmal nicht reagiert. private func installOnyxStatusItem() { let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) item.autosaveName = "onyx.menubar.main" item.button?.image = NSImage(systemSymbolName: "circle.hexagongrid.fill", accessibilityDescription: "Onyx") item.button?.image?.isTemplate = true let menu = NSMenu() menu.addItem(withTitle: String(localized: "menu.openPanel"), action: #selector(togglePanel), keyEquivalent: "").target = self menu.addItem(.separator()) menu.addItem(withTitle: String(localized: "menu.settings"), action: #selector(openSettings), keyEquivalent: ",").target = self menu.addItem(.separator()) menu.addItem(withTitle: String(localized: "menu.quit"), action: #selector(quit), keyEquivalent: "q").target = self item.menu = menu onyxItem = item } private func observeMenuBarPreference() { guard let model else { return } withObservationTracking { _ = model.showsMenuBarIcon } onChange: { Task { @MainActor [weak self] in self?.onyxItem?.isVisible = model.showsMenuBarIcon self?.observeMenuBarPreference() } } onyxItem?.isVisible = model.showsMenuBarIcon } @objc private func togglePanel() { coordinator?.togglePanelUnderPointer() } @objc private func openSettings() { NSApp.activate(ignoringOtherApps: true) NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) } @objc private func quit() { NSApp.terminate(nil) } } /// Der Inhalt des Panels. Liest `model.layout` und zeichnet sich damit neu, /// sobald in den Einstellungen etwas geändert wird. private struct NotchPanelContent: View { let model: AppModel var body: some View { WidgetGrid(placements: model.layout) } }