1
0
Fork 0
mirror of synced 2024-06-17 01:54:46 +12:00

Add ImageWidget and LibraryWidget from #196

Add the Image and Library widgets from #196. In this iteration they replace the image `QLabel` in the existing widgets.

The `PaintWidget` in the `InstallingWidget` has been replaced by the future `LibraryWidget` that has progress indication.

The `ImageWidget` was also used to replace the image `QLabel` in `GameInfo` and `GameDlc` widgets.

Signed-off-by: loathingKernel <142770+loathingKernel@users.noreply.github.com>
This commit is contained in:
loathingKernel 2022-06-19 20:42:49 +03:00
parent 8888cb3aee
commit 6335293eef
14 changed files with 337 additions and 211 deletions

View file

@ -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)

View file

@ -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"))

View file

@ -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:

View file

@ -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"))

View file

@ -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":

View file

@ -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)

View file

@ -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"<h4>Error</h4>")
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"<h4>Error</h4>", 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"<h4>{self.game.app_title}</h4>")
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)

View file

@ -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

View file

@ -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)

View file

@ -25,20 +25,7 @@
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<layout class="QHBoxLayout" name="dlc_layout" stretch="0,0,1,0">
<item>
<widget class="QLabel" name="image">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="text">
<string notr="true">image</string>
</property>
</widget>
</item>
<layout class="QHBoxLayout" name="dlc_layout" stretch="0,1,0">
<item>
<widget class="QWidget" name="dlc_info" native="true">
<layout class="QFormLayout" name="dlc_info_layout">

View file

@ -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)

View file

@ -14,19 +14,6 @@
<string>Game Info</string>
</property>
<layout class="QHBoxLayout" name="layout_game_info">
<item alignment="Qt::AlignTop">
<widget class="QLabel" name="image">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="layout_game_info_form">
<property name="leftMargin">

View file

@ -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)

View file

@ -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()