Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UX parity between prototypes and coded client #270

Merged
merged 1 commit into from
Mar 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, \
sssoleileraaa marked this conversation as resolved.
Show resolved Hide resolved
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)
119 changes: 76 additions & 43 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()
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.source_list = SourceList(left_column)
left_layout.addWidget(self.source_list)

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
Loading
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
Loading
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
Loading
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading