fix(print): open the driver's printing preferences without blocking the event loop

Properties… in the Plot dialog froze the application on Windows.

`on_printer_properties` called `DocumentPropertiesW` with `DM_IN_PROMPT`
from inside the closure handed to `iced::window::run`, and iced runs that
closure synchronously on the winit event-loop thread. The driver's
property sheet wants to run its own modal message loop on that same
thread, so the two loops collide: the sheet is never created at all, the
window stops painting, and the thread spins at 100% of one core with no
way back.

Measured on Windows 11 x86-64: the pinned thread is the window's own UI
thread, and compstui.dll plus the driver's PrintConfig.dll are loaded
into the process, so execution does reach the driver's property-sheet
code and never leaves it. It reproduces with a physical printer as
readily as with Microsoft Print to PDF, so the mechanism was broken for
every printer, not only the virtual one named in the report.

The system's own printing-preferences UI is now started as a separate
process, which pumps its own messages and leaves ours free, so the
freeze is impossible by construction rather than merely unlikely. The
command that does it already existed for the case where no printer is
selected; it now serves a named printer too, and the in-process modal
path is gone. Preferences still reach the job: the sheet writes the
per-user DEVMODE that the plot path reads back through
`DocumentPropertiesW`, which was confirmed by changing the orientation
in the sheet and watching the value the plot path reads change with it.

The new test pins down that the printer name must reach `printui` as a
single argument: split across several it matches nothing and the process
exits 0 without drawing anything, which would be a failure with nothing
to show the user.

Fixes #1474

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
mechpanosandClaude Opus 5 committed 2026-09-28 15:40:39 +03:00
1 parent d40e6c8d1d
commit 9ba2493027
24 files changed
+35 -166

No files matched your search

+16 -39
View File
@@ -4414,9 +4414,9 @@ impl OpenCADStudio {
///
/// 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
/// `lp -o`. On Windows the system opens the driver's own printing
/// preferences in its own process, and the print job honours what the
/// user saves there. 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> {
// "Default" is a real printer: its preferences sheet is the one to
@@ -4427,12 +4427,18 @@ impl OpenCADStudio {
.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();
self.open_printer_settings(None);
return Task::none();
};
#[cfg(target_os = "windows")]
{
return self.edit_windows_printer_preferences(printer);
// The driver's sheet is modal: it pumps messages itself until the
// user closes it. Hosted on our event-loop thread it starves the
// loop it is sharing, so the window stops painting and a core
// spins. The system's own preferences process pumps its own
// messages instead, which leaves ours free.
self.open_printer_settings(Some(&printer));
return Task::none();
}
#[allow(unreachable_code)]
{
@@ -4454,40 +4460,11 @@ impl OpenCADStudio {
}
}
/// 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(),
) {
/// The platform's printer settings surface: the driver's printing
/// preferences for `printer`, or — without one — a settings panel on
/// Linux, the printers control panel on Windows, System Settings on macOS.
fn open_printer_settings(&mut self, printer: Option<&str>) {
match crate::io::print_to_printer::open_printer_properties(printer) {
Ok(()) => self
.command_line
.push_info(crate::t!("Opened printer properties.").as_ref()),