Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 110 additions & 3 deletions App/SnapGlass/EditorTests/CanvasTextEntryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import Testing

@MainActor
struct CanvasTextEntryTests {
private func image() throws -> CGImage {
private func image(width: Int = 800, height: Int = 600) throws -> CGImage {
let context = try #require(
CGContext(
data: nil, width: 800, height: 600, bitsPerComponent: 8,
data: nil, width: width, height: height, bitsPerComponent: 8,
bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue))
context.setFillColor(NSColor.white.cgColor)
context.fill(CGRect(x: 0, y: 0, width: 800, height: 600))
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
return try #require(context.makeImage())
}

Expand Down Expand Up @@ -102,6 +102,113 @@ struct CanvasTextEntryTests {
#expect(canvas.canvasTextEditor == nil)
}

@Test func activeCanvasEditorAppliesStyleUpdatesWithoutReplacingDraft() throws {
let canvas = EditableAnnotationCanvasNSView(frame: CGRect(x: 0, y: 0, width: 800, height: 600))
canvas.image = try image()
let session = UUID()
let node = AnnotationNode(
tool: .text, color: NSColor.red.cgColor,
points: [CGPoint(x: 0.2, y: 0.3)], text: "Original", fontSize: 24)
canvas.updateTextEntry(id: session, node: node, onCommit: { _ in }, onCancel: {})
let editor = try #require(canvas.canvasTextEditor)
editor.string = "Live draft"
editor.setSelectedRange(NSRange(location: 4, length: 0))

var updated = node
updated.color = NSColor.blue.cgColor
updated.fillColor = NSColor.black.withAlphaComponent(0.4).cgColor
updated.opacity = 0.6
updated.fontName = "Menlo"
updated.fontSize = 48
updated.textAlignment = .trailing
canvas.updateTextEntry(id: session, node: updated, onCommit: { _ in }, onCancel: {})

#expect(canvas.canvasTextEditor === editor)
#expect(editor.string == "Live draft")
#expect(editor.selectedRange() == NSRange(location: 4, length: 0))
#expect(editor.sourceNode?.fontSize == 48)
#expect(editor.font?.pointSize == 48)
#expect(editor.font?.familyName == NSFont(name: "Menlo", size: 48)?.familyName)
#expect(editor.alignment == .right)
#expect(editor.drawsBackground)
#expect(editor.textColor?.usingColorSpace(.sRGB)?.blueComponent ?? 0 > 0.8)
}

@Test func editedTextStyleIsDraftedUntilCommitAndCancelRestoresInspector() throws {
let model = try model()
model.beginTextEntry(at: CGPoint(x: 0.2, y: 0.3))
model.textDraft = "Original"
model.commitTextEntry()
let original = try #require(model.document?.nodes.first)
model.beginTextEditing(original)

model.fontName = "Menlo"
model.fontSize = 48
model.textAlignment = .trailing
model.annotationOpacity = 0.6
model.fillEnabled = true
model.fillColor = .black
model.updateSelectedStyle()

let draft = try #require(model.pendingTextNode)
#expect(draft.fontName == "Menlo")
#expect(draft.fontSize == 48)
#expect(draft.textAlignment == .trailing)
#expect(draft.opacity == 0.6)
#expect(draft.fillColor != nil)
#expect(model.document?.nodes.first?.fontSize == 24)

model.cancelTextEntry()
#expect(model.document?.nodes.first?.fontSize == 24)
#expect(model.fontSize == 24)

model.beginTextEditing(original)
model.textDraft = "Updated"
model.fontName = "Menlo"
model.fontSize = 48
model.textAlignment = .trailing
model.commitTextEntry()
#expect(model.document?.nodes.first?.text == "Updated")
#expect(model.document?.nodes.first?.fontName == "Menlo")
#expect(model.document?.nodes.first?.fontSize == 48)
#expect(model.document?.nodes.first?.textAlignment == .trailing)

model.undo()
#expect(model.document?.nodes.first?.text == "Original")
#expect(model.document?.nodes.first?.fontSize == 24)
}

@Test func fittedLongTextAccountsForWrappingAtImageWidth() throws {
let model = EditorViewModel()
let narrowImage = try image(width: 160, height: 600)
model.document = model.interactor.createDocument(from: narrowImage)
model.beginTextEntry(at: .zero)
model.fontSize = 24
model.textDraft = Array(repeating: "wrapping", count: 12).joined(separator: " ")
model.commitTextEntry()

let node = try #require(model.document?.nodes.first)
#expect(node.normalizedRect.width <= 1)
#expect(node.normalizedRect.height > 0.2)
}

@Test func smallTextContainerIncludesAllLaidOutGlyphs() throws {
let canvas = EditableAnnotationCanvasNSView(frame: CGRect(x: 0, y: 0, width: 800, height: 600))
canvas.image = try image()
let node = AnnotationNode(
tool: .text, color: NSColor.red.cgColor,
points: [CGPoint(x: 0.2, y: 0.3)], text: "Small 中文", fontSize: 24)
canvas.updateTextEntry(id: UUID(), node: node, onCommit: { _ in }, onCancel: {})
let editor = try #require(canvas.canvasTextEditor)
let container = try #require(editor.textContainer)
let layoutManager = try #require(editor.layoutManager)
layoutManager.ensureLayout(for: container)
let usedHeight = layoutManager.usedRect(for: container).height

#expect(container.containerSize.height >= ceil(usedHeight) + 1)
#expect(editor.bounds.height >= ceil(usedHeight) + editor.textContainerInset.height * 2 + 1)
}

@Test func nativeReturnModesAndEscape() throws {
for newline in [false, true] {
let editor = CanvasTextEditor(frame: CGRect(x: 0, y: 0, width: 300, height: 100))
Expand Down
84 changes: 84 additions & 0 deletions App/SnapGlass/EditorTests/EditorHistoryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import AnnotationCore
import AppKit
import HistoryCore
import Testing

@testable import EditorInteractionTests

final class HistoryRequestRecorder: @unchecked Sendable {
private let lock = NSLock()
private var entries: [(mode: EditorHistorySaveMode, source: UUID?)] = []

func append(_ mode: EditorHistorySaveMode, _ source: UUID?) {
lock.lock()
entries.append((mode, source))
lock.unlock()
}

var all: [(mode: EditorHistorySaveMode, source: UUID?)] {
lock.lock()
defer { lock.unlock() }
return entries
}
}

@MainActor
struct EditorHistoryTests {
private func image(width: Int = 400, height: Int = 300) throws -> CGImage {
let context = try #require(
CGContext(
data: nil, width: width, height: height, bitsPerComponent: 8,
bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue))
context.setFillColor(NSColor.white.cgColor)
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
return try #require(context.makeImage())
}

@Test func saveToHistoryReportsModeAndSource() async throws {
let sourceID = UUID()
let requests = HistoryRequestRecorder()
let model = EditorViewModel(
image: try image(),
context: EditorCaptureContext(
captureMode: "area", supportsVerticalTrim: false,
startsInVerticalTrim: false, sourceEntryID: sourceID
),
historySaver: { image, mode, source in
#expect(image.width == 400)
requests.append(mode, source)
}
)
await model.saveToHistory(mode: .overwriteOriginal)
let first = requests.all
#expect(first.count == 1)
#expect(first[0].mode == .overwriteOriginal)
#expect(first[0].source == sourceID)

await model.saveToHistory(mode: .newRecord)
let all = requests.all
#expect(all.count == 2)
#expect(all[1].mode == .newRecord)
#expect(all[1].source == sourceID)
}

@Test func overwriteWithoutSourceIsRejected() async throws {
let requests = HistoryRequestRecorder()
let model = EditorViewModel(
image: try image(),
historySaver: { _, mode, source in requests.append(mode, source) }
)
await model.saveToHistory(mode: .overwriteOriginal)
#expect(requests.all.isEmpty)
#expect(model.toastMessage?.type == .error)
}

@Test func failedSaveSurfacesErrorToast() async throws {
let model = EditorViewModel(
image: try image(),
historySaver: { _, _, _ in throw HistoryError.entryNotFound(id: UUID()) }
)
await model.saveToHistory(mode: .newRecord)
#expect(model.toastMessage?.type == .error)
}
}
45 changes: 45 additions & 0 deletions App/SnapGlass/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"Selection style" = "Selection style";
"Rectangle" = "Rectangle";
"Freeform" = "Freeform";
"Overlay preview" = "Overlay preview";
"Live screen" = "Live screen";
"Static snapshot" = "Static snapshot";
"Static snapshot freezes the screen when selection starts; unavailable screens appear black." = "Static snapshot freezes the screen when selection starts; unavailable screens appear black.";
"Saved image format" = "Saved image format";
"PNG (lossless)" = "PNG (lossless)";
"JPEG (smaller)" = "JPEG (smaller)";
Expand Down Expand Up @@ -371,3 +375,44 @@
"Record picked colors" = "Record picked colors";
"Maximum color entries" = "Maximum color entries";
"Colors are stored encrypted on this Mac only. Copying a color with the picker records it here." = "Colors are stored encrypted on this Mac only. Copying a color with the picker records it here.";

/* Settings gaps */
"Developer Mode" = "Developer Mode";
"Diagnostics" = "Diagnostics";
"Disable languages you rarely use to prevent visually similar characters (such as Japanese kanji and Chinese hanzi) from being misrecognized." = "Disable languages you rarely use to prevent visually similar characters (such as Japanese kanji and Chinese hanzi) from being misrecognized.";
"History segment" = "History segment";
"Record colors copied with the area or editor color picker" = "Record colors copied with the area or editor color picker";
"History unavailable" = "History unavailable";
"No captures yet" = "No captures yet";

/* Editor history */
"Save to History" = "Save to History";
"Save as New Record" = "Save as New Record";
"Overwrite Original Record" = "Overwrite Original Record";
"Save the annotated image into history" = "Save the annotated image into history";
"Saved to history" = "Saved to history";
"History updated; original kept for restore" = "History updated; original kept for restore";
"No original record to overwrite; save as a new record instead" = "No original record to overwrite; save as a new record instead";
"Restore Original Image" = "Restore Original Image";
"Original image restored" = "Original image restored";
"Copy Line" = "Copy Line";
"Add as Text Annotation" = "Add as Text Annotation";

/* Toasts */
"No text found" = "No text found";
"Text copied to clipboard" = "Text copied to clipboard";
"OCR completed" = "OCR completed";
"Copied OCR text" = "Copied OCR text";
"Copied to clipboard" = "Copied to clipboard";
"No image found in clipboard" = "No image found in clipboard";
"Scrolling capture cancelled" = "Scrolling capture cancelled";
"Scroll the window, then capture the next frame" = "Scroll the window, then capture the next frame";
"No visual change detected; scroll and try again" = "No visual change detected; scroll and try again";
"Frame %d captured" = "Frame %d captured";
"Saved to %@" = "Saved to %@";
"Capture failed: %@" = "Capture failed: %@";
"OCR failed: %@" = "OCR failed: %@";
"History save failed: %@" = "History save failed: %@";
"Scroll frame failed: %@" = "Scroll frame failed: %@";
"Save failed: %@" = "Save failed: %@";
"Copy failed: %@" = "Copy failed: %@";
45 changes: 45 additions & 0 deletions App/SnapGlass/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"Selection style" = "選択スタイル";
"Rectangle" = "矩形";
"Freeform" = "自由選択";
"Overlay preview" = "オーバーレイプレビュー";
"Live screen" = "ライブ画面";
"Static snapshot" = "静止スナップショット";
"Static snapshot freezes the screen when selection starts; unavailable screens appear black." = "静止スナップショットは選択開始時の画面を固定し、取得できない画面は黒く表示します。";
"Saved image format" = "保存形式";
"PNG (lossless)" = "PNG(ロスレス)";
"JPEG (smaller)" = "JPEG(より小さい)";
Expand Down Expand Up @@ -371,3 +375,44 @@
"Record picked colors" = "ピックしたカラーを記録";
"Maximum color entries" = "カラー履歴の最大件数";
"Colors are stored encrypted on this Mac only. Copying a color with the picker records it here." = "カラーはこの Mac にのみ暗号化して保存されます。ピッカーでコピーするとここに記録されます。";

/* Settings gaps */
"Developer Mode" = "開発者モード";
"Diagnostics" = "診断";
"Disable languages you rarely use to prevent visually similar characters (such as Japanese kanji and Chinese hanzi) from being misrecognized." = "使用頻度の低い言語を無効にすると、字形が似た文字(日本語の漢字や中国語の漢字など)の誤認識を防げます。";
"History segment" = "履歴セグメント";
"Record colors copied with the area or editor color picker" = "領域撮影またはエディタのカラーピッカーでコピーしたカラーを記録";
"History unavailable" = "履歴を利用できません";
"No captures yet" = "まだ撮影がありません";

/* Editor history */
"Save to History" = "履歴に保存";
"Save as New Record" = "新規レコードとして保存";
"Overwrite Original Record" = "元のレコードを上書き";
"Save the annotated image into history" = "注釈付き画像を履歴に保存";
"Saved to history" = "履歴に保存しました";
"History updated; original kept for restore" = "履歴を更新しました。元の画像は復元用に保持されます";
"No original record to overwrite; save as a new record instead" = "上書きできる元のレコードがないため、新規レコードとして保存します";
"Restore Original Image" = "元の画像を復元";
"Original image restored" = "元の画像を復元しました";
"Copy Line" = "行をコピー";
"Add as Text Annotation" = "テキスト注釈として追加";

/* Toasts */
"No text found" = "テキストが検出されませんでした";
"Text copied to clipboard" = "テキストをクリップボードにコピーしました";
"OCR completed" = "文字認識が完了しました";
"Copied OCR text" = "認識したテキストをコピーしました";
"Copied to clipboard" = "クリップボードにコピーしました";
"No image found in clipboard" = "クリップボードに画像が見つかりません";
"Scrolling capture cancelled" = "スクロール撮影をキャンセルしました";
"Scroll the window, then capture the next frame" = "ウィンドウをスクロールして、次のフレームを撮影してください";
"No visual change detected; scroll and try again" = "画面に変化がありません。スクロールして再試行してください";
"Frame %d captured" = "%d フレーム目を撮影しました";
"Saved to %@" = "%@ に保存しました";
"Capture failed: %@" = "撮影に失敗しました:%@";
"OCR failed: %@" = "文字認識に失敗しました:%@";
"History save failed: %@" = "履歴の保存に失敗しました:%@";
"Scroll frame failed: %@" = "スクロールフレームの撮影に失敗しました:%@";
"Save failed: %@" = "保存に失敗しました:%@";
"Copy failed: %@" = "コピーに失敗しました:%@";
45 changes: 45 additions & 0 deletions App/SnapGlass/Resources/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"Selection style" = "선택 스타일";
"Rectangle" = "사각형";
"Freeform" = "자유 선택";
"Overlay preview" = "오버레이 미리보기";
"Live screen" = "실시간 화면";
"Static snapshot" = "정적 스냅샷";
"Static snapshot freezes the screen when selection starts; unavailable screens appear black." = "정적 스냅샷은 선택을 시작할 때 화면을 고정하며, 캡처할 수 없는 화면은 검게 표시됩니다.";
"Saved image format" = "저장 형식";
"PNG (lossless)" = "PNG(무손실)";
"JPEG (smaller)" = "JPEG(더 작은 용량)";
Expand Down Expand Up @@ -371,3 +375,44 @@
"Record picked colors" = "복사한 색상 기록";
"Maximum color entries" = "최대 색상 항목 수";
"Colors are stored encrypted on this Mac only. Copying a color with the picker records it here." = "색상은 이 Mac에만 암호화되어 저장됩니다. 피커로 색상을 복사하면 여기에 기록됩니다.";

/* Settings gaps */
"Developer Mode" = "개발자 모드";
"Diagnostics" = "진단";
"Disable languages you rarely use to prevent visually similar characters (such as Japanese kanji and Chinese hanzi) from being misrecognized." = "자주 사용하지 않는 언어를 비활성화하면 글자가 비슷한 문자(일본어 한자, 중국어 한자 등)가 잘못 인식되는 것을 방지할 수 있습니다.";
"History segment" = "기록 구분";
"Record colors copied with the area or editor color picker" = "영역 캡처 또는 편집기 색상 선택기로 복사한 색상 기록";
"History unavailable" = "기록을 사용할 수 없습니다";
"No captures yet" = "아직 캡처가 없습니다";

/* Editor history */
"Save to History" = "기록에 저장";
"Save as New Record" = "새 항목으로 저장";
"Overwrite Original Record" = "원본 항목 덮어쓰기";
"Save the annotated image into history" = "주석이 달린 이미지를 기록에 저장";
"Saved to history" = "기록에 저장했습니다";
"History updated; original kept for restore" = "기록을 업데이트했습니다. 원본은 복원용으로 보관됩니다";
"No original record to overwrite; save as a new record instead" = "덮어쓸 원본 항목이 없어 새 항목으로 저장합니다";
"Restore Original Image" = "원본 이미지 복원";
"Original image restored" = "원본 이미지를 복원했습니다";
"Copy Line" = "줄 복사";
"Add as Text Annotation" = "텍스트 주석으로 추가";

/* Toasts */
"No text found" = "텍스트를 찾을 수 없습니다";
"Text copied to clipboard" = "텍스트를 클립보드에 복사했습니다";
"OCR completed" = "텍스트 인식 완료";
"Copied OCR text" = "인식된 텍스트를 복사했습니다";
"Copied to clipboard" = "클립보드에 복사했습니다";
"No image found in clipboard" = "클립보드에서 이미지를 찾을 수 없습니다";
"Scrolling capture cancelled" = "스크롤 캡처를 취소했습니다";
"Scroll the window, then capture the next frame" = "창을 스크롤한 다음 다음 프레임을 캡처하세요";
"No visual change detected; scroll and try again" = "화면 변화가 감지되지 않았습니다. 스크롤 후 다시 시도하세요";
"Frame %d captured" = "%d번째 프레임을 캡처했습니다";
"Saved to %@" = "%@에 저장했습니다";
"Capture failed: %@" = "캡처 실패: %@";
"OCR failed: %@" = "텍스트 인식 실패: %@";
"History save failed: %@" = "기록 저장 실패: %@";
"Scroll frame failed: %@" = "스크롤 프레임 캡처 실패: %@";
"Save failed: %@" = "저장 실패: %@";
"Copy failed: %@" = "복사 실패: %@";
Loading
Loading