Move repo from git to gitea
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
393
portprotonqt/custom_widgets.py
Normal file
393
portprotonqt/custom_widgets.py
Normal file
@ -0,0 +1,393 @@
|
||||
import numpy as np
|
||||
from PySide6.QtWidgets import QLabel, QPushButton, QWidget, QLayout, QStyleOption, QLayoutItem
|
||||
from PySide6.QtCore import Qt, Signal, QRect, QPoint, QSize
|
||||
from PySide6.QtGui import QFont, QFontMetrics, QPainter
|
||||
|
||||
def compute_layout(nat_sizes, rect_width, spacing, max_scale):
|
||||
"""
|
||||
Вычисляет расположение элементов с учетом отступов и возможного увеличения карточек.
|
||||
nat_sizes: массив (N, 2) с натуральными размерами элементов (ширина, высота).
|
||||
rect_width: доступная ширина контейнера.
|
||||
spacing: отступ между элементами.
|
||||
max_scale: максимальный коэффициент масштабирования (например, 1.2).
|
||||
|
||||
Возвращает:
|
||||
result: массив (N, 4), где каждая строка содержит [x, y, new_width, new_height].
|
||||
total_height: итоговая высота всех рядов.
|
||||
"""
|
||||
N = nat_sizes.shape[0]
|
||||
result = np.zeros((N, 4), dtype=np.int32)
|
||||
y = 0
|
||||
i = 0
|
||||
while i < N:
|
||||
sum_width = 0
|
||||
row_max_height = 0
|
||||
count = 0
|
||||
j = i
|
||||
# Подбираем количество элементов для текущего ряда
|
||||
while j < N:
|
||||
w = nat_sizes[j, 0]
|
||||
# Если уже есть хотя бы один элемент и следующий не помещается с учетом spacing, выходим
|
||||
if count > 0 and (sum_width + spacing + w) > rect_width:
|
||||
break
|
||||
sum_width += w
|
||||
count += 1
|
||||
h = nat_sizes[j, 1]
|
||||
if h > row_max_height:
|
||||
row_max_height = h
|
||||
j += 1
|
||||
# Доступная ширина ряда с учетом обязательных отступов между элементами
|
||||
available_width = rect_width - spacing * (count - 1)
|
||||
desired_scale = available_width / sum_width if sum_width > 0 else 1.0
|
||||
# Разрешаем увеличение карточек, но не более max_scale
|
||||
scale = desired_scale if desired_scale < max_scale else max_scale
|
||||
# Выравниваем по левому краю (offset = 0)
|
||||
x = 0
|
||||
for k in range(i, j):
|
||||
new_w = int(nat_sizes[k, 0] * scale)
|
||||
new_h = int(nat_sizes[k, 1] * scale)
|
||||
result[k, 0] = x
|
||||
result[k, 1] = y
|
||||
result[k, 2] = new_w
|
||||
result[k, 3] = new_h
|
||||
x += new_w + spacing
|
||||
y += int(row_max_height * scale) + spacing
|
||||
i = j
|
||||
return result, y
|
||||
|
||||
class FlowLayout(QLayout):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.itemList = []
|
||||
# Устанавливаем отступы контейнера в 0 и задаем spacing между карточками
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self._spacing = 3 # отступ между карточками
|
||||
self._max_scale = 1.2 # максимальное увеличение карточек (например, на 20%)
|
||||
|
||||
def addItem(self, item: QLayoutItem) -> None:
|
||||
self.itemList.append(item)
|
||||
|
||||
def takeAt(self, index: int) -> QLayoutItem:
|
||||
if 0 <= index < len(self.itemList):
|
||||
return self.itemList.pop(index)
|
||||
raise IndexError("Index out of range")
|
||||
|
||||
def count(self) -> int:
|
||||
return len(self.itemList)
|
||||
|
||||
def itemAt(self, index: int) -> QLayoutItem | None:
|
||||
if 0 <= index < len(self.itemList):
|
||||
return self.itemList[index]
|
||||
return None
|
||||
|
||||
def expandingDirections(self):
|
||||
return Qt.Orientation(0)
|
||||
|
||||
def hasHeightForWidth(self):
|
||||
return True
|
||||
|
||||
def heightForWidth(self, width):
|
||||
return self.doLayout(QRect(0, 0, width, 0), True)
|
||||
|
||||
def setGeometry(self, rect):
|
||||
super().setGeometry(rect)
|
||||
self.doLayout(rect, False)
|
||||
|
||||
def sizeHint(self):
|
||||
return self.minimumSize()
|
||||
|
||||
def minimumSize(self):
|
||||
size = QSize()
|
||||
for item in self.itemList:
|
||||
size = size.expandedTo(item.minimumSize())
|
||||
margins = self.contentsMargins()
|
||||
size += QSize(margins.left() + margins.right(),
|
||||
margins.top() + margins.bottom())
|
||||
return size
|
||||
|
||||
def doLayout(self, rect, testOnly):
|
||||
N = len(self.itemList)
|
||||
if N == 0:
|
||||
return 0
|
||||
|
||||
# Собираем натуральные размеры всех элементов в массив NumPy
|
||||
nat_sizes = np.empty((N, 2), dtype=np.int32)
|
||||
for i, item in enumerate(self.itemList):
|
||||
s = item.sizeHint()
|
||||
nat_sizes[i, 0] = s.width()
|
||||
nat_sizes[i, 1] = s.height()
|
||||
|
||||
# Вычисляем геометрию с учетом spacing и max_scale через numba-функцию
|
||||
geom_array, total_height = compute_layout(nat_sizes, rect.width(), self._spacing, self._max_scale)
|
||||
|
||||
if not testOnly:
|
||||
for i, item in enumerate(self.itemList):
|
||||
x = geom_array[i, 0] + rect.x()
|
||||
y = geom_array[i, 1] + rect.y()
|
||||
w = geom_array[i, 2]
|
||||
h = geom_array[i, 3]
|
||||
item.setGeometry(QRect(QPoint(x, y), QSize(w, h)))
|
||||
|
||||
return total_height
|
||||
|
||||
class ClickableLabel(QLabel):
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, *args, icon=None, icon_size=16, icon_space=5, **kwargs):
|
||||
"""
|
||||
Поддерживаются вызовы:
|
||||
- ClickableLabel("текст", parent=...) – первый аргумент строка,
|
||||
- ClickableLabel(parent, text="...") – если первым аргументом передается родитель.
|
||||
|
||||
Аргументы:
|
||||
icon: QIcon или None – иконка, которая будет отрисована вместе с текстом.
|
||||
icon_size: int – размер иконки (ширина и высота).
|
||||
icon_space: int – отступ между иконкой и текстом.
|
||||
"""
|
||||
if args and isinstance(args[0], str):
|
||||
text = args[0]
|
||||
parent = kwargs.get("parent", None)
|
||||
super().__init__(text, parent)
|
||||
elif args and isinstance(args[0], QWidget):
|
||||
parent = args[0]
|
||||
text = kwargs.get("text", "")
|
||||
super().__init__(parent)
|
||||
self.setText(text)
|
||||
else:
|
||||
text = ""
|
||||
parent = kwargs.get("parent", None)
|
||||
super().__init__(text, parent)
|
||||
|
||||
self._icon = icon
|
||||
self._icon_size = icon_size
|
||||
self._icon_space = icon_space
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
def setIcon(self, icon):
|
||||
"""Устанавливает иконку и перерисовывает виджет."""
|
||||
self._icon = icon
|
||||
self.update()
|
||||
|
||||
def icon(self):
|
||||
"""Возвращает текущую иконку."""
|
||||
return self._icon
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Переопределяем отрисовку: рисуем иконку и текст в одном лейбле."""
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
rect = self.contentsRect()
|
||||
alignment = self.alignment()
|
||||
|
||||
icon_size = self._icon_size
|
||||
spacing = self._icon_space
|
||||
|
||||
icon_rect = QRect()
|
||||
text_rect = QRect()
|
||||
text = self.text()
|
||||
|
||||
if self._icon:
|
||||
# Получаем QPixmap нужного размера
|
||||
pixmap = self._icon.pixmap(icon_size, icon_size)
|
||||
icon_rect = QRect(0, 0, icon_size, icon_size)
|
||||
icon_rect.moveTop(rect.top() + (rect.height() - icon_size) // 2)
|
||||
else:
|
||||
pixmap = None
|
||||
|
||||
fm = QFontMetrics(self.font())
|
||||
text_width = fm.horizontalAdvance(text)
|
||||
text_height = fm.height()
|
||||
total_width = text_width + (icon_size + spacing if pixmap else 0)
|
||||
|
||||
if alignment & Qt.AlignmentFlag.AlignHCenter:
|
||||
x = rect.left() + (rect.width() - total_width) // 2
|
||||
elif alignment & Qt.AlignmentFlag.AlignRight:
|
||||
x = rect.right() - total_width
|
||||
else:
|
||||
x = rect.left()
|
||||
|
||||
y = rect.top() + (rect.height() - text_height) // 2
|
||||
|
||||
if pixmap:
|
||||
icon_rect.moveLeft(x)
|
||||
text_rect = QRect(x + icon_size + spacing, y, text_width, text_height)
|
||||
else:
|
||||
text_rect = QRect(x, y, text_width, text_height)
|
||||
|
||||
option = QStyleOption()
|
||||
option.initFrom(self)
|
||||
if pixmap:
|
||||
painter.drawPixmap(icon_rect, pixmap)
|
||||
self.style().drawItemText(
|
||||
painter,
|
||||
text_rect,
|
||||
alignment,
|
||||
self.palette(),
|
||||
self.isEnabled(),
|
||||
text,
|
||||
self.foregroundRole(),
|
||||
)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit()
|
||||
event.accept()
|
||||
else:
|
||||
super().mousePressEvent(event)
|
||||
|
||||
class AutoSizeButton(QPushButton):
|
||||
def __init__(self, *args, icon=None, icon_size=16,
|
||||
min_font_size=6, max_font_size=14, padding=20, update_size=True, **kwargs):
|
||||
if args and isinstance(args[0], str):
|
||||
text = args[0]
|
||||
parent = kwargs.get("parent", None)
|
||||
super().__init__(text, parent)
|
||||
elif args and isinstance(args[0], QWidget):
|
||||
parent = args[0]
|
||||
text = kwargs.get("text", "")
|
||||
super().__init__(text, parent)
|
||||
else:
|
||||
text = ""
|
||||
parent = kwargs.get("parent", None)
|
||||
super().__init__(text, parent)
|
||||
|
||||
self._icon = icon
|
||||
self._icon_size = icon_size
|
||||
self._alignment = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||||
self._min_font_size = min_font_size
|
||||
self._max_font_size = max_font_size
|
||||
self._padding = padding
|
||||
self._update_size = update_size
|
||||
self._original_font = self.font()
|
||||
self._original_text = self.text()
|
||||
|
||||
if self._icon:
|
||||
self.setIcon(self._icon)
|
||||
self.setIconSize(QSize(self._icon_size, self._icon_size))
|
||||
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.setFlat(True)
|
||||
|
||||
# Изначально выставляем минимальную ширину
|
||||
self.setMinimumWidth(50)
|
||||
self.adjustFontSize()
|
||||
|
||||
def setAlignment(self, alignment):
|
||||
self._alignment = alignment
|
||||
self.update()
|
||||
|
||||
def alignment(self):
|
||||
return self._alignment
|
||||
|
||||
def setText(self, text):
|
||||
self._original_text = text
|
||||
if not self._update_size:
|
||||
super().setText(text)
|
||||
else:
|
||||
super().setText(text)
|
||||
self.adjustFontSize()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
if self._update_size:
|
||||
self.adjustFontSize()
|
||||
|
||||
def adjustFontSize(self):
|
||||
if not self._original_text:
|
||||
return
|
||||
|
||||
if not self._update_size:
|
||||
return
|
||||
|
||||
# Определяем доступную ширину внутри кнопки
|
||||
available_width = self.width()
|
||||
if self._icon:
|
||||
available_width -= self._icon_size
|
||||
|
||||
margins = self.contentsMargins()
|
||||
available_width -= (margins.left() + margins.right() + self._padding * 2)
|
||||
|
||||
font = QFont(self._original_font)
|
||||
text = self._original_text
|
||||
|
||||
# Подбираем максимально возможный размер шрифта, при котором текст укладывается
|
||||
chosen_size = self._max_font_size
|
||||
for font_size in range(self._max_font_size, self._min_font_size - 1, -1):
|
||||
font.setPointSize(font_size)
|
||||
fm = QFontMetrics(font)
|
||||
text_width = fm.horizontalAdvance(text)
|
||||
if text_width <= available_width:
|
||||
chosen_size = font_size
|
||||
break
|
||||
|
||||
font.setPointSize(chosen_size)
|
||||
self.setFont(font)
|
||||
|
||||
# После выбора шрифта вычисляем требуемую ширину для полного отображения текста
|
||||
fm = QFontMetrics(font)
|
||||
text_width = fm.horizontalAdvance(text)
|
||||
required_width = text_width + margins.left() + margins.right() + self._padding * 2
|
||||
if self._icon:
|
||||
required_width += self._icon_size
|
||||
|
||||
# Если текущая ширина меньше требуемой, обновляем минимальную ширину
|
||||
if self.width() < required_width:
|
||||
self.setMinimumWidth(required_width)
|
||||
|
||||
super().setText(text)
|
||||
|
||||
def sizeHint(self):
|
||||
if not self._update_size:
|
||||
return super().sizeHint()
|
||||
else:
|
||||
# Вычисляем оптимальный размер кнопки на основе текста и отступов
|
||||
font = self.font()
|
||||
fm = QFontMetrics(font)
|
||||
text_width = fm.horizontalAdvance(self._original_text)
|
||||
margins = self.contentsMargins()
|
||||
width = text_width + margins.left() + margins.right() + self._padding * 2
|
||||
if self._icon:
|
||||
width += self._icon_size
|
||||
height = fm.height() + margins.top() + margins.bottom() + self._padding
|
||||
return QSize(width, height)
|
||||
|
||||
|
||||
class NavLabel(QLabel):
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, text="", parent=None):
|
||||
super().__init__(text, parent)
|
||||
self.setWordWrap(True)
|
||||
self.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._checkable = False
|
||||
self._isChecked = False
|
||||
self.setProperty("checked", self._isChecked)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
# Explicitly enable focus
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
|
||||
def setCheckable(self, checkable):
|
||||
self._checkable = checkable
|
||||
|
||||
def setChecked(self, checked):
|
||||
if self._checkable:
|
||||
self._isChecked = checked
|
||||
self.setProperty("checked", checked)
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
self.update()
|
||||
|
||||
def isChecked(self):
|
||||
return self._isChecked
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
# Ensure widget can take focus on click
|
||||
self.setFocus(Qt.FocusReason.MouseFocusReason)
|
||||
if self._checkable:
|
||||
self.setChecked(not self._isChecked)
|
||||
self.clicked.emit()
|
||||
event.accept()
|
||||
else:
|
||||
super().mousePressEvent(event)
|
Reference in New Issue
Block a user