1
0
Fork 0
mirror of synced 2024-05-18 11:32:50 +12:00
Rare/rare/widgets/collapsible_widget.py
2024-02-21 13:30:41 +02:00

246 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,
QVBoxLayout,
QGridLayout,
QSizePolicy,
QGroupBox,
QLabel,
)
from rare.utils.misc import qta_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:
pass
@abstractmethod
def click(self) -> None:
pass
@abstractmethod
def addToLayout(self, widget: QWidget) -> None:
pass
@abstractmethod
def sizeHint(self) -> QSize:
pass
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, 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(qta_icon("fa.arrow-right"))
self.toggle_button.setCheckable(True)
self.toggle_button.setChecked(False)
self.title_label = QLabel(self)
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)
def setTitle(self, title: str):
self.title_label.setText(title)
def setText(self, text: str):
self.toggle_button.setText(text)
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 = qta_icon("fa.arrow-down") if checked else qta_icon("fa.arrow-right")
self.toggle_button.setIcon(arrow_type)
super(CollapsibleFrame, self).animationStart(checked)
class CollapsibleGroupBox(QGroupBox, CollapsibleBase):
def __init__(self, animation_duration: int = 200, parent=None):
super(CollapsibleGroupBox, self).__init__(parent=parent)
self.setup(animation_duration)
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)
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
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()
collapsible_frame.setWidget(widget_frame)
collapsible_frame.setTitle("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()
collapsible_group.setWidget(widget_group)
collapsible_group.setTitle("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_())