Carregar ficheiros para "src/gui"

This commit is contained in:
2024-12-19 09:22:17 -08:00
parent e67f685b37
commit f1306be447
5 changed files with 1425 additions and 0 deletions

168
src/gui/map_comparison.py Normal file
View 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)

View 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

View 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
View 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
View 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