YoutubeDownloader/app/ui/layouts/flow_layout.py
Louis Mazin 32a4a157be rebuild
2026-06-28 17:52:15 +02:00

279 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()