diff --git a/qtapputils/qthelpers.py b/qtapputils/qthelpers.py index 2c89181..56615ab 100644 --- a/qtapputils/qthelpers.py +++ b/qtapputils/qthelpers.py @@ -10,7 +10,7 @@ """Qt utilities""" from __future__ import annotations -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, Any, Optional if TYPE_CHECKING: from qtpy.QtGui import QIcon from qtapputils.widgets.waitingspinner import WaitingSpinner @@ -23,11 +23,11 @@ from math import pi # ---- Third party imports -from qtpy.QtGui import QKeySequence +from qtpy.QtGui import QKeySequence, QColor, QPalette from qtpy.QtCore import QByteArray, Qt, QSize, QEventLoop, QTimer from qtpy.QtWidgets import ( QWidget, QSizePolicy, QToolButton, QApplication, QStyleFactory, QAction, - QToolBar) + QToolBar, QStyleOption) def qbytearray_to_hexstate(qba): @@ -40,6 +40,85 @@ def hexstate_to_qbytearray(hexstate): return QByteArray().fromHex(str(hexstate).encode('utf-8')) +def get_qcolor(color: QColor | tuple | list | str) -> QColor: + """ + Return a QColor from various input formats: + - QColor instance (returned as is) + - RGB or RGBA tuple/list of ints (e.g. (r,g,b) or (r,g,b,a)) + - RGB Hex string (e.g. '#FFAA00') or QPalette color role (e.g. 'window') + - Qt color name string (e.g. 'red', 'blue', 'lightgray') + + Parameters + ---------- + color : QColor, tuple/list, or str + The color specification. + + Returns + ------- + QColor + The corresponding QColor object. + + Raises + ------ + ValueError + If the color argument is not valid. + """ + # QColor instance + if isinstance(color, QColor): + return color + + # Tuple/list RGB(A) + if isinstance(color, (tuple, list)): + return QColor(*color) + + # String: hex code or color name + if isinstance(color, str): + # Accept hex (with '#') or color name + if color.startswith('#') or color in QColor.colorNames(): + return QColor(color) + + try: + return getattr(QStyleOption().palette, color)().color() + except AttributeError: + pass + + raise ValueError(f"Unknown color string: {color!r}") + + raise ValueError(f"Cannot convert argument to QColor: {color!r}") + + +def set_widget_palette( + widget: QWidget, + bgcolor: Optional[Any] = None, + fgcolor: Optional[Any] = None): + """ + Set the background and foreground color of a QWidget. + + If colors are None, keeps the current palette value. Also enables + autoFillBackground for proper background rendering on most widgets. + + Parameters + ---------- + widget : QWidget + The widget whose palette will be set. + bgcolor : optional + Background color specification (QColor, tuple/list, or str). + fgcolor : optional + Foreground color specification (QColor, tuple/list, or str). + """ + palette = widget.palette() + if bgcolor is not None: + palette.setColor(widget.backgroundRole(), get_qcolor(bgcolor)) + + # Ensure background fills for most widgets. + widget.setAutoFillBackground(True) + + if fgcolor is not None: + palette.setColor(widget.foregroundRole(), get_qcolor(fgcolor)) + + widget.setPalette(palette) + + def create_mainwindow_toolbar( title: str, iconsize: int = None, areas: int = Qt.TopToolBarArea, movable: bool = False, floatable: bool = False, diff --git a/qtapputils/tests/test_qthelpers.py b/qtapputils/tests/test_qthelpers.py index e0e9bd0..59a5675 100644 --- a/qtapputils/tests/test_qthelpers.py +++ b/qtapputils/tests/test_qthelpers.py @@ -14,11 +14,15 @@ from itertools import product # ---- Third party imports +from PyQt5.QtWidgets import QWidget from qtpy.QtCore import Qt, QTimer +from qtpy.QtGui import QColor import pytest # ---- Local imports -from qtapputils.qthelpers import format_tooltip, create_waitspinner, qtwait +from qtapputils.qthelpers import ( + format_tooltip, create_waitspinner, qtwait, get_qcolor, + set_widget_palette) # ============================================================================= @@ -140,5 +144,92 @@ def test_qtwait_timeout(qtbot): assert "Timeout reached!" in str(excinfo.value) +def test_get_qcolor(): + """ + Test that get_qcolor correctly creates a QColor from various + input formats. + """ + # Test from QColor. + color = QColor(10, 20, 30) + result = get_qcolor(color) + assert isinstance(result, QColor) + assert result == color + + # Test from RGB tuple. + result = get_qcolor((100, 150, 200)) + assert isinstance(result, QColor) + assert result.red() == 100 + assert result.green() == 150 + assert result.blue() == 200 + + # Test from RGBA list. + result = get_qcolor([10, 20, 30, 40]) + assert isinstance(result, QColor) + assert result.red() == 10 + assert result.green() == 20 + assert result.blue() == 30 + assert result.alpha() == 40 + + # Test from HEX string. + result = get_qcolor("#336699") + assert isinstance(result, QColor) + assert result.red() == 51 + assert result.green() == 102 + assert result.blue() == 153 + + # Test from color name. + result = get_qcolor("red") + assert isinstance(result, QColor) + assert result.red() == 255 + assert result.green() == 0 + assert result.blue() == 0 + + # Test from invalid string. + with pytest.raises(ValueError): + get_qcolor("notacolor") + + # Test from invalid type. + with pytest.raises(ValueError): + get_qcolor(12345) + + +def test_set_widget_palette(qtbot): + """ + Test set_widget_palette for background, foreground, both, and None cases. + """ + # Background only + widget = QWidget() + set_widget_palette(widget, bgcolor=(10, 20, 30)) + + bg_color = widget.palette().color(widget.backgroundRole()) + assert isinstance(bg_color, QColor) + assert (bg_color.red(), bg_color.green(), bg_color.blue()) == (10, 20, 30) + assert widget.autoFillBackground() is True + + # Foreground only + widget = QWidget() + set_widget_palette(widget, fgcolor="#FF00AA") + + fg_color = widget.palette().color(widget.foregroundRole()) + assert isinstance(fg_color, QColor) + assert (fg_color.red(), fg_color.green(), fg_color.blue()) == (255, 0, 170) + + # Both background and foreground + widget = QWidget() + set_widget_palette(widget, bgcolor="blue", fgcolor="yellow") + + bg_color = widget.palette().color(widget.backgroundRole()) + fg_color = widget.palette().color(widget.foregroundRole()) + assert (bg_color.red(), bg_color.green(), bg_color.blue()) == (0, 0, 255) + assert (fg_color.red(), fg_color.green(), fg_color.blue()) == (255, 255, 0) + + # None for both + widget = QWidget() + orig_palette = widget.palette() + set_widget_palette(widget, bgcolor=None, fgcolor=None) + + assert widget.palette() == orig_palette + + if __name__ == "__main__": pytest.main(['-x', __file__, '-v', '-rw'])