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>
114 lines
3.9 KiB
Swift
114 lines
3.9 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|