Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cac85402a3 | ||
|
|
d4b0970ed2 |
@@ -6,6 +6,12 @@ final class ResultWindow {
|
|||||||
static let shared = ResultWindow()
|
static let shared = ResultWindow()
|
||||||
|
|
||||||
private var windows: [NSWindow] = []
|
private var windows: [NSWindow] = []
|
||||||
|
/// Kept alive: the hosting controller drives the SwiftUI view we attach
|
||||||
|
/// directly as the window's contentView. Using contentViewController
|
||||||
|
/// would install Auto Layout constraints that force the window to the
|
||||||
|
/// view's fitting size — it shrank whenever the layout branch changed
|
||||||
|
/// (e.g. toggling Actual Size).
|
||||||
|
private var hosts: [ObjectIdentifier: NSHostingController<ResultView>] = [:]
|
||||||
private var nextOrigin = NSPoint(x: 60, y: 60)
|
private var nextOrigin = NSPoint(x: 60, y: 60)
|
||||||
private var openCount = 0
|
private var openCount = 0
|
||||||
|
|
||||||
@@ -13,15 +19,21 @@ final class ResultWindow {
|
|||||||
func show(image: CGImage) {
|
func show(image: CGImage) {
|
||||||
let model = CaptureModel(image: image)
|
let model = CaptureModel(image: image)
|
||||||
let w = NSWindow()
|
let w = NSWindow()
|
||||||
w.contentViewController = NSHostingController(rootView: ResultView(model: model) {
|
let host = NSHostingController(rootView: ResultView(model: model) {
|
||||||
w.close()
|
w.close()
|
||||||
})
|
})
|
||||||
|
w.contentView = host.view
|
||||||
|
hosts[ObjectIdentifier(w)] = host
|
||||||
w.title = "Kapture — Screenshot" + (openCount > 0 ? " \(openCount + 1)" : "")
|
w.title = "Kapture — Screenshot" + (openCount > 0 ? " \(openCount + 1)" : "")
|
||||||
w.level = .floating
|
w.level = .floating
|
||||||
w.styleMask = [.titled, .closable, .resizable]
|
w.styleMask = [.titled, .closable, .resizable]
|
||||||
w.setContentSize(NSSize(width: 694, height: 520))
|
w.setContentSize(NSSize(width: 694, height: 520))
|
||||||
w.isReleasedWhenClosed = false
|
w.isReleasedWhenClosed = false
|
||||||
w.setFrameOrigin(nextOrigin)
|
w.setFrameOrigin(nextOrigin)
|
||||||
|
// Activate so the FIRST click on any control (Actual Size, Copy, the
|
||||||
|
// mode picker…) reaches it. Without this, an inactive LSUIElement
|
||||||
|
// app's windows swallow the initial click just to activate.
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
w.makeKeyAndOrderFront(nil)
|
w.makeKeyAndOrderFront(nil)
|
||||||
|
|
||||||
windows.append(w)
|
windows.append(w)
|
||||||
@@ -40,6 +52,7 @@ final class ResultWindow {
|
|||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
self.windows.removeAll { $0 === window }
|
self.windows.removeAll { $0 === window }
|
||||||
|
self.hosts[ObjectIdentifier(window)] = nil
|
||||||
self.openCount = max(0, self.openCount - 1)
|
self.openCount = max(0, self.openCount - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +120,11 @@ final class CaptureModel: ObservableObject {
|
|||||||
/// Whether the pen is active (drag draws instead of selecting)
|
/// Whether the pen is active (drag draws instead of selecting)
|
||||||
@Published var tool: Tool = .select
|
@Published var tool: Tool = .select
|
||||||
/// Actual-size view: 1 image pixel per screen pixel, scrollable.
|
/// Actual-size view: 1 image pixel per screen pixel, scrollable.
|
||||||
@Published var isActualSize = false
|
/// Persisted so the choice carries over to new windows and launches.
|
||||||
|
@Published var isActualSize: Bool = UserDefaults.standard.bool(forKey: CaptureModel.actualSizeKey) {
|
||||||
|
didSet { UserDefaults.standard.set(isActualSize, forKey: CaptureModel.actualSizeKey) }
|
||||||
|
}
|
||||||
|
private static let actualSizeKey = "actualSizeView"
|
||||||
/// Current drawing color
|
/// Current drawing color
|
||||||
@Published var drawColor: NSColor = .systemRed
|
@Published var drawColor: NSColor = .systemRed
|
||||||
/// Stroke thickness in points (image space)
|
/// Stroke thickness in points (image space)
|
||||||
@@ -525,8 +542,61 @@ private struct CapIconButton: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Result window
|
/// Reports the hosting window's backing scale factor. Used by the
|
||||||
|
/// actual-size view so one image pixel maps to exactly one device pixel.
|
||||||
|
/// SwiftUI's `displayScale` environment is unreliable on macOS and does not
|
||||||
|
/// follow the window when it moves between displays of different densities.
|
||||||
|
private struct WindowScaleReader: NSViewRepresentable {
|
||||||
|
let onChange: (CGFloat) -> Void
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> ScaleProbeView {
|
||||||
|
let view = ScaleProbeView()
|
||||||
|
view.onChange = onChange
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: ScaleProbeView, context: Context) {
|
||||||
|
nsView.onChange = onChange
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ScaleProbeView: NSView {
|
||||||
|
var onChange: ((CGFloat) -> Void)?
|
||||||
|
private var observers: [NSObjectProtocol] = []
|
||||||
|
private var lastScale: CGFloat = 0
|
||||||
|
|
||||||
|
override func viewDidMoveToWindow() {
|
||||||
|
super.viewDidMoveToWindow()
|
||||||
|
observers.forEach(NotificationCenter.default.removeObserver)
|
||||||
|
observers = []
|
||||||
|
report()
|
||||||
|
guard let window else { return }
|
||||||
|
for name in [NSWindow.didChangeBackingPropertiesNotification,
|
||||||
|
NSWindow.didChangeScreenNotification] {
|
||||||
|
observers.append(NotificationCenter.default.addObserver(
|
||||||
|
forName: name, object: window, queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.report()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func report() {
|
||||||
|
let scale = window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2
|
||||||
|
guard scale != lastScale else { return }
|
||||||
|
lastScale = scale
|
||||||
|
// Defer: this runs during layout, and the callback writes @State.
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.onChange?(scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
observers.forEach(NotificationCenter.default.removeObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Result window
|
||||||
struct ResultView: View {
|
struct ResultView: View {
|
||||||
@ObservedObject var model: CaptureModel
|
@ObservedObject var model: CaptureModel
|
||||||
let onClose: () -> Void
|
let onClose: () -> Void
|
||||||
@@ -714,7 +784,7 @@ struct ResultView: View {
|
|||||||
? "Adjust the crop — Return to apply, Esc to cancel."
|
? "Adjust the crop — Return to apply, Esc to cancel."
|
||||||
: (model.hasSelection
|
: (model.hasSelection
|
||||||
? "Region selected — OCR it, or adjust it with Crop."
|
? "Region selected — OCR it, or adjust it with Crop."
|
||||||
: (model.isActualSize ? "Actual size — scroll to pan, drag to select." : "Drag to select a region, or Get All Text.")))
|
: (model.isActualSize ? "Actual size — 1:1 pixels, scroll to pan." : "Drag to select a region, or Get All Text.")))
|
||||||
: "Selectable text"))
|
: "Selectable text"))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -871,15 +941,21 @@ struct ScreenshotPane: View {
|
|||||||
model.cropRect = newRect
|
model.cropRect = newRect
|
||||||
}
|
}
|
||||||
|
|
||||||
@Environment(\.displayScale) private var displayScale
|
@State private var windowScale: CGFloat = 2
|
||||||
|
|
||||||
|
/// Image pixels to window points. Actual size is 1 image pixel per
|
||||||
|
/// device pixel, i.e. 1/windowScale points.
|
||||||
|
private var actualSizeScale: CGFloat {
|
||||||
|
1 / max(windowScale, 0.01)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
if model.isActualSize {
|
if model.isActualSize {
|
||||||
// Actual size: 1 image pixel per screen pixel (view scale =
|
// Actual size: 1 image pixel per screen pixel (view scale =
|
||||||
// 1/displayScale). Scrollable when larger than the viewport;
|
// 1/windowScale). Scrollable when larger than the viewport;
|
||||||
// centred when smaller.
|
// centred when smaller.
|
||||||
let scale = 1 / max(displayScale, 0.01)
|
let scale = actualSizeScale
|
||||||
let drawSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
|
let drawSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
|
||||||
let origin = CGPoint(
|
let origin = CGPoint(
|
||||||
x: max(0, (geo.size.width - drawSize.width) / 2),
|
x: max(0, (geo.size.width - drawSize.width) / 2),
|
||||||
@@ -898,6 +974,9 @@ struct ScreenshotPane: View {
|
|||||||
.frame(width: geo.size.width, height: geo.size.height)
|
.frame(width: geo.size.width, height: geo.size.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.background(WindowScaleReader { scale in
|
||||||
|
windowScale = scale
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The image card with annotations, selection and crop overlays, plus
|
/// The image card with annotations, selection and crop overlays, plus
|
||||||
|
|||||||
+2
-2
@@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
|
|||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>Icon</string>
|
<string>Icon</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.11.1</string>
|
<string>1.11.3</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>111</string>
|
<string>113</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
|
|||||||
Reference in New Issue
Block a user