1
0
Fork 0
mirror of synced 2024-05-19 12:02:54 +12:00
Rare/rare/widgets/collapsible_widget.py
loathingKernel 692ffa99bc 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.
2022-11-03 21:23:48 +02:00

239 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_())