mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-29 08:04:32 +00:00
Working and Hidden - with gemini api
This commit is contained in:
parent
2f7821f4fd
commit
fb4f7a92c4
4 changed files with 236 additions and 522 deletions
|
|
@ -1186,6 +1186,12 @@
|
||||||
},
|
},
|
||||||
"Screen Assistant:" : {
|
"Screen Assistant:" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Screenshot options" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Screenshot Type" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Search clipboard..." : {
|
"Search clipboard..." : {
|
||||||
|
|
||||||
|
|
@ -1419,6 +1425,9 @@
|
||||||
},
|
},
|
||||||
"System Performance Monitor" : {
|
"System Performance Monitor" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Take area screenshot" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"TheDynamicIsland" : {
|
"TheDynamicIsland" : {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -606,18 +606,38 @@ struct StreamingChatMessageBubble: View {
|
||||||
struct ScreenshotButton: View {
|
struct ScreenshotButton: View {
|
||||||
@ObservedObject var screenAssistantManager = ScreenAssistantManager.shared
|
@ObservedObject var screenAssistantManager = ScreenAssistantManager.shared
|
||||||
@StateObject private var screenshotTool = ScreenshotSnippingTool.shared
|
@StateObject private var screenshotTool = ScreenshotSnippingTool.shared
|
||||||
|
@State private var showingScreenshotOptions = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: startScreenshot) {
|
HStack(spacing: 4) {
|
||||||
|
// Main screenshot button
|
||||||
|
Button(action: startQuickScreenshot) {
|
||||||
Image(systemName: getIconName())
|
Image(systemName: getIconName())
|
||||||
.foregroundColor(getIconColor())
|
.foregroundColor(getIconColor())
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
.help(getHelpText())
|
.help("Take area screenshot")
|
||||||
.disabled(screenshotTool.isSnipping)
|
.disabled(screenshotTool.isSnipping)
|
||||||
.scaleEffect(screenshotTool.isSnipping ? 1.1 : 1.0)
|
.scaleEffect(screenshotTool.isSnipping ? 1.1 : 1.0)
|
||||||
.animation(.easeInOut(duration: 0.2), value: screenshotTool.isSnipping)
|
.animation(.easeInOut(duration: 0.2), value: screenshotTool.isSnipping)
|
||||||
|
|
||||||
|
// Options dropdown button
|
||||||
|
Button(action: { showingScreenshotOptions.toggle() }) {
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.system(size: 12))
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.help("Screenshot options")
|
||||||
|
.disabled(screenshotTool.isSnipping)
|
||||||
|
.popover(isPresented: $showingScreenshotOptions) {
|
||||||
|
ScreenshotOptionsPopover { type in
|
||||||
|
startScreenshot(type: type)
|
||||||
|
showingScreenshotOptions = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getIconName() -> String {
|
private func getIconName() -> String {
|
||||||
|
|
@ -636,21 +656,22 @@ struct ScreenshotButton: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getHelpText() -> String {
|
private func startQuickScreenshot() {
|
||||||
return screenshotTool.isSnipping ? "Taking screenshot..." : "Take screenshot"
|
// Default to area screenshot for quick action
|
||||||
|
startScreenshot(type: .area)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startScreenshot() {
|
private func startScreenshot(type: ScreenshotSnippingTool.ScreenshotType) {
|
||||||
// Start snipping with direct callback (ScreenshotApp-based approach)
|
// Start snipping with direct callback (ScreenshotApp-based approach)
|
||||||
screenshotTool.startSnipping { [weak screenAssistantManager] screenshotURL in
|
screenshotTool.startSnipping(type: type) { [weak screenAssistantManager] screenshotURL in
|
||||||
guard let manager = screenAssistantManager else {
|
guard let manager = screenAssistantManager else {
|
||||||
print("❌ ScreenshotTool: ScreenAssistantManager deallocated during callback")
|
print("❌ ScreenshotTool: ScreenAssistantManager deallocated during callback")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
print("📁 ScreenshotTool: Adding screenshot to chat: \(screenshotURL.lastPathComponent)")
|
print("📁 ScreenshotTool: Adding \(type.displayName.lowercased()) screenshot to chat: \(screenshotURL.lastPathComponent)")
|
||||||
manager.addFiles([screenshotURL])
|
manager.addFiles([screenshotURL])
|
||||||
print("📸 Screenshot captured and added to chat successfully")
|
print("📸 \(type.displayName) screenshot captured and added to chat successfully")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -670,3 +691,140 @@ struct ChatPanelsVisualEffectView: NSViewRepresentable {
|
||||||
|
|
||||||
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {}
|
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Screenshot Options Popover (Hidden from Screen Recording)
|
||||||
|
struct ScreenshotOptionsPopover: View {
|
||||||
|
let onOptionSelected: (ScreenshotSnippingTool.ScreenshotType) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Text("Screenshot Type")
|
||||||
|
.font(.headline)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
ScreenshotOptionButton(
|
||||||
|
type: .area,
|
||||||
|
description: "Select an area",
|
||||||
|
onTap: onOptionSelected
|
||||||
|
)
|
||||||
|
|
||||||
|
ScreenshotOptionButton(
|
||||||
|
type: .window,
|
||||||
|
description: "Select a window",
|
||||||
|
onTap: onOptionSelected
|
||||||
|
)
|
||||||
|
|
||||||
|
ScreenshotOptionButton(
|
||||||
|
type: .full,
|
||||||
|
description: "Capture full screen",
|
||||||
|
onTap: onOptionSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.frame(width: 200)
|
||||||
|
.background(
|
||||||
|
ScreenshotPopoverBackground()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Screenshot Option Button
|
||||||
|
struct ScreenshotOptionButton: View {
|
||||||
|
let type: ScreenshotSnippingTool.ScreenshotType
|
||||||
|
let description: String
|
||||||
|
let onTap: (ScreenshotSnippingTool.ScreenshotType) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: { onTap(type) }) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: type.iconName)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.frame(width: 20, alignment: .center)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(type.displayName)
|
||||||
|
.font(.system(size: 14, weight: .medium))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text(description)
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.fill(Color.clear)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.stroke(Color.clear, lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.fill(Color(NSColor.controlBackgroundColor))
|
||||||
|
.opacity(0.5)
|
||||||
|
)
|
||||||
|
.onHover { isHovered in
|
||||||
|
// Add subtle hover effect if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Screenshot Popover Background (Hidden from Screen Recording)
|
||||||
|
struct ScreenshotPopoverBackground: NSViewRepresentable {
|
||||||
|
func makeNSView(context: Context) -> NSView {
|
||||||
|
let view = NSView()
|
||||||
|
|
||||||
|
// Apply screen capture protection like our other panels
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let window = view.window {
|
||||||
|
self.updateScreenCaptureVisibility(window: window)
|
||||||
|
self.setupScreenCaptureObserver(window: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: NSView, context: Context) {
|
||||||
|
// Update screen capture protection when view updates
|
||||||
|
if let window = nsView.window {
|
||||||
|
updateScreenCaptureVisibility(window: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateScreenCaptureVisibility(window: NSWindow) {
|
||||||
|
let shouldHide = Defaults[.hidePanelsFromScreenCapture]
|
||||||
|
|
||||||
|
if shouldHide {
|
||||||
|
// Hide from screen capture and recording
|
||||||
|
window.sharingType = .none
|
||||||
|
print("🙈 ScreenshotPopover: Hidden from screen capture and recordings")
|
||||||
|
} else {
|
||||||
|
// Allow normal screen capture
|
||||||
|
window.sharingType = .readOnly
|
||||||
|
print("👁️ ScreenshotPopover: Visible in screen capture and recordings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupScreenCaptureObserver(window: NSWindow) {
|
||||||
|
// Observe changes to hidePanelsFromScreenCapture setting
|
||||||
|
Defaults.observe(.hidePanelsFromScreenCapture) { [weak window] change in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let window = window else { return }
|
||||||
|
self.updateScreenCaptureVisibility(window: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,40 @@ class ScreenshotSnippingTool: NSObject, ObservableObject {
|
||||||
@Published var isSnipping = false
|
@Published var isSnipping = false
|
||||||
private var completion: ((URL) -> Void)?
|
private var completion: ((URL) -> Void)?
|
||||||
|
|
||||||
|
// MARK: - Screenshot Types (Based on ScreenshotApp)
|
||||||
|
enum ScreenshotType {
|
||||||
|
case full
|
||||||
|
case window
|
||||||
|
case area
|
||||||
|
|
||||||
|
var processArguments: [String] {
|
||||||
|
switch self {
|
||||||
|
case .full:
|
||||||
|
return ["-c"] // -c = clipboard
|
||||||
|
case .window:
|
||||||
|
return ["-cw"] // -c = clipboard, -w = window selection
|
||||||
|
case .area:
|
||||||
|
return ["-cs"] // -c = clipboard, -s = area selection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .full: return "Full Screen"
|
||||||
|
case .window: return "Window"
|
||||||
|
case .area: return "Area"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var iconName: String {
|
||||||
|
switch self {
|
||||||
|
case .full: return "rectangle.dashed"
|
||||||
|
case .window: return "macwindow"
|
||||||
|
case .area: return "viewfinder.rectangular"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum ScreenshotError: Error {
|
enum ScreenshotError: Error {
|
||||||
case captureFailed
|
case captureFailed
|
||||||
case noImageInPasteboard
|
case noImageInPasteboard
|
||||||
|
|
@ -25,26 +59,39 @@ class ScreenshotSnippingTool: NSObject, ObservableObject {
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Simple API (Based on ScreenshotApp Implementation)
|
// MARK: - Enhanced API (Based on ScreenshotApp Implementation)
|
||||||
func startSnipping(completion: @escaping (URL) -> Void) {
|
func startSnipping(type: ScreenshotType = .area, completion: @escaping (URL) -> Void) {
|
||||||
guard !isSnipping else { return }
|
guard !isSnipping else { return }
|
||||||
|
|
||||||
print("🖼️ ScreenshotTool: Starting area screenshot using screencapture tool")
|
print("🖼️ ScreenshotTool: Starting \(type.displayName.lowercased()) screenshot using screencapture tool")
|
||||||
self.completion = completion
|
self.completion = completion
|
||||||
isSnipping = true
|
isSnipping = true
|
||||||
|
|
||||||
// Use the same approach as ScreenshotApp - direct screencapture command
|
// Use the same approach as ScreenshotApp - direct screencapture command
|
||||||
takeAreaScreenshot()
|
takeScreenshot(type: type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Convenience Methods for Different Types
|
||||||
|
func startAreaScreenshot(completion: @escaping (URL) -> Void) {
|
||||||
|
startSnipping(type: .area, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startFullScreenshot(completion: @escaping (URL) -> Void) {
|
||||||
|
startSnipping(type: .full, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startWindowScreenshot(completion: @escaping (URL) -> Void) {
|
||||||
|
startSnipping(type: .window, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - ScreenshotApp-Style Implementation
|
// MARK: - ScreenshotApp-Style Implementation
|
||||||
private func takeAreaScreenshot() {
|
private func takeScreenshot(type: ScreenshotType) {
|
||||||
let task = Process()
|
let task = Process()
|
||||||
task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture")
|
task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture")
|
||||||
task.arguments = ["-cs"] // -c = clipboard, -s = area selection
|
task.arguments = type.processArguments
|
||||||
|
|
||||||
do {
|
do {
|
||||||
print("📸 ScreenshotTool: Running screencapture -cs command")
|
print("📸 ScreenshotTool: Running screencapture \(type.processArguments.joined(separator: " ")) command")
|
||||||
try task.run()
|
try task.run()
|
||||||
task.waitUntilExit()
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,500 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue