Source code for edifice.base_components.flow_view

# https://doc.qt.io/qtforpython-6/examples/example_widgets_layouts_flowlayout.html

# Copyright (C) 2013 Riverbank Computing Limited.
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

"""PySide6 port of the widgets/layouts/flowlayout example from Qt v6.x"""

from __future__ import annotations

import logging
import typing

from edifice.qt import QT_VERSION

if QT_VERSION == "PyQt6" and not typing.TYPE_CHECKING:
    from PyQt6.QtCore import QPoint, QRect, QSize, Qt
    from PyQt6.QtWidgets import QLayout, QLayoutItem, QSizePolicy, QWidget
else:
    from PySide6.QtCore import QPoint, QRect, QSize, Qt
    from PySide6.QtWidgets import QLayout, QLayoutItem, QSizePolicy, QWidget

from .base_components import Element, QtWidgetElement, _get_widget_children, _LinearView, _WidgetTree

logger = logging.getLogger("Edifice")


class FlowLayout(QLayout):
    def __init__(self, parent=None):
        super().__init__(parent)

        self._item_list: list[QLayoutItem] = []

    def __del__(self):
        item = self.takeAt(0)
        while item:
            item = self.takeAt(0)

    def addItem(self, item):
        self._item_list.append(item)

    def count(self):
        return len(self._item_list)

    def itemAt(self, index):
        if 0 <= index < len(self._item_list):
            return self._item_list[index]

        return None

    def takeAt(self, index):
        if 0 <= index < len(self._item_list):
            return self._item_list.pop(index)

        return None

    # We need insertWidget like the one in QBoxLayout to support Edifice.
    # https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QBoxLayout.html#PySide6.QtWidgets.QBoxLayout.insertWidget
    #
    # But the QLayout API gives us only addWidget and removeWidget.
    # https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QLayout.html
    #
    # So we have to write insertWidget in terms of addWidget and removeWidget.
    #
    # This is crazy, but maybe we won’t do a lot of inserting into the middle
    # of a large FlowLayout?
    def insertWidget(self, index, w: QWidget):
        stack = []
        while len(self._item_list) > index:
            w_ = self._item_list[-1].widget()
            self.removeWidget(w_)
            stack.append(w_)
        self.addWidget(w)
        while len(stack) > 0:
            self.addWidget(stack.pop())

    def expandingDirections(self):
        return Qt.Orientation(0)

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, width):
        height = self._do_layout(QRect(0, 0, width, 0), True)
        return height

    def setGeometry(self, rect):
        super().setGeometry(rect)
        self._do_layout(rect, False)

    def sizeHint(self):
        return self.minimumSize()

    def minimumSize(self):
        size = QSize()

        for item in self._item_list:
            size = size.expandedTo(item.minimumSize())

        size += QSize(2 * self.contentsMargins().top(), 2 * self.contentsMargins().top())
        return size

    def _do_layout(self, rect, test_only):
        x = rect.x()
        y = rect.y()
        line_height = 0
        spacing = self.spacing()

        for item in self._item_list:
            style = item.widget().style()
            layout_spacing_x = style.layoutSpacing(
                QSizePolicy.ControlType.PushButton,
                QSizePolicy.ControlType.PushButton,
                Qt.Orientation.Horizontal,
            )
            layout_spacing_y = style.layoutSpacing(
                QSizePolicy.ControlType.PushButton,
                QSizePolicy.ControlType.PushButton,
                Qt.Orientation.Vertical,
            )
            space_x = spacing + layout_spacing_x
            space_y = spacing + layout_spacing_y
            next_x = x + item.sizeHint().width() + space_x
            if next_x - space_x > rect.right() and line_height > 0:
                x = rect.x()
                y = y + line_height + space_y
                next_x = x + item.sizeHint().width() + space_x
                line_height = 0

            if not test_only:
                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))

            x = next_x
            line_height = max(line_height, item.sizeHint().height())

        return y + line_height - rect.y()


[docs] class FlowView(_LinearView[QWidget]): """ Flow-style layout widget. Displays its children horizonally left-to-right and wraps into multiple rows. The height of each row is determined by the tallest child in that row. This component has similar behavior to an `HTML CSS wrap flex container <https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap>`_. .. note:: The :class:`FlowView` element is implemented in Python because Qt does not provide any :code:`QLayout` which behaves this way. Currently the :class:`FlowView` has O(N\ :sup:`2`) time complexity for adding children because of technical limitation of the Qt API. """
[docs] def __init__(self, **kwargs): super().__init__(**kwargs) self.underlying = None
def _delete_child(self, i, old_child: QtWidgetElement): assert self.underlying_layout is not None child_node = self.underlying_layout.takeAt(i) assert child_node is not None child_node.widget().deleteLater() def _soft_delete_child(self, i, old_child: QtWidgetElement): assert self.underlying_layout is not None child_node = self.underlying_layout.takeAt(i) assert child_node is not None def _add_child(self, i, child_component: QWidget): self.underlying_layout.insertWidget(i, child_component) def _initialize(self): self.underlying = QWidget() self.underlying_layout = FlowLayout() self.underlying.setObjectName(str(id(self))) if self.underlying_layout is not None: self.underlying.setLayout(self.underlying_layout) self.underlying_layout.setContentsMargins(0, 0, 0, 0) self.underlying_layout.setSpacing(0) else: self.underlying.setMinimumSize(100, 100) def _qt_update_commands( self, widget_trees: dict[Element, _WidgetTree], newprops, ): if self.underlying is None: self._initialize() assert self.underlying is not None children = _get_widget_children(widget_trees, self) commands = self._recompute_children(children) commands.extend( super()._qt_update_commands_super(widget_trees, newprops, self.underlying, self.underlying_layout) ) return commands