main_window_qt.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. # -*- coding: utf-8 -*-
  2. import os
  3. import sys
  4. import webbrowser
  5. from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
  6. QPushButton, QLabel, QStackedWidget, QFrame, QMessageBox)
  7. from PyQt6.QtCore import Qt, QSize, QTimer, pyqtSignal
  8. from PyQt6.QtGui import QIcon, QPixmap, QColor, QPalette
  9. from PyQt6.QtWidgets import QApplication
  10. IS_WINDOWS = sys.platform == "win32"
  11. if IS_WINDOWS:
  12. import ctypes
  13. from core.translations import TRANSLATIONS
  14. from core.themes import get_theme
  15. from utils import paths
  16. from gui.views.dashboard_view_qt import DashboardViewQt
  17. from gui.views.settings_view_qt import SettingsViewQt
  18. from gui.views.calibration_view_qt import CalibrationView
  19. from utils.paths import SVG_ICONS, ASSETS_DIR
  20. from gui.utils_qt import get_svg_pixmap
  21. class MainWindowQt(QMainWindow):
  22. # Signal émis quand l'UI est prête — utilisé par main_qt.py pour le fade-in
  23. ui_ready = pyqtSignal()
  24. def __init__(self, controller):
  25. super().__init__()
  26. # 1. PROTECTION RADICALE (Imports déjà faits en haut du fichier)
  27. self.setBackgroundRole(QPalette.ColorRole.Window)
  28. self.setAutoFillBackground(True)
  29. self.all_translations = TRANSLATIONS
  30. # On définit la couleur de la palette immédiatement
  31. pal = self.palette()
  32. dark_color = QColor("#2b2b2b")
  33. pal.setColor(QPalette.ColorRole.Window, dark_color)
  34. pal.setColor(QPalette.ColorRole.Base, dark_color)
  35. pal.setColor(QPalette.ColorRole.Button, dark_color)
  36. self.setPalette(pal)
  37. # NOTE : WA_NoSystemBackground supprimé — il masque les boutons de fenêtre
  38. # sous Linux et n'est pas nécessaire avec setAutoFillBackground(True).
  39. # On force le style CSS sur la Window ET le CentralWidget
  40. self.setStyleSheet("QWidget#CentralWidget { background-color: #2b2b2b; color: white; }")
  41. self.controller = controller
  42. self.config_manager = controller
  43. self.controller._main_window = self # ref pour que les vues remontent jusqu'ici
  44. self.version = "0.99b"
  45. # 2. CHARGEMENT DES TEXTES
  46. lang = self.config_manager.get_item("machine_settings", "language", "English")
  47. self.texts = TRANSLATIONS.get(lang, TRANSLATIONS["English"]).get("topbar", {})
  48. # 3. Configuration fenêtre (Géométrie uniquement, PAS DE SHOW)
  49. self.setWindowTitle(f"ALIG - Advanced Laser Imaging Generator v{self.version}")
  50. self._setup_window_init()
  51. self._setup_icon()
  52. # 4. Layout Principal
  53. self.central_widget = QWidget()
  54. self.central_widget.setObjectName("CentralWidget")
  55. self.setCentralWidget(self.central_widget)
  56. self.main_layout = QVBoxLayout(self.central_widget)
  57. self.main_layout.setContentsMargins(0, 0, 0, 0)
  58. self.main_layout.setSpacing(0)
  59. # 5. Composants
  60. self.setup_top_bar()
  61. self.content_area = QStackedWidget()
  62. self.main_layout.addWidget(self.content_area)
  63. # 6. Contenu (On charge TOUT maintenant)
  64. QTimer.singleShot(0, self._post_init_ui)
  65. def _post_init_ui(self):
  66. # Appliquer le thème en premier pour éviter un double rendu
  67. self.update_ui_theme()
  68. self.show_dashboard()
  69. # Signale à main_qt.py que l'UI est prête (fade-in, show, etc.)
  70. self.ui_ready.emit()
  71. def _preload_raster_view(self):
  72. """Crée raster_view en avance pour un basculement immédiat au clic."""
  73. if not hasattr(self, 'raster_view'):
  74. from gui.views.raster_view_qt import RasterViewQt
  75. self.raster_view = RasterViewQt(parent=self, controller=self)
  76. self.raster_view._main_window = self
  77. self.content_area.addWidget(self.raster_view)
  78. self.raster_view.apply_theme(self.get_theme_colors())
  79. def _setup_window_init(self):
  80. data = self.config_manager.get_section("window_settings")
  81. self.resize(1300, 900)
  82. def _setup_icon(self):
  83. """Configuration de l'icône de fenêtre et de la barre des tâches"""
  84. icon_path = paths.LOGO_ALIG
  85. if os.path.exists(icon_path):
  86. # 1. Définir l'icône de la fenêtre principale
  87. self.setWindowIcon(QIcon(icon_path))
  88. # 2. Fix pour la barre des tâches Windows (AppUserModelID)
  89. if IS_WINDOWS:
  90. try:
  91. myappid = f'momo.alig.lasergenerator.{self.version}'
  92. ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
  93. except Exception as e:
  94. print(f"Erreur lors du réglage de l'AppUserModelID : {e}")
  95. else:
  96. print(f"Alerte : Icône introuvable à l'emplacement {icon_path}")
  97. def setup_top_bar(self):
  98. self.top_bar = QFrame()
  99. #self.top_bar.setUpdatesEnabled(False)
  100. self.top_bar.setAutoFillBackground(True)
  101. self.top_bar.setObjectName("topBar")
  102. self.top_bar.setFixedHeight(50)
  103. self.top_bar.setStyleSheet("""
  104. /* On cible la barre elle-même */
  105. #topBar {
  106. background-color: #2b2b2b;
  107. border-bottom: 1px solid #3d3d3d;
  108. }
  109. /* On force tous les widgets à l'intérieur à ne pas avoir de bordure
  110. et un fond transparent */
  111. #topBar QWidget {
  112. border: none;
  113. background-color: transparent;
  114. }
  115. """)
  116. layout = QHBoxLayout(self.top_bar)
  117. layout.setContentsMargins(10, 0, 10, 1)
  118. layout.setSpacing(10)
  119. # --- INITIALISATION MAP DE TRADUCTION ---
  120. if not hasattr(self, 'translation_map'):
  121. self.translation_map = {}
  122. # 1. GAUCHE : Bouton HOME & Titre
  123. self.btn_home = QPushButton()
  124. home_pixmap = get_svg_pixmap(SVG_ICONS["HOME"], size=QSize(22, 22), color_hex="#FFFFFF")
  125. if not home_pixmap.isNull():
  126. self.btn_home.setIcon(QIcon(home_pixmap))
  127. self.btn_home.setIconSize(QSize(22, 22))
  128. self.btn_home.setFixedSize(40, 40)
  129. self.btn_home.setCursor(Qt.CursorShape.PointingHandCursor)
  130. self.btn_home.setStyleSheet("""
  131. QPushButton { background: transparent; border: none; border-radius: 5px; }
  132. QPushButton:hover { background-color: #3d3d3d; }
  133. """)
  134. self.btn_home.setObjectName("home_btn")
  135. self.btn_home.clicked.connect(self.show_dashboard)
  136. layout.addWidget(self.btn_home)
  137. # Label pour le titre de la vue (Dashboard / Settings / etc.)
  138. self.view_title = QLabel("")
  139. self.view_title.setStyleSheet("color: white; font-weight: bold; font-size: 14px; margin-left: 5px;")
  140. layout.addWidget(self.view_title)
  141. # --- RESSORT (Pousse tout le reste à droite) ---
  142. layout.addStretch()
  143. # 2. DROITE : GitHub et Settings
  144. # Bouton Support (Jaune)
  145. self.btn_support = QPushButton(self.texts.get("support", "Support"))
  146. self.btn_support.setCursor(Qt.CursorShape.PointingHandCursor)
  147. self.btn_support.setMinimumHeight(30)
  148. self.btn_support.setStyleSheet("""
  149. QPushButton {
  150. background-color: #FFDD00;
  151. color: black;
  152. font-weight: bold;
  153. border-radius: 4px;
  154. padding: 0px 15px;
  155. font-size: 12px;
  156. }
  157. QPushButton:hover { background-color: #f7d000; }
  158. """)
  159. import webbrowser
  160. self.btn_support.clicked.connect(lambda: webbrowser.open("https://buymeacoffee.com/momo830"))
  161. layout.addWidget(self.btn_support)
  162. # ENREGISTREMENT TRADUCTION SUPPORT
  163. self.translation_map[self.btn_support] = "support"
  164. # Bouton GitHub (Bleu ciel)
  165. self.btn_github = QPushButton("🌐 GitHub")
  166. self.btn_github.setCursor(Qt.CursorShape.PointingHandCursor)
  167. self.btn_github.setStyleSheet("""
  168. QPushButton {
  169. background: transparent;
  170. color: #1F6AA5;
  171. border: none;
  172. font-weight: bold;
  173. font-size: 12px;
  174. }
  175. QPushButton:hover { color: #3B8ED0; text-decoration: underline; }
  176. """)
  177. self.btn_github.setObjectName("github_btn")
  178. self.btn_github.clicked.connect(lambda: webbrowser.open("https://github.com/MoMo830/ALIG"))
  179. layout.addWidget(self.btn_github)
  180. # Petit séparateur vertical "|"
  181. self.separator = QLabel("|")
  182. self.separator.setStyleSheet("color: #444444; font-weight: bold;")
  183. layout.addWidget(self.separator)
  184. # 3. Bouton Settings (Icône SVG Gear)
  185. self.btn_settings = QPushButton()
  186. settings_pixmap = get_svg_pixmap(SVG_ICONS.get("SETTINGS", SVG_ICONS["GEAR"]), size=QSize(20, 20), color_hex="#FFFFFF")
  187. if not settings_pixmap.isNull():
  188. self.btn_settings.setIcon(QIcon(settings_pixmap))
  189. self.btn_settings.setIconSize(QSize(20, 20))
  190. else:
  191. self.btn_settings.setText("⚙️")
  192. self.btn_settings.setFixedSize(40, 40)
  193. self.btn_settings.setCursor(Qt.CursorShape.PointingHandCursor)
  194. self.btn_settings.setToolTip(self.texts.get("settings", "Settings")) # Tooltip traduit
  195. self.btn_settings.setStyleSheet("""
  196. QPushButton { background: transparent; color: white; border: none; }
  197. QPushButton:hover { background-color: #3d3d3d; border-radius: 5px; }
  198. """)
  199. self.btn_settings.setObjectName("settings_topbar_btn")
  200. self.btn_settings.clicked.connect(self.show_settings_mode)
  201. layout.addWidget(self.btn_settings)
  202. # btn_settings a une icône — pas de setText, on met à jour le tooltip dans update_ui_language
  203. # Enfin, on ajoute la TopBar au layout principal de la fenêtre
  204. self.main_layout.addWidget(self.top_bar)
  205. def update_ui_language(self):
  206. """Met à jour les textes de toute l'interface après un changement de langue."""
  207. lang = self.config_manager.get_item("machine_settings", "language", "English")
  208. from core.translations import TRANSLATIONS
  209. self.translations = TRANSLATIONS.get(lang, TRANSLATIONS["English"])
  210. self.texts = self.translations.get("topbar", {})
  211. if hasattr(self, 'translation_map'):
  212. from gui.utils_qt import translate_ui_widgets
  213. translate_ui_widgets(self.translation_map, self.texts)
  214. # Tooltip du bouton settings (icône — pas de setText)
  215. if hasattr(self, 'btn_settings'):
  216. self.btn_settings.setToolTip(self.texts.get("settings", "Settings"))
  217. # Propager à TOUTES les vues instanciées
  218. for attr in ("dashboard_view", "raster_view", "settings_view",
  219. "calibration_view", "checker_view"):
  220. view = getattr(self, attr, None)
  221. if view is None:
  222. continue
  223. if hasattr(view, '_apply_language'):
  224. view._apply_language(lang, self.translations)
  225. elif hasattr(view, 'update_ui_language'):
  226. view.update_ui_language()
  227. elif hasattr(view, 'update_texts'):
  228. view.update_texts()
  229. current_view = self.content_area.currentWidget()
  230. if current_view and not any(
  231. current_view is getattr(self, a, None)
  232. for a in ("dashboard_view", "raster_view", "settings_view",
  233. "calibration_view", "checker_view")
  234. ):
  235. if hasattr(current_view, 'update_ui_language'):
  236. current_view.update_ui_language()
  237. elif hasattr(current_view, 'update_texts'):
  238. current_view.update_texts()
  239. # view_title EN DERNIER — après toute propagation pour ne pas être écrasé
  240. if current_view:
  241. from gui.views.settings_view_qt import SettingsViewQt
  242. from gui.views.calibration_view_qt import CalibrationView
  243. title_key = "dashboard"
  244. if isinstance(current_view, SettingsViewQt):
  245. title_key = "settings"
  246. elif isinstance(current_view, CalibrationView):
  247. title_key = "calibration"
  248. self.view_title.setText(self.texts.get(title_key, title_key).upper())
  249. def update_ui_theme(self):
  250. """Met à jour les couleurs et les icônes de l'interface globale"""
  251. colors = self.get_theme_colors()
  252. bg_style = f"QWidget#CentralWidget {{ background-color: {colors['bg_card']}; }}"
  253. self.central_widget.setStyleSheet(bg_style)
  254. self.content_area.setStyleSheet("background: transparent; border: none;")
  255. self.top_bar.setStyleSheet(f"""
  256. QFrame#topBar {{
  257. background-color: {colors['bg_card']};
  258. border-bottom: 1px solid {colors['border']};
  259. }}
  260. QFrame#topBar QWidget {{
  261. border: none;
  262. background-color: transparent;
  263. }}
  264. """)
  265. # 4. Mise à jour du titre et des séparateurs
  266. self.view_title.setStyleSheet(f"""
  267. color: {colors['text']};
  268. font-weight: bold;
  269. font-size: 14px;
  270. margin-left: 5px;
  271. border: none;
  272. background: transparent;
  273. """)
  274. self.separator.setStyleSheet(f"color: {colors['border']}; font-weight: bold; background: transparent;")
  275. # 5. Re-génération des icônes SVG de la TopBar
  276. home_pix = get_svg_pixmap(SVG_ICONS["HOME"], QSize(22, 22), colors['text'])
  277. self.btn_home.setIcon(QIcon(home_pix))
  278. settings_pix = get_svg_pixmap(SVG_ICONS.get("SETTINGS", SVG_ICONS["GEAR"]), QSize(20, 20), colors['text'])
  279. if not settings_pix.isNull():
  280. self.btn_settings.setIcon(QIcon(settings_pix))
  281. # 6. Propager le changement à TOUTES les vues instanciées
  282. for attr in ("dashboard_view", "raster_view", "settings_view",
  283. "calibration_view", "checker_view"):
  284. view = getattr(self, attr, None)
  285. if view and hasattr(view, 'apply_theme'):
  286. view.apply_theme(colors)
  287. # checker est recréé à chaque appel — propager aussi à la vue courante si non couverte
  288. current_view = self.content_area.currentWidget()
  289. if current_view and hasattr(current_view, 'apply_theme'):
  290. if not any(current_view is getattr(self, a, None)
  291. for a in ("dashboard_view", "raster_view", "settings_view",
  292. "calibration_view", "checker_view")):
  293. current_view.apply_theme(colors)
  294. # --- Routage ---
  295. def show_dashboard(self):
  296. self.view_title.setText(self.texts.get("dashboard", "DASHBOARD").upper())
  297. if not hasattr(self, 'dashboard_view'):
  298. self.dashboard_view = DashboardViewQt(controller=self)
  299. self.dashboard_view._main_window = self
  300. self.content_area.addWidget(self.dashboard_view)
  301. self.dashboard_view.apply_theme(self.get_theme_colors())
  302. else:
  303. self.dashboard_view.refresh()
  304. self.current_view = self.dashboard_view
  305. self.content_area.setCurrentWidget(self.dashboard_view)
  306. def show_settings_mode(self):
  307. """Affiche et rafraîchit la vue des réglages"""
  308. # 1. On s'assure que les traductions sont à jour dans le controller
  309. # (Au cas où l'utilisateur vient de changer de langue)
  310. lang_code = self.controller.get_item("machine_settings", "language", "English")
  311. from core.translations import TRANSLATIONS
  312. self.controller.translations = TRANSLATIONS.get(lang_code, TRANSLATIONS["English"])
  313. # 2. Gestion de l'instance de la vue
  314. if not hasattr(self, 'settings_view'):
  315. self.settings_view = SettingsViewQt(self.controller)
  316. self.settings_view._main_window = self
  317. self.content_area.addWidget(self.settings_view)
  318. self.settings_view.apply_theme(self.get_theme_colors())
  319. else:
  320. # OPTIONNEL : Si la vue existe déjà, on peut forcer un rafraîchissement
  321. # des textes internes si nécessaire
  322. self.settings_view.texts = self.controller.translations["settings"]
  323. # 3. Mise à jour du titre de la zone de contenu
  324. self.view_title.setText(self.texts.get("settings", "SETTINGS").upper())
  325. # 4. Affichage
  326. self.content_area.setCurrentWidget(self.settings_view)
  327. def show_raster_mode(self, image_to_load=None):
  328. """Affiche la vue Raster Qt — pré-créée au démarrage pour un basculement immédiat."""
  329. self.view_title.setText(self.texts.get("raster", "RASTER MODE").upper())
  330. if not hasattr(self, "raster_view"):
  331. # Fallback si _preload_raster_view n'a pas encore tourné
  332. from gui.views.raster_view_qt import RasterViewQt
  333. self.raster_view = RasterViewQt(parent=self, controller=self)
  334. self.raster_view._main_window = self
  335. self.content_area.addWidget(self.raster_view)
  336. self.raster_view.apply_theme(self.get_theme_colors())
  337. else:
  338. self.raster_view.load_settings()
  339. self.current_view = self.raster_view
  340. self.content_area.setCurrentWidget(self.raster_view)
  341. def show_simulation(self, engine, payload, return_view="dashboard"):
  342. """Lance la vue de simulation Qt migrée."""
  343. self.view_title.setText("SIMULATION")
  344. from gui.views.simulation_view_qt import SimulationViewQt
  345. sim_view = SimulationViewQt(
  346. parent=self,
  347. controller=self,
  348. engine=engine,
  349. payload=payload,
  350. return_view=return_view
  351. )
  352. self.content_area.addWidget(sim_view)
  353. self.content_area.setCurrentWidget(sim_view)
  354. self.current_view = sim_view
  355. sim_view.apply_theme(self.get_theme_colors())
  356. def show_calibration_mode(self):
  357. """Affiche la vue de Calibration réelle"""
  358. # 1. Mise à jour du titre
  359. # On essaie de récupérer la traduction, sinon fallback sur "CALIBRATION"
  360. title = self.texts.get("calibration", "CALIBRATION")
  361. self.view_title.setText(title.upper())
  362. # 2. Gestion de l'instance de la vue (Lazy Loading)
  363. if not hasattr(self, 'calibration_view'):
  364. self.calibration_view = CalibrationView(parent=self, controller=self.controller)
  365. self.calibration_view._main_window = self
  366. self.content_area.addWidget(self.calibration_view)
  367. self.calibration_view.apply_theme(self.get_theme_colors())
  368. else:
  369. # Optionnel : Rafraîchir les textes si la langue a changé
  370. if hasattr(self.calibration_view, 'update_ui_language'):
  371. self.calibration_view.update_ui_language()
  372. # 3. Basculement sur la vue
  373. self.content_area.setCurrentWidget(self.calibration_view)
  374. def show_checker_mode(self, gcode_path=None):
  375. """Affiche la vue Checker (lecteur / visualiseur de G-Code existant).
  376. Crée une nouvelle instance à chaque appel, comme show_simulation.
  377. Optionnellement, ouvre directement un fichier si gcode_path est fourni."""
  378. self.view_title.setText("G-CODE CHECKER")
  379. from gui.views.checker_view_qt import CheckerViewQt
  380. checker_view = CheckerViewQt(
  381. parent=self,
  382. controller=self,
  383. return_view='dashboard'
  384. )
  385. self.checker_view = checker_view # stocké pour _apply_language
  386. self.content_area.addWidget(checker_view)
  387. self.content_area.setCurrentWidget(checker_view)
  388. self.current_view = checker_view
  389. checker_view.apply_theme(self.get_theme_colors())
  390. # Ouverture directe si un chemin est passé (ex: depuis l'historique)
  391. if gcode_path and os.path.isfile(gcode_path):
  392. checker_view._load_file(gcode_path)
  393. def get_theme_colors(self):
  394. theme = self.config_manager.get_item("machine_settings", "theme", "Dark")
  395. return get_theme(theme)
  396. def closeEvent(self, event):
  397. """Gère la fermeture (remplace on_closing)"""
  398. # Sauvegarde config...
  399. is_maximized = self.isMaximized()
  400. window_data = {
  401. "is_maximized": is_maximized,
  402. "geometry": f"{self.width()}x{self.height()}+{self.x()}+{self.y()}"
  403. }
  404. self.config_manager.set_section("window_settings", window_data)
  405. self.config_manager.save()
  406. event.accept()