diff --git a/src/testium/interpreter/process.py b/src/testium/interpreter/process.py index bb0112e..f79a0ab 100644 --- a/src/testium/interpreter/process.py +++ b/src/testium/interpreter/process.py @@ -8,6 +8,7 @@ import copy from lib.string_queue import StringQueue from lib.tum_except import print_exception, ETUMRuntimeError, ETUMSyntaxError import libs.testium as tm +import interpreter.utils.globdict as globdict from interpreter.utils.params import expanse from interpreter.utils.test_ctrl import TestSetController from interpreter.utils.test_init import ( @@ -255,6 +256,7 @@ Is the python exec path correct ?""" try: test_run_init() print(test_run_header()) + globdict.set_update_queue(self.__squeue) test_set.execute() finally: if test_set.success(): @@ -274,6 +276,7 @@ Is the python exec path correct ?""" engine.join() # Sends signal to the GUI self.send_finished() + globdict.set_update_queue(None) restore_gd(gdict) except Exception as e: print_exception(e) @@ -311,6 +314,9 @@ Is the python exec path correct ?""" "enabled_state": test_set.getEnabledState, "process_param": self.process_param, "set_test_outputs": self.set_test_outputs, + "get_gd_vars": self.get_gd_vars, + "set_gd_var": self.set_gd_var, + "del_gd_var": self.del_gd_var, "set_enabled_state": test_set.setEnabledState, "check_uncheck_all": test_set.checkUncheckAll, "get_folded": test_set.getFolded, @@ -344,6 +350,25 @@ Is the python exec path correct ?""" def set_test_outputs(self, outputs: list): tm.setgd("test_outputs", outputs) + def get_gd_vars(self): + import json + result = {} + for k, v in globdict.global_dict.items(): + if k.startswith("_"): + continue + try: + json.dumps(v) + result[k] = v + except (TypeError, ValueError): + pass + return result + + def set_gd_var(self, name: str, value): + tm.setgd(name, value) + + def del_gd_var(self, name: str): + tm.delgd(name) + def process_control_commands(self, tctrl): term = False while (not term) and (not self.__closed): diff --git a/src/testium/interpreter/utils/globdict.py b/src/testium/interpreter/utils/globdict.py index 6f5ab15..c12721a 100644 --- a/src/testium/interpreter/utils/globdict.py +++ b/src/testium/interpreter/utils/globdict.py @@ -1,3 +1,4 @@ +import json from threading import Lock @@ -5,6 +6,30 @@ global_dict = {} global_dict_lock = Lock() +_update_queue = None + + +def set_update_queue(q): + global _update_queue + _update_queue = q + + +def _push_update(key, value): + if _update_queue is None or key.startswith("_"): + return + try: + json.dumps(value) + _update_queue.put({"type": "gd_update", "key": key, "value": value}) + except (TypeError, ValueError): + pass + + +def _push_delete(key): + if _update_queue is None or key.startswith("_"): + return + _update_queue.put({"type": "gd_delete", "key": key}) + + # Global dictionnary helper functions def gd(name, default=None): ''' Function which returns a variable from the global dictionary of testium @@ -31,6 +56,7 @@ def setgd(name, value): ''' with global_dict_lock: global_dict.update({name: value}) + _push_update(name, value) def delgd(name): ''' Function which removes a variable from the global dictionary of testium @@ -44,6 +70,7 @@ def delgd(name): del global_dict[name] except: pass + _push_delete(name) def cleargd(): with global_dict_lock: diff --git a/src/testium/main_win/f1_win/d_f1_win.py b/src/testium/main_win/f1_win/d_f1_win.py index 8c3112b..c88090a 100644 --- a/src/testium/main_win/f1_win/d_f1_win.py +++ b/src/testium/main_win/f1_win/d_f1_win.py @@ -1,11 +1,16 @@ +import ast +import json import os -import sys -import subprocess import re +import subprocess +import sys -from PySide6.QtWidgets import QDialog +from PySide6.QtWidgets import ( + QDialog, QDialogButtonBox, QHeaderView, QMenu, QMessageBox, + QPushButton, QTextEdit, QVBoxLayout, +) from PySide6.QtGui import QSyntaxHighlighter, QTextCharFormat, QColor, QFont, QDesktopServices -from PySide6.QtCore import Qt, QUrl +from PySide6.QtCore import Qt, QUrl, Slot from main_win.f1_win.f1_win_core import Ui_F1Dialog @@ -16,58 +21,253 @@ class YamlHighlighter(QSyntaxHighlighter): self.highlightingRules = [] - # --- KEY formatting (before colon) --- key_format = QTextCharFormat() - key_format.setForeground(QColor("#268bd2")) # Solarized blue + key_format.setForeground(QColor("#268bd2")) key_format.setFontWeight(QFont.Bold) self.highlightingRules.append((r"^\s*[^:]+(?=:)", key_format)) - # --- VALUE formatting (strings) --- value_format = QTextCharFormat() - value_format.setForeground(QColor("#2aa198")) # teal + value_format.setForeground(QColor("#2aa198")) self.highlightingRules.append((r":\s*[^#\n]+", value_format)) - # --- Booleans (true/false) --- bool_format = QTextCharFormat() - bool_format.setForeground(QColor("#b58900")) # yellow + bool_format.setForeground(QColor("#b58900")) bool_format.setFontWeight(QFont.Bold) self.highlightingRules.append((r"\b(true|false)\b", bool_format)) - # --- Numbers --- num_format = QTextCharFormat() - num_format.setForeground(QColor("#d33682")) # magenta + num_format.setForeground(QColor("#d33682")) self.highlightingRules.append((r"\b[0-9]+\b", num_format)) - # --- Comments (# ...) --- comment_format = QTextCharFormat() - comment_format.setForeground(QColor("#586e75")) # gray + comment_format.setForeground(QColor("#586e75")) self.highlightingRules.append((r"#.*", comment_format)) def highlightBlock(self, text): for pattern, fmt in self.highlightingRules: - for match in re.finditer(pattern, text): start, end = match.span() - self.setFormat(start, end-start, fmt) + self.setFormat(start, end - start, fmt) + + +class GdVarEditDialog(QDialog): + """JSON editor dialog for dict/list values.""" + + def __init__(self, key, value, parent=None): + super().__init__(parent) + self.setWindowTitle(f"Edit: {key}") + self.result_value = None + + layout = QVBoxLayout(self) + + self._edit = QTextEdit() + self._edit.setPlainText(json.dumps(value, indent=2)) + font = QFont("Monospace") + font.setStyleHint(QFont.StyleHint.TypeWriter) + font.setPointSize(9) + self._edit.setFont(font) + layout.addWidget(self._edit) + + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) + buttons.accepted.connect(self._on_ok) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + self.resize(400, 300) + + def _on_ok(self): + try: + self.result_value = json.loads(self._edit.toPlainText()) + self.accept() + except json.JSONDecodeError as e: + QMessageBox.warning(self, "Invalid JSON", str(e)) class DialogF1(QDialog): - def __init__(self, parent = None): + def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_F1Dialog() self.ui.setupUi(self) self.highlighter = YamlHighlighter(self.ui.TestContentEdit.document()) - self.setWindowFlags( - Qt.Window | Qt.WindowStaysOnTopHint | Qt.Tool - ) + self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint | Qt.Tool) + self.ui.ButtLocOpen.clicked.connect(self.on_butlocopen_click) self.ui.ButtClose.clicked.connect(self.close) + self._service = None + self._key_rows = {} + self._updating = False + self._mono_font = QFont("Monospace") + self._mono_font.setStyleHint(QFont.StyleHint.TypeWriter) + self._mono_bold_font = QFont("Monospace") + self._mono_bold_font.setStyleHint(QFont.StyleHint.TypeWriter) + self._mono_bold_font.setBold(True) + + self._setup_vars_tab() + + def _setup_vars_tab(self): + table = self.ui.varsTable + table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed) + table.setColumnWidth(2, 36) + table.verticalHeader().setVisible(False) + table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + table.customContextMenuRequested.connect(self._on_context_menu) + table.cellChanged.connect(self._on_cell_changed) + table.setEnabled(False) + self.ui.addVarButton.setEnabled(False) + self.ui.addVarButton.clicked.connect(self._on_add_var) + + def load_initial_vars(self, vars_dict: dict): + for key, value in vars_dict.items(): + self.gd_var_updated(key, value) + + def set_service(self, service): + self._service = service + enabled = service is not None + self.ui.varsTable.setEnabled(enabled) + self.ui.addVarButton.setEnabled(enabled) + if not enabled: + self._updating = True + try: + self.ui.varsTable.setRowCount(0) + finally: + self._updating = False + self._key_rows.clear() + + @Slot(str, object) + def gd_var_updated(self, key, value): + if key in self._key_rows: + self._refresh_row(self._key_rows[key], key, value) + else: + self._updating = True + try: + row = self.ui.varsTable.rowCount() + self.ui.varsTable.insertRow(row) + finally: + self._updating = False + self._key_rows[key] = row + self._refresh_row(row, key, value) + + @Slot(str) + def gd_var_deleted(self, key): + if key not in self._key_rows: + return + row = self._key_rows.pop(key) + self._updating = True + try: + self.ui.varsTable.removeRow(row) + finally: + self._updating = False + self._key_rows = {k: (r - 1 if r > row else r) for k, r in self._key_rows.items()} + + def _refresh_row(self, row, key, value): + from PySide6.QtWidgets import QTableWidgetItem + self._updating = True + try: + table = self.ui.varsTable + + key_item = QTableWidgetItem(key) + key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable) + key_item.setFont(self._mono_bold_font) + table.setItem(row, 0, key_item) + + display = self._display_value(value) + val_item = QTableWidgetItem(display) + val_item.setData(Qt.ItemDataRole.UserRole, value) + val_item.setToolTip(self._full_tooltip(value)) + val_item.setFont(self._mono_font) + if self._is_complex(value): + val_item.setFlags(val_item.flags() & ~Qt.ItemFlag.ItemIsEditable) + table.setItem(row, 1, val_item) + + if self._is_complex(value): + btn = QPushButton("[…]") + captured_key = key + btn.clicked.connect(lambda: self._on_edit_complex(captured_key)) + table.setCellWidget(row, 2, btn) + else: + table.setCellWidget(row, 2, None) + table.setItem(row, 2, QTableWidgetItem()) + finally: + self._updating = False + + def _is_complex(self, value): + return isinstance(value, (dict, list)) + + def _display_value(self, value): + if self._is_complex(value): + text = repr(value) + return (text[:60] + "…") if len(text) > 60 else text + return repr(value) + + def _full_tooltip(self, value): + try: + text = json.dumps(value, indent=2) + except (TypeError, ValueError): + text = repr(value) + escaped = text.replace("&", "&").replace("<", "<").replace(">", ">") + return f"
{escaped}"
+
+ def _on_cell_changed(self, row, col):
+ if self._updating or col != 1 or self._service is None:
+ return
+ from PySide6.QtWidgets import QTableWidgetItem
+ key_item = self.ui.varsTable.item(row, 0)
+ val_item = self.ui.varsTable.item(row, 1)
+ if key_item is None or val_item is None:
+ return
+ key = key_item.text()
+ text = val_item.text()
+ try:
+ value = ast.literal_eval(text)
+ except (ValueError, SyntaxError):
+ value = text
+ self._service.set_gd_var(key, value)
+
+ def _on_edit_complex(self, key):
+ if key not in self._key_rows:
+ return
+ val_item = self.ui.varsTable.item(self._key_rows[key], 1)
+ if val_item is None:
+ return
+ value = val_item.data(Qt.ItemDataRole.UserRole)
+ dlg = GdVarEditDialog(key, value, self)
+ if dlg.exec() == QDialog.DialogCode.Accepted and self._service is not None:
+ self._service.set_gd_var(key, dlg.result_value)
+
+ def _on_add_var(self):
+ key = self.ui.newKeyEdit.text().strip()
+ value_text = self.ui.newValueEdit.text().strip()
+ if not key or self._service is None:
+ return
+ try:
+ value = ast.literal_eval(value_text)
+ except (ValueError, SyntaxError):
+ value = value_text
+ self._service.set_gd_var(key, value)
+ self.ui.newKeyEdit.clear()
+ self.ui.newValueEdit.clear()
+
+ def _on_context_menu(self, pos):
+ row = self.ui.varsTable.rowAt(pos.y())
+ if row < 0:
+ return
+ key_item = self.ui.varsTable.item(row, 0)
+ if key_item is None or self._service is None:
+ return
+ key = key_item.text()
+ menu = QMenu(self)
+ delete_action = menu.addAction("Delete")
+ if menu.exec(self.ui.varsTable.mapToGlobal(pos)) == delete_action:
+ self._service.del_gd_var(key)
+
def on_butlocopen_click(self):
file = self.ui.sequenceFileNameLineEdit.text()
if os.path.exists(file):
- if sys.platform.startswith("win"): # Windows
+ if sys.platform.startswith("win"):
subprocess.Popen(f'explorer "{file}"')
- else: # Linux / autres
+ else:
subprocess.Popen(["xdg-open", file])
- QDesktopServices.openUrl(QUrl.fromLocalFile(file))
\ No newline at end of file
+ QDesktopServices.openUrl(QUrl.fromLocalFile(file))
diff --git a/src/testium/main_win/f1_win/f1_win_core.py b/src/testium/main_win/f1_win/f1_win_core.py
index 1e2b1e5..4549fb7 100644
--- a/src/testium/main_win/f1_win/f1_win_core.py
+++ b/src/testium/main_win/f1_win/f1_win_core.py
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'f1_win_core.ui'
##
-## Created by: Qt User Interface Compiler version 6.10.1
+## Created by: Qt User Interface Compiler version 6.11.0
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -16,8 +16,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QDialog, QFormLayout, QHBoxLayout,
- QLabel, QLineEdit, QPushButton, QSizePolicy,
- QSpacerItem, QTextEdit, QToolButton, QVBoxLayout,
+ QHeaderView, QLabel, QLineEdit, QPushButton,
+ QSizePolicy, QSpacerItem, QTabWidget, QTableWidget,
+ QTableWidgetItem, QTextEdit, QToolButton, QVBoxLayout,
QWidget)
import f1_win_rc
@@ -25,7 +26,7 @@ class Ui_F1Dialog(object):
def setupUi(self, F1Dialog):
if not F1Dialog.objectName():
F1Dialog.setObjectName(u"F1Dialog")
- F1Dialog.resize(400, 300)
+ F1Dialog.resize(550, 450)
icon = QIcon()
if QIcon.hasThemeIcon(QIcon.ThemeIcon.HelpAbout):
icon = QIcon.fromTheme(QIcon.ThemeIcon.HelpAbout)
@@ -36,19 +37,20 @@ class Ui_F1Dialog(object):
F1Dialog.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
self.verticalLayout_2 = QVBoxLayout(F1Dialog)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
- self.horizontalLayout_2 = QHBoxLayout()
- self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
-
- self.verticalLayout_2.addLayout(self.horizontalLayout_2)
-
+ self.tabWidget = QTabWidget(F1Dialog)
+ self.tabWidget.setObjectName(u"tabWidget")
+ self.tabTestItem = QWidget()
+ self.tabTestItem.setObjectName(u"tabTestItem")
+ self.verticalLayout_tab0 = QVBoxLayout(self.tabTestItem)
+ self.verticalLayout_tab0.setObjectName(u"verticalLayout_tab0")
self.formLayout = QFormLayout()
self.formLayout.setObjectName(u"formLayout")
- self.typeLabel = QLabel(F1Dialog)
+ self.typeLabel = QLabel(self.tabTestItem)
self.typeLabel.setObjectName(u"typeLabel")
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.typeLabel)
- self.typeLineEdit = QLineEdit(F1Dialog)
+ self.typeLineEdit = QLineEdit(self.tabTestItem)
self.typeLineEdit.setObjectName(u"typeLineEdit")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -59,20 +61,20 @@ class Ui_F1Dialog(object):
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.typeLineEdit)
- self.sequenceFileNameLabel = QLabel(F1Dialog)
+ self.sequenceFileNameLabel = QLabel(self.tabTestItem)
self.sequenceFileNameLabel.setObjectName(u"sequenceFileNameLabel")
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.sequenceFileNameLabel)
self.horizontalLayout_3 = QHBoxLayout()
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
- self.sequenceFileNameLineEdit = QLineEdit(F1Dialog)
+ self.sequenceFileNameLineEdit = QLineEdit(self.tabTestItem)
self.sequenceFileNameLineEdit.setObjectName(u"sequenceFileNameLineEdit")
self.sequenceFileNameLineEdit.setReadOnly(True)
self.horizontalLayout_3.addWidget(self.sequenceFileNameLineEdit)
- self.ButtLocOpen = QToolButton(F1Dialog)
+ self.ButtLocOpen = QToolButton(self.tabTestItem)
self.ButtLocOpen.setObjectName(u"ButtLocOpen")
self.horizontalLayout_3.addWidget(self.ButtLocOpen)
@@ -81,18 +83,61 @@ class Ui_F1Dialog(object):
self.formLayout.setLayout(1, QFormLayout.ItemRole.FieldRole, self.horizontalLayout_3)
- self.verticalLayout_2.addLayout(self.formLayout)
+ self.verticalLayout_tab0.addLayout(self.formLayout)
- self.label = QLabel(F1Dialog)
+ self.label = QLabel(self.tabTestItem)
self.label.setObjectName(u"label")
- self.verticalLayout_2.addWidget(self.label)
+ self.verticalLayout_tab0.addWidget(self.label)
- self.TestContentEdit = QTextEdit(F1Dialog)
+ self.TestContentEdit = QTextEdit(self.tabTestItem)
self.TestContentEdit.setObjectName(u"TestContentEdit")
self.TestContentEdit.setReadOnly(True)
- self.verticalLayout_2.addWidget(self.TestContentEdit)
+ self.verticalLayout_tab0.addWidget(self.TestContentEdit)
+
+ self.tabWidget.addTab(self.tabTestItem, "")
+ self.tabVariables = QWidget()
+ self.tabVariables.setObjectName(u"tabVariables")
+ self.verticalLayout_tab1 = QVBoxLayout(self.tabVariables)
+ self.verticalLayout_tab1.setObjectName(u"verticalLayout_tab1")
+ self.varsTable = QTableWidget(self.tabVariables)
+ if (self.varsTable.columnCount() < 3):
+ self.varsTable.setColumnCount(3)
+ __qtablewidgetitem = QTableWidgetItem()
+ self.varsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
+ __qtablewidgetitem1 = QTableWidgetItem()
+ self.varsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
+ __qtablewidgetitem2 = QTableWidgetItem()
+ self.varsTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
+ self.varsTable.setObjectName(u"varsTable")
+
+ self.verticalLayout_tab1.addWidget(self.varsTable)
+
+ self.addVarLayout = QHBoxLayout()
+ self.addVarLayout.setObjectName(u"addVarLayout")
+ self.newKeyEdit = QLineEdit(self.tabVariables)
+ self.newKeyEdit.setObjectName(u"newKeyEdit")
+
+ self.addVarLayout.addWidget(self.newKeyEdit)
+
+ self.newValueEdit = QLineEdit(self.tabVariables)
+ self.newValueEdit.setObjectName(u"newValueEdit")
+
+ self.addVarLayout.addWidget(self.newValueEdit)
+
+ self.addVarButton = QPushButton(self.tabVariables)
+ self.addVarButton.setObjectName(u"addVarButton")
+ self.addVarButton.setMaximumSize(QSize(30, 16777215))
+
+ self.addVarLayout.addWidget(self.addVarButton)
+
+
+ self.verticalLayout_tab1.addLayout(self.addVarLayout)
+
+ self.tabWidget.addTab(self.tabVariables, "")
+
+ self.verticalLayout_2.addWidget(self.tabWidget)
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.setObjectName(u"horizontalLayout")
@@ -113,6 +158,9 @@ class Ui_F1Dialog(object):
self.retranslateUi(F1Dialog)
+ self.tabWidget.setCurrentIndex(0)
+
+
QMetaObject.connectSlotsByName(F1Dialog)
# setupUi
@@ -122,6 +170,15 @@ class Ui_F1Dialog(object):
self.sequenceFileNameLabel.setText(QCoreApplication.translate("F1Dialog", u"Test file name", None))
self.ButtLocOpen.setText(QCoreApplication.translate("F1Dialog", u"...", None))
self.label.setText(QCoreApplication.translate("F1Dialog", u"Test content:", None))
+ self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabTestItem), QCoreApplication.translate("F1Dialog", u"Test item", None))
+ ___qtablewidgetitem = self.varsTable.horizontalHeaderItem(0)
+ ___qtablewidgetitem.setText(QCoreApplication.translate("F1Dialog", u"Key", None))
+ ___qtablewidgetitem1 = self.varsTable.horizontalHeaderItem(1)
+ ___qtablewidgetitem1.setText(QCoreApplication.translate("F1Dialog", u"Value", None))
+ self.newKeyEdit.setPlaceholderText(QCoreApplication.translate("F1Dialog", u"New key", None))
+ self.newValueEdit.setPlaceholderText(QCoreApplication.translate("F1Dialog", u"Value", None))
+ self.addVarButton.setText(QCoreApplication.translate("F1Dialog", u"+", None))
+ self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabVariables), QCoreApplication.translate("F1Dialog", u"Variables", None))
self.ButtClose.setText(QCoreApplication.translate("F1Dialog", u"Close", None))
# retranslateUi
diff --git a/src/testium/main_win/f1_win/f1_win_core.ui b/src/testium/main_win/f1_win/f1_win_core.ui
index fb48f24..93727b1 100644
--- a/src/testium/main_win/f1_win/f1_win_core.ui
+++ b/src/testium/main_win/f1_win/f1_win_core.ui
@@ -6,8 +6,8 @@