diff --git a/src/pdf_equilibrist/ui/easter_egg.py b/src/pdf_equilibrist/ui/easter_egg.py new file mode 100644 index 0000000..102c6a3 --- /dev/null +++ b/src/pdf_equilibrist/ui/easter_egg.py @@ -0,0 +1,76 @@ +"""Easter-egg : 7 clics sur le logo de la barre de titre lancent une courte pluie « Matrix » verte.""" +from __future__ import annotations +import math +import random + +from PyQt6.QtWidgets import QWidget +from PyQt6.QtGui import QPainter, QColor, QFont +from PyQt6.QtCore import Qt, QTimer + +ACCENT = (107, 191, 78) # vert du logo, #6BBF4E +CLICKS_NEEDED = 7 +CLICK_WINDOW_MS = 2500 # délai max entre deux clics pour que la série continue +DURATION_MS = 4000 +FRAME_MS = 45 +CELL = 16 # taille d'une colonne / d'un caractère (px) +GLYPHS = "01ABCDEF<>/|=+*" # ASCII seulement : aucune police spéciale requise +# random sert uniquement à l'effet visuel (B311 : aucun usage cryptographique) + + +class MatrixRain(QWidget): + """Calque transparent aux clics, posé sur une fenêtre, qui se détruit tout seul.""" + + def __init__(self, host: QWidget): + super().__init__(host) + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) + self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground) + self.setGeometry(host.rect()) + self._cols = max(1, self.width() // CELL) + self._heads = [random.uniform(-30, 0) for _ in range(self._cols)] # nosec B311 + self._speeds = [random.uniform(0.5, 1.4) for _ in range(self._cols)] # nosec B311 + self._tick = 0 + self._timer = QTimer(self) + self._timer.timeout.connect(self._step) + self._timer.start(FRAME_MS) + QTimer.singleShot(DURATION_MS, self._finish) + self.show() + self.raise_() + + def _step(self): + self._tick += 1 + for i in range(self._cols): + self._heads[i] += self._speeds[i] + self.update() + + def _finish(self): + self._timer.stop() + self.hide() + self.deleteLater() + + def paintEvent(self, _event): + p = QPainter(self) + font = QFont("Consolas") + font.setPixelSize(CELL - 2) + p.setFont(font) + # Fondu d'entrée et de sortie + pulsation du vert d'accent + t = self._tick * FRAME_MS / DURATION_MS + fade = min(1.0, t * 6, (1 - t) * 6) if 0 <= t <= 1 else 0 + pulse = 0.75 + 0.25 * math.sin(self._tick * 0.35) + rows = self.height() // CELL + 1 + for c in range(self._cols): + head = int(self._heads[c]) + for k in range(14): # traînée de 14 caractères + r = head - k + if not 0 <= r < rows: + continue + alpha = int(255 * fade * pulse * (1 - k / 14)) + col = QColor(255, 255, 255, alpha) if k == 0 else QColor(*ACCENT, alpha) + p.setPen(col) + p.drawText(c * CELL, r * CELL + CELL - 3, random.choice(GLYPHS)) # nosec B311 + p.end() + + +def trigger(window: QWidget) -> None: + """Lance la pluie sur ``window`` (ignoré si une est déjà en cours).""" + if window.findChild(MatrixRain) is None: + MatrixRain(window) diff --git a/src/pdf_equilibrist/ui/title_bar.py b/src/pdf_equilibrist/ui/title_bar.py index 80a5bf8..f77e698 100644 --- a/src/pdf_equilibrist/ui/title_bar.py +++ b/src/pdf_equilibrist/ui/title_bar.py @@ -52,7 +52,8 @@ QFrame, ) from PyQt6.QtGui import QPixmap, QIcon, QPainter, QColor, QFont, QPen -from PyQt6.QtCore import Qt, pyqtSignal, QSize, QRectF +from PyQt6.QtCore import Qt, pyqtSignal, QSize, QRectF, QEvent +import time from pdf_equilibrist import theme ACCENT = "#6BBF4E" @@ -225,6 +226,9 @@ def __init__(self, parent=None): self._logo.setFixedSize(26, 38) self._logo.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self._logo) + self._logo_clicks = 0 # easter-egg : voir ui/easter_egg.py + self._logo_last_click = 0.0 + self._logo.installEventFilter(self) # ── Nom de l'application ────────────────────────────────────────────── self._app_name = QLabel("PDF Equilibrist") @@ -408,6 +412,26 @@ def update_max_button(self, is_maximized: bool): # ── Drag et double-clic ─────────────────────────────────────────────────── + def eventFilter(self, obj, event): + """Compte les clics sur le logo (7 de suite = easter-egg). + + Un 2ᵉ clic rapide arrive en ``MouseButtonDblClick`` et un clic non traité remonte à la + barre (déplacement / agrandir-restaurer) : on absorbe donc les deux sur le logo. + """ + if obj is self._logo and event.type() in (QEvent.Type.MouseButtonPress, + QEvent.Type.MouseButtonDblClick): + from pdf_equilibrist.ui import easter_egg + now = time.monotonic() + if (now - self._logo_last_click) * 1000 > easter_egg.CLICK_WINDOW_MS: + self._logo_clicks = 0 + self._logo_last_click = now + self._logo_clicks += 1 + if self._logo_clicks >= easter_egg.CLICKS_NEEDED: + self._logo_clicks = 0 + easter_egg.trigger(self.window()) + return True + return False + def mousePressEvent(self, event): """ Lance le déplacement natif de la fenêtre au clic gauche.