From 18cbc03c93406f422ed11cf7e4f4f1c11a469329 Mon Sep 17 00:00:00 2001 From: Allie Crevier <4522213+creviera@users.noreply.github.com> Date: Fri, 8 Mar 2019 09:39:30 -0800 Subject: [PATCH] UX parity between prototypes and coded client --- securedrop_client/gui/main.py | 38 +++--- securedrop_client/gui/widgets.py | 117 ++++++++++++------ .../resources/images/branding.png | Bin 0 -> 22768 bytes securedrop_client/resources/images/delete.png | Bin 0 -> 2275 bytes .../resources/images/refresh.png | Bin 0 -> 1535 bytes securedrop_client/resources/images/send.png | Bin 0 -> 2275 bytes securedrop_client/resources/images/trash.png | Bin 0 -> 1846 bytes tests/gui/test_main.py | 18 +-- tests/gui/test_widgets.py | 9 +- 9 files changed, 114 insertions(+), 68 deletions(-) create mode 100644 securedrop_client/resources/images/branding.png create mode 100644 securedrop_client/resources/images/delete.png create mode 100644 securedrop_client/resources/images/refresh.png create mode 100644 securedrop_client/resources/images/send.png create mode 100644 securedrop_client/resources/images/trash.png diff --git a/securedrop_client/gui/main.py b/securedrop_client/gui/main.py index 877da095c8..ae08e2c867 100644 --- a/securedrop_client/gui/main.py +++ b/securedrop_client/gui/main.py @@ -20,13 +20,13 @@ along with this program. If not, see . """ 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__) @@ -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") @@ -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) @@ -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: ' + 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): """ @@ -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) diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index 56833dbb4c..d2327cd649 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -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 @@ -43,29 +43,44 @@ 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.setMaximumSize(30, 30) + refresh_pixmap = load_image('refresh.png') + refresh_icon = QIcon(refresh_pixmap) + self.refresh.setIcon(refresh_icon) + self.refresh.setIconSize(refresh_pixmap.rect().size()) self.refresh.setVisible(False) + self.logo = QLabel() + self.logo.setStyleSheet('background-color: #CCC;') + self.logo.setPixmap(load_image('branding.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): """ @@ -84,7 +99,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) @@ -132,27 +147,29 @@ 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) + # TODO: move this to the main window QStatusBar + # 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): @@ -174,8 +191,8 @@ def set_conversation(self, widget): if old_widget: old_widget.widget().setVisible(False) - self.view_layout.addWidget(widget) widget.setVisible(True) + self.view_layout.addWidget(widget) class SourceList(QListWidget): @@ -372,11 +389,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) @@ -485,7 +504,7 @@ 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__() @@ -493,7 +512,6 @@ def __init__(self, message_id: str, text: str, update_signal) -> None: layout = QVBoxLayout() self.setLayout(layout) - self.message = QLabel(html.escape(text, quote=False)) self.message.setWordWrap(True) self.message.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -507,6 +525,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)) @@ -528,12 +547,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) @@ -542,10 +563,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): @@ -678,6 +696,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) @@ -693,9 +712,9 @@ 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) def clear_conversation(self): @@ -788,14 +807,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) @@ -816,7 +836,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): @@ -830,12 +850,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: @@ -915,12 +943,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 = "

%s

" % (text,) + html_text = '

{}

'.format(text) + super().__init__(_(html_text)) + + +class LastUpdatedLabel(QLabel): + """Time the conversation was last updated.""" + + def __init__(self, last_updated): + html_text = '

{}

'.format(arrow.get(last_updated).humanize()) super().__init__(_(html_text)) - self.setAlignment(Qt.AlignCenter) class SourceProfileShortWidget(QWidget): @@ -939,10 +974,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_update) + 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) diff --git a/securedrop_client/resources/images/branding.png b/securedrop_client/resources/images/branding.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa7ce482a482cdc46c1aa6d6753b61e84f1f035 GIT binary patch literal 22768 zcmeEuXH-*Z+b%^wkRsB%bb|;ap;wU(fdD}TMU-BoHvtg^l-@%NB`76;m8O8A0-=Q} zMFc^lBMM3rK~c}MoK$qfYY-;aw@L%-4hBg5d6bIO#e<&&P z3b?^Rx*6HSkEU*3 z--kVR2l1nGu=0^pLqdx$*m|E{`iSW9np)zIaF*EYM}8?(+w98`rkx91ymH3k2G($n zQAr@%2!G~g`6=yVtF1SNeQ6q5Mx8RsITuNx{x{-IScwV$^}3!Q_WNa?e*Lf4L8**` zQkE3Fyg%tfYhGgR+{{iE?XXu9)RC@EQBxsd5;xgV=`ln3tZix@opi&|_H8OsSalK> zJ=;ad|E$Zz8b0U(2Ro}fSbhgrwMQ3kPFK8YnvmuCdb0m3x zapbexmKzyB6!Ic945RdL^G5ja7dIBH{1TRFV#uB}a==|^=&R!qLdvnN_zQ$b^$79Ux*zX4$d5+-X}dC83@;`W&zk~=bs2HK z8E^gsbGS>-wlZuT9b0@Bi%A-aPTq?Y>F5RvzdgcP%dM*!G>?2Sy+M+mrCPXKigm}) zVpDU%X}N8m2_!Zg=ehGv8oj~(sQh5O^C3N}>V*50*`9r3DKjn#?zx4q#QI8?lg*VI z)u=_U$-9bIil3|NJ(nDI?>r%;+(Q)my8M~>{kZ*(En5V^09KyK{~Wi`*nN?)MjETG zS1W1qu0>y+-^CVrx}h?>$b$jLKhF`mgHxRT3_q!vrC?>z<&;>y-GkW*tJ%+`von60 zP$ro{`(U%=yz)C!wGH;Ccxeaat1h)fJ2RE2)KDY* zJWPINe5Ytf;B@11Mf5uB)p?!RE63+y9_r(m(w}R%z6a;ON)-hZ4)B9IQQU#GEB>iv zYk3$A{pA06vG2bnLjUXayI6p#@w-@n!tqzJ0Dt`Te?otp&^z?|RbhDI*K0qA(eKwS zk=7l9fk2Y* zmV?-OO1Bt#@B=BTwp0FgLrZy+;p1q;xkcKd3cIRF6KYIpW?_XS?Qnx1EnYifit^m*V5ztak~~pmu=qS z?lL%<@kX#Y_%qUpcqr6r-FYT9{J_RfwfZmb2?M zIrgk$$0hMI4CSF|iuT{}PsXBRpE4#J63T2B<=--HGCX?#mh37To-}jVPVstBC;Na> zNNNx3W2+>4d72PwAr}H&NKW_ubKYH8b(*b_Y?Xqtg3c3p1|J-heP$P&!wl9`<p~&hPx4ea0pTXaf<)-N+0`*^THwz_U_{ zKP{bKm)lF#t=zdM!N!q~$bD70RGw{m20R1?!%L%l7=x40JL$GfD= ze%{+x_IgoZkD*E_bgD{JeWajbA_h;j?Do!Y^%l5Pn;7B7c*KO~v@w2hZ=AIoa~=^Y(g<1nHFNb@;uhZq&wzg9Z*5DJ;SI-$w zqs|I!KeGaRKg{`g%7Ku!3o(ttny(_-_x%z%>f_bbHx92QZK|G~E5bO*vgd4SQ z&}d{lhHV*k^21vW<+eW`Yp1_I@pxEbUqZaCDjwclRt#(cC2R-Y;O#X4~(JH!$=bn!2@8mg2Sb(7v1v*F#s}+Tw+a zsf`VLBGCJUi2JkQ9!0PNhUj`GHK!T$Qt!?+_N45PV(lgt@X&V zQ!r+yG4XvD#&%xACcs z0!&w(QysMIHr_c~lGm=Gi_zjDRV{4h5Sly1g`G=uJT`|bdNi+D+PIe2>G-v zF&5e9PEK*>Z=bU^6K{7iSZnk}Z&WyHm7M6WOEONtakeZiLGVr^$ca8Dv}oLZDs0

CxnSa~%qi(b7%Id>Ty-!8RYQWmIrtCh!G;Bn#a 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 E0Kq@sr~m)} literal 0 HcmV?d00001 diff --git a/securedrop_client/resources/images/delete.png b/securedrop_client/resources/images/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..6c9656c98a183ad6d284c315642aa9f5a344d413 GIT binary patch literal 2275 zcmeAS@N?(olHy`uVBq!ia0vp^Nq5QjEnx?oJHr&dIz4aySb-B8wRq zxP?KOkzv*x2?hoh?#z&gk_cZPtK|G#y~LFKq*T3%+ybC#1_ql7D* z7iAWdWaj57fXq!y$}cUkRZ;?31P4%e<`%#$$}5KY3g|!mio^naLp=k1Y??|k(-6)> z(FKyhsV%i46NfgC3`83^Fd%NU0fwlRZ+=Q3NvfTZfuW_YfswAEX^5e*m7$T9 zp{b2Nni_-;!MdG`QWHz^i$e1AbL<>J5vCB3kzbNuoRMFk;OwjbG|^l^!#6QGGY=%F z2{8^?6Owr#uUYvQWu^kd92By4hBo>bV(984gHnt0b4tPL&5fWMkOk2-IOpdU6r~my zBU^whhOQoAerR51i5<)eG+}gI;7|Z2%c9I=&%BbORAFK^Y7+nY0 zsz@ZOjFGKEl7MQo(FbK0r0fI9n_y93=CtDirVv<0x8riFnBWR5779IG978;KuZDT& zgocXjU0xf)lAU2y>96NB} zhzsN6{z)8}Qa)0_S<4SKoL|zLApCG@LB8_ zz&7X57J=!#N{UO)EJ+mKEZVp7=;X(jH*S#P+yAETAWo$xiyh(Yw3!6A8Je!Ru(Kd{AH$` zz6|5?_Oi3*WoGXCSQzdo#&YLDjp?l^>+~j>DRNIXUM?51xMqTDMssAA3s^Y6jed52~-CKIN>G`dFf0DWr;(Z@j-~1uF?dtVaLf7}k?kxE^EBDc> zInvU$OFFluZhP_b?Zk$Ymp?zBXR+_<73Fhsn`TwBt+}*h_i`J{pU-WrBl_lRS6@(l zD{IMr`1#rFpAUD-&TyVv7j#;lakZ;%+=ram(@)>OHF4iL|Ap*3xW(-be{oJ$j11{L zbFeME{=d0BU*Gdsxec#gRu2db=`mf){Cdq~IKC>{jIR8evuR+pH@RW9IM}2Kg z$^s(^&dciZU)R2jyPIesEhgua@+tXow1~m#7P-d1*QE86O#>_?HFo}fH#d9tE-}yc z`urKHuNBlq!oTnR_)TwT;NRV!uh#bLJ})76>-L>L8Bb=FMK!)aoE-pYE9yoYmhauj2_q?&hC)R&wCYYk8As{MWcQjeS1 z#FGt~oos)ut+nnqnOd_=at_PNr~fw_Hutn&OfxZ!n!VwCqSGs$>6w?VgfN}R5KV0T zrE+Bb_j?@+4xRCJHEQ>fi@0gfyy21UtVHd2Ub9M*6BQc&{gX`>s;&`n&V6`)k&d5$ zhn9^T%hbFb-zqmgPv=+r6M5nIu~`w1J3aVqvWmBw`5o>RKKxcSxx>$0>_7inTb{+b SN@=%2Ee}suKbLh*2~7Y(_Lz1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xps5^5D;1=Z-LwyDGpMFJRfxe-hfj%}(C7Edm=b`8V z$>7wMT9JuE8%PGC4ICH{H`)M0)XFzMB{QwkC9x#c&d9*fQrEyp*U&V?(Adh*$jZ>v zMjuTL!iQkp&PAz-CHX}m`T03^j-Uur2*}7U$uG{xFHmrHRsfo4uAt$Yn4Fmh64Znk zhpY+7JdoF{{EISEfng2`Svx};eGD;l^^rlT#rZjZ<`#|z0SQMB!?YMv`1eVe5xPtzFO9z%4y`CEnyZ+qz&NV)<-bBt<5-&0eSN;FS-qY!nY#!(wJ)?WJ=n4a~ z4CbyP33aOk=cqH&cvt5gVi7PoXTuyK^lYoxBzM^uk)%nbKUr_s@B6 z$eS~!=kmDBU`c*xEVyFX!bcabcCSz965O(^FC%!nZBzddr359Hr|;f=-@az|-$y^i z0}cJm{Zsb`n5qb0nCUXJV;!fyeD#?JSDhS+uDCEUx-2nam}*&lCWv4AKd4mpboFyt I=akR{0OCImzyJUM literal 0 HcmV?d00001 diff --git a/securedrop_client/resources/images/send.png b/securedrop_client/resources/images/send.png new file mode 100644 index 0000000000000000000000000000000000000000..1fe8922a1e116ca99d0aea918944560255858694 GIT binary patch literal 2275 zcmeAS@N?(olHy`uVBq!ia0vp^DnP8r!2~4j*3YW}QjEnx?oJHr&dIz4aySb-B8wRq zxP?KOkzv*x2?hoh?#z&gk_cZPtK|G#y~LFKq*T3%+ybC#1_ql7D* z7iAWdWaj57fXq!y$}cUkRZ;?31P4%e<`%#$$}5KY3g|!mio^naLp=k1Y??|k(-6)> z(FKyhsV%i46NfgC3`83^Fd%NU0fwlRZ+=Q3NvfTZfuW_YfswAEX^5e*m7$T9 zp{b2Nni_-;!MdG`QWHz^i$e1AbL<>J5vCB3kzbNuoRMFk;OwjbG|^l^!#6QGGY=%F z2{8^?6Owr#uUYvQWu^kd92By4hBo>bV(984gHnt0b4tPL&5fWMkOk2-IOpdU6r~my zBU^whhOQoAerR51i5<)eG+}gI;7|Z2%c9I=&%BbORAFK^Y7+nY0 zsz@ZOjFGKEl7MQo(FbK0r0fI9n_y93=CtDirVv<0x8o{1(9Z`f779IG978;K-%j(+ zkq#AUdw%Xl-`hRH_9D|oW;Mi^>t?nF1+vV}b8uOd8mJNwx`c^!>N|l&T>T;ujT0iu zg1J0g-!`!}akvPsSam>m(Uw(LT1+~2bOiRt_{yXmXT zvfsVtnKeuAiP({u{$ZDjnVzduFK?Rh!#yEEfkSIaN}cB{+o?=x4|lIQqRn&lf&X8L zqHop`Zjt;vGaC$}VkZ9op)`4~=DE3^oS92Q0yI+J$j({ZEo^-1++yZ`8~rByb4zhs zEwD;!WAV&8z7KDmoSL*~O1EcLr1+FY%Wi3{ak%?>&vE#$yBb63r`jh&1?QueNyxG^Mf`k`!w80(Ee9o{!PR;=P# zqb%Hau1Kxt?1_MtD|Y?LoqcR~QJ|)2@D{15MW>YIdCKyOq7NTpVs_)=w6qcQjniHC zFDyhQD_gnca&Tx-z&{-A=z>c4GV23G@5A>}7k8>5I$E-G1~?Wuw@= z(wyeAys9Qo*NShfIUliV)r+6ple>>D>G?8&D^M_`<+kAqj<@gSZQqptjJbaPNPUH& zQ|O_c9T#`D1+Lu~Ctva5#<3_Nv53TDReHMv_E_w!_*>Fd+~780_CDQn4=&HsWw&|D zxcviD&1$F~~cl5b81e?M$E7RujN={xO2h}9pro`N&ql>a?c zufAP%LC@#e_dnau-|P5)$a+^!=+rirgKIyYf4^3%Tzj3FQSmh4OOwue=IHHyr#918 zbicvQDe-NGa<<-YX>5p||M`ubT}-&c;T2oI`1TkEl$aWXiFE$_`uFMl`>`%puPP?C zdRVi|^6!yavDRVr#oMhZwb7E<&vo{B8q2F6%0ISc(nF3@TaBW33i}?|c%5xm`-KS8 z%~L~nn72(8=UUD6ONu{0^scV>ze^4oyBB_ZH}QEbPw=LQb;dbd%*|rLMp~@C+g{J* z^{hU|Lxvj&v?-I``5jR RlU9RT9-gj#F6*2UngFRTk+}c> literal 0 HcmV?d00001 diff --git a/securedrop_client/resources/images/trash.png b/securedrop_client/resources/images/trash.png new file mode 100644 index 0000000000000000000000000000000000000000..3bd54f6e227d43911b7cb8152e96f490c425e432 GIT binary patch literal 1846 zcmeAS@N?(olHy`uVBq!ia0vp^YCx>R!2~4Rj>OIfQjEnx?oJHr&dIz4aySb-B8wRq zxP?KOkzv*x2?hoh?#z&gk_cZPtK|G#y~LFKq*T3%+ybC#1_ql7D* z7iAWdWaj57fXq!y$}cUkRZ;?31P4%e<`%#$$}5KY3g|!mio^naLp=k1Y??|k(-6)> z(FKyhsV%i46NfgC3`83^Fd%NU0fwlRZ+=Q3NvfTZfuW_YfswAEX^5e*m7$T9 zp{b2Nni_-;!MdG`QWHz^i$e1AbL<>J5vCB3kzbNuoRMFk;OwjbG|^l^!#6QGGY=%F z2{8^?6Owr#uUYvQWu^kd92By4hBo>bV(984gHnt0b4tPL&5fWMkOk2-IOpdU6r~my zBU^whhOQoAerR51i5<)eG+}gI;7|Z2%c9I=&%Bb1=CD1iG=jY@X1s5bHr`nmBni>ORAFK^Y7+nY0 zsz`)YCML*MAxS_r+USF_3sUxh@ai}*FfbkQba4!c;Jg~< zn{mZKWS-}?yS|r>Ud{{D78PXSS88If=Uen*=?aA~f7dSx8X6q)zNl0SAL)Lxw<3$5 zRq&!#hlZHo$3^blN?UG;d0&5f&G*!~ADfgj%Zw)9^qkyuSm@sGPxpR5yM1Q1WQ^QA z7fBwUj==TY+|o`Ze-~WLIMJ2jd^1bY$!yA{n7C;l8RzZudb888Mlv8Mf~EX zmE0bK?QLGm6P(ii?|lBSCM{l0YE8_I(qAXGbxyy(IN#F6S#`Q(+1_RUIY00HH)jg- z>2Ei77QRdm7UN|~dlvq!--a_L%_H=jzNXbIZIzOITLB%7m0n>@nWA;}Vhx&K{dv4v zr#JJQVkT%kureyr7ag zo{NiH=GbMgUC-vl>R|Tu>!D)rlWC&cr`0Ae_?yD<{mQZ@pM-b(=U=)%Yf@i}*3{<( zXKIQ#_&A<`bMD!T-Fh*iR}d-kw?DbK_X*#a>+*L-R75H)YBDe{U&x_Dnl+iIeJ*uk-6azB%kP-Mshc ztm>ENLU`mwLRL@lop$)Icfi_XjZWcfgJy`aY96~%xTr$JvscJV_(kBmGo3=q_C9ZZK^p=~{vN8=&G_BAs~r*NvSP};U7 ianqtN{H8j3|HLEGwoH+f)w&0&%sgHFT-G@yGywpoqqf=r literal 0 HcmV?d00001 diff --git a/tests/gui/test_main.py b/tests/gui/test_main.py index c414afbfba..c6ae35a343 100644 --- a/tests/gui/test_main.py +++ b/tests/gui/test_main.py @@ -1,7 +1,7 @@ """ Check the core Window UI class works as expected. """ -from PyQt5.QtWidgets import QApplication, QVBoxLayout +from PyQt5.QtWidgets import QApplication, QHBoxLayout from securedrop_client.gui.main import Window from securedrop_client.resources import load_icon from securedrop_client.db import Message @@ -16,13 +16,13 @@ def test_init(mocker): Ensure the Window instance is setup in the expected manner. """ mock_li = mocker.MagicMock(return_value=load_icon('icon.png')) - mock_lo = mocker.MagicMock(return_value=QVBoxLayout()) + mock_lo = mocker.MagicMock(return_value=QHBoxLayout()) mock_lo().addWidget = mocker.MagicMock() mocker.patch('securedrop_client.gui.main.load_icon', mock_li) mock_tb = mocker.patch('securedrop_client.gui.main.ToolBar') mock_mv = mocker.patch('securedrop_client.gui.main.MainView') - mocker.patch('securedrop_client.gui.main.QVBoxLayout', mock_lo) + mocker.patch('securedrop_client.gui.main.QHBoxLayout', mock_lo) mocker.patch('securedrop_client.gui.main.QMainWindow') w = Window('mock') @@ -126,13 +126,13 @@ def test_update_error_status(mocker): def test_show_sync(mocker): """ - If there's a value display the result of its "humanize" method. + If there's a value display the result of its "humanize" method.humanize """ w = Window('mock') - w.main_view = mocker.MagicMock() + w.set_status = mocker.MagicMock() updated_on = mocker.MagicMock() w.show_sync(updated_on) - w.main_view.status.setText.assert_called_once_with('Last refresh: ' + updated_on.humanize()) + w.set_status.assert_called_once_with('Last refresh: ' + updated_on.humanize()) def test_show_sync_no_sync(mocker): @@ -140,9 +140,9 @@ def test_show_sync_no_sync(mocker): If there's no value to display, default to a "waiting" message. """ w = Window('mock') - w.main_view = mocker.MagicMock() + w.set_status = mocker.MagicMock() w.show_sync(None) - w.main_view.status.setText.assert_called_once_with('Waiting to refresh...') + w.set_status.assert_called_once_with('Waiting to refresh...', 5000) def test_set_logged_in_as(mocker): @@ -255,7 +255,7 @@ def test_conversation_pending_message(mocker): mock_source.collection = [message] mocked_add_message = mocker.patch('securedrop_client.gui.widgets.ConversationView.add_message') - mocker.patch('securedrop_client.gui.main.QVBoxLayout') + mocker.patch('securedrop_client.gui.main.QHBoxLayout') mocker.patch('securedrop_client.gui.main.QWidget') w.show_conversation_for(mock_source, True) diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index 87ffa1b349..66c23da106 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -2,7 +2,7 @@ Make sure the UI widgets are configured correctly and work as expected. """ from PyQt5.QtWidgets import QWidget, QApplication, QWidgetItem, QSpacerItem, QVBoxLayout, \ - QMessageBox + QMessageBox, QLabel from tests import factory from securedrop_client import db from securedrop_client import logic @@ -52,7 +52,7 @@ def test_ToolBar_set_logged_in_as(mocker): tb.set_logged_in_as('test') - tb.user_state.setText.assert_called_once_with('Signed in as: test') + tb.user_state.setText.assert_called_once_with('test') tb.login.setVisible.assert_called_once_with(False) tb.logout.setVisible.assert_called_once_with(True) tb.refresh.setVisible.assert_called_once_with(True) @@ -1068,6 +1068,7 @@ def test_SourceConversationWrapper_send_reply(mocker): mock_uuid = '456xyz' mocker.patch('securedrop_client.gui.widgets.uuid4', return_value=mock_uuid) mock_controller = mocker.MagicMock() + mocker.patch('securedrop_client.gui.widgets.LastUpdatedLabel', return_value=QLabel('now')) cw = SourceConversationWrapper(mock_source, 'mock home', mock_controller, True) mock_add_reply = mocker.Mock() @@ -1211,6 +1212,8 @@ def test_SourceConversationWrapper_auth_signals(mocker, homedir): mock_is_auth = mocker.MagicMock() mock_sh = mocker.patch.object(SourceConversationWrapper, '_show_or_hide_replybox') + mocker.patch('securedrop_client.gui.widgets.LastUpdatedLabel', return_value=QLabel('now')) + SourceConversationWrapper(mock_source, 'mock home', mock_controller, mock_is_auth) mock_connect.assert_called_once_with(mock_sh) @@ -1224,9 +1227,9 @@ def test_SourceConversationWrapper_set_widgets_via_auth_value(mocker, homedir): mock_source = mocker.Mock(collection=[]) mock_controller = mocker.MagicMock() + mocker.patch('securedrop_client.gui.widgets.LastUpdatedLabel', return_value=QLabel('now')) cw = SourceConversationWrapper(mock_source, 'mock home', mock_controller, True) mocker.patch.object(cw, 'layout') - mock_reply_box = mocker.patch('securedrop_client.gui.widgets.ReplyBoxWidget', return_value=QWidget()) mock_label = mocker.patch('securedrop_client.gui.widgets.QLabel', return_value=QWidget())