🚀 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.
998 lines
43 KiB
Swift
998 lines
43 KiB
Swift
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)")
|
||
}
|
||
}
|
||
} |