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:
parent
da79519e80
commit
692ffa99bc
1 changed files with 142 additions and 114 deletions
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue