diff --git a/src/gui/__init__.py b/src/gui/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/gui/__init__.py @@ -0,0 +1 @@ + diff --git a/src/gui/comparison_dialog.py b/src/gui/comparison_dialog.py new file mode 100644 index 0000000..703877f --- /dev/null +++ b/src/gui/comparison_dialog.py @@ -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.") diff --git a/src/gui/graph_viewer.py b/src/gui/graph_viewer.py new file mode 100644 index 0000000..bd5c2c3 --- /dev/null +++ b/src/gui/graph_viewer.py @@ -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)}") diff --git a/src/gui/hex_viewer.py b/src/gui/hex_viewer.py new file mode 100644 index 0000000..cd8b966 --- /dev/null +++ b/src/gui/hex_viewer.py @@ -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() diff --git a/src/gui/main_window.py b/src/gui/main_window.py new file mode 100644 index 0000000..9a55386 --- /dev/null +++ b/src/gui/main_window.py @@ -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_()