🎉 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.
This commit is contained in:
2025-06-28 16:15:15 +02:00
commit 0dabed11d2
63 changed files with 25727 additions and 0 deletions

View File

@@ -0,0 +1,998 @@
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)")
}
}
}