client/securedrop_client/gui/base/dialogs.py (157 lines of code) (raw):

""" A SecureDrop-themed modal dialog. Copyright (C) 2021 The Freedom of the Press Foundation. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ from gettext import gettext as _ from PyQt5.QtCore import QSize, Qt from PyQt5.QtGui import QIcon, QKeyEvent, QPixmap from PyQt5.QtWidgets import ( QApplication, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget, ) from securedrop_client.gui.base.misc import SvgLabel from securedrop_client.resources import load_movie, load_relative_css class ModalDialog(QDialog): DIALOG_CSS = load_relative_css(__file__, "dialogs.css") BUTTON_CSS = load_relative_css(__file__, "dialog_button.css") ERROR_DETAILS_CSS = load_relative_css(__file__, "dialog_message.css") MARGIN = 40 NO_MARGIN = 0 def __init__(self, show_header: bool = True, dangerous: bool = False) -> None: parent = QApplication.activeWindow() super().__init__(parent) self.setObjectName("ModalDialog") self.setStyleSheet(self.DIALOG_CSS) self.setModal(True) self.show_header = show_header self.dangerous = dangerous if self.dangerous: self.setProperty("class", "dangerous") # Widget for displaying error messages self.error_details = QLabel() self.error_details.setObjectName("ModalDialog_error_details") self.error_details.setStyleSheet(self.ERROR_DETAILS_CSS) self.error_details.setWordWrap(True) self.error_details.hide() # Body to display instructions and forms self.body = QLabel() self.body.setObjectName("ModalDialog_body") self.body.setWordWrap(True) self.body.setScaledContents(True) body_container = QWidget() self.body_layout = QVBoxLayout() self.body_layout.setContentsMargins( self.NO_MARGIN, self.NO_MARGIN, self.NO_MARGIN, self.NO_MARGIN ) body_container.setLayout(self.body_layout) self.body_layout.addWidget(self.body) # Main widget layout layout = QVBoxLayout(self) layout.setContentsMargins(self.MARGIN, self.MARGIN, self.MARGIN, self.MARGIN) self.setLayout(layout) if self.show_header: # Header for icon and task title header_container = QWidget() header_container_layout = QHBoxLayout() header_container.setLayout(header_container_layout) self.header_icon = SvgLabel("blank.svg", svg_size=QSize(64, 64)) self.header_icon.setObjectName("ModalDialog_header_icon") self.header_spinner = QPixmap() self.header_spinner_label = QLabel() self.header_spinner_label.setObjectName("ModalDialog_header_spinner") self.header_spinner_label.setMinimumSize(64, 64) self.header_spinner_label.setVisible(False) self.header_spinner_label.setPixmap(self.header_spinner) self.header = QLabel() self.header.setObjectName("ModalDialog_header") header_container_layout.addWidget(self.header_icon) header_container_layout.addWidget(self.header_spinner_label) header_container_layout.addWidget(self.header, alignment=Qt.AlignCenter) header_container_layout.addStretch() self.header_line = QWidget() self.header_line.setObjectName("ModalDialog_header_line") layout.addWidget(header_container) layout.addWidget(self.header_line) layout.addWidget(self.error_details) layout.addWidget(body_container) layout.addWidget(self.configure_buttons()) # Activestate animation. self.button_animation = load_movie("activestate-wide.gif") self.button_animation.setScaledSize(QSize(32, 32)) self.button_animation.frameChanged.connect(self.animate_activestate) # Header animation. self.header_animation = load_movie("header_animation.gif") self.header_animation.setScaledSize(QSize(64, 64)) self.header_animation.frameChanged.connect(self.animate_header) def configure_buttons(self) -> QWidget: # Buttons to continue and cancel window_buttons = QWidget() window_buttons.setObjectName("ModalDialog_window_buttons") button_layout = QVBoxLayout() window_buttons.setLayout(button_layout) self.cancel_button = QPushButton(_("CANCEL")) self.cancel_button.setStyleSheet(self.BUTTON_CSS) self.continue_button = QPushButton(_("CONTINUE")) self.continue_button.setStyleSheet(self.BUTTON_CSS) self.continue_button.setIconSize(QSize(21, 21)) button_box = QDialogButtonBox(Qt.Horizontal) button_box.setObjectName("ModalDialog_button_box") if self.dangerous: self.cancel_button.setAutoDefault(True) self.continue_button.setDefault(False) self.cancel_button.setObjectName("ModalDialog_primary_button") self.continue_button.setObjectName("ModalDialog_cancel_button") else: self.cancel_button.setAutoDefault(False) self.continue_button.setDefault(True) self.cancel_button.setObjectName("ModalDialog_cancel_button") self.continue_button.setObjectName("ModalDialog_primary_button") button_box.addButton(self.cancel_button, QDialogButtonBox.RejectRole) button_box.addButton(self.continue_button, QDialogButtonBox.AcceptRole) button_box.rejected.connect(self.reject) button_box.accepted.connect(self.accept) self.confirmation_label = QLabel() self.confirmation_label.setObjectName("ModalDialogConfirmation") button_layout.addWidget(self.confirmation_label, 0, Qt.AlignLeft | Qt.AlignBottom) button_layout.addWidget(button_box, alignment=Qt.AlignLeft) button_layout.setContentsMargins( self.NO_MARGIN, self.NO_MARGIN, self.NO_MARGIN, self.NO_MARGIN ) return window_buttons def keyPressEvent(self, event: QKeyEvent) -> None: if event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return: if self.cancel_button.hasFocus(): self.cancel_button.click() else: self.continue_button.click() else: super().keyPressEvent(event) def animate_activestate(self) -> None: self.continue_button.setIcon(QIcon(self.button_animation.currentPixmap())) def animate_header(self) -> None: self.header_spinner_label.setPixmap(self.header_animation.currentPixmap()) def start_animate_activestate(self) -> None: self.button_animation.start() self.continue_button.setText("") self.continue_button.setMinimumSize(QSize(142, 43)) # Reset widget stylesheets self.continue_button.setStyleSheet("") self.continue_button.setObjectName("ModalDialog_primary_button_active") self.continue_button.setStyleSheet(self.BUTTON_CSS) self.error_details.setStyleSheet("") self.error_details.setObjectName("ModalDialog_error_details_active") self.error_details.setStyleSheet(self.ERROR_DETAILS_CSS) def start_animate_header(self) -> None: self.header_icon.setVisible(False) self.header_spinner_label.setVisible(True) self.header_animation.start() def stop_animate_activestate(self) -> None: self.continue_button.setIcon(QIcon()) self.button_animation.stop() self.continue_button.setText(_("CONTINUE")) # Reset widget stylesheets self.continue_button.setStyleSheet("") self.continue_button.setObjectName("ModalDialog_primary_button") self.continue_button.setStyleSheet(self.BUTTON_CSS) self.error_details.setStyleSheet("") self.error_details.setObjectName("ModalDialog_error_details") self.error_details.setStyleSheet(self.ERROR_DETAILS_CSS) def stop_animate_header(self) -> None: self.header_icon.setVisible(True) self.header_spinner_label.setVisible(False) self.header_animation.stop() def text(self) -> str: """A text-only representation of the dialog.""" return self.body.text()