diff --git a/securedrop_client/gui/__init__.py b/securedrop_client/gui/__init__.py index 10d13f67e..39a154d30 100644 --- a/securedrop_client/gui/__init__.py +++ b/securedrop_client/gui/__init__.py @@ -24,7 +24,7 @@ from PyQt5.QtWidgets import QLabel, QHBoxLayout, QPushButton, QWidget from PyQt5.QtCore import QSize, Qt -from securedrop_client.resources import load_svg, load_icon, load_toggle_icon +from securedrop_client.resources import load_svg, load_icon class SvgToggleButton(QPushButton): @@ -54,7 +54,7 @@ def __init__(self, on: str, off: str, svg_size: str = None): layout.setSpacing(0) # Add SVG icon and set its size - self.icon = load_toggle_icon(on=on, off=off) + self.icon = load_icon(normal=on, normal_off=off) self.setIcon(self.icon) self.setIconSize(svg_size) if svg_size else self.setIconSize(QSize()) @@ -68,7 +68,7 @@ def disable(self) -> None: self.setEnabled(False) def set_icon(self, on: str, off: str) -> None: - self.icon = load_toggle_icon(on=on, off=off) + self.icon = load_icon(normal=on, normal_off=off) self.setIcon(self.icon) @@ -110,7 +110,12 @@ def __init__( layout.setSpacing(0) # Add SVG icon and set its size - self.icon = load_icon(normal=normal, disabled=disabled, active=active, selected=selected) + self.icon = load_icon( + normal=normal, + disabled=disabled, + active=active, + selected=selected, + disabled_off=disabled) self.setIcon(self.icon) self.setIconSize(svg_size) if svg_size else self.setIconSize(QSize()) diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index b950d3897..9a214df52 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -213,6 +213,8 @@ def __init__(self): selected='refresh.svg', svg_size=QSize(16, 16)) + self.active = False + # Set css id self.setObjectName('refresh_button') @@ -231,14 +233,17 @@ def setup(self, controller): self.controller.sync_events.connect(self._on_refresh_complete) def _on_clicked(self): + if self.active: + return + self.controller.sync_api(manual_refresh=True) + # This is a temporary solution for showing the icon as active for the entire duration of a # refresh, rather than for just the duration of a click. The icon image will be replaced # when the controller tells us the refresh has finished. A cleaner solution would be to # store and update our own icon mode so we don't have to reload any images. - self.setIcon(load_icon( - normal='refresh_active.svg', - disabled='refresh_offline.svg')) + self.setIcon(load_icon(normal='refresh_active.svg', disabled='refresh_offline.svg')) + self.active = True def _on_refresh_complete(self, data): if (data == 'synced'): @@ -247,9 +252,11 @@ def _on_refresh_complete(self, data): disabled='refresh_offline.svg', active='refresh_active.svg', selected='refresh.svg')) + self.active = False def enable(self): self.setEnabled(True) + self.active is False def disable(self): self.setEnabled(False) @@ -2669,8 +2676,7 @@ def focusOutEvent(self, e): def set_logged_in(self): self.setEnabled(True) source_name = "{}".format( - self.source.journalist_designation - ) + self.source.journalist_designation) placeholder = _("Compose a reply to ") + source_name self.placeholder.setText(placeholder) self.placeholder.adjustSize() diff --git a/securedrop_client/resources/__init__.py b/securedrop_client/resources/__init__.py index 70839d909..f073cd56a 100644 --- a/securedrop_client/resources/__init__.py +++ b/securedrop_client/resources/__init__.py @@ -45,37 +45,20 @@ def load_font(font_folder_name: str) -> None: QFontDatabase.addApplicationFont(directory + '/' + filename) -def load_toggle_icon(on: str, off: str) -> QIcon: +def load_icon( + normal: str, + disabled: str = None, + active: str = None, + selected: str = None, + normal_off: str = None, + disabled_off: str = None, + active_off: str = None, + selected_off: str = None, +) -> QIcon: """ - Add the contents of Scalable Vector Graphics (SVG) files provided for associated icon states, - see https://doc.qt.io/qt-5/qicon.html#State-enum. - - Parameters - ---------- - on: str - file name to the on-state image - off: str - file name to the on-state image - - Returns - ------- - QIcon - The icon that displays the contents of the SVG files. - - """ - - icon = QIcon() - - icon.addFile(path(on), state=QIcon.On) - icon.addFile(path(off), state=QIcon.Off) - - return icon - - -def load_icon(normal: str, disabled: str = None, active: str = None, selected: str = None) -> QIcon: - """ - Add the contents of Scalable Vector Graphics (SVG) files provided for associated icon modes, - see https://doc.qt.io/qt-5/qicon.html#Mode-enum. + Add the contents of Scalable Vector Graphics (SVG) files provided for associated icon modes and + states, see https://doc.qt.io/qt-5/qicon.html#Mode-enum. If the widget containing this icon is + set to checkable, then the *_off states will be displayed. Parameters ---------- @@ -87,6 +70,14 @@ def load_icon(normal: str, disabled: str = None, active: str = None, selected: s The name of the SVG file to add to the icon for QIcon.Active mode. selected: str, optional The name of the SVG file to add to the icon for QIcon.Selected mode. + normal_off: str + The name of the SVG file to add to the icon for QIcon.Normal mode. + disabled_off: str or None, optional + The name of the SVG file to add to the icon for QIcon.Disabled mode. + active_off: str, optional + The name of the SVG file to add to the icon for QIcon.Active mode. + selected_off: str, optional + The name of the SVG file to add to the icon for QIcon.Selected mode. Returns ------- @@ -100,7 +91,7 @@ def load_icon(normal: str, disabled: str = None, active: str = None, selected: s icon.addFile(path(normal), mode=QIcon.Normal, state=QIcon.On) if disabled: - icon.addFile(path(disabled), mode=QIcon.Disabled, state=QIcon.Off) + icon.addFile(path(disabled), mode=QIcon.Disabled, state=QIcon.On) if active: icon.addFile(path(active), mode=QIcon.Active, state=QIcon.On) @@ -108,6 +99,18 @@ def load_icon(normal: str, disabled: str = None, active: str = None, selected: s if selected: icon.addFile(path(selected), mode=QIcon.Selected, state=QIcon.On) + if normal_off: + icon.addFile(path(normal_off), mode=QIcon.Normal, state=QIcon.Off) + + if disabled_off: + icon.addFile(path(disabled_off), mode=QIcon.Disabled, state=QIcon.Off) + + if active_off: + icon.addFile(path(active_off), mode=QIcon.Active, state=QIcon.Off) + + if selected_off: + icon.addFile(path(selected_off), mode=QIcon.Selected, state=QIcon.Off) + return icon diff --git a/tests/gui/test_init.py b/tests/gui/test_init.py index c9a4f4cd2..3da2b0961 100644 --- a/tests/gui/test_init.py +++ b/tests/gui/test_init.py @@ -19,14 +19,14 @@ def test_SvgToggleButton_init(mocker): """ svg_size = QSize(1, 1) icon = mocker.MagicMock() - load_toggle_icon_fn = mocker.patch('securedrop_client.gui.load_toggle_icon', return_value=icon) + load_icon_fn = mocker.patch('securedrop_client.gui.load_icon', return_value=icon) setIcon_fn = mocker.patch('securedrop_client.gui.SvgToggleButton.setIcon') setIconSize_fn = mocker.patch('securedrop_client.gui.SvgToggleButton.setIconSize') stb = SvgToggleButton(on='mock_on', off='mock_off', svg_size=svg_size) assert stb.isCheckable() is True - load_toggle_icon_fn.assert_called_once_with(on='mock_on', off='mock_off') + load_icon_fn.assert_called_once_with(normal='mock_on', normal_off='mock_off') setIcon_fn.assert_called_once_with(icon) setIconSize_fn.assert_called_once_with(svg_size) @@ -68,12 +68,12 @@ def test_SvgToggleButton_set_icon(mocker): """ setIcon_fn = mocker.patch('securedrop_client.gui.SvgToggleButton.setIcon') icon = mocker.MagicMock() - load_toggle_icon_fn = mocker.patch('securedrop_client.gui.load_toggle_icon', return_value=icon) + load_icon_fn = mocker.patch('securedrop_client.gui.load_icon', return_value=icon) stb = SvgToggleButton(on='mock_on', off='mock_off') stb.set_icon(on='mock_on', off='mock_off') - load_toggle_icon_fn.assert_called_with(on='mock_on', off='mock_off') + load_icon_fn.assert_called_with(normal='mock_on', normal_off='mock_off') setIcon_fn.assert_called_with(icon) assert stb.icon == icon @@ -93,7 +93,7 @@ def test_SvgPushButton_init(mocker): assert spb.isCheckable() is False load_icon_fn.assert_called_once_with( - normal='mock1', disabled='mock2', active='mock3', selected='mock4') + normal='mock1', disabled='mock2', active='mock3', selected='mock4', disabled_off='mock2') setIcon_fn.assert_called_once_with(icon) setIconSize_fn.assert_called_once_with(svg_size) diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index b9553c856..d2611a7cf 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -178,6 +178,19 @@ def test_RefreshButton_on_clicked(mocker): rb.controller.sync_api.assert_called_once_with(manual_refresh=True) +def test_RefreshButton_on_clicked_while_active(mocker): + """ + When refresh button is clicked while active, sync_api should not be called. + """ + rb = RefreshButton() + rb.active = True + rb.controller = mocker.MagicMock() + + rb._on_clicked() + + rb.controller.sync_api.assert_not_called() + + def test_RefreshButton_on_refresh_complete(mocker): """ Make sure we are enabled after a refresh completes. @@ -185,18 +198,29 @@ def test_RefreshButton_on_refresh_complete(mocker): rb = RefreshButton() rb._on_refresh_complete('synced') assert rb.isEnabled() + assert rb.active is False def test_RefreshButton_enable(mocker): rb = RefreshButton() rb.enable() assert rb.isEnabled() + assert rb.active is False def test_RefreshButton_disable(mocker): rb = RefreshButton() rb.disable() assert not rb.isEnabled() + assert rb.active is False + + +def test_RefreshButton_disable_while_active(mocker): + rb = RefreshButton() + rb.active = True + rb.disable() + assert not rb.isEnabled() + assert rb.active is True def test_ErrorStatusBar_clear_error_status(mocker): diff --git a/tests/test_resources.py b/tests/test_resources.py index 703a9e557..8f6e3780b 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -25,7 +25,15 @@ def test_load_icon(): """ Check the load_icon function returns the expected QIcon object. """ - result = securedrop_client.resources.load_icon('icon') + result = securedrop_client.resources.load_icon( + 'normal_mock', + 'disabled_mock', + 'active_mock', + 'selected_mock', + 'normal_off_mock', + 'disabled_off_mock', + 'active_off_mock', + 'selected_off_mock') assert isinstance(result, QIcon)