diff --git a/rare/components/tabs/games/__init__.py b/rare/components/tabs/games/__init__.py index 91a91a8f..67a62ca9 100644 --- a/rare/components/tabs/games/__init__.py +++ b/rare/components/tabs/games/__init__.py @@ -87,7 +87,7 @@ class GamesTab(QStackedWidget, Ui_GamesTab): logger.warning("No Unreal engine in library found") self.ue_name = "" - self.uninstalled_info_tabs = UninstalledInfoTabs(self.ue_name, self) + self.uninstalled_info_tabs = UninstalledInfoTabs(self) self.uninstalled_info_tabs.back_clicked.connect(lambda: self.setCurrentIndex(0)) self.addWidget(self.uninstalled_info_tabs) diff --git a/rare/components/tabs/games/game_info/game_dlc.py b/rare/components/tabs/games/game_info/game_dlc.py index 0cc325f7..0f2b65ba 100644 --- a/rare/components/tabs/games/game_info/game_dlc.py +++ b/rare/components/tabs/games/game_info/game_dlc.py @@ -1,5 +1,4 @@ -from PyQt5.QtCore import Qt, pyqtSignal -from PyQt5.QtGui import QPixmap, QResizeEvent +from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QFrame, QWidget, QMessageBox from legendary.models.game import Game @@ -9,6 +8,7 @@ from rare.shared.image_manager import ImageManagerSingleton, ImageSize from rare.ui.components.tabs.games.game_info.game_dlc import Ui_GameDlc from rare.ui.components.tabs.games.game_info.game_dlc_widget import Ui_GameDlcWidget from rare.utils.models import InstallOptionsModel +from rare.widgets.image_widget import ImageWidget class GameDlc(QWidget, Ui_GameDlc): @@ -94,13 +94,15 @@ class GameDlcWidget(QFrame, Ui_GameDlcWidget): self.setupUi(self) self.dlc = dlc - self.image.setFixedSize(ImageSize.Smaller.size) + self.image = ImageWidget(self) + self.image.setFixedSize(ImageSize.Smaller) + self.dlc_layout.insertWidget(0, self.image) self.dlc_name.setText(dlc.app_title) self.version.setText(dlc.app_version()) self.app_name.setText(dlc.app_name) - self.pixmap = self.image_manager.get_pixmap(dlc.app_name) + self.image.setPixmap(self.image_manager.get_pixmap(dlc.app_name)) if installed: self.action_button.setProperty("uninstall", 1) @@ -111,26 +113,6 @@ class GameDlcWidget(QFrame, Ui_GameDlcWidget): self.action_button.clicked.connect(self.install_game) self.action_button.setText(self.tr("Install DLC")) - def resizeEvent(self, a0: QResizeEvent) -> None: - self.image.clear() - super(GameDlcWidget, self).resizeEvent(a0) - self.setPixmap(self.pixmap) - - def setPixmap(self, a0: QPixmap) -> None: - self.pixmap = a0 - self.image.setPixmap( - self.pixmap.scaledToHeight( - self.dlc_info.size().height() - - ( - self.image.contentsMargins().top() - + self.image.contentsMargins().bottom() - + self.image.lineWidth() * 2 - ), - Qt.SmoothTransformation, - ) - ) - self.image.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) - def uninstall_dlc(self): self.action_button.setDisabled(True) self.action_button.setText(self.tr("Uninstalling")) diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py index 18b4dfd2..a7ed3efd 100644 --- a/rare/components/tabs/games/game_info/game_info.py +++ b/rare/components/tabs/games/game_info/game_info.py @@ -40,6 +40,7 @@ from rare.utils.legendary_utils import VerifyWorker from rare.utils.models import InstallOptionsModel from rare.utils.steam_grades import SteamWorker from rare.utils.utils import get_size +from rare.widgets.image_widget import ImageWidget logger = getLogger("GameInfo") @@ -60,6 +61,10 @@ class GameInfo(QWidget, Ui_GameInfo): self.image_manager = ImageManagerSingleton() self.game_utils = game_utils + self.image = ImageWidget(self) + self.image.setFixedSize(ImageSize.Display) + self.layout_game_info.insertWidget(0, self.image, alignment=Qt.AlignTop) + if platform.system() == "Windows": self.lbl_grade.setVisible(False) self.grade.setVisible(False) @@ -283,11 +288,7 @@ class GameInfo(QWidget, Ui_GameInfo): self.igame = self.core.get_installed_game(self.game.app_name) self.title.setTitle(self.game.app_title) - pixmap = self.image_manager.get_pixmap(self.game.app_name) - if pixmap.isNull(): - pixmap = self.image_manager.get_pixmap(self.parent().parent().parent().ue_name) - pixmap = pixmap.scaled(ImageSize.Display.size) - self.image.setPixmap(pixmap) + self.image.setPixmap(self.image_manager.get_pixmap(self.game.app_name, color=True)) self.app_name.setText(self.game.app_name) if self.igame: diff --git a/rare/components/tabs/games/game_info/uninstalled_info.py b/rare/components/tabs/games/game_info/uninstalled_info.py index e41e6d78..79c582c9 100644 --- a/rare/components/tabs/games/game_info/uninstalled_info.py +++ b/rare/components/tabs/games/game_info/uninstalled_info.py @@ -17,16 +17,17 @@ from rare.utils.extra_widgets import SideTabWidget from rare.utils.json_formatter import QJsonModel from rare.utils.models import InstallOptionsModel from rare.utils.steam_grades import SteamWorker +from rare.widgets.image_widget import ImageWidget class UninstalledInfoTabs(SideTabWidget): - def __init__(self, ue_default_name, parent=None): + def __init__(self, parent=None): super(UninstalledInfoTabs, self).__init__(show_back=True, parent=parent) self.core = LegendaryCoreSingleton() self.signals = GlobalSignalsSingleton() self.args = ArgumentsSingleton() - self.info = UninstalledInfo(ue_default_name) + self.info = UninstalledInfo() self.info.install_button.setDisabled(self.args.offline) self.addTab(self.info, self.tr("Information")) @@ -70,7 +71,7 @@ class GameMetadataView(QTreeView): class UninstalledInfo(QWidget, Ui_GameInfo): game: Game - def __init__(self, ue_default_name, parent=None): + def __init__(self, parent=None): super(UninstalledInfo, self).__init__(parent=parent) self.setupUi(self) self.core = LegendaryCoreSingleton() @@ -78,6 +79,10 @@ class UninstalledInfo(QWidget, Ui_GameInfo): self.api_results = ApiResultsSingleton() self.image_manager = ImageManagerSingleton() + self.image = ImageWidget(self) + self.image.setFixedSize(ImageSize.Display) + self.layout_game_info.insertWidget(0, self.image, alignment=Qt.AlignTop) + self.install_button.clicked.connect(self.install_game) if platform.system() != "Windows": self.steam_worker = SteamWorker(self.core) @@ -96,7 +101,6 @@ class UninstalledInfo(QWidget, Ui_GameInfo): self.game_actions_stack.setCurrentIndex(1) self.game_actions_stack.resize(self.game_actions_stack.minimumSize()) self.lbl_platform.setText(self.tr("Platforms")) - self.ue_default_name = ue_default_name def install_game(self): self.signals.install_game.emit(InstallOptionsModel(app_name=self.game.app_name)) @@ -111,11 +115,7 @@ class UninstalledInfo(QWidget, Ui_GameInfo): available_platforms.append("macOS") self.platform.setText(", ".join(available_platforms)) - pixmap = self.image_manager.get_pixmap(game.app_name, color=False) - if pixmap.isNull(): - pixmap = self.image_manager.get_pixmap(self.ue_default_name, color=False) - pixmap = pixmap.scaled(ImageSize.Display.size) - self.image.setPixmap(pixmap) + self.image.setPixmap(self.image_manager.get_pixmap(self.game.app_name, color=True)) self.app_name.setText(self.game.app_name) self.version.setText(self.game.app_version("Windows")) diff --git a/rare/components/tabs/games/game_widgets/base_installed_widget.py b/rare/components/tabs/games/game_widgets/base_installed_widget.py index f9d64842..811aec89 100644 --- a/rare/components/tabs/games/game_widgets/base_installed_widget.py +++ b/rare/components/tabs/games/game_widgets/base_installed_widget.py @@ -4,12 +4,13 @@ from logging import getLogger from PyQt5.QtCore import pyqtSignal, QProcess, QSettings, QStandardPaths, Qt, QByteArray from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QGroupBox, QMessageBox, QAction, QLabel +from PyQt5.QtWidgets import QGroupBox, QMessageBox, QAction from rare.components.tabs.games.game_utils import GameUtils from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton from rare.shared.image_manager import ImageManagerSingleton, ImageSize from rare.utils.utils import create_desktop_link +from rare.widgets.image_widget import ImageWidget logger = getLogger("Game") @@ -63,11 +64,9 @@ class BaseInstalledWidget(QGroupBox): except AttributeError: pass - self.image = QLabel() - self.image.setFixedSize(ImageSize.Display.size) - self.image.setPixmap( - pixmap.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation) - ) + self.image = ImageWidget(self) + self.image.setFixedSize(ImageSize.Display) + self.image.setPixmap(pixmap) self.game_running = False self.offline = self.args.offline self.update_available = False @@ -144,7 +143,7 @@ class BaseInstalledWidget(QGroupBox): def reload_image(self): self.image_manager.download_image_blocking(self.game, force=True) pm = self.image_manager.get_pixmap(self.game.app_name, color=True) - self.image.setPixmap(pm.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation)) + self.image.setPixmap(pm) def create_desktop_link(self, type_of_link): if type_of_link == "desktop": diff --git a/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py b/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py index de216578..8cb1eb8a 100644 --- a/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py +++ b/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py @@ -1,10 +1,11 @@ from logging import getLogger from PyQt5.QtCore import pyqtSignal, Qt -from PyQt5.QtWidgets import QGroupBox, QLabel, QAction +from PyQt5.QtWidgets import QGroupBox, QAction from legendary.models.game import Game from rare.shared.image_manager import ImageManagerSingleton, ImageSize +from rare.widgets.image_widget import ImageWidget logger = getLogger("Uninstalled") @@ -21,9 +22,9 @@ class BaseUninstalledWidget(QGroupBox): self.game.app_title = f"{self.game.app_title} {self.game.app_name.split('_')[-1]}" self.core = core - self.image = QLabel() - self.image.setFixedSize(ImageSize.Display.size) - self.image.setPixmap(pixmap.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation)) + self.image = ImageWidget(self) + self.image.setFixedSize(ImageSize.Display) + self.image.setPixmap(pixmap) self.installing = False self.setContextMenuPolicy(Qt.ActionsContextMenu) self.setContentsMargins(0, 0, 0, 0) @@ -35,7 +36,7 @@ class BaseUninstalledWidget(QGroupBox): def reload_image(self): self.image_manager.download_image_blocking(self.game, force=True) pm = self.image_manager.get_pixmap(self.game.app_name, color=False) - self.image.setPixmap(pm.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation)) + self.image.setPixmap(pm) def install(self): self.show_uninstalled_info.emit(self.game) diff --git a/rare/components/tabs/games/game_widgets/installing_game_widget.py b/rare/components/tabs/games/game_widgets/installing_game_widget.py index 0299e313..93753ec8 100644 --- a/rare/components/tabs/games/game_widgets/installing_game_widget.py +++ b/rare/components/tabs/games/game_widgets/installing_game_widget.py @@ -1,41 +1,46 @@ -from PyQt5.QtCore import Qt, QRect -from PyQt5.QtGui import QPaintEvent, QPainter, QPixmap, QPen, QFont, QColor -from PyQt5.QtWidgets import QVBoxLayout, QLabel, QHBoxLayout, QWidget +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPixmap +from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QGroupBox, QWidget from legendary.models.game import Game from rare.shared import LegendaryCoreSingleton from rare.shared.image_manager import ImageManagerSingleton, ImageSize -from rare.utils.utils import ( - optimal_text_background, - text_color_for_background, -) +from rare.widgets.elide_label import ElideLabel +from .library_widget import LibraryWidget -class InstallingGameWidget(QWidget): +class InstallingGameWidget(QGroupBox): game: Game = None def __init__(self): super(InstallingGameWidget, self).__init__() + layout = QVBoxLayout() self.setObjectName("game_widget_icon") - self.setProperty("noBorder", 1) - self.setLayout(QVBoxLayout()) + self.setContentsMargins(0, 0, 0, 0) self.core = LegendaryCoreSingleton() + self.image_manager = ImageManagerSingleton() self.pixmap = QPixmap() - self.image_widget = PaintWidget() - self.setContentsMargins(0, 0, 0, 0) - self.image_widget.setFixedSize(ImageSize.Display.size) - self.layout().addWidget(self.image_widget) + self.image = LibraryWidget(parent=self) + self.image.setFixedSize(ImageSize.Display) + layout.addWidget(self.image) - self.title_label = QLabel(f"

Error

") - self.title_label.setAutoFillBackground(False) - self.title_label.setWordWrap(True) - self.title_label.setFixedWidth(175) + miniwidget = QWidget(self) + miniwidget.setFixedWidth(ImageSize.Display.size.width()) minilayout = QHBoxLayout() - self.setObjectName("game_widget") - minilayout.addWidget(self.title_label) - self.layout().addLayout(minilayout) + minilayout.setContentsMargins(0, 0, 0, 0) + minilayout.setSpacing(0) + miniwidget.setLayout(minilayout) + + self.title_label = ElideLabel(f"

Error

", parent=miniwidget) + self.title_label.setObjectName("game_widget") + minilayout.addWidget(self.title_label, stretch=2) + + minilayout.setAlignment(Qt.AlignTop) + layout.addWidget(miniwidget) + + self.setLayout(layout) def set_game(self, app_name): if not app_name: @@ -43,73 +48,11 @@ class InstallingGameWidget(QWidget): return self.game = self.core.get_game(app_name) self.title_label.setText(f"

{self.game.app_title}

") - - self.image_widget.set_game(self.game.app_name) + self.image.hideProgress(True) + self.image.showProgress( + self.image_manager.get_pixmap(app_name, color=True), + self.image_manager.get_pixmap(app_name, color=False), + ) def set_status(self, s: int): - self.image_widget.progress = s - self.image_widget.repaint() - - -class PaintWidget(QWidget): - color_image: QPixmap - bw_image: QPixmap - progress: int = 0 - - def __init__(self): - super(PaintWidget, self).__init__() - self.core = LegendaryCoreSingleton() - self.image_manager = ImageManagerSingleton() - - def set_game(self, app_name: str): - game = self.core.get_game(app_name, False) - self.color_image = self.image_manager.get_pixmap(game.app_name, color=False) - self.color_image = self.color_image.scaled( - ImageSize.Display.size, transformMode=Qt.SmoothTransformation - ) - self.setFixedSize(ImageSize.Display.size) - self.bw_image = self.image_manager.get_pixmap(app_name, color=False) - self.bw_image = self.bw_image.scaled( - ImageSize.Display.size, transformMode=Qt.SmoothTransformation - ) - self.progress = 0 - - pixel_list = [] - for x in range(self.color_image.width()): - for y in range(self.color_image.height()): - # convert pixmap to qimage, get pixel and remove alpha channel - pixel_list.append( - self.color_image.toImage().pixelColor(x, y).getRgb()[:-1] - ) - - self.rgb_tuple = optimal_text_background(pixel_list) - - def paintEvent(self, a0: QPaintEvent) -> None: - painter = QPainter() - painter.begin(self) - painter.setRenderHint(QPainter.Antialiasing) - painter.drawPixmap(self.rect(), self.bw_image) - - w = self.bw_image.width() * self.progress // 100 - - painter.drawPixmap( - 0, - 0, - w, - self.color_image.height(), - self.color_image.copy(QRect(0, 0, w, self.color_image.height())), - ) - - # Draw Circle - pen = QPen(QColor(*self.rgb_tuple), 3) - painter.setPen(pen) - painter.setBrush(QColor(*self.rgb_tuple)) - painter.drawEllipse( - int(self.width() / 2) - 20, int(self.height() / 2) - 20, 40, 40 - ) - - # draw text - painter.setPen(QColor(*text_color_for_background(self.rgb_tuple))) - painter.setFont(QFont(None, 16)) - painter.drawText(a0.rect(), Qt.AlignCenter, f"{self.progress}%") - painter.end() + self.image.updateProgress(s) diff --git a/rare/components/tabs/games/game_widgets/library_widget.py b/rare/components/tabs/games/game_widgets/library_widget.py new file mode 100644 index 00000000..f7cc7457 --- /dev/null +++ b/rare/components/tabs/games/game_widgets/library_widget.py @@ -0,0 +1,124 @@ +from typing import Optional, Tuple, List + +from PyQt5.QtCore import Qt, QRect, QEvent +from PyQt5.QtGui import QPainter, QPixmap, QResizeEvent, QFontMetrics, QImage, QBrush, QColor +from PyQt5.QtWidgets import QLabel + +from rare.widgets.image_widget import ImageWidget + + +class ProgressLabel(QLabel): + def __init__(self, parent): + super(ProgressLabel, self).__init__(parent=parent) + self.setObjectName(type(self).__name__) + self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + self.setFrameStyle(QLabel.StyledPanel) + + def setGeometry(self, a0: QRect) -> None: + fm = QFontMetrics(self.font()) + rect = fm.boundingRect(f" {self.text()} ") + rect.moveCenter(a0.center()) + super(ProgressLabel, self).setGeometry(rect) + + @staticmethod + def calculateColors(image: QImage) -> Tuple[QColor, QColor]: + color: List[int] = [0, 0, 0] + # take the two diagonals of the center square section + min_d = min(image.width(), image.height()) + origin_w = (image.width() - min_d) // 2 + origin_h = (image.height() - min_d) // 2 + for x, y in zip(range(origin_w, min_d), range(origin_h, min_d)): + pixel = image.pixelColor(x, y).getRgb() + color = list(map(lambda t: sum(t) // 2, zip(pixel[0:3], color))) + # take the V component of the HSV color + fg_color = QColor(0, 0, 0) if QColor(*color).value() < 127 else QColor(255, 255, 255) + bg_color = QColor(*map(lambda c: 255 - c, color)) + return bg_color, fg_color + + def setStyleSheetColors(self, bg: QColor, fg: QColor, brd: QColor): + sheet = ( + f"background-color: rgba({bg.red()}, {bg.green()}, {bg.blue()}, 65%);" + f"color: rgb({fg.red()}, {fg.green()}, {fg.blue()});" + f"border-width: 1px;" + f"border-radius: 5%;" + f"border-color: rgb({brd.red()}, {brd.green()}, {brd.blue()});" + f"font-weight: bold;" + f"font-size: 16pt;" + ) + self.setStyleSheet(sheet) + + +class LibraryWidget(ImageWidget): + _color_pixmap: Optional[QPixmap] = None + _gray_pixmap: Optional[QPixmap] = None + # lk: keep percentage to not over-generate the image + _progress: int = -1 + + def __init__( + self, + parent=None, + ) -> None: + super(LibraryWidget, self).__init__(parent) + self.progress_label = ProgressLabel(self) + self.progress_label.setVisible(False) + + def event(self, e: QEvent) -> bool: + if e.type() == QEvent.LayoutRequest: + self.progress_label.setGeometry(self.rect()) + return super(LibraryWidget, self).event(e) + + def resizeEvent(self, a0: QResizeEvent) -> None: + if self.progress_label.isVisible(): + self.progress_label.setGeometry(self.rect()) + super(LibraryWidget, self).resizeEvent(a0) + + def progressPixmap(self, color: QPixmap, gray: QPixmap, progress: int) -> QPixmap: + """ + Paints the color image over the gray images based on progress percentage + + @param color: + @param gray: + @param progress: + @return: + """ + + device = QPixmap(color.size()) + painter = QPainter(device) + painter.setRenderHint(QPainter.SmoothPixmapTransform, self._smooth_transform) + painter.setCompositionMode(QPainter.CompositionMode_Source) + # lk: Vertical loading + # prog_h = (device.height() * progress // 100) + # brush = QBrush(gray) + # painter.fillRect(device.rect().adjusted(0, 0, 0, -prog_h), brush) + # brush.setTexture(color) + # painter.fillRect(device.rect().adjusted(0, device.height() - prog_h, 0, 0), brush) + # lk: Horizontal loading + prog_w = device.width() * progress // 100 + brush = QBrush(gray) + painter.fillRect(device.rect().adjusted(prog_w, 0, 0, 0), brush) + brush.setTexture(color) + painter.fillRect(device.rect().adjusted(0, 0, prog_w - device.width(), 0), brush) + painter.end() + device.setDevicePixelRatio(color.devicePixelRatioF()) + return device + + def showProgress(self, color_pm: QPixmap, gray_pm: QPixmap) -> None: + self._color_pixmap = color_pm + self._gray_pixmap = gray_pm + bg_color, fg_color = self.progress_label.calculateColors(color_pm.toImage()) + self.progress_label.setStyleSheetColors(bg_color, fg_color, fg_color) + self.progress_label.setVisible(True) + self.progress_label.update() + self.updateProgress(0) + + def updateProgress(self, progress: int): + if progress > self._progress: + self._progress = progress + self.progress_label.setText(f"{progress:02}%") + self.setPixmap(self.progressPixmap(self._color_pixmap, self._gray_pixmap, progress)) + + def hideProgress(self, stopped: bool): + self._color_pixmap = None + self._gray_pixmap = None + self.progress_label.setVisible(stopped) + self._progress = -1 diff --git a/rare/ui/components/tabs/games/game_info/game_dlc_widget.py b/rare/ui/components/tabs/games/game_info/game_dlc_widget.py index 723a14f5..9c37b2d2 100644 --- a/rare/ui/components/tabs/games/game_info/game_dlc_widget.py +++ b/rare/ui/components/tabs/games/game_info/game_dlc_widget.py @@ -25,12 +25,6 @@ class Ui_GameDlcWidget(object): GameDlcWidget.setFrameShadow(QtWidgets.QFrame.Plain) self.dlc_layout = QtWidgets.QHBoxLayout(GameDlcWidget) self.dlc_layout.setObjectName("dlc_layout") - self.image = QtWidgets.QLabel(GameDlcWidget) - self.image.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.image.setFrameShadow(QtWidgets.QFrame.Plain) - self.image.setText("image") - self.image.setObjectName("image") - self.dlc_layout.addWidget(self.image) self.dlc_info = QtWidgets.QWidget(GameDlcWidget) self.dlc_info.setObjectName("dlc_info") self.dlc_info_layout = QtWidgets.QFormLayout(self.dlc_info) @@ -105,7 +99,7 @@ class Ui_GameDlcWidget(object): self.action_button.setText("Action") self.action_button.setObjectName("action_button") self.dlc_layout.addWidget(self.action_button, 0, QtCore.Qt.AlignTop) - self.dlc_layout.setStretch(2, 1) + self.dlc_layout.setStretch(1, 1) self.retranslateUi(GameDlcWidget) QtCore.QMetaObject.connectSlotsByName(GameDlcWidget) diff --git a/rare/ui/components/tabs/games/game_info/game_dlc_widget.ui b/rare/ui/components/tabs/games/game_info/game_dlc_widget.ui index 33ae4ef8..ff228bb4 100644 --- a/rare/ui/components/tabs/games/game_info/game_dlc_widget.ui +++ b/rare/ui/components/tabs/games/game_info/game_dlc_widget.ui @@ -25,20 +25,7 @@ QFrame::Plain - - - - - QFrame::StyledPanel - - - QFrame::Plain - - - image - - - + diff --git a/rare/ui/components/tabs/games/game_info/game_info.py b/rare/ui/components/tabs/games/game_info/game_info.py index 755b0a2f..25c80961 100644 --- a/rare/ui/components/tabs/games/game_info/game_info.py +++ b/rare/ui/components/tabs/games/game_info/game_info.py @@ -17,12 +17,6 @@ class Ui_GameInfo(object): GameInfo.resize(791, 583) self.layout_game_info = QtWidgets.QHBoxLayout(GameInfo) self.layout_game_info.setObjectName("layout_game_info") - self.image = QtWidgets.QLabel(GameInfo) - self.image.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.image.setFrameShadow(QtWidgets.QFrame.Sunken) - self.image.setText("") - self.image.setObjectName("image") - self.layout_game_info.addWidget(self.image, 0, QtCore.Qt.AlignTop) self.layout_game_info_form = QtWidgets.QGridLayout() self.layout_game_info_form.setContentsMargins(6, 6, 6, 6) self.layout_game_info_form.setSpacing(12) diff --git a/rare/ui/components/tabs/games/game_info/game_info.ui b/rare/ui/components/tabs/games/game_info/game_info.ui index a97bfa66..20a891ac 100644 --- a/rare/ui/components/tabs/games/game_info/game_info.ui +++ b/rare/ui/components/tabs/games/game_info/game_info.ui @@ -14,19 +14,6 @@ Game Info - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - - - - diff --git a/rare/utils/utils.py b/rare/utils/utils.py index 1387727d..6501591d 100644 --- a/rare/utils/utils.py +++ b/rare/utils/utils.py @@ -1,11 +1,10 @@ -import math import os import platform import shlex import subprocess import sys from logging import getLogger -from typing import List, Tuple +from typing import List import qtawesome import requests @@ -306,35 +305,6 @@ def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link= return False -def optimal_text_background(image: list) -> Tuple[int, int, int]: - """ - Finds an optimal background color for text on the image by calculating the - average color of the image and inverting it. - - The image list is supposed to be a one-dimensional list of arbitrary length - containing RGB tuples, ranging from 0 to 255. - """ - # cursed, I know - average = map(lambda value: value // len(image), map(sum, zip(*image))) - inverted = map(lambda value: 255 - value, average) - return tuple(inverted) - - -def text_color_for_background(background: Tuple[int, int, int]) -> Tuple[int, int, int]: - """ - Calculates whether a black or white text color would fit better for the - given background, and returns that color. This is done by calculating the - luminance and simple comparing of bounds - """ - # see https://alienryderflex.com/hsp.html - (red, green, blue) = background - luminance = math.sqrt(0.299 * red ** 2 + 0.587 * green ** 2 + 0.114 * blue ** 2) - if luminance < 127: - return 255, 255, 255 - else: - return 0, 0, 0 - - class WineResolverSignals(QObject): result_ready = pyqtSignal(str) diff --git a/rare/widgets/image_widget.py b/rare/widgets/image_widget.py new file mode 100644 index 00000000..c989bfd3 --- /dev/null +++ b/rare/widgets/image_widget.py @@ -0,0 +1,144 @@ +from enum import Enum +from typing import Tuple, Optional, Union + +from PyQt5.QtCore import Qt, QRectF +from PyQt5.QtGui import ( + QPaintEvent, + QPainter, + QPixmap, + QTransform, + QBrush, + QPalette, + QPainterPath, + QLinearGradient, + QColor, +) +from PyQt5.QtWidgets import QWidget + +from rare.shared.image_manager import ImageSize + +OverlayPath = Tuple[QPainterPath, Union[QColor, QLinearGradient]] + + +class ImageWidget(QWidget): + class Border(Enum): + Rounded = 0 + Squared = 1 + + _pixmap: Optional[QPixmap] = None + _opacity: float = 1.0 + _transform: QTransform + _smooth_transform: bool = False + _rounded_overlay: Optional[OverlayPath] = None + _squared_overlay: Optional[OverlayPath] = None + _image_size: Optional[ImageSize.Preset] = None + + def __init__(self, parent=None) -> None: + super(ImageWidget, self).__init__(parent=parent) + self.setObjectName(type(self).__name__) + self.setContentsMargins(0, 0, 0, 0) + self.paint_image = self.paint_image_empty + self.paint_overlay = self.paint_overlay_rounded + + def setOpacity(self, value: float) -> None: + self._opacity = value + self.update() + + def setPixmap(self, pixmap: QPixmap) -> None: + if not pixmap.isNull(): + self._pixmap = pixmap + self.paint_image = self.paint_image_cover + if not self._image_size: + self._transform = QTransform().scale( + 1 / pixmap.devicePixelRatioF(), + 1 / pixmap.devicePixelRatioF(), + ) + else: + self._transform = QTransform().scale( + 1 / pixmap.devicePixelRatioF() / self._image_size.divisor, + 1 / pixmap.devicePixelRatioF() / self._image_size.divisor, + ) + self.update() + + def setFixedSize(self, a0: ImageSize.Preset) -> None: + self._squared_overlay = None + self._rounded_overlay = None + self._image_size = a0 + self._smooth_transform = a0.smooth + super(ImageWidget, self).setFixedSize(a0.size) + + def setBorder(self, border: Border): + if border == ImageWidget.Border.Rounded: + self.paint_overlay = self.paint_overlay_rounded + else: + self.paint_overlay = self.paint_overlay_squared + self.update() + + def _generate_squared_overlay(self) -> OverlayPath: + if self._image_size is not None and self._squared_overlay is not None: + return self._squared_overlay + path = QPainterPath() + path.addRect(0, 0, self.width(), self.height()) + border = 2 + inner_path = QPainterPath() + inner_path.addRect( + QRectF( + border, + border, + self.width() - border * 2, + self.height() - border * 2, + ) + ) + gradient = QLinearGradient(0, 0, 0, self.height()) + gradient.setColorAt(0.0, Qt.black) + gradient.setColorAt(1.0, Qt.transparent) + self._squared_overlay = path.subtracted(inner_path), gradient + return self._squared_overlay + + def _generate_rounded_overlay(self) -> OverlayPath: + if self._image_size is not None and self._rounded_overlay is not None: + return self._rounded_overlay + # lk: the '-1' and '+1' are adjustments required for anti-aliasing + # lk: otherwise vertical lines would appear at the edges + path = QPainterPath() + path.addRect(-1, -1, self.width() + 2, self.height() + 2) + rounded_path = QPainterPath() + rounded_path.addRoundedRect( + QRectF(0, 0, self.width(), self.height()), + self.height() * 0.045, + self.height() * 0.045, + ) + self._rounded_overlay = path.subtracted(rounded_path), QColor(0, 0, 0, 0) + return self._rounded_overlay + + def paint_image_empty(self, painter: QPainter, a0: QPaintEvent) -> None: + # when pixmap object is not available yet, show a gray rectangle + painter.setOpacity(0.5 * self._opacity) + painter.fillRect(a0.rect(), Qt.darkGray) + + def paint_image_cover(self, painter: QPainter, a0: QPaintEvent) -> None: + painter.setOpacity(self._opacity) + brush = QBrush(self._pixmap) + # downscale the image during painting to fit the pixelratio + brush.setTransform(self._transform) + painter.fillRect(a0.rect(), brush) + + def paint_overlay_rounded(self, painter: QPainter, a0: QPaintEvent) -> None: + painter.setRenderHint(QPainter.Antialiasing, True) + painter.setOpacity(1.0) + painter.setCompositionMode(QPainter.CompositionMode_Source) + overlay, _ = self._generate_rounded_overlay() + painter.fillPath(overlay, self.palette().color(QPalette.Background)) + + def paint_overlay_squared(self, painter: QPainter, a0: QPaintEvent) -> None: + painter.setRenderHint(QPainter.Antialiasing, False) + painter.setOpacity(self._opacity) + painter.fillPath(*self._generate_squared_overlay()) + + def paintEvent(self, a0: QPaintEvent) -> None: + painter = QPainter(self) + # helps with better image quality + painter.setRenderHint(QPainter.SmoothPixmapTransform, self._smooth_transform) + self.paint_image(painter, a0) + self.paint_overlay(painter, a0) + painter.end()