добавлен readme

другая библиотека для xml (более быстрая)
сделан выбор элементов в выпадающем списке типов
исправлено вроде кривое выставлениеп return_type
изменено окно выбора переменых
    - кнопки добавит, удалить заменены на применить
    - исправлены кривые подсказки в .exe версии
This commit is contained in:
Razvalyaev 2025-07-15 13:03:11 +03:00
parent 7b720cbdf4
commit cb496bca0f
13 changed files with 17330 additions and 17052 deletions

Binary file not shown.

136
README.md Normal file
View File

@ -0,0 +1,136 @@
# DebugVarEdit — Утилита для генерации отладочных переменных C-проекта
**DebugVarEdit** — графическое приложение для Windows, предназначенное для настройки и генерации отладочных переменных (`debug_vars.c`) на основе исходного C-проекта. Работает с `makefile` проекта, сохраняет изменения в XML и позволяет удобно редактировать переменные и их типы через интерфейс.
Программа — один исполняемый файл `DebugVarEdit.exe`, не требующий установки и дополнительных зависимостей.
> Требуется Windows 7 или новее.
---
## Как использовать
1. Запустите **DebugVarEdit.exe.**
2. Укажите пути:
- **Путь к XML** — новый или существующий файл настроек переменных;
- **Путь к проекту** — путь к корню проека (папка, которая является корнем для проекта в Code Composer);
- **Путь к makefile** — путь к `makefile` относительно корня проекта;
- **Путь для debug_vars.c** — папка, куда будет сгенерирован файл `debug_vars.c` с переменными.
3. Нажмите **Сканировать переменные**:
- Программа проанализирует исходники, указанные в makefile, и найдёт все переменные.
- Результат сохранится в двух XML-файлах:
- `structs.xml` — информация обо всех структурах и typedef;
- `<ваш_файл>.xml` — список всех найденных переменных.
4. Нажмите **Добавить переменные**:
- В **левой таблице** отображаются все найденные переменные.
Для добавления переменной в проект дважды кликните по ней или нажмите кнопку `>`, чтобы переместить в правую таблицу.
- В **правой таблице** находятся переменные, выбранные для использования.
Чтобы убрать переменную из проекта, переместите её обратно в левую таблицу двойным кликом или кнопкой `<`.
- После выбора переменных нажмите **Применить**, чтобы обновить основной список переменных и включить их в проект.
5. Настройте параметры выбранных переменных:
- **En** — включение или отключение переменной для генерации;
- **Base Type** — базовый тип переменной (например, int8, uint16 и т.д.);
- **IQ Type** — формат IQ, если применимо;
- **Return Type** — формат возвращаемого значения (IQ-тип или целочисленный);
- **Shortname** — короткое имя переменной для для терминалки.
Все параметры выбираются из выпадающих списков, которые можно настроить, чтобы отображались только нужные опции.
6. Нажмите **Сгенерировать файл** для создания файла `debug_vars.c` с выбранными переменными.
---
## Возможности
- Загрузка и сохранение настроек переменных в XML-файлах.
- Автоматическое определение исходных файлов с переменными для удобства работы.
- Редактирование переменных: включение, короткого имени и типов через удобные списки.
- Подсветка ошибок при вводе (неправильные имена, слишком длинные короткие имена).
- Быстрая фильтрация переменных по столбцам.
- Автоматическая генерация файла debug_vars.c с выбранными переменными.
- Возможность сразу открыть сгенерированный файл в редакторе.
- Умное автодополнение имён переменных и полей структур.
---
## Пример XML-файла
```xml
<project proj_path="C:/myproj" makefile_path="Debug/Makefile" structs_path="debugVars/structs.xml">
<variables>
<var name="g_myvar">
<enable>true</enable>
<show_var>true</show_var>
<shortname>myv</shortname>
<pt_type>pt_float</pt_type>
<iq_type>t_iq24</iq_type>
<return_type>t_iq24</return_type>
<type>float</type>
<file>Src/main/main.c</file>
<extern>true</extern>
<static>false</static>
</var>
</variables>
</project>
```
---
# Для разработчиков
### Структура проекта:
```bash
Src
├── DebugVarEdit_GUI.py # Главное окно
├── VariableTable.py # Таблица выбранных переменных
├── VariableSelector.py # Окно выбора переменных
├── selectTable.py # Таблица переменных в окне выбора переменных
├── scanVarGUI.py # Отображение процесса сканирования переменных
├── scanVar.py # Сканирование переменных среди .c/.h файлов
├── generateVars.py # Генерация debug_vars.c
├── myXML.py # Утилиты для XML
├── parseMakefile.py # Парсинг makefile на .c/.h файлы
├── setupVars.py # Подготовка переменных для окна выбора переменных
├── libclang.dll # Бибилиотека clang
├── icon.ico # Иконка
build
├── build_and_clean.py # Билдинг проекта в .exe (через nuitka или pyinstaller)
```
### Зависимости
Для запуска приложения:
- **Python 3.7+**
- **clang** — используется для парсинга C-кода (требуется `libclang.dll`)
- **PySide2** — GUI-фреймворк
- **lxml** — работа с XML
> Python 3.7 и PySide2 рекомендуется для совместимости с Windows 7
Для сборки `.exe`:
- **Nuitka** — создает полностью автономный `.exe` без внешних зависимостей
- **PyInstaller** — создает `.exe` с зависимостью от `pythonXX.dll` (Python должен быть установлен)
### Сборка:
Если вы хотите собрать `DebugVarEdit.exe` самостоятельно из исходников, используйте скрипт **build/build_and_clean.py**. Он автоматически собирает проект с помощью Nuitka или PyInstaller:
- Собирает проект в `DebugVarEdit.exe` в корневой папке.
- Включает:
- все необходимые `.dll` (например, `libclang.dll`),
- иконку (`icon.ico`),
- плагины PySide2.
- Очищает временные папки после сборки:
- `build_temp`
- `__pycache__`
- `DebugVarEdit_GUI.*`
> Все пути, имена файлов, временные папки и выбор между Nuitka и PyInstaller можно настроить в начале файла `build_and_clean.py`.
### Установка зависимостей
```bash
pip install PySide2 lxml nuitka pyinstaller
```

View File

@ -4,7 +4,7 @@
import sys import sys
import os import os
import subprocess import subprocess
import xml.etree.ElementTree as ET import lxml.etree as ET
from generateVars import type_map from generateVars import type_map
from enum import IntEnum from enum import IntEnum
import threading import threading
@ -31,12 +31,18 @@ var_edit_title = "Редактор переменных для отладки"
xml_path_title = "Путь к XML:" xml_path_title = "Путь к XML:"
proj_path_title = "Путь к проекту:" proj_path_title = "Путь к проекту:"
makefile_path_title = "Пусть к makefile (относительно проекта)" makefile_path_title = "Пусть к makefile (относительно проекта)"
output_path_title = "Папка для debug_vars.c:" output_path_title = "Путь для для debug_vars.c:"
scan_title = "Сканировать переменные" scan_title = "Сканировать переменные"
build_title = "Сгенерировать файл" build_title = "Сгенерировать файл"
add_vars_title = "Добавить переменные" add_vars_title = "Добавить переменные"
open_output_title = "Открыть файл" open_output_title = "Открыть файл"
def set_sub_elem_text(parent, tag, text):
el = parent.find(tag)
if el is None:
el = ET.SubElement(parent, tag)
el.text = str(text)
# 3. UI: таблица с переменными # 3. UI: таблица с переменными
class VarEditor(QWidget): class VarEditor(QWidget):
def __init__(self): def __init__(self):
@ -242,8 +248,8 @@ class VarEditor(QWidget):
QMessageBox.critical(self, "Ошибка при генерации", str(e)) QMessageBox.critical(self, "Ошибка при генерации", str(e))
def update(self): def update(self, force=0):
if self._updating: if self._updating and (force==0):
return # Уже в процессе обновления — выходим, чтобы избежать рекурсии return # Уже в процессе обновления — выходим, чтобы избежать рекурсии
self._updating = True self._updating = True
@ -407,8 +413,7 @@ class VarEditor(QWidget):
return return
try: try:
self.update_all_paths() self.update(1)
self.update()
except Exception as e: except Exception as e:
self.makefile_path = None self.makefile_path = None
@ -443,9 +448,20 @@ class VarEditor(QWidget):
self.write_to_xml() self.write_to_xml()
self.update() self.update()
def write_to_xml(self, dummy=None): def write_to_xml(self, dummy=None):
t0 = time.time()
self.update_all_paths() self.update_all_paths()
def get_val(name, default=''):
return str(v_table[name] if v_table and name in v_table else v.get(name, default))
def element_differs(elem, values: dict):
for tag, new_val in values.items():
current_elem = elem.find(tag)
current_val = (current_elem.text or '').strip()
new_val_str = str(new_val or '').strip()
if current_val != new_val_str:
return True
return False
if not self.xml_path or not os.path.isfile(self.xml_path): if not self.xml_path or not os.path.isfile(self.xml_path):
print("XML файл не найден или путь пустой") print("XML файл не найден или путь пустой")
@ -470,7 +486,6 @@ class VarEditor(QWidget):
if self.makefile_path and os.path.isfile(self.makefile_path): if self.makefile_path and os.path.isfile(self.makefile_path):
rel_makefile = myXML.make_relative_path(self.makefile_path, self.proj_path) rel_makefile = myXML.make_relative_path(self.makefile_path, self.proj_path)
root.set("makefile_path", rel_makefile)
# Если результат — абсолютный путь, не записываем # Если результат — абсолютный путь, не записываем
if not os.path.isabs(rel_makefile): if not os.path.isabs(rel_makefile):
root.set("makefile_path", rel_makefile) root.set("makefile_path", rel_makefile)
@ -481,6 +496,7 @@ class VarEditor(QWidget):
if not os.path.isabs(rel_struct): if not os.path.isabs(rel_struct):
root.set("structs_path", rel_struct) root.set("structs_path", rel_struct)
t1 = time.time()
vars_elem = root.find('variables') vars_elem = root.find('variables')
if vars_elem is None: if vars_elem is None:
@ -495,6 +511,7 @@ class VarEditor(QWidget):
'extern': var_elem.findtext('extern', ''), 'extern': var_elem.findtext('extern', ''),
'static': var_elem.findtext('static', '') 'static': var_elem.findtext('static', '')
} }
var_elements_by_name = {ve.attrib.get('name'): ve for ve in vars_elem.findall('var')}
# Читаем переменные из таблицы (активные/изменённые) # Читаем переменные из таблицы (активные/изменённые)
table_vars = {v['name']: v for v in self.table.read_data()} table_vars = {v['name']: v for v in self.table.read_data()}
@ -503,63 +520,73 @@ class VarEditor(QWidget):
# Объединённый список переменных для записи # Объединённый список переменных для записи
all_names = list(all_vars_by_name.keys()) all_names = list(all_vars_by_name.keys())
t2 = time.time()
for name in all_names: for name in all_names:
v = all_vars_by_name[name] v = all_vars_by_name[name]
v_table = table_vars.get(name) v_table = table_vars.get(name)
var_elem = None var_elem = None
# Ищем уже существующий <var> в XML pt_type_val = get_val('pt_type').lower()
for ve in vars_elem.findall('var'):
if ve.attrib.get('name') == name:
var_elem = ve
break
if var_elem is None:
var_elem = ET.SubElement(vars_elem, 'var', {'name': name})
def set_sub_elem_text(parent, tag, text):
el = parent.find(tag)
if el is None:
el = ET.SubElement(parent, tag)
el.text = str(text)
pt_type_val = v_table['pt_type'] if v_table and 'pt_type' in v_table else v.get('pt_type', '')
if 'arr' in pt_type_val or 'struct' in pt_type_val or 'union' in pt_type_val: if 'arr' in pt_type_val or 'struct' in pt_type_val or 'union' in pt_type_val:
continue continue
show_var_val = str(v.get('show_var', 'false')).lower() show_var_val = str(v.get('show_var', 'false')).lower()
enable_val = str(v_table['enable'] if v_table and 'enable' in v_table else v.get('enable', 'false')).lower() enable_val = get_val('enable').lower()
set_sub_elem_text(var_elem, 'show_var', show_var_val)
set_sub_elem_text(var_elem, 'enable', enable_val)
# Тут подтягиваем из таблицы, если есть, иначе из v # Тут подтягиваем из таблицы, если есть, иначе из v
shortname_val = v_table['shortname'] if v_table and 'shortname' in v_table else v.get('shortname', '') shortname_val = get_val('shortname')
iq_type_val = v_table['iq_type'] if v_table and 'iq_type' in v_table else v.get('iq_type', '') iq_type_val = get_val('iq_type')
ret_type_val = v_table['return_type'] if v_table and 'return_type' in v_table else v.get('return_type', '') ret_type_val = get_val('return_type')
set_sub_elem_text(var_elem, 'shortname', shortname_val)
set_sub_elem_text(var_elem, 'pt_type', pt_type_val)
set_sub_elem_text(var_elem, 'iq_type', iq_type_val)
set_sub_elem_text(var_elem, 'return_type', ret_type_val)
set_sub_elem_text(var_elem, 'type', v.get('type', ''))
# file/extern/static: из original_info, либо из v # file/extern/static: из original_info, либо из v
file_val = v.get('file') or original_info.get(name, {}).get('file', '') file_val = v.get('file') or original_info.get(name, {}).get('file', '')
extern_val = v.get('extern') or original_info.get(name, {}).get('extern', '') extern_val = v.get('extern') or original_info.get(name, {}).get('extern', '')
static_val = v.get('static') or original_info.get(name, {}).get('static', '') static_val = v.get('static') or original_info.get(name, {}).get('static', '')
set_sub_elem_text(var_elem, 'file', file_val) values_to_write = {
set_sub_elem_text(var_elem, 'extern', extern_val) 'show_var': show_var_val,
set_sub_elem_text(var_elem, 'static', static_val) 'enable': enable_val,
'shortname': shortname_val,
'pt_type': pt_type_val,
'iq_type': iq_type_val,
'return_type': ret_type_val,
'type': v.get('type', ''),
'file': file_val,
'extern': extern_val,
'static': static_val
}
# Ищем уже существующий <var> в XML
var_elem = var_elements_by_name.get(name)
# Если элемента нет, это новая переменная — сразу пишем
if var_elem is None:
var_elem = ET.SubElement(vars_elem, 'var', {'name': name})
var_elements_by_name[name] = var_elem
write_all = True # обязательно записать все поля
else:
write_all = element_differs(var_elem, values_to_write)
if not write_all:
continue # Пропускаем, если нет изменений
for tag, text in values_to_write.items():
set_sub_elem_text(var_elem, tag, text)
t3 = time.time()
# Преобразуем дерево в строку # Преобразуем дерево в строку
self.table.check()
myXML.fwrite(root, self.xml_path) myXML.fwrite(root, self.xml_path)
self.table.check()
t4 = time.time()
'''print(f"[T1] parse + set paths: {t1 - t0:.3f} сек")
print(f"[T2] prepare variables: {t2 - t1:.3f} сек")
print(f"[T3] loop + updates: {t3 - t2:.3f} сек")
print(f"[T4] write to file: {t4 - t3:.3f} сек")
print(f"[TOTAL] write_to_xml total: {t4 - t0:.3f} сек")'''
except Exception as e: except Exception as e:
print(f"Ошибка при сохранении XML: {e}") print(f"Ошибка при сохранении XML: {e}")
def __open_output_file_with_program(self): def __open_output_file_with_program(self):
output_path = self.get_output_path() output_path = self.get_output_path()
if not output_path: if not output_path:

View File

@ -1,5 +1,5 @@
import re import re
import xml.etree.ElementTree as ET import lxml.etree as ET
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QPushButton, QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QPushButton,
QLineEdit, QLabel, QHeaderView, QCompleter, QCheckBox, QHBoxLayout, QSizePolicy QLineEdit, QLabel, QHeaderView, QCompleter, QCheckBox, QHBoxLayout, QSizePolicy
@ -52,12 +52,15 @@ class VariableSelectorDialog(QDialog):
self.btn_left.clicked.connect(self.on_move_left) self.btn_left.clicked.connect(self.on_move_left)
# Создаем кнопки, они остаются в диалоге # Создаем кнопки, они остаются в диалоге
self.btn_add = QPushButton("Добавить выбранные") self.btn_accept = QPushButton("Применить")
self.btn_delete = QPushButton("Удалить выбранные")
# Создаем экземпляр вашего готового виджета # Создаем экземпляр вашего готового виджета
self.vars_widget = selectTable.VariableSelectWidget(self) self.vars_widget = selectTable.VariableSelectWidget(self)
self.vars_widget.tree.itemDoubleClicked.connect(self.on_left_tree_double_click)
self.vars_widget.setObjectName("LeftTable")
self.selected_vars_widget = selectTable.VariableSelectWidget(self) self.selected_vars_widget = selectTable.VariableSelectWidget(self)
self.selected_vars_widget.tree.itemDoubleClicked.connect(self.on_rigth_tree_double_click)
self.selected_vars_widget.setObjectName("RightTable")
# --- Лэйауты --- # --- Лэйауты ---
@ -86,8 +89,7 @@ class VariableSelectorDialog(QDialog):
# Кнопки "Добавить выбранные" и "Удалить выбранные" под таблицами # Кнопки "Добавить выбранные" и "Удалить выбранные" под таблицами
buttons_layout = QVBoxLayout() buttons_layout = QVBoxLayout()
buttons_layout.addWidget(self.btn_add) buttons_layout.addWidget(self.btn_accept)
buttons_layout.addWidget(self.btn_delete)
main_layout.addLayout(buttons_layout) main_layout.addLayout(buttons_layout)
# Важно, если окно — QDialog или QWidget, установи layout # Важно, если окно — QDialog или QWidget, установи layout
@ -96,8 +98,7 @@ class VariableSelectorDialog(QDialog):
# Соединяем сигналы кнопок с методами диалога # Соединяем сигналы кнопок с методами диалога
self.btn_add.clicked.connect(self.on_add_clicked) self.btn_accept.clicked.connect(self.on_apply_clicked)
self.btn_delete.clicked.connect(self.on_delete_clicked)
# Соединяем чекбокс с методом виджета # Соединяем чекбокс с методом виджета
self.autocomplete_checkbox.stateChanged.connect(self.vars_widget.set_autocomplete) self.autocomplete_checkbox.stateChanged.connect(self.vars_widget.set_autocomplete)
@ -119,6 +120,7 @@ class VariableSelectorDialog(QDialog):
for var in data: for var in data:
if var['name'] in selected: if var['name'] in selected:
var['show_var'] = 'true' var['show_var'] = 'true'
var['enable'] = 'true'
if 'children' in var: if 'children' in var:
mark_selected_show_var(var['children']) mark_selected_show_var(var['children'])
mark_selected_show_var(self.expanded_vars) mark_selected_show_var(self.expanded_vars)
@ -159,15 +161,15 @@ class VariableSelectorDialog(QDialog):
t5 = time.perf_counter() t5 = time.perf_counter()
self.selected_vars_widget.filter_tree() self.selected_vars_widget.filter_tree()
def on_apply_clicked(self):
def on_add_clicked(self): # Получаем имена всех переменных из правой таблицы (selected_vars_widget)
# Получаем все переменные из правой таблицы (selected_vars_widget) right_var_names = set(self.selected_vars_widget.get_all_var_names())
var_names = self.selected_vars_widget.get_all_var_names()
all_items = self.selected_vars_widget.get_all_items() all_items = self.selected_vars_widget.get_all_items()
if not all_items: if not all_items:
return return
def add_to_var_map_recursively(item): # Устанавливаем show_var=true и enable=true для переменных из правой таблицы
def add_or_update_var(item):
name = item.text(0) name = item.text(0)
type_str = item.text(1) type_str = item.text(1)
@ -180,9 +182,15 @@ class VariableSelectorDialog(QDialog):
extern_val = item.data(0, Qt.UserRole + 2) extern_val = item.data(0, Qt.UserRole + 2)
static_val = item.data(0, Qt.UserRole + 3) static_val = item.data(0, Qt.UserRole + 3)
new_var = { new_var = {
'name': name, 'type': type_str, 'show_var': 'true', 'name': name,
'enable': 'true', 'shortname': name, 'pt_type': '', 'iq_type': '', 'type': type_str,
'return_type': 't_iq_none', 'file': file_val, 'show_var': 'true',
'enable': 'true',
'shortname': name,
'pt_type': '',
'iq_type': '',
'return_type': 't_iq_none',
'file': file_val,
'extern': str(extern_val).lower() if extern_val else 'false', 'extern': str(extern_val).lower() if extern_val else 'false',
'static': str(static_val).lower() if static_val else 'false', 'static': str(static_val).lower() if static_val else 'false',
} }
@ -190,71 +198,74 @@ class VariableSelectorDialog(QDialog):
self.var_map[name] = new_var self.var_map[name] = new_var
for item in all_items: for item in all_items:
add_to_var_map_recursively(item) add_or_update_var(item)
self.accept() # Сбрасываем show_var и enable у всех переменных, которых нет в правой таблице
for var in self.all_vars:
if var['name'] not in right_var_names:
var['show_var'] = 'false'
var['enable'] = 'false'
# Обновляем expanded_vars чтобы отразить новые show_var и enable
def update_expanded_vars(data):
for v in data:
name = v['name']
if name in self.var_map:
v['show_var'] = self.var_map[name]['show_var']
v['enable'] = self.var_map[name]['enable']
if 'children' in v:
update_expanded_vars(v['children'])
update_expanded_vars(self.expanded_vars)
def on_delete_clicked(self): # Обновляем отображение в виджетах
# Получаем все элементы (QTreeWidgetItem) из правой таблицы
all_items = self.selected_vars_widget.get_all_items()
if not all_items:
return
affected_names = []
def disable_var_recursively(item):
name = item.text(0)
if name in self.var_map:
self.var_map[name]['show_var'] = 'false'
self.var_map[name]['enable'] = 'false'
affected_names.append(name)
# Рекурсивно отключаем детей
for i in range(item.childCount()):
child = item.child(i)
disable_var_recursively(child)
for item in all_items:
disable_var_recursively(item)
# Обновляем XML и таблицу
self.update_xml_vars(affected_names, 'false', 'false')
self.update_vars_widget() self.update_vars_widget()
# Закрываем диалог
self.accept()
def update_xml_vars(self, names, show, enable):
"""Обновляет флаги show_var и enable в XML файле, только если у переменной нет вложенных структур."""
if not self.xml_path:
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'):
if var_elem.attrib.get('name') in names:
# Проверяем наличие вложенных структур или объединений
has_nested_structs = any(
var_elem.find(tag) is not None for tag in ('struct', 'union')
)
if not has_nested_structs:
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', show)
set_text('enable', enable)
myXML.fwrite(root, self.xml_path)
def save_checkbox_state(self): def save_checkbox_state(self):
self.settings.setValue("autocomplete_enabled", self.autocomplete_checkbox.isChecked()) self.settings.setValue("autocomplete_enabled", self.autocomplete_checkbox.isChecked())
# Обнови on_left_tree_double_click:
def on_left_tree_double_click(self, item, column):
selected_names = [item.text(0)]
if not selected_names:
return
def mark_selected_show_var(data):
for var in data:
if var['name'] in selected_names:
var['show_var'] = 'true'
var['enable'] = 'true'
if 'children' in var:
mark_selected_show_var(var['children'])
mark_selected_show_var(self.expanded_vars)
self.update_vars_widget()
# Добавь обработчик двойного клика справа (если нужно):
def on_rigth_tree_double_click(self, item, column):
selected_names = [item.text(0)]
if not selected_names:
return
def mark_selected_hide_var(data):
for var in data:
if var['name'] in selected_names:
var['show_var'] = 'false'
if 'children' in var:
mark_selected_hide_var(var['children'])
mark_selected_hide_var(self.expanded_vars)
self.update_vars_widget()
def keyPressEvent(self, event): def keyPressEvent(self, event):
if event.key() == Qt.Key_Delete: if event.key() == Qt.Key_Delete:
self.delete_selected_vars() self.delete_selected_vars()
@ -314,7 +325,6 @@ class VariableSelectorDialog(QDialog):
QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно удалить переменные.") QMessageBox.warning(self, "Ошибка", "Путь к XML не задан, невозможно удалить переменные.")
return return
import xml.etree.ElementTree as ET
root, tree = myXML.safe_parse_xml(self.xml_path) root, tree = myXML.safe_parse_xml(self.xml_path)
if root is None: if root is None:
return return

View File

@ -1,11 +1,14 @@
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QTableWidget, QTableWidgetItem, QCheckBox, QComboBox, QLineEdit, QCompleter, QTableWidget, QTableWidgetItem, QCheckBox, QComboBox, QLineEdit, QCompleter,
QAbstractItemView, QHeaderView, QLabel QAbstractItemView, QHeaderView, QLabel,
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget
) )
from PySide2.QtGui import QColor, QBrush, QPalette from PySide2.QtGui import QColor, QBrush, QPalette
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
from enum import IntEnum from enum import IntEnum
from generateVars import type_map from generateVars import type_map
import time
from typing import Dict, List
class rows(IntEnum): class rows(IntEnum):
No = 0 No = 0
@ -17,6 +20,46 @@ class rows(IntEnum):
ret_type = 6 ret_type = 6
short_name = 7 short_name = 7
class FilterDialog(QDialog):
def __init__(self, parent, options, selected, title="Выберите значения"):
super().__init__(parent)
self.setWindowTitle(title)
self.resize(250, 300)
self.selected = set(selected)
layout = QVBoxLayout(self)
scroll = QScrollArea(self)
scroll.setWidgetResizable(True)
container = QWidget()
scroll.setWidget(container)
self.checkboxes = []
vbox = QVBoxLayout(container)
for opt in options:
cb = QCheckBox(opt)
cb.setChecked(opt in self.selected)
vbox.addWidget(cb)
self.checkboxes.append(cb)
layout.addWidget(scroll)
btn_layout = QHBoxLayout()
btn_ok = QPushButton("OK")
btn_cancel = QPushButton("Отмена")
btn_layout.addWidget(btn_ok)
btn_layout.addWidget(btn_cancel)
layout.addLayout(btn_layout)
btn_ok.clicked.connect(self.accept)
btn_cancel.clicked.connect(self.reject)
def get_selected(self):
return [cb.text() for cb in self.checkboxes if cb.isChecked()]
class VariableTableWidget(QTableWidget): class VariableTableWidget(QTableWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
@ -27,7 +70,7 @@ class VariableTableWidget(QTableWidget):
'En', 'En',
'Name', 'Name',
'Origin Type', 'Origin Type',
'Pointer Type', 'Base Type',
'IQ Type', 'IQ Type',
'Return Type', 'Return Type',
'Short Name' 'Short Name'
@ -36,9 +79,18 @@ class VariableTableWidget(QTableWidget):
self.var_list = [] self.var_list = []
self.type_options = list(dict.fromkeys(type_map.values())) self.type_options = list(dict.fromkeys(type_map.values()))
self.display_type_options = [t.replace('pt_', '') for t in self.type_options] self.pt_types_all = [t.replace('pt_', '') for t in self.type_options]
self.iq_types = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)] self.iq_types_all = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)]
# Задаём базовые iq-типы (без префикса 'iq_')
self.iq_types = ['iq_none', 'iq24', 'iq19', 'iq15', 'iq10']
# Фильтруем типы из type_map.values() исключая те, что содержат 'arr' или 'ptr'
self.type_options = [t for t in set(type_map.values()) if 'arr' not in t and 'ptr' not in t]
# Формируем display_type_options без префикса 'pt_'
self.pt_types = [t.replace('pt_', '') for t in self.type_options]
self._iq_type_filter = list(self.iq_types) # Текущий фильтр iq типов (по умолчанию все)
self._pt_type_filter = list(self.pt_types)
self._ret_type_filter = list(self.iq_types)
header = self.horizontalHeader() header = self.horizontalHeader()
# Для остальных колонок — растяжение (Stretch), чтобы они заняли всю оставшуюся ширину # Для остальных колонок — растяжение (Stretch), чтобы они заняли всю оставшуюся ширину
@ -60,12 +112,13 @@ class VariableTableWidget(QTableWidget):
self.setColumnWidth(rows.type, 100) self.setColumnWidth(rows.type, 100)
self._resizing = False self._resizing = False
self.horizontalHeader().sectionResized.connect(self.on_section_resized) self.horizontalHeader().sectionResized.connect(self.on_section_resized)
self.horizontalHeader().sectionClicked.connect(self.on_header_clicked)
def populate(self, vars_list, structs, on_change_callback): def populate(self, vars_list, structs, on_change_callback):
self.var_list = vars_list self.var_list = vars_list
self.type_options = list(dict.fromkeys(type_map.values())) self.type_options = list(dict.fromkeys(type_map.values()))
self.display_type_options = [t.replace('pt_', '') for t in self.type_options] self.pt_types = [t.replace('pt_', '') for t in self.type_options]
iq_types = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)] iq_types = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)]
# --- ДО: удаляем отображение структур и union-переменных # --- ДО: удаляем отображение структур и union-переменных
@ -116,9 +169,9 @@ class VariableTableWidget(QTableWidget):
# pt_type # pt_type
pt_combo = QComboBox() pt_combo = QComboBox()
pt_combo.addItems(self.display_type_options) pt_combo.addItems(self.pt_types)
value = var['pt_type'].replace('pt_', '') value = var['pt_type'].replace('pt_', '')
if value not in self.display_type_options: if value not in self.pt_types:
pt_combo.addItem(value) pt_combo.addItem(value)
pt_combo.setCurrentText(value) pt_combo.setCurrentText(value)
pt_combo.currentTextChanged.connect(on_change_callback) pt_combo.currentTextChanged.connect(on_change_callback)
@ -135,14 +188,17 @@ class VariableTableWidget(QTableWidget):
iq_combo.setCurrentText(value) iq_combo.setCurrentText(value)
iq_combo.currentTextChanged.connect(on_change_callback) iq_combo.currentTextChanged.connect(on_change_callback)
iq_combo.setStyleSheet(style_with_padding) iq_combo.setStyleSheet(style_with_padding)
iq_combo.wheelEvent = lambda e: e.ignore()
self.setCellWidget(row, rows.iq_type, iq_combo) self.setCellWidget(row, rows.iq_type, iq_combo)
# return_type # return_type
ret_combo = QComboBox() ret_combo = QComboBox()
ret_combo.addItems(self.iq_types) ret_combo.addItems(self.iq_types)
ret_combo.setCurrentText(var.get('return_type', '')) value = var['return_type'].replace('t_', '')
ret_combo.setCurrentText(value)
ret_combo.currentTextChanged.connect(on_change_callback) ret_combo.currentTextChanged.connect(on_change_callback)
ret_combo.setStyleSheet(style_with_padding) ret_combo.setStyleSheet(style_with_padding)
ret_combo.wheelEvent = lambda e: e.ignore()
self.setCellWidget(row, rows.ret_type, ret_combo) self.setCellWidget(row, rows.ret_type, ret_combo)
# short_name # short_name
@ -155,25 +211,28 @@ class VariableTableWidget(QTableWidget):
self.check() self.check()
def check(self): def check(self):
warning_color = (QColor("#FFFACD")) # Жёлтый для длинных shortname warning_color = QColor("#FFFACD")
error_color = (QColor("#FFB6C1")) # Светло-красный для отсутствующих переменных error_color = QColor("#FFB6C1")
tooltip_shortname = "Short Name длиннее 10 символов — будет обрезано при генерации" tooltip_shortname = "Short Name длиннее 10 символов — будет обрезано при генерации"
tooltip_missing = f'Имя переменной не найдено среди переменных. Добавьте её через кнопку "Добавить переменные"' tooltip_missing = 'Имя переменной не найдено среди переменных. Добавьте её через кнопку "Добавить переменные"'
var_names_set = {v.get('name') for v in self.var_list if v.get('name')}
t0 = time.time()
self.setUpdatesEnabled(False)
for row in range(self.rowCount()): for row in range(self.rowCount()):
# Получаем имя переменной (столбец `name`) t1 = time.time()
name_widget = self.cellWidget(row, rows.name) name_widget = self.cellWidget(row, rows.name)
t2 = time.time()
name = name_widget.text() if name_widget else "" name = name_widget.text() if name_widget else ""
# Получаем shortname
short_name_edit = self.cellWidget(row, rows.short_name) short_name_edit = self.cellWidget(row, rows.short_name)
t3 = time.time()
shortname = short_name_edit.text() if short_name_edit else "" shortname = short_name_edit.text() if short_name_edit else ""
# Флаги ошибок
long_shortname = len(shortname) > 10 long_shortname = len(shortname) > 10
found = any(v.get('name') == name for v in self.var_list) found = name in var_names_set
# Выбираем цвет и подсказку
color = None color = None
tooltip = "" tooltip = ""
if not found: if not found:
@ -184,6 +243,10 @@ class VariableTableWidget(QTableWidget):
tooltip = tooltip_shortname tooltip = tooltip_shortname
self.highlight_row(row, color, tooltip) self.highlight_row(row, color, tooltip)
t4 = time.time()
self.setUpdatesEnabled(True)
#print(f"Row {row}: cellWidget(name) {t2-t1:.4f}s, cellWidget(shortname) {t3-t2:.4f}s, highlight_row {t4-t3:.4f}s")
def read_data(self): def read_data(self):
@ -209,6 +272,52 @@ class VariableTableWidget(QTableWidget):
}) })
return result return result
def on_header_clicked(self, logicalIndex):
if logicalIndex == rows.pt_type:
dlg = FilterDialog(self, self.pt_types_all, self._pt_type_filter, "Выберите Pointer Types")
if dlg.exec_():
self._pt_type_filter = dlg.get_selected()
self.update_comboboxes({rows.pt_type: self._pt_type_filter})
elif logicalIndex == rows.iq_type:
dlg = FilterDialog(self, self.iq_types_all, self._iq_type_filter, "Выберите IQ типы")
if dlg.exec_():
self._iq_type_filter = dlg.get_selected()
self.update_comboboxes({rows.iq_type: self._iq_type_filter})
elif logicalIndex == rows.ret_type:
dlg = FilterDialog(self, self.iq_types_all, self._ret_type_filter, "Выберите IQ типы")
if dlg.exec_():
self._ret_type_filter = dlg.get_selected()
self.update_comboboxes({rows.ret_type: self._ret_type_filter})
def update_comboboxes(self, columns_filters: Dict[int, List[str]]):
"""
Обновляет combobox-ячейки в указанных столбцах таблицы.
:param columns_filters: dict, где ключ индекс столбца,
значение список допустимых вариантов для combobox.
"""
for row in range(self.rowCount()):
for col, allowed_items in columns_filters.items():
combo = self.cellWidget(row, col)
if combo:
current = combo.currentText()
combo.blockSignals(True)
combo.clear()
combo.addItems(allowed_items)
if current in allowed_items:
combo.setCurrentText(current)
else:
combo.setCurrentIndex(0)
combo.blockSignals(False)
def on_section_resized(self, logicalIndex, oldSize, newSize): def on_section_resized(self, logicalIndex, oldSize, newSize):
if self._resizing: if self._resizing:
return # предотвращаем рекурсию return # предотвращаем рекурсию
@ -256,20 +365,22 @@ class VariableTableWidget(QTableWidget):
item = self.item(row, col) item = self.item(row, col)
widget = self.cellWidget(row, col) widget = self.cellWidget(row, col)
# Подсветка обычной item-ячейки (например, тип переменной)
if item is not None: if item is not None:
if color: current_bg = item.background().color() if item.background() else None
if color and current_bg != color:
item.setBackground(QBrush(color)) item.setBackground(QBrush(color))
item.setToolTip(tooltip) item.setToolTip(tooltip)
else: elif not color and current_bg is not None:
item.setBackground(QBrush(Qt.NoBrush)) item.setBackground(QBrush(Qt.NoBrush))
item.setToolTip("") item.setToolTip("")
# Подсветка виджетов — здесь главная доработка
elif widget is not None: elif widget is not None:
# Надёжная подсветка: через styleSheet if widget.styleSheet() != css_color:
widget.setStyleSheet(css_color) widget.setStyleSheet(css_color)
widget.setToolTip(tooltip if color else "") current_tip = widget.toolTip()
if color and current_tip != tooltip:
widget.setToolTip(tooltip)
elif not color and current_tip:
widget.setToolTip("")
def get_selected_var_names(self): def get_selected_var_names(self):
selected_indexes = self.selectedIndexes() selected_indexes = self.selectedIndexes()

View File

@ -6,7 +6,7 @@
import sys import sys
import os import os
import re import re
import xml.etree.ElementTree as ET import lxml.etree as ET
from pathlib import Path from pathlib import Path
from xml.dom import minidom from xml.dom import minidom
import myXML import myXML

View File

@ -1,5 +1,5 @@
import os import os
import xml.etree.ElementTree as ET from lxml import etree
def make_absolute_path(path, base_path): def make_absolute_path(path, base_path):
if not os.path.isabs(path) and os.path.isdir(base_path): if not os.path.isabs(path) and os.path.isdir(base_path):
@ -33,44 +33,38 @@ def make_relative_path(abs_path, base_path):
return abs_path.replace("\\", "/") return abs_path.replace("\\", "/")
def indent_xml(elem, level=0): def indent_xml(elem, level=0):
indent = " " # Убираем strip() — они медленные, и текст мы всё равно перезаписываем
i = "\n" + level * indent i = "\n" + level * " "
if len(elem): if len(elem):
if not elem.text or not elem.text.strip(): elem.text = elem.text or i + " "
elem.text = i + indent
for child in elem: for child in elem:
indent_xml(child, level + 1) indent_xml(child, level + 1)
if not elem.tail or not elem.tail.strip(): elem[-1].tail = elem[-1].tail or i
elem.tail = i
else: else:
if level and (not elem.tail or not elem.tail.strip()): elem.tail = elem.tail or i
elem.tail = i
def fwrite(root, xml_full_path): def fwrite(root, xml_full_path):
indent_xml(root) #indent_xml(root)
ET.ElementTree(root).write(xml_full_path, encoding="utf-8", xml_declaration=True) #ET.ElementTree(root).write(xml_full_path, encoding="utf-8", xml_declaration=True)
rough_string = etree.tostring(root, encoding="utf-8")
parsed = etree.fromstring(rough_string)
with open(xml_full_path, "wb") as f:
f.write(etree.tostring(parsed, pretty_print=True, encoding="utf-8", xml_declaration=True))
def safe_parse_xml(xml_path): def safe_parse_xml(xml_path):
"""
Безопасно парсит XML-файл.
Возвращает кортеж (root, tree) или (None, None) при ошибках.
"""
if not xml_path or not os.path.isfile(xml_path): if not xml_path or not os.path.isfile(xml_path):
print(f"Файл '{xml_path}' не найден или путь пустой") print(f"Файл '{xml_path}' не найден или путь пустой")
return None, None return None, None
try: try:
if os.path.getsize(xml_path) == 0: if os.path.getsize(xml_path) == 0:
return None, None return None, None
tree = etree.parse(xml_path)
tree = ET.parse(xml_path)
root = tree.getroot() root = tree.getroot()
return root, tree return root, tree
except etree .XMLSyntaxError as e:
except ET.ParseError as e:
print(f"Ошибка парсинга XML файла '{xml_path}': {e}") print(f"Ошибка парсинга XML файла '{xml_path}': {e}")
return None, None return None, None
except Exception as e: except Exception as e:

View File

@ -9,7 +9,7 @@ import re
import clang.cindex import clang.cindex
from clang import cindex from clang import cindex
from clang.cindex import Config from clang.cindex import Config
import xml.etree.ElementTree as ET import lxml.etree as ET
from xml.dom import minidom from xml.dom import minidom
from parseMakefile import parse_makefile from parseMakefile import parse_makefile
from collections import deque from collections import deque
@ -545,7 +545,7 @@ def read_vars_from_xml(xml_path):
'shortname': var_elem.findtext('shortname', name), 'shortname': var_elem.findtext('shortname', name),
'pt_type': var_elem.findtext('pt_type', ''), 'pt_type': var_elem.findtext('pt_type', ''),
'iq_type': var_elem.findtext('iq_type', ''), 'iq_type': var_elem.findtext('iq_type', ''),
'return_type': var_elem.findtext('return_type', 'pt_iq_none'), 'return_type': var_elem.findtext('return_type', 't_iq_none'),
'type': var_elem.findtext('type', 'unknown'), 'type': var_elem.findtext('type', 'unknown'),
'file': var_elem.findtext('file', ''), 'file': var_elem.findtext('file', ''),
'extern': get_bool('extern'), 'extern': get_bool('extern'),
@ -608,7 +608,7 @@ def generate_xml_output(proj_path, xml_path, unique_vars, h_files_needed, vars_n
'shortname': info.get('shortname', name), 'shortname': info.get('shortname', name),
'pt_type': info.get('pt_type', ''), 'pt_type': info.get('pt_type', ''),
'iq_type': info.get('iq_type', ''), 'iq_type': info.get('iq_type', ''),
'return_type': info.get('return_type', 'pt_iq_none'), 'return_type': info.get('return_type', 't_iq_none'),
'type': info.get('type', 'unknown'), 'type': info.get('type', 'unknown'),
'file': info.get('file', ''), 'file': info.get('file', ''),
'extern': info.get('extern', False), 'extern': info.get('extern', False),
@ -627,7 +627,7 @@ def generate_xml_output(proj_path, xml_path, unique_vars, h_files_needed, vars_n
ET.SubElement(var_elem, "shortname").text = info.get('shortname', name) ET.SubElement(var_elem, "shortname").text = info.get('shortname', name)
ET.SubElement(var_elem, "pt_type").text = info.get('pt_type', '') ET.SubElement(var_elem, "pt_type").text = info.get('pt_type', '')
ET.SubElement(var_elem, "iq_type").text = info.get('iq_type', '') ET.SubElement(var_elem, "iq_type").text = info.get('iq_type', '')
ET.SubElement(var_elem, "return_type").text = info.get('return_type', 'pt_iq_none') ET.SubElement(var_elem, "return_type").text = info.get('return_type', 't_iq_none')
ET.SubElement(var_elem, "type").text = info.get('type', 'unknown') ET.SubElement(var_elem, "type").text = info.get('type', 'unknown')
rel_file = make_relative_if_possible(info.get('file', ''), proj_path).replace("\\", "/") rel_file = make_relative_if_possible(info.get('file', ''), proj_path).replace("\\", "/")

View File

@ -63,82 +63,8 @@ def split_path(path):
return tokens return tokens
# Функция парсит имя с индексами в (базовое_имя, список_индексов) def is_lazy_item(item):
def parse_name_with_indices(name): return item.childCount() == 1 and item.child(0).text(0) == 'lazy_marker'
# Регулярка для имени и индексов:
# имя - подряд букв/цифр/подчёркиваний,
# индексы в квадратных скобках
base_match = re.match(r'^([a-zA-Z_]\w*)', name)
if not base_match:
return name, [] # если не совпало, возвращаем как есть
base_name = base_match.group(1)
indices = re.findall(r'\[(\d+)\]', name)
indices = list(map(int, indices))
return base_name, indices
def match_path_part(search_part, node_part):
# Если часть — индекс (вида [N]), требуем точное совпадение
if search_part.startswith('[') and search_part.endswith(']'):
return search_part == node_part
# Если search_part содержит '[', значит поиск неполный индекс
if '[' in search_part:
return node_part.startswith(search_part)
# Иначе — обычное имя: допускаем совпадение по префиксу
return node_part.startswith(search_part)
def show_matching_path(item, path_parts, level=0):
node_name = item.text(0).lower()
node_parts = split_path(node_name)
if 'project' in node_name:
a = 1
if level >= len(path_parts):
# Путь полностью пройден — показываем только этот узел (без раскрытия всех детей)
item.setHidden(False)
item.setExpanded(False)
return True
if level >= len(node_parts):
# Уровень поиска больше длины пути узла — скрываем
item.setHidden(False)
search_part = path_parts[level]
node_part = node_parts[level]
if search_part == node_part:
# Точное совпадение — показываем узел, идём вглубь только по совпадениям
item.setHidden(False)
matched_any = False
for i in range(item.childCount()):
child = item.child(i)
if show_matching_path(child, path_parts, level + 1):
matched_any = True
item.setExpanded(matched_any)
return matched_any or item.childCount() == 0
elif node_part.startswith(search_part):
# Неполное совпадение — показываем только этот узел, детей скрываем, не раскрываем
item.setHidden(False)
item.setExpanded(False)
return True
elif search_part in node_part and (level == len(path_parts)-1):
# Неполное совпадение — показываем только этот узел, детей скрываем, не раскрываем
item.setHidden(False)
item.setExpanded(False)
return True
else:
# Несовпадение — скрываем
item.setHidden(True)
return False
class VariableSelectWidget(QWidget): class VariableSelectWidget(QWidget):
@ -152,10 +78,10 @@ class VariableSelectWidget(QWidget):
self._vars_hash = None self._vars_hash = None
# --- UI Элементы --- # --- UI Элементы ---
self.search_input = QLineEdit() self.search_input = QLineEdit(self)
self.search_input.setPlaceholderText("Поиск...") self.search_input.setPlaceholderText("Поиск...")
self.tree = QTreeWidget() self.tree = QTreeWidget(self)
self.tree.setHeaderLabels(["Имя переменной", "Тип"]) self.tree.setHeaderLabels(["Имя переменной", "Тип"])
self.tree.setSelectionMode(QTreeWidget.ExtendedSelection) self.tree.setSelectionMode(QTreeWidget.ExtendedSelection)
self.tree.setRootIsDecorated(True) self.tree.setRootIsDecorated(True)
@ -166,7 +92,7 @@ class VariableSelectWidget(QWidget):
""") """)
self.tree.itemExpanded.connect(self.on_item_expanded) self.tree.itemExpanded.connect(self.on_item_expanded)
self.completer = QCompleter() self.completer = QCompleter(self)
self.completer.setCompletionMode(QCompleter.PopupCompletion) self.completer.setCompletionMode(QCompleter.PopupCompletion)
self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setCaseSensitivity(Qt.CaseInsensitive)
self.completer.setFilterMode(Qt.MatchContains) self.completer.setFilterMode(Qt.MatchContains)
@ -179,9 +105,10 @@ class VariableSelectWidget(QWidget):
layout.addWidget(self.tree) layout.addWidget(self.tree)
# --- Соединения --- # --- Соединения ---
self.search_input.textChanged.connect(self.on_search_text_changed) #self.search_input.textChanged.connect(self.on_search_text_changed)
self.search_input.textChanged.connect(lambda text: self.on_search_text_changed(text))
self.search_input.installEventFilter(self) self.search_input.installEventFilter(self)
self.completer.activated[str].connect(self.insert_completion) self.completer.activated[str].connect(lambda text: self.insert_completion(text))
# --- Публичные методы для управления виджетом снаружи --- # --- Публичные методы для управления виджетом снаружи ---
@ -221,7 +148,7 @@ class VariableSelectWidget(QWidget):
self.tree.setColumnWidth(0, 400) self.tree.setColumnWidth(0, 400)
def on_item_expanded(self, item): def on_item_expanded(self, item):
if self.is_lazy_item(item): if is_lazy_item(item):
item.removeChild(item.child(0)) item.removeChild(item.child(0))
var = item.data(0, Qt.UserRole + 100) var = item.data(0, Qt.UserRole + 100)
if var: if var:
@ -265,9 +192,59 @@ class VariableSelectWidget(QWidget):
item.setData(0, Qt.UserRole + 100, var) # Сохраняем var целиком item.setData(0, Qt.UserRole + 100, var) # Сохраняем var целиком
def show_matching_path(self, item, path_parts, level=0):
node_name = item.text(0).lower()
node_parts = split_path(node_name)
if 'project' in node_name:
a = 1
if level >= len(path_parts):
# Путь полностью пройден — показываем только этот узел (без раскрытия всех детей)
item.setHidden(False)
item.setExpanded(False)
return True
if level >= len(node_parts):
# Уровень поиска больше длины пути узла — скрываем
item.setHidden(False)
search_part = path_parts[level]
node_part = node_parts[level]
if search_part == node_part:
# Точное совпадение — показываем узел, идём вглубь только по совпадениям
item.setHidden(False)
matched_any = False
self.on_item_expanded(item)
for i in range(item.childCount()):
child = item.child(i)
if self.show_matching_path(child, path_parts, level + 1):
matched_any = True
item.setExpanded(matched_any)
return matched_any or item.childCount() == 0
elif node_part.startswith(search_part):
# Неполное совпадение — показываем только этот узел, детей скрываем, не раскрываем
item.setHidden(False)
item.setExpanded(False)
return True
elif search_part in node_part and (level == len(path_parts)-1):
# Неполное совпадение — показываем только этот узел, детей скрываем, не раскрываем
item.setHidden(False)
item.setExpanded(False)
return True
else:
# Несовпадение — скрываем
item.setHidden(True)
return False
def filter_tree(self): def filter_tree(self):
text = self.search_input.text().strip().lower() text = self.search_input.text().strip().lower()
print(f"[{self.objectName()}] Filtering tree with text: '{text}'")
path_parts = split_path(text) if text else [] path_parts = split_path(text) if text else []
if '.' not in text and '->' not in text and '[' not in text and text != '': if '.' not in text and '->' not in text and '[' not in text and text != '':
@ -282,7 +259,7 @@ class VariableSelectWidget(QWidget):
else: else:
for i in range(self.tree.topLevelItemCount()): for i in range(self.tree.topLevelItemCount()):
item = self.tree.topLevelItem(i) item = self.tree.topLevelItem(i)
show_matching_path(item, path_parts, 0) self.show_matching_path(item, path_parts, 0)
@ -381,6 +358,8 @@ class VariableSelectWidget(QWidget):
self.completer.setModel(QStringListModel(completions)) self.completer.setModel(QStringListModel(completions))
self.completer.complete() self.completer.complete()
print(f"[{self.objectName()}] Updating completions for text: '{text}' -> {len(completions)} completions found.")
print(f" Completions: {completions[:5]}") # Вывести первые 5 для проверки
return completions return completions
@ -488,6 +467,11 @@ class VariableSelectWidget(QWidget):
return True return True
def on_search_text_changed(self, text): def on_search_text_changed(self, text):
sender_widget = self.sender()
sender_name = sender_widget.objectName() if sender_widget else "Unknown Sender"
print(f"[{self.objectName()}] (Sender: {sender_name}) Search text changed: '{text}'")
self.completer.setWidget(self.search_input)
self.filter_tree() self.filter_tree()
if text == None: if text == None:
text = self.search_input.text().strip() text = self.search_input.text().strip()
@ -500,6 +484,18 @@ class VariableSelectWidget(QWidget):
else: else:
self.completer.popup().hide() self.completer.popup().hide()
def focusInEvent(self, event):
if self.completer.widget() != self.search_input:
self.completer.setWidget(self.search_input)
super().focusInEvent(event)
def _custom_focus_in_event(self, event):
# Принудительно установить виджет для completer при получении фокуса
if self.completer.widget() != self.search_input:
self.completer.setWidget(self.search_input)
super(QLineEdit, self.search_input).focusInEvent(event) # Вызвать оригинальный обработчик
def build_completion_list(self): def build_completion_list(self):
completions = [] completions = []
@ -593,9 +589,6 @@ class VariableSelectWidget(QWidget):
leaf_items.append(item) leaf_items.append(item)
return leaf_items return leaf_items
def is_lazy_item(self, item):
return item.childCount() == 1 and item.child(0).text(0) == 'lazy_marker'
def get_all_var_names(self): def get_all_var_names(self):
"""Возвращает имена всех конечных (leaf) переменных, исключая битовые поля и группы.""" """Возвращает имена всех конечных (leaf) переменных, исключая битовые поля и группы."""
@ -611,3 +604,7 @@ class VariableSelectWidget(QWidget):
"""Возвращает имена только конечных (leaf) переменных из выделенных.""" """Возвращает имена только конечных (leaf) переменных из выделенных."""
return [item.text(0) for item in self.get_selected_items() if item.text(0)] return [item.text(0) for item in self.get_selected_items() if item.text(0)]
def closeEvent(self, event):
self.completer.setWidget(None)
self.completer.deleteLater()
super().closeEvent(event)

View File

@ -1,7 +1,7 @@
import sys import sys
import os import os
import re import re
import xml.etree.ElementTree as ET import lxml.etree as ET
from generateVars import map_type_to_pt, get_iq_define, type_map from generateVars import map_type_to_pt, get_iq_define, type_map
from enum import IntEnum from enum import IntEnum
import scanVars import scanVars
@ -138,7 +138,7 @@ def parse_vars(filename, typedef_map=None):
'shortname': var.findtext('shortname', name), 'shortname': var.findtext('shortname', name),
'pt_type': pt_type, 'pt_type': pt_type,
'iq_type': iq_type, 'iq_type': iq_type,
'return_type': var.findtext('return_type', ''), 'return_type': var.findtext('return_type', 't_iq_none'),
'type': var_type, 'type': var_type,
'file': var.findtext('file', ''), 'file': var.findtext('file', ''),
'extern': var.findtext('extern', 'false') == 'true', 'extern': var.findtext('extern', 'false') == 'true',

View File

@ -2,13 +2,11 @@ import subprocess
import shutil import shutil
import os import os
from pathlib import Path from pathlib import Path
from PIL import Image # нужна библиотека Pillow для конвертации PNG в ICO
import PySide2 import PySide2
from PyInstaller.utils.hooks import collect_data_files from PyInstaller.utils.hooks import collect_data_files
# install: # install: pip install PySide2 lxml nuitka pyinstaller
# - PyInstaller # - PyInstaller
# - nuitka # - nuitka
# - Pillow
# - PySide2 # - PySide2
# - clang # - clang
@ -25,6 +23,11 @@ SPEC_PATH = WORK_PATH
ICON_PATH = SRC_PATH / "icon.png" ICON_PATH = SRC_PATH / "icon.png"
ICON_ICO_PATH = SRC_PATH / "icon.ico" ICON_ICO_PATH = SRC_PATH / "icon.ico"
TEMP_FOLDERS = [
"build_temp",
"__pycache__",
"DebugVarEdit_GUI\..*$"
]
# === Пути к DLL и прочим зависимостям === # === Пути к DLL и прочим зависимостям ===
LIBS = { LIBS = {
"libclang.dll": SRC_PATH / "libclang.dll" "libclang.dll": SRC_PATH / "libclang.dll"
@ -48,7 +51,7 @@ for name, path in LIBS.items():
print(f"WARNING: {path.name} не найден — он не будет включён в сборку") print(f"WARNING: {path.name} не найден — он не будет включён в сборку")
def clean_temp(): def clean_temp():
for folder in ["build_temp", "__pycache__", "DebugVarEdit_GUI.build", "DebugVarEdit_GUI.dist", "DebugVarEdit_GUI.onefile-build"]: for folder in TEMP_FOLDERS:
path = Path(folder) path = Path(folder)
if path.exists(): if path.exists():
shutil.rmtree(path, ignore_errors=True) shutil.rmtree(path, ignore_errors=True)

31780
structs.xml

File diff suppressed because it is too large Load Diff

1812
vars.xml

File diff suppressed because it is too large Load Diff