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