Le module de thème publie deux palettes derrière les mêmes noms : `apply()` rebranche `BG`, `TEXT`, `TAB_CSS` sur celle qui est choisie au lancement, et les cent soixante-dix appels répartis dans l'interface continuent de lire `theme.BG` sans rien savoir du thème actif. Aucun widget n'a eu à changer — c'est ce que permettait le passage systématique par `theme.X` à l'usage. Le choix est lu une seule fois, au démarrage. Les feuilles de style sont posées dans les constructeurs des fenêtres ; les repeindre à chaud supposerait de reconstruire celle qui porte l'état du tunnel, ses fils d'exécution et ses minuteries. Le message de confirmation annonce donc la prise d'effet au prochain lancement plutôt que de laisser croire le contraire. Le thème sombre reste le défaut, et rend exactement les mêmes feuilles de style qu'avant. Un thème clair n'est pas une inversion, et trois familles de valeurs vivaient en dur là où elles étaient jusqu'ici invisibles. Dans la feuille de style commune, les variantes d'état des champs et des boutons étaient des voiles blancs translucides — c'est-à-dire rien du tout sur un fond clair. Le texte posé sur un aplat saturé — bouton plein, onglet actif, bandeau, sélection — portait `TEXT`, qui devient presque noir en thème clair et disparaîtrait sur l'accent : un jeton distinct le dit, blanc dans les deux thèmes. Enfin le graphe de bande passante peignait son fond en dur, soit un panneau sombre au milieu d'une page claire ; ses sept teintes sont devenues des jetons, comme quatre couleurs de survol de la fenêtre principale. Deux endroits figeaient par ailleurs le thème au chargement de leur module et auraient gardé les teintes sombres quel que soit le réglage : la feuille de style de la fenêtre d'historique, bâtie au niveau du module, et la table état → couleur du diagnostic, évaluée comme attribut de classe. La première devient une fonction, la seconde ne retient que la puce et demande la couleur au thème au moment de l'affichage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
3.6 KiB
Python
101 lines
3.6 KiB
Python
"""Widget graphique bande passante RX/TX (QPainter, sans dépendance externe)."""
|
|
from __future__ import annotations
|
|
from collections import deque
|
|
from PyQt6.QtWidgets import QWidget
|
|
from PyQt6.QtGui import QPainter, QColor, QPen, QBrush, QFont, QPainterPath
|
|
from PyQt6.QtCore import Qt, QRect, QPointF
|
|
|
|
from app.ui import theme
|
|
|
|
|
|
def _fmt(bps: float) -> str:
|
|
if bps >= 1_048_576:
|
|
return f"{bps/1_048_576:.1f} MB/s"
|
|
if bps >= 1_024:
|
|
return f"{bps/1_024:.0f} KB/s"
|
|
return f"{bps:.0f} B/s"
|
|
|
|
|
|
class BandwidthGraph(QWidget):
|
|
"""Affiche RX (bleu) et TX (vert) sur les N dernières secondes."""
|
|
|
|
POINTS = 30 # nombre de points conservés
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._rx: deque[float] = deque([0.0] * self.POINTS, maxlen=self.POINTS)
|
|
self._tx: deque[float] = deque([0.0] * self.POINTS, maxlen=self.POINTS)
|
|
self.setMinimumHeight(70)
|
|
self.setStyleSheet("background: transparent;")
|
|
|
|
def push(self, rx_bps: float, tx_bps: float) -> None:
|
|
self._rx.append(max(0.0, rx_bps))
|
|
self._tx.append(max(0.0, tx_bps))
|
|
self.update()
|
|
|
|
def reset(self) -> None:
|
|
self._rx = deque([0.0] * self.POINTS, maxlen=self.POINTS)
|
|
self._tx = deque([0.0] * self.POINTS, maxlen=self.POINTS)
|
|
self.update()
|
|
|
|
def paintEvent(self, _):
|
|
p = QPainter(self)
|
|
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
w, h = self.width(), self.height()
|
|
pad_l, pad_r, pad_t, pad_b = 42, 8, 6, 20
|
|
|
|
# Fond
|
|
p.fillRect(0, 0, w, h, QColor(theme.GRAPH_BG))
|
|
|
|
plot_w = w - pad_l - pad_r
|
|
plot_h = h - pad_t - pad_b
|
|
|
|
max_val = max(max(self._rx), max(self._tx), 1.0)
|
|
|
|
def _path(data: deque, color: str, fill: str):
|
|
pts = list(data)
|
|
path = QPainterPath()
|
|
xs = [pad_l + i * plot_w / (len(pts) - 1) for i in range(len(pts))]
|
|
ys = [pad_t + plot_h - (v / max_val) * plot_h for v in pts]
|
|
path.moveTo(QPointF(xs[0], pad_t + plot_h))
|
|
path.lineTo(QPointF(xs[0], ys[0]))
|
|
for x, y in zip(xs[1:], ys[1:]):
|
|
path.lineTo(QPointF(x, y))
|
|
path.lineTo(QPointF(xs[-1], pad_t + plot_h))
|
|
path.closeSubpath()
|
|
p.fillPath(path, QBrush(QColor(fill)))
|
|
pen = QPen(QColor(color))
|
|
pen.setWidthF(1.5)
|
|
p.setPen(pen)
|
|
line = QPainterPath()
|
|
line.moveTo(QPointF(xs[0], ys[0]))
|
|
for x, y in zip(xs[1:], ys[1:]):
|
|
line.lineTo(QPointF(x, y))
|
|
p.drawPath(line)
|
|
|
|
_path(self._rx, theme.GRAPH_RX, theme.GRAPH_RX_FILL) # RX
|
|
_path(self._tx, theme.GRAPH_TX, theme.GRAPH_TX_FILL) # TX
|
|
|
|
# Axes
|
|
p.setPen(QPen(QColor(theme.GRAPH_AXIS)))
|
|
p.drawLine(pad_l, pad_t, pad_l, pad_t + plot_h)
|
|
p.drawLine(pad_l, pad_t + plot_h, w - pad_r, pad_t + plot_h)
|
|
|
|
# Labels Y
|
|
font = QFont("Arial", 8)
|
|
p.setFont(font)
|
|
p.setPen(QPen(QColor(theme.GRAPH_LABEL)))
|
|
for frac, label in [(0.0, _fmt(max_val)), (0.5, _fmt(max_val / 2)), (1.0, "0")]:
|
|
y = int(pad_t + frac * plot_h)
|
|
p.drawText(QRect(0, y - 8, pad_l - 2, 16),
|
|
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
|
|
label)
|
|
|
|
# Légende
|
|
p.setPen(QPen(QColor(theme.GRAPH_RX)))
|
|
p.drawText(pad_l + 4, pad_t + 12, f"↓ {_fmt(self._rx[-1])}")
|
|
p.setPen(QPen(QColor(theme.GRAPH_TX)))
|
|
p.drawText(pad_l + 90, pad_t + 12, f"↑ {_fmt(self._tx[-1])}")
|
|
|
|
p.end()
|