Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4b0970ed2 |
@@ -6,6 +6,12 @@ final class ResultWindow {
|
||||
static let shared = ResultWindow()
|
||||
|
||||
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 openCount = 0
|
||||
|
||||
@@ -13,15 +19,21 @@ final class ResultWindow {
|
||||
func show(image: CGImage) {
|
||||
let model = CaptureModel(image: image)
|
||||
let w = NSWindow()
|
||||
w.contentViewController = NSHostingController(rootView: ResultView(model: model) {
|
||||
let host = NSHostingController(rootView: ResultView(model: model) {
|
||||
w.close()
|
||||
})
|
||||
w.contentView = host.view
|
||||
hosts[ObjectIdentifier(w)] = host
|
||||
w.title = "Kapture — Screenshot" + (openCount > 0 ? " \(openCount + 1)" : "")
|
||||
w.level = .floating
|
||||
w.styleMask = [.titled, .closable, .resizable]
|
||||
w.setContentSize(NSSize(width: 694, height: 520))
|
||||
w.isReleasedWhenClosed = false
|
||||
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)
|
||||
|
||||
windows.append(w)
|
||||
@@ -40,6 +52,7 @@ final class ResultWindow {
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
self.windows.removeAll { $0 === window }
|
||||
self.hosts[ObjectIdentifier(window)] = nil
|
||||
self.openCount = max(0, self.openCount - 1)
|
||||
}
|
||||
}
|
||||
@@ -525,8 +538,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 {
|
||||
@ObservedObject var model: CaptureModel
|
||||
let onClose: () -> Void
|
||||
@@ -714,7 +780,7 @@ struct ResultView: View {
|
||||
? "Adjust the crop — Return to apply, Esc to cancel."
|
||||
: (model.hasSelection
|
||||
? "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"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -871,15 +937,21 @@ struct ScreenshotPane: View {
|
||||
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 {
|
||||
GeometryReader { geo in
|
||||
if model.isActualSize {
|
||||
// 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.
|
||||
let scale = 1 / max(displayScale, 0.01)
|
||||
let scale = actualSizeScale
|
||||
let drawSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
|
||||
let origin = CGPoint(
|
||||
x: max(0, (geo.size.width - drawSize.width) / 2),
|
||||
@@ -898,6 +970,9 @@ struct ScreenshotPane: View {
|
||||
.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
|
||||
|
||||
+2
-2
@@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Icon</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.11.1</string>
|
||||
<string>1.11.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>111</string>
|
||||
<string>112</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
|
||||
Reference in New Issue
Block a user