Skip to content

Commit a57856f

Browse files
Merge pull request #47 from geo-stack/add_multistate_button
PR: Add multi-state toolbutton
2 parents fd01424 + 9a42d13 commit a57856f

4 files changed

Lines changed: 210 additions & 1 deletion

File tree

qtapputils/testing.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# -*- coding: utf-8 -*-
2+
# -----------------------------------------------------------------------------
3+
# Copyright © QtAppUtils Project Contributors
4+
# https://github.com/geo-stack/qtapputils
5+
#
6+
# This file is part of QtAppUtils.
7+
# Licensed under the terms of the MIT License.
8+
# -----------------------------------------------------------------------------
9+
10+
"""
11+
Helper functions for testing.
12+
"""
13+
14+
from qtpy.QtGui import QIcon
15+
from qtpy.QtCore import QSize
16+
17+
18+
def icons_are_equal(icon1: QIcon, icon2: QIcon, size: QSize = QSize(16, 16)):
19+
"""
20+
Return True if two QIcon objects have identical image data at
21+
the given size.
22+
23+
Parameters
24+
----------
25+
icon1 : QIcon
26+
The first icon to compare.
27+
icon2 : QIcon
28+
The second icon to compare.
29+
size : QSize, optional
30+
The size at which to compare the icons (default is 16x16).
31+
32+
Returns
33+
-------
34+
bool
35+
True if the icons look the same at the specified size, False otherwise.
36+
"""
37+
pm1 = icon1.pixmap(size)
38+
pm2 = icon2.pixmap(size)
39+
return pm1.toImage() == pm2.toImage()

qtapputils/widgets/buttons.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# -*- coding: utf-8 -*-
2+
# -----------------------------------------------------------------------------
3+
# Copyright © QtAppUtils Project Contributors
4+
# https://github.com/geo-stack/qtapputils
5+
#
6+
# This file is part of QtAppUtils.
7+
# Licensed under the terms of the MIT License.
8+
# -----------------------------------------------------------------------------
9+
10+
11+
# ---- Third party imports
12+
from qtpy.QtCore import Signal
13+
from qtpy.QtGui import QIcon
14+
from qtpy.QtWidgets import QToolButton, QWidget
15+
16+
17+
class MultiStateToolButton(QToolButton):
18+
"""
19+
A QToolButton that cycles through a list of icons each time it is clicked.
20+
21+
Parameters
22+
----------
23+
icons : list of QIcon
24+
The list of icons to cycle through.
25+
parent : QWidget, optional
26+
The parent widget.
27+
index : int, optional
28+
The index of the starting icon (default is 0).
29+
30+
Signals
31+
-------
32+
sig_index_changed : Signal(int)
33+
Signal emitted with the new index whenever the current state changes.
34+
"""
35+
36+
sig_index_changed = Signal(int)
37+
38+
def __init__(
39+
self, icons: list[QIcon],
40+
parent: QWidget = None,
41+
index: int = 0
42+
):
43+
super().__init__(parent)
44+
45+
self.setAutoRaise(True)
46+
self.setCheckable(False)
47+
48+
self._icons = icons
49+
self._current_index = index
50+
51+
self.clicked.connect(self._handle_clicked)
52+
self._update_icon()
53+
54+
def current_index(self):
55+
return self._current_index
56+
57+
def set_current_index(self, index: int):
58+
if index >= len(self._icons):
59+
index = 0
60+
elif index < 0:
61+
index = len(self._icons) - 1
62+
63+
if index == self._current_index:
64+
return
65+
66+
self._current_index = index
67+
self._update_icon()
68+
self.sig_index_changed.emit(index)
69+
70+
def _update_icon(self):
71+
self.setIcon(self._icons[self._current_index])
72+
73+
def _handle_clicked(self):
74+
self.set_current_index(self._current_index + 1)

qtapputils/widgets/path.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
# -----------------------------------------------------------------------------
33
# Copyright © QtAppUtils Project Contributors
4-
# https://github.com/jnsebgosselin/qtapputils
4+
# https://github.com/geo-stack/qtapputils
55
#
66
# This file is part of QtAppUtils.
77
# Licensed under the terms of the MIT License.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# -*- coding: utf-8 -*-
2+
# -----------------------------------------------------------------------------
3+
# Copyright © QtAppUtils Project Contributors
4+
# https://github.com/geo-stack/qtapputils
5+
#
6+
# This file is part of QtAppUtils.
7+
# Licensed under the terms of the MIT License.
8+
# -----------------------------------------------------------------------------
9+
10+
import pytest
11+
from qtpy.QtGui import QIcon, QPixmap
12+
from qtpy.QtCore import Qt
13+
from qtpy.QtTest import QSignalSpy
14+
15+
from qtapputils.widgets.buttons import MultiStateToolButton
16+
from qtapputils.testing import icons_are_equal
17+
18+
19+
@pytest.fixture
20+
def icons():
21+
# Create dummy QIcons for testing
22+
pixmap1 = QPixmap(16, 16)
23+
pixmap1.fill(Qt.red)
24+
pixmap2 = QPixmap(16, 16)
25+
pixmap2.fill(Qt.green)
26+
pixmap3 = QPixmap(16, 16)
27+
pixmap3.fill(Qt.blue)
28+
return [QIcon(pixmap1), QIcon(pixmap2), QIcon(pixmap3)]
29+
30+
31+
@pytest.fixture
32+
def button(icons, qtbot):
33+
34+
btn = MultiStateToolButton(icons)
35+
qtbot.addWidget(btn)
36+
btn.show()
37+
qtbot.waitUntil(btn.isVisible)
38+
39+
assert btn.current_index() == 0
40+
assert icons_are_equal(btn.icon(), icons[0])
41+
42+
return btn
43+
44+
45+
def test_cycle_icons(button, qtbot, icons):
46+
"""Test icon cycles forward with clicks and wraps around."""
47+
signal_spy = QSignalSpy(button.sig_index_changed)
48+
49+
qtbot.mouseClick(button, Qt.LeftButton)
50+
51+
assert len(signal_spy) == 1
52+
assert signal_spy[-1] == [1]
53+
assert icons_are_equal(button.icon(), icons[1])
54+
55+
qtbot.mouseClick(button, Qt.LeftButton)
56+
57+
assert len(signal_spy) == 2
58+
assert signal_spy[-1] == [2]
59+
assert icons_are_equal(button.icon(), icons[2])
60+
61+
# Should wrap back to 0.
62+
qtbot.mouseClick(button, Qt.LeftButton)
63+
64+
assert len(signal_spy) == 3
65+
assert signal_spy[-1] == [0]
66+
assert icons_are_equal(button.icon(), icons[0])
67+
68+
69+
def test_set_index(button, qtbot, icons):
70+
"""Test icon is set as expected when index is set programattically."""
71+
signal_spy = QSignalSpy(button.sig_index_changed)
72+
73+
# Should wrap back to len(icons) - 1.
74+
button.set_current_index(-1)
75+
76+
assert len(signal_spy) == 1
77+
assert signal_spy[-1] == [2]
78+
assert icons_are_equal(button.icon(), icons[2])
79+
80+
# Should wrap back to 0.
81+
button.set_current_index(100)
82+
83+
assert len(signal_spy) == 2
84+
assert signal_spy[-1] == [0]
85+
assert icons_are_equal(button.icon(), icons[0])
86+
87+
# Should do nothing.
88+
button.set_current_index(100)
89+
90+
assert len(signal_spy) == 2
91+
assert signal_spy[-1] == [0]
92+
assert icons_are_equal(button.icon(), icons[0])
93+
94+
95+
if __name__ == '__main__':
96+
pytest.main(['-x', __file__, '-vv', '-rw'])

0 commit comments

Comments
 (0)