Carregar ficheiros para "src/gui"
This commit is contained in:
1
src/gui/__init__.py
Normal file
1
src/gui/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
130
src/gui/comparison_dialog.py
Normal file
130
src/gui/comparison_dialog.py
Normal 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
268
src/gui/graph_viewer.py
Normal 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
238
src/gui/hex_viewer.py
Normal 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
360
src/gui/main_window.py
Normal 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_()
|
||||
Reference in New Issue
Block a user