1
0
Fork 0
mirror of synced 2024-06-13 16:14:41 +12:00

CollapsibleWidget: Resize the widget when contents change

Currently this is achieved by toggling the checked state twice
in a programmatic way. This ensures correct animations until
a better solution is found.

In addition, now both CollapsibleFrame and CollapsibleGroupBox
inherit their common methods from the abstract class CollapsibleBase.
This commit is contained in:
loathingKernel 2022-11-03 21:23:48 +02:00
parent da79519e80
commit 692ffa99bc

View file

@ -1,26 +1,104 @@
from PyQt5.QtCore import QParallelAnimationGroup, Qt, QPropertyAnimation, QAbstractAnimation
from PyQt5.QtWidgets import QApplication, QWidget, QFrame, QToolButton, QGridLayout, QSizePolicy, QGroupBox, QSpacerItem, QLabel
from abc import abstractmethod
from typing import Optional
from PyQt5.QtCore import QParallelAnimationGroup, Qt, QPropertyAnimation, QAbstractAnimation, QSize
from PyQt5.QtWidgets import QWidget, QFrame, QToolButton, QGridLayout, QSizePolicy, QGroupBox, QLabel
from rare.utils.misc import icon
# https://newbedev.com/how-to-make-an-expandable-collapsable-section-widget-in-qt
class CollapsibleBase(object):
"""
References:
# Adapted from c++ version
https://stackoverflow.com/questions/32476006/how-to-make-an-expandable-collapsable-section-widget-in-qt
# Adapted from python version
https://newbedev.com/how-to-make-an-expandable-collapsable-section-widget-in-qt
"""
def __init__(self):
self.animation_duration = None
self.toggle_animation = None
self.content_area = None
self.content_toggle_animation = None
def setup(self, animation_duration: int = 200):
self.animation_duration = animation_duration
self.content_area: Optional[QWidget] = None
self.content_toggle_animation: Optional[QPropertyAnimation] = None
# let the entire widget grow and shrink with its content
self.toggle_animation = QParallelAnimationGroup(self)
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
@abstractmethod
def isChecked(self) -> bool:
...
@abstractmethod
def click(self) -> None:
...
@abstractmethod
def addToLayout(self, widget: QWidget) -> None:
...
@abstractmethod
def sizeHint(self) -> QSize:
...
def animationStart(self, checked):
direction = QAbstractAnimation.Forward if checked else QAbstractAnimation.Backward
self.toggle_animation.setDirection(direction)
self.toggle_animation.start()
def setWidget(self, widget: QWidget):
if widget is None or widget is self.content_area:
return
is_checked = self.isChecked()
if self.content_area is not None:
# Collapse the parent before replacing the child
if is_checked:
self.click()
self.toggle_animation.removeAnimation(self.content_toggle_animation)
self.content_area.setParent(None)
self.content_area.deleteLater()
self.content_area = widget
self.content_area.setParent(self)
self.content_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
# start out collapsed
if not is_checked:
self.content_area.setMaximumHeight(0)
self.content_area.setMinimumHeight(0)
self.content_toggle_animation = QPropertyAnimation(self.content_area, b"maximumHeight")
self.toggle_animation.addAnimation(self.content_toggle_animation)
self.addToLayout(self.content_area)
collapsed_height = self.sizeHint().height()
content_height = self.content_area.sizeHint().height()
for i in range(self.toggle_animation.animationCount() - 1):
spoiler_animation = self.toggle_animation.animationAt(i)
spoiler_animation.setDuration(self.animation_duration)
spoiler_animation.setStartValue(collapsed_height)
spoiler_animation.setEndValue(collapsed_height + content_height)
content_animation = self.toggle_animation.animationAt(self.toggle_animation.animationCount() - 1)
content_animation.setDuration(self.animation_duration)
content_animation.setStartValue(0)
content_animation.setEndValue(content_height)
if is_checked:
self.click()
class CollapsibleFrame(QFrame):
class CollapsibleFrame(QFrame, CollapsibleBase):
def __init__(
self, widget: QWidget = None, title: str = "", button_text: str = "", animation_duration: int = 200, parent=None
):
"""
References:
# Adapted from c++ version
https://stackoverflow.com/questions/32476006/how-to-make-an-expandable-collapsable-section-widget-in-qt
"""
super(CollapsibleFrame, self).__init__(parent=parent)
self.setup(animation_duration)
self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
self.content_area = None
self.animation_duration = animation_duration
self.toggle_button = QToolButton(self)
self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggle_button.setIcon(icon("fa.arrow-right"))
@ -28,23 +106,18 @@ class CollapsibleFrame(QFrame):
self.toggle_button.setCheckable(True)
self.toggle_button.setChecked(False)
self.spacer_label = QLabel(title)
font = self.spacer_label.font()
self.title_label = QLabel(title)
font = self.title_label.font()
font.setBold(True)
self.spacer_label.setFont(font)
self.spacer_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# let the entire widget grow and shrink with its content
self.toggle_animation = QParallelAnimationGroup(self)
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
self.title_label.setFont(font)
self.title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# don't waste space
self.main_layout = QGridLayout(self)
self.main_layout.setVerticalSpacing(0)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.addWidget(self.toggle_button, 0, 0, 1, 1, Qt.AlignLeft)
self.main_layout.addWidget(self.spacer_label, 0, 1, 1, 1)
self.main_layout.addWidget(self.title_label, 0, 1, 1, 1)
self.main_layout.setColumnStretch(1, 1)
self.main_layout.setRowStretch(0, 0)
self.main_layout.setRowStretch(1, 1)
@ -55,119 +128,64 @@ class CollapsibleFrame(QFrame):
if widget is not None:
self.setWidget(widget)
def isChecked(self) -> bool:
return self.toggle_button.isChecked()
def click(self) -> None:
self.toggle_button.click()
def addToLayout(self, widget: QWidget) -> None:
self.main_layout.addWidget(widget, 1, 0, 1, 2)
def sizeHint(self) -> QSize:
return super(CollapsibleFrame, self).sizeHint()
def animationStart(self, checked):
arrow_type = icon("fa.arrow-down") if checked else icon("fa.arrow-right")
direction = QAbstractAnimation.Forward if checked else QAbstractAnimation.Backward
self.toggle_button.setIcon(arrow_type)
self.toggle_animation.setDirection(direction)
self.toggle_animation.start()
def setWidget(self, widget: QWidget):
if widget is None or widget is self.content_area:
return
if self.content_area is not None:
# Collapse the parent before replacing the child
if self.toggle_button.isChecked():
self.toggle_button.click()
self.toggle_animation.removeAnimation(self.content_toggle_animation)
self.content_area.setParent(None)
self.content_area.deleteLater()
self.content_area = widget
self.content_area.setParent(self)
self.content_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.adjustSize()
# start out collapsed
if not self.toggle_button.isChecked():
self.content_area.setMaximumHeight(0)
self.content_area.setMinimumHeight(0)
self.content_toggle_animation = QPropertyAnimation(self.content_area, b"maximumHeight")
self.toggle_animation.addAnimation(self.content_toggle_animation)
self.main_layout.addWidget(self.content_area, 1, 0, 1, 2)
collapsed_height = self.sizeHint().height() - self.content_area.maximumHeight()
content_height = self.content_area.sizeHint().height()
for i in range(self.toggle_animation.animationCount() - 1):
spoiler_animation = self.toggle_animation.animationAt(i)
spoiler_animation.setDuration(self.animation_duration)
spoiler_animation.setStartValue(collapsed_height)
spoiler_animation.setEndValue(collapsed_height + content_height)
content_animation = self.toggle_animation.animationAt(self.toggle_animation.animationCount() - 1)
content_animation.setDuration(self.animation_duration)
content_animation.setStartValue(0)
content_animation.setEndValue(content_height)
super(CollapsibleFrame, self).animationStart(checked)
class CollapsibleGroupBox(QGroupBox):
class CollapsibleGroupBox(QGroupBox, CollapsibleBase):
def __init__(
self, widget: QWidget = None, title: str = "", animation_duration: int = 200, parent=None
):
"""
References:
# Adapted from c++ version
https://stackoverflow.com/questions/32476006/how-to-make-an-expandable-collapsable-section-widget-in-qt
"""
super(CollapsibleGroupBox, self).__init__(parent=parent)
self.setup(animation_duration)
self.setTitle(title)
self.setCheckable(True)
self.setChecked(False)
self.content_area = None
self.animation_duration = animation_duration
# let the entire widget grow and shrink with its content
self.toggle_animation = QParallelAnimationGroup(self)
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
# don't waste space
self.main_layout = QVBoxLayout(self)
self.main_layout.setSpacing(0)
self.main_layout.setContentsMargins(0, 0, 0, -1)
self.setLayout(self.main_layout)
self.clicked.connect(self.animationStart)
self.toggled.connect(self.animationStart)
if widget is not None:
self.setWidget(widget)
def animationStart(self, checked):
direction = QAbstractAnimation.Forward if checked else QAbstractAnimation.Backward
self.toggle_animation.setDirection(direction)
self.toggle_animation.start()
def isChecked(self) -> bool:
return super(CollapsibleGroupBox, self).isChecked()
def setWidget(self, widget: QWidget):
if widget is None or widget is self.content_area:
return
if self.content_area is not None:
self.content_area.deleteLater()
self.content_area = widget
self.content_area.setParent(self)
self.content_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
# start out collapsed
self.setChecked(False)
self.content_area.setMaximumHeight(0)
self.content_area.setMinimumHeight(0)
self.toggle_animation.addAnimation(QPropertyAnimation(self.content_area, b"maximumHeight"))
self.main_layout.addWidget(self.content_area)
collapsed_height = self.sizeHint().height() - self.content_area.maximumHeight()
content_height = self.content_area.sizeHint().height()
for i in range(self.toggle_animation.animationCount() - 1):
spoiler_animation = self.toggle_animation.animationAt(i)
spoiler_animation.setDuration(self.animation_duration)
spoiler_animation.setStartValue(collapsed_height)
spoiler_animation.setEndValue(collapsed_height + content_height)
content_animation = self.toggle_animation.animationAt(self.toggle_animation.animationCount() - 1)
content_animation.setDuration(self.animation_duration)
content_animation.setStartValue(0)
content_animation.setEndValue(content_height)
def click(self) -> None:
self.setChecked(not self.isChecked())
def addToLayout(self, widget: QWidget) -> None:
self.main_layout.addWidget(widget)
def sizeHint(self) -> QSize:
return super(CollapsibleGroupBox, self).sizeHint()
if __name__ == "__main__":
import sys
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QPushButton
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout
from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced
import rare.resources.stylesheets.RareStyle
from rare.utils.misc import set_color_pallete, set_style_sheet
from rare.utils.misc import set_style_sheet
from rare.resources.stylesheets import RareStyle
app = QApplication(sys.argv)
set_style_sheet("RareStyle")
@ -178,7 +196,7 @@ if __name__ == "__main__":
collapsible_frame = CollapsibleFrame(widget_frame, title="Frame me!")
collapsible_frame.setDisabled(False)
def replace_func(state):
def replace_func_frame(state):
widget2_frame = QWidget()
ui2_frame = Ui_InstallDialogAdvanced()
ui2_frame.setupUi(widget2_frame)
@ -187,18 +205,28 @@ if __name__ == "__main__":
ui2_frame.install_dialog_advanced_layout.removeRow(4)
collapsible_frame.setWidget(widget2_frame)
replace_button = QToolButton()
replace_button.setText("Replace me!")
replace_button.setCheckable(True)
replace_button.setChecked(False)
replace_button.clicked.connect(replace_func)
ui_group = Ui_InstallDialogAdvanced()
widget_group = QWidget()
ui_group.setupUi(widget_group)
collapsible_group = CollapsibleGroupBox(widget_group, title="Group me!")
collapsible_group.setDisabled(False)
def replace_func_group(state):
widget2_group = QWidget()
ui2_group = Ui_InstallDialogAdvanced()
ui2_group.setupUi(widget2_group)
if state:
ui2_group.install_dialog_advanced_layout.removeRow(3)
ui2_group.install_dialog_advanced_layout.removeRow(4)
collapsible_group.setWidget(widget2_group)
replace_button = QToolButton()
replace_button.setText("Replace me!")
replace_button.setCheckable(True)
replace_button.setChecked(False)
replace_button.clicked.connect(replace_func_frame)
replace_button.clicked.connect(replace_func_group)
dialog = QDialog()
dialog.setLayout(QVBoxLayout())
dialog.layout().addWidget(replace_button)