mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 05:58:12 -06:00
This commit is contained in:
@@ -3870,6 +3870,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
if let Some((p, i)) = app.views[index].comments.default_request.take() {
|
||||
app.make_comment_default(p, i);
|
||||
}
|
||||
if let Some((p, i, open)) = app.views[index].comments.attachment_request.take() {
|
||||
let doc = app.views[index].id;
|
||||
app.comment_attachment_action(doc, p, i, open);
|
||||
}
|
||||
if let Some((name, w)) = field_props {
|
||||
app.open_field_props(&name, w);
|
||||
}
|
||||
|
||||
@@ -504,6 +504,9 @@ pub struct CommentView {
|
||||
pub props_request: Option<(usize, usize)>,
|
||||
/// Make Current Properties Default was asked for: (page, index).
|
||||
pub default_request: Option<(usize, usize)>,
|
||||
/// A file attachment comment's file was asked for: (page, index, open it in a new tab
|
||||
/// rather than save it).
|
||||
pub attachment_request: Option<(usize, usize, bool)>,
|
||||
/// Replace Text: the struck-out text's quads while the replacement is typed.
|
||||
pub replace: Option<Vec<[f64; 8]>>,
|
||||
/// Attach file: where the icon goes (page, top-left in user space), for the app to pick a file.
|
||||
@@ -1595,6 +1598,23 @@ pub(crate) fn keys(ctx: &egui::Context, view: &mut DocView, tool: &mut QuickTool
|
||||
}
|
||||
}
|
||||
|
||||
/// Save attachment… and, for a PDF, Open in a new tab, first in a file attachment comment's
|
||||
/// menus as in the Attachments panel: the comment is where the reader finds its file. Reading
|
||||
/// the file changes nothing, so a read-only document offers them too.
|
||||
pub(crate) fn attachment_items(ui: &mut egui::Ui, view: &mut DocView, info: &DocInfo, a: &Annotation) {
|
||||
let source = pdfcraft_render::AttachmentSource::Annotation { page: a.page, index: a.index };
|
||||
let Some(file) = info.attachments.iter().find(|f| f.source == source) else { return };
|
||||
if ui.button(tl!("Save attachment…")).clicked() {
|
||||
view.comments.attachment_request = Some((a.page, a.index, false));
|
||||
ui.close();
|
||||
}
|
||||
if file.name.to_lowercase().ends_with(".pdf") && ui.button(tl!("Open in a new tab")).clicked() {
|
||||
view.comments.attachment_request = Some((a.page, a.index, true));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
/// Items of the canvas context menu for the selected comment (or the page).
|
||||
pub(crate) fn context_menu(ui: &mut egui::Ui, view: &mut DocView, info: &DocInfo, prefs: &CommentPrefs, allowed: bool) -> Option<CanvasAction> {
|
||||
let mut action = None;
|
||||
@@ -1602,6 +1622,7 @@ pub(crate) fn context_menu(ui: &mut egui::Ui, view: &mut DocView, info: &DocInfo
|
||||
match selected {
|
||||
Some(a) => {
|
||||
let (page, index) = (a.page, a.index);
|
||||
attachment_items(ui, view, info, a);
|
||||
if ui.add_enabled(allowed, egui::Button::new(tl!("Edit text…"))).clicked() {
|
||||
view.comments.composer = Some(Composer {
|
||||
page,
|
||||
|
||||
@@ -255,7 +255,7 @@ pub(crate) fn show(
|
||||
continue;
|
||||
}
|
||||
let thread = replies_of(a);
|
||||
if let Some(e) = card(ui, t, a, &thread, view, prefs, allowed, nav) {
|
||||
if let Some(e) = card(ui, t, a, &thread, info, view, prefs, allowed, nav) {
|
||||
edit = Some(e);
|
||||
}
|
||||
}
|
||||
@@ -268,6 +268,7 @@ fn card(
|
||||
t: &Tokens,
|
||||
a: &Annotation,
|
||||
thread: &[&Annotation],
|
||||
info: &DocInfo,
|
||||
view: &mut DocView,
|
||||
prefs: &CommentPrefs,
|
||||
allowed: bool,
|
||||
@@ -447,6 +448,7 @@ fn card(
|
||||
.layout(Layout::top_down_justified(Align::Min))
|
||||
.show(|ui| {
|
||||
ui.set_min_width(160.0);
|
||||
crate::comments::attachment_items(ui, view, info, a);
|
||||
if ui.button(tl_ctx!("comment menu", "Edit")).clicked() {
|
||||
view.comments.editing = Some((a.page, a.index, a.contents.clone().unwrap_or_default()));
|
||||
ui.close();
|
||||
|
||||
@@ -1541,6 +1541,15 @@ impl PdfCraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::attachment_action`] for the file of the attachment comment at (page, index).
|
||||
pub fn comment_attachment_action(&mut self, doc: DocId, page: usize, index: usize, open: bool) {
|
||||
let source = pdfcraft_render::AttachmentSource::Annotation { page, index };
|
||||
// The menus offer the items only for a listed file, so a miss is a comment gone since.
|
||||
if let Some(i) = self.session.get(doc).and_then(|d| d.info.attachments.iter().position(|a| a.source == source)) {
|
||||
self.attachment_action(doc, i, open);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the app draws the window's title bar, buttons and resize edges: the desktop leaves
|
||||
/// the frame to the app and Use system title bar is off.
|
||||
pub fn draws_window_frame(&self) -> bool {
|
||||
|
||||
@@ -800,6 +800,9 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
if let Some((p, i)) = app.views.get_mut(index).and_then(|v| v.comments.default_request.take()) {
|
||||
app.make_comment_default(p, i);
|
||||
}
|
||||
if let Some((p, i, open)) = app.views.get_mut(index).and_then(|v| v.comments.attachment_request.take()) {
|
||||
app.comment_attachment_action(id, p, i, open);
|
||||
}
|
||||
if let Some((p, i)) = app.views.get_mut(index).and_then(|v| v.comments.props_request.take()) {
|
||||
app.open_comment_props(p, i);
|
||||
}
|
||||
|
||||
@@ -1213,6 +1213,91 @@ fn attaching_a_file_as_a_comment() {
|
||||
assert_eq!(s.quick_tool, QuickTool::Select);
|
||||
}
|
||||
|
||||
/// One 300×200 page with a paperclip FileAttachment comment at (100, 100)–(120, 124) carrying
|
||||
/// `inner.pdf`, and no /Contents: the card shows only "Attachment" (#2565). The cross-reference
|
||||
/// table keeps the reader from mistaking the embedded PDF's objects for the outer ones.
|
||||
fn attachment_comment_fixture() -> Vec<u8> {
|
||||
let inner = "%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >> endobj\ntrailer << /Root 1 0 R >>\n%%EOF";
|
||||
let objs = [
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Annots [4 0 R] >>".into(),
|
||||
"<< /Type /Annot /Subtype /FileAttachment /Rect [100 100 120 124] /Name /Paperclip /T (Reviewer) /M (D:20261014105200) /FS 5 0 R >>".into(),
|
||||
"<< /Type /Filespec /F (inner.pdf) /UF (inner.pdf) /EF << /F 6 0 R >> >>".into(),
|
||||
format!("<< /Type /EmbeddedFile /Length {} >>\nstream\n{inner}\nendstream", inner.len()),
|
||||
];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offs = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offs.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let x = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offs {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{x}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
fn attachment_harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("annexed.pdf", None, attachment_comment_fixture()).expect("opens");
|
||||
app.set_option("left", "closed").unwrap();
|
||||
setup(&mut app);
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
fn open_tab_names(h: &Harness<'static, PdfCraftApp>) -> Vec<String> {
|
||||
let s = h.state();
|
||||
s.views.iter().filter_map(|v| s.session.get(v.id)).map(|d| d.name.clone()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_attachment_comment_opens_its_file_from_the_page_menu() {
|
||||
let mut h = attachment_harness(|app| {
|
||||
app.set_option("panel", "comments").unwrap();
|
||||
app.set_option("quick", "select").unwrap();
|
||||
});
|
||||
assert_eq!(comments(&h)[0].subtype, "FileAttachment");
|
||||
click_pt(&mut h, (110.0, 112.0));
|
||||
assert_eq!(h.state().views[0].comments.selected, Some((0, 0)), "the attachment comment is selected");
|
||||
let p = at(&h, 110.0, 112.0);
|
||||
h.hover_at(p);
|
||||
h.run_steps(1);
|
||||
h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Secondary, pressed: true, modifiers: Default::default() });
|
||||
h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Secondary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Copy text");
|
||||
// The Attachments panel is closed, so these items can only come from the comment's menu.
|
||||
h.get_by_label("Save attachment…");
|
||||
h.get_by_label("Open in a new tab").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(open_tab_names(&h), ["annexed.pdf", "inner.pdf"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_attachment_comment_opens_its_file_from_the_card_menu() {
|
||||
let mut h = attachment_harness(|app| app.set_option("panel", "comments").unwrap());
|
||||
h.state_mut().views[0].comments.selected = Some((0, 0));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("More").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Copy text");
|
||||
h.get_by_label("Save attachment…");
|
||||
h.get_by_label("Open in a new tab").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(open_tab_names(&h), ["annexed.pdf", "inner.pdf"]);
|
||||
}
|
||||
|
||||
/// Drag with the middle button (the mouse wheel) in small steps.
|
||||
fn middle_drag(h: &mut Harness<'static, PdfCraftApp>, from: Pos2, to: Pos2) {
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Middle, pressed, modifiers: egui::Modifiers::NONE };
|
||||
|
||||
@@ -3812,8 +3812,8 @@ milestone = "M5"
|
||||
status = "shipped"
|
||||
commands = ["comment.attach"]
|
||||
tools = ["comment_add"]
|
||||
evidence = ["crates/annot/src/tests.rs::files_attach_as_comments", "crates/ui-egui/tests/comments.rs::attaching_a_file_as_a_comment", "crates/automation/tests/automation.rs::drawing_comments_through_tools"]
|
||||
notes = "push pin, paperclip, graph or tag icon; the file is embedded and listed in the Attachments panel"
|
||||
evidence = ["crates/annot/src/tests.rs::files_attach_as_comments", "crates/ui-egui/tests/comments.rs::attaching_a_file_as_a_comment", "crates/ui-egui/tests/comments.rs::an_attachment_comment_opens_its_file_from_the_page_menu", "crates/ui-egui/tests/comments.rs::an_attachment_comment_opens_its_file_from_the_card_menu", "crates/automation/tests/automation.rs::drawing_comments_through_tools"]
|
||||
notes = "push pin, paperclip, graph or tag icon; the file is embedded and listed in the Attachments panel. The comment's menu, on the page and in the Comments panel, saves the file or opens a PDF in a new tab (#2565)"
|
||||
|
||||
[[feature]]
|
||||
id = "comment.record-audio"
|
||||
|
||||
Reference in New Issue
Block a user