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:
2026-07-01 17:44:20 +02:00
co-authored by Claude Opus 4.6
parent ecbae7cd21
commit 062285abbf
9 changed files with 841 additions and 6 deletions
+113
View File
@@ -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)
}
}
}