+сделано задание размера короткого имени + добавлена бета поддержка stm:+ - парс переменных из файла преокта Keil или makefile CubeIDE - запись в utf-8 для STM, вместо cp1251 для TMS - другой размер int (32 бита, вместо 16 бит) для STM
462 lines
19 KiB
Python
462 lines
19 KiB
Python
from PySide2.QtWidgets import (
|
||
QTableWidget, QTableWidgetItem, QCheckBox, QComboBox, QLineEdit, QCompleter,
|
||
QAbstractItemView, QHeaderView, QLabel, QSpacerItem, QSizePolicy, QSpinBox,
|
||
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget
|
||
)
|
||
from PySide2.QtGui import QColor, QBrush, QPalette
|
||
from PySide2.QtCore import Qt, QSettings
|
||
from enum import IntEnum
|
||
from generate_debug_vars import type_map
|
||
import time
|
||
from typing import Dict, List
|
||
|
||
class rows(IntEnum):
|
||
No = 0
|
||
include = 1
|
||
name = 2
|
||
type = 3
|
||
pt_type = 4
|
||
iq_type = 5
|
||
ret_type = 6
|
||
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 SetSizeDialog(QDialog):
|
||
"""
|
||
Диалоговое окно для выбора числового значения (размера).
|
||
"""
|
||
def __init__(self, parent=None, initial_value=10, min_value=1, max_value=50, title="Укажите размер короткого имени"):
|
||
super().__init__(parent)
|
||
self.setWindowTitle(title)
|
||
self.setFixedSize(320, 120) # Задаем фиксированный размер для аккуратного вида
|
||
|
||
# Основной вертикальный макет
|
||
main_layout = QVBoxLayout(self)
|
||
|
||
# Макет для ввода значения
|
||
input_layout = QHBoxLayout()
|
||
label = QLabel("Количество символов:", self)
|
||
|
||
self.spin_box = QSpinBox(self)
|
||
self.spin_box.setRange(min_value, max_value) # Устанавливаем диапазон допустимых значений
|
||
initial_value = parent._shortname_size
|
||
self.spin_box.setValue(initial_value) # Устанавливаем начальное значение
|
||
self.spin_box.setFocus() # Устанавливаем фокус на поле ввода
|
||
|
||
input_layout.addWidget(label)
|
||
input_layout.addWidget(self.spin_box)
|
||
main_layout.addLayout(input_layout)
|
||
|
||
# Добавляем пустое пространство для лучшего разделения
|
||
main_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding))
|
||
|
||
# Макет для кнопок
|
||
btn_layout = QHBoxLayout()
|
||
btn_layout.addStretch() # Добавляем растягивающийся элемент, чтобы кнопки были справа
|
||
|
||
btn_ok = QPushButton("OK")
|
||
btn_cancel = QPushButton("Отмена")
|
||
|
||
btn_layout.addWidget(btn_ok)
|
||
btn_layout.addWidget(btn_cancel)
|
||
|
||
main_layout.addLayout(btn_layout)
|
||
|
||
# Подключение сигналов к слотам
|
||
btn_ok.clicked.connect(self.accept) # При нажатии "OK" диалог закроется со статусом "Accepted"
|
||
btn_cancel.clicked.connect(self.reject) # При нажатии "Отмена" - со статусом "Rejected"
|
||
|
||
def get_selected_size(self):
|
||
"""
|
||
Возвращает значение, выбранное в QSpinBox.
|
||
"""
|
||
return self.spin_box.value()
|
||
|
||
class CtrlScrollComboBox(QComboBox):
|
||
def wheelEvent(self, event):
|
||
if event.modifiers() & Qt.ControlModifier:
|
||
super().wheelEvent(event)
|
||
else:
|
||
event.ignore()
|
||
|
||
class VariableTableWidget(QTableWidget):
|
||
def __init__(self, parent=None):
|
||
super().__init__(0, 8, parent)
|
||
# Таблица переменных
|
||
self.setHorizontalHeaderLabels([
|
||
'№', # новый столбец
|
||
'En',
|
||
'Name',
|
||
'Origin Type',
|
||
'Base Type',
|
||
'IQ Type',
|
||
'Return Type',
|
||
'Short Name'
|
||
])
|
||
self.setEditTriggers(QAbstractItemView.AllEditTriggers)
|
||
self.var_list = []
|
||
# Инициализируем QSettings с именем организации и приложения
|
||
self.settings = QSettings("SET", "DebugVarEdit_VarTable")
|
||
# Восстанавливаем сохранённое состояние, если есть
|
||
shortsize = self.settings.value("shortname_size", True, type=int)
|
||
self._shortname_size = shortsize
|
||
|
||
self.type_options = list(dict.fromkeys(type_map.values()))
|
||
self.pt_types_all = [t.replace('pt_', '') for t in self.type_options]
|
||
self.iq_types_all = ['iq_none', 'iq'] + [f'iq{i}' for i in range(1, 31)]
|
||
# Задаём базовые iq-типы (без префикса 'iq_')
|
||
self.iq_types = ['iq_none', 'iq', 'iq10', 'iq15', 'iq19', 'iq24']
|
||
# Фильтруем типы из type_map.values() исключая те, что содержат 'arr' или 'ptr'
|
||
type_options = [t for t in dict.fromkeys(type_map.values()) if 'arr' not in t and 'ptr' not in t and
|
||
'struct' not in t and 'union' not in t and '64' not in t]
|
||
# Формируем display_type_options без префикса 'pt_'
|
||
self.pt_types = [t.replace('pt_', '') for t in 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()
|
||
# Для остальных колонок — растяжение (Stretch), чтобы они заняли всю оставшуюся ширину
|
||
|
||
for col in range(self.columnCount()):
|
||
if col == self.columnCount() - 1:
|
||
header.setSectionResizeMode(col, QHeaderView.Stretch)
|
||
else:
|
||
header.setSectionResizeMode(col, QHeaderView.Interactive)
|
||
|
||
parent_widget = self.parentWidget()
|
||
# Сделаем колонки с номерами фиксированной ширины
|
||
self.setColumnWidth(rows.No, 30)
|
||
self.setColumnWidth(rows.include, 30)
|
||
self.setColumnWidth(rows.pt_type, 85)
|
||
self.setColumnWidth(rows.iq_type, 85)
|
||
self.setColumnWidth(rows.ret_type, 85)
|
||
|
||
self.setColumnWidth(rows.name, 300)
|
||
self.setColumnWidth(rows.type, 100)
|
||
self._resizing = False
|
||
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):
|
||
self.var_list = vars_list
|
||
|
||
# --- ДО: удаляем отображение структур и union-переменных
|
||
for var in vars_list:
|
||
pt_type = var.get('pt_type', '')
|
||
if 'struct' in pt_type or 'union' in pt_type:
|
||
var['show_var'] = 'false'
|
||
var['enable'] = 'false'
|
||
|
||
|
||
filtered_vars = [v for v in vars_list if v.get('show_var', 'false') == 'true']
|
||
self.setRowCount(len(filtered_vars))
|
||
self.verticalHeader().setVisible(False)
|
||
style_with_padding = "padding-left: 5px; padding-right: 5px; font-size: 14pt; font-family: 'Segoe UI';"
|
||
|
||
|
||
|
||
|
||
for row, var in enumerate(filtered_vars):
|
||
# №
|
||
no_item = QTableWidgetItem(str(row))
|
||
no_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||
self.setItem(row, rows.No, no_item)
|
||
|
||
# Enable
|
||
cb = QCheckBox()
|
||
cb.setChecked(var.get('enable', 'false') == 'true')
|
||
cb.stateChanged.connect(on_change_callback)
|
||
cb.setStyleSheet(style_with_padding)
|
||
self.setCellWidget(row, rows.include, cb)
|
||
|
||
# Name
|
||
name_edit = QLineEdit(var['name'])
|
||
if var['type'] in structs:
|
||
completer = QCompleter(structs[var['type']].keys())
|
||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||
name_edit.setCompleter(completer)
|
||
name_edit.textChanged.connect(on_change_callback)
|
||
name_edit.setStyleSheet(style_with_padding)
|
||
self.setCellWidget(row, rows.name, name_edit)
|
||
|
||
# Origin Type (readonly)
|
||
origin_item = QTableWidgetItem(var.get('type', ''))
|
||
origin_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||
origin_item.setToolTip(var.get('type', '')) # Всплывающая подсказка
|
||
origin_item.setForeground(QBrush(Qt.black))
|
||
self.setItem(row, rows.type, origin_item)
|
||
|
||
# pt_type
|
||
pt_combo = CtrlScrollComboBox()
|
||
pt_combo.addItems(self.pt_types)
|
||
value = var['pt_type'].replace('pt_', '')
|
||
if value not in self.pt_types:
|
||
pt_combo.addItem(value)
|
||
pt_combo.setCurrentText(value)
|
||
pt_combo.currentTextChanged.connect(on_change_callback)
|
||
pt_combo.setStyleSheet(style_with_padding)
|
||
self.setCellWidget(row, rows.pt_type, pt_combo)
|
||
|
||
# iq_type
|
||
iq_combo = CtrlScrollComboBox()
|
||
iq_combo.addItems(self.iq_types)
|
||
value = var['iq_type'].replace('t_', '')
|
||
if value not in self.iq_types:
|
||
iq_combo.addItem(value)
|
||
iq_combo.setCurrentText(value)
|
||
iq_combo.currentTextChanged.connect(on_change_callback)
|
||
iq_combo.setStyleSheet(style_with_padding)
|
||
self.setCellWidget(row, rows.iq_type, iq_combo)
|
||
|
||
# return_type
|
||
ret_combo = CtrlScrollComboBox()
|
||
ret_combo.addItems(self.iq_types)
|
||
value = var['return_type'].replace('t_', '')
|
||
ret_combo.setCurrentText(value)
|
||
ret_combo.currentTextChanged.connect(on_change_callback)
|
||
ret_combo.setStyleSheet(style_with_padding)
|
||
self.setCellWidget(row, rows.ret_type, ret_combo)
|
||
|
||
# short_name
|
||
short_name_val = var.get('shortname', var['name'])
|
||
short_name_edit = QLineEdit(short_name_val)
|
||
short_name_edit.textChanged.connect(on_change_callback)
|
||
short_name_edit.setStyleSheet(style_with_padding)
|
||
self.setCellWidget(row, rows.short_name, short_name_edit)
|
||
|
||
self.check()
|
||
|
||
def check(self):
|
||
warning_color = QColor("#FFFACD")
|
||
error_color = QColor("#FFB6C1")
|
||
tooltip_shortname = "Short Name длиннее 10 символов — будет обрезано при генерации"
|
||
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()):
|
||
t1 = time.time()
|
||
name_widget = self.cellWidget(row, rows.name)
|
||
t2 = time.time()
|
||
name = name_widget.text() if name_widget else ""
|
||
|
||
short_name_edit = self.cellWidget(row, rows.short_name)
|
||
t3 = time.time()
|
||
shortname = short_name_edit.text() if short_name_edit else ""
|
||
|
||
long_shortname = len(shortname) > self._shortname_size
|
||
found = name in var_names_set
|
||
|
||
color = None
|
||
tooltip = ""
|
||
if not found:
|
||
color = error_color
|
||
tooltip = tooltip_missing
|
||
elif long_shortname:
|
||
color = warning_color
|
||
tooltip = tooltip_shortname
|
||
|
||
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):
|
||
result = []
|
||
for row in range(self.rowCount()):
|
||
cb = self.cellWidget(row, rows.include)
|
||
name = self.cellWidget(row, rows.name).text()
|
||
pt = self.cellWidget(row, rows.pt_type).currentText()
|
||
iq = self.cellWidget(row, rows.iq_type).currentText()
|
||
ret = self.cellWidget(row, rows.ret_type).currentText()
|
||
shortname = self.cellWidget(row, rows.short_name).text()
|
||
origin_type = self.item(row, rows.type).text()
|
||
|
||
result.append({
|
||
'show_var': True,
|
||
'enable': cb.isChecked(),
|
||
'name': name,
|
||
'pt_type': f'pt_{pt}',
|
||
'iq_type': f't_{iq}',
|
||
'return_type': f't_{ret}',
|
||
'shortname': shortname,
|
||
'type': origin_type,
|
||
})
|
||
return result
|
||
|
||
|
||
|
||
def on_header_clicked(self, logicalIndex):
|
||
if logicalIndex == rows.pt_type:
|
||
dlg = FilterDialog(self, self.pt_types_all, self._pt_type_filter, "Выберите базовые типы")
|
||
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})
|
||
|
||
elif logicalIndex == rows.short_name:
|
||
dlg = SetSizeDialog(self)
|
||
if dlg.exec_():
|
||
self._shortname_size = dlg.get_selected_size()
|
||
self.settings.setValue("shortname_size", self._shortname_size)
|
||
self.check()
|
||
|
||
|
||
|
||
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):
|
||
if self._resizing:
|
||
return # предотвращаем рекурсию
|
||
|
||
min_width = 50
|
||
delta = newSize - oldSize
|
||
right_index = logicalIndex + 1
|
||
|
||
if right_index >= self.columnCount():
|
||
# Если правая колока - нет соседа, ограничиваем минимальную ширину
|
||
if newSize < min_width:
|
||
self._resizing = True
|
||
self.setColumnWidth(logicalIndex, min_width)
|
||
self._resizing = False
|
||
return
|
||
|
||
self._resizing = True
|
||
try:
|
||
right_width = self.columnWidth(right_index)
|
||
new_right_width = right_width - delta
|
||
|
||
# Если соседняя колонка станет уже минимальной - подкорректируем левую
|
||
if new_right_width < min_width:
|
||
new_right_width = min_width
|
||
newSize = oldSize + (right_width - min_width)
|
||
self.setColumnWidth(logicalIndex, newSize)
|
||
|
||
self.setColumnWidth(right_index, new_right_width)
|
||
finally:
|
||
self._resizing = False
|
||
|
||
|
||
|
||
|
||
def highlight_row(self, row: int, color: QColor = None, tooltip: str = ""):
|
||
"""
|
||
Подсвечивает строку таблицы цветом `color`, не меняя шрифт.
|
||
Работает с QLineEdit, QComboBox, QCheckBox (включая обёртки).
|
||
Если `color=None`, сбрасывает подсветку.
|
||
"""
|
||
css_reset = "background-color: none; font: inherit;"
|
||
css_color = f"background-color: {color.name()};" if color else css_reset
|
||
|
||
for col in range(self.columnCount()):
|
||
item = self.item(row, col)
|
||
widget = self.cellWidget(row, col)
|
||
|
||
if item is not None:
|
||
current_bg = item.background().color() if item.background() else None
|
||
if color and current_bg != color:
|
||
item.setBackground(QBrush(color))
|
||
item.setToolTip(tooltip)
|
||
elif not color and current_bg is not None:
|
||
item.setBackground(QBrush(Qt.NoBrush))
|
||
item.setToolTip("")
|
||
elif widget is not None:
|
||
if widget.styleSheet() != css_color:
|
||
widget.setStyleSheet(css_color)
|
||
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):
|
||
selected_indexes = self.selectedIndexes()
|
||
selected_rows = set(index.row() for index in selected_indexes)
|
||
names = []
|
||
|
||
for row in selected_rows:
|
||
name_widget = self.cellWidget(row, rows.name)
|
||
if name_widget:
|
||
name = name_widget.text()
|
||
if name:
|
||
names.append(name)
|
||
return names
|