diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index f03629e41..5be6d88d7 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -448,6 +448,9 @@ impl OpenCADStudio { "PLOT" | "PRINT" => { return Some(Task::done(Message::PlotDialogOpen)); } + "PRINTALL" => { + return Some(Task::done(Message::PrintAllOpen)); + } "EXPORT" | "EXPORTPDF" => { return Some(Task::done(Message::PlotExport)); } diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index aadc94dcc..377ae4057 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -282,6 +282,7 @@ inventory::submit!(crate::command::CommandRegistration { // Standard aliases for existing commands. "BMAKE", "EXPORTPDF", + "PRINTALL", "DDIM", // Inquiry: list the whole drawing database. "DBLIST", diff --git a/src/app/mod.rs b/src/app/mod.rs index 0e9d2d550..32c88ecaa 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -669,6 +669,16 @@ pub(super) struct OpenCADStudio { /// Snapshot of the dialog's settings taken when it opened, restored by the /// `` list entry. plot_prev: Option, + /// Paper layouts shown by Print All, in tab order with their selection. + print_all_layouts: Vec<(String, bool)>, + /// True while the Plot dialog is editing settings for Print All. + print_all_options: bool, + /// Settings restored when the Print All options dialog is cancelled. + print_all_options_prev: Option, + /// Plot style restored together with cancelled Print All options. + print_all_plot_style_prev: Option>, + /// Plot window restored together with cancelled Print All options. + print_all_plot_window_prev: Option>, // ── Plot Style Table ────────────────────────────────────────────────── /// Currently loaded CTB/STB table (None = no override). @@ -1409,6 +1419,7 @@ pub enum ModalKind { LayerStateManager, LayerStateEditor, Plot, + PrintAll, LayoutManager, Plotstyle, TextStyle, @@ -2455,6 +2466,23 @@ pub enum Message { PlotDialogOpen, /// An edit inside the Plot / Print dialog. PlotDlg(crate::ui::window::plot::PlotDlgMsg), + /// Open the paper-layout batch output dialog. + PrintAllOpen, + /// Toggle one paper layout in the batch. + PrintAllToggle(String), + /// Select or clear every paper layout in the batch. + PrintAllSelectAll, + PrintAllSelectNone, + /// Edit the shared batch output settings in the Plot dialog. + PrintAllOptions, + /// Save the selected layouts as one multi-page PDF. + PrintAllPdf, + /// Callback after the multi-page PDF path is picked or cancelled. + PrintAllPdfPath(Option), + /// Send the selected layouts to the configured printer as one job. + PrintAllPrint, + /// Completion of a Print All PDF or printer job. + PrintAllFinished(Result), // ── Plot Style Table ───────────────────────────────────────────────── /// Open file dialog to load a CTB/STB plot style table. PlotStyleLoad, @@ -2896,6 +2924,11 @@ impl OpenCADStudio { plot_orientation: crate::io::paper_sizes::Orientation::Landscape, plot_dialog: crate::ui::window::plot::PlotDialogState::default(), plot_prev: None, + print_all_layouts: Vec::new(), + print_all_options: false, + print_all_options_prev: None, + print_all_plot_style_prev: None, + print_all_plot_window_prev: None, opening: None, open_job_serial: 0, recovery_report: None, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index d772d7336..4e1d8b810 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -2408,6 +2408,192 @@ pub(super) fn on_open_file(&mut self) -> Task { } } + pub(super) fn on_print_all_open(&mut self) -> Task { + self.print_all_layouts = self.tabs[self.active_tab] + .scene + .layout_names() + .into_iter() + .filter(|name| name != "Model") + .map(|name| (name, true)) + .collect(); + self.active_modal = Some(crate::app::ModalKind::PrintAll); + self.reset_modal_geometry(); + Task::none() + } + + pub(super) fn on_print_all_options(&mut self) -> Task { + let previous = self.plot_dialog.clone(); + let previous_style = self.active_plot_style.clone(); + let previous_window = self.plot_window; + let task = self.on_plot_dialog_open(); + self.print_all_options_prev = Some(previous); + self.print_all_plot_style_prev = Some(previous_style); + self.print_all_plot_window_prev = Some(previous_window); + self.print_all_options = true; + self.plot_dialog.paper_space = true; + self.plot_dialog.area = "Layout".into(); + task + } + + fn print_all_pages(&mut self) -> Result, String> { + let available = self.tabs[self.active_tab].scene.layout_names(); + let selected: Vec = self + .print_all_layouts + .iter() + .filter(|(name, checked)| *checked && available.contains(name)) + .map(|(name, _)| name.clone()) + .collect(); + if selected.is_empty() { + return Err(crate::t!("Select at least one layout.").into_owned()); + } + + let i = self.active_tab; + let original_layout = self.tabs[i].scene.current_layout.clone(); + let original_viewport = self.tabs[i].scene.active_viewport; + let dialog = self.plot_dialog.clone(); + let mut pages = Vec::with_capacity(selected.len()); + for name in selected { + // Plot helpers read the active layout. Swap only this transient + // selector so the drawing's saved active-space metadata is not + // touched while the owned page snapshot is collected. + { + let scene = &mut self.tabs[i].scene; + scene.current_layout = name; + scene.active_viewport = None; + } + let ( + wires, + hatches, + wipeouts, + group_splits, + paper_w, + paper_h, + offset_x, + offset_y, + rotation_deg, + scale, + clip, + ) = self.layout_plot_params_for("Layout"); + pages.push(crate::io::pdf_export::PdfPageInput { + wires, + hatches, + wipeouts, + paper_w, + paper_h, + offset_x, + offset_y, + rotation_deg, + scale, + clip, + options: Self::pdf_plot_options(&dialog, group_splits), + }); + } + self.tabs[i].scene.current_layout = original_layout; + self.tabs[i].scene.active_viewport = original_viewport; + Ok(pages) + } + + pub(super) fn on_print_all_pdf_path_some( + &mut self, + path: std::path::PathBuf, + ) -> Task { + let dialog = self.plot_dialog.clone(); + if dialog.style_missing { + self.command_line.push_error(crate::tf!( + "Plot style table '{}' is not loaded.", + dialog.style_name + ).as_ref()); + return Task::none(); + } + let pages = match self.print_all_pages() { + Ok(pages) => pages, + Err(error) => { + self.command_line.push_error(&error); + return Task::none(); + } + }; + let plot_style = self.dialog_plot_style(&dialog); + let worker_path = path.clone(); + self.save_config(); + self.close_active_modal(); + let work = move || { + crate::io::pdf_export::export_pdf_pages( + &pages, + &worker_path, + plot_style.as_ref(), + ) + .map(|_| format!("Exported {} layouts to {}", pages.len(), worker_path.display())) + .map_err(|error| format!("Export failed: {error}")) + }; + self.run_print_all_work(dialog.background, work) + } + + pub(super) fn on_print_all_print(&mut self) -> Task { + #[cfg(target_arch = "wasm32")] + { + self.command_line.push_error( + crate::t!("Printing is not available in the web version.").as_ref(), + ); + Task::none() + } + #[cfg(not(target_arch = "wasm32"))] + { + let dialog = self.plot_dialog.clone(); + if dialog.style_missing { + self.command_line.push_error(crate::tf!( + "Plot style table '{}' is not loaded.", + dialog.style_name + ).as_ref()); + return Task::none(); + } + let pages = match self.print_all_pages() { + Ok(pages) => pages, + Err(error) => { + self.command_line.push_error(&error); + return Task::none(); + } + }; + let plot_style = self.dialog_plot_style(&dialog); + let options = self.plot_print_options(&dialog, Default::default()); + let stamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + let temp_path = std::env::temp_dir().join(format!( + "open_cad_studio_print_all_{stamp}.pdf" + )); + self.save_config(); + self.close_active_modal(); + self.command_line.push_info( + crate::t!("Sending selected layouts to the system printer…").as_ref(), + ); + let work = move || { + crate::io::pdf_export::export_pdf_pages( + &pages, + &temp_path, + plot_style.as_ref(), + ) + .and_then(|_| { + crate::io::print_to_printer::print_existing_pdf(&temp_path, &options) + }) + .map(|printer| format!("Sent {} layouts to printer: {printer}", pages.len())) + .map_err(|error| format!("Print failed: {error}")) + }; + self.run_print_all_work(dialog.background, work) + } + } + + fn run_print_all_work(&mut self, background: bool, work: F) -> Task + where + F: FnOnce() -> Result + Send + 'static, + { + if background { + background_task(work, Message::PrintAllFinished) + } else { + Task::done(Message::PrintAllFinished(work())) + } + } + fn run_plot_work( &mut self, background: bool, @@ -2940,6 +3126,9 @@ pub(super) fn on_open_file(&mut self) -> Task { Task::none() } M::Area(s) => { + if self.print_all_options && s != "Layout" { + return Task::none(); + } // A paper Layout page setup commonly carries physical-sheet // origin and 1:1 scale. Those values are correct only for // Layout; carrying them into Window/Extents/Display moves a @@ -3040,6 +3229,9 @@ pub(super) fn on_open_file(&mut self) -> Task { Task::none() } M::PickWindow => { + if self.print_all_options { + return Task::none(); + } let i = self.active_tab; if self.tabs[i].scene.current_layout != "Model" && self.tabs[i].scene.active_viewport.is_some() @@ -3060,14 +3252,24 @@ pub(super) fn on_open_file(&mut self) -> Task { } M::SelectSetup(name) => { self.select_page_setup(&name); + if self.print_all_options { + self.plot_dialog.paper_space = true; + self.plot_dialog.area = "Layout".into(); + } Task::none() } M::SetCurrent => { + if self.print_all_options { + return Task::none(); + } self.apply_dialog_to_layout(); self.command_line.push_info(crate::t!("Page setup applied to the layout.").as_ref()); Task::none() } M::NewSetup => { + if self.print_all_options { + return Task::none(); + } // Create a setup from the current editor values, then start an // inline rename so the user can name it. let name = self.next_page_setup_name("Setup"); @@ -3081,6 +3283,9 @@ pub(super) fn on_open_file(&mut self) -> Task { Task::none() } M::CopySetup => { + if self.print_all_options { + return Task::none(); + } // Duplicate the selected entry — a layout OR a named setup — // into a new standalone named page setup. let sel = self.plot_dialog.selected_setup.clone(); @@ -3103,6 +3308,9 @@ pub(super) fn on_open_file(&mut self) -> Task { Task::none() } M::RenameStart(name) => { + if self.print_all_options { + return Task::none(); + } // Only standalone named setups can be renamed. if is_layout_entry(&name) || is_special_entry(&name) { return Task::none(); @@ -3116,6 +3324,9 @@ pub(super) fn on_open_file(&mut self) -> Task { Task::none() } M::DeleteSetup => { + if self.print_all_options { + return Task::none(); + } let sel = self.plot_dialog.selected_setup.clone(); if !sel.is_empty() && !is_layout_entry(&sel) { self.tabs[self.active_tab].scene.page_setup_delete(&sel); @@ -3153,7 +3364,30 @@ pub(super) fn on_open_file(&mut self) -> Task { self.plot_dialog.name_rename = false; Task::none() } + M::Preview if self.print_all_options => Task::none(), M::Preview => self.on_plot_dlg_commit(true), + M::Commit if self.print_all_options => { + if self.plot_dialog.style_missing { + self.command_line.push_error(crate::tf!( + "Plot style table '{}' is not loaded.", + self.plot_dialog.style_name + ).as_ref()); + return Task::none(); + } + self.plot_dialog.paper_space = true; + self.plot_dialog.area = "Layout".into(); + self.sync_dialog_plot_runtime(); + self.save_config(); + self.print_all_options = false; + self.print_all_options_prev = None; + self.print_all_plot_style_prev = None; + if let Some(previous) = self.print_all_plot_window_prev.take() { + self.plot_window = previous; + } + self.active_modal = Some(crate::app::ModalKind::PrintAll); + self.reset_modal_geometry(); + Task::none() + } M::Commit => self.on_plot_dlg_commit(false), } } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 71a698297..5e0832102 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -121,6 +121,21 @@ impl OpenCADStudio { /// changes, and the ribbon tool that launched the dialog is de-highlighted. fn close_active_modal(&mut self) { use super::ModalKind::*; + if self.active_modal == Some(Plot) && self.print_all_options { + if let Some(previous) = self.print_all_options_prev.take() { + self.plot_dialog = previous; + } + if let Some(previous) = self.print_all_plot_style_prev.take() { + self.active_plot_style = previous; + } + if let Some(previous) = self.print_all_plot_window_prev.take() { + self.plot_window = previous; + } + self.print_all_options = false; + self.active_modal = Some(PrintAll); + self.reset_modal_geometry(); + return; + } if matches!( self.active_modal, Some(TextStyle | DimStyle | TableStyle | MLeaderStyle | MlStyle) @@ -5461,6 +5476,73 @@ impl OpenCADStudio { } Message::PlotDialogOpen => self.on_plot_dialog_open(), Message::PlotDlg(m) => self.on_plot_dlg(m), + Message::PrintAllOpen => self.on_print_all_open(), + Message::PrintAllToggle(name) => { + if let Some((_, selected)) = self + .print_all_layouts + .iter_mut() + .find(|(layout, _)| layout == &name) + { + *selected = !*selected; + } + Task::none() + } + Message::PrintAllSelectAll => { + for (_, selected) in &mut self.print_all_layouts { + *selected = true; + } + Task::none() + } + Message::PrintAllSelectNone => { + for (_, selected) in &mut self.print_all_layouts { + *selected = false; + } + Task::none() + } + Message::PrintAllOptions => self.on_print_all_options(), + Message::PrintAllPdf => { + let i = self.active_tab; + let stem = self.tabs[i] + .current_path + .as_deref() + .and_then(|path| path.file_stem()) + .map(|name| format!("{}_layouts", name.to_string_lossy())) + .unwrap_or_else(|| "drawing_layouts".into()); + #[cfg(not(target_arch = "wasm32"))] + { + let Some(window_id) = self.main_window else { + return Task::done(Message::PrintAllPdfPath(None)); + }; + iced::window::run(window_id, move |parent| { + crate::io::pdf_export::pick_pdf_path_owned(stem, parent) + }) + .map(Message::PrintAllPdfPath) + } + #[cfg(target_arch = "wasm32")] + { + let _ = stem; + self.command_line.push_error( + crate::t!("PDF export is not available in the web version.").as_ref(), + ); + Task::none() + } + } + Message::PrintAllPdfPath(None) => Task::none(), + Message::PrintAllPdfPath(Some(path)) => self.on_print_all_pdf_path_some(path), + Message::PrintAllPrint => self.on_print_all_print(), + Message::PrintAllFinished(result) => { + match result { + Ok(message) => self.command_line.push_info(&message), + Err(error) => { + self.command_line.push_error(&error); + if self.active_modal.is_none() { + self.active_modal = Some(super::ModalKind::PrintAll); + self.reset_modal_geometry(); + } + } + } + Task::none() + } // ── Plot / Export ───────────────────────────────────────────── Message::PlotExport => { diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 44cf27536..15565caca 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -21,6 +21,7 @@ impl OpenCADStudio { Some(K::LayerStateManager) => crate::tr!("modal-layer-state-manager"), Some(K::LayerStateEditor) => crate::tr!("modal-edit-layer-state"), Some(K::Plot) => crate::tr!("modal-plot"), + Some(K::PrintAll) => t!("Print All").into_owned(), Some(K::LayoutManager) => crate::tr!("modal-layout-manager"), Some(K::ScaleManager) => crate::tr!("modal-scale-manager"), Some(K::AnnoObjectScale) => crate::tr!("modal-annotation-object-scale"), @@ -238,9 +239,27 @@ impl OpenCADStudio { ex, 760, 540, - |flow| crate::ui::window::plot::view_window(&self.plot_dialog, flow), + |flow| { + crate::ui::window::plot::view_window( + &self.plot_dialog, + self.print_all_options, + flow, + ) + }, ) } + super::super::ModalKind::PrintAll => sized_flow( + ex, + 520, + 420, + |flow| { + crate::ui::window::print_all::view_window( + &self.print_all_layouts, + self.plot_dialog.printer.as_deref(), + flow, + ) + }, + ), super::super::ModalKind::LayoutManager => { let i = self.active_tab; let layouts = self.tabs[i].scene.layout_names(); diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 780f64939..d7923f49e 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -46,6 +46,15 @@ pub fn export_pdf( Err("PDF export is not available in the web version.".into()) } +#[cfg(target_arch = "wasm32")] +pub fn export_pdf_pages( + _pages: &[PdfPageInput], + _path: &Path, + _plot_style: Option<&PlotStyleTable>, +) -> Result<(), String> { + Err("PDF export is not available in the web version.".into()) +} + #[cfg(target_arch = "wasm32")] pub async fn pick_pdf_path_owned(_stem: String) -> Option { None @@ -83,6 +92,22 @@ pub struct PlotGroupSplits { pub wipeouts: usize, } +/// Owned render data for one page in a multi-page PDF. +#[cfg_attr(target_arch = "wasm32", allow(dead_code))] +pub struct PdfPageInput { + pub wires: std::sync::Arc>, + pub hatches: Vec, + pub wipeouts: Vec, + pub paper_w: f64, + pub paper_h: f64, + pub offset_x: f64, + pub offset_y: f64, + pub rotation_deg: i32, + pub scale: f32, + pub clip: Option<(f32, f32, f32, f32)>, + pub options: PdfPlotOptions, +} + impl Default for PdfPlotOptions { fn default() -> Self { Self { @@ -138,6 +163,21 @@ pub fn export_pdf( file.write_all(&bytes).map_err(|e| e.to_string()) } +/// Export several independently sized pages into one PDF file. +#[cfg(not(target_arch = "wasm32"))] +pub fn export_pdf_pages( + pages: &[PdfPageInput], + path: &Path, + plot_style: Option<&PlotStyleTable>, +) -> Result<(), String> { + if pages.is_empty() { + return Err("No pages were selected.".into()); + } + let bytes = build_pdf_pages(pages, plot_style); + let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?; + file.write_all(&bytes).map_err(|e| e.to_string()) +} + /// Show a parented PDF save-file dialog and return the chosen path. /// /// The parent comes from `iced::window::run`, keeping the portal request tied @@ -181,6 +221,65 @@ fn build_pdf( options: PdfPlotOptions, ) -> Vec { let mut doc = PdfDocument::new("Open CAD Studio Export"); + append_pdf_page( + &mut doc, + wires, + hatches, + wipeouts, + paper_w, + paper_h, + ox, + oy, + rotation_deg, + scale, + clip, + plot_style, + options, + ); + let mut warnings = Vec::new(); + doc.save(&PdfSaveOptions::default(), &mut warnings) +} + +#[cfg(not(target_arch = "wasm32"))] +fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>) -> Vec { + let mut doc = PdfDocument::new("Open CAD Studio Export"); + for page in pages { + append_pdf_page( + &mut doc, + &page.wires, + &page.hatches, + &page.wipeouts, + page.paper_w as f32, + page.paper_h as f32, + page.offset_x, + page.offset_y, + page.rotation_deg, + page.scale, + page.clip, + plot_style, + page.options, + ); + } + let mut warnings = Vec::new(); + doc.save(&PdfSaveOptions::default(), &mut warnings) +} + +#[cfg(not(target_arch = "wasm32"))] +fn append_pdf_page( + doc: &mut PdfDocument, + wires: &[WireModel], + hatches: &[HatchModel], + wipeouts: &[HatchModel], + paper_w: f32, + paper_h: f32, + ox: f64, + oy: f64, + rotation_deg: i32, + scale: f32, + clip: Option<(f32, f32, f32, f32)>, + plot_style: Option<&PlotStyleTable>, + options: PdfPlotOptions, +) { let mut ops: Vec = Vec::new(); // White page background. @@ -474,9 +573,6 @@ fn build_pdf( let page = PdfPage::new(Mm(paper_w), Mm(paper_h), ops); doc.pages.push(page); - - let mut warnings = Vec::new(); - doc.save(&PdfSaveOptions::default(), &mut warnings) } /// Build a PDF dash array (in points) from a WireModel linetype pattern. diff --git a/src/modules/layout/mod.rs b/src/modules/layout/mod.rs index 35c1cbff2..699fdc5b4 100644 --- a/src/modules/layout/mod.rs +++ b/src/modules/layout/mod.rs @@ -18,10 +18,10 @@ pub fn paper_space_tools() -> Vec { event: ModuleEvent::Command("PAGESETUP".to_string()), }, ToolDef { - id: "PLOT", - label: "Export PDF", + id: "PRINTALL", + label: "Print All", icon: IconKind::Svg(include_bytes!("../../../assets/icons/plot.svg")), - event: ModuleEvent::Command("PLOT".to_string()), + event: ModuleEvent::Command("PRINTALL".to_string()), }, ] } @@ -57,10 +57,10 @@ impl CadModule for LayoutModule { } .into(), ToolDef { - id: "PLOT", - label: "Export PDF", + id: "PRINTALL", + label: "Print All", icon: IconKind::Svg(include_bytes!("../../../assets/icons/plot.svg")), - event: ModuleEvent::Command("PLOT".to_string()), + event: ModuleEvent::Command("PRINTALL".to_string()), } .into(), ], diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index 0355456ff..0877e26de 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -2,6 +2,7 @@ pub mod about; pub mod layout_manager; pub mod layer_state_manager; pub mod plot; +pub mod print_all; pub mod plugin_manager; pub mod shortcuts; pub mod layers; diff --git a/src/ui/window/plot.rs b/src/ui/window/plot.rs index 044cee5bf..e4ac7e993 100644 --- a/src/ui/window/plot.rs +++ b/src/ui/window/plot.rs @@ -490,11 +490,18 @@ fn choices(items: &[&str]) -> Vec { pub fn view_window( s: &PlotDialogState, + print_all_options: bool, sizing: crate::ui::modal::ModalSizing, ) -> Element<'_, Message> { let width = sizing.width; let height = sizing.height; - let action = if s.to_file { t!("Export PDF") } else { t!("Print") }; + let action = if print_all_options { + t!("Apply") + } else if s.to_file { + t!("Export PDF") + } else { + t!("Print") + }; let is_special = s.selected_setup == SETUP_NONE || s.selected_setup == SETUP_PREV; let sel_is_layout = s.selected_setup.len() >= 2 && s.selected_setup.starts_with('*') @@ -578,23 +585,26 @@ pub fn view_window( left: 12.0, }); + let mut new_button = button(text(t!("New")).size(11)) + .style(btn(false)) + .padding([4, 12]); + if !print_all_options { + new_button = new_button.on_press(Message::PlotDlg(PlotDlgMsg::NewSetup)); + } let mut copy_button = button(text(t!("Copy")).size(11)) .style(btn(false)) .padding([4, 12]); - if can_copy { + if can_copy && !print_all_options { copy_button = copy_button.on_press(Message::PlotDlg(PlotDlgMsg::CopySetup)); } let mut delete_button = button(text(t!("Delete")).size(11)) .style(btn(false)) .padding([4, 12]); - if is_named { + if is_named && !print_all_options { delete_button = delete_button.on_press(Message::PlotDlg(PlotDlgMsg::DeleteSetup)); } let left_bar = row![ - button(text(t!("New")).size(11)) - .on_press(Message::PlotDlg(PlotDlgMsg::NewSetup)) - .style(btn(false)) - .padding([4, 12]), + new_button, copy_button, delete_button, ] @@ -672,8 +682,12 @@ pub fn view_window( paper_note, ].spacing(7)); - let mut area_options = choices(&["Extents", "Display", "Window"]); - if s.paper_space { + let mut area_options = if print_all_options { + choices(&["Layout"]) + } else { + choices(&["Extents", "Display", "Window"]) + }; + if s.paper_space && !print_all_options { area_options.insert(0, PlotChoice::localized("Layout")); } let mut area_row = row![ @@ -690,7 +704,7 @@ pub fn view_window( ] .spacing(8) .align_y(iced::Center); - if s.area == "Window" { + if s.area == "Window" && !print_all_options { area_row = area_row.push( button(text(t!("Pick…")).size(11)) .on_press(Message::PlotDlg(PlotDlgMsg::PickWindow)) @@ -877,27 +891,31 @@ pub fn view_window( let body = row![list_panel, vsep(height), detail] .width(width) .height(height); - let toolbar = container( - row![ - left_bar, - Space::new().width(width), - button(text(t!("Set current")).size(11)) - .on_press(Message::PlotDlg(PlotDlgMsg::SetCurrent)) - .style(btn(false)) - .padding([4, 12]), - Space::new().width(6), - button(text(t!("Preview")).size(11)) - .on_press(Message::PlotDlg(PlotDlgMsg::Preview)) - .style(btn(false)) - .padding([4, 12]), - Space::new().width(6), - button(text(action).size(11)) - .on_press(Message::PlotDlg(PlotDlgMsg::Commit)) - .style(btn(true)) - .padding([4, 18]), - ] - .align_y(iced::Center), - ) + let mut toolbar_row = row![left_bar, Space::new().width(width)].align_y(iced::Center); + if !print_all_options { + toolbar_row = toolbar_row + .push( + button(text(t!("Set current")).size(11)) + .on_press(Message::PlotDlg(PlotDlgMsg::SetCurrent)) + .style(btn(false)) + .padding([4, 12]), + ) + .push(Space::new().width(6)) + .push( + button(text(t!("Preview")).size(11)) + .on_press(Message::PlotDlg(PlotDlgMsg::Preview)) + .style(btn(false)) + .padding([4, 12]), + ) + .push(Space::new().width(6)); + } + toolbar_row = toolbar_row.push( + button(text(action).size(11)) + .on_press(Message::PlotDlg(PlotDlgMsg::Commit)) + .style(btn(true)) + .padding([4, 18]), + ); + let toolbar = container(toolbar_row) .style(|theme: &Theme| container::Style { background: Some(Background::Color(theme.palette().background.weak.color)), ..Default::default() diff --git a/src/ui/window/print_all.rs b/src/ui/window/print_all.rs new file mode 100644 index 000000000..05b5f5265 --- /dev/null +++ b/src/ui/window/print_all.rs @@ -0,0 +1,127 @@ +use crate::app::Message; +use crate::t; +use iced::widget::{button, checkbox, column, container, row, scrollable, text, Space}; +use iced::{Background, Border, Element, Length, Theme}; + +fn button_style(primary: bool) -> impl Fn(&Theme, button::Status) -> button::Style { + move |theme, status| { + if primary { + button::primary(theme, status) + } else { + button::secondary(theme, status) + } + } +} + +fn muted(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.palette().background.base.text.scale_alpha(0.68)), + } +} + +pub fn view_window<'a>( + layouts: &'a [(String, bool)], + printer: Option<&'a str>, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let selected = layouts.iter().filter(|(_, checked)| *checked).count(); + let rows: Vec> = layouts + .iter() + .map(|(name, checked)| { + let layout = name.clone(); + container( + checkbox(*checked) + .label(name.clone()) + .on_toggle(move |_| Message::PrintAllToggle(layout.clone())) + .size(16) + .text_size(12), + ) + .padding([6, 8]) + .width(Length::Fill) + .into() + }) + .collect(); + let list: Element<'a, Message> = if rows.is_empty() { + container(text(t!("No paper layouts are available.")).size(12).style(muted)) + .center_x(Length::Fill) + .center_y(Length::Fill) + .into() + } else { + scrollable(column(rows).spacing(2)).height(sizing.height).into() + }; + let printer = printer + .filter(|name| !name.is_empty()) + .map(str::to_owned) + .unwrap_or_else(|| t!("Default printer").into_owned()); + let summary = if selected == 1 { + t!("1 layout selected").into_owned() + } else { + format!("{selected} {}", t!("layouts selected")) + }; + + let mut pdf = button(text(t!("PDF")).size(11)) + .style(button_style(false)) + .padding([5, 16]); + let mut print = button(text(t!("Print")).size(11)) + .style(button_style(true)) + .padding([5, 18]); + if selected > 0 { + pdf = pdf.on_press(Message::PrintAllPdf); + print = print.on_press(Message::PrintAllPrint); + } + + let list_panel = container(list) + .width(sizing.width) + .height(sizing.height) + .padding(4) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + border: Border { + color: theme.palette().background.neutral.color, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + }); + + container( + column![ + text(t!("Select the layouts to output. Each page is printed in Layout mode.")) + .size(12), + row![ + button(text(t!("Select all")).size(11)) + .on_press(Message::PrintAllSelectAll) + .style(button_style(false)) + .padding([4, 10]), + button(text(t!("Select none")).size(11)) + .on_press(Message::PrintAllSelectNone) + .style(button_style(false)) + .padding([4, 10]), + Space::new().width(Length::Fill), + text(summary).size(11).style(muted), + ] + .spacing(6) + .align_y(iced::Center), + list_panel, + row![ + text(format!("{}: {printer}", t!("Printer"))) + .size(11) + .style(muted), + Space::new().width(Length::Fill), + button(text(t!("Options…")).size(11)) + .on_press(Message::PrintAllOptions) + .style(button_style(false)) + .padding([5, 12]), + pdf, + print, + ] + .spacing(7) + .align_y(iced::Center), + ] + .spacing(10), + ) + .padding(14) + .width(sizing.width) + .height(sizing.height) + .into() +}