Files
shotscreen/ShotScreen/Sources/WindowCaptureManager.swift
Nick Roodenrijs 0dabed11d2 🎉 ShotScreen v1.0 - Initial Release
🚀 First official release of ShotScreen with complete feature set:

 Core Features:
- Advanced screenshot capture system
- Multi-monitor support
- Professional UI/UX design
- Automated update system with Sparkle
- Apple notarized & code signed

🛠 Technical Excellence:
- Native Swift macOS application
- Professional build & deployment pipeline
- Comprehensive error handling
- Memory optimized performance

📦 Distribution Ready:
- Professional DMG packaging
- Apple notarization complete
- No security warnings for users
- Ready for public distribution

This is the foundation release that establishes ShotScreen as a premium screenshot tool for macOS.
2025-06-28 16:15:15 +02:00

998 lines
43 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AppKit
import ScreenCaptureKit
import SwiftUI
import UniformTypeIdentifiers
import ObjectiveC
// MARK: - Window Capture Manager
@available(macOS 12.3, *)
class WindowCaptureManager: NSObject, ObservableObject {
// MARK: - Properties
weak var screenshotApp: ScreenshotApp?
var determinedMainScreen: NSScreen! // Stores the true main screen for the current selection session
// Available content for capture
@Published var availableWindows: [SCWindow] = []
@Published var availableDisplays: [SCDisplay] = []
@Published var isCapturing = false
@Published var captureError: String?
// Window selection state
@Published var isWindowSelectionActive = false
private var windowSelectionOverlay: WindowSelectionOverlay?
private var globalMouseMonitor: Any?
// MARK: - Initialization
override init() {
super.init()
}
convenience init(screenshotApp: ScreenshotApp) {
self.init()
self.screenshotApp = screenshotApp
}
// MARK: - Content Discovery
func refreshAvailableContent() async {
print("🔄 Refreshing available content for window capture...")
do {
let shareableContent = try await SCShareableContent.current
await updateAvailableContent(shareableContent)
print("✅ Content refreshed successfully")
} catch {
await handleContentRefreshError(error)
}
}
// MARK: - Private Helper Methods for Content Discovery
private func updateAvailableContent(_ shareableContent: SCShareableContent) async {
let filteredWindows = filterShareableWindows(shareableContent.windows)
let validDisplays = shareableContent.displays.filter { $0.width > 0 && $0.height > 0 }
await MainActor.run {
self.availableWindows = filteredWindows
self.availableDisplays = validDisplays
self.captureError = nil
}
logContentSummary(windows: filteredWindows, displays: validDisplays)
}
private func filterShareableWindows(_ windows: [SCWindow]) -> [SCWindow] {
return windows.filter { window in
// Filter criteria for capturable windows
guard window.isOnScreen,
window.frame.width > 50,
window.frame.height > 50,
let app = window.owningApplication,
app.applicationName != "WindowServer",
app.applicationName != "Dock" else {
return false
}
// Exclude our own app's windows
if let ourBundleID = Bundle.main.bundleIdentifier,
app.bundleIdentifier == ourBundleID {
return false
}
return true
}
}
private func logContentSummary(windows: [SCWindow], displays: [SCDisplay]) {
print("📊 Content Summary:")
print(" Windows: \(windows.count) capturable windows found")
print(" Displays: \(displays.count) displays found")
if windows.count > 0 {
print("🪟 Sample windows:")
for (index, window) in windows.prefix(5).enumerated() {
let app = window.owningApplication?.applicationName ?? "Unknown"
let title = window.title?.isEmpty == false ? window.title! : "No Title"
print(" \(index + 1). \(title) (\(app)) - Layer: \(window.windowLayer)")
}
if windows.count > 5 {
print(" ... and \(windows.count - 5) more windows")
}
}
}
private func handleContentRefreshError(_ error: Error) async {
await MainActor.run {
self.captureError = "Failed to get shareable content: \(error.localizedDescription)"
print("❌ WindowCaptureManager Error: \(error)")
}
}
// MARK: - Window Selection Mode
func activateWindowSelectionMode() {
print("🎬 ACTIVATE: Starting window selection mode...")
print("🪟 Activating window selection mode")
// Determine the actual main screen (with origin 0,0) for consistent coordinate calculations
self.determinedMainScreen = NSScreen.screens.first { $0.frame.origin == .zero }
if self.determinedMainScreen == nil {
print("⚠️ CRITICAL: Could not find screen with origin (0,0). Falling back to default NSScreen.main.")
self.determinedMainScreen = NSScreen.main! // Fallback, though this might be the unreliable one
}
print("✨ Using determined main screen for this session: \(determinedMainScreen.customLocalizedName) with frame \(determinedMainScreen.frame)")
print("🎬 ACTIVATE: Starting async task to refresh content and show overlay...")
Task {
print("🎬 TASK: Inside async task, refreshing content...")
await refreshAvailableContent()
await MainActor.run {
print("🎬 TASK: Content refreshed, setting flags and showing overlay...")
self.isWindowSelectionActive = true
self.showWindowSelectionOverlay()
print("🎬 TASK: Window selection overlay should now be shown")
}
}
}
// MARK: - Window Selection Mode Deactivation
func deactivateWindowSelectionMode() {
print("🪟 Deactivating window selection mode")
isWindowSelectionActive = false
hideWindowSelectionOverlay()
// Remove any mouse monitors
if let monitor = globalMouseMonitor {
NSEvent.removeMonitor(monitor)
globalMouseMonitor = nil
}
}
private func showWindowSelectionOverlay() {
// Create overlay window that shows available windows
let overlay = WindowSelectionOverlay(windowCaptureManager: self)
overlay.show()
windowSelectionOverlay = overlay
}
private func hideWindowSelectionOverlay() {
windowSelectionOverlay?.hide()
windowSelectionOverlay = nil
}
private func setupWindowSelectionMonitoring() {
globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown]) { [weak self] event in
guard let self = self, self.isWindowSelectionActive else { return }
let location = NSEvent.mouseLocation
self.handleWindowSelection(at: location)
}
}
private func removeWindowSelectionMonitoring() {
if let monitor = globalMouseMonitor {
NSEvent.removeMonitor(monitor)
globalMouseMonitor = nil
}
}
private func handleWindowSelection(at location: NSPoint) {
// HERSTEL: Zoek naar een window
if let window = findWindowAt(location: location) {
let windowTitleForPrint = window.title ?? "Untitled"
print("🎯 Window selected: \(windowTitleForPrint) at \(location)")
deactivateWindowSelectionMode()
Task {
await captureWindow(window) // Gebruik de captureWindow methode
}
} else {
print(" No window found at \(location), canceling")
deactivateWindowSelectionMode()
}
}
// MARK: - Window Detection
func findWindowAt(location: NSPoint) -> SCWindow? {
NSLog("🎯 Finding window at global location: %@", NSStringFromPoint(location))
// Step 1: Validate main screen reference
guard let mainScreenRef = validateMainScreenReference() else {
return nil
}
// Step 2: Find the screen containing the mouse location
guard let mouseScreen = findScreenContaining(location: location) else {
return nil
}
// Step 3: Search for window at location
return searchWindowsAt(location: location, mouseScreen: mouseScreen, mainScreenRef: mainScreenRef)
}
// MARK: - Private Helper Methods for Window Detection
private func validateMainScreenReference() -> NSScreen? {
guard let mainScreenRef = self.determinedMainScreen else {
NSLog("❌ No determined main screen available for findWindowAt")
return nil
}
return mainScreenRef
}
private func findScreenContaining(location: NSPoint) -> NSScreen? {
for screen in NSScreen.screens {
if screen.frame.contains(location) {
NSLog("🎯 Mouse is on NSScreen: '%@' (Frame: %@)", screen.customLocalizedName, NSStringFromRect(screen.frame))
return screen
}
}
NSLog("❌ Mouse location not on any NSScreen: %@", NSStringFromPoint(location))
return nil
}
private func searchWindowsAt(location: NSPoint, mouseScreen: NSScreen, mainScreenRef: NSScreen) -> SCWindow? {
// Sort windows by layer, so frontmost windows are checked first
let sortedWindows = availableWindows.sorted { $0.windowLayer > $1.windowLayer }
NSLog("🎯 Checking \(sortedWindows.count) sorted windows.")
for window in sortedWindows {
if let foundWindow = checkWindowAtLocation(window: window, location: location,
mouseScreen: mouseScreen, mainScreenRef: mainScreenRef) {
return foundWindow
}
}
NSLog(" ❌ No window found at global location: %@ on screen %@", NSStringFromPoint(location), mouseScreen.customLocalizedName)
return nil
}
private func checkWindowAtLocation(window: SCWindow, location: NSPoint,
mouseScreen: NSScreen, mainScreenRef: NSScreen) -> SCWindow? {
let scWinFrame = window.frame
let globalWindowFrame = self.convertSCWindowFrameToGlobal(scWinFrame, mainScreen: mainScreenRef)
// Check 1: Does the window intersect with the screen the mouse is on?
guard mouseScreen.frame.intersects(globalWindowFrame) else {
return nil
}
// Check 2: Does the global window frame contain the mouse location?
guard globalWindowFrame.contains(location) else {
return nil
}
NSLog(" 🎉 FOUND window by global coords: '%@' (App: %@) on screen '%@'",
window.title ?? "Untitled",
window.owningApplication?.applicationName ?? "Unknown",
mouseScreen.customLocalizedName)
return window
}
// MARK: - Window Capture Methods
/// Capture a specific window by SCWindow object
func captureWindow(_ window: SCWindow) async {
print("📸 WindowCaptureManager: Capturing window: \(window.title ?? "Untitled") using ScreenCaptureKitProvider")
// Step 1: Initialize capture state
await initializeCaptureState()
// Step 2: Validate screen capture provider
guard let provider = validateScreenCaptureProvider() else {
await handleCaptureError("ScreenCaptureProvider not available.", code: 3)
return
}
// Step 3: Perform capture
await performWindowCapture(window: window, provider: provider)
}
// MARK: - Private Helper Methods for Window Capture
private func initializeCaptureState() async {
await MainActor.run {
isCapturing = true
captureError = nil
}
}
private func validateScreenCaptureProvider() -> ScreenCaptureKitProvider? {
return self.screenshotApp?.screenCaptureProvider
}
private func performWindowCapture(window: SCWindow, provider: ScreenCaptureKitProvider) async {
if let image = await provider.captureWindow(window: window) {
await handleSuccessfulCapture(image: image)
} else {
await handleCaptureError("Failed to capture window with ScreenCaptureKitProvider.", code: 2)
}
}
private func handleSuccessfulCapture(image: NSImage) async {
await MainActor.run {
self.isCapturing = false
self.screenshotApp?.processCapture(image: image)
self.deactivateWindowSelectionMode()
print("✅ Window captured successfully and selection mode deactivated.")
}
}
private func handleCaptureError(_ description: String, code: Int) async {
await MainActor.run {
self.isCapturing = false
self.captureError = NSError(domain: "WindowCaptureError", code: code,
userInfo: [NSLocalizedDescriptionKey: description]).localizedDescription
self.deactivateWindowSelectionMode()
print("Error: \(description)")
}
}
// MARK: - Core Graphics Capture Method (Old - Replaced by ScreenCaptureKitProvider)
/*
private func captureWindowWithCoreGraphics(_ window: SCWindow) async {
let windowID = window.windowID
let imageRef = CGWindowListCreateImage(.null, .optionIncludingWindow, windowID, .bestResolution)
guard let cgImage = imageRef else {
await MainActor.run {
self.captureError = "Failed to capture window"
self.isCapturing = false
}
return
}
let nsImage = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
await MainActor.run {
self.isCapturing = false
// Process the captured image through the main app
self.screenshotApp?.processWindowCapture(image: nsImage, windowTitle: window.title)
}
}
*/
/// Capture window by application name
func captureWindowByApp(appName: String) async {
await refreshAvailableContent()
guard let window = availableWindows.first(where: {
$0.owningApplication?.applicationName == appName
}) else {
await MainActor.run {
self.captureError = "No window found for app: \(appName)"
}
return
}
await captureWindow(window)
}
/// Capture frontmost window
func captureFrontmostWindow() async {
await refreshAvailableContent()
// Get the frontmost window (highest window level)
guard let frontmostWindow = availableWindows.max(by: { $0.windowLayer < $1.windowLayer }) else {
await MainActor.run {
self.captureError = "No frontmost window found"
}
return
}
await captureWindow(frontmostWindow)
}
/// Capture window containing a specific point
func captureWindowAt(point: NSPoint) async {
await refreshAvailableContent()
guard let window = findWindowAt(location: point) else {
await MainActor.run {
self.captureError = "No window found at specified location"
}
return
}
await captureWindow(window)
}
// MARK: - Settings Integration
func getWindowCaptureSettings() -> WindowCaptureSettings {
return WindowCaptureSettings(
includeCursor: UserDefaults.standard.bool(forKey: "windowCaptureIncludeCursor"),
showSelectionUI: UserDefaults.standard.bool(forKey: "windowCaptureShowSelectionUI")
)
}
func saveWindowCaptureSettings(_ settings: WindowCaptureSettings) {
UserDefaults.standard.set(settings.includeCursor, forKey: "windowCaptureIncludeCursor")
UserDefaults.standard.set(settings.showSelectionUI, forKey: "windowCaptureShowSelectionUI")
}
// MARK: - Coordinate Conversion Helper
// Converts an SCWindow.frame (origin top-left of main screen, Y-down) to Global coordinates (origin bottom-left of main screen, Y-up)
func convertSCWindowFrameToGlobal(_ scWindowFrame: CGRect, mainScreen: NSScreen) -> CGRect {
let globalX = scWindowFrame.origin.x
// Y of top edge of scWindow in Global Y-up (from main screen bottom)
let globalY_topEdge = mainScreen.frame.height - scWindowFrame.origin.y
// Y of bottom edge of scWindow in Global Y-up (this becomes the origin for the CGRect)
let globalY_bottomEdge_forOrigin = globalY_topEdge - scWindowFrame.height
return CGRect(x: globalX, y: globalY_bottomEdge_forOrigin, width: scWindowFrame.width, height: scWindowFrame.height)
}
// MARK: - Window Detection (Wordt Display Detection)
func findDisplayAt(location: NSPoint) -> SCDisplay? {
NSLog("🎯 Finding display at global location: %@", NSStringFromPoint(location))
// We gebruiken de beschikbare SCDisplay objecten direct.
// Hun frames zijn in pixels, oorsprong linksboven van *dat specifieke display*.
// Om te checken of de muis (globale punten, oorsprong linksonder hoofdmenu) binnen een display valt,
// moeten we de SCDisplay.frame converteren naar globale punten, of de muislocatie naar de coördinaten van elk display.
// Makkelijker: gebruik NSScreen.screens, vind de NSScreen waar de muis op is, en zoek dan de SCDisplay met dezelfde displayID.
var currentMouseNSScreen: NSScreen?
for s in NSScreen.screens {
if s.frame.contains(location) {
currentMouseNSScreen = s
break
}
}
guard let mouseNSScreen = currentMouseNSScreen else {
NSLog("❌ Mouse location %@ not on any NSScreen.", NSStringFromPoint(location))
return nil
}
NSLog("🎯 Mouse is on NSScreen: '%@' (Frame: %@), DisplayID: \(mouseNSScreen.displayID)", mouseNSScreen.customLocalizedName, NSStringFromRect(mouseNSScreen.frame))
// Zoek de SCDisplay die overeenkomt met deze NSScreen
if let matchedDisplay = availableDisplays.first(where: { $0.displayID == mouseNSScreen.displayID }) {
NSLog(" 🎉 FOUND display by ID match: DisplayID \(matchedDisplay.displayID)")
return matchedDisplay
}
NSLog(" ❌ No SCDisplay found matching NSScreen with DisplayID \(mouseNSScreen.displayID) at global location: %@", NSStringFromPoint(location))
return nil
}
// MARK: - Window Capture Methods (Wordt Display Capture Methods)
/// Capture a specific display by SCDisplay object
func captureDisplay(_ display: SCDisplay) async {
print("🖥️ WindowCaptureManager: Capturing display ID \(display.displayID) using ScreenCaptureKitProvider")
// Converteer SCDisplay naar NSScreen
guard let targetNSScreen = NSScreen.screens.first(where: { $0.displayID == display.displayID }) else {
await MainActor.run {
self.isCapturing = false
self.captureError = NSError(domain: "DisplayCaptureError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not find NSScreen matching SCDisplay ID \(display.displayID)."]).localizedDescription
self.deactivateWindowSelectionMode()
print("Error: Could not find NSScreen for SCDisplay ID \(display.displayID).")
}
return
}
print("🖥️ Corresponderende NSScreen gevonden: \(targetNSScreen.customLocalizedName)")
await MainActor.run {
isCapturing = true
captureError = nil
}
if let provider = self.screenshotApp?.screenCaptureProvider {
// ScreenCaptureKitProvider.captureScreen verwacht een NSScreen
// De excludingWindows parameter is optioneel en wordt hier nil gelaten,
// omdat we het hele scherm capturen en desktop icons apart worden behandeld in de provider.
if let image = await provider.captureScreen(screen: targetNSScreen, excludingWindows: nil) {
await MainActor.run {
self.isCapturing = false
self.screenshotApp?.processCapture(image: image)
self.deactivateWindowSelectionMode()
print("✅ Display captured successfully and selection mode deactivated.")
}
} else {
await MainActor.run {
self.isCapturing = false
self.captureError = NSError(domain: "DisplayCaptureError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to capture display with ScreenCaptureKitProvider."]).localizedDescription
self.deactivateWindowSelectionMode()
print("Error: Failed to capture display ID \(display.displayID) with ScreenCaptureKitProvider.")
}
}
} else {
await MainActor.run {
self.isCapturing = false
self.captureError = NSError(domain: "DisplayCaptureError", code: 3, userInfo: [NSLocalizedDescriptionKey: "ScreenCaptureProvider not available."]).localizedDescription
self.deactivateWindowSelectionMode()
print("Error: ScreenCaptureProvider not available for display capture.")
}
}
}
}
// MARK: - Window Capture Settings
struct WindowCaptureSettings {
var includeCursor: Bool = true
var showSelectionUI: Bool = true
}
// MARK: - Window Selection Overlay
@available(macOS 12.3, *)
class WindowSelectionOverlay: NSObject {
private var windowCaptureManager: WindowCaptureManager
private var overlayWindows: [NSWindow] = [] // Dit zijn de semi-transparante overlay windows per scherm
private var globalMouseMonitor: Any?
init(windowCaptureManager: WindowCaptureManager) {
self.windowCaptureManager = windowCaptureManager
super.init()
}
func show() {
print("🎬 OVERLAY SHOW: Starting to create overlays...")
hide() // Clean up any existing overlays
print("🪟 WindowCaptureManager: Starting window selection on \(NSScreen.screens.count) screens")
for (index, screen) in NSScreen.screens.enumerated() {
print(" Screen \(index): \(screen.customLocalizedName) - Frame: \(screen.frame)")
}
// Create a separate overlay window for each screen
for (index, screen) in NSScreen.screens.enumerated() {
print("🎬 OVERLAY SHOW: Creating overlay for screen \(index): \(screen.customLocalizedName)")
let overlayWindow = createOverlayWindow(for: screen)
overlayWindows.append(overlayWindow)
print("🎬 OVERLAY SHOW: Window created, ordering front...")
// IMPROVED: Proper window ordering and activation
overlayWindow.orderFrontRegardless()
overlayWindow.makeKeyAndOrderFront(nil)
print("🎬 OVERLAY SHOW: Window ordered front, checking content view...")
// Force the content view to display immediately
if let contentView = overlayWindow.contentView {
print("🎬 OVERLAY SHOW: Content view found, forcing display...")
contentView.needsDisplay = true
contentView.displayIfNeeded()
print("🎨 FORCED DISPLAY for screen \(screen.customLocalizedName)")
} else {
print("❌ OVERLAY SHOW: NO CONTENT VIEW found for screen \(screen.customLocalizedName)")
}
}
print("🎬 OVERLAY SHOW: All overlays created, setting up mouse monitoring...")
// Setup global mouse monitoring for all screens
setupGlobalMouseMonitoring()
// Force app activation
NSApp.activate(ignoringOtherApps: true)
print("🪟 Window selection overlay shown on \(overlayWindows.count) screens")
// ADDITIONAL: Force a refresh after a short delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
print("🎬 DELAYED REFRESH: Starting...")
for (index, window) in self.overlayWindows.enumerated() {
if let contentView = window.contentView {
contentView.needsDisplay = true
print("🎨 DELAYED REFRESH for window \(index)")
} else {
print("❌ DELAYED REFRESH: NO CONTENT VIEW for window \(index)")
}
}
}
// Debug: List available windows with their positions
print("🪟 Available windows for capture:")
for (index, window) in windowCaptureManager.availableWindows.enumerated() {
let frame = window.frame
let app = window.owningApplication?.applicationName ?? "Unknown"
print(" \(index): \(window.title ?? "Untitled") (\(app)) - Frame: \(frame)")
}
}
func hide() {
// Remove all overlay windows
for window in overlayWindows {
window.orderOut(nil)
}
overlayWindows.removeAll()
// Remove global mouse monitor
if let monitor = globalMouseMonitor {
NSEvent.removeMonitor(monitor)
globalMouseMonitor = nil
}
print("🪟 Window selection overlay hidden")
}
private func createOverlayWindow(for screen: NSScreen) -> NSWindow {
let window = NSWindow(
contentRect: screen.frame,
styleMask: [.borderless, .fullSizeContentView],
backing: .buffered,
defer: false
)
// FIXED: Window configuration for proper drawing and event handling
window.backgroundColor = NSColor.clear // Clear background so we can draw our own
window.isOpaque = false
window.level = NSWindow.Level.floating // Lower level but still above normal windows
window.ignoresMouseEvents = false
window.acceptsMouseMovedEvents = true
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .ignoresCycle]
// CRITICAL: Ensure window can receive events and display properly
window.displaysWhenScreenProfileChanges = true
window.animationBehavior = .none
window.hidesOnDeactivate = false
// IMPORTANT: Enable automatic invalidation for proper drawing
window.invalidateShadow()
window.viewsNeedDisplay = true
// Create content view for this screen
let overlayView = WindowSelectionOverlayView(
windowCaptureManager: windowCaptureManager,
screen: screen
)
// CRITICAL: Properly configure the view for drawing
overlayView.wantsLayer = false // Use direct drawing for better control
overlayView.needsDisplay = true
window.contentView = overlayView
// Position window exactly on this screen
window.setFrame(screen.frame, display: true) // Force display update
print("🪟 Created overlay window for screen: \(screen.customLocalizedName) at level \(window.level.rawValue)")
print("🪟 Window frame: \(window.frame)")
print("🪟 Content view frame: \(overlayView.frame)")
print("🪟 Content view needsDisplay: \(overlayView.needsDisplay)")
return window
}
private func setupGlobalMouseMonitoring() {
globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .keyDown]) { [weak self] event in
guard let self = self else { return }
if event.type == .leftMouseDown {
let location = NSEvent.mouseLocation
print("🎯 Global mouse click at: \(location) for window capture")
print("🔥 DEBUG: Available windows: \(self.windowCaptureManager.availableWindows.count)")
// 🔥 FIXED: In window capture mode, any click should capture the window
print("🪟 Click detected in window capture mode - capturing window")
self.handleMouseClick(at: location)
} else if event.type == .keyDown && event.keyCode == 53 { // ESC
print("⌨️ ESC key pressed, canceling window/display capture")
self.windowCaptureManager.deactivateWindowSelectionMode()
}
}
print("🎯 Global mouse monitoring setup for window capture")
}
private func handleMouseClick(at location: NSPoint) {
// HERSTEL: Zoek naar een window
if let window = windowCaptureManager.findWindowAt(location: location) {
let selectedWindowTitle = window.title ?? "Untitled"
print("🎯 Window selected: \(selectedWindowTitle) at \(location)")
windowCaptureManager.deactivateWindowSelectionMode()
Task {
await windowCaptureManager.captureWindow(window) // Gebruik de captureWindow methode
}
} else {
print(" No window found at \(location), canceling")
windowCaptureManager.deactivateWindowSelectionMode()
}
}
}
// MARK: - Window Selection Overlay View
@available(macOS 12.3, *)
class WindowSelectionOverlayView: NSView {
private var windowCaptureManager: WindowCaptureManager
private var screen: NSScreen // Het NSScreen object waar deze view op getekend wordt
private var hoveredWindow: SCWindow? // HERSTEL: terug naar SCWindow
private var trackingArea: NSTrackingArea?
init(windowCaptureManager: WindowCaptureManager, screen: NSScreen) {
self.windowCaptureManager = windowCaptureManager
self.screen = screen
super.init(frame: NSRect(origin: .zero, size: screen.frame.size))
self.wantsLayer = false
self.layerContentsRedrawPolicy = .onSetNeedsDisplay
self.autoresizingMask = [.width, .height]
self.translatesAutoresizingMaskIntoConstraints = true
NSLog("🎨✅ VIEW INIT for screen: %@, Frame: %@, Bounds: %@", screen.customLocalizedName, NSStringFromRect(self.frame), NSStringFromRect(self.bounds))
setupTrackingArea()
self.needsDisplay = true
DispatchQueue.main.async {
NSLog("🎨✅ VIEW INIT DELAYED DISPLAY for screen: %@", self.screen.customLocalizedName)
self.needsDisplay = true
self.displayIfNeeded() // Force display after setup
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupTrackingArea() {
// Remove existing tracking area
if let existingArea = trackingArea {
removeTrackingArea(existingArea)
}
// Create new tracking area that covers this view
trackingArea = NSTrackingArea(
rect: bounds,
options: [.activeAlways, .mouseMoved, .inVisibleRect],
owner: self,
userInfo: nil
)
if let trackingArea = trackingArea {
addTrackingArea(trackingArea)
}
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
setupTrackingArea()
}
override func draw(_ dirtyRect: NSRect) {
// NSLog("🎨 DRAWING ENTRY POINT for view on screen: %@ with dirtyRect: %@", self.window?.screen?.customLocalizedName ?? "UnknownScreen", NSStringFromRect(dirtyRect))
super.draw(dirtyRect)
NSColor.black.withAlphaComponent(0.05).setFill()
dirtyRect.fill() // Lichte overlay over het hele scherm
// HERSTEL: Teken highlight voor hoveredWindow
guard let currentHoveredWindow = self.hoveredWindow,
let currentViewNSScreen = self.window?.screen, // Het NSScreen waar deze view op is
let mainScreenForConversion = self.windowCaptureManager.determinedMainScreen else {
// Als er geen window gehovered is, teken dan alleen de instructietekst op het hoofdscherm
if self.window?.screen == self.windowCaptureManager.determinedMainScreen { // Gebruik determinedMainScreen voor consistentie
drawInstructionText(on: self.window!.screen!, viewSize: self.bounds.size)
}
return
}
let scWindowFrame = currentHoveredWindow.frame
// Converteer SCWindow.frame naar Globale AppKit coördinaten (Y-omhoog, oorsprong linksonder hoofd NSScreen)
let globalHoveredWindowFrame = self.windowCaptureManager.convertSCWindowFrameToGlobal(scWindowFrame, mainScreen: mainScreenForConversion)
// Converteer de globale frame naar coördinaten lokaal aan *deze* view.
// De (0,0) van deze view is de linksonderhoek van het currentViewNSScreen.
let localHighlightX = globalHoveredWindowFrame.origin.x - currentViewNSScreen.frame.origin.x
let localHighlightY = globalHoveredWindowFrame.origin.y - currentViewNSScreen.frame.origin.y
let rectToHighlightInViewCoordinates = NSRect(x: localHighlightX,
y: localHighlightY,
width: globalHoveredWindowFrame.width,
height: globalHoveredWindowFrame.height)
// NSLog("🎨 DRAW on view for NSScreen \'%@\': Highlighting window \'%@\'", currentViewNSScreen.customLocalizedName, currentHoveredWindow.title ?? "N/A")
// NSLog("🎨 SCWindow Frame: %@ (Layer: \(currentHoveredWindow.windowLayer))", NSStringFromRect(scWindowFrame))
// NSLog("🎨 Global Hovered Frame: %@", NSStringFromRect(globalHoveredWindowFrame))
// NSLog("🎨 Calculated Highlight Rect (local to view on screen \'%@\'): %@", currentViewNSScreen.customLocalizedName, NSStringFromRect(rectToHighlightInViewCoordinates))
// Teken alleen als de highlight rect daadwerkelijk de bounds van deze view snijdt.
// Dit is belangrijk omdat een venster over meerdere schermen kan spannen.
if self.bounds.intersects(rectToHighlightInViewCoordinates) {
NSColor.blue.withAlphaComponent(0.3).setFill()
// We clippen de highlight path naar de intersectie met de view bounds voor het geval het venster groter is dan dit schermsegment.
let drawingRect = self.bounds.intersection(rectToHighlightInViewCoordinates)
let highlightPath = NSBezierPath(rect: drawingRect)
highlightPath.fill()
NSColor.blue.withAlphaComponent(0.8).setStroke()
highlightPath.lineWidth = 2 // Standaard lijndikte voor venster
highlightPath.stroke()
// NSLog("🎨 Window highlight drawn (intersected rect: %@).", NSStringFromRect(drawingRect))
} else {
// NSLog("🎨 Highlight rect %@ does NOT intersect view bounds %@. Not drawing highlight on this screen segment.", NSStringFromRect(rectToHighlightInViewCoordinates), NSStringFromRect(self.bounds))
}
// Teken instructietekst (alleen op de primary overlay)
if currentViewNSScreen == self.windowCaptureManager.determinedMainScreen { // Gebruik determinedMainScreen
drawInstructionText(on: currentViewNSScreen, viewSize: self.bounds.size)
}
}
private func drawInstructionText(on screen: NSScreen, viewSize: NSSize) {
// HERSTEL: Tekst voor window selectie
let instructionText = "Hover to select a window, Click to capture, ESC to cancel"
let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: 18, weight: .medium),
.foregroundColor: NSColor.white.withAlphaComponent(0.9),
.strokeColor: NSColor.black.withAlphaComponent(0.5),
.strokeWidth: -2.0,
.paragraphStyle: {
let style = NSMutableParagraphStyle()
style.alignment = .center
return style
}()
]
let attributedString = NSAttributedString(string: instructionText, attributes: attributes)
let textSize = attributedString.size()
let textRect = NSRect(x: (viewSize.width - textSize.width) / 2,
y: viewSize.height - textSize.height - 30,
width: textSize.width,
height: textSize.height)
let backgroundPadding: CGFloat = 10
let backgroundRect = NSRect(
x: textRect.origin.x - backgroundPadding,
y: textRect.origin.y - backgroundPadding,
width: textRect.width + (2 * backgroundPadding),
height: textRect.height + (2 * backgroundPadding)
)
let BORDER_RADIUS: CGFloat = 10
let textBackgroundPath = NSBezierPath(roundedRect: backgroundRect, xRadius: BORDER_RADIUS, yRadius: BORDER_RADIUS)
NSColor.black.withAlphaComponent(0.4).setFill()
textBackgroundPath.fill()
attributedString.draw(in: textRect)
// NSLog("🎨 Instruction text drawn on main screen.") // Keep this one less verbose for now
}
override func mouseMoved(with event: NSEvent) {
let globalLocation = NSEvent.mouseLocation
var currentMouseNSScreen: NSScreen?
for s in NSScreen.screens {
if s.frame.contains(globalLocation) {
currentMouseNSScreen = s
break
}
}
// Als de muis niet op het scherm van DEZE specifieke overlay view is, reset hoveredWindow voor DEZE view.
// Dit zorgt ervoor dat een venster dat over meerdere schermen spant, alleen gehighlight wordt op het scherm waar de muis is.
guard let mouseNSScreen = currentMouseNSScreen, mouseNSScreen.displayID == self.screen.displayID else {
if hoveredWindow != nil {
hoveredWindow = nil
needsDisplay = true
}
return
}
// Muis is op het scherm van deze view. Kijk welk SCWindow (indien aanwezig) gehovered wordt.
let foundWindow = windowCaptureManager.findWindowAt(location: globalLocation)
if hoveredWindow != foundWindow { // Vergelijk SCWindow objecten direct
hoveredWindow = foundWindow
needsDisplay = true
// if foundWindow != nil {
// NSLog("🖱 Hovered window SET to: \'%@\' (ID: \(foundWindow!.windowID), Layer: \(foundWindow!.windowLayer)) on screen %@", foundWindow!.title ?? "N/A", self.screen.customLocalizedName)
// } else {
// NSLog("🖱 💨 No window hovered on screen %@ at %@", self.screen.customLocalizedName, NSStringFromPoint(globalLocation))
// }
}
}
// 🔥 NIEUW: Handle mouse clicks directly in the overlay view
override func mouseDown(with event: NSEvent) {
let globalLocation = NSEvent.mouseLocation
print("🔥 OVERLAY: Mouse click detected at \(globalLocation)")
// Find window at click location
if let window = windowCaptureManager.findWindowAt(location: globalLocation) {
let selectedWindowTitle = window.title ?? "Untitled"
print("🎯 ✅ OVERLAY: Window selected: \(selectedWindowTitle) at \(globalLocation)")
// Deactivate window selection mode first
windowCaptureManager.deactivateWindowSelectionMode()
// Capture the window
Task {
print("🔥 OVERLAY: Starting window capture task for \(selectedWindowTitle)")
await windowCaptureManager.captureWindow(window)
print("🔥 OVERLAY: Window capture task completed for \(selectedWindowTitle)")
}
} else {
print("❌ OVERLAY: No window found at \(globalLocation)")
print("🔥 DEBUG: Available windows count: \(windowCaptureManager.availableWindows.count)")
// Extra debug: List all windows and their positions
for (index, win) in windowCaptureManager.availableWindows.enumerated() {
let frame = win.frame
let isPointInFrame = NSPointInRect(globalLocation, frame)
print("🔥 DEBUG: Window \(index): \(win.title ?? "Untitled") - Frame: \(frame) - Contains point: \(isPointInFrame)")
}
windowCaptureManager.deactivateWindowSelectionMode()
}
}
override func keyDown(with event: NSEvent) {
if event.keyCode == 53 { // ESC
windowCaptureManager.deactivateWindowSelectionMode()
}
}
override var acceptsFirstResponder: Bool {
return true
}
}
// MARK: - ScreenshotApp Extension for Window Capture Integration
extension ScreenshotApp {
// Add window capture manager property to ScreenshotApp
private static var windowCaptureManagerKey: UInt8 = 0
var windowCaptureManager: WindowCaptureManager? {
get {
return objc_getAssociatedObject(self, &ScreenshotApp.windowCaptureManagerKey) as? WindowCaptureManager
}
set {
objc_setAssociatedObject(self, &ScreenshotApp.windowCaptureManagerKey, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
func initializeWindowCaptureManager() {
if #available(macOS 12.3, *) {
windowCaptureManager = WindowCaptureManager(screenshotApp: self)
print("✅ WindowCaptureManager initialized")
} else {
print("⚠️ WindowCaptureManager requires macOS 12.3 or later")
}
}
func processWindowCapture(image: NSImage, windowTitle: String?) {
// Process window capture similar to regular screenshot capture
print("🪟 Processing window capture: \(windowTitle ?? "Untitled Window")")
let tempURL = createTempUrl()
setTempFileURL(tempURL)
guard let tiffData = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiffData),
let pngData = bitmap.representation(using: .png, properties: [:]) else {
print("❌ Failed to convert window capture to PNG")
return
}
do {
try pngData.write(to: tempURL)
print("💾 Window capture saved temporarily: \(tempURL.path)")
// Play sound if enabled
if SettingsManager.shared.playSoundOnCapture {
NSSound(named: "Glass")?.play()
}
// Show preview with window title context
showPreview(image: image, windowTitle: windowTitle)
} catch {
print("❌ Failed to save window capture: \(error)")
}
}
private func showPreview(image: NSImage, windowTitle: String?) {
// Use existing preview logic but potentially add window title to UI
showPreview(image: image)
// You could extend the preview UI to show window title
if let title = windowTitle {
print("📸 Window captured: \(title)")
}
}
}