dashboard_view_qt.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062
  1. # -*- coding: utf-8 -*-
  2. import os
  3. from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame,
  4. QLabel, QScrollArea, QGridLayout, QPushButton,
  5. QStackedWidget)
  6. from PyQt6.QtCore import Qt, QSize, QTimer, QPoint
  7. from PyQt6.QtGui import QPixmap, QIcon, QFont
  8. from PyQt6.QtWidgets import QGraphicsDropShadowEffect
  9. from PyQt6.QtGui import QColor
  10. from core.translations import TRANSLATIONS
  11. from utils.paths import THUMBNAILS_DIR, ASSETS_DIR, SVG_ICONS
  12. from gui.utils_qt import get_svg_pixmap
  13. from gui.onboarding_widget import OnboardingWidget, HighlightOverlay
  14. class DashboardViewQt(QWidget):
  15. def __init__(self, controller):
  16. super().__init__()
  17. self.controller = controller
  18. self.setStyleSheet("background-color: #2b2b2b; color: white;")
  19. self.translation_map = {}
  20. # Récupération des textes
  21. lang = self.controller.config_manager.get_item("machine_settings", "language", "English")
  22. self.texts = TRANSLATIONS.get(lang, TRANSLATIONS["English"])["dashboard"]
  23. self.text = TRANSLATIONS.get(lang, TRANSLATIONS["English"]).get("topbar", {})
  24. # 1. Layout Global VERTICAL
  25. self.main_layout = QVBoxLayout(self)
  26. self.main_layout.setContentsMargins(30, 20, 30, 20)
  27. self.main_layout.setSpacing(5)
  28. # 2. LOGO en haut (Pleine largeur)
  29. self.setup_logo_header()
  30. # --- ligne de séparation ---
  31. line = QFrame()
  32. line.setFixedHeight(5)
  33. line.setStyleSheet(f"""
  34. background-color: #444;
  35. margin-left: 50px;
  36. margin-right: 50px;
  37. """)
  38. self.main_layout.addWidget(line)
  39. # 3. Conteneur HORIZONTAL pour le reste
  40. self.content_layout = QHBoxLayout()
  41. self.content_layout.setSpacing(30)
  42. self.main_layout.addLayout(self.content_layout)
  43. # 4. Appels des fonctions de création
  44. self.setup_left_column() # Ira dans content_layout (à gauche)
  45. self.setup_right_column() # Ira dans content_layout (à droite)
  46. # --- THUMBNAILS ---
  47. self.load_thumbnails()
  48. QTimer.singleShot(0, self._init_history_area)
  49. def setup_logo_header(self):
  50. """Crée le bandeau titre ALIG en haut de la vue"""
  51. self.logo_label = QLabel("ALIG")
  52. self.logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
  53. # Effet de lueur (Glow)
  54. glow = QGraphicsDropShadowEffect()
  55. glow.setBlurRadius(40)
  56. glow.setColor(QColor(31, 106, 165, 200))
  57. glow.setOffset(0)
  58. self.logo_label.setGraphicsEffect(glow)
  59. # Style agrandi pour le mode plein écran
  60. self.logo_label.setStyleSheet("""
  61. font-size: 70px;
  62. font-weight: 900;
  63. color: #ADE1FF;
  64. letter-spacing: 15px;
  65. margin-bottom: 10px;
  66. background: transparent;
  67. """)
  68. # On l'ajoute au layout VERTICAL principal
  69. self.main_layout.addWidget(self.logo_label)
  70. def setup_left_column(self):
  71. left_container = QFrame()
  72. left_container.setFixedWidth(420)
  73. left_container.setStyleSheet("background: transparent;")
  74. self._left_container = left_container
  75. layout = QVBoxLayout(left_container)
  76. layout.setContentsMargins(0, 0, 0, 10)
  77. # 1. Zone de défilement
  78. scroll = QScrollArea()
  79. scroll.setWidgetResizable(True)
  80. scroll.setFrameShape(QFrame.Shape.NoFrame)
  81. scroll.setStyleSheet("background: transparent;")
  82. scroll.viewport().setStyleSheet("background: transparent;")
  83. content = QWidget()
  84. content.setStyleSheet("background: transparent;")
  85. self.modes_layout = QVBoxLayout(content)
  86. self.modes_layout.setSpacing(15)
  87. self.modes_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
  88. # --- LISTE DES MODES MISE À JOUR ---
  89. # On utilise maintenant SVG_ICONS pour les chemins
  90. modes = [
  91. ("raster_title", "raster_desc", self.controller.show_raster_mode, SVG_ICONS["RASTER"], "normal"),
  92. ("dithering_title", "dithering_desc", None, SVG_ICONS["DITHER"], "disabled"),
  93. ("infill_title", "infill_desc", None, SVG_ICONS["INFILL"], "disabled"),
  94. ("parser_title", "parser_desc", self.controller.show_checker_mode, SVG_ICONS["GCODE"], "normal"),
  95. ("calibration_title", "calibration_desc", self.controller.show_calibration_mode,SVG_ICONS["LATENCY"], "normal"),
  96. ("settings_title", "settings_desc", self.controller.show_settings_mode, SVG_ICONS["GEAR"], "normal"),
  97. ]
  98. for title_key, desc_key, callback, icon_source, state in modes:
  99. card = self.create_mode_card(title_key, desc_key, callback, icon_source, state)
  100. self.modes_layout.addWidget(card)
  101. scroll.setWidget(content)
  102. layout.addWidget(scroll)
  103. # --- SECTION CRÉDITS ---
  104. credits_label = QLabel(self.text.get("credits", "By MoMo"))
  105. credits_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
  106. credits_label.setStyleSheet("color: #444444; font-size: 12px; border: none; background: transparent;")
  107. layout.addWidget(credits_label)
  108. self.content_layout.addWidget(left_container)
  109. def create_mode_card(self, title_key, desc_key, callback, icon_data, state):
  110. """Crée une carte de mode et enregistre ses labels pour la traduction"""
  111. from gui.utils_qt import get_svg_pixmap
  112. card = QFrame()
  113. card.setAttribute(Qt.WidgetAttribute.WA_Hover)
  114. card.setObjectName("modeCard")
  115. card.icon_data = icon_data
  116. card.state = state
  117. card.setFixedHeight(80)
  118. is_disabled = (state == "disabled" or callback is None)
  119. bg_color = "#1e1e1e" if is_disabled else "#2b2b2b"
  120. border_color = "#252525" if is_disabled else "#3d3d3d"
  121. title_color = "#777777" if is_disabled else "white"
  122. desc_color = "#555555" if is_disabled else "gray"
  123. icon_color = "#555555" if is_disabled else "#FFFFFF"
  124. style = f"""
  125. QFrame#modeCard {{
  126. background-color: {bg_color};
  127. border: 2px solid {border_color};
  128. border-radius: 15px;
  129. }}
  130. """
  131. if not is_disabled:
  132. style += "QFrame#modeCard:hover { border-color: #1F6AA5; background-color: #333333; }"
  133. card.setCursor(Qt.CursorShape.PointingHandCursor)
  134. card.setStyleSheet(style)
  135. layout = QHBoxLayout(card)
  136. layout.setContentsMargins(15, 10, 15, 10)
  137. layout.setSpacing(15)
  138. # Icône
  139. icon_label = QLabel()
  140. icon_label.setFixedWidth(45)
  141. if isinstance(icon_data, str) and icon_data.endswith(".svg"):
  142. pix = get_svg_pixmap(icon_data, size=QSize(35, 35), color_hex=icon_color)
  143. icon_label.setPixmap(pix)
  144. elif isinstance(icon_data, str) and icon_data.endswith(".png"):
  145. path = os.path.join(ASSETS_DIR, icon_data)
  146. pix = QPixmap(path).scaled(40, 40, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
  147. icon_label.setPixmap(pix)
  148. else:
  149. icon_label.setText(icon_data)
  150. icon_label.setFont(QFont("Arial", 24))
  151. icon_label.setStyleSheet(f"color: {title_color}; border: none; background: transparent;")
  152. icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
  153. layout.addWidget(icon_label)
  154. # Zone de texte
  155. text_layout = QVBoxLayout()
  156. text_layout.setSpacing(2)
  157. # --- TITRE (Enregistré dans la map) ---
  158. t_lbl = QLabel(self.texts.get(title_key, title_key))
  159. t_lbl.setStyleSheet(f"color: {title_color}; font-weight: bold; font-size: 15px; border: none; background: transparent;")
  160. self.translation_map[t_lbl] = title_key # Stockage de la clé
  161. # --- DESCRIPTION (Enregistrée dans la map) ---
  162. d_lbl = QLabel(self.texts.get(desc_key, desc_key))
  163. d_lbl.setWordWrap(True)
  164. d_lbl.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
  165. d_lbl.setStyleSheet(f"color: {desc_color}; font-size: 11px; border: none; background: transparent;")
  166. self.translation_map[d_lbl] = desc_key # Stockage de la clé
  167. text_layout.addWidget(t_lbl)
  168. text_layout.addWidget(d_lbl)
  169. text_layout.addStretch()
  170. layout.addLayout(text_layout)
  171. layout.setStretch(1, 1)
  172. card.icon_label = icon_label
  173. card.title_label = t_lbl
  174. card.desc_label = d_lbl
  175. if not is_disabled:
  176. card.mousePressEvent = lambda e: callback()
  177. else:
  178. card.mousePressEvent = lambda e: None
  179. for child in card.findChildren(QLabel):
  180. child.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
  181. return card
  182. def setup_right_column(self):
  183. right_container = QWidget()
  184. right_container.setStyleSheet("background: transparent;")
  185. self._right_container = right_container
  186. layout = QVBoxLayout(right_container)
  187. layout.setContentsMargins(0, 0, 0, 0)
  188. # # Titre A.L.I.G.
  189. # title = QLabel("ALIG")
  190. # title.setAlignment(Qt.AlignmentFlag.AlignCenter)
  191. # glow = QGraphicsDropShadowEffect()
  192. # glow.setBlurRadius(100)
  193. # glow.setColor(QColor(31, 106, 165, 200)) # Bleu ALIG
  194. # glow.setOffset(0)
  195. # title.setGraphicsEffect(glow)
  196. # title.setStyleSheet("font-size: 60px; font-weight: 900; color: #ADE1FF; letter-spacing: 10px;")
  197. # layout.addWidget(title)
  198. # Zone Historique (Simplifiée pour l'instant)
  199. self.history_label = QLabel(self.texts.get("history", "History"))
  200. self.history_label.setStyleSheet("font-weight: bold; color: white; margin-top: 10px; background: transparent; border: none;")
  201. layout.addWidget(self.history_label)
  202. self.history_area = QScrollArea()
  203. self.history_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
  204. self.history_area.setWidgetResizable(True)
  205. self.history_area.setStyleSheet("background: transparent; border-radius: 10px; border: 1px solid #333;")
  206. self.history_area.viewport().setStyleSheet("background: transparent;")
  207. self.history_area.verticalScrollBar().setStyleSheet("""
  208. QScrollBar:vertical {
  209. border: none;
  210. background: #202020;
  211. width: 10px;
  212. margin: 0px 0px 0px 0px;
  213. }
  214. QScrollBar::handle:vertical {
  215. background: #3e3e3e;
  216. min-height: 20px;
  217. border-radius: 5px;
  218. }
  219. QScrollBar::handle:vertical:hover {
  220. background: #1F6AA5; /* Ton bleu ALIG au survol */
  221. }
  222. QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
  223. height: 0px;
  224. }
  225. QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
  226. background: none;
  227. }
  228. """)
  229. self.history_area.verticalScrollBar().setSingleStep(15)
  230. # Conteneur stacké : page 0 = onboarding, page 1 = grille vignettes
  231. self._history_stack_widget = QWidget()
  232. self._history_stack_widget.setStyleSheet("background: transparent;")
  233. stack_outer = QVBoxLayout(self._history_stack_widget)
  234. stack_outer.setContentsMargins(0, 0, 0, 0)
  235. stack_outer.setSpacing(0)
  236. self._history_stack = QStackedWidget()
  237. self._history_stack.setStyleSheet("background: transparent;")
  238. # Page 0 — onboarding (créé dans _init_history_area)
  239. self._onboarding_placeholder = QWidget()
  240. self._onboarding_placeholder.setStyleSheet("background: transparent;")
  241. self._history_stack.addWidget(self._onboarding_placeholder)
  242. # Page 1 — grille de vignettes
  243. thumb_widget = QWidget()
  244. thumb_widget.setStyleSheet("background: transparent;")
  245. self.thumb_grid = QGridLayout(thumb_widget)
  246. self.thumb_grid.setSpacing(15)
  247. self._history_stack.addWidget(thumb_widget)
  248. stack_outer.addWidget(self._history_stack)
  249. self.history_area.setWidget(self._history_stack_widget)
  250. layout.addWidget(self.history_area)
  251. # Statistiques
  252. self.setup_stats(layout)
  253. self.content_layout.addWidget(right_container)
  254. def load_thumbnails(self):
  255. thumb_dir = THUMBNAILS_DIR
  256. self.all_pixmaps = [] # On stocke des QPixmap au lieu de CTkImage
  257. if not os.path.exists(thumb_dir):
  258. os.makedirs(thumb_dir, exist_ok=True)
  259. self.render_grid()
  260. return
  261. try:
  262. # Récupération et tri des fichiers (identique à ton ancienne logique)
  263. files = [f for f in os.listdir(thumb_dir) if f.lower().endswith(".png")]
  264. files = [f for f in files if os.path.isfile(os.path.join(thumb_dir, f))]
  265. files.sort(key=lambda x: os.path.getmtime(os.path.join(thumb_dir, x)), reverse=True)
  266. for file in files:
  267. try:
  268. path = os.path.join(thumb_dir, file)
  269. if os.path.getsize(path) == 0: continue
  270. # En Qt, on charge directement le chemin avec QPixmap
  271. pixmap = QPixmap(path)
  272. if not pixmap.isNull():
  273. # On redimensionne tout de suite pour garder de bonnes perfs
  274. pixmap = pixmap.scaled(200, 200, Qt.AspectRatioMode.KeepAspectRatio,
  275. Qt.TransformationMode.SmoothTransformation)
  276. self.all_pixmaps.append(pixmap)
  277. except Exception as e:
  278. print(f"Erreur chargement vignette {file}: {e}")
  279. except Exception as e:
  280. print(f"Erreur accès dossier thumbnails: {e}")
  281. self.render_grid()
  282. def render_grid(self):
  283. """Affiche les vignettes en s'assurant de ne JAMAIS déclencher le scroll horizontal"""
  284. # S'assurer qu'on est bien sur la page vignettes
  285. if hasattr(self, "_history_stack") and self._history_stack.currentIndex() != 1:
  286. return
  287. # 1. Nettoyage
  288. while self.thumb_grid.count():
  289. item = self.thumb_grid.takeAt(0)
  290. if item.widget(): item.widget().deleteLater()
  291. if not hasattr(self, 'all_pixmaps') or not self.all_pixmaps:
  292. return
  293. # 2. CALCUL DU NOMBRE DE COLONNES
  294. # On définit une largeur MINIMALE pour une vignette
  295. min_item_width = 200
  296. # IMPORTANT : On retire une marge de sécurité (30px) pour la scrollbar verticale
  297. # et les éventuels paddings/borders de la grille.
  298. available_width = self.history_area.viewport().width() - 30
  299. # Sécurité si le widget n'est pas encore totalement rendu
  300. if available_width < 100: available_width = 400
  301. max_columns = max(1, available_width // min_item_width)
  302. # 3. CONFIGURATION DU STRETCH
  303. # On reset les colonnes précédentes (Qt garde les stretchs en mémoire sinon)
  304. for i in range(self.thumb_grid.columnCount() + 1):
  305. self.thumb_grid.setColumnStretch(i, 0)
  306. for i in range(max_columns):
  307. self.thumb_grid.setColumnStretch(i, 1)
  308. # 4. PLACEMENT DES VIGNETTES
  309. # Largeur précise d'une colonne pour le calcul des images
  310. col_w = available_width // max_columns
  311. for i, pixmap in enumerate(self.all_pixmaps):
  312. row = i // max_columns
  313. col = i % max_columns
  314. container = QFrame()
  315. container.setFixedHeight(220)
  316. # Utilise les couleurs du thème courant si disponibles
  317. _c = getattr(self, "_current_colors", None)
  318. if _c:
  319. bg = _c["bg_card"]
  320. brd = _c["border"]
  321. else:
  322. bg = "#2b2b2b"
  323. brd = "#3d3d3d"
  324. container.setStyleSheet(f"""
  325. QFrame {{
  326. background-color: {bg};
  327. border-radius: 8px;
  328. border: 1px solid {brd};
  329. }}
  330. QFrame:hover {{ border-color: #1F6AA5; background-color: {bg}; }}
  331. """)
  332. v_layout = QVBoxLayout(container)
  333. v_layout.setContentsMargins(5, 5, 5, 5) # Espace interne au cadre
  334. img_label = QLabel()
  335. # On scale l'image pour qu'elle soit un peu plus petite que le cadre
  336. # afin de ne pas "forcer" sur les bords du layout
  337. scaled_pix = pixmap.scaled(
  338. col_w - 20, 190,
  339. Qt.AspectRatioMode.KeepAspectRatio,
  340. Qt.TransformationMode.SmoothTransformation
  341. )
  342. img_label.setPixmap(scaled_pix)
  343. img_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
  344. img_label.setStyleSheet("border: none; background: transparent;")
  345. v_layout.addWidget(img_label)
  346. self.thumb_grid.addWidget(container, row, col)
  347. # 5. ALIGNEMENT ET SPACING
  348. self.thumb_grid.setSpacing(10) # Espace entre les vignettes
  349. self.thumb_grid.setAlignment(Qt.AlignmentFlag.AlignTop)
  350. def _init_history_area(self):
  351. """Décide si on affiche l'onboarding ou directement les vignettes."""
  352. cfg = self.controller.config_manager
  353. first_launch = cfg.get_item("app_state", "onboarding_done", False)
  354. if not first_launch:
  355. lang = self.controller.config_manager.get_item("machine_settings", "language", "English")
  356. self._build_onboarding(lang, is_initial=True)
  357. # Overlay placé sur la fenêtre principale
  358. self._hl_overlay = HighlightOverlay(self.window())
  359. self._hl_overlay.resize(self.window().size())
  360. self._hl_overlay.hide()
  361. self._history_stack.setCurrentIndex(0)
  362. else:
  363. self._history_stack.setCurrentIndex(1)
  364. self.render_grid()
  365. def restart_onboarding(self):
  366. """Relance l'onboarding depuis zéro — appelé depuis settings reset.
  367. Recrée l'overlay, remet les cartes en état désactivé, lance le watcher.
  368. """
  369. lang = self.controller.config_manager.get_item(
  370. "machine_settings", "language", "English")
  371. # S'assurer que l'overlay est bien positionné sur la fenêtre principale
  372. main_win = self.window()
  373. if not hasattr(self, '_hl_overlay'):
  374. from gui.onboarding_widget import HighlightOverlay
  375. self._hl_overlay = HighlightOverlay(main_win)
  376. self._hl_overlay.setParent(main_win)
  377. self._hl_overlay.resize(main_win.size())
  378. self._hl_overlay.move(0, 0)
  379. self._hl_overlay.hide()
  380. self._last_highlight_names = []
  381. # Restaurer le style des cartes avant de les redésactiver
  382. self._set_mode_cards_enabled(True)
  383. # Reconstruire et afficher l'onboarding
  384. self._build_onboarding(lang, is_initial=True)
  385. self._history_stack.setCurrentIndex(0)
  386. self._apply_history_area_style()
  387. def _build_onboarding(self, lang, step=0, is_initial=False):
  388. """Crée (ou recrée) l'OnboardingWidget pour la langue donnée.
  389. Remplace le widget courant dans _history_stack[0].
  390. step : étape à restaurer après reconstruction.
  391. is_initial : True = premier lancement, active le watcher géométrie."""
  392. from core.translations import TRANSLATIONS
  393. colors = getattr(self, "_current_colors", None) or {
  394. "text": "#dddddd", "text_secondary": "#aaaaaa",
  395. "bg_card_alt": "#2a2a2a", "bg_entry": "#2b2b2b",
  396. "border_strong": "#555", "border": "#333",
  397. "bg_card": "#2b2b2b",
  398. }
  399. onb_t = TRANSLATIONS.get(lang, TRANSLATIONS["English"]).get("onboarding", {})
  400. new_onb = OnboardingWidget(
  401. colors, translations=onb_t,
  402. current_lang=lang,
  403. emit_initial_highlight=False, # on gère le highlight manuellement
  404. parent=self
  405. )
  406. new_onb.finished.connect(self._on_onboarding_finished)
  407. new_onb.request_highlight.connect(self._on_request_highlight)
  408. new_onb.clear_highlight.connect(self._on_clear_highlight)
  409. new_onb.language_changed.connect(self._on_onboarding_lang_changed)
  410. # Restaurer l'étape courante
  411. if step > 0:
  412. new_onb._step = step
  413. new_onb._stack.setCurrentIndex(step)
  414. new_onb._refresh(emit_highlight=False)
  415. # Remplacer dans le stack et s'assurer qu'il est visible
  416. old_w = self._history_stack.widget(0)
  417. self._history_stack.removeWidget(old_w)
  418. old_w.deleteLater()
  419. self._history_stack.insertWidget(0, new_onb)
  420. self._history_stack.setCurrentIndex(0)
  421. self._onboarding = new_onb
  422. # Désactiver les cartes pendant l'onboarding
  423. self._set_mode_cards_enabled(False)
  424. if is_initial:
  425. # Premier lancement : attendre que la géométrie soit stable
  426. # (fenêtre potentiellement maximisée)
  427. new_onb._emit_initial_highlight = True
  428. new_onb._geom_attempts = 0
  429. new_onb._last_win_size = None
  430. from PyQt6.QtCore import QTimer
  431. new_onb._geom_timer = QTimer(new_onb)
  432. new_onb._geom_timer.timeout.connect(new_onb._wait_geometry_stable)
  433. new_onb._geom_timer.start(60)
  434. else:
  435. # Rebuild (changement de langue) : highlight immédiat sans flash
  436. new_onb._emit_highlight()
  437. def _set_mode_cards_enabled(self, enabled: bool):
  438. """Active ou désactive les cartes via CSS uniquement.
  439. QGraphicsOpacityEffect est évité car il propage son rendu
  440. aux widgets frères (dont l'onboarding)."""
  441. for i in range(self.modes_layout.count()):
  442. item = self.modes_layout.itemAt(i)
  443. if not item:
  444. continue
  445. card = item.widget()
  446. if not card:
  447. continue
  448. card.setEnabled(enabled)
  449. if enabled:
  450. # Restaurer le style normal
  451. c = getattr(self, '_current_colors', {})
  452. is_dis = getattr(card, 'state', '') == 'disabled'
  453. bg = c.get('bg_main', '#1e1e1e') if is_dis else c.get('bg_card', '#2b2b2b')
  454. brd = c.get('border', '#3d3d3d')
  455. hov = c.get('hover_card', '#333333')
  456. base = (
  457. f'QFrame#modeCard {{ background-color: {bg};'
  458. f' border: 2px solid {brd}; border-radius: 15px; }}'
  459. )
  460. if not is_dis:
  461. base += (
  462. f' QFrame#modeCard:hover {{ border-color: #1F6AA5;'
  463. f' background-color: {hov}; }}'
  464. )
  465. card.setStyleSheet(base)
  466. # apply_theme restaurera les styles complets au prochain cycle
  467. else:
  468. # Griser via CSS — fond sombre, textes très discrets
  469. card.setStyleSheet(
  470. 'QFrame#modeCard { background-color: #1c1c1c;'
  471. ' border: 2px solid #252525; border-radius: 15px; }'
  472. )
  473. # NE PAS toucher aux labels — setEnabled(False) suffit
  474. # pour bloquer les clics sans affecter le rendu visuel
  475. pass
  476. def _apply_history_area_style(self, colors=None):
  477. """Applique le style de history_area selon l'etat de l'onboarding."""
  478. if colors is None:
  479. colors = getattr(self, '_current_colors', {})
  480. onboarding_active = (
  481. hasattr(self, '_history_stack')
  482. and self._history_stack.currentIndex() == 0
  483. )
  484. if onboarding_active:
  485. brd = colors.get('border', '#333333') if colors else '#333333'
  486. self.history_area.setStyleSheet(
  487. f"background: transparent; border-radius: 10px;"
  488. f" border: 1px solid {brd};"
  489. )
  490. self.history_area.viewport().setStyleSheet("background: transparent;")
  491. else:
  492. bg = colors.get('bg_card', '#2b2b2b')
  493. brd = colors.get('border', '#333333')
  494. self.history_area.setStyleSheet(
  495. f"background-color: {bg};"
  496. " border-radius: 10px;"
  497. f" border: 1px solid {brd};"
  498. )
  499. self.history_area.viewport().setStyleSheet("")
  500. def _on_request_highlight(self, names: list):
  501. self._last_highlight_names = names # mémorisé pour recalcul au resize
  502. """
  503. Reçoit la liste des noms de widgets à mettre en exergue.
  504. Localise chaque widget dans la fenêtre principale et
  505. construit les cibles pour HighlightOverlay.
  506. """
  507. if not hasattr(self, '_hl_overlay'):
  508. return
  509. main_win = self.window()
  510. self._hl_overlay.setParent(main_win)
  511. self._hl_overlay.resize(main_win.size()) # recalcul si fenêtre maximisée
  512. self._hl_overlay.move(0, 0)
  513. # Point d'ancrage = centre de la history_area converti en coords main_win
  514. anchor_local = QPoint(
  515. self.history_area.width() // 2,
  516. self.history_area.height() // 2,
  517. )
  518. anchor = self.history_area.mapTo(main_win, anchor_local)
  519. targets = []
  520. onboarding_already_added = False
  521. for name in names:
  522. if name == 'onboarding_area':
  523. # Utiliser le widget onboarding lui-même pour le rect exact
  524. onb_widget = getattr(self, '_onboarding', None)
  525. if onb_widget and onb_widget.isVisible():
  526. tl_onb = onb_widget.mapTo(main_win, onb_widget.rect().topLeft())
  527. rect_onb = onb_widget.rect().translated(tl_onb)
  528. else:
  529. # Fallback : viewport de history_area
  530. vp = self.history_area.viewport()
  531. tl_onb = vp.mapTo(main_win, vp.rect().topLeft())
  532. rect_onb = vp.rect().translated(tl_onb)
  533. targets.insert(0, {'rect': rect_onb, 'no_border': True})
  534. onboarding_already_added = True
  535. continue
  536. if name == 'modes_column':
  537. # Rect englobant les 3 premières cartes (Raster, Calibration, Checker)
  538. rects = []
  539. for i in range(self.modes_layout.count()):
  540. if len(rects) >= 3:
  541. break
  542. item = self.modes_layout.itemAt(i)
  543. if not item: continue
  544. card = item.widget()
  545. if card and card.objectName() == 'modeCard':
  546. tl = card.mapTo(main_win, card.rect().topLeft())
  547. rects.append(card.rect().translated(tl))
  548. if rects:
  549. from PyQt6.QtCore import QRect
  550. united = rects[0]
  551. for r in rects[1:]:
  552. united = united.united(r)
  553. targets.append({'rect': united})
  554. continue
  555. widget = self._find_highlight_widget(name)
  556. if widget is None:
  557. continue
  558. tl = widget.mapTo(main_win, widget.rect().topLeft())
  559. rect = widget.rect().translated(tl)
  560. extra = {}
  561. # Boutons topbar : padding réduit pour éviter le débordement hors fenêtre
  562. if name in ('settings_topbar_btn', 'home_btn', 'github_btn'):
  563. extra['small_padding'] = True
  564. targets.append({'rect': rect, **extra})
  565. if targets:
  566. self._hl_overlay.show_highlights(targets)
  567. def _find_highlight_widget(self, name: str):
  568. """Cherche le widget correspondant au nom dans toute l'application."""
  569. main_win = self.window()
  570. if name == 'onboarding_area':
  571. return self.history_area
  572. elif name == 'all_mode_cards':
  573. # Retourne None ici — traité séparément dans _on_request_highlight
  574. return None
  575. elif name == 'parser_card':
  576. for i in range(self.modes_layout.count()):
  577. item = self.modes_layout.itemAt(i)
  578. if not item: continue
  579. card = item.widget()
  580. if card and hasattr(card, 'icon_data'):
  581. from utils.paths import SVG_ICONS
  582. if card.icon_data == SVG_ICONS.get('GCODE', ''):
  583. return card
  584. elif name == 'calibration_card':
  585. for i in range(self.modes_layout.count()):
  586. item = self.modes_layout.itemAt(i)
  587. if not item: continue
  588. card = item.widget()
  589. if card and hasattr(card, 'icon_data'):
  590. from utils.paths import SVG_ICONS
  591. if card.icon_data == SVG_ICONS.get('LATENCY', ''):
  592. return card
  593. elif name == 'settings_card':
  594. for i in range(self.modes_layout.count()):
  595. item = self.modes_layout.itemAt(i)
  596. if not item: continue
  597. card = item.widget()
  598. if card and hasattr(card, 'icon_data'):
  599. from utils.paths import SVG_ICONS
  600. if card.icon_data == SVG_ICONS.get('GEAR', ''):
  601. return card
  602. elif name in ('settings_topbar_btn', 'home_btn', 'github_btn'):
  603. return self._find_topbar_btn(name, main_win)
  604. return None
  605. # Mots-clés par bouton : objectName, toolTip, text (insensible à la casse)
  606. _TOPBAR_KEYWORDS = {
  607. 'settings_topbar_btn': ('setting', 'gear', 'reglage', 'einstellung'),
  608. 'home_btn': ('home', 'dashboard', 'accueil', 'startseite'),
  609. 'github_btn': ('github', 'git'),
  610. }
  611. def _find_topbar_btn(self, name, main_win):
  612. """Recherche un bouton topbar par objectName en priorité, puis mots-clés."""
  613. from PyQt6.QtWidgets import QPushButton
  614. # 1. Recherche directe par objectName (le plus fiable)
  615. btn = main_win.findChild(QPushButton, name)
  616. if btn:
  617. return btn
  618. # 2. Fallback : mots-clés dans objectName/toolTip/text
  619. # limité aux boutons dans la topbar (Y < 80px)
  620. keywords = self._TOPBAR_KEYWORDS.get(name, ())
  621. topbar_max_y = 80
  622. for btn in main_win.findChildren(QPushButton):
  623. pos_in_win = btn.mapTo(main_win, btn.rect().topLeft())
  624. if pos_in_win.y() > topbar_max_y:
  625. continue
  626. all_text = ' '.join([
  627. btn.objectName().lower(),
  628. btn.toolTip().lower(),
  629. btn.text().lower(),
  630. ])
  631. if any(kw in all_text for kw in keywords):
  632. return btn
  633. return None
  634. # home = plus à gauche, settings = tout à droite (dernier)
  635. # github = entre les deux
  636. if name == 'home_btn':
  637. return silent[0] # premier depuis la gauche
  638. elif name == 'settings_topbar_btn':
  639. return silent[-1] # tout à droite
  640. elif name == 'github_btn':
  641. return silent[-2] if len(silent) >= 2 else silent[-1]
  642. return None
  643. def _highlight_label(self, name: str) -> str:
  644. labels = {
  645. 'calibration_card': 'Calibration',
  646. 'settings_card': 'Settings',
  647. 'settings_topbar_btn': 'Settings',
  648. }
  649. return labels.get(name, '')
  650. def _on_onboarding_lang_changed(self, lang: str):
  651. """Appelé quand l'utilisateur change de langue depuis l'onboarding.
  652. Sauvegarde, reconstruit l'onboarding traduit et notifie la main window."""
  653. # 1. Sauvegarder la langue
  654. cfg = self.controller.config_manager
  655. machine = cfg.get_section("machine_settings") or {}
  656. machine["language"] = lang
  657. cfg.set_section("machine_settings", machine)
  658. cfg.save()
  659. # 2. Mémoriser l'étape courante avant reconstruction
  660. current_step = getattr(self._onboarding, '_step', 0)
  661. # 3. Reconstruire l'onboarding avec la nouvelle langue
  662. self._on_clear_highlight()
  663. self._build_onboarding(lang, step=current_step)
  664. # 4. Notifier la fenêtre principale
  665. main_win = self.window()
  666. if main_win and hasattr(main_win, "update_ui_language"):
  667. main_win.update_ui_language()
  668. def _on_clear_highlight(self):
  669. if hasattr(self, '_hl_overlay'):
  670. self._hl_overlay.hide_highlights()
  671. # Effacer les dernières cibles pour que resizeEvent
  672. # ne puisse pas relancer le highlight après la fin
  673. self._last_highlight_names = []
  674. def _on_onboarding_finished(self, settings: dict):
  675. """Appelé quand l'utilisateur termine ou skippe l'onboarding."""
  676. # Masquer l'overlay immédiatement, avant toute autre opération
  677. self._on_clear_highlight()
  678. cfg = self.controller.config_manager
  679. # Marquer comme fait
  680. current = cfg.get_section("app_state") or {}
  681. current["onboarding_done"] = True
  682. cfg.set_section("app_state", current)
  683. # Sauvegarder les réglages machine si fournis
  684. if settings:
  685. machine = cfg.get_section("machine_settings") or {}
  686. machine.update(settings)
  687. cfg.set_section("machine_settings", machine)
  688. cfg.save()
  689. # Réactiver les cartes et passer à la grille
  690. self._set_mode_cards_enabled(True)
  691. self._history_stack.setCurrentIndex(1)
  692. self._apply_history_area_style()
  693. self.render_grid()
  694. def setup_stats(self, parent_layout):
  695. # 1. Création du conteneur principal
  696. self.stats_frame = QFrame()
  697. self.stats_frame.setFixedHeight(120)
  698. self.stats_frame.setStyleSheet("""
  699. QFrame {
  700. background-color: #1e1e1e;
  701. border-radius: 15px;
  702. border: 1px solid #3d3d3d;
  703. }
  704. """)
  705. main_layout = QHBoxLayout(self.stats_frame)
  706. main_layout.setContentsMargins(10, 10, 10, 10)
  707. # 2. Récupération et calcul des données (Logique conservée)
  708. cfg = self.controller.config_manager
  709. total_l = cfg.get_item("stats", "total_lines", 0)
  710. total_g = cfg.get_item("stats", "total_gcodes", 0)
  711. total_s = float(cfg.get_item("stats", "total_time_seconds", 0))
  712. # Conversion du temps
  713. hours = int(total_s // 3600)
  714. minutes = int((total_s % 3600) // 60)
  715. if hours > 0:
  716. time_str = f"{hours}h {minutes:02d}m"
  717. elif minutes > 0:
  718. time_str = f"{minutes}m"
  719. elif total_s > 0:
  720. time_str = "< 1m"
  721. else:
  722. time_str = "0m"
  723. # 3. Définition des items à afficher
  724. stat_items = [
  725. (f"{int(total_l):,}", self.texts.get("lines_generated", "Lines")),
  726. (str(total_g), self.texts.get("gcode_saved", "G-Codes")),
  727. (time_str, self.texts.get("total_engraving_time", "Time"))
  728. ]
  729. # 4. Création dynamique des colonnes
  730. self._stat_value_labels = []
  731. self._stat_key_labels = []
  732. for value, label in stat_items:
  733. v_layout = QVBoxLayout()
  734. v_layout.setSpacing(2) # Espace réduit entre valeur et texte
  735. # Valeur (Bleu et gras)
  736. val_lbl = QLabel(value)
  737. self._stat_value_labels.append(val_lbl)
  738. val_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
  739. val_lbl.setStyleSheet("color: #1F6AA5; font-size: 18px; font-weight: bold; border: none; background: transparent;")
  740. # Label (Petit et gris)
  741. txt_lbl = QLabel(label)
  742. self._stat_key_labels.append(txt_lbl)
  743. txt_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
  744. txt_lbl.setWordWrap(True) # Au cas où le texte est long
  745. txt_lbl.setStyleSheet("color: gray; font-size: 10px; border: none; background: transparent;")
  746. v_layout.addWidget(val_lbl)
  747. v_layout.addWidget(txt_lbl)
  748. # On ajoute chaque colonne au layout horizontal
  749. main_layout.addLayout(v_layout)
  750. # Ajouter un séparateur vertical entre les colonnes (sauf après la dernière)
  751. if label != stat_items[-1][1]:
  752. line = QFrame()
  753. line.setFrameShape(QFrame.Shape.VLine)
  754. line.setStyleSheet("color: #3d3d3d; background-color: #3d3d3d; width: 1px; margin: 15px 0;")
  755. main_layout.addWidget(line)
  756. # Ajout du bloc complet au layout de la colonne de droite
  757. parent_layout.addWidget(self.stats_frame)
  758. def resizeEvent(self, event):
  759. super().resizeEvent(event)
  760. # On ne rafraîchit la grille que si la vue est prête
  761. if hasattr(self, 'history_area'):
  762. self.render_grid()
  763. # Recalculer l'overlay si actif et qu'il reste des cibles
  764. if (hasattr(self, '_hl_overlay') and self._hl_overlay.isVisible()
  765. and getattr(self, '_last_highlight_names', [])):
  766. self._on_request_highlight(self._last_highlight_names)
  767. def apply_theme(self, colors):
  768. """Met à jour toutes les cartes du dashboard lors d'un changement de thème"""
  769. from gui.utils_qt import get_svg_pixmap
  770. from PyQt6.QtCore import QSize
  771. self._current_colors = colors
  772. # 1. Fond de la vue et containers
  773. self.setStyleSheet("background-color: transparent;")
  774. if hasattr(self, "_left_container"):
  775. self._left_container.setStyleSheet("background: transparent;")
  776. if hasattr(self, "_right_container"):
  777. self._right_container.setStyleSheet("background: transparent;")
  778. # 2. Label "History"
  779. if hasattr(self, "history_label"):
  780. self.history_label.setStyleSheet(
  781. f"font-weight: bold; color: {colors['text']}; "
  782. "margin-top: 10px; background: transparent; border: none;"
  783. )
  784. # 3. Cadre thumbnails (history_area)
  785. if hasattr(self, "history_area"):
  786. self._apply_history_area_style(colors)
  787. self.history_area.verticalScrollBar().setStyleSheet(f"""
  788. QScrollBar:vertical {{
  789. border: none;
  790. background: {colors['scrollbar_bg']};
  791. width: 10px;
  792. margin: 0px;
  793. }}
  794. QScrollBar::handle:vertical {{
  795. background: {colors['scrollbar_handle']};
  796. min-height: 20px;
  797. border-radius: 5px;
  798. }}
  799. QScrollBar::handle:vertical:hover {{
  800. background: #1F6AA5;
  801. }}
  802. QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
  803. height: 0px;
  804. }}
  805. QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{
  806. background: none;
  807. }}
  808. """)
  809. # 4. Frame de statistiques
  810. if hasattr(self, "stats_frame"):
  811. self.stats_frame.setStyleSheet(f"""
  812. QFrame {{
  813. background-color: {colors['bg_card']};
  814. border-radius: 15px;
  815. border: 1px solid {colors['border']};
  816. }}
  817. """)
  818. # 5. Cartes de mode
  819. hover_bg = colors['hover_card']
  820. for i in range(self.modes_layout.count()):
  821. item = self.modes_layout.itemAt(i)
  822. if not item: continue
  823. card = item.widget()
  824. if not card or card.objectName() != "modeCard": continue
  825. is_disabled = (card.state == "disabled")
  826. if is_disabled:
  827. bg = colors['bg_main']
  828. txt = colors['text_disabled']
  829. icon_c = colors['text_disabled']
  830. else:
  831. bg = colors["bg_card"]
  832. txt = colors["text"]
  833. icon_c = colors["text"]
  834. if is_disabled:
  835. card.setStyleSheet(f"""
  836. QFrame#modeCard {{
  837. background-color: {bg};
  838. border: 2px solid {colors['border']};
  839. border-radius: 15px;
  840. }}
  841. """)
  842. else:
  843. card.setStyleSheet(f"""
  844. QFrame#modeCard {{
  845. background-color: {bg};
  846. border: 2px solid {colors['border']};
  847. border-radius: 15px;
  848. }}
  849. QFrame#modeCard:hover {{
  850. border-color: #1F6AA5;
  851. background-color: {hover_bg};
  852. }}
  853. """)
  854. card.title_label.setStyleSheet(
  855. f"color: {txt}; font-weight: bold; font-size: 15px; "
  856. "border: none; background: transparent;"
  857. )
  858. card.desc_label.setStyleSheet(
  859. f"color: {colors['text_secondary']}; font-size: 11px; "
  860. "border: none; background: transparent;"
  861. )
  862. if isinstance(card.icon_data, str) and card.icon_data.endswith(".svg"):
  863. pix = get_svg_pixmap(card.icon_data, size=QSize(35, 35), color_hex=icon_c)
  864. card.icon_label.setPixmap(pix)
  865. # 6. Onboarding theme
  866. if hasattr(self, "_onboarding"):
  867. self._onboarding.apply_theme(colors)
  868. # 7. Re-rendu des vignettes avec les nouvelles couleurs
  869. if hasattr(self, "all_pixmaps"):
  870. self.render_grid()
  871. def _apply_language(self, lang: str, translations: dict):
  872. """Reçoit lang directement — utilisé par update_ui_language."""
  873. from core.translations import TRANSLATIONS as _TR
  874. self.texts = _TR.get(lang, _TR["English"])["dashboard"]
  875. from gui.utils_qt import translate_ui_widgets
  876. translate_ui_widgets(self.translation_map, self.texts)
  877. if hasattr(self, 'history_label'):
  878. self.history_label.setText(self.texts.get("history", "History"))
  879. stat_keys = ["lines_generated", "gcode_saved", "total_engraving_time"]
  880. for i, key in enumerate(stat_keys):
  881. labels = getattr(self, '_stat_key_labels', [])
  882. if i < len(labels):
  883. labels[i].setText(self.texts.get(key, key))
  884. def update_texts(self):
  885. """Met à jour dynamiquement tous les labels du Dashboard"""
  886. lang = self.controller.config_manager.get_item("machine_settings", "language", "English")
  887. self._apply_language(lang, {})
  888. def refresh(self):
  889. """Recharge les stats et les vignettes depuis le disque."""
  890. # Mettre à jour les stats si les labels existent déjà
  891. if hasattr(self, '_stat_value_labels') and len(self._stat_value_labels) == 3:
  892. cfg = self.controller.config_manager
  893. total_l = cfg.get_item("stats", "total_lines", 0)
  894. total_g = cfg.get_item("stats", "total_gcodes", 0)
  895. total_s = float(cfg.get_item("stats", "total_time_seconds", 0))
  896. hours = int(total_s // 3600)
  897. minutes = int((total_s % 3600) // 60)
  898. if hours > 0:
  899. time_str = f"{hours}h {minutes:02d}m"
  900. elif minutes > 0:
  901. time_str = f"{minutes}m"
  902. elif total_s > 0:
  903. time_str = "< 1m"
  904. else:
  905. time_str = "0m"
  906. self._stat_value_labels[0].setText(f"{int(total_l):,}")
  907. self._stat_value_labels[1].setText(str(total_g))
  908. self._stat_value_labels[2].setText(time_str)
  909. # Recharger les vignettes
  910. self.load_thumbnails()