|
15 | 15 |
|
16 | 16 | from __future__ import annotations |
17 | 17 |
|
| 18 | +import difflib |
18 | 19 | import json |
19 | 20 | import os |
20 | 21 | from pathlib import Path |
| 22 | +from typing import Callable |
21 | 23 |
|
22 | 24 | import yaml |
| 25 | +from PySide6.QtCore import QRegularExpression |
| 26 | +from PySide6.QtGui import QFont, QSyntaxHighlighter, QTextCharFormat, QColor |
23 | 27 | from PySide6.QtWidgets import ( |
24 | 28 | QDialog, |
25 | 29 | QTabWidget, |
@@ -63,6 +67,53 @@ def _safe_parse_json(text: str) -> bool: |
63 | 67 | return False |
64 | 68 |
|
65 | 69 |
|
| 70 | +class _SimpleHighlighter(QSyntaxHighlighter): |
| 71 | + def __init__(self, doc, mode: str = "yaml"): |
| 72 | + super().__init__(doc) |
| 73 | + self.mode = mode |
| 74 | + self.rules: list[tuple[QRegularExpression, QTextCharFormat]] = [] |
| 75 | + |
| 76 | + def _fmt(color: str, bold: bool = False) -> QTextCharFormat: |
| 77 | + fmt = QTextCharFormat() |
| 78 | + fmt.setForeground(QColor(color)) |
| 79 | + if bold: |
| 80 | + fmt.setFontWeight(QFont.Weight.Bold) |
| 81 | + return fmt |
| 82 | + |
| 83 | + key_fmt = _fmt("#9cdcfe", bold=True) |
| 84 | + str_fmt = _fmt("#ce9178") |
| 85 | + num_fmt = _fmt("#b5cea8") |
| 86 | + bool_fmt = _fmt("#569cd6", bold=True) |
| 87 | + sym_fmt = _fmt("#d4d4d4") |
| 88 | + com_fmt = _fmt("#6a9955") |
| 89 | + |
| 90 | + # Strings |
| 91 | + self.rules.append((QRegularExpression(r"'[^']*'"), str_fmt)) |
| 92 | + self.rules.append((QRegularExpression(r'"[^"]*"'), str_fmt)) |
| 93 | + # Numbers |
| 94 | + self.rules.append((QRegularExpression(r"\b\d+(?:\.\d+)?\b"), num_fmt)) |
| 95 | + # Booleans/null |
| 96 | + self.rules.append((QRegularExpression(r"\b(true|false|null|yes|no)\b", QRegularExpression.CaseInsensitiveOption), bool_fmt)) |
| 97 | + # Symbols |
| 98 | + self.rules.append((QRegularExpression(r"[{}\[\]:,]"), sym_fmt)) |
| 99 | + # Comments |
| 100 | + self.rules.append((QRegularExpression(r"#.*$"), com_fmt)) |
| 101 | + |
| 102 | + if mode == "yaml": |
| 103 | + # YAML keys (simple: key:) |
| 104 | + self.rules.append((QRegularExpression(r"^\s*[A-Za-z0-9_\-\.]+(?=\s*:)"), key_fmt)) |
| 105 | + else: |
| 106 | + # JSON keys ("key":) |
| 107 | + self.rules.append((QRegularExpression(r'"[A-Za-z0-9_\-\.]+"(?=\s*:)'), key_fmt)) |
| 108 | + |
| 109 | + def highlightBlock(self, text: str) -> None: |
| 110 | + for pattern, fmt in self.rules: |
| 111 | + it = pattern.globalMatch(text) |
| 112 | + while it.hasNext(): |
| 113 | + match = it.next() |
| 114 | + self.setFormat(match.capturedStart(), match.capturedLength(), fmt) |
| 115 | + |
| 116 | + |
66 | 117 | class AdvancedConfigEditor(QDialog): |
67 | 118 | def __init__(self, gui): |
68 | 119 | super().__init__(gui) |
@@ -91,152 +142,144 @@ def __init__(self, gui): |
91 | 142 | def _workspace_dir(self) -> str | None: |
92 | 143 | return getattr(self.gui, "workspace_dir", None) |
93 | 144 |
|
94 | | - def _setup_tab_ark(self) -> None: |
95 | | - tab = QDialog(self) |
96 | | - lay = QVBoxLayout(tab) |
97 | | - lay.setSpacing(6) |
98 | | - title = QLabel(self.gui.tr("ARK_Main_Config.yml", "ARK_Main_Config.yml")) |
99 | | - lay.addWidget(title) |
100 | | - |
101 | | - self.ark_edit = QPlainTextEdit(tab) |
102 | | - self.ark_edit.setPlaceholderText( |
103 | | - self.gui.tr("Sélectionnez un workspace pour éditer ARK_Main_Config.yml", "Select a workspace to edit ARK_Main_Config.yml") |
| 145 | + def _make_editor(self, parent) -> QPlainTextEdit: |
| 146 | + edit = QPlainTextEdit(parent) |
| 147 | + edit.setFont(QFont("Consolas", 10)) |
| 148 | + edit.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) |
| 149 | + return edit |
| 150 | + |
| 151 | + def _show_diff(self, title: str, before: str, after: str) -> None: |
| 152 | + diff = "\n".join( |
| 153 | + difflib.unified_diff( |
| 154 | + before.splitlines(), |
| 155 | + after.splitlines(), |
| 156 | + fromfile="original", |
| 157 | + tofile="modifié", |
| 158 | + lineterm="", |
| 159 | + ) |
104 | 160 | ) |
105 | | - lay.addWidget(self.ark_edit, 1) |
106 | | - |
107 | | - btns = QHBoxLayout() |
108 | | - btn_reload = QPushButton(self.gui.tr("Recharger", "Reload")) |
109 | | - btn_save = QPushButton(self.gui.tr("Enregistrer", "Save")) |
110 | | - btn_open = QPushButton(self.gui.tr("Ouvrir fichier…", "Open file…")) |
111 | | - btns.addWidget(btn_reload) |
112 | | - btns.addWidget(btn_save) |
113 | | - btns.addStretch(1) |
114 | | - btns.addWidget(btn_open) |
115 | | - lay.addLayout(btns) |
116 | | - |
117 | | - btn_reload.clicked.connect(self._load_ark_config) |
118 | | - btn_save.clicked.connect(self._save_ark_config) |
119 | | - btn_open.clicked.connect(lambda: self._open_any_file(self.ark_edit)) |
120 | | - |
121 | | - self.tabs.addTab(tab, self.gui.tr("ARK Config", "ARK Config")) |
122 | | - self._load_ark_config() |
123 | | - |
124 | | - def _setup_tab_bcasl(self) -> None: |
| 161 | + if not diff.strip(): |
| 162 | + QMessageBox.information(self, title, self.gui.tr("Aucune différence.", "No differences.")) |
| 163 | + return |
| 164 | + dlg = QDialog(self) |
| 165 | + dlg.setWindowTitle(title) |
| 166 | + dlg.resize(900, 600) |
| 167 | + lay = QVBoxLayout(dlg) |
| 168 | + view = QPlainTextEdit(dlg) |
| 169 | + view.setReadOnly(True) |
| 170 | + view.setFont(QFont("Consolas", 10)) |
| 171 | + view.setPlainText(diff) |
| 172 | + lay.addWidget(view) |
| 173 | + btn = QPushButton(self.gui.tr("Fermer", "Close")) |
| 174 | + btn.clicked.connect(dlg.close) |
| 175 | + lay.addWidget(btn) |
| 176 | + dlg.exec() |
| 177 | + |
| 178 | + def _build_tab( |
| 179 | + self, |
| 180 | + label: str, |
| 181 | + path_getter: Callable[[], str | None], |
| 182 | + is_yaml: bool, |
| 183 | + tab_title: str, |
| 184 | + ) -> tuple[QDialog, QPlainTextEdit, QLabel]: |
125 | 185 | tab = QDialog(self) |
126 | 186 | lay = QVBoxLayout(tab) |
127 | 187 | lay.setSpacing(6) |
128 | | - title = QLabel(self.gui.tr("bcasl.yml", "bcasl.yml")) |
129 | | - lay.addWidget(title) |
130 | | - |
131 | | - self.bcasl_edit = QPlainTextEdit(tab) |
132 | | - self.bcasl_edit.setPlaceholderText( |
133 | | - self.gui.tr("Sélectionnez un workspace pour éditer bcasl.yml", "Select a workspace to edit bcasl.yml") |
134 | | - ) |
135 | | - lay.addWidget(self.bcasl_edit, 1) |
136 | 188 |
|
137 | | - btns = QHBoxLayout() |
138 | | - btn_reload = QPushButton(self.gui.tr("Recharger", "Reload")) |
139 | | - btn_save = QPushButton(self.gui.tr("Enregistrer", "Save")) |
140 | | - btn_open = QPushButton(self.gui.tr("Ouvrir fichier…", "Open file…")) |
141 | | - btns.addWidget(btn_reload) |
142 | | - btns.addWidget(btn_save) |
143 | | - btns.addStretch(1) |
144 | | - btns.addWidget(btn_open) |
145 | | - lay.addLayout(btns) |
| 189 | + title = QLabel(label) |
| 190 | + lay.addWidget(title) |
146 | 191 |
|
147 | | - btn_reload.clicked.connect(self._load_bcasl_config) |
148 | | - btn_save.clicked.connect(self._save_bcasl_config) |
149 | | - btn_open.clicked.connect(lambda: self._open_any_file(self.bcasl_edit)) |
| 192 | + path_label = QLabel("") |
| 193 | + path_label.setStyleSheet("color: #888;") |
| 194 | + lay.addWidget(path_label) |
150 | 195 |
|
151 | | - self.tabs.addTab(tab, self.gui.tr("BCASL Config", "BCASL Config")) |
152 | | - self._load_bcasl_config() |
| 196 | + editor = self._make_editor(tab) |
| 197 | + lay.addWidget(editor, 1) |
153 | 198 |
|
154 | | - def _setup_tab_pref(self) -> None: |
155 | | - tab = QDialog(self) |
156 | | - lay = QVBoxLayout(tab) |
157 | | - lay.setSpacing(6) |
158 | | - title = QLabel(self.gui.tr("Préférences Workspace (.ark/pref.json)", "Workspace Preferences (.ark/pref.json)")) |
159 | | - lay.addWidget(title) |
160 | | - |
161 | | - self.pref_edit = QPlainTextEdit(tab) |
162 | | - self.pref_edit.setPlaceholderText( |
163 | | - self.gui.tr("Sélectionnez un workspace pour éditer .ark/pref.json", "Select a workspace to edit .ark/pref.json") |
164 | | - ) |
165 | | - lay.addWidget(self.pref_edit, 1) |
| 199 | + _SimpleHighlighter(editor.document(), "yaml" if is_yaml else "json") |
166 | 200 |
|
167 | 201 | btns = QHBoxLayout() |
168 | 202 | btn_reload = QPushButton(self.gui.tr("Recharger", "Reload")) |
169 | 203 | btn_save = QPushButton(self.gui.tr("Enregistrer", "Save")) |
| 204 | + btn_diff = QPushButton(self.gui.tr("Voir diff", "View diff")) |
170 | 205 | btn_open = QPushButton(self.gui.tr("Ouvrir fichier…", "Open file…")) |
171 | 206 | btns.addWidget(btn_reload) |
172 | 207 | btns.addWidget(btn_save) |
| 208 | + btns.addWidget(btn_diff) |
173 | 209 | btns.addStretch(1) |
174 | 210 | btns.addWidget(btn_open) |
175 | 211 | lay.addLayout(btns) |
176 | 212 |
|
177 | | - btn_reload.clicked.connect(self._load_pref) |
178 | | - btn_save.clicked.connect(self._save_pref) |
179 | | - btn_open.clicked.connect(lambda: self._open_any_file(self.pref_edit)) |
| 213 | + def _load(): |
| 214 | + path = path_getter() |
| 215 | + if not path: |
| 216 | + editor.setPlainText("") |
| 217 | + path_label.setText("") |
| 218 | + return |
| 219 | + path_label.setText(path) |
| 220 | + editor.setPlainText(_read_text(path)) |
| 221 | + |
| 222 | + def _save(): |
| 223 | + path = path_getter() |
| 224 | + if not path: |
| 225 | + return |
| 226 | + text = editor.toPlainText() |
| 227 | + if text.strip(): |
| 228 | + if is_yaml and not _safe_parse_yaml(text): |
| 229 | + QMessageBox.warning(self, self.gui.tr("Erreur", "Error"), self.gui.tr("YAML invalide.", "Invalid YAML.")) |
| 230 | + return |
| 231 | + if not is_yaml and not _safe_parse_json(text): |
| 232 | + QMessageBox.warning(self, self.gui.tr("Erreur", "Error"), self.gui.tr("JSON invalide.", "Invalid JSON.")) |
| 233 | + return |
| 234 | + _write_text(path, text) |
| 235 | + |
| 236 | + def _diff(): |
| 237 | + path = path_getter() |
| 238 | + if not path: |
| 239 | + return |
| 240 | + before = _read_text(path) |
| 241 | + after = editor.toPlainText() |
| 242 | + self._show_diff(self.gui.tr("Diff du fichier", "File diff"), before, after) |
| 243 | + |
| 244 | + def _open_any(): |
| 245 | + path, _ = QFileDialog.getOpenFileName(self, self.gui.tr("Ouvrir un fichier", "Open file"), "", "*.*") |
| 246 | + if not path: |
| 247 | + return |
| 248 | + editor.setPlainText(_read_text(path)) |
| 249 | + path_label.setText(path) |
| 250 | + |
| 251 | + btn_reload.clicked.connect(_load) |
| 252 | + btn_save.clicked.connect(_save) |
| 253 | + btn_diff.clicked.connect(_diff) |
| 254 | + btn_open.clicked.connect(_open_any) |
| 255 | + |
| 256 | + self.tabs.addTab(tab, tab_title) |
| 257 | + _load() |
| 258 | + return tab, editor, path_label |
180 | 259 |
|
181 | | - self.tabs.addTab(tab, self.gui.tr("Workspace Pref", "Workspace Pref")) |
182 | | - self._load_pref() |
183 | | - |
184 | | - def _open_any_file(self, editor: QPlainTextEdit) -> None: |
185 | | - path, _ = QFileDialog.getOpenFileName(self, self.gui.tr("Ouvrir un fichier", "Open file"), "", "*.*") |
186 | | - if not path: |
187 | | - return |
188 | | - editor.setPlainText(_read_text(path)) |
189 | | - |
190 | | - def _load_ark_config(self) -> None: |
191 | | - ws = self._workspace_dir() |
192 | | - if not ws: |
193 | | - return |
194 | | - path = os.path.join(ws, "ARK_Main_Config.yml") |
195 | | - self.ark_edit.setPlainText(_read_text(path)) |
196 | | - |
197 | | - def _save_ark_config(self) -> None: |
198 | | - ws = self._workspace_dir() |
199 | | - if not ws: |
200 | | - return |
201 | | - text = self.ark_edit.toPlainText() |
202 | | - if text.strip() and not _safe_parse_yaml(text): |
203 | | - QMessageBox.warning(self, self.gui.tr("Erreur", "Error"), self.gui.tr("YAML invalide.", "Invalid YAML.")) |
204 | | - return |
205 | | - path = os.path.join(ws, "ARK_Main_Config.yml") |
206 | | - _write_text(path, text) |
207 | | - |
208 | | - def _load_bcasl_config(self) -> None: |
209 | | - ws = self._workspace_dir() |
210 | | - if not ws: |
211 | | - return |
212 | | - path = os.path.join(ws, "bcasl.yml") |
213 | | - self.bcasl_edit.setPlainText(_read_text(path)) |
214 | | - |
215 | | - def _save_bcasl_config(self) -> None: |
| 260 | + def _setup_tab_ark(self) -> None: |
216 | 261 | ws = self._workspace_dir() |
217 | | - if not ws: |
218 | | - return |
219 | | - text = self.bcasl_edit.toPlainText() |
220 | | - if text.strip() and not _safe_parse_yaml(text): |
221 | | - QMessageBox.warning(self, self.gui.tr("Erreur", "Error"), self.gui.tr("YAML invalide.", "Invalid YAML.")) |
222 | | - return |
223 | | - path = os.path.join(ws, "bcasl.yml") |
224 | | - _write_text(path, text) |
| 262 | + self._build_tab( |
| 263 | + self.gui.tr("ARK_Main_Config.yml", "ARK_Main_Config.yml"), |
| 264 | + lambda: os.path.join(ws, "ARK_Main_Config.yml") if ws else None, |
| 265 | + True, |
| 266 | + self.gui.tr("ARK Config", "ARK Config"), |
| 267 | + ) |
225 | 268 |
|
226 | | - def _load_pref(self) -> None: |
| 269 | + def _setup_tab_bcasl(self) -> None: |
227 | 270 | ws = self._workspace_dir() |
228 | | - if not ws: |
229 | | - return |
230 | | - path = os.path.join(ws, ".ark", "pref.json") |
231 | | - self.pref_edit.setPlainText(_read_text(path)) |
| 271 | + self._build_tab( |
| 272 | + self.gui.tr("bcasl.yml", "bcasl.yml"), |
| 273 | + lambda: os.path.join(ws, "bcasl.yml") if ws else None, |
| 274 | + True, |
| 275 | + self.gui.tr("BCASL Config", "BCASL Config"), |
| 276 | + ) |
232 | 277 |
|
233 | | - def _save_pref(self) -> None: |
| 278 | + def _setup_tab_pref(self) -> None: |
234 | 279 | ws = self._workspace_dir() |
235 | | - if not ws: |
236 | | - return |
237 | | - text = self.pref_edit.toPlainText() |
238 | | - if text.strip() and not _safe_parse_json(text): |
239 | | - QMessageBox.warning(self, self.gui.tr("Erreur", "Error"), self.gui.tr("JSON invalide.", "Invalid JSON.")) |
240 | | - return |
241 | | - path = os.path.join(ws, ".ark", "pref.json") |
242 | | - _write_text(path, text) |
| 280 | + self._build_tab( |
| 281 | + self.gui.tr("Préférences Workspace (.ark/pref.json)", "Workspace Preferences (.ark/pref.json)"), |
| 282 | + lambda: os.path.join(ws, ".ark", "pref.json") if ws else None, |
| 283 | + False, |
| 284 | + self.gui.tr("Workspace Pref", "Workspace Pref"), |
| 285 | + ) |
0 commit comments