feat(ui): un thème clair, et plus une couleur hors du module qui les tient
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>
This commit is contained in:
+38
-10
@@ -102,7 +102,7 @@ class AdminWindow(QDialog):
|
||||
|
||||
banner = QLabel(" WGSecure — Configuration Administrateur")
|
||||
banner.setFixedHeight(40)
|
||||
banner.setStyleSheet(theme.banner_style(theme.BG_SUNKEN))
|
||||
banner.setStyleSheet(theme.banner_style(theme.BG_SUNKEN, theme.TEXT))
|
||||
layout.addWidget(banner)
|
||||
|
||||
# Onglets de premier niveau : config qui conditionne la connexion
|
||||
@@ -941,6 +941,18 @@ class AdminWindow(QDialog):
|
||||
|
||||
lay.addWidget(grp2)
|
||||
|
||||
grp3 = QGroupBox("Apparence")
|
||||
form3 = QFormLayout(grp3)
|
||||
|
||||
self._cmb_theme = QComboBox()
|
||||
for cle, libelle in theme.PALETTES.items():
|
||||
self._cmb_theme.addItem(libelle, cle)
|
||||
actuel = self._cmb_theme.findData(theme.CURRENT)
|
||||
self._cmb_theme.setCurrentIndex(actuel if actuel >= 0 else 0)
|
||||
self._row(form3, "Thème :", self._cmb_theme)
|
||||
|
||||
lay.addWidget(grp3)
|
||||
|
||||
btn_apply = QPushButton("✅ Appliquer les paramètres")
|
||||
btn_apply.clicked.connect(self._apply_settings)
|
||||
lay.addWidget(btn_apply)
|
||||
@@ -952,12 +964,26 @@ class AdminWindow(QDialog):
|
||||
self._cfg.set("ui", "auto_reconnect", self._chk_autorecon.isChecked())
|
||||
self._cfg.set("ui", "reconnect_interval", self._spin_interval.value())
|
||||
self._cfg.set("ui", "auto_connect_on_startup", self._chk_auto_connect.isChecked())
|
||||
# Le thème n'est lu qu'au lancement : les feuilles de style sont
|
||||
# posées dans les constructeurs des fenêtres, et les repeindre
|
||||
# supposerait de reconstruire celle qui porte le tunnel. Le dire plutôt
|
||||
# que de laisser croire à un réglage sans effet.
|
||||
choisi = self._cmb_theme.currentData()
|
||||
theme_change = choisi != (self._cfg.get("ui", "theme") or "dark")
|
||||
self._cfg.set("ui", "theme", choisi)
|
||||
|
||||
autostart = self._chk_autostart.isChecked()
|
||||
if autostart != bool(self._cfg.get("ui", "autostart")):
|
||||
self._cfg.set_autostart(autostart)
|
||||
else:
|
||||
self._cfg.save()
|
||||
QMessageBox.information(self, "Paramètres", "Paramètres enregistrés.")
|
||||
|
||||
message = "Paramètres enregistrés."
|
||||
if theme_change:
|
||||
message += ("\n\nLe thème « "
|
||||
f"{theme.PALETTES.get(choisi, choisi)} » s'appliquera "
|
||||
"au prochain démarrage de WGSecure.")
|
||||
QMessageBox.information(self, "Paramètres", message)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Onglet Sécurité
|
||||
@@ -1579,13 +1605,11 @@ class AdminWindow(QDialog):
|
||||
v.addWidget(btn_close)
|
||||
dlg.exec()
|
||||
|
||||
#: Rendu d'une étape de diagnostic : puce, couleur du libellé.
|
||||
_STEP_STYLE = {
|
||||
"ok": ("✅", theme.OK_TEXT),
|
||||
"warn": ("⚠️", theme.WARN_TEXT),
|
||||
"fail": ("❌", theme.FAIL_TEXT),
|
||||
"skip": ("⏭️", theme.SKIP_TEXT),
|
||||
}
|
||||
#: Puce d'une étape de diagnostic. La couleur ne figure pas ici : évaluée
|
||||
#: à l'import comme le reste d'un attribut de classe, elle aurait retenu
|
||||
#: les teintes du thème d'alors. Elle est demandée au thème à l'affichage
|
||||
#: (cf. `theme.state_text`).
|
||||
_STEP_BULLET = {"ok": "✅", "warn": "⚠️", "fail": "❌", "skip": "⏭️"}
|
||||
|
||||
def _run_test(self):
|
||||
if self._diag_worker is not None:
|
||||
@@ -1628,7 +1652,11 @@ class AdminWindow(QDialog):
|
||||
# chaque cellule renvoyant à la ligne.
|
||||
rows = ['<table width="100%" cellspacing="0" cellpadding="0">']
|
||||
for st in steps:
|
||||
bullet, color = self._STEP_STYLE.get(st["state"], ("•", theme.TEXT))
|
||||
if st["state"] in self._STEP_BULLET:
|
||||
bullet = self._STEP_BULLET[st["state"]]
|
||||
color = theme.state_text(st["state"])
|
||||
else:
|
||||
bullet, color = "•", theme.TEXT
|
||||
weight = "bold" if st["state"] == "fail" else "normal"
|
||||
rows.append(
|
||||
'<tr>'
|
||||
|
||||
+9
-7
@@ -5,6 +5,8 @@ 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:
|
||||
@@ -43,7 +45,7 @@ class BandwidthGraph(QWidget):
|
||||
pad_l, pad_r, pad_t, pad_b = 42, 8, 6, 20
|
||||
|
||||
# Fond
|
||||
p.fillRect(0, 0, w, h, QColor("#17202a"))
|
||||
p.fillRect(0, 0, w, h, QColor(theme.GRAPH_BG))
|
||||
|
||||
plot_w = w - pad_l - pad_r
|
||||
plot_h = h - pad_t - pad_b
|
||||
@@ -71,18 +73,18 @@ class BandwidthGraph(QWidget):
|
||||
line.lineTo(QPointF(x, y))
|
||||
p.drawPath(line)
|
||||
|
||||
_path(self._rx, "#5dade2", "#1a3a52") # RX bleu
|
||||
_path(self._tx, "#58d68d", "#1a3d2b") # TX vert
|
||||
_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("#2e4057")))
|
||||
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("#5d6d7e")))
|
||||
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),
|
||||
@@ -90,9 +92,9 @@ class BandwidthGraph(QWidget):
|
||||
label)
|
||||
|
||||
# Légende
|
||||
p.setPen(QPen(QColor("#5dade2")))
|
||||
p.setPen(QPen(QColor(theme.GRAPH_RX)))
|
||||
p.drawText(pad_l + 4, pad_t + 12, f"↓ {_fmt(self._rx[-1])}")
|
||||
p.setPen(QPen(QColor("#58d68d")))
|
||||
p.setPen(QPen(QColor(theme.GRAPH_TX)))
|
||||
p.drawText(pad_l + 90, pad_t + 12, f"↑ {_fmt(self._tx[-1])}")
|
||||
|
||||
p.end()
|
||||
|
||||
@@ -10,7 +10,13 @@ from PyQt6.QtGui import QColor, QFont
|
||||
from app.core import history as hist
|
||||
from app.ui import theme
|
||||
|
||||
_CSS = f"""
|
||||
def _css() -> str:
|
||||
"""Feuille de style de la fenêtre d'historique.
|
||||
|
||||
Fonction et non constante : bâtie à l'import, elle retenait les couleurs
|
||||
du thème chargé à ce moment-là et ignorait celui appliqué au démarrage.
|
||||
"""
|
||||
return f"""
|
||||
QDialog {{ background: {theme.BG_SUNKEN}; }}
|
||||
QLabel {{ color: {theme.TEXT}; background: transparent; }}
|
||||
QTableWidget {{
|
||||
@@ -21,12 +27,14 @@ _CSS = f"""
|
||||
background: {theme.BG_RAISED}; color: {theme.TEXT}; padding: 5px;
|
||||
border: none; font-weight: bold; font-size: 11px;
|
||||
}}
|
||||
QTableWidget::item:selected {{ background: {theme.ACCENT}; }}
|
||||
QTableWidget::item:selected {{ background: {theme.ACCENT};
|
||||
color: {theme.ON_SOLID}; }}
|
||||
QPushButton {{
|
||||
background: {theme.BG_RAISED}; color: {theme.TEXT}; border-radius: 5px;
|
||||
padding: 6px 14px; border: none;
|
||||
}}
|
||||
QPushButton:hover {{ background: {theme.ACCENT_HOVER}; }}
|
||||
QPushButton:hover {{ background: {theme.ACCENT_HOVER};
|
||||
color: {theme.ON_SOLID}; }}
|
||||
"""
|
||||
|
||||
|
||||
@@ -35,7 +43,7 @@ class HistoryDialog(QDialog):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("WGSecure — Historique des sessions")
|
||||
self.setMinimumSize(680, 420)
|
||||
self.setStyleSheet(_CSS)
|
||||
self.setStyleSheet(_css())
|
||||
self._build_ui()
|
||||
self._load()
|
||||
|
||||
@@ -64,7 +72,7 @@ class HistoryDialog(QDialog):
|
||||
self._table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self._table.setAlternatingRowColors(True)
|
||||
self._table.setStyleSheet(
|
||||
_CSS +
|
||||
_css() +
|
||||
f"QTableWidget {{ alternate-background-color: {theme.BG_RAISED}; }}"
|
||||
)
|
||||
layout.addWidget(self._table)
|
||||
|
||||
+12
-10
@@ -553,9 +553,9 @@ class MainWindow(QMainWindow):
|
||||
self._btn_connect = QPushButton("Se connecter")
|
||||
self._btn_connect.setFixedHeight(44)
|
||||
self._btn_connect.setStyleSheet(
|
||||
f"QPushButton {{ background: {theme.OK_SOLID}; color: {theme.TEXT}; font-size: 14px;"
|
||||
f"QPushButton {{ background: {theme.OK_SOLID}; color: {theme.ON_SOLID}; font-size: 14px;"
|
||||
" font-weight: bold; border-radius: 6px; border: none; }"
|
||||
"QPushButton:hover { background: #27ae60; }"
|
||||
f"QPushButton:hover {{ background: {theme.OK_HOVER}; }}"
|
||||
f"QPushButton:disabled {{ background: {theme.BG_RAISED}; color: {theme.TEXT_FAINT}; }}"
|
||||
)
|
||||
self._btn_connect.clicked.connect(self._on_connect)
|
||||
@@ -690,14 +690,16 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _style_access_button(self, expected: bool):
|
||||
"""Met le bouton en avant seulement quand un code est réellement dû."""
|
||||
bg, hover = ((theme.WARN_SOLID, "#d68910") if expected
|
||||
else (theme.BG_RAISED, theme.ACCENT_HOVER))
|
||||
bg, hover, fg = ((theme.WARN_SOLID, theme.WARN_HOVER, theme.ON_SOLID)
|
||||
if expected
|
||||
else (theme.BG_RAISED, theme.ACCENT_HOVER, theme.TEXT))
|
||||
weight = "bold" if expected else "normal"
|
||||
self._btn_access.setStyleSheet(
|
||||
f"QPushButton {{ background: {bg}; color: {theme.TEXT};"
|
||||
f"QPushButton {{ background: {bg}; color: {fg};"
|
||||
f" font-size: 12px; font-weight: {weight}; border-radius: 6px;"
|
||||
" border: none; }"
|
||||
f"QPushButton:hover {{ background: {hover}; }}"
|
||||
f"QPushButton:hover {{ background: {hover};"
|
||||
f" color: {theme.ON_SOLID}; }}"
|
||||
)
|
||||
|
||||
def _sync_window_height(self):
|
||||
@@ -1085,9 +1087,9 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
self._btn_connect.setText("Se déconnecter")
|
||||
self._btn_connect.setStyleSheet(
|
||||
f"QPushButton {{ background: {theme.FAIL_SOLID}; color: {theme.TEXT}; font-size: 14px;"
|
||||
f"QPushButton {{ background: {theme.FAIL_SOLID}; color: {theme.ON_SOLID}; font-size: 14px;"
|
||||
" font-weight: bold; border-radius: 6px; border: none; }"
|
||||
"QPushButton:hover { background: #c0392b; }"
|
||||
f"QPushButton:hover {{ background: {theme.FAIL_SOLID}; }}"
|
||||
)
|
||||
self._info_labels["rx_tx"].setText(
|
||||
f"↓ {info.get('rx_bytes','—')} ↑ {info.get('tx_bytes','—')}"
|
||||
@@ -1109,9 +1111,9 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
self._btn_connect.setText("Se connecter")
|
||||
self._btn_connect.setStyleSheet(
|
||||
f"QPushButton {{ background: {theme.OK_SOLID}; color: {theme.TEXT}; font-size: 14px;"
|
||||
f"QPushButton {{ background: {theme.OK_SOLID}; color: {theme.ON_SOLID}; font-size: 14px;"
|
||||
" font-weight: bold; border-radius: 6px; border: none; }"
|
||||
"QPushButton:hover { background: #27ae60; }"
|
||||
f"QPushButton:hover {{ background: {theme.OK_HOVER}; }}"
|
||||
)
|
||||
self._info_labels["rx_tx"].setText("—")
|
||||
self._info_labels["handshake"].setText("—")
|
||||
|
||||
+321
-153
@@ -5,77 +5,308 @@ distinctes dans le seul panneau d'administration, cinq bandeaux d'onglets sans
|
||||
rapport de teinte entre eux, et des blocs de résultat dont le fond ne
|
||||
correspondait ni à celui de la page ni à celui de leurs voisins. Tout passe
|
||||
désormais par ce module — une couleur ne se choisit plus au cas par cas.
|
||||
|
||||
Deux palettes cohabitent. Les noms exportés — `BG`, `TEXT`, `TAB_CSS`… — ne
|
||||
changent pas : `apply()` les rebranche sur la palette choisie au démarrage,
|
||||
et les cent soixante-dix appels répartis dans l'interface continuent de lire
|
||||
`theme.BG` sans rien savoir du thème actif. C'est ce qui permet d'ajouter une
|
||||
palette sans toucher à un seul widget.
|
||||
|
||||
Le choix est lu une fois, au lancement (cf. `main.py`) : les feuilles de style
|
||||
sont posées dans les constructeurs des fenêtres, et une bascule à chaud
|
||||
demanderait de les reconstruire — celle qui porte l'état du tunnel comprise.
|
||||
"""
|
||||
|
||||
# ── Fonds ────────────────────────────────────────────────────────────────────
|
||||
BG = "#1c2833" # fond de l'application et de toutes les pages
|
||||
BG_SUNKEN = "#17202a" # zones en creux : configurations, blocs de code
|
||||
BG_RAISED = "#253545" # éléments posés sur le fond : listes, en-têtes secondaires
|
||||
BORDER = "rgba(255,255,255,0.18)"
|
||||
BORDER_SOFT = "rgba(255,255,255,0.10)"
|
||||
INPUT_BG = "rgba(255,255,255,0.10)" # fond des champs de saisie et listes
|
||||
# ── Palettes ────────────────────────────────────────────────────────────────
|
||||
# Chaque palette porte les mêmes clés ; ce sont elles que `apply()` publie
|
||||
# comme variables de module. Tout ce qui dépend du thème vit ici, y compris
|
||||
# ce qui était auparavant écrit au fil des feuilles de style ci-dessous —
|
||||
# une valeur en dur dans `TAB_CSS` aurait été un fond blanc translucide posé
|
||||
# sur un thème clair, c'est-à-dire rien du tout.
|
||||
|
||||
# ── Textes ───────────────────────────────────────────────────────────────────
|
||||
TEXT = "white"
|
||||
TEXT_MUTED = "rgba(255,255,255,0.70)"
|
||||
TEXT_FAINT = "rgba(255,255,255,0.50)"
|
||||
TEXT_CODE = "#a8d8ea" # texte à chasse fixe (aperçus de config, rapports)
|
||||
_DARK = {
|
||||
# Fonds
|
||||
"BG": "#1c2833", # fond de l'application et de toutes les pages
|
||||
"BG_SUNKEN": "#17202a", # zones en creux : configurations, blocs de code
|
||||
"BG_RAISED": "#253545", # posé sur le fond : listes, en-têtes secondaires
|
||||
"BORDER": "rgba(255,255,255,0.18)",
|
||||
"BORDER_SOFT": "rgba(255,255,255,0.10)",
|
||||
"INPUT_BG": "rgba(255,255,255,0.10)", # champs de saisie et listes
|
||||
# Variantes d'état des champs et boutons, jusqu'ici en dur dans TAB_CSS.
|
||||
"INPUT_BG_READONLY": "rgba(255,255,255,0.06)",
|
||||
"INPUT_BG_DISABLED": "rgba(255,255,255,0.04)",
|
||||
"SPIN_BUTTON_BG": "rgba(255,255,255,0.15)",
|
||||
"BUTTON_DISABLED_BG": "rgba(255,255,255,0.08)",
|
||||
|
||||
# ── Accent ───────────────────────────────────────────────────────────────────
|
||||
ACCENT = "#2471a3"
|
||||
ACCENT_HOVER = "#1a5276"
|
||||
ACCENT_ACTIVE = "#154360"
|
||||
ACCENT_LIGHT = "#5dade2"
|
||||
# Textes
|
||||
"TEXT": "white",
|
||||
"TEXT_MUTED": "rgba(255,255,255,0.70)",
|
||||
"TEXT_FAINT": "rgba(255,255,255,0.50)",
|
||||
"TEXT_CODE": "#a8d8ea", # chasse fixe (aperçus de config, rapports)
|
||||
# Texte posé sur un aplat saturé — bouton plein, onglet actif, bandeau.
|
||||
# Distinct de TEXT : sur un thème clair, TEXT est presque noir et
|
||||
# deviendrait illisible sur un fond d'accent.
|
||||
"ON_SOLID": "white",
|
||||
"BANNER_TEXT": "white",
|
||||
|
||||
# ── États ────────────────────────────────────────────────────────────────────
|
||||
# Chaque état porte un trio : teinte du texte, fond translucide, bouton plein.
|
||||
OK_TEXT, OK_BG, OK_SOLID = "#a9dfbf", "rgba(39,174,96,0.18)", "#1e8449"
|
||||
WARN_TEXT, WARN_BG, WARN_SOLID = "#f7dc6f", "rgba(241,196,15,0.15)", "#b7950b"
|
||||
FAIL_TEXT, FAIL_BG, FAIL_SOLID = "#f1948a", "rgba(231,76,60,0.18)", "#c0392b"
|
||||
SKIP_TEXT, SKIP_BG = "rgba(255,255,255,0.45)", "rgba(255,255,255,0.05)"
|
||||
# Accent
|
||||
"ACCENT": "#2471a3",
|
||||
"ACCENT_HOVER": "#1a5276",
|
||||
"ACCENT_ACTIVE": "#154360",
|
||||
"ACCENT_LIGHT": "#5dade2",
|
||||
# Fond de l'encart d'introduction : l'accent très dilué.
|
||||
"HINT_BG": "rgba(36,113,163,0.16)",
|
||||
|
||||
# Teintes de survol des boutons pleins ok/fail — reprises telles quelles à
|
||||
# plusieurs endroits (panneau admin, fenêtre principale) : un seul nom leur
|
||||
# évite de dériver l'une de l'autre au fil des modifications.
|
||||
OK_HOVER = "#27ae60"
|
||||
FAIL_HOVER = "#e74c3c"
|
||||
# États — chacun porte un trio : texte, fond translucide, bouton plein.
|
||||
"OK_TEXT": "#a9dfbf", "OK_BG": "rgba(39,174,96,0.18)", "OK_SOLID": "#1e8449",
|
||||
"WARN_TEXT": "#f7dc6f", "WARN_BG": "rgba(241,196,15,0.15)", "WARN_SOLID": "#b7950b",
|
||||
"FAIL_TEXT": "#f1948a", "FAIL_BG": "rgba(231,76,60,0.18)", "FAIL_SOLID": "#c0392b",
|
||||
"SKIP_TEXT": "rgba(255,255,255,0.45)",
|
||||
"SKIP_BG": "rgba(255,255,255,0.05)",
|
||||
# Survols des boutons pleins — nommés pour ne pas dériver l'un de l'autre.
|
||||
"OK_HOVER": "#27ae60",
|
||||
"WARN_HOVER": "#d68910",
|
||||
"FAIL_HOVER": "#e74c3c",
|
||||
|
||||
_STATE = {
|
||||
"ok": (OK_TEXT, OK_BG),
|
||||
"warn": (WARN_TEXT, WARN_BG),
|
||||
"fail": (FAIL_TEXT, FAIL_BG),
|
||||
"skip": (SKIP_TEXT, SKIP_BG),
|
||||
"idle": (TEXT_MUTED, "transparent"),
|
||||
# Badge de ping (fenêtre principale). Teintes propres, volontairement plus
|
||||
# tranchées que les aplats d'état : le badge est petit et lu d'un coup.
|
||||
"PING_GOOD_BG": "#1e8449", "PING_GOOD_FG": "white",
|
||||
"PING_MEDIUM_BG": "#d4ac0d", "PING_MEDIUM_FG": "black",
|
||||
"PING_BAD_BG": "#922b21", "PING_BAD_FG": "white",
|
||||
|
||||
# Graphe de bande passante : fond, puis trait et remplissage par série.
|
||||
"GRAPH_BG": "#17202a",
|
||||
"GRAPH_RX": "#5dade2", "GRAPH_RX_FILL": "#1a3a52",
|
||||
"GRAPH_TX": "#58d68d", "GRAPH_TX_FILL": "#1a3d2b",
|
||||
"GRAPH_AXIS": "#2e4057", "GRAPH_LABEL": "#5d6d7e",
|
||||
|
||||
# Bandeaux d'onglets. Six teintes calées sur la même luminosité et la même
|
||||
# saturation : les onglets restent distinguables sans que l'un paraisse
|
||||
# plus clair ou plus saturé que les autres.
|
||||
"BANNER": {
|
||||
"wireguard": "#1a5276",
|
||||
"keys": "#1a4a6e",
|
||||
"test": "#1a4a7a",
|
||||
"settings": "#155e52",
|
||||
"security": "#7a3b1a",
|
||||
"about": "#243b53",
|
||||
},
|
||||
}
|
||||
|
||||
# ── Badge de ping (fenêtre principale) ──────────────────────────────────────
|
||||
PING_CSS = {
|
||||
"good": "background:#1e8449; color:white;",
|
||||
"medium": "background:#d4ac0d; color:black;",
|
||||
"bad": "background:#922b21; color:white;",
|
||||
"offline": f"background:{BG_RAISED}; color:{TEXT_FAINT};",
|
||||
# Le thème clair n'est pas l'inverse du sombre : inverser une luminance donne
|
||||
# des aplats criards et des textes d'état illisibles. Les fonds descendent
|
||||
# vers un gris très pâle plutôt que vers le blanc pur — un blanc plein fait
|
||||
# vibrer les bordures et fatigue sur une fenêtre qui reste ouverte la journée.
|
||||
# Les teintes d'état, elles, sont assombries : `#a9dfbf` est un vert lisible
|
||||
# sur ardoise et invisible sur papier.
|
||||
#
|
||||
# L'accent, les bandeaux et les aplats pleins ne bougent pas : ils portent
|
||||
# déjà un texte blanc dans les deux thèmes, et les garder identiques donne à
|
||||
# l'application la même identité d'un thème à l'autre.
|
||||
_LIGHT = {
|
||||
"BG": "#f4f6f8",
|
||||
"BG_SUNKEN": "#e9edf1",
|
||||
"BG_RAISED": "#ffffff",
|
||||
"BORDER": "rgba(28,40,51,0.22)",
|
||||
"BORDER_SOFT": "rgba(28,40,51,0.12)",
|
||||
"INPUT_BG": "#ffffff",
|
||||
"INPUT_BG_READONLY": "rgba(28,40,51,0.05)",
|
||||
"INPUT_BG_DISABLED": "rgba(28,40,51,0.03)",
|
||||
"SPIN_BUTTON_BG": "rgba(28,40,51,0.10)",
|
||||
"BUTTON_DISABLED_BG": "rgba(28,40,51,0.08)",
|
||||
|
||||
"TEXT": "#1c2833", # l'ardoise du thème sombre, passée au texte
|
||||
"TEXT_MUTED": "rgba(28,40,51,0.72)",
|
||||
"TEXT_FAINT": "rgba(28,40,51,0.50)",
|
||||
"TEXT_CODE": "#1a5276",
|
||||
"ON_SOLID": "white",
|
||||
"BANNER_TEXT": "white",
|
||||
|
||||
"ACCENT": "#2471a3",
|
||||
"ACCENT_HOVER": "#1a5276",
|
||||
"ACCENT_ACTIVE": "#154360",
|
||||
# Plus soutenu que dans le thème sombre : `#5dade2` sert aussi de couleur
|
||||
# de texte au journal, et se perd sur un fond pâle.
|
||||
"ACCENT_LIGHT": "#2e86c1",
|
||||
"HINT_BG": "rgba(36,113,163,0.10)",
|
||||
|
||||
"OK_TEXT": "#1a6b3c", "OK_BG": "rgba(39,174,96,0.14)", "OK_SOLID": "#1e8449",
|
||||
"WARN_TEXT": "#8a6d0b", "WARN_BG": "rgba(241,196,15,0.22)", "WARN_SOLID": "#b7950b",
|
||||
"FAIL_TEXT": "#a5281b", "FAIL_BG": "rgba(231,76,60,0.12)", "FAIL_SOLID": "#c0392b",
|
||||
"SKIP_TEXT": "rgba(28,40,51,0.45)",
|
||||
"SKIP_BG": "rgba(28,40,51,0.05)",
|
||||
"OK_HOVER": "#27ae60",
|
||||
"WARN_HOVER": "#d68910",
|
||||
"FAIL_HOVER": "#e74c3c",
|
||||
|
||||
"PING_GOOD_BG": "#1e8449", "PING_GOOD_FG": "white",
|
||||
"PING_MEDIUM_BG": "#b7950b", "PING_MEDIUM_FG": "white",
|
||||
"PING_BAD_BG": "#922b21", "PING_BAD_FG": "white",
|
||||
|
||||
# Le graphe garde son fond en creux, pâle comme le reste ; les deux séries
|
||||
# sont assombries pour rester lisibles dessus, et leur remplissage devient
|
||||
# un voile de la même teinte au lieu d'un aplat sombre.
|
||||
"GRAPH_BG": "#e9edf1",
|
||||
"GRAPH_RX": "#2e86c1", "GRAPH_RX_FILL": "#cfe3f2",
|
||||
"GRAPH_TX": "#1e8449", "GRAPH_TX_FILL": "#d3efdd",
|
||||
"GRAPH_AXIS": "#c3ccd6", "GRAPH_LABEL": "#5d6d7e",
|
||||
|
||||
"BANNER": {
|
||||
"wireguard": "#1a5276",
|
||||
"keys": "#1a4a6e",
|
||||
"test": "#1a4a7a",
|
||||
"settings": "#155e52",
|
||||
"security": "#7a3b1a",
|
||||
"about": "#243b53",
|
||||
},
|
||||
}
|
||||
|
||||
# ── Entrées de journal (fenêtre principale) ─────────────────────────────────
|
||||
LOG_COLORS = {
|
||||
"success": OK_TEXT,
|
||||
"error": FAIL_TEXT,
|
||||
"warning": WARN_TEXT,
|
||||
"info": ACCENT_LIGHT,
|
||||
}
|
||||
#: Palettes proposées, dans l'ordre où l'interface les présente.
|
||||
PALETTES = {"dark": "Sombre", "light": "Clair"}
|
||||
_PALETTES = {"dark": _DARK, "light": _LIGHT}
|
||||
|
||||
# ── Bandeaux d'onglets ───────────────────────────────────────────────────────
|
||||
# Six teintes calées sur la même luminosité et la même saturation : les
|
||||
# onglets restent distinguables sans que l'un paraisse plus clair ou plus
|
||||
# saturé que les autres, ce qui était le cas des valeurs choisies à la main.
|
||||
BANNER = {
|
||||
"wireguard": "#1a5276",
|
||||
"keys": "#1a4a6e",
|
||||
"test": "#1a4a7a",
|
||||
"settings": "#155e52",
|
||||
"security": "#7a3b1a",
|
||||
"about": "#243b53",
|
||||
}
|
||||
#: Palette active. Renseignée par `apply()`, lue par le panneau de réglages.
|
||||
CURRENT = "dark"
|
||||
|
||||
|
||||
def apply(name: str) -> str:
|
||||
"""Rend `name` active et recalcule tout ce qui en dépend.
|
||||
|
||||
Rend le nom réellement appliqué : un réglage inconnu — configuration
|
||||
écrite à la main, retour à une version antérieure — retombe sur le thème
|
||||
sombre plutôt que de faire échouer le démarrage de l'interface.
|
||||
|
||||
À appeler avant de construire la moindre fenêtre : les feuilles de style
|
||||
sont posées dans les constructeurs et ne sont pas relues ensuite.
|
||||
"""
|
||||
global CURRENT
|
||||
CURRENT = name if name in _PALETTES else "dark"
|
||||
globals().update(_PALETTES[CURRENT])
|
||||
_derive()
|
||||
return CURRENT
|
||||
|
||||
|
||||
def _derive() -> None:
|
||||
"""Recompose les valeurs dérivées de la palette qui vient d'être publiée.
|
||||
|
||||
Les fonctions du module lisent les jetons à l'appel et suivent donc
|
||||
d'elles-mêmes ; seules les constantes calculées une fois à l'import —
|
||||
feuilles de style complètes et tables d'état — doivent être refaites.
|
||||
"""
|
||||
globals().update(
|
||||
_STATE={
|
||||
"ok": (OK_TEXT, OK_BG),
|
||||
"warn": (WARN_TEXT, WARN_BG),
|
||||
"fail": (FAIL_TEXT, FAIL_BG),
|
||||
"skip": (SKIP_TEXT, SKIP_BG),
|
||||
"idle": (TEXT_MUTED, "transparent"),
|
||||
},
|
||||
PING_CSS={
|
||||
"good": f"background:{PING_GOOD_BG}; color:{PING_GOOD_FG};",
|
||||
"medium": f"background:{PING_MEDIUM_BG}; color:{PING_MEDIUM_FG};",
|
||||
"bad": f"background:{PING_BAD_BG}; color:{PING_BAD_FG};",
|
||||
"offline": f"background:{BG_RAISED}; color:{TEXT_FAINT};",
|
||||
},
|
||||
LOG_COLORS={
|
||||
"success": OK_TEXT,
|
||||
"error": FAIL_TEXT,
|
||||
"warning": WARN_TEXT,
|
||||
"info": ACCENT_LIGHT,
|
||||
},
|
||||
TAB_CSS=f"""
|
||||
QWidget {{ background-color: {BG}; color: {TEXT}; }}
|
||||
QLabel {{ color: {TEXT}; background: transparent; }}
|
||||
QCheckBox {{ color: {TEXT}; background: transparent; }}
|
||||
QGroupBox {{
|
||||
color: {TEXT};
|
||||
background: transparent;
|
||||
border: 1px solid {BORDER};
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QGroupBox::title {{
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 4px;
|
||||
color: {TEXT};
|
||||
}}
|
||||
QLineEdit {{
|
||||
background: {INPUT_BG};
|
||||
color: {TEXT};
|
||||
border: 1px solid {BORDER};
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
}}
|
||||
QLineEdit:read-only {{ background: {INPUT_BG_READONLY}; }}
|
||||
QLineEdit:disabled {{ background: {INPUT_BG_DISABLED};
|
||||
color: {TEXT_FAINT}; }}
|
||||
QSpinBox {{
|
||||
background: {INPUT_BG};
|
||||
color: {TEXT};
|
||||
border: 1px solid {BORDER};
|
||||
border-radius: 4px;
|
||||
padding: 3px 6px;
|
||||
}}
|
||||
QSpinBox::up-button, QSpinBox::down-button {{
|
||||
background: {SPIN_BUTTON_BG};
|
||||
}}
|
||||
QComboBox {{
|
||||
background: {INPUT_BG};
|
||||
color: {TEXT};
|
||||
border: 1px solid {BORDER};
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
}}
|
||||
QComboBox::drop-down {{ border: none; }}
|
||||
QComboBox QAbstractItemView {{
|
||||
background: {BG_RAISED};
|
||||
color: {TEXT};
|
||||
selection-background-color: {ACCENT};
|
||||
selection-color: {ON_SOLID};
|
||||
}}
|
||||
QTextEdit {{
|
||||
background: {BG_SUNKEN};
|
||||
color: {TEXT_CODE};
|
||||
border: 1px solid {BORDER};
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QPushButton {{
|
||||
background: {ACCENT};
|
||||
color: {ON_SOLID};
|
||||
border-radius: 5px;
|
||||
padding: 7px 12px;
|
||||
border: none;
|
||||
}}
|
||||
QPushButton:hover {{ background: {ACCENT_HOVER}; }}
|
||||
QPushButton:checked {{ background: {ACCENT_ACTIVE}; }}
|
||||
QPushButton:disabled {{ background: {BUTTON_DISABLED_BG};
|
||||
color: {TEXT_FAINT}; }}
|
||||
""",
|
||||
TABBAR_CSS=f"""
|
||||
QTabWidget::pane {{ border: none; background: {BG}; }}
|
||||
QTabBar::tab {{
|
||||
background: {BG_RAISED};
|
||||
color: {TEXT_MUTED};
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
}}
|
||||
QTabBar::tab:selected {{ background: {ACCENT}; color: {ON_SOLID}; }}
|
||||
QTabBar::tab:hover:!selected {{ background: {ACCENT_HOVER};
|
||||
color: {ON_SOLID}; }}
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def state_text(state: str) -> str:
|
||||
"""Couleur de texte d'un état, pour un rendu qui se passe de fond.
|
||||
|
||||
Le panneau de diagnostic tenait sa propre table état → couleur, bâtie à
|
||||
l'import de son module : elle gardait donc les teintes du thème chargé à
|
||||
ce moment-là, quel que soit celui appliqué ensuite. La question revient
|
||||
ici, où la réponse suit la palette active.
|
||||
"""
|
||||
return _STATE.get(state, _STATE["idle"])[0]
|
||||
|
||||
|
||||
def result_style(state: str, font_size: int = 11) -> str:
|
||||
@@ -116,109 +347,40 @@ def scrollarea_style() -> str:
|
||||
)
|
||||
|
||||
|
||||
def banner_style(color: str) -> str:
|
||||
return (f"background: {color}; color: {TEXT}; font-size: 13px;"
|
||||
" font-weight: bold; padding-left: 8px;")
|
||||
def banner_style(color: str, fg: str = "") -> str:
|
||||
"""Bandeau de titre posé en tête d'une fenêtre ou d'une page.
|
||||
|
||||
`fg` vaut le blanc des bandeaux d'onglets, qui sont des aplats saturés.
|
||||
Le bandeau de titre du panneau d'administration, lui, prend le fond en
|
||||
creux de la fenêtre : en thème clair, c'est un gris très pâle sur lequel
|
||||
du blanc ne se voit pas. Il passe donc la couleur de texte ordinaire.
|
||||
"""
|
||||
return (f"background: {color}; color: {fg or BANNER_TEXT};"
|
||||
" font-size: 13px; font-weight: bold; padding-left: 8px;")
|
||||
|
||||
|
||||
#: CSS sombre commun à toutes les pages.
|
||||
TAB_CSS = f"""
|
||||
QWidget {{ background-color: {BG}; color: {TEXT}; }}
|
||||
QLabel {{ color: {TEXT}; background: transparent; }}
|
||||
QCheckBox {{ color: {TEXT}; background: transparent; }}
|
||||
QGroupBox {{
|
||||
color: {TEXT};
|
||||
background: transparent;
|
||||
border: 1px solid {BORDER};
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
QGroupBox::title {{
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 4px;
|
||||
color: {TEXT};
|
||||
}}
|
||||
QLineEdit {{
|
||||
background: {INPUT_BG};
|
||||
color: {TEXT};
|
||||
border: 1px solid {BORDER};
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
}}
|
||||
QLineEdit:read-only {{ background: rgba(255,255,255,0.06); }}
|
||||
QLineEdit:disabled {{ background: rgba(255,255,255,0.04);
|
||||
color: {TEXT_FAINT}; }}
|
||||
QSpinBox {{
|
||||
background: {INPUT_BG};
|
||||
color: {TEXT};
|
||||
border: 1px solid {BORDER};
|
||||
border-radius: 4px;
|
||||
padding: 3px 6px;
|
||||
}}
|
||||
QSpinBox::up-button, QSpinBox::down-button {{
|
||||
background: rgba(255,255,255,0.15);
|
||||
}}
|
||||
QComboBox {{
|
||||
background: {INPUT_BG};
|
||||
color: {TEXT};
|
||||
border: 1px solid {BORDER};
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
}}
|
||||
QComboBox::drop-down {{ border: none; }}
|
||||
QComboBox QAbstractItemView {{
|
||||
background: {BG_RAISED};
|
||||
color: {TEXT};
|
||||
selection-background-color: {ACCENT};
|
||||
}}
|
||||
QTextEdit {{
|
||||
background: {BG_SUNKEN};
|
||||
color: {TEXT_CODE};
|
||||
border: 1px solid {BORDER};
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QPushButton {{
|
||||
background: {ACCENT};
|
||||
color: {TEXT};
|
||||
border-radius: 5px;
|
||||
padding: 7px 12px;
|
||||
border: none;
|
||||
}}
|
||||
QPushButton:hover {{ background: {ACCENT_HOVER}; }}
|
||||
QPushButton:checked {{ background: {ACCENT_ACTIVE}; }}
|
||||
QPushButton:disabled {{ background: rgba(255,255,255,0.08);
|
||||
color: {TEXT_FAINT}; }}
|
||||
"""
|
||||
def primary_button_style(solid: str = "", hover: str = "") -> str:
|
||||
"""Bouton d'action principale d'une page (lancer un test, par exemple).
|
||||
|
||||
#: Barre d'onglets, utilisée par le panneau principal et les sous-onglets.
|
||||
TABBAR_CSS = f"""
|
||||
QTabWidget::pane {{ border: none; background: {BG}; }}
|
||||
QTabBar::tab {{
|
||||
background: {BG_RAISED};
|
||||
color: {TEXT_MUTED};
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
}}
|
||||
QTabBar::tab:selected {{ background: {ACCENT}; color: {TEXT}; }}
|
||||
QTabBar::tab:hover:!selected {{ background: {ACCENT_HOVER};
|
||||
color: {TEXT}; }}
|
||||
"""
|
||||
|
||||
|
||||
def primary_button_style(solid: str = OK_SOLID, hover: str = OK_HOVER) -> str:
|
||||
"""Bouton d'action principale d'une page (lancer un test, par exemple)."""
|
||||
return (f"QPushButton {{ padding: 9px; background: {solid}; color: {TEXT};"
|
||||
Les valeurs par défaut sont résolues à l'appel, et non à la définition :
|
||||
prises dans la signature, elles auraient figé les teintes de la palette
|
||||
active au moment de l'import — soit toujours celles du thème sombre.
|
||||
"""
|
||||
solid = solid or OK_SOLID
|
||||
hover = hover or OK_HOVER
|
||||
return (f"QPushButton {{ padding: 9px; background: {solid};"
|
||||
f" color: {ON_SOLID};"
|
||||
f" border-radius: 5px; font-weight: bold; border: none; }}"
|
||||
f"QPushButton:hover {{ background: {hover}; }}")
|
||||
|
||||
|
||||
def secondary_button_style() -> str:
|
||||
"""Bouton d'action secondaire d'une page (à côté d'une action principale)."""
|
||||
return (f"QPushButton {{ padding: 9px; background: {BG_RAISED}; color: {TEXT};"
|
||||
return (f"QPushButton {{ padding: 9px; background: {BG_RAISED};"
|
||||
f" color: {TEXT};"
|
||||
f" border-radius: 5px; border: none; }}"
|
||||
f"QPushButton:hover {{ background: {ACCENT_HOVER}; }}")
|
||||
f"QPushButton:hover {{ background: {ACCENT_HOVER};"
|
||||
f" color: {ON_SOLID}; }}")
|
||||
|
||||
|
||||
def hint_style() -> str:
|
||||
@@ -227,7 +389,13 @@ def hint_style() -> str:
|
||||
Assez contrasté pour se détacher du fond de page, assez sobre pour ne pas
|
||||
concurrencer le contenu : un liseré d'accent à gauche, pas de cadre plein.
|
||||
"""
|
||||
return (f"background: rgba(36,113,163,0.16);"
|
||||
return (f"background: {HINT_BG};"
|
||||
f" border-left: 3px solid {ACCENT_LIGHT};"
|
||||
f" border-radius: 4px; padding: 9px 11px;"
|
||||
f" color: {TEXT_MUTED}; font-size: 11px;")
|
||||
|
||||
|
||||
# Palette par défaut publiée dès l'import : le module reste utilisable sans
|
||||
# initialisation explicite, et `main.py` n'a qu'à rappeler `apply()` avec le
|
||||
# réglage lu en configuration.
|
||||
apply("dark")
|
||||
|
||||
Reference in New Issue
Block a user