278 lines
7.4 KiB
Python
278 lines
7.4 KiB
Python
from enum import Enum
|
|
|
|
from PyQt6.QtCore import Qt, QRect, QSize, QPoint, QMargins
|
|
from PyQt6.QtWidgets import QLayout
|
|
|
|
|
|
class JustifyMode(Enum):
|
|
NONE = 0
|
|
SPACE_BETWEEN = 1
|
|
SPACE_AROUND = 2
|
|
SPACE_EVENLY = 3
|
|
CENTER = 4
|
|
|
|
|
|
class LastRowMode(Enum):
|
|
LEFT = 0
|
|
JUSTIFY = 1
|
|
MATCH_PREVIOUS = 2
|
|
CENTER = 3
|
|
|
|
|
|
class FlowLayout(QLayout):
|
|
def __init__(
|
|
self,
|
|
parent=None,
|
|
justify_mode=JustifyMode.SPACE_BETWEEN,
|
|
last_row_mode=LastRowMode.LEFT,
|
|
):
|
|
super().__init__(parent)
|
|
|
|
if parent is not None:
|
|
self.setContentsMargins(QMargins(0, 0, 0, 0))
|
|
|
|
self._items = []
|
|
self.justify_mode = justify_mode
|
|
self.last_row_mode = last_row_mode
|
|
|
|
# ------------------------------------------------------------------
|
|
# QLayout API
|
|
# ------------------------------------------------------------------
|
|
|
|
def addItem(self, item):
|
|
self._items.append(item)
|
|
|
|
def count(self):
|
|
return len(self._items)
|
|
|
|
def itemAt(self, index):
|
|
if 0 <= index < len(self._items):
|
|
return self._items[index]
|
|
return None
|
|
|
|
def takeAt(self, index):
|
|
if 0 <= index < len(self._items):
|
|
return self._items.pop(index)
|
|
return None
|
|
|
|
def expandingDirections(self):
|
|
return Qt.Orientation(0)
|
|
|
|
def hasHeightForWidth(self):
|
|
return True
|
|
|
|
def heightForWidth(self, width):
|
|
return self._do_layout(QRect(0, 0, width, 0), True)
|
|
|
|
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._items:
|
|
size = size.expandedTo(item.minimumSize())
|
|
|
|
margins = self.contentsMargins()
|
|
|
|
size += QSize(
|
|
margins.left() + margins.right(),
|
|
margins.top() + margins.bottom(),
|
|
)
|
|
|
|
return size
|
|
|
|
# ------------------------------------------------------------------
|
|
# Internal
|
|
# ------------------------------------------------------------------
|
|
|
|
def _build_rows(self, available_width):
|
|
rows = []
|
|
|
|
current_row = []
|
|
current_width = 0
|
|
|
|
base_spacing = max(0, self.spacing())
|
|
|
|
for item in self._items:
|
|
width = item.sizeHint().width()
|
|
|
|
if not current_row:
|
|
current_row.append(item)
|
|
current_width = width
|
|
continue
|
|
|
|
projected = current_width + base_spacing + width
|
|
|
|
if projected > available_width:
|
|
rows.append(current_row)
|
|
|
|
current_row = [item]
|
|
current_width = width
|
|
else:
|
|
current_row.append(item)
|
|
current_width = projected
|
|
|
|
if current_row:
|
|
rows.append(current_row)
|
|
|
|
return rows
|
|
|
|
def _layout_row(
|
|
self,
|
|
row,
|
|
rect,
|
|
y,
|
|
test_only,
|
|
justify_mode,
|
|
spacing_override=None,
|
|
):
|
|
widths = [item.sizeHint().width() for item in row]
|
|
heights = [item.sizeHint().height() for item in row]
|
|
|
|
total_width = sum(widths)
|
|
row_height = max(heights)
|
|
|
|
count = len(row)
|
|
gaps = max(0, count - 1)
|
|
|
|
available_width = rect.width()
|
|
|
|
x = rect.x()
|
|
|
|
used_spacing = self.spacing()
|
|
|
|
# --------------------------------------------------------------
|
|
# Explicit spacing override (MATCH_PREVIOUS)
|
|
# --------------------------------------------------------------
|
|
|
|
if spacing_override is not None:
|
|
used_spacing = spacing_override
|
|
|
|
for item, w, h in zip(row, widths, heights):
|
|
if not test_only:
|
|
item.setGeometry(QRect(int(round(x)), y, w, h))
|
|
|
|
x += w + used_spacing
|
|
|
|
return row_height, used_spacing
|
|
|
|
# --------------------------------------------------------------
|
|
# LEFT / NONE
|
|
# --------------------------------------------------------------
|
|
|
|
if justify_mode == JustifyMode.NONE:
|
|
used_spacing = max(0, self.spacing())
|
|
|
|
for item, w, h in zip(row, widths, heights):
|
|
if not test_only:
|
|
item.setGeometry(QRect(int(round(x)), y, w, h))
|
|
|
|
x += w + used_spacing
|
|
|
|
return row_height, used_spacing
|
|
|
|
# --------------------------------------------------------------
|
|
# CENTER
|
|
# --------------------------------------------------------------
|
|
|
|
if justify_mode == JustifyMode.CENTER:
|
|
used_spacing = max(0, self.spacing())
|
|
|
|
content_width = total_width
|
|
|
|
if count > 1:
|
|
content_width += gaps * used_spacing
|
|
|
|
x += max(0, (available_width - content_width) / 2)
|
|
|
|
for item, w, h in zip(row, widths, heights):
|
|
if not test_only:
|
|
item.setGeometry(QRect(int(round(x)), y, w, h))
|
|
|
|
x += w + used_spacing
|
|
|
|
return row_height, used_spacing
|
|
|
|
# --------------------------------------------------------------
|
|
# SPACE_*
|
|
# --------------------------------------------------------------
|
|
|
|
free_space = max(0, available_width - total_width)
|
|
|
|
if justify_mode == JustifyMode.SPACE_BETWEEN:
|
|
if gaps > 0:
|
|
used_spacing = free_space / gaps
|
|
else:
|
|
used_spacing = 0
|
|
|
|
elif justify_mode == JustifyMode.SPACE_AROUND:
|
|
used_spacing = free_space / count if count else 0
|
|
x += used_spacing / 2
|
|
|
|
elif justify_mode == JustifyMode.SPACE_EVENLY:
|
|
used_spacing = free_space / (count + 1) if count else 0
|
|
x += used_spacing
|
|
|
|
for item, w, h in zip(row, widths, heights):
|
|
if not test_only:
|
|
item.setGeometry(QRect(int(round(x)), y, w, h))
|
|
|
|
x += w + used_spacing
|
|
|
|
return row_height, used_spacing
|
|
|
|
def _do_layout(self, rect, test_only):
|
|
rows = self._build_rows(rect.width())
|
|
|
|
y = rect.y()
|
|
|
|
vertical_spacing = max(0, self.spacing())
|
|
|
|
previous_spacing = None
|
|
|
|
for row_index, row in enumerate(rows):
|
|
is_last_row = row_index == len(rows) - 1
|
|
|
|
spacing_override = None
|
|
row_mode = self.justify_mode
|
|
|
|
if is_last_row:
|
|
|
|
if self.last_row_mode == LastRowMode.LEFT:
|
|
row_mode = JustifyMode.NONE
|
|
|
|
elif self.last_row_mode == LastRowMode.CENTER:
|
|
row_mode = JustifyMode.CENTER
|
|
|
|
elif self.last_row_mode == LastRowMode.JUSTIFY:
|
|
row_mode = self.justify_mode
|
|
|
|
elif (
|
|
self.last_row_mode == LastRowMode.MATCH_PREVIOUS
|
|
and previous_spacing is not None
|
|
):
|
|
spacing_override = previous_spacing
|
|
row_mode = JustifyMode.NONE
|
|
|
|
row_height, used_spacing = self._layout_row(
|
|
row=row,
|
|
rect=rect,
|
|
y=y,
|
|
test_only=test_only,
|
|
justify_mode=row_mode,
|
|
spacing_override=spacing_override,
|
|
)
|
|
|
|
previous_spacing = used_spacing
|
|
|
|
y += row_height + vertical_spacing
|
|
|
|
if rows:
|
|
y -= vertical_spacing
|
|
|
|
return y - rect.y() |