diff --git a/securedrop_client/gui/auth/dialog.py b/securedrop_client/gui/auth/dialog.py index f0f56aa0b..52d194342 100644 --- a/securedrop_client/gui/auth/dialog.py +++ b/securedrop_client/gui/auth/dialog.py @@ -37,6 +37,7 @@ from securedrop_client.gui.auth.sign_in import LoginErrorBar, SignInButton from securedrop_client.gui.auth.use_offline import LoginOfflineLink from securedrop_client.gui.base import PasswordEdit +from securedrop_client.gui.base.checkbox import SDCheckBox from securedrop_client.logic import Controller from securedrop_client.resources import load_image @@ -96,6 +97,9 @@ def __init__(self, parent: QWidget) -> None: self.password_label = QLabel(_("Passphrase")) self.password_field = PasswordEdit(self) + self.check = SDCheckBox() + self.check.checkbox.stateChanged.connect(self.password_field.on_toggle_password_Action) + self.tfa_label = QLabel(_("Two-Factor Code")) self.tfa_field = QLineEdit() @@ -117,7 +121,7 @@ def __init__(self, parent: QWidget) -> None: form_layout.addWidget(QWidget(self)) form_layout.addWidget(self.password_label) form_layout.addWidget(self.password_field) - form_layout.addWidget(QWidget(self)) + form_layout.addWidget(self.check, alignment=Qt.AlignRight) form_layout.addWidget(self.tfa_label) form_layout.addWidget(self.tfa_field) form_layout.addWidget(buttons) diff --git a/securedrop_client/gui/base/checkbox.css b/securedrop_client/gui/base/checkbox.css new file mode 100644 index 000000000..0ba544ed8 --- /dev/null +++ b/securedrop_client/gui/base/checkbox.css @@ -0,0 +1,18 @@ +#ShowPassphrase_widget QFrame{ + padding: 5px 3px 3px 5px; +} +#ShowPassphrase_widget QLabel{ + font-family: 'Source Sans Pro'; + font-weight: 500; + font-size: 12px; + line-height: 15px; + padding: 0px 0px 0px 3px; +} + +#ShowPassphrase_widget QFrame:hover { + background-color: rgba(230, 253, 255, 0.2); +} + +#ShowPassphrase_widget QLabel:hover { + background-color: none; +} diff --git a/securedrop_client/gui/base/checkbox.py b/securedrop_client/gui/base/checkbox.py new file mode 100644 index 000000000..b7692d123 --- /dev/null +++ b/securedrop_client/gui/base/checkbox.py @@ -0,0 +1,51 @@ +""" +SecureDrop customized passphrase checkbox control + +A checkbox control created to toggle with hiding and showing PasswordEdit passphrases. +Consists of a QCheckBox and a QLabel positioned horizontally within a QFrame. +Present in the Sign-in and Export Dialog. +""" +from gettext import gettext as _ + +from pkg_resources import resource_string +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QCursor, QFont, QMouseEvent +from PyQt5.QtWidgets import QCheckBox, QFrame, QHBoxLayout, QLabel, QWidget + + +class SDCheckBox(QWidget): + clicked = pyqtSignal() + CHECKBOX_CSS = resource_string(__name__, "checkbox.css").decode("utf-8") + PASSPHRASE_LABEL_SPACING = 1 + + def __init__(self) -> None: + super().__init__() + self.setObjectName("ShowPassphrase_widget") + self.setStyleSheet(self.CHECKBOX_CSS) + + font = QFont() + font.setLetterSpacing(QFont.AbsoluteSpacing, self.PASSPHRASE_LABEL_SPACING) + + self.layout = QHBoxLayout() + self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.setSpacing(0) + self.setLayout(self.layout) + + self.frame = QFrame() + self.frame.setLayout(QHBoxLayout()) + self.frame.layout().setContentsMargins(0, 0, 0, 0) + self.frame.layout().setSpacing(0) + + self.checkbox = QCheckBox() + self.label = QLabel(_("Show Passphrase")) + self.label.setFont(font) + + self.layout.addWidget(self.frame) + self.frame.layout().addWidget(self.checkbox) + self.frame.layout().addWidget(self.label) + self.frame.setCursor(QCursor(Qt.PointingHandCursor)) + + self.clicked.connect(self.checkbox.click) + + def mousePressEvent(self, e: QMouseEvent) -> None: + self.clicked.emit() diff --git a/securedrop_client/gui/base/inputs.py b/securedrop_client/gui/base/inputs.py index 66f1c6cd2..ff2167fac 100644 --- a/securedrop_client/gui/base/inputs.py +++ b/securedrop_client/gui/base/inputs.py @@ -18,8 +18,6 @@ """ from PyQt5.QtWidgets import QDialog, QLineEdit -from securedrop_client.resources import load_icon - class PasswordEdit(QLineEdit): """ @@ -30,20 +28,13 @@ def __init__(self, parent: QDialog) -> None: self.parent = parent super().__init__(self.parent) - self.visibleIcon = load_icon("eye_visible.svg") - self.hiddenIcon = load_icon("eye_hidden.svg") - self.setEchoMode(QLineEdit.Password) - self.togglepasswordAction = self.addAction(self.hiddenIcon, QLineEdit.TrailingPosition) - self.togglepasswordAction.triggered.connect(self.on_toggle_password_Action) self.password_shown = False def on_toggle_password_Action(self) -> None: if not self.password_shown: self.setEchoMode(QLineEdit.Normal) self.password_shown = True - self.togglepasswordAction.setIcon(self.visibleIcon) else: self.setEchoMode(QLineEdit.Password) self.password_shown = False - self.togglepasswordAction.setIcon(self.hiddenIcon) diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index 980f3f594..92decafa6 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -84,6 +84,7 @@ SvgPushButton, SvgToggleButton, ) +from securedrop_client.gui.base.checkbox import SDCheckBox from securedrop_client.gui.conversation import DeleteConversationDialog from securedrop_client.gui.source import DeleteSourceDialog from securedrop_client.logic import Controller @@ -2683,8 +2684,13 @@ def __init__(self, controller: Controller, file_uuid: str, file_name: str) -> No effect.setBlurRadius(4) effect.setColor(QColor("#aaa")) self.passphrase_field.setGraphicsEffect(effect) + + check = SDCheckBox() + check.checkbox.stateChanged.connect(self.passphrase_field.on_toggle_password_Action) + passphrase_form_layout.addWidget(passphrase_label) passphrase_form_layout.addWidget(self.passphrase_field) + passphrase_form_layout.addWidget(check, alignment=Qt.AlignRight) self.body_layout.addWidget(self.passphrase_form) self.passphrase_form.hide() diff --git a/securedrop_client/locale/messages.pot b/securedrop_client/locale/messages.pot index de4b31e3f..a5023dcea 100644 --- a/securedrop_client/locale/messages.pot +++ b/securedrop_client/locale/messages.pot @@ -271,6 +271,9 @@ msgstr "" msgid "USE OFFLINE" msgstr "" +msgid "Show Passphrase" +msgstr "" + msgid "CANCEL" msgstr "" diff --git a/tests/gui/base/test_inputs.py b/tests/gui/base/test_inputs.py index 0b801ef8c..5181d37d7 100644 --- a/tests/gui/base/test_inputs.py +++ b/tests/gui/base/test_inputs.py @@ -1,4 +1,4 @@ -from PyQt5.QtWidgets import QApplication, QLineEdit +from PyQt5.QtWidgets import QApplication, QCheckBox, QLineEdit from securedrop_client.gui.base import PasswordEdit @@ -6,9 +6,11 @@ def test_PasswordEdit(mocker): + checkbox = QCheckBox() passwordline = PasswordEdit(None) - passwordline.togglepasswordAction.trigger() + passwordline.on_toggle_password_Action() + checkbox.isChecked() assert passwordline.echoMode() == QLineEdit.Normal - passwordline.togglepasswordAction.trigger() + passwordline.on_toggle_password_Action() assert passwordline.echoMode() == QLineEdit.Password diff --git a/tests/gui/base/test_sdcheckbox.py b/tests/gui/base/test_sdcheckbox.py new file mode 100644 index 000000000..291de8ac4 --- /dev/null +++ b/tests/gui/base/test_sdcheckbox.py @@ -0,0 +1,15 @@ +from PyQt5.QtTest import QSignalSpy +from PyQt5.QtWidgets import QApplication + +from securedrop_client.gui.base.checkbox import SDCheckBox + +app = QApplication([]) + + +def test_SDCheckBox(): + checkbox_area = SDCheckBox() + signal_emissions = QSignalSpy(checkbox_area.clicked) + + checkbox_area.mousePressEvent(None) + + assert len(signal_emissions) == 1 diff --git a/tests/integration/test_styles_sdclient.py b/tests/integration/test_styles_sdclient.py index 7961c1b71..85455eedd 100644 --- a/tests/integration/test_styles_sdclient.py +++ b/tests/integration/test_styles_sdclient.py @@ -154,9 +154,9 @@ def test_styles_for_login_dialog(mocker, main_window): form = login_dialog.layout().itemAt(2).widget() form_children_qlabel = form.findChildren(QLabel) for c in form_children_qlabel: - assert "Montserrat" == c.font().family() + assert "Montserrat" == c.font().family() or "Source Sans Pro" == c.font().family() assert QFont.DemiBold - 1 == c.font().weight() - assert 13 == c.font().pixelSize() + assert 13 == c.font().pixelSize() or 12 == c.font().pixelSize() assert "#ffffff" == c.palette().color(QPalette.Foreground).name() form_children_qlineedit = form.findChildren(QLineEdit) for c in form_children_qlineedit: @@ -550,7 +550,7 @@ def test_styles_for_export_dialog(export_dialog): passphrase_children_qlabel = export_dialog.passphrase_form.findChildren(QLabel) for c in passphrase_children_qlabel: - assert "Montserrat" == c.font().family() + assert "Montserrat" == c.font().family() or "Source Sans Pro" == c.font().family() assert QFont.DemiBold - 1 == c.font().weight() assert 12 == c.font().pixelSize() assert "#2a319d" == c.palette().color(QPalette.Foreground).name()