feat: add PDF search with Ctrl+F#4
Merged
Merged
Conversation
Search bar with previous/next navigation and highlight overlay. Current match highlighted in orange, other matches in yellow. Auto-scrolls to current match. Esc to close search bar. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
nelsonduarte
added a commit
that referenced
this pull request
Jun 5, 2026
* fix(editor): add i18n keys for Round 9 audit fixes
Twelve new keys added across all 8 shipping locales for the encryption
preservation prompt, forms / pending warnings, signature dialog empty
validation, note-deletion label, and form-load failure feedback. Parity
preserved at 8 locales x 595 keys.
Keys: editor.encrypt.warning_*, editor.encrypt.save_*,
editor.forms.has_pending, editor.forms.undo_unavailable,
editor.forms.load_failed, editor.forms.no_fields,
editor.signature.empty_{draw,type,import}, edit.label.note_delete.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(editor): signature dialog rejects empty tabs + captures live stroke
Three signature dialog UX fixes:
- _on_accept previously returned silently when Draw was empty, Type was
blank, or Import was unset. The dialog now surfaces a QMessageBox
warning per case (editor.signature.empty_{draw,type,import}) so the
user understands why nothing happened.
- _SignatureCanvas.is_empty / to_image now include the in-progress
stroke. Clicking OK with the mouse button still held no longer drops
the final stroke.
- File-open filter for the Import tab now lists *.tif and *.tiff so
scanned signatures imported from older capture devices show up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(editor): cache overlay pixmaps, clamp redact rect, theme inline edit
Canvas-side Round 9 fixes:
- Overlay image/signature pixmaps now go through an LRU cache keyed by
(path, mtime). paintEvent previously rebuilt QPixmap(path) per overlay
per repaint -- with 30+ stamps the cost dominated scroll latency.
The mtime in the cache key auto-invalidates when the signature file
is regenerated on disk.
- _rect_to_pdf now intersects the dragged rect with the start page's
bbox and returns None for a zero-area / fully off-page result.
Cross-page redact drags previously mapped to the start page only and
PyMuPDF silently truncated the off-page portion.
- mouseReleaseEvent skips the rect_selected emit when the clamped rect
is None so the redact pipeline never sees a degenerate Rect.
- Notes now also persist the source annotation type + bbox when
discovered late via context-menu, so the tab can register a stable
delete_annot pending edit (xref isn't preserved across the
release_doc/fitz.open round-trip).
- paintEvent uses enumerate() for the overlay index instead of an
O(n) list.index call (LOW polish).
- _style_inline_edit picks a theme-aware background instead of the
hardcoded #FFFFFFE6 which clashed on dark pages.
- _commit_inline / _cancel_inline reset insert-mode style state so the
next insert doesn't inherit stale font/size/colour.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(editor): preserve encryption when saving protected PDFs (CRIT)
The save path stripped encryption silently, downgrading password-
protected input files to plaintext on every save. Both the fitz path
(_run) and the pypdf forms path (_apply_forms) now detect encrypted
input and prompt the user with three explicit choices:
- Keep protection (re-encrypt the output with AES-256, the captured
user password used as both user_pw and owner_pw)
- Save without password (previous behaviour, but now opt-in)
- Cancel (abort the save, nothing is written)
Documented limitation: owner == user. We only captured a single
password at load time; the original owner password is not recoverable
from the input file. Future enhancement would be a separate prompt for
the owner password.
Adds shared mode-index constants (_MODE_REDACT etc.) for readability —
call-sites can now write `if idx == _MODE_FORMS:` instead of `== 5`.
New i18n keys: editor.encrypt.warning_title, editor.encrypt.warning_text,
editor.encrypt.save_protected, editor.encrypt.save_unprotected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(editor): warn before forms apply discards pending edits (HIGH)
When the user opened a PDF, made overlay/redact/note edits, then
switched to Forms mode and hit Save, the Forms apply path ran first
and the pending edits were silently discarded.
_run now checks for pending edits before delegating to _apply_forms in
Forms mode and pops a Yes/No QMessageBox explaining the trade-off.
Default button is No so accidental Enter doesn't lose data.
New i18n key: editor.forms.has_pending.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(editor): persist note deletion + push to redo stack (HIGH)
Two related note bugs:
1) Existing-annotation deletes evaporated on save. The canvas mutated
_canvas._doc in-memory but _run did release_doc() + fitz.open(path),
so the deletion was lost. Existing notes now carry their annot type
+ bbox at load time; deleting one registers a `delete_annot` pending
edit which _run applies against the freshly-reopened doc by matching
on (type, bbox) instead of xref (xref is not preserved across reopen).
2) Pending-note delete didn't push to _redo_stack -- Ctrl+Y could not
restore the note, and a subsequent Ctrl+Z recovered some other
arbitrary edit. _on_note_deleted now appends the removed entry to
_redo_stack (without clearing it, so unrelated redo state is kept).
Also adds a label entry + KeyError-safe .get() fallback in _add so
unknown future edit types don't crash the QListWidget update.
New i18n key: edit.label.note_delete.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(editor): mode switch UX + image dialog flap + forms undo guard
Three UX fixes around mode switching and undo:
- Image mode (#4): Clicking the Image mode button no longer reopens
the file picker every single time. It now mirrors the signature flow
and only fires QFileDialog if no image is chosen yet (or the chosen
file has since vanished).
- Inline edit (#9): Switching modes while _inline_edit is open used to
leave the typed text in limbo. _on_mode_btn now cancels the in-flight
inline edit explicitly.
- Forms undo (#8): Forms-mode edits live in the QTableWidget itself and
are intentionally not tracked in _pending. Ctrl+Z previously did
nothing silently; now _undo emits a status hint, and the undo/redo
button tooltips swap to a "not available in Forms mode" string while
the mode is active.
New i18n key: editor.forms.undo_unavailable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(editor): surface form-load failures + auto-regen widget appearance
Two related Forms-mode polish items:
- _load_form_fields previously swallowed every exception silently, so
a malformed PDF read produced an empty table that looked identical
to a PDF with no form fields. The except path now logs the exception
and shows editor.forms.load_failed in a new status label below the
table. A successful-but-empty load shows editor.forms.no_fields so
the user can tell the two states apart.
- _apply_forms now passes auto_regenerate=True to pypdf
update_page_form_field_values so the rendered widget appearance
picks up the new value in third-party viewers that don't honour
NeedAppearances.
New i18n keys: editor.forms.load_failed, editor.forms.no_fields.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(editor): regression tests for Round 9 audit fixes
Thirty source-level + behavioural tests covering each of the 12 fixes
plus the LOW polish items. Pattern mirrors tests/test_round8_fixes.py:
read the touched source, assert the new wiring is present, so a future
refactor that drops a guard produces a visible test failure.
Behavioural coverage (no full Qt event loop required):
- _on_note_deleted pushes the popped entry onto _redo_stack and keeps
the QListWidget in lock-step.
- _load_overlay_pixmap LRU cache returns the same QPixmap instance for
a repeated (path, mtime) call.
i18n parity: explicitly verifies all 12 new keys exist in every shipping
locale + asserts the global key set is consistent across locales.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(editor): persist existing-note deletion via _pending path
Note delete persistence was only triggered on late-discovery via
_annot_note_at. The common case (notes loaded in
_load_existing_annotations and queued into _pending with
_existing=True) never reached the delete_annot enqueue branch — the
first for loop in _on_note_deleted matched on text+page and returned
early, leaving the dedicated _existing block unreachable.
Now both paths enqueue a delete_annot edit, fully persisting the
deletion to the output file. The original note dict is stashed on the
edit (_original_note) so _undo can restore the canvas overlay if the
user reverses the action.
Additionally:
- _undo now re-adds the original note to _pending when a delete_annot
is rolled back, keeping the canvas in sync with the pending queue.
- New test_note_deleted_existing_enqueues_delete_annot covers the
enqueue + undo-restore behaviour end-to-end with a Qt-light stub.
- Optional cleanup: dropped the dead pass-only elif branch in
canvas.mouseReleaseEvent and removed the redundant try/except around
_fitz_permissions_of (already returns -1 on any failure).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(editor): cosmetic cleanup per CodeQL notes
- Remove dead _MODE_* constants in editor/tab.py (declared but never read).
- Drop orphan `import re` in tests/test_editor_audit_r9.py.
- Rename `app = QApplication(...)` to `_app = ...` in 3 test bodies
to match the suppression pattern used elsewhere.
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Test plan
🤖 Generated with Claude Code