debugVarTool/Src/VariableSelector.py
Razvalyaev e4fcfd11d7 Доработана функции для считывания переменных
Добавлены бета-функции для считывания переменны по адресу

+фиксы багов

future:
- в селекторе сделать две таблички для всех переменных и для выборанных
- по кнопке переносить переменные из всех в выбранные
- переменные из выбранных и добавлять в основную табличку
- сделать отдельный класс для таблички - который будет принимать спиоск переменных для отображения
2025-07-11 16:46:51 +03:00

699 lines
27 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
import xml.etree.ElementTree as ET
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 VariableTable
import setupVars
import myXML
import time
array_re = re.compile(r'^(\w+)\[(\d+)\]$')
class VariableSelectorDialog(QDialog):
def __init__(self, table, 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._bckspc_pressed = False # флаг подавления добавления разделителя
self.table = table
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.manual_completion_active = False
# --- Добавляем чекбокс для автодополнения ---
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, vars_list=None):
if vars_list is None:
vars_list = self.expanded_vars
self.tree.clear()
self.node_index.clear()
for var in vars_list:
self.add_tree_item_recursively(None, var)
header = self.tree.header()
header.setSectionResizeMode(QHeaderView.Interactive) # вручную можно менять
self.tree.setColumnWidth(0, 400)
self.tree.resizeColumnToContents(1)
def expand_to_level(self, item, level, current_level=0):
"""
Рекурсивно раскрывает узлы до заданного уровня.
"""
if current_level < level:
item.setExpanded(True)
else:
item.setExpanded(False)
for i in range(item.childCount()):
self.expand_to_level(item.child(i), level, current_level + 1)
def filter_tree(self):
text = self.search_input.text().strip().lower()
path_parts = text.split('.') if text else []
filtered_vars = filter_vars(self.expanded_vars, path_parts)
# Сначала перерисовываем дерево
self.populate_tree(filtered_vars)
# Теперь node_index уже пересоздан — можно работать
expand_level = len(path_parts) - 1 if path_parts else 0
for i in range(self.tree.topLevelItemCount()):
item = self.tree.topLevelItem(i)
self.expand_to_level(item, expand_level)
# Раскрываем путь до точного совпадения
if path_parts:
fullname = '.'.join(path_parts)
node = self.node_index.get(fullname.lower())
if node:
parent = node.parent()
while parent:
parent.setExpanded(True)
parent = parent.parent()
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_exact_node(parts):
# Ищем точный узел по полному пути, используя node_index
# Постепенно собираем fullname из parts
if not parts:
return None
fullname = parts[0]
for p in parts[1:]:
fullname += '.' + p
return self.node_index.get(fullname.lower())
if is_index_suggestion:
base_text = text[:-1] # убираем '['
parent_node = self.find_node_by_fullname(base_text)
if not parent_node:
# если base_text может содержать индекс типа foo[12], попробуем очистить
base_text_clean = re.sub(r'\[\d+\]$', '', base_text)
parent_node = self.find_node_by_fullname(base_text_clean)
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:
# Путь завершен, показываем детей узла
node = self.find_node_by_fullname(text[:-1])
if node:
completions.extend(node.child(i).text(0) for i in range(node.childCount()))
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:
node = find_exact_node(path_parts)
if node:
for i in range(node.childCount()):
child = node.child(i)
name = child.text(0)
# Оптимизируем split_path - кэширование
name_parts = child.data(0, Qt.UserRole + 10)
if name_parts is None:
name_parts = self.split_path(name)
child.setData(0, Qt.UserRole + 10, name_parts)
if not name_parts:
continue
last_part = name_parts[-1].lower()
if prefix == '' or prefix in last_part: # здесь изменено
completions.append(name)
self.completer.complete()
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
if not self._bckspc_pressed:
self.search_input.setText(text)
self.search_input.setCursorPosition(len(text))
self.run_completions(text)
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:
self.manual_completion_active = True
text = self.search_input.text().strip()
self.run_completions(text)
elif event.key() == Qt.Key_Escape:
# Esc — выключаем ручной режим и скрываем подсказки, если autocomplete выключен
if not self.autocomplete_checkbox.isChecked():
self.manual_completion_active = False
self.completer.popup().hide()
return True
if event.key() == Qt.Key_Backspace:
self._bckspc_pressed = True
else:
self._bckspc_pressed = False
return super().eventFilter(obj, event)
def run_completions(self, text):
completions = self.update_completions(text)
if not self.autocomplete_checkbox.isChecked() and self._bckspc_pressed:
text = text[:-1]
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(text + '->'):
separator += '->'
elif rest.startswith(text + '.'):
separator += '.'
elif '[' in rest:
separator += '[' # для массивов
else:
separator += '.' # fallback
if not self._bckspc_pressed:
self.search_input.setText(text + separator)
completions = self.update_completions(text)
self.completer.setModel(QStringListModel(completions))
self.completer.complete()
return True
# Иначе просто показываем подсказки
self.completer.setModel(QStringListModel(completions))
if completions:
self.completer.complete()
return True
def on_search_text_changed(self, text):
self.filter_tree()
if text == None:
text = self.search_input.text().strip()
if self.autocomplete_checkbox.isChecked():
self.run_completions(text)
else:
# Если выключено, показываем подсказки только если флаг ручного вызова True
if self.manual_completion_active:
self.run_completions(text)
else:
self.completer.popup().hide()
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
root, tree = myXML.safe_parse_xml(self.xml_path)
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.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:
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
root, tree = myXML.safe_parse_xml(self.xml_path)
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.table.populate(self.all_vars, self.structs, None)
# Проверка пути к 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
root, tree = myXML.safe_parse_xml(self.xml_path)
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]
# Удаляем из expanded_vars (тоже глобально)
def filter_out_selected(vars_list):
filtered = []
for v in vars_list:
if v['name'] not in selected_names:
# Рекурсивно фильтруем детей, если есть
if 'children' in v:
v = v.copy()
v['children'] = filter_out_selected(v['children'])
filtered.append(v)
return filtered
self.expanded_vars[:] = filter_out_selected(self.expanded_vars)
if removed_any:
myXML.fwrite(root, self.xml_path)
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
def filter_vars(vars_list, path_parts):
"""Рекурсивно фильтруем vars_list по path_parts и возвращаем только подходящие."""
filtered = []
def matches_path(name, search_parts):
name_parts = name.lower().split('.')
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
for var in vars_list:
fullname = var.get('fullname', var['name']) # желательно иметь полное имя
# Если фильтра нет — берем всё
if not path_parts or matches_path(fullname, path_parts):
# Копируем узел с рекурсией по детям
new_var = var.copy()
if 'children' in var:
new_var['children'] = filter_vars(var['children'], path_parts)
filtered.append(new_var)
else:
# Но даже если этот узел не подходит, может подойти его потомок
if 'children' in var:
child_filtered = filter_vars(var['children'], path_parts)
if child_filtered:
new_var = var.copy()
new_var['children'] = child_filtered
filtered.append(new_var)
return filtered