Add image editor with annotation tools
Integrates an editor accessible via "Letzte Aufnahme bearbeiten…" or "Bild bearbeiten…" in the menu bar. Supports arrow, rectangle, ellipse, and text annotations with color picker, line width, undo/redo, and export to file or clipboard. New files in Sources/Editor/: - AnnotationItem/Document: data model and state management - AnnotationCanvasView: NSView with CG rendering and mouse interaction - AnnotationRenderer: shared rendering for canvas and image export - EditorToolbarView: SwiftUI toolbar with tool selection and actions - EditorWindowController: window lifecycle following SettingsWindowController pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
final class EditorWindowController: NSWindowController, NSWindowDelegate {
|
||||
|
||||
private let annotationDoc = AnnotationDocument()
|
||||
private let baseImage: NSImage
|
||||
private let sourceURL: URL?
|
||||
private let completion: (NSImage?) -> Void
|
||||
private let canvasView = AnnotationCanvasView()
|
||||
|
||||
init(image: NSImage, sourceURL: URL?, completion: @escaping (NSImage?) -> Void) {
|
||||
self.baseImage = image
|
||||
self.sourceURL = sourceURL
|
||||
self.completion = completion
|
||||
|
||||
let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1280, height: 800)
|
||||
let maxWidth = screenFrame.width * 0.8
|
||||
let maxHeight = screenFrame.height * 0.8
|
||||
let scale = min(maxWidth / image.size.width, maxHeight / image.size.height, 1.0)
|
||||
let windowWidth = max(image.size.width * scale, 600)
|
||||
let windowHeight = max(image.size.height * scale, 400) + 52 // toolbar height
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: windowWidth, height: windowHeight),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
window.title = "Bildeditor"
|
||||
window.isReleasedWhenClosed = false
|
||||
window.center()
|
||||
window.minSize = NSSize(width: 500, height: 400)
|
||||
|
||||
super.init(window: window)
|
||||
window.delegate = self
|
||||
|
||||
canvasView.baseImage = image
|
||||
canvasView.document = annotationDoc
|
||||
canvasView.onDocumentChanged = { [weak self] in
|
||||
self?.canvasView.needsDisplay = true
|
||||
}
|
||||
canvasView.autoresizingMask = [.width, .height]
|
||||
|
||||
let toolbarHosting = NSHostingView(rootView: EditorToolbarView(
|
||||
document: annotationDoc,
|
||||
onSave: { [weak self] in self?.save() },
|
||||
onCopyToClipboard: { [weak self] in self?.copyToClipboard() },
|
||||
onCancel: { [weak self] in self?.cancel() }
|
||||
))
|
||||
toolbarHosting.translatesAutoresizingMaskIntoConstraints = true
|
||||
|
||||
let container = NSView(frame: window.contentRect(forFrameRect: window.frame))
|
||||
container.autoresizingMask = [.width, .height]
|
||||
|
||||
toolbarHosting.frame = NSRect(x: 0, y: container.bounds.height - 52,
|
||||
width: container.bounds.width, height: 52)
|
||||
toolbarHosting.autoresizingMask = [.width, .minYMargin]
|
||||
|
||||
canvasView.frame = NSRect(x: 0, y: 0,
|
||||
width: container.bounds.width,
|
||||
height: container.bounds.height - 52)
|
||||
canvasView.autoresizingMask = [.width, .height]
|
||||
|
||||
container.addSubview(toolbarHosting)
|
||||
container.addSubview(canvasView)
|
||||
window.contentView = container
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func showEditor() {
|
||||
NSApplication.shared.setActivationPolicy(.regular)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
window?.makeFirstResponder(canvasView)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
private func renderFinal() -> NSImage {
|
||||
AnnotationRenderer.render(annotations: annotationDoc.items, onto: baseImage)
|
||||
}
|
||||
|
||||
private func save() {
|
||||
let image = renderFinal()
|
||||
completion(image)
|
||||
close()
|
||||
}
|
||||
|
||||
private func copyToClipboard() {
|
||||
let image = renderFinal()
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.writeObjects([image])
|
||||
close()
|
||||
}
|
||||
|
||||
private func cancel() {
|
||||
completion(nil)
|
||||
close()
|
||||
}
|
||||
|
||||
// MARK: - NSWindowDelegate
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
let visibleWindows = NSApplication.shared.windows.filter { $0.isVisible && $0 != window }
|
||||
if visibleWindows.isEmpty {
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user