diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index b90b78850..cbc188dfc 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -1304,6 +1304,43 @@ def clear_message(self): self.hide() +class PasswordEdit(QLineEdit): + """ + A LineEdit with icons to show/hide password entries + """ + CSS = '''QLineEdit { + border-radius: 0px; + height: 30px; + margin: 0px 0px 0px 0px; + } + ''' + + def __init__(self, parent): + self.parent = parent + super().__init__(self.parent) + + # Set styles + self.setStyleSheet(self.CSS) + + self.visibleIcon = load_icon("eye_visible.svg") + self.hiddenIcon = load_icon("eye_hidden.svg") + + self.setEchoMode(QLineEdit.Password) + self.togglepasswordAction = self.addAction(self.visibleIcon, QLineEdit.TrailingPosition) + self.togglepasswordAction.triggered.connect(self.on_toggle_password_Action) + self.password_shown = False + + def on_toggle_password_Action(self): + if not self.password_shown: + self.setEchoMode(QLineEdit.Normal) + self.password_shown = True + self.togglepasswordAction.setIcon(self.hiddenIcon) + else: + self.setEchoMode(QLineEdit.Password) + self.password_shown = False + self.togglepasswordAction.setIcon(self.visibleIcon) + + class LoginDialog(QDialog): """ A dialog to display the login form. @@ -1319,7 +1356,7 @@ class LoginDialog(QDialog): #login_form QLineEdit { border-radius: 0px; height: 30px; - margin: 0px 0px 10px 0px; + margin: 0px 0px 0px 0px; } ''' @@ -1371,8 +1408,7 @@ def __init__(self, parent): self.username_field = QLineEdit() self.password_label = QLabel(_('Passphrase')) - self.password_field = QLineEdit() - self.password_field.setEchoMode(QLineEdit.Password) + self.password_field = PasswordEdit(self) self.tfa_label = QLabel(_('Two-Factor Code')) self.tfa_field = QLineEdit() @@ -1390,8 +1426,10 @@ def __init__(self, parent): form_layout.addWidget(self.username_label) form_layout.addWidget(self.username_field) + 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.tfa_label) form_layout.addWidget(self.tfa_field) form_layout.addWidget(buttons) diff --git a/securedrop_client/resources/images/eye_hidden.svg b/securedrop_client/resources/images/eye_hidden.svg new file mode 100644 index 000000000..193f3d8c1 --- /dev/null +++ b/securedrop_client/resources/images/eye_hidden.svg @@ -0,0 +1,23 @@ + + + + Group 7 + Created with Sketch. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/securedrop_client/resources/images/eye_visible.svg b/securedrop_client/resources/images/eye_visible.svg new file mode 100644 index 000000000..26cdc2ba7 --- /dev/null +++ b/securedrop_client/resources/images/eye_visible.svg @@ -0,0 +1,23 @@ + + + + Group 7 + Created with Sketch. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index d2611a7cf..56e4fa753 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -6,7 +6,8 @@ from PyQt5.QtCore import Qt, QEvent from PyQt5.QtGui import QFocusEvent -from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QMessageBox, QMainWindow +from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QMessageBox, QMainWindow, \ + QLineEdit from sqlalchemy.orm import scoped_session, sessionmaker from securedrop_client import db, logic @@ -16,7 +17,7 @@ DeleteSourceMessageBox, DeleteSourceAction, SourceMenu, TopPane, LeftPane, RefreshButton, \ ErrorStatusBar, ActivityStatusBar, UserProfile, UserButton, UserMenu, LoginButton, \ ReplyBoxWidget, ReplyTextEdit, SourceConversationWrapper, StarToggleButton, LoginOfflineLink, \ - LoginErrorBar, EmptyConversationView, ExportDialog, PrintDialog + LoginErrorBar, EmptyConversationView, ExportDialog, PrintDialog, PasswordEdit from tests import factory @@ -2322,6 +2323,15 @@ def test_DeleteSourceAction_init(mocker): ) +def test_PasswordEdit(mocker): + passwordline = PasswordEdit(None) + passwordline.togglepasswordAction.trigger() + + assert passwordline.echoMode() == QLineEdit.Normal + passwordline.togglepasswordAction.trigger() + assert passwordline.echoMode() == QLineEdit.Password + + def test_DeleteSourceAction_trigger(mocker): mock_controller = mocker.MagicMock() mock_source = mocker.MagicMock()