"""Small, size-aware widgets used by the generation workspace.""" from __future__ import annotations from PySide6.QtCore import QRect, QSize, Qt from PySide6.QtGui import QColor, QPainter, QPainterPath, QPixmap from PySide6.QtWidgets import ( QFrame, QHBoxLayout, QLabel, QListWidget, QSizePolicy, QStyledItemDelegate, QStyle, QVBoxLayout, QWidget, ) class ImagePreview(QWidget): """Paint an image without letting its pixel dimensions enlarge the layout.""" def __init__(self, text: str = "No image selected", parent=None): super().__init__(parent) self._image = QPixmap() self._text = text self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding) self.setMinimumSize(80, 80) def sizeHint(self): return QSize(360, 260) def setPixmap(self, image: QPixmap): self._image = image self.update() def setText(self, text: str): self._text = text self.update() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) painter.fillRect(self.rect(), QColor("#040d15")) if self._image.isNull(): painter.setPen(QColor("#89a7bc")) painter.drawText(self.rect().adjusted(18, 12, -18, -12), Qt.AlignCenter | Qt.TextWordWrap, self._text) return size = self._image.size() size.scale(self.size() - QSize(12, 12), Qt.KeepAspectRatio) destination = QRect(0, 0, size.width(), size.height()) destination.moveCenter(self.rect().center()) painter.setRenderHint(QPainter.SmoothPixmapTransform) painter.drawPixmap(destination, self._image) class ElidedLabel(QLabel): """Single-line metadata that cannot force a panel wider than its contents.""" def __init__(self, text="", parent=None): super().__init__(text, parent) self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) self.setMinimumWidth(0) self.setMinimumHeight(20) self.setTextFormat(Qt.PlainText) def setText(self, text): super().setText(text) self.setToolTip(text) def paintEvent(self, event): painter = QPainter(self) painter.setPen(self.palette().color(self.foregroundRole())) painter.drawText(self.contentsRect(), Qt.AlignVCenter | Qt.AlignLeft, self.fontMetrics().elidedText(self.text(), Qt.ElideRight, self.contentsRect().width())) def section(number: str, title: str, subtitle: str): card = QFrame() card.setObjectName("GenerationSection") layout = QVBoxLayout(card) layout.setContentsMargins(12, 10, 12, 12) layout.setSpacing(8) header = QHBoxLayout() badge = QLabel(number) badge.setObjectName("GenerationNumber") badge.setAlignment(Qt.AlignCenter) badge.setFixedSize(30, 30) header.addWidget(badge) labels = QVBoxLayout() labels.setSpacing(0) heading = QLabel(title) heading.setObjectName("GenerationHeading") labels.addWidget(heading) if subtitle: detail = QLabel(subtitle) detail.setProperty("muted", True) detail.setWordWrap(True) labels.addWidget(detail) header.addLayout(labels, 1) layout.addLayout(header) return card, layout, header def field(label: str, widget: QWidget): container = QWidget() layout = QVBoxLayout(container) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(4) layout.addWidget(QLabel(label)) layout.addWidget(widget) container.setMinimumWidth(0) return container class GeneratorCardDelegate(QStyledItemDelegate): def sizeHint(self, option, index): size = self.parent().gridSize() return QSize(max(176, size.width()), 182) def paint(self, painter, option, index): painter.save() painter.setRenderHint(QPainter.Antialiasing) rect = option.rect.adjusted(4, 4, -4, -4) selected = bool(option.state & QStyle.State_Selected) hovered = bool(option.state & QStyle.State_MouseOver) painter.setPen(QColor("#16b7f2" if selected or hovered else "#204c68")) painter.setBrush(QColor("#0b2030" if selected or hovered else "#081925")) painter.drawRoundedRect(rect, 7, 7) image_rect = rect.adjusted(5, 5, -5, -69) icon = index.data(Qt.DecorationRole) if icon: image = icon.pixmap(QSize(480, 240)) if not image.isNull(): image = image.scaled(image_rect.size(), Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) painter.save() clip = QPainterPath() clip.addRoundedRect(image_rect, 4, 4) painter.setClipPath(clip) painter.drawPixmap(image_rect.center().x() - image.width() // 2, image_rect.center().y() - image.height() // 2, image) painter.restore() lines = str(index.data(Qt.DisplayRole) or "").split("\n") painter.setPen(QColor("#edf5fb")) font = painter.font() font.setBold(True) painter.setFont(font) painter.drawText(rect.adjusted(10, rect.height() - 64, -10, -42), Qt.AlignVCenter, painter.fontMetrics().elidedText(lines[0], Qt.ElideRight, rect.width() - 20)) font.setBold(False) painter.setFont(font) painter.setPen(QColor("#94b3c9")) painter.drawText(rect.adjusted(10, rect.height() - 43, -10, -22), Qt.AlignVCenter, lines[1] if len(lines) > 1 else "") painter.setPen(QColor("#38c4ff")) painter.drawText(rect.adjusted(10, rect.height() - 24, -10, -3), Qt.AlignVCenter, "Open →") painter.restore() class GeneratorCards(QListWidget): def __init__(self): super().__init__() self.setViewMode(QListWidget.IconMode) self.setResizeMode(QListWidget.Adjust) self.setMovement(QListWidget.Static) self.setMouseTracking(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setItemDelegate(GeneratorCardDelegate(self)) self.setGridSize(QSize(220, 182)) self.setMinimumHeight(190) self.setMaximumHeight(212) def resizeEvent(self, event): super().resizeEvent(event) self.fit_cards() def keyPressEvent(self, event): if event.key() in (Qt.Key_Return, Qt.Key_Enter) and self.currentItem() is not None: self.itemClicked.emit(self.currentItem()) event.accept() return super().keyPressEvent(event) def fit_cards(self): available = max(180, self.viewport().width() - 14) columns = max(1, min(max(1, self.count()), 4, available // 176)) self.setGridSize(QSize(available // columns, 182))