Berechtigungsdialog und Einstellungsfenster reparieren

Zwei Fehler, beide durch Messen gefunden statt durch Raten — und einer davon
war meine eigene fehlerhafte Diagnose: `log show` wurde von zsh abgefangen, ich
hatte stderr umgeleitet und die Fehlermeldung nie gesehen. Meine Aussage "keine
TCC-Einträge, also wurde nie gefragt" stützte sich damit auf einen Befehl, der
gar nicht lief.

Einstellungsfenster: SwiftUIs Settings-Szene wird über den privaten Selektor
showSettingsWindow: geöffnet, dessen Name sich zwischen macOS-Versionen schon
geändert hat und der bei .accessory-Apps ohne Menüleiste unzuverlässig ankommt.
Der Menüeintrag tat schlicht nichts. Jetzt ein eigenes NSWindow.

Kalenderberechtigung, zwei Ursachen übereinander:

Xcode lagert im Debug-Build den Programmcode in eine eigene dylib aus (für
SwiftUI-Vorschauen). Das Hauptprogramm ist dann nur ein Rumpf, und TCC ordnete
die Anfrage dem Rumpf zu — kTCCServiceCalendar erreichte tccd überhaupt nicht.
ENABLE_DEBUG_DYLIB = NO behebt das; danach erscheint die Anfrage im Protokoll.

Danach immer noch kein Dialog: promptType 1, aber promptPolicy 0. TCC bereitet
den Dialog vor und entscheidet sich dagegen, weil keine App im Vordergrund ist,
an die er gehören könnte. NSApp.activate davor hilft nicht — Aktivierung wirkt
asynchron und ist beim Aufruf noch nicht durch. Deshalb wird beim ersten Start
das Einstellungsfenster auf dem Reiter Berechtigungen geöffnet: dort steht ein
echtes Fenster im Vordergrund, und der Nutzer weiß, warum gefragt wird.

Folgefehler dabei: ohne Berechtigung zeigte das Widget "Nichts mehr für heute"
— eine Aussage, die es ohne Zugriff gar nicht treffen kann. refresh() fragt
jetzt nicht mehr selbst nach und meldet ohne Berechtigung ehrlich `denied`.

Fehler aus requestFullAccessToEvents werden nicht mehr mit try? verschluckt.
This commit is contained in:
Guido Schmit
2026-08-10 18:20:21 +02:00
parent 70d4d67cca
commit 40bd83e49e
11 changed files with 774 additions and 141 deletions

View File

@@ -29,6 +29,11 @@
<!-- Berechtigungstexte. Sie erscheinen wörtlich im Systemdialog, deshalb
steht hier, wofür Onyx den Zugriff braucht — nicht, dass es ihn braucht. -->
<!-- Der Alt-Schlüssel bleibt gesetzt: Teile von EventKit prüfen ihn
weiterhin, und ein fehlender Verwendungszweck äußert sich nicht als
Fehler, sondern als ausbleibender Dialog. -->
<key>NSCalendarsUsageDescription</key>
<string>Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben.</string>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben.</string>
<key>NSLocationWhenInUseUsageDescription</key>

View File

@@ -1,135 +1,453 @@
{
"sourceLanguage" : "en",
"strings" : {
"menu.openPanel" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Panel öffnen" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Open Panel" } }
"sourceLanguage": "en",
"strings": {
"menu.openPanel": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Panel öffnen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Open Panel"
}
}
}
},
"menu.settings" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Einstellungen …" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Settings…" } }
"menu.quit": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Onyx beenden"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Quit Onyx"
}
}
}
},
"menu.quit" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Onyx beenden" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Quit Onyx" } }
"menu.settings": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Einstellungen …"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Settings…"
}
}
}
},
"settings.tab.widgets" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Widgets" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Widgets" } }
"settings.display.all": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Allen Displays"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "All displays"
}
}
}
},
"settings.tab.display" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Anzeige" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Display" } }
"settings.display.builtInOnly": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Nur dem internen Display"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Built-in display only"
}
}
}
},
"settings.widgets.hint" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Die Reihenfolge bestimmt die Anordnung im Panel. Jedes Widget rückt an die erste Stelle, an die es passt." } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "The order determines the arrangement in the panel. Each widget takes the first spot it fits into." } }
"settings.display.hint": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Auf Displays ohne echte Notch erscheint oben mittig ein Balken, der sich genauso verhält. Ist der Deckel geschlossen und kein internes Display vorhanden, erscheint das Panel trotzdem auf einem externen — sonst wäre Onyx nicht erreichbar."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "On displays without a real notch, a bar appears at the top centre and behaves the same. With the lid closed and no built-in display present, the panel still appears on an external one — otherwise Onyx would be unreachable."
}
}
}
},
"settings.widgets.inPanel" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Im Panel" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "In the panel" } }
"settings.display.menuBarIcon": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Symbol in der Menüleiste anzeigen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Show icon in the menu bar"
}
}
}
},
"settings.widgets.available" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Verfügbar" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Available" } }
"settings.display.menuBarIcon.hint": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Über das Symbol erreichst du Panel, Einstellungen und Beenden. Ohne es kommst du nur über die Notch an Onyx heran."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "The icon gives you the panel, settings and quit. Without it, the notch is the only way to reach Onyx."
}
}
}
},
"settings.widgets.empty" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Widgets im Panel. Das Panel bleibt leer, bis du eins hinzufügst." } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "No widgets in the panel. It stays empty until you add one." } }
"settings.display.policy": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Panel anzeigen auf"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Show panel on"
}
}
}
},
"settings.widgets.add" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Hinzufügen" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Add" } }
"settings.size.large": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Groß (2×2)"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Large (2×2)"
}
}
}
},
"settings.widgets.remove" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Aus dem Panel entfernen" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Remove from panel" } }
"settings.size.medium": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Mittel (2×1)"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Medium (2×1)"
}
}
}
},
"settings.size.small" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Klein (1×1)" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Small (1×1)" } }
"settings.size.small": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Klein (1×1)"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Small (1×1)"
}
}
}
},
"settings.size.medium" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Mittel (2×1)" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Medium (2×1)" } }
"settings.size.wide": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Breit (4×1)"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Wide (4×1)"
}
}
}
},
"settings.size.large" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Groß (2×2)" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Large (2×2)" } }
"settings.tab.display": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Anzeige"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Display"
}
}
}
},
"settings.size.wide" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Breit (4×1)" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Wide (4×1)" } }
"settings.tab.widgets": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Widgets"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Widgets"
}
}
}
},
"settings.display.policy" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Panel anzeigen auf" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Show panel on" } }
"settings.widgets.add": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Hinzufügen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Add"
}
}
}
},
"settings.display.builtInOnly" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Nur dem internen Display" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Built-in display only" } }
"settings.widgets.available": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Verfügbar"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Available"
}
}
}
},
"settings.display.all" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Allen Displays" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "All displays" } }
"settings.widgets.empty": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Keine Widgets im Panel. Das Panel bleibt leer, bis du eins hinzufügst."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "No widgets in the panel. It stays empty until you add one."
}
}
}
},
"settings.display.hint" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Auf Displays ohne echte Notch erscheint oben mittig ein Balken, der sich genauso verhält. Ist der Deckel geschlossen und kein internes Display vorhanden, erscheint das Panel trotzdem auf einem externen — sonst wäre Onyx nicht erreichbar." } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "On displays without a real notch, a bar appears at the top centre and behaves the same. With the lid closed and no built-in display present, the panel still appears on an external one — otherwise Onyx would be unreachable." } }
"settings.widgets.hint": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Die Reihenfolge bestimmt die Anordnung im Panel. Jedes Widget rückt an die erste Stelle, an die es passt."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "The order determines the arrangement in the panel. Each widget takes the first spot it fits into."
}
}
}
},
"settings.display.menuBarIcon" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Symbol in der Menüleiste anzeigen" } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "Show icon in the menu bar" } }
"settings.widgets.inPanel": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Im Panel"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "In the panel"
}
}
}
},
"settings.display.menuBarIcon.hint" : {
"localizations" : {
"de" : { "stringUnit" : { "state" : "translated", "value" : "Über das Symbol erreichst du Panel, Einstellungen und Beenden. Ohne es kommst du nur über die Notch an Onyx heran." } },
"en" : { "stringUnit" : { "state" : "translated", "value" : "The icon gives you the panel, settings and quit. Without it, the notch is the only way to reach Onyx." } }
"settings.widgets.remove": {
"extractionState": "stale",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Aus dem Panel entfernen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Remove from panel"
}
}
}
},
"settings.tab.permissions": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Berechtigungen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Permissions"
}
}
}
},
"settings.permissions.calendar": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Kalender"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Calendar"
}
}
}
},
"settings.permissions.granted": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Erteilt"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Granted"
}
}
}
},
"settings.permissions.request": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Anfragen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Request"
}
}
}
},
"settings.permissions.calendar.why": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Onyx shows your events in the calendar widget. It only ever reads, never writes."
}
}
}
},
"settings.permissions.openSystem": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Systemeinstellungen öffnen"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Open System Settings"
}
}
}
}
},
"version" : "1.0"
}
"version": "1.0"
}

View File

@@ -15,15 +15,10 @@ struct OnyxApp: App {
// 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)
}
}
// Eine Szene ist Pflicht, benutzt wird sie nicht: das
// Einstellungsfenster führt SettingsWindowController selbst.
// Siehe die Begründung dort.
Settings { EmptyView() }
}
}
@@ -35,6 +30,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var menuBar: MenuBarController?
private var onyxItem: NSStatusItem?
private var calendarModel: CalendarModel?
private let settingsWindow = SettingsWindowController()
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)
@@ -78,6 +74,38 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
installOnyxStatusItem()
observeMenuBarPreference()
requestPermissions()
}
/// Berechtigungen beim Start anfragen, nicht erst wenn ein Widget sichtbar
/// wird.
///
/// Das Panel ist beim Start zu. Eine an die View gebundene Anfrage kommt
/// deshalb erst, wenn jemand die Notch berührt und bis dahin taucht Onyx
/// in den Systemeinstellungen gar nicht auf, es gibt also nichts, das man
/// dort freischalten könnte. Genau dieser Zustand ist beim Testen
/// aufgetreten.
private func requestPermissions() {
guard let calendarModel else { return }
// Immer einmal laden: mit Berechtigung kommen Termine, ohne sie zeigt
// das Widget den Berechtigungshinweis statt einer leeren Liste.
calendarModel.refresh()
guard !calendarModel.hasAccess else { return }
// Nicht im Hintergrund anfragen.
//
// Onyx ist eine App ohne Fenster. TCC bereitet den Dialog zwar vor
// (promptType 1), entscheidet sich dann aber dagegen (promptPolicy 0),
// weil keine App im Vordergrund ist, an die er gehören könnte. Die
// Anfrage kommt kommentarlos mit `false` zurück, der Status bleibt
// `notDetermined` und in den Systemeinstellungen taucht Onyx nie auf.
// `NSApp.activate` davor hilft nicht: Aktivierung wirkt asynchron und
// ist beim Aufruf noch nicht durch.
//
// Deshalb der ehrliche Weg: beim ersten Start das Einstellungsfenster
// öffnen. Dort ist ein echtes Fenster im Vordergrund, der Dialog
// erscheint zuverlässig und der Nutzer weiß, warum gefragt wird.
showSettings(tab: .permissions)
}
func applicationWillTerminate(_ notification: Notification) {
@@ -140,9 +168,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
@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 openSettings() { showSettings(tab: .widgets) }
private func showSettings(tab: SettingsTab) {
guard let model, let calendarModel else { return }
settingsWindow.show(model: model, calendarModel: calendarModel, tab: tab)
}
@objc private func quit() { NSApp.terminate(nil) }

View File

@@ -2,21 +2,90 @@ import SwiftUI
import OnyxDesign
import OnyxNotch
import OnyxWidgetKit
import CalendarProvider
enum SettingsTab: Hashable {
case widgets, display, permissions
}
struct SettingsView: View {
@Bindable var model: AppModel
let calendarModel: CalendarModel
@Binding var selectedTab: SettingsTab
var body: some View {
TabView {
TabView(selection: $selectedTab) {
WidgetSettings(model: model)
.tabItem { Label("settings.tab.widgets", systemImage: "square.grid.2x2") }
.tag(SettingsTab.widgets)
DisplaySettings(model: model)
.tabItem { Label("settings.tab.display", systemImage: "macbook") }
.tag(SettingsTab.display)
PermissionSettings(calendarModel: calendarModel)
.tabItem { Label("settings.tab.permissions", systemImage: "hand.raised") }
.tag(SettingsTab.permissions)
}
.frame(width: 520, height: 420)
}
}
// MARK: - Berechtigungen
/// Zeigt den Zustand, statt ihn nur spürbar zu machen.
///
/// Eine verweigerte Berechtigung äußert sich sonst als leeres Widget, und der
/// Weg zurück ist nicht offensichtlich: hat macOS die Anfrage einmal
/// abgelehnt bekommen, fragt es nie wieder es bleibt nur der Gang in die
/// Systemeinstellungen.
private struct PermissionSettings: View {
let calendarModel: CalendarModel
@State private var granted = false
var body: some View {
Form {
Section {
HStack {
Label("settings.permissions.calendar", systemImage: "calendar")
Spacer()
if granted {
Label("settings.permissions.granted", systemImage: "checkmark.circle.fill")
.foregroundStyle(.green)
.labelStyle(.titleAndIcon)
} else {
Button("settings.permissions.request") {
Task {
let ok = await calendarModel.requestAccessNow()
granted = ok
// macOS fragt nach einer Ablehnung nicht erneut.
// Dann hilft nur noch der Weg über die
// Systemeinstellungen also dorthin führen.
if !ok { openPrivacySettings() }
}
}
}
}
Text("settings.permissions.calendar.why")
.font(.callout)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Section {
Button("settings.permissions.openSystem") { openPrivacySettings() }
}
}
.formStyle(.grouped)
.padding()
.onAppear { granted = calendarModel.hasAccess }
}
private func openPrivacySettings() {
guard let url = URL(string:
"x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars") else { return }
NSWorkspace.shared.open(url)
}
}
// MARK: - Widgets
private struct WidgetSettings: View {

View File

@@ -0,0 +1,56 @@
import AppKit
import SwiftUI
import CalendarProvider
/// Führt das Einstellungsfenster selbst.
///
/// SwiftUIs `Settings`-Szene wird über `NSApp.sendAction(showSettingsWindow:)`
/// geöffnet ein privater Selektor, dessen Name sich zwischen macOS-Versionen
/// schon geändert hat (`showPreferencesWindow:` davor) und der bei
/// `.accessory`-Apps ohne Menüleiste unzuverlässig ankommt. Genau das ist hier
/// passiert: der Menüeintrag tat nichts.
///
/// Ein eigenes `NSWindow` hat diese Abhängigkeit nicht. Es kostet ein paar
/// Zeilen mehr und funktioniert dafür.
@MainActor
final class SettingsWindowController: NSObject, NSWindowDelegate {
private var window: NSWindow?
/// Muss außerhalb der View liegen, damit ein erneutes Öffnen den Reiter setzen kann.
private var selectedTab: SettingsTab = .widgets
func show(model: AppModel, calendarModel: CalendarModel, tab: SettingsTab = .widgets) {
selectedTab = tab
if window == nil {
let hosting = NSHostingController(
rootView: SettingsView(model: model,
calendarModel: calendarModel,
selectedTab: Binding(
get: { [weak self] in self?.selectedTab ?? .widgets },
set: { [weak self] in self?.selectedTab = $0 })))
let window = NSWindow(contentViewController: hosting)
window.title = "Onyx"
window.styleMask = [.titled, .closable, .miniaturizable]
window.isReleasedWhenClosed = false
window.delegate = self
window.center()
// Über die Position freuen sich Nutzer mit mehreren Displays:
// beim zweiten Öffnen steht es wieder da, wo sie es hingeschoben haben.
window.setFrameAutosaveName("onyx.settings")
self.window = window
}
// Ohne `activate` bleibt das Fenster einer .accessory-App hinter der
// aktiven App liegen und wirkt, als wäre nichts passiert.
NSApp.activate(ignoringOtherApps: true)
window?.makeKeyAndOrderFront(nil)
}
func windowWillClose(_ notification: Notification) {
// Zurück in den Hintergrund: Onyx hat sonst weiter den Fokus, obwohl
// kein Fenster mehr offen ist.
NSApp.hide(nil)
}
}