Skip to content

Commit 9b8da00

Browse files
committed
refactor: Consolidate coordinate conversion methods into CoordinateUtils for better reusability
1 parent f3c71c0 commit 9b8da00

File tree

4 files changed

+103
-75
lines changed

4 files changed

+103
-75
lines changed

ClickIt/Core/Click/ClickCoordinator.swift

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ class ClickCoordinator: ObservableObject {
249249
print("[Dynamic Debug] Current mouse position (AppKit): \(appKitPosition)")
250250

251251
// Convert from AppKit coordinates to CoreGraphics coordinates with multi-monitor support
252-
let cgPosition = convertAppKitToCoreGraphicsMultiMonitor(appKitPosition)
252+
let cgPosition = CoordinateUtils.convertAppKitToCoreGraphics(appKitPosition)
253253
print("[Dynamic Debug] Converted to CoreGraphics coordinates: \(cgPosition)")
254254
return cgPosition
255255
}
@@ -269,7 +269,7 @@ class ClickCoordinator: ObservableObject {
269269
await MainActor.run {
270270
if configuration.useDynamicMouseTracking {
271271
// Convert back to AppKit coordinates for overlay positioning
272-
let appKitLocation = convertCoreGraphicsToAppKitMultiMonitor(location)
272+
let appKitLocation = CoordinateUtils.convertCoreGraphicsToAppKit(location)
273273
print("[Dynamic Debug] Overlay position (AppKit): \(appKitLocation)")
274274
VisualFeedbackOverlay.shared.updateOverlay(at: appKitLocation, isActive: true)
275275
} else {
@@ -304,7 +304,7 @@ class ClickCoordinator: ObservableObject {
304304
await MainActor.run {
305305
if configuration.useDynamicMouseTracking {
306306
// Convert back to AppKit coordinates for pulse positioning
307-
let appKitLocation = convertCoreGraphicsToAppKitMultiMonitor(location)
307+
let appKitLocation = CoordinateUtils.convertCoreGraphicsToAppKit(location)
308308
VisualFeedbackOverlay.shared.showClickPulse(at: appKitLocation)
309309
} else {
310310
VisualFeedbackOverlay.shared.showClickPulse(at: location)
@@ -360,47 +360,8 @@ class ClickCoordinator: ObservableObject {
360360
averageClickTime = 0
361361
}
362362

363-
/// Converts AppKit coordinates to CoreGraphics coordinates for multi-monitor setups
364-
private func convertAppKitToCoreGraphicsMultiMonitor(_ appKitPosition: CGPoint) -> CGPoint {
365-
// Find which screen contains this point
366-
for screen in NSScreen.screens {
367-
if screen.frame.contains(appKitPosition) {
368-
// Convert using the specific screen's coordinate system
369-
let cgY = screen.frame.maxY - appKitPosition.y
370-
let cgPosition = CGPoint(x: appKitPosition.x, y: cgY)
371-
print("[Multi-Monitor Debug] AppKit \(appKitPosition) → CoreGraphics \(cgPosition) on screen \(screen.frame)")
372-
return cgPosition
373-
}
374-
}
375-
376-
// Fallback to main screen if no screen contains the point
377-
let mainScreenHeight = NSScreen.main?.frame.height ?? 0
378-
let fallbackPosition = CGPoint(x: appKitPosition.x, y: mainScreenHeight - appKitPosition.y)
379-
print("[Multi-Monitor Debug] Fallback conversion: AppKit \(appKitPosition) → CoreGraphics \(fallbackPosition)")
380-
return fallbackPosition
381-
}
382-
383-
/// Converts CoreGraphics coordinates back to AppKit coordinates for multi-monitor setups
384-
private func convertCoreGraphicsToAppKitMultiMonitor(_ cgPosition: CGPoint) -> CGPoint {
385-
// Find which screen this CoreGraphics position would map to
386-
// This is a reverse lookup - we need to find the screen that would contain the original AppKit position
387-
for screen in NSScreen.screens {
388-
// Check if this position could have come from this screen
389-
let potentialAppKitY = screen.frame.maxY - cgPosition.y
390-
let potentialAppKitPosition = CGPoint(x: cgPosition.x, y: potentialAppKitY)
391-
392-
if screen.frame.contains(potentialAppKitPosition) {
393-
print("[Multi-Monitor Debug] CoreGraphics \(cgPosition) → AppKit \(potentialAppKitPosition) on screen \(screen.frame)")
394-
return potentialAppKitPosition
395-
}
396-
}
397-
398-
// Fallback to main screen conversion
399-
let mainScreenHeight = NSScreen.main?.frame.height ?? 0
400-
let fallbackPosition = CGPoint(x: cgPosition.x, y: mainScreenHeight - cgPosition.y)
401-
print("[Multi-Monitor Debug] Fallback reverse conversion: CoreGraphics \(cgPosition) → AppKit \(fallbackPosition)")
402-
return fallbackPosition
403-
}
363+
// MARK: - Coordinate conversion methods removed
364+
// These have been consolidated into CoordinateUtils for reuse across the codebase
404365
}
405366

406367
// MARK: - Supporting Types

ClickIt/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<key>CFBundlePackageType</key>
2020
<string>APPL</string>
2121
<key>CFBundleShortVersionString</key>
22-
<string>1.5.0</string>
22+
<string>1.5.1</string>
2323
<key>CFBundleVersion</key>
2424
<string>$(CURRENT_PROJECT_VERSION)</string>
2525
<key>LSMinimumSystemVersion</key>

Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -294,39 +294,12 @@ class ClickItViewModel: ObservableObject {
294294

295295
/// Checks if a position is within any available screen bounds (supports multiple monitors)
296296
private func isPositionWithinAnyScreen(_ position: CGPoint) -> Bool {
297-
for screen in NSScreen.screens {
298-
if screen.frame.contains(position) {
299-
print("[Timer Debug] Position \(position) is valid on screen: \(screen.frame)")
300-
return true
301-
}
302-
}
303-
print("[Timer Debug] Position \(position) is not within any screen bounds")
304-
return false
297+
return CoordinateUtils.isPositionWithinAnyScreen(position)
305298
}
306299

307300
/// Converts AppKit coordinates to CoreGraphics coordinates for multi-monitor setups
308301
private func convertAppKitToCoreGraphics(_ appKitPosition: CGPoint) -> CGPoint {
309-
// Find which screen contains this point
310-
for screen in NSScreen.screens {
311-
if screen.frame.contains(appKitPosition) {
312-
// FIXED: Proper multi-monitor coordinate conversion
313-
// AppKit Y increases upward from screen bottom
314-
// CoreGraphics Y increases downward from screen top
315-
// Formula: CG_Y = screen.origin.Y + (screen.height - (AppKit_Y - screen.origin.Y))
316-
let relativeY = appKitPosition.y - screen.frame.origin.y // Y relative to screen bottom
317-
let cgY = screen.frame.origin.y + (screen.frame.height - relativeY) // Convert to CG coordinates
318-
let cgPosition = CGPoint(x: appKitPosition.x, y: cgY)
319-
print("[Timer Debug] Multi-monitor conversion: AppKit \(appKitPosition) → CoreGraphics \(cgPosition) on screen \(screen.frame)")
320-
print("[Timer Debug] Calculation: relativeY=\(relativeY), cgY=\(screen.frame.origin.y) + (\(screen.frame.height) - \(relativeY)) = \(cgY)")
321-
return cgPosition
322-
}
323-
}
324-
325-
// Fallback to main screen if no screen contains the point
326-
let mainScreenHeight = NSScreen.main?.frame.height ?? 0
327-
let fallbackPosition = CGPoint(x: appKitPosition.x, y: mainScreenHeight - appKitPosition.y)
328-
print("[Timer Debug] Fallback conversion: AppKit \(appKitPosition) → CoreGraphics \(fallbackPosition)")
329-
return fallbackPosition
302+
return CoordinateUtils.convertAppKitToCoreGraphics(appKitPosition)
330303
}
331304

332305
// MARK: - Emergency Stop Configuration Methods
@@ -434,7 +407,7 @@ class ClickItViewModel: ObservableObject {
434407

435408
// Validate cursor position is within any available screen bounds
436409
guard isPositionWithinAnyScreen(appKitPosition) else {
437-
let allScreens = NSScreen.screens.map { $0.frame }
410+
let allScreens = CoordinateUtils.getAllScreenFrames()
438411
print("[Timer Debug] Error: cursor position \(appKitPosition) is outside all screen bounds \(allScreens)")
439412
appStatus = .error("Invalid cursor position when timer expired")
440413
return
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// swiftlint:disable file_header
2+
import Foundation
3+
import CoreGraphics
4+
import AppKit
5+
6+
/// Utility functions for coordinate system conversions between AppKit and CoreGraphics
7+
/// with proper multi-monitor support
8+
struct CoordinateUtils {
9+
10+
// MARK: - AppKit to CoreGraphics Conversion
11+
12+
/// Converts AppKit coordinates to CoreGraphics coordinates for multi-monitor setups
13+
/// - Parameter appKitPosition: Position in AppKit coordinate system (Y increases upward from screen bottom)
14+
/// - Returns: Position in CoreGraphics coordinate system (Y increases downward from screen top)
15+
static func convertAppKitToCoreGraphics(_ appKitPosition: CGPoint) -> CGPoint {
16+
// Find which screen contains this point
17+
for screen in NSScreen.screens {
18+
if screen.frame.contains(appKitPosition) {
19+
// FIXED: Proper multi-monitor coordinate conversion
20+
// AppKit Y increases upward from screen bottom
21+
// CoreGraphics Y increases downward from screen top
22+
// Formula: CG_Y = screen.origin.Y + (screen.height - (AppKit_Y - screen.origin.Y))
23+
let relativeY = appKitPosition.y - screen.frame.origin.y // Y relative to screen bottom
24+
let cgY = screen.frame.origin.y + (screen.frame.height - relativeY) // Convert to CG coordinates
25+
let cgPosition = CGPoint(x: appKitPosition.x, y: cgY)
26+
27+
print("[CoordinateUtils] Multi-monitor conversion: AppKit \(appKitPosition) → CoreGraphics \(cgPosition) on screen \(screen.frame)")
28+
print("[CoordinateUtils] Calculation: relativeY=\(relativeY), cgY=\(screen.frame.origin.y) + (\(screen.frame.height) - \(relativeY)) = \(cgY)")
29+
30+
return cgPosition
31+
}
32+
}
33+
34+
// Fallback to main screen if no screen contains the point
35+
let mainScreenHeight = NSScreen.main?.frame.height ?? 0
36+
let fallbackPosition = CGPoint(x: appKitPosition.x, y: mainScreenHeight - appKitPosition.y)
37+
print("[CoordinateUtils] Fallback conversion: AppKit \(appKitPosition) → CoreGraphics \(fallbackPosition)")
38+
return fallbackPosition
39+
}
40+
41+
// MARK: - CoreGraphics to AppKit Conversion
42+
43+
/// Converts CoreGraphics coordinates back to AppKit coordinates for multi-monitor setups
44+
/// - Parameter cgPosition: Position in CoreGraphics coordinate system (Y increases downward from screen top)
45+
/// - Returns: Position in AppKit coordinate system (Y increases upward from screen bottom)
46+
static func convertCoreGraphicsToAppKit(_ cgPosition: CGPoint) -> CGPoint {
47+
// Find which screen this CoreGraphics position would map to
48+
// This is a reverse lookup - we need to find the screen that would contain the original AppKit position
49+
for screen in NSScreen.screens {
50+
// Check if this position could have come from this screen
51+
let potentialAppKitY = screen.frame.maxY - cgPosition.y
52+
let potentialAppKitPosition = CGPoint(x: cgPosition.x, y: potentialAppKitY)
53+
54+
if screen.frame.contains(potentialAppKitPosition) {
55+
print("[CoordinateUtils] CoreGraphics \(cgPosition) → AppKit \(potentialAppKitPosition) on screen \(screen.frame)")
56+
return potentialAppKitPosition
57+
}
58+
}
59+
60+
// Fallback to main screen conversion
61+
let mainScreenHeight = NSScreen.main?.frame.height ?? 0
62+
let fallbackPosition = CGPoint(x: cgPosition.x, y: mainScreenHeight - cgPosition.y)
63+
print("[CoordinateUtils] Fallback reverse conversion: CoreGraphics \(cgPosition) → AppKit \(fallbackPosition)")
64+
return fallbackPosition
65+
}
66+
67+
// MARK: - Multi-Monitor Support Functions
68+
69+
/// Checks if a position is within any available screen bounds
70+
/// - Parameter position: Position to check (in AppKit coordinates)
71+
/// - Returns: True if position is within any screen bounds
72+
static func isPositionWithinAnyScreen(_ position: CGPoint) -> Bool {
73+
for screen in NSScreen.screens {
74+
if screen.frame.contains(position) {
75+
print("[CoordinateUtils] Position \(position) is valid on screen: \(screen.frame)")
76+
return true
77+
}
78+
}
79+
print("[CoordinateUtils] Position \(position) is not within any screen bounds")
80+
return false
81+
}
82+
83+
/// Gets all available screen frames for debugging purposes
84+
/// - Returns: Array of all screen frames
85+
static func getAllScreenFrames() -> [CGRect] {
86+
return NSScreen.screens.map { $0.frame }
87+
}
88+
89+
/// Gets the main screen frame
90+
/// - Returns: Main screen frame, or zero rect if unavailable
91+
static func getMainScreenFrame() -> CGRect {
92+
return NSScreen.main?.frame ?? CGRect.zero
93+
}
94+
}

0 commit comments

Comments
 (0)