Working Screen Assistant with snipping

This commit is contained in:
Hariharan Mudaliar 2025-09-22 00:39:40 +05:30
commit 2f7821f4fd
20 changed files with 4007 additions and 2 deletions

View file

@ -0,0 +1,500 @@
// This file has been replaced - keeping for reference only
// MARK: - Screenshot Snipping Tool
class ScreenshotSnippingTool: NSObject, ObservableObject {
static let shared = ScreenshotSnippingTool()
@Published var isSnipping = false
@Published var hasPermissions = false
@Published var permissionStatus: PermissionStatus = .unknown
private var snippingWindow: SnippingWindow?
enum PermissionStatus {
case unknown
case granted
case denied
case restricted
}
override init() {
super.init()
checkPermissionsAsync()
}
private func checkPermissionsAsync() {
Task {
await checkPermissions()
}
}
@MainActor
func checkPermissions() async {
print("🔍 ScreenshotTool: Checking permissions for bundle: \(Bundle.main.bundleIdentifier ?? "unknown")")
do {
// First try a simple content check to see if we can access ScreenCaptureKit
let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false)
// If we get here without error, permissions are granted
self.hasPermissions = true
self.permissionStatus = .granted
print("✅ ScreenshotTool: Permissions granted for \(content.displays.count) displays")
} catch let error as NSError {
print("❌ ScreenshotTool: Permission error - Code: \(error.code), Domain: \(error.domain)")
// Handle specific error codes
switch error.code {
case -3801: // User declined TCC permission
self.hasPermissions = false
self.permissionStatus = .denied
print("❌ ScreenshotTool: Permission denied by user - TCC database entry missing or denied")
case -3802: // App is not authorized for screen capture
self.hasPermissions = false
self.permissionStatus = .restricted
print("❌ ScreenshotTool: App not authorized for screen capture - System restriction")
default:
self.hasPermissions = false
self.permissionStatus = .unknown
print("❌ ScreenshotTool: Unknown permission error - \(error.localizedDescription)")
}
// Provide user guidance
DispatchQueue.main.async {
self.showPermissionGuidance()
}
}
}
private func showPermissionGuidance() {
let alert = NSAlert()
alert.messageText = "Screen Recording Permission Required"
alert.informativeText = """
Dynamic Island needs screen recording permission to capture screenshots.
To enable this:
1. Open System Settings (or System Preferences)
2. Go to Privacy & Security → Screen Recording
3. Enable 'Dynamic Island'
4. Restart the app if needed
"""
alert.alertStyle = .warning
alert.addButton(withTitle: "Open System Settings")
alert.addButton(withTitle: "Cancel")
let response = alert.runModal()
if response == .alertFirstButtonReturn {
// Open System Settings to Screen Recording
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") {
NSWorkspace.shared.open(url)
}
}
}
func requestPermissions() async {
// Only request if we don't already have them
guard !hasPermissions else { return }
await checkPermissions()
}
func startSnipping() {
guard !isSnipping else { return }
// Check permissions first
if !hasPermissions {
print("❌ ScreenshotTool: No permissions - checking status")
Task {
await requestPermissions()
}
return
}
print("🖼️ ScreenshotTool: Starting screen snipping")
isSnipping = true
// Hide all app windows temporarily
hideAppWindows()
// Create snipping overlay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.createSnippingOverlay()
}
}
private func hideAppWindows() {
for window in NSApp.windows {
window.orderOut(nil)
}
}
private func restoreAppWindows() {
for window in NSApp.windows {
if window != snippingWindow {
window.orderFront(nil)
}
}
}
private func createSnippingOverlay() {
guard let screen = NSScreen.main else {
print("❌ ScreenshotTool: No main screen available")
finishSnipping()
return
}
print("🎯 ScreenshotTool: Creating snipping overlay")
snippingWindow = SnippingWindow(screen: screen) { [weak self] rect in
guard let self = self else {
print("❌ ScreenshotTool: Self deallocated during area selection")
return
}
self.captureScreenArea(rect)
}
guard let window = snippingWindow else {
print("❌ ScreenshotTool: Failed to create snipping window")
finishSnipping()
return
}
// Just make it key and order front, don't try to make it main
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
// Activate the app to ensure proper focus
NSApp.activate(ignoringOtherApps: true)
print("✅ ScreenshotTool: Snipping overlay created and activated")
}
private func captureScreenArea(_ rect: CGRect) {
print("🖼️ ScreenshotTool: Capturing area: \(rect)")
Task {
do {
let screenshot = try await captureScreenshot(rect: rect)
DispatchQueue.main.async { [weak self] in
guard let self = self else {
print("❌ ScreenshotTool: Self deallocated during screenshot callback")
return
}
print("✅ ScreenshotTool: Screenshot saved successfully")
// Capture screenshot URL before cleanup
let screenshotURL = screenshot
// Finish snipping first to clean up window state
self.finishSnipping()
// Post notification after cleanup with captured URL
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
print("📸 ScreenshotTool: Posting screenshot notification")
NotificationCenter.default.post(
name: Notification.Name("screenshotCaptured"),
object: screenshotURL
)
print("✅ ScreenshotTool: Notification posted successfully")
}
}
} catch {
print("❌ ScreenshotTool: Failed to capture screenshot - \(error)")
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Show error to user
self.showScreenshotError(error)
self.finishSnipping()
}
}
}
}
private func showScreenshotError(_ error: Error) {
let alert = NSAlert()
alert.messageText = "Screenshot Failed"
alert.informativeText = "Failed to capture screenshot: \(error.localizedDescription)"
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
private func captureScreenshot(rect: CGRect) async throws -> URL {
// Use ScreenCaptureKit for modern screenshot capture
let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
guard let display = content.displays.first else {
throw ScreenshotError.noDisplayFound
}
let config = SCStreamConfiguration()
config.width = Int(rect.width)
config.height = Int(rect.height)
config.sourceRect = rect
config.pixelFormat = kCVPixelFormatType_32BGRA
config.showsCursor = false
let filter = SCContentFilter(display: display, excludingWindows: [])
let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config)
// Ensure screenshot directory exists
let screenshotDir = ScreenAssistantManager.screenshotDataDirectory
if !FileManager.default.fileExists(atPath: screenshotDir.path) {
try FileManager.default.createDirectory(at: screenshotDir, withIntermediateDirectories: true)
}
// Save to temporary file
let filename = "screenshot_\(Date().timeIntervalSince1970).png"
let screenshotURL = screenshotDir.appendingPathComponent(filename)
// Create NSImage from CGImage and convert to PNG
let nsImage = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height))
guard let imageData = nsImage.tiffRepresentation,
let bitmapRep = NSBitmapImageRep(data: imageData),
let pngData = bitmapRep.representation(using: NSBitmapImageRep.FileType.png, properties: [:]) else {
throw ScreenshotError.imageConversionFailed
}
do {
try pngData.write(to: screenshotURL)
print("✅ ScreenshotTool: Saved screenshot to: \(screenshotURL.path)")
return screenshotURL
} catch {
print("❌ ScreenshotTool: Failed to write screenshot file - \(error)")
throw ScreenshotError.fileWriteError
}
}
private func finishSnipping() {
guard isSnipping else {
print("⚠️ ScreenshotTool: finishSnipping called but not currently snipping")
return
}
print("🔄 ScreenshotTool: Finishing snipping process")
isSnipping = false
// Close and cleanup snipping window safely
if let window = snippingWindow {
window.orderOut(nil)
window.close()
snippingWindow = nil
print("🪟 ScreenshotTool: Snipping window closed and cleared")
}
// Restore app windows with delay to prevent conflicts
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.restoreAppWindows()
print("🏠 ScreenshotTool: App windows restored")
}
print("✅ ScreenshotTool: Snipping process completed")
}
func cancelSnipping() {
finishSnipping()
}
}
// MARK: - Snipping Window
class SnippingWindow: NSWindow {
private let onAreaSelected: (CGRect) -> Void
private var selectionOverlay: SelectionOverlayView?
init(screen: NSScreen, onAreaSelected: @escaping (CGRect) -> Void) {
self.onAreaSelected = onAreaSelected
super.init(
contentRect: screen.frame,
styleMask: [.borderless, .fullSizeContentView],
backing: .buffered,
defer: false
)
setupWindow(screen: screen)
setupContent()
}
override var canBecomeKey: Bool {
return true
}
override var canBecomeMain: Bool {
return true
}
private func setupWindow(screen: NSScreen) {
backgroundColor = NSColor.black.withAlphaComponent(0.3)
isOpaque = false
level = .screenSaver
ignoresMouseEvents = false
acceptsMouseMovedEvents = true
isMovableByWindowBackground = false
titleVisibility = .hidden
titlebarAppearsTransparent = true
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
setFrame(screen.frame, display: true)
}
private func setupContent() {
selectionOverlay = SelectionOverlayView { [weak self] rect in
self?.onAreaSelected(rect)
}
if let overlay = selectionOverlay {
let hostingView = NSHostingView(rootView: overlay)
contentView = hostingView
}
}
override func keyDown(with event: NSEvent) {
if event.keyCode == 53 { // ESC key
close()
ScreenshotSnippingTool.shared.cancelSnipping()
} else {
super.keyDown(with: event)
}
}
}
// MARK: - Selection Overlay View
struct SelectionOverlayView: NSViewRepresentable {
let onAreaSelected: (CGRect) -> Void
func makeNSView(context: Context) -> SelectionView {
let view = SelectionView()
view.onAreaSelected = onAreaSelected
return view
}
func updateNSView(_ nsView: SelectionView, context: Context) {}
}
// MARK: - Selection View
class SelectionView: NSView {
var onAreaSelected: ((CGRect) -> Void)?
private var startPoint: CGPoint?
private var currentRect: CGRect = .zero
private var isDragging = false
override func awakeFromNib() {
super.awakeFromNib()
wantsLayer = true
layer?.backgroundColor = NSColor.clear.cgColor
}
override func mouseDown(with event: NSEvent) {
startPoint = convert(event.locationInWindow, from: nil)
isDragging = true
currentRect = .zero
needsDisplay = true
}
override func mouseDragged(with event: NSEvent) {
guard let start = startPoint, isDragging else { return }
let currentPoint = convert(event.locationInWindow, from: nil)
let x = min(start.x, currentPoint.x)
let y = min(start.y, currentPoint.y)
let width = abs(currentPoint.x - start.x)
let height = abs(currentPoint.y - start.y)
currentRect = CGRect(x: x, y: y, width: width, height: height)
needsDisplay = true
}
override func mouseUp(with event: NSEvent) {
guard isDragging, !currentRect.isEmpty else {
isDragging = false
return
}
isDragging = false
// Convert to screen coordinates
if let window = self.window {
let screenRect = window.convertToScreen(currentRect)
// Flip Y coordinate for screen capture
let flippedRect = CGRect(
x: screenRect.origin.x,
y: NSScreen.main!.frame.height - screenRect.origin.y - screenRect.height,
width: screenRect.width,
height: screenRect.height
)
onAreaSelected?(flippedRect)
}
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard isDragging, !currentRect.isEmpty else { return }
// Draw selection rectangle
NSColor.systemBlue.withAlphaComponent(0.3).setFill()
currentRect.fill()
NSColor.systemBlue.setStroke()
let path = NSBezierPath(rect: currentRect)
path.lineWidth = 2.0
path.stroke()
// Draw selection info
let infoText = String(format: "%.0f × %.0f", currentRect.width, currentRect.height)
let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: 12),
.foregroundColor: NSColor.white,
.backgroundColor: NSColor.black.withAlphaComponent(0.8)
]
let attributedString = NSAttributedString(string: infoText, attributes: attributes)
let textRect = CGRect(
x: currentRect.maxX - 80,
y: currentRect.minY - 20,
width: 80,
height: 20
)
attributedString.draw(in: textRect)
}
}
// MARK: - Screenshot Error Types
enum ScreenshotError: Error, LocalizedError {
case noDisplayFound
case imageConversionFailed
case fileWriteError
var errorDescription: String? {
switch self {
case .noDisplayFound:
return "No display found for screenshot capture"
case .imageConversionFailed:
return "Failed to convert screenshot image"
case .fileWriteError:
return "Failed to save screenshot file"
}
}
}
// MARK: - ScreenAssistantManager Extension for Screenshot Directory
extension ScreenAssistantManager {
static var screenshotDataDirectory: URL {
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let appDir = appSupport.appendingPathComponent(bundleIdentifier)
let screenshotDir = appDir.appendingPathComponent("Screenshots")
// Create directory if it doesn't exist
try? FileManager.default.createDirectory(at: screenshotDir, withIntermediateDirectories: true)
return screenshotDir
}
}