diff --git a/qtapputils/testing.py b/qtapputils/testing.py new file mode 100644 index 0000000..0c3b71a --- /dev/null +++ b/qtapputils/testing.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © QtAppUtils Project Contributors +# https://github.com/geo-stack/qtapputils +# +# This file is part of QtAppUtils. +# Licensed under the terms of the MIT License. +# ----------------------------------------------------------------------------- + +""" +Helper functions for testing. +""" + +from qtpy.QtGui import QIcon +from qtpy.QtCore import QSize + + +def icons_are_equal(icon1: QIcon, icon2: QIcon, size: QSize = QSize(16, 16)): + """ + Return True if two QIcon objects have identical image data at + the given size. + + Parameters + ---------- + icon1 : QIcon + The first icon to compare. + icon2 : QIcon + The second icon to compare. + size : QSize, optional + The size at which to compare the icons (default is 16x16). + + Returns + ------- + bool + True if the icons look the same at the specified size, False otherwise. + """ + pm1 = icon1.pixmap(size) + pm2 = icon2.pixmap(size) + return pm1.toImage() == pm2.toImage() diff --git a/qtapputils/widgets/buttons.py b/qtapputils/widgets/buttons.py new file mode 100644 index 0000000..4b65f7a --- /dev/null +++ b/qtapputils/widgets/buttons.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © QtAppUtils Project Contributors +# https://github.com/geo-stack/qtapputils +# +# This file is part of QtAppUtils. +# Licensed under the terms of the MIT License. +# ----------------------------------------------------------------------------- + + +# ---- Third party imports +from qtpy.QtCore import Signal +from qtpy.QtGui import QIcon +from qtpy.QtWidgets import QToolButton, QWidget + + +class MultiStateToolButton(QToolButton): + """ + A QToolButton that cycles through a list of icons each time it is clicked. + + Parameters + ---------- + icons : list of QIcon + The list of icons to cycle through. + parent : QWidget, optional + The parent widget. + index : int, optional + The index of the starting icon (default is 0). + + Signals + ------- + sig_index_changed : Signal(int) + Signal emitted with the new index whenever the current state changes. + """ + + sig_index_changed = Signal(int) + + def __init__( + self, icons: list[QIcon], + parent: QWidget = None, + index: int = 0 + ): + super().__init__(parent) + + self.setAutoRaise(True) + self.setCheckable(False) + + self._icons = icons + self._current_index = index + + self.clicked.connect(self._handle_clicked) + self._update_icon() + + def current_index(self): + return self._current_index + + def set_current_index(self, index: int): + if index >= len(self._icons): + index = 0 + elif index < 0: + index = len(self._icons) - 1 + + if index == self._current_index: + return + + self._current_index = index + self._update_icon() + self.sig_index_changed.emit(index) + + def _update_icon(self): + self.setIcon(self._icons[self._current_index]) + + def _handle_clicked(self): + self.set_current_index(self._current_index + 1) diff --git a/qtapputils/widgets/path.py b/qtapputils/widgets/path.py index f34cdce..248db80 100644 --- a/qtapputils/widgets/path.py +++ b/qtapputils/widgets/path.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # ----------------------------------------------------------------------------- # Copyright © QtAppUtils Project Contributors -# https://github.com/jnsebgosselin/qtapputils +# https://github.com/geo-stack/qtapputils # # This file is part of QtAppUtils. # Licensed under the terms of the MIT License. diff --git a/qtapputils/widgets/tests/test_buttons.py b/qtapputils/widgets/tests/test_buttons.py new file mode 100644 index 0000000..e7a7eda --- /dev/null +++ b/qtapputils/widgets/tests/test_buttons.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright © QtAppUtils Project Contributors +# https://github.com/geo-stack/qtapputils +# +# This file is part of QtAppUtils. +# Licensed under the terms of the MIT License. +# ----------------------------------------------------------------------------- + +import pytest +from qtpy.QtGui import QIcon, QPixmap +from qtpy.QtCore import Qt +from qtpy.QtTest import QSignalSpy + +from qtapputils.widgets.buttons import MultiStateToolButton +from qtapputils.testing import icons_are_equal + + +@pytest.fixture +def icons(): + # Create dummy QIcons for testing + pixmap1 = QPixmap(16, 16) + pixmap1.fill(Qt.red) + pixmap2 = QPixmap(16, 16) + pixmap2.fill(Qt.green) + pixmap3 = QPixmap(16, 16) + pixmap3.fill(Qt.blue) + return [QIcon(pixmap1), QIcon(pixmap2), QIcon(pixmap3)] + + +@pytest.fixture +def button(icons, qtbot): + + btn = MultiStateToolButton(icons) + qtbot.addWidget(btn) + btn.show() + qtbot.waitUntil(btn.isVisible) + + assert btn.current_index() == 0 + assert icons_are_equal(btn.icon(), icons[0]) + + return btn + + +def test_cycle_icons(button, qtbot, icons): + """Test icon cycles forward with clicks and wraps around.""" + signal_spy = QSignalSpy(button.sig_index_changed) + + qtbot.mouseClick(button, Qt.LeftButton) + + assert len(signal_spy) == 1 + assert signal_spy[-1] == [1] + assert icons_are_equal(button.icon(), icons[1]) + + qtbot.mouseClick(button, Qt.LeftButton) + + assert len(signal_spy) == 2 + assert signal_spy[-1] == [2] + assert icons_are_equal(button.icon(), icons[2]) + + # Should wrap back to 0. + qtbot.mouseClick(button, Qt.LeftButton) + + assert len(signal_spy) == 3 + assert signal_spy[-1] == [0] + assert icons_are_equal(button.icon(), icons[0]) + + +def test_set_index(button, qtbot, icons): + """Test icon is set as expected when index is set programattically.""" + signal_spy = QSignalSpy(button.sig_index_changed) + + # Should wrap back to len(icons) - 1. + button.set_current_index(-1) + + assert len(signal_spy) == 1 + assert signal_spy[-1] == [2] + assert icons_are_equal(button.icon(), icons[2]) + + # Should wrap back to 0. + button.set_current_index(100) + + assert len(signal_spy) == 2 + assert signal_spy[-1] == [0] + assert icons_are_equal(button.icon(), icons[0]) + + # Should do nothing. + button.set_current_index(100) + + assert len(signal_spy) == 2 + assert signal_spy[-1] == [0] + assert icons_are_equal(button.icon(), icons[0]) + + +if __name__ == '__main__': + pytest.main(['-x', __file__, '-vv', '-rw'])