Compare commits

...

3 Commits

Author SHA1 Message Date
Talley Lambert
c0c3a387bb chore: changelog v0.7.3 2025-03-28 15:18:47 -04:00
Hanjin Liu
5ce74b8198 feat: toggle switch (#284)
* implement toggle switch

* rename, inherit QCheckBox

* fix pyside6

* reimplement with QAbstractButton

* refactor methods

* fix sizeHint

* suggestions

* make sizes customizable

* parse as int

* Add doc to test function

Co-authored-by: Grzegorz Bokota <bokota+github@gmail.com>

---------

Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
Co-authored-by: Grzegorz Bokota <bokota+github@gmail.com>
2025-03-28 15:09:59 -04:00
Peter Sobolewski
0b2602b460 Enh: Adds a filterable kwarg to ColormapComboBox enabling filtering (like Catalog) (#278) 2025-03-17 19:16:44 -04:00
9 changed files with 673 additions and 14 deletions

View File

@@ -1,5 +1,14 @@
# Changelog
## [v0.7.3](https://github.com/pyapp-kit/superqt/tree/v0.7.3) (2025-03-28)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.7.2...v0.7.3)
**Implemented enhancements:**
- feat: toggle switch [\#284](https://github.com/pyapp-kit/superqt/pull/284) ([hanjinliu](https://github.com/hanjinliu))
- Enh: Adds a filterable kwarg to ColormapComboBox enabling filtering \(like Catalog\) [\#278](https://github.com/pyapp-kit/superqt/pull/278) ([psobolewskiPhD](https://github.com/psobolewskiPhD))
## [v0.7.2](https://github.com/pyapp-kit/superqt/tree/v0.7.2) (2025-03-17)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.7.1...v0.7.2)

67
examples/toggle_switch.py Normal file
View File

@@ -0,0 +1,67 @@
from qtpy import QtCore, QtGui
from qtpy.QtWidgets import QApplication, QStyle, QVBoxLayout, QWidget
from superqt import QToggleSwitch
from superqt.switch import QStyleOptionToggleSwitch
QSS_EXAMPLE = """
QToggleSwitch {
qproperty-onColor: red;
qproperty-handleSize: 12;
qproperty-switchWidth: 30;
qproperty-switchHeight: 16;
}
"""
class QRectangleToggleSwitch(QToggleSwitch):
"""A rectangle shaped toggle switch."""
def drawGroove(
self,
painter: QtGui.QPainter,
rect: QtCore.QRectF,
option: QStyleOptionToggleSwitch,
) -> None:
"""Draw the groove of the switch."""
painter.setPen(QtCore.Qt.PenStyle.NoPen)
is_checked = option.state & QStyle.StateFlag.State_On
painter.setBrush(option.on_color if is_checked else option.off_color)
painter.setOpacity(0.8)
painter.drawRect(rect)
def drawHandle(self, painter, rect, option):
"""Draw the handle of the switch."""
painter.drawRect(rect)
class QToggleSwitchWithText(QToggleSwitch):
"""A toggle switch with text on the handle."""
def drawHandle(
self,
painter: QtGui.QPainter,
rect: QtCore.QRectF,
option: QStyleOptionToggleSwitch,
) -> None:
super().drawHandle(painter, rect, option)
text = "ON" if option.state & QStyle.StateFlag.State_On else "OFF"
painter.setPen(QtGui.QPen(QtGui.QColor("black")))
font = painter.font()
font.setPointSize(5)
painter.setFont(font)
painter.drawText(rect, QtCore.Qt.AlignmentFlag.AlignCenter, text)
app = QApplication([])
widget = QWidget()
layout = QVBoxLayout(widget)
layout.addWidget(QToggleSwitch("original"))
switch_styled = QToggleSwitch("stylesheet")
switch_styled.setStyleSheet(QSS_EXAMPLE)
layout.addWidget(switch_styled)
layout.addWidget(QRectangleToggleSwitch("rectangle"))
layout.addWidget(QToggleSwitchWithText("with text"))
widget.show()
app.exec()

View File

@@ -22,6 +22,7 @@ from .sliders import (
QRangeSlider,
)
from .spinbox import QLargeIntSpinBox
from .switch import QToggleSwitch
from .utils import (
QFlowLayout,
QMessageHandler,
@@ -51,6 +52,7 @@ __all__ = [
"QSearchableComboBox",
"QSearchableListWidget",
"QSearchableTreeWidget",
"QToggleSwitch",
"ensure_main_thread",
"ensure_object_thread",
]

View File

@@ -3,11 +3,12 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from cmap import Colormap
from qtpy.QtCore import Qt, Signal
from qtpy.QtCore import QSortFilterProxyModel, QStringListModel, Qt, Signal
from qtpy.QtWidgets import (
QButtonGroup,
QCheckBox,
QComboBox,
QCompleter,
QDialog,
QDialogButtonBox,
QSizePolicy,
@@ -26,6 +27,7 @@ if TYPE_CHECKING:
from collections.abc import Sequence
from cmap._colormap import ColorStopsLike
from qtpy.QtGui import QKeyEvent
CMAP_ROLE = Qt.ItemDataRole.UserRole + 1
@@ -45,6 +47,9 @@ class QColormapComboBox(QComboBox):
add_colormap_text: str, optional
The text to display for the "Add Colormap..." item.
Default is "Add Colormap...".
filterable: bool, optional
Whether the user can filter colormaps by typing in the line edit.
Default is True. Can also be set with `setFilterable`.
"""
currentColormapChanged = Signal(Colormap)
@@ -55,18 +60,20 @@ class QColormapComboBox(QComboBox):
*,
allow_user_colormaps: bool = False,
add_colormap_text: str = "Add Colormap...",
filterable: bool = True,
) -> None:
# init QComboBox
super().__init__(parent)
self._add_color_text: str = add_colormap_text
self._allow_user_colors: bool = allow_user_colormaps
self._last_cmap: Colormap | None = None
self._filterable: bool = False
self.setLineEdit(_PopupColormapLineEdit(self))
self.lineEdit().setReadOnly(True)
line_edit = _PopupColormapLineEdit(self, allow_invalid=False)
self.setLineEdit(line_edit)
self.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
self.setItemDelegate(QColormapItemDelegate(self))
self.currentIndexChanged.connect(self._on_index_changed)
# there's a little bit of a potential bug here:
# if the user clicks on the "Add Colormap..." item
# then an indexChanged signal will be emitted, but it may not
@@ -75,6 +82,33 @@ class QColormapComboBox(QComboBox):
self.setUserAdditionsAllowed(allow_user_colormaps)
# Create a proxy model to handle filtering
self._proxy_model = QSortFilterProxyModel(self)
# use string list model as source model
self._proxy_model.setSourceModel(QStringListModel(self))
self._proxy_model.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
# Setup completer
self._completer = QCompleter(self)
self._completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
self._completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
self._completer.setFilterMode(Qt.MatchFlag.MatchContains)
self._completer.setModel(self._proxy_model)
# set the delegate for both the popup and the combobox
if popup := self._completer.popup():
popup.setItemDelegate(self.itemDelegate())
# Update completer model when items change
if model := self.model():
model.rowsInserted.connect(self._update_completer_model)
model.rowsRemoved.connect(self._update_completer_model)
self.setFilterable(filterable)
self.currentIndexChanged.connect(self._on_index_changed)
line_edit.editingFinished.connect(self._on_editing_finished)
def userAdditionsAllowed(self) -> bool:
"""Returns whether the user can add custom colors."""
return self._allow_user_colors
@@ -96,9 +130,26 @@ class QColormapComboBox(QComboBox):
elif not self._allow_user_colors:
self.removeItem(idx)
def setFilterable(self, filterable: bool) -> None:
"""Set whether the user can enter/filter colormaps by typing in the line edit.
If enabled, the user can select the text in the line edit and type to
filter the list of colormaps. The completer will show a list of matching
colormaps as the user types. If disabled, the user can only select from
the combo box dropdown.
"""
self._filterable = bool(filterable)
self.setCompleter(self._completer if self._filterable else None)
self.lineEdit().setReadOnly(not self._filterable)
def isFilterable(self) -> bool:
"""Returns whether the user can filter the list of colormaps."""
return self._filterable
def clear(self) -> None:
super().clear()
self.setUserAdditionsAllowed(self._allow_user_colors)
self._update_completer_model()
def itemColormap(self, index: int) -> Colormap | None:
"""Returns the color of the item at the given index."""
@@ -124,14 +175,23 @@ class QColormapComboBox(QComboBox):
# make sure the "Add Colormap..." item is last
idx = self.findData(self._add_color_text, Qt.ItemDataRole.DisplayRole)
if idx >= 0:
with signals_blocked(self):
self.removeItem(idx)
self.addItem(self._add_color_text)
self._block_completer_update = True
try:
with signals_blocked(self):
self.removeItem(idx)
self.addItem(self._add_color_text)
finally:
self._block_completer_update = False
def addColormaps(self, colors: Sequence[Any]) -> None:
"""Adds colors to the QComboBox."""
for color in colors:
self.addColormap(color)
self._block_completer_update = True
try:
for color in colors:
self.addColormap(color)
finally:
self._block_completer_update = False
self._update_completer_model()
def currentColormap(self) -> Colormap | None:
"""Returns the currently selected Colormap or None if not yet selected."""
@@ -173,6 +233,37 @@ class QColormapComboBox(QComboBox):
self.lineEdit().setColormap(colormap)
self._last_cmap = colormap
def _update_completer_model(self) -> None:
"""Update the completer's model with current items."""
if getattr(self, "_block_completer_update", False):
return
# Ensure we are updating the source model of the proxy
if isinstance(src_model := self._proxy_model.sourceModel(), QStringListModel):
words = [
txt
for i in range(self.count())
if (txt := self.itemText(i)) != self._add_color_text
]
src_model.setStringList(words)
self._proxy_model.invalidate()
def _on_editing_finished(self) -> None:
text = self.lineEdit().text()
if (cmap := try_cast_colormap(text)) is not None:
self.currentColormapChanged.emit(cmap)
# if the cmap is not in the list, add it
if self.findData(cmap, CMAP_ROLE) < 0:
self.addColormap(cmap)
def keyPressEvent(self, e: QKeyEvent | None) -> None:
if e and e.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return):
# select the first completion when pressing enter if the popup is visible
if (completer := self.completer()) and completer.completionCount():
self.lineEdit().setText(completer.currentCompletion()) # type: ignore
return super().keyPressEvent(e)
CATEGORIES = ("sequential", "diverging", "cyclic", "qualitative", "miscellaneous")
@@ -220,7 +311,9 @@ class _PopupColormapLineEdit(QColormapLineEdit):
Without this, only the down arrow will show the popup. And if mousePressEvent
is used instead, the popup will show and then immediately hide.
Also ensure that the popup is not shown when the user selects text.
"""
parent = self.parent()
if parent and hasattr(parent, "showPopup"):
parent.showPopup()
if not self.hasSelectedText():
parent = self.parent()
if parent and hasattr(parent, "showPopup"):
parent.showPopup()

View File

@@ -43,6 +43,13 @@ class QColormapLineEdit(QLineEdit):
checkerboard_size : int, optional
Size (in pixels) of the checkerboard pattern to draw behind colormaps with
transparency, by default 4. If 0, no checkerboard is drawn.
allow_invalid : bool, optional
If True, the user can enter any text, even if it does not represent a valid
colormap (and `fallback_cmap` will be shown if it's invalid). If False, the text
will be validated when editing is finished or focus is lost, and if the text is
not a valid colormap, it will be reverted to the first available valid option
from the completer, or, if that's not available, the last valid colormap.
Default is True. This is only settable at initialization.
"""
def __init__(
@@ -53,6 +60,7 @@ class QColormapLineEdit(QLineEdit):
fallback_cmap: Colormap | str | None = "gray",
missing_icon: QIcon | QStyle.StandardPixmap = MISSING,
checkerboard_size: int = 4,
allow_invalid: bool = True,
) -> None:
super().__init__(parent)
self.setFractionalColormapWidth(fractional_colormap_width)
@@ -69,6 +77,45 @@ class QColormapLineEdit(QLineEdit):
self._cmap: Colormap | None = None # current colormap
self.textChanged.connect(self.setColormap)
self._lastValidColormap: Colormap | None = None
if not allow_invalid:
self.editingFinished.connect(self._validate)
def _validate(self) -> None:
"""Called when editing is finished or focus is lost.
If the current text does not represent a valid colormap, revert to the first
available valid option from the completer, or, if that's not available, revert
to the last valid colormap.
"""
if self._cmap is None:
candidate = self._fist_completer_option()
if candidate is not None:
self.setColormap(candidate)
self.setText(candidate.name.rsplit(":", 1)[-1])
elif self._lastValidColormap is not None:
self.setColormap(self._lastValidColormap)
self.setText(self._lastValidColormap.name.rsplit(":", 1)[-1])
# Optionally, if neither is available, you might decide to clear the text.
else:
# Update the last valid value.
self._lastValidColormap = self._cmap
def _fist_completer_option(self) -> Colormap | None:
"""Return the first valid Colormap from the completer's current filtered list.
or None if no valid option is available.
"""
if (
(completer := self.completer()) is None
or (model := completer.model()) is None
or model.rowCount() == 0
):
return None
first_item = model.index(0, 0).data(Qt.ItemDataRole.DisplayRole)
return try_cast_colormap(first_item)
def setFractionalColormapWidth(self, fraction: float) -> None:
self._colormap_fraction: float = float(fraction)
align = Qt.AlignmentFlag.AlignVCenter

View File

@@ -0,0 +1,3 @@
from superqt.switch._toggle_switch import QStyleOptionToggleSwitch, QToggleSwitch
__all__ = ["QStyleOptionToggleSwitch", "QToggleSwitch"]

View File

@@ -0,0 +1,321 @@
from __future__ import annotations
from typing import overload
from qtpy import QtCore, QtGui
from qtpy import QtWidgets as QtW
from qtpy.QtCore import Property, Qt
class QStyleOptionToggleSwitch(QtW.QStyleOptionButton):
def __init__(self):
super().__init__()
self.on_color = QtGui.QColor("#4D79C7")
self.off_color = QtGui.QColor("#909090")
self.handle_color = QtGui.QColor("#d5d5d5")
self.switch_width = 24
self.switch_height = 12
self.handle_size = 14
# these aren't yet overrideable in QToggleSwitch
self.margin = 2
self.text_alignment = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
class QToggleSwitch(QtW.QAbstractButton):
StyleOption = QStyleOptionToggleSwitch
@overload
def __init__(self, parent: QtW.QWidget | None = ...) -> None: ...
@overload
def __init__(self, text: str | None, parent: QtW.QWidget | None = ...) -> None: ...
def __init__( # type: ignore [misc] # overload
self, text: str | None = None, parent: QtW.QWidget | None = None
) -> None:
if isinstance(text, QtW.QWidget):
if parent is not None:
raise TypeError("No overload of QToggleSwitch matches the arguments")
parent = text
text = None
# attributes for drawing the switch
self._on_color = QtGui.QColor("#4D79C7")
self._off_color = QtGui.QColor("#909090")
self._handle_color = QtGui.QColor("#d5d5d5")
self._switch_width = 24
self._switch_height = 12
self._handle_size = 14
self._offset_value = 8.0
super().__init__(parent)
self.setCheckable(True)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.toggled.connect(self._animate_handle)
self._anim = QtCore.QPropertyAnimation(self, b"_offset", self)
self._anim.setDuration(120)
self._offset_value = self._offset_for_checkstate(False)
if text:
self.setText(text)
def initStyleOption(self, option: QStyleOptionToggleSwitch) -> None:
"""Initialize the style option for the switch."""
option.initFrom(self)
option.text = self.text()
option.icon = self.icon()
option.iconSize = self.iconSize()
option.state |= (
QtW.QStyle.StateFlag.State_On
if self.isChecked()
else QtW.QStyle.StateFlag.State_Off
)
option.on_color = self.onColor
option.off_color = self.offColor
option.handle_color = self.handleColor
option.switch_width = self.switchWidth
option.switch_height = self.switchHeight
option.handle_size = self.handleSize
def paintEvent(self, a0: QtGui.QPaintEvent | None) -> None:
p = QtGui.QPainter(self)
opt = QStyleOptionToggleSwitch()
self.initStyleOption(opt)
self.drawGroove(p, self._groove_rect(opt), opt)
p.save()
self.drawHandle(p, self._handle_rect(opt), opt)
p.restore()
self.drawText(p, self._text_rect(opt), opt)
p.end()
def minimumSizeHint(self) -> QtCore.QSize:
return self.sizeHint()
def setAnimationDuration(self, msec: int) -> None:
"""Set the duration of the animation in milliseconds.
To disable animation, set duration to 0.
"""
self._anim.setDuration(msec)
def animationDuration(self) -> int:
"""Return the duration of the animation in milliseconds."""
return self._anim.duration()
def sizeHint(self) -> QtCore.QSize:
self.ensurePolished()
opt = QStyleOptionToggleSwitch()
self.initStyleOption(opt)
fm = QtGui.QFontMetrics(self.font())
text_size = fm.size(0, self.text())
height = max(opt.switch_height, text_size.height()) + opt.margin * 2
width = opt.switch_width + text_size.width() + opt.margin * 2 + 8
return QtCore.QSize(width, height)
### Re-implementable methods for drawing the switch ###
def drawGroove(
self,
painter: QtGui.QPainter,
rect: QtCore.QRectF,
option: QStyleOptionToggleSwitch,
) -> None:
"""Draw the groove of the switch.
Parameters
----------
painter : QtGui.QPainter
The painter to use for drawing.
rect : QtCore.QRectF
The rectangle in which to draw the groove.
option : QStyleOptionToggleSwitch
The style options used for drawing.
"""
painter.setPen(Qt.PenStyle.NoPen)
is_checked = option.state & QtW.QStyle.StateFlag.State_On
is_enabled = option.state & QtW.QStyle.StateFlag.State_Enabled
# draw the groove
if is_enabled:
painter.setBrush(option.on_color if is_checked else option.off_color)
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True)
painter.setOpacity(0.8)
else:
painter.setBrush(option.off_color)
painter.setOpacity(0.6)
half_height = option.switch_height / 2
painter.drawRoundedRect(rect, half_height, half_height)
def drawHandle(
self,
painter: QtGui.QPainter,
rect: QtCore.QRectF,
option: QStyleOptionToggleSwitch,
) -> None:
"""Draw the handle of the switch.
Parameters
----------
painter : QtGui.QPainter
The painter to use for drawing.
rect : QtCore.QRectF
The rectangle in which to draw the handle.
option : QStyleOptionToggleSwitch
The style options used for drawing.
"""
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(option.handle_color)
painter.setOpacity(1.0)
painter.drawEllipse(rect)
def drawText(
self,
painter: QtGui.QPainter,
rect: QtCore.QRectF,
option: QStyleOptionToggleSwitch,
) -> None:
"""Draw the text next to the switch.
Parameters
----------
painter : QtGui.QPainter
The painter to use for drawing.
rect : QtCore.QRectF
The rectangle in which to draw the text.
option : QStyleOptionToggleSwitch
The style options used for drawing.
"""
# TODO:
# using self.style().drawControl(CE_PushButtonLabel ...)
# might provide a more native experience.
text_color = option.palette.color(self.foregroundRole())
pen = QtGui.QPen(text_color, 1)
painter.setPen(pen)
painter.drawText(rect, int(option.text_alignment), option.text)
### Properties ###
def _get_onColor(self) -> QtGui.QColor:
return self._on_color
def _set_onColor(self, color: QtGui.QColor | QtGui.QBrush) -> None:
self._on_color = QtGui.QColor(color)
self.update()
onColor = Property(QtGui.QColor, _get_onColor, _set_onColor)
"""Color of the switch groove when it is on."""
def _get_offColor(self) -> QtGui.QColor:
return self._off_color
def _set_offColor(self, color: QtGui.QColor | QtGui.QBrush) -> None:
self._off_color = QtGui.QColor(color)
self.update()
offColor = Property(QtGui.QColor, _get_offColor, _set_offColor)
"""Color of the switch groove when it is off."""
def _get_handleColor(self) -> QtGui.QColor:
return self._handle_color
def _set_handleColor(self, color: QtGui.QColor | QtGui.QBrush) -> None:
self._handle_color = QtGui.QColor(color)
self.update()
handleColor = Property(QtGui.QColor, _get_handleColor, _set_handleColor)
"""Color of the switch handle."""
def _get_switchWidth(self) -> int:
return self._switch_width
def _set_switchWidth(self, width: int) -> None:
self._switch_width = width
self._offset_value = self._offset_for_checkstate(self.isChecked())
self.update()
switchWidth = Property(int, _get_switchWidth, _set_switchWidth)
"""Width of the switch groove."""
def _get_switchHeight(self) -> int:
return self._switch_height
def _set_switchHeight(self, height: int) -> None:
self._switch_height = height
self._offset_value = self._offset_for_checkstate(self.isChecked())
self.update()
switchHeight = Property(int, _get_switchHeight, _set_switchHeight)
"""Height of the switch groove."""
def _get_handleSize(self) -> int:
return self._handle_size
def _set_handleSize(self, size: int) -> None:
self._handle_size = size
self.update()
handleSize = Property(int, _get_handleSize, _set_handleSize)
"""Width/height of the switch handle."""
### Other private methods ###
def _animate_handle(self, val: bool) -> None:
end = self._offset_for_checkstate(val)
if self._anim.duration():
self._anim.setStartValue(self._offset_for_checkstate(not val))
self._anim.setEndValue(end)
self._anim.start()
else:
self._set_offset(end)
def _get_offset(self) -> float:
return self._offset_value
def _set_offset(self, offset: float) -> None:
self._offset_value = offset
self.update()
_offset = Property(float, _get_offset, _set_offset)
def _offset_for_checkstate(self, val: bool) -> float:
opt = QStyleOptionToggleSwitch()
self.initStyleOption(opt)
if val:
offset = opt.margin + opt.switch_width - opt.switch_height / 2
else:
offset = opt.margin + opt.switch_height / 2
return offset
def _groove_rect(self, opt: QStyleOptionToggleSwitch) -> QtCore.QRectF:
return QtCore.QRectF(
opt.margin, self._vertical_offset(opt), opt.switch_width, opt.switch_height
)
def _handle_rect(self, opt: QStyleOptionToggleSwitch) -> QtCore.QRectF:
return QtCore.QRectF(
self._offset_value - opt.handle_size / 2,
self._vertical_offset(opt) - (opt.handle_size - opt.switch_height) / 2,
opt.handle_size,
opt.handle_size,
)
def _text_rect(self, opt: QStyleOptionToggleSwitch) -> QtCore.QRectF:
# If handle is bigger than groove, adjust the text to the right of the handle.
# If groove is bigger, adjust the text to the right of the groove.
return QtCore.QRectF(
opt.switch_width
+ max(opt.handle_size - opt.switch_height, 0) // 2
+ opt.margin * 2
+ 2,
0,
self.width() - opt.switch_width - opt.margin * 2,
self.height(),
)
def _vertical_offset(self, opt: QStyleOptionToggleSwitch) -> int:
"""Offset for the vertical centering of the switch."""
return (self.height() - opt.switch_height) // 2 + opt.margin

View File

@@ -76,8 +76,9 @@ def test_catalog_combo(qtbot):
assert wdg.currentColormap() == Colormap("viridis")
def test_cmap_combo(qtbot):
wdg = QColormapComboBox(allow_user_colormaps=True)
@pytest.mark.parametrize("filterable", [False, True])
def test_cmap_combo(qtbot, filterable):
wdg = QColormapComboBox(allow_user_colormaps=True, filterable=filterable)
qtbot.addWidget(wdg)
wdg.show()
assert wdg.userAdditionsAllowed()

116
tests/test_toggle_switch.py Normal file
View File

@@ -0,0 +1,116 @@
from unittest.mock import Mock
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication, QCheckBox, QVBoxLayout, QWidget
from superqt import QToggleSwitch
def test_on_and_off(qtbot):
wdg = QToggleSwitch()
qtbot.addWidget(wdg)
wdg.show()
assert not wdg.isChecked()
wdg.setChecked(True)
assert wdg.isChecked()
QApplication.processEvents()
wdg.setChecked(False)
assert not wdg.isChecked()
QApplication.processEvents()
wdg.setChecked(False)
assert not wdg.isChecked()
wdg.toggle()
assert wdg.isChecked()
wdg.toggle()
assert not wdg.isChecked()
wdg.click()
assert wdg.isChecked()
wdg.click()
assert not wdg.isChecked()
QApplication.processEvents()
def test_get_set(qtbot):
wdg = QToggleSwitch()
qtbot.addWidget(wdg)
wdg.onColor = "#ff0000"
assert wdg.onColor.name() == "#ff0000"
wdg.offColor = "#00ff00"
assert wdg.offColor.name() == "#00ff00"
wdg.handleColor = "#0000ff"
assert wdg.handleColor.name() == "#0000ff"
wdg.setText("new text")
assert wdg.text() == "new text"
wdg.switchWidth = 100
assert wdg.switchWidth == 100
wdg.switchHeight = 100
assert wdg.switchHeight == 100
wdg.handleSize = 80
assert wdg.handleSize == 80
def test_mouse_click(qtbot):
wdg = QToggleSwitch()
mock = Mock()
wdg.toggled.connect(mock)
qtbot.addWidget(wdg)
assert not wdg.isChecked()
mock.assert_not_called()
qtbot.mouseClick(wdg, Qt.MouseButton.LeftButton)
assert wdg.isChecked()
mock.assert_called_once_with(True)
qtbot.mouseClick(wdg, Qt.MouseButton.LeftButton)
assert not wdg.isChecked()
def test_signal_emission_order(qtbot):
"""Check if event emmision is same for QToggleSwitch and QCheckBox"""
wdg = QToggleSwitch()
emitted_from_toggleswitch = []
wdg.toggled.connect(lambda: emitted_from_toggleswitch.append("toggled"))
wdg.pressed.connect(lambda: emitted_from_toggleswitch.append("pressed"))
wdg.clicked.connect(lambda: emitted_from_toggleswitch.append("clicked"))
wdg.released.connect(lambda: emitted_from_toggleswitch.append("released"))
qtbot.addWidget(wdg)
checkbox = QCheckBox()
emitted_from_checkbox = []
checkbox.toggled.connect(lambda: emitted_from_checkbox.append("toggled"))
checkbox.pressed.connect(lambda: emitted_from_checkbox.append("pressed"))
checkbox.clicked.connect(lambda: emitted_from_checkbox.append("clicked"))
checkbox.released.connect(lambda: emitted_from_checkbox.append("released"))
qtbot.addWidget(checkbox)
emitted_from_toggleswitch.clear()
emitted_from_checkbox.clear()
wdg.toggle()
checkbox.toggle()
assert emitted_from_toggleswitch
assert emitted_from_toggleswitch == emitted_from_checkbox
emitted_from_toggleswitch.clear()
emitted_from_checkbox.clear()
wdg.click()
checkbox.click()
assert emitted_from_toggleswitch
assert emitted_from_toggleswitch == emitted_from_checkbox
def test_multiple_lines(qtbot):
container = QWidget()
layout = QVBoxLayout(container)
wdg0 = QToggleSwitch("line1\nline2\nline3")
wdg1 = QToggleSwitch("line1\nline2")
checkbox = QCheckBox()
layout.addWidget(wdg0)
layout.addWidget(wdg1)
layout.addWidget(checkbox)
container.show()
qtbot.addWidget(container)
assert wdg0.text() == "line1\nline2\nline3"
assert wdg1.text() == "line1\nline2"
assert wdg0.sizeHint().height() > wdg1.sizeHint().height()
assert wdg1.sizeHint().height() > checkbox.sizeHint().height()
assert wdg0.height() > wdg1.height()
assert wdg1.height() > checkbox.height()