debugVarTool/Src/VariableSelector.py

633 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 myXML
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.node_index = {}
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.build_completion_list()
self.populate_tree()
def get_full_item_name(self, item):
names = []
while item:
names.append(item.text(0))
item = item.parent()
return '.'.join(reversed(names))
def build_completion_list(self):
# Собираем список полных имён всех переменных и вложенных полей
completions = []
def recurse(var, prefix=''):
fullname = f"{prefix}.{var['name']}" if prefix else var['name']
completions.append(fullname)
for child in var.get('children', []):
recurse(child, fullname)
for v in self.expanded_vars:
recurse(v)
self.all_completions = completions
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)
full_name = self.get_full_item_name(item)
self.node_index[full_name.lower()] = item
# Делаем 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()
self.node_index.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()
# По умолчанию не совпадает
matched = False
# Проверяем путь
if not path_parts:
matched = True
elif path_matches_search(name, path_parts[:level+1]):
matched = True
# Исключаем "плоские" элементы на верхнем уровне при глубоком поиске
if level == 0 and item.childCount() == 0 and len(path_parts) > 1:
matched = False
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 find_node_by_path(self, root_vars, path_list):
current_level = root_vars
node = None
for part in path_list:
node = None
for var in current_level:
if var['name'] == part:
node = var
break
if node is None:
return None
current_level = node.get('children', [])
return node
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] if parts else []
prefix = parts[-1].lower() if parts else ''
ends_with_sep = text.endswith('.') or text.endswith('->') or text.endswith('[')
is_index_suggestion = text.endswith('[')
completions = []
def find_path_items(parts):
items = [self.tree.topLevelItem(i) for i in range(self.tree.topLevelItemCount())]
for part in parts:
part_lower = part.lower()
matched = []
for item in items:
name_parts = self.split_path(item.text(0).lower())
if name_parts and name_parts[-1] == 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 is_index_suggestion:
# Предлагаем индексы (ищем всех детей с форматом foo[0], foo[1], ...)
base_text = text[:-1] # убираем '[', получаем, например, 'foo'
parent_node = self.find_node_by_fullname(base_text)
if not parent_node:
parent_node = self.find_node_by_fullname(base_text.rstrip('0123456789[]'))
if parent_node:
seen = set()
for i in range(parent_node.childCount()):
child = parent_node.child(i)
cname = child.text(0)
m = re.match(rf'^{re.escape(base_text)}\[(\d+)\]$', cname)
if m and cname not in seen:
completions.append(cname)
seen.add(cname)
self.completer.setModel(QStringListModel(completions))
return completions
if ends_with_sep:
# Завершён путь (например: project.adc[0].), показываем детей
node = self.find_node_by_fullname(text[:-1])
if node:
for i in range(node.childCount()):
completions.append(node.child(i).text(0))
elif not path_parts:
# Первый уровень — по вхождению
for i in range(self.tree.topLevelItemCount()):
item = self.tree.topLevelItem(i)
name = item.text(0).lower()
if prefix in name:
completions.append(item.text(0))
else:
# Углубляемся: на последнем уровне используем startswith(prefix)
matched_items = find_path_items(path_parts)
for item in matched_items:
for i in range(item.childCount()):
child = item.child(i)
name = child.text(0)
name_parts = self.split_path(name)
if not name_parts:
continue
last_part = name_parts[-1].lower()
if prefix == '' or last_part.startswith(prefix):
completions.append(name)
self.completer.setModel(QStringListModel(completions))
return completions
# Функция для поиска узла с полным именем
def find_node_by_fullname(self, name):
return self.node_index.get(name.lower())
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('->') or text.endswith('[')):
# Определяем разделитель по имени первого ребёнка
child_name = node.child(0).text(0)
if child_name.startswith(text + '->'):
text += '->'
elif child_name.startswith(text + '.'):
text += '.'
elif '[' in child_name:
text += '[' # для массивов
else:
text += '.' # fallback
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 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 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):
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 # Чтобы в будущем не добавлялось повторно
self.done(QDialog.Accepted)
def on_delete_clicked(self):
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')
myXML.fwrite(root, self.xml_path)
self.populate_tree()
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:
myXML.fwrite(root, self.xml_path)
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):
"""
Разбивает путь на компоненты:
- 'foo[2].bar[1]->baz' → ['foo', [2]', 'bar', '[1]' 'baz']
"""
tokens = []
token = ''
i = 0
while i < len(path):
c = path[i]
# Разделители: '->' и '.'
if c == '-' and path[i:i+2] == '->':
if token:
tokens.append(token)
token = ''
i += 2
continue
elif c == '.':
if token:
tokens.append(token)
token = ''
i += 1
continue
elif c == '[':
# Заканчиваем текущий токен, если есть
if token:
tokens.append(token)
token = ''
# Собираем индекс [N]
idx = ''
while i < len(path) and path[i] != ']':
idx += path[i]
i += 1
if i < len(path) and path[i] == ']':
idx += ']'
i += 1
tokens.append(idx)
continue
else:
token += c
i += 1
if token:
tokens.append(token)
return tokens