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 @@
+
+
\ 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 @@
+
+
\ 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()