diff --git a/resources/l10n/Localizable.strings b/resources/l10n/Localizable.strings index 4938d1c4..bceb48ab 100644 --- a/resources/l10n/Localizable.strings +++ b/resources/l10n/Localizable.strings @@ -55,6 +55,9 @@ /* No comment provided by engineer. */ "AltTab is currently set to show Applications. This setting is only available when AltTab is set to show Windows." = "AltTab is currently set to show Applications. This setting is only available when AltTab is set to show Windows."; +/* Menubar callout */ +"AltTab is running without Screen Recording permissions. Thumbnails won’t show." = "AltTab is running without Screen Recording permissions. Thumbnails won’t show."; + /* No comment provided by engineer. */ "AltTab needs some permissions" = "AltTab needs some permissions"; @@ -187,6 +190,9 @@ /* No comment provided by engineer. */ "General" = "General"; +/* Menubar callout button */ +"Grant permission" = "Grant permission"; + /* No comment provided by engineer. */ "Hide" = "Hide"; @@ -226,9 +232,18 @@ /* No comment provided by engineer. */ "Ignore shortcuts when active" = "Ignore shortcuts when active"; +/* No comment provided by engineer. */ +"Language" = "Language"; + +/* No comment provided by engineer. */ +"Language Change" = "Language Change"; + /* No comment provided by engineer. */ "Large" = "Large"; +/* No comment provided by engineer. */ +"Later" = "Later"; + /* No comment provided by engineer. */ "Latest releases" = "Latest releases"; @@ -310,6 +325,9 @@ /* No comment provided by engineer. */ "Reset preferences and restart…" = "Reset preferences and restart…"; +/* No comment provided by engineer. */ +"Restart Now" = "Restart Now"; + /* No comment provided by engineer. */ "Screen including menu bar" = "Screen including menu bar"; @@ -427,6 +445,9 @@ /* No comment provided by engineer. */ "Size" = "Size"; +/* No comment provided by engineer. */ +"Skipped" = "Skipped"; + /* No comment provided by engineer. */ "Small" = "Small"; @@ -454,6 +475,12 @@ /* No comment provided by engineer. */ "System" = "System"; +/* No comment provided by engineer. */ +"System Default" = "System Default"; + +/* No comment provided by engineer. */ +"The application needs to restart to apply the language change." = "The application needs to restart to apply the language change."; + /* No comment provided by engineer. */ "Theme" = "Theme"; @@ -481,6 +508,9 @@ /* No comment provided by engineer. */ "Updates policy" = "Updates policy"; +/* No comment provided by engineer. */ +"Use the app without this permission. Thumbnails won’t show." = "Use the app without this permission. Thumbnails won’t show."; + /* No comment provided by engineer. */ "Version" = "Version"; @@ -525,15 +555,3 @@ /* No comment provided by engineer. */ "You didn’t write your email, thus can’t receive any response." = "You didn’t write your email, thus can’t receive any response."; - -"Language" = "Language"; - -"System Default" = "System Default"; - -"Language Change" = "Language Change"; - -"The application needs to restart to apply the language change." = "The application needs to restart to apply the language change."; - -"Restart Now" = "Restart Now"; - -"Later" = "Later"; diff --git a/src/logic/Preferences.swift b/src/logic/Preferences.swift index 2201e36c..d6dd1eaf 100644 --- a/src/logic/Preferences.swift +++ b/src/logic/Preferences.swift @@ -101,6 +101,7 @@ class Preferences { "hideWindowlessApps": "false", "hideThumbnails": "false", "previewFocusedWindow": "false", + "screenRecordingPermissionSkipped": "false", ] // system preferences @@ -137,6 +138,7 @@ class Preferences { static var startAtLogin: Bool { defaults.bool("startAtLogin") } static var blacklist: [BlacklistEntry] { jsonDecode([BlacklistEntry].self, defaults.string("blacklist")) } static var previewFocusedWindow: Bool { defaults.bool("previewFocusedWindow") } + static var screenRecordingPermissionSkipped: Bool { defaults.bool("screenRecordingPermissionSkipped") } // macro values static var appearanceStyle: AppearanceStylePreference { defaults.macroPref("appearanceStyle", AppearanceStylePreference.allCases) } diff --git a/src/logic/SystemPermissions.swift b/src/logic/SystemPermissions.swift index 9f8d5cb8..eb319e3c 100644 --- a/src/logic/SystemPermissions.swift +++ b/src/logic/SystemPermissions.swift @@ -2,20 +2,23 @@ import Cocoa // macOS has some privacy restrictions. The user needs to grant certain permissions, app by app, in System Preferences > Security & Privacy class SystemPermissions { - static var timer: Timer! + static var preStartupPermissionsPassed = false + static var timerPermissionsToUpdatePermissionsWindow: Timer? + static var timerPermissionsRemovedWhileAltTabIsRunning: Timer? - static func accessibilityIsGranted() -> Bool { + static func accessibilityIsGranted() -> PermissionStatus { if #available(macOS 10.9, *) { - return AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeRetainedValue(): false] as CFDictionary) + return AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeRetainedValue(): false] as CFDictionary) ? .granted : .notGranted } - return true + return .granted } - static func screenRecordingIsGranted() -> Bool { + static func screenRecordingIsGranted() -> PermissionStatus { if #available(macOS 10.15, *) { - return screenRecordingIsGranted_() + return screenRecordingIsGranted_() ? .granted : + (Preferences.screenRecordingPermissionSkipped ? .skipped : .notGranted) } - return true + return .granted } // workaround: public API CGPreflightScreenCaptureAccess and private API SLSRequestScreenCaptureAccess exist, but @@ -33,65 +36,83 @@ class SystemPermissions { ) != nil } - static func observePermissionsPostStartup() { + static func observePermissionsRemovedWhileAltTabIsRunning() { var counter = 0 - timer = Timer(timeInterval: 5, repeats: true, block: { _ in - if !accessibilityIsGranted() { - App.app.restart() - } - if !screenRecordingIsGranted() { - // permission check may yield a false negative during wake-up - // we restart after 2 negative checks - if counter >= 2 { + timerPermissionsRemovedWhileAltTabIsRunning = Timer(timeInterval: 5, repeats: true, block: { _ in + DispatchQueue.main.async { + let accessibility = accessibilityIsGranted() + let screenRecording = screenRecordingIsGranted() + logger.d(accessibility, screenRecording, preStartupPermissionsPassed) + Menubar.permissionCalloutMenuItems?.forEach { + $0.isHidden = screenRecording != .skipped + } + if accessibility == .notGranted { App.app.restart() + } + if screenRecording == .notGranted { + // permission check may yield a false negative during wake-up + // we restart after 2 negative checks + if counter >= 2 { + App.app.restart() + } else { + counter += 1 + } } else { - counter += 1 + counter = 0 } - } else { - counter = 0 } }) - timer.tolerance = 4.9 - CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timer, .defaultMode) + timerPermissionsRemovedWhileAltTabIsRunning!.tolerance = 4.9 + CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timerPermissionsRemovedWhileAltTabIsRunning!, .defaultMode) } - static func observePermissionsPreStartup(_ startupBlock: @escaping () -> Void) { - if #available(macOS 10.15, *) { + static func observePermissionsToUpdatePermissionsWindow(_ startupBlock: @escaping () -> Void) { + if #available(macOS 10.15, *), !preStartupPermissionsPassed { // this call triggers the permission prompt, however it's the only way to force the app to be listed with a checkbox SLSRequestScreenCaptureAccess() } - timer = Timer(timeInterval: 0.1, repeats: true) { _ in - let accessibility = accessibilityIsGranted() - let screenRecording = screenRecordingIsGranted() + timerPermissionsToUpdatePermissionsWindow = Timer(timeInterval: 0.1, repeats: true) { _ in DispatchQueue.main.async { - if accessibility && screenRecording { - timer.invalidate() - App.app.permissionsWindow?.close() - startupBlock() - } else { - if accessibility != App.app.permissionsWindow.accessibilityView.isPermissionGranted { - App.app.permissionsWindow.accessibilityView.updatePermissionStatus(accessibility) + let accessibility = accessibilityIsGranted() + let screenRecording = screenRecordingIsGranted() + logger.d(accessibility, screenRecording, preStartupPermissionsPassed) + Menubar.permissionCalloutMenuItems?.forEach { + $0.isHidden = screenRecording != .skipped + } + if accessibility != App.app.permissionsWindow?.accessibilityView?.permissionStatus { + App.app.permissionsWindow?.accessibilityView.updatePermissionStatus(accessibility) + } + + if #available(macOS 10.15, *), screenRecording != App.app.permissionsWindow?.screenRecordingView?.permissionStatus { + App.app.permissionsWindow?.screenRecordingView?.updatePermissionStatus(screenRecording) + } + if !preStartupPermissionsPassed { + if accessibility != .notGranted && screenRecording != .notGranted { + preStartupPermissionsPassed = true + App.app.permissionsWindow?.close() + startupBlock() } - if #available(macOS 10.15, *), screenRecording != App.app.permissionsWindow.screenRecordingView.isPermissionGranted { - App.app.permissionsWindow.screenRecordingView.updatePermissionStatus(screenRecording) + } else { + if accessibility == .notGranted || screenRecording == .notGranted { + App.app.restart() } } } } - timer.tolerance = 0.09 - CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timer, .defaultMode) + timerPermissionsToUpdatePermissionsWindow!.tolerance = 0.09 + CFRunLoopAddTimer(BackgroundWork.systemPermissionsThread.runLoop, timerPermissionsToUpdatePermissionsWindow!, .defaultMode) } static func ensurePermissionsAreGranted(_ continueAppStartup: @escaping () -> Void) { let startupBlock = { - observePermissionsPostStartup() + observePermissionsRemovedWhileAltTabIsRunning() continueAppStartup() } - if accessibilityIsGranted() && screenRecordingIsGranted() { + if accessibilityIsGranted() != .notGranted && screenRecordingIsGranted() != .notGranted { + preStartupPermissionsPassed = true startupBlock() } else { - App.app.permissionsWindow.show() - observePermissionsPreStartup(startupBlock) + App.app.permissionsWindow.show(startupBlock) } } } diff --git a/src/ui/App.swift b/src/ui/App.swift index 88b02e12..5ccefdfa 100644 --- a/src/ui/App.swift +++ b/src/ui/App.swift @@ -18,7 +18,6 @@ class App: AppCenterApplication, NSApplicationDelegate { static let repository = "https://github.com/lwouis/alt-tab-macos" static let website = "https://alt-tab-macos.netlify.app" static var app: App! - var menubar: Menubar! var thumbnailsPanel: ThumbnailsPanel! var previewPanel: PreviewPanel! var preferencesWindow: PreferencesWindow! @@ -59,14 +58,14 @@ class App: AppCenterApplication, NSApplicationDelegate { PFMoveToApplicationsFolderIfNecessary() #endif AXUIElement.setGlobalTimeout() + Preferences.initialize() BackgroundWork.startSystemPermissionThread() permissionsWindow = PermissionsWindow() SystemPermissions.ensurePermissionsAreGranted { [weak self] in guard let self = self else { return } BackgroundWork.start() - Preferences.initialize() Appearance.update() - self.menubar = Menubar() + Menubar.initialize() self.loadMainMenuXib() self.thumbnailsPanel = ThumbnailsPanel() self.previewPanel = PreviewPanel() @@ -80,7 +79,7 @@ class App: AppCenterApplication, NSApplicationDelegate { DispatchQueue.main.async { () -> () in Windows.sortByLevel() } self.preloadWindows() #if DEBUG - self.showPreferencesWindow() +// self.showPreferencesWindow() #endif } } @@ -117,6 +116,7 @@ class App: AppCenterApplication, NSApplicationDelegate { func restart() { // we use -n to open a new instance, to avoid calling applicationShouldHandleReopen // we use Bundle.main.bundlePath in case of multiple AltTab versions on the machine + printStackTrace() Process.launchedProcess(launchPath: "/usr/bin/open", arguments: ["-n", Bundle.main.bundlePath]) App.shared.terminate(self) } @@ -180,7 +180,7 @@ class App: AppCenterApplication, NSApplicationDelegate { } @objc func checkPermissions(_ sender: NSMenuItem) { - permissionsWindow.show() + permissionsWindow.show({}) } @objc func supportProject() { diff --git a/src/ui/Menubar.swift b/src/ui/Menubar.swift index 0126113a..c67a6df7 100644 --- a/src/ui/Menubar.swift +++ b/src/ui/Menubar.swift @@ -1,14 +1,21 @@ import Cocoa class Menubar { - var statusItem: NSStatusItem! - var menu: NSMenu! + static var statusItem: NSStatusItem! + static var menu: NSMenu! + static var permissionCalloutMenuItems: [NSMenuItem]? - init() { + static func initialize() { menu = NSMenu() menu.title = App.name // perf: prevent going through expensive code-path within appkit + let permissionCalloutMenuItem = NSMenuItem(title: "", action: nil, keyEquivalent: "") + permissionCalloutMenuItem.view = PermissionCallout() + let calloutSeparator = NSMenuItem.separator() + permissionCalloutMenuItems = [permissionCalloutMenuItem, calloutSeparator] + menu.addItem(permissionCalloutMenuItem) + menu.addItem(calloutSeparator) menu.addItem( - withTitle: String(format: NSLocalizedString("About %@", comment: "Menubar option. %@ is AltTab"), App.name), + withTitle: String(format: NSLocalizedString("About %@", comment: "Menubar option. %@ is AltTab"), App.name), action: #selector(App.app.showAboutTab), keyEquivalent: "") menu.addItem(NSMenuItem.separator()) @@ -48,16 +55,16 @@ class Menubar { menubarIconCallback(nil) } - @objc func statusItemOnClick() { + @objc static func statusItemOnClick() { // NSApp.currentEvent == nil if the icon is "clicked" through VoiceOver if let type = NSApp.currentEvent?.type, type != .leftMouseDown { App.app.showUi() } else { - statusItem.popUpMenu(App.app.menubar.menu) + statusItem.popUpMenu(Menubar.menu) } } - func menubarIconCallback(_: NSControl?) { + static func menubarIconCallback(_: NSControl?) { if Preferences.menubarIcon == .hidden { statusItem.isVisible = false } else { @@ -65,7 +72,7 @@ class Menubar { } } - private func loadPreferredIcon() { + static private func loadPreferredIcon() { let i = imageIndexFromPreference() let image = NSImage(named: "menubar-" + i)! image.isTemplate = i == "3" ? false : true @@ -74,7 +81,7 @@ class Menubar { statusItem.button!.imageScaling = .scaleProportionallyUpOrDown } - private func imageIndexFromPreference() -> String { + static private func imageIndexFromPreference() -> String { switch Preferences.menubarIcon { case .outlined: return "1" case .filled: return "2" @@ -82,4 +89,25 @@ class Menubar { default: return "4" } } -} \ No newline at end of file +} + +class PermissionCallout: StackView { + convenience init() { + let label = NSTextField(wrappingLabelWithString: NSLocalizedString("AltTab is running without Screen Recording permissions. Thumbnails won’t show.", comment: "Menubar callout")) + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .white + label.preferredMaxLayoutWidth = 250 + label.isSelectable = false + label.widthAnchor.constraint(equalToConstant: 250).isActive = true + let button = NSButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.attributedTitle = NSAttributedString(string: NSLocalizedString("Grant permission", comment: "Menubar callout button"), attributes: [NSAttributedString.Key.foregroundColor: NSColor.white]) + button.onAction = { _ in + Preferences.remove("screenRecordingPermissionSkipped") + App.app.restart() + } + self.init([label, button], .vertical, true, top: 8, right: 15, bottom: 10, left: 15) + wantsLayer = true + layer!.backgroundColor = NSColor.purple.cgColor + } +} diff --git a/src/ui/permission-window/PermissionView.swift b/src/ui/permission-window/PermissionView.swift index 68e5f541..3ba6e087 100644 --- a/src/ui/permission-window/PermissionView.swift +++ b/src/ui/permission-window/PermissionView.swift @@ -3,11 +3,12 @@ import Cocoa class PermissionView: StackView { static let greenColor = NSColor(srgbRed: 0.38, green: 0.75, blue: 0.33, alpha: 0.2) static let redColor = NSColor(srgbRed: 0.90, green: 0.35, blue: 0.32, alpha: 0.2) + static let yellowColor = NSColor(srgbRed: 0.83, green: 0.66, blue: 0.07, alpha: 0.2) var status: NSTextField! - var isPermissionGranted = false + var permissionStatus = PermissionStatus.notGranted - convenience init(_ iconName: String, _ title: String, _ justification: String, _ buttonText: String, _ buttonUrl: String, _ checkFunction: @escaping () -> Bool) { + convenience init(_ iconName: String, _ title: String, _ justification: String, _ buttonText: String, _ buttonUrl: String, _ checkFunction: @escaping () -> PermissionStatus, _ skipCheckbox: NSView? = nil) { let icon = NSImageView(image: NSImage.initCopy(iconName)) icon.translatesAutoresizingMaskIntoConstraints = false icon.fit() @@ -29,25 +30,41 @@ class PermissionView: StackView { let buttonStack = NSStackView(views: [button, status]) buttonStack.translatesAutoresizingMaskIntoConstraints = false buttonStack.alignment = .centerY - self.init([titleStack, justification, buttonStack], .vertical, top: GridView.interPadding, right: GridView.interPadding, bottom: GridView.interPadding, left: GridView.interPadding) + var views = [titleStack, justification, buttonStack] + if let skipCheckbox = skipCheckbox { + views.append(skipCheckbox) + } + self.init(views, .vertical, top: GridView.interPadding, right: GridView.interPadding, bottom: GridView.interPadding, left: GridView.interPadding) self.status = status wantsLayer = true layer!.cornerRadius = GridView.interPadding / 2 updatePermissionStatus(checkFunction()) } - func updatePermissionStatus(_ isPermissionGranted: Bool) { - self.isPermissionGranted = isPermissionGranted - if isPermissionGranted { - let color = PermissionView.greenColor - status.stringValue = "● " + NSLocalizedString("Allowed", comment: "") - status.textColor = color.withAlphaComponent(1) - layer!.backgroundColor = color.cgColor - } else { - let color = PermissionView.redColor - status.stringValue = "● " + NSLocalizedString("Not allowed", comment: "") - status.textColor = color.withAlphaComponent(1) - layer!.backgroundColor = color.cgColor + func updatePermissionStatus(_ permissionStatus: PermissionStatus) { + self.permissionStatus = permissionStatus + var color: NSColor + var label: String + switch permissionStatus { + case .granted: + color = PermissionView.greenColor + label = NSLocalizedString("Allowed", comment: "") + case .notGranted: + color = PermissionView.redColor + label = NSLocalizedString("Not allowed", comment: "") + + case .skipped: + color = PermissionView.yellowColor + label = NSLocalizedString("Skipped", comment: "") } + status.stringValue = "● " + label + status.textColor = color.withAlphaComponent(1) + layer!.backgroundColor = color.cgColor } } + +enum PermissionStatus { + case granted + case notGranted + case skipped +} diff --git a/src/ui/permission-window/PermissionsWindow.swift b/src/ui/permission-window/PermissionsWindow.swift index 9b9e8cd2..16da42ac 100644 --- a/src/ui/permission-window/PermissionsWindow.swift +++ b/src/ui/permission-window/PermissionsWindow.swift @@ -11,7 +11,7 @@ class PermissionsWindow: NSWindow, NSWindowDelegate { setupView() } - func show() { + func show(_ startupBlock: @escaping () -> Void) { accessibilityView.updatePermissionStatus(SystemPermissions.accessibilityIsGranted()) if #available(macOS 10.15, *) { screenRecordingView.updatePermissionStatus(SystemPermissions.screenRecordingIsGranted()) @@ -19,17 +19,24 @@ class PermissionsWindow: NSWindow, NSWindowDelegate { center() App.shared.activate(ignoringOtherApps: true) makeKeyAndOrderFront(nil) + SystemPermissions.observePermissionsToUpdatePermissionsWindow(startupBlock) } - func windowShouldClose(_ sender: NSWindow) -> Bool { - if !SystemPermissions.accessibilityIsGranted() || !SystemPermissions.screenRecordingIsGranted() { - logger.e("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Accessibility.", - "Please authorize and re-launch.", - "See https://help.rescuetime.com/article/59-how-do-i-enable-accessibility-permissions-on-mac-osx", - separator: "\n") - App.shared.terminate(self) + + + func windowWillClose(_ notification: Notification) { + logger.d("PermissionsWindow windowWillClose", SystemPermissions.preStartupPermissionsPassed) + if !SystemPermissions.preStartupPermissionsPassed { + if SystemPermissions.accessibilityIsGranted() == .notGranted || SystemPermissions.screenRecordingIsGranted() == .notGranted { + logger.e("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Accessibility.", + "Please authorize and re-launch.", + "See https://help.rescuetime.com/article/59-how-do-i-enable-accessibility-permissions-on-mac-osx", + separator: "\n") + App.shared.terminate(self) + } + } else { + SystemPermissions.timerPermissionsToUpdatePermissionsWindow?.invalidate() } - return true } private func setupWindow() { @@ -66,7 +73,8 @@ class PermissionsWindow: NSWindow, NSWindowDelegate { NSLocalizedString("This permission is needed to show screenshots and titles of open windows", comment: ""), NSLocalizedString("Open Screen Recording Preferences…", comment: ""), "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture", - SystemPermissions.screenRecordingIsGranted + SystemPermissions.screenRecordingIsGranted, + StackView(LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Use the app without this permission. Thumbnails won’t show.", comment: ""), "screenRecordingPermissionSkipped", labelPosition: .right)) ) rows.append([screenRecordingView]) } diff --git a/src/ui/preferences-window/LabelAndControl.swift b/src/ui/preferences-window/LabelAndControl.swift index 84bf613a..d7848995 100644 --- a/src/ui/preferences-window/LabelAndControl.swift +++ b/src/ui/preferences-window/LabelAndControl.swift @@ -107,7 +107,6 @@ class LabelAndControl: NSObject { return views } - // periphery:ignore static func makeLabelWithCheckbox(_ labelText: String, _ rawName: String, extraAction: ActionClosure? = nil, labelPosition: LabelPosition = .leftWithSeparator) -> [NSView] { let checkbox = NSButton(checkboxWithTitle: labelPosition == .right ? labelText : " ", target: nil, action: nil) checkbox.translatesAutoresizingMaskIntoConstraints = false diff --git a/src/ui/preferences-window/tabs/GeneralTab.swift b/src/ui/preferences-window/tabs/GeneralTab.swift index af0a2d7b..4587eeff 100644 --- a/src/ui/preferences-window/tabs/GeneralTab.swift +++ b/src/ui/preferences-window/tabs/GeneralTab.swift @@ -7,7 +7,7 @@ class GeneralTab { let startAtLogin = TableGroupView.Row(leftTitle: NSLocalizedString("Start at login", comment: ""), rightViews: [LabelAndControl.makeSwitch("startAtLogin", extraAction: startAtLoginCallback)]) let menubarIcon = TableGroupView.Row(leftTitle: NSLocalizedString("Menubar icon", comment: ""), - rightViews: [LabelAndControl.makeDropdown("menubarIcon", MenubarIconPreference.allCases, extraAction: App.app.menubar.menubarIconCallback)]) + rightViews: [LabelAndControl.makeDropdown("menubarIcon", MenubarIconPreference.allCases, extraAction: Menubar.menubarIconCallback)]) let language = TableGroupView.Row(leftTitle: NSLocalizedString("Language", comment: ""), rightViews: [LabelAndControl.makeDropdown("language", LanguagePreference.allCases, extraAction: setLanguageCallback)]) let resetPreferences = NSButton(title: NSLocalizedString("Reset preferences and restart…", comment: ""), target: self, action: #selector(GeneralTab.resetPreferences)) @@ -40,8 +40,8 @@ class GeneralTab { } private static func enableDraggingOffMenubarIcon(_ menubarIconDropdown: NSPopUpButton) { - App.app.menubar.statusItem.behavior = .removalAllowed - menubarIsVisibleObserver = App.app.menubar.statusItem.observe(\.isVisible, options: [.old, .new]) { _, change in + Menubar.statusItem.behavior = .removalAllowed + menubarIsVisibleObserver = Menubar.statusItem.observe(\.isVisible, options: [.old, .new]) { _, change in if change.oldValue == true && change.newValue == false { let hiddenIndex = Int(MenubarIconPreference.hidden.rawValue)! menubarIconDropdown.selectItem(at: hiddenIndex)