feat(plot): page setup manager — named setups, layouts, <none>/<previous>
Restructure the plot dialog into a style-manager-style master/detail: a left list of page setups plus a right settings editor. The list shows <none> (defaults + PDF) and <previous> (the settings the dialog opened with), then the drawing's layouts (*Model*, *Layout1*, …) and its standalone named page setups. - Named page setups live in the ACAD_PLOTSETTINGS dictionary (src/scene/page_setup.rs: names/get/save/delete/rename, creating the dictionary if absent); DWG round-trip verified. - Toolbar New / Copy / Delete | Set current / Preview / Print. Selecting a row loads it into the editor; Set current writes to the active layout. Copy duplicates a layout or a setup into a new named setup; Delete/Rename apply only to named setups. - Scene::plot_settings_for(name) generalizes effective_plot_settings so any layout's settings can be loaded; on open the settings are snapshotted for <previous>. - Default selection: model space → <none>; a layout → its own setup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d37aacd895
commit
a1ee4d9269
8 changed files with 649 additions and 25 deletions
|
|
@ -521,6 +521,9 @@ pub(super) struct OpenCADStudio {
|
||||||
plot_orientation: crate::io::paper_sizes::Orientation,
|
plot_orientation: crate::io::paper_sizes::Orientation,
|
||||||
/// Backing state for the full Plot / Print dialog.
|
/// Backing state for the full Plot / Print dialog.
|
||||||
plot_dialog: crate::ui::window::plot::PlotDialogState,
|
plot_dialog: crate::ui::window::plot::PlotDialogState,
|
||||||
|
/// Snapshot of the dialog's settings taken when it opened, restored by the
|
||||||
|
/// `<previous>` list entry.
|
||||||
|
plot_prev: Option<crate::ui::window::plot::PlotDialogState>,
|
||||||
|
|
||||||
// ── Plot Style Table ──────────────────────────────────────────────────
|
// ── Plot Style Table ──────────────────────────────────────────────────
|
||||||
/// Currently loaded CTB/STB table (None = no override).
|
/// Currently loaded CTB/STB table (None = no override).
|
||||||
|
|
@ -2140,6 +2143,7 @@ impl OpenCADStudio {
|
||||||
plot_format: crate::io::paper_sizes::PaperSize::A4,
|
plot_format: crate::io::paper_sizes::PaperSize::A4,
|
||||||
plot_orientation: crate::io::paper_sizes::Orientation::Landscape,
|
plot_orientation: crate::io::paper_sizes::Orientation::Landscape,
|
||||||
plot_dialog: crate::ui::window::plot::PlotDialogState::load(),
|
plot_dialog: crate::ui::window::plot::PlotDialogState::load(),
|
||||||
|
plot_prev: None,
|
||||||
opening: None,
|
opening: None,
|
||||||
pending_close: None,
|
pending_close: None,
|
||||||
save_dialog_format: "DWG 2018".to_string(),
|
save_dialog_format: "DWG 2018".to_string(),
|
||||||
|
|
|
||||||
|
|
@ -1253,6 +1253,19 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
let deg = ps.rotation.to_degrees() as i32;
|
let deg = ps.rotation.to_degrees() as i32;
|
||||||
d.rotation = format!("{deg}°");
|
d.rotation = format!("{deg}°");
|
||||||
}
|
}
|
||||||
|
self.plot_dialog.name_input = None;
|
||||||
|
self.plot_dialog.name_rename = false;
|
||||||
|
// Refresh the list (<none> / <previous> / layouts / named setups) and
|
||||||
|
// snapshot the just-loaded settings as the `<previous>` restore point.
|
||||||
|
self.refresh_page_setups();
|
||||||
|
self.plot_prev = Some(self.plot_dialog.clone());
|
||||||
|
// Default selection: model space → <none>; a layout → its own setup.
|
||||||
|
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
|
||||||
|
if cur == "Model" {
|
||||||
|
self.select_page_setup(crate::ui::window::plot::SETUP_NONE);
|
||||||
|
} else {
|
||||||
|
self.select_page_setup(&format!("*{cur}*"));
|
||||||
|
}
|
||||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -1350,17 +1363,255 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
Task::done(Message::Command("PLOTWINDOW".into()))
|
Task::done(Message::Command("PLOTWINDOW".into()))
|
||||||
}
|
}
|
||||||
|
M::SelectSetup(name) => {
|
||||||
|
self.select_page_setup(&name);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
M::SetCurrent => {
|
||||||
|
self.apply_dialog_to_layout();
|
||||||
|
self.command_line.push_info("Page setup applied to the layout.");
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
M::NewSetup => {
|
||||||
|
// 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");
|
||||||
|
let ps = self.dialog_to_plotsettings();
|
||||||
|
self.tabs[self.active_tab].scene.page_setup_save(&name, ps);
|
||||||
|
self.tabs[self.active_tab].dirty = true;
|
||||||
|
self.plot_dialog.selected_setup = name.clone();
|
||||||
|
self.refresh_page_setups();
|
||||||
|
self.plot_dialog.name_input = Some(name);
|
||||||
|
self.plot_dialog.name_rename = true;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
M::CopySetup => {
|
||||||
|
// 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();
|
||||||
|
let scene = &self.tabs[self.active_tab].scene;
|
||||||
|
let (src_ps, base) = if is_layout_entry(&sel) {
|
||||||
|
let ln = layout_entry_name(&sel);
|
||||||
|
(scene.plot_settings_for(ln), format!("{ln} copy"))
|
||||||
|
} else {
|
||||||
|
(scene.page_setup_get(&sel), format!("{sel} copy"))
|
||||||
|
};
|
||||||
|
if let Some(ps) = src_ps {
|
||||||
|
let name = self.next_page_setup_name(&base);
|
||||||
|
self.tabs[self.active_tab].scene.page_setup_save(&name, ps);
|
||||||
|
self.tabs[self.active_tab].dirty = true;
|
||||||
|
self.plot_dialog.selected_setup = name.clone();
|
||||||
|
self.refresh_page_setups();
|
||||||
|
self.plot_dialog.name_input = Some(name);
|
||||||
|
self.plot_dialog.name_rename = true;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
M::RenameStart(name) => {
|
||||||
|
// Only standalone named setups can be renamed.
|
||||||
|
if is_layout_entry(&name) || is_special_entry(&name) {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.plot_dialog.selected_setup = name.clone();
|
||||||
|
if let Some(ps) = self.tabs[self.active_tab].scene.page_setup_get(&name) {
|
||||||
|
self.load_plotsettings_into_dialog(&ps);
|
||||||
|
}
|
||||||
|
self.plot_dialog.name_input = Some(name);
|
||||||
|
self.plot_dialog.name_rename = true;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
M::DeleteSetup => {
|
||||||
|
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);
|
||||||
|
self.tabs[self.active_tab].dirty = true;
|
||||||
|
self.plot_dialog.selected_setup.clear();
|
||||||
|
self.refresh_page_setups();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
M::NameInput(s) => {
|
||||||
|
self.plot_dialog.name_input = Some(s);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
M::NameCommit => {
|
||||||
|
if let Some(name) = self.plot_dialog.name_input.take() {
|
||||||
|
let name = name.trim().to_string();
|
||||||
|
if !name.is_empty() {
|
||||||
|
if self.plot_dialog.name_rename {
|
||||||
|
let old = self.plot_dialog.selected_setup.clone();
|
||||||
|
self.tabs[self.active_tab].scene.page_setup_rename(&old, &name);
|
||||||
|
} else {
|
||||||
|
let ps = self.dialog_to_plotsettings();
|
||||||
|
self.tabs[self.active_tab].scene.page_setup_save(&name, ps);
|
||||||
|
}
|
||||||
|
self.tabs[self.active_tab].dirty = true;
|
||||||
|
self.plot_dialog.selected_setup = name;
|
||||||
|
self.refresh_page_setups();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.plot_dialog.name_rename = false;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
M::NameCancel => {
|
||||||
|
self.plot_dialog.name_input = None;
|
||||||
|
self.plot_dialog.name_rename = false;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
M::Preview => self.on_plot_dlg_commit(true),
|
M::Preview => self.on_plot_dlg_commit(true),
|
||||||
M::Commit => self.on_plot_dlg_commit(false),
|
M::Commit => self.on_plot_dlg_commit(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the dialog's page settings to the layout, then either open a
|
fn refresh_page_setups(&mut self) {
|
||||||
/// preview PDF, export a PDF, or send the job to the chosen printer.
|
use crate::ui::window::plot::{SETUP_NONE, SETUP_PREV};
|
||||||
fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
|
let scene = &self.tabs[self.active_tab].scene;
|
||||||
|
// <none> / <previous>, then layouts (`*name*`), then named setups.
|
||||||
|
let mut list = vec![SETUP_NONE.to_string(), SETUP_PREV.to_string()];
|
||||||
|
list.extend(scene.layout_names().into_iter().map(|n| format!("*{n}*")));
|
||||||
|
list.extend(scene.page_setup_names());
|
||||||
|
self.plot_dialog.page_setups = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a page-setup list selection to the editor. Handles the pseudo
|
||||||
|
/// entries (`<none>` / `<previous>`), layout rows (`*name*`) and named
|
||||||
|
/// setups.
|
||||||
|
fn select_page_setup(&mut self, name: &str) {
|
||||||
|
use crate::ui::window::plot::{SETUP_NONE, SETUP_PREV};
|
||||||
|
self.plot_dialog.selected_setup = name.to_string();
|
||||||
|
if name == SETUP_NONE {
|
||||||
|
// No page setup: default geometry + PDF output.
|
||||||
|
let is_model = self.tabs[self.active_tab].scene.current_layout == "Model";
|
||||||
|
let d = &mut self.plot_dialog;
|
||||||
|
d.to_file = true;
|
||||||
|
d.paper = "A4".into();
|
||||||
|
d.orientation = "Landscape".into();
|
||||||
|
d.area = if is_model { "Window".into() } else { "Layout".into() };
|
||||||
|
d.center = true;
|
||||||
|
d.offset_x = "0.0".into();
|
||||||
|
d.offset_y = "0.0".into();
|
||||||
|
d.rotation = "0°".into();
|
||||||
|
d.scale = "Fit".into();
|
||||||
|
} else if name == SETUP_PREV {
|
||||||
|
if let Some(prev) = self.plot_prev.clone() {
|
||||||
|
self.plot_dialog.copy_settings_from(&prev);
|
||||||
|
}
|
||||||
|
} else if is_layout_entry(name) {
|
||||||
|
if let Some(ps) = self
|
||||||
|
.tabs[self.active_tab]
|
||||||
|
.scene
|
||||||
|
.plot_settings_for(layout_entry_name(name))
|
||||||
|
{
|
||||||
|
self.load_plotsettings_into_dialog(&ps);
|
||||||
|
}
|
||||||
|
} else if let Some(ps) = self.tabs[self.active_tab].scene.page_setup_get(name) {
|
||||||
|
self.load_plotsettings_into_dialog(&ps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A page-setup name based on `base` that isn't already taken (`base`,
|
||||||
|
/// `base 2`, `base 3`, …).
|
||||||
|
fn next_page_setup_name(&self, base: &str) -> String {
|
||||||
|
let existing = self.tabs[self.active_tab].scene.page_setup_names();
|
||||||
|
if !existing.iter().any(|n| n == base) {
|
||||||
|
return base.to_string();
|
||||||
|
}
|
||||||
|
(2..)
|
||||||
|
.map(|i| format!("{base} {i}"))
|
||||||
|
.find(|c| !existing.iter().any(|n| n == c))
|
||||||
|
.unwrap_or_else(|| base.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a `PlotSettings` from the current dialog fields (for saving a named
|
||||||
|
/// page setup).
|
||||||
|
fn dialog_to_plotsettings(&self) -> acadrust::objects::PlotSettings {
|
||||||
|
use acadrust::objects::{
|
||||||
|
PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType,
|
||||||
|
};
|
||||||
|
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
|
||||||
|
let d = &self.plot_dialog;
|
||||||
|
let paper = match d.paper.as_str() {
|
||||||
|
"A3" => PaperSize::A3,
|
||||||
|
"A2" => PaperSize::A2,
|
||||||
|
"A1" => PaperSize::A1,
|
||||||
|
"A0" => PaperSize::A0,
|
||||||
|
_ => PaperSize::A4,
|
||||||
|
};
|
||||||
|
let orient = if d.orientation == "Portrait" {
|
||||||
|
Orientation::Portrait
|
||||||
|
} else {
|
||||||
|
Orientation::Landscape
|
||||||
|
};
|
||||||
|
let (w, h) = sheet_mm(paper, orient);
|
||||||
|
let mut ps = PlotSettings::new("");
|
||||||
|
ps.paper_width = w;
|
||||||
|
ps.paper_height = h;
|
||||||
|
ps.paper_units = PlotPaperUnits::Millimeters;
|
||||||
|
ps.plot_type = match d.area.as_str() {
|
||||||
|
"Window" => PlotType::Window,
|
||||||
|
"Layout" => PlotType::Layout,
|
||||||
|
_ => PlotType::Extents,
|
||||||
|
};
|
||||||
|
ps.flags.plot_centered = d.center;
|
||||||
|
ps.origin_x = d.offset_x.parse::<f64>().unwrap_or(0.0);
|
||||||
|
ps.origin_y = d.offset_y.parse::<f64>().unwrap_or(0.0);
|
||||||
|
let rot: i16 = d.rotation.trim_end_matches('°').parse().unwrap_or(0);
|
||||||
|
ps.rotation = match rot {
|
||||||
|
90 => PlotRotation::Degrees90,
|
||||||
|
180 => PlotRotation::Degrees180,
|
||||||
|
270 => PlotRotation::Degrees270,
|
||||||
|
_ => PlotRotation::None,
|
||||||
|
};
|
||||||
|
if d.scale == "Fit" {
|
||||||
|
ps.set_scale_to_fit();
|
||||||
|
} else {
|
||||||
|
let (num, den) = parse_plot_scale(&d.scale);
|
||||||
|
ps.scale_type = ScaledType::CustomScale;
|
||||||
|
ps.scale_numerator = num;
|
||||||
|
ps.scale_denominator = den;
|
||||||
|
}
|
||||||
|
ps.printer_name = d.printer.clone().unwrap_or_default();
|
||||||
|
ps.current_style_sheet = d.style_name.clone();
|
||||||
|
ps
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a `PlotSettings` into the dialog editor fields.
|
||||||
|
fn load_plotsettings_into_dialog(&mut self, ps: &acadrust::objects::PlotSettings) {
|
||||||
|
use acadrust::objects::PlotType;
|
||||||
|
let (paper, orient) = paper_label_from_dims(ps.paper_width, ps.paper_height);
|
||||||
|
let d = &mut self.plot_dialog;
|
||||||
|
d.paper = paper;
|
||||||
|
d.orientation = orient;
|
||||||
|
d.area = match ps.plot_type {
|
||||||
|
PlotType::Window => "Window",
|
||||||
|
PlotType::Layout => "Layout",
|
||||||
|
_ => "Extents",
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
d.center = ps.flags.plot_centered;
|
||||||
|
d.offset_x = format!("{:.2}", ps.origin_x);
|
||||||
|
d.offset_y = format!("{:.2}", ps.origin_y);
|
||||||
|
let deg = ps.rotation.to_degrees() as i32;
|
||||||
|
d.rotation = format!("{deg}°");
|
||||||
|
d.scale = if ps.is_scale_to_fit() {
|
||||||
|
"Fit".into()
|
||||||
|
} else {
|
||||||
|
let n = ps.scale_numerator;
|
||||||
|
let m = ps.scale_denominator;
|
||||||
|
if (n - 1.0).abs() < 1e-9 {
|
||||||
|
format!("1:{}", m as i64)
|
||||||
|
} else if (m - 1.0).abs() < 1e-9 {
|
||||||
|
format!("{}:1", n as i64)
|
||||||
|
} else {
|
||||||
|
"Fit".into()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the current dialog fields into the active layout's plot settings.
|
||||||
|
/// Shared by the "Set current" action and the print/export commit.
|
||||||
|
fn apply_dialog_to_layout(&mut self) {
|
||||||
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
|
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
|
||||||
// Remember the user's print preferences across sessions.
|
|
||||||
self.plot_dialog.save();
|
|
||||||
let d = self.plot_dialog.clone();
|
let d = self.plot_dialog.clone();
|
||||||
let paper = match d.paper.as_str() {
|
let paper = match d.paper.as_str() {
|
||||||
"A3" => PaperSize::A3,
|
"A3" => PaperSize::A3,
|
||||||
|
|
@ -1375,9 +1626,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
Orientation::Landscape
|
Orientation::Landscape
|
||||||
};
|
};
|
||||||
let (sheet_w, sheet_h) = sheet_mm(paper, orient);
|
let (sheet_w, sheet_h) = sheet_mm(paper, orient);
|
||||||
// Persist the dialog's page settings into the layout's PlotSettings /
|
|
||||||
// paper_limits, then reuse the tested layout-plot derivation. `plot_scale`
|
|
||||||
// feeds the model-space window plot path.
|
|
||||||
self.plot_format = paper;
|
self.plot_format = paper;
|
||||||
self.plot_orientation = orient;
|
self.plot_orientation = orient;
|
||||||
self.plot_scale = d.scale.clone();
|
self.plot_scale = d.scale.clone();
|
||||||
|
|
@ -1388,6 +1636,17 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
self.apply_plot_page_settings(
|
self.apply_plot_page_settings(
|
||||||
sheet_w, sheet_h, extents, d.center, off_x, off_y, rotation, &d.scale,
|
sheet_w, sheet_h, extents, d.center, off_x, off_y, rotation, &d.scale,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the dialog's page settings to the layout, then either open a
|
||||||
|
/// preview PDF, export a PDF, or send the job to the chosen printer.
|
||||||
|
fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
|
||||||
|
// Remember the user's print preferences across sessions.
|
||||||
|
self.plot_dialog.save();
|
||||||
|
let d = self.plot_dialog.clone();
|
||||||
|
// Persist the dialog's page settings into the layout, then reuse the
|
||||||
|
// tested layout-plot derivation.
|
||||||
|
self.apply_dialog_to_layout();
|
||||||
self.active_modal = None;
|
self.active_modal = None;
|
||||||
|
|
||||||
let plot_style = if d.with_styles {
|
let plot_style = if d.with_styles {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,39 @@ fn format_annotation_scale_name(anno: f32) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `<none>` / `<previous>` are pseudo-entries, not real page setups.
|
||||||
|
pub(super) fn is_special_entry(s: &str) -> bool {
|
||||||
|
s == crate::ui::window::plot::SETUP_NONE || s == crate::ui::window::plot::SETUP_PREV
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A page-setup list entry wrapped in `*…*` is a layout (its embedded page
|
||||||
|
/// setup), not a standalone named page setup.
|
||||||
|
pub(super) fn is_layout_entry(s: &str) -> bool {
|
||||||
|
s.len() >= 2 && s.starts_with('*') && s.ends_with('*')
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The layout name inside a `*name*` list entry.
|
||||||
|
pub(super) fn layout_entry_name(s: &str) -> &str {
|
||||||
|
s.trim_start_matches('*').trim_end_matches('*')
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Infer the closest A-series paper label and orientation from sheet
|
||||||
|
/// dimensions (mm). Falls back to A4 when nothing is close.
|
||||||
|
pub(super) fn paper_label_from_dims(w: f64, h: f64) -> (String, String) {
|
||||||
|
use crate::io::paper_sizes::PaperSize;
|
||||||
|
let orient = if w >= h { "Landscape" } else { "Portrait" };
|
||||||
|
let (short, long) = if w <= h { (w, h) } else { (h, w) };
|
||||||
|
let mut best = ("A4".to_string(), f64::INFINITY);
|
||||||
|
for p in PaperSize::ALL {
|
||||||
|
let (pw, ph) = p.dimensions_mm(); // portrait: pw < ph
|
||||||
|
let err = (pw - short).abs() + (ph - long).abs();
|
||||||
|
if err < best.1 {
|
||||||
|
best = (p.label().to_string(), err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(best.0, orient.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn parse_plot_scale(s: &str) -> (f64, f64) {
|
pub(super) fn parse_plot_scale(s: &str) -> (f64, f64) {
|
||||||
if s == "Fit" {
|
if s == "Fit" {
|
||||||
return (1.0, 1.0);
|
return (1.0, 1.0);
|
||||||
|
|
|
||||||
|
|
@ -1466,7 +1466,7 @@ impl OpenCADStudio {
|
||||||
PluginManager => (520, 460),
|
PluginManager => (520, 460),
|
||||||
UpdateNotice => (560, 460),
|
UpdateNotice => (560, 460),
|
||||||
Layers => (900, 360),
|
Layers => (900, 360),
|
||||||
Plot => (660, 540),
|
Plot => (760, 540),
|
||||||
LayoutManager => (640, 320),
|
LayoutManager => (640, 320),
|
||||||
Plotstyle => (780, 540),
|
Plotstyle => (780, 540),
|
||||||
TextStyle => (860, 480),
|
TextStyle => (860, 480),
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ impl OpenCADStudio {
|
||||||
sized(tab.layers.view_window(), 900, 360)
|
sized(tab.layers.view_window(), 900, 360)
|
||||||
}
|
}
|
||||||
super::super::ModalKind::Plot => {
|
super::super::ModalKind::Plot => {
|
||||||
sized(crate::ui::window::plot::view_window(&self.plot_dialog), 660, 540)
|
sized(crate::ui::window::plot::view_window(&self.plot_dialog), 760, 540)
|
||||||
}
|
}
|
||||||
super::super::ModalKind::LayoutManager => {
|
super::super::ModalKind::LayoutManager => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ mod camera_ops;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod modify;
|
mod modify;
|
||||||
mod mspace;
|
mod mspace;
|
||||||
|
mod page_setup;
|
||||||
mod paper;
|
mod paper;
|
||||||
mod preview;
|
mod preview;
|
||||||
mod project;
|
mod project;
|
||||||
|
|
@ -1677,14 +1678,20 @@ impl Scene {
|
||||||
/// AutoCAD files keep their settings embedded, so without this fallback the
|
/// AutoCAD files keep their settings embedded, so without this fallback the
|
||||||
/// plot/PDF path would ignore the file's rotation, origin and scale.
|
/// plot/PDF path would ignore the file's rotation, origin and scale.
|
||||||
pub fn effective_plot_settings(&self) -> Option<acadrust::objects::PlotSettings> {
|
pub fn effective_plot_settings(&self) -> Option<acadrust::objects::PlotSettings> {
|
||||||
|
self.plot_settings_for(&self.current_layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plot settings for a specific layout by name: its standalone
|
||||||
|
/// `PlotSettings` object if one exists, else synthesized from the `Layout`
|
||||||
|
/// object's embedded fields.
|
||||||
|
pub fn plot_settings_for(&self, name: &str) -> Option<acadrust::objects::PlotSettings> {
|
||||||
use acadrust::objects::{
|
use acadrust::objects::{
|
||||||
ObjectType, PaperMargin, PlotPaperUnits, PlotRotation, PlotSettings, PlotType,
|
ObjectType, PaperMargin, PlotPaperUnits, PlotRotation, PlotSettings, PlotType,
|
||||||
PlotWindow, ScaledType,
|
PlotWindow, ScaledType,
|
||||||
};
|
};
|
||||||
let name = &self.current_layout;
|
|
||||||
if let Some(ps) = self.document.objects.values().find_map(|o| {
|
if let Some(ps) = self.document.objects.values().find_map(|o| {
|
||||||
if let ObjectType::PlotSettings(ps) = o {
|
if let ObjectType::PlotSettings(ps) = o {
|
||||||
if &ps.page_name == name {
|
if ps.page_name.as_str() == name {
|
||||||
return Some(ps.clone());
|
return Some(ps.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1694,7 +1701,7 @@ impl Scene {
|
||||||
}
|
}
|
||||||
self.document.objects.values().find_map(|o| {
|
self.document.objects.values().find_map(|o| {
|
||||||
let ObjectType::Layout(l) = o else { return None };
|
let ObjectType::Layout(l) = o else { return None };
|
||||||
if &l.name != name {
|
if l.name.as_str() != name {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut ps = PlotSettings::new(l.name.clone());
|
let mut ps = PlotSettings::new(l.name.clone());
|
||||||
|
|
|
||||||
126
src/scene/page_setup.rs
Normal file
126
src/scene/page_setup.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
//! Named page setups — the reusable plot configurations a drawing stores in
|
||||||
|
//! its `ACAD_PLOTSETTINGS` dictionary (each a standalone `PlotSettings` object
|
||||||
|
//! keyed by name). These are distinct from a layout's own embedded plot
|
||||||
|
//! settings: a named setup can be applied to any layout. CRUD here keeps the
|
||||||
|
//! dictionary + objects consistent so the result round-trips through DWG/DXF.
|
||||||
|
|
||||||
|
use super::Scene;
|
||||||
|
use acadrust::objects::{Dictionary, ObjectType, PlotSettings};
|
||||||
|
use acadrust::Handle;
|
||||||
|
|
||||||
|
impl Scene {
|
||||||
|
/// Names of the document's named page setups, in dictionary order.
|
||||||
|
pub fn page_setup_names(&self) -> Vec<String> {
|
||||||
|
let dh = self.document.header.acad_plotsettings_dict_handle;
|
||||||
|
match self.document.objects.get(&dh) {
|
||||||
|
Some(ObjectType::Dictionary(d)) => d.entries.iter().map(|(k, _)| k.clone()).collect(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clone the named page setup's `PlotSettings`, or `None` if absent.
|
||||||
|
pub fn page_setup_get(&self, name: &str) -> Option<PlotSettings> {
|
||||||
|
let dh = self.document.header.acad_plotsettings_dict_handle;
|
||||||
|
let ObjectType::Dictionary(d) = self.document.objects.get(&dh)? else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let h = d.entries.iter().find(|(k, _)| k == name).map(|(_, h)| *h)?;
|
||||||
|
match self.document.objects.get(&h)? {
|
||||||
|
ObjectType::PlotSettings(ps) => Some(ps.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle of the `ACAD_PLOTSETTINGS` dictionary, creating it (and its entry
|
||||||
|
/// in the root named-objects dictionary) if the drawing has none yet.
|
||||||
|
fn ensure_plotsettings_dict(&mut self) -> Handle {
|
||||||
|
let dh = self.document.header.acad_plotsettings_dict_handle;
|
||||||
|
if matches!(self.document.objects.get(&dh), Some(ObjectType::Dictionary(_))) {
|
||||||
|
return dh;
|
||||||
|
}
|
||||||
|
let root = self.document.header.named_objects_dict_handle;
|
||||||
|
let mut dict = Dictionary::new();
|
||||||
|
dict.handle = self.document.allocate_handle();
|
||||||
|
dict.owner = root;
|
||||||
|
let new_handle = dict.handle;
|
||||||
|
self.document
|
||||||
|
.objects
|
||||||
|
.insert(new_handle, ObjectType::Dictionary(dict));
|
||||||
|
self.document.header.acad_plotsettings_dict_handle = new_handle;
|
||||||
|
if let Some(ObjectType::Dictionary(rd)) = self.document.objects.get_mut(&root) {
|
||||||
|
rd.entries.retain(|(k, _)| k != "ACAD_PLOTSETTINGS");
|
||||||
|
rd.entries.push(("ACAD_PLOTSETTINGS".to_string(), new_handle));
|
||||||
|
}
|
||||||
|
new_handle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle of the named page setup, if it exists.
|
||||||
|
fn page_setup_handle(&self, name: &str) -> Option<Handle> {
|
||||||
|
let dh = self.document.header.acad_plotsettings_dict_handle;
|
||||||
|
let ObjectType::Dictionary(d) = self.document.objects.get(&dh)? else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
d.entries.iter().find(|(k, _)| k == name).map(|(_, h)| *h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create or update the named page setup from `ps` (its `page_name` and
|
||||||
|
/// `owner` are set here). Existing entries are updated in place; new ones
|
||||||
|
/// are inserted and registered in the dictionary.
|
||||||
|
pub fn page_setup_save(&mut self, name: &str, mut ps: PlotSettings) {
|
||||||
|
let dict_handle = self.ensure_plotsettings_dict();
|
||||||
|
ps.page_name = name.to_string();
|
||||||
|
ps.owner = dict_handle;
|
||||||
|
if let Some(h) = self.page_setup_handle(name) {
|
||||||
|
ps.handle = h;
|
||||||
|
self.document.objects.insert(h, ObjectType::PlotSettings(ps));
|
||||||
|
} else {
|
||||||
|
ps.handle = self.document.allocate_handle();
|
||||||
|
let h = ps.handle;
|
||||||
|
self.document.objects.insert(h, ObjectType::PlotSettings(ps));
|
||||||
|
if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) {
|
||||||
|
d.entries.push((name.to_string(), h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the named page setup (object + dictionary entry). No-op if absent.
|
||||||
|
pub fn page_setup_delete(&mut self, name: &str) {
|
||||||
|
let dict_handle = self.document.header.acad_plotsettings_dict_handle;
|
||||||
|
let handle = if let Some(ObjectType::Dictionary(d)) =
|
||||||
|
self.document.objects.get_mut(&dict_handle)
|
||||||
|
{
|
||||||
|
let h = d.entries.iter().find(|(k, _)| k == name).map(|(_, h)| *h);
|
||||||
|
d.entries.retain(|(k, _)| k != name);
|
||||||
|
h
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(h) = handle {
|
||||||
|
self.document.objects.remove(&h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rename a named page setup (dictionary key + the object's `page_name`).
|
||||||
|
/// No-op if `old` is absent or `new` already exists.
|
||||||
|
pub fn page_setup_rename(&mut self, old: &str, new: &str) {
|
||||||
|
if old == new || new.trim().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.page_setup_handle(new).is_some() {
|
||||||
|
return; // name collision
|
||||||
|
}
|
||||||
|
let dict_handle = self.document.header.acad_plotsettings_dict_handle;
|
||||||
|
let mut renamed = None;
|
||||||
|
if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) {
|
||||||
|
if let Some(e) = d.entries.iter_mut().find(|(k, _)| k == old) {
|
||||||
|
e.0 = new.to_string();
|
||||||
|
renamed = Some(e.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(h) = renamed {
|
||||||
|
if let Some(ObjectType::PlotSettings(ps)) = self.document.objects.get_mut(&h) {
|
||||||
|
ps.page_name = new.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::io::paper_sizes::PaperSize;
|
use crate::io::paper_sizes::PaperSize;
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space,
|
button, checkbox, column, container, mouse_area, pick_list, row, scrollable, text, text_input,
|
||||||
|
Space,
|
||||||
};
|
};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
|
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
|
||||||
|
|
||||||
|
|
@ -18,11 +19,18 @@ const TEXT: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||||||
const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
|
const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
|
||||||
const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
|
const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
|
||||||
const FIELD: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 };
|
const FIELD: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 };
|
||||||
|
const ACTIVE: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
|
||||||
|
const LIST: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 };
|
||||||
|
|
||||||
/// Sentinel entries in the printer dropdown (not real printer names).
|
/// Sentinel entries in the printer dropdown (not real printer names).
|
||||||
pub const OUT_DEFAULT: &str = "System default printer";
|
pub const OUT_DEFAULT: &str = "System default printer";
|
||||||
pub const OUT_PDF: &str = "Save to PDF file…";
|
pub const OUT_PDF: &str = "Save to PDF file…";
|
||||||
|
|
||||||
|
/// Top-of-list entries: no page setup (defaults + PDF), and the last-used
|
||||||
|
/// settings captured when the dialog opened.
|
||||||
|
pub const SETUP_NONE: &str = "<none>";
|
||||||
|
pub const SETUP_PREV: &str = "<previous>";
|
||||||
|
|
||||||
/// One of the many boolean plot options (folded into a single message so the
|
/// One of the many boolean plot options (folded into a single message so the
|
||||||
/// dialog needn't carry a variant per checkbox).
|
/// dialog needn't carry a variant per checkbox).
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
|
@ -62,6 +70,25 @@ pub enum PlotDlgMsg {
|
||||||
LoadStyle,
|
LoadStyle,
|
||||||
ClearStyle,
|
ClearStyle,
|
||||||
PickWindow,
|
PickWindow,
|
||||||
|
// ── Named page-setup manager ─────────────────────────────────────────
|
||||||
|
/// Pick a named page setup (loads its values into the editor).
|
||||||
|
SelectSetup(String),
|
||||||
|
/// Write the current editor values into the active layout.
|
||||||
|
SetCurrent,
|
||||||
|
/// Create a new named page setup from the current editor values.
|
||||||
|
NewSetup,
|
||||||
|
/// Duplicate the selected page setup.
|
||||||
|
CopySetup,
|
||||||
|
/// Begin an inline rename of the given page setup row.
|
||||||
|
RenameStart(String),
|
||||||
|
/// Delete the selected named page setup.
|
||||||
|
DeleteSetup,
|
||||||
|
/// Live edit of the new/rename name field.
|
||||||
|
NameInput(String),
|
||||||
|
/// Confirm the new/rename name.
|
||||||
|
NameCommit,
|
||||||
|
/// Cancel the new/rename name row.
|
||||||
|
NameCancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transient state backing the Plot dialog. Seeded from the layout's plot
|
/// Transient state backing the Plot dialog. Seeded from the layout's plot
|
||||||
|
|
@ -98,6 +125,14 @@ pub struct PlotDialogState {
|
||||||
pub save_layout: bool,
|
pub save_layout: bool,
|
||||||
/// Display name of the active plot style table ("" = none).
|
/// Display name of the active plot style table ("" = none).
|
||||||
pub style_name: String,
|
pub style_name: String,
|
||||||
|
/// Named page setups in the document (refreshed when the dialog opens).
|
||||||
|
pub page_setups: Vec<String>,
|
||||||
|
/// Currently selected named page setup ("" = none / current layout).
|
||||||
|
pub selected_setup: String,
|
||||||
|
/// When `Some`, a name-entry row is showing (for New / Rename).
|
||||||
|
pub name_input: Option<String>,
|
||||||
|
/// `true` when `name_input` is renaming the selected setup, else creating.
|
||||||
|
pub name_rename: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PlotDialogState {
|
impl Default for PlotDialogState {
|
||||||
|
|
@ -128,11 +163,45 @@ impl Default for PlotDialogState {
|
||||||
stamp: false,
|
stamp: false,
|
||||||
save_layout: false,
|
save_layout: false,
|
||||||
style_name: String::new(),
|
style_name: String::new(),
|
||||||
|
page_setups: Vec::new(),
|
||||||
|
selected_setup: String::new(),
|
||||||
|
name_input: None,
|
||||||
|
name_rename: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlotDialogState {
|
impl PlotDialogState {
|
||||||
|
/// Copy the plot-setting fields (paper, scale, output options, …) from
|
||||||
|
/// `o`, leaving list / rename / runtime UI state untouched. Used to restore
|
||||||
|
/// the `<previous>` snapshot.
|
||||||
|
pub fn copy_settings_from(&mut self, o: &PlotDialogState) {
|
||||||
|
self.printer = o.printer.clone();
|
||||||
|
self.to_file = o.to_file;
|
||||||
|
self.paper = o.paper.clone();
|
||||||
|
self.orientation = o.orientation.clone();
|
||||||
|
self.rotation = o.rotation.clone();
|
||||||
|
self.copies = o.copies.clone();
|
||||||
|
self.area = o.area.clone();
|
||||||
|
self.center = o.center;
|
||||||
|
self.offset_x = o.offset_x.clone();
|
||||||
|
self.offset_y = o.offset_y.clone();
|
||||||
|
self.scale = o.scale.clone();
|
||||||
|
self.scale_lw = o.scale_lw;
|
||||||
|
self.quality = o.quality.clone();
|
||||||
|
self.dpi = o.dpi.clone();
|
||||||
|
self.shade = o.shade.clone();
|
||||||
|
self.mono = o.mono;
|
||||||
|
self.lineweights = o.lineweights;
|
||||||
|
self.with_styles = o.with_styles;
|
||||||
|
self.transparency = o.transparency;
|
||||||
|
self.paperspace_last = o.paperspace_last;
|
||||||
|
self.hide_paperspace = o.hide_paperspace;
|
||||||
|
self.stamp = o.stamp;
|
||||||
|
self.save_layout = o.save_layout;
|
||||||
|
self.style_name = o.style_name.clone();
|
||||||
|
}
|
||||||
|
|
||||||
/// Load the persisted print preferences (printer, copies, quality, output
|
/// Load the persisted print preferences (printer, copies, quality, output
|
||||||
/// options) from `<config>/OpenCADStudio/plot.txt`. Drawing-specific fields
|
/// options) from `<config>/OpenCADStudio/plot.txt`. Drawing-specific fields
|
||||||
/// (paper, scale, offset, rotation…) are NOT persisted here — they are
|
/// (paper, scale, offset, rotation…) are NOT persisted here — they are
|
||||||
|
|
@ -271,6 +340,50 @@ fn section_label<'a>(s: &'static str) -> Element<'a, Message> {
|
||||||
text(s).size(11).color(DIM).into()
|
text(s).size(11).color(DIM).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn vsep<'a>() -> Element<'a, Message> {
|
||||||
|
container(Space::new().width(1).height(Fill))
|
||||||
|
.width(1)
|
||||||
|
.height(Fill)
|
||||||
|
.style(|_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(BORDER)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One row in the page-setup list: a selectable name, or an inline rename field
|
||||||
|
/// when this row is being renamed.
|
||||||
|
fn setup_row<'a>(
|
||||||
|
name: &'a str,
|
||||||
|
selected: &str,
|
||||||
|
renaming: Option<&str>,
|
||||||
|
rename_buf: &'a str,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
if renaming == Some(name) {
|
||||||
|
return text_input("", rename_buf)
|
||||||
|
.on_input(|v| Message::PlotDlg(PlotDlgMsg::NameInput(v)))
|
||||||
|
.on_submit(Message::PlotDlg(PlotDlgMsg::NameCommit))
|
||||||
|
.style(field_style)
|
||||||
|
.size(11)
|
||||||
|
.padding([4, 8])
|
||||||
|
.width(Fill)
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
let is_sel = name == selected;
|
||||||
|
let cell = container(text(name.to_string()).size(11).color(TEXT))
|
||||||
|
.padding([4, 8])
|
||||||
|
.width(Fill)
|
||||||
|
.style(move |_: &Theme| container::Style {
|
||||||
|
background: is_sel.then_some(Background::Color(ACTIVE)),
|
||||||
|
text_color: Some(TEXT),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
mouse_area(cell)
|
||||||
|
.on_press(Message::PlotDlg(PlotDlgMsg::SelectSetup(name.to_string())))
|
||||||
|
.on_double_click(Message::PlotDlg(PlotDlgMsg::RenameStart(name.to_string())))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
/// A `label : dropdown` row. `ctor` turns the picked string into a dialog
|
/// A `label : dropdown` row. `ctor` turns the picked string into a dialog
|
||||||
/// message.
|
/// message.
|
||||||
fn drop_row<'a>(
|
fn drop_row<'a>(
|
||||||
|
|
@ -326,22 +439,52 @@ fn strs(items: &[&str]) -> Vec<String> {
|
||||||
pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
|
pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
|
||||||
// ── Toolbar: Cancel … Preview Print/Export ──────────────────────────
|
// ── Toolbar: Cancel … Preview Print/Export ──────────────────────────
|
||||||
let action = if s.to_file { "Export PDF" } else { "Print" };
|
let action = if s.to_file { "Export PDF" } else { "Print" };
|
||||||
|
// `<none>` / `<previous>` are pseudo-entries; layout rows are `*name*`.
|
||||||
|
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('*')
|
||||||
|
&& s.selected_setup.ends_with('*');
|
||||||
|
let can_copy = !s.selected_setup.is_empty() && !is_special;
|
||||||
|
let is_named = can_copy && !sel_is_layout;
|
||||||
|
let mut left_bar = row![button(text("New").size(11))
|
||||||
|
.on_press(Message::PlotDlg(PlotDlgMsg::NewSetup))
|
||||||
|
.style(btn(false))
|
||||||
|
.padding([4, 12])]
|
||||||
|
.spacing(4);
|
||||||
|
if can_copy {
|
||||||
|
left_bar = left_bar.push(
|
||||||
|
button(text("Copy").size(11))
|
||||||
|
.on_press(Message::PlotDlg(PlotDlgMsg::CopySetup))
|
||||||
|
.style(btn(false))
|
||||||
|
.padding([4, 12]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if is_named {
|
||||||
|
left_bar = left_bar.push(
|
||||||
|
button(text("Delete").size(11))
|
||||||
|
.on_press(Message::PlotDlg(PlotDlgMsg::DeleteSetup))
|
||||||
|
.style(btn(false))
|
||||||
|
.padding([4, 12]),
|
||||||
|
);
|
||||||
|
}
|
||||||
let toolbar = container(
|
let toolbar = container(
|
||||||
row![
|
row![
|
||||||
button(text("Cancel").size(12))
|
left_bar,
|
||||||
.on_press(Message::PlotDlg(PlotDlgMsg::Close))
|
|
||||||
.style(btn(false))
|
|
||||||
.padding([4, 14]),
|
|
||||||
Space::new().width(Fill),
|
Space::new().width(Fill),
|
||||||
button(text("Preview").size(12))
|
button(text("Set current").size(11))
|
||||||
|
.on_press(Message::PlotDlg(PlotDlgMsg::SetCurrent))
|
||||||
|
.style(btn(false))
|
||||||
|
.padding([4, 12]),
|
||||||
|
Space::new().width(6),
|
||||||
|
button(text("Preview").size(11))
|
||||||
.on_press(Message::PlotDlg(PlotDlgMsg::Preview))
|
.on_press(Message::PlotDlg(PlotDlgMsg::Preview))
|
||||||
.style(btn(false))
|
.style(btn(false))
|
||||||
.padding([4, 12]),
|
.padding([4, 12]),
|
||||||
Space::new().width(8),
|
Space::new().width(6),
|
||||||
button(text(action).size(12))
|
button(text(action).size(11))
|
||||||
.on_press(Message::PlotDlg(PlotDlgMsg::Commit))
|
.on_press(Message::PlotDlg(PlotDlgMsg::Commit))
|
||||||
.style(btn(true))
|
.style(btn(true))
|
||||||
.padding([4, 20]),
|
.padding([4, 18]),
|
||||||
]
|
]
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
)
|
)
|
||||||
|
|
@ -364,6 +507,54 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
|
||||||
|
|
||||||
let paper_opts: Vec<String> = PaperSize::ALL.iter().map(|p| p.label().to_string()).collect();
|
let paper_opts: Vec<String> = PaperSize::ALL.iter().map(|p| p.label().to_string()).collect();
|
||||||
|
|
||||||
|
// ── Left panel: named page-setup list (click selects, double-click renames)
|
||||||
|
let renaming = if s.name_rename && !s.selected_setup.is_empty() {
|
||||||
|
Some(s.selected_setup.as_str())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let rename_buf = s.name_input.as_deref().unwrap_or("");
|
||||||
|
let rows: Vec<Element<'_, Message>> = s
|
||||||
|
.page_setups
|
||||||
|
.iter()
|
||||||
|
.map(|name| setup_row(name, &s.selected_setup, renaming, rename_buf))
|
||||||
|
.collect();
|
||||||
|
let list_body: Element<'_, Message> = if rows.is_empty() {
|
||||||
|
container(text("(no page setups)").size(11).color(DIM))
|
||||||
|
.padding([6, 8])
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
scrollable(column(rows).spacing(1)).height(Fill).into()
|
||||||
|
};
|
||||||
|
let list_panel = container(
|
||||||
|
column![
|
||||||
|
text("Page setups").size(10).color(DIM),
|
||||||
|
container(list_body)
|
||||||
|
.style(|_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(LIST)),
|
||||||
|
border: Border {
|
||||||
|
color: BORDER,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 3.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.padding(2),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.height(Fill),
|
||||||
|
)
|
||||||
|
.width(160)
|
||||||
|
.height(Fill)
|
||||||
|
.padding(iced::Padding {
|
||||||
|
top: 12.0,
|
||||||
|
right: 8.0,
|
||||||
|
bottom: 12.0,
|
||||||
|
left: 12.0,
|
||||||
|
});
|
||||||
|
|
||||||
// ── Left column ──────────────────────────────────────────────────────
|
// ── Left column ──────────────────────────────────────────────────────
|
||||||
let style_label: String = if s.style_name.is_empty() {
|
let style_label: String = if s.style_name.is_empty() {
|
||||||
"(none)".into()
|
"(none)".into()
|
||||||
|
|
@ -507,10 +698,14 @@ pub fn view_window(s: &PlotDialogState) -> Element<'_, Message> {
|
||||||
.spacing(9)
|
.spacing(9)
|
||||||
.width(Fill);
|
.width(Fill);
|
||||||
|
|
||||||
let body = row![left, right].spacing(18).padding(14).width(Fill);
|
let detail = scrollable(
|
||||||
let content = scrollable(body).width(Fill).height(Fill);
|
container(row![left, right].spacing(18).width(Fill)).padding(14),
|
||||||
|
)
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill);
|
||||||
|
let body = row![list_panel, vsep(), detail].height(Fill);
|
||||||
|
|
||||||
container(column![toolbar, hdivider(), content].spacing(0))
|
container(column![toolbar, hdivider(), body].spacing(0))
|
||||||
.style(|_: &Theme| container::Style {
|
.style(|_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(BG)),
|
background: Some(Background::Color(BG)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue