Junior Tech Artist here!
Hey there I have adapted a FlowLayout into a custom Collapsible Scroll Area and am getting a few issues (due to my lack of Qt knowledge).
I found the FlowLayout by someone else from about 7 years ago, and have just switched out the QtCore for QtWidgets where it was erroring. It seems to be working properly.
However when I put it into the custom Collapsible Scroll Area it has issues when you open one of the collapsed areas. The QVBoxLayout housing the FlowLayout does not resize to the FlowLayout vertically. However when you resize the window horizontally the layout stretches out and properly shows the FlowLayout (if they end up in a single line).
My goals here were:
- Have the layout open to the correct size depending on the amount of space the FlowLayout needs, and also to resize when you adjust the window.
- To have the collapse resize the distance between the other collapsed layouts. They seem to get really funky the more you open/close.
- Resolve the need to have the layout focused(? highlighted ? clicked twice ?) in order to collapse.
My thoughts are that the TestWindow()'s box_inner_layout
is not correctly resizing? A colleague mentioned about doing a resizeEvent
(?), but he doesn’t have time to look at it, as this is a personal project. I’ve never rewritten a base class function. Whoever wrote the FlowLayout knew far more than I, and was able to rewrite a few events it seems to make that part work correctly.
Any help/insight would be huuuugely appreciated!
Sorry for the dump of code but the easiest way I could share exactly what’s being used.
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets
from shiboken2 import wrapInstance
import os
import maya.cmds as cmds
import maya.OpenMayaUI as omui
def maya_main_window():
"""
Return the Maya main window widget as a Python object
"""
main_window_ptr = omui.MQtUtil.mainWindow()
if not main_window_ptr:
return None
return wrapInstance(long(main_window_ptr), QtWidgets.QWidget)
class TestWindow(QtWidgets.QDialog):
def __init__(self, parent=maya_main_window()):
super(TestWindow, self).__init__(parent)
window_title = "test_window"
self.setWindowTitle(window_title)
if cmds.about(ntOS=True):
self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
elif cmds.about(macOS=True):
self.setWindowFlags(QtCore.Qt.Tool)
self.create_content()
self.setMinimumWidth(211)
def create_content(self):
main_layout = QtWidgets.QVBoxLayout(self)
for i in range(10):
collapse_box = CollapsibleBox(str(i))
main_layout.addWidget(collapse_box)
box_inner_layout = QtWidgets.QVBoxLayout(collapse_box)
flow = FlowLayout(parent=None)
for j in range(12):
btn = QtWidgets.QPushButton()
btn.setFixedSize(36, 36)
flow.addWidget(btn)
box_inner_layout.addLayout(flow)
collapse_box.setContentLayout(box_inner_layout)
class CollapsibleBox(QtWidgets.QWidget):
def __init__(self, title="", parent=None):
super(CollapsibleBox, self).__init__(parent)
self.toggle_button = QtWidgets.QToolButton(
text=title, checkable=True, checked=False
)
self.toggle_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
self.toggle_button.setArrowType(QtCore.Qt.RightArrow)
self.toggle_button.pressed.connect(self.on_pressed)
self.toggle_animation = QtCore.QParallelAnimationGroup(self)
self.content_area = QtWidgets.QScrollArea(maximumHeight=0, minimumHeight=0)
self.content_area.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
lay = QtWidgets.QVBoxLayout(self)
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.toggle_button)
lay.addWidget(self.content_area)
self.toggle_animation.addAnimation(
QtCore.QPropertyAnimation(self, b"minimumHeight")
)
self.toggle_animation.addAnimation(
QtCore.QPropertyAnimation(self, b"maximumHeight")
)
self.toggle_animation.addAnimation(
QtCore.QPropertyAnimation(self.content_area, b"maximumHeight")
)
def on_pressed(self):
checked = self.toggle_button.isChecked()
print(checked)
self.toggle_button.setArrowType(
QtCore.Qt.DownArrow if not checked else QtCore.Qt.RightArrow
)
self.toggle_animation.setDirection(
QtCore.QAbstractAnimation.Forward
if not checked
else QtCore.QAbstractAnimation.Backward
)
self.toggle_animation.start()
def setContentLayout(self, layout):
lay = self.content_area.layout()
del lay
self.content_area.setLayout(layout)
collapsed_height = (
self.sizeHint().height() - self.content_area.maximumHeight()
)
content_height = layout.sizeHint().height()
for i in range(self.toggle_animation.animationCount()):
animation = self.toggle_animation.animationAt(i)
animation.setDuration(0)
animation.setStartValue(collapsed_height)
animation.setEndValue(collapsed_height + content_height)
content_animation = self.toggle_animation.animationAt(
self.toggle_animation.animationCount() - 1
)
content_animation.setDuration(0)
content_animation.setStartValue(0)
content_animation.setEndValue(content_height)
class FlowLayout(QtWidgets.QLayout):
"""Custom layout that mimics the behaviour of a flow layout"""
def __init__(self, parent=None, margin=0, spacing=2):
"""
Create a new FlowLayout instance.
This layout will reorder the items automatically.
@param parent (QWidget)
@param margin (int)
@param spacing (int)
"""
super(FlowLayout, self).__init__(parent)
# Set margin and spacing
if parent is not None: self.setMargin(margin)
self.setSpacing(spacing)
self.itemList = []
def __del__(self):
"""Delete all the items in this layout"""
item = self.takeAt(0)
while item:
item = self.takeAt(0)
def addItem(self, item):
"""Add an item at the end of the layout.
This is automatically called when you do addWidget()
item (QWidgetItem)"""
self.itemList.append(item)
def count(self):
"""Get the number of items in the this layout
@return (int)"""
return len(self.itemList)
def itemAt(self, index):
"""Get the item at the given index
@param index (int)
@return (QWidgetItem)"""
if index >= 0 and index < len(self.itemList):
return self.itemList[index]
return None
def takeAt(self, index):
"""Remove an item at the given index
@param index (int)
@return (None)"""
if index >= 0 and index < len(self.itemList):
return self.itemList.pop(index)
return None
def insertWidget(self, index, widget):
"""Insert a widget at a given index
@param index (int)
@param widget (QWidget)"""
item = QtGui.QWidgetItem(widget)
self.itemList.insert(index, item)
def expandingDirections(self):
"""This layout grows only in the horizontal dimension"""
return QtCore.Qt.Orientations(QtCore.Qt.Horizontal)
def hasHeightForWidth(self):
"""If this layout's preferred height depends on its width
@return (boolean) Always True"""
return True
def heightForWidth(self, width):
"""Get the preferred height a layout item with the given width
@param width (int)"""
height = self.doLayout(QtCore.QRect(0, 0, width, 0), True)
return height
def setGeometry(self, rect):
"""Set the geometry of this layout
@param rect (QRect)"""
super(FlowLayout, self).setGeometry(rect)
self.doLayout(rect, False)
def sizeHint(self):
"""Get the preferred size of this layout
@return (QSize) The minimum size"""
return self.minimumSize()
def minimumSize(self):
"""Get the minimum size of this layout
@return (QSize)"""
# Calculate the size
size = QtCore.QSize()
for item in self.itemList:
size = size.expandedTo(item.minimumSize())
# Add the margins
size += QtCore.QSize(2 * self.margin(), 2 * self.margin())
return size
def doLayout(self, rect, testOnly):
"""Layout all the items
@param rect (QRect) Rect where in the items have to be laid out
@param testOnly (boolean) Do the actual layout"""
x = rect.x()
y = rect.y()
lineHeight = 0
for item in self.itemList:
wid = item.widget()
spaceX = self.spacing()
spaceY = self.spacing()
nextX = x + item.sizeHint().width() + spaceX
if nextX - spaceX > rect.right() and lineHeight > 0:
x = rect.x()
y = y + lineHeight + spaceY
nextX = x + item.sizeHint().width() + spaceX
lineHeight = 0
if not testOnly:
item.setGeometry(QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint()))
x = nextX
lineHeight = max(lineHeight, item.sizeHint().height())
return y + lineHeight - rect.y()
gui = TestWindow()
gui.show()