# Window.py -- Pamhyr # Copyright (C) 2023-2025 INRAE # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -*- coding: utf-8 -*- import copy import sys import csv from time import time import logging from tools import trace, timer, logger_exception from Modules import Modules from PyQt5.QtGui import ( QKeySequence, ) from PyQt5.QtCore import ( QModelIndex, Qt, QEvent, QCoreApplication ) from PyQt5.QtWidgets import ( QApplication, QMainWindow, QFileDialog, QCheckBox, QUndoStack, QShortcut, QTableView, QAbstractItemView, QHeaderView, QVBoxLayout, QAction, ) from Model.Geometry.Reach import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ from View.Tools.PamhyrWindow import PamhyrWindow from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.Tools.Plot.PamhyrCanvas import MplCanvas from View.Geometry.Profile.Plot import Plot from View.Geometry.Profile.Table import GeometryProfileTableModel from View.Geometry.Profile.Translate import GeometryProfileTranslate from View.Geometry.PurgeDialog import PurgeDialog _translate = QCoreApplication.translate logger = logging.getLogger() class ProfileWindow(PamhyrWindow): _pamhyr_ui = "GeometryCrossSection" _pamhyr_name = "Geometry cross-section" def __init__(self, profile=None, study=None, config=None, parent=None): self._profile = profile trad = GeometryProfileTranslate() name = ( trad[self._pamhyr_name] + f" - {self._profile.name} {self._profile.rk}" ) super(ProfileWindow, self).__init__( title=name, study=study, config=config, trad=trad, parent=parent ) self._hash_data.append(profile) self.setup_table() self.setup_plot() self.setup_connections() def setup_table(self): if self._study.is_read_only(): editable_headers = [] else: editable_headers = ["name", "x", "y", "z"] table_headers = self._trad.get_dict("table_headers") table = self.find(QTableView, "tableView") self._tablemodel = GeometryProfileTableModel( table_view=table, table_headers=table_headers, editable_headers=editable_headers, data=self._profile, undo=self._undo_stack ) table.setModel(self._tablemodel) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) def setup_plot(self): self._tablemodel.blockSignals(True) self._canvas = MplCanvas(width=3, height=4, dpi=100) self._canvas.setObjectName("canvas") self._toolbar = PamhyrPlotToolbar( self._canvas, self, items=["home", "zoom", "save", "iso", "back/forward", "move"] ) self._plot_layout = self.find(QVBoxLayout, "verticalLayout") self._plot_layout.addWidget(self._toolbar) self._plot_layout.addWidget(self._canvas) self._plot = Plot( canvas=self._canvas, data=self._profile, trad=self._trad, toolbar=self._toolbar, table=self.find(QTableView, "tableView"), parent=self ) self._plot.draw() self._tablemodel.blockSignals(False) def setup_connections(self): if self._study.is_read_only(): actions = {} else: actions = { "action_sort_asc": self.sort_Y_ascending, "action_sort_des": self.sort_Y_descending, "action_up": self.move_up, "action_down": self.move_down, "action_add": self.add, "action_delete": self.delete, "action_purge": self.purge, "action_reverse": self.reverse, } for action in actions: self.find(QAction, action)\ .triggered.connect(actions[action]) table = self.find(QTableView, f"tableView") table.selectionModel()\ .selectionChanged\ .connect(self.update_points_selection) self._tablemodel.dataChanged.connect(self.update) def update_points_selection(self): rows = self.index_selected_rows() self._plot.select_points_from_indices(rows) def update(self): self.update_plot() self._propagate_update(key=Modules.GEOMETRY) def _update(self, redraw=False, propagate=True): if redraw: self.update_plot() if propagate: self._propagate_update(key=Modules.GEOMETRY) def update_plot(self): self._tablemodel.blockSignals(True) self._plot.update() self._tablemodel.blockSignals(False) def _propagated_update(self, key=Modules(0)): if Modules.GEOMETRY not in key: return self._tablemodel.layoutChanged.emit() self._update(redraw=True, propagate=False) def index_selected_row(self): table = self.find(QTableView, "tableView") rows = table.selectionModel()\ .selectedRows() if len(rows) == 0: return 0 return rows[0].row() def index_selected_rows(self): table = self.find(QTableView, "tableView") return list( map( lambda r: r.row(), table.selectionModel().selectedRows() ) ) def add(self): table = self.find(QTableView, "tableView") if len(table.selectedIndexes()) == 0: self._tablemodel.insert_row(self._tablemodel.rowCount()) else: row = self.index_selected_row() self._tablemodel.insert_row(row + 1) self.update() def delete(self): table = self.find(QTableView, "tableView") rows = sorted( list( set( [index.row() for index in table.selectedIndexes()] ) ) ) if len(rows) > 0: self._tablemodel.remove_rows(rows) self.update() def sort_X_ascending(self): self._tablemodel.sort('x', order=Qt.AscendingOrder) self.update() def sort_X_descending(self): self._tablemodel.sort('x', order=Qt.DescendingOrder) self.update() def sort_Y_ascending(self): self._tablemodel.sort('y', order=Qt.AscendingOrder) self.update() def sort_Y_descending(self): self._tablemodel.sort('y', order=Qt.DescendingOrder) self.update() def move_down(self): rows = list( set( [index.row() for index in self.find(QTableView, "tableView").selectedIndexes()] ) ) for row in rows: if row < self._tablemodel.rowCount() - 1: self._tablemodel.move_down(row) self.update() def move_up(self): rows = list( set( [index.row() for index in self.find(QTableView, "tableView").selectedIndexes()] ) ) for row in rows: if 0 < row: self._tablemodel.move_up(row) self.update() def purge(self): self._tablemodel.purge() self.update() def purge(self): try: dlg = PurgeDialog( trad=self._trad, parent=self ) if dlg.exec(): self._tablemodel.purge(dlg.np_purge) self._plot.draw() except Exception as e: logger_exception(e) return def reverse(self): self._tablemodel.reverse() self.update() def _copy(self): table = self.find(QTableView, "tableView") rows = table.selectionModel().selectedRows() data = [] data.append(["name", "x", "y", "z"]) for row in rows: point = self._profile.point(row.row()) data.append( [ point.name, point.x, point.y, point.z ] ) self.copyTableIntoClipboard(data) def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: return if len(header) != 0: header.append("profile") # for row in data: # row.append(self._profile) row = self.index_selected_row() self._tablemodel.paste(row, header, data) self.update() def _undo(self): self._tablemodel.undo() self.update() def _redo(self): self._tablemodel.redo() self.update()