From dde1107a2332a17341be943b444e6d659397589a Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Fri, 1 Mar 2019 10:17:07 +0100 Subject: [PATCH 1/2] OWPythonScript: dropping and pasting of python scripts The current script is replaced (in an undoable way) with the pasted script. --- Orange/widgets/data/owpythonscript.py | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Orange/widgets/data/owpythonscript.py b/Orange/widgets/data/owpythonscript.py index 5d2c5bc0f38..5389e845a1f 100644 --- a/Orange/widgets/data/owpythonscript.py +++ b/Orange/widgets/data/owpythonscript.py @@ -17,6 +17,7 @@ ) from AnyQt.QtCore import Qt, QRegExp, QByteArray, QItemSelectionModel +from Orange.canvas.gui.utils import OSX_NSURL_toLocalFile from Orange.data import Table from Orange.base import Learner, Model from Orange.widgets import widget, gui @@ -35,6 +36,20 @@ def text_format(foreground=Qt.black, weight=QFont.Normal): return fmt +def to_local_file(url): + return OSX_NSURL_toLocalFile(url) or url.toLocalFile() + + +def read_file_content(url, limit=None): + filename = to_local_file(url) + try: + with open(filename, encoding="utf-8", errors='strict') as f: + text = f.read(limit) + return text + except (OSError, UnicodeDecodeError): + return None + + class PythonSyntaxHighlighter(QSyntaxHighlighter): def __init__(self, parent=None): @@ -138,6 +153,24 @@ def keyPressEvent(self, event): else: super().keyPressEvent(event) + def insertFromMimeData(self, source): + """ + Reimplemented from QPlainTextEdit.insertFromMimeData. + """ + urls = source.urls() + if urls: + self.pasteFile(urls[0]) + else: + super().insertFromMimeData(source) + + def pasteFile(self, url): + new = read_file_content(url) + if new: + # inserting text like this allows undo + cursor = QTextCursor(self.document()) + cursor.select(QTextCursor.Document) + cursor.insertText(new) + class PythonConsole(QPlainTextEdit, code.InteractiveConsole): def __init__(self, locals=None, parent=None): @@ -524,6 +557,8 @@ def __init__(self): if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) + self.setAcceptDrops(True) + self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved) self.controlArea.layout().addStretch(1) self.resize(800, 600) @@ -708,6 +743,20 @@ def commit(self): out_var = None getattr(self.Outputs, signal).send(out_var) + def dragEnterEvent(self, event): + urls = event.mimeData().urls() + if urls: + # try reading the file as text + c = read_file_content(urls[0], limit=1000) + if c is not None: + event.acceptProposedAction() + + def dropEvent(self, event): + """Handle file drops""" + urls = event.mimeData().urls() + if urls: + self.text.pasteFile(urls[0]) + if __name__ == "__main__": # pragma: no cover WidgetPreview(OWPythonScript).run() From d0b36bc0a65f49f97b70798f9a19a5338e1a8fb6 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Fri, 1 Mar 2019 11:21:45 +0100 Subject: [PATCH 2/2] OWPythonScript: dropping scripts tests --- Orange/widgets/data/owpythonscript.py | 7 +- .../widgets/data/tests/test_owpythonscript.py | 78 ++++++++++++++++++- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/Orange/widgets/data/owpythonscript.py b/Orange/widgets/data/owpythonscript.py index 5389e845a1f..5a2117d56f6 100644 --- a/Orange/widgets/data/owpythonscript.py +++ b/Orange/widgets/data/owpythonscript.py @@ -40,8 +40,7 @@ def to_local_file(url): return OSX_NSURL_toLocalFile(url) or url.toLocalFile() -def read_file_content(url, limit=None): - filename = to_local_file(url) +def read_file_content(filename, limit=None): try: with open(filename, encoding="utf-8", errors='strict') as f: text = f.read(limit) @@ -164,7 +163,7 @@ def insertFromMimeData(self, source): super().insertFromMimeData(source) def pasteFile(self, url): - new = read_file_content(url) + new = read_file_content(to_local_file(url)) if new: # inserting text like this allows undo cursor = QTextCursor(self.document()) @@ -747,7 +746,7 @@ def dragEnterEvent(self, event): urls = event.mimeData().urls() if urls: # try reading the file as text - c = read_file_content(urls[0], limit=1000) + c = read_file_content(to_local_file(urls[0]), limit=1000) if c is not None: event.acceptProposedAction() diff --git a/Orange/widgets/data/tests/test_owpythonscript.py b/Orange/widgets/data/tests/test_owpythonscript.py index 3abace1ebba..17b5d0c1b2c 100644 --- a/Orange/widgets/data/tests/test_owpythonscript.py +++ b/Orange/widgets/data/tests/test_owpythonscript.py @@ -1,8 +1,12 @@ # Test methods with long descriptive names can omit docstrings # pylint: disable=missing-docstring +from AnyQt.QtCore import QMimeData, QUrl, QPoint, Qt +from AnyQt.QtGui import QDragEnterEvent, QDropEvent + from Orange.data import Table from Orange.classification import LogisticRegressionLearner -from Orange.widgets.data.owpythonscript import OWPythonScript +from Orange.tests import named_file +from Orange.widgets.data.owpythonscript import OWPythonScript, read_file_content from Orange.widgets.tests.base import WidgetTest from Orange.widgets.widget import OWWidget @@ -134,3 +138,75 @@ def test_store_current_script(self): self.widget = self.create_widget(OWPythonScript, stored_settings=settings) script = self.widget.text.toPlainText() self.assertEqual("42", script) + + def test_read_file_content(self): + with named_file("Content", suffix=".42") as fn: + # valid file opens + content = read_file_content(fn) + self.assertEqual("Content", content) + # invalid utf-8 file does not + with open(fn, "wb") as f: + f.write(b"\xc3\x28") + content = read_file_content(fn) + self.assertIsNone(content) + + def test_script_insert_mime_text(self): + current = self.widget.text.toPlainText() + insert = "test\n" + cursor = self.widget.text.cursor() + cursor.setPos(0, 0) + mime = QMimeData() + mime.setText(insert) + self.widget.text.insertFromMimeData(mime) + self.assertEqual(insert + current, self.widget.text.toPlainText()) + + def test_script_insert_mime_file(self): + with named_file("test", suffix=".42") as fn: + previous = self.widget.text.toPlainText() + mime = QMimeData() + url = QUrl.fromLocalFile(fn) + mime.setUrls([url]) + self.widget.text.insertFromMimeData(mime) + self.assertEqual("test", self.widget.text.toPlainText()) + self.widget.text.undo() + self.assertEqual(previous, self.widget.text.toPlainText()) + + def test_dragEnterEvent_accepts_text(self): + with named_file("Content", suffix=".42") as fn: + event = self._drag_enter_event(QUrl.fromLocalFile(fn)) + self.widget.dragEnterEvent(event) + self.assertTrue(event.isAccepted()) + + def test_dragEnterEvent_rejects_binary(self): + with named_file("", suffix=".42") as fn: + with open(fn, "wb") as f: + f.write(b"\xc3\x28") + event = self._drag_enter_event(QUrl.fromLocalFile(fn)) + self.widget.dragEnterEvent(event) + self.assertFalse(event.isAccepted()) + + def _drag_enter_event(self, url): + # make sure data does not get garbage collected before it used + self.event_data = data = QMimeData() + data.setUrls([QUrl(url)]) + return QDragEnterEvent( + QPoint(0, 0), Qt.MoveAction, data, + Qt.NoButton, Qt.NoModifier) + + def test_dropEvent_replaces_file(self): + with named_file("test", suffix=".42") as fn: + previous = self.widget.text.toPlainText() + event = self._drop_event(QUrl.fromLocalFile(fn)) + self.widget.dropEvent(event) + self.assertEqual("test", self.widget.text.toPlainText()) + self.widget.text.undo() + self.assertEqual(previous, self.widget.text.toPlainText()) + + def _drop_event(self, url): + # make sure data does not get garbage collected before it used + self.event_data = data = QMimeData() + data.setUrls([QUrl(url)]) + + return QDropEvent( + QPoint(0, 0), Qt.MoveAction, data, + Qt.NoButton, Qt.NoModifier, QDropEvent.Drop)