Carregar ficheiros para "src/gui"
This commit is contained in:
168
src/gui/map_comparison.py
Normal file
168
src/gui/map_comparison.py
Normal file
@ -0,0 +1,168 @@
|
||||
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QComboBox, QPushButton, QSpinBox, QCheckBox)
|
||||
from PySide6.QtCore import Qt
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.figure import Figure
|
||||
import numpy as np
|
||||
from core.file_handler import MapData
|
||||
|
||||
class MapComparisonWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.map1 = None
|
||||
self.map2 = None
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Controls
|
||||
controls = QHBoxLayout()
|
||||
|
||||
# View type selection
|
||||
view_layout = QHBoxLayout()
|
||||
view_layout.addWidget(QLabel("Visualização:"))
|
||||
self.view_type = QComboBox()
|
||||
self.view_type.addItems([
|
||||
"Lado a Lado",
|
||||
"Diferença",
|
||||
"Diferença %",
|
||||
"Sobreposição"
|
||||
])
|
||||
self.view_type.currentIndexChanged.connect(self.update_view)
|
||||
view_layout.addWidget(self.view_type)
|
||||
controls.addLayout(view_layout)
|
||||
|
||||
# Threshold controls
|
||||
threshold_layout = QHBoxLayout()
|
||||
threshold_layout.addWidget(QLabel("Limite de Diferença:"))
|
||||
self.threshold = QSpinBox()
|
||||
self.threshold.setRange(0, 100)
|
||||
self.threshold.setValue(5)
|
||||
self.threshold.setSuffix("%")
|
||||
self.threshold.valueChanged.connect(self.update_view)
|
||||
threshold_layout.addWidget(self.threshold)
|
||||
controls.addLayout(threshold_layout)
|
||||
|
||||
# Display options
|
||||
options_layout = QHBoxLayout()
|
||||
self.show_labels = QCheckBox("Mostrar Valores")
|
||||
self.show_labels.setChecked(True)
|
||||
self.show_labels.stateChanged.connect(self.update_view)
|
||||
options_layout.addWidget(self.show_labels)
|
||||
controls.addLayout(options_layout)
|
||||
|
||||
layout.addLayout(controls)
|
||||
|
||||
# Matplotlib figure
|
||||
self.figure = Figure(figsize=(10, 6))
|
||||
self.canvas = FigureCanvas(self.figure)
|
||||
layout.addWidget(self.canvas)
|
||||
|
||||
def set_maps(self, map1: MapData, map2: MapData):
|
||||
"""Set maps to compare"""
|
||||
self.map1 = map1
|
||||
self.map2 = map2
|
||||
self.update_view()
|
||||
|
||||
def update_view(self):
|
||||
"""Update the visualization"""
|
||||
if not self.map1 or not self.map2:
|
||||
return
|
||||
|
||||
self.figure.clear()
|
||||
view = self.view_type.currentText()
|
||||
|
||||
if view == "Lado a Lado":
|
||||
self._plot_side_by_side()
|
||||
elif view == "Diferença":
|
||||
self._plot_difference()
|
||||
elif view == "Diferença %":
|
||||
self._plot_percentage_difference()
|
||||
else: # Sobreposição
|
||||
self._plot_overlay()
|
||||
|
||||
self.canvas.draw()
|
||||
|
||||
def _plot_side_by_side(self):
|
||||
"""Plot maps side by side"""
|
||||
gs = self.figure.add_gridspec(1, 2)
|
||||
ax1 = self.figure.add_subplot(gs[0, 0])
|
||||
ax2 = self.figure.add_subplot(gs[0, 1])
|
||||
|
||||
# Plot first map
|
||||
im1 = ax1.imshow(self.map1.data, aspect='auto', cmap='viridis')
|
||||
self.figure.colorbar(im1, ax=ax1)
|
||||
ax1.set_title(f"{self.map1.name} (Original)")
|
||||
|
||||
# Plot second map
|
||||
im2 = ax2.imshow(self.map2.data, aspect='auto', cmap='viridis')
|
||||
self.figure.colorbar(im2, ax=ax2)
|
||||
ax2.set_title(f"{self.map2.name} (Modificado)")
|
||||
|
||||
if self.show_labels.isChecked():
|
||||
self._add_value_labels(ax1, self.map1.data)
|
||||
self._add_value_labels(ax2, self.map2.data)
|
||||
|
||||
def _plot_difference(self):
|
||||
"""Plot absolute difference between maps"""
|
||||
ax = self.figure.add_subplot(111)
|
||||
|
||||
diff = self.map2.data - self.map1.data
|
||||
im = ax.imshow(diff, aspect='auto', cmap='RdBu_r')
|
||||
self.figure.colorbar(im)
|
||||
|
||||
ax.set_title("Diferença Absoluta")
|
||||
|
||||
if self.show_labels.isChecked():
|
||||
self._add_value_labels(ax, diff)
|
||||
|
||||
def _plot_percentage_difference(self):
|
||||
"""Plot percentage difference between maps"""
|
||||
ax = self.figure.add_subplot(111)
|
||||
|
||||
# Calculate percentage difference
|
||||
diff_percent = np.zeros_like(self.map1.data)
|
||||
mask = self.map1.data != 0
|
||||
diff_percent[mask] = ((self.map2.data[mask] - self.map1.data[mask]) /
|
||||
np.abs(self.map1.data[mask])) * 100
|
||||
|
||||
# Apply threshold
|
||||
threshold = self.threshold.value()
|
||||
diff_percent[np.abs(diff_percent) < threshold] = 0
|
||||
|
||||
im = ax.imshow(diff_percent, aspect='auto', cmap='RdBu_r')
|
||||
self.figure.colorbar(im)
|
||||
|
||||
ax.set_title(f"Diferença Percentual (Limite: {threshold}%)")
|
||||
|
||||
if self.show_labels.isChecked():
|
||||
self._add_value_labels(ax, diff_percent, "%.1f%%")
|
||||
|
||||
def _plot_overlay(self):
|
||||
"""Plot maps overlaid with transparency"""
|
||||
ax = self.figure.add_subplot(111)
|
||||
|
||||
# Plot original map
|
||||
im1 = ax.imshow(self.map1.data, aspect='auto', cmap='viridis', alpha=0.5)
|
||||
|
||||
# Plot modified map with transparency
|
||||
im2 = ax.imshow(self.map2.data, aspect='auto', cmap='magma', alpha=0.5)
|
||||
|
||||
# Add separate colorbars
|
||||
self.figure.colorbar(im1, ax=ax, label='Original')
|
||||
self.figure.colorbar(im2, ax=ax, label='Modificado')
|
||||
|
||||
ax.set_title("Sobreposição de Mapas")
|
||||
|
||||
if self.show_labels.isChecked():
|
||||
self._add_value_labels(ax, self.map2.data)
|
||||
|
||||
def _add_value_labels(self, ax, data, fmt="%.2f"):
|
||||
"""Add value labels to the plot"""
|
||||
for i in range(data.shape[0]):
|
||||
for j in range(data.shape[1]):
|
||||
ax.text(j, i, fmt % data[i, j],
|
||||
ha='center', va='center',
|
||||
color='white', fontsize=8)
|
||||
507
src/gui/map_manager_dialog.py
Normal file
507
src/gui/map_manager_dialog.py
Normal file
@ -0,0 +1,507 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QComboBox, QSpinBox, QRadioButton, QGroupBox, QPushButton,
|
||||
QFormLayout, QDoubleSpinBox, QCheckBox, QTextEdit, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from core.file_handler import MapData
|
||||
import numpy as np
|
||||
|
||||
class MapManagerDialog(QDialog):
|
||||
def __init__(self, map_data: MapData, parent=None):
|
||||
super().__init__(parent)
|
||||
self.map_data = map_data
|
||||
self.map_viewer = None # Reference to map viewer
|
||||
self.active_address_field = None # Track which address field to update
|
||||
self.setWindowTitle("Map Manager")
|
||||
self.setMinimumWidth(800)
|
||||
self.setMinimumHeight(600)
|
||||
self.setup_ui()
|
||||
self.load_map_data()
|
||||
|
||||
def set_map_viewer(self, map_viewer):
|
||||
"""Set reference to map viewer for cursor position"""
|
||||
self.map_viewer = map_viewer
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Map Properties Group
|
||||
map_group = QGroupBox("Map properties")
|
||||
map_layout = QFormLayout()
|
||||
|
||||
# Name
|
||||
self.name_combo = QComboBox()
|
||||
self.name_combo.setEditable(True)
|
||||
self.name_combo.addItem(self.map_data.name)
|
||||
map_layout.addRow("Name:", self.name_combo)
|
||||
|
||||
# Start address
|
||||
addr_layout = QHBoxLayout()
|
||||
self.addr_edit = QLineEdit(self.map_data.address)
|
||||
self.addr_edit.textChanged.connect(self.on_address_changed)
|
||||
self.get_2d_btn = QPushButton("Get from 2D cursor")
|
||||
self.get_2d_btn.clicked.connect(self.on_get_2d_cursor)
|
||||
addr_layout.addWidget(self.addr_edit)
|
||||
addr_layout.addWidget(self.get_2d_btn)
|
||||
addr_layout.addStretch()
|
||||
map_layout.addRow("Start address:", addr_layout)
|
||||
|
||||
# Size
|
||||
size_layout = QHBoxLayout()
|
||||
self.cols_spin = QSpinBox()
|
||||
self.cols_spin.setRange(1, 100)
|
||||
self.cols_spin.setValue(self.map_data.cols)
|
||||
self.rows_spin = QSpinBox()
|
||||
self.rows_spin.setRange(1, 100)
|
||||
self.rows_spin.setValue(self.map_data.rows)
|
||||
size_layout.addWidget(self.cols_spin)
|
||||
size_layout.addWidget(QLabel("x"))
|
||||
size_layout.addWidget(self.rows_spin)
|
||||
size_layout.addStretch()
|
||||
map_layout.addRow("Size(col*row):", size_layout)
|
||||
|
||||
# Data Format
|
||||
self.format_combo = QComboBox()
|
||||
self.format_combo.addItems([
|
||||
"8-bit",
|
||||
"16-bit Lo-Hi",
|
||||
"16-bit Hi-Lo",
|
||||
"32-bit Lo-Hi",
|
||||
"32-bit Hi-Lo",
|
||||
"Float"
|
||||
])
|
||||
self.format_combo.setCurrentText(self.map_data.data_format)
|
||||
map_layout.addRow("Data Format:", self.format_combo)
|
||||
|
||||
# Options
|
||||
options_layout = QHBoxLayout()
|
||||
self.original_radio = QRadioButton("Original")
|
||||
self.difference_radio = QRadioButton("Difference")
|
||||
self.percent_radio = QRadioButton("Percent")
|
||||
self.sign_check = QCheckBox("Sign")
|
||||
self.swap_check = QCheckBox("Swap axis")
|
||||
self.hex_radio = QRadioButton("Hexadecimal")
|
||||
self.dec_radio = QRadioButton("Decimal")
|
||||
self.reciprocal_check = QCheckBox("Reciprocal")
|
||||
|
||||
options_layout.addWidget(self.original_radio)
|
||||
options_layout.addWidget(self.difference_radio)
|
||||
options_layout.addWidget(self.percent_radio)
|
||||
options_layout.addWidget(self.sign_check)
|
||||
options_layout.addWidget(self.swap_check)
|
||||
options_layout.addWidget(self.hex_radio)
|
||||
options_layout.addWidget(self.dec_radio)
|
||||
options_layout.addWidget(self.reciprocal_check)
|
||||
map_layout.addRow("", options_layout)
|
||||
|
||||
# Set initial option states
|
||||
if self.map_data.display_mode == "Original":
|
||||
self.original_radio.setChecked(True)
|
||||
elif self.map_data.display_mode == "Difference":
|
||||
self.difference_radio.setChecked(True)
|
||||
else:
|
||||
self.percent_radio.setChecked(True)
|
||||
|
||||
self.sign_check.setChecked(self.map_data.is_signed)
|
||||
self.swap_check.setChecked(self.map_data.swap_axis)
|
||||
self.hex_radio.setChecked(self.map_data.display_hex)
|
||||
self.dec_radio.setChecked(not self.map_data.display_hex)
|
||||
self.reciprocal_check.setChecked(self.map_data.is_reciprocal)
|
||||
|
||||
# Value range
|
||||
range_layout = QHBoxLayout()
|
||||
self.min_value = QDoubleSpinBox()
|
||||
self.min_value.setRange(-999999, 999999)
|
||||
self.min_value.setValue(self.map_data.value_min)
|
||||
self.max_value = QDoubleSpinBox()
|
||||
self.max_value.setRange(-999999, 999999)
|
||||
self.max_value.setValue(self.map_data.value_max)
|
||||
self.auto_btn = QPushButton("Auto")
|
||||
self.auto_btn.clicked.connect(self.calculate_auto_range)
|
||||
range_layout.addWidget(self.min_value)
|
||||
range_layout.addWidget(QLabel("-"))
|
||||
range_layout.addWidget(self.max_value)
|
||||
range_layout.addWidget(self.auto_btn)
|
||||
map_layout.addRow("Value range:", range_layout)
|
||||
|
||||
# Factor and Precision
|
||||
factor_layout = QHBoxLayout()
|
||||
self.factor_spin = QDoubleSpinBox()
|
||||
self.factor_spin.setDecimals(6)
|
||||
self.factor_spin.setValue(self.map_data.factor)
|
||||
self.precision_spin = QSpinBox()
|
||||
self.precision_spin.setRange(0, 6)
|
||||
self.precision_spin.setValue(self.map_data.precision)
|
||||
factor_layout.addWidget(self.factor_spin)
|
||||
factor_layout.addWidget(QLabel("Precision:"))
|
||||
factor_layout.addWidget(self.precision_spin)
|
||||
map_layout.addRow("Factor:", factor_layout)
|
||||
|
||||
# Offset and buttons
|
||||
offset_layout = QHBoxLayout()
|
||||
self.offset_spin = QDoubleSpinBox()
|
||||
self.offset_spin.setValue(self.map_data.offset)
|
||||
self.bar_btn = QPushButton("Bar")
|
||||
self.c_btn = QPushButton("C")
|
||||
self.percent_btn = QPushButton("%")
|
||||
self.one_btn = QPushButton("1")
|
||||
|
||||
self.bar_btn.clicked.connect(lambda: self.apply_unit_conversion("bar"))
|
||||
self.c_btn.clicked.connect(lambda: self.apply_unit_conversion("C"))
|
||||
self.percent_btn.clicked.connect(lambda: self.apply_unit_conversion("%"))
|
||||
self.one_btn.clicked.connect(lambda: self.apply_unit_conversion("1"))
|
||||
|
||||
offset_layout.addWidget(self.offset_spin)
|
||||
offset_layout.addWidget(self.bar_btn)
|
||||
offset_layout.addWidget(self.c_btn)
|
||||
offset_layout.addWidget(self.percent_btn)
|
||||
offset_layout.addWidget(self.one_btn)
|
||||
map_layout.addRow("Offset:", offset_layout)
|
||||
|
||||
# Formula
|
||||
formula_layout = QHBoxLayout()
|
||||
self.formula_combo = QComboBox()
|
||||
self.formula_combo.setEditable(True)
|
||||
self.formula_combo.addItem(self.map_data.formula)
|
||||
self.fx_btn = QPushButton("f(x)")
|
||||
self.fx_btn.clicked.connect(self.edit_formula)
|
||||
formula_layout.addWidget(self.formula_combo)
|
||||
formula_layout.addWidget(self.fx_btn)
|
||||
map_layout.addRow("Formula:", formula_layout)
|
||||
|
||||
map_group.setLayout(map_layout)
|
||||
layout.addWidget(map_group)
|
||||
|
||||
# Axis X Group
|
||||
axis_x_group = QGroupBox("Axis X")
|
||||
self.axis_x_widgets = self.create_axis_layout(
|
||||
self.map_data.x_name,
|
||||
self.map_data.x_address,
|
||||
self.map_data.x_data_source,
|
||||
self.map_data.x_data_format,
|
||||
self.map_data.x_step,
|
||||
self.map_data.x_factor,
|
||||
self.map_data.x_offset,
|
||||
self.map_data.x_precision,
|
||||
self.map_data.x_is_signed,
|
||||
self.map_data.x_is_inverted,
|
||||
self.map_data.x_display_hex,
|
||||
self.map_data.x_is_reciprocal,
|
||||
self.map_data.x_formula
|
||||
)
|
||||
axis_x_group.setLayout(self.axis_x_widgets['layout'])
|
||||
layout.addWidget(axis_x_group)
|
||||
|
||||
# Axis Y Group
|
||||
axis_y_group = QGroupBox("Axis Y")
|
||||
self.axis_y_widgets = self.create_axis_layout(
|
||||
self.map_data.y_name,
|
||||
self.map_data.y_address,
|
||||
self.map_data.y_data_source,
|
||||
self.map_data.y_data_format,
|
||||
self.map_data.y_step,
|
||||
self.map_data.y_factor,
|
||||
self.map_data.y_offset,
|
||||
self.map_data.y_precision,
|
||||
self.map_data.y_is_signed,
|
||||
self.map_data.y_is_inverted,
|
||||
self.map_data.y_display_hex,
|
||||
self.map_data.y_is_reciprocal,
|
||||
self.map_data.y_formula
|
||||
)
|
||||
axis_y_group.setLayout(self.axis_y_widgets['layout'])
|
||||
layout.addWidget(axis_y_group)
|
||||
|
||||
# Buttons
|
||||
buttons_layout = QHBoxLayout()
|
||||
save_btn = QPushButton("Save")
|
||||
save_btn.clicked.connect(self.save_changes)
|
||||
cancel_btn = QPushButton("Cancel")
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
buttons_layout.addStretch()
|
||||
buttons_layout.addWidget(save_btn)
|
||||
buttons_layout.addWidget(cancel_btn)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
def create_axis_layout(self, name, address, data_source, data_format, step,
|
||||
factor, offset, precision, is_signed, is_inverted,
|
||||
display_hex, is_reciprocal, formula):
|
||||
widgets = {}
|
||||
layout = QFormLayout()
|
||||
|
||||
# Name
|
||||
widgets['name_combo'] = QComboBox()
|
||||
widgets['name_combo'].setEditable(True)
|
||||
widgets['name_combo'].addItem(name)
|
||||
layout.addRow("Name:", widgets['name_combo'])
|
||||
|
||||
# Start address
|
||||
addr_layout = QHBoxLayout()
|
||||
widgets['addr_edit'] = QLineEdit(address)
|
||||
widgets['addr_edit'].textChanged.connect(self.on_address_changed)
|
||||
widgets['get_2d_btn'] = QPushButton("Get from 2D cursor")
|
||||
widgets['get_2d_btn'].clicked.connect(self.on_get_2d_cursor)
|
||||
addr_layout.addWidget(widgets['addr_edit'])
|
||||
addr_layout.addWidget(widgets['get_2d_btn'])
|
||||
layout.addRow("Start address:", addr_layout)
|
||||
|
||||
# Data Source and Format
|
||||
widgets['source_combo'] = QComboBox()
|
||||
widgets['source_combo'].addItem("EPROM")
|
||||
widgets['source_combo'].setCurrentText(data_source)
|
||||
layout.addRow("Data Source:", widgets['source_combo'])
|
||||
|
||||
widgets['format_combo'] = QComboBox()
|
||||
widgets['format_combo'].addItems([
|
||||
"8-bit",
|
||||
"16-bit Lo-Hi",
|
||||
"16-bit Hi-Lo",
|
||||
"32-bit Lo-Hi",
|
||||
"32-bit Hi-Lo",
|
||||
"Float"
|
||||
])
|
||||
widgets['format_combo'].setCurrentText(data_format)
|
||||
layout.addRow("Data Format:", widgets['format_combo'])
|
||||
|
||||
# Step
|
||||
widgets['step_spin'] = QSpinBox()
|
||||
widgets['step_spin'].setValue(step)
|
||||
layout.addRow("Step:", widgets['step_spin'])
|
||||
|
||||
# Factor, Offset, Precision
|
||||
factor_layout = QHBoxLayout()
|
||||
widgets['factor_spin'] = QDoubleSpinBox()
|
||||
widgets['factor_spin'].setDecimals(6)
|
||||
widgets['factor_spin'].setValue(factor)
|
||||
widgets['offset_spin'] = QDoubleSpinBox()
|
||||
widgets['offset_spin'].setValue(offset)
|
||||
widgets['precision_spin'] = QSpinBox()
|
||||
widgets['precision_spin'].setValue(precision)
|
||||
factor_layout.addWidget(widgets['factor_spin'])
|
||||
factor_layout.addWidget(QLabel("Offset:"))
|
||||
factor_layout.addWidget(widgets['offset_spin'])
|
||||
factor_layout.addWidget(QLabel("Precision:"))
|
||||
factor_layout.addWidget(widgets['precision_spin'])
|
||||
layout.addRow("Factor:", factor_layout)
|
||||
|
||||
# Options
|
||||
options_layout = QHBoxLayout()
|
||||
widgets['sign_check'] = QCheckBox("Sign")
|
||||
widgets['sign_check'].setChecked(is_signed)
|
||||
widgets['invert_check'] = QCheckBox("Invert")
|
||||
widgets['invert_check'].setChecked(is_inverted)
|
||||
widgets['hex_radio'] = QRadioButton("Hexadecimal")
|
||||
widgets['hex_radio'].setChecked(display_hex)
|
||||
widgets['dec_radio'] = QRadioButton("Decimal")
|
||||
widgets['dec_radio'].setChecked(not display_hex)
|
||||
widgets['reciprocal_check'] = QCheckBox("Reciprocal")
|
||||
widgets['reciprocal_check'].setChecked(is_reciprocal)
|
||||
options_layout.addWidget(widgets['sign_check'])
|
||||
options_layout.addWidget(widgets['invert_check'])
|
||||
options_layout.addWidget(widgets['hex_radio'])
|
||||
options_layout.addWidget(widgets['dec_radio'])
|
||||
options_layout.addWidget(widgets['reciprocal_check'])
|
||||
layout.addRow("", options_layout)
|
||||
|
||||
# Formula
|
||||
formula_layout = QHBoxLayout()
|
||||
widgets['formula_combo'] = QComboBox()
|
||||
widgets['formula_combo'].setEditable(True)
|
||||
widgets['formula_combo'].addItem(formula)
|
||||
widgets['fx_btn'] = QPushButton("f(x)")
|
||||
widgets['fx_btn'].clicked.connect(self.edit_formula)
|
||||
widgets['bar_btn'] = QPushButton("Bar")
|
||||
widgets['c_btn'] = QPushButton("C")
|
||||
widgets['percent_btn'] = QPushButton("%")
|
||||
widgets['one_btn'] = QPushButton("1")
|
||||
|
||||
widgets['bar_btn'].clicked.connect(lambda: self.apply_unit_conversion("bar"))
|
||||
widgets['c_btn'].clicked.connect(lambda: self.apply_unit_conversion("C"))
|
||||
widgets['percent_btn'].clicked.connect(lambda: self.apply_unit_conversion("%"))
|
||||
widgets['one_btn'].clicked.connect(lambda: self.apply_unit_conversion("1"))
|
||||
|
||||
formula_layout.addWidget(widgets['formula_combo'])
|
||||
formula_layout.addWidget(widgets['fx_btn'])
|
||||
formula_layout.addWidget(widgets['bar_btn'])
|
||||
formula_layout.addWidget(widgets['c_btn'])
|
||||
formula_layout.addWidget(widgets['percent_btn'])
|
||||
formula_layout.addWidget(widgets['one_btn'])
|
||||
layout.addRow("Formula:", formula_layout)
|
||||
|
||||
widgets['layout'] = layout
|
||||
return widgets
|
||||
|
||||
def calculate_auto_range(self):
|
||||
"""Calculate min and max values from the data"""
|
||||
if self.map_data.data is not None:
|
||||
min_val = np.min(self.map_data.data)
|
||||
max_val = np.max(self.map_data.data)
|
||||
self.min_value.setValue(min_val)
|
||||
self.max_value.setValue(max_val)
|
||||
|
||||
def apply_unit_conversion(self, unit):
|
||||
"""Apply unit conversion factors"""
|
||||
if unit == "bar":
|
||||
self.factor_spin.setValue(0.001)
|
||||
self.precision_spin.setValue(3)
|
||||
elif unit == "C":
|
||||
self.offset_spin.setValue(-273.15)
|
||||
self.precision_spin.setValue(1)
|
||||
elif unit == "%":
|
||||
self.factor_spin.setValue(100)
|
||||
self.precision_spin.setValue(1)
|
||||
elif unit == "1":
|
||||
self.factor_spin.setValue(1)
|
||||
self.offset_spin.setValue(0)
|
||||
self.precision_spin.setValue(0)
|
||||
|
||||
def edit_formula(self):
|
||||
"""Open formula editor"""
|
||||
# TODO: Implement formula editor
|
||||
QMessageBox.information(self, "Formula Editor", "Formula editor not implemented yet")
|
||||
|
||||
def on_get_2d_cursor(self):
|
||||
"""Get address from 2D cursor"""
|
||||
if not self.map_viewer:
|
||||
QMessageBox.warning(self, "Warning", "Map viewer not available")
|
||||
return
|
||||
|
||||
# Get the button that was clicked
|
||||
sender = self.sender()
|
||||
|
||||
# Determine which address field to update based on the button
|
||||
if sender == self.get_2d_btn:
|
||||
self.active_address_field = self.addr_edit
|
||||
elif sender == self.axis_x_widgets['get_2d_btn']:
|
||||
self.active_address_field = self.axis_x_widgets['addr_edit']
|
||||
elif sender == self.axis_y_widgets['get_2d_btn']:
|
||||
self.active_address_field = self.axis_y_widgets['addr_edit']
|
||||
else:
|
||||
return
|
||||
|
||||
# Get cursor address from map viewer
|
||||
address = self.map_viewer.get_cursor_address()
|
||||
if not address:
|
||||
QMessageBox.warning(self, "Warning", "No valid cursor position available")
|
||||
return
|
||||
|
||||
# Validate the address
|
||||
if not self.map_viewer.validate_address(address):
|
||||
QMessageBox.warning(self, "Warning", "Selected address is outside valid range for this map")
|
||||
return
|
||||
|
||||
# Update the address field with validated address
|
||||
self.active_address_field.setText(f"0x{address}")
|
||||
|
||||
# Provide visual feedback
|
||||
self.active_address_field.setStyleSheet("background-color: #e6ffe6;") # Light green
|
||||
QTimer.singleShot(1000, lambda: self.active_address_field.setStyleSheet(""))
|
||||
|
||||
def validate_address_input(self, address: str) -> bool:
|
||||
"""Validate address input format and range"""
|
||||
try:
|
||||
# Remove 0x prefix if present
|
||||
clean_addr = address.replace("0x", "").strip()
|
||||
|
||||
# Check if it's a valid hex number
|
||||
if not all(c in '0123456789ABCDEFabcdef' for c in clean_addr):
|
||||
return False
|
||||
|
||||
# Check length (max 6 digits for 24-bit address)
|
||||
if len(clean_addr) > 6:
|
||||
return False
|
||||
|
||||
# Convert to integer and check range
|
||||
addr_int = int(clean_addr, 16)
|
||||
if not (0 <= addr_int <= 0xFFFFFF):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def on_address_changed(self, text: str):
|
||||
"""Handle address input changes"""
|
||||
sender = self.sender()
|
||||
|
||||
# Validate address format
|
||||
if text and not self.validate_address_input(text):
|
||||
sender.setStyleSheet("background-color: #ffe6e6;") # Light red
|
||||
else:
|
||||
sender.setStyleSheet("")
|
||||
|
||||
def save_changes(self):
|
||||
"""Save changes to the map data"""
|
||||
try:
|
||||
# Update main map properties
|
||||
self.map_data.name = self.name_combo.currentText()
|
||||
self.map_data.address = self.addr_edit.text()
|
||||
self.map_data.cols = self.cols_spin.value()
|
||||
self.map_data.rows = self.rows_spin.value()
|
||||
self.map_data.data_format = self.format_combo.currentText()
|
||||
|
||||
# Display mode
|
||||
if self.original_radio.isChecked():
|
||||
self.map_data.display_mode = "Original"
|
||||
elif self.difference_radio.isChecked():
|
||||
self.map_data.display_mode = "Difference"
|
||||
else:
|
||||
self.map_data.display_mode = "Percent"
|
||||
|
||||
# Options
|
||||
self.map_data.is_signed = self.sign_check.isChecked()
|
||||
self.map_data.swap_axis = self.swap_check.isChecked()
|
||||
self.map_data.display_hex = self.hex_radio.isChecked()
|
||||
self.map_data.is_reciprocal = self.reciprocal_check.isChecked()
|
||||
|
||||
# Values
|
||||
self.map_data.value_min = self.min_value.value()
|
||||
self.map_data.value_max = self.max_value.value()
|
||||
self.map_data.factor = self.factor_spin.value()
|
||||
self.map_data.offset = self.offset_spin.value()
|
||||
self.map_data.precision = self.precision_spin.value()
|
||||
self.map_data.formula = self.formula_combo.currentText()
|
||||
|
||||
# X-axis properties
|
||||
x = self.axis_x_widgets
|
||||
self.map_data.x_name = x['name_combo'].currentText()
|
||||
self.map_data.x_address = x['addr_edit'].text()
|
||||
self.map_data.x_data_source = x['source_combo'].currentText()
|
||||
self.map_data.x_data_format = x['format_combo'].currentText()
|
||||
self.map_data.x_step = x['step_spin'].value()
|
||||
self.map_data.x_factor = x['factor_spin'].value()
|
||||
self.map_data.x_offset = x['offset_spin'].value()
|
||||
self.map_data.x_precision = x['precision_spin'].value()
|
||||
self.map_data.x_is_signed = x['sign_check'].isChecked()
|
||||
self.map_data.x_is_inverted = x['invert_check'].isChecked()
|
||||
self.map_data.x_display_hex = x['hex_radio'].isChecked()
|
||||
self.map_data.x_is_reciprocal = x['reciprocal_check'].isChecked()
|
||||
self.map_data.x_formula = x['formula_combo'].currentText()
|
||||
|
||||
# Y-axis properties
|
||||
y = self.axis_y_widgets
|
||||
self.map_data.y_name = y['name_combo'].currentText()
|
||||
self.map_data.y_address = y['addr_edit'].text()
|
||||
self.map_data.y_data_source = y['source_combo'].currentText()
|
||||
self.map_data.y_data_format = y['format_combo'].currentText()
|
||||
self.map_data.y_step = y['step_spin'].value()
|
||||
self.map_data.y_factor = y['factor_spin'].value()
|
||||
self.map_data.y_offset = y['offset_spin'].value()
|
||||
self.map_data.y_precision = y['precision_spin'].value()
|
||||
self.map_data.y_is_signed = y['sign_check'].isChecked()
|
||||
self.map_data.y_is_inverted = y['invert_check'].isChecked()
|
||||
self.map_data.y_display_hex = y['hex_radio'].isChecked()
|
||||
self.map_data.y_is_reciprocal = y['reciprocal_check'].isChecked()
|
||||
self.map_data.y_formula = y['formula_combo'].currentText()
|
||||
|
||||
self.accept()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to save changes: {str(e)}")
|
||||
|
||||
def load_map_data(self):
|
||||
# Load map data from file
|
||||
# TODO: Implement map data loading
|
||||
pass
|
||||
136
src/gui/map_selection_dialog.py
Normal file
136
src/gui/map_selection_dialog.py
Normal file
@ -0,0 +1,136 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem,
|
||||
QPushButton, QProgressBar, QLabel, QProgressDialog, QApplication, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
|
||||
from core.edc15p_maps import list_maps_by_group, EDC15P_MAPS
|
||||
|
||||
class MapSelectionDialog(QDialog):
|
||||
map_selected = Signal(str) # Emits map_id
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Selecionar Mapa")
|
||||
self.resize(400, 600)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Map tree
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setHeaderLabel("Mapas Disponíveis")
|
||||
self.tree.itemDoubleClicked.connect(self.on_item_double_clicked)
|
||||
layout.addWidget(self.tree)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.open_button = QPushButton("Abrir")
|
||||
self.open_button.clicked.connect(self.accept)
|
||||
self.open_button.setEnabled(False) # Disabled until selection
|
||||
button_layout.addWidget(self.open_button)
|
||||
|
||||
cancel_button = QPushButton("Cancelar")
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(cancel_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Progress bar (hidden by default)
|
||||
self.progress_label = QLabel("Carregando...")
|
||||
self.progress_label.hide()
|
||||
layout.addWidget(self.progress_label)
|
||||
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.hide()
|
||||
layout.addWidget(self.progress_bar)
|
||||
|
||||
# Connect selection changed signal
|
||||
self.tree.itemSelectionChanged.connect(self.on_selection_changed)
|
||||
|
||||
def populate_tree(self):
|
||||
"""Populate tree with maps"""
|
||||
try:
|
||||
# Add maps grouped by category
|
||||
groups = list_maps_by_group()
|
||||
total_groups = len(groups)
|
||||
for i, (group_name, maps) in enumerate(groups.items()):
|
||||
group_item = QTreeWidgetItem(self.tree, [group_name])
|
||||
group_item.setFlags(group_item.flags() & ~Qt.ItemIsSelectable) # Make group non-selectable
|
||||
|
||||
for map_def in maps:
|
||||
# Find map ID
|
||||
map_id = next((k for k, v in EDC15P_MAPS.items() if v == map_def), None)
|
||||
if map_id:
|
||||
# Create map item
|
||||
map_item = QTreeWidgetItem(group_item)
|
||||
map_item.setText(0, map_def.name)
|
||||
map_item.setData(0, Qt.UserRole, map_id)
|
||||
map_item.setData(0, Qt.UserRole + 1, "map") # Mark as map item
|
||||
|
||||
# Create details folder
|
||||
details_item = QTreeWidgetItem(map_item, ["Detalhes"])
|
||||
details_item.setFlags(details_item.flags() & ~Qt.ItemIsSelectable) # Make non-selectable
|
||||
|
||||
# Add details as child items
|
||||
details = [
|
||||
f"Endereço: {map_def.address}",
|
||||
f"Dimensões: {map_def.rows}x{map_def.cols}",
|
||||
f"Unidades: {map_def.units}",
|
||||
f"Descrição: {map_def.description}"
|
||||
]
|
||||
for detail in details:
|
||||
detail_item = QTreeWidgetItem(details_item, [detail])
|
||||
detail_item.setFlags(detail_item.flags() & ~Qt.ItemIsSelectable) # Make non-selectable
|
||||
|
||||
self.progress_bar.setValue((i + 1) * 100 // total_groups)
|
||||
QApplication.processEvents()
|
||||
|
||||
self.tree.expandAll()
|
||||
self.progress_bar.setValue(100)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Error populating tree: {str(e)}")
|
||||
|
||||
def get_selected_map_id(self) -> str:
|
||||
"""Get the selected map ID"""
|
||||
items = self.tree.selectedItems()
|
||||
if items:
|
||||
item = items[0]
|
||||
# Check if it's a map item
|
||||
if item.data(0, Qt.UserRole + 1) == "map":
|
||||
map_id = item.data(0, Qt.UserRole)
|
||||
return map_id
|
||||
return None
|
||||
|
||||
def on_selection_changed(self):
|
||||
"""Handle selection change"""
|
||||
map_id = self.get_selected_map_id()
|
||||
self.open_button.setEnabled(map_id is not None)
|
||||
|
||||
def on_item_double_clicked(self, item, column):
|
||||
"""Handle double click on item"""
|
||||
if item.data(0, Qt.UserRole + 1) == "map":
|
||||
map_id = item.data(0, Qt.UserRole)
|
||||
if map_id:
|
||||
self.map_selected.emit(map_id)
|
||||
self.accept()
|
||||
|
||||
def show_progress(self, show: bool = True):
|
||||
"""Show or hide progress indicators"""
|
||||
self.progress_label.setVisible(show)
|
||||
self.progress_bar.setVisible(show)
|
||||
if show:
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate progress
|
||||
|
||||
def accept(self):
|
||||
"""Handle dialog acceptance"""
|
||||
map_id = self.get_selected_map_id()
|
||||
if map_id:
|
||||
self.map_selected.emit(map_id)
|
||||
super().accept()
|
||||
else:
|
||||
QMessageBox.warning(self, "Warning", "No map selected")
|
||||
|
||||
186
src/gui/map_tools.py
Normal file
186
src/gui/map_tools.py
Normal file
@ -0,0 +1,186 @@
|
||||
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QPushButton, QLabel, QSpinBox, QDoubleSpinBox,
|
||||
QGroupBox, QMessageBox)
|
||||
from PySide6.QtCore import Signal
|
||||
|
||||
class MapToolsWidget(QWidget):
|
||||
# Signals
|
||||
interpolationRequested = Signal(int, int, int, int) # x1, y1, x2, y2
|
||||
smoothingRequested = Signal(int, int, int, int, float) # x1, y1, x2, y2, factor
|
||||
valueChangeRequested = Signal(int, int, float) # x, y, value
|
||||
percentageChangeRequested = Signal(int, int, int, int, float) # x1, y1, x2, y2, percentage
|
||||
resetRequested = Signal(int, int, int, int) # x1, y1, x2, y2
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Selection group
|
||||
selection_group = QGroupBox("Seleção")
|
||||
selection_layout = QGridLayout()
|
||||
|
||||
# Start position
|
||||
selection_layout.addWidget(QLabel("Início:"), 0, 0)
|
||||
|
||||
self.start_x = QSpinBox()
|
||||
self.start_x.setMinimum(0)
|
||||
selection_layout.addWidget(QLabel("X:"), 0, 1)
|
||||
selection_layout.addWidget(self.start_x, 0, 2)
|
||||
|
||||
self.start_y = QSpinBox()
|
||||
self.start_y.setMinimum(0)
|
||||
selection_layout.addWidget(QLabel("Y:"), 0, 3)
|
||||
selection_layout.addWidget(self.start_y, 0, 4)
|
||||
|
||||
# End position
|
||||
selection_layout.addWidget(QLabel("Fim:"), 1, 0)
|
||||
|
||||
self.end_x = QSpinBox()
|
||||
self.end_x.setMinimum(0)
|
||||
selection_layout.addWidget(QLabel("X:"), 1, 1)
|
||||
selection_layout.addWidget(self.end_x, 1, 2)
|
||||
|
||||
self.end_y = QSpinBox()
|
||||
self.end_y.setMinimum(0)
|
||||
selection_layout.addWidget(QLabel("Y:"), 1, 3)
|
||||
selection_layout.addWidget(self.end_y, 1, 4)
|
||||
|
||||
selection_group.setLayout(selection_layout)
|
||||
layout.addWidget(selection_group)
|
||||
|
||||
# Tools group
|
||||
tools_group = QGroupBox("Ferramentas")
|
||||
tools_layout = QVBoxLayout()
|
||||
|
||||
# Interpolation
|
||||
interpolate_btn = QPushButton("Interpolar")
|
||||
interpolate_btn.clicked.connect(self.interpolate)
|
||||
tools_layout.addWidget(interpolate_btn)
|
||||
|
||||
# Smoothing
|
||||
smooth_layout = QHBoxLayout()
|
||||
smooth_layout.addWidget(QLabel("Fator:"))
|
||||
|
||||
self.smooth_factor = QDoubleSpinBox()
|
||||
self.smooth_factor.setMinimum(0.1)
|
||||
self.smooth_factor.setMaximum(10.0)
|
||||
self.smooth_factor.setValue(1.0)
|
||||
self.smooth_factor.setSingleStep(0.1)
|
||||
smooth_layout.addWidget(self.smooth_factor)
|
||||
|
||||
smooth_btn = QPushButton("Suavizar")
|
||||
smooth_btn.clicked.connect(self.smooth)
|
||||
smooth_layout.addWidget(smooth_btn)
|
||||
|
||||
tools_layout.addLayout(smooth_layout)
|
||||
|
||||
# Value change
|
||||
value_layout = QHBoxLayout()
|
||||
value_layout.addWidget(QLabel("Valor:"))
|
||||
|
||||
self.value_input = QDoubleSpinBox()
|
||||
self.value_input.setMinimum(-999999.0)
|
||||
self.value_input.setMaximum(999999.0)
|
||||
self.value_input.setDecimals(3)
|
||||
value_layout.addWidget(self.value_input)
|
||||
|
||||
value_btn = QPushButton("Definir")
|
||||
value_btn.clicked.connect(self.set_value)
|
||||
value_layout.addWidget(value_btn)
|
||||
|
||||
tools_layout.addLayout(value_layout)
|
||||
|
||||
# Percentage change
|
||||
percentage_layout = QHBoxLayout()
|
||||
percentage_layout.addWidget(QLabel("%:"))
|
||||
|
||||
self.percentage_input = QDoubleSpinBox()
|
||||
self.percentage_input.setMinimum(-100.0)
|
||||
self.percentage_input.setMaximum(100.0)
|
||||
self.percentage_input.setDecimals(1)
|
||||
percentage_layout.addWidget(self.percentage_input)
|
||||
|
||||
percentage_btn = QPushButton("Alterar")
|
||||
percentage_btn.clicked.connect(self.change_percentage)
|
||||
percentage_layout.addWidget(percentage_btn)
|
||||
|
||||
tools_layout.addLayout(percentage_layout)
|
||||
|
||||
# Reset
|
||||
reset_btn = QPushButton("Restaurar Original")
|
||||
reset_btn.clicked.connect(self.reset)
|
||||
tools_layout.addWidget(reset_btn)
|
||||
|
||||
tools_group.setLayout(tools_layout)
|
||||
layout.addWidget(tools_group)
|
||||
|
||||
def set_map_size(self, rows: int, cols: int):
|
||||
"""Update spinbox ranges based on map size"""
|
||||
self.start_x.setMaximum(cols - 1)
|
||||
self.end_x.setMaximum(cols - 1)
|
||||
self.start_y.setMaximum(rows - 1)
|
||||
self.end_y.setMaximum(rows - 1)
|
||||
|
||||
# Set end values to maximum
|
||||
self.end_x.setValue(cols - 1)
|
||||
self.end_y.setValue(rows - 1)
|
||||
|
||||
def get_selection(self):
|
||||
"""Get the current selection coordinates"""
|
||||
return (
|
||||
min(self.start_x.value(), self.end_x.value()),
|
||||
min(self.start_y.value(), self.end_y.value()),
|
||||
max(self.start_x.value(), self.end_x.value()),
|
||||
max(self.start_y.value(), self.end_y.value())
|
||||
)
|
||||
|
||||
def interpolate(self):
|
||||
"""Request interpolation of selected region"""
|
||||
x1, y1, x2, y2 = self.get_selection()
|
||||
self.interpolationRequested.emit(x1, y1, x2, y2)
|
||||
|
||||
def smooth(self):
|
||||
"""Request smoothing of selected region"""
|
||||
x1, y1, x2, y2 = self.get_selection()
|
||||
self.smoothingRequested.emit(x1, y1, x2, y2, self.smooth_factor.value())
|
||||
|
||||
def set_value(self):
|
||||
"""Request value change"""
|
||||
x1, y1, x2, y2 = self.get_selection()
|
||||
if x1 == x2 and y1 == y2:
|
||||
self.valueChangeRequested.emit(x1, y1, self.value_input.value())
|
||||
else:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirmar Alteração",
|
||||
"Deseja aplicar este valor a toda a região selecionada?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
for x in range(x1, x2 + 1):
|
||||
for y in range(y1, y2 + 1):
|
||||
self.valueChangeRequested.emit(x, y, self.value_input.value())
|
||||
|
||||
def change_percentage(self):
|
||||
"""Request percentage change in region"""
|
||||
x1, y1, x2, y2 = self.get_selection()
|
||||
self.percentageChangeRequested.emit(x1, y1, x2, y2, self.percentage_input.value())
|
||||
|
||||
def reset(self):
|
||||
"""Request reset of selected region"""
|
||||
x1, y1, x2, y2 = self.get_selection()
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirmar Restauração",
|
||||
"Deseja restaurar os valores originais da região selecionada?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
self.resetRequested.emit(x1, y1, x2, y2)
|
||||
428
src/gui/map_viewer.py
Normal file
428
src/gui/map_viewer.py
Normal file
@ -0,0 +1,428 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget,
|
||||
QTableWidgetItem, QComboBox, QLabel, QSplitter, QToolBar,
|
||||
QTreeWidget, QTreeWidgetItem, QMenu, QDialog, QStatusBar,
|
||||
QSizePolicy, QHeaderView, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, QTimer
|
||||
from PySide6.QtGui import QKeySequence, QColor, QBrush
|
||||
from PySide6.QtGui import QAction
|
||||
from gui.map_tools import MapToolsWidget
|
||||
from core.edc15p_maps import EDC15P_MAPS, list_maps_by_group
|
||||
from core.command import CommandHistory, MapValueCommand
|
||||
from core.file_handler import MapData
|
||||
import numpy as np
|
||||
|
||||
class MapViewer(QWidget):
|
||||
map_changed = Signal(str) # Emitted when map selection changes
|
||||
cursor_position_changed = Signal(int, int, str) # Updated to include address
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.current_file = None
|
||||
self.current_map = None
|
||||
self.current_cursor_pos = (0, 0) # Track cursor position
|
||||
self.command_history = CommandHistory()
|
||||
self._ignore_cell_changes = False # Flag to prevent recursive updates
|
||||
self.highlighted_cell = None
|
||||
self.scroll_step = 1 # Number of cells to scroll per wheel step
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# Create splitter
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# Left side - Tree view for map selection
|
||||
self.map_tree = QTreeWidget()
|
||||
self.map_tree.setHeaderLabel("Maps")
|
||||
self.map_tree.setMinimumWidth(200)
|
||||
self.map_tree.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
|
||||
|
||||
# Right side container
|
||||
right_widget = QWidget()
|
||||
right_layout = QVBoxLayout(right_widget)
|
||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||
right_layout.setSpacing(0)
|
||||
|
||||
# Tools widget
|
||||
self.tools_widget = MapToolsWidget()
|
||||
self.tools_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
||||
right_layout.addWidget(self.tools_widget)
|
||||
|
||||
# Table for map data
|
||||
self.table = QTableWidget()
|
||||
self.table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.table.horizontalHeader().setStretchLastSection(True)
|
||||
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.table.verticalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
|
||||
# Make the table adjust to available space
|
||||
self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
self.table.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
|
||||
# Add table to layout
|
||||
right_layout.addWidget(self.table)
|
||||
|
||||
# Add widgets to splitter
|
||||
splitter.addWidget(self.map_tree)
|
||||
splitter.addWidget(right_widget)
|
||||
|
||||
# Set splitter properties
|
||||
splitter.setStretchFactor(0, 0) # Tree doesn't stretch
|
||||
splitter.setStretchFactor(1, 1) # Table area stretches
|
||||
|
||||
# Add splitter to main layout
|
||||
layout.addWidget(splitter)
|
||||
|
||||
# Add status bar
|
||||
self.status_bar = QStatusBar()
|
||||
self.status_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
layout.addWidget(self.status_bar)
|
||||
|
||||
# Connect signals
|
||||
self.map_tree.itemClicked.connect(self.on_map_selected)
|
||||
self.table.cellClicked.connect(self.on_cell_clicked)
|
||||
self.table.cellChanged.connect(self.on_cell_changed)
|
||||
|
||||
# Set context menu
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.show_context_menu)
|
||||
|
||||
def set_ecu_file(self, ecu_file):
|
||||
"""Set the current ECU file and update UI"""
|
||||
self.current_file = ecu_file
|
||||
self.update_map_tree()
|
||||
|
||||
def update_map_tree(self):
|
||||
"""Update the map tree with available maps"""
|
||||
self.map_tree.clear()
|
||||
|
||||
if not self.current_file:
|
||||
return
|
||||
|
||||
# Get grouped maps
|
||||
grouped_maps = list_maps_by_group()
|
||||
|
||||
# Add maps grouped by category
|
||||
for group_name, maps_list in grouped_maps.items():
|
||||
# Create group item
|
||||
group_item = QTreeWidgetItem(self.map_tree, [group_name])
|
||||
group_item.setFlags(group_item.flags() & ~Qt.ItemIsSelectable)
|
||||
|
||||
for map_def in maps_list:
|
||||
# Get map ID from EDC15P_MAPS
|
||||
map_id = next((k for k, v in EDC15P_MAPS.items() if v.name == map_def.name), None)
|
||||
if not map_id:
|
||||
continue
|
||||
|
||||
# Create map item
|
||||
map_item = QTreeWidgetItem(group_item, [map_def.name])
|
||||
map_item.setData(0, Qt.UserRole, map_id) # Store map ID
|
||||
map_item.setData(0, Qt.UserRole + 1, "map") # Mark as map item
|
||||
|
||||
# Add details as child items
|
||||
details = [
|
||||
f"Address: {map_def.address}",
|
||||
f"Size: {map_def.rows}x{map_def.cols}",
|
||||
f"Units: {map_def.units}",
|
||||
f"Description: {map_def.description}"
|
||||
]
|
||||
|
||||
for detail in details:
|
||||
detail_item = QTreeWidgetItem(map_item, [detail])
|
||||
detail_item.setFlags(detail_item.flags() & ~Qt.ItemIsSelectable)
|
||||
|
||||
# Expand all items
|
||||
self.map_tree.expandAll()
|
||||
|
||||
def on_map_selected(self, item):
|
||||
"""Handle map selection changes"""
|
||||
# Check if it's a map item
|
||||
if item.data(0, Qt.UserRole + 1) != "map":
|
||||
return
|
||||
|
||||
# Get map ID from item data
|
||||
map_id = item.data(0, Qt.UserRole)
|
||||
if not map_id:
|
||||
return
|
||||
|
||||
try:
|
||||
# Get map data from file
|
||||
if not self.current_file or map_id not in self.current_file.maps:
|
||||
return
|
||||
|
||||
# Load map data
|
||||
self.current_map = self.current_file.maps[map_id]
|
||||
self.update_table()
|
||||
self.map_changed.emit(map_id)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Error loading map: {str(e)}")
|
||||
|
||||
def undo(self):
|
||||
"""Undo last change"""
|
||||
if self.command_history.undo():
|
||||
self.update_table()
|
||||
self._update_undo_redo_state()
|
||||
|
||||
def redo(self):
|
||||
"""Redo last undone change"""
|
||||
if self.command_history.redo():
|
||||
self.update_table()
|
||||
self._update_undo_redo_state()
|
||||
|
||||
def _update_undo_redo_state(self):
|
||||
"""Update undo/redo action states"""
|
||||
self.undo_action.setEnabled(self.command_history.can_undo())
|
||||
self.redo_action.setEnabled(self.command_history.can_redo())
|
||||
|
||||
def on_cell_changed(self, row, col):
|
||||
"""Handle cell value changes"""
|
||||
if self._ignore_cell_changes or not self.current_map:
|
||||
return
|
||||
|
||||
try:
|
||||
item = self.table.item(row, col)
|
||||
if not item:
|
||||
return
|
||||
|
||||
# Get the new value from the item
|
||||
try:
|
||||
value = float(item.text())
|
||||
except ValueError:
|
||||
# Reset to original value if input is invalid
|
||||
self._ignore_cell_changes = True
|
||||
try:
|
||||
original_value = self.current_map.data[row][col]
|
||||
item.setText(str(original_value))
|
||||
finally:
|
||||
self._ignore_cell_changes = False
|
||||
return
|
||||
|
||||
# Create and execute command
|
||||
old_value = self.current_map.data[row][col]
|
||||
if old_value != value:
|
||||
command = MapValueCommand(self.current_map, row, col, old_value, value)
|
||||
self.command_history.execute(command)
|
||||
|
||||
except Exception as e:
|
||||
# Reset to original value
|
||||
self._ignore_cell_changes = True
|
||||
try:
|
||||
original_value = self.current_map.data[row][col]
|
||||
item = QTableWidgetItem(str(original_value))
|
||||
self.table.setItem(row, col, item)
|
||||
finally:
|
||||
self._ignore_cell_changes = False
|
||||
|
||||
def on_cell_clicked(self, row, col):
|
||||
"""Handle cell click event"""
|
||||
self.current_cursor_pos = (row, col)
|
||||
address = self.get_cursor_address()
|
||||
|
||||
# Update status bar
|
||||
self.status_bar.showMessage(f"Position: ({row}, {col}) | Address: 0x{address}")
|
||||
|
||||
# Emit signal with position and address
|
||||
self.cursor_position_changed.emit(row, col, address)
|
||||
|
||||
# Highlight the clicked cell
|
||||
self.highlight_cell(row, col)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
"""Handle mouse wheel events"""
|
||||
if not self.current_map or not self.table:
|
||||
return
|
||||
|
||||
# Get current selection
|
||||
current = self.table.currentItem()
|
||||
if not current:
|
||||
return
|
||||
|
||||
row = current.row()
|
||||
col = current.column()
|
||||
|
||||
# Calculate new position based on wheel direction
|
||||
if event.angleDelta().y() > 0: # Scroll up
|
||||
new_row = max(0, row - self.scroll_step)
|
||||
else: # Scroll down
|
||||
new_row = min(self.current_map.rows - 1, row + self.scroll_step)
|
||||
|
||||
# If shift is pressed, move horizontally instead
|
||||
if event.modifiers() & Qt.ShiftModifier:
|
||||
if event.angleDelta().y() > 0: # Scroll left
|
||||
new_col = max(0, col - self.scroll_step)
|
||||
else: # Scroll right
|
||||
new_col = min(self.current_map.cols - 1, col + self.scroll_step)
|
||||
new_row = row
|
||||
else:
|
||||
new_col = col
|
||||
|
||||
# Select new cell
|
||||
self.table.setCurrentCell(new_row, new_col)
|
||||
|
||||
# Update cursor position and emit signal
|
||||
self.current_cursor_pos = (new_row, new_col)
|
||||
address = self.get_cursor_address()
|
||||
self.cursor_position_changed.emit(new_row, new_col, address)
|
||||
|
||||
# Ensure the cell is visible
|
||||
self.table.scrollToItem(self.table.item(new_row, new_col))
|
||||
|
||||
# Highlight the new cell
|
||||
self.highlight_cell(new_row, new_col)
|
||||
|
||||
# Accept the event
|
||||
event.accept()
|
||||
|
||||
def highlight_cell(self, row, col, duration_ms=1000):
|
||||
"""Temporarily highlight a cell"""
|
||||
# Reset previous highlight if any
|
||||
if self.highlighted_cell:
|
||||
prev_row, prev_col = self.highlighted_cell
|
||||
item = self.table.item(prev_row, prev_col)
|
||||
if item:
|
||||
item.setBackground(QBrush()) # Clear background
|
||||
|
||||
# Highlight new cell
|
||||
item = self.table.item(row, col)
|
||||
if item:
|
||||
item.setBackground(QBrush(QColor(255, 255, 0, 100))) # Light yellow
|
||||
self.highlighted_cell = (row, col)
|
||||
|
||||
# Clear highlight after duration
|
||||
QTimer.singleShot(duration_ms, lambda: self.clear_highlight(row, col))
|
||||
|
||||
def clear_highlight(self, row, col):
|
||||
"""Clear cell highlight"""
|
||||
if self.highlighted_cell == (row, col):
|
||||
item = self.table.item(row, col)
|
||||
if item:
|
||||
item.setBackground(QBrush())
|
||||
self.highlighted_cell = None
|
||||
|
||||
def validate_address(self, address: str) -> bool:
|
||||
"""Validate if an address is within the valid range of the current map"""
|
||||
try:
|
||||
# Convert address to integer
|
||||
addr_int = int(address.replace("0x", ""), 16)
|
||||
|
||||
# Get map boundaries
|
||||
base_addr = int(self.current_map.address, 16)
|
||||
bytes_per_value = {
|
||||
"8-bit": 1,
|
||||
"16-bit Lo-Hi": 2,
|
||||
"16-bit Hi-Lo": 2,
|
||||
"32-bit Lo-Hi": 4,
|
||||
"32-bit Hi-Lo": 4,
|
||||
"Float": 4
|
||||
}.get(self.current_map.data_format, 2)
|
||||
|
||||
total_bytes = self.current_map.rows * self.current_map.cols * bytes_per_value
|
||||
end_addr = base_addr + total_bytes
|
||||
|
||||
# Check if address is within bounds
|
||||
return base_addr <= addr_int < end_addr
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def get_cursor_address(self) -> str:
|
||||
"""Calculate address at current cursor position"""
|
||||
if not self.current_map:
|
||||
return ""
|
||||
|
||||
try:
|
||||
base_addr = int(self.current_map.address, 16)
|
||||
row, col = self.current_cursor_pos
|
||||
|
||||
# Calculate bytes per value based on data format
|
||||
bytes_per_value = {
|
||||
"8-bit": 1,
|
||||
"16-bit Lo-Hi": 2,
|
||||
"16-bit Hi-Lo": 2,
|
||||
"32-bit Lo-Hi": 4,
|
||||
"32-bit Hi-Lo": 4,
|
||||
"Float": 4
|
||||
}.get(self.current_map.data_format, 2)
|
||||
|
||||
# Calculate offset based on position
|
||||
offset = (row * self.current_map.cols + col) * bytes_per_value
|
||||
address = base_addr + offset
|
||||
|
||||
# Validate the calculated address
|
||||
if 0 <= address <= 0xFFFFFF: # Max 24-bit address
|
||||
return f"{address:06X}"
|
||||
else:
|
||||
return ""
|
||||
|
||||
except (ValueError, TypeError):
|
||||
return ""
|
||||
|
||||
def update_table(self):
|
||||
"""Update table with current map data"""
|
||||
if not self.current_map:
|
||||
return
|
||||
|
||||
self._ignore_cell_changes = True
|
||||
try:
|
||||
self.table.setRowCount(self.current_map.rows)
|
||||
self.table.setColumnCount(self.current_map.cols)
|
||||
|
||||
# Set headers
|
||||
x_headers = [f"{x:.0f}" for x in self.current_map.x_axis]
|
||||
y_headers = [f"{y:.0f}" for y in self.current_map.y_axis]
|
||||
self.table.setHorizontalHeaderLabels(x_headers)
|
||||
self.table.setVerticalHeaderLabels(y_headers)
|
||||
|
||||
# Fill table with data
|
||||
for row in range(self.current_map.rows):
|
||||
for col in range(self.current_map.cols):
|
||||
value = self.current_map.get_value(row, col)
|
||||
item = QTableWidgetItem(f"{value:.2f}")
|
||||
item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.table.setItem(row, col, item)
|
||||
|
||||
# Adjust table size to fit content
|
||||
self.table.resizeColumnsToContents()
|
||||
self.table.resizeRowsToContents()
|
||||
|
||||
# Ensure minimum cell size
|
||||
min_cell_width = 60
|
||||
min_cell_height = 25
|
||||
for col in range(self.current_map.cols):
|
||||
if self.table.columnWidth(col) < min_cell_width:
|
||||
self.table.setColumnWidth(col, min_cell_width)
|
||||
for row in range(self.current_map.rows):
|
||||
if self.table.rowHeight(row) < min_cell_height:
|
||||
self.table.setRowHeight(row, min_cell_height)
|
||||
|
||||
finally:
|
||||
self._ignore_cell_changes = False
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""Handle window resize events"""
|
||||
super().resizeEvent(event)
|
||||
# Adjust table columns and rows when window is resized
|
||||
if self.current_map:
|
||||
self.table.resizeColumnsToContents()
|
||||
self.table.resizeRowsToContents()
|
||||
|
||||
def show_context_menu(self, position):
|
||||
"""Show context menu for the map viewer"""
|
||||
from .map_manager_dialog import MapManagerDialog
|
||||
|
||||
context_menu = QMenu(self)
|
||||
map_manager_action = context_menu.addAction("Map Manager")
|
||||
|
||||
action = context_menu.exec_(self.mapToGlobal(position))
|
||||
if action == map_manager_action:
|
||||
dialog = MapManagerDialog(self.current_map, self)
|
||||
dialog.set_map_viewer(self) # Set reference to map viewer
|
||||
if dialog.exec_() == QDialog.Accepted:
|
||||
# Handle the changes from the dialog
|
||||
self.update_table() # Refresh the display
|
||||
Reference in New Issue
Block a user