diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po index 3e10cd1..fd7cbbb 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po @@ -51,6 +51,12 @@ msgstr "Avec Qt 5, les tailles des panneaux ancrables enregistrées à la fermet msgid "Closing the main window (unless it is only hidden, see `hide_on_close`) now releases it as the instance returned by `get_instance`, which creates a new window afterwards: the closed window, with its widgets and data, is no longer kept in memory until another main window is created." msgstr "La fermeture de la fenêtre principale (sauf si elle est seulement masquée, voir `hide_on_close`) la libère désormais en tant qu'instance renvoyée par `get_instance`, qui crée ensuite une nouvelle fenêtre : la fenêtre fermée, avec ses widgets et ses données, n'est plus conservée en mémoire jusqu'à la création d'une autre fenêtre principale." +msgid "High-DPI displays" +msgstr "Écrans haute résolution" + +msgid "The splash screen and the watermark of empty plots are no longer blurry with a display scaling greater than 100% (e.g. 125%): when their image has a `@2x` high-resolution variant (e.g. `splash@2x.png` next to `splash.png`), it is used to display them at the resolution of the screen instead of upscaling the image. The fallback splash screen, shown when no image is available, is also drawn at the resolution of the screen." +msgstr "L'écran de démarrage et le filigrane des graphiques vides ne sont plus flous avec une mise à l'échelle de l'affichage supérieure à 100 % (par exemple 125 %) : lorsque leur image dispose d'une variante haute résolution `@2x` (par exemple `splash@2x.png` à côté de `splash.png`), celle-ci est utilisée pour les afficher à la résolution de l'écran au lieu d'agrandir l'image. L'écran de démarrage de repli, affiché lorsqu'aucune image n'est disponible, est lui aussi dessiné à la résolution de l'écran." + msgid "Qt helpers" msgstr "Utilitaires Qt" diff --git a/doc/release_notes/release_1.01.md b/doc/release_notes/release_1.01.md index a2a0353..8668e4b 100644 --- a/doc/release_notes/release_1.01.md +++ b/doc/release_notes/release_1.01.md @@ -36,6 +36,10 @@ * With Qt 5, the dock widget sizes saved on exit are now restored at startup: the window is sized and laid out before its state is restored, so tabified docks no longer shrink to the default window size and leave the extra width to the central widget. * Closing the main window (unless it is only hidden, see `hide_on_close`) now releases it as the instance returned by `get_instance`, which creates a new window afterwards: the closed window, with its widgets and data, is no longer kept in memory until another main window is created. +### High-DPI displays ### + +* The splash screen and the watermark of empty plots are no longer blurry with a display scaling greater than 100% (e.g. 125%): when their image has a `@2x` high-resolution variant (e.g. `splash@2x.png` next to `splash.png`), it is used to display them at the resolution of the screen instead of upscaling the image. The fallback splash screen, shown when no image is available, is also drawn at the resolution of the screen. + ### Qt helpers ### * `block_signals` now restores the previous signal-blocking state of the widget and of its children instead of unblocking them unconditionally: nested blocking contexts and widgets already blocked by the caller keep their state. diff --git a/sigimax/tests/widgets/test_splashscreen_resource.py b/sigimax/tests/widgets/test_splashscreen_resource.py index 3f5dd73..ab9dc54 100644 --- a/sigimax/tests/widgets/test_splashscreen_resource.py +++ b/sigimax/tests/widgets/test_splashscreen_resource.py @@ -6,6 +6,8 @@ from unittest.mock import patch +from guidata.configtools import get_pixmap +from qtpy import QtCore as QC from qtpy import QtGui as QG from sigimax.widgets.splashscreen import SigimaXSplashScreen, SplashScreenConfig @@ -32,6 +34,32 @@ def test_splash_resolves_image_basename(tmp_path) -> None: splash.close() +def test_splash_uses_hidpi_variant(tmp_path) -> None: + """The ``@2x`` variant of the image is used on high-DPI displays.""" + image_path = tmp_path / "splash.png" + for path, size, color in ( + (image_path, (64, 32), "red"), + (tmp_path / "splash@2x.png", (128, 64), "blue"), + ): + pixmap = QG.QPixmap(*size) + pixmap.fill(QG.QColor(color)) + assert pixmap.save(str(path)) + + config = SplashScreenConfig(image_path=str(image_path), show_progress=False) + with patch( + "sigimax.widgets.splashscreen.get_pixmap", + side_effect=lambda name, default: get_pixmap(name, default, 2.0), + ): + splash = SigimaXSplashScreen(config) + + pixmap = splash.pixmap() + assert pixmap.devicePixelRatio() == 2.0 + assert pixmap.size() == QC.QSize(128, 64) + assert pixmap.toImage().pixelColor(0, 0) == QG.QColor("blue") + assert splash.size() == QC.QSize(64, 32) + splash.close() + + def test_missing_splash_resource_uses_fallback() -> None: """An unresolved configured image must not prevent application startup.""" config = SplashScreenConfig( @@ -45,8 +73,8 @@ def test_missing_splash_resource_uses_fallback() -> None: splash = SigimaXSplashScreen(config) assert not splash.pixmap().isNull() - assert splash.pixmap().size().width() == 480 - assert splash.pixmap().size().height() == 280 + pixmap = splash.pixmap() + assert pixmap.size() / pixmap.devicePixelRatio() == QC.QSize(480, 280) splash.close() diff --git a/sigimax/widgets/plotdock.py b/sigimax/widgets/plotdock.py index 627cd9b..d055190 100644 --- a/sigimax/widgets/plotdock.py +++ b/sigimax/widgets/plotdock.py @@ -26,10 +26,12 @@ "SigimaXPlotWidget", ] +import os.path as osp import warnings from typing import TYPE_CHECKING import numpy as np +from guidata.configtools import get_pixmap from guidata.qthelpers import is_dark_theme from guidata.widgets.dockable import DockableWidget from plotpy.constants import PlotType @@ -329,12 +331,17 @@ def _apply_dock_location(self) -> None: def _setup_watermark(self) -> None: """Create the watermark label from the configured image path. - If ``Conf.watermark_image_path`` is empty, no watermark is created. + If ``Conf.watermark_image_path`` is empty, no watermark is created. On + high-DPI displays, the ``@2x`` high-resolution variant of the image is used + if available (see :func:`guidata.configtools.get_pixmap`). """ path = get_conf().watermark_image_path.get() if path: self.watermark = QW.QLabel() - pixmap = QG.QPixmap(path) + if osp.isfile(path): + pixmap = get_pixmap(osp.abspath(path), default=None) + else: + pixmap = QG.QPixmap(path) self.watermark.setPixmap(pixmap) else: self.watermark = None diff --git a/sigimax/widgets/splashscreen.py b/sigimax/widgets/splashscreen.py index 89767fa..1390de3 100644 --- a/sigimax/widgets/splashscreen.py +++ b/sigimax/widgets/splashscreen.py @@ -42,9 +42,10 @@ from __future__ import annotations import dataclasses +import os.path as osp from typing import TYPE_CHECKING -from guidata.configtools import get_image_file_path +from guidata.configtools import get_image_file_path, get_pixmap from qtpy import QtCore as QC from qtpy import QtGui as QG from qtpy import QtWidgets as QW @@ -67,8 +68,11 @@ class SplashScreenConfig: Args: image_path: Absolute or relative path to the splash image (PNG, SVG, - or any format supported by :class:`QPixmap`). If empty or ``None``, - the splash screen is disabled. + or any format supported by :class:`QPixmap`), or name of an image + registered in guidata's image paths. A ``@2x`` high-resolution variant + of the image (e.g. ``splash@2x.png`` next to ``splash.png``) is used on + high-DPI displays, if available. If empty or ``None``, the splash screen + is disabled. app_name: Application name overlaid on the splash image. app_version: Application version overlaid on the splash image. tagline: Optional subtitle displayed below the version. @@ -177,18 +181,24 @@ def from_conf(cls) -> SigimaXSplashScreen | None: def _load_pixmap(self) -> QG.QPixmap: """Load the splash image as a :class:`QPixmap`. + The image is loaded at the resolution of the display: on high-DPI displays, + its ``@2x`` high-resolution variant (e.g. ``splash@2x.png`` next to + ``splash.png``) is used if available (see + :func:`guidata.configtools.get_pixmap`). + Returns: The loaded pixmap. If the image cannot be loaded, a minimal fallback pixmap is generated. """ path = self._config.image_path or "" - pixmap = QG.QPixmap(path) - if pixmap.isNull() and path: + if path and not osp.isfile(path): try: - resolved_path = get_image_file_path(path, default=None) + path = get_image_file_path(path, default=None) except RuntimeError: - resolved_path = "" - pixmap = QG.QPixmap(resolved_path) + path = "" + pixmap = QG.QPixmap() + if path: + pixmap = get_pixmap(osp.abspath(path), default=None) if pixmap.isNull(): pixmap = self._create_fallback_pixmap() return pixmap @@ -197,11 +207,13 @@ def _create_fallback_pixmap(self) -> QG.QPixmap: """Create a minimal fallback pixmap when no image is available. Returns: - A 480x280 pixmap with the application name drawn on a dark - background. + A 480x280 pixmap (logical size, drawn at the resolution of the + display) with the application name drawn on a dark background. """ width, height = 480, 280 - pixmap = QG.QPixmap(width, height) + ratio = QG.QGuiApplication.instance().devicePixelRatio() + pixmap = QG.QPixmap(round(width * ratio), round(height * ratio)) + pixmap.setDevicePixelRatio(ratio) pixmap.fill(QG.QColor(40, 40, 40)) painter = QG.QPainter(pixmap)