Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for macOS 13 Ventura #267

Merged
merged 7 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions DockDoor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.1.5;
PRODUCT_BUNDLE_IDENTIFIER = com.ethanbills.DockDoor;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -591,7 +591,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.1.5;
PRODUCT_BUNDLE_IDENTIFIER = com.ethanbills.DockDoor;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
46 changes: 26 additions & 20 deletions DockDoor/Components/Marquee.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ struct TheMarquee<C: View>: View {
if !animating { return }
let offsetAmount = contentSize.width + spacingBetweenElements
let duration = offsetAmount / speedPtsPerSec

withAnimation(.linear(duration: duration)) {
offset = -offsetAmount
} completion: {
}

// Simulate the completion handler taht only available on macOS 14.0+
doAfter(duration) {
offset = 0
doAfter(secsBeforeLooping) {
if animating {
Expand Down Expand Up @@ -63,8 +67,8 @@ struct TheMarquee<C: View>: View {
.measure($containerSize)
.compositingGroup()
.opacity(measured ? 1 : 0)
.onChange(of: containerSize) { _, _ in startAnimation() }
ShlomoCode marked this conversation as resolved.
Show resolved Hide resolved
.onChange(of: contentSize) { _, _ in startAnimation() }
.onChange(of: containerSize) { _ in startAnimation() }
.onChange(of: contentSize) { _ in startAnimation() }
.onAppear { startAnimation() }
.onDisappear { animating = false }
}
Expand All @@ -76,23 +80,25 @@ extension View {
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)
if #available(macOS 14.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 {
Expand Down
9 changes: 6 additions & 3 deletions DockDoor/Utilities/DockObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ final class DockObserver {
private func setupEventTap() {
guard AXIsProcessTrusted() else {
print("Debug: Accessibility permission not granted")
MessageUtil.showMessage(title: String(localized: "Permission error"),
message: String(localized: "You need to give DockDoor access to the accessibility API in order for it to function."),
completion: { _ in SystemPreferencesHelper.openAccessibilityPreferences() })
MessageUtil.showAlert(
title: String(localized: "Permission error"),
message: String(localized: "You need to give DockDoor access to the accessibility API in order for it to function."),
actions: [.ok],
completion: { _ in SystemPreferencesHelper.openAccessibilityPreferences() }
)
return
}

Expand Down
27 changes: 19 additions & 8 deletions DockDoor/Utilities/MessageUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,31 @@ enum MessageUtil {
case cancel
}

static func showMessage(title: String, message: String, completion: @escaping (ButtonAction) -> Void) {
static func showAlert(title: String, message: String, actions: [ButtonAction], completion: ((ButtonAction) -> Void)? = nil) {
let alert = NSAlert()
alert.messageText = title
alert.informativeText = message
alert.alertStyle = .warning
alert.addButton(withTitle: String(localized: "OK"))
alert.addButton(withTitle: String(localized: "Cancel"))

for action in actions {
switch action {
case .ok:
alert.addButton(withTitle: String(localized: "OK"))
case .cancel:
alert.addButton(withTitle: String(localized: "Cancel"))
}
}

let modalResult = alert.runModal()
switch modalResult {
case .alertFirstButtonReturn: // OK button
completion(.ok)
default: // Cancel button or other (e.g., window closed)
completion(.cancel)
let buttonAction: ButtonAction = switch modalResult {
case .alertFirstButtonReturn:
actions[0]
case .alertSecondButtonReturn:
actions[1]
default:
actions.last ?? .cancel
}

completion?(buttonAction)
}
}
15 changes: 10 additions & 5 deletions DockDoor/Utilities/Misc Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import Cocoa
import Defaults

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()
MessageUtil.showAlert(
title: String(localized: "Restart required"),
message: String(localized: "Please restart the application to apply your changes. Click OK to quit the app."),
actions: [.ok, .cancel],
completion: { result in
if result == .ok {
let appDelegate = NSApplication.shared.delegate as! AppDelegate
appDelegate.restartApp()
}
}
})
)
}

func resetDefaultsToDefaultValues() {
Expand Down
58 changes: 45 additions & 13 deletions DockDoor/Utilities/WindowUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,34 +77,66 @@ enum WindowUtil {

// MARK: - Helper Functions

static func captureWindowImage(window: SCWindow) async throws -> CGImage {
clearExpiredCache()
/// Captures an image of a window using legacy methods for macOS versions earlier than 14.0.
/// This function is used as a fallback when the ScreenCaptureKit's captureScreenshot API are not available.
private static func captureImageLegacy(of window: SCWindow) async throws -> CGImage {
let windowRect = CGRect(
x: window.frame.origin.x,
y: window.frame.origin.y,
width: CGFloat(window.frame.width),
height: CGFloat(window.frame.height)
)

if let cachedImage = getCachedImage(window: window) {
return cachedImage
guard let cgImage = CGWindowListCreateImage(windowRect, .optionIncludingWindow, window.windowID, [.boundsIgnoreFraming, .bestResolution]) else {
throw NSError(domain: "WindowCaptureError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to create image for window"])
}

return cgImage
}

/// Captures an image of a window using ScreenCaptureKit's for macOS versions 14.0 and later.
@available(macOS 14.0, *)
private static func captureImageModern(of window: SCWindow) async throws -> CGImage {
let filter = SCContentFilter(desktopIndependentWindow: window)
let config = SCStreamConfiguration()

// Get the scale factor of the display containing the window
let scaleFactor = await getScaleFactorForWindow(windowID: window.windowID)
// Convert points to pixels
let width = Int(window.frame.width * scaleFactor) / Int(Defaults[.windowPreviewImageScale])
let height = Int(window.frame.height * scaleFactor) / Int(Defaults[.windowPreviewImageScale])

config.width = width
config.height = height
config.scalesToFit = false
config.backgroundColor = .clear
config.captureResolution = .best
config.ignoreGlobalClipDisplay = true
config.ignoreShadowsDisplay = true
config.shouldBeOpaque = false
if #available(macOS 14.2, *) { config.includeChildWindows = false }
config.showsCursor = false

// Get the scale factor of the display containing the window
let scaleFactor = await getScaleFactorForWindow(windowID: window.windowID)
if #available(macOS 14.2, *) {
config.includeChildWindows = false
}

// Convert points to pixels
config.width = Int(window.frame.width * scaleFactor) / Int(Defaults[.windowPreviewImageScale])
config.height = Int(window.frame.height * scaleFactor) / Int(Defaults[.windowPreviewImageScale])
return try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config)
}

config.showsCursor = false
config.captureResolution = .best
ShlomoCode marked this conversation as resolved.
Show resolved Hide resolved
/// Main function to capture a window image using ScreenCaptureKit, with fallback to legacy methods for older macOS versions.
static func captureWindowImage(window: SCWindow) async throws -> CGImage {
clearExpiredCache()

let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config)
if let cachedImage = getCachedImage(window: window) {
return cachedImage
}

// Use ScreenCaptureKit's API if available, otherwise fall back to a legacy (deprecated) API
let image: CGImage = if #available(macOS 14.0, *) {
try await captureImageModern(of: window)
} else {
try await captureImageLegacy(of: window)
}

let cachedImage = CachedImage(image: image, timestamp: Date(), windowname: window.title)
imageCache[window.windowID] = cachedImage
Expand Down
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the window switcher work with this? This was the main reason DD is min macOS 14, the observable property allows us to reflect the changes in index when using the switcher. I'm not sure if @published properties are a slot-in replacement - however I may be wrong.

Copy link
Contributor Author

@ShlomoCode ShlomoCode Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to work fine, although I'm actually not very familiar with the windows switcher. Is there anything special I should look out for?

CleanShot.2024-08-23.at.04.15.05.mp4

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try it out in a bit

Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import Defaults
import FluidGradient
import SwiftUI

@Observable class ScreenCenteredFloatingWindow {
class ScreenCenteredFloatingWindow: ObservableObject {
static let shared = ScreenCenteredFloatingWindow()

var currIndex: Int = 0
var windowSwitcherActive: Bool = false
var fullWindowPreviewActive: Bool = false
@Published var currIndex: Int = 0
@Published var windowSwitcherActive: Bool = false
@Published var fullWindowPreviewActive: Bool = false

enum WindowState {
case windowSwitcher
Expand Down
4 changes: 2 additions & 2 deletions DockDoor/Views/Hover Window/WindowPreviewHoverContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ struct WindowPreviewHoverContainer: View {
runUIUpdates()
}
}
.onChange(of: ScreenCenteredFloatingWindow.shared.currIndex) { _, newIndex in
.onChange(of: ScreenCenteredFloatingWindow.shared.currIndex) { newIndex in
withAnimation {
scrollProxy.scrollTo("\(appName)-\(newIndex)", anchor: .center)
}
}
.onChange(of: windows) { _, _ in
.onChange(of: windows) { _ in
runUIUpdates()
}
}
Expand Down
26 changes: 20 additions & 6 deletions DockDoor/Views/Settings/AppearanceSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ struct AppearanceSettingsView: View {
@Default(.trafficLightButtonsVisibility) var trafficLightButtonsVisibility
@Default(.trafficLightButtonsPosition) var trafficLightButtonsPosition

@State private var previousTrafficLightButtonsPosition: TrafficLightButtonsPosition
@State private var previousWindowTitlePosition: WindowTitlePosition

init() {
_previousTrafficLightButtonsPosition = State(initialValue: Defaults[.trafficLightButtonsPosition])
_previousWindowTitlePosition = State(initialValue: Defaults[.windowTitlePosition])
}

var body: some View {
VStack(alignment: .leading, spacing: 10) {
Toggle(isOn: $showAnimations, label: {
Expand All @@ -40,17 +48,20 @@ struct AppearanceSettingsView: View {
.tag(position)
}
}
.onChange(of: trafficLightButtonsPosition) { oldValue, newValue in
.onChange(of: trafficLightButtonsPosition) { newValue in
if newValue.rawValue == windowTitlePosition.rawValue {
MessageUtil.showMessage(
MessageUtil.showAlert(
title: String(localized: "Elements Overlap"),
message: String(localized: "The selected positions for Traffic Light Buttons and Window Title will overlap."),
actions: [.ok, .cancel],
completion: { result in
if result == .cancel {
trafficLightButtonsPosition = oldValue
trafficLightButtonsPosition = previousTrafficLightButtonsPosition
}
}
)
} else {
previousTrafficLightButtonsPosition = newValue
}
}
.pickerStyle(SegmentedPickerStyle())
Expand Down Expand Up @@ -111,17 +122,20 @@ struct AppearanceSettingsView: View {
.tag(position)
}
}
.onChange(of: windowTitlePosition) { oldValue, newValue in
.onChange(of: windowTitlePosition) { newValue in
if newValue.rawValue == trafficLightButtonsPosition.rawValue {
MessageUtil.showMessage(
MessageUtil.showAlert(
title: String(localized: "Elements Overlap"),
message: String(localized: "The selected positions for Traffic Light Buttons and Window Title will overlap."),
actions: [.ok, .cancel],
completion: { result in
if result == .cancel {
windowTitlePosition = oldValue
windowTitlePosition = previousWindowTitlePosition
}
}
)
} else {
previousWindowTitlePosition = newValue
}
}
.scaledToFit()
Expand Down
12 changes: 7 additions & 5 deletions DockDoor/Views/Settings/GradientColorPaletteSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ struct GradientColorPaletteSettingsView: View {
if editingIndex != nil, editingColor != nil {
ColorPicker("Edit Color", selection: $tempColor)
.labelsHidden()
.onChange(of: tempColor) { _, newValue in
.onChange(of: tempColor) { newValue in
colorUpdatePublisher.send(newValue)
}
}
Expand Down Expand Up @@ -127,9 +127,10 @@ struct GradientColorPaletteSettingsView: View {
}

private func showMinimumColorsAlert() {
MessageUtil.showMessage(
MessageUtil.showAlert(
title: "Cannot Remove Color",
message: "Minimum number of colors reached."
message: "Minimum number of colors reached.",
actions: [.ok, .cancel]
) { action in
switch action {
case .ok:
Expand All @@ -141,9 +142,10 @@ struct GradientColorPaletteSettingsView: View {
}

private func showMaximumColorsAlert() {
MessageUtil.showMessage(
MessageUtil.showAlert(
title: "Cannot Add Color",
message: "Maximum number of colors (\(maxColors)) reached."
message: "Maximum number of colors (\(maxColors)) reached.",
actions: [.ok, .cancel]
) { action in
switch action {
case .ok:
Expand Down
Loading