Skip to content

Commit

Permalink
Add methods for showing spinner in button and in header, with tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
ntoll committed Mar 18, 2020
1 parent 893546b commit 05e6f8c
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 16 deletions.
52 changes: 41 additions & 11 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QEvent, QTimer, QSize, pyqtBoundSignal, \
QObject, QPoint
from PyQt5.QtGui import QIcon, QPalette, QBrush, QColor, QFont, QLinearGradient, QKeySequence, \
QCursor, QKeyEvent, QCloseEvent
QCursor, QKeyEvent, QCloseEvent, QPixmap
from PyQt5.QtWidgets import QApplication, QListWidget, QLabel, QWidget, QListWidgetItem, \
QHBoxLayout, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, QMessageBox, \
QToolButton, QSizePolicy, QPlainTextEdit, QStatusBar, QGraphicsDropShadowEffect, QPushButton, \
Expand Down Expand Up @@ -2318,7 +2318,7 @@ class FramelessDialog(QDialog):
font-size: 12px;
color: #2a319d;
}
#header_icon {
#header_icon, #header_spinner {
min-width: 80px;
max-width: 80px;
min-height: 64px;
Expand Down Expand Up @@ -2422,9 +2422,16 @@ def __init__(self):
header_container.setLayout(header_container_layout)
self.header_icon = SvgLabel('blank.svg', svg_size=QSize(64, 64))
self.header_icon.setObjectName('header_icon')
self.header_spinner = QPixmap()
self.header_spinner_label = QLabel()
self.header_spinner_label.setObjectName("header_spinner")
self.header_spinner_label.setMinimumSize(64, 64)
self.header_spinner_label.setVisible(False)
self.header_spinner_label.setPixmap(self.header_spinner)
self.header = QLabel()
self.header.setObjectName('header')
header_container_layout.addWidget(self.header_icon)
header_container_layout.addWidget(self.header_spinner_label)
header_container_layout.addWidget(self.header, alignment=Qt.AlignCenter)
header_container_layout.addStretch()

Expand Down Expand Up @@ -2458,6 +2465,7 @@ def __init__(self):
self.continue_button = QPushButton(_('CONTINUE'))
self.continue_button.setObjectName('primary_button')
self.continue_button.setDefault(True)
self.continue_button.setIconSize(QSize(21, 21))
button_box = QDialogButtonBox(Qt.Horizontal)
button_box.setObjectName('button_box')
button_box.addButton(self.cancel_button, QDialogButtonBox.ActionRole)
Expand All @@ -2477,9 +2485,14 @@ def __init__(self):
layout.addWidget(window_buttons)

# Activestate animation.
self.animation = load_movie("activestate-spinner2.gif")
self.animation.setScaledSize(QSize(32, 32))
self.animation.frameChanged.connect(self.animate_activestate)
self.button_animation = load_movie("activestate-wide.gif")
self.button_animation.setScaledSize(QSize(32, 32))
self.button_animation.frameChanged.connect(self.animate_activestate)

# Header animation.
self.header_animation = load_movie("header_animation.gif")
self.header_animation.setScaledSize(QSize(64, 64))
self.header_animation.frameChanged.connect(self.animate_header)

def closeEvent(self, event: QCloseEvent):
# ignore any close event that doesn't come from our custom close method
Expand Down Expand Up @@ -2516,19 +2529,34 @@ def center_dialog(self):
self.move(x + x_center, y + y_center)

def animate_activestate(self):
self.continue_button.setIcon(QIcon(self.animation.currentPixmap()))
self.continue_button.setIcon(QIcon(self.button_animation.currentPixmap()))

def animate_header(self):
self.header_spinner_label.setPixmap(self.header_animation.currentPixmap())

def start_animate_activestate(self):
self.animation.start()
self.button_animation.start()
self.continue_button.setText("")
self.continue_button.setMinimumSize(QSize(150, 45))
self.continue_button.setStyleSheet("background-color: #f1f1f6; color: #fff;")
css = "background-color: #f1f1f6; color: #fff; border: None;"
self.continue_button.setStyleSheet(css)

def start_animate_header(self):
self.header_icon.setVisible(False)
self.header_spinner_label.setVisible(True)
self.header_animation.start()

def stop_animate_activestate(self):
self.continue_button.setIcon(QIcon())
self.animation.stop()
self.button_animation.stop()
self.continue_button.setText(_('CONTINUE'))
self.continue_button.setStyleSheet("background-color: #2a319d; color: #fff;")
css = "background-color: #2a319d; color: #fff; border: 2px solid #2a319d;"
self.continue_button.setStyleSheet(css)

def stop_animate_header(self):
self.header_icon.setVisible(True)
self.header_spinner_label.setVisible(False)
self.header_animation.stop()


class PrintDialog(FramelessDialog):
Expand Down Expand Up @@ -2577,7 +2605,6 @@ def __init__(self, controller: Controller, file_uuid: str, file_name: str):
self.generic_error_message = _('See your administrator for help.')

self._show_starting_instructions()
self.start_animate_activestate()
self._run_preflight()

def _show_starting_instructions(self):
Expand Down Expand Up @@ -2843,6 +2870,7 @@ def _run_preflight(self):

@pyqtSlot()
def _export_file(self, checked: bool = False):
self.start_animate_header()
self.controller.export_file_to_usb_drive(self.file_uuid, self.passphrase_field.text())

@pyqtSlot()
Expand All @@ -2862,10 +2890,12 @@ def _on_preflight_failure(self, error: ExportError):

@pyqtSlot()
def _on_export_success(self):
self.stop_animate_header()
self._show_success_message()

@pyqtSlot(object)
def _on_export_failure(self, error: ExportError):
self.stop_animate_header()
self._update_dialog(error.status)

def _update_dialog(self, error_status: str):
Expand Down
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 39 additions & 5 deletions tests/gui/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2136,14 +2136,19 @@ def test_FramelessDialog_center_dialog_with_no_active_window(mocker):

def test_FramelessDialog_animation_of_activestate(mocker):
dialog = FramelessDialog()
assert dialog.animation
dialog.animation.start = mocker.MagicMock()
dialog.animation.stop = mocker.MagicMock()
assert dialog.button_animation
dialog.button_animation.start = mocker.MagicMock()
dialog.button_animation.stop = mocker.MagicMock()
dialog.continue_button = mocker.MagicMock()

# Check the animation frame is updated as expected.
dialog.animate_activestate()
assert dialog.continue_button.setIcon.call_count == 1
dialog.continue_button.reset_mock()

# Check starting the animated state works as expected.
dialog.start_animate_activestate()
dialog.animation.start.assert_called_once_with()
dialog.button_animation.start.assert_called_once_with()
dialog.continue_button.setText.assert_called_once_with("")
assert dialog.continue_button.setMinimumSize.call_count == 1
assert dialog.continue_button.setStyleSheet.call_count == 1
Expand All @@ -2152,12 +2157,41 @@ def test_FramelessDialog_animation_of_activestate(mocker):

# Check stopping the animated state works as expected.
dialog.stop_animate_activestate()
dialog.animation.stop.assert_called_once_with()
dialog.button_animation.stop.assert_called_once_with()
dialog.continue_button.setText.assert_called_once_with("CONTINUE")
assert dialog.continue_button.setIcon.call_count == 1
assert dialog.continue_button.setStyleSheet.call_count == 1


def test_FramelessDialog_animation_of_header(mocker):
dialog = FramelessDialog()
assert dialog.header_animation
dialog.header_animation.start = mocker.MagicMock()
dialog.header_animation.stop = mocker.MagicMock()
dialog.header_icon.setVisible = mocker.MagicMock()
dialog.header_spinner_label.setVisible = mocker.MagicMock()
dialog.header_spinner_label.setPixmap = mocker.MagicMock()

# Check the animation frame is updated as expected.
dialog.animate_header()
assert dialog.header_spinner_label.setPixmap.call_count == 1

# Check starting the animated state works as expected.
dialog.start_animate_header()
dialog.header_animation.start.assert_called_once_with()
dialog.header_icon.setVisible.assert_called_once_with(False)
dialog.header_spinner_label.setVisible.assert_called_once_with(True)

dialog.header_icon.setVisible.reset_mock()
dialog.header_spinner_label.setVisible.reset_mock()

# Check stopping the animated state works as expected.
dialog.stop_animate_header()
dialog.header_animation.stop.assert_called_once_with()
dialog.header_icon.setVisible.assert_called_once_with(True)
dialog.header_spinner_label.setVisible.assert_called_once_with(False)


def test_ExportDialog_init(mocker):
mocker.patch(
'securedrop_client.gui.widgets.QApplication.activeWindow', return_value=QMainWindow())
Expand Down

0 comments on commit 05e6f8c

Please sign in to comment.