mirror of
https://github.com/pyapp-kit/superqt.git
synced 2026-01-04 19:31:24 +01:00
QCollapsible for Collapsible Section Control (#37)
* Update changelog to ingnore virtual environment * wip * wip * Working animation * WIP Implement tests * All tests are passing * convert to camalCase * Change function name to match functionality * convert pyside to qtcompat * move animation utils to main module * remove seperators * protect util functions * add example * remove seperators from test file * suggestions * Passing tests and ability to initialize expansion * Ensure that the test will be passed in any screen resolution * replace quick functions with parameters * Update src/superqt/collapsible/_collapsible.py Fix initial text Co-authored-by: Talley Lambert <talley.lambert@gmail.com> * Update src/superqt/collapsible/_collapsible.py Remote WindowFlags to prevent compatiblity issue. Co-authored-by: Talley Lambert <talley.lambert@gmail.com> * merge internal expand and collapse into one function * Update src/superqt/collapsible/_collapsible.py * Update tests/test_collapsible.py Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
This commit is contained in:
committed by
GitHub
parent
8001022e18
commit
789b98f892
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ __pycache__/
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
.venv/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
|
||||
15
examples/collapsible.py
Normal file
15
examples/collapsible.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Example for QCollapsible"""
|
||||
from PySide2.QtWidgets import QLabel
|
||||
from superqt import QCollapsible
|
||||
from superqt.qtcompat.QtWidgets import QApplication, QPushButton, QLabel
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
collapsible = QCollapsible("Advanced analysis")
|
||||
collapsible.addWidget(QLabel("This is the inside of the collapsible frame"))
|
||||
for i in range(10):
|
||||
collapsible.addWidget(QPushButton(f"Content button {i + 1}"))
|
||||
|
||||
collapsible.expand(animate=False)
|
||||
collapsible.show()
|
||||
app.exec_()
|
||||
@@ -6,6 +6,7 @@ except ImportError:
|
||||
|
||||
|
||||
from ._eliding_label import QElidingLabel
|
||||
from .collapsible import QCollapsible
|
||||
from .combobox import QEnumComboBox
|
||||
from .sliders import (
|
||||
QDoubleRangeSlider,
|
||||
@@ -33,4 +34,5 @@ __all__ = [
|
||||
"QMessageHandler",
|
||||
"QRangeSlider",
|
||||
"QEnumComboBox",
|
||||
"QCollapsible",
|
||||
]
|
||||
|
||||
3
src/superqt/collapsible/__init__.py
Normal file
3
src/superqt/collapsible/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from ._collapsible import QCollapsible
|
||||
|
||||
__all__ = ["QCollapsible"]
|
||||
107
src/superqt/collapsible/_collapsible.py
Normal file
107
src/superqt/collapsible/_collapsible.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""A collapsible widget to hide and unhide child widgets"""
|
||||
from typing import Optional
|
||||
|
||||
from ..qtcompat.QtCore import QAbstractAnimation, QEasingCurve, QPropertyAnimation, Qt, QMargins
|
||||
from ..qtcompat.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class QCollapsible(QFrame):
|
||||
"""
|
||||
A collapsible widget to hide and unhide child widgets.
|
||||
|
||||
Based on https://stackoverflow.com/a/68141638
|
||||
"""
|
||||
|
||||
_EXPANDED = "▼ "
|
||||
_COLLAPSED = "▲ "
|
||||
|
||||
def __init__(self, title: str = "", parent: Optional[QWidget] = None):
|
||||
super().__init__(parent)
|
||||
self._locked = False
|
||||
|
||||
self._toggle_btn = QPushButton(self._COLLAPSED + title)
|
||||
self._toggle_btn.setCheckable(True)
|
||||
self._toggle_btn.setChecked(False)
|
||||
self._toggle_btn.setStyleSheet("text-align: left; background: transparent;")
|
||||
self._toggle_btn.clicked.connect(self._toggle)
|
||||
|
||||
# frame layout
|
||||
self.setLayout(QVBoxLayout())
|
||||
self.layout().setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.layout().addWidget(self._toggle_btn)
|
||||
|
||||
# Create animators
|
||||
self._animation = QPropertyAnimation(self)
|
||||
self._animation.setPropertyName(b"maximumHeight")
|
||||
self._animation.setStartValue(0)
|
||||
self.setDuration(300)
|
||||
self.setEasingCurve(QEasingCurve.Type.InOutCubic)
|
||||
|
||||
# default content widget
|
||||
_content = QWidget()
|
||||
_content.setLayout(QVBoxLayout())
|
||||
_content.setMaximumHeight(0)
|
||||
_content.layout().setContentsMargins(QMargins(5,0,0,0))
|
||||
self.setContent(_content)
|
||||
|
||||
def setText(self, text: str):
|
||||
current = self._toggle_btn.text()[: len(self._EXPANDED)]
|
||||
self._toggle_btn.setText(current + text)
|
||||
|
||||
def setContent(self, content: QWidget):
|
||||
self._content = content
|
||||
self.layout().addWidget(self._content)
|
||||
self._animation.setTargetObject(content)
|
||||
|
||||
def content(self) -> QWidget:
|
||||
return self._content
|
||||
|
||||
def setDuration(self, msecs: int):
|
||||
self._animation.setDuration(msecs)
|
||||
|
||||
def setEasingCurve(self, easing: QEasingCurve):
|
||||
self._animation.setEasingCurve(easing)
|
||||
|
||||
def addWidget(self, widget: QWidget):
|
||||
self._content.layout().addWidget(widget)
|
||||
|
||||
def removeWidget(self, widget: QWidget):
|
||||
self._content.layout().removeWidget(widget)
|
||||
|
||||
def expand(self, animate: bool = True):
|
||||
self._expand_collapse(QAbstractAnimation.Direction.Forward, animate)
|
||||
|
||||
def collapse(self, animate: bool = True):
|
||||
self._expand_collapse(QAbstractAnimation.Direction.Backward, animate)
|
||||
|
||||
def expanded(self):
|
||||
return self._toggle_btn.isChecked()
|
||||
|
||||
def _expand_collapse(self, direction: QAbstractAnimation.Direction, animate:bool = True):
|
||||
if self._locked is True:
|
||||
return
|
||||
|
||||
forward = direction == QAbstractAnimation.Direction.Forward
|
||||
text = self._EXPANDED if forward else self._COLLAPSED
|
||||
|
||||
self._toggle_btn.setChecked(forward)
|
||||
self._toggle_btn.setText(text + self._toggle_btn.text()[len(self._EXPANDED) :])
|
||||
|
||||
if animate:
|
||||
self._animation.setDirection(direction)
|
||||
self._animation.setEndValue(self._content.sizeHint().height() + 10)
|
||||
self._animation.start()
|
||||
else:
|
||||
height = 0 if forward==False else (self._content.sizeHint().height() + 10)
|
||||
self._content.setMaximumHeight(height)
|
||||
|
||||
def _toggle(self):
|
||||
if self._locked is True:
|
||||
self._toggle_btn.setChecked(not self._toggle_btn.isChecked)
|
||||
self.expand() if self._toggle_btn.isChecked() else self.collapse()
|
||||
|
||||
def setLocked(self, locked: bool = True):
|
||||
self._locked = locked
|
||||
|
||||
def locked(self) -> bool:
|
||||
return self._locked
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
66
tests/test_collapsible.py
Normal file
66
tests/test_collapsible.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""A test module for testing collapsible"""
|
||||
|
||||
from superqt import QCollapsible
|
||||
from superqt.qtcompat.QtCore import QEasingCurve
|
||||
from superqt.qtcompat.QtWidgets import QPushButton
|
||||
|
||||
|
||||
def test_checked_initialization(qtbot):
|
||||
"""Test simple collapsible"""
|
||||
wdg1 = QCollapsible("Advanced analysis")
|
||||
wdg1.expand(False)
|
||||
assert wdg1.expanded() is True
|
||||
assert wdg1._content.maximumHeight() > 0
|
||||
|
||||
wdg2 = QCollapsible("Advanced analysis")
|
||||
wdg1.collapse(False)
|
||||
assert wdg2.expanded() is False
|
||||
assert wdg2._content.maximumHeight() == 0
|
||||
|
||||
|
||||
def test_content_hide_show(qtbot):
|
||||
"""Test collapsible with content"""
|
||||
|
||||
# Create child component
|
||||
collapsible = QCollapsible("Advanced analysis")
|
||||
for i in range(10):
|
||||
collapsible.addWidget(QPushButton(f"Content button {i + 1}"))
|
||||
|
||||
collapsible.collapse(False)
|
||||
assert collapsible.expanded() is False
|
||||
assert collapsible._content.maximumHeight() == 0
|
||||
|
||||
collapsible.expand(False)
|
||||
assert collapsible.expanded() is True
|
||||
assert collapsible._content.maximumHeight() > 0
|
||||
|
||||
|
||||
def test_locking(qtbot):
|
||||
"""Test locking collapsible"""
|
||||
wdg1 = QCollapsible()
|
||||
assert wdg1.locked() is False
|
||||
wdg1.setLocked(True)
|
||||
assert wdg1.locked() is True
|
||||
|
||||
# Simulate button press
|
||||
wdg1._toggle_btn.setChecked(True)
|
||||
wdg1._toggle()
|
||||
|
||||
assert wdg1.expanded() is False
|
||||
|
||||
|
||||
def test_changing_animation_settings(qtbot):
|
||||
"""Quick test for changing animation settings"""
|
||||
wdg = QCollapsible()
|
||||
wdg.setDuration(600)
|
||||
wdg.setEasingCurve(QEasingCurve.Type.InElastic)
|
||||
assert wdg._animation.easingCurve() == QEasingCurve.Type.InElastic
|
||||
assert wdg._animation.duration() == 600
|
||||
|
||||
|
||||
def test_changing_content(qtbot):
|
||||
"""Test changing the content"""
|
||||
content = QPushButton()
|
||||
wdg = QCollapsible()
|
||||
wdg.setContent(content)
|
||||
assert wdg._content == content
|
||||
Reference in New Issue
Block a user