Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 82 additions & 3 deletions qtapputils/qthelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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,
Expand Down
93 changes: 92 additions & 1 deletion qtapputils/tests/test_qthelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


# =============================================================================
Expand Down Expand Up @@ -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'])
Loading