refactor(ui): simplify image and page context menus

Share click handling, set crop mode in the image menu, remove duplicate export and inactive editor entries, and format shortcuts for the platform. Add a selection constructor for callers.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
This commit is contained in:
FredApps
2026-11-01 12:39:45 +01:00
co-authored by Codex GPT-6.1
parent 3c394bb860
commit 91f94ec26a
3 changed files with 111 additions and 190 deletions
+50 -111
View File
@@ -182,21 +182,7 @@ impl ImageObb {
let v_axis = (c / h_v, d / h_v);
let is_rotated = b.abs() > 1e-4 || c.abs() > 1e-4;
Self {
s00,
s10,
s11,
s01,
s_bc,
s_rc,
s_tc,
s_lc,
s_rot,
screen_center,
u_axis,
v_axis,
is_rotated,
}
Self { s00, s10, s11, s01, s_bc, s_rc, s_tc, s_lc, s_rot, screen_center, u_axis, v_axis, is_rotated }
}
pub fn from_image(im: &pdfcraft_engine::PageImage, xf: &PageXform, info: &DocInfo, page: usize) -> Self {
@@ -731,8 +717,8 @@ pub enum ViewAction {
/// Default page right-click menu actions.
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum PageMenuAction {
RotateCCW(usize),
RotateCW(usize),
RotateCCW,
RotateCW,
Print,
SelectTool,
HandTool,
@@ -740,7 +726,7 @@ pub(crate) enum PageMenuAction {
Copy,
Paste(usize, [f64; 2]),
AddComment(usize, [f64; 2]),
AddBookmark(usize),
AddBookmark,
TakeSnapshot,
EditPdf,
AddText,
@@ -763,100 +749,66 @@ pub(crate) struct PageMenuPerms {
}
pub(crate) fn default_page_context_menu(ui: &mut egui::Ui, page: usize, at: [f64; 2], perms: PageMenuPerms, action: &mut Option<PageMenuAction>) {
use PageMenuAction as A;
// Takes `choice` when `entry` was clicked, and closes the menu.
let mut pick = |entry: egui::Response, ui: &mut egui::Ui, choice: PageMenuAction| {
if entry.clicked() {
*action = Some(choice);
ui.close();
}
};
let icon = |ui: &mut egui::Ui, name: &str, tip: &str| crate::icons::button(ui, name, 24.0, false, tip);
// Top horizontal icon bar: Rotate CCW, Print, Select tool, Hand tool, Marquee Zoom
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
if crate::icons::button(ui, "rotate-ccw", 24.0, false, tl!("Rotate view counterclockwise")).clicked() {
*action = Some(PageMenuAction::RotateCCW(page));
ui.close();
}
ui.add_enabled_ui(perms.can_print, |ui| {
if crate::icons::button(ui, "printer", 24.0, false, tl!("Print")).clicked() {
*action = Some(PageMenuAction::Print);
ui.close();
}
});
if crate::icons::button(ui, "mouse-pointer-2", 24.0, false, tl!("Select Tool")).clicked() {
*action = Some(PageMenuAction::SelectTool);
ui.close();
}
if crate::icons::button(ui, "hand", 24.0, false, tl!("Hand Tool")).clicked() {
*action = Some(PageMenuAction::HandTool);
ui.close();
}
if crate::icons::button(ui, "zoom-in", 24.0, false, tl!("Marquee Zoom")).clicked() {
*action = Some(PageMenuAction::MarqueeZoom);
ui.close();
}
pick(icon(ui, "rotate-ccw", tl!("Rotate view counterclockwise")), ui, A::RotateCCW);
ui.add_enabled_ui(perms.can_print, |ui| pick(icon(ui, "printer", tl!("Print")), ui, A::Print));
pick(icon(ui, "mouse-pointer-2", tl!("Select Tool")), ui, A::SelectTool);
pick(icon(ui, "hand", tl!("Hand Tool")), ui, A::HandTool);
pick(icon(ui, "zoom-in", tl!("Marquee Zoom")), ui, A::MarqueeZoom);
});
ui.separator();
if ui.add_enabled(perms.can_copy, egui::Button::new(tl!("Copy")).shortcut_text("Ctrl+C")).clicked() {
*action = Some(PageMenuAction::Copy);
ui.close();
}
if ui.add_enabled(perms.can_paste, egui::Button::new(tl!("Paste")).shortcut_text("Ctrl+V")).clicked() {
*action = Some(PageMenuAction::Paste(page, at));
ui.close();
}
pick(
ui.add_enabled(
perms.can_copy,
egui::Button::new(tl!("Copy")).shortcut_text(crate::commands::shortcut_label(ui.ctx(), crate::commands::COPY)),
),
ui,
A::Copy,
);
pick(
ui.add_enabled(
perms.can_paste,
egui::Button::new(tl!("Paste")).shortcut_text(crate::commands::shortcut_label(ui.ctx(), pdfcraft_engine::commands::Shortcut::cmd("V"))),
),
ui,
A::Paste(page, at),
);
ui.separator();
if ui.add_enabled(perms.can_annotate, egui::Button::new(tl!("Add comment"))).clicked() {
*action = Some(PageMenuAction::AddComment(page, at));
ui.close();
}
if perms.has_annotations && ui.add_enabled(perms.can_modify, egui::Button::new(tl!("Flatten comments"))).clicked() {
*action = Some(PageMenuAction::FlattenComments);
ui.close();
}
if ui.add_enabled(perms.can_assemble, egui::Button::new(tl!("Add bookmark"))).clicked() {
*action = Some(PageMenuAction::AddBookmark(page));
ui.close();
}
if ui.add_enabled(perms.can_copy, egui::Button::new(tl!("Take a snapshot"))).clicked() {
*action = Some(PageMenuAction::TakeSnapshot);
ui.close();
pick(ui.add_enabled(perms.can_annotate, egui::Button::new(tl!("Add comment"))), ui, A::AddComment(page, at));
if perms.has_annotations {
pick(ui.add_enabled(perms.can_modify, egui::Button::new(tl!("Flatten comments"))), ui, A::FlattenComments);
}
pick(ui.add_enabled(perms.can_assemble, egui::Button::new(tl!("Add bookmark"))), ui, A::AddBookmark);
pick(ui.add_enabled(perms.can_copy, egui::Button::new(tl!("Take a snapshot"))), ui, A::TakeSnapshot);
ui.separator();
if ui.add_enabled(perms.can_modify, egui::Button::new(egui::RichText::new(tl!("Edit this PDF")).strong())).clicked() {
*action = Some(PageMenuAction::EditPdf);
ui.close();
}
if ui.add_enabled(perms.can_modify, egui::Button::new(tl!("Add text"))).clicked() {
*action = Some(PageMenuAction::AddText);
ui.close();
}
if ui.add_enabled(perms.can_modify, egui::Button::new(tl!("Add image"))).clicked() {
*action = Some(PageMenuAction::AddImage);
ui.close();
}
if ui.add_enabled(perms.can_assemble, egui::Button::new(tl!("Delete page"))).clicked() {
*action = Some(PageMenuAction::DeletePage(page));
ui.close();
}
if ui.add_enabled(perms.can_copy, egui::Button::new(tl!("Export this PDF"))).clicked() {
*action = Some(PageMenuAction::ExportPdf);
ui.close();
}
pick(ui.add_enabled(perms.can_modify, egui::Button::new(egui::RichText::new(tl!("Edit this PDF")).strong())), ui, A::EditPdf);
pick(ui.add_enabled(perms.can_modify, egui::Button::new(tl!("Add text"))), ui, A::AddText);
pick(ui.add_enabled(perms.can_modify, egui::Button::new(tl!("Add image"))), ui, A::AddImage);
pick(ui.add_enabled(perms.can_assemble, egui::Button::new(tl!("Delete page"))), ui, A::DeletePage(page));
pick(ui.add_enabled(perms.can_copy, egui::Button::new(tl!("Export this PDF"))), ui, A::ExportPdf);
ui.separator();
if ui.button(tl!("Find text or tools")).clicked() {
*action = Some(PageMenuAction::Find);
ui.close();
}
if ui.button(tl!("Rotate view clockwise")).clicked() {
*action = Some(PageMenuAction::RotateCW(page));
ui.close();
}
if ui.button(tl!("Document properties…")).clicked() {
*action = Some(PageMenuAction::DocProperties);
ui.close();
}
pick(ui.button(tl!("Find text or tools")), ui, A::Find);
pick(ui.button(tl!("Rotate view clockwise")), ui, A::RotateCW);
pick(ui.button(tl!("Document properties…")), ui, A::DocProperties);
}
impl DocView {
@@ -3951,17 +3903,6 @@ fn apply_frame_requests(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui,
}
}
}
Some(crate::edit_text_ui::ImageAction::Crop) => {
let doc_id = app.views[index].id;
if let Some(s) = app.views[index].image_selection.as_mut()
&& let Some(d) = app.session.get(doc_id)
{
let images = d.page_images(s.page);
if let Some(im) = images.get(s.index) {
s.crop = Some(crate::edit_text_ui::CropMode::new(im.visible_rect.unwrap_or(im.rect)));
}
}
}
None => {}
}
match page_action {
@@ -3980,12 +3921,10 @@ fn apply_frame_requests(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui,
Some(PageMenuAction::Paste(page, at)) => {
app.paste_image_at(page, at);
}
Some(PageMenuAction::RotateCCW(page)) => {
let _ = page;
Some(PageMenuAction::RotateCCW) => {
app.views[index].rotate_view(false);
}
Some(PageMenuAction::RotateCW(page)) => {
let _ = page;
Some(PageMenuAction::RotateCW) => {
app.views[index].rotate_view(true);
}
Some(PageMenuAction::Print) => {
@@ -4008,7 +3947,7 @@ fn apply_frame_requests(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui,
app.views[index].comments.selected = None;
app.views[index].pending_edit = Some(pdfcraft_engine::Edit::Flatten { comments: true, fields: false });
}
Some(PageMenuAction::AddBookmark(_page)) => {
Some(PageMenuAction::AddBookmark) => {
app.bookmark_action(crate::panels::BmAction::New);
}
Some(PageMenuAction::TakeSnapshot) => {
+61 -77
View File
@@ -92,6 +92,11 @@ pub struct ImageSelection {
}
impl ImageSelection {
/// Select a page image without an active gesture.
pub fn at(page: usize, index: usize) -> Self {
Self { page, index, drag: None, crop: None, crop_mode: false, crop_rect: None, turning: false, settled: None }
}
/// Cancel an in-progress text, rotation or crop drag and restore the crop it was previewing.
pub(crate) fn cancel_pointer_gesture(&mut self) {
self.drag = None;
@@ -489,7 +494,6 @@ pub enum ImageAction {
Paste(usize, Option<[f64; 2]>),
DrawFreehand,
AddComment(usize, usize),
Crop,
}
/// Where the rotate knob sits, and the edge point its stem starts from: above the middle of the
@@ -1051,8 +1055,7 @@ pub(crate) fn image_input(cx: &ImageCx<'_>, images: &[pdfcraft_engine::PageImage
_ => (resp.clicked() || resp.secondary_clicked()).then_some(hit),
};
if let Some(index) = clicked {
view.image_selection =
Some(ImageSelection { page, index, drag: None, crop: None, crop_mode: false, crop_rect: None, turning: false, settled: None });
view.image_selection = Some(ImageSelection::at(page, index));
}
if resp.secondary_clicked() {
ui.ctx().data_mut(|d| d.insert_persisted(menu_key, true));
@@ -1076,133 +1079,114 @@ pub(crate) fn image_context_menu(
use pdfcraft_engine::ImageEdit as E;
let raster = !img.is_form;
let close = |ui: &mut egui::Ui| {
ui.ctx().data_mut(|d| d.insert_persisted(image_menu_id(), false));
egui::Popup::close_all(ui.ctx());
ui.close();
// Whether `entry` was clicked; a click closes the menu.
let picked = |ui: &mut egui::Ui, entry: egui::Response| {
let clicked = entry.clicked();
if clicked {
ui.ctx().data_mut(|d| d.insert_persisted(image_menu_id(), false));
egui::Popup::close_all(ui.ctx());
ui.close();
}
clicked
};
let item = |ui: &mut egui::Ui, enabled: bool, label: &str| {
let entry = ui.add_enabled(enabled, egui::Button::new(label));
picked(ui, entry)
};
let edit = |change| Some(Edit::EditPageImage { page, index: hit, change });
// Top horizontal icon bar: Comment, Crop, Rotate CCW, Rotate CW
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
if crate::icons::button(ui, "message-square", 24.0, false, tl!("Add a sticky note here")).clicked() {
let icon = |ui: &mut egui::Ui, name: &str, tip: &str| {
let entry = crate::icons::button(ui, name, 24.0, false, tip);
picked(ui, entry)
};
if icon(ui, "message-square", tl!("Add a sticky note here")) {
*action = Some(ImageAction::AddComment(page, hit));
close(ui);
}
if raster && crate::icons::button(ui, "crop", 24.0, false, tl!("Crop")).clicked() {
*action = Some(ImageAction::Crop);
if let Some(s) = view.image_selection.as_mut() {
let rect = img.visible_rect.unwrap_or(img.rect);
s.crop = Some(CropMode::new(rect));
}
close(ui);
if raster
&& icon(ui, "crop", tl!("Crop"))
&& let Some(s) = view.image_selection.as_mut()
{
s.crop = Some(CropMode::new(img.visible_rect.unwrap_or(img.rect)));
}
if crate::icons::button(ui, "rotate-ccw", 24.0, false, tl!("Rotate Counterclockwise")).clicked() {
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::Rotate(3) });
close(ui);
if icon(ui, "rotate-ccw", tl!("Rotate Counterclockwise")) {
view.pending_edit = edit(E::Rotate(3));
}
if crate::icons::button(ui, "rotate-cw", 24.0, false, tl!("Rotate Clockwise")).clicked() {
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::Rotate(1) });
close(ui);
if icon(ui, "rotate-cw", tl!("Rotate Clockwise")) {
view.pending_edit = edit(E::Rotate(1));
}
});
ui.separator();
if ui.button(tl!("Cut Image")).clicked() {
if item(ui, true, tl!("Cut Image")) {
*action = Some(ImageAction::Cut(page, hit));
close(ui);
}
if ui.button(tl!("Copy Image")).clicked() {
if item(ui, true, tl!("Copy Image")) {
*action = Some(ImageAction::Copy(page, hit));
close(ui);
}
if ui.add_enabled(can_paste, egui::Button::new(tl!("Paste"))).clicked() {
if item(ui, can_paste, tl!("Paste")) {
*action = Some(ImageAction::Paste(page, at));
close(ui);
}
ui.separator();
let is_cropped = img.visible_rect.is_some() && img.visible_rect != Some(img.rect);
if raster && ui.add_enabled(is_cropped, egui::Button::new(tl!("Remove crop"))).clicked() {
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::RemoveCrop });
close(ui);
if raster && item(ui, is_cropped, tl!("Remove crop")) {
view.pending_edit = edit(E::RemoveCrop);
}
if ui.button(tl!("Flip horizontal")).clicked() {
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::Flip { horizontal: true } });
close(ui);
if item(ui, true, tl!("Flip horizontal")) {
view.pending_edit = edit(E::Flip { horizontal: true });
}
if ui.button(tl!("Flip vertical")).clicked() {
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::Flip { horizontal: false } });
close(ui);
if item(ui, true, tl!("Flip vertical")) {
view.pending_edit = edit(E::Flip { horizontal: false });
}
ui.menu_button(tl!("Arrange image"), |ui| {
if ui.button(tl!("Bring to front")).clicked() {
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::Arrange(pdfcraft_engine::ArrangeOrder::BringToFront) });
close(ui);
}
if ui.button(tl!("Send to back")).clicked() {
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::Arrange(pdfcraft_engine::ArrangeOrder::SendToBack) });
close(ui);
}
if ui.button(tl!("Bring forward")).clicked() {
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::Arrange(pdfcraft_engine::ArrangeOrder::BringForward) });
close(ui);
}
if ui.button(tl!("Send backward")).clicked() {
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::Arrange(pdfcraft_engine::ArrangeOrder::SendBackward) });
close(ui);
use pdfcraft_engine::ArrangeOrder as A;
for (order, label) in [
(A::BringToFront, tl!("Bring to front")),
(A::SendToBack, tl!("Send to back")),
(A::BringForward, tl!("Bring forward")),
(A::SendBackward, tl!("Send backward")),
] {
if item(ui, true, label) {
view.pending_edit = edit(E::Arrange(order));
}
}
});
if ui.add_enabled(raster, egui::Button::new(tl!("Replace image"))).clicked() {
if item(ui, raster, tl!("Replace image")) {
*action = Some(ImageAction::Replace(page, hit));
close(ui);
}
if ui.add_enabled(raster, egui::Button::new(tl!("Save Image As…"))).clicked() {
if item(ui, raster, tl!("Save Image As…")) {
*action = Some(ImageAction::Save(page, hit));
close(ui);
}
ui.separator();
if ui.button(tl!("Draw freehand")).clicked() {
if item(ui, true, tl!("Draw freehand")) {
*action = Some(ImageAction::DrawFreehand);
close(ui);
}
if ui.button(tl!("Redact Image")).clicked() {
if item(ui, true, tl!("Redact Image")) {
*action = Some(ImageAction::Redact(page, hit));
close(ui);
}
if ui.add_enabled(raster, egui::Button::new(tl!("Export image"))).clicked() {
*action = Some(ImageAction::Save(page, hit));
close(ui);
}
ui.separator();
if ui.button(tl!("Create Link…")).clicked() {
if item(ui, true, tl!("Create Link…")) {
*action = Some(ImageAction::Link(page, hit));
close(ui);
}
ui.menu_button(tl!("Edit image using"), |ui| {
ui.add_enabled(false, egui::Button::new(tl!("Default image editor")));
});
if ui.button(tl!("Recognize Text…")).clicked() {
if item(ui, true, tl!("Recognize Text…")) {
*action = Some(ImageAction::Recognize(page));
close(ui);
}
ui.separator();
if ui.button(tl!("Delete")).clicked() {
if item(ui, true, tl!("Delete")) {
view.image_selection = None;
view.pending_edit = Some(Edit::EditPageImage { page, index: hit, change: E::Delete });
close(ui);
view.pending_edit = edit(E::Delete);
}
}
-2
View File
@@ -4616,9 +4616,7 @@ fn image_right_click_context_menu_items_and_actions() {
"Replace image",
"Draw freehand",
"Redact Image",
"Export image",
"Create Link",
"Edit image using",
"Recognize Text",
] {
h.get_by_label_contains(item);