From 057ab705af1cfef2868659d0ffa6910d76073393 Mon Sep 17 00:00:00 2001 From: Paul WOISARD Date: Sun, 18 Oct 2026 10:43:57 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20easter-egg=20=E2=80=94=207=20clics?= =?UTF-8?q?=20sur=20le=20logo=20lancent=20une=20pluie=20=C2=AB=20Matrix=20?= =?UTF-8?q?=C2=BB=20verte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calque transparent aux clics sur la fenêtre, 4 s, vert d'accent qui pulse, caractères ASCII uniquement ; le clic sur le logo continue de déplacer la fenêtre. Co-Authored-By: Claude Sonnet 5.5 --- src/pdf_equilibrist/ui/easter_egg.py | 75 ++++++++++++++++++++++++++++ src/pdf_equilibrist/ui/title_bar.py | 20 +++++++- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/pdf_equilibrist/ui/easter_egg.py diff --git a/src/pdf_equilibrist/ui/easter_egg.py b/src/pdf_equilibrist/ui/easter_egg.py new file mode 100644 index 0000000..98d73ed --- /dev/null +++ b/src/pdf_equilibrist/ui/easter_egg.py @@ -0,0 +1,75 @@ +"""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 + + +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)] + self._speeds = [random.uniform(0.5, 1.4) for _ in range(self._cols)] + 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)) + 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..e7dfe4b 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,20 @@ 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) sans bloquer le déplacement.""" + if obj is self._logo and event.type() == QEvent.Type.MouseButtonPress: + 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 False + def mousePressEvent(self, event): """ Lance le déplacement natif de la fenêtre au clic gauche. From 4b481b064904d3bd7028da7147a8ac22487540ee Mon Sep 17 00:00:00 2001 From: Paul WOISARD Date: Sun, 18 Oct 2026 10:53:06 +0200 Subject: [PATCH 2/4] =?UTF-8?q?fix(easter-egg):=20absorber=20les=20clics?= =?UTF-8?q?=20du=20logo=20(double-clic=20=3D=20agrandir/restaurer=20la=20f?= =?UTF-8?q?en=C3=AAtre)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- src/pdf_equilibrist/ui/title_bar.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pdf_equilibrist/ui/title_bar.py b/src/pdf_equilibrist/ui/title_bar.py index e7dfe4b..f77e698 100644 --- a/src/pdf_equilibrist/ui/title_bar.py +++ b/src/pdf_equilibrist/ui/title_bar.py @@ -413,8 +413,13 @@ 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) sans bloquer le déplacement.""" - if obj is self._logo and event.type() == QEvent.Type.MouseButtonPress: + """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: @@ -424,6 +429,7 @@ def eventFilter(self, obj, event): 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): From 5cbd379d7962637afe523da6bb91efe403ea3bb7 Mon Sep 17 00:00:00 2001 From: Paul WOISARD Date: Sun, 18 Oct 2026 10:58:28 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fix(ci):=20Bandit=20B311=20=E2=80=94=20rand?= =?UTF-8?q?om=20utilis=C3=A9=20seulement=20pour=20l'effet=20visuel=20de=20?= =?UTF-8?q?l'easter-egg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- src/pdf_equilibrist/ui/easter_egg.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pdf_equilibrist/ui/easter_egg.py b/src/pdf_equilibrist/ui/easter_egg.py index 98d73ed..7141a79 100644 --- a/src/pdf_equilibrist/ui/easter_egg.py +++ b/src/pdf_equilibrist/ui/easter_egg.py @@ -15,6 +15,7 @@ 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.""" @@ -25,8 +26,8 @@ def __init__(self, host: QWidget): 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)] - self._speeds = [random.uniform(0.5, 1.4) for _ in range(self._cols)] + 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) @@ -65,7 +66,7 @@ def paintEvent(self, _event): 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)) + p.drawText(c * CELL, r * CELL + CELL - 3, random.choice(GLYPHS)) # nosec B311 p.end() From 954364bfd8fa01ef3959d8774bb7517311c6980a Mon Sep 17 00:00:00 2001 From: Paul WOISARD Date: Sun, 18 Oct 2026 10:58:39 +0200 Subject: [PATCH 4/4] fix(lint): deux lignes vides avant la classe (E302) Co-Authored-By: Claude Sonnet 5.5 --- src/pdf_equilibrist/ui/easter_egg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pdf_equilibrist/ui/easter_egg.py b/src/pdf_equilibrist/ui/easter_egg.py index 7141a79..102c6a3 100644 --- a/src/pdf_equilibrist/ui/easter_egg.py +++ b/src/pdf_equilibrist/ui/easter_egg.py @@ -14,9 +14,9 @@ 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."""