forked from 1b-agent/opencadstudio-upstream
feat(plot): printer driver properties and a roomier plot dialog
Properties… next to the printer used to open the CUPS web interface in a browser. It now edits the driver options of the *selected* printer the way LibreOffice's Properties… does, without leaving the plot dialog: - CUPS platforms (Linux, any desktop; macOS): an in-line editor lists the printer's PPD options as `lpoptions -l` reports them — media type, colour mode, quality, duplex, trays, scaling — with readable choice labels, Reset / Cancel / Apply. Apply keeps only the choices that differ from the driver defaults, per printer, in the plot config, and every job for that printer passes them as `lp -o key=value`. Where CUPS cannot answer, the desktop's printer settings panel opens instead of a browser. - Windows: the driver's document-properties sheet (`DocumentPropertiesW` with prompt, owned by the main window) and, on OK, the result stored as the user's printing preferences for that printer (`SetPrinter` level 9), which the job honours. `printui /e` (printing preferences) replaces the device-properties sheet as the fallback. The desktop print portal was tried first and dropped: GTK 4's print dialog loses the preselected printer when the default queue is listed before it, so the dialog always opened on the wrong printer. Dialog layout: the modal grows to 940×620; Printer / plotter and Paper size share a line with their pickers (long queue and media names now fit); Copies + Properties… and Custom… sit beneath; the custom-sheet editor labels each margin (Left / Right, Top / Bottom) and shows the unit. Also recognises `none_device`, AutoCAD's stored spelling for "no plotter", and writes it back unchanged. Tests: `lpoptions` parsing and keyword humanising, the editor flow (load, stale answer ignored, set, apply, remember, reset, error, cancel), device name round-trips including `none_device`. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
5e271e2dbb
commit
737666b894
29 files changed
+1000
-103
No files matched your search
+1
-1
@@ -63,7 +63,7 @@ naga = { version = "27", features = ["wgsl-in"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
libloading = "0.8"
|
||||
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_UI_Input", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_Security", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing"] }
|
||||
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_UI_Input", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_Security", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing", "Win32_Graphics_Gdi"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] }
|
||||
|
||||
@@ -5029,6 +5029,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = دمج الخطوط المتداخلة
|
||||
.object-lineweights = سماكات خطوط الكائنات
|
||||
.orientation = الاتجاه
|
||||
|
||||
@@ -5017,6 +5017,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Обедини припокриващи се линии
|
||||
.object-lineweights = Дебелини на линии на обект
|
||||
.orientation = Ориентация
|
||||
|
||||
@@ -5014,6 +5014,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Sloučit překrývající se čáry
|
||||
.object-lineweights = Tloušťky čar objektů
|
||||
.orientation = Orientace
|
||||
|
||||
@@ -5015,6 +5015,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Zusammenführung überlappender Leitungen
|
||||
.object-lineweights = Objektliniengewichte
|
||||
.orientation = Ausrichtung
|
||||
|
||||
@@ -5017,6 +5017,15 @@ plot =
|
||||
.custom-paper-size = Προσαρμοσμένο μέγεθος χαρτιού
|
||||
.printable-margins = Εκτυπώσιμα περιθώρια
|
||||
.enter-a-positive-width-and-height = Δώστε θετικό πλάτος και ύψος.
|
||||
.paper-size = Μέγεθος χαρτιού
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ επιλογή(-ές) driver ορισμένες για αυτόν τον εκτυπωτή.
|
||||
.printer-properties-printer = Ιδιότητες εκτυπωτή — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = Ο εκτυπωτής δεν αναφέρει επιλογές driver.
|
||||
.could-not-read-the-printer-s-options-error = Αδυναμία ανάγνωσης των επιλογών του εκτυπωτή: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Ανάγνωση των επιλογών του εκτυπωτή…
|
||||
.printing-preferences-saved-for-printer = Οι προτιμήσεις εκτύπωσης αποθηκεύτηκαν για τον εκτυπωτή __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Οι προτιμήσεις εκτύπωσης δεν άλλαξαν.
|
||||
.printer-properties = Ιδιότητες εκτυπωτή
|
||||
.merge-overlapping-lines = Συγχώνευση επικαλυπτόμενων γραμμών
|
||||
.object-lineweights = Πάχη γραμμών αντικειμένων
|
||||
.orientation = Προσανατολισμός
|
||||
|
||||
@@ -5018,6 +5018,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Merge overlapping lines
|
||||
.object-lineweights = Object lineweights
|
||||
.orientation = Orientation
|
||||
|
||||
@@ -5017,6 +5017,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Fusionar líneas solapadas
|
||||
.object-lineweights = Grosores de línea de objetos
|
||||
.orientation = Orientación
|
||||
|
||||
@@ -5016,6 +5016,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Yhdistä päällekkäiset rivit
|
||||
.object-lineweights = Objektin viivan painot
|
||||
.orientation = Suuntautuminen
|
||||
|
||||
@@ -5017,6 +5017,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Fusionner les lignes qui se chevauchent
|
||||
.object-lineweights = Poids de ligne d'objet
|
||||
.orientation = Orientation
|
||||
|
||||
@@ -5005,6 +5005,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = मर्ज ओवरलैपिंग लाइन
|
||||
.object-lineweights = ऑब्जेक्ट लाइनवेट
|
||||
.orientation = अभिविन्यास
|
||||
|
||||
@@ -5016,6 +5016,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Egyesítse az átfedő vonalakat
|
||||
.object-lineweights = Objektumvonalvastagságok
|
||||
.orientation = Tájékozódás
|
||||
|
||||
@@ -5016,6 +5016,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Unisci linee sovrapposte
|
||||
.object-lineweights = Spessori linea degli oggetti
|
||||
.orientation = Orientamento
|
||||
|
||||
@@ -5005,6 +5005,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = 重複線を結合
|
||||
.object-lineweights = オブジェクトの線の太さ
|
||||
.orientation = 方向
|
||||
|
||||
@@ -5016,6 +5016,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = 겹치는 선 병합
|
||||
.object-lineweights = 객체 선가중치
|
||||
.orientation = 오리엔테이션
|
||||
|
||||
@@ -5015,6 +5015,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Overlaplijnen samenvoegen
|
||||
.object-lineweights = Objectlijndiktes
|
||||
.orientation = Oriëntatie
|
||||
|
||||
@@ -5016,6 +5016,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Połącz nakładające się linie
|
||||
.object-lineweights = Szerokości linii obiektu
|
||||
.orientation = Orientacja
|
||||
|
||||
@@ -5017,6 +5017,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Mesclar linhas sobrepostas
|
||||
.object-lineweights = Espessuras de linha dos objetos
|
||||
.orientation = Orientação
|
||||
|
||||
@@ -5018,6 +5018,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Слияние пересекающихся линий
|
||||
.object-lineweights = Весы для объектов
|
||||
.orientation = Ориентация
|
||||
|
||||
@@ -4957,6 +4957,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = Çakışan çizgileri birleştir
|
||||
.object-lineweights = Nesne çizgi kalınlıkları
|
||||
.orientation = Yönelim
|
||||
|
||||
@@ -5000,6 +5000,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = 合并重叠线
|
||||
.object-lineweights = 对象线宽
|
||||
.orientation = 方向
|
||||
|
||||
@@ -5012,6 +5012,15 @@ plot =
|
||||
.custom-paper-size = Custom paper size
|
||||
.printable-margins = Printable margins
|
||||
.enter-a-positive-width-and-height = Enter a positive width and height.
|
||||
.paper-size = Paper size
|
||||
.count-driver-option-s-set-for-this-printer = __ocs_fmt_0__ driver option(s) set for this printer.
|
||||
.printer-properties-printer = Printer properties — __ocs_fmt_0__
|
||||
.this-printer-reports-no-driver-options = This printer reports no driver options.
|
||||
.could-not-read-the-printer-s-options-error = Could not read the printer's options: __ocs_fmt_0__
|
||||
.reading-the-printer-s-options = Reading the printer's options…
|
||||
.printing-preferences-saved-for-printer = Printing preferences saved for __ocs_fmt_0__.
|
||||
.printing-preferences-unchanged = Printing preferences unchanged.
|
||||
.printer-properties = Printer properties
|
||||
.merge-overlapping-lines = 合併重疊線
|
||||
.object-lineweights = 物件線寬
|
||||
.orientation = 方向
|
||||
|
||||
+219
-8
@@ -3935,6 +3935,85 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties… for the selected printer.
|
||||
///
|
||||
/// On CUPS platforms (Linux, macOS) the driver's options are listed in an
|
||||
/// in-line editor and applied to every job for that printer through
|
||||
/// `lp -o`. On Windows the driver's own document-properties sheet is
|
||||
/// shown and its result stored as the printer's preferences, which the
|
||||
/// print job honours. Without a named printer, or where the options
|
||||
/// cannot be listed, the platform's printer settings open instead.
|
||||
fn on_printer_properties(&mut self) -> Task<Message> {
|
||||
let Some(printer) = self.plot_dialog.printer.clone().filter(|_| !self.plot_dialog.to_file)
|
||||
else {
|
||||
self.open_printer_settings_fallback();
|
||||
return Task::none();
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
return self.edit_windows_printer_preferences(printer);
|
||||
}
|
||||
#[allow(unreachable_code)]
|
||||
{
|
||||
self.plot_dialog.printer_editor = Some(crate::ui::window::plot::PrinterOptionsDraft {
|
||||
printer: printer.clone(),
|
||||
options: None,
|
||||
choices: Default::default(),
|
||||
error: None,
|
||||
});
|
||||
let name = printer.clone();
|
||||
background_task(
|
||||
move || crate::io::print_to_printer::printer_options(&printer),
|
||||
move |result| {
|
||||
Message::PlotDlg(crate::ui::window::plot::PlotDlgMsg::PrinterOptionsLoaded(
|
||||
name, result,
|
||||
))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The driver's document-properties sheet, owned by the main window so
|
||||
/// it sits on top of the plot dialog.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn edit_windows_printer_preferences(&mut self, printer: String) -> Task<Message> {
|
||||
let Some(window) = self.main_window else {
|
||||
self.open_printer_settings_fallback();
|
||||
return Task::none();
|
||||
};
|
||||
iced::window::run(window, move |w| {
|
||||
use iced::window::raw_window_handle::RawWindowHandle;
|
||||
let owner = match w.window_handle().ok().map(|h| h.as_raw()) {
|
||||
Some(RawWindowHandle::Win32(handle)) => handle.hwnd.get(),
|
||||
_ => 0,
|
||||
};
|
||||
match crate::io::print_to_printer::edit_printer_preferences(&printer, owner) {
|
||||
Ok(true) => Ok(crate::tf!("Printing preferences saved for {printer}.").into_owned()),
|
||||
Ok(false) => Ok(crate::t!("Printing preferences unchanged.").into_owned()),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
})
|
||||
.then(|result| {
|
||||
Task::done(Message::BackgroundIoFinished(
|
||||
result.map(|message| message),
|
||||
false,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// The platform's printer settings surface: a settings panel on Linux,
|
||||
/// the printers control panel on Windows, System Settings on macOS.
|
||||
fn open_printer_settings_fallback(&mut self) {
|
||||
match crate::io::print_to_printer::open_printer_properties(
|
||||
self.plot_dialog.printer.as_deref(),
|
||||
) {
|
||||
Ok(()) => self
|
||||
.command_line
|
||||
.push_info(crate::t!("Opened printer properties.").as_ref()),
|
||||
Err(error) => self.command_line.push_error(&error),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_common_plot_dialog(&mut self) {
|
||||
self.plot_dialog.center = true;
|
||||
self.plot_dialog.offset_x = "0.0".into();
|
||||
@@ -3957,6 +4036,9 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
M::Printer(s) => {
|
||||
// Driver choices belong to a printer; the editor closes when
|
||||
// the target changes.
|
||||
self.plot_dialog.printer_editor = None;
|
||||
if s == OUT_PDF {
|
||||
self.plot_dialog.to_file = true;
|
||||
} else if s == OUT_DEFAULT {
|
||||
@@ -3979,17 +4061,73 @@ impl OpenCADStudio {
|
||||
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(),
|
||||
) {
|
||||
Ok(()) => self
|
||||
.command_line
|
||||
.push_info(crate::t!("Opened printer properties.").as_ref()),
|
||||
Err(error) => self.command_line.push_error(&error),
|
||||
M::PrinterProperties => self.on_printer_properties(),
|
||||
M::PrinterOptionsLoaded(printer, result) => {
|
||||
if let Some(draft) = self.plot_dialog.printer_editor.as_mut() {
|
||||
if draft.printer == printer {
|
||||
match result {
|
||||
Ok(options) => {
|
||||
// Start from the remembered choices, then the
|
||||
// driver's defaults.
|
||||
let remembered = self
|
||||
.plot_dialog
|
||||
.driver_options
|
||||
.get(&printer)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let draft = self.plot_dialog.printer_editor.as_mut().unwrap();
|
||||
draft.choices = options
|
||||
.iter()
|
||||
.map(|option| {
|
||||
let choice = remembered
|
||||
.get(&option.key)
|
||||
.filter(|c| option.choices.iter().any(|x| &x.keyword == *c))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| option.default.clone());
|
||||
(option.key.clone(), choice)
|
||||
})
|
||||
.collect();
|
||||
draft.options = Some(options);
|
||||
}
|
||||
Err(error) => draft.error = Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
M::PrinterOptionSet(key, choice) => {
|
||||
if let Some(draft) = self.plot_dialog.printer_editor.as_mut() {
|
||||
draft.choices.insert(key, choice);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
M::PrinterOptionsReset => {
|
||||
if let Some(draft) = self.plot_dialog.printer_editor.as_mut() {
|
||||
if let Some(options) = &draft.options {
|
||||
draft.choices = options
|
||||
.iter()
|
||||
.map(|option| (option.key.clone(), option.default.clone()))
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
M::PrinterOptionsApply => {
|
||||
if let Some(draft) = self.plot_dialog.printer_editor.take() {
|
||||
let overrides = draft.overrides();
|
||||
if overrides.is_empty() {
|
||||
self.plot_dialog.driver_options.remove(&draft.printer);
|
||||
} else {
|
||||
self.plot_dialog.driver_options.insert(draft.printer.clone(), overrides);
|
||||
}
|
||||
self.save_config();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
M::PrinterOptionsCancel => {
|
||||
self.plot_dialog.printer_editor = None;
|
||||
Task::none()
|
||||
}
|
||||
M::Paper(s) => {
|
||||
self.plot_dialog.paper = s;
|
||||
let (w, h) = plot_dialog_sheet_mm(&self.plot_dialog);
|
||||
@@ -4720,6 +4858,12 @@ impl OpenCADStudio {
|
||||
printer: d.printer.clone(),
|
||||
copies: d.copies.trim().parse::<u32>().unwrap_or(1).max(1),
|
||||
quality: Some(d.quality.clone()),
|
||||
driver_options: d
|
||||
.printer
|
||||
.as_ref()
|
||||
.and_then(|printer| d.driver_options.get(printer))
|
||||
.map(|options| options.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5220,6 +5364,73 @@ mod plot_paper_tests {
|
||||
assert!(app.plot_dialog.custom_editor.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_options_are_edited_remembered_and_sent_with_the_job() {
|
||||
use crate::io::print_to_printer::parse_lpoptions;
|
||||
const LPOPTIONS: &str = "MediaType/Media Type: *Stationery Photographic\n\
|
||||
ColorModel/Print Color Mode: *RGB Gray\n\
|
||||
cupsPrintQuality/Print Quality: *Normal High\n";
|
||||
let mut app = app_with_printer_named_sheet();
|
||||
let _ = app.on_plot_dialog_open();
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Printer("OCS Test Printer".into()));
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterProperties);
|
||||
let draft = app.plot_dialog.printer_editor.as_ref().expect("editor opens");
|
||||
assert_eq!(draft.printer, "OCS Test Printer");
|
||||
assert!(draft.options.is_none(), "options load in the background");
|
||||
// An answer for another printer is ignored; the right one fills the editor.
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterOptionsLoaded(
|
||||
"Other".into(),
|
||||
Ok(parse_lpoptions(LPOPTIONS)),
|
||||
));
|
||||
assert!(app.plot_dialog.printer_editor.as_ref().unwrap().options.is_none());
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterOptionsLoaded(
|
||||
"OCS Test Printer".into(),
|
||||
Ok(parse_lpoptions(LPOPTIONS)),
|
||||
));
|
||||
let draft = app.plot_dialog.printer_editor.as_ref().unwrap();
|
||||
assert_eq!(draft.options.as_ref().map(|o| o.len()), Some(3));
|
||||
assert_eq!(draft.choices["ColorModel"], "RGB");
|
||||
assert!(draft.overrides().is_empty(), "defaults are not overrides");
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterOptionSet("ColorModel".into(), "Gray".into()));
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterOptionSet("cupsPrintQuality".into(), "High".into()));
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterOptionsApply);
|
||||
assert!(app.plot_dialog.printer_editor.is_none(), "Apply closes the editor");
|
||||
let remembered = &app.plot_dialog.driver_options["OCS Test Printer"];
|
||||
assert_eq!(remembered.len(), 2);
|
||||
assert_eq!(remembered["ColorModel"], "Gray");
|
||||
// The job carries exactly those overrides…
|
||||
let opts = app.plot_print_options(&app.plot_dialog);
|
||||
assert_eq!(
|
||||
opts.driver_options,
|
||||
vec![("ColorModel".to_string(), "Gray".to_string()), ("cupsPrintQuality".to_string(), "High".to_string())]
|
||||
);
|
||||
// …they survive in the config, and another printer gets none of them.
|
||||
assert_eq!(app.current_config().plot.driver_options.len(), 1);
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Printer("Another".into()));
|
||||
assert!(app.plot_print_options(&app.plot_dialog).driver_options.is_empty());
|
||||
// Reopening the editor starts from the remembered choices; Reset then
|
||||
// Apply forgets them.
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Printer("OCS Test Printer".into()));
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterProperties);
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterOptionsLoaded(
|
||||
"OCS Test Printer".into(),
|
||||
Ok(parse_lpoptions(LPOPTIONS)),
|
||||
));
|
||||
assert_eq!(app.plot_dialog.printer_editor.as_ref().unwrap().choices["ColorModel"], "Gray");
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterOptionsReset);
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterOptionsApply);
|
||||
assert!(app.plot_dialog.driver_options.get("OCS Test Printer").is_none());
|
||||
// A failed listing is reported inside the editor, not lost.
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterProperties);
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterOptionsLoaded(
|
||||
"OCS Test Printer".into(),
|
||||
Err("no cups".into()),
|
||||
));
|
||||
assert_eq!(app.plot_dialog.printer_editor.as_ref().unwrap().error.as_deref(), Some("no cups"));
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::PrinterOptionsCancel);
|
||||
assert!(app.plot_dialog.printer_editor.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_sheet_from_a_drawing_keeps_its_own_dimensions() {
|
||||
let mut app = app_with_printer_named_sheet();
|
||||
|
||||
@@ -64,8 +64,8 @@ impl OpenCADStudio {
|
||||
) -> Element<'s, Message> {
|
||||
sized_flow(
|
||||
extra,
|
||||
760,
|
||||
540,
|
||||
940,
|
||||
620,
|
||||
|flow| {
|
||||
crate::ui::window::plot::view_window(
|
||||
&self.plot_dialog,
|
||||
|
||||
+18
-3
@@ -22,8 +22,10 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
/// AutoCAD's plotter configuration name for PDF output.
|
||||
pub const PDF_DEVICE_NAME: &str = "DWG To PDF.pc3";
|
||||
/// AutoCAD's spelling for "no plotter assigned".
|
||||
/// AutoCAD's spelling for "no plotter assigned" in the plot dialog…
|
||||
pub const NO_DEVICE_NAME: &str = "None";
|
||||
/// …and the name it actually stores in the drawing for that choice.
|
||||
const NO_DEVICE_STORED_NAME: &str = "none_device";
|
||||
/// The label older Open CAD Studio builds wrote into drawings for PDF output.
|
||||
/// Recognised on read so those drawings keep plotting to PDF, and always
|
||||
/// rewritten with [`PDF_DEVICE_NAME`] on save.
|
||||
@@ -44,7 +46,7 @@ impl PlotDevice {
|
||||
/// The name AutoCAD would store for this device.
|
||||
pub fn canonical_name(&self) -> String {
|
||||
match self {
|
||||
PlotDevice::None => NO_DEVICE_NAME.to_string(),
|
||||
PlotDevice::None => NO_DEVICE_STORED_NAME.to_string(),
|
||||
PlotDevice::Pdf => PDF_DEVICE_NAME.to_string(),
|
||||
PlotDevice::Printer(name) => name.clone(),
|
||||
}
|
||||
@@ -61,6 +63,10 @@ impl PlotDevice {
|
||||
if name.is_empty() || name.eq_ignore_ascii_case(NO_DEVICE_NAME) {
|
||||
return (PlotDevice::None, false);
|
||||
}
|
||||
if name.eq_ignore_ascii_case(NO_DEVICE_STORED_NAME) {
|
||||
// AutoCAD's own spelling: keep it rather than rewriting it.
|
||||
return (PlotDevice::None, true);
|
||||
}
|
||||
if name == LEGACY_PDF_LABEL {
|
||||
return (PlotDevice::Pdf, false);
|
||||
}
|
||||
@@ -387,7 +393,8 @@ mod tests {
|
||||
#[test]
|
||||
fn canonical_names_follow_autocad() {
|
||||
assert_eq!(PlotDevice::Pdf.canonical_name(), "DWG To PDF.pc3");
|
||||
assert_eq!(PlotDevice::None.canonical_name(), "None");
|
||||
// What AutoCAD writes for "None" in its plot dialog.
|
||||
assert_eq!(PlotDevice::None.canonical_name(), "none_device");
|
||||
assert_eq!(
|
||||
PlotDevice::Printer("L3270".into()).canonical_name(),
|
||||
"L3270"
|
||||
@@ -401,6 +408,14 @@ mod tests {
|
||||
PlotDevice::from_stored_name("None"),
|
||||
(PlotDevice::None, false)
|
||||
);
|
||||
assert_eq!(
|
||||
PlotDevice::from_stored_name("none_device"),
|
||||
(PlotDevice::None, true)
|
||||
);
|
||||
assert_eq!(
|
||||
PlotDevice::from_stored_name("NONE_DEVICE"),
|
||||
(PlotDevice::None, true)
|
||||
);
|
||||
assert_eq!(
|
||||
PlotDevice::from_stored_name("Save to PDF file…"),
|
||||
(PlotDevice::Pdf, false)
|
||||
|
||||
+305
-1
@@ -47,6 +47,12 @@ pub struct PrintOptions {
|
||||
/// setting wins, so the field is legitimately unread there.
|
||||
#[cfg_attr(target_os = "windows", allow(dead_code))]
|
||||
pub quality: Option<String>,
|
||||
/// Driver options chosen in the printer-properties editor, as CUPS
|
||||
/// `key=value` pairs (`ColorModel=Gray`, `MediaType=Photographic`). Read
|
||||
/// on the CUPS path (`lp -o`); Windows keeps such choices in the
|
||||
/// printer's own preferences instead.
|
||||
#[cfg_attr(target_os = "windows", allow(dead_code))]
|
||||
pub driver_options: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -238,13 +244,16 @@ fn printer_properties_command(printer: Option<&str>) -> (&'static str, Vec<Strin
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty());
|
||||
|
||||
// `/e` opens the driver's *printing preferences* (paper, tray, duplex,
|
||||
// quality — what a print job honours), not the device's admin properties
|
||||
// sheet that `/p` shows.
|
||||
#[cfg(target_os = "windows")]
|
||||
let command = match named {
|
||||
Some(name) => (
|
||||
"rundll32.exe",
|
||||
vec![
|
||||
"printui.dll,PrintUIEntry".to_string(),
|
||||
"/p".to_string(),
|
||||
"/e".to_string(),
|
||||
"/n".to_string(),
|
||||
name.to_string(),
|
||||
],
|
||||
@@ -263,6 +272,7 @@ fn printer_properties_command(printer: Option<&str>) -> (&'static str, Vec<Strin
|
||||
|
||||
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
|
||||
let command = {
|
||||
// Reached only outside Linux; Linux opens the desktop's own panel.
|
||||
let target = named
|
||||
.map(|name| format!("http://localhost:631/printers/{name}"))
|
||||
.unwrap_or_else(|| "http://localhost:631/printers".to_string());
|
||||
@@ -275,6 +285,11 @@ fn printer_properties_command(printer: Option<&str>) -> (&'static str, Vec<Strin
|
||||
/// Open the operating system's printer configuration surface.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn open_printer_properties(printer: Option<&str>) -> Result<(), String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
return open_desktop_printer_settings(printer);
|
||||
}
|
||||
#[allow(unreachable_code)]
|
||||
let (program, args) = printer_properties_command(printer);
|
||||
std::process::Command::new(program)
|
||||
.args(args)
|
||||
@@ -406,6 +421,10 @@ fn dispatch_to_printer_opts(
|
||||
};
|
||||
cmd.arg("-o").arg(format!("print-quality={pq}"));
|
||||
}
|
||||
// The driver's own options, exactly as the PPD names them.
|
||||
for (key, value) in &opts.driver_options {
|
||||
cmd.arg("-o").arg(format!("{key}={value}"));
|
||||
}
|
||||
let lp_result = cmd
|
||||
.arg("--")
|
||||
.arg(path_str.as_ref())
|
||||
@@ -523,3 +542,288 @@ mod printer_properties_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Driver options (CUPS PPD) ─────────────────────────────────────────────
|
||||
|
||||
/// One option a CUPS printer's driver exposes, as `lpoptions -l` lists it:
|
||||
/// the PPD keyword, its description, the choices, and the current default.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PrinterOption {
|
||||
/// PPD keyword, e.g. `MediaType`, passed back verbatim as `-o key=value`.
|
||||
pub key: String,
|
||||
/// The driver's description, e.g. `Media Type`.
|
||||
pub label: String,
|
||||
/// Keyword of the currently selected choice.
|
||||
pub default: String,
|
||||
pub choices: Vec<PrinterChoice>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PrinterChoice {
|
||||
/// PPD choice keyword, e.g. `PhotographicHighGloss`.
|
||||
pub keyword: String,
|
||||
/// Readable form of the keyword, e.g. `Photographic High Gloss`.
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl PrinterOption {
|
||||
pub fn label_for(&self, keyword: &str) -> String {
|
||||
self.choices
|
||||
.iter()
|
||||
.find(|choice| choice.keyword == keyword)
|
||||
.map(|choice| choice.label.clone())
|
||||
.unwrap_or_else(|| humanize_keyword(keyword))
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask CUPS for the options of `printer` (blocking, one `lpoptions` spawn).
|
||||
/// The sheet (`PageSize`) is left out: the plot dialog chooses it.
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
||||
pub fn printer_options(printer: &str) -> Result<Vec<PrinterOption>, String> {
|
||||
let output = std::process::Command::new("lpoptions")
|
||||
.args(["-p", printer, "-l"])
|
||||
.output()
|
||||
.map_err(|error| format!("lpoptions: {error}"))?;
|
||||
if !output.status.success() {
|
||||
return Err(String::from_utf8_lossy(&output.stderr).trim().to_string());
|
||||
}
|
||||
Ok(parse_lpoptions(&String::from_utf8_lossy(&output.stdout)))
|
||||
}
|
||||
|
||||
/// Windows drivers keep their options behind `DocumentProperties`; there is
|
||||
/// no list to show in-line.
|
||||
#[cfg(any(target_arch = "wasm32", target_os = "windows"))]
|
||||
pub fn printer_options(_printer: &str) -> Result<Vec<PrinterOption>, String> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// Parse `lpoptions -l` output: one option per line as
|
||||
/// `Key/Description: choice *default choice …`, the asterisk marking the
|
||||
/// current default. Numeric-range options print the same way and are kept.
|
||||
pub fn parse_lpoptions(text: &str) -> Vec<PrinterOption> {
|
||||
let mut options = Vec::new();
|
||||
for line in text.lines() {
|
||||
let Some((head, tail)) = line.split_once(':') else {
|
||||
continue;
|
||||
};
|
||||
let (key, label) = head
|
||||
.split_once('/')
|
||||
.map(|(key, label)| (key.trim(), label.trim()))
|
||||
.unwrap_or((head.trim(), head.trim()));
|
||||
if key.is_empty() || key.eq_ignore_ascii_case("PageSize") {
|
||||
continue;
|
||||
}
|
||||
let mut default = String::new();
|
||||
let choices: Vec<PrinterChoice> = tail
|
||||
.split_whitespace()
|
||||
.map(|token| {
|
||||
let (keyword, is_default) = match token.strip_prefix('*') {
|
||||
Some(keyword) => (keyword, true),
|
||||
None => (token, false),
|
||||
};
|
||||
if is_default {
|
||||
default = keyword.to_string();
|
||||
}
|
||||
PrinterChoice {
|
||||
keyword: keyword.to_string(),
|
||||
label: humanize_keyword(keyword),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if choices.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if default.is_empty() {
|
||||
default = choices[0].keyword.clone();
|
||||
}
|
||||
options.push(PrinterOption {
|
||||
key: key.to_string(),
|
||||
label: if label.is_empty() { key.to_string() } else { label.to_string() },
|
||||
default,
|
||||
choices,
|
||||
});
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
/// `PhotographicHighGloss` → `Photographic High Gloss`, `DuplexNoTumble` →
|
||||
/// `Duplex No Tumble`; keywords that are already words or numbers pass
|
||||
/// through. Consecutive capitals (`RGB`, `A4`) stay together.
|
||||
pub fn humanize_keyword(keyword: &str) -> String {
|
||||
let mut out = String::with_capacity(keyword.len() + 4);
|
||||
let chars: Vec<char> = keyword.chars().collect();
|
||||
for (index, &ch) in chars.iter().enumerate() {
|
||||
if index > 0 && ch.is_ascii_uppercase() {
|
||||
let previous = chars[index - 1];
|
||||
let next_lower = chars.get(index + 1).is_some_and(|c| c.is_ascii_lowercase());
|
||||
if previous.is_ascii_lowercase() || (previous.is_ascii_uppercase() && next_lower) {
|
||||
out.push(' ');
|
||||
}
|
||||
}
|
||||
out.push(ch);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The desktop environment's printer settings panel — where the driver
|
||||
/// options cannot be listed (no CUPS) the user still gets somewhere useful.
|
||||
/// Tries the panel of the running desktop first, then the
|
||||
/// distribution-neutral tools, and only then the CUPS web interface.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn open_desktop_printer_settings(printer: Option<&str>) -> Result<(), String> {
|
||||
let desktop = std::env::var("XDG_CURRENT_DESKTOP")
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
let mut candidates: Vec<(&str, Vec<String>)> = Vec::new();
|
||||
if desktop.contains("gnome") || desktop.contains("ubuntu") || desktop.contains("cinnamon") {
|
||||
candidates.push(("gnome-control-center", vec!["printers".into()]));
|
||||
}
|
||||
if desktop.contains("kde") || desktop.contains("plasma") {
|
||||
candidates.push(("kcmshell6", vec!["kcm_printer_manager".into()]));
|
||||
candidates.push(("kcmshell5", vec!["kcm_printer_manager".into()]));
|
||||
}
|
||||
candidates.push(("system-config-printer", Vec::new()));
|
||||
candidates.push(("gnome-control-center", vec!["printers".into()]));
|
||||
for (program, args) in candidates {
|
||||
if std::process::Command::new(program)
|
||||
.args(&args)
|
||||
.spawn()
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let target = printer
|
||||
.map(|name| format!("http://localhost:631/printers/{name}"))
|
||||
.unwrap_or_else(|| "http://localhost:631/printers".to_string());
|
||||
std::process::Command::new("xdg-open")
|
||||
.arg(target)
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|error| format!("Could not open printer settings: {error}"))
|
||||
}
|
||||
|
||||
/// Show the driver's own document-properties sheet for `printer`, owned by
|
||||
/// the window `owner` (an `HWND`, 0 for none), and on OK store the result as
|
||||
/// the user's printing preferences for that printer — which is what the
|
||||
/// print job then honours. Returns `Ok(false)` when the user cancelled.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn edit_printer_preferences(printer: &str, owner: isize) -> Result<bool, String> {
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Graphics::Gdi::DEVMODEW;
|
||||
use windows_sys::Win32::Graphics::Printing::{
|
||||
ClosePrinter, DocumentPropertiesW, OpenPrinterW, SetPrinterW, PRINTER_INFO_9W,
|
||||
};
|
||||
// winspool.h: DM_OUT_BUFFER = DM_COPY, DM_IN_PROMPT = DM_PROMPT,
|
||||
// DM_IN_BUFFER = DM_MODIFY.
|
||||
const DM_OUT_BUFFER: u32 = 2;
|
||||
const DM_IN_PROMPT: u32 = 4;
|
||||
const DM_IN_BUFFER: u32 = 8;
|
||||
const IDOK: i32 = 1;
|
||||
|
||||
let name: Vec<u16> = printer.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let mut handle: HANDLE = std::ptr::null_mut();
|
||||
// SAFETY: `name` is NUL-terminated and outlives the call; a null default
|
||||
// asks for PRINTER_ACCESS_USE, enough for per-user preferences.
|
||||
if unsafe { OpenPrinterW(name.as_ptr(), &mut handle, std::ptr::null()) } == 0 {
|
||||
return Err(format!("Could not open printer \"{printer}\""));
|
||||
}
|
||||
let owner = owner as windows_sys::Win32::Foundation::HWND;
|
||||
let outcome = (|| {
|
||||
// SAFETY: with null buffers the call only reports the DEVMODE size.
|
||||
let size = unsafe {
|
||||
DocumentPropertiesW(owner, handle, name.as_ptr(), std::ptr::null_mut(), std::ptr::null(), 0)
|
||||
};
|
||||
if size <= 0 {
|
||||
return Err(format!("Printer \"{printer}\" reports no document properties"));
|
||||
}
|
||||
// DEVMODEW is u16-aligned; a u16 buffer keeps the cast sound.
|
||||
let words = (size as usize).div_ceil(2);
|
||||
let mut current = vec![0u16; words];
|
||||
let mut edited = vec![0u16; words];
|
||||
// SAFETY: both buffers hold `size` bytes as the driver requested.
|
||||
let fetched = unsafe {
|
||||
DocumentPropertiesW(
|
||||
owner,
|
||||
handle,
|
||||
name.as_ptr(),
|
||||
current.as_mut_ptr().cast::<DEVMODEW>(),
|
||||
std::ptr::null(),
|
||||
DM_OUT_BUFFER,
|
||||
)
|
||||
};
|
||||
if fetched < 0 {
|
||||
return Err(format!("Could not read the preferences of \"{printer}\""));
|
||||
}
|
||||
edited.copy_from_slice(¤t);
|
||||
// SAFETY: as above; DM_IN_PROMPT runs the driver's modal sheet.
|
||||
let response = unsafe {
|
||||
DocumentPropertiesW(
|
||||
owner,
|
||||
handle,
|
||||
name.as_ptr(),
|
||||
edited.as_mut_ptr().cast::<DEVMODEW>(),
|
||||
current.as_ptr().cast::<DEVMODEW>(),
|
||||
DM_IN_BUFFER | DM_IN_PROMPT | DM_OUT_BUFFER,
|
||||
)
|
||||
};
|
||||
if response != IDOK {
|
||||
return Ok(false);
|
||||
}
|
||||
let info = PRINTER_INFO_9W {
|
||||
pDevMode: edited.as_mut_ptr().cast::<DEVMODEW>(),
|
||||
};
|
||||
// SAFETY: level 9 takes a PRINTER_INFO_9W; the DEVMODE outlives the call.
|
||||
if unsafe { SetPrinterW(handle, 9, (&info as *const PRINTER_INFO_9W).cast::<u8>(), 0) } == 0 {
|
||||
return Err(format!("Could not save the preferences of \"{printer}\""));
|
||||
}
|
||||
Ok(true)
|
||||
})();
|
||||
// SAFETY: `handle` came from OpenPrinterW above.
|
||||
unsafe { ClosePrinter(handle) };
|
||||
outcome
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod driver_option_tests {
|
||||
use super::*;
|
||||
|
||||
const LPOPTIONS: &str = "PageSize/Media Size: 101.6x180.6mm *A4 A4.Borderless A5\n\
|
||||
MediaType/Media Type: *Stationery PhotographicHighGloss Photographic Envelope\n\
|
||||
ColorModel/Print Color Mode: *RGB Gray AutoGray ProcessGray\n\
|
||||
Duplex/2-Sided Printing: *None DuplexNoTumble DuplexTumble\n\
|
||||
cupsPrintQuality/Print Quality: *Normal High\n\
|
||||
print-scaling/Print Scaling: auto auto-fit fill *fit none\n\
|
||||
Broken line without colon\n";
|
||||
|
||||
#[test]
|
||||
fn lpoptions_output_becomes_options_with_defaults() {
|
||||
let options = parse_lpoptions(LPOPTIONS);
|
||||
let keys: Vec<&str> = options.iter().map(|o| o.key.as_str()).collect();
|
||||
// PageSize is the plot dialog's business; everything else is listed.
|
||||
assert_eq!(
|
||||
keys,
|
||||
["MediaType", "ColorModel", "Duplex", "cupsPrintQuality", "print-scaling"]
|
||||
);
|
||||
let media = &options[0];
|
||||
assert_eq!(media.label, "Media Type");
|
||||
assert_eq!(media.default, "Stationery");
|
||||
assert_eq!(media.choices[1].keyword, "PhotographicHighGloss");
|
||||
assert_eq!(media.choices[1].label, "Photographic High Gloss");
|
||||
assert_eq!(media.label_for("Envelope"), "Envelope");
|
||||
let scaling = &options[4];
|
||||
assert_eq!(scaling.default, "fit");
|
||||
assert_eq!(scaling.choices.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keywords_read_as_words() {
|
||||
assert_eq!(humanize_keyword("DuplexNoTumble"), "Duplex No Tumble");
|
||||
assert_eq!(humanize_keyword("RGB"), "RGB");
|
||||
assert_eq!(humanize_keyword("AutoGray"), "Auto Gray");
|
||||
assert_eq!(humanize_keyword("ProcessGray"), "Process Gray");
|
||||
assert_eq!(humanize_keyword("auto-fit"), "auto-fit");
|
||||
assert_eq!(humanize_keyword("PhotographicSemiGloss"), "Photographic Semi Gloss");
|
||||
assert_eq!(humanize_keyword("A4"), "A4");
|
||||
}
|
||||
}
|
||||
@@ -1460,6 +1460,15 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"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")),
|
||||
"Paper size" => Some(("plot", "paper-size")),
|
||||
"{count} driver option(s) set for this printer." => Some(("plot", "count-driver-option-s-set-for-this-printer")),
|
||||
"Printer properties — {printer}" => Some(("plot", "printer-properties-printer")),
|
||||
"This printer reports no driver options." => Some(("plot", "this-printer-reports-no-driver-options")),
|
||||
"Could not read the printer's options: {error}" => Some(("plot", "could-not-read-the-printer-s-options-error")),
|
||||
"Reading the printer's options…" => Some(("plot", "reading-the-printer-s-options")),
|
||||
"Printing preferences saved for {printer}." => Some(("plot", "printing-preferences-saved-for-printer")),
|
||||
"Printing preferences unchanged." => Some(("plot", "printing-preferences-unchanged")),
|
||||
"Printer properties" => Some(("plot", "printer-properties")),
|
||||
"Layout renamed: '{old_name}' → '{new_name}'" => Some(("common", "layout-renamed-old-name-new-name")),
|
||||
"Layouts" => Some(("layout", "layouts")),
|
||||
"Leader" => Some(("common", "leader")),
|
||||
|
||||
+18
-11
@@ -189,18 +189,25 @@ pub fn labeled_field_enabled<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A short unlabeled text field for a compact multi-value row (`L B R T`).
|
||||
pub fn compact_field<'a>(
|
||||
placeholder: &'a str,
|
||||
/// A `label : text field` pair with its own label width, for rows that put
|
||||
/// several small values side by side.
|
||||
pub fn labeled_field_compact<'a>(
|
||||
label: Cow<'static, str>,
|
||||
value: &'a str,
|
||||
on_input: impl Fn(String) -> Message + 'a,
|
||||
width: f32,
|
||||
label_width: f32,
|
||||
field_width: f32,
|
||||
) -> Element<'a, Message> {
|
||||
text_input(placeholder, value)
|
||||
.on_input(on_input)
|
||||
.style(field_style)
|
||||
.size(12)
|
||||
.padding([3, 6])
|
||||
.width(width)
|
||||
.into()
|
||||
row![
|
||||
text(label).size(11).style(muted_style).width(label_width),
|
||||
text_input("", value)
|
||||
.on_input(on_input)
|
||||
.style(field_style)
|
||||
.size(12)
|
||||
.padding([3, 6])
|
||||
.width(field_width),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
+239
-77
@@ -153,6 +153,18 @@ pub enum PlotDlgMsg {
|
||||
Commit,
|
||||
Preview,
|
||||
PrinterProperties,
|
||||
/// The driver options of a printer arrived (or failed to), for the
|
||||
/// in-line properties editor; carries the printer's name so a late
|
||||
/// answer for a printer the user has since left is ignored.
|
||||
PrinterOptionsLoaded(String, Result<Vec<crate::io::print_to_printer::PrinterOption>, String>),
|
||||
/// One option of the properties editor was set to a choice keyword.
|
||||
PrinterOptionSet(String, String),
|
||||
/// Keep the editor's choices for the printer and close it.
|
||||
PrinterOptionsApply,
|
||||
/// Put every option back to the driver's default.
|
||||
PrinterOptionsReset,
|
||||
/// Close the editor without keeping its changes.
|
||||
PrinterOptionsCancel,
|
||||
Printer(String),
|
||||
/// A printer answered (or failed) the media query started when it was
|
||||
/// selected; carries the printer's name so a stale answer for a printer
|
||||
@@ -291,7 +303,7 @@ impl CustomPaperDraft {
|
||||
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() {
|
||||
if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) {
|
||||
return Err(CustomPaperError::Size);
|
||||
}
|
||||
let margin = |text: &str| number(text).filter(|v| v.is_finite() && *v >= 0.0).unwrap_or(0.0);
|
||||
@@ -310,6 +322,36 @@ impl CustomPaperDraft {
|
||||
}
|
||||
}
|
||||
|
||||
/// The in-line printer-properties editor: the printer it belongs to, the
|
||||
/// driver's options, and the choice currently shown for each.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PrinterOptionsDraft {
|
||||
pub printer: String,
|
||||
/// `None` while the options are still being fetched.
|
||||
pub options: Option<Vec<crate::io::print_to_printer::PrinterOption>>,
|
||||
/// Current choice keyword per option key.
|
||||
pub choices: std::collections::BTreeMap<String, String>,
|
||||
/// Why the options could not be listed, when they could not.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl PrinterOptionsDraft {
|
||||
/// The choices that differ from the driver's defaults — all a job needs
|
||||
/// to pass along.
|
||||
pub fn overrides(&self) -> std::collections::BTreeMap<String, String> {
|
||||
let Some(options) = &self.options else {
|
||||
return std::collections::BTreeMap::new();
|
||||
};
|
||||
options
|
||||
.iter()
|
||||
.filter_map(|option| {
|
||||
let choice = self.choices.get(&option.key)?;
|
||||
(choice != &option.default).then(|| (option.key.clone(), choice.clone()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -336,6 +378,13 @@ pub struct PlotDialogState {
|
||||
/// The inline custom-sheet editor while it is open.
|
||||
#[serde(skip)]
|
||||
pub custom_editor: Option<CustomPaperDraft>,
|
||||
/// Driver options chosen per printer (`printer → key → choice`), the way
|
||||
/// a plotter configuration remembers its device settings; applied to
|
||||
/// every job sent to that printer and kept across sessions.
|
||||
pub driver_options: std::collections::BTreeMap<String, std::collections::BTreeMap<String, String>>,
|
||||
/// The in-line printer-properties editor while it is open.
|
||||
#[serde(skip)]
|
||||
pub printer_editor: Option<PrinterOptionsDraft>,
|
||||
/// Chosen printer name, or `None` for the system default.
|
||||
pub printer: Option<String>,
|
||||
/// Output goes to a PDF file instead of a printer.
|
||||
@@ -403,6 +452,8 @@ impl Default for PlotDialogState {
|
||||
printer_media: None,
|
||||
custom_papers: Vec::new(),
|
||||
custom_editor: None,
|
||||
driver_options: std::collections::BTreeMap::new(),
|
||||
printer_editor: None,
|
||||
printer: None,
|
||||
to_file: false,
|
||||
paper: paper_catalog::default_paper().canonical.to_string(),
|
||||
@@ -623,25 +674,125 @@ fn choices(items: &[&str]) -> Vec<PlotChoice> {
|
||||
items.iter().map(|&value| PlotChoice::localized(value)).collect()
|
||||
}
|
||||
|
||||
/// The in-line printer-properties editor: one `label : choice` row per
|
||||
/// driver option (media type, colour mode, quality, duplex, trays…), with
|
||||
/// Reset / Cancel / Apply. Mirrors what LibreOffice's Properties… shows for a
|
||||
/// CUPS printer, without leaving the plot dialog.
|
||||
fn printer_options_editor<'a>(draft: &'a PrinterOptionsDraft, width: Length) -> Element<'a, Message> {
|
||||
let mut rows = column![section_label(Cow::Owned(crate::tf!(
|
||||
"Printer properties — {printer}",
|
||||
printer = draft.printer
|
||||
).into_owned()))]
|
||||
.spacing(6);
|
||||
match (&draft.options, &draft.error) {
|
||||
(Some(options), _) if options.is_empty() => {
|
||||
rows = rows.push(
|
||||
text(t!("This printer reports no driver options."))
|
||||
.size(10)
|
||||
.style(muted_style),
|
||||
);
|
||||
}
|
||||
(Some(options), _) => {
|
||||
for option in options {
|
||||
let current = draft
|
||||
.choices
|
||||
.get(&option.key)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| option.default.clone());
|
||||
let choices: Vec<PlotChoice> = option
|
||||
.choices
|
||||
.iter()
|
||||
.map(|choice| PlotChoice {
|
||||
raw: choice.keyword.clone(),
|
||||
localized: false,
|
||||
display: Some(choice.label.clone()),
|
||||
})
|
||||
.collect();
|
||||
let selected = choices.iter().find(|c| c.raw == current).cloned();
|
||||
let key = option.key.clone();
|
||||
rows = rows.push(form::labeled_pick_list(
|
||||
Cow::Owned(option.label.clone()),
|
||||
choices,
|
||||
selected,
|
||||
move |choice| Message::PlotDlg(PlotDlgMsg::PrinterOptionSet(key.clone(), choice.raw)),
|
||||
Length::Fill,
|
||||
));
|
||||
}
|
||||
}
|
||||
(None, Some(error)) => {
|
||||
rows = rows.push(
|
||||
text(crate::tf!("Could not read the printer's options: {error}"))
|
||||
.size(10)
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().danger.base.color),
|
||||
}),
|
||||
);
|
||||
}
|
||||
(None, None) => {
|
||||
rows = rows.push(text(t!("Reading the printer's options…")).size(10).style(muted_style));
|
||||
}
|
||||
}
|
||||
let mut buttons = row![
|
||||
button(text(t!("Reset")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::PrinterOptionsReset))
|
||||
.style(btn(false))
|
||||
.padding([4, 10]),
|
||||
Space::new().width(Length::Fill),
|
||||
button(text(t!("Cancel")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::PrinterOptionsCancel))
|
||||
.style(btn(false))
|
||||
.padding([4, 10]),
|
||||
]
|
||||
.spacing(6);
|
||||
if draft.options.is_some() {
|
||||
buttons = buttons.push(
|
||||
button(text(t!("Apply")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::PrinterOptionsApply))
|
||||
.style(btn(true))
|
||||
.padding([4, 12]),
|
||||
);
|
||||
}
|
||||
rows = rows.push(buttons);
|
||||
container(rows)
|
||||
.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()
|
||||
}
|
||||
|
||||
/// The inline editor for a user-defined sheet: name, dimensions and unit,
|
||||
/// printable margins, Add / Cancel. Numbers stay text until Add validates
|
||||
/// them.
|
||||
/// them. Every margin field carries its own side label so the four values
|
||||
/// cannot be confused, and the unit is repeated where the numbers are typed.
|
||||
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 = if draft.inches { "in" } else { "mm" };
|
||||
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)
|
||||
const SIDE_LABEL: f32 = 52.0;
|
||||
const NUMBER: f32 = 64.0;
|
||||
let dimension = |label: Cow<'static, str>, value: &'a str, ctor: fn(String) -> CustomPaperMsg| {
|
||||
form::labeled_field_compact(label, value, move |v| msg(ctor(v)), SIDE_LABEL, NUMBER)
|
||||
};
|
||||
let margin = |label: Cow<'static, str>, side: MarginSide, index: usize| {
|
||||
form::labeled_field_compact(
|
||||
label,
|
||||
&draft.margins[index],
|
||||
move |v| msg(CustomPaperMsg::Margin(side, v)),
|
||||
SIDE_LABEL,
|
||||
NUMBER,
|
||||
)
|
||||
};
|
||||
let error: Element<'_, Message> = match draft.error {
|
||||
Some(CustomPaperError::Size) => text(t!("Enter a positive width and height."))
|
||||
@@ -652,33 +803,37 @@ fn custom_paper_editor<'a>(draft: &'a CustomPaperDraft, width: Length) -> Elemen
|
||||
.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_field(t!("Name"), &draft.name, move |v| msg(CustomPaperMsg::Name(v)), 200.0),
|
||||
form::labeled_pick_list(
|
||||
t!("Units"),
|
||||
unit_options,
|
||||
unit_selected,
|
||||
move |choice| msg(CustomPaperMsg::Units(choice.raw)),
|
||||
Length::Fixed(140.0),
|
||||
Length::Fixed(150.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,
|
||||
dimension(t!("Width"), &draft.width, CustomPaperMsg::Width),
|
||||
dimension(t!("Height"), &draft.height, CustomPaperMsg::Height),
|
||||
text(unit).size(11).style(muted_style),
|
||||
]
|
||||
.spacing(6)
|
||||
.spacing(14)
|
||||
.align_y(iced::Center),
|
||||
text(format!("{} ({unit})", t!("Printable margins")))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
row![
|
||||
margin(t!("Left"), MarginSide::Left, 0),
|
||||
margin(t!("Right"), MarginSide::Right, 2),
|
||||
]
|
||||
.spacing(14),
|
||||
row![
|
||||
margin(t!("Top"), MarginSide::Top, 3),
|
||||
margin(t!("Bottom"), MarginSide::Bottom, 1),
|
||||
]
|
||||
.spacing(14),
|
||||
error,
|
||||
row![
|
||||
Space::new().width(Length::Fill),
|
||||
@@ -897,7 +1052,11 @@ pub fn view_window(
|
||||
} else {
|
||||
Space::new().height(0).into()
|
||||
};
|
||||
let mut output_row = row![
|
||||
// Heading and picker share one line — the section has nothing else to
|
||||
// announce — and the picker takes the whole row so a long queue name
|
||||
// stays readable; the driver's Properties… button sits beneath it.
|
||||
let output_field = row![
|
||||
text(t!("Printer / plotter")).size(11).style(muted_style).width(form::LABEL_WIDTH),
|
||||
iced::widget::pick_list(printer_sel, printer_opts, |value| value.to_string())
|
||||
.on_select(|choice| Message::PlotDlg(PlotDlgMsg::Printer(choice.raw)))
|
||||
.text_size(12)
|
||||
@@ -905,66 +1064,69 @@ pub fn view_window(
|
||||
.width(Length::Fill),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center);
|
||||
if !s.to_file {
|
||||
output_row = output_row.push(
|
||||
.align_y(iced::Center)
|
||||
.width(Length::Fill);
|
||||
let printer_controls: Element<'_, Message> = if s.to_file {
|
||||
Space::new().height(0).into()
|
||||
} else {
|
||||
row![
|
||||
field_row(t!("Copies"), &s.copies, PlotDlgMsg::Copies, 60),
|
||||
Space::new().width(Length::Fill),
|
||||
button(text(t!("Properties…")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::PrinterProperties))
|
||||
.style(btn(false))
|
||||
.padding([4, 8]),
|
||||
);
|
||||
}
|
||||
let output_field = column![
|
||||
text(t!("Output")).size(11).style(muted_style),
|
||||
output_row,
|
||||
]
|
||||
.spacing(4)
|
||||
.width(Length::Fill);
|
||||
let copies_row: Element<'_, Message> = if s.to_file {
|
||||
Space::new().height(0).into()
|
||||
} else {
|
||||
field_row(t!("Copies"), &s.copies, PlotDlgMsg::Copies, 60)
|
||||
};
|
||||
let printer_panel = panel(
|
||||
column![
|
||||
section_label(t!("Printer / plotter")),
|
||||
output_field,
|
||||
copies_row,
|
||||
.padding([4, 10]),
|
||||
]
|
||||
.spacing(7),
|
||||
);
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
};
|
||||
// A printer with remembered driver choices says so under its row; the
|
||||
// editor itself unfolds there when Properties… is pressed.
|
||||
let printer_note: Element<'_, Message> = match (&s.printer_editor, &s.printer) {
|
||||
(Some(draft), _) => printer_options_editor(draft, width),
|
||||
(None, Some(printer)) if s.driver_options.get(printer).is_some_and(|o| !o.is_empty()) => {
|
||||
let count = s.driver_options[printer].len();
|
||||
text(crate::tf!("{count} driver option(s) set for this printer."))
|
||||
.size(10)
|
||||
.style(muted_style)
|
||||
.into()
|
||||
}
|
||||
_ => Space::new().height(0).into(),
|
||||
};
|
||||
let printer_panel = panel(column![output_field, printer_controls, printer_note].spacing(7));
|
||||
|
||||
// ── 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 {
|
||||
// The picker takes the whole row so long media names stay readable; the
|
||||
// custom-sheet buttons sit right-aligned beneath it, and the editor
|
||||
// unfolds in their place.
|
||||
let size_row = drop_row(t!("Paper size"), paper_opts, Some(paper_sel), PlotDlgMsg::Paper, Length::Fill);
|
||||
let custom_controls: Element<'_, Message> = match &s.custom_editor {
|
||||
Some(draft) => custom_paper_editor(draft, width),
|
||||
None => Space::new().height(0).into(),
|
||||
None => {
|
||||
let mut buttons = row![Space::new().width(Length::Fill)].spacing(6);
|
||||
if selected_is_custom {
|
||||
buttons = buttons.push(
|
||||
button(text(t!("Remove")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::CustomPaper(CustomPaperMsg::Remove)))
|
||||
.style(btn(false))
|
||||
.padding([4, 10]),
|
||||
);
|
||||
}
|
||||
buttons
|
||||
.push(
|
||||
button(text(t!("Custom…")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::CustomPaper(CustomPaperMsg::Open)))
|
||||
.style(btn(false))
|
||||
.padding([4, 10]),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
};
|
||||
// Like the printer row: the section is its one picker, so heading and
|
||||
// control share a line.
|
||||
let paper_panel = panel(column![
|
||||
section_label(t!("Paper")),
|
||||
size_row,
|
||||
custom_editor,
|
||||
custom_controls,
|
||||
paper_source_note,
|
||||
paper_note,
|
||||
].spacing(7));
|
||||
|
||||
Reference in new issue
Block a user