Skip to content

Commit

Permalink
UX parity between prototypes and coded client
Browse files Browse the repository at this point in the history
  • Loading branch information
sssoleileraaa committed Mar 19, 2019
1 parent 8ff8880 commit 36c25c9
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 67 deletions.
38 changes: 23 additions & 15 deletions securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QDesktopWidget, QStatusBar
from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget, \
QStatusBar
from typing import List

from securedrop_client import __version__
from securedrop_client.db import Source
from securedrop_client.gui.widgets import (ToolBar, MainView, LoginDialog,
SourceConversationWrapper)
from securedrop_client.gui.widgets import ToolBar, MainView, LoginDialog, SourceConversationWrapper
from securedrop_client.resources import load_icon

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -57,18 +57,29 @@ def __init__(self, sdc_home: str):
self.setWindowTitle(_("SecureDrop Client {}").format(__version__))
self.setWindowIcon(load_icon(self.icon))

self.central_widget = QWidget()
central_widget_layout = QVBoxLayout()
central_widget_layout.setContentsMargins(0, 0, 0, 0)
self.central_widget.setLayout(central_widget_layout)
self.setCentralWidget(self.central_widget)

self.status_bar = QStatusBar(self)
self.status_bar.setStyleSheet('background-color: #fff;')
central_widget_layout.addWidget(self.status_bar)

self.widget = QWidget()
widget_layout = QVBoxLayout()
widget_layout = QHBoxLayout()
widget_layout.setContentsMargins(0, 0, 0, 0)
self.widget.setLayout(widget_layout)
self.setCentralWidget(self.widget)

self.tool_bar = ToolBar(self.widget)

self.main_view = MainView(self.widget)
self.main_view.source_list.itemSelectionChanged.connect(self.on_source_changed)

widget_layout.addWidget(self.tool_bar, 1)
widget_layout.addWidget(self.main_view, 6)
widget_layout.addWidget(self.main_view, 8)

central_widget_layout.addWidget(self.widget)

# Cache a dict of source.uuid -> SourceConversationWrapper
# We do this to not create/destroy widgets constantly (because it causes UI "flicker")
Expand All @@ -88,9 +99,7 @@ def setup(self, controller):
self.controller = controller # Reference the Client logic instance.
self.tool_bar.setup(self, controller)

self.status_bar = QStatusBar(self)
self.setStatusBar(self.status_bar)
self.set_status('Started SecureDrop Client. Please sign in.', 20000)
self.set_status(_('Started SecureDrop Client. Please sign in.'), 20000)

self.login_dialog = LoginDialog(self)
self.main_view.setup(self.controller)
Expand Down Expand Up @@ -144,10 +153,9 @@ def show_sync(self, updated_on):
Display a message indicating the data-sync state.
"""
if updated_on:
self.main_view.status.setText('Last refresh: ' +
updated_on.humanize())
self.set_status(_('Last refresh: {}').format(updated_on.humanize()))
else:
self.main_view.status.setText(_('Waiting to refresh...'))
self.set_status(_('Waiting to refresh...'), 5000)

def set_logged_in_as(self, username):
"""
Expand Down Expand Up @@ -188,9 +196,9 @@ def show_conversation_for(self, source: Source, is_authenticated: bool):

self.main_view.set_conversation(conversation_container)

def set_status(self, message, duration=5000):
def set_status(self, message, duration=0):
"""
Display a status message to the user. Optionally, supply a duration
(in milliseconds), the default value being a duration of 5 seconds.
(in milliseconds), the default will continuously show the message.
"""
self.status_bar.showMessage(message, duration)
113 changes: 73 additions & 40 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QListWidget, QLabel, QWidget, QListWidgetItem, QHBoxLayout, \
QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, \
QMessageBox, QToolButton, QSizePolicy, QTextEdit
QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, QMessageBox, \
QToolButton, QSizePolicy, QTextEdit
from typing import List
from uuid import uuid4

Expand All @@ -43,29 +43,42 @@ class ToolBar(QWidget):

def __init__(self, parent: QWidget):
super().__init__(parent)
layout = QHBoxLayout(self)
self.logo = QLabel()
self.logo.setPixmap(load_image('header_logo.png'))
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)

self.user_state = QLabel(_("Signed out."))
self.user_state = QLabel(_('Signed out.'))

self.login = QPushButton(_('Sign in'))
self.login.setMaximumSize(80, 30)
self.login.clicked.connect(self.on_login_clicked)

self.logout = QPushButton(_('Sign out'))
self.logout.clicked.connect(self.on_logout_clicked)
self.logout.setMaximumSize(80, 30)
self.logout.setVisible(False)

self.refresh = QPushButton(_('Refresh'))
self.refresh = QPushButton()
self.refresh.clicked.connect(self.on_refresh_clicked)
self.refresh.setVisible(False)
self.refresh.setMaximumSize(30, 30)
refresh_pixmap = load_image('refresh.svg')

self.refresh.setIcon(QIcon(refresh_pixmap))
self.refresh.show()

self.logo = QLabel()
self.logo.setPixmap(load_image('icon.png'))
self.logo.setMinimumSize(200, 200)

journalist_layout = QHBoxLayout(self)
journalist_layout.addWidget(self.refresh, 1)
journalist_layout.addWidget(self.user_state, 5)
journalist_layout.addWidget(self.login, 5)
journalist_layout.addWidget(self.logout, 5)
journalist_layout.addStretch()

layout.addLayout(journalist_layout)
layout.addWidget(self.logo)
layout.addStretch()
layout.addWidget(self.user_state)
layout.addWidget(self.login)
layout.addWidget(self.logout)
layout.addWidget(self.refresh)

def setup(self, window, controller):
"""
Expand All @@ -84,7 +97,7 @@ def set_logged_in_as(self, username):
"""
Update the UI to reflect that the user is logged in as "username".
"""
self.user_state.setText(_('Signed in as: ' + html.escape(username)))
self.user_state.setText((html.escape(username)))
self.login.setVisible(False)
self.logout.setVisible(True)
self.refresh.setVisible(True)
Expand Down Expand Up @@ -132,27 +145,28 @@ class MainView(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.layout = QHBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)

left_column = QWidget(parent=self)
left_layout = QVBoxLayout()
left_layout.setContentsMargins(0, 0, 0, 0)
left_column.setLayout(left_layout)

self.status = QLabel(_('Waiting to refresh...'))
self.error_status = QLabel('')
self.error_status.setObjectName('error_label')

left_layout.addWidget(self.status)
left_layout.addWidget(self.error_status)

self.source_list = SourceList(left_column)
left_layout.addWidget(self.source_list)

self.layout.addWidget(left_column, 2)
self.layout.addWidget(left_column, 4)

self.view_holder = QWidget()
self.view_layout = QVBoxLayout()
self.view_layout.setContentsMargins(0, 0, 0, 0)
self.view_holder = QWidget()
self.view_holder.setLayout(self.view_layout)

self.layout.addWidget(self.view_holder, 6)

def setup(self, controller):
Expand Down Expand Up @@ -372,11 +386,13 @@ def setup(self, controller):
self.setWindowTitle(_('Sign in to SecureDrop'))

main_layout = QHBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addStretch()
self.setLayout(main_layout)

form = QWidget()
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
form.setLayout(layout)

main_layout.addWidget(form)
Expand Down Expand Up @@ -486,15 +502,14 @@ class SpeechBubble(QWidget):
and journalist.
"""

css = "padding: 10px; min-height:20px;border: 1px solid #999; border-radius: 18px;"
css = "padding:8px; min-height:32px; border:1px solid #999; border-radius:18px;"

def __init__(self, message_id: str, text: str, update_signal) -> None:
super().__init__()
self.message_id = message_id

layout = QVBoxLayout()
self.setLayout(layout)

self.message = QLabel(html.escape(text, quote=False))
self.message.setWordWrap(True)
self.message.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
Expand All @@ -508,6 +523,7 @@ def _update_text(self, message_id: str, text: str) -> None:
Conditionally update this SpeechBubble's text if and only if the message_id of the emitted
signal matches the message_id of this speech bubble.
"""

if message_id == self.message_id:
self.message.setText(html.escape(text, quote=False))

Expand All @@ -529,12 +545,14 @@ def __init__(self,
"""
super().__init__()
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)

label = SpeechBubble(message_id, message, update_signal)

if align != "left":
# Float right...
layout.addStretch(5)
label.setStyleSheet(label.css + 'border-bottom-right-radius: 0px;')
label.setStyleSheet(label.css)

layout.addWidget(label, 6)

Expand All @@ -543,10 +561,7 @@ def __init__(self,
layout.addStretch(5)
label.setStyleSheet(label.css + 'border-bottom-left-radius: 0px;')

layout.setContentsMargins(0, 0, 0, 0)

self.setLayout(layout)
self.setContentsMargins(0, 0, 0, 0)


class MessageWidget(ConversationWidget):
Expand Down Expand Up @@ -679,6 +694,7 @@ def __init__(self, source_db_object: Source, sdc_home: str, controller: Client,

self.container = QWidget()
self.conversation_layout = QVBoxLayout()
self.conversation_layout.setContentsMargins(0, 0, 0, 0)
self.container.setLayout(self.conversation_layout)
self.container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

Expand All @@ -694,6 +710,7 @@ def __init__(self, source_db_object: Source, sdc_home: str, controller: Client,
sb.rangeChanged.connect(self.update_conversation_position)

main_layout = QVBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(self.scroll)
self.setLayout(main_layout)
self.update_conversation(self.source.collection)
Expand Down Expand Up @@ -788,14 +805,15 @@ def __init__(
self.sdc_home = sdc_home

self.layout = QVBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)

self.conversation = ConversationView(self.source, self.sdc_home, self.controller,
parent=self)
self.source_profile = SourceProfileShortWidget(self.source, self.controller)

self.layout.addWidget(self.source_profile)
self.layout.addWidget(self.conversation)
self.layout.addWidget(self.source_profile, 1)
self.layout.addWidget(self.conversation, 9)

self.controller.authentication_state.connect(self._show_or_hide_replybox)
self._show_or_hide_replybox(is_authenticated)
Expand All @@ -816,7 +834,7 @@ def _show_or_hide_replybox(self, show: bool) -> None:
old_widget.widget().deleteLater()

self.reply_box = new_widget
self.layout.addWidget(new_widget)
self.layout.addWidget(new_widget, 3)


class ReplyBoxWidget(QWidget):
Expand All @@ -830,12 +848,20 @@ def __init__(self, conversation: SourceConversationWrapper) -> None:

self.text_edit = QTextEdit()

self.send_button = QPushButton('Send')
self.send_button = QPushButton()
self.send_button.clicked.connect(self.send_reply)
self.send_button.setMaximumSize(40, 40)

layout = QHBoxLayout()
button_pixmap = load_image('send.png')
button_icon = QIcon(button_pixmap)
self.send_button.setIcon(button_icon)
self.send_button.setIconSize(button_pixmap.rect().size())

layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.text_edit)
layout.addWidget(self.send_button)

layout.addWidget(self.send_button, 0, Qt.AlignRight)
self.setLayout(layout)

def send_reply(self) -> None:
Expand Down Expand Up @@ -915,12 +941,19 @@ def __init__(self, source, controller):


class TitleLabel(QLabel):
"""Centered aligned, HTML heading level 3 label."""
"""The title for a conversation."""

def __init__(self, text):
html_text = "<h3>%s</h3>" % (text,)
super().__init__(_(html_text))
self.setAlignment(Qt.AlignCenter)
html_text = _('<h1>{}</h1>').format(text)
super().__init__(html_text)


class LastUpdatedLabel(QLabel):
"""Time the conversation was last updated."""

def __init__(self, last_updated):
html_text = _('<h3>{}</h3>').format(arrow.get(last_updated).humanize())
super().__init__(html_text)


class SourceProfileShortWidget(QWidget):
Expand All @@ -939,10 +972,10 @@ def __init__(self, source, controller):
self.layout = QHBoxLayout()
self.setLayout(self.layout)

widgets = (
TitleLabel(self.source.journalist_designation),
SourceMenuButton(self.source, self.controller),
)
self.title = TitleLabel(self.source.journalist_designation)
self.updated = LastUpdatedLabel(self.source.last_updated)
self.menu = SourceMenuButton(self.source, self.controller)

for widget in widgets:
self.layout.addWidget(widget)
self.layout.addWidget(self.title, 10, Qt.AlignLeft)
self.layout.addWidget(self.updated, 1, Qt.AlignRight)
self.layout.addWidget(self.menu, 1, Qt.AlignRight)
Binary file added securedrop_client/resources/images/delete.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions securedrop_client/resources/images/refresh.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added securedrop_client/resources/images/send.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added securedrop_client/resources/images/trash.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 36c25c9

Please sign in to comment.