From 594d5f64c99f62ea0063391c7a95b0dccdedcc1b Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Tue, 29 Oct 2024 16:55:43 +0800 Subject: [PATCH 01/20] refactor: icon / text / icon and text are supported in push button --- siui/components/button.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/siui/components/button.py b/siui/components/button.py index d8a79a7..9d28b76 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -118,7 +118,7 @@ def textRectAndIconRect(self) -> (QRectF, QRect): text_width, self.height() - self.style_data.border_height - 1) pixmap_rect = QRect((self.width() - icon_width - text_width - gap) // 2, - ((self.height() - self.style_data.border_height - 1) - icon_height) // 2, + ((self.height() - self.style_data.border_height) - icon_height) // 2, icon_width, icon_height) From bac4215f33fc6f689bf5f4319f81e181788e276d Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Wed, 30 Oct 2024 13:55:49 +0800 Subject: [PATCH 02/20] refactor: `SiProgressPushButton` --- siui/components/button.py | 351 +++++++++++++++++++++++++++----------- 1 file changed, 256 insertions(+), 95 deletions(-) diff --git a/siui/components/button.py b/siui/components/button.py index 9d28b76..623a7c0 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -2,10 +2,11 @@ # replace button once it's done. Now it's draft, code may be ugly and verbose temporarily. from __future__ import annotations +import random from dataclasses import dataclass -from PyQt5.QtCore import QEvent, QRect, QRectF, QSize, Qt -from PyQt5.QtGui import QColor, QFontMetrics, QIcon, QPainter, QPainterPath, QPaintEvent, QPixmap +from PyQt5.QtCore import QEvent, QRect, QRectF, QSize, Qt, QTimer +from PyQt5.QtGui import QColor, QFontMetrics, QIcon, QLinearGradient, QPainter, QPainterPath, QPaintEvent, QPixmap from PyQt5.QtSvg import QSvgRenderer from PyQt5.QtWidgets import QPushButton, QWidget @@ -14,34 +15,124 @@ @dataclass -class PushButtonStyleData: - idle_color = SiColor.toArray("#00FFFFFF") - hover_color = SiColor.toArray("#10FFFFFF") - click_color = SiColor.toArray("#40FFFFFF") - background_color = SiColor.toArray("#2d2932", "rgba") +class ButtonStyleData: + idle_color = SiColor.toArray("#00baadc7") + hover_color = SiColor.toArray("#1abaadc7") + click_color = SiColor.toArray("#50baadc7") button_color = SiColor.toArray("#4C4554", "rgba") - border_radius: int = 4 - border_inner_radius: int = 3 - border_height: int = 3 + border_radius: int = 7 icon_text_gap: int = 4 -class SiPushButtonRefactor(QPushButton): +class ABCButton(QPushButton): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) - self.style_data = PushButtonStyleData() - self._initStyle() - - self.animation = SiExpAnimation(self) - self.animation.setFactor(1 / 8) - self.animation.setBias(0.2) - self.animation.setTarget(self.style_data.idle_color) - self.animation.setCurrent(self.style_data.idle_color) - self.animation.ticked.connect(self.animate) + self.style_data = None + self.highlight_ani = SiExpAnimation(self) + self.highlight_ani.setFactor(1/8) + self.highlight_ani.setBias(0.2) + self.highlight_ani.setTarget(SiColor.toArray("#00FFFFFF")) + self.highlight_ani.setCurrent(SiColor.toArray("#00FFFFFF")) + self.highlight_ani.ticked.connect(self._onAnimationTicked) self.clicked.connect(self._onButtonClicked) + def setToolTip(self, tooltip: str) -> None: + super().setToolTip(tooltip) + self._updateToolTip() + + def setIconTextGap(self, gap: int) -> None: + self.style_data.icon_text_gap = gap + self.update() + + def setBorderRadius(self, r: int) -> None: + self.style_data.border_radius = r + self.update() + + def setButtonColor(self, code: str) -> None: + self.style_data.button_color = SiColor.toArray(code, "rgba") + self.update() + + def setSvgIcon(self, svg_data: bytes) -> None: + pixmap = QPixmap(64, 64) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + svg_renderer = QSvgRenderer(svg_data) + svg_renderer.render(painter) + painter.end() + self.setIcon(QIcon(pixmap)) + self.update() + + def sizeHint(self): + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + text_height = font_metrics.height() + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + preferred_width = text_width + icon_width + gap + 32 + preferred_height = max(32, text_height, icon_height) + return QSize(preferred_width, preferred_height) + + @property + def styleData(self) -> PushButtonStyleData: + return self.style_data + + def _showToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(self) + tool_tip_window.show_() + + def _hideToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(None) + tool_tip_window.hide_() + + def _updateToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: + tool_tip_window.setText(self.toolTip()) + + def _onAnimationTicked(self, _) -> None: + raise NotImplementedError() + + def _onButtonClicked(self): + raise NotImplementedError() + + def event(self, event): + if event.type() == QEvent.ToolTip: + return True # 忽略工具提示事件 + return super().event(event) + + def enterEvent(self, a0): + super().enterEvent(a0) + self._showToolTip() + self._updateToolTip() + + def leaveEvent(self, a0): + super().leaveEvent(a0) + self._hideToolTip() + + +@dataclass +class PushButtonStyleData(ButtonStyleData): + background_color = SiColor.toArray("#2d2932", "rgba") + border_inner_radius: int = 4 + border_height: int = 3 + + +class SiPushButtonRefactor(ABCButton): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.style_data = PushButtonStyleData() + self._initStyle() + def _initStyle(self): self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) self.setStyleSheet("color: #DFDFDF;") @@ -70,9 +161,17 @@ def withTextAndIcon(cls, text: str, icon: str, parent: QWidget | None = None) -> def bottomBorderHeight(self) -> int: return self.style_data.border_height - @property - def styleData(self) -> PushButtonStyleData: - return self.style_data + def setBackgroundColor(self, code: str) -> None: + self.style_data.background_color = SiColor.toArray(code, "rgba") + self.update() + + def setBorderInnerRadius(self, r: int) -> None: + self.style_data.border_inner_radius = r + self.update() + + def setBorderHeight(self, h: int) -> None: + self.style_data.border_height = h + self.update() def _drawBackgroundPath(self, rect: QRect) -> QPainterPath: radius = self.style_data.border_radius @@ -95,7 +194,7 @@ def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: painter.drawPath(self._drawButtonPath(rect)) def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(QColor(SiColor.toCode(self.animation.current_))) + painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) painter.drawPath(self._drawButtonPath(rect)) def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: @@ -106,6 +205,13 @@ def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: def _drawPixmapRect(self, painter: QPainter, rect: QRectF) -> None: painter.drawPixmap(rect, self.icon().pixmap(64, 64)) + def _onAnimationTicked(self, _) -> None: + self.update() + + def _onButtonClicked(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.start() + def textRectAndIconRect(self) -> (QRectF, QRect): font_metrics = QFontMetrics(self.font()) text_width = font_metrics.width(self.text()) @@ -124,98 +230,98 @@ def textRectAndIconRect(self) -> (QRectF, QRect): return text_rect, pixmap_rect - def _onButtonClicked(self) -> None: - self.animation.setCurrent(self.style_data.click_color) - self.animation.start() + def enterEvent(self, event) -> None: + super().enterEvent(event) + self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.start() + self._showToolTip() + self._updateToolTip() - def _showToolTip(self) -> None: - tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") - if tool_tip_window is not None and self.toolTip() != "": - tool_tip_window.setNowInsideOf(self) - tool_tip_window.show_() + def leaveEvent(self, event) -> None: + super().leaveEvent(event) + self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.start() + self._hideToolTip() - def _hideToolTip(self) -> None: - tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") - if tool_tip_window is not None and self.toolTip() != "": - tool_tip_window.setNowInsideOf(None) - tool_tip_window.hide_() + def paintEvent(self, event: QPaintEvent) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.setPen(Qt.PenStyle.NoPen) - def _updateToolTip(self) -> None: - tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") - if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: - tool_tip_window.setText(self.toolTip()) + rect = self.rect() + text_rect, icon_rect = self.textRectAndIconRect() + self._drawBackgroundRect(painter, rect) + self._drawButtonRect(painter, rect) + self._drawHighLightRect(painter, rect) + self._drawTextRect(painter, text_rect) + self._drawPixmapRect(painter, icon_rect) - def animate(self, _) -> None: - self.update() - def setToolTip(self, tooltip: str) -> None: - super().setToolTip(tooltip) - self._updateToolTip() +@dataclass +class FlatButtonStyleData(ButtonStyleData): + pass - def setButtonColor(self, code: str) -> None: - self.style_data.button_color = SiColor.toArray(code, "rgba") - self.update() - def setBackgroundColor(self, code: str) -> None: - self.style_data.background_color = SiColor.toArray(code, "rgba") - self.update() +class SiFlatButton(ABCButton): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) - def setBorderRadius(self, r: int) -> None: - self.style_data.border_radius = r - self.update() + self.style_data = FlatButtonStyleData() + self._initStyle() - def setBorderInnerRadius(self, r: int) -> None: - self.style_data.border_inner_radius = r - self.update() + def _initStyle(self): + self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) + self.setStyleSheet("color: #DFDFDF;") + self.setIconSize(QSize(20, 20)) - def setBorderHeight(self, h: int) -> None: - self.style_data.border_height = h - self.update() + def _drawButtonPath(self, rect: QRect) -> QPainterPath: + radius = self.style_data.border_radius + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height()), radius, radius) + return path - def setIconTextGap(self, gap: int) -> None: - self.style_data.icon_text_gap = gap - self.update() + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(*self.style_data.button_color)) + painter.drawPath(self._drawButtonPath(rect)) - def setSvgIcon(self, svg_data: bytes) -> None: - pixmap = QPixmap(64, 64) - pixmap.fill(Qt.transparent) - painter = QPainter(pixmap) - painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) - svg_renderer = QSvgRenderer(svg_data) - svg_renderer.render(painter) - painter.end() - self.setIcon(QIcon(pixmap)) - self.update() + def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) + painter.drawPath(self._drawButtonPath(rect)) - def sizeHint(self): + def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: + painter.setPen(self.palette().text().color()) + painter.setFont(self.font()) + painter.drawText(rect, Qt.AlignCenter, self.text()) + + def _drawPixmapRect(self, painter: QPainter, rect: QRectF) -> None: + painter.drawPixmap(rect, self.icon().pixmap(64, 64)) + + def textRectAndIconRect(self) -> (QRectF, QRect): font_metrics = QFontMetrics(self.font()) text_width = font_metrics.width(self.text()) - text_height = font_metrics.height() icon_width = self.iconSize().width() if not self.icon().isNull() else 0 icon_height = self.iconSize().height() if not self.icon().isNull() else 0 gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 - preferred_width = text_width + icon_width + gap + 24 - preferred_height = max(32, text_height, icon_height) - return QSize(preferred_width, preferred_height) + text_rect = QRectF((self.width() - icon_width - text_width - gap) / 2 + icon_width + gap, + 0, + text_width, + self.height()) + pixmap_rect = QRect((self.width() - icon_width - text_width - gap) // 2, + (self.height() - icon_height) // 2, + icon_width, + icon_height) - def event(self, event): - if event.type() == QEvent.ToolTip: - return True # 忽略工具提示事件 - return super().event(event) + return text_rect, pixmap_rect - def enterEvent(self, event) -> None: - super().enterEvent(event) - self.animation.setTarget(self.style_data.hover_color) - self.animation.start() - self._showToolTip() - self._updateToolTip() + def _onAnimationTicked(self, _) -> None: + self.update() - def leaveEvent(self, event) -> None: - super().leaveEvent(event) - self.animation.setTarget(self.style_data.idle_color) - self.animation.start() - self._hideToolTip() + def _onButtonClicked(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.start() def paintEvent(self, event: QPaintEvent) -> None: painter = QPainter(self) @@ -226,8 +332,63 @@ def paintEvent(self, event: QPaintEvent) -> None: rect = self.rect() text_rect, icon_rect = self.textRectAndIconRect() - self._drawBackgroundRect(painter, rect) self._drawButtonRect(painter, rect) self._drawHighLightRect(painter, rect) self._drawTextRect(painter, text_rect) self._drawPixmapRect(painter, icon_rect) + + def enterEvent(self, event) -> None: + super().enterEvent(event) + self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.start() + self._showToolTip() + self._updateToolTip() + + def leaveEvent(self, event) -> None: + super().leaveEvent(event) + self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.start() + self._hideToolTip() + + +@dataclass +class ProgressPushButtonStyleData(PushButtonStyleData): + progress_color = SiColor.toArray("#806799", "rgba") + + +class SiProgressPushButton(SiPushButtonRefactor): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.style_data = ProgressPushButtonStyleData() + self.progress_ = 0 + + self.progress_ani = SiExpAnimation(self) + self.progress_ani.setFactor(1/6) + self.progress_ani.setBias(0.005) + self.progress_ani.setTarget(0) + self.progress_ani.setCurrent(0) + self.progress_ani.ticked.connect(lambda _: self.update()) + + @property + def progress(self): + return self.progress_ + + def setProgress(self, p: float, ani: bool = True) -> None: + p = max(0.0, min(p, 1.0)) + self.progress_ = p + if ani is True: + self.progress_ani.setTarget(p) + self.progress_ani.start() + else: + self.progress_ani.setTarget(p) + self.progress_ani.setCurrent(p) + self.progress_ani.stop() + self.update() + + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) + gradient.setColorAt(self.progress_ani.current_ - 0.001, QColor(*self.style_data.progress_color)) + gradient.setColorAt(self.progress_ani.current_, QColor(*self.style_data.button_color)) + painter.setBrush(gradient) + painter.drawPath(self._drawButtonPath(rect)) From 86154a14a1dc1479b28d607f4debdb04c5464902 Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Wed, 30 Oct 2024 17:58:52 +0800 Subject: [PATCH 03/20] feat: new `init` method for easier initialization for SiExpAnimation --- siui/core/animation.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/siui/core/animation.py b/siui/core/animation.py index fdb5e82..21cd3f9 100644 --- a/siui/core/animation.py +++ b/siui/core/animation.py @@ -1,3 +1,5 @@ +from typing import Any + import numpy from PyQt5.QtCore import QObject, QTimer, pyqtSignal @@ -154,6 +156,13 @@ def __init__(self, parent=None): self.factor = 1/2 self.bias = 1 + def init(self, factor: float, bias: float, current: Any, target: Any, fps: int = 60): + self.setFactor(factor) + self.setBias(bias) + self.setCurrent(current) + self.setTarget(target) + self.setFPS(fps) + def setFactor(self, factor: float): """ Set the factor of the animation. From 9775e9c5ff1ec8421a7e464e75fe21c2ac4e92da Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Wed, 30 Oct 2024 17:59:33 +0800 Subject: [PATCH 04/20] feat: color animation in SiProgressPushButton --- siui/components/button.py | 47 +++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/siui/components/button.py b/siui/components/button.py index 623a7c0..21c6a01 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -354,6 +354,7 @@ def leaveEvent(self, event) -> None: @dataclass class ProgressPushButtonStyleData(PushButtonStyleData): progress_color = SiColor.toArray("#806799", "rgba") + complete_color = SiColor.toArray("#519868", "rgba") class SiProgressPushButton(SiPushButtonRefactor): @@ -364,31 +365,53 @@ def __init__(self, parent: QWidget | None = None) -> None: self.progress_ = 0 self.progress_ani = SiExpAnimation(self) - self.progress_ani.setFactor(1/6) - self.progress_ani.setBias(0.005) - self.progress_ani.setTarget(0) - self.progress_ani.setCurrent(0) + self.progress_ani.init(1/6, 0.005, 0, 0) self.progress_ani.ticked.connect(lambda _: self.update()) + self.progress_color_ani = SiExpAnimation(self) + self.progress_color_ani.init(1/8, 0.01, self.style_data.progress_color, self.style_data.progress_color) + self.progress_color_ani.ticked.connect(lambda _: self.update()) + # + # self.test_timer = QTimer(self) + # self.test_timer.setInterval(500) + # self.test_timer.timeout.connect(lambda: self.setProgress(random.random() * 3)) + # self.test_timer.start() + @property def progress(self): return self.progress_ def setProgress(self, p: float, ani: bool = True) -> None: - p = max(0.0, min(p, 1.0)) - self.progress_ = p + self.progress_ = max(0.0, min(p, 1.0)) + self._updateProgress(ani) + self._updateCompleteState() + self.update() + + def _updateProgress(self, ani: bool): if ani is True: - self.progress_ani.setTarget(p) + self.progress_ani.setTarget(self.progress_) self.progress_ani.start() else: - self.progress_ani.setTarget(p) - self.progress_ani.setCurrent(p) + self.progress_ani.setTarget(self.progress_) + self.progress_ani.setCurrent(self.progress_) self.progress_ani.stop() - self.update() + + def _updateCompleteState(self): + if self.progress_ == 1.0: + self.progress_color_ani.setTarget(self.style_data.complete_color) + self.progress_color_ani.start() + else: + self.progress_color_ani.setTarget(self.style_data.progress_color) + self.progress_color_ani.start() + + def setProgressColor(self, code: str) -> None: + self.style_data.progress_color = SiColor.toArray(code, "rgba") + self.update() def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + p = min(self.progress_ani.current_, 1) # prevent progress exceeding caused by using animation. gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) - gradient.setColorAt(self.progress_ani.current_ - 0.001, QColor(*self.style_data.progress_color)) - gradient.setColorAt(self.progress_ani.current_, QColor(*self.style_data.button_color)) + gradient.setColorAt(p - 0.0001, QColor(*self.progress_color_ani.current_)) + gradient.setColorAt(p, QColor(*self.style_data.button_color)) painter.setBrush(gradient) painter.drawPath(self._drawButtonPath(rect)) From 5367495b2828ff068455f1cba3a61349184f5693 Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Wed, 30 Oct 2024 19:47:37 +0800 Subject: [PATCH 05/20] refactor: `SiLongPressButtonRefactor` --- siui/components/button.py | 132 ++++++++++++++++++++++++++++++++------ 1 file changed, 113 insertions(+), 19 deletions(-) diff --git a/siui/components/button.py b/siui/components/button.py index 21c6a01..7655e6f 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -5,7 +5,7 @@ import random from dataclasses import dataclass -from PyQt5.QtCore import QEvent, QRect, QRectF, QSize, Qt, QTimer +from PyQt5.QtCore import QEvent, QRect, QRectF, QSize, Qt, QTimer, pyqtSignal from PyQt5.QtGui import QColor, QFontMetrics, QIcon, QLinearGradient, QPainter, QPainterPath, QPaintEvent, QPixmap from PyQt5.QtSvg import QSvgRenderer from PyQt5.QtWidgets import QPushButton, QWidget @@ -24,6 +24,32 @@ class ButtonStyleData: icon_text_gap: int = 4 +@dataclass +class FlatButtonStyleData(ButtonStyleData): + pass + + +@dataclass +class PushButtonStyleData(ButtonStyleData): + background_color = SiColor.toArray("#2d2932", "rgba") + border_inner_radius: int = 4 + border_height: int = 3 + + +@dataclass +class ProgressPushButtonStyleData(PushButtonStyleData): + progress_color = SiColor.toArray("#806799", "rgba") + complete_color = SiColor.toArray("#519868", "rgba") + + +@dataclass +class LongPressButtonStyleData(PushButtonStyleData): + progress_color = SiColor.toArray("#DA3462", "rgba") + button_color = SiColor.toArray("#932a48", "rgba") + background_color = SiColor.toArray("#642d41", "rgba") + click_color = SiColor.toArray("#40FFFFFF") + + class ABCButton(QPushButton): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -119,13 +145,6 @@ def leaveEvent(self, a0): self._hideToolTip() -@dataclass -class PushButtonStyleData(ButtonStyleData): - background_color = SiColor.toArray("#2d2932", "rgba") - border_inner_radius: int = 4 - border_height: int = 3 - - class SiPushButtonRefactor(ABCButton): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -259,11 +278,6 @@ def paintEvent(self, event: QPaintEvent) -> None: self._drawPixmapRect(painter, icon_rect) -@dataclass -class FlatButtonStyleData(ButtonStyleData): - pass - - class SiFlatButton(ABCButton): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -351,12 +365,6 @@ def leaveEvent(self, event) -> None: self._hideToolTip() -@dataclass -class ProgressPushButtonStyleData(PushButtonStyleData): - progress_color = SiColor.toArray("#806799", "rgba") - complete_color = SiColor.toArray("#519868", "rgba") - - class SiProgressPushButton(SiPushButtonRefactor): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -415,3 +423,89 @@ def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: gradient.setColorAt(p, QColor(*self.style_data.button_color)) painter.setBrush(gradient) painter.drawPath(self._drawButtonPath(rect)) + + +class SiLongPressButtonRefactor(SiPushButtonRefactor): + longPressed = pyqtSignal() + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.style_data = LongPressButtonStyleData() + self.progress_ = 0 + + self.progress_ani = SiExpAnimation(self) + self.progress_ani.init(0, 0.1, 0, 0) + self.progress_ani.ticked.connect(lambda _: self.update()) + self.progress_ani.ticked.connect(print) + + self.go_backwards_timer = QTimer(self) + self.go_backwards_timer.setSingleShot(True) + self.go_backwards_timer.setInterval(500) + self.go_backwards_timer.timeout.connect(self._goBackwards) + + self.mouse_pressed_timer = QTimer(self) + self.mouse_pressed_timer.setInterval(1000//60) + self.mouse_pressed_timer.timeout.connect(self._onMousePressed) + + @property + def progress(self): + return self.progress_ + + def setProgress(self, p: float, ani: bool = True) -> None: + self.progress_ = max(0.0, min(p, 1.0)) + self._updateProgress(ani) + self.update() + + def _stepLength(self) -> float: + return (1 - self.progress_) / 16 + 0.001 + + def _onMousePressed(self): + self.setProgress(self.progress_ + self._stepLength(), ani=False) + + def _goBackwards(self, delay: int = 0): + self.progress_ = 0 + self.progress_ani.setTarget(0) + self.progress_ani.start(delay) + + def _updateProgress(self, ani: bool): + if ani is True: + self.progress_ani.setTarget(self.progress_) + self.progress_ani.start() + else: + self.progress_ani.setTarget(self.progress_) + self.progress_ani.setCurrent(self.progress_) + self.progress_ani.stop() + + if self.progress_ == 1.0: + self.mouse_pressed_timer.stop() + self.go_backwards_timer.stop() + self.longPressed.emit() + self._onLongPressed() + self._goBackwards(200) + + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + p = min(self.progress_ani.current_, 1) # prevent progress exceeding caused by using animation. + gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) + gradient.setColorAt(p - 0.0001, QColor(*self.style_data.progress_color)) + gradient.setColorAt(p, QColor(*self.style_data.button_color)) + painter.setBrush(gradient) + painter.drawPath(self._drawButtonPath(rect)) + + def _onButtonClicked(self) -> None: + pass + + def _onLongPressed(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.start() + + def mousePressEvent(self, e): + super().mousePressEvent(e) + if self.progress_ani.isActive() is False and self.mouse_pressed_timer.isActive() is False: + self.mouse_pressed_timer.start() + self.go_backwards_timer.stop() + + def mouseReleaseEvent(self, e): + super().mouseReleaseEvent(e) + self.mouse_pressed_timer.stop() + self.go_backwards_timer.start() From f880dd0bc89bf2cc12f2c007ba1859d23699ee6e Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Wed, 30 Oct 2024 20:03:46 +0800 Subject: [PATCH 06/20] refactor: `SiLongPressButtonRefactor` --- siui/components/button.py | 47 +++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/siui/components/button.py b/siui/components/button.py index 7655e6f..d47b001 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -379,14 +379,9 @@ def __init__(self, parent: QWidget | None = None) -> None: self.progress_color_ani = SiExpAnimation(self) self.progress_color_ani.init(1/8, 0.01, self.style_data.progress_color, self.style_data.progress_color) self.progress_color_ani.ticked.connect(lambda _: self.update()) - # - # self.test_timer = QTimer(self) - # self.test_timer.setInterval(500) - # self.test_timer.timeout.connect(lambda: self.setProgress(random.random() * 3)) - # self.test_timer.start() @property - def progress(self): + def progress(self) -> float: return self.progress_ def setProgress(self, p: float, ani: bool = True) -> None: @@ -395,7 +390,7 @@ def setProgress(self, p: float, ani: bool = True) -> None: self._updateCompleteState() self.update() - def _updateProgress(self, ani: bool): + def _updateProgress(self, ani: bool) -> None: if ani is True: self.progress_ani.setTarget(self.progress_) self.progress_ani.start() @@ -404,7 +399,7 @@ def _updateProgress(self, ani: bool): self.progress_ani.setCurrent(self.progress_) self.progress_ani.stop() - def _updateCompleteState(self): + def _updateCompleteState(self) -> None: if self.progress_ == 1.0: self.progress_color_ani.setTarget(self.style_data.complete_color) self.progress_color_ani.start() @@ -435,9 +430,9 @@ def __init__(self, parent: QWidget | None = None) -> None: self.progress_ = 0 self.progress_ani = SiExpAnimation(self) - self.progress_ani.init(0, 0.1, 0, 0) + self.progress_ani.init(-1/16, 0.12, 0, 0) self.progress_ani.ticked.connect(lambda _: self.update()) - self.progress_ani.ticked.connect(print) + # self.progress_ani.ticked.connect(print) self.go_backwards_timer = QTimer(self) self.go_backwards_timer.setSingleShot(True) @@ -449,7 +444,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self.mouse_pressed_timer.timeout.connect(self._onMousePressed) @property - def progress(self): + def progress(self) -> float: return self.progress_ def setProgress(self, p: float, ani: bool = True) -> None: @@ -460,15 +455,13 @@ def setProgress(self, p: float, ani: bool = True) -> None: def _stepLength(self) -> float: return (1 - self.progress_) / 16 + 0.001 - def _onMousePressed(self): + def _onMousePressed(self) -> None: self.setProgress(self.progress_ + self._stepLength(), ani=False) - def _goBackwards(self, delay: int = 0): - self.progress_ = 0 - self.progress_ani.setTarget(0) - self.progress_ani.start(delay) + def _onButtonClicked(self) -> None: + pass # disable flashes on mouse click - def _updateProgress(self, ani: bool): + def _updateProgress(self, ani: bool) -> None: if ani is True: self.progress_ani.setTarget(self.progress_) self.progress_ani.start() @@ -484,6 +477,15 @@ def _updateProgress(self, ani: bool): self._onLongPressed() self._goBackwards(200) + def _onLongPressed(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.start() + + def _goBackwards(self, delay: int = 0) -> None: + self.progress_ = 0 + self.progress_ani.setTarget(0) + self.progress_ani.start(delay) + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: p = min(self.progress_ani.current_, 1) # prevent progress exceeding caused by using animation. gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) @@ -492,20 +494,13 @@ def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: painter.setBrush(gradient) painter.drawPath(self._drawButtonPath(rect)) - def _onButtonClicked(self) -> None: - pass - - def _onLongPressed(self) -> None: - self.highlight_ani.setCurrent(self.style_data.click_color) - self.highlight_ani.start() - - def mousePressEvent(self, e): + def mousePressEvent(self, e) -> None: super().mousePressEvent(e) if self.progress_ani.isActive() is False and self.mouse_pressed_timer.isActive() is False: self.mouse_pressed_timer.start() self.go_backwards_timer.stop() - def mouseReleaseEvent(self, e): + def mouseReleaseEvent(self, e) -> None: super().mouseReleaseEvent(e) self.mouse_pressed_timer.stop() self.go_backwards_timer.start() From 63dcb4a69a0d631b42dfc1a5f212aa5351af6fcc Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Thu, 31 Oct 2024 19:45:24 +0800 Subject: [PATCH 07/20] refactor: `SiToggleButtonRefactor` --- siui/components/button.py | 238 +++++++++++++++++++++++--------------- 1 file changed, 147 insertions(+), 91 deletions(-) diff --git a/siui/components/button.py b/siui/components/button.py index d47b001..2c07b9a 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -50,6 +50,12 @@ class LongPressButtonStyleData(PushButtonStyleData): click_color = SiColor.toArray("#40FFFFFF") +@dataclass +class ToggleButtonStyleData(ButtonStyleData): + toggled_text_color = SiColor.toArray("#DFDFDF", "rgba") + toggled_button_color = SiColor.toArray("#519868", "rgba") + + class ABCButton(QPushButton): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -64,6 +70,10 @@ def __init__(self, parent: QWidget | None = None) -> None: self.highlight_ani.ticked.connect(self._onAnimationTicked) self.clicked.connect(self._onButtonClicked) + def flash(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.try_to_start() + def setToolTip(self, tooltip: str) -> None: super().setToolTip(tooltip) self._updateToolTip() @@ -91,7 +101,7 @@ def setSvgIcon(self, svg_data: bytes) -> None: self.setIcon(QIcon(pixmap)) self.update() - def sizeHint(self): + def sizeHint(self) -> QSize: font_metrics = QFontMetrics(self.font()) text_width = font_metrics.width(self.text()) text_height = font_metrics.height() @@ -127,7 +137,7 @@ def _updateToolTip(self) -> None: def _onAnimationTicked(self, _) -> None: raise NotImplementedError() - def _onButtonClicked(self): + def _onButtonClicked(self) -> None: raise NotImplementedError() def event(self, event): @@ -228,8 +238,7 @@ def _onAnimationTicked(self, _) -> None: self.update() def _onButtonClicked(self) -> None: - self.highlight_ani.setCurrent(self.style_data.click_color) - self.highlight_ani.start() + self.flash() def textRectAndIconRect(self) -> (QRectF, QRect): font_metrics = QFontMetrics(self.font()) @@ -278,93 +287,6 @@ def paintEvent(self, event: QPaintEvent) -> None: self._drawPixmapRect(painter, icon_rect) -class SiFlatButton(ABCButton): - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) - - self.style_data = FlatButtonStyleData() - self._initStyle() - - def _initStyle(self): - self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) - self.setStyleSheet("color: #DFDFDF;") - self.setIconSize(QSize(20, 20)) - - def _drawButtonPath(self, rect: QRect) -> QPainterPath: - radius = self.style_data.border_radius - path = QPainterPath() - path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height()), radius, radius) - return path - - def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(QColor(*self.style_data.button_color)) - painter.drawPath(self._drawButtonPath(rect)) - - def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) - painter.drawPath(self._drawButtonPath(rect)) - - def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: - painter.setPen(self.palette().text().color()) - painter.setFont(self.font()) - painter.drawText(rect, Qt.AlignCenter, self.text()) - - def _drawPixmapRect(self, painter: QPainter, rect: QRectF) -> None: - painter.drawPixmap(rect, self.icon().pixmap(64, 64)) - - def textRectAndIconRect(self) -> (QRectF, QRect): - font_metrics = QFontMetrics(self.font()) - text_width = font_metrics.width(self.text()) - icon_width = self.iconSize().width() if not self.icon().isNull() else 0 - icon_height = self.iconSize().height() if not self.icon().isNull() else 0 - gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 - - text_rect = QRectF((self.width() - icon_width - text_width - gap) / 2 + icon_width + gap, - 0, - text_width, - self.height()) - pixmap_rect = QRect((self.width() - icon_width - text_width - gap) // 2, - (self.height() - icon_height) // 2, - icon_width, - icon_height) - - return text_rect, pixmap_rect - - def _onAnimationTicked(self, _) -> None: - self.update() - - def _onButtonClicked(self) -> None: - self.highlight_ani.setCurrent(self.style_data.click_color) - self.highlight_ani.start() - - def paintEvent(self, event: QPaintEvent) -> None: - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) - painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - painter.setPen(Qt.PenStyle.NoPen) - - rect = self.rect() - text_rect, icon_rect = self.textRectAndIconRect() - self._drawButtonRect(painter, rect) - self._drawHighLightRect(painter, rect) - self._drawTextRect(painter, text_rect) - self._drawPixmapRect(painter, icon_rect) - - def enterEvent(self, event) -> None: - super().enterEvent(event) - self.highlight_ani.setTarget(self.style_data.hover_color) - self.highlight_ani.start() - self._showToolTip() - self._updateToolTip() - - def leaveEvent(self, event) -> None: - super().leaveEvent(event) - self.highlight_ani.setTarget(self.style_data.idle_color) - self.highlight_ani.start() - self._hideToolTip() - - class SiProgressPushButton(SiPushButtonRefactor): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -504,3 +426,137 @@ def mouseReleaseEvent(self, e) -> None: super().mouseReleaseEvent(e) self.mouse_pressed_timer.stop() self.go_backwards_timer.start() + + +class SiFlatButton(ABCButton): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.style_data = FlatButtonStyleData() + self._initStyle() + + def _initStyle(self): + self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) + self.setStyleSheet("color: #DFDFDF;") + self.setIconSize(QSize(20, 20)) + + def _drawButtonPath(self, rect: QRect) -> QPainterPath: + radius = self.style_data.border_radius + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height()), radius, radius) + return path + + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(*self.style_data.button_color)) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: + painter.setPen(self.palette().text().color()) + painter.setFont(self.font()) + painter.drawText(rect, Qt.AlignCenter, self.text()) + + def _drawPixmapRect(self, painter: QPainter, rect: QRectF) -> None: + painter.drawPixmap(rect, self.icon().pixmap(64, 64)) + + def textRectAndIconRect(self) -> (QRectF, QRect): + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + text_rect = QRectF((self.width() - icon_width - text_width - gap) / 2 + icon_width + gap, + 0, + text_width, + self.height()) + pixmap_rect = QRect((self.width() - icon_width - text_width - gap) // 2, + (self.height() - icon_height) // 2, + icon_width, + icon_height) + + return text_rect, pixmap_rect + + def _onAnimationTicked(self, _) -> None: + self.update() + + def _onButtonClicked(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.start() + + def paintEvent(self, event: QPaintEvent) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.setPen(Qt.PenStyle.NoPen) + + rect = self.rect() + text_rect, icon_rect = self.textRectAndIconRect() + self._drawButtonRect(painter, rect) + self._drawHighLightRect(painter, rect) + self._drawTextRect(painter, text_rect) + self._drawPixmapRect(painter, icon_rect) + + def enterEvent(self, event) -> None: + super().enterEvent(event) + self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.start() + self._showToolTip() + self._updateToolTip() + + def leaveEvent(self, event) -> None: + super().leaveEvent(event) + self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.start() + self._hideToolTip() + + +class SiToggleButtonRefactor(SiFlatButton): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.setCheckable(True) + self.style_data = ToggleButtonStyleData() + + self.toggle_btn_color_ani = SiExpAnimation(self) + self.toggle_btn_color_ani.init(1/8, 0.01, self.style_data.button_color, self.style_data.button_color) + self.toggle_btn_color_ani.ticked.connect(lambda _: self.update()) + + self.toggle_text_color_ani = SiExpAnimation(self) + self.toggle_text_color_ani.init(1/8, 0.01, (223, 223, 223, 255), (223, 223, 223, 255)) + self.toggle_text_color_ani.ticked.connect(lambda _: self.update()) + + self.toggled.connect(self._onButtonToggled) + + def setToggledButtonColor(self, code: str) -> None: + self.style_data.toggled_button_color = SiColor.toArray(code, "rgba") + self.update() + + def setToggledTextColor(self, code: str) -> None: + self.style_data.toggled_text_color = SiColor.toArray(code, "rgba") + self.update() + + def _onButtonToggled(self, state: bool) -> None: + if state: + self.toggle_btn_color_ani.setTarget(self.style_data.toggled_button_color) + self.toggle_text_color_ani.setTarget(self.style_data.toggled_text_color) + self.toggle_btn_color_ani.try_to_start() + self.toggle_text_color_ani.try_to_start() + else: + self.toggle_btn_color_ani.setTarget(self.style_data.button_color) + self.toggle_text_color_ani.setTarget(self.palette().text().color().getRgb()) + self.toggle_btn_color_ani.try_to_start() + self.toggle_text_color_ani.try_to_start() + + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(*self.toggle_btn_color_ani.current_)) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: + painter.setPen(QColor(*self.toggle_text_color_ani.current_)) + painter.setFont(self.font()) + painter.drawText(rect, Qt.AlignCenter, self.text()) From e77248a131db21cbc5031a56c047c20392df125a Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Thu, 31 Oct 2024 20:17:27 +0800 Subject: [PATCH 08/20] flatten new button.py for further discussion on proj structure --- siui/components/button_flattened.py | 1246 +++++++++++++++++++++++++++ 1 file changed, 1246 insertions(+) create mode 100644 siui/components/button_flattened.py diff --git a/siui/components/button_flattened.py b/siui/components/button_flattened.py new file mode 100644 index 0000000..da6b149 --- /dev/null +++ b/siui/components/button_flattened.py @@ -0,0 +1,1246 @@ +# NOTE This is the refactor of button component. It's working in progress. It will +# replace button once it's done. Now it's draft, code may be ugly and verbose temporarily. +from __future__ import annotations + +import random +from dataclasses import dataclass + +from PyQt5.QtCore import QEvent, QRect, QRectF, QSize, Qt, QTimer, pyqtSignal +from PyQt5.QtGui import QColor, QFontMetrics, QIcon, QLinearGradient, QPainter, QPainterPath, QPaintEvent, QPixmap +from PyQt5.QtSvg import QSvgRenderer +from PyQt5.QtWidgets import QPushButton, QWidget + +from siui.core import GlobalFont, SiColor, SiExpAnimation, SiGlobal +from siui.gui import SiFont + + +@dataclass +class ButtonStyleData: + idle_color = SiColor.toArray("#00baadc7") + hover_color = SiColor.toArray("#1abaadc7") + click_color = SiColor.toArray("#50baadc7") + button_color = SiColor.toArray("#4C4554", "rgba") + border_radius: int = 7 + icon_text_gap: int = 4 + + +@dataclass +class FlatButtonStyleData(ButtonStyleData): + pass + + +@dataclass +class PushButtonStyleData(ButtonStyleData): + background_color = SiColor.toArray("#2d2932", "rgba") + border_inner_radius: int = 4 + border_height: int = 3 + + +@dataclass +class ProgressPushButtonStyleData(PushButtonStyleData): + progress_color = SiColor.toArray("#806799", "rgba") + complete_color = SiColor.toArray("#519868", "rgba") + + +@dataclass +class LongPressButtonStyleData(PushButtonStyleData): + progress_color = SiColor.toArray("#DA3462", "rgba") + button_color = SiColor.toArray("#932a48", "rgba") + background_color = SiColor.toArray("#642d41", "rgba") + click_color = SiColor.toArray("#40FFFFFF") + + +@dataclass +class ToggleButtonStyleData(ButtonStyleData): + toggled_text_color = SiColor.toArray("#DFDFDF", "rgba") + toggled_button_color = SiColor.toArray("#519868", "rgba") + + +class ABCButton(QPushButton): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.style_data = None + + self.highlight_ani = SiExpAnimation(self) + self.highlight_ani.init(1/8, 0.2, SiColor.toArray("#00FFFFFF"), SiColor.toArray("#00FFFFFF")) + self.highlight_ani.ticked.connect(self._onAnimationTicked) + + self.clicked.connect(self._onButtonClicked) + + def flash(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.try_to_start() + + def setToolTip(self, tooltip: str) -> None: + super().setToolTip(tooltip) + self._updateToolTip() + + def setIconTextGap(self, gap: int) -> None: + self.style_data.icon_text_gap = gap + self.update() + + def setBorderRadius(self, r: int) -> None: + self.style_data.border_radius = r + self.update() + + def setButtonColor(self, code: str) -> None: + self.style_data.button_color = SiColor.toArray(code, "rgba") + self.update() + + def setSvgIcon(self, svg_data: bytes) -> None: + pixmap = QPixmap(64, 64) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + svg_renderer = QSvgRenderer(svg_data) + svg_renderer.render(painter) + painter.end() + self.setIcon(QIcon(pixmap)) + self.update() + + def sizeHint(self) -> QSize: + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + text_height = font_metrics.height() + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + preferred_width = text_width + icon_width + gap + 32 + preferred_height = max(32, text_height, icon_height) + return QSize(preferred_width, preferred_height) + + @property + def styleData(self) -> PushButtonStyleData: + return self.style_data + + def _showToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(self) + tool_tip_window.show_() + + def _hideToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(None) + tool_tip_window.hide_() + + def _updateToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: + tool_tip_window.setText(self.toolTip()) + + def _onAnimationTicked(self, _) -> None: + raise NotImplementedError() + + def _onButtonClicked(self) -> None: + raise NotImplementedError() + + def event(self, event): + if event.type() == QEvent.ToolTip: + return True # 忽略工具提示事件 + return super().event(event) + + def enterEvent(self, a0): + super().enterEvent(a0) + self._showToolTip() + self._updateToolTip() + + def leaveEvent(self, a0): + super().leaveEvent(a0) + self._hideToolTip() + + +class SiPushButtonRefactor(QPushButton): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.style_data = PushButtonStyleData() + + self.highlight_ani = SiExpAnimation(self) + self.highlight_ani.init(1/8, 0.2, SiColor.toArray("#00FFFFFF"), SiColor.toArray("#00FFFFFF")) + self.highlight_ani.ticked.connect(self._onAnimationTicked) + + self._initStyle() + self.clicked.connect(self._onButtonClicked) + + def flash(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.try_to_start() + + def setToolTip(self, tooltip: str) -> None: + super().setToolTip(tooltip) + self._updateToolTip() + + def setIconTextGap(self, gap: int) -> None: + self.style_data.icon_text_gap = gap + self.update() + + def setBorderRadius(self, r: int) -> None: + self.style_data.border_radius = r + self.update() + + def setButtonColor(self, code: str) -> None: + self.style_data.button_color = SiColor.toArray(code, "rgba") + self.update() + + def setSvgIcon(self, svg_data: bytes) -> None: + pixmap = QPixmap(64, 64) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + svg_renderer = QSvgRenderer(svg_data) + svg_renderer.render(painter) + painter.end() + self.setIcon(QIcon(pixmap)) + self.update() + + def sizeHint(self) -> QSize: + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + text_height = font_metrics.height() + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + preferred_width = text_width + icon_width + gap + 32 + preferred_height = max(32, text_height, icon_height) + return QSize(preferred_width, preferred_height) + + @property + def styleData(self) -> PushButtonStyleData: + return self.style_data + + def _showToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(self) + tool_tip_window.show_() + + def _hideToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(None) + tool_tip_window.hide_() + + def _updateToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: + tool_tip_window.setText(self.toolTip()) + + def event(self, event): + if event.type() == QEvent.ToolTip: + return True # 忽略工具提示事件 + return super().event(event) + + def _initStyle(self): + self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) + self.setStyleSheet("color: #DFDFDF;") + self.setIconSize(QSize(20, 20)) + + @classmethod + def withText(cls, text: str, parent: QWidget | None = None) -> "SiPushButton": + obj = cls(parent) + obj.setText(text) + return obj + + @classmethod + def withIcon(cls, icon: QIcon, parent: QWidget | None = None) -> "SiPushButton": + obj = cls(parent) + obj.setIcon(icon) + return obj + + @classmethod + def withTextAndIcon(cls, text: str, icon: str, parent: QWidget | None = None) -> "SiPushButton": + obj = cls(parent) + obj.setText(text) + obj.setIcon(QIcon(icon)) + return obj + + @property + def bottomBorderHeight(self) -> int: + return self.style_data.border_height + + def setBackgroundColor(self, code: str) -> None: + self.style_data.background_color = SiColor.toArray(code, "rgba") + self.update() + + def setBorderInnerRadius(self, r: int) -> None: + self.style_data.border_inner_radius = r + self.update() + + def setBorderHeight(self, h: int) -> None: + self.style_data.border_height = h + self.update() + + def _drawBackgroundPath(self, rect: QRect) -> QPainterPath: + radius = self.style_data.border_radius + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height()), radius, radius) + return path + + def _drawBackgroundRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(*self.style_data.background_color)) + painter.drawPath(self._drawBackgroundPath(rect)) + + def _drawButtonPath(self, rect: QRect) -> QPainterPath: + radius = self.style_data.border_inner_radius + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height() - self.bottomBorderHeight), radius, radius) + return path + + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(*self.style_data.button_color)) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: + painter.setPen(self.palette().text().color()) + painter.setFont(self.font()) + painter.drawText(rect, Qt.AlignCenter, self.text()) + + def _drawPixmapRect(self, painter: QPainter, rect: QRectF) -> None: + painter.drawPixmap(rect, self.icon().pixmap(64, 64)) + + def _onAnimationTicked(self, _) -> None: + self.update() + + def _onButtonClicked(self) -> None: + self.flash() + + def textRectAndIconRect(self) -> (QRectF, QRect): + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + text_rect = QRectF((self.width() - icon_width - text_width - gap) / 2 + icon_width + gap, + 0, + text_width, + self.height() - self.style_data.border_height - 1) + pixmap_rect = QRect((self.width() - icon_width - text_width - gap) // 2, + ((self.height() - self.style_data.border_height) - icon_height) // 2, + icon_width, + icon_height) + + return text_rect, pixmap_rect + + def enterEvent(self, event) -> None: + super().enterEvent(event) + self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.start() + self._showToolTip() + self._updateToolTip() + + def leaveEvent(self, event) -> None: + super().leaveEvent(event) + self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.start() + self._hideToolTip() + + def paintEvent(self, event: QPaintEvent) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.setPen(Qt.PenStyle.NoPen) + + rect = self.rect() + text_rect, icon_rect = self.textRectAndIconRect() + self._drawBackgroundRect(painter, rect) + self._drawButtonRect(painter, rect) + self._drawHighLightRect(painter, rect) + self._drawTextRect(painter, text_rect) + self._drawPixmapRect(painter, icon_rect) + + +class SiProgressPushButton(QPushButton): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.style_data = ProgressPushButtonStyleData() + self.progress_ = 0 + + self.progress_ani = SiExpAnimation(self) + self.progress_ani.init(1/6, 0.005, 0, 0) + self.progress_ani.ticked.connect(lambda _: self.update()) + + self.progress_color_ani = SiExpAnimation(self) + self.progress_color_ani.init(1/8, 0.01, self.style_data.progress_color, self.style_data.progress_color) + self.progress_color_ani.ticked.connect(lambda _: self.update()) + + self.highlight_ani = SiExpAnimation(self) + self.highlight_ani.init(1/8, 0.2, SiColor.toArray("#00FFFFFF"), SiColor.toArray("#00FFFFFF")) + self.highlight_ani.ticked.connect(self._onAnimationTicked) + + self.clicked.connect(self._onButtonClicked) + + self._initStyle() + + def flash(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.try_to_start() + + def setToolTip(self, tooltip: str) -> None: + super().setToolTip(tooltip) + self._updateToolTip() + + def setIconTextGap(self, gap: int) -> None: + self.style_data.icon_text_gap = gap + self.update() + + def setBorderRadius(self, r: int) -> None: + self.style_data.border_radius = r + self.update() + + def setButtonColor(self, code: str) -> None: + self.style_data.button_color = SiColor.toArray(code, "rgba") + self.update() + + def setSvgIcon(self, svg_data: bytes) -> None: + pixmap = QPixmap(64, 64) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + svg_renderer = QSvgRenderer(svg_data) + svg_renderer.render(painter) + painter.end() + self.setIcon(QIcon(pixmap)) + self.update() + + def sizeHint(self) -> QSize: + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + text_height = font_metrics.height() + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + preferred_width = text_width + icon_width + gap + 32 + preferred_height = max(32, text_height, icon_height) + return QSize(preferred_width, preferred_height) + + @property + def styleData(self) -> PushButtonStyleData: + return self.style_data + + def _showToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(self) + tool_tip_window.show_() + + def _hideToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(None) + tool_tip_window.hide_() + + def _updateToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: + tool_tip_window.setText(self.toolTip()) + + def event(self, event): + if event.type() == QEvent.ToolTip: + return True # 忽略工具提示事件 + return super().event(event) + + def _initStyle(self): + self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) + self.setStyleSheet("color: #DFDFDF;") + self.setIconSize(QSize(20, 20)) + + @classmethod + def withText(cls, text: str, parent: QWidget | None = None) -> "SiPushButton": + obj = cls(parent) + obj.setText(text) + return obj + + @classmethod + def withIcon(cls, icon: QIcon, parent: QWidget | None = None) -> "SiPushButton": + obj = cls(parent) + obj.setIcon(icon) + return obj + + @classmethod + def withTextAndIcon(cls, text: str, icon: str, parent: QWidget | None = None) -> "SiPushButton": + obj = cls(parent) + obj.setText(text) + obj.setIcon(QIcon(icon)) + return obj + + @property + def bottomBorderHeight(self) -> int: + return self.style_data.border_height + + def setBackgroundColor(self, code: str) -> None: + self.style_data.background_color = SiColor.toArray(code, "rgba") + self.update() + + def setBorderInnerRadius(self, r: int) -> None: + self.style_data.border_inner_radius = r + self.update() + + def setBorderHeight(self, h: int) -> None: + self.style_data.border_height = h + self.update() + + def _drawBackgroundPath(self, rect: QRect) -> QPainterPath: + radius = self.style_data.border_radius + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height()), radius, radius) + return path + + def _drawBackgroundRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(*self.style_data.background_color)) + painter.drawPath(self._drawBackgroundPath(rect)) + + def _drawButtonPath(self, rect: QRect) -> QPainterPath: + radius = self.style_data.border_inner_radius + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height() - self.bottomBorderHeight), radius, radius) + return path + + def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: + painter.setPen(self.palette().text().color()) + painter.setFont(self.font()) + painter.drawText(rect, Qt.AlignCenter, self.text()) + + def _drawPixmapRect(self, painter: QPainter, rect: QRectF) -> None: + painter.drawPixmap(rect, self.icon().pixmap(64, 64)) + + def _onAnimationTicked(self, _) -> None: + self.update() + + def _onButtonClicked(self) -> None: + self.flash() + + def textRectAndIconRect(self) -> (QRectF, QRect): + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + text_rect = QRectF((self.width() - icon_width - text_width - gap) / 2 + icon_width + gap, + 0, + text_width, + self.height() - self.style_data.border_height - 1) + pixmap_rect = QRect((self.width() - icon_width - text_width - gap) // 2, + ((self.height() - self.style_data.border_height) - icon_height) // 2, + icon_width, + icon_height) + + return text_rect, pixmap_rect + + def enterEvent(self, event) -> None: + super().enterEvent(event) + self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.start() + self._showToolTip() + self._updateToolTip() + + def leaveEvent(self, event) -> None: + super().leaveEvent(event) + self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.start() + self._hideToolTip() + + def paintEvent(self, event: QPaintEvent) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.setPen(Qt.PenStyle.NoPen) + + rect = self.rect() + text_rect, icon_rect = self.textRectAndIconRect() + self._drawBackgroundRect(painter, rect) + self._drawButtonRect(painter, rect) + self._drawHighLightRect(painter, rect) + self._drawTextRect(painter, text_rect) + self._drawPixmapRect(painter, icon_rect) + + @property + def progress(self) -> float: + return self.progress_ + + def setProgress(self, p: float, ani: bool = True) -> None: + self.progress_ = max(0.0, min(p, 1.0)) + self._updateProgress(ani) + self._updateCompleteState() + self.update() + + def _updateProgress(self, ani: bool) -> None: + if ani is True: + self.progress_ani.setTarget(self.progress_) + self.progress_ani.start() + else: + self.progress_ani.setTarget(self.progress_) + self.progress_ani.setCurrent(self.progress_) + self.progress_ani.stop() + + def _updateCompleteState(self) -> None: + if self.progress_ == 1.0: + self.progress_color_ani.setTarget(self.style_data.complete_color) + self.progress_color_ani.start() + else: + self.progress_color_ani.setTarget(self.style_data.progress_color) + self.progress_color_ani.start() + + def setProgressColor(self, code: str) -> None: + self.style_data.progress_color = SiColor.toArray(code, "rgba") + self.update() + + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + p = min(self.progress_ani.current_, 1) # prevent progress exceeding caused by using animation. + gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) + gradient.setColorAt(p - 0.0001, QColor(*self.progress_color_ani.current_)) + gradient.setColorAt(p, QColor(*self.style_data.button_color)) + painter.setBrush(gradient) + painter.drawPath(self._drawButtonPath(rect)) + + +class SiLongPressButtonRefactor(QPushButton): + longPressed = pyqtSignal() + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.style_data = LongPressButtonStyleData() + self.progress_ = 0 + + self.progress_ani = SiExpAnimation(self) + self.progress_ani.init(-1/16, 0.12, 0, 0) + self.progress_ani.ticked.connect(lambda _: self.update()) + + self.go_backwards_timer = QTimer(self) + self.go_backwards_timer.setSingleShot(True) + self.go_backwards_timer.setInterval(500) + self.go_backwards_timer.timeout.connect(self._goBackwards) + + self.mouse_pressed_timer = QTimer(self) + self.mouse_pressed_timer.setInterval(1000//60) + self.mouse_pressed_timer.timeout.connect(self._onMousePressed) + + self.highlight_ani = SiExpAnimation(self) + self.highlight_ani.init(1/8, 0.2, SiColor.toArray("#00FFFFFF"), SiColor.toArray("#00FFFFFF")) + self.highlight_ani.ticked.connect(self._onAnimationTicked) + + self._initStyle() + self.clicked.connect(self._onButtonClicked) + + def flash(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.try_to_start() + + def setToolTip(self, tooltip: str) -> None: + super().setToolTip(tooltip) + self._updateToolTip() + + def setIconTextGap(self, gap: int) -> None: + self.style_data.icon_text_gap = gap + self.update() + + def setBorderRadius(self, r: int) -> None: + self.style_data.border_radius = r + self.update() + + def setButtonColor(self, code: str) -> None: + self.style_data.button_color = SiColor.toArray(code, "rgba") + self.update() + + def setSvgIcon(self, svg_data: bytes) -> None: + pixmap = QPixmap(64, 64) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + svg_renderer = QSvgRenderer(svg_data) + svg_renderer.render(painter) + painter.end() + self.setIcon(QIcon(pixmap)) + self.update() + + def sizeHint(self) -> QSize: + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + text_height = font_metrics.height() + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + preferred_width = text_width + icon_width + gap + 32 + preferred_height = max(32, text_height, icon_height) + return QSize(preferred_width, preferred_height) + + @property + def styleData(self) -> PushButtonStyleData: + return self.style_data + + def _showToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(self) + tool_tip_window.show_() + + def _hideToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(None) + tool_tip_window.hide_() + + def _updateToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: + tool_tip_window.setText(self.toolTip()) + + def event(self, event): + if event.type() == QEvent.ToolTip: + return True # 忽略工具提示事件 + return super().event(event) + + def _initStyle(self): + self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) + self.setStyleSheet("color: #DFDFDF;") + self.setIconSize(QSize(20, 20)) + + @classmethod + def withText(cls, text: str, parent: QWidget | None = None) -> "SiPushButton": + obj = cls(parent) + obj.setText(text) + return obj + + @classmethod + def withIcon(cls, icon: QIcon, parent: QWidget | None = None) -> "SiPushButton": + obj = cls(parent) + obj.setIcon(icon) + return obj + + @classmethod + def withTextAndIcon(cls, text: str, icon: str, parent: QWidget | None = None) -> "SiPushButton": + obj = cls(parent) + obj.setText(text) + obj.setIcon(QIcon(icon)) + return obj + + @property + def bottomBorderHeight(self) -> int: + return self.style_data.border_height + + def setBackgroundColor(self, code: str) -> None: + self.style_data.background_color = SiColor.toArray(code, "rgba") + self.update() + + def setBorderInnerRadius(self, r: int) -> None: + self.style_data.border_inner_radius = r + self.update() + + def setBorderHeight(self, h: int) -> None: + self.style_data.border_height = h + self.update() + + def _drawBackgroundPath(self, rect: QRect) -> QPainterPath: + radius = self.style_data.border_radius + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height()), radius, radius) + return path + + def _drawBackgroundRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(*self.style_data.background_color)) + painter.drawPath(self._drawBackgroundPath(rect)) + + def _drawButtonPath(self, rect: QRect) -> QPainterPath: + radius = self.style_data.border_inner_radius + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height() - self.bottomBorderHeight), radius, radius) + return path + + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + p = min(self.progress_ani.current_, 1) # prevent progress exceeding caused by using animation. + gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) + gradient.setColorAt(p - 0.0001, QColor(*self.style_data.progress_color)) + gradient.setColorAt(p, QColor(*self.style_data.button_color)) + painter.setBrush(gradient) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: + painter.setPen(self.palette().text().color()) + painter.setFont(self.font()) + painter.drawText(rect, Qt.AlignCenter, self.text()) + + def _drawPixmapRect(self, painter: QPainter, rect: QRectF) -> None: + painter.drawPixmap(rect, self.icon().pixmap(64, 64)) + + def _onAnimationTicked(self, _) -> None: + self.update() + + def _onButtonClicked(self) -> None: + pass # disable flashes on mouse click + + def textRectAndIconRect(self) -> (QRectF, QRect): + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + text_rect = QRectF((self.width() - icon_width - text_width - gap) / 2 + icon_width + gap, + 0, + text_width, + self.height() - self.style_data.border_height - 1) + pixmap_rect = QRect((self.width() - icon_width - text_width - gap) // 2, + ((self.height() - self.style_data.border_height) - icon_height) // 2, + icon_width, + icon_height) + + return text_rect, pixmap_rect + + def enterEvent(self, event) -> None: + super().enterEvent(event) + self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.start() + self._showToolTip() + self._updateToolTip() + + def leaveEvent(self, event) -> None: + super().leaveEvent(event) + self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.start() + self._hideToolTip() + + def paintEvent(self, event: QPaintEvent) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.setPen(Qt.PenStyle.NoPen) + + rect = self.rect() + text_rect, icon_rect = self.textRectAndIconRect() + self._drawBackgroundRect(painter, rect) + self._drawButtonRect(painter, rect) + self._drawHighLightRect(painter, rect) + self._drawTextRect(painter, text_rect) + self._drawPixmapRect(painter, icon_rect) + + @property + def progress(self) -> float: + return self.progress_ + + def setProgress(self, p: float, ani: bool = True) -> None: + self.progress_ = max(0.0, min(p, 1.0)) + self._updateProgress(ani) + self.update() + + def _stepLength(self) -> float: + return (1 - self.progress_) / 16 + 0.001 + + def _onMousePressed(self) -> None: + self.setProgress(self.progress_ + self._stepLength(), ani=False) + + def _updateProgress(self, ani: bool) -> None: + if ani is True: + self.progress_ani.setTarget(self.progress_) + self.progress_ani.start() + else: + self.progress_ani.setTarget(self.progress_) + self.progress_ani.setCurrent(self.progress_) + self.progress_ani.stop() + + if self.progress_ == 1.0: + self.mouse_pressed_timer.stop() + self.go_backwards_timer.stop() + self.longPressed.emit() + self._onLongPressed() + self._goBackwards(200) + + def _onLongPressed(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.start() + + def _goBackwards(self, delay: int = 0) -> None: + self.progress_ = 0 + self.progress_ani.setTarget(0) + self.progress_ani.start(delay) + + def mousePressEvent(self, e) -> None: + super().mousePressEvent(e) + if self.progress_ani.isActive() is False and self.mouse_pressed_timer.isActive() is False: + self.mouse_pressed_timer.start() + self.go_backwards_timer.stop() + + def mouseReleaseEvent(self, e) -> None: + super().mouseReleaseEvent(e) + self.mouse_pressed_timer.stop() + self.go_backwards_timer.start() + + +class SiFlatButton(QPushButton): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.style_data = FlatButtonStyleData() + + self.highlight_ani = SiExpAnimation(self) + self.highlight_ani.init(1/8, 0.2, SiColor.toArray("#00FFFFFF"), SiColor.toArray("#00FFFFFF")) + self.highlight_ani.ticked.connect(self._onAnimationTicked) + + self.clicked.connect(self._onButtonClicked) + + self._initStyle() + + def flash(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.try_to_start() + + def setToolTip(self, tooltip: str) -> None: + super().setToolTip(tooltip) + self._updateToolTip() + + def setIconTextGap(self, gap: int) -> None: + self.style_data.icon_text_gap = gap + self.update() + + def setBorderRadius(self, r: int) -> None: + self.style_data.border_radius = r + self.update() + + def setButtonColor(self, code: str) -> None: + self.style_data.button_color = SiColor.toArray(code, "rgba") + self.update() + + def setSvgIcon(self, svg_data: bytes) -> None: + pixmap = QPixmap(64, 64) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + svg_renderer = QSvgRenderer(svg_data) + svg_renderer.render(painter) + painter.end() + self.setIcon(QIcon(pixmap)) + self.update() + + def sizeHint(self) -> QSize: + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + text_height = font_metrics.height() + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + preferred_width = text_width + icon_width + gap + 32 + preferred_height = max(32, text_height, icon_height) + return QSize(preferred_width, preferred_height) + + @property + def styleData(self) -> PushButtonStyleData: + return self.style_data + + def _showToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(self) + tool_tip_window.show_() + + def _hideToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(None) + tool_tip_window.hide_() + + def _updateToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: + tool_tip_window.setText(self.toolTip()) + + def _onAnimationTicked(self, _) -> None: + self.update() + + def _onButtonClicked(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.start() + + def event(self, event): + if event.type() == QEvent.ToolTip: + return True # 忽略工具提示事件 + return super().event(event) + + def _initStyle(self): + self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) + self.setStyleSheet("color: #DFDFDF;") + self.setIconSize(QSize(20, 20)) + + def _drawButtonPath(self, rect: QRect) -> QPainterPath: + radius = self.style_data.border_radius + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height()), radius, radius) + return path + + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(*self.style_data.button_color)) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: + painter.setPen(self.palette().text().color()) + painter.setFont(self.font()) + painter.drawText(rect, Qt.AlignCenter, self.text()) + + def _drawPixmapRect(self, painter: QPainter, rect: QRectF) -> None: + painter.drawPixmap(rect, self.icon().pixmap(64, 64)) + + def textRectAndIconRect(self) -> (QRectF, QRect): + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + text_rect = QRectF((self.width() - icon_width - text_width - gap) / 2 + icon_width + gap, + 0, + text_width, + self.height()) + pixmap_rect = QRect((self.width() - icon_width - text_width - gap) // 2, + (self.height() - icon_height) // 2, + icon_width, + icon_height) + + return text_rect, pixmap_rect + + def paintEvent(self, event: QPaintEvent) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.setPen(Qt.PenStyle.NoPen) + + rect = self.rect() + text_rect, icon_rect = self.textRectAndIconRect() + self._drawButtonRect(painter, rect) + self._drawHighLightRect(painter, rect) + self._drawTextRect(painter, text_rect) + self._drawPixmapRect(painter, icon_rect) + + def enterEvent(self, event) -> None: + super().enterEvent(event) + self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.start() + self._showToolTip() + self._updateToolTip() + + def leaveEvent(self, event) -> None: + super().leaveEvent(event) + self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.start() + self._hideToolTip() + + +class SiToggleButtonRefactor(QPushButton): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.setCheckable(True) + self.style_data = ToggleButtonStyleData() + + self.toggle_btn_color_ani = SiExpAnimation(self) + self.toggle_btn_color_ani.init(1/8, 0.01, self.style_data.button_color, self.style_data.button_color) + self.toggle_btn_color_ani.ticked.connect(lambda _: self.update()) + + self.toggle_text_color_ani = SiExpAnimation(self) + self.toggle_text_color_ani.init(1/8, 0.01, (223, 223, 223, 255), (223, 223, 223, 255)) + self.toggle_text_color_ani.ticked.connect(lambda _: self.update()) + + self.highlight_ani = SiExpAnimation(self) + self.highlight_ani.init(1/8, 0.2, SiColor.toArray("#00FFFFFF"), SiColor.toArray("#00FFFFFF")) + self.highlight_ani.ticked.connect(self._onAnimationTicked) + + self.clicked.connect(self._onButtonClicked) + self.toggled.connect(self._onButtonToggled) + + self._initStyle() + + def flash(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.try_to_start() + + def setToolTip(self, tooltip: str) -> None: + super().setToolTip(tooltip) + self._updateToolTip() + + def setIconTextGap(self, gap: int) -> None: + self.style_data.icon_text_gap = gap + self.update() + + def setBorderRadius(self, r: int) -> None: + self.style_data.border_radius = r + self.update() + + def setButtonColor(self, code: str) -> None: + self.style_data.button_color = SiColor.toArray(code, "rgba") + self.update() + + def setSvgIcon(self, svg_data: bytes) -> None: + pixmap = QPixmap(64, 64) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + svg_renderer = QSvgRenderer(svg_data) + svg_renderer.render(painter) + painter.end() + self.setIcon(QIcon(pixmap)) + self.update() + + def sizeHint(self) -> QSize: + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + text_height = font_metrics.height() + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + preferred_width = text_width + icon_width + gap + 32 + preferred_height = max(32, text_height, icon_height) + return QSize(preferred_width, preferred_height) + + @property + def styleData(self) -> ToggleButtonStyleData: + return self.style_data + + def _showToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(self) + tool_tip_window.show_() + + def _hideToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(None) + tool_tip_window.hide_() + + def _updateToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: + tool_tip_window.setText(self.toolTip()) + + def _onAnimationTicked(self, _) -> None: + self.update() + + def _onButtonClicked(self) -> None: + self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.start() + + def event(self, event): + if event.type() == QEvent.ToolTip: + return True # 忽略工具提示事件 + return super().event(event) + + def _initStyle(self): + self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) + self.setStyleSheet("color: #DFDFDF;") + self.setIconSize(QSize(20, 20)) + + def _drawButtonPath(self, rect: QRect) -> QPainterPath: + radius = self.style_data.border_radius + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height()), radius, radius) + return path + + def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawPixmapRect(self, painter: QPainter, rect: QRectF) -> None: + painter.drawPixmap(rect, self.icon().pixmap(64, 64)) + + def textRectAndIconRect(self) -> (QRectF, QRect): + font_metrics = QFontMetrics(self.font()) + text_width = font_metrics.width(self.text()) + icon_width = self.iconSize().width() if not self.icon().isNull() else 0 + icon_height = self.iconSize().height() if not self.icon().isNull() else 0 + gap = self.style_data.icon_text_gap if text_width > 0 and icon_width > 0 else 0 + + text_rect = QRectF((self.width() - icon_width - text_width - gap) / 2 + icon_width + gap, + 0, + text_width, + self.height()) + pixmap_rect = QRect((self.width() - icon_width - text_width - gap) // 2, + (self.height() - icon_height) // 2, + icon_width, + icon_height) + + return text_rect, pixmap_rect + + def paintEvent(self, event: QPaintEvent) -> None: + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.setPen(Qt.PenStyle.NoPen) + + rect = self.rect() + text_rect, icon_rect = self.textRectAndIconRect() + self._drawButtonRect(painter, rect) + self._drawHighLightRect(painter, rect) + self._drawTextRect(painter, text_rect) + self._drawPixmapRect(painter, icon_rect) + + def enterEvent(self, event) -> None: + super().enterEvent(event) + self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.start() + self._showToolTip() + self._updateToolTip() + + def leaveEvent(self, event) -> None: + super().leaveEvent(event) + self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.start() + self._hideToolTip() + + def setToggledButtonColor(self, code: str) -> None: + self.style_data.toggled_button_color = SiColor.toArray(code, "rgba") + self.update() + + def setToggledTextColor(self, code: str) -> None: + self.style_data.toggled_text_color = SiColor.toArray(code, "rgba") + self.update() + + def _onButtonToggled(self, state: bool) -> None: + if state: + self.toggle_btn_color_ani.setTarget(self.style_data.toggled_button_color) + self.toggle_text_color_ani.setTarget(self.style_data.toggled_text_color) + self.toggle_btn_color_ani.try_to_start() + self.toggle_text_color_ani.try_to_start() + else: + self.toggle_btn_color_ani.setTarget(self.style_data.button_color) + self.toggle_text_color_ani.setTarget(self.palette().text().color().getRgb()) + self.toggle_btn_color_ani.try_to_start() + self.toggle_text_color_ani.try_to_start() + + def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: + painter.setBrush(QColor(*self.toggle_btn_color_ani.current_)) + painter.drawPath(self._drawButtonPath(rect)) + + def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: + painter.setPen(QColor(*self.toggle_text_color_ani.current_)) + painter.setFont(self.font()) + painter.drawText(rect, Qt.AlignCenter, self.text()) From cc78ba85b3eb820dfaf485b6c9d7f17648fbe23f Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Sat, 2 Nov 2024 17:13:52 +0800 Subject: [PATCH 09/20] refactor: new SiExpAnimation Inherits QAbstractAnimation --- siui/core/animation.py | 127 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/siui/core/animation.py b/siui/core/animation.py index 21cd3f9..4cac557 100644 --- a/siui/core/animation.py +++ b/siui/core/animation.py @@ -1,7 +1,8 @@ from typing import Any import numpy -from PyQt5.QtCore import QObject, QTimer, pyqtSignal +from PyQt5.QtCore import QAbstractAnimation, QObject, QPoint, QPointF, QRect, QRectF, QSize, QSizeF, QTimer, pyqtSignal +from PyQt5.QtGui import QColor global_fps = 60 @@ -377,3 +378,127 @@ def fromToken(self, aim_token: str) -> ABCSiAnimation: if token == aim_token: return ani raise ValueError(f"未在代号组中找到传入的代号:{aim_token}") + + +class TypeConversionFuncs: + functions = { + QPoint.__name__: [ + lambda x: numpy.array((x.x(), x.y())), + lambda x: QPoint(int(x[0]), int(x[1])) + ], + QPointF.__name__: [ + lambda x: numpy.array((x.x(), x.y())), + lambda x: QPointF(float(x[0]), float(x[1])), + ], + QSize.__name__: [ + lambda x: numpy.array((x.width(), x.height())), + lambda x: QSize(int(x[0]), int(x[1])), + ], + QSizeF.__name__: [ + lambda x: numpy.array((x.width(), x.height())), + lambda x: QSizeF(float(x[0]), float(x[1])), + ], + QRect.__name__: [ + lambda x: numpy.array((x.x(), x.y(), x.width(), x.height())), + lambda x: QRect(int(x[0]), int(x[1]), int(x[2]), int(x[3])) + ], + QRectF.__name__: [ + lambda x: numpy.array((x.x(), x.y(), x.width(), x.height())), + lambda x: QRect(float(x[0]), float(x[1]), float(x[2]), float(x[3])) + ], + QColor.__name__: [ + lambda x: numpy.array(x.getRgb()), + lambda x: QColor(int(x[0]), int(x[1]), int(x[2]), int(x[3])) + ] + } + + +class SiExpAnimationRefactor(QAbstractAnimation): + valueChanged = pyqtSignal(object) + + def __init__(self, target: QObject, property_name=None, parent=None): + super().__init__(parent) + self._target = target + self._property_name = None + self._property_type = None + self._in_func = None + self._out_func = None + self._end_value = None + self._current_value = None + self.factor = 1/4 + self.bias = 0.5 + + if property_name is not None: + self.setPropertyName(property_name) + + def init(self, factor: float, bias: float) -> None: + self.factor = factor + self.bias = bias + + def target(self) -> QObject: + return self._target + + def propertyName(self) -> str: + return self._property_name + + def endValue(self) -> Any: + return self._end_value + + def currentValue(self) -> Any: + return self._target.property(self._property_name) + + def distance(self) -> numpy.array: + return self._end_value - self._current_value + + def duration(self) -> int: + return -1 + + def setPropertyName(self, name: str) -> None: + self._property_name = name + self._property_type = type(self._target.property(name)) + self._loadConversionFuncs() + self._end_value = self._in_func(self._target.property(name)) + self._current_value = self._in_func(self._target.property(name)) + + def setEndValue(self, value: Any): + if isinstance(value, self._property_type): + self._end_value = self._in_func(value) + else: + self._end_value = numpy.array(value) + + def setCurrentValue(self, value: Any): + if isinstance(value, self._property_type): + self._current_value = self._in_func(value) + else: + self._current_value = numpy.array(value) + + def updateCurrentTime(self, _): + # print(self.distance()) + if (self.distance() == 0).all(): + self.stop() + return + + distance = self._end_value - self._current_value + flag = numpy.array(abs(distance) <= self.bias, dtype="int8") + step = abs(distance) * self.factor + self.bias # 基本指数动画运算 + step = step * (numpy.array(distance > 0, dtype="int8") * 2 - 1) # 确定动画方向 + step = step * (1 - flag) + distance * flag # 差距小于偏置的项,返回差距 + + self._current_value = self._current_value + step + self._target.setProperty(self._property_name, self._out_func(self._current_value)) + + def _loadConversionFuncs(self) -> None: + if self._property_type.__name__ in TypeConversionFuncs.functions.keys(): + self._in_func = TypeConversionFuncs.functions.get(self._property_type.__name__)[0] + self._out_func = TypeConversionFuncs.functions.get(self._property_type.__name__)[1] + else: + self._in_func = lambda x: numpy.array(x) + self._out_func = lambda x: self._property_type(int(x)) + + + + + # + # def setProperty(self, name, value): + # pass + # property. \ No newline at end of file From 1ea040d0d69806229ed232f6f49f79886e12dc5e Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Sun, 3 Nov 2024 09:38:44 +0800 Subject: [PATCH 10/20] refactor: new SiExpAnimation Inherits QAbstractAnimation --- siui/core/animation.py | 44 ++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/siui/core/animation.py b/siui/core/animation.py index 4cac557..f0f1bd8 100644 --- a/siui/core/animation.py +++ b/siui/core/animation.py @@ -383,31 +383,31 @@ def fromToken(self, aim_token: str) -> ABCSiAnimation: class TypeConversionFuncs: functions = { QPoint.__name__: [ - lambda x: numpy.array((x.x(), x.y())), + lambda x: numpy.array((x.x(), x.y()), dtype="float32"), lambda x: QPoint(int(x[0]), int(x[1])) ], QPointF.__name__: [ - lambda x: numpy.array((x.x(), x.y())), + lambda x: numpy.array((x.x(), x.y()), dtype="float32"), lambda x: QPointF(float(x[0]), float(x[1])), ], QSize.__name__: [ - lambda x: numpy.array((x.width(), x.height())), + lambda x: numpy.array((x.width(), x.height()), dtype="float32"), lambda x: QSize(int(x[0]), int(x[1])), ], QSizeF.__name__: [ - lambda x: numpy.array((x.width(), x.height())), + lambda x: numpy.array((x.width(), x.height()), dtype="float32"), lambda x: QSizeF(float(x[0]), float(x[1])), ], QRect.__name__: [ - lambda x: numpy.array((x.x(), x.y(), x.width(), x.height())), + lambda x: numpy.array((x.x(), x.y(), x.width(), x.height()), dtype="float32"), lambda x: QRect(int(x[0]), int(x[1]), int(x[2]), int(x[3])) ], QRectF.__name__: [ - lambda x: numpy.array((x.x(), x.y(), x.width(), x.height())), + lambda x: numpy.array((x.x(), x.y(), x.width(), x.height()), dtype="float32"), lambda x: QRect(float(x[0]), float(x[1]), float(x[2]), float(x[3])) ], QColor.__name__: [ - lambda x: numpy.array(x.getRgb()), + lambda x: numpy.array(x.getRgb(), dtype="float32"), lambda x: QColor(int(x[0]), int(x[1]), int(x[2]), int(x[3])) ] } @@ -416,7 +416,7 @@ class TypeConversionFuncs: class SiExpAnimationRefactor(QAbstractAnimation): valueChanged = pyqtSignal(object) - def __init__(self, target: QObject, property_name=None, parent=None): + def __init__(self, target: QObject, property_name=None, parent=None) -> None: super().__init__(parent) self._target = target self._property_name = None @@ -431,9 +431,11 @@ def __init__(self, target: QObject, property_name=None, parent=None): if property_name is not None: self.setPropertyName(property_name) - def init(self, factor: float, bias: float) -> None: + def init(self, factor: float, bias: float, current_value, end_value) -> None: self.factor = factor self.bias = bias + self.setCurrentValue(current_value) + self.setEndValue(end_value) def target(self) -> QObject: return self._target @@ -444,8 +446,11 @@ def propertyName(self) -> str: def endValue(self) -> Any: return self._end_value - def currentValue(self) -> Any: - return self._target.property(self._property_name) + def currentValue(self, raw=False) -> Any: + if raw is True: + return self._current_value + else: + return self._out_func(self._current_value) def distance(self) -> numpy.array: return self._end_value - self._current_value @@ -460,19 +465,19 @@ def setPropertyName(self, name: str) -> None: self._end_value = self._in_func(self._target.property(name)) self._current_value = self._in_func(self._target.property(name)) - def setEndValue(self, value: Any): + def setEndValue(self, value: Any) -> None: if isinstance(value, self._property_type): self._end_value = self._in_func(value) else: self._end_value = numpy.array(value) - def setCurrentValue(self, value: Any): + def setCurrentValue(self, value: Any) -> None: if isinstance(value, self._property_type): self._current_value = self._in_func(value) else: self._current_value = numpy.array(value) - def updateCurrentTime(self, _): + def updateCurrentTime(self, _) -> None: # print(self.distance()) if (self.distance() == 0).all(): self.stop() @@ -486,6 +491,7 @@ def updateCurrentTime(self, _): self._current_value = self._current_value + step self._target.setProperty(self._property_name, self._out_func(self._current_value)) + self.valueChanged.emit(self._current_value) def _loadConversionFuncs(self) -> None: if self._property_type.__name__ in TypeConversionFuncs.functions.keys(): @@ -493,12 +499,4 @@ def _loadConversionFuncs(self) -> None: self._out_func = TypeConversionFuncs.functions.get(self._property_type.__name__)[1] else: self._in_func = lambda x: numpy.array(x) - self._out_func = lambda x: self._property_type(int(x)) - - - - - # - # def setProperty(self, name, value): - # pass - # property. \ No newline at end of file + self._out_func = lambda x: self._property_type(numpy.array(x, dtype="int32")) From dff1e85736d460d47733262c0cd57add384e7d0d Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Sun, 3 Nov 2024 09:39:50 +0800 Subject: [PATCH 11/20] refactor: use new SiExpAnimationRefactor as a test --- siui/components/button_flattened.py | 41 ++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/siui/components/button_flattened.py b/siui/components/button_flattened.py index da6b149..c600d93 100644 --- a/siui/components/button_flattened.py +++ b/siui/components/button_flattened.py @@ -5,12 +5,13 @@ import random from dataclasses import dataclass -from PyQt5.QtCore import QEvent, QRect, QRectF, QSize, Qt, QTimer, pyqtSignal +from PyQt5.QtCore import QEvent, QRect, QRectF, QSize, Qt, QTimer, pyqtSignal, pyqtProperty, QObject from PyQt5.QtGui import QColor, QFontMetrics, QIcon, QLinearGradient, QPainter, QPainterPath, QPaintEvent, QPixmap from PyQt5.QtSvg import QSvgRenderer from PyQt5.QtWidgets import QPushButton, QWidget from siui.core import GlobalFont, SiColor, SiExpAnimation, SiGlobal +from siui.core.animation import SiExpAnimationRefactor from siui.gui import SiFont @@ -153,22 +154,42 @@ def leaveEvent(self, a0): self._hideToolTip() -class SiPushButtonRefactor(QPushButton): +class SiButtonStyleBase(QObject): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) + print(123) + + +class SiPushButtonRefactor(QPushButton, SiButtonStyleBase): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self._my_color = QColor(255, 255, 255, 0) + self.style_data = PushButtonStyleData() + self.style_data.idle_color = SiColor.toArray("#00baadc7", "rgba") + self.style_data.hover_color = SiColor.toArray("#1abaadc7", "rgba") + self.style_data.click_color = SiColor.toArray("#50baadc7", "rgba") - self.highlight_ani = SiExpAnimation(self) - self.highlight_ani.init(1/8, 0.2, SiColor.toArray("#00FFFFFF"), SiColor.toArray("#00FFFFFF")) - self.highlight_ani.ticked.connect(self._onAnimationTicked) + self.highlight_ani = SiExpAnimationRefactor(self, "my_color") + self.highlight_ani.init(1/8, 0.2, SiColor.toArray("#FFFFFF00"), SiColor.toArray("#FFFFFF00")) self._initStyle() self.clicked.connect(self._onButtonClicked) + @pyqtProperty(QColor) + def my_color(self): + return self._my_color + + @my_color.setter + def my_color(self, value: QColor): + self._my_color = value + self._onAnimationTicked(value) + def flash(self) -> None: - self.highlight_ani.setCurrent(self.style_data.click_color) - self.highlight_ani.try_to_start() + self.highlight_ani.setCurrentValue(self.style_data.click_color) + self.highlight_ani.start() def setToolTip(self, tooltip: str) -> None: super().setToolTip(tooltip) @@ -296,7 +317,7 @@ def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: painter.drawPath(self._drawButtonPath(rect)) def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) + painter.setBrush(self.property("my_color")) painter.drawPath(self._drawButtonPath(rect)) def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: @@ -333,14 +354,14 @@ def textRectAndIconRect(self) -> (QRectF, QRect): def enterEvent(self, event) -> None: super().enterEvent(event) - self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.setEndValue(self.style_data.hover_color) self.highlight_ani.start() self._showToolTip() self._updateToolTip() def leaveEvent(self, event) -> None: super().leaveEvent(event) - self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.setEndValue(self.style_data.idle_color) self.highlight_ani.start() self._hideToolTip() From d4676d659c1a531e075cd509d85d81170e04e8bc Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Sun, 3 Nov 2024 09:40:48 +0800 Subject: [PATCH 12/20] fix: ghost window caused by tooltip --- siui/components/tooltip/tooltip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/siui/components/tooltip/tooltip.py b/siui/components/tooltip/tooltip.py index 4767e81..ec09254 100644 --- a/siui/components/tooltip/tooltip.py +++ b/siui/components/tooltip/tooltip.py @@ -81,7 +81,7 @@ def hide_(self): def _completely_hid_signal_handler(self, target): if target == 0: self.completely_hid = True - self.resize(2 * self.margin, 36 + 2 * self.margin) # 变单行内容的高度,宽度不足以显示任何内容 + self.resize(0, 36 + 2 * self.margin) # 变单行内容的高度,宽度不足以显示任何内容 # 2024.11.1 宽度设0解决幽灵窗口 self.text_label.setText("") # 清空文本内容 def setNowInsideOf(self, widget): From e8b700d7f557a52ab9cab33d704d23539a4de494 Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Sun, 3 Nov 2024 09:42:25 +0800 Subject: [PATCH 13/20] feat: new `toPixmap` in parser.py --- .../components/page_widgets/page_widgets.py | 60 +++++++++-- siui/components/label.py | 100 ++++++++++++++++++ siui/core/color.py | 9 +- siui/gui/icons/parser.py | 15 ++- 4 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 siui/components/label.py diff --git a/examples/Gallery for siui/components/page_widgets/page_widgets.py b/examples/Gallery for siui/components/page_widgets/page_widgets.py index bb39175..2cb5f5c 100644 --- a/examples/Gallery for siui/components/page_widgets/page_widgets.py +++ b/examples/Gallery for siui/components/page_widgets/page_widgets.py @@ -3,7 +3,7 @@ import numpy from PyQt5.QtCore import Qt -from PyQt5.QtGui import QCursor +from PyQt5.QtGui import QCursor, QIcon from siui.components import ( SiCircularProgressBar, @@ -14,7 +14,12 @@ SiTitledWidgetGroup, SiWidget, ) -from siui.components.button import SiPushButtonRefactor +from siui.components.button_flattened import ( + SiLongPressButtonRefactor, + SiProgressPushButton, + SiPushButtonRefactor, + SiToggleButtonRefactor, SiFlatButton, +) from siui.components.combobox import SiComboBox from siui.components.menu import SiMenu from siui.components.page import SiPage @@ -250,6 +255,50 @@ def __init__(self, *args, **kwargs): with self.titled_widgets_group as group: group.addTitle("按钮") + # 按钮 + self.refactor_buttons = OptionCardPlaneForWidgetDemos(self) + self.refactor_buttons.setSourceCodeURL("https://github.com/ChinaIceF/PyQt-SiliconUI/blob/main/siui/components" + "/widgets/button.py") + self.refactor_buttons.setTitle("重构的按钮") + + self.refactor_pushbutton = SiPushButtonRefactor(self) + self.refactor_pushbutton.setText("Confirm") + self.refactor_pushbutton.setSvgIcon(SiGlobal.siui.iconpack.get("ic_fluent_mail_checkmark_filled")) + self.refactor_pushbutton.adjustSize() + + self.refactor_progress_button = SiProgressPushButton(self) + self.refactor_progress_button.setText("Downloading") + self.refactor_progress_button.setSvgIcon(SiGlobal.siui.iconpack.get("ic_fluent_arrow_download_filled")) + self.refactor_progress_button.setToolTip("Click me to set a random progress value.") + self.refactor_progress_button.clicked.connect(lambda: self.refactor_progress_button.setProgress(random.random() * 1.3)) + self.refactor_progress_button.adjustSize() + + self.refactor_long_press_button = SiLongPressButtonRefactor(self) + self.refactor_long_press_button.setText("Delete Files") + self.refactor_long_press_button.setToolTip("Hold me to confirm.
Your files will be lost forever!") + self.refactor_long_press_button.setSvgIcon(SiGlobal.siui.iconpack.get("ic_fluent_delete_filled")) + self.refactor_long_press_button.longPressed.connect(lambda: self.refactor_long_press_button.setToolTip("Deleted!")) + self.refactor_long_press_button.adjustSize() + + self.refactor_flat_button = SiFlatButton(self) + self.refactor_flat_button.setText("Flat Button") + self.refactor_flat_button.setSvgIcon(SiGlobal.siui.iconpack.get("ic_fluent_wrench_settings_filled")) + self.refactor_flat_button.setButtonColor("#00FFFFFF") + self.refactor_flat_button.adjustSize() + + self.refactor_toggle_button = SiToggleButtonRefactor(self) + self.refactor_toggle_button.setText("Auto Save") + self.refactor_toggle_button.setSvgIcon(SiGlobal.siui.iconpack.get("ic_fluent_save_filled")) + self.refactor_toggle_button.adjustSize() + + self.refactor_buttons.body().addWidget(self.refactor_pushbutton) + self.refactor_buttons.body().addWidget(self.refactor_progress_button) + self.refactor_buttons.body().addWidget(self.refactor_long_press_button) + self.refactor_buttons.body().addWidget(self.refactor_flat_button) + self.refactor_buttons.body().addWidget(self.refactor_toggle_button) + self.refactor_buttons.body().addPlaceholder(12) + self.refactor_buttons.adjustSize() + # 按钮 self.push_buttons = OptionCardPlaneForWidgetDemos(self) self.push_buttons.setSourceCodeURL("https://github.com/ChinaIceF/PyQt-SiliconUI/blob/main/siui/components" @@ -259,11 +308,6 @@ def __init__(self, *args, **kwargs): container_push_buttons = SiDenseHContainer(self) container_push_buttons.setFixedHeight(32) - self.debug_new_button = SiPushButtonRefactor(self) - self.debug_new_button.resize(128, 32) - self.debug_new_button.setText("新按钮") - self.debug_new_button.setToolTip("我是工具提示") - self.demo_push_button_normal = SiPushButton(self) self.demo_push_button_normal.resize(128, 32) self.demo_push_button_normal.attachment().setText("普通按钮") @@ -277,7 +321,6 @@ def __init__(self, *args, **kwargs): self.demo_push_button_long_press.resize(128, 32) self.demo_push_button_long_press.attachment().setText("长按按钮") - container_push_buttons.addWidget(self.debug_new_button) container_push_buttons.addWidget(self.demo_push_button_normal) container_push_buttons.addWidget(self.demo_push_button_transition) container_push_buttons.addWidget(self.demo_push_button_long_press) @@ -387,6 +430,7 @@ def __init__(self, *args, **kwargs): self.checkboxes.body().addPlaceholder(12) self.checkboxes.adjustSize() + group.addWidget(self.refactor_buttons) group.addWidget(self.push_buttons) group.addWidget(self.flat_buttons) group.addWidget(self.switches) diff --git a/siui/components/label.py b/siui/components/label.py new file mode 100644 index 0000000..d69f8fc --- /dev/null +++ b/siui/components/label.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from dataclasses import dataclass + +import numpy +from PyQt5.QtCore import QEvent +from PyQt5.QtWidgets import QLabel, QWidget + +from siui.core import SiColor, SiGlobal + + +@dataclass +class SiLabelStyleData: + text_color = SiColor.toArray("#00FFFFFF") + background_color = SiColor.toArray("#00FFFFFF") + border_bottom_left_radius: int = 4 + border_bottom_right_radius: int = 4 + border_top_left_radius: int = 4 + border_top_right_radius: int = 4 + + +class SiLabelRefactor(QLabel): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.style_data = SiLabelStyleData() + + @property + def textColor(self) -> numpy.ndarray: + return self.style_data.text_color + + def backgroundColor(self) -> numpy.ndarray: + return self.style_data.background_color + + def borderRadius(self) -> tuple: + return (self.style_data.border_bottom_left_radius, self.style_data.border_bottom_right_radius, + self.style_data.border_top_left_radius, self.style_data.border_top_right_radius) + + def setTextColor(self, code: str | tuple) -> None: + self.style_data.text_color = SiColor.toArray(code) + self.update() + + def setBackgroundColor(self, code: str | tuple) -> None: + self.style_data.background_color = SiColor.toArray(code) + self.update() + + def setBorderRadius(self, *radius: int): + """ + set the border radius of this label. + accepts 1 or 4 param(s). + """ + if len(radius) == 1: + self.style_data.border_bottom_left_radius = radius[0] + self.style_data.border_bottom_right_radius = radius[0] + self.style_data.border_top_left_radius = radius[0] + self.style_data.border_top_right_radius = radius[0] + elif len(radius) == 4: + self.style_data.border_bottom_left_radius = radius[0] + self.style_data.border_bottom_right_radius = radius[1] + self.style_data.border_top_left_radius = radius[2] + self.style_data.border_top_right_radius = radius[3] + else: + raise ValueError(f"setBorderRadius expects 1 or 4 param, but {len(radius)} are given.") + self.update() + + def event(self, event): + if event.type() == QEvent.ToolTip: + return True # 忽略工具提示事件 + return super().event(event) + + def _showToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(self) + tool_tip_window.show_() + + def _hideToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and self.toolTip() != "": + tool_tip_window.setNowInsideOf(None) + tool_tip_window.hide_() + + def _updateToolTip(self) -> None: + tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") + if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: + tool_tip_window.setText(self.toolTip()) + + def setToolTip(self, tooltip) -> None: + super().setToolTip(tooltip) + self._updateToolTip() + + def enterEvent(self, event) -> None: + super().enterEvent(event) + self._showToolTip() + self._updateToolTip() + + def leaveEvent(self, event) -> None: + super().leaveEvent(event) + self._hideToolTip() + diff --git a/siui/core/color.py b/siui/core/color.py index bc2d352..db40e84 100644 --- a/siui/core/color.py +++ b/siui/core/color.py @@ -1,3 +1,4 @@ +from __future__ import annotations from enum import Enum, auto from typing import Union @@ -103,11 +104,17 @@ def RGB_to_RGBA(code: str): @classmethod def toArray(cls, - code: str, + code: str | tuple, c_format: str = "argb"): """ transform `#AARRGGBB` or `#RRGGBB` into `array(A, R, G, B, dtype=int16)` + if code is already be a list / tuple / ndarray, the method returns ndarray. """ + if isinstance(code, numpy.ndarray): + return code + if isinstance(code, (list, tuple)): + return numpy.array(code) + code = cls.RGB_to_RGBA(code) code = code.lstrip("#") a, r, g, b = int(code[0:2], 16), int(code[2:4], 16), int(code[4:6], 16), int(code[6:8], 16) diff --git a/siui/gui/icons/parser.py b/siui/gui/icons/parser.py index fb1f9b5..ec136ae 100644 --- a/siui/gui/icons/parser.py +++ b/siui/gui/icons/parser.py @@ -1,6 +1,8 @@ import os -from PyQt5.QtCore import QByteArray +from PyQt5.QtCore import QByteArray, QSize, Qt +from PyQt5.QtGui import QPainter, QPixmap +from PyQt5.QtSvg import QSvgRenderer class GlobalIconPack: @@ -79,3 +81,14 @@ def getDict(self, class_name=None) -> dict: def getClassNames(self) -> dict.keys: return self.icons_classified.keys() + + def toPixmap(self, name: str, size: QSize = QSize(64, 64), color_code: str = None): + svg_bytes = self.get(name, color_code) + pixmap = QPixmap(size) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + svg_renderer = QSvgRenderer(svg_bytes) + svg_renderer.render(painter) + painter.end() + return pixmap From 1d8b6ab6b894feb5e9419634bf17f5a26d1a81a7 Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Sun, 3 Nov 2024 13:03:30 +0800 Subject: [PATCH 14/20] refactor: adopt new `SiExpAnimationRefactor` in button.py --- .../components/page_widgets/page_widgets.py | 5 +- siui/components/button.py | 250 ++++++++++-------- siui/core/animation.py | 26 +- siui/core/color.py | 14 +- 4 files changed, 176 insertions(+), 119 deletions(-) diff --git a/examples/Gallery for siui/components/page_widgets/page_widgets.py b/examples/Gallery for siui/components/page_widgets/page_widgets.py index 2cb5f5c..90c2d49 100644 --- a/examples/Gallery for siui/components/page_widgets/page_widgets.py +++ b/examples/Gallery for siui/components/page_widgets/page_widgets.py @@ -14,11 +14,12 @@ SiTitledWidgetGroup, SiWidget, ) -from siui.components.button_flattened import ( +from siui.components.button import ( + SiFlatButton, SiLongPressButtonRefactor, SiProgressPushButton, SiPushButtonRefactor, - SiToggleButtonRefactor, SiFlatButton, + SiToggleButtonRefactor, ) from siui.components.combobox import SiComboBox from siui.components.menu import SiMenu diff --git a/siui/components/button.py b/siui/components/button.py index 2c07b9a..6361493 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -5,21 +5,22 @@ import random from dataclasses import dataclass -from PyQt5.QtCore import QEvent, QRect, QRectF, QSize, Qt, QTimer, pyqtSignal +from PyQt5.QtCore import QEvent, QRect, QRectF, QSize, Qt, QTimer, pyqtProperty, pyqtSignal from PyQt5.QtGui import QColor, QFontMetrics, QIcon, QLinearGradient, QPainter, QPainterPath, QPaintEvent, QPixmap from PyQt5.QtSvg import QSvgRenderer from PyQt5.QtWidgets import QPushButton, QWidget from siui.core import GlobalFont, SiColor, SiExpAnimation, SiGlobal +from siui.core.animation import SiExpAnimationRefactor from siui.gui import SiFont @dataclass class ButtonStyleData: - idle_color = SiColor.toArray("#00baadc7") - hover_color = SiColor.toArray("#1abaadc7") - click_color = SiColor.toArray("#50baadc7") - button_color = SiColor.toArray("#4C4554", "rgba") + idle_color = QColor("#00baadc7") + hover_color = QColor("#1abaadc7") + click_color = QColor("#50baadc7") + button_color = QColor("#4C4554") border_radius: int = 7 icon_text_gap: int = 4 @@ -31,29 +32,29 @@ class FlatButtonStyleData(ButtonStyleData): @dataclass class PushButtonStyleData(ButtonStyleData): - background_color = SiColor.toArray("#2d2932", "rgba") + background_color = QColor("#2d2932") border_inner_radius: int = 4 border_height: int = 3 @dataclass class ProgressPushButtonStyleData(PushButtonStyleData): - progress_color = SiColor.toArray("#806799", "rgba") - complete_color = SiColor.toArray("#519868", "rgba") + progress_color = QColor("#806799") + complete_color = QColor("#519868") @dataclass class LongPressButtonStyleData(PushButtonStyleData): - progress_color = SiColor.toArray("#DA3462", "rgba") - button_color = SiColor.toArray("#932a48", "rgba") - background_color = SiColor.toArray("#642d41", "rgba") - click_color = SiColor.toArray("#40FFFFFF") + progress_color = QColor("#DA3462") + button_color = QColor("#932a48") + background_color = QColor("#642d41") + click_color = QColor("#40FFFFFF") @dataclass class ToggleButtonStyleData(ButtonStyleData): - toggled_text_color = SiColor.toArray("#DFDFDF", "rgba") - toggled_button_color = SiColor.toArray("#519868", "rgba") + toggled_text_color = QColor("#DFDFDF") + toggled_button_color = QColor("#519868") class ABCButton(QPushButton): @@ -61,18 +62,65 @@ def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self.style_data = None + self._highlight_rect_color = QColor("#00FFFFFF") + self._progress = 0 + self._progress_rect_color = None + self._button_rect_color = None + self._text_color = None + + self.highlight_ani = SiExpAnimationRefactor(self, "highlightRectColor") + self.highlight_ani.init(1/8, 0.2, self._highlight_rect_color, self._highlight_rect_color) - self.highlight_ani = SiExpAnimation(self) - self.highlight_ani.setFactor(1/8) - self.highlight_ani.setBias(0.2) - self.highlight_ani.setTarget(SiColor.toArray("#00FFFFFF")) - self.highlight_ani.setCurrent(SiColor.toArray("#00FFFFFF")) - self.highlight_ani.ticked.connect(self._onAnimationTicked) self.clicked.connect(self._onButtonClicked) + @pyqtProperty(QColor) + def highlightRectColor(self): + return self._highlight_rect_color + + @highlightRectColor.setter + def highlightRectColor(self, value: QColor): + self._highlight_rect_color = value + self.update() + + @pyqtProperty(QColor) + def buttonRectColor(self): + return self._button_rect_color + + @buttonRectColor.setter + def buttonRectColor(self, color: QColor): + self._button_rect_color = color + self.update() + + @pyqtProperty(QColor) + def textColor(self): + return self._text_color + + @textColor.setter + def textColor(self, color: QColor): + self._text_color = color + self.update() + + @pyqtProperty(float) + def progress(self): + return self._progress + + @progress.setter + def progress(self, value: float): + self._progress = max(0.0, min(value, 1.0)) + self.update() + + @pyqtProperty(QColor) + def progressRectColor(self): + return self._progress_rect_color + + @progressRectColor.setter + def progressRectColor(self, value: QColor): + self._progress_rect_color = value + self.update() + def flash(self) -> None: - self.highlight_ani.setCurrent(self.style_data.click_color) - self.highlight_ani.try_to_start() + self.highlight_ani.setCurrentValue(self.style_data.click_color) + self.highlight_ani.start() def setToolTip(self, tooltip: str) -> None: super().setToolTip(tooltip) @@ -86,8 +134,8 @@ def setBorderRadius(self, r: int) -> None: self.style_data.border_radius = r self.update() - def setButtonColor(self, code: str) -> None: - self.style_data.button_color = SiColor.toArray(code, "rgba") + def setButtonColor(self, color: QColor | str) -> None: + self.style_data.button_color = QColor(color) self.update() def setSvgIcon(self, svg_data: bytes) -> None: @@ -168,19 +216,19 @@ def _initStyle(self): self.setIconSize(QSize(20, 20)) @classmethod - def withText(cls, text: str, parent: QWidget | None = None) -> "SiPushButton": + def withText(cls, text: str, parent: QWidget | None = None): obj = cls(parent) obj.setText(text) return obj @classmethod - def withIcon(cls, icon: QIcon, parent: QWidget | None = None) -> "SiPushButton": + def withIcon(cls, icon: QIcon, parent: QWidget | None = None): obj = cls(parent) obj.setIcon(icon) return obj @classmethod - def withTextAndIcon(cls, text: str, icon: str, parent: QWidget | None = None) -> "SiPushButton": + def withTextAndIcon(cls, text: str, icon: str, parent: QWidget | None = None): obj = cls(parent) obj.setText(text) obj.setIcon(QIcon(icon)) @@ -209,7 +257,7 @@ def _drawBackgroundPath(self, rect: QRect) -> QPainterPath: return path def _drawBackgroundRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(QColor(*self.style_data.background_color)) + painter.setBrush(self.style_data.background_color) painter.drawPath(self._drawBackgroundPath(rect)) def _drawButtonPath(self, rect: QRect) -> QPainterPath: @@ -219,11 +267,11 @@ def _drawButtonPath(self, rect: QRect) -> QPainterPath: return path def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(QColor(*self.style_data.button_color)) + painter.setBrush(self.style_data.button_color) painter.drawPath(self._drawButtonPath(rect)) def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) + painter.setBrush(self._highlight_rect_color) painter.drawPath(self._drawButtonPath(rect)) def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: @@ -234,9 +282,6 @@ def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: def _drawPixmapRect(self, painter: QPainter, rect: QRectF) -> None: painter.drawPixmap(rect, self.icon().pixmap(64, 64)) - def _onAnimationTicked(self, _) -> None: - self.update() - def _onButtonClicked(self) -> None: self.flash() @@ -260,14 +305,14 @@ def textRectAndIconRect(self) -> (QRectF, QRect): def enterEvent(self, event) -> None: super().enterEvent(event) - self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.setEndValue(self.style_data.hover_color) self.highlight_ani.start() self._showToolTip() self._updateToolTip() def leaveEvent(self, event) -> None: super().leaveEvent(event) - self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.setEndValue(self.style_data.idle_color) self.highlight_ani.start() self._hideToolTip() @@ -292,52 +337,48 @@ def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self.style_data = ProgressPushButtonStyleData() - self.progress_ = 0 + self._progress_rect_color = self.style_data.progress_color + self._progress = 0 - self.progress_ani = SiExpAnimation(self) + self.progress_ani = SiExpAnimationRefactor(self, "progress") self.progress_ani.init(1/6, 0.005, 0, 0) - self.progress_ani.ticked.connect(lambda _: self.update()) - - self.progress_color_ani = SiExpAnimation(self) - self.progress_color_ani.init(1/8, 0.01, self.style_data.progress_color, self.style_data.progress_color) - self.progress_color_ani.ticked.connect(lambda _: self.update()) - @property - def progress(self) -> float: - return self.progress_ + self.progress_color_ani = SiExpAnimationRefactor(self, "progressRectColor") + self.progress_color_ani.init(1/8, 0.01, self._progress_rect_color, self._progress_rect_color) def setProgress(self, p: float, ani: bool = True) -> None: - self.progress_ = max(0.0, min(p, 1.0)) + self._progress = max(0.0, min(p, 1.0)) self._updateProgress(ani) self._updateCompleteState() - self.update() + # self.update() def _updateProgress(self, ani: bool) -> None: if ani is True: - self.progress_ani.setTarget(self.progress_) + self.progress_ani.setEndValue(self._progress) self.progress_ani.start() else: - self.progress_ani.setTarget(self.progress_) - self.progress_ani.setCurrent(self.progress_) + self.progress_ani.setEndValue(self._progress) + self.progress_ani.setCurrentValue(self._progress) self.progress_ani.stop() def _updateCompleteState(self) -> None: - if self.progress_ == 1.0: - self.progress_color_ani.setTarget(self.style_data.complete_color) + if self.progress_ani.endValue() == 1.0: + self.progress_color_ani.setEndValue(self.style_data.complete_color) self.progress_color_ani.start() else: - self.progress_color_ani.setTarget(self.style_data.progress_color) + self.progress_color_ani.setEndValue(self.style_data.progress_color) self.progress_color_ani.start() - def setProgressColor(self, code: str) -> None: - self.style_data.progress_color = SiColor.toArray(code, "rgba") + def setProgressColor(self, color: QColor | str) -> None: + self.style_data.progress_color = QColor(color) + self._updateCompleteState() self.update() def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: - p = min(self.progress_ani.current_, 1) # prevent progress exceeding caused by using animation. + p = min(self._progress, 1) # prevent progress exceeding caused by using animation. gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) - gradient.setColorAt(p - 0.0001, QColor(*self.progress_color_ani.current_)) - gradient.setColorAt(p, QColor(*self.style_data.button_color)) + gradient.setColorAt(p - 0.0001, self._progress_rect_color) + gradient.setColorAt(p, self.style_data.button_color) painter.setBrush(gradient) painter.drawPath(self._drawButtonPath(rect)) @@ -349,12 +390,11 @@ def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self.style_data = LongPressButtonStyleData() - self.progress_ = 0 + self._progress = 0 - self.progress_ani = SiExpAnimation(self) + self.progress_ani = SiExpAnimationRefactor(self, "progress") self.progress_ani.init(-1/16, 0.12, 0, 0) - self.progress_ani.ticked.connect(lambda _: self.update()) - # self.progress_ani.ticked.connect(print) + self.progress_ani.valueChanged.connect(print) self.go_backwards_timer = QTimer(self) self.go_backwards_timer.setSingleShot(True) @@ -365,34 +405,31 @@ def __init__(self, parent: QWidget | None = None) -> None: self.mouse_pressed_timer.setInterval(1000//60) self.mouse_pressed_timer.timeout.connect(self._onMousePressed) - @property - def progress(self) -> float: - return self.progress_ - def setProgress(self, p: float, ani: bool = True) -> None: - self.progress_ = max(0.0, min(p, 1.0)) + self._progress = max(0.0, min(p, 1.0)) self._updateProgress(ani) + self.progress_ani.update() self.update() def _stepLength(self) -> float: - return (1 - self.progress_) / 16 + 0.001 + return (1 - self._progress) / 16 + 0.001 def _onMousePressed(self) -> None: - self.setProgress(self.progress_ + self._stepLength(), ani=False) + self.setProgress(self._progress + self._stepLength(), ani=False) def _onButtonClicked(self) -> None: pass # disable flashes on mouse click def _updateProgress(self, ani: bool) -> None: if ani is True: - self.progress_ani.setTarget(self.progress_) + self.progress_ani.setEndValue(self._progress) self.progress_ani.start() else: - self.progress_ani.setTarget(self.progress_) - self.progress_ani.setCurrent(self.progress_) + self.progress_ani.setEndValue(self._progress) + self.progress_ani.setCurrentValue(self._progress) self.progress_ani.stop() - if self.progress_ == 1.0: + if self._progress == 1.0: self.mouse_pressed_timer.stop() self.go_backwards_timer.stop() self.longPressed.emit() @@ -400,25 +437,24 @@ def _updateProgress(self, ani: bool) -> None: self._goBackwards(200) def _onLongPressed(self) -> None: - self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.setCurrentValue(self.style_data.click_color) self.highlight_ani.start() def _goBackwards(self, delay: int = 0) -> None: - self.progress_ = 0 - self.progress_ani.setTarget(0) - self.progress_ani.start(delay) + self.progress_ani.setEndValue(0) + self.progress_ani.startAfter(delay) def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: - p = min(self.progress_ani.current_, 1) # prevent progress exceeding caused by using animation. + p = min(self._progress, 1) # prevent progress exceeding caused by using animation. gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) - gradient.setColorAt(p - 0.0001, QColor(*self.style_data.progress_color)) - gradient.setColorAt(p, QColor(*self.style_data.button_color)) + gradient.setColorAt(p - 0.0001, self.style_data.progress_color) + gradient.setColorAt(p, self.style_data.button_color) painter.setBrush(gradient) painter.drawPath(self._drawButtonPath(rect)) def mousePressEvent(self, e) -> None: super().mousePressEvent(e) - if self.progress_ani.isActive() is False and self.mouse_pressed_timer.isActive() is False: + if self.progress_ani.state() != self.progress_ani.State.Running and not self.mouse_pressed_timer.isActive(): self.mouse_pressed_timer.start() self.go_backwards_timer.stop() @@ -447,11 +483,11 @@ def _drawButtonPath(self, rect: QRect) -> QPainterPath: return path def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(QColor(*self.style_data.button_color)) + painter.setBrush(self.style_data.button_color) painter.drawPath(self._drawButtonPath(rect)) def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(QColor(SiColor.toCode(self.highlight_ani.current_))) + painter.setBrush(self._highlight_rect_color) painter.drawPath(self._drawButtonPath(rect)) def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: @@ -484,7 +520,7 @@ def _onAnimationTicked(self, _) -> None: self.update() def _onButtonClicked(self) -> None: - self.highlight_ani.setCurrent(self.style_data.click_color) + self.highlight_ani.setCurrentValue(self.style_data.click_color) self.highlight_ani.start() def paintEvent(self, event: QPaintEvent) -> None: @@ -503,14 +539,14 @@ def paintEvent(self, event: QPaintEvent) -> None: def enterEvent(self, event) -> None: super().enterEvent(event) - self.highlight_ani.setTarget(self.style_data.hover_color) + self.highlight_ani.setEndValue(self.style_data.hover_color) self.highlight_ani.start() self._showToolTip() self._updateToolTip() def leaveEvent(self, event) -> None: super().leaveEvent(event) - self.highlight_ani.setTarget(self.style_data.idle_color) + self.highlight_ani.setEndValue(self.style_data.idle_color) self.highlight_ani.start() self._hideToolTip() @@ -518,45 +554,45 @@ def leaveEvent(self, event) -> None: class SiToggleButtonRefactor(SiFlatButton): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) - self.setCheckable(True) + self.style_data = ToggleButtonStyleData() + self._button_rect_color = self.style_data.button_color + self._text_color = self.style_data.toggled_text_color - self.toggle_btn_color_ani = SiExpAnimation(self) - self.toggle_btn_color_ani.init(1/8, 0.01, self.style_data.button_color, self.style_data.button_color) - self.toggle_btn_color_ani.ticked.connect(lambda _: self.update()) + self.toggle_btn_color_ani = SiExpAnimationRefactor(self, "buttonRectColor") + self.toggle_btn_color_ani.init(1/8, 0.01, self._button_rect_color, self._button_rect_color) - self.toggle_text_color_ani = SiExpAnimation(self) - self.toggle_text_color_ani.init(1/8, 0.01, (223, 223, 223, 255), (223, 223, 223, 255)) - self.toggle_text_color_ani.ticked.connect(lambda _: self.update()) + self.toggle_text_color_ani = SiExpAnimationRefactor(self, "textColor") + self.toggle_text_color_ani.init(1/8, 0.01, self._text_color, self._text_color) self.toggled.connect(self._onButtonToggled) - def setToggledButtonColor(self, code: str) -> None: - self.style_data.toggled_button_color = SiColor.toArray(code, "rgba") + def setToggledButtonColor(self, color: QColor | str) -> None: + self.style_data.toggled_button_color = QColor(color) self.update() - def setToggledTextColor(self, code: str) -> None: - self.style_data.toggled_text_color = SiColor.toArray(code, "rgba") + def setToggledTextColor(self, color: QColor | str) -> None: + self.style_data.toggled_text_color = QColor(color) self.update() def _onButtonToggled(self, state: bool) -> None: if state: - self.toggle_btn_color_ani.setTarget(self.style_data.toggled_button_color) - self.toggle_text_color_ani.setTarget(self.style_data.toggled_text_color) - self.toggle_btn_color_ani.try_to_start() - self.toggle_text_color_ani.try_to_start() + self.toggle_btn_color_ani.setEndValue(self.style_data.toggled_button_color) + self.toggle_text_color_ani.setEndValue(self.style_data.toggled_text_color) + self.toggle_btn_color_ani.start() + self.toggle_text_color_ani.start() else: - self.toggle_btn_color_ani.setTarget(self.style_data.button_color) - self.toggle_text_color_ani.setTarget(self.palette().text().color().getRgb()) - self.toggle_btn_color_ani.try_to_start() - self.toggle_text_color_ani.try_to_start() + self.toggle_btn_color_ani.setEndValue(self.style_data.button_color) + self.toggle_text_color_ani.setEndValue(self.palette().text().color()) + self.toggle_btn_color_ani.start() + self.toggle_text_color_ani.start() def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(QColor(*self.toggle_btn_color_ani.current_)) + painter.setBrush(self.property("buttonRectColor")) painter.drawPath(self._drawButtonPath(rect)) def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: - painter.setPen(QColor(*self.toggle_text_color_ani.current_)) + painter.setPen(self.property("textColor")) painter.setFont(self.font()) painter.drawText(rect, Qt.AlignCenter, self.text()) diff --git a/siui/core/animation.py b/siui/core/animation.py index f0f1bd8..593f6fd 100644 --- a/siui/core/animation.py +++ b/siui/core/animation.py @@ -418,6 +418,8 @@ class SiExpAnimationRefactor(QAbstractAnimation): def __init__(self, target: QObject, property_name=None, parent=None) -> None: super().__init__(parent) + self.start_after_timer = QTimer(self) + self._target = target self._property_name = None self._property_type = None @@ -443,8 +445,11 @@ def target(self) -> QObject: def propertyName(self) -> str: return self._property_name - def endValue(self) -> Any: - return self._end_value + def endValue(self, raw=False) -> Any: + if raw is True: + return self._end_value + else: + return self._out_func(self._end_value) def currentValue(self, raw=False) -> Any: if raw is True: @@ -458,6 +463,20 @@ def distance(self) -> numpy.array: def duration(self) -> int: return -1 + def start(self, *args, **kwargs): + if self.state() != QAbstractAnimation.State.Running: + super().start(*args, **kwargs) + + def startAfter(self, msec: int): + self.start_after_timer.singleShot(msec, self.start) + + def update(self): + self.setCurrentValue(self._target.property(self._property_name)) + + def updateAndStart(self): + self.update() + self.start() + def setPropertyName(self, name: str) -> None: self._property_name = name self._property_type = type(self._target.property(name)) @@ -476,6 +495,7 @@ def setCurrentValue(self, value: Any) -> None: self._current_value = self._in_func(value) else: self._current_value = numpy.array(value) + self.valueChanged.emit(self._current_value) def updateCurrentTime(self, _) -> None: # print(self.distance()) @@ -499,4 +519,4 @@ def _loadConversionFuncs(self) -> None: self._out_func = TypeConversionFuncs.functions.get(self._property_type.__name__)[1] else: self._in_func = lambda x: numpy.array(x) - self._out_func = lambda x: self._property_type(numpy.array(x, dtype="int32")) + self._out_func = lambda x: self._property_type(numpy.array(x, dtype="float32")) diff --git a/siui/core/color.py b/siui/core/color.py index db40e84..5143cc7 100644 --- a/siui/core/color.py +++ b/siui/core/color.py @@ -105,7 +105,7 @@ def RGB_to_RGBA(code: str): @classmethod def toArray(cls, code: str | tuple, - c_format: str = "argb"): + to_format: str = "argb"): """ transform `#AARRGGBB` or `#RRGGBB` into `array(A, R, G, B, dtype=int16)` if code is already be a list / tuple / ndarray, the method returns ndarray. @@ -119,14 +119,14 @@ def toArray(cls, code = code.lstrip("#") a, r, g, b = int(code[0:2], 16), int(code[2:4], 16), int(code[4:6], 16), int(code[6:8], 16) - c_format = c_format.lower() - if c_format not in ["rgba", "argb", "rgb"]: - raise ValueError(f"{c_format} is not a valid format (rgba, argb, rgb)") - if c_format == "rgba": + to_format = to_format.lower() + if to_format not in ["rgba", "argb", "rgb"]: + raise ValueError(f"{to_format} is not a valid format (rgba, argb, rgb)") + if to_format == "rgba": return numpy.array([r, g, b, a], dtype=numpy.int16) - if c_format == "argb": + if to_format == "argb": return numpy.array([a, r, g, b], dtype=numpy.int16) - if c_format == "rgb": + if to_format == "rgb": return numpy.array([r, g, b], dtype=numpy.int16) @staticmethod From 9a05bbe5e0e249b1b40d8570c68a4c6f1e7f6bca Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Mon, 4 Nov 2024 13:30:34 +0800 Subject: [PATCH 15/20] refactor: use Property class for better reference --- siui/components/button.py | 83 +++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/siui/components/button.py b/siui/components/button.py index 6361493..5b3ec34 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -2,28 +2,42 @@ # replace button once it's done. Now it's draft, code may be ugly and verbose temporarily. from __future__ import annotations -import random from dataclasses import dataclass -from PyQt5.QtCore import QEvent, QRect, QRectF, QSize, Qt, QTimer, pyqtProperty, pyqtSignal +from PyQt5.QtCore import QEvent, QObject, QRect, QRectF, QSize, Qt, QTimer, pyqtProperty, pyqtSignal from PyQt5.QtGui import QColor, QFontMetrics, QIcon, QLinearGradient, QPainter, QPainterPath, QPaintEvent, QPixmap from PyQt5.QtSvg import QSvgRenderer from PyQt5.QtWidgets import QPushButton, QWidget -from siui.core import GlobalFont, SiColor, SiExpAnimation, SiGlobal +from siui.core import GlobalFont, SiColor, SiGlobal from siui.core.animation import SiExpAnimationRefactor from siui.gui import SiFont @dataclass -class ButtonStyleData: +class ButtonStyleData(QObject): + font = SiFont.tokenized(GlobalFont.S_NORMAL) + + text_color = QColor("#DFDFDF") idle_color = QColor("#00baadc7") hover_color = QColor("#1abaadc7") click_color = QColor("#50baadc7") button_color = QColor("#4C4554") + progress_color = QColor("#806799") + complete_color = QColor("#519868") + background_color = QColor("#2d2932") + toggled_text_color = QColor("#DFDFDF") + toggled_button_color = QColor("#519868") + + border_inner_radius: int = 4 border_radius: int = 7 + border_height: int = 3 icon_text_gap: int = 4 + def __init__(self, parent: QWidget, name: str): + super().__init__(parent) + self.setObjectName(name) + @dataclass class FlatButtonStyleData(ButtonStyleData): @@ -58,17 +72,26 @@ class ToggleButtonStyleData(ButtonStyleData): class ABCButton(QPushButton): + class Property: + textColor = "textColor" + buttonRectColor = "buttonRectColor" + progressRectColor = "progressRectColor" + highlightRectColor = "highlightRectColor" + backgroundRectColor = "backgroundRectColor" + progress = "progress" + def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self.style_data = None - self._highlight_rect_color = QColor("#00FFFFFF") self._progress = 0 + self._highlight_rect_color = QColor("#00FFFFFF") self._progress_rect_color = None + self._background_rect_color = None self._button_rect_color = None self._text_color = None - self.highlight_ani = SiExpAnimationRefactor(self, "highlightRectColor") + self.highlight_ani = SiExpAnimationRefactor(self, self.Property.highlightRectColor) self.highlight_ani.init(1/8, 0.2, self._highlight_rect_color, self._highlight_rect_color) self.clicked.connect(self._onButtonClicked) @@ -91,6 +114,15 @@ def buttonRectColor(self, color: QColor): self._button_rect_color = color self.update() + @pyqtProperty(QColor) + def backgroundRectColor(self): + return self._background_rect_color + + @backgroundRectColor.setter + def backgroundRectColor(self, color: QColor): + self._background_rect_color = color + self.update() + @pyqtProperty(QColor) def textColor(self): return self._text_color @@ -118,6 +150,9 @@ def progressRectColor(self, value: QColor): self._progress_rect_color = value self.update() + def reloadStyle(self): + raise NotImplementedError() + def flash(self) -> None: self.highlight_ani.setCurrentValue(self.style_data.click_color) self.highlight_ani.start() @@ -211,8 +246,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self._initStyle() def _initStyle(self): - self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) - self.setStyleSheet("color: #DFDFDF;") + self.setFont(self.style_data.font) self.setIconSize(QSize(20, 20)) @classmethod @@ -271,11 +305,11 @@ def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: painter.drawPath(self._drawButtonPath(rect)) def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(self._highlight_rect_color) + painter.setBrush(self._highlight_rect_color) # use property variable painter.drawPath(self._drawButtonPath(rect)) def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: - painter.setPen(self.palette().text().color()) + painter.setPen(self.style_data.text_color) painter.setFont(self.font()) painter.drawText(rect, Qt.AlignCenter, self.text()) @@ -340,10 +374,10 @@ def __init__(self, parent: QWidget | None = None) -> None: self._progress_rect_color = self.style_data.progress_color self._progress = 0 - self.progress_ani = SiExpAnimationRefactor(self, "progress") + self.progress_ani = SiExpAnimationRefactor(self, self.Property.progress) self.progress_ani.init(1/6, 0.005, 0, 0) - self.progress_color_ani = SiExpAnimationRefactor(self, "progressRectColor") + self.progress_color_ani = SiExpAnimationRefactor(self, self.Property.progressRectColor) self.progress_color_ani.init(1/8, 0.01, self._progress_rect_color, self._progress_rect_color) def setProgress(self, p: float, ani: bool = True) -> None: @@ -377,7 +411,7 @@ def setProgressColor(self, color: QColor | str) -> None: def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: p = min(self._progress, 1) # prevent progress exceeding caused by using animation. gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) - gradient.setColorAt(p - 0.0001, self._progress_rect_color) + gradient.setColorAt(p - 0.0001, self._progress_rect_color) # use property variable gradient.setColorAt(p, self.style_data.button_color) painter.setBrush(gradient) painter.drawPath(self._drawButtonPath(rect)) @@ -392,9 +426,9 @@ def __init__(self, parent: QWidget | None = None) -> None: self.style_data = LongPressButtonStyleData() self._progress = 0 - self.progress_ani = SiExpAnimationRefactor(self, "progress") + self.progress_ani = SiExpAnimationRefactor(self, self.Property.progress) self.progress_ani.init(-1/16, 0.12, 0, 0) - self.progress_ani.valueChanged.connect(print) + # self.progress_ani.valueChanged.connect(print) self.go_backwards_timer = QTimer(self) self.go_backwards_timer.setSingleShot(True) @@ -472,8 +506,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self._initStyle() def _initStyle(self): - self.setFont(SiFont.tokenized(GlobalFont.S_NORMAL)) - self.setStyleSheet("color: #DFDFDF;") + self.setFont(self.style_data.font) self.setIconSize(QSize(20, 20)) def _drawButtonPath(self, rect: QRect) -> QPainterPath: @@ -487,11 +520,11 @@ def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: painter.drawPath(self._drawButtonPath(rect)) def _drawHighLightRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(self._highlight_rect_color) + painter.setBrush(self._highlight_rect_color) # use property variable painter.drawPath(self._drawButtonPath(rect)) def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: - painter.setPen(self.palette().text().color()) + painter.setPen(self.style_data.text_color) painter.setFont(self.font()) painter.drawText(rect, Qt.AlignCenter, self.text()) @@ -558,12 +591,12 @@ def __init__(self, parent: QWidget | None = None) -> None: self.style_data = ToggleButtonStyleData() self._button_rect_color = self.style_data.button_color - self._text_color = self.style_data.toggled_text_color + self._text_color = self.style_data.text_color - self.toggle_btn_color_ani = SiExpAnimationRefactor(self, "buttonRectColor") + self.toggle_btn_color_ani = SiExpAnimationRefactor(self, self.Property.buttonRectColor) self.toggle_btn_color_ani.init(1/8, 0.01, self._button_rect_color, self._button_rect_color) - self.toggle_text_color_ani = SiExpAnimationRefactor(self, "textColor") + self.toggle_text_color_ani = SiExpAnimationRefactor(self, self.Property.textColor) self.toggle_text_color_ani.init(1/8, 0.01, self._text_color, self._text_color) self.toggled.connect(self._onButtonToggled) @@ -584,15 +617,15 @@ def _onButtonToggled(self, state: bool) -> None: self.toggle_text_color_ani.start() else: self.toggle_btn_color_ani.setEndValue(self.style_data.button_color) - self.toggle_text_color_ani.setEndValue(self.palette().text().color()) + self.toggle_text_color_ani.setEndValue(self.style_data.text_color) self.toggle_btn_color_ani.start() self.toggle_text_color_ani.start() def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: - painter.setBrush(self.property("buttonRectColor")) + painter.setBrush(self._button_rect_color) # use property variable painter.drawPath(self._drawButtonPath(rect)) def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: - painter.setPen(self.property("textColor")) + painter.setPen(self._text_color) # use property variable painter.setFont(self.font()) painter.drawText(rect, Qt.AlignCenter, self.text()) From 6148b5dd874d50ef1ea123f534fe8f56a4db729d Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Wed, 6 Nov 2024 00:46:36 +0800 Subject: [PATCH 16/20] refactor: new `reloadStyleData` method for style reloading when style data is updated --- siui/components/button.py | 52 ++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/siui/components/button.py b/siui/components/button.py index 5b3ec34..5db6197 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -73,12 +73,12 @@ class ToggleButtonStyleData(ButtonStyleData): class ABCButton(QPushButton): class Property: - textColor = "textColor" - buttonRectColor = "buttonRectColor" - progressRectColor = "progressRectColor" - highlightRectColor = "highlightRectColor" - backgroundRectColor = "backgroundRectColor" - progress = "progress" + TextColor = "textColor" + ButtonRectColor = "buttonRectColor" + ProgressRectColor = "progressRectColor" + HighlightRectColor = "highlightRectColor" + BackgroundRectColor = "backgroundRectColor" + Progress = "progress" def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -91,10 +91,12 @@ def __init__(self, parent: QWidget | None = None) -> None: self._button_rect_color = None self._text_color = None - self.highlight_ani = SiExpAnimationRefactor(self, self.Property.highlightRectColor) + self.highlight_ani = SiExpAnimationRefactor(self, self.Property.HighlightRectColor) self.highlight_ani.init(1/8, 0.2, self._highlight_rect_color, self._highlight_rect_color) self.clicked.connect(self._onButtonClicked) + self.clicked.connect(lambda: print("Ni好")) + self.clicked.disconnect() @pyqtProperty(QColor) def highlightRectColor(self): @@ -150,7 +152,7 @@ def progressRectColor(self, value: QColor): self._progress_rect_color = value self.update() - def reloadStyle(self): + def reloadStyleData(self): raise NotImplementedError() def flash(self) -> None: @@ -272,6 +274,10 @@ def withTextAndIcon(cls, text: str, icon: str, parent: QWidget | None = None): def bottomBorderHeight(self) -> int: return self.style_data.border_height + def reloadStyleData(self) -> None: + self.setFont(self.style_data.font) + self.update() + def setBackgroundColor(self, code: str) -> None: self.style_data.background_color = SiColor.toArray(code, "rgba") self.update() @@ -374,10 +380,10 @@ def __init__(self, parent: QWidget | None = None) -> None: self._progress_rect_color = self.style_data.progress_color self._progress = 0 - self.progress_ani = SiExpAnimationRefactor(self, self.Property.progress) + self.progress_ani = SiExpAnimationRefactor(self, self.Property.Progress) self.progress_ani.init(1/6, 0.005, 0, 0) - self.progress_color_ani = SiExpAnimationRefactor(self, self.Property.progressRectColor) + self.progress_color_ani = SiExpAnimationRefactor(self, self.Property.ProgressRectColor) self.progress_color_ani.init(1/8, 0.01, self._progress_rect_color, self._progress_rect_color) def setProgress(self, p: float, ani: bool = True) -> None: @@ -386,7 +392,12 @@ def setProgress(self, p: float, ani: bool = True) -> None: self._updateCompleteState() # self.update() - def _updateProgress(self, ani: bool) -> None: + def reloadStyleData(self) -> None: + self.setFont(self.style_data.font) + self._updateCompleteState() + self.update() + + def _updateProgress(self, ani: bool = True) -> None: if ani is True: self.progress_ani.setEndValue(self._progress) self.progress_ani.start() @@ -426,7 +437,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self.style_data = LongPressButtonStyleData() self._progress = 0 - self.progress_ani = SiExpAnimationRefactor(self, self.Property.progress) + self.progress_ani = SiExpAnimationRefactor(self, self.Property.Progress) self.progress_ani.init(-1/16, 0.12, 0, 0) # self.progress_ani.valueChanged.connect(print) @@ -445,6 +456,10 @@ def setProgress(self, p: float, ani: bool = True) -> None: self.progress_ani.update() self.update() + def reloadStyleData(self) -> None: + self.setFont(self.style_data.font) + self.update() + def _stepLength(self) -> float: return (1 - self._progress) / 16 + 0.001 @@ -509,6 +524,10 @@ def _initStyle(self): self.setFont(self.style_data.font) self.setIconSize(QSize(20, 20)) + def reloadStyleData(self) -> None: + self.setFont(self.style_data.font) + self.update() + def _drawButtonPath(self, rect: QRect) -> QPainterPath: radius = self.style_data.border_radius path = QPainterPath() @@ -593,14 +612,19 @@ def __init__(self, parent: QWidget | None = None) -> None: self._button_rect_color = self.style_data.button_color self._text_color = self.style_data.text_color - self.toggle_btn_color_ani = SiExpAnimationRefactor(self, self.Property.buttonRectColor) + self.toggle_btn_color_ani = SiExpAnimationRefactor(self, self.Property.ButtonRectColor) self.toggle_btn_color_ani.init(1/8, 0.01, self._button_rect_color, self._button_rect_color) - self.toggle_text_color_ani = SiExpAnimationRefactor(self, self.Property.textColor) + self.toggle_text_color_ani = SiExpAnimationRefactor(self, self.Property.TextColor) self.toggle_text_color_ani.init(1/8, 0.01, self._text_color, self._text_color) self.toggled.connect(self._onButtonToggled) + def reloadStyleData(self) -> None: + self.setFont(self.style_data.font) + self._onButtonToggled(self.isChecked()) + self.update() + def setToggledButtonColor(self, color: QColor | str) -> None: self.style_data.toggled_button_color = QColor(color) self.update() From 8483db58630d29cf7f01a118bdbc15ae9f44dd72 Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Wed, 6 Nov 2024 16:01:15 +0800 Subject: [PATCH 17/20] refactor: impl `GlobalStyleManager` to distribute style data --- .../components/page_widgets/page_widgets.py | 1 - siui/components/button.py | 224 +++++++++++------- 2 files changed, 139 insertions(+), 86 deletions(-) diff --git a/examples/Gallery for siui/components/page_widgets/page_widgets.py b/examples/Gallery for siui/components/page_widgets/page_widgets.py index 90c2d49..e0c30e4 100644 --- a/examples/Gallery for siui/components/page_widgets/page_widgets.py +++ b/examples/Gallery for siui/components/page_widgets/page_widgets.py @@ -284,7 +284,6 @@ def __init__(self, *args, **kwargs): self.refactor_flat_button = SiFlatButton(self) self.refactor_flat_button.setText("Flat Button") self.refactor_flat_button.setSvgIcon(SiGlobal.siui.iconpack.get("ic_fluent_wrench_settings_filled")) - self.refactor_flat_button.setButtonColor("#00FFFFFF") self.refactor_flat_button.adjustSize() self.refactor_toggle_button = SiToggleButtonRefactor(self) diff --git a/siui/components/button.py b/siui/components/button.py index 5db6197..8128b64 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -2,73 +2,163 @@ # replace button once it's done. Now it's draft, code may be ugly and verbose temporarily. from __future__ import annotations -from dataclasses import dataclass +import dataclasses +from dataclasses import dataclass, fields from PyQt5.QtCore import QEvent, QObject, QRect, QRectF, QSize, Qt, QTimer, pyqtProperty, pyqtSignal -from PyQt5.QtGui import QColor, QFontMetrics, QIcon, QLinearGradient, QPainter, QPainterPath, QPaintEvent, QPixmap +from PyQt5.QtGui import ( + QColor, + QFont, + QFontMetrics, + QIcon, + QLinearGradient, + QPainter, + QPainterPath, + QPaintEvent, + QPixmap, +) from PyQt5.QtSvg import QSvgRenderer from PyQt5.QtWidgets import QPushButton, QWidget -from siui.core import GlobalFont, SiColor, SiGlobal +from siui.core import GlobalFont, SiGlobal from siui.core.animation import SiExpAnimationRefactor from siui.gui import SiFont +class SiStyleAttr: + Font = "font" + TextColor = "text_color" + IdleColor = "idle_color" + HoverColor = "hover_color" + ClickColor = "click_color" + ButtonColor = "button_color" + ProgressColor = "progress_color" + CompleteColor = "complete_color" + BackgroundColor = "background_color" + ToggledTextColor = "toggled_text_color" + ToggledButtonColor = "toggled_button_color" + BorderInnerRadius = "border_inner_radius" + BorderRadius = "border_radius" + BorderHeight = "border_height" + IconTextGap = "icon_text_gap" + + +SA = SiStyleAttr + + +class GlobalStyleManager: + class Theme: + Bright = 0 + Dark = 1 + + style_dark = { + "Button": { + "default": { + SA.Font: SiFont.tokenized(GlobalFont.S_NORMAL), + SA.TextColor: QColor("#DFDFDF"), + SA.IdleColor: QColor("#00baadc7"), + SA.HoverColor: QColor("#1abaadc7"), + SA.ClickColor: QColor("#50baadc7"), + SA.ButtonColor: QColor("#4C4554"), + SA.ProgressColor: QColor("#806799"), + SA.CompleteColor: QColor("#519868"), + SA.BackgroundColor: QColor("#2d2932"), + SA.ToggledTextColor: QColor("#DFDFDF"), + SA.ToggledButtonColor: QColor("#519868"), + SA.BorderInnerRadius: 4, + SA.BorderRadius: 7, + SA.BorderHeight: 3, + SA.IconTextGap: 4, + }, + "FlatButtonStyleData": { + SA.ButtonColor: QColor("#004C4554") + }, + "LongPressButtonStyleData": { + SA.ProgressColor: QColor("#DA3462"), + SA.ButtonColor: QColor("#932a48"), + SA.BackgroundColor: QColor("#642d41"), + SA.ClickColor: QColor("#40FFFFFF"), + }, + "ToggleButtonStyleData": {}, + "PushButtonStyleData": {}, + "ProgressPushButtonStyleData": {}, + } + } + + def updateWidgetStyleData(self, widget: QObject) -> None: + try: + instance = widget.style_data + except NameError: + return + self.updateStyleData(instance) + + def updateStyleData(self, instance: QObject) -> None: + style_dict = self.style_dark + instance_class_name = instance.__class__.__name__ + for type_name, type_dict in zip(style_dict.keys(), style_dict.values()): + if type_name not in instance.STYLE_TYPES: + continue + + for class_name, class_dict in zip(type_dict.keys(), type_dict.values()): + if instance_class_name == class_name or class_name == "default": + self._setAttribute(instance, class_dict) + + @staticmethod + def _setAttribute(instance, class_dict: dict) -> None: + for attr_name, value in zip(class_dict.keys(), class_dict.values()): + setattr(instance, attr_name, value) + + @dataclass class ButtonStyleData(QObject): - font = SiFont.tokenized(GlobalFont.S_NORMAL) - - text_color = QColor("#DFDFDF") - idle_color = QColor("#00baadc7") - hover_color = QColor("#1abaadc7") - click_color = QColor("#50baadc7") - button_color = QColor("#4C4554") - progress_color = QColor("#806799") - complete_color = QColor("#519868") - background_color = QColor("#2d2932") - toggled_text_color = QColor("#DFDFDF") - toggled_button_color = QColor("#519868") - - border_inner_radius: int = 4 - border_radius: int = 7 - border_height: int = 3 - icon_text_gap: int = 4 - - def __init__(self, parent: QWidget, name: str): - super().__init__(parent) - self.setObjectName(name) + STYLE_TYPES = ["Button"] + font: QFont -@dataclass + text_color: QColor + idle_color: QColor + hover_color: QColor + click_color: QColor + button_color: QColor + progress_color: QColor + complete_color: QColor + background_color: QColor + toggled_text_color: QColor + toggled_button_color: QColor + + border_inner_radius: int + border_radius: int + border_height: int + icon_text_gap: int + + def __init__(self): + super().__init__() + GlobalStyleManager().updateStyleData(self) + + +@dataclass(init=False) class FlatButtonStyleData(ButtonStyleData): pass -@dataclass +@dataclass(init=False) class PushButtonStyleData(ButtonStyleData): - background_color = QColor("#2d2932") - border_inner_radius: int = 4 - border_height: int = 3 + pass -@dataclass +@dataclass(init=False) class ProgressPushButtonStyleData(PushButtonStyleData): - progress_color = QColor("#806799") - complete_color = QColor("#519868") + pass -@dataclass +@dataclass(init=False) class LongPressButtonStyleData(PushButtonStyleData): - progress_color = QColor("#DA3462") - button_color = QColor("#932a48") - background_color = QColor("#642d41") - click_color = QColor("#40FFFFFF") + pass -@dataclass +@dataclass(init=False) class ToggleButtonStyleData(ButtonStyleData): - toggled_text_color = QColor("#DFDFDF") - toggled_button_color = QColor("#519868") + pass class ABCButton(QPushButton): @@ -92,11 +182,9 @@ def __init__(self, parent: QWidget | None = None) -> None: self._text_color = None self.highlight_ani = SiExpAnimationRefactor(self, self.Property.HighlightRectColor) - self.highlight_ani.init(1/8, 0.2, self._highlight_rect_color, self._highlight_rect_color) + self.highlight_ani.init(1 / 8, 0.2, self._highlight_rect_color, self._highlight_rect_color) self.clicked.connect(self._onButtonClicked) - self.clicked.connect(lambda: print("Ni好")) - self.clicked.disconnect() @pyqtProperty(QColor) def highlightRectColor(self): @@ -167,14 +255,6 @@ def setIconTextGap(self, gap: int) -> None: self.style_data.icon_text_gap = gap self.update() - def setBorderRadius(self, r: int) -> None: - self.style_data.border_radius = r - self.update() - - def setButtonColor(self, color: QColor | str) -> None: - self.style_data.button_color = QColor(color) - self.update() - def setSvgIcon(self, svg_data: bytes) -> None: pixmap = QPixmap(64, 64) pixmap.fill(Qt.transparent) @@ -278,18 +358,6 @@ def reloadStyleData(self) -> None: self.setFont(self.style_data.font) self.update() - def setBackgroundColor(self, code: str) -> None: - self.style_data.background_color = SiColor.toArray(code, "rgba") - self.update() - - def setBorderInnerRadius(self, r: int) -> None: - self.style_data.border_inner_radius = r - self.update() - - def setBorderHeight(self, h: int) -> None: - self.style_data.border_height = h - self.update() - def _drawBackgroundPath(self, rect: QRect) -> QPainterPath: radius = self.style_data.border_radius path = QPainterPath() @@ -381,16 +449,15 @@ def __init__(self, parent: QWidget | None = None) -> None: self._progress = 0 self.progress_ani = SiExpAnimationRefactor(self, self.Property.Progress) - self.progress_ani.init(1/6, 0.005, 0, 0) + self.progress_ani.init(1 / 6, 0.005, 0, 0) self.progress_color_ani = SiExpAnimationRefactor(self, self.Property.ProgressRectColor) - self.progress_color_ani.init(1/8, 0.01, self._progress_rect_color, self._progress_rect_color) + self.progress_color_ani.init(1 / 8, 0.01, self._progress_rect_color, self._progress_rect_color) def setProgress(self, p: float, ani: bool = True) -> None: self._progress = max(0.0, min(p, 1.0)) self._updateProgress(ani) self._updateCompleteState() - # self.update() def reloadStyleData(self) -> None: self.setFont(self.style_data.font) @@ -414,16 +481,11 @@ def _updateCompleteState(self) -> None: self.progress_color_ani.setEndValue(self.style_data.progress_color) self.progress_color_ani.start() - def setProgressColor(self, color: QColor | str) -> None: - self.style_data.progress_color = QColor(color) - self._updateCompleteState() - self.update() - def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: p = min(self._progress, 1) # prevent progress exceeding caused by using animation. gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) gradient.setColorAt(p - 0.0001, self._progress_rect_color) # use property variable - gradient.setColorAt(p, self.style_data.button_color) + gradient.setColorAt(p, self.style_data.button_color) painter.setBrush(gradient) painter.drawPath(self._drawButtonPath(rect)) @@ -438,7 +500,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self._progress = 0 self.progress_ani = SiExpAnimationRefactor(self, self.Property.Progress) - self.progress_ani.init(-1/16, 0.12, 0, 0) + self.progress_ani.init(-1 / 16, 0.12, 0, 0) # self.progress_ani.valueChanged.connect(print) self.go_backwards_timer = QTimer(self) @@ -447,7 +509,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self.go_backwards_timer.timeout.connect(self._goBackwards) self.mouse_pressed_timer = QTimer(self) - self.mouse_pressed_timer.setInterval(1000//60) + self.mouse_pressed_timer.setInterval(1000 // 60) self.mouse_pressed_timer.timeout.connect(self._onMousePressed) def setProgress(self, p: float, ani: bool = True) -> None: @@ -497,7 +559,7 @@ def _drawButtonRect(self, painter: QPainter, rect: QRect) -> None: p = min(self._progress, 1) # prevent progress exceeding caused by using animation. gradient = QLinearGradient(rect.left(), rect.top(), rect.right(), rect.top()) gradient.setColorAt(p - 0.0001, self.style_data.progress_color) - gradient.setColorAt(p, self.style_data.button_color) + gradient.setColorAt(p, self.style_data.button_color) painter.setBrush(gradient) painter.drawPath(self._drawButtonPath(rect)) @@ -613,10 +675,10 @@ def __init__(self, parent: QWidget | None = None) -> None: self._text_color = self.style_data.text_color self.toggle_btn_color_ani = SiExpAnimationRefactor(self, self.Property.ButtonRectColor) - self.toggle_btn_color_ani.init(1/8, 0.01, self._button_rect_color, self._button_rect_color) + self.toggle_btn_color_ani.init(1 / 8, 0.01, self._button_rect_color, self._button_rect_color) self.toggle_text_color_ani = SiExpAnimationRefactor(self, self.Property.TextColor) - self.toggle_text_color_ani.init(1/8, 0.01, self._text_color, self._text_color) + self.toggle_text_color_ani.init(1 / 8, 0.01, self._text_color, self._text_color) self.toggled.connect(self._onButtonToggled) @@ -625,14 +687,6 @@ def reloadStyleData(self) -> None: self._onButtonToggled(self.isChecked()) self.update() - def setToggledButtonColor(self, color: QColor | str) -> None: - self.style_data.toggled_button_color = QColor(color) - self.update() - - def setToggledTextColor(self, color: QColor | str) -> None: - self.style_data.toggled_text_color = QColor(color) - self.update() - def _onButtonToggled(self, state: bool) -> None: if state: self.toggle_btn_color_ani.setEndValue(self.style_data.toggled_button_color) From 94533bf02861480521422581e1d1754a4937fd81 Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Sat, 16 Nov 2024 19:33:39 +0800 Subject: [PATCH 18/20] feat: impl button scaling ani --- siui/components/button.py | 150 ++++++++++++++++++++++++++++++-------- siui/core/animation.py | 24 +++++- 2 files changed, 140 insertions(+), 34 deletions(-) diff --git a/siui/components/button.py b/siui/components/button.py index 8128b64..4477c44 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -2,10 +2,20 @@ # replace button once it's done. Now it's draft, code may be ugly and verbose temporarily. from __future__ import annotations -import dataclasses -from dataclasses import dataclass, fields - -from PyQt5.QtCore import QEvent, QObject, QRect, QRectF, QSize, Qt, QTimer, pyqtProperty, pyqtSignal +from dataclasses import dataclass + +from PyQt5.QtCore import ( + QEvent, + QObject, + QPointF, + QRect, + QRectF, + QSize, + Qt, + QTimer, + pyqtProperty, + pyqtSignal, +) from PyQt5.QtGui import ( QColor, QFont, @@ -70,16 +80,18 @@ class Theme: SA.BorderHeight: 3, SA.IconTextGap: 4, }, - "FlatButtonStyleData": { - SA.ButtonColor: QColor("#004C4554") - }, "LongPressButtonStyleData": { SA.ProgressColor: QColor("#DA3462"), SA.ButtonColor: QColor("#932a48"), SA.BackgroundColor: QColor("#642d41"), SA.ClickColor: QColor("#40FFFFFF"), }, - "ToggleButtonStyleData": {}, + "FlatButtonStyleData": { + SA.ButtonColor: QColor("#004C4554") + }, + "ToggleButtonStyleData": { + SA.ButtonColor: QColor("#004C4554"), + }, "PushButtonStyleData": {}, "ProgressPushButtonStyleData": {}, } @@ -163,6 +175,7 @@ class ToggleButtonStyleData(ButtonStyleData): class ABCButton(QPushButton): class Property: + ScaleFactor = "scaleFactor" TextColor = "textColor" ButtonRectColor = "buttonRectColor" ProgressRectColor = "progressRectColor" @@ -175,6 +188,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self.style_data = None self._progress = 0 + self._scale_factor = 1 self._highlight_rect_color = QColor("#00FFFFFF") self._progress_rect_color = None self._background_rect_color = None @@ -186,6 +200,15 @@ def __init__(self, parent: QWidget | None = None) -> None: self.clicked.connect(self._onButtonClicked) + @pyqtProperty(float) + def scaleFactor(self): + return self._scale_factor + + @scaleFactor.setter + def scaleFactor(self, value: float): + self._scale_factor = value + self.update() + @pyqtProperty(QColor) def highlightRectColor(self): return self._highlight_rect_color @@ -299,9 +322,6 @@ def _updateToolTip(self) -> None: if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: tool_tip_window.setText(self.toolTip()) - def _onAnimationTicked(self, _) -> None: - raise NotImplementedError() - def _onButtonClicked(self) -> None: raise NotImplementedError() @@ -326,11 +346,29 @@ def __init__(self, parent: QWidget | None = None) -> None: self.style_data = PushButtonStyleData() self._initStyle() + self._scale_factor = 1 + + self.scale_factor_ani = SiExpAnimationRefactor(self, self.Property.ScaleFactor) + self.scale_factor_ani.init(1/16, 0, 1, 1) def _initStyle(self): self.setFont(self.style_data.font) self.setIconSize(QSize(20, 20)) + def mousePressEvent(self, e): + super().mousePressEvent(e) + self.scale_factor_ani.setFactor(1/16) + self.scale_factor_ani.setBias(0) + self.scale_factor_ani.setEndValue(0.9) + self.scale_factor_ani.start() + + def mouseReleaseEvent(self, e): + super().mouseReleaseEvent(e) + self.scale_factor_ani.setFactor(1/4) + self.scale_factor_ani.setBias(0.001) + self.scale_factor_ani.setEndValue(1) + self.scale_factor_ani.start() + @classmethod def withText(cls, text: str, parent: QWidget | None = None): obj = cls(parent) @@ -425,19 +463,35 @@ def leaveEvent(self, event) -> None: self._hideToolTip() def paintEvent(self, event: QPaintEvent) -> None: + rect = self.rect() + text_rect, icon_rect = self.textRectAndIconRect() + device_pixel_ratio = self.devicePixelRatioF() + + buffer = QPixmap(rect.size() * device_pixel_ratio) + buffer.setDevicePixelRatio(device_pixel_ratio) + buffer.fill(Qt.transparent) + + buffer_painter = QPainter(buffer) + buffer_painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + buffer_painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + buffer_painter.setRenderHint(QPainter.RenderHint.Antialiasing) + buffer_painter.setPen(Qt.PenStyle.NoPen) + + self._drawBackgroundRect(buffer_painter, rect) + self._drawButtonRect(buffer_painter, rect) + self._drawHighLightRect(buffer_painter, rect) + self._drawPixmapRect(buffer_painter, icon_rect) + self._drawTextRect(buffer_painter, text_rect) + buffer_painter.end() + + a = self._scale_factor painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) - painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) painter.setRenderHint(QPainter.RenderHint.Antialiasing) - painter.setPen(Qt.PenStyle.NoPen) + painter.translate(QPointF(rect.width() * (1-a) / 2, rect.height() * (1-a) / 2)) + painter.scale(a, a) - rect = self.rect() - text_rect, icon_rect = self.textRectAndIconRect() - self._drawBackgroundRect(painter, rect) - self._drawButtonRect(painter, rect) - self._drawHighLightRect(painter, rect) - self._drawTextRect(painter, text_rect) - self._drawPixmapRect(painter, icon_rect) + painter.drawPixmap(0, 0, buffer) class SiProgressPushButton(SiPushButtonRefactor): @@ -501,7 +555,6 @@ def __init__(self, parent: QWidget | None = None) -> None: self.progress_ani = SiExpAnimationRefactor(self, self.Property.Progress) self.progress_ani.init(-1 / 16, 0.12, 0, 0) - # self.progress_ani.valueChanged.connect(print) self.go_backwards_timer = QTimer(self) self.go_backwards_timer.setSingleShot(True) @@ -581,6 +634,10 @@ def __init__(self, parent: QWidget | None = None) -> None: self.style_data = FlatButtonStyleData() self._initStyle() + self._scale_factor = 1 + + self.scale_factor_ani = SiExpAnimationRefactor(self, self.Property.ScaleFactor) + self.scale_factor_ani.init(1/16, 0, 1, 1) def _initStyle(self): self.setFont(self.style_data.font) @@ -630,26 +687,39 @@ def textRectAndIconRect(self) -> (QRectF, QRect): return text_rect, pixmap_rect - def _onAnimationTicked(self, _) -> None: - self.update() - def _onButtonClicked(self) -> None: self.highlight_ani.setCurrentValue(self.style_data.click_color) self.highlight_ani.start() def paintEvent(self, event: QPaintEvent) -> None: + rect = self.rect() + text_rect, icon_rect = self.textRectAndIconRect() + device_pixel_ratio = self.devicePixelRatioF() + + buffer = QPixmap(rect.size() * device_pixel_ratio) + buffer.setDevicePixelRatio(device_pixel_ratio) + buffer.fill(Qt.transparent) + + buffer_painter = QPainter(buffer) + buffer_painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + buffer_painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + buffer_painter.setRenderHint(QPainter.RenderHint.Antialiasing) + buffer_painter.setPen(Qt.PenStyle.NoPen) + + self._drawButtonRect(buffer_painter, rect) + self._drawHighLightRect(buffer_painter, rect) + self._drawPixmapRect(buffer_painter, icon_rect) + self._drawTextRect(buffer_painter, text_rect) + buffer_painter.end() + + a = self._scale_factor painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) - painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) painter.setRenderHint(QPainter.RenderHint.Antialiasing) - painter.setPen(Qt.PenStyle.NoPen) + painter.translate(QPointF(rect.width() * (1-a) / 2, rect.height() * (1-a) / 2)) + painter.scale(a, a) - rect = self.rect() - text_rect, icon_rect = self.textRectAndIconRect() - self._drawButtonRect(painter, rect) - self._drawHighLightRect(painter, rect) - self._drawTextRect(painter, text_rect) - self._drawPixmapRect(painter, icon_rect) + painter.drawPixmap(0, 0, buffer) def enterEvent(self, event) -> None: super().enterEvent(event) @@ -664,6 +734,22 @@ def leaveEvent(self, event) -> None: self.highlight_ani.start() self._hideToolTip() + def mousePressEvent(self, e): + super().mousePressEvent(e) + self.scale_factor_ani.setFactor(1/16) + self.scale_factor_ani.setBias(0) + self.scale_factor_ani.setEndValue(0.9) + self.scale_factor_ani.start() + + def mouseReleaseEvent(self, e): + super().mouseReleaseEvent(e) + self.scale_factor_ani.setFactor(1/4) + self.scale_factor_ani.setBias(0.001) + self.scale_factor_ani.setEndValue(1) + self.scale_factor_ani.start() + + + class SiToggleButtonRefactor(SiFlatButton): def __init__(self, parent: QWidget | None = None) -> None: diff --git a/siui/core/animation.py b/siui/core/animation.py index 593f6fd..81a7624 100644 --- a/siui/core/animation.py +++ b/siui/core/animation.py @@ -1,7 +1,18 @@ from typing import Any import numpy -from PyQt5.QtCore import QAbstractAnimation, QObject, QPoint, QPointF, QRect, QRectF, QSize, QSizeF, QTimer, pyqtSignal +from PyQt5.QtCore import ( + QAbstractAnimation, + QObject, + QPoint, + QPointF, + QRect, + QRectF, + QSize, + QSizeF, + QTimer, + pyqtSignal, +) from PyQt5.QtGui import QColor global_fps = 60 @@ -439,6 +450,12 @@ def init(self, factor: float, bias: float, current_value, end_value) -> None: self.setCurrentValue(current_value) self.setEndValue(end_value) + def setFactor(self, factor: float): + self.factor = factor + + def setBias(self, bias: float): + self.bias = bias + def target(self) -> QObject: return self._target @@ -510,8 +527,11 @@ def updateCurrentTime(self, _) -> None: step = step * (1 - flag) + distance * flag # 差距小于偏置的项,返回差距 self._current_value = self._current_value + step - self._target.setProperty(self._property_name, self._out_func(self._current_value)) self.valueChanged.emit(self._current_value) + try: + self._target.setProperty(self._property_name, self._out_func(self._current_value)) + except RuntimeError: + pass def _loadConversionFuncs(self) -> None: if self._property_type.__name__ in TypeConversionFuncs.functions.keys(): From d31aaa060d368f0e04d473bfda00cca943bfbee0 Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Sat, 16 Nov 2024 19:37:18 +0800 Subject: [PATCH 19/20] chore: opt coding style --- siui/components/button.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/siui/components/button.py b/siui/components/button.py index 4477c44..1121d6f 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -263,9 +263,15 @@ def progressRectColor(self, value: QColor): self._progress_rect_color = value self.update() + def styleData(self) -> PushButtonStyleData: + return self.style_data + def reloadStyleData(self): raise NotImplementedError() + def _onButtonClicked(self) -> None: + raise NotImplementedError() + def flash(self) -> None: self.highlight_ani.setCurrentValue(self.style_data.click_color) self.highlight_ani.start() @@ -301,10 +307,6 @@ def sizeHint(self) -> QSize: preferred_height = max(32, text_height, icon_height) return QSize(preferred_width, preferred_height) - @property - def styleData(self) -> PushButtonStyleData: - return self.style_data - def _showToolTip(self) -> None: tool_tip_window = SiGlobal.siui.windows.get("TOOL_TIP") if tool_tip_window is not None and self.toolTip() != "": @@ -322,9 +324,6 @@ def _updateToolTip(self) -> None: if tool_tip_window is not None and tool_tip_window.nowInsideOf() == self: tool_tip_window.setText(self.toolTip()) - def _onButtonClicked(self) -> None: - raise NotImplementedError() - def event(self, event): if event.type() == QEvent.ToolTip: return True # 忽略工具提示事件 @@ -393,7 +392,6 @@ def bottomBorderHeight(self) -> int: return self.style_data.border_height def reloadStyleData(self) -> None: - self.setFont(self.style_data.font) self.update() def _drawBackgroundPath(self, rect: QRect) -> QPainterPath: @@ -514,7 +512,6 @@ def setProgress(self, p: float, ani: bool = True) -> None: self._updateCompleteState() def reloadStyleData(self) -> None: - self.setFont(self.style_data.font) self._updateCompleteState() self.update() @@ -572,7 +569,6 @@ def setProgress(self, p: float, ani: bool = True) -> None: self.update() def reloadStyleData(self) -> None: - self.setFont(self.style_data.font) self.update() def _stepLength(self) -> float: @@ -644,7 +640,6 @@ def _initStyle(self): self.setIconSize(QSize(20, 20)) def reloadStyleData(self) -> None: - self.setFont(self.style_data.font) self.update() def _drawButtonPath(self, rect: QRect) -> QPainterPath: @@ -769,7 +764,6 @@ def __init__(self, parent: QWidget | None = None) -> None: self.toggled.connect(self._onButtonToggled) def reloadStyleData(self) -> None: - self.setFont(self.style_data.font) self._onButtonToggled(self.isChecked()) self.update() From 9eb9639bdbe092eee440efe1c5d7a4c851edbbc8 Mon Sep 17 00:00:00 2001 From: ChinaIceF <1489423523@qq.com> Date: Sat, 16 Nov 2024 23:56:18 +0800 Subject: [PATCH 20/20] refactor: SiSwitch --- .../components/page_widgets/page_widgets.py | 5 +- siui/components/button.py | 157 +++++++++++++++++- 2 files changed, 158 insertions(+), 4 deletions(-) diff --git a/examples/Gallery for siui/components/page_widgets/page_widgets.py b/examples/Gallery for siui/components/page_widgets/page_widgets.py index e0c30e4..90b2cc8 100644 --- a/examples/Gallery for siui/components/page_widgets/page_widgets.py +++ b/examples/Gallery for siui/components/page_widgets/page_widgets.py @@ -19,7 +19,7 @@ SiLongPressButtonRefactor, SiProgressPushButton, SiPushButtonRefactor, - SiToggleButtonRefactor, + SiToggleButtonRefactor, SiSwitchRefactor, ) from siui.components.combobox import SiComboBox from siui.components.menu import SiMenu @@ -291,11 +291,14 @@ def __init__(self, *args, **kwargs): self.refactor_toggle_button.setSvgIcon(SiGlobal.siui.iconpack.get("ic_fluent_save_filled")) self.refactor_toggle_button.adjustSize() + self.refactor_switch = SiSwitchRefactor(self) + self.refactor_buttons.body().addWidget(self.refactor_pushbutton) self.refactor_buttons.body().addWidget(self.refactor_progress_button) self.refactor_buttons.body().addWidget(self.refactor_long_press_button) self.refactor_buttons.body().addWidget(self.refactor_flat_button) self.refactor_buttons.body().addWidget(self.refactor_toggle_button) + self.refactor_buttons.body().addWidget(self.refactor_switch) self.refactor_buttons.body().addPlaceholder(12) self.refactor_buttons.adjustSize() diff --git a/siui/components/button.py b/siui/components/button.py index 1121d6f..74f8fb3 100644 --- a/siui/components/button.py +++ b/siui/components/button.py @@ -25,7 +25,7 @@ QPainter, QPainterPath, QPaintEvent, - QPixmap, + QPixmap, QPen, ) from PyQt5.QtSvg import QSvgRenderer from PyQt5.QtWidgets import QPushButton, QWidget @@ -744,8 +744,6 @@ def mouseReleaseEvent(self, e): self.scale_factor_ani.start() - - class SiToggleButtonRefactor(SiFlatButton): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -787,3 +785,156 @@ def _drawTextRect(self, painter: QPainter, rect: QRect) -> None: painter.setPen(self._text_color) # use property variable painter.setFont(self.font()) painter.drawText(rect, Qt.AlignCenter, self.text()) + + +@dataclass +class SwitchStyleData(QObject): + STYLE_TYPES = ["Switch"] + + background_color_starting: QColor = QColor("#52389a") + background_color_ending: QColor = QColor("#9c4e8b") + frame_color: QColor = QColor("#D2D2D2") + thumb_color_checked: QColor = QColor("#100912") + thumb_color_unchecked: QColor = QColor("#D2D2D2") + + +class SiSwitchRefactor(QPushButton): + class Property: + Progress = "progress" + ScaleFactor = "scaleFactor" + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setCheckable(True) + + self.style_data = SwitchStyleData() + self._progress = 0 + self._scale_factor = 1 + + self._initStyle() + + self.scale_factor_ani = SiExpAnimationRefactor(self, self.Property.ScaleFactor) + self.scale_factor_ani.init(1/16, 0, 1, 1) + + self.progress_ani = SiExpAnimationRefactor(self, self.Property.Progress) + self.progress_ani.init(1/4, 0.01, 0, 0) + + self.clicked.connect(self._onClicked) + + def _initStyle(self) -> None: + self.setFixedSize(40, 20) + + @pyqtProperty(float) + def scaleFactor(self): + return self._scale_factor + + @scaleFactor.setter + def scaleFactor(self, value: float): + self._scale_factor = value + self.update() + + @pyqtProperty(float) + def progress(self) -> float: + return self._progress + + @progress.setter + def progress(self, value: float) -> None: + self._progress = value + self.update() + + def _onClicked(self): + if self.isChecked(): + self.progress_ani.setEndValue(1) + self.progress_ani.start() + else: + self.progress_ani.setEndValue(0) + self.progress_ani.start() + + def _drawBackgroundPath(self, rect: QRect) -> QPainterPath: + radius = rect.height() / 2 + path = QPainterPath() + path.addRoundedRect(QRectF(0, 0, rect.width(), rect.height()), radius, radius) + return path + + def _drawFramePath(self, rect: QRect) -> QPainterPath: + width = 0.5 + radius = rect.height() / 2 + path = QPainterPath() + path.addRoundedRect(QRectF(width, width, rect.width() - 2 * width, rect.height() - 2 * width), radius, radius) + return path + + def _drawThumbPath(self, rect: QRect) -> QPainterPath: + p = self._progress + radius = rect.height() / 2 - 3 + width = radius * 2 + (p * (1-p)) ** 1 * 16 + height = radius * 2 + track_length = rect.width() - width - 6 + x = 3 + track_length * p + y = 3 + path = QPainterPath() + path.addRoundedRect(QRectF(x, y, width, height), radius, radius) + return path + + def _drawBackgroundRect(self, painter: QPainter, rect: QRect) -> None: + if self._progress > 0.5: + gradient = QLinearGradient(0, 0, rect.width(), rect.height()) + gradient.setColorAt(0, self.style_data.background_color_starting) + gradient.setColorAt(1, self.style_data.background_color_ending) + + painter.setBrush(gradient) + painter.drawPath(self._drawBackgroundPath(rect)) + + def _drawFrameRect(self, painter: QPainter, rect: QRect) -> None: + if self._progress <= 0.5: + pen = QPen(self.style_data.frame_color) + pen.setWidth(1) + painter.setPen(pen) + painter.drawPath(self._drawFramePath(rect)) + painter.setPen(Qt.NoPen) + + def _drawThumbRect(self, painter: QPainter, rect: QRect) -> None: + color = self.style_data.thumb_color_checked if self._progress > 0.5 else self.style_data.thumb_color_unchecked + painter.setBrush(color) + painter.drawPath(self._drawThumbPath(rect)) + + def paintEvent(self, a0) -> None: + rect = self.rect() + device_pixel_ratio = self.devicePixelRatioF() + + buffer = QPixmap(rect.size() * device_pixel_ratio) + buffer.setDevicePixelRatio(device_pixel_ratio) + buffer.fill(Qt.transparent) + + buffer_painter = QPainter(buffer) + buffer_painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + buffer_painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + buffer_painter.setRenderHint(QPainter.RenderHint.Antialiasing) + buffer_painter.setPen(Qt.PenStyle.NoPen) + + self._drawBackgroundRect(buffer_painter, rect) + self._drawFrameRect(buffer_painter, rect) + self._drawThumbRect(buffer_painter, rect) + buffer_painter.end() + + a = self._scale_factor + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.translate(QPointF(rect.width() * (1-a) / 2, rect.height() * (1-a) / 2)) + painter.scale(a, a) + + painter.drawPixmap(0, 0, buffer) + + def mousePressEvent(self, e): + super().mousePressEvent(e) + self.scale_factor_ani.setFactor(1/16) + self.scale_factor_ani.setBias(0) + self.scale_factor_ani.setEndValue(0.9) + self.scale_factor_ani.start() + + def mouseReleaseEvent(self, e): + super().mouseReleaseEvent(e) + self.scale_factor_ani.setFactor(1/4) + self.scale_factor_ani.setBias(0.001) + self.scale_factor_ani.setEndValue(1) + self.scale_factor_ani.start() \ No newline at end of file