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