main_window_qt.py 21 KB

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