Change icon used in Collapsible widget (#140)

* add ability to change icon

* fix icon setting so it will load properly on start up

* remove check on icon length.  not necessary anymore

* fix test

* reduce duplicate code.  expose _COLLAPSED and _EXPANDED to user on creation of QCollapsible widget

* add ability to set icon with string or icon.

* add tests for adding, setting icons

* fix test.

* fix test for icons

* move file

* fix test

* remove hardcoded size.  Use font size

* add test docstring

* fix test.  chnage expanded/collapsed names

* remove unnecessary strings

* update example.  add getter functions.  remove lines.  change function name

* put default string in init.  add getter tests

* update test

* cleanup typing and fix set setCollapsedIcon

Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
This commit is contained in:
Pam
2022-11-30 19:45:07 -06:00
committed by GitHub
parent ad2f05d908
commit 7b2d8bfb2d
3 changed files with 113 additions and 25 deletions

View File

@@ -6,6 +6,8 @@ from superqt import QCollapsible
app = QApplication([])
collapsible = QCollapsible("Advanced analysis")
collapsible.setCollapsedIcon("+")
collapsible.setExpandedIcon("-")
collapsible.addWidget(QLabel("This is the inside of the collapsible frame"))
for i in range(10):
collapsible.addWidget(QPushButton(f"Content button {i + 1}"))

View File

@@ -1,5 +1,5 @@
"""A collapsible widget to hide and unhide child widgets"""
from typing import Optional
from typing import Optional, Union
from qtpy.QtCore import (
QEasingCurve,
@@ -7,9 +7,11 @@ from qtpy.QtCore import (
QMargins,
QObject,
QPropertyAnimation,
QRect,
Qt,
Signal,
)
from qtpy.QtGui import QIcon, QPainter, QPalette, QPixmap
from qtpy.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
@@ -21,18 +23,24 @@ class QCollapsible(QFrame):
Based on https://stackoverflow.com/a/68141638
"""
_EXPANDED = ""
_COLLAPSED = ""
toggled = Signal(bool)
def __init__(self, title: str = "", parent: Optional[QWidget] = None):
def __init__(
self,
title: str = "",
parent: Optional[QWidget] = None,
expandedIcon: Optional[Union[QIcon, str]] = "",
collapsedIcon: Optional[Union[QIcon, str]] = "",
):
super().__init__(parent)
self._locked = False
self._is_animating = False
self._text = title
self._toggle_btn = QPushButton(self._COLLAPSED + title)
self._toggle_btn = QPushButton(title)
self._toggle_btn.setCheckable(True)
self.setCollapsedIcon(icon=collapsedIcon)
self.setExpandedIcon(icon=expandedIcon)
self._toggle_btn.setStyleSheet("text-align: left; border: none; outline: none;")
self._toggle_btn.toggled.connect(self._toggle)
@@ -56,16 +64,16 @@ class QCollapsible(QFrame):
_content.layout().setContentsMargins(QMargins(5, 0, 0, 0))
self.setContent(_content)
def setText(self, text: str):
def setText(self, text: str) -> None:
"""Set the text of the toggle button."""
current = self._toggle_btn.text()[: len(self._EXPANDED)]
current = self._toggle_btn.text()
self._toggle_btn.setText(current + text)
def text(self) -> str:
"""Return the text of the toggle button."""
return self._toggle_btn.text()[len(self._EXPANDED) :]
return self._toggle_btn.text()
def setContent(self, content: QWidget):
def setContent(self, content: QWidget) -> None:
"""Replace central widget (the widget that gets expanded/collapsed)."""
self._content = content
self.layout().addWidget(self._content)
@@ -75,29 +83,69 @@ class QCollapsible(QFrame):
"""Return the current content widget."""
return self._content
def setDuration(self, msecs: int):
def _convert_string_to_icon(self, symbol: str) -> QIcon:
"""Create a QIcon from a string."""
size = self._toggle_btn.font().pointSize()
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
color = self._toggle_btn.palette().color(QPalette.ColorRole.WindowText)
painter.setPen(color)
painter.drawText(QRect(0, 0, size, size), Qt.AlignmentFlag.AlignCenter, symbol)
painter.end()
return QIcon(pixmap)
def expandedIcon(self) -> QIcon:
"""Returns the icon used when the widget is expanded."""
return self._expanded_icon
def setExpandedIcon(self, icon: Optional[Union[QIcon, str]] = None) -> None:
"""Set the icon on the toggle button when the widget is expanded."""
if icon and isinstance(icon, QIcon):
self._expanded_icon = icon
elif icon and isinstance(icon, str):
self._expanded_icon = self._convert_string_to_icon(icon)
if self.isExpanded():
self._toggle_btn.setIcon(self._expanded_icon)
def collapsedIcon(self) -> QIcon:
"""Returns the icon used when the widget is collapsed."""
return self._collapsed_icon
def setCollapsedIcon(self, icon: Optional[Union[QIcon, str]] = None) -> None:
"""Set the icon on the toggle button when the widget is collapsed."""
if icon and isinstance(icon, QIcon):
self._collapsed_icon = icon
elif icon and isinstance(icon, str):
self._collapsed_icon = self._convert_string_to_icon(icon)
if not self.isExpanded():
self._toggle_btn.setIcon(self._collapsed_icon)
def setDuration(self, msecs: int) -> None:
"""Set duration of the collapse/expand animation."""
self._animation.setDuration(msecs)
def setEasingCurve(self, easing: QEasingCurve):
def setEasingCurve(self, easing: QEasingCurve) -> None:
"""Set the easing curve for the collapse/expand animation"""
self._animation.setEasingCurve(easing)
def addWidget(self, widget: QWidget):
def addWidget(self, widget: QWidget) -> None:
"""Add a widget to the central content widget's layout."""
widget.installEventFilter(self)
self._content.layout().addWidget(widget)
def removeWidget(self, widget: QWidget):
def removeWidget(self, widget: QWidget) -> None:
"""Remove widget from the central content widget's layout."""
self._content.layout().removeWidget(widget)
widget.removeEventFilter(self)
def expand(self, animate: bool = True):
def expand(self, animate: bool = True) -> None:
"""Expand (show) the collapsible section"""
self._expand_collapse(QPropertyAnimation.Direction.Forward, animate)
def collapse(self, animate: bool = True):
def collapse(self, animate: bool = True) -> None:
"""Collapse (hide) the collapsible section"""
self._expand_collapse(QPropertyAnimation.Direction.Backward, animate)
@@ -105,7 +153,7 @@ class QCollapsible(QFrame):
"""Return whether the collapsible section is visible"""
return self._toggle_btn.isChecked()
def setLocked(self, locked: bool = True):
def setLocked(self, locked: bool = True) -> None:
"""Set whether collapse/expand is disabled"""
self._locked = locked
self._toggle_btn.setCheckable(not locked)
@@ -119,7 +167,7 @@ class QCollapsible(QFrame):
direction: QPropertyAnimation.Direction,
animate: bool = True,
emit: bool = True,
):
) -> None:
"""Set values for the widget based on whether it is expanding or collapsing.
An emit flag is included so that the toggle signal is only called once (it
@@ -129,10 +177,9 @@ class QCollapsible(QFrame):
return
forward = direction == QPropertyAnimation.Direction.Forward
text = self._EXPANDED if forward else self._COLLAPSED
icon = self._expanded_icon if forward else self._collapsed_icon
self._toggle_btn.setIcon(icon)
self._toggle_btn.setChecked(forward)
self._toggle_btn.setText(text + self._toggle_btn.text()[len(self._EXPANDED) :])
_content_height = self._content.sizeHint().height() + 10
if animate:
@@ -145,7 +192,7 @@ class QCollapsible(QFrame):
if emit:
self.toggled.emit(direction == QPropertyAnimation.Direction.Forward)
def _toggle(self):
def _toggle(self) -> None:
self.expand() if self.isExpanded() else self.collapse()
def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
@@ -160,5 +207,5 @@ class QCollapsible(QFrame):
)
return False
def _on_animation_done(self):
def _on_animation_done(self) -> None:
self._is_animating = False

View File

@@ -1,11 +1,23 @@
"""A test module for testing collapsible"""
from qtpy.QtCore import QEasingCurve, Qt
from qtpy.QtWidgets import QPushButton
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QPushButton, QStyle, QWidget
from superqt import QCollapsible
def _get_builtin_icon(name: str) -> QIcon:
"""Get a built-in icon from the Qt library."""
widget = QWidget()
try:
pixmap = getattr(QStyle.StandardPixmap, f"SP_{name}")
except AttributeError:
pixmap = getattr(QStyle, f"SP_{name}")
return widget.style().standardIcon(pixmap)
def test_checked_initialization(qtbot):
"""Test simple collapsible"""
wdg1 = QCollapsible("Advanced analysis")
@@ -84,7 +96,7 @@ def test_changing_text(qtbot):
wdg = QCollapsible()
wdg.setText("Hi new text")
assert wdg.text() == "Hi new text"
assert wdg._toggle_btn.text() == QCollapsible._COLLAPSED + "Hi new text"
assert wdg._toggle_btn.text() == "Hi new text"
def test_toggle_signal(qtbot):
@@ -98,3 +110,30 @@ def test_toggle_signal(qtbot):
with qtbot.waitSignal(wdg.toggled, timeout=500):
wdg.collapse()
def test_getting_icon(qtbot):
"""Test setting string as toggle button."""
wdg = QCollapsible("test")
assert isinstance(wdg.expandedIcon(), QIcon)
assert isinstance(wdg.collapsedIcon(), QIcon)
def test_setting_icon(qtbot):
"""Test setting icon for toggle button."""
icon1 = _get_builtin_icon("ArrowRight")
icon2 = _get_builtin_icon("ArrowDown")
wdg = QCollapsible("test", expandedIcon=icon1, collapsedIcon=icon2)
assert wdg._expanded_icon == icon1
assert wdg._collapsed_icon == icon2
def test_setting_symbol_icon(qtbot):
"""Test setting string as toggle button."""
wdg = QCollapsible("test")
icon1 = wdg._convert_string_to_icon("+")
icon2 = wdg._convert_string_to_icon("-")
wdg.setCollapsedIcon(icon=icon1)
assert wdg._collapsed_icon == icon1
wdg.setExpandedIcon(icon=icon2)
assert wdg._expanded_icon == icon2