diff --git a/README.md b/README.md index a603173..eaebd33 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ - [x] UI chế độ "Sửa nội dung": overlay đối tượng, sửa text tại chỗ WYSIWYG, thêm chữ/ảnh, xoá, cỡ chữ/màu, undo/redo, lưu - [x] **Iteration 2 — GIỮ FONT khi sửa (chuẩn Foxit)**: sửa tại chỗ giữ nguyên font gốc/nhúng (kể cả tiếng Việt); thiếu glyph mới thay font CÙNG HỌ (`fontmatch.rs`); đổi cỡ/màu không đụng font (fix bug phóng đại kép); sửa CẢ DÒNG (gộp run); WYSIWYG khi gõ; B/I + đổi font family; kéo-thả live; dọn file tạm - [x] **Iteration 3 — Reflow đoạn "như Word"**: double-click đoạn nhiều dòng → sửa cả đoạn, tự bẻ dòng theo bề rộng khối (đo hmtx bằng ttf-parser), giữ font (nhúng lại bytes gốc / font chuẩn base-14 / cùng họ), giữ nhịp baseline; Enter = ngắt cứng + - [x] **Đúp chuột từ viewer thường (chuẩn Foxit)**: đang xem, đúp vào chữ → tự vào chế độ sửa đúng trang + mở ngay ô sửa tại đoạn đó; đúp vùng trống không làm gì - [ ] Follow-up: reflow v2 (justify, khối xoay, đa font), xoay/lật/clip, viền/opacity/căn lề, convert text→path — xem [docs/12-phase4-summary.md](docs/12-phase4-summary.md) - **Checklist test cho bạn**: [docs/13-phase4-user-tests.md](docs/13-phase4-user-tests.md) (mới: E17–E30) - [x] **Phase 5 — Bảo mật** ✅ Iteration 1 + 2 hoàn tất (62/62 test engine xanh) diff --git a/app/src/main.js b/app/src/main.js index 3354759..45e841f 100644 --- a/app/src/main.js +++ b/app/src/main.js @@ -37,6 +37,7 @@ const state = { editScale: 1, // px/pt của ảnh stage editArm: null, // 'text' | 'image' khi đang chờ click đặt; null = không editPendingImage: null, // đường dẫn ảnh chờ đặt (Thêm ảnh) + editPendingPoint: null, // điểm PDF chờ mở ô sửa (đúp chuột từ viewer thường) editColor: [0, 0, 0], // màu chữ áp khi sửa/thêm text editTemps: [], // mọi file tạm đã materialize trong phiên sửa (để dọn) secMode: false, // thanh Bảo mật (Phase 5) đang mở @@ -2033,11 +2034,11 @@ function openCropDialog(pageIdx, rectPdf) { const EDIT_STAGE_W = 820; // px bề rộng ảnh trang khi sửa -function enterEditMode() { +function enterEditMode(pageIndex) { if (!state.path) { $("status").textContent = "Hãy mở file trước khi sửa nội dung"; return; } if (state.organizeMode) exitOrganizeMode(); state.editMode = true; - state.editPage = state.current; + state.editPage = Number.isInteger(pageIndex) ? pageIndex : state.current; state.editBase = state.path; state.editUndo = []; state.editRedo = []; @@ -2079,6 +2080,34 @@ function toggleEditMode() { else enterEditMode(); } +// Đúp chuột vào CHỮ trên viewer thường (chuẩn Foxit Editor): tự vào chế độ +// Sửa nội dung đúng trang + mở ngay ô sửa tại đoạn vừa đúp. Đúp vùng trống +// hoặc đang cầm công cụ chú thích thì không làm gì. +async function onViewerDblClick(e) { + if (state.editMode || state.organizeMode || state.tool) return; + // Chú thích có handler đúp riêng (freetext) hoặc là element tương tác — bỏ qua. + if (e.target.closest(".a-editor, .a-hl, .a-box, .a-text, .a-note, .redact-mark")) return; + const slot = e.target.closest(".page-slot"); + if (!slot) return; + const idx = Number(slot.dataset.index); + const r = slot.getBoundingClientRect(); + const p = cssToPdf(idx, e.clientX - r.left, e.clientY - r.top); + + let objs; + try { + objs = await invoke("edit_list_objects", { path: state.path, page: idx, password: null }); + } catch (_) { + return; // trang không đọc được object → giữ nguyên viewer + } + if (!pickTextAt(objs, p.x, p.y)) return; // đúp vùng trống: như Foxit, không làm gì + + // Bỏ selection từ đúp chuột (browser tự bôi 1 từ trong text layer). + const sel = window.getSelection(); + if (sel) sel.removeAllRanges(); + state.editPendingPoint = { x: p.x, y: p.y }; + enterEditMode(idx); +} + // Đọc lại object + render ảnh trang hiện tại từ editBase, dựng overlay box. async function loadEditPage() { const p = state.pages[state.editPage]; @@ -2097,6 +2126,44 @@ async function loadEditPage() { } $("edSave").disabled = state.editBase === state.path; // chưa có thay đổi nào updateEditUndoButtons(); + + // Đúp chuột từ viewer thường (kiểu Foxit): vào thẳng ô sửa tại điểm đã đúp. + const pending = state.editPendingPoint; + if (pending) { + state.editPendingPoint = null; + const o = pickTextAt(state.editObjects, pending.x, pending.y); + if (o) { + selectEditObject(o.index); + startTextEdit(o); + } + } +} + +// Text object dưới điểm (x,y) điểm PDF: ưu tiên box CHỨA điểm (nới 3pt, chọn +// box nhỏ nhất khi chồng nhau), fallback box gần nhất trong 12pt. +function pickTextAt(objs, x, y) { + const texts = objs.filter((o) => o.kind === "text"); + const pad = 3; + const containing = texts.filter( + (o) => x >= o.rect.left - pad && x <= o.rect.right + pad && y >= o.rect.bottom - pad && y <= o.rect.top + pad + ); + if (containing.length) { + containing.sort( + (a, b) => + (a.rect.right - a.rect.left) * (a.rect.top - a.rect.bottom) - + (b.rect.right - b.rect.left) * (b.rect.top - b.rect.bottom) + ); + return containing[0]; + } + let best = null; + let bestD = 12; + for (const o of texts) { + const dx = Math.max(o.rect.left - x, 0, x - o.rect.right); + const dy = Math.max(o.rect.bottom - y, 0, y - o.rect.top); + const d = Math.hypot(dx, dy); + if (d < bestD) { bestD = d; best = o; } + } + return best; } function editBoxStyle(rect) { @@ -3299,6 +3366,7 @@ $("saveAnnots").addEventListener("click", saveAnnots); $("undoBtn").addEventListener("click", () => (state.editMode ? editUndo() : undo())); $("redoBtn").addEventListener("click", () => (state.editMode ? editRedo() : redo())); $("pages").addEventListener("mousedown", onPagesMouseDown); +$("pages").addEventListener("dblclick", onViewerDblClick); window.addEventListener("mousemove", onPagesMouseMove); window.addEventListener("mouseup", onPagesMouseUp); diff --git a/docs/13-phase4-user-tests.md b/docs/13-phase4-user-tests.md index 0e90158..cad4340 100644 --- a/docs/13-phase4-user-tests.md +++ b/docs/13-phase4-user-tests.md @@ -41,6 +41,13 @@ cargo run | E24 | Sửa text tiếng Việt trên file có font nhúng (file Word xuất PDF) | Đúng dấu **và giữ nguyên font nhúng** (mở lại ở Foxit xem tên font trong Edit) | | | E25 | Sửa nhiều lần rồi thoát chế độ sửa (không lưu) | Không còn file `ff_edit_*.pdf` rác trong thư mục %TEMP% | | +## Bổ sung — Đúp chuột vào chữ từ viewer thường (chuẩn Foxit Editor) +| # | Thao tác | Kỳ vọng | KQ | +|---|----------|---------|----| +| E31 | Đang XEM bình thường (không bật chế độ sửa), **đúp chuột vào 1 dòng chữ** | Tự vào chế độ Sửa nội dung đúng trang đó và Ô SỬA mở sẵn ngay tại dòng/đoạn vừa đúp | | +| E32 | Đúp chuột vào VÙNG TRỐNG của trang | Không có gì xảy ra (như Foxit) | | +| E33 | Đang cầm công cụ chú thích (highlight/note...) rồi đúp vào chữ | KHÔNG nhảy vào chế độ sửa (công cụ chú thích được ưu tiên) | | + ## Bổ sung Iteration 3 — Sửa cả đoạn với reflow "như Word" | # | Thao tác | Kỳ vọng | KQ | |---|----------|---------|----|