From c50933d4d783989dc06174faf013038bdcd22cfb Mon Sep 17 00:00:00 2001 From: ShlomoCode <78599753+ShlomoCode@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:54:06 +0300 Subject: [PATCH] run swiftformat swiftformat . --swiftversion 5.0 add lint github action add build phase in Xcode --- .github/workflows/lint.yml | 10 + .github/workflows/pr-lint.yml | 4 +- BuildTools/Empty.swift | 0 BuildTools/Package.resolved | 16 ++ BuildTools/Package.swift | 11 + DockDoor.xcodeproj/project.pbxproj | 25 ++ DockDoor/AppDelegate.swift | 88 +++---- DockDoor/Components/BlurView.swift | 9 +- DockDoor/Components/FluidGradient.swift | 4 +- DockDoor/Components/HiddenModifier.swift | 4 +- DockDoor/Components/Marquee.swift | 94 ++++---- DockDoor/Components/StackedShadow.swift | 10 +- DockDoor/Extensions/AXUIElement.swift | 82 +++---- DockDoor/Extensions/ColorHex.swift | 4 +- DockDoor/Extensions/dockStyle.swift | 5 +- DockDoor/Utilities/App Icon.swift | 38 +-- DockDoor/Utilities/DockObserver.swift | 172 ++++++------- DockDoor/Utilities/DockUtils.swift | 57 ++--- DockDoor/Utilities/KeybindHelper.swift | 63 +++-- DockDoor/Utilities/LimitedTaskGroup.swift | 18 +- DockDoor/Utilities/MessageUtil.swift | 3 +- DockDoor/Utilities/Misc Utils.swift | 38 ++- .../Utilities/SpaceWindowCacheManager.swift | 41 ++-- .../WindowManipulationObservers.swift | 57 ++--- DockDoor/Utilities/WindowUtil.swift | 228 +++++++++--------- DockDoor/Views/FirstTimeView.swift | 25 +- .../Hover Window/FullSizePreviewView.swift | 6 +- .../SharedPreviewWindowCoordinator.swift | 176 +++++++------- .../Hover Window/Traffic Light Buttons.swift | 12 +- .../Views/Hover Window/WindowPreview.swift | 26 +- .../WindowPreviewHoverContainer.swift | 62 ++--- .../Settings/AppearanceSettingsView.swift | 26 +- .../Views/Settings/MainSettingsView.swift | 66 ++--- .../Settings/PermissionsSettingsView.swift | 22 +- .../Views/Settings/UpdateSettingsView.swift | 39 ++- .../Settings/WindowSwitcherSettingsView.swift | 45 ++-- DockDoor/Views/Settings/settings.swift | 8 +- DockDoor/consts.swift | 70 +++--- 38 files changed, 862 insertions(+), 802 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 BuildTools/Empty.swift create mode 100644 BuildTools/Package.resolved create mode 100644 BuildTools/Package.swift diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..b77339d7 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,10 @@ +name: Lint +on: pull_request + +jobs: + Lint: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: SwiftFormat + run: swiftformat --lint --swiftversion 5.0 . --reporter github-actions-log \ No newline at end of file diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 387f1430..4dff034b 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -1,4 +1,4 @@ -name: "Lint PR" +name: "PR Title Convention" on: pull_request_target: @@ -13,7 +13,7 @@ permissions: jobs: main: - name: Validate PR title + name: Validate PR Title runs-on: ubuntu-latest steps: - uses: amannn/action-semantic-pull-request@v5 diff --git a/BuildTools/Empty.swift b/BuildTools/Empty.swift new file mode 100644 index 00000000..e69de29b diff --git a/BuildTools/Package.resolved b/BuildTools/Package.resolved new file mode 100644 index 00000000..8f334560 --- /dev/null +++ b/BuildTools/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "SwiftFormat", + "repositoryURL": "https://github.com/nicklockwood/SwiftFormat", + "state": { + "branch": null, + "revision": "d6309f7440889427426143b4a0b100b959d3f3e6", + "version": "0.54.3" + } + } + ] + }, + "version": 1 +} diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift new file mode 100644 index 00000000..4dcb93cd --- /dev/null +++ b/BuildTools/Package.swift @@ -0,0 +1,11 @@ +// swift-tools-version:5.1 +import PackageDescription + +let package = Package( + name: "BuildTools", + platforms: [.macOS(.v10_11)], + dependencies: [ + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.54.3"), + ], + targets: [.target(name: "BuildTools", path: "")] +) diff --git a/DockDoor.xcodeproj/project.pbxproj b/DockDoor.xcodeproj/project.pbxproj index d5c95aae..c332dbb8 100644 --- a/DockDoor.xcodeproj/project.pbxproj +++ b/DockDoor.xcodeproj/project.pbxproj @@ -245,6 +245,7 @@ isa = PBXNativeTarget; buildConfigurationList = BB157B742C0E31CF00997315 /* Build configuration list for PBXNativeTarget "DockDoor" */; buildPhases = ( + 054B715D2C69264800E662F3 /* Run Script */, BB157B612C0E31CE00997315 /* Sources */, BB157B622C0E31CE00997315 /* Frameworks */, BB157B632C0E31CE00997315 /* Resources */, @@ -322,6 +323,28 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 054B715D2C69264800E662F3 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd BuildTools\nSDKROOT=(xcrun --sdk macosx --show-sdk-path)\n#swift package update #Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\nswift run -c release swiftformat \"$SRCROOT\" --swiftversion 5.0\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ BB157B612C0E31CE00997315 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -503,6 +526,7 @@ DEVELOPMENT_TEAM = 2Q775S63Q3; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = DockDoor/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = DockDoor; @@ -535,6 +559,7 @@ DEVELOPMENT_TEAM = 2Q775S63Q3; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = DockDoor/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = DockDoor; diff --git a/DockDoor/AppDelegate.swift b/DockDoor/AppDelegate.swift index 603b8759..5a662d9b 100644 --- a/DockDoor/AppDelegate.swift +++ b/DockDoor/AppDelegate.swift @@ -6,17 +6,17 @@ // import Cocoa -import SwiftUI import Defaults import Settings import Sparkle +import SwiftUI class SettingsWindowControllerDelegate: NSObject, NSWindowDelegate { - func windowDidBecomeKey(_ notification: Notification) { + func windowDidBecomeKey(_: Notification) { NSApp.setActivationPolicy(.regular) // Show dock icon on open settings window } - - func windowWillClose(_ notification: Notification) { + + func windowWillClose(_: Notification) { NSApp.setActivationPolicy(.accessory) // Hide dock icon back } } @@ -26,9 +26,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var appClosureObserver: WindowManipulationObservers? private var keybindHelper: KeybindHelper? private var statusBarItem: NSStatusItem? - + private var updaterController: SPUStandardUpdaterController - + // settings private var firstTimeWindow: NSWindow? private lazy var settingsWindowController = SettingsWindowController( @@ -37,33 +37,32 @@ class AppDelegate: NSObject, NSApplicationDelegate { AppearanceSettingsViewController(), WindowSwitcherSettingsViewController(), PermissionsSettingsViewController(), - UpdatesSettingsViewController(updater: updaterController.updater) + UpdatesSettingsViewController(updater: updaterController.updater), ] ) private let settingsWindowControllerDelegate = SettingsWindowControllerDelegate() - + override init() { - self.updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) - self.updaterController.startUpdater() + updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + updaterController.startUpdater() super.init() - + if let zoomButton = settingsWindowController.window?.standardWindowButton(.zoomButton) { zoomButton.isEnabled = false } - + settingsWindowController.window?.delegate = settingsWindowControllerDelegate } - - - func applicationDidFinishLaunching(_ aNotification: Notification) { + + func applicationDidFinishLaunching(_: Notification) { NSApplication.shared.setActivationPolicy(.accessory) // Hide the menubar and dock icons - + if Defaults[.showMenuBarIcon] { - self.setupMenuBar() + setupMenuBar() } else { - self.removeMenuBar() + removeMenuBar() } - + if !Defaults[.launched] { handleFirstTimeLaunch() } else { @@ -74,12 +73,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } } - - func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { - self.openSettingsWindow(nil) + + func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool { + openSettingsWindow(nil) return false } - + func setupMenuBar() { guard statusBarItem == nil else { return } let icon = NSImage(systemSymbolName: "door.right.hand.open", accessibilityDescription: nil)! @@ -88,80 +87,81 @@ class AppDelegate: NSObject, NSApplicationDelegate { button.image = icon button.action = #selector(statusBarButtonClicked(_:)) button.target = self - + // Create Menu Items let openSettingsMenuItem = NSMenuItem(title: String(localized: "Open Settings"), action: #selector(openSettingsWindow(_:)), keyEquivalent: "") openSettingsMenuItem.target = self let quitMenuItem = NSMenuItem(title: String(localized: "Quit DockDoor"), action: #selector(quitAppWrapper), keyEquivalent: "q") quitMenuItem.target = self - + // Create the Menu let menu = NSMenu() menu.addItem(openSettingsMenuItem) menu.addItem(NSMenuItem.separator()) menu.addItem(quitMenuItem) - + button.menu = menu } } - + func removeMenuBar() { - guard let statusBarItem = statusBarItem else { return } + guard let statusBarItem else { return } NSStatusBar.system.removeStatusItem(statusBarItem) self.statusBarItem = nil } - - @objc func statusBarButtonClicked(_ sender: Any?) { + + @objc func statusBarButtonClicked(_: Any?) { // Show the menu if let button = statusBarItem?.button { button.menu?.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.maxY), in: button) } } - + @objc private func quitAppWrapper() { quitApp() } - - @objc func openSettingsWindow(_ sender: Any?) { + + @objc func openSettingsWindow(_: Any?) { settingsWindowController.show() } - + func quitApp() { NSApplication.shared.terminate(nil) } - + func restartApp() { // we use -n to open a new instance, to avoid calling applicationShouldHandleReopen // we use Bundle.main.bundlePath in case of multiple DockDoor versions on the machine Process.launchedProcess(launchPath: "/usr/bin/open", arguments: ["-n", Bundle.main.bundlePath]) - self.quitApp() + quitApp() } - + private func handleFirstTimeLaunch() { let contentView = FirstTimeView() - + // Save that the app has launched Defaults[.launched] = true - + // Create a hosting controller let hostingController = NSHostingController(rootView: contentView) - + // Create the settings window firstTimeWindow = NSWindow( contentRect: NSRect(origin: .zero, size: NSSize(width: 400, height: 400)), styleMask: [.titled, .closable, .resizable], - backing: .buffered, defer: false) + backing: .buffered, defer: false + ) firstTimeWindow?.center() firstTimeWindow?.setFrameAutosaveName("DockDoor Permissions") firstTimeWindow?.contentView = hostingController.view firstTimeWindow?.title = "DockDoor Permissions" - + // Make the window key and order it front firstTimeWindow?.makeKeyAndOrderFront(nil) - + // Calculate the preferred size of the SwiftUI view let preferredSize = hostingController.view.fittingSize - + // Resize the window to fit the content view firstTimeWindow?.setContentSize(preferredSize) } diff --git a/DockDoor/Components/BlurView.swift b/DockDoor/Components/BlurView.swift index b07d5e03..88bd7945 100644 --- a/DockDoor/Components/BlurView.swift +++ b/DockDoor/Components/BlurView.swift @@ -8,21 +8,20 @@ import SwiftUI struct BlurView: NSViewRepresentable { - func makeNSView(context: Context) -> NSVisualEffectView { + func makeNSView(context _: Context) -> NSVisualEffectView { let effectView = NSVisualEffectView() effectView.state = .active effectView.material = .popover return effectView } - func updateNSView(_ nsView: NSVisualEffectView, context: Context) { - } + func updateNSView(_: NSVisualEffectView, context _: Context) {} } struct MaterialBlurView: NSViewRepresentable { var material: NSVisualEffectView.Material - func makeNSView(context: Context) -> NSVisualEffectView { + func makeNSView(context _: Context) -> NSVisualEffectView { let view = NSVisualEffectView() view.material = material view.blendingMode = .behindWindow @@ -30,5 +29,5 @@ struct MaterialBlurView: NSViewRepresentable { return view } - func updateNSView(_ nsView: NSVisualEffectView, context: Context) {} + func updateNSView(_: NSVisualEffectView, context _: Context) {} } diff --git a/DockDoor/Components/FluidGradient.swift b/DockDoor/Components/FluidGradient.swift index cadd6375..b0e21005 100644 --- a/DockDoor/Components/FluidGradient.swift +++ b/DockDoor/Components/FluidGradient.swift @@ -5,8 +5,8 @@ // Created by Ethan Bills on 7/11/24. // -import SwiftUI import FluidGradient +import SwiftUI func fluidGradient() -> some View { FluidGradient( @@ -20,7 +20,7 @@ func fluidGradient() -> some View { struct FluidGradientBorder: ViewModifier { let cornerRadius: CGFloat let lineWidth: CGFloat - + func body(content: Content) -> some View { content .overlay( diff --git a/DockDoor/Components/HiddenModifier.swift b/DockDoor/Components/HiddenModifier.swift index c088c44a..133fed5e 100644 --- a/DockDoor/Components/HiddenModifier.swift +++ b/DockDoor/Components/HiddenModifier.swift @@ -1,5 +1,5 @@ // -// ReadModifier.swift +// HiddenModifier.swift // OpenArtemis // // Created by Ethan Bills on 1/1/24. @@ -17,6 +17,6 @@ struct HiddenModifier: ViewModifier { extension View { func markHidden(isHidden: Bool) -> some View { - self.modifier(HiddenModifier(isHidden: isHidden)) + modifier(HiddenModifier(isHidden: isHidden)) } } diff --git a/DockDoor/Components/Marquee.swift b/DockDoor/Components/Marquee.swift index 661e6a20..93840a4b 100644 --- a/DockDoor/Components/Marquee.swift +++ b/DockDoor/Components/Marquee.swift @@ -1,12 +1,12 @@ // -// TheMarquee.swift +// Marquee.swift // NotchNook // // Created by Igor Marcossi on 16/06/24. // -import SwiftUI import SmoothGradient +import SwiftUI struct TheMarquee: View { var width: Double @@ -30,7 +30,7 @@ struct TheMarquee: View { if !measured || !shouldMove || animating { return } animating = true doAfter(secsBeforeLooping) { - self.animLoop() + animLoop() } } @@ -43,8 +43,8 @@ struct TheMarquee: View { } completion: { offset = 0 doAfter(secsBeforeLooping) { - if self.animating { - self.animLoop() + if animating { + animLoop() } } } @@ -56,7 +56,7 @@ struct TheMarquee: View { content() .measure($contentSize) - if measured && shouldMove { + if measured, shouldMove { content() } } @@ -79,60 +79,58 @@ struct TheMarquee: View { extension View { func fadeOnEdges(axis: Axis, fadeLength: Double, disable: Bool = false) -> some View { - self - .mask { - if (!disable) { - GeometryReader { geo in - DynStack(direction: axis, spacing: 0) { - SmoothLinearGradient( - from: .black.opacity(0), - to: .black.opacity(1), - startPoint: axis == .horizontal ? .leading : .top, - endPoint: axis == .horizontal ? .trailing : .bottom, - curve: .easeInOut - ) - .frame(width: axis == .horizontal ? fadeLength : nil, height: axis == .vertical ? fadeLength : nil) - Color.black.frame(maxWidth: .infinity) - SmoothLinearGradient( - from: .black.opacity(0), - to: .black.opacity(1), - startPoint: axis == .horizontal ? .trailing : .bottom, - endPoint: axis == .horizontal ? .leading : .top, - curve: .easeInOut - ) - .frame(width: axis == .horizontal ? fadeLength : nil, height: axis == .vertical ? fadeLength : nil) - } + mask { + if !disable { + GeometryReader { _ in + DynStack(direction: axis, spacing: 0) { + SmoothLinearGradient( + from: .black.opacity(0), + to: .black.opacity(1), + startPoint: axis == .horizontal ? .leading : .top, + endPoint: axis == .horizontal ? .trailing : .bottom, + curve: .easeInOut + ) + .frame(width: axis == .horizontal ? fadeLength : nil, height: axis == .vertical ? fadeLength : nil) + Color.black.frame(maxWidth: .infinity) + SmoothLinearGradient( + from: .black.opacity(0), + to: .black.opacity(1), + startPoint: axis == .horizontal ? .trailing : .bottom, + endPoint: axis == .horizontal ? .leading : .top, + curve: .easeInOut + ) + .frame(width: axis == .horizontal ? fadeLength : nil, height: axis == .vertical ? fadeLength : nil) } - } else { - Color.black } + } else { + Color.black } + } } } extension View { func measure(_ sizeBinding: Binding) -> some View { - self - .background { - Color.clear - .background( - GeometryReader { geometry in - Color.clear - .preference(key: ViewSizeKey.self, value: geometry.size) - } - ) - .onPreferenceChange(ViewSizeKey.self) { size in - sizeBinding.wrappedValue = size + background { + Color.clear + .background( + GeometryReader { geometry in + Color.clear + .preference(key: ViewSizeKey.self, value: geometry.size) } - } + ) + .onPreferenceChange(ViewSizeKey.self) { size in + sizeBinding.wrappedValue = size + } + } } } struct ViewSizeKey: PreferenceKey { - static var defaultValue: CGSize = .zero - static func reduce(value: inout CGSize, nextValue: () -> CGSize) { - value = value + nextValue() - } + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = value + nextValue() + } } func doAfter(_ seconds: Double, action: @escaping () -> Void) { @@ -141,6 +139,6 @@ func doAfter(_ seconds: Double, action: @escaping () -> Void) { extension CGSize { static func + (lhs: CGSize, rhs: CGSize) -> CGSize { - return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) + CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) } } diff --git a/DockDoor/Components/StackedShadow.swift b/DockDoor/Components/StackedShadow.swift index 7a82a1d4..cdf7020e 100644 --- a/DockDoor/Components/StackedShadow.swift +++ b/DockDoor/Components/StackedShadow.swift @@ -13,7 +13,7 @@ struct StackedShadow: ViewModifier { var x: CGFloat var y: CGFloat var color: Color - + init(stacked count: Int, radius: CGFloat = 10, x: CGFloat = 0, y: CGFloat = 0, color: Color = .black) { self.count = count self.radius = radius @@ -21,20 +21,20 @@ struct StackedShadow: ViewModifier { self.y = y self.color = color } - + func body(content: Content) -> some View { content .shadow(color: color.opacity(Double(1) / Double(count)), radius: radius, x: x, y: y) .modifier(RecursiveShadow(count: count - 1, radius: radius, x: x, y: y, color: color)) } - + private struct RecursiveShadow: ViewModifier { var count: Int var radius: CGFloat var x: CGFloat var y: CGFloat var color: Color - + func body(content: Content) -> some View { if count > 0 { content @@ -49,6 +49,6 @@ struct StackedShadow: ViewModifier { extension View { func shadow(stacked count: Int, radius: CGFloat = 10, x: CGFloat = 0, y: CGFloat = 0, color: Color = .black) -> some View { - self.modifier(StackedShadow(stacked: count, radius: radius, x: x, y: y, color: color)) + modifier(StackedShadow(stacked: count, radius: radius, x: x, y: y, color: color)) } } diff --git a/DockDoor/Extensions/AXUIElement.swift b/DockDoor/Extensions/AXUIElement.swift index 42dfec9f..cefa52c7 100644 --- a/DockDoor/Extensions/AXUIElement.swift +++ b/DockDoor/Extensions/AXUIElement.swift @@ -1,37 +1,37 @@ -import Cocoa -import ApplicationServices.HIServices.AXUIElement -import ApplicationServices.HIServices.AXValue +import ApplicationServices.HIServices.AXActionConstants +import ApplicationServices.HIServices.AXAttributeConstants import ApplicationServices.HIServices.AXError import ApplicationServices.HIServices.AXRoleConstants -import ApplicationServices.HIServices.AXAttributeConstants -import ApplicationServices.HIServices.AXActionConstants +import ApplicationServices.HIServices.AXUIElement +import ApplicationServices.HIServices.AXValue +import Cocoa extension AXUIElement { func axCallWhichCanThrow(_ result: AXError, _ successValue: inout T) throws -> T? { switch result { case .success: return successValue - // .cannotComplete can happen if the app is unresponsive; we throw in that case to retry until the call succeeds + // .cannotComplete can happen if the app is unresponsive; we throw in that case to retry until the call succeeds case .cannotComplete: throw AxError.runtimeError - // for other errors it's pointless to retry + // for other errors it's pointless to retry default: return nil } } - + func cgWindowId() throws -> CGWindowID? { var id = CGWindowID(0) return try axCallWhichCanThrow(_AXUIElementGetWindow(self, &id), &id) } - + func pid() throws -> pid_t? { var pid = pid_t(0) return try axCallWhichCanThrow(AXUIElementGetPid(self, &pid), &pid) } - + func attribute(_ key: String, _ _: T.Type) throws -> T? { var value: AnyObject? return try axCallWhichCanThrow(AXUIElementCopyAttributeValue(self, key as CFString, &value), &value) as? T } - + private func value(_ key: String, _ target: T, _ type: AXValueType) throws -> T? { if let a = try attribute(key, AXValue.self) { var value = target @@ -40,73 +40,73 @@ extension AXUIElement { } return nil } - + func position() throws -> CGPoint? { - return try value(kAXPositionAttribute, CGPoint.zero, .cgPoint) + try value(kAXPositionAttribute, CGPoint.zero, .cgPoint) } - + func size() throws -> CGSize? { - return try value(kAXSizeAttribute, CGSize.zero, .cgSize) + try value(kAXSizeAttribute, CGSize.zero, .cgSize) } - + func title() throws -> String? { - return try attribute(kAXTitleAttribute, String.self) + try attribute(kAXTitleAttribute, String.self) } - + func parent() throws -> AXUIElement? { - return try attribute(kAXParentAttribute, AXUIElement.self) + try attribute(kAXParentAttribute, AXUIElement.self) } - + func children() throws -> [AXUIElement]? { - return try attribute(kAXChildrenAttribute, [AXUIElement].self) + try attribute(kAXChildrenAttribute, [AXUIElement].self) } - + func windows() throws -> [AXUIElement]? { - return try attribute(kAXWindowsAttribute, [AXUIElement].self) + try attribute(kAXWindowsAttribute, [AXUIElement].self) } - + func isMinimized() throws -> Bool { - return try attribute(kAXMinimizedAttribute, Bool.self) == true + try attribute(kAXMinimizedAttribute, Bool.self) == true } - + func isFullscreen() throws -> Bool { - return try attribute(kAXFullscreenAttribute, Bool.self) == true + try attribute(kAXFullscreenAttribute, Bool.self) == true } - + func focusedWindow() throws -> AXUIElement? { - return try attribute(kAXFocusedWindowAttribute, AXUIElement.self) + try attribute(kAXFocusedWindowAttribute, AXUIElement.self) } - + func role() throws -> String? { - return try attribute(kAXRoleAttribute, String.self) + try attribute(kAXRoleAttribute, String.self) } - + func subrole() throws -> String? { - return try attribute(kAXSubroleAttribute, String.self) + try attribute(kAXSubroleAttribute, String.self) } - + func appIsRunning() throws -> Bool? { - return try attribute(kAXIsApplicationRunningAttribute, Bool.self) + try attribute(kAXIsApplicationRunningAttribute, Bool.self) } - + func closeButton() throws -> AXUIElement? { - return try attribute(kAXCloseButtonAttribute, AXUIElement.self) + try attribute(kAXCloseButtonAttribute, AXUIElement.self) } - + func subscribeToNotification(_ axObserver: AXObserver, _ notification: String, _ callback: (() -> Void)? = nil) throws { let result = AXObserverAddNotification(axObserver, self, notification as CFString, nil) if result == .success || result == .notificationAlreadyRegistered { callback?() - } else if result != .notificationUnsupported && result != .notImplemented { + } else if result != .notificationUnsupported, result != .notImplemented { throw AxError.runtimeError } } - + func setAttribute(_ key: String, _ value: Any) throws { var unused: Void = () try axCallWhichCanThrow(AXUIElementSetAttributeValue(self, key as CFString, value as CFTypeRef), &unused) } - + func performAction(_ action: String) throws { var unused: Void = () try axCallWhichCanThrow(AXUIElementPerformAction(self, action as CFString), &unused) diff --git a/DockDoor/Extensions/ColorHex.swift b/DockDoor/Extensions/ColorHex.swift index 5b46ef14..1d1cb395 100644 --- a/DockDoor/Extensions/ColorHex.swift +++ b/DockDoor/Extensions/ColorHex.swift @@ -1,5 +1,5 @@ // -// Extensions.swift +// ColorHex.swift // DockDoor // // Created by ShlomoCode on 10/07/2024. @@ -28,7 +28,7 @@ extension Color { .sRGB, red: Double(r) / 255, green: Double(g) / 255, - blue: Double(b) / 255, + blue: Double(b) / 255, opacity: Double(a) / 255 ) } diff --git a/DockDoor/Extensions/dockStyle.swift b/DockDoor/Extensions/dockStyle.swift index 91407420..bed7e3ad 100644 --- a/DockDoor/Extensions/dockStyle.swift +++ b/DockDoor/Extensions/dockStyle.swift @@ -9,7 +9,7 @@ import SwiftUI struct DockStyleModifier: ViewModifier { let cornerRadius: Double - + func body(content: Content) -> some View { content .background { @@ -32,7 +32,6 @@ struct DockStyleModifier: ViewModifier { extension View { func dockStyle(cornerRadius: Double = 19) -> some View { - self - .modifier(DockStyleModifier(cornerRadius: cornerRadius)) + modifier(DockStyleModifier(cornerRadius: cornerRadius)) } } diff --git a/DockDoor/Utilities/App Icon.swift b/DockDoor/Utilities/App Icon.swift index 5f81c454..68a821cb 100644 --- a/DockDoor/Utilities/App Icon.swift +++ b/DockDoor/Utilities/App Icon.swift @@ -7,67 +7,67 @@ import AppKit -struct AppIconUtil { +enum AppIconUtil { // MARK: - Properties - + private static var iconCache: [String: (image: NSImage, timestamp: Date)] = [:] private static let cacheExpiryInterval: TimeInterval = 3600 // 1 hour - + // MARK: - App Icons - + static func getIcon(file path: URL) -> NSImage? { let cacheKey = path.path removeExpiredCacheEntries() - + if let cachedEntry = iconCache[cacheKey], Date().timeIntervalSince(cachedEntry.timestamp) < cacheExpiryInterval { return cachedEntry.image } - + guard FileManager.default.fileExists(atPath: path.path) else { return nil } - + let icon = NSWorkspace.shared.icon(forFile: path.path) iconCache[cacheKey] = (image: icon, timestamp: Date()) return icon } - + static func getIcon(bundleID: String) -> NSImage? { removeExpiredCacheEntries() - + if let cachedEntry = iconCache[bundleID], Date().timeIntervalSince(cachedEntry.timestamp) < cacheExpiryInterval { return cachedEntry.image } - + guard let path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) else { return nil } - + let icon = getIcon(file: path) iconCache[bundleID] = (image: icon!, timestamp: Date()) return icon } - + static func getIcon(application: String) -> NSImage? { - return getIcon(bundleID: application) + getIcon(bundleID: application) } - + // MARK: - Bundles - + static func bundle(forBundleID: String) -> Bundle? { guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: forBundleID) else { return nil } - + return Bundle(url: url) } - + // MARK: - Cache Management - + static func clearCache() { iconCache.removeAll() } - + private static func removeExpiredCacheEntries() { let now = Date() iconCache = iconCache.filter { now.timeIntervalSince($0.value.timestamp) < cacheExpiryInterval } diff --git a/DockDoor/Utilities/DockObserver.swift b/DockDoor/Utilities/DockObserver.swift index c30a76e4..1bf81308 100644 --- a/DockDoor/Utilities/DockObserver.swift +++ b/DockDoor/Utilities/DockObserver.swift @@ -4,41 +4,41 @@ // Created by Ethan Bills on 6/3/24. // -import Cocoa import ApplicationServices +import Cocoa final class DockObserver { static let shared = DockObserver() - + private var lastAppName: String? private var lastMouseLocation: CGPoint? private let mouseUpdateThreshold: CGFloat = 5.0 private var eventTap: CFMachPort? - + private var hoverProcessingTask: Task? private var isProcessing: Bool = false - - private var dockAppProcessIdentifier: pid_t? = nil - + + private var dockAppProcessIdentifier: pid_t? + private init() { setupEventTap() setupDockApp() } - + deinit { - if let eventTap = eventTap { + if let eventTap { CGEvent.tapEnable(tap: eventTap, enable: false) CFMachPortInvalidate(eventTap) CFRunLoopRemoveSource(CFRunLoopGetCurrent(), CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0), CFRunLoopMode.commonModes) } } - + private func setupDockApp() { if let dockAppPid = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == "com.apple.dock" })?.processIdentifier { - self.dockAppProcessIdentifier = dockAppPid + dockAppProcessIdentifier = dockAppPid } } - + private func setupEventTap() { guard AXIsProcessTrusted() else { print("Debug: Accessibility permission not granted") @@ -47,10 +47,10 @@ final class DockObserver { completion: { _ in SystemPreferencesHelper.openAccessibilityPreferences() }) return } - - func eventTapCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged? { + + func eventTapCallback(proxy _: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged? { let observer = Unmanaged.fromOpaque(refcon!).takeUnretainedValue() - + if type == .mouseMoved { let mouseLocation = event.location observer.handleMouseEvent(mouseLocation: mouseLocation) @@ -60,15 +60,15 @@ final class DockObserver { SharedPreviewWindowCoordinator.shared.hideWindow() } } - + return Unmanaged.passUnretained(event) } - + let eventsOfInterest: CGEventMask = (1 << CGEventType.mouseMoved.rawValue) | - (1 << CGEventType.rightMouseDown.rawValue) | - (1 << CGEventType.leftMouseDown.rawValue) | - (1 << CGEventType.otherMouseDown.rawValue) - + (1 << CGEventType.rightMouseDown.rawValue) | + (1 << CGEventType.leftMouseDown.rawValue) | + (1 << CGEventType.otherMouseDown.rawValue) + eventTap = CGEvent.tapCreate( tap: .cghidEventTap, place: .headInsertEventTap, @@ -77,8 +77,8 @@ final class DockObserver { callback: eventTapCallback, userInfo: Unmanaged.passUnretained(self).toOpaque() ) - - if let eventTap = eventTap { + + if let eventTap { let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0) CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, CFRunLoopMode.commonModes) CGEvent.tapEnable(tap: eventTap, enable: true) @@ -86,49 +86,50 @@ final class DockObserver { print("Failed to create CGEvent tap.") } } - + private func handleMouseEvent(mouseLocation: CGPoint) { hoverProcessingTask?.cancel() - + hoverProcessingTask = Task { [weak self] in - guard let self = self else { return } + guard let self else { return } try Task.checkCancellation() self.processMouseEvent(mouseLocation: mouseLocation) } } - + private func processMouseEvent(mouseLocation: CGPoint) { guard !isProcessing, !ScreenCenteredFloatingWindow.shared.windowSwitcherActive else { return } isProcessing = true - + defer { isProcessing = false } - - guard let lastMouseLocation = lastMouseLocation else { + + guard let lastMouseLocation else { self.lastMouseLocation = mouseLocation return } - + // Ignore minor movements - if abs(mouseLocation.x - lastMouseLocation.x) < mouseUpdateThreshold && - abs(mouseLocation.y - lastMouseLocation.y) < mouseUpdateThreshold { + if abs(mouseLocation.x - lastMouseLocation.x) < mouseUpdateThreshold, + abs(mouseLocation.y - lastMouseLocation.y) < mouseUpdateThreshold + { return } self.lastMouseLocation = mouseLocation - + // Capture the current mouseLocation let currentMouseLocation = mouseLocation - + if let dockIconAppName = getDockIconAtLocation(currentMouseLocation) { if dockIconAppName != lastAppName { lastAppName = dockIconAppName - + Task { [weak self] in - guard let self = self else { return } + guard let self else { return } do { let activeWindows = try await WindowUtil.activeWindows(for: dockIconAppName) - + await MainActor.run { if activeWindows.isEmpty { SharedPreviewWindowCoordinator.shared.hideWindow() @@ -157,7 +158,7 @@ final class DockObserver { } } else { Task { @MainActor [weak self] in - guard let self = self else { return } + guard let self else { return } let mouseScreen = DockObserver.screenContainingPoint(currentMouseLocation) ?? NSScreen.main! let convertedMouseLocation = DockObserver.nsPointFromCGPoint(currentMouseLocation, forScreen: mouseScreen) if !SharedPreviewWindowCoordinator.shared.frame.contains(convertedMouseLocation) { @@ -167,50 +168,50 @@ final class DockObserver { } } } - + func getDockIconFrameAtLocation(_ mouseLocation: CGPoint) -> CGRect? { guard let dockAppProcessIdentifier else { return nil } - + let axDockApp = AXUIElementCreateApplication(dockAppProcessIdentifier) - + var dockItems: CFTypeRef? let dockItemsResult = AXUIElementCopyAttributeValue(axDockApp, kAXChildrenAttribute as CFString, &dockItems) - + guard dockItemsResult == .success, let items = dockItems as? [AXUIElement] else { return nil } - + let axList = items.first { element in var role: CFTypeRef? let roleResult = AXUIElementCopyAttributeValue(element, kAXRoleAttribute as CFString, &role) return roleResult == .success && (role as? String) == kAXListRole } - + guard let list = axList else { return nil } - + var axChildren: CFTypeRef? let childrenResult = AXUIElementCopyAttributeValue(list, kAXChildrenAttribute as CFString, &axChildren) - + guard childrenResult == .success, let children = axChildren as? [AXUIElement] else { return nil } - + // Adjust mouse location to match the coordinate system of the dock icons let adjustedMouseLocation = CGPoint( x: mouseLocation.x, y: (DockObserver.screenContainingPoint(mouseLocation)?.frame.height ?? NSScreen.main!.frame.height) - mouseLocation.y ) - + for element in children { var positionValue: CFTypeRef? var sizeValue: CFTypeRef? let positionResult = AXUIElementCopyAttributeValue(element, kAXPositionAttribute as CFString, &positionValue) let sizeResult = AXUIElementCopyAttributeValue(element, kAXSizeAttribute as CFString, &sizeValue) - + if positionResult == .success, sizeResult == .success { let position = positionValue as! AXValue let size = sizeValue as! AXValue @@ -218,53 +219,53 @@ final class DockObserver { AXValueGetValue(position, .cgPoint, &positionPoint) var sizeCGSize = CGSize.zero AXValueGetValue(size, .cgSize, &sizeCGSize) - + let iconRect = CGRect(origin: positionPoint, size: sizeCGSize) - + if iconRect.contains(adjustedMouseLocation) { return iconRect } } } - + return nil } - + func getDockIconAtLocation(_ mouseLocation: CGPoint) -> String? { guard let dockAppProcessIdentifier else { return nil } - + let axDockApp = AXUIElementCreateApplication(dockAppProcessIdentifier) - + var dockItems: CFTypeRef? let dockItemsResult = AXUIElementCopyAttributeValue(axDockApp, kAXChildrenAttribute as CFString, &dockItems) - + guard dockItemsResult == .success, let items = dockItems as? [AXUIElement] else { return nil } - + let axList = items.first { element in var role: CFTypeRef? AXUIElementCopyAttributeValue(element, kAXRoleAttribute as CFString, &role) return (role as? String) == kAXListRole } - + guard axList != nil else { return nil } - + var axChildren: CFTypeRef? AXUIElementCopyAttributeValue(axList!, kAXChildrenAttribute as CFString, &axChildren) - + guard let children = axChildren as? [AXUIElement] else { return nil } - + for element in children { var positionValue: CFTypeRef? var sizeValue: CFTypeRef? let positionResult = AXUIElementCopyAttributeValue(element, kAXPositionAttribute as CFString, &positionValue) let sizeResult = AXUIElementCopyAttributeValue(element, kAXSizeAttribute as CFString, &sizeValue) - + if positionResult == .success, sizeResult == .success { let position = positionValue as! AXValue let size = sizeValue as! AXValue @@ -272,7 +273,7 @@ final class DockObserver { AXValueGetValue(position, .cgPoint, &positionPoint) var sizeCGSize = CGSize.zero AXValueGetValue(size, .cgSize, &sizeCGSize) - + let iconRect = CGRect(origin: positionPoint, size: sizeCGSize) if iconRect.contains(mouseLocation) { var value: CFTypeRef? @@ -287,38 +288,40 @@ final class DockObserver { } } } - + return nil } - + func isMouseWithinDock(_ mouseLocation: CGPoint) -> Bool { - return getDockIconAtLocation(mouseLocation) != nil + getDockIconAtLocation(mouseLocation) != nil } - + static func screenContainingPoint(_ point: CGPoint) -> NSScreen? { let screens = NSScreen.screens guard let primaryScreen = screens.first else { return nil } - + for screen in screens { let (offsetLeft, offsetTop) = computeOffsets(for: screen, primaryScreen: primaryScreen) - - if point.x >= offsetLeft && point.x <= offsetLeft + screen.frame.size.width && - point.y >= offsetTop && point.y <= offsetTop + screen.frame.size.height { + + if point.x >= offsetLeft, point.x <= offsetLeft + screen.frame.size.width, + point.y >= offsetTop, point.y <= offsetTop + screen.frame.size.height + { return screen } } - + return primaryScreen } - + static func nsPointFromCGPoint(_ point: CGPoint, forScreen: NSScreen?) -> NSPoint { guard let screen = forScreen, - let primaryScreen = NSScreen.screens.first else { + let primaryScreen = NSScreen.screens.first + else { return NSPoint(x: point.x, y: point.y) } - + let (_, offsetTop) = computeOffsets(for: screen, primaryScreen: primaryScreen) - + let y: CGFloat if screen == primaryScreen { y = screen.frame.size.height - point.y @@ -326,31 +329,32 @@ final class DockObserver { let screenBottomOffset = primaryScreen.frame.size.height - (screen.frame.size.height + offsetTop) y = screen.frame.size.height + screenBottomOffset - (point.y - offsetTop) } - + return NSPoint(x: point.x, y: y) } - + static func cgPointFromNSPoint(_ point: CGPoint, forScreen: NSScreen?) -> CGPoint { guard let screen = forScreen, - let primaryScreen = NSScreen.screens.first else { + let primaryScreen = NSScreen.screens.first + else { return CGPoint(x: point.x, y: point.y) } - + let (_, offsetTop) = computeOffsets(for: screen, primaryScreen: primaryScreen) let menuScreenHeight = screen.frame.maxY - + return CGPoint(x: point.x, y: menuScreenHeight - point.y + offsetTop) } - + private static func computeOffsets(for screen: NSScreen, primaryScreen: NSScreen) -> (CGFloat, CGFloat) { var offsetLeft = screen.frame.origin.x var offsetTop = primaryScreen.frame.size.height - (screen.frame.origin.y + screen.frame.size.height) - + if screen == primaryScreen { offsetTop = 0 offsetLeft = 0 } - + return (offsetLeft, offsetTop) } } diff --git a/DockDoor/Utilities/DockUtils.swift b/DockDoor/Utilities/DockUtils.swift index 16c700c7..29bf04ca 100644 --- a/DockDoor/Utilities/DockUtils.swift +++ b/DockDoor/Utilities/DockUtils.swift @@ -15,64 +15,65 @@ enum DockPosition { class DockUtils { static let shared = DockUtils() - + private let dockDefaults: UserDefaults? // Store a single instance - + private init() { dockDefaults = UserDefaults(suiteName: "com.apple.dock") } - + func isDockHidingEnabled() -> Bool { if let dockAutohide = dockDefaults?.bool(forKey: "autohide") { return dockAutohide } - + return false } - + func countIcons() -> (Int, Int) { let persistentAppsCount = dockDefaults?.array(forKey: "persistent-apps")?.count ?? 0 let recentAppsCount = dockDefaults?.array(forKey: "recent-apps")?.count ?? 0 return (persistentAppsCount + recentAppsCount, (persistentAppsCount > 0 && recentAppsCount > 0) ? 1 : 0) } - + func calculateDockWidth() -> CGFloat { let countIcons = countIcons() let iconCount = countIcons.0 let numberOfDividers = countIcons.1 let tileSize = tileSize() - + let baseWidth = tileSize * CGFloat(iconCount) let dividerWidth: CGFloat = 10.0 let totalDividerWidth = CGFloat(numberOfDividers) * dividerWidth - - if self.isMagnificationEnabled(), - let largeSize = dockDefaults?.object(forKey: "largesize") as? CGFloat { + + if isMagnificationEnabled(), + let largeSize = dockDefaults?.object(forKey: "largesize") as? CGFloat + { let extraWidth = (largeSize - tileSize) * CGFloat(iconCount) * 0.5 return baseWidth + extraWidth + totalDividerWidth } - + return baseWidth + totalDividerWidth } - + private func tileSize() -> CGFloat { - return dockDefaults?.double(forKey: "tilesize") ?? 0 + dockDefaults?.double(forKey: "tilesize") ?? 0 } - + private func largeSize() -> CGFloat { - return dockDefaults?.double(forKey: "largesize") ?? 0 + dockDefaults?.double(forKey: "largesize") ?? 0 } - + func isMagnificationEnabled() -> Bool { - return dockDefaults?.bool(forKey: "magnification") ?? false + dockDefaults?.bool(forKey: "magnification") ?? false } - + func calculateDockHeight(_ forScreen: NSScreen?) -> CGFloat { - if self.isDockHidingEnabled() { + if isDockHidingEnabled() { return abs(largeSize() - tileSize()) } else { if let currentScreen = forScreen { - switch self.getDockPosition() { + switch getDockPosition() { case .right, .left: let size = abs(currentScreen.frame.width - currentScreen.visibleFrame.width) return size @@ -86,18 +87,18 @@ class DockUtils { return 0.0 } } - + func getStatusBarHeight(screen: NSScreen?) -> CGFloat { var statusBarHeight = 0.0 - if let screen = screen { + if let screen { statusBarHeight = screen.frame.height - screen.visibleFrame.height - (screen.visibleFrame.origin.y - screen.frame.origin.y) - 1 } return statusBarHeight } - + func getDockPosition() -> DockPosition { guard let orientation = dockDefaults?.string(forKey: "orientation")?.lowercased() else { - if NSScreen.main!.visibleFrame.origin.y == 0 && !self.isDockHidingEnabled() { + if NSScreen.main!.visibleFrame.origin.y == 0, !isDockHidingEnabled() { if NSScreen.main!.visibleFrame.origin.x == 0 { return .right } else { @@ -107,12 +108,12 @@ class DockUtils { return .bottom } } - + switch orientation { - case "left": return .left + case "left": return .left case "bottom": return .bottom - case "right": return .right - default: return .unknown + case "right": return .right + default: return .unknown } } } diff --git a/DockDoor/Utilities/KeybindHelper.swift b/DockDoor/Utilities/KeybindHelper.swift index 8534f535..f6d10e9a 100644 --- a/DockDoor/Utilities/KeybindHelper.swift +++ b/DockDoor/Utilities/KeybindHelper.swift @@ -21,87 +21,86 @@ class KeybindHelper { private var eventTap: CFMachPort? private var runLoopSource: CFRunLoopSource? private var modifierValue: Int = 0 - + private init() { setupEventTap() } - + deinit { removeEventTap() } - + private func setupEventTap() { let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue) | (1 << CGEventType.flagsChanged.rawValue) - + eventTap = CGEvent.tapCreate( tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: CGEventMask(eventMask), - callback: { proxy, type, event, refcon -> Unmanaged? in + callback: { proxy, type, event, _ -> Unmanaged? in return KeybindHelper.shared.handleEvent(proxy: proxy, type: type, event: event) }, userInfo: nil ) - - guard let eventTap = eventTap else { + + guard let eventTap else { print("Failed to create event tap.") return } - + runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0) CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) CGEvent.tapEnable(tap: eventTap, enable: true) } - + private func removeEventTap() { - if let eventTap = eventTap, let runLoopSource = runLoopSource { + if let eventTap, let runLoopSource { CGEvent.tapEnable(tap: eventTap, enable: false) CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) self.eventTap = nil self.runLoopSource = nil } } - - private func handleEvent(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent) -> Unmanaged? { + + private func handleEvent(proxy _: CGEventTapProxy, type: CGEventType, event: CGEvent) -> Unmanaged? { let keyCode = event.getIntegerValueField(.keyboardEventKeycode) let keyBoardShortcutSaved: UserKeyBind = Defaults[.UserKeybind] // UserDefaults.standard.getKeybind()! let shiftKeyCurrentlyPressed = event.flags.contains(.maskShift) var userDefinedKeyCurrentlyPressed = false - - if ((type == .flagsChanged) && (!Defaults[.defaultCMDTABKeybind])){ + + if type == .flagsChanged, !Defaults[.defaultCMDTABKeybind] { // New Keybind that the user has enforced, includes the modifier keys if event.flags.contains(.maskControl) { modifierValue = Defaults[.Int64maskControl] userDefinedKeyCurrentlyPressed = true - } - else if event.flags.contains(.maskAlternate) { + } else if event.flags.contains(.maskAlternate) { modifierValue = Defaults[.Int64maskAlternate] userDefinedKeyCurrentlyPressed = true } handleModifierEvent(modifierKeyPressed: userDefinedKeyCurrentlyPressed, shiftKeyPressed: shiftKeyCurrentlyPressed) } - - else if ((type == .flagsChanged) && (Defaults[.defaultCMDTABKeybind])){ + + else if type == .flagsChanged, Defaults[.defaultCMDTABKeybind] { // Default MacOS CMD + TAB keybind replaced handleModifierEvent(modifierKeyPressed: event.flags.contains(.maskCommand), shiftKeyPressed: shiftKeyCurrentlyPressed) } - - else if (type == .keyDown){ - if (isModifierKeyPressed && keyCode == keyBoardShortcutSaved.keyCode && modifierValue == keyBoardShortcutSaved.modifierFlags) || (Defaults[.defaultCMDTABKeybind] && event.flags.contains(.maskCommand) && keyCode == 48) { // Tab key - if SharedPreviewWindowCoordinator.shared.isVisible { // Check if HoverWindow is already shown - SharedPreviewWindowCoordinator.shared.cycleWindows(goBackwards: isShiftKeyPressed) // Cycle windows based on Shift key state + + else if type == .keyDown { + if (isModifierKeyPressed && keyCode == keyBoardShortcutSaved.keyCode && modifierValue == keyBoardShortcutSaved.modifierFlags) || (Defaults[.defaultCMDTABKeybind] && event.flags.contains(.maskCommand) && keyCode == 48) { // Tab key + if SharedPreviewWindowCoordinator.shared.isVisible { // Check if HoverWindow is already shown + SharedPreviewWindowCoordinator.shared.cycleWindows(goBackwards: isShiftKeyPressed) // Cycle windows based on Shift key state } else { - showHoverWindow() // Initialize HoverWindow if it's not open + showHoverWindow() // Initialize HoverWindow if it's not open } - return nil // Suppress the Tab key event + return nil // Suppress the Tab key event } } - + return Unmanaged.passUnretained(event) } - - private func handleModifierEvent(modifierKeyPressed : Bool, shiftKeyPressed : Bool){ + + private func handleModifierEvent(modifierKeyPressed: Bool, shiftKeyPressed: Bool) { if modifierKeyPressed != isModifierKeyPressed { isModifierKeyPressed = modifierKeyPressed } @@ -109,20 +108,20 @@ class KeybindHelper { if shiftKeyPressed != isShiftKeyPressed { isShiftKeyPressed = shiftKeyPressed } - + if !isModifierKeyPressed { SharedPreviewWindowCoordinator.shared.hideWindow() SharedPreviewWindowCoordinator.shared.selectAndBringToFrontCurrentWindow() } } - + private func showHoverWindow() { Task { [weak self] in do { - guard let self = self else { return } + guard let self else { return } let windows = try await WindowUtil.activeWindows(for: "") await MainActor.run { [weak self] in - guard let self = self else { return } + guard let self else { return } if self.isModifierKeyPressed { SharedPreviewWindowCoordinator.shared.showWindow(appName: "Alt-Tab", windows: windows, overrideDelay: true, centeredHoverWindowState: .windowSwitcher, onWindowTap: { SharedPreviewWindowCoordinator.shared.hideWindow() }) diff --git a/DockDoor/Utilities/LimitedTaskGroup.swift b/DockDoor/Utilities/LimitedTaskGroup.swift index f804d58d..9b22c7ce 100644 --- a/DockDoor/Utilities/LimitedTaskGroup.swift +++ b/DockDoor/Utilities/LimitedTaskGroup.swift @@ -10,12 +10,12 @@ actor LimitedTaskGroup { private let maxConcurrentTasks: Int private var runningTasks = 0 private let semaphore: AsyncSemaphore - + init(maxConcurrentTasks: Int) { self.maxConcurrentTasks = maxConcurrentTasks - self.semaphore = AsyncSemaphore(value: maxConcurrentTasks) + semaphore = AsyncSemaphore(value: maxConcurrentTasks) } - + func addTask(_ operation: @escaping () async throws -> T) { let task = Task { await semaphore.wait() @@ -24,17 +24,17 @@ actor LimitedTaskGroup { } tasks.append(task) } - + func waitForAll() async throws -> [T] { defer { tasks.removeAll() } - + return try await withThrowingTaskGroup(of: T.self) { group in for task in tasks { group.addTask { try await task.value } } - + var results: [T] = [] for try await result in group { results.append(result) @@ -47,11 +47,11 @@ actor LimitedTaskGroup { actor AsyncSemaphore { private var value: Int private var waiters: [CheckedContinuation] = [] - + init(value: Int) { self.value = value } - + func wait() async { if value > 0 { value -= 1 @@ -61,7 +61,7 @@ actor AsyncSemaphore { } } } - + func signal() { if let waiter = waiters.first { waiters.removeFirst() diff --git a/DockDoor/Utilities/MessageUtil.swift b/DockDoor/Utilities/MessageUtil.swift index ac7c374a..03b55d5f 100644 --- a/DockDoor/Utilities/MessageUtil.swift +++ b/DockDoor/Utilities/MessageUtil.swift @@ -7,8 +7,7 @@ import Cocoa -struct MessageUtil { - +enum MessageUtil { enum ButtonAction { case ok case cancel diff --git a/DockDoor/Utilities/Misc Utils.swift b/DockDoor/Utilities/Misc Utils.swift index 9d8115e6..d593e5cf 100644 --- a/DockDoor/Utilities/Misc Utils.swift +++ b/DockDoor/Utilities/Misc Utils.swift @@ -5,27 +5,28 @@ // Created by Ethan Bills on 6/13/24. // +import Carbon import Cocoa import Defaults -import Carbon -func askUserToRestartApplication () -> Void { +func askUserToRestartApplication() { MessageUtil.showMessage(title: String(localized: "Restart required"), message: String(localized: "Please restart the application to apply your changes. Click OK to quit the app."), completion: { result in if result == .ok { let appDelegate = NSApplication.shared.delegate as! AppDelegate appDelegate.restartApp() - }}) + } + }) } func resetDefaultsToDefaultValues() { Defaults.removeAll() - + // reset the launched value Defaults[.launched] = true } func getWindowSize() -> CGSize { - return CGSize(width: optimisticScreenSizeWidth / Defaults[.sizingMultiplier], height: optimisticScreenSizeHeight / Defaults[.sizingMultiplier]) + CGSize(width: optimisticScreenSizeWidth / Defaults[.sizingMultiplier], height: optimisticScreenSizeHeight / Defaults[.sizingMultiplier]) } // Helper extension to calculate distance between CGPoints @@ -46,24 +47,21 @@ func measureString(_ string: String, fontSize: CGFloat, fontWeight: NSFont.Weigh return size } -struct modifierConverter { +enum modifierConverter { static func toString(_ modifierIntValue: Int) -> String { if modifierIntValue == Defaults[.Int64maskCommand] { return "⌘" - } - else if modifierIntValue == Defaults[.Int64maskAlternate] { + } else if modifierIntValue == Defaults[.Int64maskAlternate] { return "⌥" - } - else if modifierIntValue == Defaults[.Int64maskControl] { + } else if modifierIntValue == Defaults[.Int64maskControl] { return "⌃" - } - else { + } else { return " " } } } -struct KeyCodeConverter { +enum KeyCodeConverter { static func toString(_ keyCode: UInt16) -> String { switch keyCode { case 48: @@ -75,21 +73,21 @@ struct KeyCodeConverter { case 36: return "↩︎" // Return symbol default: - + let source = TISCopyCurrentKeyboardInputSource().takeUnretainedValue() let layoutData = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) - + guard let data = layoutData else { return "?" } - + let layout = unsafeBitCast(data, to: CFData.self) let keyboardLayout = unsafeBitCast(CFDataGetBytePtr(layout), to: UnsafePointer.self) - + var keysDown: UInt32 = 0 var chars = [UniChar](repeating: 0, count: 4) - var realLength: Int = 0 - + var realLength = 0 + let result = UCKeyTranslate(keyboardLayout, keyCode, UInt16(kUCKeyActionDisplay), @@ -100,7 +98,7 @@ struct KeyCodeConverter { chars.count, &realLength, &chars) - + if result == noErr { return String(utf16CodeUnits: chars, count: realLength) } else { diff --git a/DockDoor/Utilities/SpaceWindowCacheManager.swift b/DockDoor/Utilities/SpaceWindowCacheManager.swift index f3ed943f..d80ddaec 100644 --- a/DockDoor/Utilities/SpaceWindowCacheManager.swift +++ b/DockDoor/Utilities/SpaceWindowCacheManager.swift @@ -12,19 +12,19 @@ class SpaceWindowCacheManager { private let queue = DispatchQueue(label: "com.dockdoor.cacheQueue", attributes: .concurrent) private var desktopSpaceWindowCache: [String: Set] = [:] private var appNameBundleIdTracker: [String: String] = [:] - + func readCache(bundleId: String) -> Set { queue.sync { - return desktopSpaceWindowCache[bundleId] ?? [] + desktopSpaceWindowCache[bundleId] ?? [] } } - + func writeCache(bundleId: String, windowSet: Set) { queue.async(flags: .barrier) { self.desktopSpaceWindowCache[bundleId] = windowSet } } - + func updateCache(bundleId: String, update: @escaping (inout Set) -> Void) { queue.async(flags: .barrier) { var windowSet = self.desktopSpaceWindowCache[bundleId] ?? [] @@ -32,14 +32,14 @@ class SpaceWindowCacheManager { self.desktopSpaceWindowCache[bundleId] = windowSet } } - + func removeFromCache(bundleId: String, windowId: CGWindowID) { queue.async(flags: .barrier) { if var windowSet = self.desktopSpaceWindowCache[bundleId] { if windowSet.isEmpty { self.desktopSpaceWindowCache.removeValue(forKey: bundleId) } else { - if let windowToRemove = windowSet.first(where: { $0.id == windowId}) { + if let windowToRemove = windowSet.first(where: { $0.id == windowId }) { windowSet.remove(windowToRemove) self.desktopSpaceWindowCache[bundleId] = windowSet } else { @@ -49,11 +49,11 @@ class SpaceWindowCacheManager { } } } - + func getAllWindows() -> [WindowInfo] { queue.sync { let sortedWindows = desktopSpaceWindowCache.values.flatMap { $0 }.sorted(by: { $0.lastUsed > $1.lastUsed }) - + // If there are at least two windows, swap the first and second if sortedWindows.count >= 2 { var modifiedWindows = sortedWindows @@ -64,53 +64,54 @@ class SpaceWindowCacheManager { } } } - + // New functions for appNameBundleIdTracker - + func updateAppNameBundleIdTracker(app: SCRunningApplication, nonLocalName: String) { queue.async(flags: .barrier) { self.appNameBundleIdTracker[app.applicationName] = app.bundleIdentifier self.appNameBundleIdTracker[nonLocalName] = app.bundleIdentifier } } - + func addToBundleIDTracker(applicationName: String, bundleID: String) { queue.async(flags: .barrier) { - if !self.appNameBundleIdTracker.contains(where: {$0.key == applicationName}) { + if !self.appNameBundleIdTracker.contains(where: { $0.key == applicationName }) { self.appNameBundleIdTracker[applicationName] = bundleID } } } - + func findBundleID(for applicationName: String) -> String? { - return queue.sync { + queue.sync { // First, try to get the bundle ID directly from the tracker if let bundleID = self.appNameBundleIdTracker[applicationName] { return bundleID } - + // If not found, try to find a matching application for (appName, bundleId) in self.appNameBundleIdTracker { if applicationName.contains(appName) || appName.contains(applicationName) { return bundleId } - + // Check non-localized name if let nonLocalizedName = self.getNonLocalizedAppName(forBundleIdentifier: bundleId), - applicationName.contains(nonLocalizedName) || nonLocalizedName.contains(applicationName) { + applicationName.contains(nonLocalizedName) || nonLocalizedName.contains(applicationName) + { return bundleId } } - + return nil } } - + private func getNonLocalizedAppName(forBundleIdentifier bundleIdentifier: String) -> String? { guard let bundleURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) else { return nil } - + let bundle = Bundle(url: bundleURL) return bundle?.object(forInfoDictionaryKey: "CFBundleName") as? String } diff --git a/DockDoor/Utilities/WindowManipulationObservers.swift b/DockDoor/Utilities/WindowManipulationObservers.swift index b736d3a4..0fd06448 100644 --- a/DockDoor/Utilities/WindowManipulationObservers.swift +++ b/DockDoor/Utilities/WindowManipulationObservers.swift @@ -5,9 +5,9 @@ // Created by Ethan Bills on 6/30/24. // -import Cocoa -import ApplicationServices import AppKit +import ApplicationServices +import Cocoa class WindowManipulationObservers { static let shared = WindowManipulationObservers() @@ -15,35 +15,36 @@ class WindowManipulationObservers { static var trackedElements: Set = [] static var debounceWorkItem: DispatchWorkItem? static var lastWindowCreationTime: [String: Date] = [:] - static let windowCreationDebounceInterval: TimeInterval = 1.0 // 1 second debounce - + static let windowCreationDebounceInterval: TimeInterval = 1.0 // 1 second debounce + private init() { setupObservers() } - + private func setupObservers() { let notificationCenter = NSWorkspace.shared.notificationCenter notificationCenter.addObserver(self, selector: #selector(appDidLaunch(_:)), name: NSWorkspace.didLaunchApplicationNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(appDidTerminate(_:)), name: NSWorkspace.didTerminateApplicationNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(appDidActivate(_:)), name: NSWorkspace.didActivateApplicationNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(appDidHide(_:)), name: NSApplication.didHideNotification, object: nil) - + // Set up observers for already running applications - NSWorkspace.shared.runningApplications.forEach { app in + for app in NSWorkspace.shared.runningApplications { if app.activationPolicy == .regular { createObserverForApp(app) } } } - + @objc private func appDidLaunch(_ notification: Notification) { guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, - app.activationPolicy == .regular else { + app.activationPolicy == .regular + else { return } createObserverForApp(app) } - + @objc private func appDidTerminate(_ notification: Notification) { guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return @@ -52,23 +53,23 @@ class WindowManipulationObservers { removeObserverForApp(app) SharedPreviewWindowCoordinator.shared.hideWindow() } - - @objc private func appDidActivate(_ notification: Notification) { + + @objc private func appDidActivate(_: Notification) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { if SharedPreviewWindowCoordinator.shared.isVisible { SharedPreviewWindowCoordinator.shared.hideWindow() } } } - + private func createObserverForApp(_ app: NSRunningApplication) { let pid = app.processIdentifier - + var observer: AXObserver? let result = AXObserverCreate(pid, axObserverCallback, &observer) - guard result == .success, let observer = observer else { return } + guard result == .success, let observer else { return } WindowUtil.addAppToBundleIDTracker(applicationName: app.localizedName ?? "", bundleID: app.bundleIdentifier ?? "") - + let appElement = AXUIElementCreateApplication(pid) AXObserverAddNotification(observer, appElement, kAXWindowCreatedNotification as CFString, UnsafeMutableRawPointer(bitPattern: Int(pid))) AXObserverAddNotification(observer, appElement, kAXUIElementDestroyedNotification as CFString, UnsafeMutableRawPointer(bitPattern: Int(pid))) @@ -77,36 +78,36 @@ class WindowManipulationObservers { AXObserverAddNotification(observer, appElement, kAXApplicationHiddenNotification as CFString, UnsafeMutableRawPointer(bitPattern: Int(pid))) AXObserverAddNotification(observer, appElement, kAXApplicationShownNotification as CFString, UnsafeMutableRawPointer(bitPattern: Int(pid))) AXObserverAddNotification(observer, appElement, kAXFocusedUIElementChangedNotification as CFString, UnsafeMutableRawPointer(bitPattern: Int(pid))) - + CFRunLoopAddSource(CFRunLoopGetMain(), AXObserverGetRunLoopSource(observer), .defaultMode) - + observers[pid] = observer } - + @objc private func appDidHide(_ notification: Notification) { guard let app = notification.object as? NSRunningApplication else { return } let pid = app.processIdentifier let bundleID = app.bundleIdentifier ?? "" - + WindowUtil.updateStatusOfWindowCache(pid: pid, bundleID: bundleID, isParentAppHidden: true) } - + private func removeObserverForApp(_ app: NSRunningApplication) { let pid = app.processIdentifier guard let observer = observers[pid] else { return } - + let appElement = AXUIElementCreateApplication(pid) AXObserverRemoveNotification(observer, appElement, kAXWindowCreatedNotification as CFString) AXObserverRemoveNotification(observer, appElement, kAXUIElementDestroyedNotification as CFString) AXObserverRemoveNotification(observer, appElement, kAXWindowMiniaturizedNotification as CFString) AXObserverRemoveNotification(observer, appElement, kAXWindowDeminiaturizedNotification as CFString) AXObserverRemoveNotification(observer, appElement, kAXFocusedUIElementChangedNotification as CFString) - + observers.removeValue(forKey: pid) } - + deinit { - observers.forEach { pid, observer in + for (pid, observer) in observers { let appElement = AXUIElementCreateApplication(pid) AXObserverRemoveNotification(observer, appElement, kAXWindowCreatedNotification as CFString) AXObserverRemoveNotification(observer, appElement, kAXUIElementDestroyedNotification as CFString) @@ -118,10 +119,10 @@ class WindowManipulationObservers { } } -func axObserverCallback(observer: AXObserver, element: AXUIElement, notificationName: CFString, userData: UnsafeMutableRawPointer?) -> Void { - guard let userData = userData else { return } +func axObserverCallback(observer _: AXObserver, element: AXUIElement, notificationName: CFString, userData: UnsafeMutableRawPointer?) { + guard let userData else { return } let pid = pid_t(Int(bitPattern: userData)) - + DispatchQueue.main.async { if let app = NSRunningApplication(processIdentifier: pid) { switch notificationName as String { diff --git a/DockDoor/Utilities/WindowUtil.swift b/DockDoor/Utilities/WindowUtil.swift index cd5c512b..4cd187c7 100644 --- a/DockDoor/Utilities/WindowUtil.swift +++ b/DockDoor/Utilities/WindowUtil.swift @@ -1,16 +1,16 @@ // -// WindowManager.swift +// WindowUtil.swift // DockDoor // // Created by Ethan Bills on 6/3/24. // -import Cocoa import ApplicationServices -import ScreenCaptureKit +import Cocoa import Defaults +import ScreenCaptureKit -let filteredBundleIdentifiers: [String] = ["com.apple.notificationcenterui"] // filters desktop widgets +let filteredBundleIdentifiers: [String] = ["com.apple.notificationcenterui"] // filters desktop widgets struct WindowInfo: Identifiable, Hashable { let id: CGWindowID @@ -26,14 +26,14 @@ struct WindowInfo: Identifiable, Hashable { var isMinimized: Bool var isHidden: Bool var lastUsed: Date - + func hash(into hasher: inout Hasher) { hasher.combine(id) hasher.combine(bundleID) } - + static func == (lhs: WindowInfo, rhs: WindowInfo) -> Bool { - return lhs.id == rhs.id && lhs.bundleID == rhs.bundleID + lhs.id == rhs.id && lhs.bundleID == rhs.bundleID } } @@ -48,28 +48,28 @@ struct CachedAppIcon { let timestamp: Date } -final class WindowUtil { +enum WindowUtil { private static var imageCache: [CGWindowID: CachedImage] = [:] private static let cacheQueue = DispatchQueue(label: "com.dockdoor.cacheQueue", attributes: .concurrent) private static var cacheExpirySeconds: Double = Defaults[.screenCaptureCacheLifespan] - + private static let desktopSpaceWindowCacheManager = SpaceWindowCacheManager() - + // MARK: - Cache Management - + static func clearExpiredCache() { let now = Date() cacheQueue.async(flags: .barrier) { imageCache = imageCache.filter { now.timeIntervalSince($0.value.timestamp) <= cacheExpirySeconds } } } - + static func resetCache() { cacheQueue.async(flags: .barrier) { imageCache.removeAll() } } - + static func clearWindowCache(for bundleId: String) { desktopSpaceWindowCacheManager.writeCache(bundleId: bundleId, windowSet: []) } @@ -81,68 +81,68 @@ final class WindowUtil { static func updateWindowCache(for bundleId: String, update: @escaping (inout Set) -> Void) { desktopSpaceWindowCacheManager.updateCache(bundleId: bundleId, update: update) } - + // MARK: - Helper Functions - + static func captureWindowImage(window: SCWindow) async throws -> CGImage { clearExpiredCache() - + if let cachedImage = getCachedImage(window: window) { return cachedImage } - + let filter = SCContentFilter(desktopIndependentWindow: window) let config = SCStreamConfiguration() - + config.scalesToFit = false config.backgroundColor = .clear config.ignoreGlobalClipDisplay = true config.ignoreShadowsDisplay = true config.shouldBeOpaque = false if #available(macOS 14.2, *) { config.includeChildWindows = false } - + // Get the scale factor of the display containing the window let scaleFactor = await getScaleFactorForWindow(windowID: window.windowID) - + // Convert points to pixels config.width = Int(window.frame.width * scaleFactor) / Int(Defaults[.windowPreviewImageScale]) config.height = Int(window.frame.height * scaleFactor) / Int(Defaults[.windowPreviewImageScale]) - + config.showsCursor = false config.captureResolution = .best - + let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) - + let cachedImage = CachedImage(image: image, timestamp: Date(), windowname: window.title) imageCache[window.windowID] = cachedImage - + return image } - + // Helper function to get the scale factor for a given window private static func getScaleFactorForWindow(windowID: CGWindowID) async -> CGFloat { - return await MainActor.run { + await MainActor.run { guard let window = NSApplication.shared.window(withWindowNumber: Int(windowID)) else { return NSScreen.main?.backingScaleFactor ?? 2.0 } - + if NSScreen.screens.count > 1 { if let currentScreen = window.screen { return currentScreen.backingScaleFactor } } - + return NSScreen.main?.backingScaleFactor ?? 2.0 } } - + private static func getCachedImage(window: SCWindow) -> CGImage? { if let cachedImage = imageCache[window.windowID], cachedImage.windowname == window.title, Date().timeIntervalSince(cachedImage.timestamp) <= cacheExpirySeconds { return cachedImage.image } return nil } - + static func isValidElement(_ element: AXUIElement) -> Bool { do { let _ = try element.role() @@ -151,7 +151,7 @@ final class WindowUtil { return false } } - + static func findWindow(matchingWindow window: SCWindow, in axWindows: [AXUIElement]) -> AXUIElement? { for axWindow in axWindows { if let cgWindowId = try? axWindow.cgWindowId(), window.windowID == cgWindowId { @@ -160,60 +160,60 @@ final class WindowUtil { // Fallback metohd // TODO: May never be needed - + if let windowTitle = window.title, let axTitle = try? axWindow.title(), isFuzzyMatch(windowTitle: windowTitle, axTitleString: axTitle) { return axWindow } - + if let axPosition = try? axWindow.position(), let axSize = try? axWindow.size(), axPosition != .zero, axSize != .zero { - let positionThreshold: CGFloat = 10 let sizeThreshold: CGFloat = 10 - + let positionMatch = abs(axPosition.x - window.frame.origin.x) <= positionThreshold && - abs(axPosition.y - window.frame.origin.y) <= positionThreshold - + abs(axPosition.y - window.frame.origin.y) <= positionThreshold + let sizeMatch = abs(axSize.width - window.frame.size.width) <= sizeThreshold && - abs(axSize.height - window.frame.size.height) <= sizeThreshold - - if positionMatch && sizeMatch { + abs(axSize.height - window.frame.size.height) <= sizeThreshold + + if positionMatch, sizeMatch { return axWindow } } } - + return nil } - + static func isFuzzyMatch(windowTitle: String, axTitleString: String) -> Bool { let axTitleWords = axTitleString.lowercased().split(separator: " ") let windowTitleWords = windowTitle.lowercased().split(separator: " ") - + let matchingWords = axTitleWords.filter { windowTitleWords.contains($0) } let matchPercentage = Double(matchingWords.count) / Double(windowTitleWords.count) - + return matchPercentage >= 0.90 || matchPercentage.isNaN || axTitleString.lowercased().contains(windowTitle.lowercased()) } - + static func getRunningApplication(named applicationName: String) -> NSRunningApplication? { - return NSWorkspace.shared.runningApplications.first { + NSWorkspace.shared.runningApplications.first { applicationName.contains($0.localizedName ?? "") || ($0.localizedName?.contains(applicationName) ?? false) } } - + // MARK: - Desktop Cache Retrievers + static func getAllWindowInfosAsList() -> [WindowInfo] { - return desktopSpaceWindowCacheManager.getAllWindows() + desktopSpaceWindowCacheManager.getAllWindows() } - + // MARK: - Window Manipulation Functions - + static func toggleMinimize(windowInfo: WindowInfo) { if windowInfo.isMinimized { if let app = NSRunningApplication(processIdentifier: windowInfo.pid), app.isHidden { app.unhide() } - + do { try windowInfo.axElement.setAttribute(kAXMinimizedAttribute, false) NSRunningApplication(processIdentifier: windowInfo.pid)?.activate() @@ -230,30 +230,30 @@ final class WindowUtil { } updateWindowDateTime(windowInfo) } - + static func toggleHidden(windowInfo: WindowInfo) { let newHiddenState = !windowInfo.isHidden - + do { try windowInfo.appAxElement.setAttribute(kAXHiddenAttribute, newHiddenState) } catch { print("Error toggling hidden state of application") return } - + if !newHiddenState { NSRunningApplication(processIdentifier: windowInfo.pid)?.activate() focusOnSpecificWindow(windowInfo: windowInfo) } updateWindowDateTime(windowInfo) } - + static func focusOnSpecificWindow(windowInfo: WindowInfo) { guard let windows = try? windowInfo.appAxElement.windows() else { print("Failed to get windows for the application") return } - + for window in windows { if let title = try? window.title(), isFuzzyMatch(windowTitle: windowInfo.windowName ?? "", axTitleString: title) { try? window.performAction(kAXRaiseAction) @@ -261,10 +261,10 @@ final class WindowUtil { return } } - + print("Failed to find and focus on the specific window") } - + static func toggleFullScreen(windowInfo: WindowInfo) { if let isCurrentlyInFullScreen = try? windowInfo.axElement.isFullscreen() { do { @@ -276,7 +276,7 @@ final class WindowUtil { print("Failed to determine current full screen state") } } - + static func bringWindowToFront(windowInfo: WindowInfo) { do { try windowInfo.axElement.performAction(kAXRaiseAction) @@ -289,16 +289,16 @@ final class WindowUtil { } else { throw NSError(domain: "ApplicationNotFound", code: 2, userInfo: [NSLocalizedDescriptionKey: "No running application found with PID \(windowInfo.pid)"]) } - + updateWindowDateTime(windowInfo) } catch { if NSRunningApplication(processIdentifier: windowInfo.pid)?.activate(options: [.activateAllWindows]) != true || (try? windowInfo.axElement.setAttribute(kAXFrontmostAttribute, true)) == nil { print("Failed to bring window to front with fallback attempts.") removeWindowFromDesktopSpaceCache(with: windowInfo.id, in: windowInfo.bundleID) - } + } } } - + static func updateWindowDateTime(_ windowInfo: WindowInfo) { desktopSpaceWindowCacheManager.updateCache(bundleId: windowInfo.bundleID) { windowSet in if let index = windowSet.firstIndex(where: { $0.id == windowInfo.id }) { @@ -309,7 +309,7 @@ final class WindowUtil { } } } - + static func updateWindowDateTime(with bundleID: String, pid: pid_t) { desktopSpaceWindowCacheManager.updateCache(bundleId: bundleID) { windowSet in @@ -330,13 +330,13 @@ final class WindowUtil { } } } - + static func closeWindow(windowInfo: WindowInfo) { guard windowInfo.closeButton != nil else { print("Error: closeButton is nil.") return } - + do { try windowInfo.closeButton?.performAction(kAXPressAction) removeWindowFromDesktopSpaceCache(with: windowInfo.id, in: windowInfo.bundleID) @@ -345,14 +345,14 @@ final class WindowUtil { return } } - + static func quitApp(windowInfo: WindowInfo, force: Bool) { guard let app = NSRunningApplication(processIdentifier: windowInfo.pid) else { print("No running application associated with PID \(windowInfo.pid)") NSSound.beep() return } - + removeWindowFromDesktopSpaceCache(with: windowInfo.bundleID, removeAll: true) if force { app.forceTerminate() @@ -360,105 +360,107 @@ final class WindowUtil { app.terminate() } } - + // MARK: - Active Window Handling - + static func activeWindows(for applicationName: String) async throws -> [WindowInfo] { let content = try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: true) - + let group = LimitedTaskGroup(maxConcurrentTasks: 4) var foundApp: SCRunningApplication? var nonLocalName: String? var potentialMatches: [SCRunningApplication] = [] - + for window in content.windows { if let app = window.owningApplication, - let tempNonLocalName = getNonLocalizedAppName(forBundleIdentifier: app.bundleIdentifier) { - + let tempNonLocalName = getNonLocalizedAppName(forBundleIdentifier: app.bundleIdentifier) + { desktopSpaceWindowCacheManager.updateAppNameBundleIdTracker(app: app, nonLocalName: tempNonLocalName) - + if applicationName.contains(app.applicationName) || app.applicationName.contains(applicationName) { potentialMatches.append(app) } - + if applicationName.isEmpty || (app.applicationName == applicationName) || (tempNonLocalName == applicationName) { await group.addTask { - return try await fetchWindowInfo(window: window, applicationName: applicationName) + try await fetchWindowInfo(window: window, applicationName: applicationName) } foundApp = app nonLocalName = tempNonLocalName } } } - + if foundApp == nil, let bestGuessApp = potentialMatches.first { foundApp = bestGuessApp - + if let bundleId = foundApp?.bundleIdentifier, - let tempNonLocalName = getNonLocalizedAppName(forBundleIdentifier: bundleId) { + let tempNonLocalName = getNonLocalizedAppName(forBundleIdentifier: bundleId) + { desktopSpaceWindowCacheManager.updateAppNameBundleIdTracker(app: bestGuessApp, nonLocalName: tempNonLocalName) nonLocalName = tempNonLocalName } - + for window in content.windows { if let app = window.owningApplication, app == bestGuessApp { await group.addTask { - return try await fetchWindowInfo(window: window, applicationName: applicationName) + try await fetchWindowInfo(window: window, applicationName: applicationName) } } } } - + let results = try await group.waitForAll() let activeWindows = results.compactMap { $0 }.filter { !$0.appName.isEmpty && !$0.bundleID.isEmpty } - + if applicationName.isEmpty { return desktopSpaceWindowCacheManager.getAllWindows() } - + if let nonLocalName { let bundleId = desktopSpaceWindowCacheManager.findBundleID(for: nonLocalName) ?? foundApp?.bundleIdentifier if let bundleId { return Array(desktopSpaceWindowCacheManager.readCache(bundleId: bundleId)) } } - + // Fallback to findAllWindowsInDesktopCacheForApplication if no SCRunningApplication is found and applicationName isn't empty - if foundApp == nil && !applicationName.isEmpty { + if foundApp == nil, !applicationName.isEmpty { if let cachedWindows = findAllWindowsInDesktopCacheForApplication(for: applicationName) { return cachedWindows } } - + return activeWindows } - - private static func fetchWindowInfo(window: SCWindow, applicationName: String) async throws -> WindowInfo? { + + private static func fetchWindowInfo(window: SCWindow, applicationName _: String) async throws -> WindowInfo? { let windowID = window.windowID - + guard let owningApplication = window.owningApplication, window.isOnScreen, window.windowLayer == 0, window.frame.size.width >= 0, window.frame.size.height >= 0, !filteredBundleIdentifiers.contains(owningApplication.bundleIdentifier), - !(window.frame.size.width < 100 || window.frame.size.height < 100) || window.title?.isEmpty == false else { + !(window.frame.size.width < 100 || window.frame.size.height < 100) || window.title?.isEmpty == false + else { return nil } - + let pid = owningApplication.processID let appElement = AXUIElementCreateApplication(pid) - + guard let axWindows = try? appElement.windows(), !axWindows.isEmpty else { return nil } - + guard let windowRef = findWindow(matchingWindow: window, in: axWindows) else { return nil } - + let closeButton = try? windowRef.closeButton() - + var windowInfo = WindowInfo(id: windowID, window: window, appName: owningApplication.applicationName, @@ -471,9 +473,8 @@ final class WindowUtil { closeButton: closeButton, isMinimized: false, isHidden: false, - lastUsed: Date() - ) - + lastUsed: Date()) + do { windowInfo.image = try await captureWindowImage(window: window) updateDesktopSpaceWindowCache(with: windowInfo) @@ -483,7 +484,7 @@ final class WindowUtil { return nil } } - + static func updateDesktopSpaceWindowCache(with windowInfo: WindowInfo) { desktopSpaceWindowCacheManager.updateCache(bundleId: windowInfo.bundleID) { windowSet in if let matchingWindow = windowSet.first(where: { $0.id == windowInfo.id }) { @@ -494,21 +495,20 @@ final class WindowUtil { matchingWindowCopy.isMinimized = windowInfo.isMinimized windowSet.remove(matchingWindow) windowSet.insert(matchingWindowCopy) - } - else { + } else { windowSet.insert(windowInfo) } } } - + static func findWindowInDesktopSpaceCache(for windowID: CGWindowID, in bundleID: String) -> WindowInfo? { - return desktopSpaceWindowCacheManager.readCache(bundleId: bundleID).first { $0.id == windowID } + desktopSpaceWindowCacheManager.readCache(bundleId: bundleID).first { $0.id == windowID } } - + static func removeWindowFromDesktopSpaceCache(with id: CGWindowID, in bundleID: String) { desktopSpaceWindowCacheManager.removeFromCache(bundleId: bundleID, windowId: id) } - + static func removeWindowFromDesktopSpaceCache(with bundleID: String, removeAll: Bool) { if removeAll { desktopSpaceWindowCacheManager.writeCache(bundleId: bundleID, windowSet: []) @@ -527,7 +527,7 @@ final class WindowUtil { } } } - + static func updateStatusOfWindowCache(pid: pid_t, bundleID: String, isParentAppHidden: Bool) { let appElement = AXUIElementCreateApplication(pid) if let windows = try? appElement.windows() { @@ -546,7 +546,7 @@ final class WindowUtil { }) } } - + if isParentAppHidden { cachedWindows = Set(cachedWindows.map { windowInfo in var updatedWindow = windowInfo @@ -557,24 +557,24 @@ final class WindowUtil { } } } - + private static func getNonLocalizedAppName(forBundleIdentifier bundleIdentifier: String) -> String? { guard let bundleURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) else { return nil } - + let bundle = Bundle(url: bundleURL) return bundle?.object(forInfoDictionaryKey: "CFBundleName") as? String } - + static func findAllWindowsInDesktopCacheForApplication(for applicationName: String) -> [WindowInfo]? { let bundleID = desktopSpaceWindowCacheManager.findBundleID(for: applicationName) - - if let bundleID = bundleID { + + if let bundleID { let windowSet = desktopSpaceWindowCacheManager.readCache(bundleId: bundleID) return windowSet.isEmpty ? nil : Array(windowSet).sorted(by: { $0.lastUsed > $1.lastUsed }) } - + return nil } } diff --git a/DockDoor/Views/FirstTimeView.swift b/DockDoor/Views/FirstTimeView.swift index 712ffd3f..84f6f7cf 100644 --- a/DockDoor/Views/FirstTimeView.swift +++ b/DockDoor/Views/FirstTimeView.swift @@ -9,7 +9,7 @@ import SwiftUI struct FirstTimeView: View { @State private var showPermissions = false - + var body: some View { HStack { VStack(spacing: 20) { @@ -18,36 +18,36 @@ struct FirstTimeView: View { .aspectRatio(contentMode: .fit) .frame(width: 100, height: 100) .foregroundColor(.blue) - + Text("Welcome to DockDoor!") .font(.largeTitle) .fontWeight(.bold) - + Text("Enhance your dock experience!") .font(.subheadline) .foregroundColor(.secondary) - + Button("Get Started") { openPermissionsWindow() } .buttonStyle(LinkButtonStyle()) } .padding() - + Divider() - + VStack(alignment: .leading, spacing: 20) { Text("Why we need permissions") .font(.title2) .fontWeight(.bold) - + VStack(alignment: .leading, spacing: 10) { Text("Accessibility:") .font(.headline) Text("• To detect when you hover over the dock") Text("• Enables real-time interaction with dock items") } - + VStack(alignment: .leading, spacing: 10) { Text("Screen Capturing:") .font(.headline) @@ -59,18 +59,19 @@ struct FirstTimeView: View { } .dockStyle(cornerRadius: 0) } - + private func openPermissionsWindow() { let contentView = PermissionsSettingsView() - + // Create the hosting controller with the PermView let hostingController = NSHostingController(rootView: contentView) - + // Create the settings window let permissionsWindow = NSWindow( contentRect: NSRect(origin: .zero, size: NSSize(width: 200, height: 200)), styleMask: [.titled, .closable, .resizable], - backing: .buffered, defer: false) + backing: .buffered, defer: false + ) permissionsWindow.center() permissionsWindow.setFrameAutosaveName("DockDoor Permissions") permissionsWindow.contentView = hostingController.view diff --git a/DockDoor/Views/Hover Window/FullSizePreviewView.swift b/DockDoor/Views/Hover Window/FullSizePreviewView.swift index ba15184f..f11fa6ff 100644 --- a/DockDoor/Views/Hover Window/FullSizePreviewView.swift +++ b/DockDoor/Views/Hover Window/FullSizePreviewView.swift @@ -5,15 +5,15 @@ // Created by Ethan Bills on 7/11/24. // -import SwiftUI import Defaults +import SwiftUI struct FullSizePreviewView: View { let windowInfo: WindowInfo let maxSize: CGSize - + @Default(.uniformCardRadius) var uniformCardRadius - + var body: some View { VStack(alignment: .center) { Group { diff --git a/DockDoor/Views/Hover Window/SharedPreviewWindowCoordinator.swift b/DockDoor/Views/Hover Window/SharedPreviewWindowCoordinator.swift index 48bb41b4..995b8d34 100644 --- a/DockDoor/Views/Hover Window/SharedPreviewWindowCoordinator.swift +++ b/DockDoor/Views/Hover Window/SharedPreviewWindowCoordinator.swift @@ -5,37 +5,37 @@ // Created by Ethan Bills on 6/5/24. // -import SwiftUI import Defaults import FluidGradient +import SwiftUI @Observable class ScreenCenteredFloatingWindow { static let shared = ScreenCenteredFloatingWindow() - + var currIndex: Int = 0 var windowSwitcherActive: Bool = false var fullWindowPreviewActive: Bool = false - + enum WindowState { case windowSwitcher case fullWindowPreview case both } - + func setShowing(_ state: WindowState? = .both, toState: Bool) { switch state { case .windowSwitcher: - self.windowSwitcherActive = toState + windowSwitcherActive = toState case .fullWindowPreview: - self.fullWindowPreviewActive = toState + fullWindowPreviewActive = toState case .both: - self.windowSwitcherActive = toState - self.fullWindowPreviewActive = toState + windowSwitcherActive = toState + fullWindowPreviewActive = toState case .none: return } } - + func setIndex(to: Int) { withAnimation(.easeInOut) { self.currIndex = to @@ -45,26 +45,26 @@ import FluidGradient final class SharedPreviewWindowCoordinator: NSWindow { static let shared = SharedPreviewWindowCoordinator() - + private var appName: String = "" private var windows: [WindowInfo] = [] private var onWindowTap: (() -> Void)? private var hostingView: NSHostingView? private var fullPreviewWindow: NSWindow? - + var windowSize: CGSize = getWindowSize() - + private var previousHoverWindowOrigin: CGPoint? - + private let debounceDelay: TimeInterval = 0.1 private var debounceWorkItem: DispatchWorkItem? private var lastShowTime: Date? - + private init() { super.init(contentRect: .zero, styleMask: .borderless, backing: .buffered, defer: false) setupWindow() } - + // Setup window properties private func setupWindow() { level = .floating @@ -72,17 +72,17 @@ final class SharedPreviewWindowCoordinator: NSWindow { collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] backgroundColor = .clear hasShadow = false - + let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways] - let trackingArea = NSTrackingArea(rect: self.frame, options: options, owner: self, userInfo: nil) + let trackingArea = NSTrackingArea(rect: frame, options: options, owner: self, userInfo: nil) contentView?.addTrackingArea(trackingArea) } - + // Hide the window and reset its state func hideWindow() { DispatchQueue.main.async { [weak self] in - guard let self = self, self.isVisible else { return } - + guard let self, self.isVisible else { return } + self.hideFullPreviewWindow() self.contentView = nil self.hostingView = nil @@ -93,24 +93,25 @@ final class SharedPreviewWindowCoordinator: NSWindow { self.orderOut(nil) } } - + // Update the content view size and position private func updateContentViewSizeAndPosition(mouseLocation: CGPoint? = nil, mouseScreen: NSScreen, animated: Bool, centerOnScreen: Bool = false, - centeredHoverWindowState: ScreenCenteredFloatingWindow.WindowState? = nil) { + centeredHoverWindowState: ScreenCenteredFloatingWindow.WindowState? = nil) + { guard hostingView != nil else { return } - + ScreenCenteredFloatingWindow.shared.setShowing(centeredHoverWindowState, toState: centerOnScreen) // Reset the hosting view let hoverView = WindowPreviewHoverContainer(appName: appName, windows: windows, onWindowTap: onWindowTap, - dockPosition: DockUtils.shared.getDockPosition(), bestGuessMonitor: mouseScreen) + dockPosition: DockUtils.shared.getDockPosition(), bestGuessMonitor: mouseScreen) let newHostingView = NSHostingView(rootView: hoverView) - self.contentView = newHostingView - self.hostingView = newHostingView + contentView = newHostingView + hostingView = newHostingView let newHoverWindowSize = newHostingView.fittingSize - + let position = centerOnScreen ? centerWindowOnScreen(size: newHoverWindowSize, screen: mouseScreen) : calculateWindowPosition(mouseLocation: mouseLocation, windowSize: newHoverWindowSize, screen: mouseScreen) @@ -130,80 +131,79 @@ final class SharedPreviewWindowCoordinator: NSWindow { fullPreviewWindow?.backgroundColor = .clear fullPreviewWindow?.hasShadow = true } - + let padding: CGFloat = 40 let maxSize = CGSize( width: screen.visibleFrame.width - padding * 2, height: screen.visibleFrame.height - padding * 2 ) - + let previewView = FullSizePreviewView(windowInfo: windowInfo, maxSize: maxSize) let hostingView = NSHostingView(rootView: previewView) fullPreviewWindow?.contentView = hostingView - + let centerPoint = centerWindowOnScreen(size: maxSize, screen: screen) fullPreviewWindow?.setFrame(CGRect(origin: centerPoint, size: maxSize), display: true) fullPreviewWindow?.makeKeyAndOrderFront(nil) } - + func hideFullPreviewWindow() { fullPreviewWindow?.orderOut(nil) fullPreviewWindow = nil } - + // Center window on screen private func centerWindowOnScreen(size: CGSize, screen: NSScreen) -> CGPoint { - return CGPoint( + CGPoint( x: screen.frame.midX - (size.width / 2), y: screen.frame.midY - (size.height / 2) ) } - + // Calculate window position based on the given dock icon frame and dock position private func calculateWindowPosition(mouseLocation: CGPoint?, windowSize: CGSize, screen: NSScreen) -> CGPoint { - guard let mouseLocation = mouseLocation else { return .zero } - + guard let mouseLocation else { return .zero } + let dockIconFrame = DockObserver.shared.getDockIconFrameAtLocation(mouseLocation) ?? .zero - + var xPosition = dockIconFrame.isEmpty ? mouseLocation.x : dockIconFrame.midX var yPosition = dockIconFrame.isEmpty ? mouseLocation.y : dockIconFrame.midY - + let screenFrame = screen.frame let dockPosition = DockUtils.shared.getDockPosition() let dockHeight = DockUtils.shared.calculateDockHeight(screen) - + // Adjust position based on dock position - switch dockPosition { - case .bottom: - yPosition = screenFrame.minY + dockHeight - xPosition -= (windowSize.width / 2) - case .left: - xPosition = screenFrame.minX + dockHeight - yPosition = screenFrame.height - yPosition - (windowSize.height / 2) - case .right: - xPosition = screenFrame.maxX - dockHeight - windowSize.width - yPosition = screenFrame.height - yPosition - (windowSize.height / 2) - default: - xPosition -= (windowSize.width / 2) - yPosition -= (windowSize.height / 2) - } - + switch dockPosition { + case .bottom: + yPosition = screenFrame.minY + dockHeight + xPosition -= (windowSize.width / 2) + case .left: + xPosition = screenFrame.minX + dockHeight + yPosition = screenFrame.height - yPosition - (windowSize.height / 2) + case .right: + xPosition = screenFrame.maxX - dockHeight - windowSize.width + yPosition = screenFrame.height - yPosition - (windowSize.height / 2) + default: + xPosition -= (windowSize.width / 2) + yPosition -= (windowSize.height / 2) + } + // Ensure window stays within screen bounds xPosition = max(screenFrame.minX, min(xPosition, screenFrame.maxX - windowSize.width)) + (dockPosition != .bottom ? Defaults[.bufferFromDock] : 0) yPosition = max(screenFrame.minY, min(yPosition, screenFrame.maxY - windowSize.height)) + (dockPosition == .bottom ? Defaults[.bufferFromDock] : 0) - + return CGPoint(x: xPosition, y: yPosition) } - - + // Apply window frame with optional animation private func applyWindowFrame(_ frame: CGRect, animated: Bool) { let shouldAnimate = animated && frame != self.frame - + if shouldAnimate { let distanceThreshold: CGFloat = 1800 let distance = previousHoverWindowOrigin.map { frame.origin.distance(to: $0) } ?? distanceThreshold + 1 - + if distance > distanceThreshold || !Defaults[.showAnimations] { setFrame(frame, display: true) } else { @@ -217,23 +217,24 @@ final class SharedPreviewWindowCoordinator: NSWindow { setFrame(frame, display: true) } } - + // Show window with debounce logic func showWindow(appName: String, windows: [WindowInfo], mouseLocation: CGPoint? = nil, mouseScreen: NSScreen? = nil, overrideDelay: Bool = false, centeredHoverWindowState: ScreenCenteredFloatingWindow.WindowState? = nil, - onWindowTap: (() -> Void)? = nil) { + onWindowTap: (() -> Void)? = nil) + { let now = Date() let delay = overrideDelay ? 0.0 : Defaults[.hoverWindowOpenDelay] - + debounceWorkItem?.cancel() - - let isHoverWindowShowing = self.isVisible - - if let lastShowTime = lastShowTime, now.timeIntervalSince(lastShowTime) < debounceDelay { + + let isHoverWindowShowing = isVisible + + if let lastShowTime, now.timeIntervalSince(lastShowTime) < debounceDelay { let workItem = DispatchWorkItem { [weak self] in self?.performShowWindow(appName: appName, windows: windows, mouseLocation: mouseLocation, mouseScreen: mouseScreen, centeredHoverWindowState: centeredHoverWindowState, onWindowTap: onWindowTap) } - + debounceWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + debounceDelay, execute: workItem) } else { @@ -243,28 +244,29 @@ final class SharedPreviewWindowCoordinator: NSWindow { let workItem = DispatchWorkItem { [weak self] in self?.performShowWindow(appName: appName, windows: windows, mouseLocation: mouseLocation, mouseScreen: mouseScreen, centeredHoverWindowState: centeredHoverWindowState, onWindowTap: onWindowTap) } - + debounceWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) } } - + lastShowTime = now } - + // Perform the actual window showing private func performShowWindow(appName: String, windows: [WindowInfo], mouseLocation: CGPoint?, mouseScreen: NSScreen?, centeredHoverWindowState: ScreenCenteredFloatingWindow.WindowState? = nil, - onWindowTap: (() -> Void)?) { + onWindowTap: (() -> Void)?) + { let shouldCenterOnScreen = centeredHoverWindowState != .none - + guard !windows.isEmpty else { return } - + DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - + guard let self else { return } + let screen = mouseScreen ?? NSScreen.main! - + hideFullPreviewWindow() // clean up any lingering fullscreen previews before presenting a new one // If in full window preview mode, show the full preview window and return early @@ -274,40 +276,40 @@ final class SharedPreviewWindowCoordinator: NSWindow { self.appName = appName self.windows = windows self.onWindowTap = onWindowTap - + self.updateHostingView(appName: appName, windows: windows, onWindowTap: onWindowTap, screen: screen) - + self.updateContentViewSizeAndPosition(mouseLocation: mouseLocation, mouseScreen: screen, animated: true, centerOnScreen: shouldCenterOnScreen, centeredHoverWindowState: centeredHoverWindowState) } - + self.makeKeyAndOrderFront(nil) } } - + // Update or create the hosting view private func updateHostingView(appName: String, windows: [WindowInfo], onWindowTap: (() -> Void)?, screen: NSScreen) { let hoverView = WindowPreviewHoverContainer(appName: appName, windows: windows, onWindowTap: onWindowTap, dockPosition: DockUtils.shared.getDockPosition(), bestGuessMonitor: screen) - - if let existingHostingView = self.hostingView { + + if let existingHostingView = hostingView { existingHostingView.rootView = hoverView } else { let newHostingView = NSHostingView(rootView: hoverView) - self.contentView = newHostingView - self.hostingView = newHostingView + contentView = newHostingView + hostingView = newHostingView } } - + // Cycle through windows func cycleWindows(goBackwards: Bool) { guard !windows.isEmpty else { return } - + let currentIndex = ScreenCenteredFloatingWindow.shared.currIndex let newIndex = (currentIndex + (goBackwards ? -1 : 1) + windows.count) % windows.count ScreenCenteredFloatingWindow.shared.setIndex(to: newIndex) } - + // Select and bring to front the current window func selectAndBringToFrontCurrentWindow() { guard !windows.isEmpty else { return } diff --git a/DockDoor/Views/Hover Window/Traffic Light Buttons.swift b/DockDoor/Views/Hover Window/Traffic Light Buttons.swift index 0117b8cb..12c3723f 100644 --- a/DockDoor/Views/Hover Window/Traffic Light Buttons.swift +++ b/DockDoor/Views/Hover Window/Traffic Light Buttons.swift @@ -12,9 +12,9 @@ struct TrafficLightButtons: View { let displayMode: TrafficLightButtonsVisibility let hoveringOverParentWindow: Bool let onAction: () -> Void - + @State private var isHovering = false - + var body: some View { HStack(spacing: 6) { buttonFor(action: .quit, symbol: "power", color: Color(hex: "290133"), fillColor: .purple) @@ -31,7 +31,7 @@ struct TrafficLightButtons: View { } } } - + private var opacity: Double { switch displayMode { case .dimmedOnPreviewHover: @@ -44,7 +44,7 @@ struct TrafficLightButtons: View { return 0 } } - + private func buttonFor(action: WindowAction, symbol: String, color: Color, fillColor: Color) -> some View { Button(action: { performAction(action) @@ -61,7 +61,7 @@ struct TrafficLightButtons: View { .buttonStyle(.plain) .font(.system(size: 13)) } - + private func performAction(_ action: WindowAction) { switch action { case .quit: @@ -74,7 +74,7 @@ struct TrafficLightButtons: View { WindowUtil.toggleFullScreen(windowInfo: windowInfo) } } - + private enum WindowAction { case quit, close, minimize, toggleFullScreen } diff --git a/DockDoor/Views/Hover Window/WindowPreview.swift b/DockDoor/Views/Hover Window/WindowPreview.swift index 51ab804e..8bf1e39e 100644 --- a/DockDoor/Views/Hover Window/WindowPreview.swift +++ b/DockDoor/Views/Hover Window/WindowPreview.swift @@ -5,8 +5,8 @@ // Created by Ethan Bills on 7/4/24. // -import SwiftUI import Defaults +import SwiftUI struct WindowPreview: View { let windowInfo: WindowInfo @@ -23,7 +23,7 @@ struct WindowPreview: View { @Default(.windowTitleVisibility) var windowTitleVisibility @Default(.trafficLightButtonsVisibility) var trafficLightButtonsVisibility @Default(.trafficLightButtonsPosition) var trafficLightButtonsPosition - + // preview popup action handlers @Default(.tapEquivalentInterval) var tapEquivalentInterval @Default(.previewHoverAction) var previewHoverAction @@ -33,7 +33,7 @@ struct WindowPreview: View { @State private var fullPreviewTimer: Timer? private var calculatedMaxDimensions: CGSize { - CGSize(width: self.bestGuessMonitor.frame.width * 0.75, height: self.bestGuessMonitor.frame.height * 0.75) + CGSize(width: bestGuessMonitor.frame.width * 0.75, height: bestGuessMonitor.frame.height * 0.75) } var calculatedSize: CGSize { @@ -68,7 +68,7 @@ struct WindowPreview: View { .frame(width: calculatedSize.width, height: calculatedSize.height, alignment: .center) .frame(maxWidth: calculatedMaxDimensions.width, maxHeight: calculatedMaxDimensions.height) } - + var body: some View { let isHighlightedInWindowSwitcher = (index == ScreenCenteredFloatingWindow.shared.currIndex && ScreenCenteredFloatingWindow.shared.windowSwitcherActive) let selected = isHoveringOverDockPeekPreview || isHighlightedInWindowSwitcher @@ -96,7 +96,7 @@ struct WindowPreview: View { return .topLeading } }()) { - if showWindowTitle && (windowTitleDisplayCondition == .all || (windowTitleDisplayCondition == .windowSwitcherOnly && ScreenCenteredFloatingWindow.shared.windowSwitcherActive) || (windowTitleDisplayCondition == .dockPreviewsOnly && !ScreenCenteredFloatingWindow.shared.windowSwitcherActive)) { + if showWindowTitle, windowTitleDisplayCondition == .all || (windowTitleDisplayCondition == .windowSwitcherOnly && ScreenCenteredFloatingWindow.shared.windowSwitcherActive) || (windowTitleDisplayCondition == .dockPreviewsOnly && !ScreenCenteredFloatingWindow.shared.windowSwitcherActive) { windowTitleOverlay(selected: selected) } } @@ -124,7 +124,7 @@ struct WindowPreview: View { .contentShape(Rectangle()) .onHover { over in withAnimation(.snappy(duration: 0.175)) { - if (!ScreenCenteredFloatingWindow.shared.windowSwitcherActive) { + if !ScreenCenteredFloatingWindow.shared.windowSwitcherActive { isHoveringOverDockPeekPreview = over handleFullPreviewHover(isHovering: over, action: previewHoverAction) } else { @@ -138,12 +138,12 @@ struct WindowPreview: View { } private func handleFullPreviewHover(isHovering: Bool, action: PreviewHoverAction) { - if isHovering && !ScreenCenteredFloatingWindow.shared.windowSwitcherActive { + if isHovering, !ScreenCenteredFloatingWindow.shared.windowSwitcherActive { switch action { case .none: // Do nothing for .none break - + case .tap: // If the interval is 0, immediately trigger the tap action if tapEquivalentInterval == 0 { @@ -156,7 +156,7 @@ struct WindowPreview: View { } } } - + case .previewFullSize: // If the interval is 0, show the full window preview immediately if tapEquivalentInterval == 0 { @@ -190,11 +190,11 @@ struct WindowPreview: View { fullPreviewTimer = nil } } - + private func handleWindowTap() { - if (windowInfo.isMinimized) { + if windowInfo.isMinimized { WindowUtil.toggleMinimize(windowInfo: windowInfo) - } else if (windowInfo.isHidden) { + } else if windowInfo.isHidden { WindowUtil.toggleHidden(windowInfo: windowInfo) } else { WindowUtil.bringWindowToFront(windowInfo: windowInfo) @@ -204,7 +204,7 @@ struct WindowPreview: View { @ViewBuilder private func windowTitleOverlay(selected: Bool) -> some View { - if (windowTitleVisibility == .alwaysVisible || selected), let windowTitle = windowInfo.window?.title, !windowTitle.isEmpty, (windowTitle != windowInfo.appName || ScreenCenteredFloatingWindow.shared.windowSwitcherActive) { + if windowTitleVisibility == .alwaysVisible || selected, let windowTitle = windowInfo.window?.title, !windowTitle.isEmpty, windowTitle != windowInfo.appName || ScreenCenteredFloatingWindow.shared.windowSwitcherActive { let maxLabelWidth = calculatedSize.width - 50 let stringMeasurementWidth = measureString(windowTitle, fontSize: 12).width + 5 let width = maxLabelWidth > stringMeasurementWidth ? stringMeasurementWidth : maxLabelWidth diff --git a/DockDoor/Views/Hover Window/WindowPreviewHoverContainer.swift b/DockDoor/Views/Hover Window/WindowPreviewHoverContainer.swift index 06224a35..4394aba5 100644 --- a/DockDoor/Views/Hover Window/WindowPreviewHoverContainer.swift +++ b/DockDoor/Views/Hover Window/WindowPreviewHoverContainer.swift @@ -1,12 +1,12 @@ // -// HoverView.swift +// WindowPreviewHoverContainer.swift // DockDoor // // Created by Ethan Bills on 7/11/24. // -import SwiftUI import Defaults +import SwiftUI struct WindowPreviewHoverContainer: View { let appName: String @@ -14,27 +14,27 @@ struct WindowPreviewHoverContainer: View { let onWindowTap: (() -> Void)? let dockPosition: DockPosition let bestGuessMonitor: NSScreen - + @Default(.uniformCardRadius) var uniformCardRadius @Default(.showAppName) var showAppName @Default(.appNameStyle) var appNameStyle @Default(.windowTitlePosition) var windowTitlePosition - + @State private var showWindows: Bool = false @State private var hasAppeared: Bool = false @State private var appIcon: NSImage? = nil - + var maxWindowDimension: CGPoint { let thickness = SharedPreviewWindowCoordinator.shared.windowSize.height var maxWidth: CGFloat = 300 var maxHeight: CGFloat = 300 - + for window in windows { if let cgImage = window.image { let cgSize = CGSize(width: cgImage.width, height: cgImage.height) let widthBasedOnHeight = (cgSize.width * thickness) / cgSize.height let heightBasedOnWidth = (cgSize.height * thickness) / cgSize.width - + if dockPosition == .bottom || ScreenCenteredFloatingWindow.shared.windowSwitcherActive { maxWidth = max(maxWidth, widthBasedOnHeight) maxHeight = thickness @@ -44,10 +44,10 @@ struct WindowPreviewHoverContainer: View { } } } - + return CGPoint(x: maxWidth, y: maxHeight) } - + var body: some View { let orientationIsHorizontal = dockPosition == .bottom || ScreenCenteredFloatingWindow.shared.windowSwitcherActive ZStack { @@ -58,14 +58,14 @@ struct WindowPreviewHoverContainer: View { WindowPreview(windowInfo: windows[index], onTap: onWindowTap, index: index, dockPosition: dockPosition, maxWindowDimension: maxWindowDimension, bestGuessMonitor: bestGuessMonitor, uniformCardRadius: uniformCardRadius) - .id("\(appName)-\(index)") + .id("\(appName)-\(index)") } } .padding(14) .onAppear { if !hasAppeared { hasAppeared.toggle() - self.runUIUpdates() + runUIUpdates() } } .onChange(of: ScreenCenteredFloatingWindow.shared.currIndex) { _, newIndex in @@ -73,8 +73,8 @@ struct WindowPreviewHoverContainer: View { scrollProxy.scrollTo("\(appName)-\(newIndex)", anchor: .center) } } - .onChange(of: self.windows) { _, _ in - self.runUIUpdates() + .onChange(of: windows) { _, _ in + runUIUpdates() } } .opacity(showWindows ? 1 : 0.8) @@ -85,18 +85,18 @@ struct WindowPreviewHoverContainer: View { .overlay(alignment: .topLeading) { hoverTitleBaseView(labelSize: measureString(appName, fontSize: 14)) } - .padding(.top, (!ScreenCenteredFloatingWindow.shared.windowSwitcherActive && appNameStyle == .popover && showAppName) ? 30 : 0) // Provide empty space above the window preview for the Popover title style when hovering over the Dock + .padding(.top, (!ScreenCenteredFloatingWindow.shared.windowSwitcherActive && appNameStyle == .popover && showAppName) ? 30 : 0) // Provide empty space above the window preview for the Popover title style when hovering over the Dock .padding(.all, 24) - .frame(maxWidth: self.bestGuessMonitor.visibleFrame.width, maxHeight: self.bestGuessMonitor.visibleFrame.height) + .frame(maxWidth: bestGuessMonitor.visibleFrame.width, maxHeight: bestGuessMonitor.visibleFrame.height) } - + @ViewBuilder private func hoverTitleBaseView(labelSize: CGSize) -> some View { - if !ScreenCenteredFloatingWindow.shared.windowSwitcherActive && showAppName { + if !ScreenCenteredFloatingWindow.shared.windowSwitcherActive, showAppName { switch appNameStyle { case .default: HStack(spacing: 2) { - if let appIcon = appIcon { + if let appIcon { Image(nsImage: appIcon) .resizable() .scaledToFit() @@ -111,7 +111,7 @@ struct WindowPreviewHoverContainer: View { .padding(EdgeInsets(top: -11.5, leading: 15, bottom: -1.5, trailing: 1.5)) case .embedded: HStack(spacing: 2) { - if let appIcon = appIcon { + if let appIcon { Image(nsImage: appIcon) .resizable() .scaledToFit() @@ -129,7 +129,7 @@ struct WindowPreviewHoverContainer: View { HStack { Spacer() HStack(spacing: 2) { - if let appIcon = appIcon { + if let appIcon { Image(nsImage: appIcon) .resizable() .scaledToFit() @@ -150,7 +150,7 @@ struct WindowPreviewHoverContainer: View { } } } - + @ViewBuilder private func hoverTitleLabelView(labelSize: CGSize) -> some View { switch appNameStyle { @@ -172,7 +172,7 @@ struct WindowPreviewHoverContainer: View { gradient: Gradient( colors: [ Color.white.opacity(1.0), - Color.white.opacity(0.35) + Color.white.opacity(0.35), ] ), startPoint: .top, @@ -182,30 +182,30 @@ struct WindowPreviewHoverContainer: View { ) .blur(radius: 5) } - .frame(width: labelSize.width + 30) + .frame(width: labelSize.width + 30) ) case .embedded, .popover: Text(appName) } } - + private func runUIUpdates() { - self.runAnimation() - self.loadAppIcon() + runAnimation() + loadAppIcon() } - + private func runAnimation() { - self.showWindows = false - + showWindows = false + withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) { showWindows = true } } - + private func loadAppIcon() { if let bundleID = windows.first?.bundleID, let icon = AppIconUtil.getIcon(bundleID: bundleID) { DispatchQueue.main.async { - self.appIcon = icon + appIcon = icon } } } diff --git a/DockDoor/Views/Settings/AppearanceSettingsView.swift b/DockDoor/Views/Settings/AppearanceSettingsView.swift index 2f18bc0d..4de08e17 100644 --- a/DockDoor/Views/Settings/AppearanceSettingsView.swift +++ b/DockDoor/Views/Settings/AppearanceSettingsView.swift @@ -5,9 +5,9 @@ // Created by ShlomoCode on 09/07/2024. // -import SwiftUI import Defaults import LaunchAtLogin +import SwiftUI struct AppearanceSettingsView: View { @Default(.showAnimations) var showAnimations @@ -20,17 +20,17 @@ struct AppearanceSettingsView: View { @Default(.windowTitlePosition) var windowTitlePosition @Default(.trafficLightButtonsVisibility) var trafficLightButtonsVisibility @Default(.trafficLightButtonsPosition) var trafficLightButtonsPosition - + var body: some View { VStack(alignment: .leading, spacing: 10) { Toggle(isOn: $showAnimations, label: { Text("Enable Hover Window Sliding Animation") }) - + Toggle(isOn: $uniformCardRadius, label: { Text("Use Uniform Image Preview Radius") }) - + Picker("Traffic Light Buttons Visibility", selection: $trafficLightButtonsVisibility) { ForEach(TrafficLightButtonsVisibility.allCases, id: \.self) { visibility in Text(visibility.localizedName) @@ -40,7 +40,7 @@ struct AppearanceSettingsView: View { .pickerStyle(MenuPickerStyle()) .scaledToFit() .layoutPriority(1) - + Picker("Traffic Light Buttons Position", selection: $trafficLightButtonsPosition) { ForEach(TrafficLightButtonsPosition.allCases, id: \.self) { position in Text(position.localizedName) @@ -63,13 +63,13 @@ struct AppearanceSettingsView: View { .pickerStyle(SegmentedPickerStyle()) .scaledToFit() .layoutPriority(1) - + Divider() - + Toggle(isOn: $showAppName) { Text("Show App Name in Dock Previews") } - + Picker(String(localized: "App Name Style"), selection: $appNameStyle) { ForEach(AppNameStyle.allCases, id: \.self) { style in Text(style.localizedName) @@ -80,13 +80,13 @@ struct AppearanceSettingsView: View { .scaledToFit() .layoutPriority(1) .disabled(!showAppName) - + Divider() - + Toggle(isOn: $showWindowTitle) { Text("Show Window Title in Previews") } - + Group { Picker("Show Window Title in", selection: $windowTitleDisplayCondition) { ForEach(WindowTitleDisplayCondition.allCases, id: \.self) { condtion in @@ -102,7 +102,7 @@ struct AppearanceSettingsView: View { } .pickerStyle(MenuPickerStyle()) .scaledToFit() - + Picker("Window Title Visibility", selection: $windowTitleVisibility) { ForEach(WindowTitleVisibility.allCases, id: \.self) { visibility in Text(visibility.localizedName) @@ -111,7 +111,7 @@ struct AppearanceSettingsView: View { } .scaledToFit() .pickerStyle(MenuPickerStyle()) - + Picker("Window Title Position", selection: $windowTitlePosition) { ForEach(WindowTitlePosition.allCases, id: \.self) { position in Text(position.localizedName) diff --git a/DockDoor/Views/Settings/MainSettingsView.swift b/DockDoor/Views/Settings/MainSettingsView.swift index e0765e81..ae5b8eef 100644 --- a/DockDoor/Views/Settings/MainSettingsView.swift +++ b/DockDoor/Views/Settings/MainSettingsView.swift @@ -5,9 +5,9 @@ // Created by Ethan Bills on 6/13/24. // -import SwiftUI import Defaults import LaunchAtLogin +import SwiftUI var decimalFormatter: NumberFormatter { let formatter = NumberFormatter() @@ -24,7 +24,7 @@ struct MainSettingsView: View { @Default(.previewHoverAction) var previewHoverAction @Default(.bufferFromDock) var bufferFromDock @Default(.windowPreviewImageScale) var windowPreviewImageScale - + var body: some View { VStack(alignment: .leading, spacing: 10) { Section { @@ -32,17 +32,17 @@ struct MainSettingsView: View { Text("Want to support development?") Link("Buy me a coffee here, thank you!", destination: URL(string: "https://www.buymeacoffee.com/keplercafe")!) } - + HStack { Text("Want to see the app in your language?") Link("Contribute translation here!", destination: URL(string: "https://crowdin.com/project/dockdoor/invite?h=895e3c085646d3c07fa36a97044668e02149115")!) } } - + Divider() - + LaunchAtLogin.Toggle(String(localized: "Launch DockDoor at login")) - + Toggle(isOn: $showMenuBarIcon, label: { Text("Show Menu Bar Icon") }) @@ -54,7 +54,7 @@ struct MainSettingsView: View { appDelegate.removeMenuBar() } } - + Button("Reset All Settings to Defaults") { showResetConfirmation() } @@ -62,23 +62,23 @@ struct MainSettingsView: View { let appDelegate = NSApplication.shared.delegate as! AppDelegate appDelegate.quitApp() } - + Divider() - + HStack { Text("Hover Window Open Delay") .layoutPriority(1) Spacer() - Slider(value: $hoverWindowOpenDelay, in: 0...2, step: 0.1) + Slider(value: $hoverWindowOpenDelay, in: 0 ... 2, step: 0.1) TextField("", value: $hoverWindowOpenDelay, formatter: decimalFormatter) .frame(width: 38) .textFieldStyle(RoundedBorderTextFieldStyle()) Text("seconds") } - - VStack(alignment: .leading){ + + VStack(alignment: .leading) { HStack { - Slider(value: $bufferFromDock, in: -200...200, step: 20) { + Slider(value: $bufferFromDock, in: -200 ... 200, step: 20) { Text("Window Buffer") } .buttonStyle(PlainButtonStyle()) @@ -91,30 +91,30 @@ struct MainSettingsView: View { .font(.footnote) .foregroundColor(.gray) } - + SizePickerView() - + HStack { Text("Window Image Cache Lifespan") .layoutPriority(1) Spacer() - Slider(value: $screenCaptureCacheLifespan, in: 0...60, step: 5) + Slider(value: $screenCaptureCacheLifespan, in: 0 ... 60, step: 5) TextField("", value: $screenCaptureCacheLifespan, formatter: NumberFormatter()) .frame(width: 38) .textFieldStyle(RoundedBorderTextFieldStyle()) Text("seconds") } - + HStack { Text("Window Image Resolution Scale (higher means lower resolution)") .layoutPriority(1) Spacer() - Slider(value: $windowPreviewImageScale, in: 1...4, step: 1) + Slider(value: $windowPreviewImageScale, in: 1 ... 4, step: 1) TextField("", value: $windowPreviewImageScale, formatter: NumberFormatter()) .frame(width: 38) .textFieldStyle(RoundedBorderTextFieldStyle()) } - + Picker("Preview Hover Action", selection: $previewHoverAction) { ForEach(PreviewHoverAction.allCases, id: \.self) { action in Text(action.localizedName).tag(action) @@ -122,11 +122,11 @@ struct MainSettingsView: View { } .pickerStyle(MenuPickerStyle()) .scaledToFit() - + HStack { Text("Preview Hover Delay") Spacer() - Slider(value: $tapEquivalentInterval, in: 0...2, step: 0.1) + Slider(value: $tapEquivalentInterval, in: 0 ... 2, step: 0.1) TextField("", value: $tapEquivalentInterval, formatter: decimalFormatter) .frame(width: 38) .textFieldStyle(RoundedBorderTextFieldStyle()) @@ -137,7 +137,7 @@ struct MainSettingsView: View { .padding(20) .frame(minWidth: 650) } - + private func showResetConfirmation() { MessageUtil.showMessage( title: String(localized: "Reset to Defaults"), @@ -157,21 +157,21 @@ struct MainSettingsView: View { struct SizePickerView: View { @Default(.sizingMultiplier) var sizingMultiplier @Default(.bufferFromDock) var bufferFromDock - + var body: some View { VStack(spacing: 20) { Picker("Window Size", selection: $sizingMultiplier) { - ForEach(2...10, id: \.self) { size in + ForEach(2 ... 10, id: \.self) { size in Text(getLabel(for: CGFloat(size))).tag(CGFloat(size)) } } .scaledToFit() - .onChange(of: sizingMultiplier) { _, newValue in + .onChange(of: sizingMultiplier) { _, _ in SharedPreviewWindowCoordinator.shared.windowSize = getWindowSize() } } } - + private func getLabel(for size: CGFloat) -> String { switch size { case 2: @@ -179,19 +179,19 @@ struct SizePickerView: View { case 3: return String(localized: "Default (Medium Large)", comment: "Window size option") case 4: - return String(localized:"Medium", comment: "Window size option") + return String(localized: "Medium", comment: "Window size option") case 5: - return String(localized:"Small", comment: "Window size option") + return String(localized: "Small", comment: "Window size option") case 6: - return String(localized:"Extra Small", comment: "Window size option") + return String(localized: "Extra Small", comment: "Window size option") case 7: - return String(localized:"Extra Extra Small", comment: "Window size option") + return String(localized: "Extra Extra Small", comment: "Window size option") case 8: - return String(localized:"What is this? A window for ANTS?", comment: "Window size option") + return String(localized: "What is this? A window for ANTS?", comment: "Window size option") case 9: - return String(localized:"Subatomic", comment: "Window size option") + return String(localized: "Subatomic", comment: "Window size option") case 10: - return String(localized:"Can you even see this?", comment: "Window size option") + return String(localized: "Can you even see this?", comment: "Window size option") default: return "Unknown Size" } diff --git a/DockDoor/Views/Settings/PermissionsSettingsView.swift b/DockDoor/Views/Settings/PermissionsSettingsView.swift index a70cb8c4..9d6f9486 100644 --- a/DockDoor/Views/Settings/PermissionsSettingsView.swift +++ b/DockDoor/Views/Settings/PermissionsSettingsView.swift @@ -5,9 +5,9 @@ // Created by Ethan Bills on 6/14/24. // -import SwiftUI -import Combine import AppKit +import Combine +import SwiftUI class PermissionsChecker: ObservableObject { @Published var accessibilityPermission: Bool = false @@ -53,7 +53,7 @@ class PermissionsChecker: ObservableObject { struct PermissionsSettingsView: View { @StateObject private var permissionsChecker = PermissionsChecker() - + var body: some View { VStack(alignment: .leading, spacing: 20) { HStack { @@ -61,11 +61,11 @@ struct PermissionsSettingsView: View { .foregroundColor(permissionsChecker.accessibilityPermission ? .green : .red) .scaleEffect(permissionsChecker.accessibilityPermission ? 1.2 : 1.0) .padding(10) - + Text("Accessibility Permissions") .font(.headline) } - + HStack { Image(systemName: permissionsChecker.screenRecordingPermission ? "checkmark.circle.fill" : "xmark.circle.fill") .foregroundColor(permissionsChecker.screenRecordingPermission ? .green : .red) @@ -75,7 +75,7 @@ struct PermissionsSettingsView: View { Text("Screen Recording Permissions") .font(.headline) } - + Button(action: openAccessibilityPreferences) { HStack { Image(systemName: "hand.raised.fill") @@ -83,7 +83,7 @@ struct PermissionsSettingsView: View { } } .buttonStyle(.bordered) - + Button(action: openScreenRecordingPreferences) { HStack { Image(systemName: "video.fill") @@ -91,7 +91,7 @@ struct PermissionsSettingsView: View { } } .buttonStyle(.bordered) - + Text("Please Restart the App to Apply Changes! :)") .font(.footnote) .foregroundColor(.secondary) @@ -99,18 +99,18 @@ struct PermissionsSettingsView: View { let appDelegate = NSApplication.shared.delegate as! AppDelegate appDelegate.restartApp() }) - .buttonStyle(.bordered) + .buttonStyle(.bordered) Spacer() } .padding([.top, .leading, .trailing], 20) .frame(minWidth: 650) } - + private func openAccessibilityPreferences() { SystemPreferencesHelper.openAccessibilityPreferences() } - + private func openScreenRecordingPreferences() { SystemPreferencesHelper.openScreenRecordingPreferences() } diff --git a/DockDoor/Views/Settings/UpdateSettingsView.swift b/DockDoor/Views/Settings/UpdateSettingsView.swift index 4f3662af..bfc3d851 100644 --- a/DockDoor/Views/Settings/UpdateSettingsView.swift +++ b/DockDoor/Views/Settings/UpdateSettingsView.swift @@ -5,8 +5,8 @@ // Created by Ethan Bills on 6/23/24. // -import SwiftUI import Sparkle +import SwiftUI final class UpdaterViewModel: ObservableObject { @Published var canCheckForUpdates = false @@ -14,33 +14,33 @@ final class UpdaterViewModel: ObservableObject { @Published var currentVersion: String @Published var isAutomaticChecksEnabled: Bool @Published var updateStatus: UpdateStatus = .noUpdates - + private let updater: SPUUpdater - + enum UpdateStatus { case noUpdates case checking case available(version: String) case error(String) } - + init(updater: SPUUpdater) { self.updater = updater - self.currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown" - self.isAutomaticChecksEnabled = updater.automaticallyChecksForUpdates - + currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown" + isAutomaticChecksEnabled = updater.automaticallyChecksForUpdates + updater.publisher(for: \.canCheckForUpdates) .assign(to: &$canCheckForUpdates) - + updater.publisher(for: \.lastUpdateCheckDate) .assign(to: &$lastUpdateCheckDate) } - + func checkForUpdates() { updateStatus = .checking updater.checkForUpdates() } - + func toggleAutomaticChecks() { isAutomaticChecksEnabled.toggle() updater.automaticallyChecksForUpdates = isAutomaticChecksEnabled @@ -49,15 +49,15 @@ final class UpdaterViewModel: ObservableObject { struct UpdateSettingsView: View { @StateObject private var viewModel: UpdaterViewModel - + init(updater: SPUUpdater) { _viewModel = StateObject(wrappedValue: UpdaterViewModel(updater: updater)) } - + var body: some View { VStack(alignment: .center) { updateStatusView.bold().padding(1) - + HStack(alignment: .center) { VStack(alignment: .center) { HStack(alignment: .center) { @@ -70,22 +70,21 @@ struct UpdateSettingsView: View { } } } - + Button(action: viewModel.checkForUpdates) { Label("Check for Updates", systemImage: "arrow.triangle.2.circlepath") } .disabled(!viewModel.canCheckForUpdates) - + Toggle("Automatically check for updates", isOn: $viewModel.isAutomaticChecksEnabled) .onChange(of: viewModel.isAutomaticChecksEnabled) { _, _ in viewModel.toggleAutomaticChecks() } - } .padding(10) .frame(width: 650) } - + private var updateStatusView: some View { Group { switch viewModel.updateStatus { @@ -95,20 +94,20 @@ struct UpdateSettingsView: View { case .checking: ProgressView() .scaleEffect(0.7) - case .available(let version): + case let .available(version): VStack { Label("Update available", systemImage: "arrow.down.circle.fill") .foregroundColor(.blue) Text("Version \(version)") .font(.caption) } - case .error(let message): + case let .error(message): Label(message, systemImage: "exclamationmark.triangle.fill") .foregroundColor(.red) } } } - + private let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .medium diff --git a/DockDoor/Views/Settings/WindowSwitcherSettingsView.swift b/DockDoor/Views/Settings/WindowSwitcherSettingsView.swift index 7485ca90..d83a3cae 100644 --- a/DockDoor/Views/Settings/WindowSwitcherSettingsView.swift +++ b/DockDoor/Views/Settings/WindowSwitcherSettingsView.swift @@ -5,10 +5,9 @@ // Created by Hasan Sultan on 6/25/24. // - -import SwiftUI -import Defaults import Carbon +import Defaults +import SwiftUI class KeybindModel: ObservableObject { @Published var modifierKey: Int @@ -16,10 +15,9 @@ class KeybindModel: ObservableObject { @Published var currentKeybind: UserKeyBind? init() { - self.modifierKey = Defaults[.UserKeybind].modifierFlags - self.currentKeybind = Defaults[.UserKeybind] + modifierKey = Defaults[.UserKeybind].modifierFlags + currentKeybind = Defaults[.UserKeybind] } - } struct WindowSwitcherSettingsView: View { @@ -29,8 +27,8 @@ struct WindowSwitcherSettingsView: View { VStack(alignment: .leading, spacing: 10) { Toggle(isOn: $enableWindowSwitcher, label: { Text("Enable Window Switcher") - }).onChange(of: enableWindowSwitcher){ - _, newValue in + }).onChange(of: enableWindowSwitcher) { + _, _ in askUserToRestartApplication() } // Default CMD + TAB implementation checkbox @@ -51,13 +49,13 @@ struct WindowSwitcherSettingsView: View { struct InitializationKeyPickerView: View { @ObservedObject var viewModel = KeybindModel() - + var body: some View { VStack(spacing: 20) { Text("Set Initialization Key and Keybind") .font(.headline) .padding(.top, 20) - + Picker("Initialization Key", selection: $viewModel.modifierKey) { Text("Control (⌃)").tag(Defaults[.Int64maskControl]) Text("Option (⌥)").tag(Defaults[.Int64maskAlternate]) @@ -66,17 +64,17 @@ struct InitializationKeyPickerView: View { .pickerStyle(SegmentedPickerStyle()) .padding(.horizontal) .scaledToFit() - + Text("Press any key combination after holding the initialization key to set the keybind.") .multilineTextAlignment(.center) .padding(.horizontal) - + Button(action: { viewModel.isRecording = true }) { Text(viewModel.isRecording ? "Press the key combination..." : "Start Recording Keybind") } .keyboardShortcut(.defaultAction) .padding(.bottom, 20) - + if let keybind = viewModel.currentKeybind { Text("Current Keybind: \(printCurrentKeybind(keybind))") .padding() @@ -95,7 +93,7 @@ struct InitializationKeyPickerView: View { .frame(maxWidth: .infinity, alignment: .leading) .padding() } - + func printCurrentKeybind(_ shortcut: UserKeyBind) -> String { var parts: [String] = [] parts.append(modifierConverter.toString(shortcut.modifierFlags)) @@ -108,28 +106,27 @@ struct ShortcutCaptureView: NSViewRepresentable { @Binding var currentKeybind: UserKeyBind? @Binding var isRecording: Bool @Binding var modifierKey: Int - - func makeNSView(context: Context) -> NSView { + + func makeNSView(context _: Context) -> NSView { let view = NSView() NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in - guard self.isRecording else { + guard isRecording else { return event } - self.isRecording = false - if event.keyCode == 48 && modifierKey == Defaults[.Int64maskCommand] { // User has chosen the default Mac OS window switcher keybind + isRecording = false + if event.keyCode == 48, modifierKey == Defaults[.Int64maskCommand] { // User has chosen the default Mac OS window switcher keybind // Set the default CMDTAB Defaults[.defaultCMDTABKeybind] = true Defaults[.UserKeybind] = UserKeyBind(keyCode: 48, modifierFlags: Defaults[.Int64maskControl]) - self.currentKeybind = Defaults[.UserKeybind] + currentKeybind = Defaults[.UserKeybind] return event } Defaults[.UserKeybind] = UserKeyBind(keyCode: event.keyCode, modifierFlags: modifierKey) - self.currentKeybind = Defaults[.UserKeybind] + currentKeybind = Defaults[.UserKeybind] return nil } return view } - - - func updateNSView(_ nsView: NSView, context: Context) {} + + func updateNSView(_: NSView, context _: Context) {} } diff --git a/DockDoor/Views/Settings/settings.swift b/DockDoor/Views/Settings/settings.swift index 2465d4c6..7b688eb2 100644 --- a/DockDoor/Views/Settings/settings.swift +++ b/DockDoor/Views/Settings/settings.swift @@ -20,7 +20,7 @@ extension Settings.PaneIdentifier { let GeneralSettingsViewController: () -> SettingsPane = { let paneView = Settings.Pane( identifier: .general, - title: String(localized:"General", comment: "Settings tab title"), + title: String(localized: "General", comment: "Settings tab title"), toolbarIcon: NSImage(systemSymbolName: "gearshape.fill", accessibilityDescription: String(localized: "General settings"))! ) { MainSettingsView() @@ -32,7 +32,7 @@ let GeneralSettingsViewController: () -> SettingsPane = { let AppearanceSettingsViewController: () -> SettingsPane = { let paneView = Settings.Pane( identifier: .appearance, - title: String(localized:"Appearance", comment: "Settings Tab"), + title: String(localized: "Appearance", comment: "Settings Tab"), toolbarIcon: NSImage(systemSymbolName: "wand.and.stars.inverse", accessibilityDescription: String(localized: "Appearance settings"))! ) { AppearanceSettingsView() @@ -56,7 +56,7 @@ let WindowSwitcherSettingsViewController: () -> SettingsPane = { let PermissionsSettingsViewController: () -> SettingsPane = { let paneView = Settings.Pane( identifier: .permissions, - title: String(localized:"Permissions", comment: "Settings tab title"), + title: String(localized: "Permissions", comment: "Settings tab title"), toolbarIcon: NSImage(systemSymbolName: "lock.shield", accessibilityDescription: String(localized: "Permissions settings"))! ) { PermissionsSettingsView() @@ -68,7 +68,7 @@ let PermissionsSettingsViewController: () -> SettingsPane = { func UpdatesSettingsViewController(updater: SPUUpdater) -> SettingsPane { let paneView = Settings.Pane( identifier: .updates, - title: String(localized:"Updates", comment: "Settings tab title"), + title: String(localized: "Updates", comment: "Settings tab title"), toolbarIcon: NSImage(systemSymbolName: "arrow.triangle.2.circlepath", accessibilityDescription: String(localized: "Update settings"))! ) { UpdateSettingsView(updater: updater) diff --git a/DockDoor/consts.swift b/DockDoor/consts.swift index 65b7fe10..1e3780a7 100644 --- a/DockDoor/consts.swift +++ b/DockDoor/consts.swift @@ -16,44 +16,44 @@ let roughHeightCap = optimisticScreenSizeHeight / 3 let roughWidthCap = optimisticScreenSizeWidth / 3 extension Defaults.Keys { - static let sizingMultiplier = Key("sizingMultiplier", default: 3 ) - static let bufferFromDock = Key("bufferFromDock", default: 0 ) - static let hoverWindowOpenDelay = Key("openDelay", default: 0 ) - - static let screenCaptureCacheLifespan = Key("screenCaptureCacheLifespan", default: 60 ) - static let windowPreviewImageScale = Key("windowPreviewImageScale", default: 1 ) - - static let uniformCardRadius = Key("uniformCardRadius", default: true ) - static let tapEquivalentInterval = Key("tapEquivalentInterval", default: 1.5 ) - static let previewHoverAction = Key("previewHoverAction", default: .none ) - - static let showAnimations = Key("showAnimations", default: true ) - static let enableWindowSwitcher = Key("enableWindowSwitcher", default: true ) + static let sizingMultiplier = Key("sizingMultiplier", default: 3) + static let bufferFromDock = Key("bufferFromDock", default: 0) + static let hoverWindowOpenDelay = Key("openDelay", default: 0) + + static let screenCaptureCacheLifespan = Key("screenCaptureCacheLifespan", default: 60) + static let windowPreviewImageScale = Key("windowPreviewImageScale", default: 1) + + static let uniformCardRadius = Key("uniformCardRadius", default: true) + static let tapEquivalentInterval = Key("tapEquivalentInterval", default: 1.5) + static let previewHoverAction = Key("previewHoverAction", default: .none) + + static let showAnimations = Key("showAnimations", default: true) + static let enableWindowSwitcher = Key("enableWindowSwitcher", default: true) static let showMenuBarIcon = Key("showMenuBarIcon", default: true) - static let defaultCMDTABKeybind = Key("defaultCMDTABKeybind", default: true ) - static let launched = Key("launched", default: false ) - static let Int64maskCommand = Key("Int64maskCommand", default: 1048840 ) - static let Int64maskControl = Key("Int64maskControl", default: 262401 ) - static let Int64maskAlternate = Key("Int64maskAlternate", default: 524576 ) + static let defaultCMDTABKeybind = Key("defaultCMDTABKeybind", default: true) + static let launched = Key("launched", default: false) + static let Int64maskCommand = Key("Int64maskCommand", default: 1_048_840) + static let Int64maskControl = Key("Int64maskControl", default: 262_401) + static let Int64maskAlternate = Key("Int64maskAlternate", default: 524_576) static let UserKeybind = Key("UserKeybind", default: UserKeyBind(keyCode: 48, modifierFlags: Defaults[.Int64maskControl])) - + static let showAppName = Key("showAppName", default: true) - static let appNameStyle = Key("appNameStyle", default: .default ) - - static let showWindowTitle = Key("showWindowTitle", default: true ) + static let appNameStyle = Key("appNameStyle", default: .default) + + static let showWindowTitle = Key("showWindowTitle", default: true) static let windowTitleDisplayCondition = Key("windowTitleDisplayCondition", default: .all) static let windowTitleVisibility = Key("windowTitleVisibility", default: .whenHoveringPreview) - static let windowTitlePosition = Key("windowTitlePosition", default: .bottomLeft ) - - static let trafficLightButtonsVisibility = Key("trafficLightButtonsVisibility", default: .dimmedOnPreviewHover ) + static let windowTitlePosition = Key("windowTitlePosition", default: .bottomLeft) + + static let trafficLightButtonsVisibility = Key("trafficLightButtonsVisibility", default: .dimmedOnPreviewHover) static let trafficLightButtonsPosition = Key("trafficLightButtonsPosition", default: .topLeft) } enum WindowTitleDisplayCondition: String, CaseIterable, Defaults.Serializable { - case all = "all" - case dockPreviewsOnly = "dockPreviewsOnly" - case windowSwitcherOnly = "windowSwitcherOnly" - + case all + case dockPreviewsOnly + case windowSwitcherOnly + var localizedName: String { switch self { case .all: @@ -71,7 +71,7 @@ enum WindowTitlePosition: String, CaseIterable, Defaults.Serializable { case bottomRight case topRight case topLeft - + var localizedName: String { switch self { case .bottomLeft: @@ -90,7 +90,7 @@ enum AppNameStyle: String, CaseIterable, Defaults.Serializable { case `default` case embedded case popover - + var localizedName: String { switch self { case .default: @@ -106,7 +106,7 @@ enum AppNameStyle: String, CaseIterable, Defaults.Serializable { enum WindowTitleVisibility: String, CaseIterable, Defaults.Serializable { case whenHoveringPreview case alwaysVisible - + var localizedName: String { switch self { case .whenHoveringPreview: @@ -122,7 +122,7 @@ enum TrafficLightButtonsVisibility: String, CaseIterable, Defaults.Serializable case dimmedOnPreviewHover case fullOpacityOnPreviewHover case alwaysVisible - + var localizedName: String { switch self { case .never: @@ -142,7 +142,7 @@ enum TrafficLightButtonsPosition: String, CaseIterable, Defaults.Serializable { case topRight case bottomRight case bottomLeft - + var localizedName: String { switch self { case .topLeft: @@ -161,7 +161,7 @@ enum PreviewHoverAction: String, CaseIterable, Defaults.Serializable { case none case tap case previewFullSize - + var localizedName: String { switch self { case .none: