Skip to content

Commit b35e4da

Browse files
committed
feat: ajout de la gestion des verrous (Locks) en GUI et correction d'un bug dans le workspace
- Implémentation de LockDialog pour lister et utiliser les fichiers .lock.yml. - Ajout de la méthode rebuild_from_lock pour une reconstruction fidèle. - Intégration du bouton de gestion des verrous dans les variantes IDE-like et classique. - Correction d'une erreur UnboundLocalError lors du chargement d'un workspace (added_count).
1 parent abe4c5c commit b35e4da

6 files changed

Lines changed: 296 additions & 2 deletions

File tree

Ui/Gui/Dialogs/CompilerDialog.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,104 @@ def _after_bcasl(_report=None) -> None:
394394
run_bcasl_before_compile(self, _after_bcasl)
395395

396396

397+
def rebuild_from_lock(self, lock_path: Path) -> None:
398+
"""Rebuild the project using a specific lock file."""
399+
try:
400+
from Core.Locking import (
401+
build_context_from_lock,
402+
load_yaml_file,
403+
)
404+
405+
log_i18n_level(
406+
self,
407+
"info",
408+
f"🔒 Chargement du verrou: {lock_path.name}",
409+
f"🔒 Loading lock file: {lock_path.name}",
410+
)
411+
lock_payload = load_yaml_file(lock_path)
412+
engine_id = str(((lock_payload.get("engine") or {}).get("name")) or "").strip()
413+
414+
if not engine_id:
415+
raise ValueError("Invalid lock file: missing engine name")
416+
417+
# Context from lock
418+
context = build_context_from_lock(lock_payload)
419+
420+
# Engine config from lock
421+
engine_config = ((lock_payload.get("engine") or {}).get("config")) or {}
422+
engine_config = dict(engine_config) if isinstance(engine_config, dict) else {}
423+
424+
# Retrieve engine instance
425+
engine = None
426+
try:
427+
import Core.engine as engines_loader
428+
engine = engines_loader.registry.get_instance(engine_id)
429+
except Exception:
430+
engine = None
431+
432+
if engine is None:
433+
try:
434+
engine = create(engine_id)
435+
except Exception as e:
436+
log_i18n_level(
437+
self,
438+
"error",
439+
f"Erreur création moteur '{engine_id}': {e}",
440+
f"Engine creation error '{engine_id}': {e}",
441+
)
442+
return
443+
444+
self.set_controls_enabled(False)
445+
_set_progress_indeterminate(self)
446+
447+
log_i18n_level(
448+
self,
449+
"info",
450+
f"Démarrage de la reconstruction avec {engine.name}...",
451+
f"Starting rebuild with {engine.name}...",
452+
)
453+
454+
main_process = get_main_process()
455+
if not hasattr(main_process, "_gui_connected"):
456+
main_process.output_ready.connect(lambda msg: _handle_output(self, msg))
457+
main_process.error_ready.connect(lambda msg: _handle_error(self, msg))
458+
main_process.progress_update.connect(
459+
lambda pct, msg: _handle_progress(self, pct, msg)
460+
)
461+
main_process.log_message.connect(
462+
lambda level, msg: _handle_log(self, level, msg)
463+
)
464+
main_process.compilation_started.connect(
465+
lambda info: _handle_compilation_started(self, info)
466+
)
467+
main_process.compilation_finished.connect(
468+
lambda code, info: handle_finished(self, code, info)
469+
)
470+
main_process.state_changed.connect(
471+
lambda state: _handle_state_changed(self, state)
472+
)
473+
main_process._gui_connected = True
474+
475+
success = main_process.compile_from_context(
476+
workspace=self.workspace_dir,
477+
engine_id=engine_id,
478+
context=context,
479+
engine_config=engine_config,
480+
)
481+
482+
if not success:
483+
self.set_controls_enabled(True)
484+
485+
except Exception as e:
486+
log_i18n_level(
487+
self,
488+
"error",
489+
f"Erreur lors de la reconstruction: {e}",
490+
f"Rebuild error: {e}",
491+
)
492+
self.set_controls_enabled(True)
493+
494+
397495
def start_compilation_process(self, engine_id: str, file_path: str) -> bool:
398496
"""Start a single compilation process using MainProcess and EngineRunner with build locking."""
399497
try:

Ui/Gui/Dialogs/LockDialog.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 Ague Samuel Amen
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""
17+
LockDialog — Dialog pour gérer et reconstruire à partir de fichiers lock.
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import os
23+
from pathlib import Path
24+
from typing import Any
25+
26+
import yaml
27+
from PySide6.QtCore import Qt
28+
from PySide6.QtGui import QFont
29+
from PySide6.QtWidgets import (
30+
QDialog,
31+
QHBoxLayout,
32+
QLabel,
33+
QListWidget,
34+
QListWidgetItem,
35+
QMessageBox,
36+
QPlainTextEdit,
37+
QPushButton,
38+
QSplitter,
39+
QVBoxLayout,
40+
QWidget,
41+
)
42+
43+
from Core.Locking import load_yaml_file
44+
45+
46+
class LockDialog(QDialog):
47+
"""Dialog pour lister les fichiers .lock.yml et relancer un build."""
48+
49+
def __init__(self, gui):
50+
super().__init__(gui)
51+
self.gui = gui
52+
self.setWindowTitle(gui.tr("Gestion des Verrous (Locks)", "Lock Management"))
53+
self.resize(900, 600)
54+
55+
layout = QVBoxLayout(self)
56+
57+
ws = getattr(self.gui, "workspace_dir", None)
58+
ws_label = QLabel(gui.tr(f"Workspace: {ws}", f"Workspace: {ws}"))
59+
ws_label.setStyleSheet("color: #888;")
60+
layout.addWidget(ws_label)
61+
62+
splitter = QSplitter(Qt.Orientation.Horizontal)
63+
64+
# Liste des verrous
65+
self.list_widget = QListWidget()
66+
self.list_widget.itemSelectionChanged.connect(self._on_selection_changed)
67+
splitter.addWidget(self.list_widget)
68+
69+
# Détails du verrou
70+
self.details_view = QPlainTextEdit()
71+
self.details_view.setReadOnly(True)
72+
self.details_view.setFont(QFont("Consolas", 10))
73+
splitter.addWidget(self.details_view)
74+
75+
splitter.setSizes([300, 600])
76+
layout.addWidget(splitter, 1)
77+
78+
# Actions
79+
btn_layout = QHBoxLayout()
80+
self.btn_rebuild = QPushButton(gui.tr("Reconstruire (Rebuild)", "Rebuild"))
81+
self.btn_rebuild.setEnabled(False)
82+
self.btn_rebuild.clicked.connect(self._do_rebuild)
83+
84+
self.btn_open_dir = QPushButton(gui.tr("Ouvrir dossier", "Open Directory"))
85+
self.btn_open_dir.clicked.connect(self._open_lock_dir)
86+
87+
btn_close = QPushButton(gui.tr("Fermer", "Close"))
88+
btn_close.clicked.connect(self.close)
89+
90+
btn_layout.addWidget(self.btn_rebuild)
91+
btn_layout.addWidget(self.btn_open_dir)
92+
btn_layout.addStretch(1)
93+
btn_layout.addWidget(btn_close)
94+
layout.addLayout(btn_layout)
95+
96+
self._refresh_list()
97+
98+
def _get_lock_dir(self) -> Path | None:
99+
ws = getattr(self.gui, "workspace_dir", None)
100+
if not ws:
101+
return None
102+
return Path(ws) / ".ark" / "lock"
103+
104+
def _refresh_list(self):
105+
self.list_widget.clear()
106+
lock_dir = self._get_lock_dir()
107+
if not lock_dir or not lock_dir.exists():
108+
return
109+
110+
locks = sorted(lock_dir.glob("*.lock.yml"), key=lambda p: p.stat().st_mtime, reverse=True)
111+
for path in locks:
112+
item = QListWidgetItem(path.name)
113+
item.setData(Qt.ItemDataRole.UserRole, str(path))
114+
self.list_widget.addItem(item)
115+
116+
def _on_selection_changed(self):
117+
items = self.list_widget.selectedItems()
118+
if not items:
119+
self.details_view.clear()
120+
self.btn_rebuild.setEnabled(False)
121+
return
122+
123+
path = items[0].data(Qt.ItemDataRole.UserRole)
124+
try:
125+
data = load_yaml_file(Path(path))
126+
# Formatage simplifié pour l'affichage
127+
display = yaml.safe_dump(data, allow_unicode=True, sort_keys=False)
128+
self.details_view.setPlainText(display)
129+
self.btn_rebuild.setEnabled(True)
130+
except Exception as e:
131+
self.details_view.setPlainText(f"Error loading lock: {e}")
132+
self.btn_rebuild.setEnabled(False)
133+
134+
def _do_rebuild(self):
135+
items = self.list_widget.selectedItems()
136+
if not items:
137+
return
138+
139+
path = items[0].data(Qt.ItemDataRole.UserRole)
140+
answer = QMessageBox.question(
141+
self,
142+
self.gui.tr("Confirmer Rebuild", "Confirm Rebuild"),
143+
self.gui.tr(
144+
"Voulez-vous reconstruire le projet à partir de ce verrou ?",
145+
"Do you want to rebuild the project using this lock file?"
146+
)
147+
)
148+
if answer != QMessageBox.StandardButton.Yes:
149+
return
150+
151+
self.close()
152+
# On délègue le rebuild à la fenêtre principale
153+
if hasattr(self.gui, "rebuild_from_lock"):
154+
self.gui.rebuild_from_lock(Path(path))
155+
else:
156+
QMessageBox.critical(self, "Error", "rebuild_from_lock method not implemented in GUI")
157+
158+
def _open_lock_dir(self):
159+
lock_dir = self._get_lock_dir()
160+
if not lock_dir or not lock_dir.exists():
161+
return
162+
163+
import platform
164+
import subprocess
165+
system = platform.system()
166+
if system == "Windows":
167+
os.startfile(str(lock_dir))
168+
elif system == "Darwin":
169+
subprocess.run(["open", str(lock_dir)])
170+
else:
171+
subprocess.run(["xdg-open", str(lock_dir)])
172+
173+
174+
def open_lock_dialog(gui):
175+
"""Helper pour ouvrir le dialog."""
176+
dlg = LockDialog(gui)
177+
dlg.exec()

Ui/Gui/Dialogs/WorkspaceDialog.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,8 @@ def apply_workspace_selection(
226226
# Suppression de la rétro-compatibilité avec exclusion_patterns racine
227227
# exclusion_patterns = ark_config.get("exclusion_patterns", [])
228228

229-
for f in files:
230-
excluded_count = 0
229+
added_count = 0
230+
excluded_count = 0
231231

232232
import time
233233

Ui/Gui/Gui.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,13 +356,19 @@ def apply_file_filter(self, text: Optional[str] = None) -> None:
356356
_on_dep_pip_output,
357357
suggest_missing_dependencies,
358358
)
359+
def open_lock_dialog(self):
360+
"""Open the build lock management dialog."""
361+
from Ui.Gui.Dialogs.LockDialog import open_lock_dialog
362+
open_lock_dialog(self)
363+
359364
from Ui.Gui.Dialogs.CompilerDialog import (
360365
_continue_compile_all,
361366
cancel_all_compilations,
362367
compile_all,
363368
handle_finished,
364369
handle_stderr,
365370
handle_stdout,
371+
rebuild_from_lock,
366372
show_error_dialog,
367373
start_compilation_process,
368374
try_install_missing_modules,

Ui/Gui/IdeLikeGui/connections.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,12 @@ def _setup_more_tools_menu(self) -> None:
362362
)
363363
menu.addAction(act_advanced)
364364

365+
act_lock = QAction(menu)
366+
act_lock.triggered.connect(
367+
lambda: getattr(self, "open_lock_dialog", lambda: None)()
368+
)
369+
menu.addAction(act_lock)
370+
365371
act_export = QAction(menu)
366372
act_export.triggered.connect(
367373
lambda: getattr(self, "export_config", lambda: None)()
@@ -395,6 +401,7 @@ def _setup_more_tools_menu(self) -> None:
395401
"language": act_language,
396402
"theme": act_theme,
397403
"advanced": act_advanced,
404+
"lock": act_lock,
398405
"export": act_export,
399406
"import": act_import,
400407
"save_engines": act_save_engines,
@@ -590,6 +597,7 @@ def _widget_text(attr: str) -> str:
590597
"theme": _widget_text("select_theme") or _label(theme_key, "Theme", "Theme"),
591598
"advanced": _widget_text("advanced_cfg_btn")
592599
or _label("advanced_config", "Configurations avancees", "Advanced config"),
600+
"lock": _label("lock_manager", "Gestion des verrous (Locks)", "Lock management"),
593601
"export": _widget_text("btn_export_config")
594602
or _label("export_config", "Exporter config", "Export config"),
595603
"import": _widget_text("btn_import_config")

Ui/Gui/UiConnection.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ def _set(widget, icon_name: str, size: int = 18) -> None:
396396
_set(getattr(self, "btn_remove_file", None), "minus-circle.svg")
397397
_set(getattr(self, "btn_suggest_deps", None), "search.svg")
398398
_set(getattr(self, "btn_bc_loader", None), "sliders.svg")
399+
_set(getattr(self, "btn_lock_manager", None), "lock.svg")
399400
_set(getattr(self, "btn_show_stats", None), "bar-chart-2.svg")
400401
_set(getattr(self, "btn_help", None), "help-circle.svg")
401402

@@ -439,6 +440,7 @@ def _find(cls, name: str):
439440
self.btn_suggest_deps = _find(QPushButton, "btn_suggest_deps")
440441
self.btn_bc_loader = _find(QPushButton, "btn_bc_loader")
441442
self.btn_acasl_loader = _find(QPushButton, "btn_acasl_loader")
443+
self.btn_lock_manager = _find(QPushButton, "btn_lock_manager")
442444

443445
self.progress = _find(QProgressBar, "progress")
444446
self.log = _find(QTextEdit, "log")
@@ -539,6 +541,9 @@ def _connect_text(widget, handler) -> None:
539541
except Exception:
540542
pass
541543

544+
if self.btn_lock_manager:
545+
_connect_clicked(self.btn_lock_manager, self.open_lock_dialog)
546+
542547
_connect_clicked(self.btn_help, self.show_help_dialog)
543548

544549
if self.btn_show_stats:

0 commit comments

Comments
 (0)