From f1306be447dd5fff13be7550959ccab6837766af Mon Sep 17 00:00:00 2001 From: godax84 Date: Thu, 19 Dec 2024 09:22:17 -0800 Subject: [PATCH] Carregar ficheiros para "src/gui" --- src/gui/map_comparison.py | 168 +++++++++++ src/gui/map_manager_dialog.py | 507 ++++++++++++++++++++++++++++++++ src/gui/map_selection_dialog.py | 136 +++++++++ src/gui/map_tools.py | 186 ++++++++++++ src/gui/map_viewer.py | 428 +++++++++++++++++++++++++++ 5 files changed, 1425 insertions(+) create mode 100644 src/gui/map_comparison.py create mode 100644 src/gui/map_manager_dialog.py create mode 100644 src/gui/map_selection_dialog.py create mode 100644 src/gui/map_tools.py create mode 100644 src/gui/map_viewer.py diff --git a/src/gui/map_comparison.py b/src/gui/map_comparison.py new file mode 100644 index 0000000..01e4364 --- /dev/null +++ b/src/gui/map_comparison.py @@ -0,0 +1,168 @@ +from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QComboBox, QPushButton, QSpinBox, QCheckBox) +from PySide6.QtCore import Qt +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure +import numpy as np +from core.file_handler import MapData + +class MapComparisonWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.map1 = None + self.map2 = None + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Controls + controls = QHBoxLayout() + + # View type selection + view_layout = QHBoxLayout() + view_layout.addWidget(QLabel("Visualização:")) + self.view_type = QComboBox() + self.view_type.addItems([ + "Lado a Lado", + "Diferença", + "Diferença %", + "Sobreposição" + ]) + self.view_type.currentIndexChanged.connect(self.update_view) + view_layout.addWidget(self.view_type) + controls.addLayout(view_layout) + + # Threshold controls + threshold_layout = QHBoxLayout() + threshold_layout.addWidget(QLabel("Limite de Diferença:")) + self.threshold = QSpinBox() + self.threshold.setRange(0, 100) + self.threshold.setValue(5) + self.threshold.setSuffix("%") + self.threshold.valueChanged.connect(self.update_view) + threshold_layout.addWidget(self.threshold) + controls.addLayout(threshold_layout) + + # Display options + options_layout = QHBoxLayout() + self.show_labels = QCheckBox("Mostrar Valores") + self.show_labels.setChecked(True) + self.show_labels.stateChanged.connect(self.update_view) + options_layout.addWidget(self.show_labels) + controls.addLayout(options_layout) + + layout.addLayout(controls) + + # Matplotlib figure + self.figure = Figure(figsize=(10, 6)) + self.canvas = FigureCanvas(self.figure) + layout.addWidget(self.canvas) + + def set_maps(self, map1: MapData, map2: MapData): + """Set maps to compare""" + self.map1 = map1 + self.map2 = map2 + self.update_view() + + def update_view(self): + """Update the visualization""" + if not self.map1 or not self.map2: + return + + self.figure.clear() + view = self.view_type.currentText() + + if view == "Lado a Lado": + self._plot_side_by_side() + elif view == "Diferença": + self._plot_difference() + elif view == "Diferença %": + self._plot_percentage_difference() + else: # Sobreposição + self._plot_overlay() + + self.canvas.draw() + + def _plot_side_by_side(self): + """Plot maps side by side""" + gs = self.figure.add_gridspec(1, 2) + ax1 = self.figure.add_subplot(gs[0, 0]) + ax2 = self.figure.add_subplot(gs[0, 1]) + + # Plot first map + im1 = ax1.imshow(self.map1.data, aspect='auto', cmap='viridis') + self.figure.colorbar(im1, ax=ax1) + ax1.set_title(f"{self.map1.name} (Original)") + + # Plot second map + im2 = ax2.imshow(self.map2.data, aspect='auto', cmap='viridis') + self.figure.colorbar(im2, ax=ax2) + ax2.set_title(f"{self.map2.name} (Modificado)") + + if self.show_labels.isChecked(): + self._add_value_labels(ax1, self.map1.data) + self._add_value_labels(ax2, self.map2.data) + + def _plot_difference(self): + """Plot absolute difference between maps""" + ax = self.figure.add_subplot(111) + + diff = self.map2.data - self.map1.data + im = ax.imshow(diff, aspect='auto', cmap='RdBu_r') + self.figure.colorbar(im) + + ax.set_title("Diferença Absoluta") + + if self.show_labels.isChecked(): + self._add_value_labels(ax, diff) + + def _plot_percentage_difference(self): + """Plot percentage difference between maps""" + ax = self.figure.add_subplot(111) + + # Calculate percentage difference + diff_percent = np.zeros_like(self.map1.data) + mask = self.map1.data != 0 + diff_percent[mask] = ((self.map2.data[mask] - self.map1.data[mask]) / + np.abs(self.map1.data[mask])) * 100 + + # Apply threshold + threshold = self.threshold.value() + diff_percent[np.abs(diff_percent) < threshold] = 0 + + im = ax.imshow(diff_percent, aspect='auto', cmap='RdBu_r') + self.figure.colorbar(im) + + ax.set_title(f"Diferença Percentual (Limite: {threshold}%)") + + if self.show_labels.isChecked(): + self._add_value_labels(ax, diff_percent, "%.1f%%") + + def _plot_overlay(self): + """Plot maps overlaid with transparency""" + ax = self.figure.add_subplot(111) + + # Plot original map + im1 = ax.imshow(self.map1.data, aspect='auto', cmap='viridis', alpha=0.5) + + # Plot modified map with transparency + im2 = ax.imshow(self.map2.data, aspect='auto', cmap='magma', alpha=0.5) + + # Add separate colorbars + self.figure.colorbar(im1, ax=ax, label='Original') + self.figure.colorbar(im2, ax=ax, label='Modificado') + + ax.set_title("Sobreposição de Mapas") + + if self.show_labels.isChecked(): + self._add_value_labels(ax, self.map2.data) + + def _add_value_labels(self, ax, data, fmt="%.2f"): + """Add value labels to the plot""" + for i in range(data.shape[0]): + for j in range(data.shape[1]): + ax.text(j, i, fmt % data[i, j], + ha='center', va='center', + color='white', fontsize=8) diff --git a/src/gui/map_manager_dialog.py b/src/gui/map_manager_dialog.py new file mode 100644 index 0000000..1d06b3a --- /dev/null +++ b/src/gui/map_manager_dialog.py @@ -0,0 +1,507 @@ +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QComboBox, QSpinBox, QRadioButton, QGroupBox, QPushButton, + QFormLayout, QDoubleSpinBox, QCheckBox, QTextEdit, QMessageBox +) +from PySide6.QtCore import Qt, QTimer +from core.file_handler import MapData +import numpy as np + +class MapManagerDialog(QDialog): + def __init__(self, map_data: MapData, parent=None): + super().__init__(parent) + self.map_data = map_data + self.map_viewer = None # Reference to map viewer + self.active_address_field = None # Track which address field to update + self.setWindowTitle("Map Manager") + self.setMinimumWidth(800) + self.setMinimumHeight(600) + self.setup_ui() + self.load_map_data() + + def set_map_viewer(self, map_viewer): + """Set reference to map viewer for cursor position""" + self.map_viewer = map_viewer + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Map Properties Group + map_group = QGroupBox("Map properties") + map_layout = QFormLayout() + + # Name + self.name_combo = QComboBox() + self.name_combo.setEditable(True) + self.name_combo.addItem(self.map_data.name) + map_layout.addRow("Name:", self.name_combo) + + # Start address + addr_layout = QHBoxLayout() + self.addr_edit = QLineEdit(self.map_data.address) + self.addr_edit.textChanged.connect(self.on_address_changed) + self.get_2d_btn = QPushButton("Get from 2D cursor") + self.get_2d_btn.clicked.connect(self.on_get_2d_cursor) + addr_layout.addWidget(self.addr_edit) + addr_layout.addWidget(self.get_2d_btn) + addr_layout.addStretch() + map_layout.addRow("Start address:", addr_layout) + + # Size + size_layout = QHBoxLayout() + self.cols_spin = QSpinBox() + self.cols_spin.setRange(1, 100) + self.cols_spin.setValue(self.map_data.cols) + self.rows_spin = QSpinBox() + self.rows_spin.setRange(1, 100) + self.rows_spin.setValue(self.map_data.rows) + size_layout.addWidget(self.cols_spin) + size_layout.addWidget(QLabel("x")) + size_layout.addWidget(self.rows_spin) + size_layout.addStretch() + map_layout.addRow("Size(col*row):", size_layout) + + # Data Format + self.format_combo = QComboBox() + self.format_combo.addItems([ + "8-bit", + "16-bit Lo-Hi", + "16-bit Hi-Lo", + "32-bit Lo-Hi", + "32-bit Hi-Lo", + "Float" + ]) + self.format_combo.setCurrentText(self.map_data.data_format) + map_layout.addRow("Data Format:", self.format_combo) + + # Options + options_layout = QHBoxLayout() + self.original_radio = QRadioButton("Original") + self.difference_radio = QRadioButton("Difference") + self.percent_radio = QRadioButton("Percent") + self.sign_check = QCheckBox("Sign") + self.swap_check = QCheckBox("Swap axis") + self.hex_radio = QRadioButton("Hexadecimal") + self.dec_radio = QRadioButton("Decimal") + self.reciprocal_check = QCheckBox("Reciprocal") + + options_layout.addWidget(self.original_radio) + options_layout.addWidget(self.difference_radio) + options_layout.addWidget(self.percent_radio) + options_layout.addWidget(self.sign_check) + options_layout.addWidget(self.swap_check) + options_layout.addWidget(self.hex_radio) + options_layout.addWidget(self.dec_radio) + options_layout.addWidget(self.reciprocal_check) + map_layout.addRow("", options_layout) + + # Set initial option states + if self.map_data.display_mode == "Original": + self.original_radio.setChecked(True) + elif self.map_data.display_mode == "Difference": + self.difference_radio.setChecked(True) + else: + self.percent_radio.setChecked(True) + + self.sign_check.setChecked(self.map_data.is_signed) + self.swap_check.setChecked(self.map_data.swap_axis) + self.hex_radio.setChecked(self.map_data.display_hex) + self.dec_radio.setChecked(not self.map_data.display_hex) + self.reciprocal_check.setChecked(self.map_data.is_reciprocal) + + # Value range + range_layout = QHBoxLayout() + self.min_value = QDoubleSpinBox() + self.min_value.setRange(-999999, 999999) + self.min_value.setValue(self.map_data.value_min) + self.max_value = QDoubleSpinBox() + self.max_value.setRange(-999999, 999999) + self.max_value.setValue(self.map_data.value_max) + self.auto_btn = QPushButton("Auto") + self.auto_btn.clicked.connect(self.calculate_auto_range) + range_layout.addWidget(self.min_value) + range_layout.addWidget(QLabel("-")) + range_layout.addWidget(self.max_value) + range_layout.addWidget(self.auto_btn) + map_layout.addRow("Value range:", range_layout) + + # Factor and Precision + factor_layout = QHBoxLayout() + self.factor_spin = QDoubleSpinBox() + self.factor_spin.setDecimals(6) + self.factor_spin.setValue(self.map_data.factor) + self.precision_spin = QSpinBox() + self.precision_spin.setRange(0, 6) + self.precision_spin.setValue(self.map_data.precision) + factor_layout.addWidget(self.factor_spin) + factor_layout.addWidget(QLabel("Precision:")) + factor_layout.addWidget(self.precision_spin) + map_layout.addRow("Factor:", factor_layout) + + # Offset and buttons + offset_layout = QHBoxLayout() + self.offset_spin = QDoubleSpinBox() + self.offset_spin.setValue(self.map_data.offset) + self.bar_btn = QPushButton("Bar") + self.c_btn = QPushButton("C") + self.percent_btn = QPushButton("%") + self.one_btn = QPushButton("1") + + self.bar_btn.clicked.connect(lambda: self.apply_unit_conversion("bar")) + self.c_btn.clicked.connect(lambda: self.apply_unit_conversion("C")) + self.percent_btn.clicked.connect(lambda: self.apply_unit_conversion("%")) + self.one_btn.clicked.connect(lambda: self.apply_unit_conversion("1")) + + offset_layout.addWidget(self.offset_spin) + offset_layout.addWidget(self.bar_btn) + offset_layout.addWidget(self.c_btn) + offset_layout.addWidget(self.percent_btn) + offset_layout.addWidget(self.one_btn) + map_layout.addRow("Offset:", offset_layout) + + # Formula + formula_layout = QHBoxLayout() + self.formula_combo = QComboBox() + self.formula_combo.setEditable(True) + self.formula_combo.addItem(self.map_data.formula) + self.fx_btn = QPushButton("f(x)") + self.fx_btn.clicked.connect(self.edit_formula) + formula_layout.addWidget(self.formula_combo) + formula_layout.addWidget(self.fx_btn) + map_layout.addRow("Formula:", formula_layout) + + map_group.setLayout(map_layout) + layout.addWidget(map_group) + + # Axis X Group + axis_x_group = QGroupBox("Axis X") + self.axis_x_widgets = self.create_axis_layout( + self.map_data.x_name, + self.map_data.x_address, + self.map_data.x_data_source, + self.map_data.x_data_format, + self.map_data.x_step, + self.map_data.x_factor, + self.map_data.x_offset, + self.map_data.x_precision, + self.map_data.x_is_signed, + self.map_data.x_is_inverted, + self.map_data.x_display_hex, + self.map_data.x_is_reciprocal, + self.map_data.x_formula + ) + axis_x_group.setLayout(self.axis_x_widgets['layout']) + layout.addWidget(axis_x_group) + + # Axis Y Group + axis_y_group = QGroupBox("Axis Y") + self.axis_y_widgets = self.create_axis_layout( + self.map_data.y_name, + self.map_data.y_address, + self.map_data.y_data_source, + self.map_data.y_data_format, + self.map_data.y_step, + self.map_data.y_factor, + self.map_data.y_offset, + self.map_data.y_precision, + self.map_data.y_is_signed, + self.map_data.y_is_inverted, + self.map_data.y_display_hex, + self.map_data.y_is_reciprocal, + self.map_data.y_formula + ) + axis_y_group.setLayout(self.axis_y_widgets['layout']) + layout.addWidget(axis_y_group) + + # Buttons + buttons_layout = QHBoxLayout() + save_btn = QPushButton("Save") + save_btn.clicked.connect(self.save_changes) + cancel_btn = QPushButton("Cancel") + cancel_btn.clicked.connect(self.reject) + buttons_layout.addStretch() + buttons_layout.addWidget(save_btn) + buttons_layout.addWidget(cancel_btn) + layout.addLayout(buttons_layout) + + def create_axis_layout(self, name, address, data_source, data_format, step, + factor, offset, precision, is_signed, is_inverted, + display_hex, is_reciprocal, formula): + widgets = {} + layout = QFormLayout() + + # Name + widgets['name_combo'] = QComboBox() + widgets['name_combo'].setEditable(True) + widgets['name_combo'].addItem(name) + layout.addRow("Name:", widgets['name_combo']) + + # Start address + addr_layout = QHBoxLayout() + widgets['addr_edit'] = QLineEdit(address) + widgets['addr_edit'].textChanged.connect(self.on_address_changed) + widgets['get_2d_btn'] = QPushButton("Get from 2D cursor") + widgets['get_2d_btn'].clicked.connect(self.on_get_2d_cursor) + addr_layout.addWidget(widgets['addr_edit']) + addr_layout.addWidget(widgets['get_2d_btn']) + layout.addRow("Start address:", addr_layout) + + # Data Source and Format + widgets['source_combo'] = QComboBox() + widgets['source_combo'].addItem("EPROM") + widgets['source_combo'].setCurrentText(data_source) + layout.addRow("Data Source:", widgets['source_combo']) + + widgets['format_combo'] = QComboBox() + widgets['format_combo'].addItems([ + "8-bit", + "16-bit Lo-Hi", + "16-bit Hi-Lo", + "32-bit Lo-Hi", + "32-bit Hi-Lo", + "Float" + ]) + widgets['format_combo'].setCurrentText(data_format) + layout.addRow("Data Format:", widgets['format_combo']) + + # Step + widgets['step_spin'] = QSpinBox() + widgets['step_spin'].setValue(step) + layout.addRow("Step:", widgets['step_spin']) + + # Factor, Offset, Precision + factor_layout = QHBoxLayout() + widgets['factor_spin'] = QDoubleSpinBox() + widgets['factor_spin'].setDecimals(6) + widgets['factor_spin'].setValue(factor) + widgets['offset_spin'] = QDoubleSpinBox() + widgets['offset_spin'].setValue(offset) + widgets['precision_spin'] = QSpinBox() + widgets['precision_spin'].setValue(precision) + factor_layout.addWidget(widgets['factor_spin']) + factor_layout.addWidget(QLabel("Offset:")) + factor_layout.addWidget(widgets['offset_spin']) + factor_layout.addWidget(QLabel("Precision:")) + factor_layout.addWidget(widgets['precision_spin']) + layout.addRow("Factor:", factor_layout) + + # Options + options_layout = QHBoxLayout() + widgets['sign_check'] = QCheckBox("Sign") + widgets['sign_check'].setChecked(is_signed) + widgets['invert_check'] = QCheckBox("Invert") + widgets['invert_check'].setChecked(is_inverted) + widgets['hex_radio'] = QRadioButton("Hexadecimal") + widgets['hex_radio'].setChecked(display_hex) + widgets['dec_radio'] = QRadioButton("Decimal") + widgets['dec_radio'].setChecked(not display_hex) + widgets['reciprocal_check'] = QCheckBox("Reciprocal") + widgets['reciprocal_check'].setChecked(is_reciprocal) + options_layout.addWidget(widgets['sign_check']) + options_layout.addWidget(widgets['invert_check']) + options_layout.addWidget(widgets['hex_radio']) + options_layout.addWidget(widgets['dec_radio']) + options_layout.addWidget(widgets['reciprocal_check']) + layout.addRow("", options_layout) + + # Formula + formula_layout = QHBoxLayout() + widgets['formula_combo'] = QComboBox() + widgets['formula_combo'].setEditable(True) + widgets['formula_combo'].addItem(formula) + widgets['fx_btn'] = QPushButton("f(x)") + widgets['fx_btn'].clicked.connect(self.edit_formula) + widgets['bar_btn'] = QPushButton("Bar") + widgets['c_btn'] = QPushButton("C") + widgets['percent_btn'] = QPushButton("%") + widgets['one_btn'] = QPushButton("1") + + widgets['bar_btn'].clicked.connect(lambda: self.apply_unit_conversion("bar")) + widgets['c_btn'].clicked.connect(lambda: self.apply_unit_conversion("C")) + widgets['percent_btn'].clicked.connect(lambda: self.apply_unit_conversion("%")) + widgets['one_btn'].clicked.connect(lambda: self.apply_unit_conversion("1")) + + formula_layout.addWidget(widgets['formula_combo']) + formula_layout.addWidget(widgets['fx_btn']) + formula_layout.addWidget(widgets['bar_btn']) + formula_layout.addWidget(widgets['c_btn']) + formula_layout.addWidget(widgets['percent_btn']) + formula_layout.addWidget(widgets['one_btn']) + layout.addRow("Formula:", formula_layout) + + widgets['layout'] = layout + return widgets + + def calculate_auto_range(self): + """Calculate min and max values from the data""" + if self.map_data.data is not None: + min_val = np.min(self.map_data.data) + max_val = np.max(self.map_data.data) + self.min_value.setValue(min_val) + self.max_value.setValue(max_val) + + def apply_unit_conversion(self, unit): + """Apply unit conversion factors""" + if unit == "bar": + self.factor_spin.setValue(0.001) + self.precision_spin.setValue(3) + elif unit == "C": + self.offset_spin.setValue(-273.15) + self.precision_spin.setValue(1) + elif unit == "%": + self.factor_spin.setValue(100) + self.precision_spin.setValue(1) + elif unit == "1": + self.factor_spin.setValue(1) + self.offset_spin.setValue(0) + self.precision_spin.setValue(0) + + def edit_formula(self): + """Open formula editor""" + # TODO: Implement formula editor + QMessageBox.information(self, "Formula Editor", "Formula editor not implemented yet") + + def on_get_2d_cursor(self): + """Get address from 2D cursor""" + if not self.map_viewer: + QMessageBox.warning(self, "Warning", "Map viewer not available") + return + + # Get the button that was clicked + sender = self.sender() + + # Determine which address field to update based on the button + if sender == self.get_2d_btn: + self.active_address_field = self.addr_edit + elif sender == self.axis_x_widgets['get_2d_btn']: + self.active_address_field = self.axis_x_widgets['addr_edit'] + elif sender == self.axis_y_widgets['get_2d_btn']: + self.active_address_field = self.axis_y_widgets['addr_edit'] + else: + return + + # Get cursor address from map viewer + address = self.map_viewer.get_cursor_address() + if not address: + QMessageBox.warning(self, "Warning", "No valid cursor position available") + return + + # Validate the address + if not self.map_viewer.validate_address(address): + QMessageBox.warning(self, "Warning", "Selected address is outside valid range for this map") + return + + # Update the address field with validated address + self.active_address_field.setText(f"0x{address}") + + # Provide visual feedback + self.active_address_field.setStyleSheet("background-color: #e6ffe6;") # Light green + QTimer.singleShot(1000, lambda: self.active_address_field.setStyleSheet("")) + + def validate_address_input(self, address: str) -> bool: + """Validate address input format and range""" + try: + # Remove 0x prefix if present + clean_addr = address.replace("0x", "").strip() + + # Check if it's a valid hex number + if not all(c in '0123456789ABCDEFabcdef' for c in clean_addr): + return False + + # Check length (max 6 digits for 24-bit address) + if len(clean_addr) > 6: + return False + + # Convert to integer and check range + addr_int = int(clean_addr, 16) + if not (0 <= addr_int <= 0xFFFFFF): + return False + + return True + + except ValueError: + return False + + def on_address_changed(self, text: str): + """Handle address input changes""" + sender = self.sender() + + # Validate address format + if text and not self.validate_address_input(text): + sender.setStyleSheet("background-color: #ffe6e6;") # Light red + else: + sender.setStyleSheet("") + + def save_changes(self): + """Save changes to the map data""" + try: + # Update main map properties + self.map_data.name = self.name_combo.currentText() + self.map_data.address = self.addr_edit.text() + self.map_data.cols = self.cols_spin.value() + self.map_data.rows = self.rows_spin.value() + self.map_data.data_format = self.format_combo.currentText() + + # Display mode + if self.original_radio.isChecked(): + self.map_data.display_mode = "Original" + elif self.difference_radio.isChecked(): + self.map_data.display_mode = "Difference" + else: + self.map_data.display_mode = "Percent" + + # Options + self.map_data.is_signed = self.sign_check.isChecked() + self.map_data.swap_axis = self.swap_check.isChecked() + self.map_data.display_hex = self.hex_radio.isChecked() + self.map_data.is_reciprocal = self.reciprocal_check.isChecked() + + # Values + self.map_data.value_min = self.min_value.value() + self.map_data.value_max = self.max_value.value() + self.map_data.factor = self.factor_spin.value() + self.map_data.offset = self.offset_spin.value() + self.map_data.precision = self.precision_spin.value() + self.map_data.formula = self.formula_combo.currentText() + + # X-axis properties + x = self.axis_x_widgets + self.map_data.x_name = x['name_combo'].currentText() + self.map_data.x_address = x['addr_edit'].text() + self.map_data.x_data_source = x['source_combo'].currentText() + self.map_data.x_data_format = x['format_combo'].currentText() + self.map_data.x_step = x['step_spin'].value() + self.map_data.x_factor = x['factor_spin'].value() + self.map_data.x_offset = x['offset_spin'].value() + self.map_data.x_precision = x['precision_spin'].value() + self.map_data.x_is_signed = x['sign_check'].isChecked() + self.map_data.x_is_inverted = x['invert_check'].isChecked() + self.map_data.x_display_hex = x['hex_radio'].isChecked() + self.map_data.x_is_reciprocal = x['reciprocal_check'].isChecked() + self.map_data.x_formula = x['formula_combo'].currentText() + + # Y-axis properties + y = self.axis_y_widgets + self.map_data.y_name = y['name_combo'].currentText() + self.map_data.y_address = y['addr_edit'].text() + self.map_data.y_data_source = y['source_combo'].currentText() + self.map_data.y_data_format = y['format_combo'].currentText() + self.map_data.y_step = y['step_spin'].value() + self.map_data.y_factor = y['factor_spin'].value() + self.map_data.y_offset = y['offset_spin'].value() + self.map_data.y_precision = y['precision_spin'].value() + self.map_data.y_is_signed = y['sign_check'].isChecked() + self.map_data.y_is_inverted = y['invert_check'].isChecked() + self.map_data.y_display_hex = y['hex_radio'].isChecked() + self.map_data.y_is_reciprocal = y['reciprocal_check'].isChecked() + self.map_data.y_formula = y['formula_combo'].currentText() + + self.accept() + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to save changes: {str(e)}") + + def load_map_data(self): + # Load map data from file + # TODO: Implement map data loading + pass diff --git a/src/gui/map_selection_dialog.py b/src/gui/map_selection_dialog.py new file mode 100644 index 0000000..9d9faec --- /dev/null +++ b/src/gui/map_selection_dialog.py @@ -0,0 +1,136 @@ +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, + QPushButton, QProgressBar, QLabel, QProgressDialog, QApplication, QMessageBox +) +from PySide6.QtCore import Qt, Signal + +from core.edc15p_maps import list_maps_by_group, EDC15P_MAPS + +class MapSelectionDialog(QDialog): + map_selected = Signal(str) # Emits map_id + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Selecionar Mapa") + self.resize(400, 600) + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Map tree + self.tree = QTreeWidget() + self.tree.setHeaderLabel("Mapas Disponíveis") + self.tree.itemDoubleClicked.connect(self.on_item_double_clicked) + layout.addWidget(self.tree) + + # Buttons + button_layout = QHBoxLayout() + + self.open_button = QPushButton("Abrir") + self.open_button.clicked.connect(self.accept) + self.open_button.setEnabled(False) # Disabled until selection + button_layout.addWidget(self.open_button) + + cancel_button = QPushButton("Cancelar") + cancel_button.clicked.connect(self.reject) + button_layout.addWidget(cancel_button) + + layout.addLayout(button_layout) + + # Progress bar (hidden by default) + self.progress_label = QLabel("Carregando...") + self.progress_label.hide() + layout.addWidget(self.progress_label) + + self.progress_bar = QProgressBar() + self.progress_bar.hide() + layout.addWidget(self.progress_bar) + + # Connect selection changed signal + self.tree.itemSelectionChanged.connect(self.on_selection_changed) + + def populate_tree(self): + """Populate tree with maps""" + try: + # Add maps grouped by category + groups = list_maps_by_group() + total_groups = len(groups) + for i, (group_name, maps) in enumerate(groups.items()): + group_item = QTreeWidgetItem(self.tree, [group_name]) + group_item.setFlags(group_item.flags() & ~Qt.ItemIsSelectable) # Make group non-selectable + + for map_def in maps: + # Find map ID + map_id = next((k for k, v in EDC15P_MAPS.items() if v == map_def), None) + if map_id: + # Create map item + map_item = QTreeWidgetItem(group_item) + map_item.setText(0, map_def.name) + map_item.setData(0, Qt.UserRole, map_id) + map_item.setData(0, Qt.UserRole + 1, "map") # Mark as map item + + # Create details folder + details_item = QTreeWidgetItem(map_item, ["Detalhes"]) + details_item.setFlags(details_item.flags() & ~Qt.ItemIsSelectable) # Make non-selectable + + # Add details as child items + details = [ + f"Endereço: {map_def.address}", + f"Dimensões: {map_def.rows}x{map_def.cols}", + f"Unidades: {map_def.units}", + f"Descrição: {map_def.description}" + ] + for detail in details: + detail_item = QTreeWidgetItem(details_item, [detail]) + detail_item.setFlags(detail_item.flags() & ~Qt.ItemIsSelectable) # Make non-selectable + + self.progress_bar.setValue((i + 1) * 100 // total_groups) + QApplication.processEvents() + + self.tree.expandAll() + self.progress_bar.setValue(100) + + except Exception as e: + QMessageBox.critical(self, "Error", f"Error populating tree: {str(e)}") + + def get_selected_map_id(self) -> str: + """Get the selected map ID""" + items = self.tree.selectedItems() + if items: + item = items[0] + # Check if it's a map item + if item.data(0, Qt.UserRole + 1) == "map": + map_id = item.data(0, Qt.UserRole) + return map_id + return None + + def on_selection_changed(self): + """Handle selection change""" + map_id = self.get_selected_map_id() + self.open_button.setEnabled(map_id is not None) + + def on_item_double_clicked(self, item, column): + """Handle double click on item""" + if item.data(0, Qt.UserRole + 1) == "map": + map_id = item.data(0, Qt.UserRole) + if map_id: + self.map_selected.emit(map_id) + self.accept() + + def show_progress(self, show: bool = True): + """Show or hide progress indicators""" + self.progress_label.setVisible(show) + self.progress_bar.setVisible(show) + if show: + self.progress_bar.setRange(0, 0) # Indeterminate progress + + def accept(self): + """Handle dialog acceptance""" + map_id = self.get_selected_map_id() + if map_id: + self.map_selected.emit(map_id) + super().accept() + else: + QMessageBox.warning(self, "Warning", "No map selected") + diff --git a/src/gui/map_tools.py b/src/gui/map_tools.py new file mode 100644 index 0000000..522ce34 --- /dev/null +++ b/src/gui/map_tools.py @@ -0,0 +1,186 @@ +from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, + QPushButton, QLabel, QSpinBox, QDoubleSpinBox, + QGroupBox, QMessageBox) +from PySide6.QtCore import Signal + +class MapToolsWidget(QWidget): + # Signals + interpolationRequested = Signal(int, int, int, int) # x1, y1, x2, y2 + smoothingRequested = Signal(int, int, int, int, float) # x1, y1, x2, y2, factor + valueChangeRequested = Signal(int, int, float) # x, y, value + percentageChangeRequested = Signal(int, int, int, int, float) # x1, y1, x2, y2, percentage + resetRequested = Signal(int, int, int, int) # x1, y1, x2, y2 + + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Selection group + selection_group = QGroupBox("Seleção") + selection_layout = QGridLayout() + + # Start position + selection_layout.addWidget(QLabel("Início:"), 0, 0) + + self.start_x = QSpinBox() + self.start_x.setMinimum(0) + selection_layout.addWidget(QLabel("X:"), 0, 1) + selection_layout.addWidget(self.start_x, 0, 2) + + self.start_y = QSpinBox() + self.start_y.setMinimum(0) + selection_layout.addWidget(QLabel("Y:"), 0, 3) + selection_layout.addWidget(self.start_y, 0, 4) + + # End position + selection_layout.addWidget(QLabel("Fim:"), 1, 0) + + self.end_x = QSpinBox() + self.end_x.setMinimum(0) + selection_layout.addWidget(QLabel("X:"), 1, 1) + selection_layout.addWidget(self.end_x, 1, 2) + + self.end_y = QSpinBox() + self.end_y.setMinimum(0) + selection_layout.addWidget(QLabel("Y:"), 1, 3) + selection_layout.addWidget(self.end_y, 1, 4) + + selection_group.setLayout(selection_layout) + layout.addWidget(selection_group) + + # Tools group + tools_group = QGroupBox("Ferramentas") + tools_layout = QVBoxLayout() + + # Interpolation + interpolate_btn = QPushButton("Interpolar") + interpolate_btn.clicked.connect(self.interpolate) + tools_layout.addWidget(interpolate_btn) + + # Smoothing + smooth_layout = QHBoxLayout() + smooth_layout.addWidget(QLabel("Fator:")) + + self.smooth_factor = QDoubleSpinBox() + self.smooth_factor.setMinimum(0.1) + self.smooth_factor.setMaximum(10.0) + self.smooth_factor.setValue(1.0) + self.smooth_factor.setSingleStep(0.1) + smooth_layout.addWidget(self.smooth_factor) + + smooth_btn = QPushButton("Suavizar") + smooth_btn.clicked.connect(self.smooth) + smooth_layout.addWidget(smooth_btn) + + tools_layout.addLayout(smooth_layout) + + # Value change + value_layout = QHBoxLayout() + value_layout.addWidget(QLabel("Valor:")) + + self.value_input = QDoubleSpinBox() + self.value_input.setMinimum(-999999.0) + self.value_input.setMaximum(999999.0) + self.value_input.setDecimals(3) + value_layout.addWidget(self.value_input) + + value_btn = QPushButton("Definir") + value_btn.clicked.connect(self.set_value) + value_layout.addWidget(value_btn) + + tools_layout.addLayout(value_layout) + + # Percentage change + percentage_layout = QHBoxLayout() + percentage_layout.addWidget(QLabel("%:")) + + self.percentage_input = QDoubleSpinBox() + self.percentage_input.setMinimum(-100.0) + self.percentage_input.setMaximum(100.0) + self.percentage_input.setDecimals(1) + percentage_layout.addWidget(self.percentage_input) + + percentage_btn = QPushButton("Alterar") + percentage_btn.clicked.connect(self.change_percentage) + percentage_layout.addWidget(percentage_btn) + + tools_layout.addLayout(percentage_layout) + + # Reset + reset_btn = QPushButton("Restaurar Original") + reset_btn.clicked.connect(self.reset) + tools_layout.addWidget(reset_btn) + + tools_group.setLayout(tools_layout) + layout.addWidget(tools_group) + + def set_map_size(self, rows: int, cols: int): + """Update spinbox ranges based on map size""" + self.start_x.setMaximum(cols - 1) + self.end_x.setMaximum(cols - 1) + self.start_y.setMaximum(rows - 1) + self.end_y.setMaximum(rows - 1) + + # Set end values to maximum + self.end_x.setValue(cols - 1) + self.end_y.setValue(rows - 1) + + def get_selection(self): + """Get the current selection coordinates""" + return ( + min(self.start_x.value(), self.end_x.value()), + min(self.start_y.value(), self.end_y.value()), + max(self.start_x.value(), self.end_x.value()), + max(self.start_y.value(), self.end_y.value()) + ) + + def interpolate(self): + """Request interpolation of selected region""" + x1, y1, x2, y2 = self.get_selection() + self.interpolationRequested.emit(x1, y1, x2, y2) + + def smooth(self): + """Request smoothing of selected region""" + x1, y1, x2, y2 = self.get_selection() + self.smoothingRequested.emit(x1, y1, x2, y2, self.smooth_factor.value()) + + def set_value(self): + """Request value change""" + x1, y1, x2, y2 = self.get_selection() + if x1 == x2 and y1 == y2: + self.valueChangeRequested.emit(x1, y1, self.value_input.value()) + else: + reply = QMessageBox.question( + self, + "Confirmar Alteração", + "Deseja aplicar este valor a toda a região selecionada?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + for x in range(x1, x2 + 1): + for y in range(y1, y2 + 1): + self.valueChangeRequested.emit(x, y, self.value_input.value()) + + def change_percentage(self): + """Request percentage change in region""" + x1, y1, x2, y2 = self.get_selection() + self.percentageChangeRequested.emit(x1, y1, x2, y2, self.percentage_input.value()) + + def reset(self): + """Request reset of selected region""" + x1, y1, x2, y2 = self.get_selection() + reply = QMessageBox.question( + self, + "Confirmar Restauração", + "Deseja restaurar os valores originais da região selecionada?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.resetRequested.emit(x1, y1, x2, y2) diff --git a/src/gui/map_viewer.py b/src/gui/map_viewer.py new file mode 100644 index 0000000..042b8a4 --- /dev/null +++ b/src/gui/map_viewer.py @@ -0,0 +1,428 @@ +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, + QTableWidgetItem, QComboBox, QLabel, QSplitter, QToolBar, + QTreeWidget, QTreeWidgetItem, QMenu, QDialog, QStatusBar, + QSizePolicy, QHeaderView, QMessageBox +) +from PySide6.QtCore import Qt, Signal, QTimer +from PySide6.QtGui import QKeySequence, QColor, QBrush +from PySide6.QtGui import QAction +from gui.map_tools import MapToolsWidget +from core.edc15p_maps import EDC15P_MAPS, list_maps_by_group +from core.command import CommandHistory, MapValueCommand +from core.file_handler import MapData +import numpy as np + +class MapViewer(QWidget): + map_changed = Signal(str) # Emitted when map selection changes + cursor_position_changed = Signal(int, int, str) # Updated to include address + + def __init__(self, parent=None): + super().__init__(parent) + self.current_file = None + self.current_map = None + self.current_cursor_pos = (0, 0) # Track cursor position + self.command_history = CommandHistory() + self._ignore_cell_changes = False # Flag to prevent recursive updates + self.highlighted_cell = None + self.scroll_step = 1 # Number of cells to scroll per wheel step + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + # Create splitter + splitter = QSplitter(Qt.Horizontal) + + # Left side - Tree view for map selection + self.map_tree = QTreeWidget() + self.map_tree.setHeaderLabel("Maps") + self.map_tree.setMinimumWidth(200) + self.map_tree.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) + + # Right side container + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.setSpacing(0) + + # Tools widget + self.tools_widget = MapToolsWidget() + self.tools_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) + right_layout.addWidget(self.tools_widget) + + # Table for map data + self.table = QTableWidget() + self.table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.table.horizontalHeader().setStretchLastSection(True) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.table.verticalHeader().setSectionResizeMode(QHeaderView.Stretch) + + # Make the table adjust to available space + self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.table.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + # Add table to layout + right_layout.addWidget(self.table) + + # Add widgets to splitter + splitter.addWidget(self.map_tree) + splitter.addWidget(right_widget) + + # Set splitter properties + splitter.setStretchFactor(0, 0) # Tree doesn't stretch + splitter.setStretchFactor(1, 1) # Table area stretches + + # Add splitter to main layout + layout.addWidget(splitter) + + # Add status bar + self.status_bar = QStatusBar() + self.status_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + layout.addWidget(self.status_bar) + + # Connect signals + self.map_tree.itemClicked.connect(self.on_map_selected) + self.table.cellClicked.connect(self.on_cell_clicked) + self.table.cellChanged.connect(self.on_cell_changed) + + # Set context menu + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_context_menu) + + def set_ecu_file(self, ecu_file): + """Set the current ECU file and update UI""" + self.current_file = ecu_file + self.update_map_tree() + + def update_map_tree(self): + """Update the map tree with available maps""" + self.map_tree.clear() + + if not self.current_file: + return + + # Get grouped maps + grouped_maps = list_maps_by_group() + + # Add maps grouped by category + for group_name, maps_list in grouped_maps.items(): + # Create group item + group_item = QTreeWidgetItem(self.map_tree, [group_name]) + group_item.setFlags(group_item.flags() & ~Qt.ItemIsSelectable) + + for map_def in maps_list: + # Get map ID from EDC15P_MAPS + map_id = next((k for k, v in EDC15P_MAPS.items() if v.name == map_def.name), None) + if not map_id: + continue + + # Create map item + map_item = QTreeWidgetItem(group_item, [map_def.name]) + map_item.setData(0, Qt.UserRole, map_id) # Store map ID + map_item.setData(0, Qt.UserRole + 1, "map") # Mark as map item + + # Add details as child items + details = [ + f"Address: {map_def.address}", + f"Size: {map_def.rows}x{map_def.cols}", + f"Units: {map_def.units}", + f"Description: {map_def.description}" + ] + + for detail in details: + detail_item = QTreeWidgetItem(map_item, [detail]) + detail_item.setFlags(detail_item.flags() & ~Qt.ItemIsSelectable) + + # Expand all items + self.map_tree.expandAll() + + def on_map_selected(self, item): + """Handle map selection changes""" + # Check if it's a map item + if item.data(0, Qt.UserRole + 1) != "map": + return + + # Get map ID from item data + map_id = item.data(0, Qt.UserRole) + if not map_id: + return + + try: + # Get map data from file + if not self.current_file or map_id not in self.current_file.maps: + return + + # Load map data + self.current_map = self.current_file.maps[map_id] + self.update_table() + self.map_changed.emit(map_id) + + except Exception as e: + QMessageBox.critical(self, "Error", f"Error loading map: {str(e)}") + + def undo(self): + """Undo last change""" + if self.command_history.undo(): + self.update_table() + self._update_undo_redo_state() + + def redo(self): + """Redo last undone change""" + if self.command_history.redo(): + self.update_table() + self._update_undo_redo_state() + + def _update_undo_redo_state(self): + """Update undo/redo action states""" + self.undo_action.setEnabled(self.command_history.can_undo()) + self.redo_action.setEnabled(self.command_history.can_redo()) + + def on_cell_changed(self, row, col): + """Handle cell value changes""" + if self._ignore_cell_changes or not self.current_map: + return + + try: + item = self.table.item(row, col) + if not item: + return + + # Get the new value from the item + try: + value = float(item.text()) + except ValueError: + # Reset to original value if input is invalid + self._ignore_cell_changes = True + try: + original_value = self.current_map.data[row][col] + item.setText(str(original_value)) + finally: + self._ignore_cell_changes = False + return + + # Create and execute command + old_value = self.current_map.data[row][col] + if old_value != value: + command = MapValueCommand(self.current_map, row, col, old_value, value) + self.command_history.execute(command) + + except Exception as e: + # Reset to original value + self._ignore_cell_changes = True + try: + original_value = self.current_map.data[row][col] + item = QTableWidgetItem(str(original_value)) + self.table.setItem(row, col, item) + finally: + self._ignore_cell_changes = False + + def on_cell_clicked(self, row, col): + """Handle cell click event""" + self.current_cursor_pos = (row, col) + address = self.get_cursor_address() + + # Update status bar + self.status_bar.showMessage(f"Position: ({row}, {col}) | Address: 0x{address}") + + # Emit signal with position and address + self.cursor_position_changed.emit(row, col, address) + + # Highlight the clicked cell + self.highlight_cell(row, col) + + def wheelEvent(self, event): + """Handle mouse wheel events""" + if not self.current_map or not self.table: + return + + # Get current selection + current = self.table.currentItem() + if not current: + return + + row = current.row() + col = current.column() + + # Calculate new position based on wheel direction + if event.angleDelta().y() > 0: # Scroll up + new_row = max(0, row - self.scroll_step) + else: # Scroll down + new_row = min(self.current_map.rows - 1, row + self.scroll_step) + + # If shift is pressed, move horizontally instead + if event.modifiers() & Qt.ShiftModifier: + if event.angleDelta().y() > 0: # Scroll left + new_col = max(0, col - self.scroll_step) + else: # Scroll right + new_col = min(self.current_map.cols - 1, col + self.scroll_step) + new_row = row + else: + new_col = col + + # Select new cell + self.table.setCurrentCell(new_row, new_col) + + # Update cursor position and emit signal + self.current_cursor_pos = (new_row, new_col) + address = self.get_cursor_address() + self.cursor_position_changed.emit(new_row, new_col, address) + + # Ensure the cell is visible + self.table.scrollToItem(self.table.item(new_row, new_col)) + + # Highlight the new cell + self.highlight_cell(new_row, new_col) + + # Accept the event + event.accept() + + def highlight_cell(self, row, col, duration_ms=1000): + """Temporarily highlight a cell""" + # Reset previous highlight if any + if self.highlighted_cell: + prev_row, prev_col = self.highlighted_cell + item = self.table.item(prev_row, prev_col) + if item: + item.setBackground(QBrush()) # Clear background + + # Highlight new cell + item = self.table.item(row, col) + if item: + item.setBackground(QBrush(QColor(255, 255, 0, 100))) # Light yellow + self.highlighted_cell = (row, col) + + # Clear highlight after duration + QTimer.singleShot(duration_ms, lambda: self.clear_highlight(row, col)) + + def clear_highlight(self, row, col): + """Clear cell highlight""" + if self.highlighted_cell == (row, col): + item = self.table.item(row, col) + if item: + item.setBackground(QBrush()) + self.highlighted_cell = None + + def validate_address(self, address: str) -> bool: + """Validate if an address is within the valid range of the current map""" + try: + # Convert address to integer + addr_int = int(address.replace("0x", ""), 16) + + # Get map boundaries + base_addr = int(self.current_map.address, 16) + bytes_per_value = { + "8-bit": 1, + "16-bit Lo-Hi": 2, + "16-bit Hi-Lo": 2, + "32-bit Lo-Hi": 4, + "32-bit Hi-Lo": 4, + "Float": 4 + }.get(self.current_map.data_format, 2) + + total_bytes = self.current_map.rows * self.current_map.cols * bytes_per_value + end_addr = base_addr + total_bytes + + # Check if address is within bounds + return base_addr <= addr_int < end_addr + + except ValueError: + return False + + def get_cursor_address(self) -> str: + """Calculate address at current cursor position""" + if not self.current_map: + return "" + + try: + base_addr = int(self.current_map.address, 16) + row, col = self.current_cursor_pos + + # Calculate bytes per value based on data format + bytes_per_value = { + "8-bit": 1, + "16-bit Lo-Hi": 2, + "16-bit Hi-Lo": 2, + "32-bit Lo-Hi": 4, + "32-bit Hi-Lo": 4, + "Float": 4 + }.get(self.current_map.data_format, 2) + + # Calculate offset based on position + offset = (row * self.current_map.cols + col) * bytes_per_value + address = base_addr + offset + + # Validate the calculated address + if 0 <= address <= 0xFFFFFF: # Max 24-bit address + return f"{address:06X}" + else: + return "" + + except (ValueError, TypeError): + return "" + + def update_table(self): + """Update table with current map data""" + if not self.current_map: + return + + self._ignore_cell_changes = True + try: + self.table.setRowCount(self.current_map.rows) + self.table.setColumnCount(self.current_map.cols) + + # Set headers + x_headers = [f"{x:.0f}" for x in self.current_map.x_axis] + y_headers = [f"{y:.0f}" for y in self.current_map.y_axis] + self.table.setHorizontalHeaderLabels(x_headers) + self.table.setVerticalHeaderLabels(y_headers) + + # Fill table with data + for row in range(self.current_map.rows): + for col in range(self.current_map.cols): + value = self.current_map.get_value(row, col) + item = QTableWidgetItem(f"{value:.2f}") + item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) + self.table.setItem(row, col, item) + + # Adjust table size to fit content + self.table.resizeColumnsToContents() + self.table.resizeRowsToContents() + + # Ensure minimum cell size + min_cell_width = 60 + min_cell_height = 25 + for col in range(self.current_map.cols): + if self.table.columnWidth(col) < min_cell_width: + self.table.setColumnWidth(col, min_cell_width) + for row in range(self.current_map.rows): + if self.table.rowHeight(row) < min_cell_height: + self.table.setRowHeight(row, min_cell_height) + + finally: + self._ignore_cell_changes = False + + def resizeEvent(self, event): + """Handle window resize events""" + super().resizeEvent(event) + # Adjust table columns and rows when window is resized + if self.current_map: + self.table.resizeColumnsToContents() + self.table.resizeRowsToContents() + + def show_context_menu(self, position): + """Show context menu for the map viewer""" + from .map_manager_dialog import MapManagerDialog + + context_menu = QMenu(self) + map_manager_action = context_menu.addAction("Map Manager") + + action = context_menu.exec_(self.mapToGlobal(position)) + if action == map_manager_action: + dialog = MapManagerDialog(self.current_map, self) + dialog.set_map_viewer(self) # Set reference to map viewer + if dialog.exec_() == QDialog.Accepted: + # Handle the changes from the dialog + self.update_table() # Refresh the display