| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062 |
- # -*- coding: utf-8 -*-
- import os
- from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame,
- QLabel, QScrollArea, QGridLayout, QPushButton,
- QStackedWidget)
- from PyQt6.QtCore import Qt, QSize, QTimer, QPoint
- from PyQt6.QtGui import QPixmap, QIcon, QFont
- from PyQt6.QtWidgets import QGraphicsDropShadowEffect
- from PyQt6.QtGui import QColor
- from core.translations import TRANSLATIONS
- from utils.paths import THUMBNAILS_DIR, ASSETS_DIR, SVG_ICONS
- from gui.utils_qt import get_svg_pixmap
- from gui.onboarding_widget import OnboardingWidget, HighlightOverlay
- class DashboardViewQt(QWidget):
- def __init__(self, controller):
- super().__init__()
- self.controller = controller
- self.setStyleSheet("background-color: #2b2b2b; color: white;")
- self.translation_map = {}
-
- # Récupération des textes
- lang = self.controller.config_manager.get_item("machine_settings", "language", "English")
- self.texts = TRANSLATIONS.get(lang, TRANSLATIONS["English"])["dashboard"]
- self.text = TRANSLATIONS.get(lang, TRANSLATIONS["English"]).get("topbar", {})
- # 1. Layout Global VERTICAL
- self.main_layout = QVBoxLayout(self)
- self.main_layout.setContentsMargins(30, 20, 30, 20)
- self.main_layout.setSpacing(5)
- # 2. LOGO en haut (Pleine largeur)
- self.setup_logo_header()
- # --- ligne de séparation ---
- line = QFrame()
- line.setFixedHeight(5)
- line.setStyleSheet(f"""
- background-color: #444;
- margin-left: 50px;
- margin-right: 50px;
- """)
- self.main_layout.addWidget(line)
- # 3. Conteneur HORIZONTAL pour le reste
- self.content_layout = QHBoxLayout()
- self.content_layout.setSpacing(30)
- self.main_layout.addLayout(self.content_layout)
- # 4. Appels des fonctions de création
- self.setup_left_column() # Ira dans content_layout (à gauche)
- self.setup_right_column() # Ira dans content_layout (à droite)
- # --- THUMBNAILS ---
- self.load_thumbnails()
- QTimer.singleShot(0, self._init_history_area)
- def setup_logo_header(self):
- """Crée le bandeau titre ALIG en haut de la vue"""
- self.logo_label = QLabel("ALIG")
- self.logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
-
- # Effet de lueur (Glow)
- glow = QGraphicsDropShadowEffect()
- glow.setBlurRadius(40)
- glow.setColor(QColor(31, 106, 165, 200))
- glow.setOffset(0)
- self.logo_label.setGraphicsEffect(glow)
- # Style agrandi pour le mode plein écran
- self.logo_label.setStyleSheet("""
- font-size: 70px;
- font-weight: 900;
- color: #ADE1FF;
- letter-spacing: 15px;
- margin-bottom: 10px;
- background: transparent;
- """)
-
- # On l'ajoute au layout VERTICAL principal
- self.main_layout.addWidget(self.logo_label)
- def setup_left_column(self):
- left_container = QFrame()
- left_container.setFixedWidth(420)
- left_container.setStyleSheet("background: transparent;")
- self._left_container = left_container
- layout = QVBoxLayout(left_container)
- layout.setContentsMargins(0, 0, 0, 10)
- # 1. Zone de défilement
- scroll = QScrollArea()
- scroll.setWidgetResizable(True)
- scroll.setFrameShape(QFrame.Shape.NoFrame)
- scroll.setStyleSheet("background: transparent;")
- scroll.viewport().setStyleSheet("background: transparent;")
- content = QWidget()
- content.setStyleSheet("background: transparent;")
- self.modes_layout = QVBoxLayout(content)
- self.modes_layout.setSpacing(15)
- self.modes_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
- # --- LISTE DES MODES MISE À JOUR ---
- # On utilise maintenant SVG_ICONS pour les chemins
- modes = [
- ("raster_title", "raster_desc", self.controller.show_raster_mode, SVG_ICONS["RASTER"], "normal"),
- ("dithering_title", "dithering_desc", None, SVG_ICONS["DITHER"], "disabled"),
- ("infill_title", "infill_desc", None, SVG_ICONS["INFILL"], "disabled"),
- ("parser_title", "parser_desc", self.controller.show_checker_mode, SVG_ICONS["GCODE"], "normal"),
- ("calibration_title", "calibration_desc", self.controller.show_calibration_mode,SVG_ICONS["LATENCY"], "normal"),
- ("settings_title", "settings_desc", self.controller.show_settings_mode, SVG_ICONS["GEAR"], "normal"),
- ]
- for title_key, desc_key, callback, icon_source, state in modes:
- card = self.create_mode_card(title_key, desc_key, callback, icon_source, state)
- self.modes_layout.addWidget(card)
- scroll.setWidget(content)
- layout.addWidget(scroll)
- # --- SECTION CRÉDITS ---
- credits_label = QLabel(self.text.get("credits", "By MoMo"))
- credits_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
- credits_label.setStyleSheet("color: #444444; font-size: 12px; border: none; background: transparent;")
- layout.addWidget(credits_label)
- self.content_layout.addWidget(left_container)
-
- def create_mode_card(self, title_key, desc_key, callback, icon_data, state):
- """Crée une carte de mode et enregistre ses labels pour la traduction"""
- from gui.utils_qt import get_svg_pixmap
-
- card = QFrame()
- card.setAttribute(Qt.WidgetAttribute.WA_Hover)
- card.setObjectName("modeCard")
- card.icon_data = icon_data
- card.state = state
- card.setFixedHeight(80)
-
- is_disabled = (state == "disabled" or callback is None)
- bg_color = "#1e1e1e" if is_disabled else "#2b2b2b"
- border_color = "#252525" if is_disabled else "#3d3d3d"
- title_color = "#777777" if is_disabled else "white"
- desc_color = "#555555" if is_disabled else "gray"
- icon_color = "#555555" if is_disabled else "#FFFFFF"
- style = f"""
- QFrame#modeCard {{
- background-color: {bg_color};
- border: 2px solid {border_color};
- border-radius: 15px;
- }}
- """
- if not is_disabled:
- style += "QFrame#modeCard:hover { border-color: #1F6AA5; background-color: #333333; }"
- card.setCursor(Qt.CursorShape.PointingHandCursor)
- card.setStyleSheet(style)
- layout = QHBoxLayout(card)
- layout.setContentsMargins(15, 10, 15, 10)
- layout.setSpacing(15)
- # Icône
- icon_label = QLabel()
- icon_label.setFixedWidth(45)
- if isinstance(icon_data, str) and icon_data.endswith(".svg"):
- pix = get_svg_pixmap(icon_data, size=QSize(35, 35), color_hex=icon_color)
- icon_label.setPixmap(pix)
- elif isinstance(icon_data, str) and icon_data.endswith(".png"):
- path = os.path.join(ASSETS_DIR, icon_data)
- pix = QPixmap(path).scaled(40, 40, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
- icon_label.setPixmap(pix)
- else:
- icon_label.setText(icon_data)
- icon_label.setFont(QFont("Arial", 24))
- icon_label.setStyleSheet(f"color: {title_color}; border: none; background: transparent;")
-
- icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
- layout.addWidget(icon_label)
- # Zone de texte
- text_layout = QVBoxLayout()
- text_layout.setSpacing(2)
- # --- TITRE (Enregistré dans la map) ---
- t_lbl = QLabel(self.texts.get(title_key, title_key))
- t_lbl.setStyleSheet(f"color: {title_color}; font-weight: bold; font-size: 15px; border: none; background: transparent;")
- self.translation_map[t_lbl] = title_key # Stockage de la clé
-
- # --- DESCRIPTION (Enregistrée dans la map) ---
- d_lbl = QLabel(self.texts.get(desc_key, desc_key))
- d_lbl.setWordWrap(True)
- d_lbl.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
- d_lbl.setStyleSheet(f"color: {desc_color}; font-size: 11px; border: none; background: transparent;")
- self.translation_map[d_lbl] = desc_key # Stockage de la clé
-
- text_layout.addWidget(t_lbl)
- text_layout.addWidget(d_lbl)
- text_layout.addStretch()
-
- layout.addLayout(text_layout)
- layout.setStretch(1, 1)
- card.icon_label = icon_label
- card.title_label = t_lbl
- card.desc_label = d_lbl
- if not is_disabled:
- card.mousePressEvent = lambda e: callback()
- else:
- card.mousePressEvent = lambda e: None
- for child in card.findChildren(QLabel):
- child.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
- return card
-
- def setup_right_column(self):
- right_container = QWidget()
- right_container.setStyleSheet("background: transparent;")
- self._right_container = right_container
- layout = QVBoxLayout(right_container)
- layout.setContentsMargins(0, 0, 0, 0)
- # # Titre A.L.I.G.
- # title = QLabel("ALIG")
- # title.setAlignment(Qt.AlignmentFlag.AlignCenter)
- # glow = QGraphicsDropShadowEffect()
- # glow.setBlurRadius(100)
- # glow.setColor(QColor(31, 106, 165, 200)) # Bleu ALIG
- # glow.setOffset(0)
- # title.setGraphicsEffect(glow)
- # title.setStyleSheet("font-size: 60px; font-weight: 900; color: #ADE1FF; letter-spacing: 10px;")
-
- # layout.addWidget(title)
- # Zone Historique (Simplifiée pour l'instant)
- self.history_label = QLabel(self.texts.get("history", "History"))
- self.history_label.setStyleSheet("font-weight: bold; color: white; margin-top: 10px; background: transparent; border: none;")
- layout.addWidget(self.history_label)
- self.history_area = QScrollArea()
- self.history_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
- self.history_area.setWidgetResizable(True)
- self.history_area.setStyleSheet("background: transparent; border-radius: 10px; border: 1px solid #333;")
- self.history_area.viewport().setStyleSheet("background: transparent;")
- self.history_area.verticalScrollBar().setStyleSheet("""
- QScrollBar:vertical {
- border: none;
- background: #202020;
- width: 10px;
- margin: 0px 0px 0px 0px;
- }
- QScrollBar::handle:vertical {
- background: #3e3e3e;
- min-height: 20px;
- border-radius: 5px;
- }
- QScrollBar::handle:vertical:hover {
- background: #1F6AA5; /* Ton bleu ALIG au survol */
- }
- QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
- height: 0px;
- }
- QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
- background: none;
- }
- """)
- self.history_area.verticalScrollBar().setSingleStep(15)
-
- # Conteneur stacké : page 0 = onboarding, page 1 = grille vignettes
- self._history_stack_widget = QWidget()
- self._history_stack_widget.setStyleSheet("background: transparent;")
- stack_outer = QVBoxLayout(self._history_stack_widget)
- stack_outer.setContentsMargins(0, 0, 0, 0)
- stack_outer.setSpacing(0)
- self._history_stack = QStackedWidget()
- self._history_stack.setStyleSheet("background: transparent;")
- # Page 0 — onboarding (créé dans _init_history_area)
- self._onboarding_placeholder = QWidget()
- self._onboarding_placeholder.setStyleSheet("background: transparent;")
- self._history_stack.addWidget(self._onboarding_placeholder)
- # Page 1 — grille de vignettes
- thumb_widget = QWidget()
- thumb_widget.setStyleSheet("background: transparent;")
- self.thumb_grid = QGridLayout(thumb_widget)
- self.thumb_grid.setSpacing(15)
- self._history_stack.addWidget(thumb_widget)
- stack_outer.addWidget(self._history_stack)
- self.history_area.setWidget(self._history_stack_widget)
- layout.addWidget(self.history_area)
- # Statistiques
- self.setup_stats(layout)
- self.content_layout.addWidget(right_container)
- def load_thumbnails(self):
- thumb_dir = THUMBNAILS_DIR
- self.all_pixmaps = [] # On stocke des QPixmap au lieu de CTkImage
-
- if not os.path.exists(thumb_dir):
- os.makedirs(thumb_dir, exist_ok=True)
- self.render_grid()
- return
- try:
- # Récupération et tri des fichiers (identique à ton ancienne logique)
- files = [f for f in os.listdir(thumb_dir) if f.lower().endswith(".png")]
- files = [f for f in files if os.path.isfile(os.path.join(thumb_dir, f))]
- files.sort(key=lambda x: os.path.getmtime(os.path.join(thumb_dir, x)), reverse=True)
- for file in files:
- try:
- path = os.path.join(thumb_dir, file)
- if os.path.getsize(path) == 0: continue
- # En Qt, on charge directement le chemin avec QPixmap
- pixmap = QPixmap(path)
- if not pixmap.isNull():
- # On redimensionne tout de suite pour garder de bonnes perfs
- pixmap = pixmap.scaled(200, 200, Qt.AspectRatioMode.KeepAspectRatio,
- Qt.TransformationMode.SmoothTransformation)
- self.all_pixmaps.append(pixmap)
-
- except Exception as e:
- print(f"Erreur chargement vignette {file}: {e}")
-
- except Exception as e:
- print(f"Erreur accès dossier thumbnails: {e}")
- self.render_grid()
- def render_grid(self):
- """Affiche les vignettes en s'assurant de ne JAMAIS déclencher le scroll horizontal"""
- # S'assurer qu'on est bien sur la page vignettes
- if hasattr(self, "_history_stack") and self._history_stack.currentIndex() != 1:
- return
- # 1. Nettoyage
- while self.thumb_grid.count():
- item = self.thumb_grid.takeAt(0)
- if item.widget(): item.widget().deleteLater()
- if not hasattr(self, 'all_pixmaps') or not self.all_pixmaps:
- return
- # 2. CALCUL DU NOMBRE DE COLONNES
- # On définit une largeur MINIMALE pour une vignette
- min_item_width = 200
-
- # IMPORTANT : On retire une marge de sécurité (30px) pour la scrollbar verticale
- # et les éventuels paddings/borders de la grille.
- available_width = self.history_area.viewport().width() - 30
-
- # Sécurité si le widget n'est pas encore totalement rendu
- if available_width < 100: available_width = 400
- max_columns = max(1, available_width // min_item_width)
- # 3. CONFIGURATION DU STRETCH
- # On reset les colonnes précédentes (Qt garde les stretchs en mémoire sinon)
- for i in range(self.thumb_grid.columnCount() + 1):
- self.thumb_grid.setColumnStretch(i, 0)
-
- for i in range(max_columns):
- self.thumb_grid.setColumnStretch(i, 1)
- # 4. PLACEMENT DES VIGNETTES
- # Largeur précise d'une colonne pour le calcul des images
- col_w = available_width // max_columns
- for i, pixmap in enumerate(self.all_pixmaps):
- row = i // max_columns
- col = i % max_columns
-
- container = QFrame()
- container.setFixedHeight(220)
- # Utilise les couleurs du thème courant si disponibles
- _c = getattr(self, "_current_colors", None)
- if _c:
- bg = _c["bg_card"]
- brd = _c["border"]
- else:
- bg = "#2b2b2b"
- brd = "#3d3d3d"
- container.setStyleSheet(f"""
- QFrame {{
- background-color: {bg};
- border-radius: 8px;
- border: 1px solid {brd};
- }}
- QFrame:hover {{ border-color: #1F6AA5; background-color: {bg}; }}
- """)
-
- v_layout = QVBoxLayout(container)
- v_layout.setContentsMargins(5, 5, 5, 5) # Espace interne au cadre
-
- img_label = QLabel()
- # On scale l'image pour qu'elle soit un peu plus petite que le cadre
- # afin de ne pas "forcer" sur les bords du layout
- scaled_pix = pixmap.scaled(
- col_w - 20, 190,
- Qt.AspectRatioMode.KeepAspectRatio,
- Qt.TransformationMode.SmoothTransformation
- )
-
- img_label.setPixmap(scaled_pix)
- img_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
- img_label.setStyleSheet("border: none; background: transparent;")
-
- v_layout.addWidget(img_label)
- self.thumb_grid.addWidget(container, row, col)
- # 5. ALIGNEMENT ET SPACING
- self.thumb_grid.setSpacing(10) # Espace entre les vignettes
- self.thumb_grid.setAlignment(Qt.AlignmentFlag.AlignTop)
- def _init_history_area(self):
- """Décide si on affiche l'onboarding ou directement les vignettes."""
- cfg = self.controller.config_manager
- first_launch = cfg.get_item("app_state", "onboarding_done", False)
- if not first_launch:
- lang = self.controller.config_manager.get_item("machine_settings", "language", "English")
- self._build_onboarding(lang, is_initial=True)
- # Overlay placé sur la fenêtre principale
- self._hl_overlay = HighlightOverlay(self.window())
- self._hl_overlay.resize(self.window().size())
- self._hl_overlay.hide()
- self._history_stack.setCurrentIndex(0)
- else:
- self._history_stack.setCurrentIndex(1)
- self.render_grid()
- def restart_onboarding(self):
- """Relance l'onboarding depuis zéro — appelé depuis settings reset.
- Recrée l'overlay, remet les cartes en état désactivé, lance le watcher.
- """
- lang = self.controller.config_manager.get_item(
- "machine_settings", "language", "English")
- # S'assurer que l'overlay est bien positionné sur la fenêtre principale
- main_win = self.window()
- if not hasattr(self, '_hl_overlay'):
- from gui.onboarding_widget import HighlightOverlay
- self._hl_overlay = HighlightOverlay(main_win)
- self._hl_overlay.setParent(main_win)
- self._hl_overlay.resize(main_win.size())
- self._hl_overlay.move(0, 0)
- self._hl_overlay.hide()
- self._last_highlight_names = []
- # Restaurer le style des cartes avant de les redésactiver
- self._set_mode_cards_enabled(True)
- # Reconstruire et afficher l'onboarding
- self._build_onboarding(lang, is_initial=True)
- self._history_stack.setCurrentIndex(0)
- self._apply_history_area_style()
- def _build_onboarding(self, lang, step=0, is_initial=False):
- """Crée (ou recrée) l'OnboardingWidget pour la langue donnée.
- Remplace le widget courant dans _history_stack[0].
- step : étape à restaurer après reconstruction.
- is_initial : True = premier lancement, active le watcher géométrie."""
- from core.translations import TRANSLATIONS
- colors = getattr(self, "_current_colors", None) or {
- "text": "#dddddd", "text_secondary": "#aaaaaa",
- "bg_card_alt": "#2a2a2a", "bg_entry": "#2b2b2b",
- "border_strong": "#555", "border": "#333",
- "bg_card": "#2b2b2b",
- }
- onb_t = TRANSLATIONS.get(lang, TRANSLATIONS["English"]).get("onboarding", {})
- new_onb = OnboardingWidget(
- colors, translations=onb_t,
- current_lang=lang,
- emit_initial_highlight=False, # on gère le highlight manuellement
- parent=self
- )
- new_onb.finished.connect(self._on_onboarding_finished)
- new_onb.request_highlight.connect(self._on_request_highlight)
- new_onb.clear_highlight.connect(self._on_clear_highlight)
- new_onb.language_changed.connect(self._on_onboarding_lang_changed)
- # Restaurer l'étape courante
- if step > 0:
- new_onb._step = step
- new_onb._stack.setCurrentIndex(step)
- new_onb._refresh(emit_highlight=False)
- # Remplacer dans le stack et s'assurer qu'il est visible
- old_w = self._history_stack.widget(0)
- self._history_stack.removeWidget(old_w)
- old_w.deleteLater()
- self._history_stack.insertWidget(0, new_onb)
- self._history_stack.setCurrentIndex(0)
- self._onboarding = new_onb
- # Désactiver les cartes pendant l'onboarding
- self._set_mode_cards_enabled(False)
- if is_initial:
- # Premier lancement : attendre que la géométrie soit stable
- # (fenêtre potentiellement maximisée)
- new_onb._emit_initial_highlight = True
- new_onb._geom_attempts = 0
- new_onb._last_win_size = None
- from PyQt6.QtCore import QTimer
- new_onb._geom_timer = QTimer(new_onb)
- new_onb._geom_timer.timeout.connect(new_onb._wait_geometry_stable)
- new_onb._geom_timer.start(60)
- else:
- # Rebuild (changement de langue) : highlight immédiat sans flash
- new_onb._emit_highlight()
- def _set_mode_cards_enabled(self, enabled: bool):
- """Active ou désactive les cartes via CSS uniquement.
- QGraphicsOpacityEffect est évité car il propage son rendu
- aux widgets frères (dont l'onboarding)."""
- for i in range(self.modes_layout.count()):
- item = self.modes_layout.itemAt(i)
- if not item:
- continue
- card = item.widget()
- if not card:
- continue
- card.setEnabled(enabled)
- if enabled:
- # Restaurer le style normal
- c = getattr(self, '_current_colors', {})
- is_dis = getattr(card, 'state', '') == 'disabled'
- bg = c.get('bg_main', '#1e1e1e') if is_dis else c.get('bg_card', '#2b2b2b')
- brd = c.get('border', '#3d3d3d')
- hov = c.get('hover_card', '#333333')
- base = (
- f'QFrame#modeCard {{ background-color: {bg};'
- f' border: 2px solid {brd}; border-radius: 15px; }}'
- )
- if not is_dis:
- base += (
- f' QFrame#modeCard:hover {{ border-color: #1F6AA5;'
- f' background-color: {hov}; }}'
- )
- card.setStyleSheet(base)
- # apply_theme restaurera les styles complets au prochain cycle
- else:
- # Griser via CSS — fond sombre, textes très discrets
- card.setStyleSheet(
- 'QFrame#modeCard { background-color: #1c1c1c;'
- ' border: 2px solid #252525; border-radius: 15px; }'
- )
- # NE PAS toucher aux labels — setEnabled(False) suffit
- # pour bloquer les clics sans affecter le rendu visuel
- pass
- def _apply_history_area_style(self, colors=None):
- """Applique le style de history_area selon l'etat de l'onboarding."""
- if colors is None:
- colors = getattr(self, '_current_colors', {})
- onboarding_active = (
- hasattr(self, '_history_stack')
- and self._history_stack.currentIndex() == 0
- )
- if onboarding_active:
- brd = colors.get('border', '#333333') if colors else '#333333'
- self.history_area.setStyleSheet(
- f"background: transparent; border-radius: 10px;"
- f" border: 1px solid {brd};"
- )
- self.history_area.viewport().setStyleSheet("background: transparent;")
- else:
- bg = colors.get('bg_card', '#2b2b2b')
- brd = colors.get('border', '#333333')
- self.history_area.setStyleSheet(
- f"background-color: {bg};"
- " border-radius: 10px;"
- f" border: 1px solid {brd};"
- )
- self.history_area.viewport().setStyleSheet("")
- def _on_request_highlight(self, names: list):
- self._last_highlight_names = names # mémorisé pour recalcul au resize
- """
- Reçoit la liste des noms de widgets à mettre en exergue.
- Localise chaque widget dans la fenêtre principale et
- construit les cibles pour HighlightOverlay.
- """
- if not hasattr(self, '_hl_overlay'):
- return
- main_win = self.window()
- self._hl_overlay.setParent(main_win)
- self._hl_overlay.resize(main_win.size()) # recalcul si fenêtre maximisée
- self._hl_overlay.move(0, 0)
- # Point d'ancrage = centre de la history_area converti en coords main_win
- anchor_local = QPoint(
- self.history_area.width() // 2,
- self.history_area.height() // 2,
- )
- anchor = self.history_area.mapTo(main_win, anchor_local)
- targets = []
- onboarding_already_added = False
- for name in names:
- if name == 'onboarding_area':
- # Utiliser le widget onboarding lui-même pour le rect exact
- onb_widget = getattr(self, '_onboarding', None)
- if onb_widget and onb_widget.isVisible():
- tl_onb = onb_widget.mapTo(main_win, onb_widget.rect().topLeft())
- rect_onb = onb_widget.rect().translated(tl_onb)
- else:
- # Fallback : viewport de history_area
- vp = self.history_area.viewport()
- tl_onb = vp.mapTo(main_win, vp.rect().topLeft())
- rect_onb = vp.rect().translated(tl_onb)
- targets.insert(0, {'rect': rect_onb, 'no_border': True})
- onboarding_already_added = True
- continue
- if name == 'modes_column':
- # Rect englobant les 3 premières cartes (Raster, Calibration, Checker)
- rects = []
- for i in range(self.modes_layout.count()):
- if len(rects) >= 3:
- break
- item = self.modes_layout.itemAt(i)
- if not item: continue
- card = item.widget()
- if card and card.objectName() == 'modeCard':
- tl = card.mapTo(main_win, card.rect().topLeft())
- rects.append(card.rect().translated(tl))
- if rects:
- from PyQt6.QtCore import QRect
- united = rects[0]
- for r in rects[1:]:
- united = united.united(r)
- targets.append({'rect': united})
- continue
- widget = self._find_highlight_widget(name)
- if widget is None:
- continue
- tl = widget.mapTo(main_win, widget.rect().topLeft())
- rect = widget.rect().translated(tl)
- extra = {}
- # Boutons topbar : padding réduit pour éviter le débordement hors fenêtre
- if name in ('settings_topbar_btn', 'home_btn', 'github_btn'):
- extra['small_padding'] = True
- targets.append({'rect': rect, **extra})
- if targets:
- self._hl_overlay.show_highlights(targets)
- def _find_highlight_widget(self, name: str):
- """Cherche le widget correspondant au nom dans toute l'application."""
- main_win = self.window()
- if name == 'onboarding_area':
- return self.history_area
- elif name == 'all_mode_cards':
- # Retourne None ici — traité séparément dans _on_request_highlight
- return None
- elif name == 'parser_card':
- for i in range(self.modes_layout.count()):
- item = self.modes_layout.itemAt(i)
- if not item: continue
- card = item.widget()
- if card and hasattr(card, 'icon_data'):
- from utils.paths import SVG_ICONS
- if card.icon_data == SVG_ICONS.get('GCODE', ''):
- return card
- elif name == 'calibration_card':
- for i in range(self.modes_layout.count()):
- item = self.modes_layout.itemAt(i)
- if not item: continue
- card = item.widget()
- if card and hasattr(card, 'icon_data'):
- from utils.paths import SVG_ICONS
- if card.icon_data == SVG_ICONS.get('LATENCY', ''):
- return card
- elif name == 'settings_card':
- for i in range(self.modes_layout.count()):
- item = self.modes_layout.itemAt(i)
- if not item: continue
- card = item.widget()
- if card and hasattr(card, 'icon_data'):
- from utils.paths import SVG_ICONS
- if card.icon_data == SVG_ICONS.get('GEAR', ''):
- return card
- elif name in ('settings_topbar_btn', 'home_btn', 'github_btn'):
- return self._find_topbar_btn(name, main_win)
- return None
- # Mots-clés par bouton : objectName, toolTip, text (insensible à la casse)
- _TOPBAR_KEYWORDS = {
- 'settings_topbar_btn': ('setting', 'gear', 'reglage', 'einstellung'),
- 'home_btn': ('home', 'dashboard', 'accueil', 'startseite'),
- 'github_btn': ('github', 'git'),
- }
- def _find_topbar_btn(self, name, main_win):
- """Recherche un bouton topbar par objectName en priorité, puis mots-clés."""
- from PyQt6.QtWidgets import QPushButton
- # 1. Recherche directe par objectName (le plus fiable)
- btn = main_win.findChild(QPushButton, name)
- if btn:
- return btn
- # 2. Fallback : mots-clés dans objectName/toolTip/text
- # limité aux boutons dans la topbar (Y < 80px)
- keywords = self._TOPBAR_KEYWORDS.get(name, ())
- topbar_max_y = 80
- for btn in main_win.findChildren(QPushButton):
- pos_in_win = btn.mapTo(main_win, btn.rect().topLeft())
- if pos_in_win.y() > topbar_max_y:
- continue
- all_text = ' '.join([
- btn.objectName().lower(),
- btn.toolTip().lower(),
- btn.text().lower(),
- ])
- if any(kw in all_text for kw in keywords):
- return btn
- return None
- # home = plus à gauche, settings = tout à droite (dernier)
- # github = entre les deux
- if name == 'home_btn':
- return silent[0] # premier depuis la gauche
- elif name == 'settings_topbar_btn':
- return silent[-1] # tout à droite
- elif name == 'github_btn':
- return silent[-2] if len(silent) >= 2 else silent[-1]
- return None
- def _highlight_label(self, name: str) -> str:
- labels = {
- 'calibration_card': 'Calibration',
- 'settings_card': 'Settings',
- 'settings_topbar_btn': 'Settings',
- }
- return labels.get(name, '')
- def _on_onboarding_lang_changed(self, lang: str):
- """Appelé quand l'utilisateur change de langue depuis l'onboarding.
- Sauvegarde, reconstruit l'onboarding traduit et notifie la main window."""
- # 1. Sauvegarder la langue
- cfg = self.controller.config_manager
- machine = cfg.get_section("machine_settings") or {}
- machine["language"] = lang
- cfg.set_section("machine_settings", machine)
- cfg.save()
- # 2. Mémoriser l'étape courante avant reconstruction
- current_step = getattr(self._onboarding, '_step', 0)
- # 3. Reconstruire l'onboarding avec la nouvelle langue
- self._on_clear_highlight()
- self._build_onboarding(lang, step=current_step)
- # 4. Notifier la fenêtre principale
- main_win = self.window()
- if main_win and hasattr(main_win, "update_ui_language"):
- main_win.update_ui_language()
- def _on_clear_highlight(self):
- if hasattr(self, '_hl_overlay'):
- self._hl_overlay.hide_highlights()
- # Effacer les dernières cibles pour que resizeEvent
- # ne puisse pas relancer le highlight après la fin
- self._last_highlight_names = []
- def _on_onboarding_finished(self, settings: dict):
- """Appelé quand l'utilisateur termine ou skippe l'onboarding."""
- # Masquer l'overlay immédiatement, avant toute autre opération
- self._on_clear_highlight()
- cfg = self.controller.config_manager
- # Marquer comme fait
- current = cfg.get_section("app_state") or {}
- current["onboarding_done"] = True
- cfg.set_section("app_state", current)
- # Sauvegarder les réglages machine si fournis
- if settings:
- machine = cfg.get_section("machine_settings") or {}
- machine.update(settings)
- cfg.set_section("machine_settings", machine)
- cfg.save()
- # Réactiver les cartes et passer à la grille
- self._set_mode_cards_enabled(True)
- self._history_stack.setCurrentIndex(1)
- self._apply_history_area_style()
- self.render_grid()
- def setup_stats(self, parent_layout):
- # 1. Création du conteneur principal
- self.stats_frame = QFrame()
- self.stats_frame.setFixedHeight(120)
- self.stats_frame.setStyleSheet("""
- QFrame {
- background-color: #1e1e1e;
- border-radius: 15px;
- border: 1px solid #3d3d3d;
- }
- """)
-
- main_layout = QHBoxLayout(self.stats_frame)
- main_layout.setContentsMargins(10, 10, 10, 10)
- # 2. Récupération et calcul des données (Logique conservée)
- cfg = self.controller.config_manager
- total_l = cfg.get_item("stats", "total_lines", 0)
- total_g = cfg.get_item("stats", "total_gcodes", 0)
- total_s = float(cfg.get_item("stats", "total_time_seconds", 0))
- # Conversion du temps
- hours = int(total_s // 3600)
- minutes = int((total_s % 3600) // 60)
-
- if hours > 0:
- time_str = f"{hours}h {minutes:02d}m"
- elif minutes > 0:
- time_str = f"{minutes}m"
- elif total_s > 0:
- time_str = "< 1m"
- else:
- time_str = "0m"
- # 3. Définition des items à afficher
- stat_items = [
- (f"{int(total_l):,}", self.texts.get("lines_generated", "Lines")),
- (str(total_g), self.texts.get("gcode_saved", "G-Codes")),
- (time_str, self.texts.get("total_engraving_time", "Time"))
- ]
- # 4. Création dynamique des colonnes
- self._stat_value_labels = []
- self._stat_key_labels = []
- for value, label in stat_items:
- v_layout = QVBoxLayout()
- v_layout.setSpacing(2) # Espace réduit entre valeur et texte
-
- # Valeur (Bleu et gras)
- val_lbl = QLabel(value)
- self._stat_value_labels.append(val_lbl)
- val_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
- val_lbl.setStyleSheet("color: #1F6AA5; font-size: 18px; font-weight: bold; border: none; background: transparent;")
-
- # Label (Petit et gris)
- txt_lbl = QLabel(label)
- self._stat_key_labels.append(txt_lbl)
- txt_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
- txt_lbl.setWordWrap(True) # Au cas où le texte est long
- txt_lbl.setStyleSheet("color: gray; font-size: 10px; border: none; background: transparent;")
-
- v_layout.addWidget(val_lbl)
- v_layout.addWidget(txt_lbl)
-
- # On ajoute chaque colonne au layout horizontal
- main_layout.addLayout(v_layout)
-
- # Ajouter un séparateur vertical entre les colonnes (sauf après la dernière)
- if label != stat_items[-1][1]:
- line = QFrame()
- line.setFrameShape(QFrame.Shape.VLine)
- line.setStyleSheet("color: #3d3d3d; background-color: #3d3d3d; width: 1px; margin: 15px 0;")
- main_layout.addWidget(line)
- # Ajout du bloc complet au layout de la colonne de droite
- parent_layout.addWidget(self.stats_frame)
- def resizeEvent(self, event):
- super().resizeEvent(event)
- # On ne rafraîchit la grille que si la vue est prête
- if hasattr(self, 'history_area'):
- self.render_grid()
- # Recalculer l'overlay si actif et qu'il reste des cibles
- if (hasattr(self, '_hl_overlay') and self._hl_overlay.isVisible()
- and getattr(self, '_last_highlight_names', [])):
- self._on_request_highlight(self._last_highlight_names)
- def apply_theme(self, colors):
- """Met à jour toutes les cartes du dashboard lors d'un changement de thème"""
- from gui.utils_qt import get_svg_pixmap
- from PyQt6.QtCore import QSize
- self._current_colors = colors
- # 1. Fond de la vue et containers
- self.setStyleSheet("background-color: transparent;")
- if hasattr(self, "_left_container"):
- self._left_container.setStyleSheet("background: transparent;")
- if hasattr(self, "_right_container"):
- self._right_container.setStyleSheet("background: transparent;")
- # 2. Label "History"
- if hasattr(self, "history_label"):
- self.history_label.setStyleSheet(
- f"font-weight: bold; color: {colors['text']}; "
- "margin-top: 10px; background: transparent; border: none;"
- )
- # 3. Cadre thumbnails (history_area)
- if hasattr(self, "history_area"):
- self._apply_history_area_style(colors)
- self.history_area.verticalScrollBar().setStyleSheet(f"""
- QScrollBar:vertical {{
- border: none;
- background: {colors['scrollbar_bg']};
- width: 10px;
- margin: 0px;
- }}
- QScrollBar::handle:vertical {{
- background: {colors['scrollbar_handle']};
- min-height: 20px;
- border-radius: 5px;
- }}
- QScrollBar::handle:vertical:hover {{
- background: #1F6AA5;
- }}
- QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
- height: 0px;
- }}
- QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{
- background: none;
- }}
- """)
- # 4. Frame de statistiques
- if hasattr(self, "stats_frame"):
- self.stats_frame.setStyleSheet(f"""
- QFrame {{
- background-color: {colors['bg_card']};
- border-radius: 15px;
- border: 1px solid {colors['border']};
- }}
- """)
- # 5. Cartes de mode
- hover_bg = colors['hover_card']
- for i in range(self.modes_layout.count()):
- item = self.modes_layout.itemAt(i)
- if not item: continue
- card = item.widget()
- if not card or card.objectName() != "modeCard": continue
- is_disabled = (card.state == "disabled")
- if is_disabled:
- bg = colors['bg_main']
- txt = colors['text_disabled']
- icon_c = colors['text_disabled']
- else:
- bg = colors["bg_card"]
- txt = colors["text"]
- icon_c = colors["text"]
- if is_disabled:
- card.setStyleSheet(f"""
- QFrame#modeCard {{
- background-color: {bg};
- border: 2px solid {colors['border']};
- border-radius: 15px;
- }}
- """)
- else:
- card.setStyleSheet(f"""
- QFrame#modeCard {{
- background-color: {bg};
- border: 2px solid {colors['border']};
- border-radius: 15px;
- }}
- QFrame#modeCard:hover {{
- border-color: #1F6AA5;
- background-color: {hover_bg};
- }}
- """)
- card.title_label.setStyleSheet(
- f"color: {txt}; font-weight: bold; font-size: 15px; "
- "border: none; background: transparent;"
- )
- card.desc_label.setStyleSheet(
- f"color: {colors['text_secondary']}; font-size: 11px; "
- "border: none; background: transparent;"
- )
- if isinstance(card.icon_data, str) and card.icon_data.endswith(".svg"):
- pix = get_svg_pixmap(card.icon_data, size=QSize(35, 35), color_hex=icon_c)
- card.icon_label.setPixmap(pix)
- # 6. Onboarding theme
- if hasattr(self, "_onboarding"):
- self._onboarding.apply_theme(colors)
- # 7. Re-rendu des vignettes avec les nouvelles couleurs
- if hasattr(self, "all_pixmaps"):
- self.render_grid()
- def _apply_language(self, lang: str, translations: dict):
- """Reçoit lang directement — utilisé par update_ui_language."""
- from core.translations import TRANSLATIONS as _TR
- self.texts = _TR.get(lang, _TR["English"])["dashboard"]
- from gui.utils_qt import translate_ui_widgets
- translate_ui_widgets(self.translation_map, self.texts)
- if hasattr(self, 'history_label'):
- self.history_label.setText(self.texts.get("history", "History"))
- stat_keys = ["lines_generated", "gcode_saved", "total_engraving_time"]
- for i, key in enumerate(stat_keys):
- labels = getattr(self, '_stat_key_labels', [])
- if i < len(labels):
- labels[i].setText(self.texts.get(key, key))
- def update_texts(self):
- """Met à jour dynamiquement tous les labels du Dashboard"""
- lang = self.controller.config_manager.get_item("machine_settings", "language", "English")
- self._apply_language(lang, {})
- def refresh(self):
- """Recharge les stats et les vignettes depuis le disque."""
- # Mettre à jour les stats si les labels existent déjà
- if hasattr(self, '_stat_value_labels') and len(self._stat_value_labels) == 3:
- cfg = self.controller.config_manager
- total_l = cfg.get_item("stats", "total_lines", 0)
- total_g = cfg.get_item("stats", "total_gcodes", 0)
- total_s = float(cfg.get_item("stats", "total_time_seconds", 0))
- hours = int(total_s // 3600)
- minutes = int((total_s % 3600) // 60)
- if hours > 0:
- time_str = f"{hours}h {minutes:02d}m"
- elif minutes > 0:
- time_str = f"{minutes}m"
- elif total_s > 0:
- time_str = "< 1m"
- else:
- time_str = "0m"
- self._stat_value_labels[0].setText(f"{int(total_l):,}")
- self._stat_value_labels[1].setText(str(total_g))
- self._stat_value_labels[2].setText(time_str)
- # Recharger les vignettes
- self.load_thumbnails()
|