Skip to content

Commit b018143

Browse files
authored
Merge pull request #3611 from markotoplak/script-drop
[ENH] OWPythonScript: dropping and pasting of python scripts
2 parents 26f3f87 + d0b36bc commit b018143

File tree

2 files changed

+125
-1
lines changed

2 files changed

+125
-1
lines changed

Orange/widgets/data/owpythonscript.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818
from AnyQt.QtCore import Qt, QRegExp, QByteArray, QItemSelectionModel
1919

20+
from Orange.canvas.gui.utils import OSX_NSURL_toLocalFile
2021
from Orange.data import Table
2122
from Orange.base import Learner, Model
2223
from Orange.widgets import widget, gui
@@ -35,6 +36,19 @@ def text_format(foreground=Qt.black, weight=QFont.Normal):
3536
return fmt
3637

3738

39+
def to_local_file(url):
40+
return OSX_NSURL_toLocalFile(url) or url.toLocalFile()
41+
42+
43+
def read_file_content(filename, limit=None):
44+
try:
45+
with open(filename, encoding="utf-8", errors='strict') as f:
46+
text = f.read(limit)
47+
return text
48+
except (OSError, UnicodeDecodeError):
49+
return None
50+
51+
3852
class PythonSyntaxHighlighter(QSyntaxHighlighter):
3953
def __init__(self, parent=None):
4054

@@ -138,6 +152,24 @@ def keyPressEvent(self, event):
138152
else:
139153
super().keyPressEvent(event)
140154

155+
def insertFromMimeData(self, source):
156+
"""
157+
Reimplemented from QPlainTextEdit.insertFromMimeData.
158+
"""
159+
urls = source.urls()
160+
if urls:
161+
self.pasteFile(urls[0])
162+
else:
163+
super().insertFromMimeData(source)
164+
165+
def pasteFile(self, url):
166+
new = read_file_content(to_local_file(url))
167+
if new:
168+
# inserting text like this allows undo
169+
cursor = QTextCursor(self.document())
170+
cursor.select(QTextCursor.Document)
171+
cursor.insertText(new)
172+
141173

142174
class PythonConsole(QPlainTextEdit, code.InteractiveConsole):
143175
def __init__(self, locals=None, parent=None):
@@ -524,6 +556,8 @@ def __init__(self):
524556
if self.splitterState is not None:
525557
self.splitCanvas.restoreState(QByteArray(self.splitterState))
526558

559+
self.setAcceptDrops(True)
560+
527561
self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved)
528562
self.controlArea.layout().addStretch(1)
529563
self.resize(800, 600)
@@ -708,6 +742,20 @@ def commit(self):
708742
out_var = None
709743
getattr(self.Outputs, signal).send(out_var)
710744

745+
def dragEnterEvent(self, event):
746+
urls = event.mimeData().urls()
747+
if urls:
748+
# try reading the file as text
749+
c = read_file_content(to_local_file(urls[0]), limit=1000)
750+
if c is not None:
751+
event.acceptProposedAction()
752+
753+
def dropEvent(self, event):
754+
"""Handle file drops"""
755+
urls = event.mimeData().urls()
756+
if urls:
757+
self.text.pasteFile(urls[0])
758+
711759

712760
if __name__ == "__main__": # pragma: no cover
713761
WidgetPreview(OWPythonScript).run()

Orange/widgets/data/tests/test_owpythonscript.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# Test methods with long descriptive names can omit docstrings
22
# pylint: disable=missing-docstring
3+
from AnyQt.QtCore import QMimeData, QUrl, QPoint, Qt
4+
from AnyQt.QtGui import QDragEnterEvent, QDropEvent
5+
36
from Orange.data import Table
47
from Orange.classification import LogisticRegressionLearner
5-
from Orange.widgets.data.owpythonscript import OWPythonScript
8+
from Orange.tests import named_file
9+
from Orange.widgets.data.owpythonscript import OWPythonScript, read_file_content
610
from Orange.widgets.tests.base import WidgetTest
711
from Orange.widgets.widget import OWWidget
812

@@ -134,3 +138,75 @@ def test_store_current_script(self):
134138
self.widget = self.create_widget(OWPythonScript, stored_settings=settings)
135139
script = self.widget.text.toPlainText()
136140
self.assertEqual("42", script)
141+
142+
def test_read_file_content(self):
143+
with named_file("Content", suffix=".42") as fn:
144+
# valid file opens
145+
content = read_file_content(fn)
146+
self.assertEqual("Content", content)
147+
# invalid utf-8 file does not
148+
with open(fn, "wb") as f:
149+
f.write(b"\xc3\x28")
150+
content = read_file_content(fn)
151+
self.assertIsNone(content)
152+
153+
def test_script_insert_mime_text(self):
154+
current = self.widget.text.toPlainText()
155+
insert = "test\n"
156+
cursor = self.widget.text.cursor()
157+
cursor.setPos(0, 0)
158+
mime = QMimeData()
159+
mime.setText(insert)
160+
self.widget.text.insertFromMimeData(mime)
161+
self.assertEqual(insert + current, self.widget.text.toPlainText())
162+
163+
def test_script_insert_mime_file(self):
164+
with named_file("test", suffix=".42") as fn:
165+
previous = self.widget.text.toPlainText()
166+
mime = QMimeData()
167+
url = QUrl.fromLocalFile(fn)
168+
mime.setUrls([url])
169+
self.widget.text.insertFromMimeData(mime)
170+
self.assertEqual("test", self.widget.text.toPlainText())
171+
self.widget.text.undo()
172+
self.assertEqual(previous, self.widget.text.toPlainText())
173+
174+
def test_dragEnterEvent_accepts_text(self):
175+
with named_file("Content", suffix=".42") as fn:
176+
event = self._drag_enter_event(QUrl.fromLocalFile(fn))
177+
self.widget.dragEnterEvent(event)
178+
self.assertTrue(event.isAccepted())
179+
180+
def test_dragEnterEvent_rejects_binary(self):
181+
with named_file("", suffix=".42") as fn:
182+
with open(fn, "wb") as f:
183+
f.write(b"\xc3\x28")
184+
event = self._drag_enter_event(QUrl.fromLocalFile(fn))
185+
self.widget.dragEnterEvent(event)
186+
self.assertFalse(event.isAccepted())
187+
188+
def _drag_enter_event(self, url):
189+
# make sure data does not get garbage collected before it used
190+
self.event_data = data = QMimeData()
191+
data.setUrls([QUrl(url)])
192+
return QDragEnterEvent(
193+
QPoint(0, 0), Qt.MoveAction, data,
194+
Qt.NoButton, Qt.NoModifier)
195+
196+
def test_dropEvent_replaces_file(self):
197+
with named_file("test", suffix=".42") as fn:
198+
previous = self.widget.text.toPlainText()
199+
event = self._drop_event(QUrl.fromLocalFile(fn))
200+
self.widget.dropEvent(event)
201+
self.assertEqual("test", self.widget.text.toPlainText())
202+
self.widget.text.undo()
203+
self.assertEqual(previous, self.widget.text.toPlainText())
204+
205+
def _drop_event(self, url):
206+
# make sure data does not get garbage collected before it used
207+
self.event_data = data = QMimeData()
208+
data.setUrls([QUrl(url)])
209+
210+
return QDropEvent(
211+
QPoint(0, 0), Qt.MoveAction, data,
212+
Qt.NoButton, Qt.NoModifier, QDropEvent.Drop)

0 commit comments

Comments
 (0)