From 3b60aa273b7298d161655d07d825750cba3dee99 Mon Sep 17 00:00:00 2001 From: Michele Primavera Date: Mon, 21 Oct 2024 17:59:03 +0200 Subject: [PATCH 1/5] Let Ice work without the screen recording permission Implements #75 --- Ice/Main/AppState.swift | 4 +++- .../ItemManagement/MenuBarItemImageCache.swift | 7 ++++++- .../ItemManagement/MenuBarItemManager.swift | 13 +++++++++++-- Ice/MenuBar/Search/MenuBarSearchPanel.swift | 4 +++- Ice/Permissions/Permission.swift | 7 +++++++ Ice/Permissions/PermissionsManager.swift | 2 +- .../MenuBarLayoutSettingsPane.swift | 15 +++++++++++++++ Ice/UI/IceBar/IceBar.swift | 5 ++++- Ice/Utilities/ScreenCapture.swift | 16 ++++++++++++++++ 9 files changed, 66 insertions(+), 7 deletions(-) diff --git a/Ice/Main/AppState.swift b/Ice/Main/AppState.swift index 77872bf2..d06d8efa 100644 --- a/Ice/Main/AppState.swift +++ b/Ice/Main/AppState.swift @@ -146,7 +146,9 @@ final class AppState: ObservableObject { return } Task.detached { - await self.imageCache.updateCacheWithoutChecks(sections: MenuBarSection.Name.allCases) + if ScreenCapture.cachedCheckPermissions(reset: true) { + await self.imageCache.updateCacheWithoutChecks(sections: MenuBarSection.Name.allCases) + } } } .store(in: &c) diff --git a/Ice/MenuBar/ItemManagement/MenuBarItemImageCache.swift b/Ice/MenuBar/ItemManagement/MenuBarItemImageCache.swift index 1a6857c7..97910ab0 100644 --- a/Ice/MenuBar/ItemManagement/MenuBarItemImageCache.swift +++ b/Ice/MenuBar/ItemManagement/MenuBarItemImageCache.swift @@ -63,7 +63,9 @@ final class MenuBarItemImageCache: ObservableObject { return } Task.detached { - await self.updateCache() + if (ScreenCapture.cachedCheckPermissions()) { + await self.updateCache() + } } } .store(in: &c) @@ -81,6 +83,9 @@ final class MenuBarItemImageCache: ObservableObject { /// the given section. @MainActor func cacheFailed(for section: MenuBarSection.Name) -> Bool { + guard ScreenCapture.cachedCheckPermissions() else { + return true + } let items = appState?.itemManager.itemCache[section] ?? [] guard !items.isEmpty else { return false diff --git a/Ice/MenuBar/ItemManagement/MenuBarItemManager.swift b/Ice/MenuBar/ItemManagement/MenuBarItemManager.swift index 04a2ed1f..07888fef 100644 --- a/Ice/MenuBar/ItemManagement/MenuBarItemManager.swift +++ b/Ice/MenuBar/ItemManagement/MenuBarItemManager.swift @@ -181,7 +181,9 @@ final class MenuBarItemManager: ObservableObject { return } Task { - await self.cacheItemsIfNeeded() + if(ScreenCapture.cachedCheckPermissions()) { + await self.cacheItemsIfNeeded() + } } } .store(in: &c) @@ -193,7 +195,9 @@ final class MenuBarItemManager: ObservableObject { return } Task { - await self.cacheItemsIfNeeded() + if(ScreenCapture.cachedCheckPermissions()) { + await self.cacheItemsIfNeeded() + } } } .store(in: &c) @@ -313,6 +317,11 @@ extension MenuBarItemManager { /// Caches the current menu bar items if needed, ensuring that the control /// items are in the correct order. func cacheItemsIfNeeded() async { + guard ScreenCapture.cachedCheckPermissions() else { + logSkippingCache(reason: "Ice not having screen recording permission") + return + } + do { try await waitForItemsToStopMoving(timeout: .seconds(1)) } catch is TaskTimeoutError { diff --git a/Ice/MenuBar/Search/MenuBarSearchPanel.swift b/Ice/MenuBar/Search/MenuBarSearchPanel.swift index 1ec5f78b..089d24a9 100644 --- a/Ice/MenuBar/Search/MenuBarSearchPanel.swift +++ b/Ice/MenuBar/Search/MenuBarSearchPanel.swift @@ -101,7 +101,9 @@ final class MenuBarSearchPanel: NSPanel { // Important that we set the navigation state before updating the cache. appState.navigationState.isSearchPresented = true - await appState.imageCache.updateCache() + if(ScreenCapture.cachedCheckPermissions()) { + await appState.imageCache.updateCache() + } let hostingView = MenuBarSearchHostingView(appState: appState, panel: self) hostingView.setFrameSize(hostingView.intrinsicContentSize) diff --git a/Ice/Permissions/Permission.swift b/Ice/Permissions/Permission.swift index 8b478cfd..1bb5f27f 100644 --- a/Ice/Permissions/Permission.swift +++ b/Ice/Permissions/Permission.swift @@ -21,6 +21,8 @@ class Permission: ObservableObject { let title: String /// Descriptive details for the permission. let details: [String] + /// Can Ice work without this? + let required: Bool /// The URL of the settings pane to open. private let settingsURL: URL? @@ -39,18 +41,21 @@ class Permission: ObservableObject { /// - Parameters: /// - title: The title of the permission. /// - details: Descriptive details for the permission. + /// - required: Defines wether this permission is required for Ice to work. /// - settingsURL: The URL of the settings pane to open. /// - check: A function that checks permissions. /// - request: A function that requests permissions. init( title: String, details: [String], + required: Bool, settingsURL: URL?, check: @escaping () -> Bool, request: @escaping () -> Void ) { self.title = title self.details = details + self.required = required self.settingsURL = settingsURL self.check = check self.request = request @@ -117,6 +122,7 @@ final class AccessibilityPermission: Permission { "Get real-time information about the menu bar.", "Arrange menu bar items.", ], + required: true, settingsURL: nil, check: { checkIsProcessTrusted() @@ -138,6 +144,7 @@ final class ScreenRecordingPermission: Permission { "Edit the menu bar's appearance.", "Display images of individual menu bar items.", ], + required: false, settingsURL: URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"), check: { ScreenCapture.checkPermissions() diff --git a/Ice/Permissions/PermissionsManager.swift b/Ice/Permissions/PermissionsManager.swift index 7910051f..efa91181 100644 --- a/Ice/Permissions/PermissionsManager.swift +++ b/Ice/Permissions/PermissionsManager.swift @@ -30,7 +30,7 @@ final class PermissionsManager: ObservableObject { accessibilityPermission.$hasPermission .combineLatest(screenRecordingPermission.$hasPermission) .sink { [weak self] hasPermission1, hasPermission2 in - self?.hasAllPermissions = hasPermission1 && hasPermission2 + self?.hasAllPermissions = (hasPermission1 || self?.accessibilityPermission.required == false) && (hasPermission2 || self?.screenRecordingPermission.required == false) } .store(in: &c) diff --git a/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift b/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift index 01f85e7e..3ddeb3be 100644 --- a/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift +++ b/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift @@ -36,6 +36,21 @@ struct MenuBarLayoutSettingsPane: View { } } } + + if(!ScreenCapture.cachedCheckPermissions()) { + IceGroupBox { + AnnotationView( + alignment: .center, + font: .callout.bold() + ) { + Label { + Text("This pane requires the screen recording permission to work.") + } icon: { + Image(systemName: "exclamationmark.triangle") + } + } + } + } } @ViewBuilder diff --git a/Ice/UI/IceBar/IceBar.swift b/Ice/UI/IceBar/IceBar.swift index 36ec8f3a..cedd5baa 100644 --- a/Ice/UI/IceBar/IceBar.swift +++ b/Ice/UI/IceBar/IceBar.swift @@ -161,7 +161,10 @@ final class IceBarPanel: NSPanel { currentSection = section await appState.itemManager.cacheItemsIfNeeded() - await appState.imageCache.updateCache() + + if(ScreenCapture.cachedCheckPermissions()) { + await appState.imageCache.updateCache() + } contentView = IceBarHostingView(appState: appState, colorManager: colorManager, section: section) { [weak self] in self?.close() diff --git a/Ice/Utilities/ScreenCapture.swift b/Ice/Utilities/ScreenCapture.swift index ea9372e7..404bde27 100644 --- a/Ice/Utilities/ScreenCapture.swift +++ b/Ice/Utilities/ScreenCapture.swift @@ -8,6 +8,22 @@ import ScreenCaptureKit /// A namespace for screen capture operations. enum ScreenCapture { + private static var lastCheckResult: Bool? + static func cachedCheckPermissions(reset: Bool = false) -> Bool { + // Now that we can work without this permission, this call gets called way more often. + // According to the energy meter, this has some minor impact on energy consumption + // Let's cache the result until we are asked not to (e.g. the settings window is visible) + if !reset && lastCheckResult != nil { + return lastCheckResult! + } + + let realResult = checkPermissions() + + lastCheckResult = realResult + + return realResult + } + /// Returns a Boolean value that indicates whether the app has been granted screen capture permissions. static func checkPermissions() -> Bool { for item in MenuBarItem.getMenuBarItems(onScreenOnly: false, activeSpaceOnly: true) { From 5b11d6c3c36aae2c72ae646a8cc0358da563d3b0 Mon Sep 17 00:00:00 2001 From: Michele Primavera Date: Mon, 21 Oct 2024 20:48:00 +0200 Subject: [PATCH 2/5] Fix missing items from hidden sections --- Ice/MenuBar/ItemManagement/MenuBarItemManager.swift | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/Ice/MenuBar/ItemManagement/MenuBarItemManager.swift b/Ice/MenuBar/ItemManagement/MenuBarItemManager.swift index 07888fef..04a2ed1f 100644 --- a/Ice/MenuBar/ItemManagement/MenuBarItemManager.swift +++ b/Ice/MenuBar/ItemManagement/MenuBarItemManager.swift @@ -181,9 +181,7 @@ final class MenuBarItemManager: ObservableObject { return } Task { - if(ScreenCapture.cachedCheckPermissions()) { - await self.cacheItemsIfNeeded() - } + await self.cacheItemsIfNeeded() } } .store(in: &c) @@ -195,9 +193,7 @@ final class MenuBarItemManager: ObservableObject { return } Task { - if(ScreenCapture.cachedCheckPermissions()) { - await self.cacheItemsIfNeeded() - } + await self.cacheItemsIfNeeded() } } .store(in: &c) @@ -317,11 +313,6 @@ extension MenuBarItemManager { /// Caches the current menu bar items if needed, ensuring that the control /// items are in the correct order. func cacheItemsIfNeeded() async { - guard ScreenCapture.cachedCheckPermissions() else { - logSkippingCache(reason: "Ice not having screen recording permission") - return - } - do { try await waitForItemsToStopMoving(timeout: .seconds(1)) } catch is TaskTimeoutError { From 8ebbcf1bf6a4e071e4b1ecdd1123aa6cd3b66a5b Mon Sep 17 00:00:00 2001 From: Michele Primavera Date: Mon, 21 Oct 2024 22:14:36 +0200 Subject: [PATCH 3/5] Make swiftlint happy --- .../ItemManagement/MenuBarItemImageCache.swift | 2 +- Ice/MenuBar/Search/MenuBarSearchPanel.swift | 2 +- .../SettingsPanes/MenuBarLayoutSettingsPane.swift | 4 ++-- Ice/UI/IceBar/IceBar.swift | 4 ++-- Ice/Utilities/ScreenCapture.swift | 12 +++++++----- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Ice/MenuBar/ItemManagement/MenuBarItemImageCache.swift b/Ice/MenuBar/ItemManagement/MenuBarItemImageCache.swift index 97910ab0..64a7caca 100644 --- a/Ice/MenuBar/ItemManagement/MenuBarItemImageCache.swift +++ b/Ice/MenuBar/ItemManagement/MenuBarItemImageCache.swift @@ -63,7 +63,7 @@ final class MenuBarItemImageCache: ObservableObject { return } Task.detached { - if (ScreenCapture.cachedCheckPermissions()) { + if ScreenCapture.cachedCheckPermissions() { await self.updateCache() } } diff --git a/Ice/MenuBar/Search/MenuBarSearchPanel.swift b/Ice/MenuBar/Search/MenuBarSearchPanel.swift index 089d24a9..c815ae75 100644 --- a/Ice/MenuBar/Search/MenuBarSearchPanel.swift +++ b/Ice/MenuBar/Search/MenuBarSearchPanel.swift @@ -101,7 +101,7 @@ final class MenuBarSearchPanel: NSPanel { // Important that we set the navigation state before updating the cache. appState.navigationState.isSearchPresented = true - if(ScreenCapture.cachedCheckPermissions()) { + if ScreenCapture.cachedCheckPermissions() { await appState.imageCache.updateCache() } diff --git a/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift b/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift index 3ddeb3be..3e12bbcf 100644 --- a/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift +++ b/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift @@ -36,8 +36,8 @@ struct MenuBarLayoutSettingsPane: View { } } } - - if(!ScreenCapture.cachedCheckPermissions()) { + + if !ScreenCapture.cachedCheckPermissions() { IceGroupBox { AnnotationView( alignment: .center, diff --git a/Ice/UI/IceBar/IceBar.swift b/Ice/UI/IceBar/IceBar.swift index cedd5baa..1e12b804 100644 --- a/Ice/UI/IceBar/IceBar.swift +++ b/Ice/UI/IceBar/IceBar.swift @@ -161,8 +161,8 @@ final class IceBarPanel: NSPanel { currentSection = section await appState.itemManager.cacheItemsIfNeeded() - - if(ScreenCapture.cachedCheckPermissions()) { + + if ScreenCapture.cachedCheckPermissions() { await appState.imageCache.updateCache() } diff --git a/Ice/Utilities/ScreenCapture.swift b/Ice/Utilities/ScreenCapture.swift index 404bde27..a2f9c571 100644 --- a/Ice/Utilities/ScreenCapture.swift +++ b/Ice/Utilities/ScreenCapture.swift @@ -13,17 +13,19 @@ enum ScreenCapture { // Now that we can work without this permission, this call gets called way more often. // According to the energy meter, this has some minor impact on energy consumption // Let's cache the result until we are asked not to (e.g. the settings window is visible) - if !reset && lastCheckResult != nil { - return lastCheckResult! + if !reset { + if let lastCheckResultUnrawpped = lastCheckResult { + return lastCheckResultUnrawpped + } } let realResult = checkPermissions() - + lastCheckResult = realResult - + return realResult } - + /// Returns a Boolean value that indicates whether the app has been granted screen capture permissions. static func checkPermissions() -> Bool { for item in MenuBarItem.getMenuBarItems(onScreenOnly: false, activeSpaceOnly: true) { From 17496a158c790146beb6cca318df67919edd1c5a Mon Sep 17 00:00:00 2001 From: Jordan Baird Date: Sun, 27 Oct 2024 14:58:04 -0600 Subject: [PATCH 4/5] More complete screen-recording permissions implementation --- Ice/Main/AppDelegate.swift | 5 +- Ice/Permissions/Permission.swift | 17 +++--- Ice/Permissions/PermissionsManager.swift | 55 +++++++++++++++---- Ice/Permissions/PermissionsView.swift | 42 ++++++++++++-- .../SettingsPanes/AdvancedSettingsPane.swift | 26 +++++++++ .../MenuBarLayoutSettingsPane.swift | 34 ++++++------ Ice/UI/IceBar/IceBar.swift | 38 ++++++++----- 7 files changed, 163 insertions(+), 54 deletions(-) diff --git a/Ice/Main/AppDelegate.swift b/Ice/Main/AppDelegate.swift index 4e93dec0..ee237727 100644 --- a/Ice/Main/AppDelegate.swift +++ b/Ice/Main/AppDelegate.swift @@ -49,9 +49,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } // If we have the required permissions, set up the shared app state. // Otherwise, open the permissions window. - if appState.permissionsManager.hasAllPermissions { + switch appState.permissionsManager.permissionsState { + case .hasAllPermissions, .hasRequiredPermissions: appState.performSetup() - } else { + case .missingPermissions: appState.activate(withPolicy: .regular) appState.openPermissionsWindow() } diff --git a/Ice/Permissions/Permission.swift b/Ice/Permissions/Permission.swift index 1bb5f27f..dd82abd1 100644 --- a/Ice/Permissions/Permission.swift +++ b/Ice/Permissions/Permission.swift @@ -13,7 +13,7 @@ import ScreenCaptureKit /// An object that encapsulates the behavior of checking for and requesting /// a specific permission for the app. @MainActor -class Permission: ObservableObject { +class Permission: ObservableObject, Identifiable { /// A Boolean value that indicates whether the app has this permission. @Published private(set) var hasPermission = false @@ -21,8 +21,8 @@ class Permission: ObservableObject { let title: String /// Descriptive details for the permission. let details: [String] - /// Can Ice work without this? - let required: Bool + /// A Boolean value that indicates if the app can work without this permission. + let isRequired: Bool /// The URL of the settings pane to open. private let settingsURL: URL? @@ -41,21 +41,21 @@ class Permission: ObservableObject { /// - Parameters: /// - title: The title of the permission. /// - details: Descriptive details for the permission. - /// - required: Defines wether this permission is required for Ice to work. + /// - isRequired: A Boolean value that indicates if the app can work without this permission. /// - settingsURL: The URL of the settings pane to open. /// - check: A function that checks permissions. /// - request: A function that requests permissions. init( title: String, details: [String], - required: Bool, + isRequired: Bool, settingsURL: URL?, check: @escaping () -> Bool, request: @escaping () -> Void ) { self.title = title self.details = details - self.required = required + self.isRequired = isRequired self.settingsURL = settingsURL self.check = check self.request = request @@ -86,6 +86,7 @@ class Permission: ObservableObject { /// Asynchronously waits for the app to be granted this permission. func waitForPermission() async { + configureCancellables() guard !hasPermission else { return } @@ -122,7 +123,7 @@ final class AccessibilityPermission: Permission { "Get real-time information about the menu bar.", "Arrange menu bar items.", ], - required: true, + isRequired: true, settingsURL: nil, check: { checkIsProcessTrusted() @@ -144,7 +145,7 @@ final class ScreenRecordingPermission: Permission { "Edit the menu bar's appearance.", "Display images of individual menu bar items.", ], - required: false, + isRequired: false, settingsURL: URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"), check: { ScreenCapture.checkPermissions() diff --git a/Ice/Permissions/PermissionsManager.swift b/Ice/Permissions/PermissionsManager.swift index efa91181..c991d047 100644 --- a/Ice/Permissions/PermissionsManager.swift +++ b/Ice/Permissions/PermissionsManager.swift @@ -4,42 +4,75 @@ // import Combine +import Foundation /// A type that manages the permissions of the app. @MainActor final class PermissionsManager: ObservableObject { - /// A Boolean value that indicates whether the app has been granted all permissions. - @Published var hasAllPermissions: Bool = false + /// The state of the granted permissions for the app. + enum PermissionsState { + case missingPermissions + case hasAllPermissions + case hasRequiredPermissions + } + + /// The state of the granted permissions for the app. + @Published var permissionsState = PermissionsState.missingPermissions + + let accessibilityPermission: AccessibilityPermission - let accessibilityPermission = AccessibilityPermission() + let screenRecordingPermission: ScreenRecordingPermission - let screenRecordingPermission = ScreenRecordingPermission() + let allPermissions: [Permission] private(set) weak var appState: AppState? private var cancellables = Set() + var requiredPermissions: [Permission] { + allPermissions.filter { $0.isRequired } + } + init(appState: AppState) { self.appState = appState + self.accessibilityPermission = AccessibilityPermission() + self.screenRecordingPermission = ScreenRecordingPermission() + self.allPermissions = [ + accessibilityPermission, + screenRecordingPermission, + ] configureCancellables() } private func configureCancellables() { var c = Set() - accessibilityPermission.$hasPermission - .combineLatest(screenRecordingPermission.$hasPermission) - .sink { [weak self] hasPermission1, hasPermission2 in - self?.hasAllPermissions = (hasPermission1 || self?.accessibilityPermission.required == false) && (hasPermission2 || self?.screenRecordingPermission.required == false) + Publishers.Merge( + accessibilityPermission.$hasPermission.mapToVoid(), + screenRecordingPermission.$hasPermission.mapToVoid() + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] in + guard let self else { + return + } + if allPermissions.allSatisfy({ $0.hasPermission }) { + permissionsState = .hasAllPermissions + } else if requiredPermissions.allSatisfy({ $0.hasPermission }) { + permissionsState = .hasRequiredPermissions + } else { + permissionsState = .missingPermissions } - .store(in: &c) + } + .store(in: &c) cancellables = c } /// Stops running all permissions checks. func stopAllChecks() { - accessibilityPermission.stopCheck() - screenRecordingPermission.stopCheck() + for permission in allPermissions { + permission.stopCheck() + } } } diff --git a/Ice/Permissions/PermissionsView.swift b/Ice/Permissions/PermissionsView.swift index 6b8c2ae8..8b743009 100644 --- a/Ice/Permissions/PermissionsView.swift +++ b/Ice/Permissions/PermissionsView.swift @@ -9,6 +9,22 @@ struct PermissionsView: View { @EnvironmentObject var permissionsManager: PermissionsManager @Environment(\.openWindow) private var openWindow + private var continueButtonText: LocalizedStringKey { + if case .hasRequiredPermissions = permissionsManager.permissionsState { + "Continue in Limited Mode" + } else { + "Continue" + } + } + + private var continueButtonForegroundStyle: some ShapeStyle { + if case .hasRequiredPermissions = permissionsManager.permissionsState { + AnyShapeStyle(.yellow) + } else { + AnyShapeStyle(.primary) + } + } + var body: some View { VStack(spacing: 0) { headerView @@ -72,8 +88,9 @@ struct PermissionsView: View { @ViewBuilder private var permissionsGroupStack: some View { VStack(spacing: 7.5) { - permissionBox(permissionsManager.accessibilityPermission) - permissionBox(permissionsManager.screenRecordingPermission) + ForEach(permissionsManager.allPermissions) { permission in + permissionBox(permission) + } } } @@ -106,10 +123,11 @@ struct PermissionsView: View { appState.permissionsWindow?.close() appState.appDelegate?.openSettingsWindow() } label: { - Text("Continue") + Text(continueButtonText) .frame(maxWidth: .infinity) + .foregroundStyle(continueButtonForegroundStyle) } - .disabled(!permissionsManager.hasAllPermissions) + .disabled(permissionsManager.permissionsState == .missingPermissions) } @ViewBuilder @@ -154,6 +172,22 @@ struct PermissionsView: View { } } .allowsHitTesting(!permission.hasPermission) + + if !permission.isRequired { + IceGroupBox { + AnnotationView( + alignment: .center, + font: .callout.bold() + ) { + Label { + Text("Ice can work in a limited mode without this permission.") + } icon: { + Image(systemName: "checkmark.shield") + .foregroundStyle(.green) + } + } + } + } } .padding(10) .frame(maxWidth: .infinity) diff --git a/Ice/Settings/SettingsPanes/AdvancedSettingsPane.swift b/Ice/Settings/SettingsPanes/AdvancedSettingsPane.swift index 4a7cda65..eeb0f436 100644 --- a/Ice/Settings/SettingsPanes/AdvancedSettingsPane.swift +++ b/Ice/Settings/SettingsPanes/AdvancedSettingsPane.swift @@ -41,6 +41,9 @@ struct AdvancedSettingsPane: View { showOnHoverDelaySlider tempShowIntervalSlider } + IceSection("Permissions") { + allPermissions + } } } @@ -134,6 +137,29 @@ struct AdvancedSettingsPane: View { private var showAllSectionsOnUserDrag: some View { Toggle("Show all sections when Command + dragging menu bar items", isOn: manager.bindings.showAllSectionsOnUserDrag) } + + @ViewBuilder + private var allPermissions: some View { + ForEach(appState.permissionsManager.allPermissions) { permission in + IceLabeledContent { + if permission.hasPermission { + Label { + Text("Permission Granted") + } icon: { + Image(systemName: "checkmark.circle") + .foregroundStyle(.green) + } + } else { + Button("Grant Permission") { + permission.performRequest() + } + } + } label: { + Text(permission.title) + } + .frame(height: 22) + } + } } #Preview { diff --git a/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift b/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift index 3e12bbcf..b1038248 100644 --- a/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift +++ b/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift @@ -9,7 +9,9 @@ struct MenuBarLayoutSettingsPane: View { @EnvironmentObject var appState: AppState var body: some View { - if appState.menuBarManager.isMenuBarHiddenBySystemUserDefaults { + if !ScreenCapture.cachedCheckPermissions() { + missingScreenRecordingPermission + } else if appState.menuBarManager.isMenuBarHiddenBySystemUserDefaults { cannotArrange } else { IceForm(alignment: .leading, spacing: 20) { @@ -36,21 +38,6 @@ struct MenuBarLayoutSettingsPane: View { } } } - - if !ScreenCapture.cachedCheckPermissions() { - IceGroupBox { - AnnotationView( - alignment: .center, - font: .callout.bold() - ) { - Label { - Text("This pane requires the screen recording permission to work.") - } icon: { - Image(systemName: "exclamationmark.triangle") - } - } - } - } } @ViewBuilder @@ -69,6 +56,21 @@ struct MenuBarLayoutSettingsPane: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) } + @ViewBuilder + private var missingScreenRecordingPermission: some View { + VStack { + Text("Menu bar layout requires screen recording permissions") + .font(.title2) + + Button { + appState.navigationState.settingsNavigationIdentifier = .advanced + } label: { + Text("Go to Advanced Settings") + } + .buttonStyle(.link) + } + } + @ViewBuilder private func layoutBar(for section: MenuBarSection.Name) -> some View { if diff --git a/Ice/UI/IceBar/IceBar.swift b/Ice/UI/IceBar/IceBar.swift index 1e12b804..6287e05f 100644 --- a/Ice/UI/IceBar/IceBar.swift +++ b/Ice/UI/IceBar/IceBar.swift @@ -166,7 +166,7 @@ final class IceBarPanel: NSPanel { await appState.imageCache.updateCache() } - contentView = IceBarHostingView(appState: appState, colorManager: colorManager, section: section) { [weak self] in + contentView = IceBarHostingView(appState: appState, colorManager: colorManager, screen: screen, section: section) { [weak self] in self?.close() } @@ -199,11 +199,12 @@ private final class IceBarHostingView: NSHostingView { init( appState: AppState, colorManager: IceBarColorManager, + screen: NSScreen, section: MenuBarSection.Name, closePanel: @escaping () -> Void ) { super.init( - rootView: IceBarContentView(section: section, closePanel: closePanel) + rootView: IceBarContentView(screen: screen, section: section, closePanel: closePanel) .environmentObject(appState) .environmentObject(appState.imageCache) .environmentObject(appState.itemManager) @@ -239,6 +240,7 @@ private struct IceBarContentView: View { @State private var frame = CGRect.zero @State private var scrollIndicatorsFlashTrigger = 0 + let screen: NSScreen let section: MenuBarSection.Name let closePanel: () -> Void @@ -255,19 +257,14 @@ private struct IceBarContentView: View { } private var verticalPadding: CGFloat { - if let screen = imageCache.screen { - guard !screen.hasNotch else { - return 0 - } - } - return 2 + screen.hasNotch ? 0 : 2 } var contentHeight: CGFloat? { - guard let menuBarHeight = imageCache.menuBarHeight else { + guard let menuBarHeight = imageCache.menuBarHeight ?? screen.getMenuBarHeight() else { return nil } - if configuration.shapeKind != .none && configuration.isInset && imageCache.screen?.hasNotch == true { + if configuration.shapeKind != .none && configuration.isInset && screen.hasNotch { return menuBarHeight - appState.appearanceManager.menuBarInsetAmount * 2 } return menuBarHeight @@ -307,12 +304,27 @@ private struct IceBarContentView: View { @ViewBuilder private var content: some View { - if menuBarManager.isMenuBarHiddenBySystemUserDefaults { + if !ScreenCapture.cachedCheckPermissions() { + HStack { + Text("The Ice Bar requires screen recording permissions.") + + Button { + closePanel() + appState.navigationState.settingsNavigationIdentifier = .advanced + appState.appDelegate?.openSettingsWindow() + } label: { + Text("Open Ice Settings") + } + .buttonStyle(.plain) + .foregroundStyle(.link) + } + .padding(.horizontal, 10) + } else if menuBarManager.isMenuBarHiddenBySystemUserDefaults { Text("Ice cannot display menu bar items for automatically hidden menu bars") - .padding(.horizontal, 5) + .padding(.horizontal, 10) } else if imageCache.cacheFailed(for: section) { Text("Unable to display menu bar items") - .padding(.horizontal, 5) + .padding(.horizontal, 10) } else { ScrollView(.horizontal) { HStack(spacing: 0) { From f0b7fd44c5d4b1f9f3b72d6f01ec33345be98cff Mon Sep 17 00:00:00 2001 From: Jordan Baird Date: Sun, 27 Oct 2024 15:00:36 -0600 Subject: [PATCH 5/5] Use function-local cache --- Ice/Utilities/ScreenCapture.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Ice/Utilities/ScreenCapture.swift b/Ice/Utilities/ScreenCapture.swift index a2f9c571..3952a976 100644 --- a/Ice/Utilities/ScreenCapture.swift +++ b/Ice/Utilities/ScreenCapture.swift @@ -8,20 +8,23 @@ import ScreenCaptureKit /// A namespace for screen capture operations. enum ScreenCapture { - private static var lastCheckResult: Bool? static func cachedCheckPermissions(reset: Bool = false) -> Bool { + enum Context { + static var lastCheckResult: Bool? + } + // Now that we can work without this permission, this call gets called way more often. // According to the energy meter, this has some minor impact on energy consumption // Let's cache the result until we are asked not to (e.g. the settings window is visible) if !reset { - if let lastCheckResultUnrawpped = lastCheckResult { - return lastCheckResultUnrawpped + if let lastCheckResult = Context.lastCheckResult { + return lastCheckResult } } let realResult = checkPermissions() - lastCheckResult = realResult + Context.lastCheckResult = realResult return realResult }