Carregar ficheiros para "src/gui"

This commit is contained in:
2024-12-19 09:21:49 -08:00
parent 10713c22cf
commit e67f685b37
5 changed files with 997 additions and 0 deletions

1
src/gui/__init__.py Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1,130 @@
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QFileDialog, QTextEdit, QProgressBar)
from PySide6.QtCore import Qt
from core.file_handler import ECUFile
from core.file_comparison import FileComparison
import os
class ComparisonDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Comparar Arquivos")
self.setModal(True)
self.resize(800, 600)
self.file1_path = ""
self.file2_path = ""
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# File selection
file1_layout = QHBoxLayout()
self.file1_label = QLabel("Arquivo 1: Não selecionado")
file1_btn = QPushButton("Selecionar...")
file1_btn.clicked.connect(lambda: self.select_file(1))
file1_layout.addWidget(self.file1_label)
file1_layout.addWidget(file1_btn)
layout.addLayout(file1_layout)
file2_layout = QHBoxLayout()
self.file2_label = QLabel("Arquivo 2: Não selecionado")
file2_btn = QPushButton("Selecionar...")
file2_btn.clicked.connect(lambda: self.select_file(2))
file2_layout.addWidget(self.file2_label)
file2_layout.addWidget(file2_btn)
layout.addLayout(file2_layout)
# Progress bar
self.progress = QProgressBar()
self.progress.setVisible(False)
layout.addWidget(self.progress)
# Results area
self.results = QTextEdit()
self.results.setReadOnly(True)
layout.addWidget(self.results)
# Buttons
button_layout = QHBoxLayout()
compare_btn = QPushButton("Comparar")
compare_btn.clicked.connect(self.compare_files)
button_layout.addWidget(compare_btn)
self.export_btn = QPushButton("Exportar Relatório")
self.export_btn.clicked.connect(self.export_report)
self.export_btn.setEnabled(False)
button_layout.addWidget(self.export_btn)
close_btn = QPushButton("Fechar")
close_btn.clicked.connect(self.close)
button_layout.addWidget(close_btn)
layout.addLayout(button_layout)
def select_file(self, file_num):
file_path, _ = QFileDialog.getOpenFileName(
self,
f"Selecionar Arquivo {file_num}",
"",
"ECU Files (*.bin *.hex);;All Files (*.*)"
)
if file_path:
if file_num == 1:
self.file1_path = file_path
self.file1_label.setText(f"Arquivo 1: {os.path.basename(file_path)}")
else:
self.file2_path = file_path
self.file2_label.setText(f"Arquivo 2: {os.path.basename(file_path)}")
def compare_files(self):
if not self.file1_path or not self.file2_path:
self.results.setText("Por favor, selecione dois arquivos para comparar.")
return
self.progress.setVisible(True)
self.progress.setRange(0, 0) # Indeterminate progress
# Load files
file1 = ECUFile(self.file1_path)
file2 = ECUFile(self.file2_path)
if not file1.read_file() or not file2.read_file():
self.results.setText("Erro ao ler os arquivos.")
self.progress.setVisible(False)
return
# Compare files
comparison = FileComparison(file1, file2)
has_differences = comparison.compare_files()
map_differences = comparison.compare_maps()
# Show results
self.results.setText(comparison.get_difference_summary())
self.progress.setVisible(False)
self.export_btn.setEnabled(True)
# Store comparison for export
self.current_comparison = comparison
def export_report(self):
if not hasattr(self, 'current_comparison'):
return
file_path, _ = QFileDialog.getSaveFileName(
self,
"Salvar Relatório",
"",
"Text Files (*.txt);;All Files (*.*)"
)
if file_path:
if self.current_comparison.export_difference_report(file_path):
self.results.append("\n\nRelatório exportado com sucesso!")
else:
self.results.append("\n\nErro ao exportar relatório.")

268
src/gui/graph_viewer.py Normal file
View File

@ -0,0 +1,268 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QComboBox,
QLabel, QToolBar, QSizePolicy, QSlider, QSpinBox,
QScrollArea, QPushButton, QMessageBox
)
from PySide6.QtCore import Qt
import numpy as np
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT
from matplotlib.figure import Figure
import struct
class GraphViewer(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.raw_data = None
self.current_data = None
self.current_format = "8-bit"
self.zoom_level = 1.0
self.view_points = 1000 # Number of points to show at once
self.current_offset = 0 # Starting offset
self.vertical_line = None # Store the vertical line
self.scroll_step = 100 # Points to scroll per wheel step
self.mouse_on_plot = False # Track if mouse is on plot
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Top controls
controls_layout = QHBoxLayout()
# Format selector
format_layout = QHBoxLayout()
format_label = QLabel("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.currentTextChanged.connect(self.on_format_changed)
format_layout.addWidget(format_label)
format_layout.addWidget(self.format_combo)
controls_layout.addLayout(format_layout)
# Points control
points_layout = QHBoxLayout()
points_label = QLabel("Points to View:")
self.points_spin = QSpinBox()
self.points_spin.setMinimum(5)
self.points_spin.setMaximum(1500)
self.points_spin.setValue(1000)
self.points_spin.valueChanged.connect(self.on_points_changed)
points_layout.addWidget(points_label)
points_layout.addWidget(self.points_spin)
controls_layout.addLayout(points_layout)
# Zoom controls
zoom_layout = QHBoxLayout()
zoom_label = QLabel("Zoom:")
self.zoom_slider = QSlider(Qt.Horizontal)
self.zoom_slider.setMinimum(10)
self.zoom_slider.setMaximum(400)
self.zoom_slider.setValue(100)
self.zoom_slider.valueChanged.connect(self.on_zoom_changed)
zoom_layout.addWidget(zoom_label)
zoom_layout.addWidget(self.zoom_slider)
controls_layout.addLayout(zoom_layout)
# Position slider
position_layout = QHBoxLayout()
position_label = QLabel("Position:")
self.position_slider = QSlider(Qt.Horizontal)
self.position_slider.setMinimum(0)
self.position_slider.setMaximum(0) # Will be updated when data is loaded
self.position_slider.valueChanged.connect(self.on_position_changed)
position_layout.addWidget(position_label)
position_layout.addWidget(self.position_slider)
controls_layout.addLayout(position_layout)
controls_layout.addStretch()
layout.addLayout(controls_layout)
# Create scroll area for the plot
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
# Container for the plot
plot_container = QWidget()
plot_layout = QVBoxLayout(plot_container)
# Matplotlib figure
self.figure = Figure(figsize=(12, 6))
self.canvas = FigureCanvas(self.figure)
self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Connect mouse events
self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
self.canvas.mpl_connect('axes_leave_event', self.on_mouse_leave)
# Add matplotlib toolbar
self.toolbar = NavigationToolbar2QT(self.canvas, self)
plot_layout.addWidget(self.toolbar)
plot_layout.addWidget(self.canvas)
scroll_area.setWidget(plot_container)
layout.addWidget(scroll_area)
self.axes = self.figure.add_subplot(111)
self.plot = None
def on_points_changed(self, value):
self.view_points = value
if self.current_data is not None:
self.update_plot()
self.update_slider_range()
def on_position_changed(self, value):
self.current_offset = value
if self.current_data is not None:
self.update_plot()
def update_slider_range(self):
if self.current_data is not None:
max_offset = max(0, len(self.current_data) - self.view_points)
self.position_slider.setMaximum(max_offset)
self.position_slider.setPageStep(self.view_points // 2)
self.position_slider.setSingleStep(self.view_points // 10)
def on_format_changed(self, format_text):
self.current_format = format_text
if self.current_data is not None:
self.process_data()
self.update_slider_range()
self.update_plot()
def on_zoom_changed(self, value):
self.zoom_level = value / 100.0
self.update_plot()
def process_data(self):
"""Process the raw data according to the selected format"""
try:
# Convert data based on selected format
if not hasattr(self, 'raw_data'):
return
if self.current_format == "8-bit":
self.current_data = np.frombuffer(self.raw_data, dtype=np.uint8)
elif self.current_format in ["16-bit Lo-Hi", "16-bit Hi-Lo"]:
values = np.frombuffer(self.raw_data, dtype=np.uint16)
if self.current_format == "16-bit Hi-Lo":
values = ((values & 0xFF) << 8) | (values >> 8)
self.current_data = values
elif self.current_format in ["32-bit Lo-Hi", "32-bit Hi-Lo"]:
values = np.frombuffer(self.raw_data, dtype=np.uint32)
if self.current_format == "32-bit Hi-Lo":
values = ((values & 0xFF) << 24) | ((values & 0xFF00) << 8) | \
((values & 0xFF0000) >> 8) | (values >> 24)
self.current_data = values
elif self.current_format == "Float":
self.current_data = np.frombuffer(self.raw_data, dtype=np.float32)
except Exception as e:
QMessageBox.critical(self, "Error", f"Error processing data: {str(e)}")
def update_plot(self):
if self.current_data is None:
return
# Clear previous plot
self.axes.clear()
# Get the visible portion of data
end_offset = min(self.current_offset + self.view_points, len(self.current_data))
visible_data = self.current_data[self.current_offset:end_offset]
x_values = np.arange(self.current_offset, end_offset)
# Plot data as a single line
self.axes.plot(x_values, visible_data, 'b-', linewidth=1, label='Data')
# Set labels
self.axes.set_xlabel('Byte Offset')
self.axes.set_ylabel('Value')
# Set title with current view range
self.axes.set_title(f'ECU Data ({self.current_format}) - Showing bytes {self.current_offset:,} to {end_offset:,}')
# Add grid for better readability
self.axes.grid(True, linestyle='--', alpha=0.7)
# Apply zoom
xlim = self.axes.get_xlim()
center_x = np.mean(xlim)
width = (xlim[1] - xlim[0]) / self.zoom_level
self.axes.set_xlim(center_x - width/2, center_x + width/2)
# Update canvas
self.figure.tight_layout()
self.canvas.draw()
def wheelEvent(self, event):
"""Handle mouse wheel events for horizontal scrolling"""
if not self.current_data is None and self.mouse_on_plot:
# Calculate new offset based on wheel direction
if event.angleDelta().y() > 0: # Scroll up/left
self.current_offset = max(0, self.current_offset - self.scroll_step)
else: # Scroll down/right
max_offset = max(0, len(self.current_data) - self.view_points)
self.current_offset = min(max_offset, self.current_offset + self.scroll_step)
# Update position slider and plot
self.position_slider.blockSignals(True) # Prevent recursive updates
self.position_slider.setValue(self.current_offset)
self.position_slider.blockSignals(False)
self.update_plot() # Explicitly update the plot
# Accept the event
event.accept()
def on_mouse_move(self, event):
"""Handle mouse movement over the plot"""
if event.inaxes is None:
self.mouse_on_plot = False
return
self.mouse_on_plot = True
# Remove old vertical line if it exists
if self.vertical_line:
self.vertical_line.remove()
# Draw new vertical line at mouse x position
self.vertical_line = self.axes.axvline(x=event.xdata, color='r', linestyle='--', alpha=0.5)
# Update the canvas
self.canvas.draw()
def on_mouse_leave(self, event):
"""Handle mouse leaving the plot area"""
self.mouse_on_plot = False
if self.vertical_line:
self.vertical_line.remove()
self.vertical_line = None
self.canvas.draw()
def update_data(self, data: bytes):
"""Update the graph viewer with new data"""
try:
# Store the raw data
self.raw_data = data
# Reset position
self.current_offset = 0
# Process the data according to current format
self.process_data()
# Update slider range
self.update_slider_range()
# Update the plot
self.update_plot()
except Exception as e:
QMessageBox.critical(self, "Error", f"Error updating graph data: {str(e)}")

238
src/gui/hex_viewer.py Normal file
View File

@ -0,0 +1,238 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget,
QTableWidgetItem, QComboBox, QLabel, QScrollArea,
QRadioButton, QButtonGroup
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QBrush
import struct
class HexViewer(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
self.current_offset = 0
self.current_data = None
self.highlight_start = -1
self.highlight_end = -1
self.bytes_per_row = 16
self.display_hex = True
def setup_ui(self):
layout = QVBoxLayout(self)
# Header with position info and controls
header_layout = QHBoxLayout()
self.position_label = QLabel("Offset: 0x000000")
header_layout.addWidget(self.position_label)
# Display format radio buttons
format_group = QButtonGroup(self)
self.hex_radio = QRadioButton("Hexadecimal")
self.dec_radio = QRadioButton("Decimal")
self.hex_radio.setChecked(True)
format_group.addButton(self.hex_radio)
format_group.addButton(self.dec_radio)
format_group.buttonClicked.connect(self.on_display_format_changed)
header_layout.addWidget(self.hex_radio)
header_layout.addWidget(self.dec_radio)
# Data format selector
format_label = QLabel("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.currentTextChanged.connect(self.on_format_changed)
header_layout.addWidget(format_label)
header_layout.addWidget(self.format_combo)
header_layout.addStretch()
layout.addLayout(header_layout)
# Create scroll area
scroll = QScrollArea()
scroll.setWidgetResizable(True)
layout.addWidget(scroll)
# Container for hex and ASCII tables
container = QWidget()
container_layout = QHBoxLayout(container)
scroll.setWidget(container)
# Hex table
self.table = QTableWidget()
self.table.setColumnCount(17) # Address + 16 bytes
self.table.setHorizontalHeaderLabels(["Addr"] + [f"{i:02X}" for i in range(16)])
container_layout.addWidget(self.table, stretch=2)
# ASCII table
self.ascii_table = QTableWidget()
self.ascii_table.setColumnCount(16) # 16 characters per row
self.ascii_table.setHorizontalHeaderLabels([str(i) for i in range(16)])
self.ascii_table.verticalHeader().hide()
container_layout.addWidget(self.ascii_table, stretch=1)
# Set column widths
self.table.setColumnWidth(0, 80) # Address column
for i in range(1, 17):
self.table.setColumnWidth(i, 50) # Data columns
for i in range(16):
self.ascii_table.setColumnWidth(i, 20) # ASCII columns
def set_offset(self, offset: int):
"""Set the starting offset for the hex view"""
self.current_offset = offset
self.position_label.setText(f"Offset: 0x{offset:06X}")
self.update_view()
# Scroll to the offset
row = (offset - self.current_offset) // self.bytes_per_row
self.table.scrollToItem(self.table.item(row, 0))
self.ascii_table.scrollToItem(self.ascii_table.item(row, 0))
def update_data(self, data: bytes):
"""Update the hex viewer with new data"""
self.current_data = data
self.update_view()
def highlight_range(self, start: int, end: int):
"""Highlight a range of bytes"""
self.highlight_start = start
self.highlight_end = end
self.update_view()
def update_range(self, offset: int, data: bytes):
"""Update a specific range of bytes"""
if self.current_data is None:
return
# Update the data
end_offset = offset + len(data)
self.current_data = (
self.current_data[:offset] +
data +
self.current_data[end_offset:]
)
self.update_view()
def on_display_format_changed(self, button):
"""Handle display format change between hex and decimal"""
self.display_hex = button == self.hex_radio
self.update_view()
def on_format_changed(self, format_text: str):
"""Handle data format change"""
self.update_view()
def format_value(self, data: bytes, format_text: str, display_hex: bool) -> str:
"""Format bytes according to selected format"""
try:
if format_text == "8-bit":
value = data[0]
return f"{value:02X}" if display_hex else f"{value:3d}"
elif format_text == "16-bit Lo-Hi":
value = int.from_bytes(data[:2], 'little')
return f"{value:04X}" if display_hex else f"{value:5d}"
elif format_text == "16-bit Hi-Lo":
value = int.from_bytes(data[:2], 'big')
return f"{value:04X}" if display_hex else f"{value:5d}"
elif format_text == "32-bit Lo-Hi":
value = int.from_bytes(data[:4], 'little')
return f"{value:08X}" if display_hex else f"{value:10d}"
elif format_text == "32-bit Hi-Lo":
value = int.from_bytes(data[:4], 'big')
return f"{value:08X}" if display_hex else f"{value:10d}"
elif format_text == "Float":
value = struct.unpack('f', data[:4])[0]
return f"{value:.6f}"
except (IndexError, struct.error):
return ""
return ""
def get_format_size(self, format_text: str) -> int:
"""Get number of bytes needed for current format"""
if format_text == "8-bit":
return 1
elif format_text.startswith("16-bit"):
return 2
else: # 32-bit and Float
return 4
def get_ascii_char(self, byte: int) -> str:
"""Convert byte to ASCII character if printable"""
if 32 <= byte <= 126: # Printable ASCII range
return chr(byte)
return "."
def update_view(self):
"""Update the hex view table"""
if self.current_data is None:
return
format_text = self.format_combo.currentText()
bytes_per_value = self.get_format_size(format_text)
# Calculate number of rows needed
num_rows = (len(self.current_data) + self.bytes_per_row - 1) // self.bytes_per_row
self.table.setRowCount(num_rows)
self.ascii_table.setRowCount(num_rows)
# Fill tables
for row in range(num_rows):
# Add address
addr = self.current_offset + (row * self.bytes_per_row)
addr_item = QTableWidgetItem(f"{addr:06X}")
addr_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.table.setItem(row, 0, addr_item)
# Process each byte position
for col in range(self.bytes_per_row):
idx = row * self.bytes_per_row + col
if idx < len(self.current_data):
# Get the appropriate number of bytes for the current format
remaining_bytes = len(self.current_data) - idx
if remaining_bytes >= bytes_per_value:
data_slice = self.current_data[idx:idx + bytes_per_value]
value_text = self.format_value(data_slice, format_text, self.display_hex)
else:
# Not enough bytes for current format, display as single byte
value_text = f"{self.current_data[idx]:02X}" if self.display_hex else f"{self.current_data[idx]:3d}"
item = QTableWidgetItem(value_text)
item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
# Highlight if in range
if self.highlight_start <= idx < self.highlight_end:
item.setBackground(QBrush(QColor(255, 255, 0, 100)))
self.table.setItem(row, col + 1, item)
# Add ASCII representation
ascii_char = self.get_ascii_char(self.current_data[idx])
ascii_item = QTableWidgetItem(ascii_char)
ascii_item.setTextAlignment(Qt.AlignCenter)
if self.highlight_start <= idx < self.highlight_end:
ascii_item.setBackground(QBrush(QColor(255, 255, 0, 100)))
self.ascii_table.setItem(row, col, ascii_item)
else:
self.table.setItem(row, col + 1, QTableWidgetItem(""))
self.ascii_table.setItem(row, col, QTableWidgetItem(""))
# Resize rows to content
self.table.resizeRowsToContents()
self.ascii_table.resizeRowsToContents()

360
src/gui/main_window.py Normal file
View File

@ -0,0 +1,360 @@
import os
from PySide6.QtWidgets import (
QMainWindow, QFileDialog, QMessageBox, QWidget, QVBoxLayout,
QMenuBar, QDialog, QProgressDialog, QApplication, QToolBar,
QStackedWidget, QSplitter, QPushButton
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QAction
from gui.map_viewer import MapViewer
from gui.hex_viewer import HexViewer
from gui.graph_viewer import GraphViewer
from gui.map_selection_dialog import MapSelectionDialog
from gui.comparison_dialog import ComparisonDialog
from gui.map_comparison import MapComparisonWidget
from core.file_handler import ECUFile
import numpy as np
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.current_file = None
self.setWindowTitle("EDC15 Map Editor")
self.resize(1200, 800)
# Create central widget
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout = QVBoxLayout(self.central_widget)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Create toolbar
self.create_toolbar()
# Create stacked widget for different views
self.stacked_widget = QStackedWidget()
self.layout.addWidget(self.stacked_widget)
# Create viewers
self.graph_viewer = GraphViewer()
self.hex_viewer = HexViewer()
self.map_viewer = MapViewer()
# Add viewers to stacked widget
self.stacked_widget.addWidget(self.graph_viewer) # Index 0 - Graph View
self.stacked_widget.addWidget(self.hex_viewer) # Index 1 - Hex View
self.stacked_widget.addWidget(self.map_viewer) # Index 2 - Map View
# Create menu bar
self.create_menu_bar()
def create_toolbar(self):
"""Create main toolbar"""
toolbar = QToolBar()
toolbar.setMovable(False)
self.addToolBar(toolbar)
# Add view buttons
maps_action = toolbar.addAction("Maps")
maps_action.triggered.connect(lambda: self.switch_view(2)) # Map View
hex_action = toolbar.addAction("HEX")
hex_action.triggered.connect(lambda: self.switch_view(1)) # Hex View
graph_action = toolbar.addAction("2D View")
graph_action.triggered.connect(lambda: self.switch_view(0)) # Graph View
def create_menu_bar(self):
menubar = self.menuBar()
# File menu
file_menu = menubar.addMenu("Arquivo")
open_action = file_menu.addAction("Open File")
open_action.setShortcut("Ctrl+O")
open_action.triggered.connect(self.open_file)
save_action = file_menu.addAction("Salvar")
save_action.setShortcut("Ctrl+S")
save_action.triggered.connect(self.save_file)
save_as_action = file_menu.addAction("Salvar Como...")
save_as_action.setShortcut("Ctrl+Shift+S")
save_as_action.triggered.connect(self.save_file_as)
restore_action = file_menu.addAction("Restaurar Backup")
restore_action.triggered.connect(self.restore_backup)
file_menu.addSeparator()
file_menu.addAction("Sair").triggered.connect(self.close)
# Tools menu
tools_menu = menubar.addMenu("Ferramentas")
compare_action = tools_menu.addAction("Comparar Arquivos")
compare_action.triggered.connect(self.show_comparison_dialog)
compare_maps_action = tools_menu.addAction("Comparar Mapas")
compare_maps_action.triggered.connect(self.show_map_comparison)
tools_menu.addAction("Validar Arquivo")
tools_menu.addAction("Converter Formato")
# Help menu
help_menu = menubar.addMenu("Ajuda")
help_menu.addAction("Documentação")
about_action = help_menu.addAction("Sobre")
about_action.triggered.connect(self.show_about)
def switch_view(self, index: int):
"""Switch between different views"""
self.stacked_widget.setCurrentIndex(index)
# Update data if needed
if self.current_file:
if index == 0: # Graph View
data = np.frombuffer(self.current_file.get_raw_content(), dtype=np.uint8)
self.graph_viewer.update_data(data)
elif index == 1: # Hex View
self.hex_viewer.update_data(self.current_file.get_raw_content())
elif index == 2: # Map View
self.show_map_selection()
def open_file(self):
"""Open and load an ECU file"""
try:
filepath, _ = QFileDialog.getOpenFileName(
self,
"Abrir Arquivo",
"",
"Arquivos ECU (*.bin);;Todos os Arquivos (*.*)"
)
if filepath:
# Show progress dialog
progress = QProgressDialog("Carregando arquivo...", "Cancelar", 0, 100, self)
progress.setWindowModality(Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setValue(0)
QApplication.processEvents()
# Create ECU file object
self.current_file = ECUFile(filepath)
progress.setValue(30)
QApplication.processEvents()
# Read file
if self.current_file.read_file():
progress.setValue(60)
QApplication.processEvents()
# Update window title
self.setWindowTitle(f"EDC15 Map Editor - {os.path.basename(filepath)}")
# Show graph view and update data
self.switch_view(0) # Switch to graph view
data = np.frombuffer(self.current_file.get_raw_content(), dtype=np.uint8)
self.graph_viewer.update_data(data)
progress.setValue(100)
else:
progress.cancel()
QMessageBox.critical(self, "Erro", "Não foi possível ler o arquivo selecionado.")
except Exception as e:
QMessageBox.critical(self, "Erro", f"Error opening file: {e}")
def show_map_selection(self):
"""Show map selection dialog"""
if not self.current_file:
return
dialog = MapSelectionDialog(self)
dialog.populate_tree()
def on_map_selected(map_id):
"""Handle map selection"""
# Show progress
progress = QProgressDialog("Carregando mapa...", "Cancelar", 0, 100, self)
progress.setWindowModality(Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setValue(0)
QApplication.processEvents()
try:
# Initialize map viewer with file
self.map_viewer.set_ecu_file(self.current_file)
progress.setValue(30)
QApplication.processEvents()
def find_and_select_map(map_id):
for group_idx in range(self.map_viewer.map_tree.topLevelItemCount()):
group_item = self.map_viewer.map_tree.topLevelItem(group_idx)
for map_idx in range(group_item.childCount()):
map_item = group_item.child(map_idx)
if map_item.data(0, Qt.UserRole) == map_id:
self.map_viewer.map_tree.setCurrentItem(map_item)
return True
return False
if not find_and_select_map(map_id):
QMessageBox.critical(self, "Erro", f"Map {map_id} not found in tree")
progress.setValue(100)
except Exception as e:
progress.cancel()
QMessageBox.critical(self, "Erro", f"Erro ao carregar mapa: {e}")
# Connect the map_selected signal
dialog.map_selected.connect(on_map_selected)
# Show the dialog
if dialog.exec_() == QDialog.Accepted:
pass
else:
pass
def save_file(self):
"""Save current file"""
if not self.current_file:
return
progress = QProgressDialog("Salvando arquivo...", "Cancelar", 0, 100, self)
progress.setWindowModality(Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setValue(0)
QApplication.processEvents()
if not self.current_file.save_file():
progress.cancel()
QMessageBox.critical(
self,
"Erro",
"Não foi possível salvar o arquivo."
)
else:
progress.setValue(100)
def save_file_as(self):
"""Save file with a new name"""
if not self.current_file:
return
file_path, _ = QFileDialog.getSaveFileName(
self,
"Salvar Como",
"",
"Arquivos ECU (*.bin);;Todos os Arquivos (*.*)"
)
if not file_path:
return
progress = QProgressDialog("Salvando arquivo...", "Cancelar", 0, 100, self)
progress.setWindowModality(Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setValue(0)
QApplication.processEvents()
if not self.current_file.save_file(file_path):
progress.cancel()
QMessageBox.critical(
self,
"Erro",
"Não foi possível salvar o arquivo."
)
else:
progress.setValue(100)
def restore_backup(self):
"""Restore from backup file"""
if not self.current_file:
QMessageBox.warning(
self,
"Aviso",
"Nenhum arquivo aberto."
)
return
reply = QMessageBox.question(
self,
"Restaurar Backup",
"Tem certeza que deseja restaurar o backup? Todas as alterações serão perdidas.",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
progress = QProgressDialog("Restaurando backup...", "Cancelar", 0, 100, self)
progress.setWindowModality(Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setValue(0)
QApplication.processEvents()
if not self.current_file.restore_backup():
progress.cancel()
QMessageBox.critical(
self,
"Erro",
"Não foi possível restaurar o backup."
)
return
progress.setValue(50)
QApplication.processEvents()
# Update current view
current_view = self.stacked_widget.currentIndex()
self.switch_view(current_view)
progress.setValue(100)
def show_about(self):
"""Show about dialog"""
QMessageBox.about(
self,
"Sobre EDC15 Map Editor",
"EDC15 Map Editor\n\n"
"Software para edição e análise de centralinas EDC15\n"
"Versão 0.1.0\n\n"
"Desenvolvido em Python com PySide6"
)
def show_comparison_dialog(self):
"""Show the file comparison dialog"""
dialog = ComparisonDialog(self)
dialog.exec_()
def show_map_comparison(self):
"""Show map comparison dialog"""
if not self.current_file or not self.current_file.backup_path:
QMessageBox.warning(
self,
"Aviso",
"É necessário ter um arquivo aberto com backup para comparar mapas."
)
return
# Create backup file handler
backup_file = ECUFile(self.current_file.backup_path)
if not backup_file.read_file():
QMessageBox.critical(
self,
"Erro",
"Não foi possível ler o arquivo de backup."
)
return
# Create dialog
dialog = QDialog(self)
dialog.setWindowTitle("Comparação de Mapas")
dialog.resize(1000, 600)
layout = QVBoxLayout(dialog)
# Create comparison widget
comparison = MapComparisonWidget(dialog)
layout.addWidget(comparison)
# Show dialog
dialog.exec_()