import SwiftUI import AppKit import OnyxNotch import OnyxWidgetKit import OnyxMenuBar /// Der geteilte Zustand von Onyx: was im Panel liegt und auf welchen Displays. /// /// Änderungen wirken sofort. Eine Einstellung, die erst nach einem Neustart /// greift, ist in einer App ohne Fenster praktisch unauffindbar — man sieht ja /// nicht, dass etwas passiert ist. @MainActor @Observable final class AppModel { private let store: LayoutStore private let defaults: UserDefaults /// Wird gerufen, wenn sich die Panelgröße geändert haben könnte. var onLayoutChanged: (([WidgetPlacement]) -> Void)? /// Wird gerufen, wenn Panels auf anderen Displays erscheinen sollen. var onDisplayPolicyChanged: ((DisplayPolicy) -> Void)? var layout: [WidgetPlacement] { didSet { guard layout != oldValue else { return } persistLayout() onLayoutChanged?(layout) } } var displayPolicy: DisplayPolicy { didSet { guard displayPolicy != oldValue else { return } defaults.set(displayPolicy.rawValue, forKey: Keys.displayPolicy) onDisplayPolicyChanged?(displayPolicy) } } var showsMenuBarIcon: Bool { didSet { defaults.set(showsMenuBarIcon, forKey: Keys.showsMenuBarIcon) } } /// Ob Onyx im Dock auftaucht. /// /// Standardmäßig nicht: Onyx ist ein Werkzeug, das in der Notch und in der /// Menüleiste lebt, hat kein Hauptfenster und wäre im Dock ein Symbol, das /// beim Anklicken nichts öffnet. Es taucht dann auch im Programmumschalter /// auf und steht dort zwischen den Programmen, mit denen man arbeitet. var showsDockIcon: Bool { didSet { guard showsDockIcon != oldValue else { return } defaults.set(showsDockIcon, forKey: Keys.showsDockIcon) NSApp.setActivationPolicy(showsDockIcon ? .regular : .accessory) } } /// Der Menüleisten-Controller. Wird nach dem Aufbau gesetzt, damit /// Änderungen aus den Einstellungen sofort durchschlagen. weak var menuBar: MenuBarController? /// Die gespeicherten Einstellungen eines Menüleisten-Moduls. /// /// Standardmäßig ist **kein** Modul an. Sechs Symbole beim ersten Start /// wären eine Zumutung — wer sie will, schaltet sie einzeln ein. func menuBarSettings(for moduleID: String) -> MenuBarModuleSettings { guard let data = defaults.data(forKey: Keys.menuBarModule(moduleID)), let decoded = try? JSONDecoder().decode(MenuBarModuleSettings.self, from: data) else { return MenuBarModuleSettings() } return decoded } func setMenuBarSettings(_ settings: MenuBarModuleSettings, for moduleID: String) { if let data = try? JSONEncoder().encode(settings) { defaults.set(data, forKey: Keys.menuBarModule(moduleID)) } menuBar?.setEnabled(settings.isEnabled, forModuleID: moduleID) menuBar?.setPresentation(settings.presentation, forModuleID: moduleID) menuBarRevision += 1 } /// Zählt hoch, wenn sich ein Modul ändert — die Einstellungen zeichnen sich /// daran neu, ohne dass jedes Modul einzeln beobachtet werden müsste. private(set) var menuBarRevision = 0 private enum Keys { static func menuBarModule(_ id: String) -> String { "onyx.menubar.module.\(id)" } static let displayPolicy = "onyx.displayPolicy" static let showsMenuBarIcon = "onyx.showsMenuBarIcon" static let showsDockIcon = "onyx.showsDockIcon" } init(store: LayoutStore = .standard(), defaults: UserDefaults = .standard) { self.store = store self.defaults = defaults let known = Set(WidgetRegistry.shared.all.map(\.id)) self.layout = store.load(knownWidgetIDs: known) self.displayPolicy = defaults.string(forKey: Keys.displayPolicy) .flatMap(DisplayPolicy.init(rawValue:)) ?? .builtInOnly // Standardmäßig sichtbar: das Menüleistensymbol ist der einzige Weg // zurück, wenn die Notch einmal nicht reagiert. self.showsMenuBarIcon = defaults.object(forKey: Keys.showsMenuBarIcon) as? Bool ?? true self.showsDockIcon = defaults.object(forKey: Keys.showsDockIcon) as? Bool ?? false } // MARK: - Widgets var panelContentSize: CGSize { WidgetGrid.panelSize(for: layout, availableWidth: Self.availableWidth) } /// Wie breit das Panel höchstens werden darf. /// /// Der **schmalste** aktive Bildschirm gibt den Ausschlag: dasselbe Panel /// erscheint auf allen Displays, und eines, das auf dem Notebook über den /// Rand ragt, wäre auf dem großen Monitor kein Trost. Der Abzug lässt Luft /// zum Rand — ein Panel, das exakt bis zur Kante geht, wirkt eingeklemmt. static var availableWidth: CGFloat { let narrowest = NSScreen.screens.map(\.frame.width).min() ?? 1440 return narrowest - 120 } func isInPanel(_ widgetID: String) -> Bool { layout.contains { $0.widgetID == widgetID } } func add(_ widget: any OnyxWidget) { guard !isInPanel(widget.id) else { return } layout.append(WidgetPlacement(widgetID: widget.id)) } func remove(_ widgetID: String) { layout.removeAll { $0.widgetID == widgetID } } func move(fromOffsets source: IndexSet, toOffset destination: Int) { layout.move(fromOffsets: source, toOffset: destination) } private func persistLayout() { do { try store.save(layout) } catch { // Ein fehlgeschlagener Schreibvorgang darf die App nicht anhalten; // beim nächsten Start greift dann das Standardlayout. NSLog("Onyx: Layout konnte nicht gespeichert werden — \(error.localizedDescription)") } } }