692ffa99bc
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.
238 lines
8.7 KiB
Python
238 lines
8.7 KiB
Python
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
|
|
|
|
|
|
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, CollapsibleBase):
|
|
def __init__(
|
|
self, widget: QWidget = None, title: str = "", button_text: str = "", animation_duration: int = 200, parent=None
|
|
):
|
|
super(CollapsibleFrame, self).__init__(parent=parent)
|
|
self.setup(animation_duration)
|
|
self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
|
|
|
|
self.toggle_button = QToolButton(self)
|
|
self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
|
self.toggle_button.setIcon(icon("fa.arrow-right"))
|
|
self.toggle_button.setText(button_text)
|
|
self.toggle_button.setCheckable(True)
|
|
self.toggle_button.setChecked(False)
|
|
|
|
self.title_label = QLabel(title)
|
|
font = self.title_label.font()
|
|
font.setBold(True)
|
|
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.title_label, 0, 1, 1, 1)
|
|
self.main_layout.setColumnStretch(1, 1)
|
|
self.main_layout.setRowStretch(0, 0)
|
|
self.main_layout.setRowStretch(1, 1)
|
|
self.setLayout(self.main_layout)
|
|
|
|
self.toggle_button.clicked.connect(self.animationStart)
|
|
|
|
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")
|
|
self.toggle_button.setIcon(arrow_type)
|
|
super(CollapsibleFrame, self).animationStart(checked)
|
|
|
|
|
|
class CollapsibleGroupBox(QGroupBox, CollapsibleBase):
|
|
def __init__(
|
|
self, widget: QWidget = None, title: str = "", animation_duration: int = 200, parent=None
|
|
):
|
|
super(CollapsibleGroupBox, self).__init__(parent=parent)
|
|
self.setup(animation_duration)
|
|
self.setTitle(title)
|
|
self.setCheckable(True)
|
|
self.setChecked(False)
|
|
|
|
# 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.toggled.connect(self.animationStart)
|
|
|
|
if widget is not None:
|
|
self.setWidget(widget)
|
|
|
|
def isChecked(self) -> bool:
|
|
return super(CollapsibleGroupBox, self).isChecked()
|
|
|
|
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
|
|
from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced
|
|
from rare.utils.misc import set_style_sheet
|
|
from rare.resources.stylesheets import RareStyle
|
|
app = QApplication(sys.argv)
|
|
|
|
set_style_sheet("RareStyle")
|
|
|
|
ui_frame = Ui_InstallDialogAdvanced()
|
|
widget_frame = QWidget()
|
|
ui_frame.setupUi(widget_frame)
|
|
collapsible_frame = CollapsibleFrame(widget_frame, title="Frame me!")
|
|
collapsible_frame.setDisabled(False)
|
|
|
|
def replace_func_frame(state):
|
|
widget2_frame = QWidget()
|
|
ui2_frame = Ui_InstallDialogAdvanced()
|
|
ui2_frame.setupUi(widget2_frame)
|
|
if state:
|
|
ui2_frame.install_dialog_advanced_layout.removeRow(3)
|
|
ui2_frame.install_dialog_advanced_layout.removeRow(4)
|
|
collapsible_frame.setWidget(widget2_frame)
|
|
|
|
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)
|
|
dialog.layout().addWidget(collapsible_frame)
|
|
dialog.layout().addWidget(collapsible_group)
|
|
dialog.layout().setSizeConstraint(QVBoxLayout.SetFixedSize)
|
|
dialog.show()
|
|
sys.exit(app.exec_())
|
|
|