Phase 2 abgeschlossen: Einstellungsfenster und Lokalisierung

Einstellungen wirken sofort statt erst nach einem Neustart. In einer App ohne
Fenster ist eine verzögerte Änderung praktisch unauffindbar — man sieht ja
nicht, dass etwas passiert ist. AppModel meldet Layout- und Anzeigeänderungen
an den Koordinator, das Panel zeichnet sich neu und die Panelgröße zieht mit.

Widgets lassen sich hinzufügen, entfernen, in der Größe ändern und per
Drag-and-Drop umsortieren. Angeboten werden nur die Größen, die ein Widget
sinnvoll ausfüllt — ein Mini-Monat in 1x1 wäre unleserlich.

Lokalisierung als String Catalog in DE und EN, Quellsprache Englisch damit die
Schlüssel im Code lesbar bleiben. Der Katalog von OnyxCore musste ausdrücklich
als Ressource deklariert werden: ohne das gibt es kein Bundle.module und die
Übersetzungen wären zur Laufzeit unauffindbar. Im gebauten Bundle liegen jetzt
de.lproj und en.lproj, auch für das Paket.

Kein Menüleisten-Tab: solange es keine Module gibt, wäre das eine Oberfläche
für ein Feature, das noch nicht existiert. Er kommt mit Phase 5.

80 Tests grün.
This commit is contained in:
Guido Schmit
2026-08-10 18:03:34 +02:00
parent c0eb44b0b0
commit dd25d788c0
8 changed files with 533 additions and 32 deletions

View File

@@ -11,21 +11,29 @@ struct OnyxApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
var body: some Scene {
// Onyx hat kein Hauptfenster. Alles lebt im Notch-Panel und in der
// Menüleiste; die Einstellungen kommen in Phase 2 als eigenes Fenster.
Settings { EmptyView() }
// 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 layout: [WidgetPlacement] = []
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)
@@ -39,29 +47,35 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
PlaceholderWidgets.registerAll()
let known = Set(WidgetRegistry.shared.all.map(\.id))
layout = LayoutStore.standard().load(knownWidgetIDs: known)
let placements = layout
let coordinator = NotchCoordinator(policy: .builtInOnly) { _, presentation in
let model = AppModel()
self.model = model
let coordinator = NotchCoordinator(policy: model.displayPolicy) { _, presentation in
NotchHostingView(
rootView: NotchContainer(presentation: presentation) {
WidgetGrid(placements: placements)
NotchPanelContent(model: model)
},
presentation: presentation)
}
coordinator.start()
// Die Panelgröße folgt dem Layout, nicht umgekehrt: wer ein Widget
// hinzufügt, soll nicht auch noch die Fenstergröße nachziehen müssen.
let size = WidgetGrid.panelSize(for: layout)
coordinator.activeControllers.forEach { $0.contentSize = size }
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) {
@@ -69,6 +83,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
menuBar?.stop()
}
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() {
@@ -79,19 +101,32 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
item.button?.image?.isTemplate = true
let menu = NSMenu()
menu.addItem(withTitle: "Panel öffnen", action: #selector(togglePanel), keyEquivalent: "")
.target = self
menu.addItem(withTitle: String(localized: "menu.openPanel"),
action: #selector(togglePanel), keyEquivalent: "").target = self
menu.addItem(.separator())
menu.addItem(withTitle: "Einstellungen …", action: #selector(openSettings), keyEquivalent: ",")
.target = self
menu.addItem(withTitle: String(localized: "menu.settings"),
action: #selector(openSettings), keyEquivalent: ",").target = self
menu.addItem(.separator())
menu.addItem(withTitle: "Onyx beenden", action: #selector(quit), keyEquivalent: "q")
.target = self
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() {
@@ -102,3 +137,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
@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)
}
}