| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872 |
- from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QScrollArea, QFrame, QLineEdit,
- QSlider, QComboBox, QTextEdit, QCheckBox,
- QSizePolicy)
- from PyQt6.QtCore import Qt, QTimer
- from PyQt6.QtGui import QFont
- from PyQt6.QtWidgets import QMessageBox
- import os
- import shutil
- import tempfile
- from utils.paths import SVG_ICONS
- from core.themes import get_theme
- #from utils.paths import ARROW_DOWN, CIRCLE
- from gui.switch import Switch
- class SettingsViewQt(QWidget):
- def __init__(self, controller):
- super().__init__()
- self.controller = controller
- self.translation_map = {}
-
- # 1. Chargement des traductions
- if hasattr(self.controller, 'translations'):
- self.texts = self.controller.translations["settings"]
- else:
- from core.translations import TRANSLATIONS
- self.texts = TRANSLATIONS["English"]["settings"]
- self.controls = {}
- self._section_frames = [] # Références directes aux frames de section
- self.loading = True
- # Couleurs du thème — initialisées dark, mises à jour par apply_theme
- self._theme_colors = get_theme('Dark')
- # 2. Configuration du Layout Principal
- self.main_layout = QVBoxLayout(self)
- self.main_layout.setContentsMargins(30, 30, 30, 30)
- self.main_layout.setSpacing(20)
- # 3. Construction de l'interface graphique
- self.setup_header() # Titre et bouton Save
- self.setup_scroll_area() # Colonnes et widgets
- # 4. Chargement des données du ConfigManager
- self.load_settings()
-
- # 5. Fin de l'initialisation
- self.loading = False # Autorise maintenant le mark_as_changed
- # ── Helpers de style (lisent _theme_colors) ──────────────────────────
- def _c(self, key, dark_val, light_val):
- # Conservé pour compatibilité — préférer colors['key'] directement
- return dark_val if self._theme_colors.get('suffix', '_DARK') == '_DARK' else light_val
- def _entry_style(self):
- c = self._theme_colors
- return (f"QLineEdit{{background-color:{c['bg_entry']};border:1px solid {c['border_strong']};"
- f"border-radius:4px;color:{c['text']};padding:3px;}}")
- @staticmethod
- def _arrow_svg_path(color: str) -> str:
- """Retourne le chemin vers un SVG de flèche recolorisé (tempfile)."""
- src = SVG_ICONS.get("ARROW_DOWN", "")
- if not src or not os.path.isfile(src):
- return src.replace("\\", "/")
- try:
- with open(src, "r", encoding="utf-8") as f:
- svg = f.read()
- import re
- svg = re.sub(r'fill\s*=\s*"[^"]*"', f'fill="{color}"', svg)
- svg = re.sub(r'fill\s*:\s*[^;}"]+', f'fill:{color}', svg)
- svg = re.sub(r'stroke\s*=\s*"[^"]*"', f'stroke="{color}"', svg)
- svg = re.sub(r'stroke\s*:\s*[^;}"]+', f'stroke:{color}', svg)
- tmp = tempfile.NamedTemporaryFile(
- suffix=f'_arrow_{color.strip("#")}.svg',
- delete=False, mode='w', encoding='utf-8'
- )
- tmp.write(svg)
- tmp.close()
- return tmp.name.replace("\\", "/")
- except Exception:
- return src.replace("\\", "/")
- def _combo_style(self):
- c = self._theme_colors
- arrow_path = self._arrow_svg_path(c['arrow_color'])
- return (
- f"QComboBox{{background-color:{c['bg_entry']};border:1px solid {c['border_strong']};"
- f"border-radius:5px;padding:3px 30px 3px 10px;color:{c['text']};}}"
- f"QComboBox::drop-down{{subcontrol-origin:padding;subcontrol-position:top right;"
- f"width:25px;border:none;background:transparent;}}"
- f"QComboBox::down-arrow{{image:url({arrow_path});width:12px;height:8px;}}"
- f"QComboBox QAbstractItemView{{background-color:{c['bg_entry']};color:{c['text']};"
- f"selection-background-color:{c['combo_selection']};border:1px solid {c['border_strong']};}}"
- )
- def _textedit_style(self):
- c = self._theme_colors
- return (f"QTextEdit{{background-color:{c['bg_entry_alt']};border:1px solid {c['border_strong']};"
- f"border-radius:6px;color:#8fbdf0;padding:5px;}}")
- def _label_row_style(self):
- return f"color:{self._theme_colors['text']};font-size:13px;border:none;background:transparent;"
- def _section_frame_style(self):
- c = self._theme_colors
- return (f"QFrame{{background-color:{c['bg_card_alt']};border:1px solid {c['border_card']};"
- f"border-radius:12px;}}"
- f"QLabel{{border:none;background:transparent;color:{c['text']};}}")
- def setup_header(self):
- """Crée l'en-tête avec titre et bouton Sauvegarder"""
- header_layout = QHBoxLayout()
-
- # Initialisation du registre si absent
- if not hasattr(self, 'translation_map'):
- self.translation_map = {}
- # Titre principal
- self.title_label = QLabel(self.texts.get("title", "Settings"))
- self.title_label.setObjectName("headerTitle") # Identifiant pour le style CSS et thèmes
- self.title_label.setStyleSheet("font-size: 28px; font-weight: bold; color: #1F6AA5;")
-
- # Enregistrement pour la traduction automatique
- self.translation_map[self.title_label] = "title"
-
- # Bouton Sauvegarder
- self.btn_save = QPushButton(self.texts.get("btn_save", "Save"))
- self.btn_save.setFixedSize(150, 40)
- self.btn_save.setCursor(Qt.CursorShape.PointingHandCursor)
- self.btn_save.clicked.connect(self.save_all_settings)
- self.set_button_style("idle")
-
- # Enregistrement pour la traduction automatique
- self.translation_map[self.btn_save] = "btn_save"
-
- # Assemblage du layout
- header_layout.addWidget(self.title_label)
- header_layout.addStretch()
- header_layout.addWidget(self.btn_save)
-
- self.main_layout.addLayout(header_layout)
- def setup_scroll_area(self):
- """Zone défilante contenant les colonnes de réglages"""
- scroll = QScrollArea()
- self._main_scroll = scroll
- scroll.setWidgetResizable(True)
- scroll.setFrameShape(QFrame.Shape.NoFrame)
- scroll.setStyleSheet("background: transparent;")
- container = QWidget()
- self.content_layout = QHBoxLayout(container)
- self.content_layout.setSpacing(30)
- self.content_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
- # Création des deux colonnes (Gauche & Droite)
- self.left_col = QVBoxLayout()
- self.right_col = QVBoxLayout()
-
- self.content_layout.addLayout(self.left_col, 1)
- self.content_layout.addLayout(self.right_col, 1)
- # --- COLONNE GAUCHE (G-Code & Hardware) ---
- self.setup_left_column()
- # --- COLONNE DROITE (Apparence & Maintenance) ---
- self.setup_right_column()
- scroll.setWidget(container)
- self.main_layout.addWidget(scroll)
- def setup_left_column(self):
- # --- SECTION G-CODE ---
- # On passe la CLÉ "sec_gcode" au lieu du texte traduit
- sec_gcode = self.create_section(self.left_col, "sec_gcode")
-
- # Mode de commande (M67 / S)
- self.create_dropdown(sec_gcode, "label_cmd_mode",
- ["M67 (Analog)", "S (Spindle)"], "cmd_mode")
-
- # Entrées simples
- self.create_simple_input(sec_gcode, "label_output_e", "m67_e_num", precision=0)
- self.create_simple_input(sec_gcode, "label_ctrl_max", "ctrl_max", precision=0)
-
- # Mode d'allumage (M3/M4)
- self.create_dropdown(sec_gcode, "label_firing", ["M3/M5", "M4/M5"], "firing_mode")
-
- # Extension de fichier
- self.create_simple_input(sec_gcode, "label_extension", "gcode_extension")
- # --- SECTION HARDWARE ---
- sec_hw = self.create_section(self.left_col, "sec_hardware")
- # Sliders
- self.create_slider_input(sec_hw, "label_latency", -20, 20, 0, "laser_latency")
- self.create_slider_input(sec_hw, "label_overscan", 0, 50, 10, "premove")
- self.create_slider_input(sec_hw, "hor_linestep", 0.01, 0.5, 0.1, "hor_linestep", decimals=4)
- self.create_slider_input(sec_hw, "ver_linestep", 0.01, 0.5, 0.1, "ver_linestep", decimals=4)
- # --- SECTION SCRIPTS ---
- sec_scripts = self.create_section(self.left_col, "sec_scripts")
- self.create_script_box(sec_scripts, "label_header", "custom_header")
- self.create_script_box(sec_scripts, "label_footer", "custom_footer")
- self.left_col.addStretch()
- def setup_right_column(self):
- # --- SECTION APPARENCE ---
- sec_app = self.create_section(self.right_col, "sec_appearance")
-
- # Thème et Langue
- self.create_dropdown(sec_app, "label_theme", ["Dark", "Light", "System"], "theme")
- if hasattr(self.controller, 'all_translations'):
- available_languages = sorted(list(self.controller.all_translations.keys()))
- else:
- # Fallback de sécurité si le controller n'est pas encore prêt
- available_languages = ["English", "Français", "Deutsch"]
-
- self.create_dropdown(sec_app, "label_lang", available_languages, "language")
-
- # Switch Vignettes
- self.create_switch(sec_app, "enable_thumbnails", "enable_thumbnails")
- # --- SECTION MAINTENANCE ---
- sec_maint = self.create_section(self.right_col, "maintenance_data")
-
- # Bouton Effacer Vignettes
- self.btn_clear_data = QPushButton(self.texts["erase_thumbnails"])
- self.translation_map[self.btn_clear_data] = "erase_thumbnails"
- self.btn_clear_data.setFixedHeight(35)
- self.btn_clear_data.setCursor(Qt.CursorShape.PointingHandCursor)
- self.btn_clear_data.setStyleSheet("""
- QPushButton { background-color: #444; color: white; border-radius: 6px; font-weight: bold; }
- QPushButton:hover { background-color: #942121; }
- """)
- self.btn_clear_data.clicked.connect(self.clear_thumbnails_and_stats)
- sec_maint.addWidget(self.btn_clear_data)
- # Bouton Reset Paramètres
- self.btn_reset_all = QPushButton(self.texts["reset_all_parameters"])
- self.translation_map[self.btn_reset_all] = "reset_all_parameters"
- self.btn_reset_all.setFixedHeight(35)
- self.btn_reset_all.setCursor(Qt.CursorShape.PointingHandCursor)
- self.btn_reset_all.setStyleSheet("""
- QPushButton { background-color: transparent; color: #ddd; border: 1px solid #8b0000; border-radius: 6px; }
- QPushButton:hover { background-color: #8b0000; color: white; }
- """)
- self.btn_reset_all.clicked.connect(self.reset_settings)
- sec_maint.addWidget(self.btn_reset_all)
- self.right_col.addStretch()
- # --- HELPERS DE CONSTRUCTION ---
- def create_section(self, layout, title_key):
- """Crée un bloc section et enregistre le label pour la traduction centralisée"""
- section_frame = QFrame()
- section_frame.setObjectName("sectionFrame")
- section_frame.setStyleSheet(self._section_frame_style())
- self._section_frames.append(section_frame)
- sec_layout = QVBoxLayout(section_frame)
- sec_layout.setContentsMargins(15, 15, 15, 15)
- sec_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
-
- # --- TRADUCTION INITIALE ---
- # On récupère le texte, si la clé n'existe pas, on affiche la clé
- title_text = self.texts.get(title_key, title_key).upper()
-
- title_lbl = QLabel(title_text)
- title_lbl.setStyleSheet("color: #3a9ad9; font-weight: bold; font-size: 12px; border: none;")
- sec_layout.addWidget(title_lbl)
-
- # --- ENREGISTREMENT CENTRALISÉ ---
- if not hasattr(self, 'translation_map'):
- self.translation_map = {}
-
- # On lie l'objet Label à sa clé pour update_texts()
- self.translation_map[title_lbl] = title_key
-
- layout.addWidget(section_frame)
- return sec_layout
- def create_input_row(self, layout, label_key, widget, key=None):
- row = QWidget()
- row.setStyleSheet("border: none; background: transparent;")
- row_layout = QHBoxLayout(row)
- row_layout.setContentsMargins(0, 5, 0, 5)
-
- # Initialisation de la map si elle n'existe pas
- if not hasattr(self, 'translation_map'):
- self.translation_map = {}
- # --- LOGIQUE DE TRADUCTION AMÉLIORÉE ---
- if key:
- # 1. On vérifie si label_key est DIRECTEMENT une clé de traduction (ex: "enable_thumbnails")
- if label_key in self.texts:
- translation_key = label_key
-
- # 2. Sinon, est-ce qu'elle a déjà un préfixe connu ?
- elif label_key.startswith("label_") or label_key.startswith("sec_"):
- translation_key = label_key
-
- # 3. Sinon, on tente d'ajouter "label_" par défaut (ex: "extension" -> "label_extension")
- else:
- translation_key = f"label_{label_key}"
-
- # On récupère le texte final
- display_text = self.texts.get(translation_key, translation_key)
- lbl = QLabel(display_text)
-
- # On enregistre l'objet QLabel avec sa clé trouvée
- self.translation_map[lbl] = translation_key
- else:
- # Cas sans clé technique (juste du texte statique)
- lbl = QLabel(label_key)
- lbl.setStyleSheet(self._label_row_style())
-
- row_layout.addWidget(lbl)
- row_layout.addStretch()
- row_layout.addWidget(widget)
- layout.addWidget(row)
- def create_simple_input(self, layout, label_text, key, precision=2):
- """Crée une ligne avec un Label et un QLineEdit sans valeur par défaut externe"""
- edit = QLineEdit()
- edit.setFixedWidth(100)
- edit.setStyleSheet(self._entry_style())
- # On initialise à 0 par défaut, formaté selon la précision
- initial_value = 0
- fmt = "{:d}" if precision == 0 else f"{{:.{precision}f}}"
- edit.setText(fmt.format(initial_value))
-
- edit.textChanged.connect(self.mark_as_changed)
-
- # On ajoute la ligne au layout via votre méthode parente
- self.create_input_row(layout, label_text, edit, key=key)
-
- # On stocke les infos dans le dictionnaire de contrôle
- self.controls[key] = {
- "entry": edit,
- "precision": precision,
- "is_int": (precision == 0)
- }
- # --- LOGIQUE DE STYLE DU BOUTON ---
- def set_button_style(self, state):
- if state == "idle":
- style = "background-color: #1F6AA5; color: white; border-radius: 8px; font-weight: bold;"
- elif state == "saved":
- style = "background-color: #2d5a27; color: white; border-radius: 8px; font-weight: bold;"
- elif state == "changed":
- style = "background-color: #3B8ED0; color: white; border-radius: 8px; font-weight: bold; border: 2px solid white;"
-
- self.btn_save.setStyleSheet(style)
- def mark_as_changed(self):
- if not self.loading:
- self.set_button_style("changed")
- # --- LOGIQUE DE SAUVEGARDE ---
- def save_all_settings(self):
- """Récupère toutes les valeurs de l'interface et les sauvegarde"""
- try:
- def get_float(key):
- try:
- return float(self.controls[key]["entry"].text().replace(',', '.'))
- except:
- return 0.0
- new_settings = {
- "theme": self.controls["theme"]["combo"].currentText(),
- "language": self.controls["language"]["combo"].currentText(),
- "enable_thumbnails": self.controls["enable_thumbnails"]["check"].isChecked(),
- "cmd_mode": self.controls["cmd_mode"]["combo"].currentText(),
- "firing_mode": self.controls["firing_mode"]["combo"].currentText(),
- "m67_e_num": int(float(self.controls["m67_e_num"]["entry"].text() or "0")),
- "ctrl_max": int(float(self.controls["ctrl_max"]["entry"].text() or "0")),
- "gcode_extension": self.controls["gcode_extension"]["entry"].text(),
- "laser_latency": get_float("laser_latency"),
- "premove": get_float("premove"),
- "hor_linestep": get_float("hor_linestep"),
- "ver_linestep": get_float("ver_linestep"),
- "custom_header": self.controls["custom_header"]["text"].toPlainText(),
- "custom_footer": self.controls["custom_footer"]["text"].toPlainText()
- }
- self.controller.set_section("machine_settings", new_settings)
-
- if self.controller.save():
- from core.translations import TRANSLATIONS
- new_lang = new_settings["language"]
- print(f"[SAVE] langue sauvée: {new_lang}")
- self.texts = TRANSLATIONS.get(new_lang, TRANSLATIONS["English"])["settings"]
- self.update_texts()
- self.set_button_style("saved")
- self.btn_save.setText(f"✓ {self.texts.get('btn_save', 'Save')}")
- main_window = (getattr(self, '_main_window', None)
- or getattr(self.controller, '_main_window', None)
- or self.window())
- print(f"[SAVE] main_window trouvé: {main_window}")
- print(f"[SAVE] has update_ui_language: {hasattr(main_window, 'update_ui_language')}")
- if main_window and hasattr(main_window, 'update_ui_language'):
- main_window.update_ui_language()
- if main_window and hasattr(main_window, 'update_ui_theme'):
- main_window.update_ui_theme()
- QTimer.singleShot(2000, self.reset_save_btn)
- else:
- raise Exception("Erreur d'écriture disque")
- except Exception as e:
- import traceback
- print(f"[SAVE] ERREUR: {e}")
- traceback.print_exc()
- self.btn_save.setStyleSheet("background-color: #942121; color: white; border-radius: 8px;")
- def _apply_language(self, lang: str, translations: dict):
- """Reçoit lang et translations directement — pas de relecture config."""
- from core.translations import TRANSLATIONS as _TR
- self.texts = _TR.get(lang, _TR["English"])["settings"]
- from gui.utils_qt import translate_ui_widgets
- translate_ui_widgets(self.translation_map, self.texts)
- def update_texts(self):
- """Met à jour dynamiquement tous les labels de la vue settings"""
- # 1. Recharger les textes
- lang = self.controller.get_item("machine_settings", "language", "English")
- from core.translations import TRANSLATIONS
- self.texts = TRANSLATIONS.get(lang, TRANSLATIONS["English"])["settings"]
-
- # 2. Traduire TOUT d'un coup (Titre, bouton save, sections, labels)
- from gui.utils_qt import translate_ui_widgets
- translate_ui_widgets(self.translation_map, self.texts)
-
- # 3. Optionnel : Si vous avez d'autres boutons fixes (Reset, Clear data)
- # n'oubliez pas de les ajouter à la map dans leur setup respectif !
- def reset_save_btn(self):
- self.set_button_style("idle")
- self.btn_save.setText(self.texts["btn_save"])
- def create_slider_input(self, layout, label_text, min_v, max_v, default, key, decimals=2):
- """
- Crée une rangée avec Label + Slider + Entry.
- decimals : nombre de chiffres après la virgule (0 pour des entiers).
- """
- # 1. Calcul du facteur d'échelle pour le slider
- # 10^decimals (ex: 0 -> 1, 1 -> 10, 2 -> 100)
- multiplier = 10 ** decimals
-
- right_side_widget = QWidget()
- right_side_widget.setFixedWidth(180)
-
- right_side_layout = QHBoxLayout(right_side_widget)
- right_side_layout.setContentsMargins(0, 0, 0, 0)
- right_side_layout.setSpacing(10)
- # 2. Le Slider
- slider = QSlider(Qt.Orientation.Horizontal)
- slider.setRange(int(min_v * multiplier), int(max_v * multiplier))
- slider.setValue(int(default * multiplier))
- slider.setFixedWidth(120)
-
- # 3. Le champ de saisie (Entry)
- # Formatage initial selon decimals
- initial_val = int(default) if decimals == 0 else default
- entry = QLineEdit(str(initial_val))
- entry.setFixedWidth(50)
- entry.setAlignment(Qt.AlignmentFlag.AlignCenter)
- entry.setStyleSheet(self._entry_style())
- # --- GESTION DES CONNEXIONS ---
-
- def on_slider_moved(v):
- val = v / multiplier
- # Formatage dynamique : entier si 0, sinon float
- text = str(int(val)) if decimals == 0 else f"{val:.{decimals}f}"
- entry.setText(text)
- self.mark_as_changed()
- def on_entry_finished():
- try:
- raw_text = entry.text().replace(',', '.')
- val = float(raw_text) if raw_text else 0.0
- slider.setValue(int(val * multiplier))
- self.mark_as_changed()
- except ValueError:
- # Optionnel : remettre la valeur du slider si la saisie est invalide
- on_slider_moved(slider.value())
- slider.valueChanged.connect(on_slider_moved)
- entry.editingFinished.connect(on_entry_finished)
- right_side_layout.addWidget(slider)
- right_side_layout.addWidget(entry)
- # 4. Intégration dans la ligne
- self.create_input_row(layout, label_text, right_side_widget, key=key)
-
- # Stockage
- self.controls[key] = {"slider": slider, "entry": entry, "decimals": decimals}
- def create_script_box(self, layout, label_key, key):
- """Crée un bloc de texte multi-lignes pour les scripts G-Code avec traduction"""
- container = QWidget()
- v_box = QVBoxLayout(container)
- v_box.setContentsMargins(0, 10, 0, 5)
- v_box.setSpacing(5)
-
- # --- TRADUCTION INITIALE ---
- # On va chercher la traduction de la clé (ex: "label_header")
- display_text = self.texts.get(label_key, label_key)
- lbl = QLabel(display_text)
- lbl.setStyleSheet(f"color:{self._theme_colors.get('text_secondary','#bbbbbb')};"
- "font-size:11px;font-weight:bold;border:none;background:transparent;")
- lbl.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
-
- # --- ENREGISTREMENT POUR LA TRADUCTION DYNAMIQUE ---
- if not hasattr(self, 'translation_map'):
- self.translation_map = {}
- self.translation_map[lbl] = label_key
-
- # Zone de texte
- text_edit = QTextEdit()
- text_edit.setFixedHeight(80)
- text_edit.setFont(QFont("Consolas", 10))
- text_edit.setStyleSheet(self._textedit_style())
- text_edit.textChanged.connect(self.mark_as_changed)
-
- v_box.addWidget(lbl)
- v_box.addWidget(text_edit)
-
- layout.addWidget(container, alignment=Qt.AlignmentFlag.AlignTop)
-
- # Stockage pour la sauvegarde (attention à utiliser la même structure que les autres)
- self.controls[key] = {"text": text_edit}
- def create_dropdown(self, layout, label_text, options, key):
- """Crée un menu déroulant et délègue la gestion du label à create_input_row"""
- combo = QComboBox()
- combo.addItems(options)
- combo.setFixedWidth(150)
-
- combo.setStyleSheet(self._combo_style())
- combo.currentIndexChanged.connect(self.mark_as_changed)
- self.create_input_row(layout, label_text, combo, key=key)
-
- # Stockage pour la récupération des données
- self.controls[key] = {"combo": combo}
- def create_switch(self, layout, label_key, key):
- """Crée une ligne avec un Label et un Switch"""
- check = Switch()
-
- #check.toggled.connect(lambda v: print(f"Switch {key}:", v))
- check.stateChanged.connect(self.mark_as_changed)
- self.create_input_row(layout, label_key, check, key=key)
-
- self.controls[key] = {"check": check}
- def ask_confirmation(self, title, message):
- """Affiche une boîte de dialogue de confirmation stylisée"""
- msg_box = QMessageBox(self)
- msg_box.setWindowTitle(title)
- msg_box.setText(message)
- msg_box.setIcon(QMessageBox.Icon.Question)
-
- # Boutons personnalisés
- yes_button = msg_box.addButton(self.texts.get("confirm_yes", "Confirmer"), QMessageBox.ButtonRole.YesRole)
- no_button = msg_box.addButton(self.texts.get("confirm_no", "Annuler"), QMessageBox.ButtonRole.NoRole)
-
- msg_box.setDefaultButton(no_button)
-
- # Style Dark pour la boîte
- msg_box.setStyleSheet("""
- QMessageBox { background-color: #2b2b2b; }
- QLabel { color: white; font-size: 13px; }
- QPushButton {
- background-color: #444; color: white; border-radius: 4px;
- padding: 5px 15px; min-width: 80px;
- }
- QPushButton:hover { background-color: #1F6AA5; }
- """)
-
- msg_box.exec()
-
- return msg_box.clickedButton() == yes_button
- def load_settings(self):
- self.loading = True
- data = self.controller.get_section("machine_settings")
- # --- LOGIQUE SÉCURISÉE ---
-
- # 1. ComboBox
- for key in ["theme", "language", "cmd_mode", "firing_mode"]:
- if key in self.controls:
- val = data.get(key, "") # Si None, on prend une chaîne vide
- index = self.controls[key]["combo"].findText(str(val))
- if index >= 0:
- self.controls[key]["combo"].setCurrentIndex(index)
- # 2. LineEdits
- for key in ["m67_e_num", "ctrl_max"]:
- if key in self.controls:
- raw = data.get(key, 0)
- try:
- val = int(float(str(raw))) # gère "0", 0, 0.0, "1000.0"
- except (ValueError, TypeError):
- val = 0
- self.controls[key]["entry"].setText(str(val))
- for key in ["gcode_extension"]:
- if key in self.controls:
- self.controls[key]["entry"].setText(str(data.get(key, "")))
- # 3. Sliders
- for key in ["laser_latency", "premove"]:
- if key in self.controls:
- # On force une valeur de secours (0.0) si le manager renvoie None
- raw_val = data.get(key)
- val = float(raw_val) if raw_val is not None else 0.0
-
- self.controls[key]["slider"].setValue(int(val * 100))
- self.controls[key]["entry"].setText(f"{val:.2f}")
- for key in ["hor_linestep", "ver_linestep"]:
- if key in self.controls:
- raw_val = data.get(key)
- val = float(raw_val) if raw_val is not None else 0.1
- self.controls[key]["slider"].setValue(int(val * 10000))
- self.controls[key]["entry"].setText(f"{val:.4f}")
- # 4. Scripts & Switches
- if "custom_header" in self.controls:
- self.controls["custom_header"]["text"].setPlainText(data.get("custom_header", ""))
- if "custom_footer" in self.controls:
- self.controls["custom_footer"]["text"].setPlainText(data.get("custom_footer", ""))
- if "enable_thumbnails" in self.controls:
- self.controls["enable_thumbnails"]["check"].setChecked(bool(data.get("enable_thumbnails", True)))
- self.loading = False
- self.set_button_style("idle")
- def showEvent(self, event):
- """Recharge les valeurs depuis la config à chaque fois que la vue devient visible.
- Indispensable pour refléter les sauvegardes faites depuis d'autres vues (ex: calibration)."""
- super().showEvent(event)
- self.load_settings()
- def clear_thumbnails_and_stats(self):
- """Supprime les vignettes physiques et réinitialise les statistiques dans le JSON"""
-
- # 1. Demande de confirmation à l'utilisateur
- if not self.ask_confirmation(self.texts["erase_thumbnails"], self.texts["erase_thumbnails_confirm"]):
- return
- # 2. NETTOYAGE DES FICHIERS (Vignettes)
- # On définit le chemin vers le dossier des miniatures
- target_dir = os.path.join(os.getcwd(), "assets", "thumbnails")
- files_purged = 0
-
- if os.path.exists(target_dir):
- try:
- for filename in os.listdir(target_dir):
- # On préserve le fichier .gitkeep s'il existe
- if filename == ".gitkeep":
- continue
-
- file_path = os.path.join(target_dir, filename)
-
- try:
- if os.path.isfile(file_path) or os.path.islink(file_path):
- os.unlink(file_path) # Suppression du fichier
- elif os.path.isdir(file_path):
- shutil.rmtree(file_path) # Suppression du dossier si présent
- files_purged += 1
- except Exception as e:
- print(f"Impossible de supprimer {filename}: {e}")
- except Exception as e:
- print(f"Erreur d'accès au dossier: {e}")
- # 3. NETTOYAGE DES STATISTIQUES (Dans le ConfigManager)
- try:
- # On définit les valeurs de remise à zéro
- empty_stats = {
- "total_lines": 0,
- "total_gcodes": 0,
- "total_time_seconds": 0.0,
- "last_project_time": 0.0
- }
-
- # Mise à jour de la section via le controller
- self.controller.set_section("stats", empty_stats)
-
- # Sauvegarde immédiate du fichier JSON
- if self.controller.save():
- # Feedback visuel : Succès
- self.btn_clear_data.setText("✓ " + self.texts["erase_thumbnails_done"])
- self.btn_clear_data.setStyleSheet("""
- QPushButton {
- background-color: #2d5a27;
- color: white;
- border-radius: 6px;
- font-weight: bold;
- }
- """)
- else:
- raise Exception("Erreur de sauvegarde JSON")
- except Exception as e:
- print(f"Erreur lors de la remise à zéro des stats: {e}")
- self.btn_clear_data.setText("Erreur")
- self.btn_clear_data.setStyleSheet("background-color: #942121; color: white;")
- # 4. Retour à l'état initial du bouton après 2 secondes
- # On utilise un QTimer pour ne pas bloquer l'interface
- QTimer.singleShot(2000, self.restore_maintenance_buttons)
- def restore_maintenance_buttons(self):
- """Restitue le style original du bouton de maintenance"""
- self.btn_clear_data.setText(self.texts["erase_thumbnails"])
- self.btn_clear_data.setStyleSheet("""
- QPushButton {
- background-color: #444;
- color: white;
- border-radius: 6px;
- font-weight: bold;
- }
- QPushButton:hover { background-color: #942121; }
- """)
- def reset_settings(self):
- if not self.ask_confirmation(
- self.texts["reset_all_parameters"],
- self.texts["reset_all_parameters_confirm"]):
- return
- # 1. Réinitialiser les paramètres machine
- if self.controller.reset_all():
- self.load_settings()
- # 2. Effacer les vignettes et stats (sans nouvelle confirmation)
- self._clear_thumbnails_silent()
- # 3. Réactiver l'onboarding au prochain lancement
- current = self.controller.get_section("app_state") or {}
- current["onboarding_done"] = False
- self.controller.set_section("app_state", current)
- self.controller.save()
- # 4. Relancer l'onboarding sur le dashboard
- main_win = self.window()
- if main_win and hasattr(main_win, "dashboard_view"):
- dv = main_win.dashboard_view
- if hasattr(dv, "restart_onboarding"):
- dv.restart_onboarding()
- main_win.show_dashboard()
- def _clear_thumbnails_silent(self):
- """Efface les vignettes et stats sans confirmation ni feedback visuel."""
- import shutil, os
- target_dir = os.path.join(os.getcwd(), "assets", "thumbnails")
- if os.path.exists(target_dir):
- for filename in os.listdir(target_dir):
- if filename == ".gitkeep":
- continue
- fp = os.path.join(target_dir, filename)
- try:
- if os.path.isfile(fp) or os.path.islink(fp):
- os.unlink(fp)
- elif os.path.isdir(fp):
- shutil.rmtree(fp)
- except Exception as e:
- print(f"Impossible de supprimer {filename}: {e}")
- empty_stats = {
- "total_lines": 0, "total_gcodes": 0,
- "total_time_seconds": 0.0, "last_project_time": 0.0
- }
- self.controller.set_section("stats", empty_stats)
- self.controller.save()
- def apply_theme(self, colors):
- """Met à jour dynamiquement les couleurs de la vue Settings"""
- self._theme_colors = colors
- text = colors['text']
- text_sec = colors['text_secondary']
- # ── Vue racine ────────────────────────────────────────────
- self.setStyleSheet("background-color: transparent;")
- # ── Scrollbar principale ─────────────────────────────────
- if hasattr(self, "_main_scroll"):
- self._main_scroll.verticalScrollBar().setStyleSheet(f"""
- QScrollBar:vertical {{
- border: none;
- background: {colors['scrollbar_bg']};
- width: 10px;
- margin: 0px;
- }}
- QScrollBar::handle:vertical {{
- background: {colors['scrollbar_handle']};
- min-height: 20px;
- border-radius: 5px;
- }}
- QScrollBar::handle:vertical:hover {{
- background: #1F6AA5;
- }}
- QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
- height: 0px;
- }}
- QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{
- background: none;
- }}
- """)
- # ── Sections ─────────────────────────────────────────────
- for frame in self._section_frames:
- frame.setStyleSheet(self._section_frame_style())
- # ── Labels ───────────────────────────────────────────────
- for lbl in self.findChildren(QLabel):
- if lbl.objectName() == "headerTitle":
- continue
- if lbl.styleSheet() and '#3a9ad9' in lbl.styleSheet():
- continue
- if lbl.styleSheet() and 'font-weight:bold' in lbl.styleSheet().replace(' ', ''):
- lbl.setStyleSheet(
- f"color:{text_sec};font-size:11px;font-weight:bold;"
- "border:none;background:transparent;"
- )
- else:
- lbl.setStyleSheet(self._label_row_style())
- # ── QLineEdit ─────────────────────────────────────────────
- for w in self.findChildren(QLineEdit):
- w.setStyleSheet(self._entry_style())
- # ── QComboBox ─────────────────────────────────────────────
- for w in self.findChildren(QComboBox):
- w.setStyleSheet(self._combo_style())
- # ── QTextEdit ─────────────────────────────────────────────
- for w in self.findChildren(QTextEdit):
- w.setStyleSheet(self._textedit_style())
- # ── Boutons maintenance ───────────────────────────────────
- if hasattr(self, 'btn_clear_data'):
- self.btn_clear_data.setStyleSheet(
- f"QPushButton{{background-color:{colors['btn_neutral']};color:{text};"
- f"border-radius:6px;font-weight:bold;}}"
- f"QPushButton:hover{{background-color:#942121;color:white;}}"
- )
- if hasattr(self, 'btn_reset_all'):
- self.btn_reset_all.setStyleSheet(
- f"QPushButton{{background-color:transparent;color:{text_sec};"
- f"border:1px solid #8b0000;border-radius:6px;}}"
- f"QPushButton:hover{{background-color:#8b0000;color:white;}}"
- )
|