diff --git a/client/securedrop_client/gui/auth/dialog.py b/client/securedrop_client/gui/auth/dialog.py index 9b6c14cd9e..f8f34e8fcf 100644 --- a/client/securedrop_client/gui/auth/dialog.py +++ b/client/securedrop_client/gui/auth/dialog.py @@ -19,7 +19,6 @@ import logging from gettext import gettext as _ -from pkg_resources import resource_string from PyQt5.QtCore import QSize, Qt from PyQt5.QtGui import QBrush, QPalette from PyQt5.QtWidgets import ( @@ -39,7 +38,7 @@ from securedrop_client.gui.base import PasswordEdit from securedrop_client.gui.base.checkbox import SDCheckBox from securedrop_client.logic import Controller -from securedrop_client.resources import load_image +from securedrop_client.resources import load_image, load_relative_css logger = logging.getLogger(__name__) @@ -82,8 +81,7 @@ def __init__(self, parent: QWidget) -> None: form = QWidget() form.setObjectName("LoginDialog_form") - styles = resource_string(__name__, "dialog.css").decode("utf-8") - self.setStyleSheet(styles) + self.setStyleSheet(load_relative_css(__file__, "dialog.css")) form_layout = QVBoxLayout() form.setLayout(form_layout) diff --git a/client/securedrop_client/gui/auth/sign_in/button.py b/client/securedrop_client/gui/auth/sign_in/button.py index 2d14753a07..f9a4ef17db 100644 --- a/client/securedrop_client/gui/auth/sign_in/button.py +++ b/client/securedrop_client/gui/auth/sign_in/button.py @@ -18,11 +18,12 @@ """ from gettext import gettext as _ -from pkg_resources import resource_string from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor, QCursor from PyQt5.QtWidgets import QGraphicsDropShadowEffect, QPushButton +from securedrop_client.resources import load_relative_css + class SignInButton(QPushButton): """ @@ -35,8 +36,7 @@ def __init__(self) -> None: # Set css id self.setObjectName("SignInButton") - styles = resource_string(__name__, "button.css").decode("utf-8") - self.setStyleSheet(styles) + self.setStyleSheet(load_relative_css(__file__, "button.css")) self.setFixedHeight(40) self.setFixedWidth(140) diff --git a/client/securedrop_client/gui/auth/sign_in/error_bar.py b/client/securedrop_client/gui/auth/sign_in/error_bar.py index 6302b00d2a..69eb71dc0c 100644 --- a/client/securedrop_client/gui/auth/sign_in/error_bar.py +++ b/client/securedrop_client/gui/auth/sign_in/error_bar.py @@ -16,11 +16,12 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ -from pkg_resources import resource_string + from PyQt5.QtCore import QSize from PyQt5.QtWidgets import QHBoxLayout, QWidget from securedrop_client.gui.base import SecureQLabel, SvgLabel +from securedrop_client.resources import load_relative_css class LoginErrorBar(QWidget): @@ -32,8 +33,7 @@ def __init__(self) -> None: super().__init__() self.setObjectName("LoginErrorBar") - styles = resource_string(__name__, "error_bar.css").decode("utf-8") - self.setStyleSheet(styles) + self.setStyleSheet(load_relative_css(__file__, "error_bar.css")) # Set layout layout = QHBoxLayout(self) diff --git a/client/securedrop_client/gui/base/checkbox.py b/client/securedrop_client/gui/base/checkbox.py index 73e3973480..39f66665ac 100644 --- a/client/securedrop_client/gui/base/checkbox.py +++ b/client/securedrop_client/gui/base/checkbox.py @@ -7,21 +7,21 @@ """ from gettext import gettext as _ -from pkg_resources import resource_string from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtGui import QCursor, QFont, QMouseEvent from PyQt5.QtWidgets import QCheckBox, QFrame, QHBoxLayout, QLabel, QWidget +from securedrop_client.resources import load_relative_css + class SDCheckBox(QWidget): clicked = pyqtSignal() - CHECKBOX_CSS = resource_string(__name__, "checkbox.css").decode("utf-8") PASSPHRASE_LABEL_SPACING = 1 def __init__(self) -> None: super().__init__() self.setObjectName("ShowPassphrase_widget") - self.setStyleSheet(self.CHECKBOX_CSS) + self.setStyleSheet(load_relative_css(__file__, "checkbox.css")) font = QFont() font.setLetterSpacing(QFont.AbsoluteSpacing, self.PASSPHRASE_LABEL_SPACING) diff --git a/client/securedrop_client/gui/base/dialogs.py b/client/securedrop_client/gui/base/dialogs.py index d5cc5feb7d..d05e68179b 100644 --- a/client/securedrop_client/gui/base/dialogs.py +++ b/client/securedrop_client/gui/base/dialogs.py @@ -18,7 +18,6 @@ """ from gettext import gettext as _ -from pkg_resources import resource_string from PyQt5.QtCore import QSize, Qt from PyQt5.QtGui import QIcon, QKeyEvent, QPixmap from PyQt5.QtWidgets import ( @@ -33,13 +32,13 @@ ) from securedrop_client.gui.base.misc import SvgLabel -from securedrop_client.resources import load_movie +from securedrop_client.resources import load_movie, load_relative_css class ModalDialog(QDialog): - DIALOG_CSS = resource_string(__name__, "dialogs.css").decode("utf-8") - BUTTON_CSS = resource_string(__name__, "dialog_button.css").decode("utf-8") - ERROR_DETAILS_CSS = resource_string(__name__, "dialog_message.css").decode("utf-8") + DIALOG_CSS = load_relative_css(__file__, "dialogs.css") + BUTTON_CSS = load_relative_css(__file__, "dialog_button.css") + ERROR_DETAILS_CSS = load_relative_css(__file__, "dialog_message.css") MARGIN = 40 NO_MARGIN = 0 diff --git a/client/securedrop_client/gui/conversation/export/export_wizard.py b/client/securedrop_client/gui/conversation/export/export_wizard.py index 2043a5bd81..6750770bb8 100644 --- a/client/securedrop_client/gui/conversation/export/export_wizard.py +++ b/client/securedrop_client/gui/conversation/export/export_wizard.py @@ -2,7 +2,6 @@ from gettext import gettext as _ from typing import List, Optional -from pkg_resources import resource_string from PyQt5.QtCore import QSize, Qt, pyqtSlot from PyQt5.QtGui import QIcon, QKeyEvent from PyQt5.QtWidgets import QAbstractButton # noqa: F401 @@ -20,7 +19,7 @@ PassphraseWizardPage, PreflightPage, ) -from securedrop_client.resources import load_movie +from securedrop_client.resources import load_movie, load_relative_css logger = logging.getLogger(__name__) @@ -34,8 +33,6 @@ class ExportWizard(QWizard): NO_MARGIN = 0 FILENAME_WIDTH_PX = 260 FILE_OPTIONS_FONT_SPACING = 1.6 - BUTTON_CSS = resource_string(__name__, "wizard_button.css").decode("utf-8") - WIZARD_CSS = resource_string(__name__, "wizard.css").decode("utf-8") # If the drive is unlocked, we don't need a passphrase; if we do need one, # it's populated later. @@ -97,6 +94,7 @@ def _style_buttons(self) -> None: self.button_animation = load_movie("activestate-wide.gif") self.button_animation.setScaledSize(QSize(32, 32)) self.button_animation.frameChanged.connect(self._animate_activestate) + button_stylesheet = load_relative_css(__file__, "wizard_button.css") # Buttons self.next_button = self.button(QWizard.WizardButton.NextButton) # type: QAbstractButton @@ -105,7 +103,7 @@ def _style_buttons(self) -> None: self.finish_button = self.button(QWizard.WizardButton.FinishButton) # type: QAbstractButton self.next_button.setObjectName("QWizardButton_PrimaryButton") - self.next_button.setStyleSheet(self.BUTTON_CSS) + self.next_button.setStyleSheet(button_stylesheet) self.next_button.setMinimumSize(QSize(142, 40)) self.next_button.setMaximumHeight(40) self.next_button.setIconSize(QSize(21, 21)) @@ -113,19 +111,19 @@ def _style_buttons(self) -> None: self.next_button.setFixedSize(QSize(142, 40)) self.cancel_button.setObjectName("QWizardButton_GenericButton") - self.cancel_button.setStyleSheet(self.BUTTON_CSS) + self.cancel_button.setStyleSheet(button_stylesheet) self.cancel_button.setMinimumSize(QSize(142, 40)) self.cancel_button.setMaximumHeight(40) self.cancel_button.setFixedSize(QSize(142, 40)) self.back_button.setObjectName("QWizardButton_GenericButton") - self.back_button.setStyleSheet(self.BUTTON_CSS) + self.back_button.setStyleSheet(button_stylesheet) self.back_button.setMinimumSize(QSize(142, 40)) self.back_button.setMaximumHeight(40) self.back_button.setFixedSize(QSize(142, 40)) self.finish_button.setObjectName("QWizardButton_GenericButton") - self.finish_button.setStyleSheet(self.BUTTON_CSS) + self.finish_button.setStyleSheet(button_stylesheet) self.finish_button.setMinimumSize(QSize(142, 40)) self.finish_button.setMaximumHeight(40) self.finish_button.setFixedSize(QSize(142, 40)) @@ -149,7 +147,7 @@ def _set_layout(self) -> None: title = ("Export %(summary)s") % {"summary": self.summary_text} self.setWindowTitle(title) self.setObjectName("QWizard_export") - self.setStyleSheet(self.WIZARD_CSS) + self.setStyleSheet(load_relative_css(__file__, "wizard.css")) self.setModal(False) self.setOptions( QWizard.NoBackButtonOnLastPage diff --git a/client/securedrop_client/gui/conversation/export/export_wizard_page.py b/client/securedrop_client/gui/conversation/export/export_wizard_page.py index e83ecb1bd4..e85b7c57c0 100644 --- a/client/securedrop_client/gui/conversation/export/export_wizard_page.py +++ b/client/securedrop_client/gui/conversation/export/export_wizard_page.py @@ -2,7 +2,6 @@ from gettext import gettext as _ from typing import Optional -from pkg_resources import resource_string from PyQt5.QtCore import QSize, Qt, pyqtSlot from PyQt5.QtGui import QColor, QFont, QPixmap from PyQt5.QtWidgets import ( @@ -21,7 +20,7 @@ from securedrop_client.gui.base.checkbox import SDCheckBox from securedrop_client.gui.base.misc import SvgLabel from securedrop_client.gui.conversation.export.export_wizard_constants import STATUS_MESSAGES, Pages -from securedrop_client.resources import load_movie +from securedrop_client.resources import load_movie, load_relative_css logger = logging.getLogger(__name__) @@ -47,9 +46,6 @@ class ExportWizardPage(QWizardPage): appears on recoverable error to help the user advance to the next stage) """ - WIZARD_CSS = resource_string(__name__, "wizard.css").decode("utf-8") - ERROR_DETAILS_CSS = resource_string(__name__, "wizard_message.css").decode("utf-8") - MARGIN = 40 PASSPHRASE_LABEL_SPACING = 0.5 NO_MARGIN = 0 @@ -94,7 +90,8 @@ def _build_layout(self) -> QVBoxLayout: Create parent layout, draw elements, return parent layout """ self.setObjectName("QWizard_export_page") - self.setStyleSheet(self.WIZARD_CSS) + self.setStyleSheet(load_relative_css(__file__, "wizard.css")) + parent_layout = QVBoxLayout(self) parent_layout.setContentsMargins(self.MARGIN, self.MARGIN, self.MARGIN, self.MARGIN) @@ -139,7 +136,7 @@ def _build_layout(self) -> QVBoxLayout: # Widget for displaying error messages (hidden by default) self.error_details = QLabel() self.error_details.setObjectName("QWizard_error_details") - self.error_details.setStyleSheet(self.ERROR_DETAILS_CSS) + self.error_details.setStyleSheet(load_relative_css(__file__, "wizard_message.css")) self.error_details.setContentsMargins( self.NO_MARGIN, self.NO_MARGIN, self.NO_MARGIN, self.NO_MARGIN ) diff --git a/client/securedrop_client/gui/main.py b/client/securedrop_client/gui/main.py index 351e47bc3d..237838af1e 100644 --- a/client/securedrop_client/gui/main.py +++ b/client/securedrop_client/gui/main.py @@ -32,7 +32,7 @@ from securedrop_client.gui.auth import LoginDialog from securedrop_client.gui.widgets import LeftPane, MainView, TopPane from securedrop_client.logic import Controller -from securedrop_client.resources import load_css, load_font, load_icon +from securedrop_client.resources import load_all_fonts, load_css, load_icon logger = logging.getLogger(__name__) @@ -59,8 +59,7 @@ def __init__( place for details / message contents / forms. """ super().__init__() - load_font("Montserrat") - load_font("Source_Sans_Pro") + load_all_fonts() self.setStyleSheet(load_css("sdclient.css")) self.setWindowTitle(_("SecureDrop Client {}").format(__version__)) self.setWindowIcon(load_icon(self.icon)) diff --git a/client/securedrop_client/resources/__init__.py b/client/securedrop_client/resources/__init__.py index 539b39b593..d31f1a7c30 100644 --- a/client/securedrop_client/resources/__init__.py +++ b/client/securedrop_client/resources/__init__.py @@ -17,33 +17,33 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ -import os +from pathlib import Path from typing import Optional -from pkg_resources import resource_filename, resource_string from PyQt5.QtCore import QDir from PyQt5.QtGui import QFontDatabase, QIcon, QMovie, QPixmap from PyQt5.QtSvg import QSvgWidget +RESOURCES_DIR = Path(__file__).parent + # Add resource directories to the search path. -QDir.addSearchPath("images", resource_filename(__name__, "images")) -QDir.addSearchPath("css", resource_filename(__name__, "css")) +QDir.addSearchPath("images", str(RESOURCES_DIR / "images")) +QDir.addSearchPath("css", str(RESOURCES_DIR / "css")) -def path(name: str, resource_dir: str = "images/") -> str: +def path(name: str) -> str: """ Return the filename for the referenced image. Qt uses unix path conventions. """ - return resource_filename(__name__, resource_dir + name) + return str(RESOURCES_DIR / "images" / name) -def load_font(font_folder_name: str) -> None: - directory = resource_filename(__name__, "fonts/") + font_folder_name - for filename in os.listdir(directory): - if filename.endswith(".ttf"): - QFontDatabase.addApplicationFont(directory + "/" + filename) +def load_all_fonts() -> None: + """Load all the fonts in the fonts/ directory""" + for font in (RESOURCES_DIR / "fonts").glob("**/*.ttf"): + QFontDatabase.addApplicationFont(str(font.absolute())) def load_icon( @@ -133,7 +133,15 @@ def load_css(name: str) -> str: """ Return the contents of the referenced CSS file in the resources. """ - return resource_string(__name__, "css/" + name).decode("utf-8") + return (RESOURCES_DIR / "css" / name).read_text() + + +def load_relative_css(file: str, name: str) -> str: + """ + Load CSS that's in the same directory as the file calling this. + The first argument should be __name__ and the second is the name of the CSS + """ + return (Path(file).parent / name).read_text() def load_movie(name: str) -> QMovie: diff --git a/client/tests/test_resources.py b/client/tests/test_resources.py index 99843cd27a..da78b2e4e4 100644 --- a/client/tests/test_resources.py +++ b/client/tests/test_resources.py @@ -8,16 +8,6 @@ from tests.helper import app # noqa: F401 -def test_path(mocker): - """ - Ensure the resource_filename function is called with the expected args and - the path function under test returns its result. - """ - r = mocker.patch("securedrop_client.resources.resource_filename", return_value="bar") - assert securedrop_client.resources.path("foo") == "bar" - r.assert_called_once_with(securedrop_client.resources.__name__, "images/foo") - - def test_load_icon(): """ Check the load_icon function returns the expected QIcon object. @@ -51,16 +41,6 @@ def test_load_image(): assert isinstance(result, QPixmap) -def test_load_css(mocker): - """ - Ensure the resource_string function is called with the expected args and - the load_css function returns its result. - """ - rs = mocker.patch("securedrop_client.resources.resource_string", return_value=b"foo") - assert "foo" == securedrop_client.resources.load_css("foo") - rs.assert_called_once_with(securedrop_client.resources.__name__, "css/foo") - - def test_load_movie(): """ Check the load_movie function returns the expected QMovie object.