Compare commits

...
12 Commits
Author SHA1 Message Date
TZGyn de642ad692 Fix flipped drawing when copying/saving annotated images
The strokes were stored in top-left image coordinates but drawn into an
AppKit CGContext with a bottom-left origin, so exports came out mirrored
and offset. Flip Y in the context before stroking so annotations land
where they were drawn.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-24 09:08:37 +08:00
TZGyn 8b27263962 Make stroke thickness per-stroke; restore Clear button
Each drawn stroke now stores its own width and color, so changing the
thickness slider no longer retroactively resizes existing strokes. The
Clear button is back in the draw toolbar.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-23 14:00:00 +08:00
TZGyn 1961d0dc9c Move draw controls into the draw toolbar
The color picker, undo/redo, and clear eraser now live in the dedicated
draw top bar instead of the main toolbar, keeping the primary actions
(Copy/Save/Get Text) uncluttered. Keyboard shortcuts (⌘Z/⌘⇧Z) remain.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-23 13:55:00 +08:00
TZGyn 41cb06dcb5 Add draw toolbar with thickness, undo/redo, and color picker
When drawing is enabled a dedicated top bar shows a thickness slider,
color picker, and undo/redo buttons (⌘Z / ⌘⇧Z). Strokes render and export
with the chosen width and color.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-23 13:39:06 +08:00
TZGyn 52e36b5c5a Add drawing color picker to the result window
A compact ColorPicker appears next to the Draw button when drawing is
enabled; strokes render and export in the chosen color (defaults to
system red).

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-23 13:33:32 +08:00
TZGyn a74a86d001 Fix vertical offset in ScreenCaptureKit crops
The captured CGImage has a top-left origin, while the crop rect was
computed in SCDisplay space (bottom-left). Flip Y within the display's
bounds when converting to pixel coordinates so the crop lands exactly
where the user selected.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-23 13:27:41 +08:00
TZGyn f50057d951 Fix screenshot offset after pixel-exact crop change
The new CGImage.cropping path expects a top-left pixel rect; the previous
CGContext redraw had implicitly flipped Y. Keep the existing SCDisplay-space
conversion so the crop matches the selected region.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-23 13:23:37 +08:00
TZGyn 16b876807d Add freehand drawing and fix residual capture pixelation
The screenshot window now supports toggling a pen to draw annotations
that are flattened into copied and saved images. Captures are also
cropped with a pixel-exact CGImage.cropping instead of a CGContext
redraw, removing residual blur on retina displays.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-23 13:21:22 +08:00
TZGyn 6a2c8df180 Modernize the result window UI
Restyles the window with a cleaner layout: icon-based toolbar buttons
(Copy/Save/Get All Text, highlighted OCR Selection action), a bottom status
bar with progress indicator and contextual hints, monospaced text tab, and
a divider separating chrome from content.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-13 09:40:51 +08:00
TZGyn c9447d2a8b Make Kapture screenshot-first with marquee text selection
The result window now works like a screenshot tool: drag on the image to
select a region, then click Get Selection Text to OCR only that area
(full-image OCR stays as Get All Text). Selection is rendered in image
coordinates so it stays accurate at any window size.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-12 21:06:09 +08:00
TZGyn 3fdc2fca95 Fix vertical offset when capturing the secondary display
The first ScreenCaptureKit Y-flip used the wrong reference edges, shifting
cropped selections on displays below the main screen. Convert each selection
per-display as scY = appKitY - NSScreen.maxY + SCDisplay.maxY, which
matches both the main and secondary displays.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-12 16:51:18 +08:00
TZGyn 590be2356f Fix captures on displays below the main screen
ScreenCaptureKit uses a flipped Y axis relative to AppKit NSScreen frames,
so selections on a display below the main screen produced out-of-bounds
crop rects and "Capture failed". Convert the selection into SCDisplay space
using the matching NSScreen's top edge before cropping.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-08-12 16:47:48 +08:00
3 changed files with 437 additions and 80 deletions
+30 -25
View File
@@ -117,22 +117,19 @@ final class ScreenCapture {
private init() {}
@available(macOS 14.0, *)
private func crop(_ image: CGImage, to rect: NSRect, display: SCDisplay) -> CGImage? {
// The captured image may be at any scale (retina 2x, secondary display
// 1x, or trimmed by ScreenCaptureKit). Derive the pixel scale from the
// image we actually got, relative to the display's point frame.
private func crop(_ image: CGImage, to cgRect: CGRect, display: SCDisplay) -> CGImage? {
// cgRect is in SCDisplay space (bottom-left origin, Y flipped vs AppKit).
// The captured image has a TOP-LEFT origin. So within the display,
// we must flip Y: pixelY = displayHeight - (cgY - displayMinY) - cgHeight.
let scaleX = CGFloat(image.width) / display.frame.width
let scaleY = CGFloat(image.height) / display.frame.height
guard scaleX > 0, scaleY > 0 else { return nil }
let cgRect = CGRect(
x: rect.minX,
y: rect.minY,
width: rect.width,
height: rect.height
)
let localMinX = cgRect.minX - display.frame.minX
let localMaxY = cgRect.maxY - display.frame.minY
let flippedY = display.frame.height - localMaxY
let pxRect = CGRect(
x: (cgRect.minX - display.frame.minX) * scaleX,
y: (cgRect.minY - display.frame.minY) * scaleY,
x: localMinX * scaleX,
y: flippedY * scaleY,
width: cgRect.width * scaleX,
height: cgRect.height * scaleY
)
@@ -140,15 +137,11 @@ final class ScreenCapture {
// display's frame (e.g. secondary monitor coordinate quirks).
let safeRect = pxRect.intersection(CGRect(x: 0, y: 0, width: CGFloat(image.width), height: CGFloat(image.height)))
guard safeRect.width >= 1, safeRect.height >= 1 else { return nil }
guard let cropCtx = CGContext(
data: nil, width: Int(safeRect.width), height: Int(safeRect.height),
bitsPerComponent: 8, bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else { return nil }
cropCtx.interpolationQuality = .high
cropCtx.draw(image, in: CGRect(x: -safeRect.minX, y: -safeRect.minY, width: CGFloat(image.width), height: CGFloat(image.height)))
return cropCtx.makeImage()
// Use a pixel-exact subimage crop instead of redrawing through a
// CGContext, which resamples and loses quality on retina captures.
let rounded = safeRect.integral
guard rounded.width >= 1, rounded.height >= 1 else { return nil }
return image.cropping(to: rounded)
}
@available(macOS 14.0, *)
@@ -177,18 +170,30 @@ final class ScreenCapture {
// width/height are output PIXELS. Multiply by the backing scale
// of the screen containing the selection so retina displays are
// captured at full resolution and 1x displays stay 1x.
let backingScale = NSScreen.screens
.first(where: { $0.frame.contains(NSPoint(x: rect.midX, y: rect.midY)) })?
.backingScaleFactor ?? 1
guard let screen = NSScreen.screens.first(where: { $0.frame.contains(NSPoint(x: rect.midX, y: rect.midY)) }) else {
completion(nil)
return
}
let backingScale = screen.backingScaleFactor
config.width = Int(CGFloat(display.width) * backingScale)
config.height = Int(CGFloat(display.height) * backingScale)
config.showsCursor = true
// ScreenCaptureKit uses a different origin than AppKit for
// screens stacked around the main display: per-display,
// SCDisplay y = appKitY - NSScreen.maxY + SCDisplay.maxY.
let scRect = CGRect(
x: rect.minX,
y: rect.minY - screen.frame.maxY + display.frame.maxY,
width: rect.width,
height: rect.height
)
guard let image = try? await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) else {
completion(nil)
return
}
completion(self.crop(image, to: rect, display: display))
completion(self.crop(image, to: scRect, display: display))
}
} else {
guard let primary = NSScreen.screens.first(where: { $0.frame.origin == .zero }) ?? NSScreen.screens.first else {
+402 -50
View File
@@ -88,6 +88,24 @@ final class CaptureModel: ObservableObject {
@Published var extractedText: String?
@Published var isOCRRunning = false
@Published var statusMessage: String?
/// Drag selection rect in IMAGE point space (nil = none)
@Published var selection: CGRect?
/// Freehand drawing strokes in IMAGE point space
@Published var strokes: [Stroke] = []
/// Redo stack of removed strokes
@Published var redoStack: [Stroke] = []
/// Whether the pen is active (drag draws instead of selecting)
@Published var isDrawing = false
/// Current drawing color
@Published var drawColor: NSColor = .systemRed
/// Stroke thickness in points (image space)
@Published var strokeWidth: CGFloat = 3.0
struct Stroke {
let points: [CGPoint]
let width: CGFloat
let color: NSColor
}
enum CaptureMode {
case screenshot
@@ -98,24 +116,104 @@ final class CaptureModel: ObservableObject {
self.image = image
}
var hasSelection: Bool { selection != nil && selection!.width >= 2 && selection!.height >= 2 }
var canUndo: Bool { !strokes.isEmpty }
var canRedo: Bool { !redoStack.isEmpty }
func undoStroke() {
guard let last = strokes.popLast() else { return }
redoStack.append(last)
}
func redoStroke() {
guard let last = redoStack.popLast() else { return }
strokes.append(last)
}
func clearAnnotations() {
strokes = []
redoStack = []
}
/// Image with drawn strokes flattened in, for copy/save/export.
var annotatedImage: CGImage {
guard !strokes.isEmpty else { return image }
let size = NSSize(width: image.width, height: image.height)
guard let rep = NSBitmapImageRep(
bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
) else { return image }
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
let ctx = NSGraphicsContext.current!.cgContext
// CGImage draws with a flipped Y in NSGraphicsContext
ctx.draw(image, in: NSRect(origin: .zero, size: size))
// Strokes are stored in TOP-LEFT image coordinates (from the SwiftUI
// canvas), but the CGContext uses a BOTTOM-LEFT origin. Flip Y so the
// strokes land where they were drawn.
ctx.saveGState()
ctx.translateBy(x: 0, y: size.height)
ctx.scaleBy(x: 1, y: -1)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
for stroke in strokes {
guard stroke.points.count > 1 else { continue }
stroke.color.setStroke()
ctx.setLineWidth(stroke.width)
ctx.beginPath()
ctx.move(to: stroke.points[0])
for point in stroke.points.dropFirst() {
ctx.addLine(to: point)
}
ctx.strokePath()
}
ctx.restoreGState()
NSGraphicsContext.restoreGraphicsState()
return rep.cgImage ?? image
}
func addStroke(_ points: [CGPoint]) {
guard points.count > 1 else { return }
strokes.append(Stroke(points: points, width: strokeWidth, color: drawColor))
redoStack = []
}
var displayText: String {
if isOCRRunning {
return "Recognizing text…"
}
return extractedText ?? "Text appears here — click “Get Text”."
return extractedText ?? "Select text in the screenshot, then click “Get Text”."
}
func runOCR() {
func cropImage(to rectInImage: CGRect) -> CGImage? {
return image.cropping(to: rectInImage)
}
func runOCR(over selectionOnly: CGRect? = nil) {
guard !isOCRRunning else { return }
isOCRRunning = true
statusMessage = nil
OCRService.shared.recognize(image) { [weak self] text in
let targetImage: CGImage
if let sel = selectionOnly {
guard let cropped = image.cropping(to: sel) else {
self.isOCRRunning = false
self.statusMessage = "Selection too small."
return
}
targetImage = cropped
} else {
targetImage = image
}
OCRService.shared.recognize(targetImage) { [weak self] text in
Task { @MainActor in
guard let self else { return }
self.isOCRRunning = false
self.mode = .text
if text.isEmpty {
self.extractedText = "No text found in the screenshot."
self.extractedText = "No text found in the selection."
} else {
self.extractedText = text
let pasteboard = NSPasteboard.general
@@ -127,6 +225,11 @@ final class CaptureModel: ObservableObject {
}
}
func runOCRSelection() {
guard let sel = selection, hasSelection else { return }
runOCR(over: sel)
}
func copyText() {
guard let text = extractedText else { return }
let pasteboard = NSPasteboard.general
@@ -138,7 +241,7 @@ final class CaptureModel: ObservableObject {
func copyImage() {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.writeObjects([NSImage(cgImage: image, size: .zero)])
pasteboard.writeObjects([NSImage(cgImage: annotatedImage, size: .zero)])
statusMessage = "Image copied to clipboard."
}
@@ -151,7 +254,7 @@ final class CaptureModel: ObservableObject {
statusMessage = "Could not create PNG file."
return
}
CGImageDestinationAddImage(dest, image, nil)
CGImageDestinationAddImage(dest, annotatedImage, nil)
if CGImageDestinationFinalize(dest) {
statusMessage = "Saved PNG."
} else {
@@ -171,73 +274,322 @@ struct ResultView: View {
let onClose: () -> Void
var body: some View {
VStack(spacing: 10) {
HStack {
VStack(spacing: 0) {
// Toolbar
HStack(spacing: 10) {
Picker("", selection: $model.mode) {
Text("Screenshot").tag(CaptureModel.CaptureMode.screenshot)
Text("Text").tag(CaptureModel.CaptureMode.text)
if model.isOCRRunning {
Text("…").tag(CaptureModel.CaptureMode.text)
}
}
.pickerStyle(.segmented)
.fixedSize()
Spacer()
Button("Copy Image") { model.copyImage() }
Button("Save PNG…") { model.savePNG() }
Button("Get Text") { model.runOCR() }
.disabled(model.isOCRRunning || model.extractedText != nil)
Button("Close") { onClose() }
}
.padding(.horizontal, 12)
.padding(.top, 10)
if model.mode == .screenshot {
GeometryReader { geo in
let imageWidth = CGFloat(model.image.width)
Image(nsImage: NSImage(cgImage: model.image, size: .zero))
.resizable()
// Downscale with smoothing (AA); upscale keeps pixels crisp.
.interpolation(imageWidth >= geo.size.width ? .high : .none)
.aspectRatio(contentMode: .fit)
Spacer()
if model.hasSelection {
Button {
model.runOCRSelection()
} label: {
Label("OCR Selection", systemImage: "text.magnifyingglass")
}
.border(Color.secondary.opacity(0.4))
.padding(.horizontal, 12)
.buttonStyle(.borderedProminent)
.disabled(model.isOCRRunning)
.help("Recognize text inside the selected region")
}
Button {
model.runOCR()
} label: {
Label("Get All Text", systemImage: "text.viewfinder")
}
.buttonStyle(.bordered)
.disabled(model.isOCRRunning)
.help("Recognize all text in the screenshot")
Divider().frame(height: 18)
Button {
model.isDrawing.toggle()
if model.isDrawing { model.selection = nil }
} label: {
Label("Draw", systemImage: model.isDrawing ? "pencil.tip.crop.circle.fill" : "pencil.tip.crop.circle")
}
.buttonStyle(.bordered)
.foregroundStyle(model.isDrawing ? .primary : .secondary)
.help("Toggle drawing on the screenshot")
Button {
model.copyImage()
} label: {
Label("Copy", systemImage: "doc.on.doc")
}
.buttonStyle(.bordered)
.help("Copy image to clipboard")
Button {
model.savePNG()
} label: {
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.help("Save screenshot as PNG");
Button {
onClose()
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help("Close")
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
Divider()
// Draw toolbar (only when drawing)
if model.isDrawing {
HStack(spacing: 14) {
Label("Draw", systemImage: "pencil.tip")
.font(.caption)
.foregroundStyle(.secondary)
Divider().frame(height: 18)
HStack(spacing: 6) {
Image(systemName: "circle.fill").font(.system(size: 4))
Image(systemName: "circle.fill").font(.system(size: 7))
Image(systemName: "circle.fill").font(.system(size: 10))
}
.foregroundStyle(.secondary)
Slider(value: $model.strokeWidth, in: 1...12, step: 0.5)
.frame(width: 120)
.help("Stroke thickness")
Divider().frame(height: 18)
ColorPicker("", selection: Binding(
get: { Color(nsColor: model.drawColor) },
set: { model.drawColor = NSColor($0) }
), supportsOpacity: false)
.labelsHidden()
.frame(width: 28)
.help("Drawing color")
Spacer()
Button {
model.undoStroke()
} label: {
Label("Undo", systemImage: "arrow.uturn.backward")
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(!model.canUndo)
.keyboardShortcut("z", modifiers: .command)
.help("Undo last stroke (⌘Z)")
Button {
model.redoStroke()
} label: {
Label("Redo", systemImage: "arrow.uturn.forward")
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(!model.canRedo)
.keyboardShortcut("z", modifiers: [.command, .shift])
.help("Redo stroke (⌘⇧Z)")
Button {
model.clearAnnotations()
} label: {
Label("Clear", systemImage: "eraser")
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(model.strokes.isEmpty)
.help("Remove all drawn annotations")
}
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(.bar)
Divider()
}
// Content
if model.mode == .screenshot {
ScreenshotPane(model: model)
.padding(12)
} else {
ZStack(alignment: .bottomTrailing) {
ZStack {
ScrollView {
Text(model.displayText)
.font(.system(.body))
.font(.system(.body, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
.padding(12)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.secondary.opacity(0.4))
)
// Status bar
HStack(spacing: 8) {
if let msg = model.statusMessage {
Text(msg)
if model.isOCRRunning {
ProgressView()
.controlSize(.small)
}
Text(model.statusMessage ?? (model.mode == .screenshot
? (model.hasSelection ? "Region selected — click OCR Selection." : "Drag to select a region, or Get All Text.")
: "Selectable text"))
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.lineLimit(1)
Spacer()
if model.mode == .text, model.extractedText != nil {
Button {
model.copyText()
} label: {
Label("Copy Text", systemImage: "doc.on.doc")
}
Button("Copy Text") { model.copyText() }
.buttonStyle(.bordered)
.controlSize(.small)
}
.padding(8)
}
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(.bar)
}
.frame(minWidth: 520, minHeight: 420)
}
}
if model.statusMessage != nil && model.mode == .screenshot {
Text(model.statusMessage!)
.font(.caption)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
struct ScreenshotPane: View {
@ObservedObject var model: CaptureModel
private var imageSize: CGSize {
CGSize(width: model.image.width, height: model.image.height)
}
private func geometry(for geo: GeometryProxy) -> (scale: CGFloat, drawSize: CGSize, origin: CGPoint) {
let size = imageSize
let scale = min(geo.size.width / size.width, geo.size.height / size.height)
let drawSize = CGSize(width: size.width * scale, height: size.height * scale)
let origin = CGPoint(
x: (geo.size.width - drawSize.width) / 2,
y: (geo.size.height - drawSize.height) / 2
)
return (scale, drawSize, origin)
}
private func toImagePoint(_ viewPoint: CGPoint, scale: CGFloat, origin: CGPoint) -> CGPoint {
CGPoint(
x: (viewPoint.x - origin.x) / scale,
y: (viewPoint.y - origin.y) / scale
)
}
private func clampSelection(_ rect: CGRect) -> CGRect {
rect.intersection(CGRect(origin: .zero, size: imageSize))
}
@State private var currentStroke: [CGPoint] = []
var body: some View {
GeometryReader { geo in
let (scale, drawSize, origin) = geometry(for: geo)
ZStack {
Image(nsImage: NSImage(cgImage: model.image, size: .zero))
.resizable()
.interpolation(imageSize.width >= geo.size.width ? .high : .none)
.aspectRatio(contentMode: .fit)
.frame(width: drawSize.width, height: drawSize.height)
.position(x: origin.x + drawSize.width / 2, y: origin.y + drawSize.height / 2)
// Completed strokes
Canvas { context, _ in
for stroke in model.strokes {
guard stroke.points.count > 1 else { continue }
var path = Path()
path.move(to: CGPoint(x: origin.x + stroke.points[0].x * scale, y: origin.y + stroke.points[0].y * scale))
for point in stroke.points.dropFirst() {
path.addLine(to: CGPoint(x: origin.x + point.x * scale, y: origin.y + point.y * scale))
}
context.stroke(path, with: .color(Color(nsColor: stroke.color)), lineWidth: stroke.width)
}
}
.padding(.bottom, 10)
.frame(minWidth: 480, minHeight: 380)
.allowsHitTesting(false)
// In-progress stroke
if !currentStroke.isEmpty {
Canvas { context, _ in
var path = Path()
if currentStroke.count > 1 {
path.move(to: CGPoint(x: origin.x + currentStroke[0].x * scale, y: origin.y + currentStroke[0].y * scale))
for point in currentStroke.dropFirst() {
path.addLine(to: CGPoint(x: origin.x + point.x * scale, y: origin.y + point.y * scale))
}
}
context.stroke(path, with: .color(Color(nsColor: model.drawColor)), lineWidth: model.strokeWidth)
}
.allowsHitTesting(false)
}
if !model.isDrawing, let sel = model.selection {
let viewRect = CGRect(
x: origin.x + sel.minX * scale,
y: origin.y + sel.minY * scale,
width: sel.width * scale,
height: sel.height * scale
)
Rectangle()
.fill(Color.accentColor.opacity(0.15))
.border(Color.accentColor, width: 1.5)
.frame(width: viewRect.width, height: viewRect.height)
.position(x: viewRect.midX, y: viewRect.midY)
.allowsHitTesting(false)
}
}
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
if model.isDrawing {
let p = toImagePoint(value.location, scale: scale, origin: origin)
let imgRect = CGRect(origin: .zero, size: imageSize)
if imgRect.contains(p) {
currentStroke.append(p)
}
} else {
let start = toImagePoint(value.startLocation, scale: scale, origin: origin)
let current = toImagePoint(value.location, scale: scale, origin: origin)
let rect = CGRect(
x: min(start.x, current.x),
y: min(start.y, current.y),
width: abs(current.x - start.x),
height: abs(current.y - start.y)
)
model.selection = clampSelection(rect)
}
}
.onEnded { _ in
if model.isDrawing {
if currentStroke.count > 1 {
model.addStroke(currentStroke)
}
currentStroke = []
} else {
if let s = model.selection, (s.width < 2 || s.height < 2) {
model.selection = nil
}
}
}
)
}
}
}
+2 -2
View File
@@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
<key>CFBundleIconFile</key>
<string>Icon</string>
<key>CFBundleShortVersionString</key>
<string>1.1.6</string>
<string>1.4.3</string>
<key>CFBundleVersion</key>
<string>16</string>
<string>43</string>
<key>LSUIElement</key>
<true/>
<key>NSHighResolutionCapable</key>