forked from 1b-agent/opencadstudio-upstream
feat(plot): custom paper sizes with printable margins
AutoCAD lets a plotter configuration carry user-defined sheets; a drawing then names them like any other media (`Roll_24_(609.60_x_1500.00_MM)`). The plot dialog now defines such sheets inline, under the Size row: name, width/height, unit, and the printable margins in that unit. Added sheets are selected at once, offered for every device after the device's own list, remembered in the plot config across sessions, and written to the layout with their own margins (a printer that reports the same size still wins with its hardware margins). A selected user-defined sheet can be removed; the sheet itself stays selectable in the drawing that uses it. `CustomPaper` / `Margins` live in the paper catalogue with serde support; `PlotDevice::margins_mm` consults the user's definitions. The Plot dialog's form helpers (button and field styles, dividers, `label : control` rows) move to `ui::style::form` so the next dialogs share them instead of copying. Tests: draft validation and seeding, catalogue naming of user sheets, device margins with user definitions, and dialog round-trips (add, select, persist, write, replace, remove, refuse an invalid size). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a7c89eca33
commit
5e271e2dbb
28 files changed
+930
-175
No files matched your search
@@ -5026,6 +5026,9 @@ plot =
|
||||
.fit-to-paper = ملاءمة للورق
|
||||
.layout-plots-the-current-sheet-using-the-selec = يطبع التخطيط الورقة الحالية باستخدام حجم الورق المحدد.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = دمج الخطوط المتداخلة
|
||||
.object-lineweights = سماكات خطوط الكائنات
|
||||
.orientation = الاتجاه
|
||||
|
||||
@@ -5014,6 +5014,9 @@ plot =
|
||||
.fit-to-paper = Побери в хартията
|
||||
.layout-plots-the-current-sheet-using-the-selec = Оформлението плотира текущия лист, използвайки избрания размер хартия.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Обедини припокриващи се линии
|
||||
.object-lineweights = Дебелини на линии на обект
|
||||
.orientation = Ориентация
|
||||
|
||||
@@ -5011,6 +5011,9 @@ plot =
|
||||
.fit-to-paper = Přizpůsobit papíru
|
||||
.layout-plots-the-current-sheet-using-the-selec = Rozvržení vykreslí aktuální list pomocí zvolené velikosti papíru.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Sloučit překrývající se čáry
|
||||
.object-lineweights = Tloušťky čar objektů
|
||||
.orientation = Orientace
|
||||
|
||||
@@ -5012,6 +5012,9 @@ plot =
|
||||
.fit-to-paper = An Papier anpassen
|
||||
.layout-plots-the-current-sheet-using-the-selec = Das Layout plottet das aktuelle Blatt mit dem ausgewählten Papierformat.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Zusammenführung überlappender Leitungen
|
||||
.object-lineweights = Objektliniengewichte
|
||||
.orientation = Ausrichtung
|
||||
|
||||
@@ -5014,6 +5014,9 @@ plot =
|
||||
.fit-to-paper = Προσαρμογή στο χαρτί
|
||||
.layout-plots-the-current-sheet-using-the-selec = Η επιλογή Διάταξη εκτυπώνει το τρέχον φύλλο με το επιλεγμένο μέγεθος χαρτιού.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Μεγέθη φύλλων και εκτυπώσιμες περιοχές όπως τα αναφέρει ο εκτυπωτής.
|
||||
.custom-paper-size = Προσαρμοσμένο μέγεθος χαρτιού
|
||||
.printable-margins = Εκτυπώσιμα περιθώρια
|
||||
.enter-a-positive-width-and-height = Δώστε θετικό πλάτος και ύψος.
|
||||
.merge-overlapping-lines = Συγχώνευση επικαλυπτόμενων γραμμών
|
||||
.object-lineweights = Πάχη γραμμών αντικειμένων
|
||||
.orientation = Προσανατολισμός
|
||||
|
||||
@@ -5015,6 +5015,9 @@ plot =
|
||||
.fit-to-paper = Fit to paper
|
||||
.layout-plots-the-current-sheet-using-the-selec = Layout plots the current sheet using the selected paper size.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Merge overlapping lines
|
||||
.object-lineweights = Object lineweights
|
||||
.orientation = Orientation
|
||||
|
||||
@@ -5014,6 +5014,9 @@ plot =
|
||||
.fit-to-paper = Ajustar al papel
|
||||
.layout-plots-the-current-sheet-using-the-selec = La presentación traza la hoja actual con el tamaño de papel seleccionado.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Fusionar líneas solapadas
|
||||
.object-lineweights = Grosores de línea de objetos
|
||||
.orientation = Orientación
|
||||
|
||||
@@ -5013,6 +5013,9 @@ plot =
|
||||
.fit-to-paper = Sovita paperille
|
||||
.layout-plots-the-current-sheet-using-the-selec = Asettelu piirtää nykyisen arkin käyttämällä valittua paperikokoa.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Yhdistä päällekkäiset rivit
|
||||
.object-lineweights = Objektin viivan painot
|
||||
.orientation = Suuntautuminen
|
||||
|
||||
@@ -5014,6 +5014,9 @@ plot =
|
||||
.fit-to-paper = Ajuster au papier
|
||||
.layout-plots-the-current-sheet-using-the-selec = La présentation trace la feuille courante avec le format de papier sélectionné.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Fusionner les lignes qui se chevauchent
|
||||
.object-lineweights = Poids de ligne d'objet
|
||||
.orientation = Orientation
|
||||
|
||||
@@ -5002,6 +5002,9 @@ plot =
|
||||
.fit-to-paper = कागज़ पर फ़िट करें
|
||||
.layout-plots-the-current-sheet-using-the-selec = लेआउट चयनित कागज़ आकार पर वर्तमान शीट प्लॉट करता है।
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = मर्ज ओवरलैपिंग लाइन
|
||||
.object-lineweights = ऑब्जेक्ट लाइनवेट
|
||||
.orientation = अभिविन्यास
|
||||
|
||||
@@ -5013,6 +5013,9 @@ plot =
|
||||
.fit-to-paper = Papírra illeszthető
|
||||
.layout-plots-the-current-sheet-using-the-selec = Az Elrendezés az aktuális lapot a kiválasztott papírmérettel ábrázolja.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Egyesítse az átfedő vonalakat
|
||||
.object-lineweights = Objektumvonalvastagságok
|
||||
.orientation = Tájékozódás
|
||||
|
||||
@@ -5013,6 +5013,9 @@ plot =
|
||||
.fit-to-paper = Adatta alla carta
|
||||
.layout-plots-the-current-sheet-using-the-selec = Layout traccia il foglio corrente utilizzando il formato carta selezionato.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Unisci linee sovrapposte
|
||||
.object-lineweights = Spessori linea degli oggetti
|
||||
.orientation = Orientamento
|
||||
|
||||
@@ -5002,6 +5002,9 @@ plot =
|
||||
.fit-to-paper = 用紙にフィット
|
||||
.layout-plots-the-current-sheet-using-the-selec = 選択した用紙サイズで現在のシートを印刷します。
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = 重複線を結合
|
||||
.object-lineweights = オブジェクトの線の太さ
|
||||
.orientation = 方向
|
||||
|
||||
@@ -5013,6 +5013,9 @@ plot =
|
||||
.fit-to-paper = 종이에 맞추기
|
||||
.layout-plots-the-current-sheet-using-the-selec = 레이아웃은 선택한 용지 크기를 사용하여 현재 시트를 플롯합니다.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = 겹치는 선 병합
|
||||
.object-lineweights = 객체 선가중치
|
||||
.orientation = 오리엔테이션
|
||||
|
||||
@@ -5012,6 +5012,9 @@ plot =
|
||||
.fit-to-paper = Aan papier aanpassen
|
||||
.layout-plots-the-current-sheet-using-the-selec = De lay-out plot het huidige vel op het geselecteerde papierformaat.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Overlaplijnen samenvoegen
|
||||
.object-lineweights = Objectlijndiktes
|
||||
.orientation = Oriëntatie
|
||||
|
||||
@@ -5013,6 +5013,9 @@ plot =
|
||||
.fit-to-paper = Dopasuj do papieru
|
||||
.layout-plots-the-current-sheet-using-the-selec = Układ drukuje bieżący arkusz przy użyciu wybranego rozmiaru papieru.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Połącz nakładające się linie
|
||||
.object-lineweights = Szerokości linii obiektu
|
||||
.orientation = Orientacja
|
||||
|
||||
@@ -5014,6 +5014,9 @@ plot =
|
||||
.fit-to-paper = Ajustar ao papel
|
||||
.layout-plots-the-current-sheet-using-the-selec = Layout plota a folha atual usando o tamanho de papel selecionado.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Mesclar linhas sobrepostas
|
||||
.object-lineweights = Espessuras de linha dos objetos
|
||||
.orientation = Orientação
|
||||
|
||||
@@ -5015,6 +5015,9 @@ plot =
|
||||
.fit-to-paper = Вписать в лист
|
||||
.layout-plots-the-current-sheet-using-the-selec = Лист печатается на выбранном формате бумаги.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Слияние пересекающихся линий
|
||||
.object-lineweights = Весы для объектов
|
||||
.orientation = Ориентация
|
||||
|
||||
@@ -4954,6 +4954,9 @@ plot =
|
||||
.fit-to-paper = Kağıda sığdır
|
||||
.layout-plots-the-current-sheet-using-the-selec = Yerleşim, geçerli sayfayı seçilen kağıt boyutuyla çizer.
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = Çakışan çizgileri birleştir
|
||||
.object-lineweights = Nesne çizgi kalınlıkları
|
||||
.orientation = Yönelim
|
||||
|
||||
@@ -4997,6 +4997,9 @@ plot =
|
||||
.fit-to-paper = 布满图纸
|
||||
.layout-plots-the-current-sheet-using-the-selec = 布局使用所选纸张尺寸打印当前图纸。
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = 合并重叠线
|
||||
.object-lineweights = 对象线宽
|
||||
.orientation = 方向
|
||||
|
||||
@@ -5009,6 +5009,9 @@ plot =
|
||||
.fit-to-paper = 布滿圖紙
|
||||
.layout-plots-the-current-sheet-using-the-selec = 配置使用所選紙張尺寸列印目前圖紙。
|
||||
.sheet-sizes-and-printable-areas-reported-by-th = Sheet sizes and printable areas reported by the printer.
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.merge-overlapping-lines = 合併重疊線
|
||||
.object-lineweights = 物件線寬
|
||||
.orientation = 方向
|
||||
|
||||
+144
-1
@@ -197,7 +197,7 @@ fn margins_for_settings(
|
||||
if paper_unchanged(d, stored) && stored_device == device {
|
||||
return stored.margins;
|
||||
}
|
||||
let margins = device.margins_mm(&plot_dialog_paper(d));
|
||||
let margins = device.margins_mm(&plot_dialog_paper(d), &d.custom_papers);
|
||||
acadrust::objects::PaperMargin::new(margins.left, margins.bottom, margins.right, margins.top)
|
||||
}
|
||||
|
||||
@@ -3855,6 +3855,86 @@ impl OpenCADStudio {
|
||||
)
|
||||
}
|
||||
|
||||
/// The inline custom-sheet editor of the Plot dialog. A sheet is added
|
||||
/// to (or replaced in) the persisted list under its media name and
|
||||
/// selected at once; the list is saved immediately so it survives a
|
||||
/// crash the way a plotter configuration would.
|
||||
fn on_custom_paper_msg(&mut self, msg: crate::ui::window::plot::CustomPaperMsg) {
|
||||
use crate::ui::window::plot::{CustomPaperDraft, CustomPaperMsg as C, MarginSide};
|
||||
let d = &mut self.plot_dialog;
|
||||
match msg {
|
||||
C::Open => {
|
||||
let paper = plot_dialog_paper(d);
|
||||
let existing = d
|
||||
.custom_papers
|
||||
.iter()
|
||||
.find(|custom| custom.canonical() == paper.canonical);
|
||||
let margins = match existing {
|
||||
Some(custom) => custom.margins_mm(),
|
||||
None => plot_dialog_device(d).margins_mm(&paper, &d.custom_papers),
|
||||
};
|
||||
let mut draft = CustomPaperDraft::from_paper(&paper, margins);
|
||||
// A catalogue sheet is only the starting size; the new sheet
|
||||
// gets its own name (AutoCAD's generic one when left blank).
|
||||
if existing.is_none() {
|
||||
draft.name.clear();
|
||||
}
|
||||
d.custom_editor = Some(draft);
|
||||
}
|
||||
C::Cancel => d.custom_editor = None,
|
||||
C::Name(value) => Self::edit_custom_draft(d, |draft| draft.name = value),
|
||||
C::Width(value) => Self::edit_custom_draft(d, |draft| draft.width = value),
|
||||
C::Height(value) => Self::edit_custom_draft(d, |draft| draft.height = value),
|
||||
C::Units(value) => Self::edit_custom_draft(d, |draft| draft.inches = value == "Inches"),
|
||||
C::Margin(side, value) => Self::edit_custom_draft(d, |draft| {
|
||||
let index = match side {
|
||||
MarginSide::Left => 0,
|
||||
MarginSide::Bottom => 1,
|
||||
MarginSide::Right => 2,
|
||||
MarginSide::Top => 3,
|
||||
};
|
||||
draft.margins[index] = value;
|
||||
}),
|
||||
C::Add => {
|
||||
let Some(draft) = d.custom_editor.as_mut() else {
|
||||
return;
|
||||
};
|
||||
match draft.build() {
|
||||
Ok(custom) => {
|
||||
let canonical = custom.canonical();
|
||||
d.custom_papers.retain(|existing| existing.canonical() != canonical);
|
||||
d.custom_papers.push(custom);
|
||||
d.custom_editor = None;
|
||||
d.paper = canonical;
|
||||
let (w, h) = plot_dialog_sheet_mm(d);
|
||||
d.paper_width_mm = w;
|
||||
d.paper_height_mm = h;
|
||||
self.save_config();
|
||||
}
|
||||
Err(error) => draft.error = Some(error),
|
||||
}
|
||||
}
|
||||
C::Remove => {
|
||||
let selected = plot_dialog_paper(d).canonical.into_owned();
|
||||
let before = d.custom_papers.len();
|
||||
d.custom_papers.retain(|custom| custom.canonical() != selected);
|
||||
if d.custom_papers.len() != before {
|
||||
self.save_config();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_custom_draft(
|
||||
d: &mut crate::ui::window::plot::PlotDialogState,
|
||||
edit: impl FnOnce(&mut crate::ui::window::plot::CustomPaperDraft),
|
||||
) {
|
||||
if let Some(draft) = d.custom_editor.as_mut() {
|
||||
edit(draft);
|
||||
draft.error = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_common_plot_dialog(&mut self) {
|
||||
self.plot_dialog.center = true;
|
||||
self.plot_dialog.offset_x = "0.0".into();
|
||||
@@ -3895,6 +3975,10 @@ impl OpenCADStudio {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
M::CustomPaper(msg) => {
|
||||
self.on_custom_paper_msg(msg);
|
||||
Task::none()
|
||||
}
|
||||
M::PrinterProperties => {
|
||||
match crate::io::print_to_printer::open_printer_properties(
|
||||
self.plot_dialog.printer.as_deref(),
|
||||
@@ -5077,6 +5161,65 @@ mod plot_paper_tests {
|
||||
assert!(app.plot_dialog.printer_media.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_custom_sheet_is_added_selected_persisted_and_written_with_its_margins() {
|
||||
use crate::ui::window::plot::{CustomPaperMsg as C, MarginSide};
|
||||
let mut app = app_with_printer_named_sheet();
|
||||
let _ = app.on_plot_dialog_open();
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_PDF.into()));
|
||||
let custom = |m: C| PlotDlgMsg::CustomPaper(m);
|
||||
let _ = app.on_plot_dlg(custom(C::Open));
|
||||
// Seeded from the selected ISO A4 on the PDF driver, nameless.
|
||||
let draft = app.plot_dialog.custom_editor.clone().expect("editor open");
|
||||
assert_eq!((draft.width.as_str(), draft.height.as_str()), ("210", "297"));
|
||||
assert_eq!(draft.margins, ["5", "17", "6", "18"]);
|
||||
assert!(draft.name.is_empty());
|
||||
let _ = app.on_plot_dlg(custom(C::Name("Roll 24".into())));
|
||||
let _ = app.on_plot_dlg(custom(C::Width("1500".into())));
|
||||
let _ = app.on_plot_dlg(custom(C::Height("609.6".into())));
|
||||
let _ = app.on_plot_dlg(custom(C::Margin(MarginSide::Left, "3".into())));
|
||||
let _ = app.on_plot_dlg(custom(C::Margin(MarginSide::Top, "4".into())));
|
||||
let _ = app.on_plot_dlg(custom(C::Add));
|
||||
assert!(app.plot_dialog.custom_editor.is_none(), "Add closes the editor");
|
||||
assert_eq!(app.plot_dialog.paper, "Roll_24_(609.60_x_1500.00_MM)");
|
||||
assert_eq!(super::plot_dialog_sheet_mm(&app.plot_dialog), (1500.0, 609.6));
|
||||
assert_eq!(app.current_config().plot.custom_papers.len(), 1, "persisted in the plot config");
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
|
||||
let ps = app.tabs[app.active_tab].scene.plot_settings_for("Layout1").unwrap();
|
||||
assert_eq!(ps.paper_size, "Roll_24_(609.60_x_1500.00_MM)");
|
||||
assert_eq!((ps.paper_width, ps.paper_height), (1500.0, 609.6));
|
||||
assert_eq!(ps.margins, acadrust::objects::PaperMargin::new(3.0, 17.0, 6.0, 4.0));
|
||||
// Re-adding the same size replaces the definition instead of duplicating it.
|
||||
let _ = app.on_plot_dlg(custom(C::Open));
|
||||
assert_eq!(app.plot_dialog.custom_editor.as_ref().map(|d| d.name.as_str()), Some("Roll 24"));
|
||||
let _ = app.on_plot_dlg(custom(C::Margin(MarginSide::Left, "9".into())));
|
||||
let _ = app.on_plot_dlg(custom(C::Add));
|
||||
assert_eq!(app.plot_dialog.custom_papers.len(), 1);
|
||||
assert_eq!(app.plot_dialog.custom_papers[0].margins.left, 9.0);
|
||||
// Remove drops the definition; the sheet itself stays selectable.
|
||||
let _ = app.on_plot_dlg(custom(C::Remove));
|
||||
assert!(app.plot_dialog.custom_papers.is_empty());
|
||||
assert_eq!(app.plot_dialog.paper, "Roll_24_(609.60_x_1500.00_MM)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_invalid_custom_sheet_is_refused_with_a_reason() {
|
||||
use crate::ui::window::plot::{CustomPaperError, CustomPaperMsg as C};
|
||||
let mut app = app_with_printer_named_sheet();
|
||||
let _ = app.on_plot_dialog_open();
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::CustomPaper(C::Open));
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::CustomPaper(C::Width("-5".into())));
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::CustomPaper(C::Add));
|
||||
let draft = app.plot_dialog.custom_editor.as_ref().expect("editor stays open");
|
||||
assert_eq!(draft.error, Some(CustomPaperError::Size));
|
||||
assert!(app.plot_dialog.custom_papers.is_empty());
|
||||
// Typing again clears the message.
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::CustomPaper(C::Width("500".into())));
|
||||
assert!(app.plot_dialog.custom_editor.as_ref().unwrap().error.is_none());
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::CustomPaper(C::Cancel));
|
||||
assert!(app.plot_dialog.custom_editor.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_sheet_from_a_drawing_keeps_its_own_dimensions() {
|
||||
let mut app = app_with_printer_named_sheet();
|
||||
|
||||
+147
-7
@@ -16,7 +16,7 @@ use std::sync::OnceLock;
|
||||
|
||||
/// Unit the media is defined in. AutoCAD keeps ISO/JIS sheets in millimetres
|
||||
/// and ANSI/ARCH sheets in inches, and the unit is part of the media name.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PaperUnits {
|
||||
Millimeters,
|
||||
Inches,
|
||||
@@ -107,6 +107,88 @@ pub enum Orientation {
|
||||
Landscape,
|
||||
}
|
||||
|
||||
/// Unprintable margins of a sheet, in the order AutoCAD stores them. The unit
|
||||
/// is whatever the context says — millimetres in a drawing, the sheet's own
|
||||
/// unit in a user-defined size.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Margins {
|
||||
pub left: f64,
|
||||
pub bottom: f64,
|
||||
pub right: f64,
|
||||
pub top: f64,
|
||||
}
|
||||
|
||||
impl Margins {
|
||||
pub const ZERO: Margins = Margins {
|
||||
left: 0.0,
|
||||
bottom: 0.0,
|
||||
right: 0.0,
|
||||
top: 0.0,
|
||||
};
|
||||
|
||||
pub const fn uniform(value: f64) -> Self {
|
||||
Margins {
|
||||
left: value,
|
||||
bottom: value,
|
||||
right: value,
|
||||
top: value,
|
||||
}
|
||||
}
|
||||
|
||||
/// The same margins expressed in millimetres when they were given in
|
||||
/// `units`.
|
||||
pub fn to_mm(self, units: PaperUnits) -> Self {
|
||||
Margins {
|
||||
left: units.to_mm(self.left),
|
||||
bottom: units.to_mm(self.bottom),
|
||||
right: units.to_mm(self.right),
|
||||
top: units.to_mm(self.top),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A sheet the user defined, kept in the application settings the way
|
||||
/// AutoCAD keeps custom sizes in a plotter configuration: a name, the
|
||||
/// dimensions in the sheet's own unit, and the printable margins.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct CustomPaper {
|
||||
/// Human name, e.g. `Roll 24`; becomes `Roll_24_(…)` in the media name.
|
||||
pub name: String,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
pub units: PaperUnits,
|
||||
/// Unprintable margins in `units`.
|
||||
pub margins: Margins,
|
||||
}
|
||||
|
||||
impl Default for CustomPaper {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: String::new(),
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
units: PaperUnits::Millimeters,
|
||||
margins: Margins::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomPaper {
|
||||
/// The catalogue sheet this definition describes.
|
||||
pub fn paper(&self) -> PaperSize {
|
||||
PaperSize::custom_named(&self.name, self.width, self.height, self.units)
|
||||
}
|
||||
|
||||
pub fn canonical(&self) -> String {
|
||||
self.paper().canonical.into_owned()
|
||||
}
|
||||
|
||||
pub fn margins_mm(&self) -> Margins {
|
||||
self.margins.to_mm(self.units)
|
||||
}
|
||||
}
|
||||
|
||||
/// One sheet of the catalogue, or a sheet reconstructed from a media name.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct PaperSize {
|
||||
@@ -156,18 +238,43 @@ impl PaperSize {
|
||||
/// in the plot dialog (`UserDefinedMetric_(420.00_x_297.00_MM)`). The
|
||||
/// dimensions are normalised to portrait so the name stays canonical.
|
||||
pub fn custom(width: f64, height: f64, units: PaperUnits) -> Self {
|
||||
let label = match units {
|
||||
PaperUnits::Millimeters => "UserDefinedMetric",
|
||||
PaperUnits::Inches => "UserDefinedInches",
|
||||
};
|
||||
Self::custom_named(label, width, height, units)
|
||||
}
|
||||
|
||||
/// A user-defined sheet under its own name, the way a plotter
|
||||
/// configuration's custom sizes are named (`Roll_24_(609.60_x_1500.00_MM)`).
|
||||
/// A blank name falls back to AutoCAD's generic one; spaces become
|
||||
/// underscores in the media name and parentheses are dropped so the name
|
||||
/// stays parseable.
|
||||
pub fn custom_named(name: &str, width: f64, height: f64, units: PaperUnits) -> Self {
|
||||
let (width, height) = if width <= height {
|
||||
(width, height)
|
||||
} else {
|
||||
(height, width)
|
||||
};
|
||||
let label = match units {
|
||||
PaperUnits::Millimeters => "UserDefinedMetric",
|
||||
PaperUnits::Inches => "UserDefinedInches",
|
||||
};
|
||||
let cleaned: String = name
|
||||
.trim()
|
||||
.chars()
|
||||
.filter(|c| !matches!(c, '(' | ')'))
|
||||
.collect::<String>()
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if cleaned.is_empty() {
|
||||
return Self::custom(width, height, units);
|
||||
}
|
||||
Self {
|
||||
canonical: Cow::Owned(canonical_name(label, width, height, units)),
|
||||
label: Cow::Borrowed(label),
|
||||
canonical: Cow::Owned(canonical_name(
|
||||
&cleaned.replace(' ', "_"),
|
||||
width,
|
||||
height,
|
||||
units,
|
||||
)),
|
||||
label: Cow::Owned(cleaned),
|
||||
series: PaperSeries::Other,
|
||||
variant: PaperVariant::Standard,
|
||||
width,
|
||||
@@ -688,6 +795,39 @@ mod tests {
|
||||
assert_eq!(default_paper().canonical, a4().canonical);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_defined_sheets_carry_their_name_and_margins() {
|
||||
let roll = CustomPaper {
|
||||
name: "Roll 24 (long)".into(),
|
||||
width: 1500.0,
|
||||
height: 609.6,
|
||||
units: PaperUnits::Millimeters,
|
||||
margins: Margins {
|
||||
left: 5.0,
|
||||
bottom: 17.0,
|
||||
right: 6.0,
|
||||
top: 18.0,
|
||||
},
|
||||
};
|
||||
assert_eq!(roll.canonical(), "Roll_24_long_(609.60_x_1500.00_MM)");
|
||||
assert_eq!(roll.paper().label, "Roll 24 long");
|
||||
assert_eq!(roll.paper().display(), "Roll 24 long (609.6 × 1500 mm)");
|
||||
// Round-trips through the parser like any AutoCAD name.
|
||||
assert_eq!(
|
||||
resolve(&roll.canonical()).unwrap().portrait_mm(),
|
||||
(609.6, 1500.0)
|
||||
);
|
||||
let arch = CustomPaper {
|
||||
name: String::new(),
|
||||
width: 30.0,
|
||||
height: 42.0,
|
||||
units: PaperUnits::Inches,
|
||||
margins: Margins::uniform(0.25),
|
||||
};
|
||||
assert_eq!(arch.canonical(), "UserDefinedInches_(30.00_x_42.00_Inches)");
|
||||
assert_eq!(arch.margins_mm(), Margins::uniform(6.35));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_centers_and_scales_within_margin() {
|
||||
// 100×100 window onto a 210×297 sheet, Fit. Limiting axis is width:
|
||||
|
||||
+57
-52
@@ -15,7 +15,8 @@
|
||||
//! a fixed table; a CUPS printer reports its media and margins over IPP
|
||||
//! (`media-col-database`), which is queried once per printer and cached.
|
||||
|
||||
use crate::io::paper_catalog::{self, PaperSize, PaperUnits, PaperVariant};
|
||||
pub use crate::io::paper_catalog::Margins;
|
||||
use crate::io::paper_catalog::{self, CustomPaper, PaperSize, PaperUnits, PaperVariant};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
@@ -73,51 +74,23 @@ impl PlotDevice {
|
||||
}
|
||||
|
||||
/// Unprintable margins the device imposes on `paper`, in millimetres.
|
||||
pub fn margins_mm(&self, paper: &PaperSize) -> Margins {
|
||||
/// A sheet the user defined carries its own margins for every device,
|
||||
/// the way a custom size in a plotter configuration does; a printer that
|
||||
/// reports the same size still wins, since it knows its hardware.
|
||||
pub fn margins_mm(&self, paper: &PaperSize, custom: &[CustomPaper]) -> Margins {
|
||||
let user_defined = custom
|
||||
.iter()
|
||||
.find(|c| c.canonical() == paper.canonical)
|
||||
.map(CustomPaper::margins_mm);
|
||||
match self {
|
||||
PlotDevice::Pdf => pdf_margins_mm(paper),
|
||||
PlotDevice::Printer(name) => printer_margins_mm(name, paper),
|
||||
PlotDevice::Pdf => user_defined.unwrap_or_else(|| pdf_margins_mm(paper)),
|
||||
PlotDevice::Printer(name) => cached_printer_capabilities(name)
|
||||
.and_then(|caps| caps.margins_for(paper))
|
||||
.or(user_defined)
|
||||
.unwrap_or(Margins::ZERO),
|
||||
// No device: AutoCAD keeps the margins of the last device or its
|
||||
// defaults; without one we show the full sheet as printable.
|
||||
PlotDevice::None => Margins::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unprintable margins in millimetres, in the order AutoCAD stores them.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Margins {
|
||||
pub left: f64,
|
||||
pub bottom: f64,
|
||||
pub right: f64,
|
||||
pub top: f64,
|
||||
}
|
||||
|
||||
impl Margins {
|
||||
pub const ZERO: Margins = Margins {
|
||||
left: 0.0,
|
||||
bottom: 0.0,
|
||||
right: 0.0,
|
||||
top: 0.0,
|
||||
};
|
||||
|
||||
pub const fn uniform(value: f64) -> Self {
|
||||
Margins {
|
||||
left: value,
|
||||
bottom: value,
|
||||
right: value,
|
||||
top: value,
|
||||
}
|
||||
}
|
||||
|
||||
/// The same margins expressed in millimetres when they were given in
|
||||
/// `units`.
|
||||
pub fn to_mm(self, units: PaperUnits) -> Self {
|
||||
Margins {
|
||||
left: units.to_mm(self.left),
|
||||
bottom: units.to_mm(self.bottom),
|
||||
right: units.to_mm(self.right),
|
||||
top: units.to_mm(self.top),
|
||||
PlotDevice::None => user_defined.unwrap_or(Margins::ZERO),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,12 +207,6 @@ pub fn printer_capabilities(printer: &str) -> Option<Arc<PrinterCapabilities>> {
|
||||
Some(caps)
|
||||
}
|
||||
|
||||
fn printer_margins_mm(printer: &str, paper: &PaperSize) -> Margins {
|
||||
cached_printer_capabilities(printer)
|
||||
.and_then(|caps| caps.margins_for(paper))
|
||||
.unwrap_or(Margins::ZERO)
|
||||
}
|
||||
|
||||
/// Name of the system default printer, when the platform reports one.
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
||||
pub fn default_printer_name() -> Option<String> {
|
||||
@@ -459,7 +426,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pdf_margins_follow_autocad_per_family_and_variant() {
|
||||
let m = |name: &str| PlotDevice::Pdf.margins_mm(&paper_catalog::resolve(name).unwrap());
|
||||
let m =
|
||||
|name: &str| PlotDevice::Pdf.margins_mm(&paper_catalog::resolve(name).unwrap(), &[]);
|
||||
// ISO sheets share one millimetre set regardless of size.
|
||||
let iso_standard = Margins {
|
||||
left: 5.0,
|
||||
@@ -517,12 +485,49 @@ mod tests {
|
||||
inch(0.03, 0.03, 0.03, 0.03)
|
||||
));
|
||||
assert_eq!(
|
||||
PlotDevice::None
|
||||
.margins_mm(&paper_catalog::resolve("ISO_A4_(210.00_x_297.00_MM)").unwrap()),
|
||||
PlotDevice::None.margins_mm(
|
||||
&paper_catalog::resolve("ISO_A4_(210.00_x_297.00_MM)").unwrap(),
|
||||
&[]
|
||||
),
|
||||
Margins::ZERO
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_defined_sheets_bring_their_own_margins() {
|
||||
let roll = CustomPaper {
|
||||
name: "Roll".into(),
|
||||
width: 900.0,
|
||||
height: 1200.0,
|
||||
units: PaperUnits::Millimeters,
|
||||
margins: Margins::uniform(3.0),
|
||||
};
|
||||
let paper = roll.paper();
|
||||
assert_eq!(
|
||||
PlotDevice::Pdf.margins_mm(&paper, &[roll.clone()]),
|
||||
Margins::uniform(3.0)
|
||||
);
|
||||
assert_eq!(
|
||||
PlotDevice::None.margins_mm(&paper, &[roll.clone()]),
|
||||
Margins::uniform(3.0)
|
||||
);
|
||||
// An unknown printer falls back to the user's margins too.
|
||||
assert_eq!(
|
||||
PlotDevice::Printer("Nowhere".into()).margins_mm(&paper, &[roll]),
|
||||
Margins::uniform(3.0)
|
||||
);
|
||||
// Without a definition a custom-looking sheet gets the family set.
|
||||
assert_eq!(
|
||||
PlotDevice::Pdf.margins_mm(&paper, &[]),
|
||||
Margins {
|
||||
left: 5.0,
|
||||
bottom: 17.0,
|
||||
right: 6.0,
|
||||
top: 18.0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipp_media_parses_sizes_margins_borderless_and_default() {
|
||||
let caps = parse_ipp_media(IPP_SAMPLE).expect("media parsed");
|
||||
|
||||
@@ -1457,6 +1457,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Layout name cannot be empty." => Some(("common", "layout-name-cannot-be-empty")),
|
||||
"Layout plots the current sheet using the selected paper size." => Some(("plot", "layout-plots-the-current-sheet-using-the-selec")),
|
||||
"Sheet sizes and printable areas reported by the printer." => Some(("plot", "sheet-sizes-and-printable-areas-reported-by-th")),
|
||||
"Custom paper size" => Some(("plot", "custom-paper-size")),
|
||||
"Printable margins" => Some(("plot", "printable-margins")),
|
||||
"Enter a positive width and height." => Some(("plot", "enter-a-positive-width-and-height")),
|
||||
"Layout renamed: '{old_name}' → '{new_name}'" => Some(("common", "layout-renamed-old-name-new-name")),
|
||||
"Layouts" => Some(("layout", "layouts")),
|
||||
"Leader" => Some(("common", "leader")),
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//! Form building blocks shared by the in-canvas dialogs.
|
||||
//!
|
||||
//! The Plot dialog grew its own `label : control` rows, field and button
|
||||
//! styles, and section dividers; every dialog that followed copied them. They
|
||||
//! live here once so a new dialog reads like the existing ones and a theme
|
||||
//! tweak lands everywhere.
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::ui::style::common::muted_style;
|
||||
use iced::widget::{button, container, pick_list, row, text, text_input, Space};
|
||||
use iced::{Background, Border, Element, Length, Theme};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Width of the label column in a `label : control` row.
|
||||
pub const LABEL_WIDTH: f32 = 92.0;
|
||||
|
||||
/// Dialog button: accent (primary action) or neutral.
|
||||
pub fn button_style(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
|
||||
move |theme: &Theme, status| {
|
||||
let palette = theme.palette();
|
||||
let pair = match (accent, status) {
|
||||
(true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
|
||||
(false, button::Status::Hovered | button::Status::Pressed) => palette.background.strong,
|
||||
(true, _) => palette.primary.base,
|
||||
_ => palette.background.weak,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Text field: base background, neutral border that turns primary on focus.
|
||||
pub fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
|
||||
let palette = theme.palette();
|
||||
let border = match status {
|
||||
text_input::Status::Focused { .. } => palette.primary.base.color,
|
||||
_ => palette.background.neutral.color,
|
||||
};
|
||||
text_input::Style {
|
||||
background: Background::Color(palette.background.base.color),
|
||||
border: Border {
|
||||
color: border,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
icon: palette.background.base.text,
|
||||
placeholder: palette.background.base.text.scale_alpha(0.48),
|
||||
value: palette.background.base.text,
|
||||
selection: palette.primary.base.color.scale_alpha(0.5),
|
||||
}
|
||||
}
|
||||
|
||||
/// One-pixel horizontal rule.
|
||||
pub fn hdivider<'a>(width: Length) -> Element<'a, Message> {
|
||||
container(Space::new().width(width).height(1))
|
||||
.width(width)
|
||||
.height(1)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.neutral.color)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// One-pixel vertical rule.
|
||||
pub fn vseparator<'a>(height: Length) -> Element<'a, Message> {
|
||||
container(Space::new().width(1).height(height))
|
||||
.width(1)
|
||||
.height(height)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.neutral.color)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Muted section heading.
|
||||
pub fn section_label<'a>(label: Cow<'static, str>) -> Element<'a, Message> {
|
||||
text(label).size(11).style(muted_style).into()
|
||||
}
|
||||
|
||||
fn row_label<'a>(label: Cow<'static, str>) -> Element<'a, Message> {
|
||||
text(label)
|
||||
.size(11)
|
||||
.style(muted_style)
|
||||
.width(LABEL_WIDTH)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A `label : dropdown` row.
|
||||
pub fn labeled_pick_list<'a, T>(
|
||||
label: Cow<'static, str>,
|
||||
options: Vec<T>,
|
||||
selected: Option<T>,
|
||||
on_select: impl Fn(T) -> Message + 'a,
|
||||
width: Length,
|
||||
) -> Element<'a, Message>
|
||||
where
|
||||
T: ToString + Clone + PartialEq + 'a,
|
||||
{
|
||||
let list = pick_list(selected, options, |value| value.to_string())
|
||||
.on_select(on_select)
|
||||
.text_size(12)
|
||||
.padding([3, 6])
|
||||
.width(width);
|
||||
row![row_label(label), list]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A `label : dropdown` row that renders read-only when `enabled` is false.
|
||||
pub fn labeled_pick_list_enabled<'a, T>(
|
||||
label: Cow<'static, str>,
|
||||
options: Vec<T>,
|
||||
selected: Option<T>,
|
||||
on_select: impl Fn(T) -> Message + 'a,
|
||||
width: Length,
|
||||
enabled: bool,
|
||||
) -> Element<'a, Message>
|
||||
where
|
||||
T: ToString + Clone + PartialEq + 'a,
|
||||
{
|
||||
if enabled {
|
||||
return labeled_pick_list(label, options, selected, on_select, width);
|
||||
}
|
||||
row![
|
||||
row_label(label),
|
||||
crate::ui::read_only::field(
|
||||
selected
|
||||
.map(|choice| choice.to_string())
|
||||
.unwrap_or_default()
|
||||
.as_str(),
|
||||
12.0,
|
||||
width,
|
||||
),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A `label : text field` row.
|
||||
pub fn labeled_field<'a>(
|
||||
label: Cow<'static, str>,
|
||||
value: &'a str,
|
||||
on_input: impl Fn(String) -> Message + 'a,
|
||||
width: f32,
|
||||
) -> Element<'a, Message> {
|
||||
row![
|
||||
row_label(label),
|
||||
text_input("", value)
|
||||
.on_input(on_input)
|
||||
.style(field_style)
|
||||
.size(12)
|
||||
.width(width),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A `label : text field` row that renders read-only when `enabled` is false.
|
||||
pub fn labeled_field_enabled<'a>(
|
||||
label: Cow<'static, str>,
|
||||
value: &'a str,
|
||||
on_input: impl Fn(String) -> Message + 'a,
|
||||
width: f32,
|
||||
enabled: bool,
|
||||
) -> Element<'a, Message> {
|
||||
if enabled {
|
||||
return labeled_field(label, value, on_input, width);
|
||||
}
|
||||
row![
|
||||
row_label(label),
|
||||
crate::ui::read_only::field(value, 12.0, Length::Fixed(width)),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A short unlabeled text field for a compact multi-value row (`L B R T`).
|
||||
pub fn compact_field<'a>(
|
||||
placeholder: &'a str,
|
||||
value: &'a str,
|
||||
on_input: impl Fn(String) -> Message + 'a,
|
||||
width: f32,
|
||||
) -> Element<'a, Message> {
|
||||
text_input(placeholder, value)
|
||||
.on_input(on_input)
|
||||
.style(field_style)
|
||||
.size(12)
|
||||
.padding([3, 6])
|
||||
.width(width)
|
||||
.into()
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub mod scale_manager;
|
||||
pub mod anno_object_scale;
|
||||
pub mod style_manager;
|
||||
pub mod common;
|
||||
pub mod form;
|
||||
|
||||
#[cfg(test)]
|
||||
mod common_tests {
|
||||
|
||||
+309
-115
@@ -5,8 +5,10 @@
|
||||
//! PDF. Styled to match the other OCS dialogs (dark pills + fields).
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::io::paper_catalog::{self, PaperSize};
|
||||
use crate::io::paper_catalog::{self, CustomPaper, Margins, PaperSize, PaperUnits};
|
||||
use crate::io::plot_device::PrinterCapabilities;
|
||||
use crate::ui::style::common::muted_style;
|
||||
use crate::ui::style::form;
|
||||
use iced::widget::{
|
||||
button, checkbox, column, container, mouse_area, row, scrollable, text, text_input,
|
||||
Space,
|
||||
@@ -75,7 +77,45 @@ impl fmt::Display for PlotChoice {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PlotChoice;
|
||||
use super::{CustomPaperDraft, CustomPaperError, PlotChoice};
|
||||
use crate::io::paper_catalog::{resolve, Margins, PaperUnits};
|
||||
|
||||
#[test]
|
||||
fn custom_sheet_draft_validates_and_builds() {
|
||||
let mut draft = CustomPaperDraft {
|
||||
name: " Roll 24 ".into(),
|
||||
width: "1500".into(),
|
||||
height: "609,6".into(),
|
||||
inches: false,
|
||||
margins: ["5".into(), "17".into(), "".into(), "abc".into()],
|
||||
error: None,
|
||||
};
|
||||
let custom = draft.build().expect("valid sheet");
|
||||
assert_eq!(custom.name, "Roll 24");
|
||||
assert_eq!((custom.width, custom.height), (1500.0, 609.6));
|
||||
assert_eq!(custom.units, PaperUnits::Millimeters);
|
||||
// Blank or unparsable margins mean no margin, never a refusal.
|
||||
assert_eq!(custom.margins, Margins { left: 5.0, bottom: 17.0, right: 0.0, top: 0.0 });
|
||||
assert_eq!(custom.canonical(), "Roll_24_(609.60_x_1500.00_MM)");
|
||||
draft.width = "0".into();
|
||||
assert_eq!(draft.build().unwrap_err(), CustomPaperError::Size);
|
||||
draft.width = "x".into();
|
||||
assert_eq!(draft.build().unwrap_err(), CustomPaperError::Size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_sheet_draft_seeds_from_a_sheet_in_its_own_unit() {
|
||||
let arch_d = resolve("ARCH_D_(24.00_x_36.00_Inches)").unwrap();
|
||||
let draft = CustomPaperDraft::from_paper(&arch_d, Margins::uniform(6.35));
|
||||
assert_eq!(draft.name, "ARCH D");
|
||||
assert_eq!((draft.width.as_str(), draft.height.as_str()), ("24", "36"));
|
||||
assert!(draft.inches);
|
||||
assert_eq!(draft.margins, ["0.25", "0.25", "0.25", "0.25"]);
|
||||
let a4 = resolve("ISO_A4_(210.00_x_297.00_MM)").unwrap();
|
||||
let draft = CustomPaperDraft::from_paper(&a4, Margins { left: 5.0, bottom: 17.0, right: 6.0, top: 18.0 });
|
||||
assert!(!draft.inches);
|
||||
assert_eq!(draft.margins, ["5", "17", "6", "18"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_view_display_preserves_the_saved_plot_area() {
|
||||
@@ -119,6 +159,8 @@ pub enum PlotDlgMsg {
|
||||
/// the user has since left is ignored.
|
||||
PrinterMedia(String, Option<std::sync::Arc<PrinterCapabilities>>),
|
||||
Paper(String),
|
||||
/// Edits of the inline "custom paper size" editor under the Size row.
|
||||
CustomPaper(CustomPaperMsg),
|
||||
Orientation(String),
|
||||
Area(String),
|
||||
Scale(String),
|
||||
@@ -153,6 +195,121 @@ pub enum PlotDlgMsg {
|
||||
NameCancel,
|
||||
}
|
||||
|
||||
/// Which side of the printable-margin row a value belongs to.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MarginSide {
|
||||
Left,
|
||||
Bottom,
|
||||
Right,
|
||||
Top,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CustomPaperMsg {
|
||||
/// Open the editor, seeded from the selected sheet.
|
||||
Open,
|
||||
Cancel,
|
||||
Name(String),
|
||||
Width(String),
|
||||
Height(String),
|
||||
/// Units picked from the dropdown, by their catalogue label.
|
||||
Units(String),
|
||||
Margin(MarginSide, String),
|
||||
/// Add (or replace) the sheet and select it.
|
||||
Add,
|
||||
/// Remove the selected user-defined sheet.
|
||||
Remove,
|
||||
}
|
||||
|
||||
/// The inline custom-sheet editor's fields, kept as typed text so a half-typed
|
||||
/// number does not snap to a value while editing. Everything is in the
|
||||
/// draft's own units.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct CustomPaperDraft {
|
||||
pub name: String,
|
||||
pub width: String,
|
||||
pub height: String,
|
||||
pub inches: bool,
|
||||
/// Left, bottom, right, top.
|
||||
pub margins: [String; 4],
|
||||
/// Why the last Add was refused, shown under the fields.
|
||||
pub error: Option<CustomPaperError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CustomPaperError {
|
||||
/// Width or height is not a positive number.
|
||||
Size,
|
||||
}
|
||||
|
||||
impl CustomPaperDraft {
|
||||
fn units(&self) -> PaperUnits {
|
||||
if self.inches {
|
||||
PaperUnits::Inches
|
||||
} else {
|
||||
PaperUnits::Millimeters
|
||||
}
|
||||
}
|
||||
|
||||
/// Seed the editor from a sheet: its dimensions and unit, and the margins
|
||||
/// given (in millimetres) converted into that unit.
|
||||
pub fn from_paper(paper: &PaperSize, margins_mm: Margins) -> Self {
|
||||
let units = paper.units;
|
||||
let fmt = |v: f64| {
|
||||
let v = units.from_mm(v);
|
||||
if (v - v.round()).abs() < 1e-6 {
|
||||
format!("{}", v.round() as i64)
|
||||
} else {
|
||||
format!("{v:.2}")
|
||||
}
|
||||
};
|
||||
let dim = |v: f64| {
|
||||
if (v - v.round()).abs() < 1e-6 {
|
||||
format!("{}", v.round() as i64)
|
||||
} else {
|
||||
format!("{v:.2}")
|
||||
}
|
||||
};
|
||||
Self {
|
||||
name: paper.label.to_string(),
|
||||
width: dim(paper.width),
|
||||
height: dim(paper.height),
|
||||
inches: units == PaperUnits::Inches,
|
||||
margins: [
|
||||
fmt(margins_mm.left),
|
||||
fmt(margins_mm.bottom),
|
||||
fmt(margins_mm.right),
|
||||
fmt(margins_mm.top),
|
||||
],
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The sheet the fields describe, or the reason they do not describe one.
|
||||
pub fn build(&self) -> Result<CustomPaper, CustomPaperError> {
|
||||
let number = |text: &str| text.trim().replace(',', ".").parse::<f64>().ok();
|
||||
let (Some(width), Some(height)) = (number(&self.width), number(&self.height)) else {
|
||||
return Err(CustomPaperError::Size);
|
||||
};
|
||||
if !(width > 0.0) || !(height > 0.0) || !width.is_finite() || !height.is_finite() {
|
||||
return Err(CustomPaperError::Size);
|
||||
}
|
||||
let margin = |text: &str| number(text).filter(|v| v.is_finite() && *v >= 0.0).unwrap_or(0.0);
|
||||
Ok(CustomPaper {
|
||||
name: self.name.trim().to_string(),
|
||||
width,
|
||||
height,
|
||||
units: self.units(),
|
||||
margins: Margins {
|
||||
left: margin(&self.margins[0]),
|
||||
bottom: margin(&self.margins[1]),
|
||||
right: margin(&self.margins[2]),
|
||||
top: margin(&self.margins[3]),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Transient state backing the Plot dialog. Seeded from the layout's plot
|
||||
/// settings when the dialog opens; consumed on commit.
|
||||
// The persisted fields form the "plot" section of the app config
|
||||
@@ -173,6 +330,12 @@ pub struct PlotDialogState {
|
||||
/// while unknown, when the platform cannot ask, or for PDF output.
|
||||
#[serde(skip)]
|
||||
pub printer_media: Option<std::sync::Arc<PrinterCapabilities>>,
|
||||
/// Sheets the user defined, offered for every device and remembered
|
||||
/// across sessions.
|
||||
pub custom_papers: Vec<CustomPaper>,
|
||||
/// The inline custom-sheet editor while it is open.
|
||||
#[serde(skip)]
|
||||
pub custom_editor: Option<CustomPaperDraft>,
|
||||
/// Chosen printer name, or `None` for the system default.
|
||||
pub printer: Option<String>,
|
||||
/// Output goes to a PDF file instead of a printer.
|
||||
@@ -238,6 +401,8 @@ impl Default for PlotDialogState {
|
||||
printers: Vec::new(),
|
||||
default_printer: None,
|
||||
printer_media: None,
|
||||
custom_papers: Vec::new(),
|
||||
custom_editor: None,
|
||||
printer: None,
|
||||
to_file: false,
|
||||
paper: paper_catalog::default_paper().canonical.to_string(),
|
||||
@@ -318,80 +483,19 @@ fn legacy_fit_to_paper_default() -> bool {
|
||||
}
|
||||
|
||||
fn btn(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
|
||||
move |theme: &Theme, st| {
|
||||
let palette = theme.palette();
|
||||
let pair = match (accent, st) {
|
||||
(true, button::Status::Hovered | button::Status::Pressed) => palette.primary.strong,
|
||||
(false, button::Status::Hovered | button::Status::Pressed) => {
|
||||
palette.background.strong
|
||||
}
|
||||
(true, _) => palette.primary.base,
|
||||
_ => palette.background.weak,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
|
||||
let palette = theme.palette();
|
||||
let border = match status {
|
||||
text_input::Status::Focused { .. } => palette.primary.base.color,
|
||||
_ => palette.background.neutral.color,
|
||||
};
|
||||
text_input::Style {
|
||||
background: Background::Color(palette.background.base.color),
|
||||
border: Border { color: border, width: 1.0, radius: 3.0.into() },
|
||||
icon: palette.background.base.text,
|
||||
placeholder: palette.background.base.text.scale_alpha(0.48),
|
||||
value: palette.background.base.text,
|
||||
selection: palette.primary.base.color.scale_alpha(0.5),
|
||||
}
|
||||
}
|
||||
|
||||
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
|
||||
iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
|
||||
}
|
||||
form::button_style(accent)
|
||||
}
|
||||
|
||||
fn hdivider<'a>(width: Length) -> Element<'a, Message> {
|
||||
container(Space::new().width(width).height(1))
|
||||
.width(width)
|
||||
.height(1)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme.palette().background.neutral.color
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
form::hdivider(width)
|
||||
}
|
||||
|
||||
fn section_label<'a>(s: Cow<'static, str>) -> Element<'a, Message> {
|
||||
text(s).size(11).style(muted_style).into()
|
||||
form::section_label(s)
|
||||
}
|
||||
|
||||
fn vsep<'a>(height: Length) -> Element<'a, Message> {
|
||||
container(Space::new().width(1).height(height))
|
||||
.width(1)
|
||||
.height(height)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme.palette().background.neutral.color
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
form::vseparator(height)
|
||||
}
|
||||
|
||||
fn setup_row<'a>(
|
||||
@@ -404,7 +508,7 @@ fn setup_row<'a>(
|
||||
return text_input("", rename_buf)
|
||||
.on_input(|value| Message::PlotDlg(PlotDlgMsg::NameInput(value)))
|
||||
.on_submit(Message::PlotDlg(PlotDlgMsg::NameCommit))
|
||||
.style(field_style)
|
||||
.style(form::field_style)
|
||||
.size(11)
|
||||
.padding([4, 8])
|
||||
.width(Fit)
|
||||
@@ -442,15 +546,7 @@ fn drop_row<'a>(
|
||||
ctor: fn(String) -> PlotDlgMsg,
|
||||
width: Length,
|
||||
) -> Element<'a, Message> {
|
||||
let pl = iced::widget::pick_list(selected, options, |value| value.to_string())
|
||||
.on_select(move |choice| Message::PlotDlg(ctor(choice.raw)))
|
||||
.text_size(12)
|
||||
.padding([3, 6])
|
||||
.width(width);
|
||||
row![text(label).size(11).style(muted_style).width(92), pl]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
form::labeled_pick_list(label, options, selected, move |choice| Message::PlotDlg(ctor(choice.raw)), width)
|
||||
}
|
||||
|
||||
fn drop_row_enabled<'a>(
|
||||
@@ -461,20 +557,14 @@ fn drop_row_enabled<'a>(
|
||||
width: Length,
|
||||
enabled: bool,
|
||||
) -> Element<'a, Message> {
|
||||
if enabled {
|
||||
return drop_row(label, options, selected, ctor, width);
|
||||
}
|
||||
row![
|
||||
text(label).size(11).style(muted_style).width(92),
|
||||
crate::ui::read_only::field(
|
||||
selected.map(|choice| choice.to_string()).unwrap_or_default().as_str(),
|
||||
12.0,
|
||||
width,
|
||||
),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
form::labeled_pick_list_enabled(
|
||||
label,
|
||||
options,
|
||||
selected,
|
||||
move |choice| Message::PlotDlg(ctor(choice.raw)),
|
||||
width,
|
||||
enabled,
|
||||
)
|
||||
}
|
||||
|
||||
/// A `label : text field` row.
|
||||
@@ -484,17 +574,7 @@ fn field_row<'a>(
|
||||
ctor: fn(String) -> PlotDlgMsg,
|
||||
width: u16,
|
||||
) -> Element<'a, Message> {
|
||||
row![
|
||||
text(label).size(11).style(muted_style).width(92),
|
||||
text_input("", value)
|
||||
.on_input(move |s| Message::PlotDlg(ctor(s)))
|
||||
.style(field_style)
|
||||
.size(12)
|
||||
.width(width as f32),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
form::labeled_field(label, value, move |s| Message::PlotDlg(ctor(s)), width as f32)
|
||||
}
|
||||
|
||||
fn field_row_enabled<'a>(
|
||||
@@ -504,19 +584,9 @@ fn field_row_enabled<'a>(
|
||||
width: u16,
|
||||
enabled: bool,
|
||||
) -> Element<'a, Message> {
|
||||
if enabled {
|
||||
return field_row(label, value, ctor, width);
|
||||
}
|
||||
row![
|
||||
text(label).size(11).style(muted_style).width(92),
|
||||
crate::ui::read_only::field(value, 12.0, Length::Fixed(width as f32)),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
form::labeled_field_enabled(label, value, move |s| Message::PlotDlg(ctor(s)), width as f32, enabled)
|
||||
}
|
||||
|
||||
/// A single option checkbox bound to a `PlotFlag`.
|
||||
fn check<'a>(label: Cow<'static, str>, on: bool, flag: PlotFlag) -> Element<'a, Message> {
|
||||
checkbox(on)
|
||||
.label(label)
|
||||
@@ -553,6 +623,92 @@ fn choices(items: &[&str]) -> Vec<PlotChoice> {
|
||||
items.iter().map(|&value| PlotChoice::localized(value)).collect()
|
||||
}
|
||||
|
||||
/// The inline editor for a user-defined sheet: name, dimensions and unit,
|
||||
/// printable margins, Add / Cancel. Numbers stay text until Add validates
|
||||
/// them.
|
||||
fn custom_paper_editor<'a>(draft: &'a CustomPaperDraft, width: Length) -> Element<'a, Message> {
|
||||
let msg = |m: CustomPaperMsg| Message::PlotDlg(PlotDlgMsg::CustomPaper(m));
|
||||
let units_label = |inches: bool| {
|
||||
if inches {
|
||||
crate::i18n::translate("Inches")
|
||||
} else {
|
||||
crate::i18n::translate("Millimeters")
|
||||
}
|
||||
};
|
||||
let unit_options: Vec<PlotChoice> = vec![
|
||||
PlotChoice::localized("Millimeters"),
|
||||
PlotChoice::localized("Inches"),
|
||||
];
|
||||
let unit_selected = Some(PlotChoice::localized(if draft.inches { "Inches" } else { "Millimeters" }));
|
||||
let margin_field = |side: MarginSide, index: usize, hint: &'a str| {
|
||||
form::compact_field(hint, &draft.margins[index], move |v| msg(CustomPaperMsg::Margin(side, v)), 56.0)
|
||||
};
|
||||
let error: Element<'_, Message> = match draft.error {
|
||||
Some(CustomPaperError::Size) => text(t!("Enter a positive width and height."))
|
||||
.size(10)
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().danger.base.color),
|
||||
})
|
||||
.into(),
|
||||
None => Space::new().height(0).into(),
|
||||
};
|
||||
let unit_hint = text(units_label(draft.inches)).size(10).style(muted_style);
|
||||
container(
|
||||
column![
|
||||
section_label(t!("Custom paper size")),
|
||||
form::labeled_field(t!("Name"), &draft.name, move |v| msg(CustomPaperMsg::Name(v)), 220.0),
|
||||
row![
|
||||
form::labeled_field(t!("Width"), &draft.width, move |v| msg(CustomPaperMsg::Width(v)), 70.0),
|
||||
form::labeled_field(t!("Height"), &draft.height, move |v| msg(CustomPaperMsg::Height(v)), 70.0),
|
||||
]
|
||||
.spacing(12),
|
||||
form::labeled_pick_list(
|
||||
t!("Units"),
|
||||
unit_options,
|
||||
unit_selected,
|
||||
move |choice| msg(CustomPaperMsg::Units(choice.raw)),
|
||||
Length::Fixed(140.0),
|
||||
),
|
||||
row![
|
||||
text(t!("Printable margins")).size(11).style(muted_style).width(form::LABEL_WIDTH),
|
||||
margin_field(MarginSide::Left, 0, "L"),
|
||||
margin_field(MarginSide::Bottom, 1, "B"),
|
||||
margin_field(MarginSide::Right, 2, "R"),
|
||||
margin_field(MarginSide::Top, 3, "T"),
|
||||
unit_hint,
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
error,
|
||||
row![
|
||||
Space::new().width(Length::Fill),
|
||||
button(text(t!("Cancel")).size(11))
|
||||
.on_press(msg(CustomPaperMsg::Cancel))
|
||||
.style(btn(false))
|
||||
.padding([4, 10]),
|
||||
button(text(t!("Add")).size(11))
|
||||
.on_press(msg(CustomPaperMsg::Add))
|
||||
.style(btn(true))
|
||||
.padding([4, 12]),
|
||||
]
|
||||
.spacing(6),
|
||||
]
|
||||
.spacing(6),
|
||||
)
|
||||
.padding(8)
|
||||
.width(width)
|
||||
.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()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn view_window(
|
||||
s: &PlotDialogState,
|
||||
print_all_options: bool,
|
||||
@@ -599,7 +755,7 @@ pub fn view_window(
|
||||
text_input("", rename_buf)
|
||||
.on_input(|value| Message::PlotDlg(PlotDlgMsg::NameInput(value)))
|
||||
.on_submit(Message::PlotDlg(PlotDlgMsg::NameCommit))
|
||||
.style(field_style)
|
||||
.style(form::field_style)
|
||||
.size(11)
|
||||
.padding([4, 8])
|
||||
.width(Length::Fill),
|
||||
@@ -708,10 +864,21 @@ pub fn view_window(
|
||||
Some(caps) => caps.media.iter().map(|media| PlotChoice::paper(&media.paper)).collect(),
|
||||
None => paper_catalog::catalog().iter().map(PlotChoice::paper).collect(),
|
||||
};
|
||||
// User-defined sheets come last, after whatever the device offers.
|
||||
for custom in &s.custom_papers {
|
||||
let choice = PlotChoice::paper(&custom.paper());
|
||||
if !paper_opts.contains(&choice) {
|
||||
paper_opts.push(choice);
|
||||
}
|
||||
}
|
||||
let paper_sel = PlotChoice::paper(&selected_paper);
|
||||
if !paper_opts.contains(&paper_sel) {
|
||||
paper_opts.insert(0, paper_sel.clone());
|
||||
}
|
||||
let selected_is_custom = s
|
||||
.custom_papers
|
||||
.iter()
|
||||
.any(|custom| custom.canonical() == selected_paper.canonical);
|
||||
let paper_source_note: Element<'_, Message> = if printer_sheets.is_some() {
|
||||
text(t!("Sheet sizes and printable areas reported by the printer."))
|
||||
.size(10)
|
||||
@@ -768,9 +935,36 @@ pub fn view_window(
|
||||
);
|
||||
|
||||
// ── Paper, area, offset, scale ────────────────────────────────────────
|
||||
// Size row with the custom-sheet controls; the editor unfolds below it.
|
||||
let mut size_row = row![
|
||||
drop_row(t!("Size"), paper_opts, Some(paper_sel), PlotDlgMsg::Paper, Length::Fill),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center);
|
||||
if s.custom_editor.is_none() {
|
||||
size_row = size_row.push(
|
||||
button(text(t!("Custom…")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::CustomPaper(CustomPaperMsg::Open)))
|
||||
.style(btn(false))
|
||||
.padding([4, 8]),
|
||||
);
|
||||
if selected_is_custom {
|
||||
size_row = size_row.push(
|
||||
button(text(t!("Remove")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::CustomPaper(CustomPaperMsg::Remove)))
|
||||
.style(btn(false))
|
||||
.padding([4, 8]),
|
||||
);
|
||||
}
|
||||
}
|
||||
let custom_editor: Element<'_, Message> = match &s.custom_editor {
|
||||
Some(draft) => custom_paper_editor(draft, width),
|
||||
None => Space::new().height(0).into(),
|
||||
};
|
||||
let paper_panel = panel(column![
|
||||
section_label(t!("Paper")),
|
||||
drop_row(t!("Size"), paper_opts, Some(paper_sel), PlotDlgMsg::Paper, width),
|
||||
size_row,
|
||||
custom_editor,
|
||||
paper_source_note,
|
||||
paper_note,
|
||||
].spacing(7));
|
||||
|
||||
Reference in new issue
Block a user