Skip to content

Commit 97dcae6

Browse files
committed
refactor(ui): replace PIL icon color overlays with Qt
1 parent 67e03bc commit 97dcae6

8 files changed

Lines changed: 43 additions & 102 deletions

File tree

‎src/tagstudio/qt/controllers/main_window.py‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
from typing import override
1010

1111
import structlog
12-
from PIL import Image, ImageQt
1312
from PySide6 import QtCore, QtGui
1413
from PySide6.QtCore import QMetaObject, QSize, QStringListModel, Qt
15-
from PySide6.QtGui import QAction, QPixmap
14+
from PySide6.QtGui import QAction
1615
from PySide6.QtWidgets import (
1716
QCheckBox,
1817
QComboBox,
@@ -554,18 +553,14 @@ def setup_search_bar(self):
554553
self.search_bar_layout.setContentsMargins(WIN_PAD, 0, 0, 0)
555554

556555
self.back_button = QPushButton(self.central_widget)
557-
back_icon: Image.Image = self.rm.bxs_left_arrow
558-
back_icon = auto_theme_overlay(back_icon, use_alpha=False)
559-
self.back_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(back_icon)))
556+
self.back_button.setIcon(auto_theme_overlay(self.rm.bxs_left_arrow, use_alpha=False))
560557
self.back_button.setObjectName("back_button")
561558
self.back_button.setMinimumSize(QSize(32, 32))
562559
self.back_button.setMaximumSize(QSize(32, 16777215))
563560
self.search_bar_layout.addWidget(self.back_button)
564561

565562
self.forward_button = QPushButton(self.central_widget)
566-
forward_icon: Image.Image = self.rm.bxs_right_arrow
567-
forward_icon = auto_theme_overlay(forward_icon, use_alpha=False)
568-
self.forward_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(forward_icon)))
563+
self.forward_button.setIcon(auto_theme_overlay(self.rm.bxs_right_arrow, use_alpha=False))
569564
self.forward_button.setIconSize(QSize(16, 16))
570565
self.forward_button.setObjectName("forward_button")
571566
self.forward_button.setMinimumSize(QSize(32, 32))

‎src/tagstudio/qt/controllers/suggest_box.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Any, override
66

77
import structlog
8-
from PIL import Image, ImageQt
8+
from PIL import Image
99
from PySide6.QtCore import Signal
1010
from PySide6.QtGui import QAction, QPixmap, QShowEvent, Qt
1111
from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget
@@ -133,8 +133,7 @@ def _connect_callbacks(self) -> None:
133133

134134
def set_hint_icon(self, icon: Image.Image | None) -> None:
135135
if icon:
136-
pixmap = QPixmap.fromImage(ImageQt.ImageQt(auto_theme_overlay(icon)))
137-
self.layout().hint_icon_action.setIcon(pixmap)
136+
self.layout().hint_icon_action.setIcon(auto_theme_overlay(icon))
138137
else:
139138
self.layout().hint_icon_action.setIcon(QPixmap())
140139

‎src/tagstudio/qt/mixed/landing.py‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,10 @@ def update_logo_color(self, style: typing.Literal["mono", "color"] = "mono"):
105105
style (str): = The style of the logo. Either "mono" or "color".
106106
"""
107107
if style == "mono":
108-
logo_im = auto_theme_overlay(LandingWidget.mono_logo)
108+
self.landing_pixmap = auto_theme_overlay(LandingWidget.mono_logo)
109109
elif style == "color":
110-
logo_im = LandingWidget.color_logo
110+
self.landing_pixmap = QPixmap.fromImage(ImageQt.ImageQt(LandingWidget.color_logo))
111111

112-
logo_final: Image.Image = Image.new(
113-
mode="RGBA", size=LandingWidget.mono_logo.size, color="#00000000"
114-
)
115-
116-
logo_final.paste(logo_im, (0, 0), mask=LandingWidget.mono_logo)
117-
118-
self.landing_pixmap = QPixmap.fromImage(ImageQt.ImageQt(logo_im))
119112
self.landing_pixmap.setDevicePixelRatio(self._pixel_ratio)
120113
self.landing_pixmap = self.landing_pixmap.scaledToWidth(
121114
self._logo_width, Qt.TransformationMode.SmoothTransformation

‎src/tagstudio/qt/mixed/media_player.py‎

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from typing import override
99

1010
import structlog
11-
from PIL import ImageQt
1211
from PySide6.QtCore import QEvent, QObject, QRectF, QSize, Qt, QUrl, QVariantAnimation
1312
from PySide6.QtGui import (
1413
QAction,
@@ -17,7 +16,6 @@
1716
QLinearGradient,
1817
QMouseEvent,
1918
QPen,
20-
QPixmap,
2119
QResizeEvent,
2220
)
2321
from PySide6.QtMultimedia import QAudioOutput, QMediaDevices, QMediaPlayer
@@ -36,7 +34,7 @@
3634

3735
from tagstudio.i18n.translations import Translations
3836
from tagstudio.qt.controllers.clickable_slider import ClickableSlider
39-
from tagstudio.qt.views.styles.color_overlay import light_overlay
37+
from tagstudio.qt.views.styles.color_overlay import apply_overlay
4038

4139
if typing.TYPE_CHECKING:
4240
from tagstudio.qt.qt_driver import QtDriver
@@ -56,18 +54,11 @@ class MediaPlayer(QGraphicsView):
5654
def __init__(self, driver: QtDriver) -> None:
5755
super().__init__()
5856
self.driver = driver
59-
self.play_icon = QPixmap.fromImage(
60-
ImageQt.ImageQt(light_overlay(self.driver.rm.bxs_right_arrow, use_alpha=False))
61-
)
62-
self.pause_icon = QPixmap.fromImage(
63-
ImageQt.ImageQt(light_overlay(self.driver.rm.pause_icon, use_alpha=False))
64-
)
65-
self.mute_icon = QPixmap.fromImage(
66-
ImageQt.ImageQt(light_overlay(self.driver.rm.mute_icon, use_alpha=False))
67-
)
68-
self.volume_icon = QPixmap.fromImage(
69-
ImageQt.ImageQt(light_overlay(self.driver.rm.volume_icon, use_alpha=False))
70-
)
57+
white = QColor(Qt.GlobalColor.white)
58+
self.play_icon = apply_overlay(self.driver.rm.bxs_right_arrow, white)
59+
self.pause_icon = apply_overlay(self.driver.rm.pause_icon, white)
60+
self.mute_icon = apply_overlay(self.driver.rm.mute_icon, white)
61+
self.volume_icon = apply_overlay(self.driver.rm.volume_icon, white)
7162

7263
slider_style = """
7364
QSlider {

‎src/tagstudio/qt/views/library_info_window_view.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,7 @@ def __init__(self, library: Library, driver: QtDriver):
291291

292292
self.dupe_file_icon = QLabel()
293293
dupe_file_image: Image.Image = self.driver.rm.dupe_file_stat
294-
dupe_file_pixmap = QPixmap.fromImage(
295-
ImageQt.ImageQt(auto_theme_overlay(dupe_file_image, use_alpha=False))
296-
)
294+
dupe_file_pixmap = auto_theme_overlay(dupe_file_image, use_alpha=False)
297295
dupe_file_pixmap.setDevicePixelRatio(self.devicePixelRatio())
298296
dupe_file_pixmap = dupe_file_pixmap.scaledToWidth(
299297
math.floor((row_height - icon_margin) * self.devicePixelRatio()),

‎src/tagstudio/qt/views/pagination_view.py‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44

55
from typing import override
66

7-
from PIL import Image, ImageQt
87
from PySide6.QtCore import QSize
9-
from PySide6.QtGui import QIntValidator, QPixmap, Qt
8+
from PySide6.QtGui import QIntValidator, Qt
109
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QPushButton, QWidget
1110

1211
from tagstudio.qt.resource_manager import ResourceManager
@@ -38,8 +37,7 @@ def __init__(self) -> None:
3837

3938
# [<] ----------------------------------
4039
self.prev_button = QPushButton()
41-
prev_icon: Image.Image = auto_theme_overlay(_rm.bxs_left_arrow, use_alpha=False)
42-
self.prev_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(prev_icon)))
40+
self.prev_button.setIcon(auto_theme_overlay(_rm.bxs_left_arrow, use_alpha=False))
4341
self.prev_button.setIconSize(QSize(12, 12))
4442
self.prev_button.setMinimumSize(self.BUTTON_SIZE)
4543
self.prev_button.setMaximumSize(self.BUTTON_SIZE)
@@ -86,8 +84,7 @@ def __init__(self) -> None:
8684

8785
# ---------------------------------- [>]
8886
self.next_button = QPushButton()
89-
next_icon: Image.Image = auto_theme_overlay(_rm.bxs_right_arrow, use_alpha=False)
90-
self.next_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(next_icon)))
87+
self.next_button.setIcon(auto_theme_overlay(_rm.bxs_right_arrow, use_alpha=False))
9188
self.next_button.setIconSize(QSize(12, 12))
9289
self.next_button.setMinimumSize(self.BUTTON_SIZE)
9390
self.next_button.setMaximumSize(self.BUTTON_SIZE)

‎src/tagstudio/qt/views/styles/color_overlay.py‎

Lines changed: 23 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,22 @@
22
# SPDX-License-Identifier: GPL-3.0-only
33

44

5-
from PIL import Image
5+
from PIL import Image, ImageQt
66
from PySide6.QtCore import QByteArray, QRectF, QSize, Qt
7-
from PySide6.QtGui import QColor, QGuiApplication, QPainter, QPixmap
7+
from PySide6.QtGui import QColor, QGuiApplication, QImage, QPainter, QPixmap
88
from PySide6.QtSvg import QSvgRenderer
99

1010
# TODO: Consolidate the built-in QT theme values with the values
1111
# here, in enums.py, and in palette.py.
12-
_THEME_DARK_FG: str = "#FFFFFF77"
13-
_THEME_LIGHT_FG: str = "#000000DD"
14-
_THEME_DARK_BG: str = "#000000DD"
15-
_THEME_LIGHT_BG: str = "#FFFFFF55"
12+
_THEME_DARK_FG = QColor(255, 255, 255, 120)
13+
_THEME_LIGHT_FG = QColor(0, 0, 0, 220)
1614

1715

1816
def theme_foreground_color() -> QColor:
1917
"""Return a translucent foreground color for icons and hint text in the current theme."""
2018
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark:
21-
return QColor(255, 255, 255, 0x77)
22-
return QColor(0, 0, 0, 0x77)
19+
return QColor(255, 255, 255, 120)
20+
return QColor(0, 0, 0, 120)
2321

2422

2523
def svg_to_pixmap(
@@ -43,58 +41,33 @@ def svg_to_pixmap(
4341

4442
def auto_theme_overlay(
4543
image: Image.Image, inverse: bool = False, use_alpha: bool = True
46-
) -> Image.Image:
44+
) -> QPixmap:
4745
"""Overlay the current foreground theme color onto an image.
4846
4947
Args:
5048
image (Image): The PIL Image object to apply an overlay to.
5149
inverse (bool): Option to invert the overlay color relative to the current theme.
52-
use_alpha (bool): Option to retain the base image's alpha value when applying the overlay.
50+
use_alpha (bool): Option to use the theme's translucent foreground color.
5351
"""
54-
dark_fg: str = _THEME_DARK_FG[:-2] if not use_alpha else _THEME_DARK_FG
55-
light_fg: str = _THEME_LIGHT_FG[:-2] if not use_alpha else _THEME_LIGHT_FG
56-
57-
overlay_color = (
58-
dark_fg if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark else light_fg
59-
)
52+
is_dark = QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
6053
if inverse:
61-
overlay_color = light_fg if overlay_color == dark_fg else dark_fg
62-
63-
im = Image.new(mode="RGBA", size=image.size, color=overlay_color)
64-
return _apply_overlay(image, im)
65-
66-
67-
def light_overlay(image: Image.Image, use_alpha: bool = True) -> Image.Image:
68-
"""Overlay the light theme foreground color onto an image.
69-
70-
Args:
71-
image (Image): The PIL Image object to apply an overlay to.
72-
use_alpha (bool): Option to retain the base image's alpha value when applying the overlay.
73-
"""
74-
overlay_color: str = _THEME_DARK_FG[:-2] if not use_alpha else _THEME_DARK_FG
75-
im = Image.new(mode="RGBA", size=image.size, color=overlay_color)
76-
return _apply_overlay(image, im)
54+
is_dark = not is_dark
55+
color = QColor(_THEME_DARK_FG if is_dark else _THEME_LIGHT_FG)
56+
if not use_alpha:
57+
color.setAlpha(255)
58+
return apply_overlay(image, color)
7759

7860

79-
def dark_overlay(image: Image.Image, use_alpha: bool = True) -> Image.Image:
80-
"""Overlay the dark theme foreground color onto an image.
61+
def apply_overlay(image: Image.Image, color: QColor) -> QPixmap:
62+
"""Apply a color overlay on top of an image using the image's alpha channel as a mask.
8163
8264
Args:
8365
image (Image): The PIL Image object to apply an overlay to.
84-
use_alpha (bool): Option to retain the base image's alpha value when applying the overlay.
66+
color (QColor): The color of the overlay.
8567
"""
86-
overlay_color: str = _THEME_LIGHT_FG[:-2] if not use_alpha else _THEME_LIGHT_FG
87-
im = Image.new(mode="RGBA", size=image.size, color=overlay_color)
88-
return _apply_overlay(image, im)
89-
90-
91-
def _apply_overlay(image: Image.Image, overlay: Image.Image) -> Image.Image:
92-
"""Apply an overlay on top of an image using the image's alpha channel as a mask.
93-
94-
Args:
95-
image (Image): The PIL Image object to apply an overlay to.
96-
overlay (Image): The PIL Image object to act as the overlay contents.
97-
"""
98-
im: Image.Image = Image.new(mode="RGBA", size=image.size, color="#00000000")
99-
im.paste(overlay, (0, 0), mask=image)
100-
return im
68+
overlaid = ImageQt.ImageQt(image).convertToFormat(QImage.Format.Format_ARGB32_Premultiplied)
69+
painter = QPainter(overlaid)
70+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
71+
painter.fillRect(overlaid.rect(), color)
72+
painter.end()
73+
return QPixmap.fromImage(overlaid)

‎src/tagstudio/qt/views/tiles/tile_view.py‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
# SPDX-License-Identifier: GPL-3.0-only
33

44

5-
from PIL import ImageQt
65
from PySide6.QtCore import QSize, Qt
7-
from PySide6.QtGui import QPixmap
86
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
97

108
from tagstudio.qt.resource_manager import ResourceManager
@@ -21,9 +19,6 @@ class TileView(QVBoxLayout):
2119
"""The layout used for a Tile widget."""
2220

2321
_rm = ResourceManager()
24-
copy_icon = auto_theme_overlay(_rm.copy, inverse=True)
25-
edit_icon = auto_theme_overlay(_rm.edit, inverse=True)
26-
trash_icon = auto_theme_overlay(_rm.trash, inverse=True)
2722

2823
def __init__(self) -> None:
2924
super().__init__()
@@ -53,7 +48,7 @@ def __init__(self) -> None:
5348
self.copy_button.setMinimumSize(_BUTTON_SIZE, _BUTTON_SIZE)
5449
self.copy_button.setMaximumSize(_BUTTON_SIZE, _BUTTON_SIZE)
5550
self.copy_button.setFlat(True)
56-
self.copy_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.copy_icon)))
51+
self.copy_button.setIcon(auto_theme_overlay(self._rm.copy, inverse=True))
5752
self.copy_button.setIconSize(QSize(_ICON_SIZE, _ICON_SIZE))
5853
self.copy_button.setCursor(Qt.CursorShape.PointingHandCursor)
5954
self.title_layout.addWidget(self.copy_button)
@@ -63,7 +58,7 @@ def __init__(self) -> None:
6358
self.edit_button.setMinimumSize(_BUTTON_SIZE, _BUTTON_SIZE)
6459
self.edit_button.setMaximumSize(_BUTTON_SIZE, _BUTTON_SIZE)
6560
self.edit_button.setFlat(True)
66-
self.edit_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.edit_icon)))
61+
self.edit_button.setIcon(auto_theme_overlay(self._rm.edit, inverse=True))
6762
self.edit_button.setIconSize(QSize(_ICON_SIZE, _ICON_SIZE))
6863
self.edit_button.setCursor(Qt.CursorShape.PointingHandCursor)
6964
self.title_layout.addWidget(self.edit_button)
@@ -73,7 +68,7 @@ def __init__(self) -> None:
7368
self.remove_button.setMinimumSize(_BUTTON_SIZE, _BUTTON_SIZE)
7469
self.remove_button.setMaximumSize(_BUTTON_SIZE, _BUTTON_SIZE)
7570
self.remove_button.setFlat(True)
76-
self.remove_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.trash_icon)))
71+
self.remove_button.setIcon(auto_theme_overlay(self._rm.trash, inverse=True))
7772
self.remove_button.setIconSize(QSize(_ICON_SIZE, _ICON_SIZE))
7873
self.remove_button.setCursor(Qt.CursorShape.PointingHandCursor)
7974
self.title_layout.addWidget(self.remove_button)

0 commit comments

Comments
 (0)