settings_view_qt.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
  2. QPushButton, QScrollArea, QFrame, QLineEdit,
  3. QSlider, QComboBox, QTextEdit, QCheckBox,
  4. QSizePolicy)
  5. from PyQt6.QtCore import Qt, QTimer
  6. from PyQt6.QtGui import QFont
  7. from PyQt6.QtWidgets import QMessageBox
  8. import os
  9. import shutil
  10. import tempfile
  11. from utils.paths import SVG_ICONS
  12. from core.themes import get_theme
  13. #from utils.paths import ARROW_DOWN, CIRCLE
  14. from gui.switch import Switch
  15. class SettingsViewQt(QWidget):
  16. def __init__(self, controller):
  17. super().__init__()
  18. self.controller = controller
  19. self.translation_map = {}
  20. # 1. Chargement des traductions
  21. if hasattr(self.controller, 'translations'):
  22. self.texts = self.controller.translations["settings"]
  23. else:
  24. from core.translations import TRANSLATIONS
  25. self.texts = TRANSLATIONS["English"]["settings"]
  26. self.controls = {}
  27. self._section_frames = [] # Références directes aux frames de section
  28. self.loading = True
  29. # Couleurs du thème — initialisées dark, mises à jour par apply_theme
  30. self._theme_colors = get_theme('Dark')
  31. # 2. Configuration du Layout Principal
  32. self.main_layout = QVBoxLayout(self)
  33. self.main_layout.setContentsMargins(30, 30, 30, 30)
  34. self.main_layout.setSpacing(20)
  35. # 3. Construction de l'interface graphique
  36. self.setup_header() # Titre et bouton Save
  37. self.setup_scroll_area() # Colonnes et widgets
  38. # 4. Chargement des données du ConfigManager
  39. self.load_settings()
  40. # 5. Fin de l'initialisation
  41. self.loading = False # Autorise maintenant le mark_as_changed
  42. # ── Helpers de style (lisent _theme_colors) ──────────────────────────
  43. def _c(self, key, dark_val, light_val):
  44. # Conservé pour compatibilité — préférer colors['key'] directement
  45. return dark_val if self._theme_colors.get('suffix', '_DARK') == '_DARK' else light_val
  46. def _entry_style(self):
  47. c = self._theme_colors
  48. return (f"QLineEdit{{background-color:{c['bg_entry']};border:1px solid {c['border_strong']};"
  49. f"border-radius:4px;color:{c['text']};padding:3px;}}")
  50. @staticmethod
  51. def _arrow_svg_path(color: str) -> str:
  52. """Retourne le chemin vers un SVG de flèche recolorisé (tempfile)."""
  53. src = SVG_ICONS.get("ARROW_DOWN", "")
  54. if not src or not os.path.isfile(src):
  55. return src.replace("\\", "/")
  56. try:
  57. with open(src, "r", encoding="utf-8") as f:
  58. svg = f.read()
  59. import re
  60. svg = re.sub(r'fill\s*=\s*"[^"]*"', f'fill="{color}"', svg)
  61. svg = re.sub(r'fill\s*:\s*[^;}"]+', f'fill:{color}', svg)
  62. svg = re.sub(r'stroke\s*=\s*"[^"]*"', f'stroke="{color}"', svg)
  63. svg = re.sub(r'stroke\s*:\s*[^;}"]+', f'stroke:{color}', svg)
  64. tmp = tempfile.NamedTemporaryFile(
  65. suffix=f'_arrow_{color.strip("#")}.svg',
  66. delete=False, mode='w', encoding='utf-8'
  67. )
  68. tmp.write(svg)
  69. tmp.close()
  70. return tmp.name.replace("\\", "/")
  71. except Exception:
  72. return src.replace("\\", "/")
  73. def _combo_style(self):
  74. c = self._theme_colors
  75. arrow_path = self._arrow_svg_path(c['arrow_color'])
  76. return (
  77. f"QComboBox{{background-color:{c['bg_entry']};border:1px solid {c['border_strong']};"
  78. f"border-radius:5px;padding:3px 30px 3px 10px;color:{c['text']};}}"
  79. f"QComboBox::drop-down{{subcontrol-origin:padding;subcontrol-position:top right;"
  80. f"width:25px;border:none;background:transparent;}}"
  81. f"QComboBox::down-arrow{{image:url({arrow_path});width:12px;height:8px;}}"
  82. f"QComboBox QAbstractItemView{{background-color:{c['bg_entry']};color:{c['text']};"
  83. f"selection-background-color:{c['combo_selection']};border:1px solid {c['border_strong']};}}"
  84. )
  85. def _textedit_style(self):
  86. c = self._theme_colors
  87. return (f"QTextEdit{{background-color:{c['bg_entry_alt']};border:1px solid {c['border_strong']};"
  88. f"border-radius:6px;color:#8fbdf0;padding:5px;}}")
  89. def _label_row_style(self):
  90. return f"color:{self._theme_colors['text']};font-size:13px;border:none;background:transparent;"
  91. def _section_frame_style(self):
  92. c = self._theme_colors
  93. return (f"QFrame{{background-color:{c['bg_card_alt']};border:1px solid {c['border_card']};"
  94. f"border-radius:12px;}}"
  95. f"QLabel{{border:none;background:transparent;color:{c['text']};}}")
  96. def setup_header(self):
  97. """Crée l'en-tête avec titre et bouton Sauvegarder"""
  98. header_layout = QHBoxLayout()
  99. # Initialisation du registre si absent
  100. if not hasattr(self, 'translation_map'):
  101. self.translation_map = {}
  102. # Titre principal
  103. self.title_label = QLabel(self.texts.get("title", "Settings"))
  104. self.title_label.setObjectName("headerTitle") # Identifiant pour le style CSS et thèmes
  105. self.title_label.setStyleSheet("font-size: 28px; font-weight: bold; color: #1F6AA5;")
  106. # Enregistrement pour la traduction automatique
  107. self.translation_map[self.title_label] = "title"
  108. # Bouton Sauvegarder
  109. self.btn_save = QPushButton(self.texts.get("btn_save", "Save"))
  110. self.btn_save.setFixedSize(150, 40)
  111. self.btn_save.setCursor(Qt.CursorShape.PointingHandCursor)
  112. self.btn_save.clicked.connect(self.save_all_settings)
  113. self.set_button_style("idle")
  114. # Enregistrement pour la traduction automatique
  115. self.translation_map[self.btn_save] = "btn_save"
  116. # Assemblage du layout
  117. header_layout.addWidget(self.title_label)
  118. header_layout.addStretch()
  119. header_layout.addWidget(self.btn_save)
  120. self.main_layout.addLayout(header_layout)
  121. def setup_scroll_area(self):
  122. """Zone défilante contenant les colonnes de réglages"""
  123. scroll = QScrollArea()
  124. self._main_scroll = scroll
  125. scroll.setWidgetResizable(True)
  126. scroll.setFrameShape(QFrame.Shape.NoFrame)
  127. scroll.setStyleSheet("background: transparent;")
  128. container = QWidget()
  129. self.content_layout = QHBoxLayout(container)
  130. self.content_layout.setSpacing(30)
  131. self.content_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
  132. # Création des deux colonnes (Gauche & Droite)
  133. self.left_col = QVBoxLayout()
  134. self.right_col = QVBoxLayout()
  135. self.content_layout.addLayout(self.left_col, 1)
  136. self.content_layout.addLayout(self.right_col, 1)
  137. # --- COLONNE GAUCHE (G-Code & Hardware) ---
  138. self.setup_left_column()
  139. # --- COLONNE DROITE (Apparence & Maintenance) ---
  140. self.setup_right_column()
  141. scroll.setWidget(container)
  142. self.main_layout.addWidget(scroll)
  143. def setup_left_column(self):
  144. # --- SECTION G-CODE ---
  145. # On passe la CLÉ "sec_gcode" au lieu du texte traduit
  146. sec_gcode = self.create_section(self.left_col, "sec_gcode")
  147. # Mode de commande (M67 / S)
  148. self.create_dropdown(sec_gcode, "label_cmd_mode",
  149. ["M67 (Analog)", "S (Spindle)"], "cmd_mode")
  150. # Entrées simples
  151. self.create_simple_input(sec_gcode, "label_output_e", "m67_e_num", precision=0)
  152. self.create_simple_input(sec_gcode, "label_ctrl_max", "ctrl_max", precision=0)
  153. # Mode d'allumage (M3/M4)
  154. self.create_dropdown(sec_gcode, "label_firing", ["M3/M5", "M4/M5"], "firing_mode")
  155. # Extension de fichier
  156. self.create_simple_input(sec_gcode, "label_extension", "gcode_extension")
  157. # --- SECTION HARDWARE ---
  158. sec_hw = self.create_section(self.left_col, "sec_hardware")
  159. # Sliders
  160. self.create_slider_input(sec_hw, "label_latency", -20, 20, 0, "laser_latency")
  161. self.create_slider_input(sec_hw, "label_overscan", 0, 50, 10, "premove")
  162. self.create_slider_input(sec_hw, "hor_linestep", 0.01, 0.5, 0.1, "hor_linestep", decimals=4)
  163. self.create_slider_input(sec_hw, "ver_linestep", 0.01, 0.5, 0.1, "ver_linestep", decimals=4)
  164. # --- SECTION SCRIPTS ---
  165. sec_scripts = self.create_section(self.left_col, "sec_scripts")
  166. self.create_script_box(sec_scripts, "label_header", "custom_header")
  167. self.create_script_box(sec_scripts, "label_footer", "custom_footer")
  168. self.left_col.addStretch()
  169. def setup_right_column(self):
  170. # --- SECTION APPARENCE ---
  171. sec_app = self.create_section(self.right_col, "sec_appearance")
  172. # Thème et Langue
  173. self.create_dropdown(sec_app, "label_theme", ["Dark", "Light", "System"], "theme")
  174. if hasattr(self.controller, 'all_translations'):
  175. available_languages = sorted(list(self.controller.all_translations.keys()))
  176. else:
  177. # Fallback de sécurité si le controller n'est pas encore prêt
  178. available_languages = ["English", "Français", "Deutsch"]
  179. self.create_dropdown(sec_app, "label_lang", available_languages, "language")
  180. # Switch Vignettes
  181. self.create_switch(sec_app, "enable_thumbnails", "enable_thumbnails")
  182. # --- SECTION MAINTENANCE ---
  183. sec_maint = self.create_section(self.right_col, "maintenance_data")
  184. # Bouton Effacer Vignettes
  185. self.btn_clear_data = QPushButton(self.texts["erase_thumbnails"])
  186. self.translation_map[self.btn_clear_data] = "erase_thumbnails"
  187. self.btn_clear_data.setFixedHeight(35)
  188. self.btn_clear_data.setCursor(Qt.CursorShape.PointingHandCursor)
  189. self.btn_clear_data.setStyleSheet("""
  190. QPushButton { background-color: #444; color: white; border-radius: 6px; font-weight: bold; }
  191. QPushButton:hover { background-color: #942121; }
  192. """)
  193. self.btn_clear_data.clicked.connect(self.clear_thumbnails_and_stats)
  194. sec_maint.addWidget(self.btn_clear_data)
  195. # Bouton Reset Paramètres
  196. self.btn_reset_all = QPushButton(self.texts["reset_all_parameters"])
  197. self.translation_map[self.btn_reset_all] = "reset_all_parameters"
  198. self.btn_reset_all.setFixedHeight(35)
  199. self.btn_reset_all.setCursor(Qt.CursorShape.PointingHandCursor)
  200. self.btn_reset_all.setStyleSheet("""
  201. QPushButton { background-color: transparent; color: #ddd; border: 1px solid #8b0000; border-radius: 6px; }
  202. QPushButton:hover { background-color: #8b0000; color: white; }
  203. """)
  204. self.btn_reset_all.clicked.connect(self.reset_settings)
  205. sec_maint.addWidget(self.btn_reset_all)
  206. self.right_col.addStretch()
  207. # --- HELPERS DE CONSTRUCTION ---
  208. def create_section(self, layout, title_key):
  209. """Crée un bloc section et enregistre le label pour la traduction centralisée"""
  210. section_frame = QFrame()
  211. section_frame.setObjectName("sectionFrame")
  212. section_frame.setStyleSheet(self._section_frame_style())
  213. self._section_frames.append(section_frame)
  214. sec_layout = QVBoxLayout(section_frame)
  215. sec_layout.setContentsMargins(15, 15, 15, 15)
  216. sec_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
  217. # --- TRADUCTION INITIALE ---
  218. # On récupère le texte, si la clé n'existe pas, on affiche la clé
  219. title_text = self.texts.get(title_key, title_key).upper()
  220. title_lbl = QLabel(title_text)
  221. title_lbl.setStyleSheet("color: #3a9ad9; font-weight: bold; font-size: 12px; border: none;")
  222. sec_layout.addWidget(title_lbl)
  223. # --- ENREGISTREMENT CENTRALISÉ ---
  224. if not hasattr(self, 'translation_map'):
  225. self.translation_map = {}
  226. # On lie l'objet Label à sa clé pour update_texts()
  227. self.translation_map[title_lbl] = title_key
  228. layout.addWidget(section_frame)
  229. return sec_layout
  230. def create_input_row(self, layout, label_key, widget, key=None):
  231. row = QWidget()
  232. row.setStyleSheet("border: none; background: transparent;")
  233. row_layout = QHBoxLayout(row)
  234. row_layout.setContentsMargins(0, 5, 0, 5)
  235. # Initialisation de la map si elle n'existe pas
  236. if not hasattr(self, 'translation_map'):
  237. self.translation_map = {}
  238. # --- LOGIQUE DE TRADUCTION AMÉLIORÉE ---
  239. if key:
  240. # 1. On vérifie si label_key est DIRECTEMENT une clé de traduction (ex: "enable_thumbnails")
  241. if label_key in self.texts:
  242. translation_key = label_key
  243. # 2. Sinon, est-ce qu'elle a déjà un préfixe connu ?
  244. elif label_key.startswith("label_") or label_key.startswith("sec_"):
  245. translation_key = label_key
  246. # 3. Sinon, on tente d'ajouter "label_" par défaut (ex: "extension" -> "label_extension")
  247. else:
  248. translation_key = f"label_{label_key}"
  249. # On récupère le texte final
  250. display_text = self.texts.get(translation_key, translation_key)
  251. lbl = QLabel(display_text)
  252. # On enregistre l'objet QLabel avec sa clé trouvée
  253. self.translation_map[lbl] = translation_key
  254. else:
  255. # Cas sans clé technique (juste du texte statique)
  256. lbl = QLabel(label_key)
  257. lbl.setStyleSheet(self._label_row_style())
  258. row_layout.addWidget(lbl)
  259. row_layout.addStretch()
  260. row_layout.addWidget(widget)
  261. layout.addWidget(row)
  262. def create_simple_input(self, layout, label_text, key, precision=2):
  263. """Crée une ligne avec un Label et un QLineEdit sans valeur par défaut externe"""
  264. edit = QLineEdit()
  265. edit.setFixedWidth(100)
  266. edit.setStyleSheet(self._entry_style())
  267. # On initialise à 0 par défaut, formaté selon la précision
  268. initial_value = 0
  269. fmt = "{:d}" if precision == 0 else f"{{:.{precision}f}}"
  270. edit.setText(fmt.format(initial_value))
  271. edit.textChanged.connect(self.mark_as_changed)
  272. # On ajoute la ligne au layout via votre méthode parente
  273. self.create_input_row(layout, label_text, edit, key=key)
  274. # On stocke les infos dans le dictionnaire de contrôle
  275. self.controls[key] = {
  276. "entry": edit,
  277. "precision": precision,
  278. "is_int": (precision == 0)
  279. }
  280. # --- LOGIQUE DE STYLE DU BOUTON ---
  281. def set_button_style(self, state):
  282. if state == "idle":
  283. style = "background-color: #1F6AA5; color: white; border-radius: 8px; font-weight: bold;"
  284. elif state == "saved":
  285. style = "background-color: #2d5a27; color: white; border-radius: 8px; font-weight: bold;"
  286. elif state == "changed":
  287. style = "background-color: #3B8ED0; color: white; border-radius: 8px; font-weight: bold; border: 2px solid white;"
  288. self.btn_save.setStyleSheet(style)
  289. def mark_as_changed(self):
  290. if not self.loading:
  291. self.set_button_style("changed")
  292. # --- LOGIQUE DE SAUVEGARDE ---
  293. def save_all_settings(self):
  294. """Récupère toutes les valeurs de l'interface et les sauvegarde"""
  295. try:
  296. def get_float(key):
  297. try:
  298. return float(self.controls[key]["entry"].text().replace(',', '.'))
  299. except:
  300. return 0.0
  301. new_settings = {
  302. "theme": self.controls["theme"]["combo"].currentText(),
  303. "language": self.controls["language"]["combo"].currentText(),
  304. "enable_thumbnails": self.controls["enable_thumbnails"]["check"].isChecked(),
  305. "cmd_mode": self.controls["cmd_mode"]["combo"].currentText(),
  306. "firing_mode": self.controls["firing_mode"]["combo"].currentText(),
  307. "m67_e_num": int(float(self.controls["m67_e_num"]["entry"].text() or "0")),
  308. "ctrl_max": int(float(self.controls["ctrl_max"]["entry"].text() or "0")),
  309. "gcode_extension": self.controls["gcode_extension"]["entry"].text(),
  310. "laser_latency": get_float("laser_latency"),
  311. "premove": get_float("premove"),
  312. "hor_linestep": get_float("hor_linestep"),
  313. "ver_linestep": get_float("ver_linestep"),
  314. "custom_header": self.controls["custom_header"]["text"].toPlainText(),
  315. "custom_footer": self.controls["custom_footer"]["text"].toPlainText()
  316. }
  317. self.controller.set_section("machine_settings", new_settings)
  318. if self.controller.save():
  319. from core.translations import TRANSLATIONS
  320. new_lang = new_settings["language"]
  321. print(f"[SAVE] langue sauvée: {new_lang}")
  322. self.texts = TRANSLATIONS.get(new_lang, TRANSLATIONS["English"])["settings"]
  323. self.update_texts()
  324. self.set_button_style("saved")
  325. self.btn_save.setText(f"✓ {self.texts.get('btn_save', 'Save')}")
  326. main_window = (getattr(self, '_main_window', None)
  327. or getattr(self.controller, '_main_window', None)
  328. or self.window())
  329. print(f"[SAVE] main_window trouvé: {main_window}")
  330. print(f"[SAVE] has update_ui_language: {hasattr(main_window, 'update_ui_language')}")
  331. if main_window and hasattr(main_window, 'update_ui_language'):
  332. main_window.update_ui_language()
  333. if main_window and hasattr(main_window, 'update_ui_theme'):
  334. main_window.update_ui_theme()
  335. QTimer.singleShot(2000, self.reset_save_btn)
  336. else:
  337. raise Exception("Erreur d'écriture disque")
  338. except Exception as e:
  339. import traceback
  340. print(f"[SAVE] ERREUR: {e}")
  341. traceback.print_exc()
  342. self.btn_save.setStyleSheet("background-color: #942121; color: white; border-radius: 8px;")
  343. def _apply_language(self, lang: str, translations: dict):
  344. """Reçoit lang et translations directement — pas de relecture config."""
  345. from core.translations import TRANSLATIONS as _TR
  346. self.texts = _TR.get(lang, _TR["English"])["settings"]
  347. from gui.utils_qt import translate_ui_widgets
  348. translate_ui_widgets(self.translation_map, self.texts)
  349. def update_texts(self):
  350. """Met à jour dynamiquement tous les labels de la vue settings"""
  351. # 1. Recharger les textes
  352. lang = self.controller.get_item("machine_settings", "language", "English")
  353. from core.translations import TRANSLATIONS
  354. self.texts = TRANSLATIONS.get(lang, TRANSLATIONS["English"])["settings"]
  355. # 2. Traduire TOUT d'un coup (Titre, bouton save, sections, labels)
  356. from gui.utils_qt import translate_ui_widgets
  357. translate_ui_widgets(self.translation_map, self.texts)
  358. # 3. Optionnel : Si vous avez d'autres boutons fixes (Reset, Clear data)
  359. # n'oubliez pas de les ajouter à la map dans leur setup respectif !
  360. def reset_save_btn(self):
  361. self.set_button_style("idle")
  362. self.btn_save.setText(self.texts["btn_save"])
  363. def create_slider_input(self, layout, label_text, min_v, max_v, default, key, decimals=2):
  364. """
  365. Crée une rangée avec Label + Slider + Entry.
  366. decimals : nombre de chiffres après la virgule (0 pour des entiers).
  367. """
  368. # 1. Calcul du facteur d'échelle pour le slider
  369. # 10^decimals (ex: 0 -> 1, 1 -> 10, 2 -> 100)
  370. multiplier = 10 ** decimals
  371. right_side_widget = QWidget()
  372. right_side_widget.setFixedWidth(180)
  373. right_side_layout = QHBoxLayout(right_side_widget)
  374. right_side_layout.setContentsMargins(0, 0, 0, 0)
  375. right_side_layout.setSpacing(10)
  376. # 2. Le Slider
  377. slider = QSlider(Qt.Orientation.Horizontal)
  378. slider.setRange(int(min_v * multiplier), int(max_v * multiplier))
  379. slider.setValue(int(default * multiplier))
  380. slider.setFixedWidth(120)
  381. # 3. Le champ de saisie (Entry)
  382. # Formatage initial selon decimals
  383. initial_val = int(default) if decimals == 0 else default
  384. entry = QLineEdit(str(initial_val))
  385. entry.setFixedWidth(50)
  386. entry.setAlignment(Qt.AlignmentFlag.AlignCenter)
  387. entry.setStyleSheet(self._entry_style())
  388. # --- GESTION DES CONNEXIONS ---
  389. def on_slider_moved(v):
  390. val = v / multiplier
  391. # Formatage dynamique : entier si 0, sinon float
  392. text = str(int(val)) if decimals == 0 else f"{val:.{decimals}f}"
  393. entry.setText(text)
  394. self.mark_as_changed()
  395. def on_entry_finished():
  396. try:
  397. raw_text = entry.text().replace(',', '.')
  398. val = float(raw_text) if raw_text else 0.0
  399. slider.setValue(int(val * multiplier))
  400. self.mark_as_changed()
  401. except ValueError:
  402. # Optionnel : remettre la valeur du slider si la saisie est invalide
  403. on_slider_moved(slider.value())
  404. slider.valueChanged.connect(on_slider_moved)
  405. entry.editingFinished.connect(on_entry_finished)
  406. right_side_layout.addWidget(slider)
  407. right_side_layout.addWidget(entry)
  408. # 4. Intégration dans la ligne
  409. self.create_input_row(layout, label_text, right_side_widget, key=key)
  410. # Stockage
  411. self.controls[key] = {"slider": slider, "entry": entry, "decimals": decimals}
  412. def create_script_box(self, layout, label_key, key):
  413. """Crée un bloc de texte multi-lignes pour les scripts G-Code avec traduction"""
  414. container = QWidget()
  415. v_box = QVBoxLayout(container)
  416. v_box.setContentsMargins(0, 10, 0, 5)
  417. v_box.setSpacing(5)
  418. # --- TRADUCTION INITIALE ---
  419. # On va chercher la traduction de la clé (ex: "label_header")
  420. display_text = self.texts.get(label_key, label_key)
  421. lbl = QLabel(display_text)
  422. lbl.setStyleSheet(f"color:{self._theme_colors.get('text_secondary','#bbbbbb')};"
  423. "font-size:11px;font-weight:bold;border:none;background:transparent;")
  424. lbl.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
  425. # --- ENREGISTREMENT POUR LA TRADUCTION DYNAMIQUE ---
  426. if not hasattr(self, 'translation_map'):
  427. self.translation_map = {}
  428. self.translation_map[lbl] = label_key
  429. # Zone de texte
  430. text_edit = QTextEdit()
  431. text_edit.setFixedHeight(80)
  432. text_edit.setFont(QFont("Consolas", 10))
  433. text_edit.setStyleSheet(self._textedit_style())
  434. text_edit.textChanged.connect(self.mark_as_changed)
  435. v_box.addWidget(lbl)
  436. v_box.addWidget(text_edit)
  437. layout.addWidget(container, alignment=Qt.AlignmentFlag.AlignTop)
  438. # Stockage pour la sauvegarde (attention à utiliser la même structure que les autres)
  439. self.controls[key] = {"text": text_edit}
  440. def create_dropdown(self, layout, label_text, options, key):
  441. """Crée un menu déroulant et délègue la gestion du label à create_input_row"""
  442. combo = QComboBox()
  443. combo.addItems(options)
  444. combo.setFixedWidth(150)
  445. combo.setStyleSheet(self._combo_style())
  446. combo.currentIndexChanged.connect(self.mark_as_changed)
  447. self.create_input_row(layout, label_text, combo, key=key)
  448. # Stockage pour la récupération des données
  449. self.controls[key] = {"combo": combo}
  450. def create_switch(self, layout, label_key, key):
  451. """Crée une ligne avec un Label et un Switch"""
  452. check = Switch()
  453. #check.toggled.connect(lambda v: print(f"Switch {key}:", v))
  454. check.stateChanged.connect(self.mark_as_changed)
  455. self.create_input_row(layout, label_key, check, key=key)
  456. self.controls[key] = {"check": check}
  457. def ask_confirmation(self, title, message):
  458. """Affiche une boîte de dialogue de confirmation stylisée"""
  459. msg_box = QMessageBox(self)
  460. msg_box.setWindowTitle(title)
  461. msg_box.setText(message)
  462. msg_box.setIcon(QMessageBox.Icon.Question)
  463. # Boutons personnalisés
  464. yes_button = msg_box.addButton(self.texts.get("confirm_yes", "Confirmer"), QMessageBox.ButtonRole.YesRole)
  465. no_button = msg_box.addButton(self.texts.get("confirm_no", "Annuler"), QMessageBox.ButtonRole.NoRole)
  466. msg_box.setDefaultButton(no_button)
  467. # Style Dark pour la boîte
  468. msg_box.setStyleSheet("""
  469. QMessageBox { background-color: #2b2b2b; }
  470. QLabel { color: white; font-size: 13px; }
  471. QPushButton {
  472. background-color: #444; color: white; border-radius: 4px;
  473. padding: 5px 15px; min-width: 80px;
  474. }
  475. QPushButton:hover { background-color: #1F6AA5; }
  476. """)
  477. msg_box.exec()
  478. return msg_box.clickedButton() == yes_button
  479. def load_settings(self):
  480. self.loading = True
  481. data = self.controller.get_section("machine_settings")
  482. # --- LOGIQUE SÉCURISÉE ---
  483. # 1. ComboBox
  484. for key in ["theme", "language", "cmd_mode", "firing_mode"]:
  485. if key in self.controls:
  486. val = data.get(key, "") # Si None, on prend une chaîne vide
  487. index = self.controls[key]["combo"].findText(str(val))
  488. if index >= 0:
  489. self.controls[key]["combo"].setCurrentIndex(index)
  490. # 2. LineEdits
  491. for key in ["m67_e_num", "ctrl_max"]:
  492. if key in self.controls:
  493. raw = data.get(key, 0)
  494. try:
  495. val = int(float(str(raw))) # gère "0", 0, 0.0, "1000.0"
  496. except (ValueError, TypeError):
  497. val = 0
  498. self.controls[key]["entry"].setText(str(val))
  499. for key in ["gcode_extension"]:
  500. if key in self.controls:
  501. self.controls[key]["entry"].setText(str(data.get(key, "")))
  502. # 3. Sliders
  503. for key in ["laser_latency", "premove"]:
  504. if key in self.controls:
  505. # On force une valeur de secours (0.0) si le manager renvoie None
  506. raw_val = data.get(key)
  507. val = float(raw_val) if raw_val is not None else 0.0
  508. self.controls[key]["slider"].setValue(int(val * 100))
  509. self.controls[key]["entry"].setText(f"{val:.2f}")
  510. for key in ["hor_linestep", "ver_linestep"]:
  511. if key in self.controls:
  512. raw_val = data.get(key)
  513. val = float(raw_val) if raw_val is not None else 0.1
  514. self.controls[key]["slider"].setValue(int(val * 10000))
  515. self.controls[key]["entry"].setText(f"{val:.4f}")
  516. # 4. Scripts & Switches
  517. if "custom_header" in self.controls:
  518. self.controls["custom_header"]["text"].setPlainText(data.get("custom_header", ""))
  519. if "custom_footer" in self.controls:
  520. self.controls["custom_footer"]["text"].setPlainText(data.get("custom_footer", ""))
  521. if "enable_thumbnails" in self.controls:
  522. self.controls["enable_thumbnails"]["check"].setChecked(bool(data.get("enable_thumbnails", True)))
  523. self.loading = False
  524. self.set_button_style("idle")
  525. def showEvent(self, event):
  526. """Recharge les valeurs depuis la config à chaque fois que la vue devient visible.
  527. Indispensable pour refléter les sauvegardes faites depuis d'autres vues (ex: calibration)."""
  528. super().showEvent(event)
  529. self.load_settings()
  530. def clear_thumbnails_and_stats(self):
  531. """Supprime les vignettes physiques et réinitialise les statistiques dans le JSON"""
  532. # 1. Demande de confirmation à l'utilisateur
  533. if not self.ask_confirmation(self.texts["erase_thumbnails"], self.texts["erase_thumbnails_confirm"]):
  534. return
  535. # 2. NETTOYAGE DES FICHIERS (Vignettes)
  536. # On définit le chemin vers le dossier des miniatures
  537. target_dir = os.path.join(os.getcwd(), "assets", "thumbnails")
  538. files_purged = 0
  539. if os.path.exists(target_dir):
  540. try:
  541. for filename in os.listdir(target_dir):
  542. # On préserve le fichier .gitkeep s'il existe
  543. if filename == ".gitkeep":
  544. continue
  545. file_path = os.path.join(target_dir, filename)
  546. try:
  547. if os.path.isfile(file_path) or os.path.islink(file_path):
  548. os.unlink(file_path) # Suppression du fichier
  549. elif os.path.isdir(file_path):
  550. shutil.rmtree(file_path) # Suppression du dossier si présent
  551. files_purged += 1
  552. except Exception as e:
  553. print(f"Impossible de supprimer {filename}: {e}")
  554. except Exception as e:
  555. print(f"Erreur d'accès au dossier: {e}")
  556. # 3. NETTOYAGE DES STATISTIQUES (Dans le ConfigManager)
  557. try:
  558. # On définit les valeurs de remise à zéro
  559. empty_stats = {
  560. "total_lines": 0,
  561. "total_gcodes": 0,
  562. "total_time_seconds": 0.0,
  563. "last_project_time": 0.0
  564. }
  565. # Mise à jour de la section via le controller
  566. self.controller.set_section("stats", empty_stats)
  567. # Sauvegarde immédiate du fichier JSON
  568. if self.controller.save():
  569. # Feedback visuel : Succès
  570. self.btn_clear_data.setText("✓ " + self.texts["erase_thumbnails_done"])
  571. self.btn_clear_data.setStyleSheet("""
  572. QPushButton {
  573. background-color: #2d5a27;
  574. color: white;
  575. border-radius: 6px;
  576. font-weight: bold;
  577. }
  578. """)
  579. else:
  580. raise Exception("Erreur de sauvegarde JSON")
  581. except Exception as e:
  582. print(f"Erreur lors de la remise à zéro des stats: {e}")
  583. self.btn_clear_data.setText("Erreur")
  584. self.btn_clear_data.setStyleSheet("background-color: #942121; color: white;")
  585. # 4. Retour à l'état initial du bouton après 2 secondes
  586. # On utilise un QTimer pour ne pas bloquer l'interface
  587. QTimer.singleShot(2000, self.restore_maintenance_buttons)
  588. def restore_maintenance_buttons(self):
  589. """Restitue le style original du bouton de maintenance"""
  590. self.btn_clear_data.setText(self.texts["erase_thumbnails"])
  591. self.btn_clear_data.setStyleSheet("""
  592. QPushButton {
  593. background-color: #444;
  594. color: white;
  595. border-radius: 6px;
  596. font-weight: bold;
  597. }
  598. QPushButton:hover { background-color: #942121; }
  599. """)
  600. def reset_settings(self):
  601. if not self.ask_confirmation(
  602. self.texts["reset_all_parameters"],
  603. self.texts["reset_all_parameters_confirm"]):
  604. return
  605. # 1. Réinitialiser les paramètres machine
  606. if self.controller.reset_all():
  607. self.load_settings()
  608. # 2. Effacer les vignettes et stats (sans nouvelle confirmation)
  609. self._clear_thumbnails_silent()
  610. # 3. Réactiver l'onboarding au prochain lancement
  611. current = self.controller.get_section("app_state") or {}
  612. current["onboarding_done"] = False
  613. self.controller.set_section("app_state", current)
  614. self.controller.save()
  615. # 4. Relancer l'onboarding sur le dashboard
  616. main_win = self.window()
  617. if main_win and hasattr(main_win, "dashboard_view"):
  618. dv = main_win.dashboard_view
  619. if hasattr(dv, "restart_onboarding"):
  620. dv.restart_onboarding()
  621. main_win.show_dashboard()
  622. def _clear_thumbnails_silent(self):
  623. """Efface les vignettes et stats sans confirmation ni feedback visuel."""
  624. import shutil, os
  625. target_dir = os.path.join(os.getcwd(), "assets", "thumbnails")
  626. if os.path.exists(target_dir):
  627. for filename in os.listdir(target_dir):
  628. if filename == ".gitkeep":
  629. continue
  630. fp = os.path.join(target_dir, filename)
  631. try:
  632. if os.path.isfile(fp) or os.path.islink(fp):
  633. os.unlink(fp)
  634. elif os.path.isdir(fp):
  635. shutil.rmtree(fp)
  636. except Exception as e:
  637. print(f"Impossible de supprimer {filename}: {e}")
  638. empty_stats = {
  639. "total_lines": 0, "total_gcodes": 0,
  640. "total_time_seconds": 0.0, "last_project_time": 0.0
  641. }
  642. self.controller.set_section("stats", empty_stats)
  643. self.controller.save()
  644. def apply_theme(self, colors):
  645. """Met à jour dynamiquement les couleurs de la vue Settings"""
  646. self._theme_colors = colors
  647. text = colors['text']
  648. text_sec = colors['text_secondary']
  649. # ── Vue racine ────────────────────────────────────────────
  650. self.setStyleSheet("background-color: transparent;")
  651. # ── Scrollbar principale ─────────────────────────────────
  652. if hasattr(self, "_main_scroll"):
  653. self._main_scroll.verticalScrollBar().setStyleSheet(f"""
  654. QScrollBar:vertical {{
  655. border: none;
  656. background: {colors['scrollbar_bg']};
  657. width: 10px;
  658. margin: 0px;
  659. }}
  660. QScrollBar::handle:vertical {{
  661. background: {colors['scrollbar_handle']};
  662. min-height: 20px;
  663. border-radius: 5px;
  664. }}
  665. QScrollBar::handle:vertical:hover {{
  666. background: #1F6AA5;
  667. }}
  668. QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
  669. height: 0px;
  670. }}
  671. QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{
  672. background: none;
  673. }}
  674. """)
  675. # ── Sections ─────────────────────────────────────────────
  676. for frame in self._section_frames:
  677. frame.setStyleSheet(self._section_frame_style())
  678. # ── Labels ───────────────────────────────────────────────
  679. for lbl in self.findChildren(QLabel):
  680. if lbl.objectName() == "headerTitle":
  681. continue
  682. if lbl.styleSheet() and '#3a9ad9' in lbl.styleSheet():
  683. continue
  684. if lbl.styleSheet() and 'font-weight:bold' in lbl.styleSheet().replace(' ', ''):
  685. lbl.setStyleSheet(
  686. f"color:{text_sec};font-size:11px;font-weight:bold;"
  687. "border:none;background:transparent;"
  688. )
  689. else:
  690. lbl.setStyleSheet(self._label_row_style())
  691. # ── QLineEdit ─────────────────────────────────────────────
  692. for w in self.findChildren(QLineEdit):
  693. w.setStyleSheet(self._entry_style())
  694. # ── QComboBox ─────────────────────────────────────────────
  695. for w in self.findChildren(QComboBox):
  696. w.setStyleSheet(self._combo_style())
  697. # ── QTextEdit ─────────────────────────────────────────────
  698. for w in self.findChildren(QTextEdit):
  699. w.setStyleSheet(self._textedit_style())
  700. # ── Boutons maintenance ───────────────────────────────────
  701. if hasattr(self, 'btn_clear_data'):
  702. self.btn_clear_data.setStyleSheet(
  703. f"QPushButton{{background-color:{colors['btn_neutral']};color:{text};"
  704. f"border-radius:6px;font-weight:bold;}}"
  705. f"QPushButton:hover{{background-color:#942121;color:white;}}"
  706. )
  707. if hasattr(self, 'btn_reset_all'):
  708. self.btn_reset_all.setStyleSheet(
  709. f"QPushButton{{background-color:transparent;color:{text_sec};"
  710. f"border:1px solid #8b0000;border-radius:6px;}}"
  711. f"QPushButton:hover{{background-color:#8b0000;color:white;}}"
  712. )