client/securedrop_client/gui/conversation/export/print_dialog.py (140 lines of code) (raw):

from gettext import gettext as _ from PyQt5.QtCore import QSize, pyqtSlot from securedrop_client.export_status import ExportStatus from securedrop_client.gui.base import ModalDialog, SecureQLabel from ....export import Export class PrintDialog(ModalDialog): FILENAME_WIDTH_PX = 260 def __init__(self, device: Export, file_name: str, filepaths: list[str]) -> None: super().__init__() self._device = device self.filepaths = filepaths self.file_name = SecureQLabel( file_name, wordwrap=False, max_length=self.FILENAME_WIDTH_PX ).text() # Hold onto the error status we receive from the Export VM. Only required # because we're reusing the same modal dialog with different text depending # on conditions, and need to pass methods to Qt handlers with a predefined # message signature. self.status: ExportStatus | None = None # Connect device signals to slots self._device.print_preflight_check_succeeded.connect( self._on_print_preflight_check_succeeded ) self._device.print_preflight_check_failed.connect(self._on_print_preflight_check_failed) # For now, connect both success and error signals to close the print dialog. self._device.print_succeeded.connect(self._on_print_complete) self._device.print_failed.connect(self._on_print_complete) # Connect parent signals to slots self.continue_button.setEnabled(False) self.continue_button.clicked.connect(self._run_preflight) # Dialog content self.starting_header = _( "Preparing to print:<br />" '<span style="font-weight:normal">{}</span>' ).format(self.file_name) self.ready_header = _( "Ready to print:<br />" '<span style="font-weight:normal">{}</span>' ).format(self.file_name) self.insert_usb_header = _("Connect USB printer") self.error_header = _("Printing failed") self.starting_message = _( "<h2>Managing printout risks</h2>" "<b>QR codes and web addresses</b>" "<br />" "Never type in and open web addresses or scan QR codes contained in printed " "documents without taking security precautions. If you are unsure how to " "manage this risk, please contact your administrator." "<br /><br />" "<b>Printer dots</b>" "<br />" "Any part of a printed page may contain identifying information " "invisible to the naked eye, such as printer dots. Please carefully " "consider this risk when working with or publishing scanned printouts." ) self.insert_usb_message = _("Please connect your printer to a USB port.") self.generic_error_message = _("See your administrator for help.") self.unprintable_type_error_message = _("This file type cannot be printed.") self._show_starting_instructions() self.start_animate_header() self._run_preflight() def _show_starting_instructions(self) -> None: self.header.setText(self.starting_header) self.body.setText(self.starting_message) self.error_details.hide() self.adjustSize() def _show_insert_usb_message(self) -> None: self.continue_button.clicked.disconnect() self.continue_button.clicked.connect(self._run_preflight) self.header.setText(self.insert_usb_header) self.body.setText(self.insert_usb_message) self.error_details.hide() self.adjustSize() def _show_unprintable_message(self) -> None: self.continue_button.clicked.disconnect() self.continue_button.clicked.connect(self.close) self.header.setText(self.error_header) self.body.setText(self.unprintable_type_error_message) self.error_details.hide() self.adjustSize() def _show_generic_error_message(self) -> None: self._show_error_message() def _show_error_message(self) -> None: """ Show error message based on ExportStatus returned. """ self.continue_button.clicked.disconnect() self.continue_button.clicked.connect(self.close) self.continue_button.setText(_("DONE")) self.header.setText(self.error_header) if self.status == ExportStatus.ERROR_UNPRINTABLE_TYPE: body_text = self.unprintable_type_error_message else: body_text = self.generic_error_message if self.status: self.body.setText( # nosemgrep: semgrep.untranslated-gui-string f"{self.status.value}: {body_text}" ) else: self.body.setText( # nosemgrep: semgrep.untranslated-gui-string body_text ) self.error_details.hide() self.adjustSize() @pyqtSlot() def _run_preflight(self) -> None: self._device.run_printer_preflight_checks() @pyqtSlot() def _print_file(self) -> None: self.start_animate_activestate() self._device.print(self.filepaths) @pyqtSlot(object) def _on_print_complete(self, status: ExportStatus) -> None: """ Send a signal to close the print dialog or display an appropriate error message. """ self.status = status self.stop_animate_activestate() if status == ExportStatus.PRINT_SUCCESS: self.close() elif self.status == ExportStatus.ERROR_PRINTER_NOT_FOUND: self._show_insert_usb_message() elif self.status == ExportStatus.ERROR_UNPRINTABLE_TYPE: self._show_unprintable_message() else: self._show_error_message() @pyqtSlot(object) def _on_print_preflight_check_succeeded(self, status: ExportStatus) -> None: # We don't use the ExportStatus for now for "success" status, # but in future work we will migrate towards a wizard-style dialog, where # success and intermediate status values all use the same PyQt slot. # If the continue button is disabled then this is the result of a background preflight check self.status = status self.stop_animate_header() self.header_icon.update_image("printer.svg", svg_size=QSize(64, 64)) self.header.setText(self.ready_header) if not self.continue_button.isEnabled(): self.continue_button.clicked.disconnect() self.continue_button.clicked.connect(self._print_file) self.continue_button.setEnabled(True) self.continue_button.setFocus() return self._print_file() @pyqtSlot(object) def _on_print_preflight_check_failed(self, status: ExportStatus) -> None: self.status = status self.stop_animate_header() self.header_icon.update_image("printer.svg", svg_size=QSize(64, 64)) # If the continue button is disabled then this is the result of a background preflight check if not self.continue_button.isEnabled(): self.continue_button.clicked.disconnect() if status == ExportStatus.ERROR_PRINTER_NOT_FOUND: self.continue_button.clicked.connect(self._show_insert_usb_message) else: self.continue_button.clicked.connect(self._show_error_message) self.continue_button.setEnabled(True) self.continue_button.setFocus() elif status == ExportStatus.ERROR_PRINTER_NOT_FOUND: self._show_insert_usb_message() else: self._show_error_message()