Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Change icon used in Collapsible widget #140

Merged
merged 20 commits into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from 18 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
2 changes: 2 additions & 0 deletions examples/qcollapsible.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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}"))
Expand Down
64 changes: 54 additions & 10 deletions src/superqt/collapsible/_collapsible.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
"""A collapsible widget to hide and unhide child widgets"""
from typing import Optional
from typing import Optional, Union

from qtpy.QtCore import (
QEasingCurve,
QEvent,
QMargins,
QObject,
QPropertyAnimation,
QRect,
Qt,
Signal,
)
from qtpy.QtGui import QIcon, QPainter, QPalette, QPixmap
from qtpy.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget


Expand All @@ -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)

Expand All @@ -58,12 +66,12 @@ def __init__(self, title: str = "", parent: Optional[QWidget] = None):

def setText(self, text: str):
"""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):
"""Replace central widget (the widget that gets expanded/collapsed)."""
Expand All @@ -75,6 +83,42 @@ def content(self) -> QWidget:
"""Return the current content widget."""
return self._content

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.transparent)
painter = QPainter(pixmap)
color = self._toggle_btn.palette().color(QPalette.WindowText)
painter.setPen(color)
painter.drawText(QRect(0, 0, size, size), Qt.AlignCenter, symbol)
painter.end()
return QIcon(pixmap)

def setExpandedIcon(self, icon: Optional[Union[QIcon, str]] = 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)

def expandedIcon(self):
"""Returns the icon used when the widget is expanded."""
return self._expanded_icon

def setCollapsedIcon(self, icon: Optional[Union[QIcon, str]] = 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)

self._toggle_btn.setIcon(self._collapsed_icon)

def collapsedIcon(self):
"""Returns the icon used when the widget is collapsed."""
return self._collapsed_icon

def setDuration(self, msecs: int):
"""Set duration of the collapse/expand animation."""
self._animation.setDuration(msecs)
Expand Down Expand Up @@ -121,10 +165,10 @@ def _expand_collapse(
return

forward = direction == QPropertyAnimation.Direction.Forward
text = self._EXPANDED if forward else self._COLLAPSED
text = self._expanded_icon if forward else self._collapsed_icon
self._toggle_btn.setIcon(text)

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:
Expand Down
47 changes: 45 additions & 2 deletions tests/test_collapsible.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
"""A test module for testing collapsible"""

import pytest
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):
"""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")
Expand Down Expand Up @@ -84,7 +97,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"
ppwadhwa marked this conversation as resolved.
Show resolved Hide resolved


def test_toggle_signal(qtbot):
Expand All @@ -98,3 +111,33 @@ def test_toggle_signal(qtbot):

with qtbot.waitSignal(wdg.toggled, timeout=500):
wdg.collapse()


@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_getting_icon(qtbot):
"""Test setting string as toggle button."""
wdg = QCollapsible("test")
assert isinstance(wdg.expandedIcon(), QIcon)
assert isinstance(wdg.collapsedIcon(), QIcon)


@pytest.mark.filterwarnings("ignore::DeprecationWarning")
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


@pytest.mark.filterwarnings("ignore::DeprecationWarning")
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