+ добавлено подсвечивание предупреждений и ошибок в таблице выбранных переменных
536 lines
21 KiB
Python
536 lines
21 KiB
Python
import re
|
||
from PySide2.QtWidgets import (
|
||
QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QPushButton,
|
||
QLineEdit, QLabel, QHeaderView, QCompleter, QCheckBox, QHBoxLayout
|
||
)
|
||
from PySide2.QtGui import QKeySequence, QKeyEvent
|
||
from PySide2.QtCore import Qt, QStringListModel, QSettings
|
||
import setupVars
|
||
import scanVars
|
||
|
||
|
||
array_re = re.compile(r'^(\w+)\[(\d+)\]$')
|
||
|
||
class VariableSelectorDialog(QDialog):
|
||
def __init__(self, all_vars, structs, typedefs, xml_path=None, parent=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("Выбор переменных")
|
||
self.setAttribute(Qt.WA_DeleteOnClose)
|
||
self.resize(600, 500)
|
||
self.selected_names = []
|
||
|
||
self.all_vars = all_vars
|
||
self.structs = structs
|
||
self.typedefs = typedefs
|
||
self.expanded_vars = []
|
||
self.var_map = {v['name']: v for v in all_vars}
|
||
|
||
self.xml_path = xml_path # сохраняем путь к xml
|
||
|
||
# --- Добавляем чекбокс для автодополнения ---
|
||
self.autocomplete_checkbox = QCheckBox("Включить автодополнение")
|
||
self.autocomplete_checkbox.setChecked(True)
|
||
|
||
# Инициализируем QSettings с именем организации и приложения
|
||
self.settings = QSettings("SET", "DebugVarEdit_VarsSelector")
|
||
# Восстанавливаем сохранённое состояние чекбокса, если есть
|
||
checked = self.settings.value("autocomplete_enabled", True, type=bool)
|
||
self.autocomplete_checkbox.setChecked(checked)
|
||
# При изменении состояния чекбокса сохраняем его
|
||
self.autocomplete_checkbox.stateChanged.connect(self.save_checkbox_state)
|
||
|
||
self.search_input = QLineEdit()
|
||
self.search_input.setPlaceholderText("Поиск по имени переменной...")
|
||
self.search_input.textChanged.connect(self.on_search_text_changed)
|
||
|
||
self.tree = QTreeWidget()
|
||
self.tree.setHeaderLabels(["Имя переменной", "Тип"])
|
||
self.tree.setSelectionMode(QTreeWidget.ExtendedSelection)
|
||
self.tree.setRootIsDecorated(True)
|
||
self.tree.setUniformRowHeights(True)
|
||
|
||
self.tree.setStyleSheet("""
|
||
QTreeWidget::item:selected {
|
||
background-color: #87CEFA;
|
||
color: black;
|
||
}
|
||
QTreeWidget::item:hover {
|
||
background-color: #D3D3D3;
|
||
}
|
||
""")
|
||
|
||
self.btn_add = QPushButton("Добавить выбранные")
|
||
self.btn_add.clicked.connect(self.on_add_clicked)
|
||
|
||
self.btn_delete = QPushButton("Удалить выбранные")
|
||
self.btn_delete.clicked.connect(self.on_delete_clicked)
|
||
|
||
self.completer = QCompleter()
|
||
self.completer.setCompletionMode(QCompleter.PopupCompletion) # важно!
|
||
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||
self.completer.setFilterMode(Qt.MatchContains)
|
||
self.completer.setWidget(self.search_input)
|
||
|
||
|
||
self.search_input.installEventFilter(self)
|
||
|
||
# Создаем горизонтальный layout для "Поиск:" и чекбокса справа
|
||
search_layout = QHBoxLayout()
|
||
label_search = QLabel("Поиск:")
|
||
search_layout.addWidget(label_search, alignment=Qt.AlignLeft)
|
||
search_layout.addStretch() # чтобы чекбокс прижался вправо
|
||
search_layout.addWidget(self.autocomplete_checkbox, alignment=Qt.AlignRight)
|
||
self.completer.activated[str].connect(self.insert_completion)
|
||
|
||
layout = QVBoxLayout()
|
||
layout.addLayout(search_layout) # заменили label и чекбокс
|
||
layout.addWidget(self.search_input)
|
||
layout.addWidget(self.tree)
|
||
layout.addWidget(self.btn_add)
|
||
layout.addWidget(self.btn_delete)
|
||
self.setLayout(layout)
|
||
|
||
self.expanded_vars = setupVars.expand_vars(self.all_vars, self.structs, self.typedefs)
|
||
|
||
self.populate_tree()
|
||
|
||
def add_tree_item_recursively(self, parent, var):
|
||
"""
|
||
Рекурсивно добавляет переменную и её дочерние поля в дерево.
|
||
Если parent == None, добавляет на верхний уровень.
|
||
"""
|
||
name = var['name']
|
||
type_str = var.get('type', '')
|
||
show_var = var.get('show_var', 'false') == 'true'
|
||
|
||
item = QTreeWidgetItem([name, type_str])
|
||
item.setData(0, Qt.UserRole, name)
|
||
|
||
# Делаем bitfield-поля неактивными
|
||
if "(bitfield:" in type_str:
|
||
item.setDisabled(True)
|
||
self.set_tool(item, "Битовые поля недоступны для выбора")
|
||
|
||
for i, attr in enumerate(['file', 'extern', 'static']):
|
||
item.setData(0, Qt.UserRole + 1 + i, var.get(attr))
|
||
|
||
if show_var:
|
||
item.setForeground(0, Qt.gray)
|
||
item.setForeground(1, Qt.gray)
|
||
self.set_tool(item, "Уже добавлена")
|
||
|
||
if parent is None:
|
||
self.tree.addTopLevelItem(item)
|
||
else:
|
||
parent.addChild(item)
|
||
|
||
for child in var.get('children', []):
|
||
self.add_tree_item_recursively(item, child)
|
||
|
||
|
||
def populate_tree(self):
|
||
self.tree.clear()
|
||
|
||
for var in self.expanded_vars:
|
||
self.add_tree_item_recursively(None, var)
|
||
|
||
header = self.tree.header()
|
||
header.setSectionResizeMode(QHeaderView.Interactive) # вручную можно менять
|
||
self.tree.setColumnWidth(0, 400)
|
||
self.tree.resizeColumnToContents(1)
|
||
""" header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||
header.setSectionResizeMode(1, QHeaderView.ResizeToContents) """
|
||
|
||
def filter_tree(self):
|
||
text = self.search_input.text().strip().lower()
|
||
path_parts = self.split_path(text) if text else []
|
||
|
||
def hide_all(item):
|
||
item.setHidden(True)
|
||
for i in range(item.childCount()):
|
||
hide_all(item.child(i))
|
||
|
||
def path_matches_search(name, search_parts):
|
||
name_parts = self.split_path(name.lower())
|
||
if len(name_parts) < len(search_parts):
|
||
return False
|
||
for sp, np in zip(search_parts, name_parts):
|
||
if not np.startswith(sp):
|
||
return False
|
||
return True
|
||
|
||
def show_matching_path(item, level=0):
|
||
name = item.text(0).lower()
|
||
# Проверяем соответствие до длины path_parts
|
||
if not path_parts:
|
||
matched = True
|
||
else:
|
||
matched = False
|
||
# Проверяем совпадение по пути
|
||
if path_matches_search(name, path_parts[:level+1]):
|
||
matched = True
|
||
|
||
item.setHidden(not matched)
|
||
|
||
# Раскрываем, если это не последний уровень поиска
|
||
if matched and level < len(path_parts) - 1:
|
||
item.setExpanded(True)
|
||
else:
|
||
item.setExpanded(False)
|
||
|
||
matched_any_child = False
|
||
for i in range(item.childCount()):
|
||
child = item.child(i)
|
||
if show_matching_path(child, level + 1):
|
||
matched_any_child = True
|
||
|
||
return matched or matched_any_child
|
||
|
||
# Если в поиске нет точки — особая логика для первого уровня
|
||
if '.' not in text and '->' not in text and text != '':
|
||
for i in range(self.tree.topLevelItemCount()):
|
||
item = self.tree.topLevelItem(i)
|
||
name = item.text(0).lower()
|
||
if text in name:
|
||
item.setHidden(False)
|
||
item.setExpanded(False) # НЕ раскрываем потомков
|
||
else:
|
||
hide_all(item)
|
||
item.setHidden(True)
|
||
else:
|
||
# Обычная логика с поиском по пути
|
||
for i in range(self.tree.topLevelItemCount()):
|
||
item = self.tree.topLevelItem(i)
|
||
hide_all(item)
|
||
show_matching_path(item, 0)
|
||
|
||
def update_completions(self, text = None):
|
||
if text is None:
|
||
text = self.search_input.text().strip()
|
||
else:
|
||
text = text.strip()
|
||
parts = self.split_path(text)
|
||
path_parts = parts[:-1]
|
||
prefix = parts[-1].lower() if not text.endswith(('.', '>')) else ''
|
||
|
||
# Если путь есть (например: project.adc или project.adc.), ищем внутри него
|
||
search_deep = len(path_parts) > 0
|
||
|
||
def find_path_items(path_parts):
|
||
items = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())]
|
||
|
||
for part in path_parts:
|
||
part_lower = part.lower()
|
||
matched = []
|
||
|
||
for item in items:
|
||
# Берём последний фрагмент имени item, разделённого точками
|
||
item_name_part = self.split_path(item.text(0))[-1].lower()
|
||
|
||
if item_name_part == part_lower:
|
||
matched.append(item)
|
||
|
||
if not matched:
|
||
return []
|
||
items = []
|
||
# Собираем детей для следующего уровня поиска
|
||
for node in matched:
|
||
for i in range(node.childCount()):
|
||
items.append(node.child(i))
|
||
|
||
return matched
|
||
|
||
if not search_deep:
|
||
# Без точки — ищем только в топ-уровне, фильтруя по prefix
|
||
items = []
|
||
for i in range(self.tree.topLevelItemCount()):
|
||
item = self.tree.topLevelItem(i)
|
||
name_part = self.split_path(item.text(0))[-1].lower()
|
||
if name_part.startswith(prefix):
|
||
items.append(item)
|
||
completions = [item.text(0) for item in items]
|
||
else:
|
||
# С точкой — углубляемся по пути и показываем имена детей
|
||
if len(path_parts) == 0:
|
||
items = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())]
|
||
else:
|
||
items = find_path_items(path_parts)
|
||
|
||
completions = []
|
||
for item in items:
|
||
for i in range(item.childCount()):
|
||
child = item.child(i)
|
||
name_part = self.split_path(child.text(0))[-1].lower()
|
||
if prefix == '' or name_part.startswith(prefix):
|
||
completions.append(child.text(0))
|
||
|
||
|
||
|
||
self.completer.setModel(QStringListModel(completions))
|
||
return completions
|
||
|
||
|
||
|
||
def eventFilter(self, obj, event):
|
||
if obj == self.search_input and isinstance(event, QKeyEvent):
|
||
if event.key() == Qt.Key_Space and event.modifiers() & Qt.ControlModifier:
|
||
completions = self.update_completions()
|
||
self.completer.complete()
|
||
text = self.search_input.text().strip()
|
||
|
||
if len(completions) == 1 and completions[0].lower() == text.lower():
|
||
# Найдем узел с таким именем
|
||
def find_exact_item(name):
|
||
stack = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())]
|
||
while stack:
|
||
node = stack.pop()
|
||
if node.text(0).lower() == name.lower():
|
||
return node
|
||
for i in range(node.childCount()):
|
||
stack.append(node.child(i))
|
||
return None
|
||
|
||
node = find_exact_item(completions[0])
|
||
if node and node.childCount() > 0:
|
||
# Используем первую подсказку, чтобы определить нужный разделитель
|
||
completions = self.update_completions(text + '.')
|
||
suggestion = completions[0]
|
||
|
||
# Ищем, какой символ идёт после текущего текста
|
||
separator = '.'
|
||
if suggestion.startswith(text):
|
||
rest = suggestion[len(text):]
|
||
if rest.startswith('->'):
|
||
separator = '->'
|
||
elif rest.startswith('.'):
|
||
separator = '.'
|
||
|
||
self.search_input.setText(text + separator)
|
||
completions = self.update_completions()
|
||
self.completer.setModel(QStringListModel(completions))
|
||
self.completer.complete()
|
||
return True
|
||
|
||
# Иначе просто показываем подсказки
|
||
self.completer.setModel(QStringListModel(completions))
|
||
if completions:
|
||
self.completer.complete()
|
||
return True
|
||
|
||
return super().eventFilter(obj, event)
|
||
|
||
# Функция для поиска узла с полным именем
|
||
def find_node_by_fullname(self, name):
|
||
stack = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())]
|
||
while stack:
|
||
node = stack.pop()
|
||
if node.text(0).lower() == name.lower():
|
||
return node
|
||
for i in range(node.childCount()):
|
||
stack.append(node.child(i))
|
||
return None
|
||
def insert_completion(self, text):
|
||
|
||
node = self.find_node_by_fullname(text)
|
||
if node and node.childCount() > 0 and not (text.endswith('.') or text.endswith('->')):
|
||
# Определяем разделитель по имени первого ребёнка
|
||
child_name = node.child(0).text(0)
|
||
if child_name.startswith(text + '->'):
|
||
text += '->'
|
||
else:
|
||
text += '.'
|
||
|
||
self.search_input.setText(text)
|
||
self.search_input.setCursorPosition(len(text))
|
||
self.update_completions()
|
||
self.completer.complete()
|
||
else:
|
||
self.search_input.setText(text)
|
||
self.search_input.setCursorPosition(len(text))
|
||
|
||
def on_search_text_changed(self, text):
|
||
if self.autocomplete_checkbox.isChecked():
|
||
completions = self.update_completions(text)
|
||
node = self.find_node_by_fullname(text)
|
||
|
||
should_show = False
|
||
|
||
if completions:
|
||
if len(completions) > 1:
|
||
should_show = True
|
||
elif len(completions) == 1:
|
||
single_node = self.find_node_by_fullname(completions[0])
|
||
if single_node and single_node.childCount() > 0:
|
||
should_show = True
|
||
elif node and node.childCount() > 0 and not (text.endswith('.') or text.endswith('->')):
|
||
should_show = True
|
||
|
||
if should_show:
|
||
self.completer.setModel(QStringListModel(completions))
|
||
self.completer.complete()
|
||
self.filter_tree()
|
||
|
||
def on_add_clicked(self):
|
||
print("on_add_clicked triggered")
|
||
self.selected_names = []
|
||
self.tree.setFocus()
|
||
|
||
for item in self.tree.selectedItems():
|
||
name = item.text(0) # имя переменной (в колонке 1)
|
||
type_str = item.text(1) # тип переменной (в колонке 2)
|
||
|
||
if not name:
|
||
continue
|
||
|
||
self.selected_names.append((name, type_str))
|
||
|
||
if name in self.var_map:
|
||
# Если переменная уже есть, просто включаем её и показываем
|
||
var = self.var_map[name]
|
||
var['show_var'] = 'true'
|
||
var['enable'] = 'true'
|
||
else:
|
||
# Создаём новый элемент переменной
|
||
# Получаем родительские параметры
|
||
file_val = item.data(0, Qt.UserRole + 1)
|
||
extern_val = item.data(0, Qt.UserRole + 2)
|
||
static_val = item.data(0, Qt.UserRole + 3)
|
||
|
||
new_var = {
|
||
'name': name,
|
||
'type': type_str,
|
||
'show_var': 'true',
|
||
'enable': 'true',
|
||
'shortname': name,
|
||
'pt_type': '',
|
||
'iq_type': '',
|
||
'return_type': 'iq_none',
|
||
'file': file_val,
|
||
'extern': str(extern_val).lower() if extern_val else 'false',
|
||
'static': str(static_val).lower() if static_val else 'false',
|
||
}
|
||
|
||
# Добавляем в список переменных
|
||
self.all_vars.append(new_var)
|
||
self.var_map[name] = new_var # Чтобы в будущем не добавлялось повторно
|
||
|
||
print("accept")
|
||
self.done(QDialog.Accepted)
|
||
|
||
|
||
def on_delete_clicked(self):
|
||
print("on_delete_clicked triggered")
|
||
selected_names = self._get_selected_var_names()
|
||
if not selected_names:
|
||
print("nothing selected")
|
||
return
|
||
|
||
# Обновляем var_map и all_vars
|
||
for name in selected_names:
|
||
if name in self.var_map:
|
||
self.var_map[name]['show_var'] = 'false'
|
||
self.var_map[name]['enable'] = 'false'
|
||
|
||
for v in self.all_vars:
|
||
if v['name'] == name:
|
||
v['show_var'] = 'false'
|
||
v['enable'] = 'false'
|
||
break
|
||
|
||
# Проверка пути к XML
|
||
if not hasattr(self, 'xml_path') or not self.xml_path:
|
||
from PySide2.QtWidgets import QMessageBox
|
||
QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно обновить переменные.")
|
||
return
|
||
|
||
import xml.etree.ElementTree as ET
|
||
tree = ET.parse(self.xml_path)
|
||
root = tree.getroot()
|
||
if root is None:
|
||
return
|
||
|
||
vars_section = root.find('variables')
|
||
if vars_section is None:
|
||
return
|
||
|
||
for var_elem in vars_section.findall('var'):
|
||
name = var_elem.attrib.get('name')
|
||
if name in selected_names:
|
||
def set_text(tag, value):
|
||
el = var_elem.find(tag)
|
||
if el is None:
|
||
el = ET.SubElement(var_elem, tag)
|
||
el.text = value
|
||
set_text('show_var', 'false')
|
||
set_text('enable', 'false')
|
||
|
||
scanVars.indent_xml(root)
|
||
ET.ElementTree(root).write(self.xml_path, encoding="utf-8", xml_declaration=True)
|
||
|
||
self.populate_tree()
|
||
print("accept")
|
||
self.done(QDialog.Accepted)
|
||
|
||
|
||
def set_tool(self, item, text):
|
||
item.setToolTip(0, text)
|
||
item.setToolTip(1, text)
|
||
|
||
|
||
def keyPressEvent(self, event):
|
||
if event.key() == Qt.Key_Delete:
|
||
self.delete_selected_vars()
|
||
else:
|
||
super().keyPressEvent(event)
|
||
|
||
def delete_selected_vars(self):
|
||
selected_names = self._get_selected_var_names()
|
||
if not selected_names:
|
||
return
|
||
|
||
# Проверка пути к XML
|
||
if not hasattr(self, 'xml_path') or not self.xml_path:
|
||
from PySide2.QtWidgets import QMessageBox
|
||
QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно удалить переменные.")
|
||
return
|
||
|
||
import xml.etree.ElementTree as ET
|
||
tree = ET.parse(self.xml_path)
|
||
root = tree.getroot()
|
||
if root is None:
|
||
return
|
||
|
||
vars_section = root.find('variables')
|
||
if vars_section is None:
|
||
return
|
||
|
||
removed_any = False
|
||
for var_elem in list(vars_section.findall('var')):
|
||
name = var_elem.attrib.get('name')
|
||
if name in selected_names:
|
||
vars_section.remove(var_elem)
|
||
removed_any = True
|
||
self.var_map.pop(name, None)
|
||
|
||
# Удаляем из all_vars (глобально)
|
||
self.all_vars[:] = [v for v in self.all_vars if v['name'] not in selected_names]
|
||
|
||
if removed_any:
|
||
scanVars.indent_xml(root)
|
||
ET.ElementTree(root).write(self.xml_path, encoding="utf-8", xml_declaration=True)
|
||
|
||
self.populate_tree()
|
||
self.filter_tree()
|
||
|
||
def _get_selected_var_names(self):
|
||
self.tree.setFocus()
|
||
return [item.text(0) for item in self.tree.selectedItems() if item.text(0)]
|
||
|
||
|
||
def save_checkbox_state(self):
|
||
self.settings.setValue("autocomplete_enabled", self.autocomplete_checkbox.isChecked())
|
||
|
||
|
||
def split_path(self, path):
|
||
# Разбиваем по точке или по -> (учитываем, что -> длиной 2 символа)
|
||
return re.split(r'\.|->', path) |