Address review findings: context menu permissions, font submenu, parity notes, and lockfile

- Grey out restricted page context-menu actions (editing, annotation, assembly, printing) on protected PDFs.
- Group installed fonts by family with search, max height, and scroll area in the format panel. Prevent row cutoff in font dropdown.
- Document Unicode character conversion behavior for superscript and subscript in parity notes.
- Revert unintended clipboard-win bump in Cargo.lock back to 5.4.1.
This commit is contained in:
Combiler01
2026-10-25 17:04:56 +05:30
parent b8b07d0a99
commit 0ef72966e5
5 changed files with 157 additions and 46 deletions
Generated
+2 -2
View File
@@ -953,9 +953,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
version = "5.4.3"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
+42 -28
View File
@@ -1020,25 +1020,31 @@ pub(crate) enum PageMenuAction {
DocProperties,
}
pub(crate) fn default_page_context_menu(
ui: &mut egui::Ui,
page: usize,
at: [f64; 2],
can_copy: bool,
can_paste: bool,
action: &mut Option<PageMenuAction>,
) {
pub(crate) struct PageMenuPerms {
pub can_modify: bool,
pub can_assemble: bool,
pub can_annotate: bool,
pub can_print: bool,
pub can_copy: bool,
pub can_paste: bool,
}
pub(crate) fn default_page_context_menu(ui: &mut egui::Ui, page: usize, at: [f64; 2], perms: PageMenuPerms, action: &mut Option<PageMenuAction>) {
// 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 Counterclockwise")).clicked() {
*action = Some(PageMenuAction::RotateCCW(page));
ui.close();
}
if crate::icons::button(ui, "printer", 24.0, false, tl!("Print")).clicked() {
*action = Some(PageMenuAction::Print);
ui.close();
}
ui.add_enabled_ui(perms.can_assemble, |ui| {
if crate::icons::button(ui, "rotate-ccw", 24.0, false, tl!("Rotate 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();
@@ -1055,49 +1061,49 @@ pub(crate) fn default_page_context_menu(
ui.separator();
if ui.add_enabled(can_copy, egui::Button::new(tl!("Copy")).shortcut_text("Ctrl+C")).clicked() {
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(can_paste, egui::Button::new(tl!("Paste")).shortcut_text("Ctrl+V")).clicked() {
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();
}
ui.separator();
if ui.button(tl!("Add comment")).clicked() {
if ui.add_enabled(perms.can_annotate, egui::Button::new(tl!("Add comment"))).clicked() {
*action = Some(PageMenuAction::AddComment(page, at));
ui.close();
}
if ui.button(tl!("Add bookmark")).clicked() {
if ui.add_enabled(perms.can_assemble, egui::Button::new(tl!("Add bookmark"))).clicked() {
*action = Some(PageMenuAction::AddBookmark(page));
ui.close();
}
if ui.button(tl!("Take a snapshot")).clicked() {
if ui.add_enabled(perms.can_copy, egui::Button::new(tl!("Take a snapshot"))).clicked() {
*action = Some(PageMenuAction::TakeSnapshot);
ui.close();
}
ui.separator();
if ui.add(egui::Button::new(egui::RichText::new(tl!("Edit this PDF")).strong())).clicked() {
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.button(tl!("Add text")).clicked() {
if ui.add_enabled(perms.can_modify, egui::Button::new(tl!("Add text"))).clicked() {
*action = Some(PageMenuAction::AddText);
ui.close();
}
if ui.button(tl!("Add image")).clicked() {
if ui.add_enabled(perms.can_modify, egui::Button::new(tl!("Add image"))).clicked() {
*action = Some(PageMenuAction::AddImage);
ui.close();
}
if ui.button(tl!("Delete page")).clicked() {
if ui.add_enabled(perms.can_assemble, egui::Button::new(tl!("Delete page"))).clicked() {
*action = Some(PageMenuAction::DeletePage(page));
ui.close();
}
if ui.button(tl!("Export this PDF")).clicked() {
if ui.add_enabled(perms.can_copy, egui::Button::new(tl!("Export this PDF"))).clicked() {
*action = Some(PageMenuAction::ExportPdf);
ui.close();
}
@@ -4682,8 +4688,16 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
}
let (page, at) = view.comments.context_at.unwrap_or((view.current, [0.0, 0.0]));
let can_copy_items = can_copy && (view.image_selection.is_some() || view.selected_text().is_some() || has_copy_image);
let can_paste = can_paste_image;
default_page_context_menu(ui, page, at, can_copy_items, can_paste, &mut page_action);
let can_paste = can_paste_image && can_modify;
let perms = PageMenuPerms {
can_modify,
can_assemble: doc.allows_assembly(),
can_annotate: allowed,
can_print: doc.allows_printing(),
can_copy: can_copy_items,
can_paste,
};
default_page_context_menu(ui, page, at, perms, &mut page_action);
});
if !resp.context_menu_opened() {
ui.ctx().data_mut(|d| d.insert_persisted(crate::edit_text_ui::image_menu_id(), false));
+65 -12
View File
@@ -77,6 +77,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, look: &mut AddedText, extras:
widgets::section_title(ui, tl!("Format text"));
// The font, across the panel.
ui.spacing_mut().combo_height = 360.0;
let width = ui.available_width();
let font_resp = egui::ComboBox::from_id_salt("format-font")
.selected_text(fonts.shown.as_str())
@@ -104,22 +105,74 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, look: &mut AddedText, extras:
out.font = Some(FontChoice::Load);
}
#[cfg(not(target_arch = "wasm32"))]
ui.menu_button(tl!("Installed fonts…"), |ui| match crate::files::installed_font_files(ui.ctx()) {
Some(list) if list.is_empty() => {
ui.label(tl!("No openly licensed document fonts found. Load a font file."));
}
Some(list) => {
for font in list {
if ui.button(&font.face).clicked() {
out.font = Some(FontChoice::Installed(font.path));
ui.close();
ui.menu_button(tl!("Installed fonts…"), |ui| {
ui.set_min_width(260.0);
match crate::files::installed_font_files(ui.ctx()) {
Some(list) if list.is_empty() => {
ui.label(tl!("No openly licensed document fonts found. Load a font file."));
}
Some(list) => {
let search_id = ui.id().with("installed-font-search");
let mut query = ui.data(|d| d.get_temp::<String>(search_id)).unwrap_or_default();
ui.horizontal(|ui| {
ui.label(egui::RichText::new("🔍").size(12.0));
let resp = ui
.add(egui::TextEdit::singleline(&mut query).hint_text(tl!("Search…")).desired_width(ui.available_width() - 8.0));
if resp.changed() {
ui.data_mut(|d| d.insert_temp(search_id, query.clone()));
}
});
ui.separator();
let query_lower = query.trim().to_lowercase();
let mut families: std::collections::BTreeMap<&str, Vec<&pdfcraft_engine::text_fonts::FontFile>> =
std::collections::BTreeMap::new();
for font in &list {
if query_lower.is_empty()
|| font.family.to_lowercase().contains(&query_lower)
|| font.face.to_lowercase().contains(&query_lower)
{
families.entry(&font.family).or_default().push(font);
}
}
if families.is_empty() {
ui.label(tl!("No openly licensed document fonts found. Load a font file."));
} else {
egui::ScrollArea::vertical().max_height(350.0).show(ui, |ui| {
for (family, faces) in families {
if faces.len() == 1 {
if let Some(&font) = faces.first() {
let label =
if font.face != font.family && !font.face.is_empty() { font.face.as_str() } else { family };
if ui.button(label).clicked() {
out.font = Some(FontChoice::Installed(font.path.clone()));
ui.close();
}
}
} else {
ui.menu_button(family, |ui| {
ui.set_min_width(180.0);
for font in faces {
let label =
font.face.strip_prefix(family).map(str::trim).filter(|s| !s.is_empty()).unwrap_or(&font.face);
if ui.button(label).clicked() {
out.font = Some(FontChoice::Installed(font.path.clone()));
ui.close();
}
}
});
}
}
});
}
}
}
None => {
ui.label(tl!("Finding document fonts…"));
None => {
ui.label(tl!("Finding document fonts…"));
}
}
});
ui.add_space(4.0);
}
})
.response;
+44
View File
@@ -3711,6 +3711,50 @@ fn default_page_context_menu_when_not_in_edit_mode() {
assert_eq!(h.state().quick_tool, pdfcraft_ui_egui::QuickTool::EditText);
}
#[test]
fn page_context_menu_items_disabled_on_protected_pdf() {
let bytes = protected("", "owner", 0b0100);
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("protected.pdf", None, bytes.clone()).expect("opens");
app
});
h.run_steps(4);
assert_eq!(h.state().quick_tool, pdfcraft_ui_egui::QuickTool::Select);
let doc_id = h.state().views[0].id;
let doc = h.state().session.get(doc_id).unwrap();
assert!(!doc.allows_modification());
assert!(!doc.allows_assembly());
assert!(!doc.allows_annotation());
let r = h.state().views[0].page_screen_rect(0).expect("on screen");
let c = r.center();
// Right click on page in Select mode
h.hover_at(c);
h.run_steps(1);
h.event(egui::Event::PointerButton { pos: c, button: egui::PointerButton::Secondary, pressed: true, modifiers: Default::default() });
h.event(egui::Event::PointerButton { pos: c, button: egui::PointerButton::Secondary, pressed: false, modifiers: Default::default() });
h.run_steps(2);
let is_disabled =
|h: &mut Harness<'static, PdfCraftApp>, label: &str| h.query_by(|n| n.label().as_deref() == Some(label) && n.is_disabled()).is_some();
// Context menu is open; check restricted items are greyed out (disabled)
assert!(is_disabled(&mut h, "Edit this PDF"), "Edit this PDF should be disabled on protected document");
assert!(is_disabled(&mut h, "Add text"), "Add text should be disabled");
assert!(is_disabled(&mut h, "Add image"), "Add image should be disabled");
assert!(is_disabled(&mut h, "Delete page"), "Delete page should be disabled");
assert!(is_disabled(&mut h, "Add comment"), "Add comment should be disabled");
assert!(is_disabled(&mut h, "Add bookmark"), "Add bookmark should be disabled");
// View-only items should still be enabled
assert!(!is_disabled(&mut h, "Select Tool"));
assert!(!is_disabled(&mut h, "Hand Tool"));
assert!(!is_disabled(&mut h, "Find text or tools"));
}
#[test]
fn image_edge_handle_stretch_and_crop_mode() {
let mut png = Vec::new();
+4 -4
View File
@@ -1947,11 +1947,11 @@ title = "Superscript and subscript"
area = "C"
tier = "P2"
milestone = "M7"
status = "planned"
commands = []
status = "partial"
commands = ["edit.edit_text"]
tools = []
evidence = []
notes = ""
evidence = ["crates/ui-egui/src/edit_text_ui.rs::superscript_and_subscript_conversion"]
notes = "Toggled in the Format text panel for the selection. Uses Unicode superscript and subscript forms (2 ↔ ²) rather than text rise (Ts) and font scaling, so the font must provide the corresponding glyphs and extracted text contains the Unicode characters."
[[feature]]
id = "edit.paragraph-alignment"