diff --git a/deeplabcut/gui/components.py b/deeplabcut/gui/components.py index cc8fa3d37..385144f45 100644 --- a/deeplabcut/gui/components.py +++ b/deeplabcut/gui/components.py @@ -14,10 +14,10 @@ from PySide6 import QtWidgets from PySide6.QtCore import Qt, Slot -from PySide6.QtGui import QIcon from deeplabcut.core.config import read_config_as_dict from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.gui.gui_assets import icon_from_resource from deeplabcut.gui.widgets import ConfigEditor @@ -671,8 +671,9 @@ def _create_message_box(text, info_text): msg.setWindowTitle("Info") msg.setMinimumWidth(900) - logo = Path("logo.png").resolve().parent / "assets" / "logo.png" - msg.setWindowIcon(QIcon(str(logo))) + # logo = Path("logo.png").resolve().parent / "assets" / "logo.png" + icon = icon_from_resource("logo.png") + msg.setWindowIcon(icon) msg.setStandardButtons(QtWidgets.QMessageBox.Ok) return msg @@ -685,8 +686,8 @@ def _create_confirmation_box(title, description): msg.setWindowTitle("Confirmation") msg.setMinimumWidth(900) - logo = Path("logo.png").resolve().parent / "assets" / "logo.png" - msg.setWindowIcon(QIcon(str(logo))) + icon = icon_from_resource("logo.png") + msg.setWindowIcon(icon) msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) return msg diff --git a/deeplabcut/gui/gui_assets.py b/deeplabcut/gui/gui_assets.py new file mode 100644 index 000000000..85dcd603d --- /dev/null +++ b/deeplabcut/gui/gui_assets.py @@ -0,0 +1,53 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +from __future__ import annotations + +from importlib.resources import files +from pathlib import Path + +from PySide6.QtGui import QIcon, QPixmap + +ASSETS_DIR = files("deeplabcut.gui").joinpath("assets") + + +def resource_bytes(*parts: str) -> bytes: + """Read a resource bundled inside deeplabcut.gui.""" + return ASSETS_DIR.joinpath(*parts).read_bytes() + + +def resource_text(*parts: str, encoding: str = "utf-8") -> str: + """Read a text resource bundled inside deeplabcut.gui.""" + return ASSETS_DIR.joinpath(*parts).read_text(encoding=encoding) + + +def get_assets_dir() -> Path: + """Get the path to the assets directory.""" + return Path(ASSETS_DIR) + + +def get_style_qss() -> str: + """Get the contents of the style.qss file.""" + return resource_text("style.qss") + + +def pixmap_from_resource(*parts: str) -> QPixmap: + pixmap = QPixmap() + data = resource_bytes(*parts) + + if not pixmap.loadFromData(data): + joined = "/".join(parts) + raise FileNotFoundError(f"Could not load GUI resource as QPixmap: {joined}") + + return pixmap + + +def icon_from_resource(*parts: str) -> QIcon: + return QIcon(pixmap_from_resource(*parts)) diff --git a/deeplabcut/gui/launch_script.py b/deeplabcut/gui/launch_script.py index d30df067c..cc5ae09ec 100644 --- a/deeplabcut/gui/launch_script.py +++ b/deeplabcut/gui/launch_script.py @@ -24,18 +24,16 @@ import PySide6.QtWidgets as QtWidgets import qdarkstyle from PySide6.QtCore import Qt -from PySide6.QtGui import QIcon, QPixmap from deeplabcut.gui import BASE_DIR +from deeplabcut.gui.gui_assets import icon_from_resource, pixmap_from_resource def launch_dlc(): app = QtWidgets.QApplication(sys.argv) - app.setWindowIcon(QIcon(str(BASE_DIR / "assets" / "logo.png"))) + app.setWindowIcon(icon_from_resource("logo.png")) screen_size = app.screens()[0].size() - pixmap = QPixmap(str(BASE_DIR / "assets" / "welcome.png")).scaledToWidth( - int(0.7 * screen_size.width()), Qt.SmoothTransformation - ) + pixmap = pixmap_from_resource("welcome.png").scaledToWidth(int(0.7 * screen_size.width()), Qt.SmoothTransformation) splash = QtWidgets.QSplashScreen(pixmap) splash.show() diff --git a/deeplabcut/gui/tabs/create_project.py b/deeplabcut/gui/tabs/create_project.py index 878888b8b..0046f7a14 100644 --- a/deeplabcut/gui/tabs/create_project.py +++ b/deeplabcut/gui/tabs/create_project.py @@ -12,11 +12,11 @@ from pathlib import Path from PySide6 import QtCore, QtWidgets -from PySide6.QtGui import QBrush, QColor, QDesktopServices, QIcon, QPainter, QPen +from PySide6.QtGui import QBrush, QColor, QDesktopServices, QPainter, QPen from deeplabcut.create_project import create_new_project, create_new_project_3d -from deeplabcut.gui import BASE_DIR from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.gui.gui_assets import icon_from_resource from deeplabcut.gui.tabs.docs import ( URL_3D, URL_MA_CONFIGURE, @@ -235,7 +235,7 @@ def lay_out_user_frame(self): self.loc_line = QtWidgets.QLineEdit(self.loc_default, user_frame) self.loc_line.setReadOnly(True) action = self.loc_line.addAction( - QIcon(str(BASE_DIR / "assets" / "icons" / "open2.png")), + icon_from_resource("icons", "open2.png"), QtWidgets.QLineEdit.TrailingPosition, ) action.triggered.connect(self.on_click) diff --git a/deeplabcut/gui/tabs/modelzoo.py b/deeplabcut/gui/tabs/modelzoo.py index 1ee07772e..1692bce31 100644 --- a/deeplabcut/gui/tabs/modelzoo.py +++ b/deeplabcut/gui/tabs/modelzoo.py @@ -15,11 +15,10 @@ import dlclibrary from PySide6 import QtWidgets from PySide6.QtCore import QRegularExpression, QSize, Qt, QTimer, Signal, Slot -from PySide6.QtGui import QIcon, QPixmap, QRegularExpressionValidator +from PySide6.QtGui import QRegularExpressionValidator import deeplabcut from deeplabcut.core.engine import Engine -from deeplabcut.gui import BASE_DIR from deeplabcut.gui.components import ( DefaultTab, VideoSelectionWidget, @@ -28,6 +27,7 @@ set_combo_items, set_layout_contents_visible, ) +from deeplabcut.gui.gui_assets import icon_from_resource, pixmap_from_resource from deeplabcut.gui.utils import move_to_separate_thread from deeplabcut.gui.widgets import ClickableLabel from deeplabcut.pose_estimation_pytorch.apis.utils import TORCHVISION_DETECTORS @@ -212,7 +212,7 @@ def _add_output_settings_section(self, layout: QtWidgets.QGridLayout): ) self.loc_line.setReadOnly(True) action = self.loc_line.addAction( - QIcon(str(BASE_DIR / "assets" / "icons" / "open2.png")), + icon_from_resource("icons", "open2.png"), QtWidgets.QLineEdit.TrailingPosition, ) action.triggered.connect(self.select_folder) @@ -250,7 +250,7 @@ def _add_tf_scales_row(self, layout: QtWidgets.QGridLayout): validator.validationChanged.connect(self._handle_validation_change) self.scales_line.setValidator(validator) tooltip_label = QtWidgets.QLabel() - tooltip_label.setPixmap(QPixmap(str(BASE_DIR / "assets" / "icons" / "help2.png")).scaledToWidth(30)) + tooltip_label.setPixmap(pixmap_from_resource("icons", "help2.png").scaledToWidth(30)) tooltip_label.setToolTip( "Approximate animal sizes in pixels, for spatial pyramid search. If left " "blank, defaults to video height +/- 50 pixels" @@ -268,7 +268,7 @@ def _add_use_adaptation_row(self, layout: QtWidgets.QGridLayout, layout_row: int self.adapt_checkbox.setStyleSheet("font-weight: bold; font-size: 16px; padding: 6px 12px;") # Add help button adapt_help_btn = QtWidgets.QToolButton() - adapt_help_btn.setIcon(QIcon(str(BASE_DIR / "assets" / "icons" / "help2.png"))) + adapt_help_btn.setIcon(icon_from_resource("icons", "help2.png")) adapt_help_btn.setIconSize(QSize(24, 24)) adapt_help_btn.setToolTip("What is video adaptation?") diff --git a/deeplabcut/gui/tabs/open_project.py b/deeplabcut/gui/tabs/open_project.py index 4ff9b2b10..f93c36415 100644 --- a/deeplabcut/gui/tabs/open_project.py +++ b/deeplabcut/gui/tabs/open_project.py @@ -11,7 +11,8 @@ from pathlib import Path from PySide6 import QtCore, QtWidgets -from PySide6.QtGui import QIcon + +from deeplabcut.gui.gui_assets import icon_from_resource class OpenProject(QtWidgets.QDialog): @@ -76,17 +77,13 @@ def open_project(self): msg.setWindowTitle("Error") msg.setMinimumWidth(400) - self.logo_dir = str(Path("logo.png").resolve().parent) + "/" - self.logo = self.logo_dir + "/assets/logo.png" - msg.setWindowIcon(QIcon(self.logo)) + icon = icon_from_resource("logo.png") + msg.setWindowIcon(icon) msg.setStandardButtons(QtWidgets.QMessageBox.Ok) msg.exec_() self.loaded = False else: - self.logo_dir = str(Path("logo.png").resolve().parent) + "/" - self.logo = self.logo_dir + "/assets/logo.png" - self.loaded = True self.accept() self.close() diff --git a/deeplabcut/gui/tabs/train_network.py b/deeplabcut/gui/tabs/train_network.py index 7298ab91a..265ca2ecb 100644 --- a/deeplabcut/gui/tabs/train_network.py +++ b/deeplabcut/gui/tabs/train_network.py @@ -11,11 +11,9 @@ from __future__ import annotations from dataclasses import dataclass -from pathlib import Path from PySide6 import QtWidgets from PySide6.QtCore import Qt, Slot -from PySide6.QtGui import QIcon import deeplabcut.compat as compat from deeplabcut.core.engine import Engine @@ -27,6 +25,7 @@ _create_label_widget, ) from deeplabcut.gui.displays.selected_shuffle_display import SelectedShuffleDisplay +from deeplabcut.gui.gui_assets import icon_from_resource from deeplabcut.gui.widgets import ConfigEditor @@ -244,9 +243,7 @@ def train_network(self): msg.setWindowTitle("Info") msg.setMinimumWidth(900) - self.logo_dir = str(Path("logo.png").resolve().parent) + "/" - self.logo = self.logo_dir + "/assets/logo.png" - msg.setWindowIcon(QIcon(self.logo)) + msg.setWindowIcon(icon_from_resource("logo.png")) msg.setStandardButtons(QtWidgets.QMessageBox.Ok) msg.exec_() diff --git a/deeplabcut/gui/window.py b/deeplabcut/gui/window.py index 2075720dd..a0eb6af42 100644 --- a/deeplabcut/gui/window.py +++ b/deeplabcut/gui/window.py @@ -20,7 +20,7 @@ from napari_deeplabcut import __version__ as NAPARI_DLC_VERSION from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtCore import Qt -from PySide6.QtGui import QAction, QIcon, QPixmap +from PySide6.QtGui import QAction, QPixmap from PySide6.QtWidgets import ( QComboBox, QLabel, @@ -36,8 +36,9 @@ from deeplabcut import auxiliaryfunctions, compat from deeplabcut.core.debug import install_debug_recorder from deeplabcut.core.engine import Engine -from deeplabcut.gui import BASE_DIR, components +from deeplabcut.gui import components from deeplabcut.gui.dialogs import create_generate_debug_log_action +from deeplabcut.gui.gui_assets import icon_from_resource, pixmap_from_resource from deeplabcut.gui.tabs import ( AnalyzeVideos, CreateTrainingDataset, @@ -213,9 +214,8 @@ def engine(self, e: Engine) -> None: msg.setWindowTitle("Info") msg.setMinimumWidth(900) - logo_dir = str(Path("logo.png").resolve().parent) + "/" - logo = logo_dir + "/assets/logo.png" - msg.setWindowIcon(QIcon(logo)) + icon = icon_from_resource("logo.png") + msg.setWindowIcon(icon) msg.setStandardButtons(QtWidgets.QMessageBox.Ok) msg.exec_() @@ -511,8 +511,8 @@ def window_set(self): palette.setColor(QtGui.QPalette.Window, QtGui.QColor("#ffffff")) self.setPalette(palette) - icon = str(BASE_DIR / "assets" / "logo.png") - self.setWindowIcon(QIcon(icon)) + # icon = str(BASE_DIR / "assets" / "logo.png") + self.setWindowIcon(icon_from_resource("logo.png")) # Set default window size and allow resizing self.resize( @@ -545,8 +545,7 @@ def _generate_welcome_page(self): image_widget = QtWidgets.QLabel(self) image_widget.setAlignment(Qt.AlignCenter) image_widget.setContentsMargins(0, 0, 0, 0) - logo = str(BASE_DIR / "assets" / "logo_transparent.png") - pixmap = QtGui.QPixmap(logo) + pixmap = pixmap_from_resource("logo_transparent.png") image_widget.setPixmap(pixmap.scaledToHeight(400, QtCore.Qt.SmoothTransformation)) self.layout.addWidget(image_widget) description = ( @@ -602,7 +601,7 @@ def create_actions(self, names): self.newAction = QAction(self) self.newAction.setText("&New Project...") - self.newAction.setIcon(QIcon(str(BASE_DIR / "assets" / "icons" / names[0]))) + self.newAction.setIcon(icon_from_resource("icons", names[0])) self.newAction.setShortcut("Ctrl+N") self.newAction.setStatusTip("Create a new project...") @@ -610,7 +609,7 @@ def create_actions(self, names): # Creating actions using the second constructor self.openAction = QAction("&Open...", self) - self.openAction.setIcon(QIcon(str(BASE_DIR / "assets" / "icons" / names[1]))) + self.openAction.setIcon(icon_from_resource("icons", names[1])) self.openAction.setShortcut("Ctrl+O") self.openAction.setStatusTip("Open a project...") self.openAction.triggered.connect(self._open_project) @@ -624,7 +623,7 @@ def create_actions(self, names): self.darkmodeAction.triggered.connect(self.darkmode) self.helpAction = QAction("&Help", self) - self.helpAction.setIcon(QIcon(str(BASE_DIR / "assets" / "icons" / names[2]))) + self.helpAction.setIcon(icon_from_resource("icons", names[2])) self.helpAction.setStatusTip("Ask for help...") self.helpAction.triggered.connect(self._ask_for_help)