fix(print): send Windows plots to the chosen printer, name the default, report failures

On Windows a plot with no printer chosen went to the PDF application's
bare "print" verb — whatever that application considered its default —
and a viewer without a "printto" verb (Edge, store readers) silently
dropped the printer the dialog had chosen. The dialog also never named the
system default, and a failed printer enumeration left only "Default" and
"PDF" with the reason written to a console nobody sees.

- The default printer is looked up by name (GetDefaultPrinterW) both for
  the dialog's "System default printer (…)" entry and before every job.
- A pure route decision (plan_print_route) resolves "Default" to that
  name and uses the PDF application only when it registered a "printto"
  verb for .pdf (AssocQueryStringW); otherwise the job goes straight to
  the spooler through the existing GDI path. The report says which:
  "Sent to printer: NAME (direct print | via the PDF application)".
- list_printers reports the system's own error; the plot dialog shows it
  under the printer row and the command line warns once. A desktop without
  CUPS is an empty list, not an error.
- PRINTERS command: default printer, listed printers (or the error) and,
  on Windows, the print route — a report to paste into an issue.
- Properties… on the Default entry opens that printer's preferences sheet.
- GDI path: the driver's dmSize is kept (its private data lives behind
  it), the print raster is no longer memoised for the session, and a
  spooled temporary PDF is removed.

Tests: route planning (named / blank / no default / printto or not), the
PRINTERS command; the two Windows-only GDI tests pass the resolved name.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-18 15:49:27 +03:00
1 parent 2b38f798c0
commit c36ae08fd0
29 files changed
+592 -112

No files matched your search

+9
View File
@@ -5111,6 +5111,15 @@ plot =
.system-default-printer = طابعة النظام الافتراضية
.what-to-plot = ما المراد طباعته
.x-mm = X (مم)
.default-printer-name = الطابعة الافتراضية: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = الطابعة الافتراضية: لم يبلّغ النظام عن أي طابعة
.printers-none-listed-by-the-system = الطابعات: لم يسرد النظام أي طابعة
.printers-count = الطابعات (__ocs_fmt_0__):
.could-not-list-printers-error = تعذّر سرد الطابعات: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = تمر المخططات عبر تطبيق PDF: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = تذهب المخططات مباشرة إلى الطابعة (لا يسجل أي تطبيق PDF أمر طباعة).
.via-the-pdf-application = عبر تطبيق PDF
.direct-print = طباعة مباشرة
.y-mm = Y (مم)
manage =
+9
View File
@@ -5099,6 +5099,15 @@ plot =
.system-default-printer = Системен принтер по подразбиране
.what-to-plot = Какво да се плотира
.x-mm = X (мм)
.default-printer-name = Принтер по подразбиране: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Принтер по подразбиране: системата не съобщава за такъв
.printers-none-listed-by-the-system = Принтери: системата не изброява нито един
.printers-count = Принтери (__ocs_fmt_0__):
.could-not-list-printers-error = Принтерите не могат да бъдат изброени: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Печатът минава през PDF приложението: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Печатът отива директно към принтера (никое PDF приложение не регистрира команда за печат).
.via-the-pdf-application = чрез PDF приложението
.direct-print = директен печат
.y-mm = Y (мм)
manage =
+9
View File
@@ -5096,6 +5096,15 @@ plot =
.system-default-printer = Výchozí tiskárna systému
.what-to-plot = Co fabulovat
.x-mm = X (mm)
.default-printer-name = Výchozí tiskárna: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Výchozí tiskárna: systém žádnou nehlásí
.printers-none-listed-by-the-system = Tiskárny: systém žádné neuvádí
.printers-count = Tiskárny (__ocs_fmt_0__):
.could-not-list-printers-error = Nelze vypsat tiskárny: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Tisk prochází aplikací PDF: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Tisk jde přímo na tiskárnu (žádná aplikace PDF neregistruje příkaz tisku).
.via-the-pdf-application = přes aplikaci PDF
.direct-print = přímý tisk
.y-mm = Y (mm)
manage =
+9
View File
@@ -5097,6 +5097,15 @@ plot =
.system-default-printer = Systemstandarddrucker
.what-to-plot = Was zu plotten
.x-mm = X (mm)
.default-printer-name = Standarddrucker: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Standarddrucker: vom System keiner gemeldet
.printers-none-listed-by-the-system = Drucker: vom System keiner aufgeführt
.printers-count = Drucker (__ocs_fmt_0__):
.could-not-list-printers-error = Drucker konnten nicht aufgelistet werden: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Plots laufen über die PDF-Anwendung: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Plots gehen direkt an den Drucker (keine PDF-Anwendung registriert einen Druckbefehl).
.via-the-pdf-application = über die PDF-Anwendung
.direct-print = Direktdruck
.y-mm = Y (mm)
manage =
+9
View File
@@ -5099,6 +5099,15 @@ plot =
.system-default-printer = Προεπιλεγμένος εκτυπωτής συστήματος
.what-to-plot = Τι θα εκτυπωθεί
.x-mm = X (mm)
.default-printer-name = Προεπιλεγμένος εκτυπωτής: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Προεπιλεγμένος εκτυπωτής: το σύστημα δεν αναφέρει κανέναν
.printers-none-listed-by-the-system = Εκτυπωτές: το σύστημα δεν αναφέρει κανέναν
.printers-count = Εκτυπωτές (__ocs_fmt_0__):
.could-not-list-printers-error = Δεν ήταν δυνατή η λίστα εκτυπωτών: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Οι εκτυπώσεις περνούν από την εφαρμογή PDF: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Οι εκτυπώσεις πάνε απευθείας στον εκτυπωτή (καμία εφαρμογή PDF δεν δηλώνει εντολή εκτύπωσης).
.via-the-pdf-application = μέσω της εφαρμογής PDF
.direct-print = απευθείας εκτύπωση
.y-mm = Y (mm)
manage =
+9
View File
@@ -5100,6 +5100,15 @@ plot =
.system-default-printer = System default printer
.what-to-plot = What to plot
.x-mm = X (mm)
.default-printer-name = Default printer: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Default printer: none reported by the system
.printers-none-listed-by-the-system = Printers: none listed by the system
.printers-count = Printers (__ocs_fmt_0__):
.could-not-list-printers-error = Could not list printers: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Plots go through the PDF application: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Plots go straight to the printer (no PDF application registers a print verb).
.via-the-pdf-application = via the PDF application
.direct-print = direct print
.y-mm = Y (mm)
manage =
+9
View File
@@ -5099,6 +5099,15 @@ plot =
.system-default-printer = Impresora predeterminada del sistema
.what-to-plot = Qué trazar
.x-mm = X (mm)
.default-printer-name = Impresora predeterminada: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Impresora predeterminada: el sistema no informa ninguna
.printers-none-listed-by-the-system = Impresoras: el sistema no lista ninguna
.printers-count = Impresoras (__ocs_fmt_0__):
.could-not-list-printers-error = No se pudieron listar las impresoras: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Los trazados pasan por la aplicación PDF: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Los trazados van directamente a la impresora (ninguna aplicación PDF registra un comando de impresión).
.via-the-pdf-application = a través de la aplicación PDF
.direct-print = impresión directa
.y-mm = Y (mm)
manage =
+9
View File
@@ -5098,6 +5098,15 @@ plot =
.system-default-printer = Järjestelmän oletustulostin
.what-to-plot = Mitä piirtää
.x-mm = X (mm)
.default-printer-name = Oletustulostin: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Oletustulostin: järjestelmä ei ilmoita yhtään
.printers-none-listed-by-the-system = Tulostimet: järjestelmä ei luettele yhtään
.printers-count = Tulostimet (__ocs_fmt_0__):
.could-not-list-printers-error = Tulostimia ei voitu luetella: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Tulosteet kulkevat PDF-sovelluksen kautta: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Tulosteet menevät suoraan tulostimelle (mikään PDF-sovellus ei rekisteröi tulostuskomentoa).
.via-the-pdf-application = PDF-sovelluksen kautta
.direct-print = suoratulostus
.y-mm = Y (mm)
manage =
+9
View File
@@ -5099,6 +5099,15 @@ plot =
.system-default-printer = Imprimante système par défaut
.what-to-plot = Quoi faire?
.x-mm = X (mm)
.default-printer-name = Imprimante par défaut : __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Imprimante par défaut : aucune signalée par le système
.printers-none-listed-by-the-system = Imprimantes : aucune répertoriée par le système
.printers-count = Imprimantes (__ocs_fmt_0__) :
.could-not-list-printers-error = Impossible de lister les imprimantes : __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Les tracés passent par l'application PDF : __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Les tracés vont directement à l'imprimante (aucune application PDF n'enregistre de commande d'impression).
.via-the-pdf-application = via l'application PDF
.direct-print = impression directe
.y-mm = Y (mm)
manage =
+9
View File
@@ -5087,6 +5087,15 @@ plot =
.system-default-printer = सिस्टम डिफ़ॉल्ट प्रिंटर
.what-to-plot = क्या प्लॉट करना है
.x-mm = एक्स (मिमी)
.default-printer-name = डिफ़ॉल्ट प्रिंटर: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = डिफ़ॉल्ट प्रिंटर: सिस्टम ने कोई नहीं बताया
.printers-none-listed-by-the-system = प्रिंटर: सिस्टम ने कोई सूचीबद्ध नहीं किया
.printers-count = प्रिंटर (__ocs_fmt_0__):
.could-not-list-printers-error = प्रिंटर सूचीबद्ध नहीं किए जा सके: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = प्लॉट PDF एप्लिकेशन के माध्यम से जाते हैं: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = प्लॉट सीधे प्रिंटर पर जाते हैं (कोई PDF एप्लिकेशन प्रिंट कमांड पंजीकृत नहीं करता)।
.via-the-pdf-application = PDF एप्लिकेशन के माध्यम से
.direct-print = सीधी प्रिंटिंग
.y-mm = Y (मिमी)
manage =
+9
View File
@@ -5098,6 +5098,15 @@ plot =
.system-default-printer = Rendszer alapértelmezett nyomtató
.what-to-plot = Mit kell kitalálni
.x-mm = X (mm)
.default-printer-name = Alapértelmezett nyomtató: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Alapértelmezett nyomtató: a rendszer nem jelez egyet sem
.printers-none-listed-by-the-system = Nyomtatók: a rendszer nem sorol fel egyet sem
.printers-count = Nyomtatók (__ocs_fmt_0__):
.could-not-list-printers-error = A nyomtatók nem listázhatók: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = A nyomtatás a PDF-alkalmazáson keresztül megy: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = A nyomtatás közvetlenül a nyomtatóra megy (egy PDF-alkalmazás sem regisztrál nyomtatási parancsot).
.via-the-pdf-application = a PDF-alkalmazáson keresztül
.direct-print = közvetlen nyomtatás
.y-mm = Y (mm)
manage =
+9
View File
@@ -5098,6 +5098,15 @@ plot =
.system-default-printer = Stampante predefinita del sistema
.what-to-plot = Cosa tracciare
.x-mm = X (mm)
.default-printer-name = Stampante predefinita: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Stampante predefinita: nessuna segnalata dal sistema
.printers-none-listed-by-the-system = Stampanti: nessuna elencata dal sistema
.printers-count = Stampanti (__ocs_fmt_0__):
.could-not-list-printers-error = Impossibile elencare le stampanti: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Le stampe passano dall'applicazione PDF: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Le stampe vanno direttamente alla stampante (nessuna applicazione PDF registra un comando di stampa).
.via-the-pdf-application = tramite l'applicazione PDF
.direct-print = stampa diretta
.y-mm = Y (mm)
manage =
+9
View File
@@ -5087,6 +5087,15 @@ plot =
.system-default-printer = システムの既定プリンター
.what-to-plot = 印刷対象
.x-mm = X(mm)
.default-printer-name = 既定のプリンター: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = 既定のプリンター: システムから報告なし
.printers-none-listed-by-the-system = プリンター: システムに一覧なし
.printers-count = プリンター(__ocs_fmt_0__):
.could-not-list-printers-error = プリンターを一覧できませんでした: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = 印刷は PDF アプリケーション経由です: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = 印刷はプリンターへ直接送られます(印刷コマンドを登録した PDF アプリケーションがありません)。
.via-the-pdf-application = PDF アプリケーション経由
.direct-print = 直接印刷
.y-mm = Y(mm)
manage =
+9
View File
@@ -5098,6 +5098,15 @@ plot =
.system-default-printer = 시스템 기본 프린터
.what-to-plot = 무엇을 플롯할 것인가
.x-mm = X (mm)
.default-printer-name = 기본 프린터: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = 기본 프린터: 시스템이 보고하지 않음
.printers-none-listed-by-the-system = 프린터: 시스템에 나열된 항목 없음
.printers-count = 프린터 (__ocs_fmt_0__):
.could-not-list-printers-error = 프린터 목록을 가져올 수 없습니다: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = 출력은 PDF 응용 프로그램을 거칩니다: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = 출력이 프린터로 직접 전송됩니다 (인쇄 명령을 등록한 PDF 응용 프로그램이 없음).
.via-the-pdf-application = PDF 응용 프로그램 경유
.direct-print = 직접 인쇄
.y-mm = Y (mm)
manage =
+9
View File
@@ -5097,6 +5097,15 @@ plot =
.system-default-printer = Standaardprinter van het systeem
.what-to-plot = Wat te plannen
.x-mm = X (mm)
.default-printer-name = Standaardprinter: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Standaardprinter: geen gemeld door het systeem
.printers-none-listed-by-the-system = Printers: geen vermeld door het systeem
.printers-count = Printers (__ocs_fmt_0__):
.could-not-list-printers-error = Kon printers niet weergeven: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Plots gaan via de PDF-toepassing: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Plots gaan rechtstreeks naar de printer (geen PDF-toepassing registreert een afdrukopdracht).
.via-the-pdf-application = via de PDF-toepassing
.direct-print = rechtstreeks afdrukken
.y-mm = Y (mm)
manage =
+9
View File
@@ -5098,6 +5098,15 @@ plot =
.system-default-printer = Domyślna drukarka systemowa
.what-to-plot = Co nakreślić
.x-mm = X (mm)
.default-printer-name = Drukarka domyślna: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Drukarka domyślna: system nie zgłasza żadnej
.printers-none-listed-by-the-system = Drukarki: system nie wymienia żadnej
.printers-count = Drukarki (__ocs_fmt_0__):
.could-not-list-printers-error = Nie można wyświetlić listy drukarek: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Wydruki przechodzą przez aplikację PDF: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Wydruki trafiają bezpośrednio do drukarki (żadna aplikacja PDF nie rejestruje polecenia drukowania).
.via-the-pdf-application = przez aplikację PDF
.direct-print = wydruk bezpośredni
.y-mm = Y (mm)
manage =
+9
View File
@@ -5099,6 +5099,15 @@ plot =
.system-default-printer = Impressora padrão do sistema
.what-to-plot = O que plotar
.x-mm = X (mm)
.default-printer-name = Impressora padrão: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Impressora padrão: nenhuma informada pelo sistema
.printers-none-listed-by-the-system = Impressoras: nenhuma listada pelo sistema
.printers-count = Impressoras (__ocs_fmt_0__):
.could-not-list-printers-error = Não foi possível listar as impressoras: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = As plotagens passam pelo aplicativo de PDF: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = As plotagens vão direto para a impressora (nenhum aplicativo de PDF registra um comando de impressão).
.via-the-pdf-application = pelo aplicativo de PDF
.direct-print = impressão direta
.y-mm = Y (mm)
manage =
+9
View File
@@ -5100,6 +5100,15 @@ plot =
.system-default-printer = Системный принтер по умолчанию
.what-to-plot = Что спланировать
.x-mm = X (мм)
.default-printer-name = Принтер по умолчанию: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Принтер по умолчанию: система не сообщает ни об одном
.printers-none-listed-by-the-system = Принтеры: система не перечисляет ни одного
.printers-count = Принтеры (__ocs_fmt_0__):
.could-not-list-printers-error = Не удалось получить список принтеров: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Печать идёт через приложение PDF: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Печать идёт напрямую на принтер (ни одно приложение PDF не регистрирует команду печати).
.via-the-pdf-application = через приложение PDF
.direct-print = прямая печать
.y-mm = Y (мм)
manage =
+9
View File
@@ -5039,6 +5039,15 @@ plot =
.system-default-printer = Varsayılan sistem yazıcısı
.what-to-plot = Yazdırılacak Alan
.x-mm = X (mm)
.default-printer-name = Varsayılan yazıcı: __ocs_fmt_0__
.default-printer-none-reported-by-the-system = Varsayılan yazıcı: sistem hiçbirini bildirmiyor
.printers-none-listed-by-the-system = Yazıcılar: sistem hiçbirini listelemiyor
.printers-count = Yazıcılar (__ocs_fmt_0__):
.could-not-list-printers-error = Yazıcılar listelenemedi: __ocs_fmt_0__
.plots-go-through-the-pdf-application-command = Çizimler PDF uygulaması üzerinden gider: __ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = Çizimler doğrudan yazıcıya gider (hiçbir PDF uygulaması yazdırma komutu kaydetmiyor).
.via-the-pdf-application = PDF uygulaması üzerinden
.direct-print = doğrudan yazdırma
.y-mm = Y (mm)
manage =
+9
View File
@@ -5082,6 +5082,15 @@ plot =
.system-default-printer = 系统默认打印机
.what-to-plot = 打印内容
.x-mm = X (毫米)
.default-printer-name = 默认打印机:__ocs_fmt_0__
.default-printer-none-reported-by-the-system = 默认打印机:系统未报告
.printers-none-listed-by-the-system = 打印机:系统未列出任何打印机
.printers-count = 打印机(__ocs_fmt_0__):
.could-not-list-printers-error = 无法列出打印机:__ocs_fmt_0__
.plots-go-through-the-pdf-application-command = 打印通过 PDF 应用程序进行:__ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = 打印直接发送到打印机(没有 PDF 应用程序注册打印命令)。
.via-the-pdf-application = 通过 PDF 应用程序
.direct-print = 直接打印
.y-mm = Y (毫米)
manage =
+9
View File
@@ -5094,6 +5094,15 @@ plot =
.system-default-printer = 系統預設印表機
.what-to-plot = 列印內容
.x-mm = X (毫米)
.default-printer-name = 預設印表機:__ocs_fmt_0__
.default-printer-none-reported-by-the-system = 預設印表機:系統未回報
.printers-none-listed-by-the-system = 印表機:系統未列出任何印表機
.printers-count = 印表機(__ocs_fmt_0__):
.could-not-list-printers-error = 無法列出印表機:__ocs_fmt_0__
.plots-go-through-the-pdf-application-command = 列印透過 PDF 應用程式進行:__ocs_fmt_0__
.plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb = 列印直接送到印表機(沒有 PDF 應用程式註冊列印命令)。
.via-the-pdf-application = 透過 PDF 應用程式
.direct-print = 直接列印
.y-mm = Y (毫米)
manage =
+29
View File
@@ -869,6 +869,15 @@ impl OpenCADStudio {
"PRINTALL" => {
return Some(Task::done(Message::PrintAllOpen));
}
// PRINTERS — what the system reports about printing, on the
// command line: the default printer, every printer it lists (or
// why it could not), and how a plot will reach them. The report
// a user pastes into a bug report instead of a screenshot.
"PRINTERS" => {
for line in crate::io::print_to_printer::printer_report() {
self.command_line.push_output(&line);
}
}
"EXPORT" | "EXPORTPDF" => {
return Some(Task::done(Message::PlotExport));
}
@@ -2094,3 +2103,23 @@ mod tests {
}
}
}
#[cfg(test)]
mod printers_command_tests {
use crate::app::OpenCADStudio;
#[test]
fn printers_reports_the_default_and_the_list() {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let before = app.command_line.history.len();
let _ = app.run_command_line("PRINTERS");
let lines: Vec<String> = app.command_line.history[before..]
.iter()
.map(|line| line.text.clone())
.collect();
// One line for the default printer, at least one for the list (a
// count and names, "none", or the system's error).
assert!(lines.len() >= 2, "{lines:?}");
}
}
+1
View File
@@ -712,6 +712,7 @@ inventory::submit!(crate::command::CommandRegistration {
"PERF",
"PERSP",
"PLOT",
"PRINTERS",
"PLOTSTYLE",
"PLOTSTYLEEDITOR",
"PLOTSTYLEPANEL",
+22 -3
View File
@@ -3960,7 +3960,16 @@ impl OpenCADStudio {
.collect();
// Keep session-only choices while loading drawing fields from the layout.
let d = &mut self.plot_dialog;
d.printers = crate::io::print_to_printer::list_printers();
match crate::io::print_to_printer::list_printers() {
Ok(printers) => {
d.printers = printers;
d.printers_error = None;
}
Err(error) => {
d.printers = Vec::new();
d.printers_error = Some(error);
}
}
d.default_printer = crate::io::plot_device::default_printer_name();
d.plot_styles = crate::io::plot_style::available_ctb_names();
d.scales = scales;
@@ -4011,6 +4020,10 @@ impl OpenCADStudio {
self.plot_dialog.scale_lw = false;
}
self.refresh_page_setups();
if let Some(error) = self.plot_dialog.printers_error.clone() {
self.command_line
.push_warning(crate::tf!("Could not list printers: {error}").as_ref());
}
self.plot_prev = Some(previous);
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
let layout_entry = format!("*{cur}*");
@@ -4179,8 +4192,14 @@ impl OpenCADStudio {
/// 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 {
// "Default" is a real printer: its preferences sheet is the one to
// open, not the system's printer list.
let chosen = self
.plot_dialog
.printer
.clone()
.or_else(|| self.plot_dialog.default_printer.clone());
let Some(printer) = chosen.filter(|_| !self.plot_dialog.to_file) else {
self.open_printer_settings_fallback();
return Task::none();
};
+8 -2
View File
@@ -232,8 +232,14 @@ pub fn default_printer_name() -> Option<String> {
(!name.is_empty()).then(|| name.to_string())
}
/// Windows default printer discovery lands with native GDI printing.
#[cfg(any(target_arch = "wasm32", target_os = "windows"))]
/// The spooler's default printer, by name — the same lookup every direct
/// print job resolves "Default" through, so the dialog names what will print.
#[cfg(target_os = "windows")]
pub fn default_printer_name() -> Option<String> {
crate::io::print_to_printer::windows_default_printer().ok()
}
#[cfg(target_arch = "wasm32")]
pub fn default_printer_name() -> Option<String> {
None
}
+314 -107
View File
@@ -80,30 +80,230 @@ pub fn print_existing_pdf(_path: &std::path::Path, _opts: &PrintOptions) -> Resu
/// Enumerate installed printers. Linux/macOS query CUPS via `lpstat -e`;
/// Windows queries the spooler's cached local and connected printer list.
/// An error is the system's own explanation (a stopped spooler, a failing
/// RPC); a desktop without CUPS at all is not an error but an empty list.
#[cfg(not(target_arch = "wasm32"))]
pub fn list_printers() -> Vec<String> {
pub fn list_printers() -> Result<Vec<String>, String> {
#[cfg(not(target_os = "windows"))]
{
std::process::Command::new("lpstat")
.arg("-e")
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect()
})
.unwrap_or_default()
let output = match std::process::Command::new("lpstat").arg("-e").output() {
Ok(output) => output,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => return Err(error.to_string()),
};
// `lpstat -e` exits non-zero with "No destinations added" when CUPS
// runs but knows no queue — an empty list, not a failure.
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect())
}
#[cfg(target_os = "windows")]
{
windows_printers().unwrap_or_else(|error| {
eprintln!("Could not enumerate printers: {error}");
Vec::new()
})
windows_printers().map_err(|error| error.to_string())
}
}
/// The printing situation as the system reports it, one line per fact:
/// the default printer, the printers listed (or the reason none were), and
/// on Windows the route a plot takes. Shown by the `PRINTERS` command.
#[cfg(not(target_arch = "wasm32"))]
pub fn printer_report() -> Vec<String> {
let mut lines = Vec::new();
match crate::io::plot_device::default_printer_name() {
Some(name) => lines.push(crate::tf!("Default printer: {name}").into_owned()),
None => lines.push(crate::t!("Default printer: none reported by the system").into_owned()),
}
match list_printers() {
Ok(printers) if printers.is_empty() => {
lines.push(crate::t!("Printers: none listed by the system").into_owned());
}
Ok(printers) => {
let count = printers.len();
lines.push(crate::tf!("Printers ({count}):").into_owned());
lines.extend(printers.into_iter().map(|name| format!(" {name}")));
}
Err(error) => lines.push(crate::tf!("Could not list printers: {error}").into_owned()),
}
#[cfg(target_os = "windows")]
{
match pdf_printto_command() {
Some(command) => lines.push(
crate::tf!("Plots go through the PDF application: {command}").into_owned(),
),
None => lines.push(
crate::t!(
"Plots go straight to the printer (no PDF application registers a print verb)."
)
.into_owned(),
),
}
}
lines
}
#[cfg(target_arch = "wasm32")]
pub fn printer_report() -> Vec<String> {
vec![crate::t!("Printing is not available in the web version.").into_owned()]
}
/// The Windows default printer, as the spooler names it.
#[cfg(target_os = "windows")]
pub(crate) fn windows_default_printer() -> std::io::Result<String> {
use windows_sys::Win32::Foundation::{GetLastError, ERROR_FILE_NOT_FOUND};
use windows_sys::Win32::Graphics::Printing::GetDefaultPrinterW;
let mut needed = 0u32;
// SAFETY: the first call only asks for the size; the second writes into
// a buffer of exactly that size.
unsafe { GetDefaultPrinterW(std::ptr::null_mut(), &mut needed) };
if needed == 0 {
// Windows answers ERROR_FILE_NOT_FOUND when no printer is the default.
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"No default printer is configured on Windows.",
));
}
let mut buf = vec![0u16; needed as usize];
if unsafe { GetDefaultPrinterW(buf.as_mut_ptr(), &mut needed) } == 0 {
let code = unsafe { GetLastError() };
if code == ERROR_FILE_NOT_FOUND {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"No default printer is configured on Windows.",
));
}
return Err(std::io::Error::from_raw_os_error(code as i32));
}
let end = buf.iter().position(|&u| u == 0).unwrap_or(0);
Ok(String::from_utf16_lossy(&buf[..end]))
}
/// The command Windows runs for the `printto` verb on `.pdf` files — the
/// registered PDF application's "print to a named printer" entry — or
/// `None` when no application registered one (Edge and store readers only
/// register `open`), in which case a plot goes straight to the spooler.
#[cfg(target_os = "windows")]
pub(crate) fn pdf_printto_command() -> Option<String> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::UI::Shell::{AssocQueryStringW, ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND};
let wide =
|s: &str| -> Vec<u16> { std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect() };
let extension = wide(".pdf");
let verb = wide("printto");
let mut len = 0u32;
// SAFETY: strings are NUL-terminated; a null buffer asks for the size,
// which the call reports with S_FALSE rather than S_OK.
let sized = unsafe {
AssocQueryStringW(
ASSOCF_NOTRUNCATE,
ASSOCSTR_COMMAND,
extension.as_ptr(),
verb.as_ptr(),
std::ptr::null_mut(),
&mut len,
)
};
if sized < 0 || len == 0 {
return None;
}
let mut buf = vec![0u16; len as usize];
let filled = unsafe {
AssocQueryStringW(
ASSOCF_NOTRUNCATE,
ASSOCSTR_COMMAND,
extension.as_ptr(),
verb.as_ptr(),
buf.as_mut_ptr(),
&mut len,
)
};
if filled < 0 {
return None;
}
let end = buf.iter().position(|&u| u == 0).unwrap_or(buf.len());
let command = String::from_utf16_lossy(&buf[..end]);
(!command.trim().is_empty()).then_some(command)
}
/// Where a Windows plot goes and how. The route is decided before any
/// system call so the decision itself can be tested anywhere.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) enum PrintRoute {
/// Hand the PDF to the registered PDF application's `printto` verb,
/// naming the printer explicitly.
PdfApplication { printer: String },
/// Rasterise in-process and draw to the printer through the spooler.
Direct { printer: String },
}
impl PrintRoute {
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn printer(&self) -> &str {
match self {
PrintRoute::PdfApplication { printer } | PrintRoute::Direct { printer } => printer,
}
}
}
/// Resolve the dialog's choice to a route. "Default" (no name) becomes the
/// system default printer *by name*, so the job can never drift to whatever
/// the PDF application considers its default; the PDF application is used
/// only when it registered a `printto` verb.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn plan_print_route(
requested: Option<&str>,
default_printer: Option<&str>,
printto_registered: bool,
) -> Result<PrintRoute, String> {
let requested = requested.map(str::trim).filter(|name| !name.is_empty());
let printer = match requested.or(default_printer.map(str::trim).filter(|n| !n.is_empty())) {
Some(name) => name.to_string(),
None => return Err("No default printer is configured on Windows.".into()),
};
Ok(if printto_registered {
PrintRoute::PdfApplication { printer }
} else {
PrintRoute::Direct { printer }
})
}
#[cfg(test)]
mod print_route_tests {
use super::{plan_print_route, PrintRoute};
#[test]
fn a_named_printer_is_kept_and_blank_means_the_default() {
assert_eq!(
plan_print_route(Some("Office Laser"), Some("Home Inkjet"), false),
Ok(PrintRoute::Direct { printer: "Office Laser".into() })
);
assert_eq!(
plan_print_route(Some(" "), Some("Home Inkjet"), false),
Ok(PrintRoute::Direct { printer: "Home Inkjet".into() })
);
assert_eq!(
plan_print_route(None, Some(" Home Inkjet "), true),
Ok(PrintRoute::PdfApplication { printer: "Home Inkjet".into() })
);
}
#[test]
fn no_printer_at_all_is_an_error_not_a_guess() {
assert!(plan_print_route(None, None, true).is_err());
assert!(plan_print_route(Some(""), Some(""), false).is_err());
}
#[test]
fn the_pdf_application_is_used_only_with_a_printto_verb() {
let direct = plan_print_route(Some("X"), None, false).unwrap();
let via_app = plan_print_route(Some("X"), None, true).unwrap();
assert!(matches!(direct, PrintRoute::Direct { .. }));
assert!(matches!(via_app, PrintRoute::PdfApplication { .. }));
assert_eq!(direct.printer(), "X");
}
}
@@ -408,7 +608,12 @@ fn oriented_printer_devmode(device_wide: &[u16], landscape: bool) -> Option<Vec<
{
return None;
}
(*dm).dmSize = std::mem::size_of::<DEVMODEW>() as u16;
// The driver's own dmSize locates its private data
// (dmDriverExtra) behind the public struct; only a driver that
// left it zero gets the public size.
if (*dm).dmSize == 0 {
(*dm).dmSize = std::mem::size_of::<DEVMODEW>() as u16;
}
(*dm).dmFields |= DM_ORIENTATION;
(*dm).Anonymous1.Anonymous1.dmOrientation = if landscape {
DMORIENT_LANDSCAPE as i16
@@ -438,7 +643,7 @@ fn oriented_printer_devmode(device_wide: &[u16], landscape: bool) -> Option<Vec<
#[cfg(target_os = "windows")]
fn gdi_raster_print(
path: &std::path::Path,
printer: Option<&str>,
printer: &str,
copies: u32,
output_file: Option<&std::path::Path>,
) -> Result<(), String> {
@@ -450,34 +655,21 @@ fn gdi_raster_print(
};
// windows-sys 0.61 groups the spooler-document calls (StartDoc/StartPage/
// EndPage/EndDoc/DOCINFOW) under Storage::Xps, not Graphics::Gdi.
use windows_sys::Win32::Graphics::Printing::GetDefaultPrinterW;
use windows_sys::Win32::Storage::Xps::{EndDoc, EndPage, StartDocW, StartPage, DOCINFOW};
let wide = |s: &str| -> Vec<u16> { std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect() };
// Resolve the target device: the named printer or the Windows default.
let device = match printer.map(str::trim).filter(|p| !p.is_empty()) {
Some(name) => name.to_string(),
None => {
let mut needed = 0u32;
// SAFETY: first call queries the size, second writes into a
// buffer of exactly that size.
unsafe { GetDefaultPrinterW(std::ptr::null_mut(), &mut needed) };
if needed == 0 {
return Err("No default printer is configured on Windows.".into());
}
let mut buf = vec![0u16; needed as usize];
if unsafe { GetDefaultPrinterW(buf.as_mut_ptr(), &mut needed) } == 0 {
return Err(format!(
"Could not query the default printer (code {}).",
unsafe { windows_sys::Win32::Foundation::GetLastError() }
));
}
String::from_utf16_lossy(&buf[..buf.iter().position(|&u| u == 0).unwrap_or(0)])
}
};
// The device is resolved by the caller (`plan_print_route`): a name the
// dialog chose or the system default looked up by name.
let device = printer.trim();
if device.is_empty() {
return Err("No printer was chosen for direct printing.".into());
}
let device = device.to_string();
let page = crate::scene::model::pdf_raster::rasterize_page_at_dpi(
// A plot is printed once; the on-screen memo of PDF pages must not keep
// a 300 DPI raster of every job for the rest of the session.
let page = crate::scene::model::pdf_raster::rasterize_page_at_dpi_uncached(
&path.to_string_lossy(),
"1",
GDI_PRINT_DPI,
@@ -657,6 +849,50 @@ pub fn open_in_viewer(path: &std::path::Path) -> Result<(), String> {
.map_err(|e| format!("Could not open preview: {e}"))
}
/// Ask the registered PDF application to print `path` on `printer` through
/// its `printto` verb, once per copy. The printer name is quoted: some
/// `printto` command templates do not quote their `%2`.
#[cfg(target_os = "windows")]
fn shell_print_to(path: &std::path::Path, printer: &str, copies: u32) -> Result<(), String> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Foundation::{GetLastError, ERROR_NO_ASSOCIATION};
use windows_sys::Win32::UI::Shell::{
ShellExecuteExW, SHELLEXECUTEINFOW, SEE_MASK_FLAG_NO_UI, SEE_MASK_NOASYNC, SE_ERR_NOASSOC,
};
use windows_sys::Win32::UI::WindowsAndMessaging::SW_HIDE;
let wide = |s: &str| -> Vec<u16> { OsStr::new(s).encode_wide().chain(Some(0)).collect() };
let path_wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let verb = wide("printto");
let params = wide(&format!("\"{printer}\""));
for _ in 0..copies.max(1) {
let mut info = SHELLEXECUTEINFOW {
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as u32,
fMask: SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC,
lpVerb: verb.as_ptr(),
lpFile: path_wide.as_ptr(),
lpParameters: params.as_ptr(),
nShow: SW_HIDE,
..Default::default()
};
// SAFETY: every pointer in `info` outlives the call.
if unsafe { ShellExecuteExW(&mut info) } == 0 {
let shell_code = info.hInstApp as usize;
let code = if (1..=32).contains(&shell_code) {
shell_code as u32
} else {
unsafe { GetLastError() }
};
if code == SE_ERR_NOASSOC || code == ERROR_NO_ASSOCIATION {
return Err("Windows has no PDF application registered with Print support.".into());
}
return Err(format!("Windows print dispatch failed (code {code})"));
}
}
Ok(())
}
/// Dispatch a PDF to a specific printer with [`PrintOptions`].
#[cfg(not(target_arch = "wasm32"))]
fn dispatch_to_printer_opts(
@@ -665,67 +901,43 @@ fn dispatch_to_printer_opts(
) -> Result<String, String> {
#[cfg(target_os = "windows")]
{
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Foundation::{GetLastError, ERROR_NO_ASSOCIATION};
use windows_sys::Win32::UI::Shell::{
ShellExecuteExW, SHELLEXECUTEINFOW, SEE_MASK_FLAG_NO_UI, SEE_MASK_NOASYNC,
SE_ERR_NOASSOC,
};
use windows_sys::Win32::UI::WindowsAndMessaging::SW_HIDE;
let wide = |s: &str| -> Vec<u16> { OsStr::new(s).encode_wide().chain(Some(0)).collect() };
let path_wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let (verb, params, label) = match opts.printer.as_deref() {
Some(p) if !p.is_empty() => (wide("printto"), Some(wide(p)), p.to_string()),
_ => (wide("print"), None, "default printer".to_string()),
};
let params_ptr = params.as_ref().map(|v| v.as_ptr()).unwrap_or(std::ptr::null());
let shell = (|| -> Result<String, String> {
for _ in 0..opts.copies.max(1) {
let mut info = SHELLEXECUTEINFOW {
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as u32,
fMask: SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC,
lpVerb: verb.as_ptr(),
lpFile: path_wide.as_ptr(),
lpParameters: params_ptr,
nShow: SW_HIDE,
..Default::default()
};
if unsafe { ShellExecuteExW(&mut info) } == 0 {
let shell_code = info.hInstApp as usize;
let code = if (1..=32).contains(&shell_code) {
shell_code as u32
} else {
unsafe { GetLastError() }
};
if code == SE_ERR_NOASSOC || code == ERROR_NO_ASSOCIATION {
return Err(
"Windows has no PDF application registered with Print support.".into(),
);
let default_printer = windows_default_printer().ok();
let route = plan_print_route(
opts.printer.as_deref(),
default_printer.as_deref(),
pdf_printto_command().is_some(),
)?;
let printer = route.printer().to_string();
let via_application = match &route {
PrintRoute::PdfApplication { .. } => {
match shell_print_to(path, &printer, opts.copies) {
Ok(()) => true,
// The PDF application refused the job: the spooler route
// still prints it, and the report says so.
Err(_) => {
gdi_raster_print(path, &printer, opts.copies, None)?;
false
}
return Err(format!("Windows print dispatch failed (code {code})"));
}
}
Ok(label)
})();
if let Ok(sent) = shell {
return Ok(sent);
PrintRoute::Direct { .. } => {
gdi_raster_print(path, &printer, opts.copies, None)?;
false
}
};
// A direct job is fully spooled when the calls return; the temporary
// PDF behind it has no reader left. The PDF application reads its
// file after ShellExecute returns, so that copy is left to the
// temp folder.
if !via_application && path.starts_with(std::env::temp_dir()) {
let _ = std::fs::remove_file(path);
}
// Fallback: hand the page straight to the spooler through GDI, with no
// PDF application involved. Modern PDF readers (Edge, store apps)
// register only an `open` verb, which used to leave every plot
// failing with "no PDF application registered".
gdi_raster_print(path, opts.printer.as_deref(), opts.copies, None).map_err(|gdi| {
format!(
"{}; direct print fallback failed: {gdi}",
shell.err().unwrap_or_default()
)
})?;
Ok(opts
.printer
.clone()
.unwrap_or_else(|| "default printer".to_string()))
let how = if via_application {
crate::t!("via the PDF application")
} else {
crate::t!("direct print")
};
Ok(format!("{printer} ({how})"))
}
#[cfg(not(target_os = "windows"))]
@@ -907,12 +1119,7 @@ mod printer_properties_tests {
let printed = std::env::temp_dir().join("ocs_gdi_ink_output.pdf");
let _ = std::fs::remove_file(&printed);
let result = super::gdi_raster_print(
&plot,
Some("Microsoft Print to PDF"),
1,
Some(&printed),
);
let result = super::gdi_raster_print(&plot, "Microsoft Print to PDF", 1, Some(&printed));
assert!(result.is_ok(), "GDI print failed: {result:?}");
// The spooled PDF must contain the black block: rasterise it and
@@ -971,7 +1178,7 @@ mod printer_properties_tests {
let printed = std::env::temp_dir().join("ocs_gdi_orientation_output.pdf");
let _ = std::fs::remove_file(&printed);
let result = super::gdi_raster_print(&plot, Some("Microsoft Print to PDF"), 1, Some(&printed));
let result = super::gdi_raster_print(&plot, "Microsoft Print to PDF", 1, Some(&printed));
assert!(result.is_ok(), "GDI print failed: {result:?}");
let page = crate::scene::model::pdf_raster::rasterize_page(
+9
View File
@@ -3144,6 +3144,15 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"World" => Some(("common", "world")),
"Write Block" => Some(("insert", "write-block")),
"X (mm)" => Some(("plot", "x-mm")),
"Default printer: {name}" => Some(("plot", "default-printer-name")),
"Default printer: none reported by the system" => Some(("plot", "default-printer-none-reported-by-the-system")),
"Printers: none listed by the system" => Some(("plot", "printers-none-listed-by-the-system")),
"Printers ({count}):" => Some(("plot", "printers-count")),
"Could not list printers: {error}" => Some(("plot", "could-not-list-printers-error")),
"Plots go through the PDF application: {command}" => Some(("plot", "plots-go-through-the-pdf-application-command")),
"Plots go straight to the printer (no PDF application registers a print verb)." => Some(("plot", "plots-go-straight-to-the-printer-no-pdf-application-registers-a-print-verb")),
"via the PDF application" => Some(("plot", "via-the-pdf-application")),
"direct print" => Some(("plot", "direct-print")),
"X = %{x}, Y = %{y}, Z = %{z}" => Some(("inquiry", "x-x-y-y-z-z")),
"X Scale" => Some(("properties", "x-scale")),
"X-Ray" => Some(("draw", "x-ray")),
+7
View File
@@ -69,6 +69,13 @@ pub fn rasterize_page_at_dpi(path: &str, page: &str, dpi: f32) -> Option<Arc<Pdf
built
}
/// Rasterise without touching the memo: for one-off consumers such as a
/// print job, whose 300 DPI page would otherwise stay cached for the whole
/// session (tens of megabytes per sheet).
pub fn rasterize_page_at_dpi_uncached(path: &str, page: &str, dpi: f32) -> Option<Arc<PdfPage>> {
rasterize_uncached(path, page, dpi)
}
fn rasterize_uncached(path: &str, page: &str, dpi: f32) -> Option<Arc<PdfPage>> {
let bytes = source_bytes(path)?;
let pdf = Pdf::new(bytes).ok()?;
+13
View File
@@ -365,6 +365,11 @@ pub struct PlotDialogState {
/// sentinels.
#[serde(skip)]
pub printers: Vec<String>,
/// Why the system gave no printer list (a stopped spooler, a failing
/// print service), shown under the printer row so an empty list is
/// never mistaken for "no printers".
#[serde(skip)]
pub printers_error: Option<String>,
/// Name of the system default printer, shown next to the default entry.
#[serde(skip)]
pub default_printer: Option<String>,
@@ -452,6 +457,7 @@ impl Default for PlotDialogState {
fn default() -> Self {
Self {
printers: Vec::new(),
printers_error: None,
default_printer: None,
printer_media: None,
custom_papers: Vec::new(),
@@ -1090,6 +1096,13 @@ pub fn view_window(
// 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, _) if s.printers_error.is_some() => {
let error = s.printers_error.clone().unwrap_or_default();
text(crate::tf!("Could not list printers: {error}"))
.size(10)
.style(muted_style)
.into()
}
(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."))