diff --git a/Cargo.lock b/Cargo.lock index 1bdf8cc713..715137d1e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index acafb9d987..42eea3b644 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -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, -) { +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) { // 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)); diff --git a/crates/ui-egui/src/format_text_ui.rs b/crates/ui-egui/src/format_text_ui.rs index 2c8347cd7d..be2a55f9d5 100644 --- a/crates/ui-egui/src/format_text_ui.rs +++ b/crates/ui-egui/src/format_text_ui.rs @@ -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::(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; diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index ec378add37..5ab98604f9 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -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(); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 04cad5839a..6f3d25cc3a 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -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"