1
0
Fork 0
mirror of synced 2024-10-01 01:37:51 +13: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 abc import abstractmethod
from PyQt5.QtWidgets import QApplication, QWidget, QFrame, QToolButton, QGridLayout, QSizePolicy, QGroupBox, QSpacerItem, QLabel 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 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__( def __init__(
self, widget: QWidget = None, title: str = "", button_text: str = "", animation_duration: int = 200, parent=None 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) super(CollapsibleFrame, self).__init__(parent=parent)
self.setup(animation_duration)
self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
self.content_area = None
self.animation_duration = animation_duration
self.toggle_button = QToolButton(self) self.toggle_button = QToolButton(self)
self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggle_button.setIcon(icon("fa.arrow-right")) self.toggle_button.setIcon(icon("fa.arrow-right"))
@ -28,23 +106,18 @@ class CollapsibleFrame(QFrame):
self.toggle_button.setCheckable(True) self.toggle_button.setCheckable(True)
self.toggle_button.setChecked(False) self.toggle_button.setChecked(False)
self.spacer_label = QLabel(title) self.title_label = QLabel(title)
font = self.spacer_label.font() font = self.title_label.font()
font.setBold(True) font.setBold(True)
self.spacer_label.setFont(font) self.title_label.setFont(font)
self.spacer_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.title_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"))
# don't waste space # don't waste space
self.main_layout = QGridLayout(self) self.main_layout = QGridLayout(self)
self.main_layout.setVerticalSpacing(0) self.main_layout.setVerticalSpacing(0)
self.main_layout.setContentsMargins(0, 0, 0, 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.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.setColumnStretch(1, 1)
self.main_layout.setRowStretch(0, 0) self.main_layout.setRowStretch(0, 0)
self.main_layout.setRowStretch(1, 1) self.main_layout.setRowStretch(1, 1)
@ -55,119 +128,64 @@ class CollapsibleFrame(QFrame):
if widget is not None: if widget is not None:
self.setWidget(widget) 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): def animationStart(self, checked):
arrow_type = icon("fa.arrow-down") if checked else icon("fa.arrow-right") 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_button.setIcon(arrow_type)
self.toggle_animation.setDirection(direction) super(CollapsibleFrame, self).animationStart(checked)
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)
class CollapsibleGroupBox(QGroupBox): class CollapsibleGroupBox(QGroupBox, CollapsibleBase):
def __init__( def __init__(
self, widget: QWidget = None, title: str = "", animation_duration: int = 200, parent=None 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) super(CollapsibleGroupBox, self).__init__(parent=parent)
self.setup(animation_duration)
self.setTitle(title) self.setTitle(title)
self.setCheckable(True) 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 # don't waste space
self.main_layout = QVBoxLayout(self) self.main_layout = QVBoxLayout(self)
self.main_layout.setSpacing(0) self.main_layout.setSpacing(0)
self.main_layout.setContentsMargins(0, 0, 0, -1) self.main_layout.setContentsMargins(0, 0, 0, -1)
self.setLayout(self.main_layout) self.setLayout(self.main_layout)
self.clicked.connect(self.animationStart) self.toggled.connect(self.animationStart)
if widget is not None: if widget is not None:
self.setWidget(widget) self.setWidget(widget)
def animationStart(self, checked): def isChecked(self) -> bool:
direction = QAbstractAnimation.Forward if checked else QAbstractAnimation.Backward return super(CollapsibleGroupBox, self).isChecked()
self.toggle_animation.setDirection(direction)
self.toggle_animation.start()
def setWidget(self, widget: QWidget): def click(self) -> None:
if widget is None or widget is self.content_area: self.setChecked(not self.isChecked())
return
if self.content_area is not None: def addToLayout(self, widget: QWidget) -> None:
self.content_area.deleteLater() self.main_layout.addWidget(widget)
self.content_area = widget
self.content_area.setParent(self) def sizeHint(self) -> QSize:
self.content_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) return super(CollapsibleGroupBox, self).sizeHint()
# 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)
if __name__ == "__main__": if __name__ == "__main__":
import sys 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 from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced
import rare.resources.stylesheets.RareStyle from rare.utils.misc import set_style_sheet
from rare.utils.misc import set_color_pallete, set_style_sheet from rare.resources.stylesheets import RareStyle
app = QApplication(sys.argv) app = QApplication(sys.argv)
set_style_sheet("RareStyle") set_style_sheet("RareStyle")
@ -178,7 +196,7 @@ if __name__ == "__main__":
collapsible_frame = CollapsibleFrame(widget_frame, title="Frame me!") collapsible_frame = CollapsibleFrame(widget_frame, title="Frame me!")
collapsible_frame.setDisabled(False) collapsible_frame.setDisabled(False)
def replace_func(state): def replace_func_frame(state):
widget2_frame = QWidget() widget2_frame = QWidget()
ui2_frame = Ui_InstallDialogAdvanced() ui2_frame = Ui_InstallDialogAdvanced()
ui2_frame.setupUi(widget2_frame) ui2_frame.setupUi(widget2_frame)
@ -187,18 +205,28 @@ if __name__ == "__main__":
ui2_frame.install_dialog_advanced_layout.removeRow(4) ui2_frame.install_dialog_advanced_layout.removeRow(4)
collapsible_frame.setWidget(widget2_frame) 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() ui_group = Ui_InstallDialogAdvanced()
widget_group = QWidget() widget_group = QWidget()
ui_group.setupUi(widget_group) ui_group.setupUi(widget_group)
collapsible_group = CollapsibleGroupBox(widget_group, title="Group me!") collapsible_group = CollapsibleGroupBox(widget_group, title="Group me!")
collapsible_group.setDisabled(False) 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 = QDialog()
dialog.setLayout(QVBoxLayout()) dialog.setLayout(QVBoxLayout())
dialog.layout().addWidget(replace_button) dialog.layout().addWidget(replace_button)