Das Raster ist weg. Widgets stehen nebeneinander, alle gleich groß, in der Reihenfolge aus den Einstellungen. Das Panel sitzt am oberen Rand — nach unten zu wachsen bedeckt den Bildschirm, nach rechts und links legt es sich in den ohnehin leeren Streifen neben der Notch. Eine zweite Reihe entsteht erst, wenn der Bildschirm keine weitere Kachel mehr hergibt. Die Größenauswahl fällt damit weg, und das ist kein Verlust: ein 2×1 neben einem 2×2 lässt oben rechts eine Lücke, die niemand füllen kann und die aussieht wie ein Fehler. Genau daher kam auch die Überlappung. Ein Test prüft jetzt für ein bis zwölf Kacheln, dass sich keine zwei schneiden und alle vollständig im Panel liegen — außerhalb heißt oben: hinter der Notch. Kacheln werden zusätzlich beschnitten, damit überquellender Inhalt nicht über den Nachbarn zeichnet. Der Kalender wählt jetzt zwischen Monatsraster und Terminliste. Das war vorher an die Kachelgröße gekoppelt und ist in Wahrheit eine Frage dessen, was man sehen will. In der Menüleiste wird die Breite gemessen statt geschätzt. Vorher stand je Darstellungsart eine feste Zahl im Code, großzügig gewählt — mit dem Ergebnis, dass neben jedem Wert Platz für ein weiteres Symbol blieb, in dem nichts stand. Gemessen wird die breiteste vorkommende Zeichenfolge, nicht die gerade angezeigte, sonst springt die Leiste bei jedem Messwert. Die Balkendarstellung bekommt ihre Breite aus der Kernzahl, die erst nach der ersten Messung feststeht. Popover schließen jetzt beim Klick daneben. `behavior = .transient` genügt nicht: es greift, solange die eigene App aktiv ist, aber Onyx läuft als .accessory und wird durch einen Klick auf ein Statuselement nicht aktiviert. Ein Klick in ein fremdes Fenster erreichte das Popover gar nicht. Ein globaler Beobachter sieht solche Klicks, ein lokaler die in der eigenen App, Escape schließt ebenfalls — und es ist immer nur eines offen. Mixer und Lüfter haben eigene Menüleistenelemente. Beides regelt man mitten in etwas anderem; der Weg über ein Einstellungsfenster war zu weit. Die Lüfter sind damit auch als Panel-Kachel echt statt Platzhalter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
153 lines
5.8 KiB
Swift
153 lines
5.8 KiB
Swift
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)")
|
|
}
|
|
}
|
|
}
|