"""Tile — a large square button + caption used on the startup screen.
Hovering the tile animates its icon so the glyph appears to grow
inside the button frame — the button itself stays the same size.
Signals:
clicked() — emitted when the tile is pressed.
"""
from __future__ import annotations
from typing import Optional
from PySide6.QtCore import (
Property,
QEasingCurve,
QEvent,
QPropertyAnimation,
QSize,
Qt,
Signal,
)
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget
class _TileButton(QPushButton):
"""QPushButton subclass with an animated `iconPixels` property so
the icon can be tweened on hover without touching the button's
outer geometry."""
def __init__(self, base_size: int, parent=None):
super().__init__(parent)
self._base_size = int(base_size)
self._icon_pixels = int(base_size)
self._anim = QPropertyAnimation(self, b"iconPixels", self)
self._anim.setDuration(140)
self._anim.setEasingCurve(QEasingCurve.OutCubic)
def _get_icon_pixels(self) -> int:
return self._icon_pixels
def _set_icon_pixels(self, v: int) -> None:
self._icon_pixels = int(v)
self.setIconSize(QSize(self._icon_pixels, self._icon_pixels))
iconPixels = Property(int, _get_icon_pixels, _set_icon_pixels)
def enterEvent(self, event: QEvent) -> None:
"""Animate the icon toward its hover-zoomed size on cursor enter."""
self._anim.stop()
self._anim.setStartValue(self._icon_pixels)
self._anim.setEndValue(int(self._base_size * 1.18)) # 18% zoom
self._anim.start()
super().enterEvent(event)
def leaveEvent(self, event: QEvent) -> None:
"""Animate the icon back to its base size on cursor leave."""
self._anim.stop()
self._anim.setStartValue(self._icon_pixels)
self._anim.setEndValue(self._base_size)
self._anim.start()
super().leaveEvent(event)
[docs]
class Tile(QWidget):
"""Large square tile with an icon and a caption, used on the startup grid.
:param text: fallback label (also used to derive initials if no icon).
:param icon: optional QIcon to render inside the tile.
:param icon_size: base icon side length in px; animates on hover.
:param tile_size: fixed side length of the tile button in px.
:param caption: caption shown under the tile; defaults to ``text``.
:ivar clicked: emitted when the tile button is pressed.
"""
def __init__(
self,
text: str,
icon: Optional[QIcon] = None,
icon_size: int = 64,
tile_size: int = 120,
caption: str = "",
parent=None,
):
super().__init__(parent)
self._text = text
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12) # room between tile and caption
layout.setAlignment(Qt.AlignHCenter)
self._button = _TileButton(icon_size)
self._button.setObjectName("Tile")
self._button.setFixedSize(tile_size, tile_size)
self._button.setCursor(Qt.PointingHandCursor)
if icon is not None:
self._button.setIcon(icon)
self._button.setIconSize(QSize(icon_size, icon_size))
else:
initials = "".join(w[0].upper() for w in text.split()[:2])[:2]
self._button.setText(initials or text[:2].upper())
self._button.clicked.connect(self.clicked.emit)
self._button.setToolTip(caption or text)
layout.addWidget(self._button, alignment=Qt.AlignHCenter)
self._caption = QLabel(caption or text)
self._caption.setObjectName("TileCaption")
self._caption.setAlignment(Qt.AlignHCenter)
self._caption.setWordWrap(True)
self._caption.setMaximumWidth(tile_size + 40)
layout.addWidget(self._caption)
@property
[docs]
def text(self) -> str:
"""The tile's text label as passed to the constructor."""
return self._text