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

Fix #415 with various cursor related changes and other UI/CSS updates. #675

Merged
merged 9 commits into from
Feb 6, 2020
67 changes: 63 additions & 4 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
from uuid import uuid4
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QEvent, QTimer, QSize, pyqtBoundSignal, \
QObject, QPoint
from PyQt5.QtGui import QIcon, QPalette, QBrush, QColor, QFont, QLinearGradient, QKeySequence
from PyQt5.QtGui import QIcon, QPalette, QBrush, QColor, QFont, QLinearGradient,\
QKeySequence, QCursor
from PyQt5.QtWidgets import QListWidget, QLabel, QWidget, QListWidgetItem, QHBoxLayout, \
QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, QMessageBox, \
QToolButton, QSizePolicy, QPlainTextEdit, QStatusBar, QGraphicsDropShadowEffect
Expand Down Expand Up @@ -548,11 +549,19 @@ def __init__(self):
self.menu = UserMenu()
self.setMenu(self.menu)

# Set cursor.
self.setCursor(QCursor(Qt.PointingHandCursor))

def setup(self, controller):
self.menu.setup(controller)

def set_username(self, username):
self.setText(_('{}').format(html.escape(username)))
formatted_name = _('{}').format(html.escape(username))
self.setText(formatted_name)
if len(formatted_name) > 21:
# The name will be truncated, so create a tooltip to display full
# name if the mouse hovers over the widget.
self.setToolTip(_('{}').format(html.escape(username)))


class UserMenu(QMenu):
Expand Down Expand Up @@ -821,6 +830,9 @@ class SourceList(QListWidget):
QListView::item:selected {
background-color: #f3f5f9;
}
QListView::item:hover{
border: 500px solid #f9f9f9;
}
'''

def __init__(self):
Expand Down Expand Up @@ -962,6 +974,9 @@ def __init__(self, source: Source):
layout = QHBoxLayout(self)
self.setLayout(layout)

# Set cursor.
self.setCursor(QCursor(Qt.PointingHandCursor))

# Remove margins and spacing
layout.setContentsMargins(self.SIDE_MARGIN, 0, self.SIDE_MARGIN, 0)
layout.setSpacing(0)
Expand Down Expand Up @@ -1074,6 +1089,10 @@ class StarToggleButton(SvgToggleButton):
#star_button {
border: none;
}
#star_button:hover {
border: 4px solid #D3D8EA;
border-radius: 8px;
}
'''

def __init__(self, source: Source):
Expand Down Expand Up @@ -1243,6 +1262,9 @@ def __init__(self):
self.setFixedHeight(40)
self.setFixedWidth(140)

# Set cursor.
self.setCursor(QCursor(Qt.PointingHandCursor))

# Set drop shadow effect
effect = QGraphicsDropShadowEffect(self)
effect.setOffset(0, 1)
Expand Down Expand Up @@ -1816,16 +1838,25 @@ class FileWidget(QWidget):
font-size: 13px;
color: #2a319d;
}
QPushButton#download_button:hover {
color: #05a6fe;
}
QLabel#file_name {
min-width: 129px;
padding-right: 8px;
padding-bottom: 4px;
padding-top: 1px;
font-family: 'Source Sans Pro';
font-weight: 700;
font-weight: 600;
font-size: 13px;
color: #2a319d;
}
QLabel#file_name:hover {
color: #05a6fe;
}
QLabel#no_file_name {
padding-right: 8px;
padding-bottom: 1px;
font-family: 'Source Sans Pro';
font-weight: 300;
font-size: 13px;
Expand Down Expand Up @@ -1868,6 +1899,7 @@ def __init__(
self.controller = controller
self.file = self.controller.get_file(file_uuid)
self.index = index
self.downloading = False

# Set styles
self.setObjectName('file_widget')
Expand Down Expand Up @@ -1900,14 +1932,17 @@ def __init__(
self.download_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.download_button.setIcon(load_icon('download_file.svg'))
self.download_button.setFont(self.file_buttons_font)
self.download_button.setCursor(QCursor(Qt.PointingHandCursor))
self.download_animation = load_movie("download_file.gif")
self.export_button = QPushButton(_('EXPORT'))
self.export_button.setObjectName('export_print')
self.export_button.setFont(self.file_buttons_font)
self.export_button.setCursor(QCursor(Qt.PointingHandCursor))
self.middot = QLabel("·")
self.print_button = QPushButton(_('PRINT'))
self.print_button.setObjectName('export_print')
self.print_button.setFont(self.file_buttons_font)
self.print_button.setCursor(QCursor(Qt.PointingHandCursor))
file_options_layout.addWidget(self.download_button)
file_options_layout.addWidget(self.export_button)
file_options_layout.addWidget(self.middot)
Expand All @@ -1921,6 +1956,7 @@ def __init__(
self.file_name = SecureQLabel(self.file.filename)
self.file_name.setObjectName('file_name')
self.file_name.installEventFilter(self)
self.file_name.setCursor(QCursor(Qt.PointingHandCursor))
self.no_file_name = SecureQLabel('ENCRYPTED FILE ON SERVER')
self.no_file_name.setObjectName('no_file_name')
self.no_file_name.setFont(file_description_font)
Expand Down Expand Up @@ -1963,16 +1999,26 @@ def __init__(
file_missing.connect(self._on_file_missing, type=Qt.QueuedConnection)

def eventFilter(self, obj, event):
if event.type() == QEvent.MouseButtonPress:
t = event.type()
if t == QEvent.MouseButtonPress:
if event.button() == Qt.LeftButton:
self._on_left_click()
# HERE BE DRAGONS. Usually I'd wrap this in an if not self.download,
# but for reasons not entirely clear, this caused a crash. The
# following odd way of expressing the same conditional doesn't cause a
# crash. Go figure... :-/
if t == QEvent.HoverEnter or t == QEvent.HoverMove and not self.downloading:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can remove the check for HoverMove it works just fine with HoverEnter by itself, unless i'm missing something?

self.download_button.setIcon(load_icon('download_file_hover.svg'))
elif t == QEvent.HoverLeave and not self.downloading:
self.download_button.setIcon(load_icon('download_file.svg'))
return QObject.event(obj, event)

@pyqtSlot(str, str, str)
def _on_file_downloaded(self, source_uuid: str, file_uuid: str, filename: str) -> None:
if file_uuid == self.file.uuid:
self.file = self.controller.get_file(self.file.uuid)
if self.file.is_downloaded:
self.downloading = False
self.file_name.setText(self.file.filename)
self.download_button.hide()
self.no_file_name.hide()
Expand All @@ -1986,6 +2032,7 @@ def _on_file_missing(self, source_uuid: str, file_uuid: str, filename: str) -> N
if file_uuid == self.file.uuid:
self.file = self.controller.get_file(self.file.uuid)
if not self.file.is_downloaded:
self.downloading = False
self.download_animation.stop()
self.download_button.setText(_('DOWNLOAD'))
self.download_button.setIcon(load_icon('download_file.svg'))
Expand Down Expand Up @@ -2049,6 +2096,7 @@ def start_button_animation(self):
"""
Update the download button to the animated "downloading" state.
"""
self.downloading = True
self.download_animation.frameChanged.connect(self.set_button_animation_frame)
self.download_animation.start()
self.download_button.setText(_(" DOWNLOADING "))
Expand Down Expand Up @@ -2675,6 +2723,10 @@ class ReplyBoxWidget(QWidget):
QPushButton {
border: none;
}
QPushButton:hover {
background: #D3D8EA;
border-radius: 8px;
}
QWidget#horizontal_line {
min-height: 2px;
max-height: 2px;
Expand Down Expand Up @@ -2733,6 +2785,9 @@ def __init__(self, source: Source, controller: Controller) -> None:
# Ensure TAB order from text edit -> send button
self.setTabOrder(self.text_edit, self.send_button)

# Set cursor.
self.send_button.setCursor(QCursor(Qt.PointingHandCursor))

# Add widgets to replybox
replybox_layout.addWidget(self.text_edit)
replybox_layout.addWidget(self.send_button, alignment=Qt.AlignBottom)
Expand Down Expand Up @@ -2832,6 +2887,8 @@ def __init__(self, source, controller):
self.placeholder.setParent(self)
self.placeholder.move(QPoint(3, 4)) # make label match text below
self.set_logged_in()
# Set cursor.
self.setCursor(QCursor(Qt.IBeamCursor))

def focusInEvent(self, e):
# override default behavior: when reply text box is focused, the placeholder
Expand Down Expand Up @@ -2945,6 +3002,8 @@ def __init__(self, source, controller):
self.setMenu(self.menu)

self.setPopupMode(QToolButton.InstantPopup)
# Set cursor.
self.setCursor(QCursor(Qt.PointingHandCursor))


class TitleLabel(QLabel):
Expand Down
14 changes: 14 additions & 0 deletions securedrop_client/resources/images/download_file_hover.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 44 additions & 1 deletion tests/gui/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,15 @@ def test_UserButton_set_username():
ub.text() == 'test_username'


def test_UserButton_set_long_username(mocker):
ub = UserButton()
ub.setToolTip = mocker.MagicMock()
ub.set_username('test_username_that_is_very_very_long')
ub.setToolTip.assert_called_once_with(
'test_username_that_is_very_very_long'
)


def test_UserMenu_setup(mocker):
um = UserMenu()
controller = mocker.MagicMock()
Expand Down Expand Up @@ -1446,7 +1455,7 @@ def test_FileWidget_init_file_downloaded(mocker, source, session):
assert not fw.file_name.isHidden()


def test_FileWidget_event_handler(mocker, session, source):
def test_FileWidget_event_handler_left_click(mocker, session, source):
"""
Left click on filename should trigger an open.
"""
Expand All @@ -1468,6 +1477,40 @@ def test_FileWidget_event_handler(mocker, session, source):
fw._on_left_click.call_count == 1


def test_FileWidget_event_handler_hover(mocker, session, source):
"""
Hover events when the file isn't being downloaded should change the
widget's icon.
"""
file_ = factory.File(source=source['source'],
is_downloaded=False,
is_decrypted=None)
session.add(file_)
session.commit()

mock_get_file = mocker.MagicMock(return_value=file_)
mock_controller = mocker.MagicMock(get_file=mock_get_file)

fw = FileWidget(file_.uuid, mock_controller, mocker.MagicMock(), mocker.MagicMock(), 0)
fw.download_button = mocker.MagicMock()

# Hover enter
test_event = QEvent(QEvent.HoverEnter)
fw.eventFilter(fw, test_event)
assert fw.download_button.setIcon.call_count == 1
fw.download_button.setIcon.reset_mock()
# Hover move
test_event = QEvent(QEvent.HoverMove)
fw.eventFilter(fw, test_event)
assert fw.download_button.setIcon.call_count == 1
fw.download_button.setIcon.reset_mock()
# Hover leave
test_event = QEvent(QEvent.HoverLeave)
fw.eventFilter(fw, test_event)
assert fw.download_button.setIcon.call_count == 1
fw.download_button.setIcon.reset_mock()


def test_FileWidget_on_left_click_download(mocker, session, source):
"""
Left click on download when file is not downloaded should trigger
Expand Down