From 012f7ce44539f2d2f5ac31f98ef8214153608c3b Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby <pierre-antoine.rouby@inrae.fr> Date: Wed, 26 Apr 2023 16:33:21 +0200 Subject: [PATCH] geometry: Copy/Paste with system clipboard. --- src/Model/Except.py | 41 +++++++++++++++++++++++++++ src/Model/Geometry/ProfileXYZ.py | 33 +++++++++++++++++---- src/View/ASubWindow.py | 11 +++++-- src/View/Geometry/GeometryWindow.py | 23 ++++++++++----- src/View/Geometry/qtableview_reach.py | 29 ++++++++++++++----- 5 files changed, 115 insertions(+), 22 deletions(-) diff --git a/src/Model/Except.py b/src/Model/Except.py index a468b037..4c71bbc2 100644 --- a/src/Model/Except.py +++ b/src/Model/Except.py @@ -116,3 +116,44 @@ class FileFormatError(ExeceptionWithMessageBox): _translate("Exception", "format because of") + f" '{self.reason}'" ) + + +class ClipboardFormatError(ExeceptionWithMessageBox): + def __init__(self, mime=None, header=None, data=None): + super(ClipboardFormatError, self).__init__( + title = _translate("Exception", "Clipboard format error") + ) + + self.mime = mime + self.header = header + self.data = data + + if self.mime is not None: + self.msg = f"Impossible to decode data to mime code '{self.mime}'" + else: + if len(self.header) == 0: + msg = _translate("Exception", "without header") + else: + msg = ( + _translate("Exception", "with header") + + f": {self.header}" + ) + + self.msg = ( + _translate("Exception", "Invalid clipboard data format:") + + f" '{self.data}' {msg}" + ) + + self.alert() + + def __str__(self): + return self.msg + + def header(self): + return _translate("Exception", "Clipboard format error") + + def short_message(self): + return _translate("Exception", "Clipboard format unknown") + + def message(self): + return self.msg diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index bd9dec94..1f2b0ddd 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -10,11 +10,12 @@ from Model.Geometry.PointXYZ import PointXYZ from Model.Geometry.Vector_1d import Vector1d class ProfileXYZ(Profile): - def __init__(self, num: int = 0, - code1: int = 0, code2: int = 0, + def __init__(self, + name: str = "", + kp: float = 0., + reach = None, nb_point: int = 0, - kp: float = 0., name: str = "", - reach = None): + code1: int = 0, code2: int = 0): """ProfileXYZ constructor Args: @@ -28,7 +29,7 @@ class ProfileXYZ(Profile): Nothing. """ super(ProfileXYZ, self).__init__( - num = num, + num = 0, name = name, kp = kp, code1 = code1, code2 = code2, @@ -36,6 +37,28 @@ class ProfileXYZ(Profile): reach = reach, ) + @classmethod + def from_data(cls, header, data): + profile = None + try: + if len(header) == 0: + profile = cls( + *data + ) + else: + valid_header = {'name', 'reach', 'kp'} + d = {} + for i, v in enumerate(data): + h = header[i].strip().lower().split(' ')[0] + if h in valid_header: + d[h] = v + + profile = cls(**d) + except Exception as e: + raise ClipboardFormatError(header, data) + + return profile + def x(self): return [point.x for point in self._points] diff --git a/src/View/ASubWindow.py b/src/View/ASubWindow.py index a47a839a..89fd37b3 100644 --- a/src/View/ASubWindow.py +++ b/src/View/ASubWindow.py @@ -24,14 +24,19 @@ class WindowToolKit(object): def __init__(self, parent=None): super(WindowToolKit, self).__init__() + def copyTableIntoClipboard(self, table): + stream = StringIO() + csv.writer(stream, delimiter='\t').writerows(table) + QApplication.clipboard().setText(stream.getvalue()) + def parseClipboardTable(self): clip = QApplication.clipboard() mime = clip.mimeData() - # print(mime.formats()) - data = mime.data('text/plain').data().decode() + if 'text/plain' not in mime.formats(): + raise ClipboardFormatError(mime='text/plain') + data = mime.data('text/plain').data().decode() has_header = csv.Sniffer().has_header(data) - print(f"header? {has_header}") header = [] values = [] diff --git a/src/View/Geometry/GeometryWindow.py b/src/View/Geometry/GeometryWindow.py index 3818b2b3..3c162535 100644 --- a/src/View/Geometry/GeometryWindow.py +++ b/src/View/Geometry/GeometryWindow.py @@ -6,7 +6,7 @@ import sys import time from copy import deepcopy -from tools import timer +from tools import timer, trace from PyQt5 import QtWidgets from PyQt5.QtGui import ( @@ -372,18 +372,27 @@ class GeometryWindow(QMainWindow, WindowToolKit): .selectionModel()\ .selectedRows() - self._clipboard = [] + table = [] + table.append(["name", "kp"]) for row in rows: - self._clipboard.append( - deepcopy( - self._reach.profile(row.row()) - ) + profile = self._reach.profile(row.row()) + table.append( + [profile.name, profile.kp] ) + self.copyTableIntoClipboard(table) + def paste(self): + header, data = self.parseClipboardTable() + + if len(header) != 0: + header.append("reach") + for row in data: + row.append(self._reach) + row = self.index_selected_row() - self._tablemodel.paste(row, self._clipboard) + self._tablemodel.paste(row, header, data) self.select_current_profile() def undo(self): diff --git a/src/View/Geometry/qtableview_reach.py b/src/View/Geometry/qtableview_reach.py index 56405933..bf6d28ef 100644 --- a/src/View/Geometry/qtableview_reach.py +++ b/src/View/Geometry/qtableview_reach.py @@ -2,6 +2,8 @@ import time +from tools import timer, trace + from PyQt5.QtGui import ( QKeySequence, QColor ) @@ -16,6 +18,7 @@ from PyQt5.QtWidgets import ( ) from Model.Geometry import Reach +from Model.Geometry.ProfileXYZ import ProfileXYZ from View.Geometry.ReachUndoCommand import * @@ -30,11 +33,16 @@ class TableEditableModel(QAbstractTableModel): self._undo_stack = undo self._reach = reach + # Hack for qtlinguist + _ = _translate("Geometry", "Name") + _ = _translate("Geometry", "Kp (m)") + _ = _translate("Geometry", "Type") + if headers is None: self.headers = [ - _translate("Geometry", "Name"), - _translate("Geometry", "Kp (m)"), - _translate("Geometry", "Type") + "Name", + "Kp (m)", + "Type" ] else: self.headers = headers @@ -79,7 +87,7 @@ class TableEditableModel(QAbstractTableModel): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: if section < len(self.headers): - return self.headers[section] + return _translate("Geometry", self.headers[section]) else: return str(section + 1) @@ -204,18 +212,25 @@ class TableEditableModel(QAbstractTableModel): self.endMoveRows() self.layoutChanged.emit() - def paste(self, row, profiles): + @trace + def paste(self, row, header, data): if row > self._reach.number_profiles: return - if len(profiles) == 0: + if len(data) == 0: return self.layoutAboutToBeChanged.emit() self._undo_stack.push( PasteCommand( - self._reach, row, profiles + self._reach, row, + list( + map( + lambda d: ProfileXYZ.from_data(header, d), + data + ) + ) ) ) -- GitLab