Skip to content

Commit 6ef5755

Browse files
authored
Merge pull request #3032 from ales-erjavec/select-columns-drag-drop
[FIX] Select Columns: Drag/drop
2 parents a7b5c63 + 29b0c58 commit 6ef5755

File tree

4 files changed

+117
-44
lines changed

4 files changed

+117
-44
lines changed

Orange/widgets/data/owselectcolumns.py

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
from AnyQt.QtWidgets import QWidget, QGridLayout
66
from AnyQt.QtWidgets import QListView # pylint: disable=unused-import
77
from AnyQt.QtCore import (
8-
Qt, QTimer, QSortFilterProxyModel, QItemSelection, QItemSelectionModel
8+
Qt, QTimer, QSortFilterProxyModel, QItemSelection, QItemSelectionModel,
9+
QMimeData
910
)
1011

11-
from Orange.util import deprecated
1212
from Orange.widgets import gui, widget
1313
from Orange.widgets.data.contexthandlers import \
1414
SelectAttributesDomainContextHandler
@@ -41,43 +41,59 @@ def source_indexes(indexes, view):
4141
return indexes
4242

4343

44-
# owloadcorpus in orange3-text used this
45-
@deprecated('Orange.widgets.utils.itemmodels.VariableListModel')
46-
def VariablesListItemModel(*args, **kwargs):
47-
return VariableListModel(*args, enable_dnd=True, **kwargs)
44+
class VariablesListItemModel(VariableListModel):
45+
"""
46+
An Variable list item model specialized for Drag and Drop.
47+
"""
48+
MIME_TYPE = "application/x-Orange-VariableListModelData"
4849

50+
def flags(self, index):
51+
flags = super().flags(index)
52+
if index.isValid():
53+
flags |= Qt.ItemIsDragEnabled
54+
else:
55+
flags |= Qt.ItemIsDropEnabled
56+
return flags
4957

50-
class ClassVarListItemModel(VariableListModel):
51-
def dropMimeData(self, mime, action, row, column, parent):
52-
""" Ensure only one variable can be dropped onto the view.
53-
"""
54-
vars = mime.property('_items')
55-
if vars is None:
56-
return False
57-
if action == Qt.IgnoreAction:
58-
return True
59-
return VariableListModel.dropMimeData(
60-
self, mime, action, row, column, parent)
58+
def supportedDropActions(self):
59+
return Qt.MoveAction # pragma: no cover
6160

61+
def supportedDragActions(self):
62+
return Qt.MoveAction # pragma: no cover
6263

63-
class ClassVariableItemView(VariablesListItemView):
64-
def __init__(self, parent=None, acceptedType=Orange.data.Variable):
65-
VariablesListItemView.__init__(self, parent, acceptedType)
66-
self.setDropIndicatorShown(False)
64+
def mimeTypes(self):
65+
return [self.MIME_TYPE]
6766

68-
def acceptsDropEvent(self, event):
67+
def mimeData(self, indexlist):
6968
"""
70-
Reimplemented
69+
Reimplemented.
7170
72-
Ensure only one variable is in the model.
71+
For efficiency reasons only the variable instances are set on the
72+
mime data (under `'_items'` property)
7373
"""
74-
accepts = super().acceptsDropEvent(event)
75-
mime = event.mimeData()
76-
vars = mime.property('_items')
77-
if vars is None:
78-
return False
74+
items = [self[index.row()] for index in indexlist]
75+
mime = QMimeData()
76+
# the encoded 'data' is empty, variables are passed by properties
77+
mime.setData(self.MIME_TYPE, b'')
78+
mime.setProperty("_items", items)
79+
return mime
80+
81+
def dropMimeData(self, mime, action, row, column, parent):
82+
"""
83+
Reimplemented.
84+
"""
85+
if action == Qt.IgnoreAction:
86+
return True # pragma: no cover
87+
if not mime.hasFormat(self.MIME_TYPE):
88+
return False # pragma: no cover
89+
variables = mime.property("_items")
90+
if variables is None:
91+
return False # pragma: no cover
92+
if row < 0:
93+
row = self.rowCount()
7994

80-
return accepts
95+
self[row:row] = variables
96+
return True
8197

8298

8399
class OWSelectAttributes(widget.OWWidget):
@@ -126,7 +142,7 @@ def update_on_change(view):
126142
box = gui.vBox(self.controlArea, "Available Variables",
127143
addToLayout=False)
128144

129-
self.available_attrs = VariableListModel(enable_dnd=True)
145+
self.available_attrs = VariablesListItemModel()
130146
filter_edit, self.available_attrs_view = variables_filter(
131147
parent=self, model=self.available_attrs)
132148
box.layout().addWidget(filter_edit)
@@ -143,7 +159,7 @@ def dropcompleted(action):
143159
layout.addWidget(box, 0, 0, 3, 1)
144160

145161
box = gui.vBox(self.controlArea, "Features", addToLayout=False)
146-
self.used_attrs = VariableListModel(enable_dnd=True)
162+
self.used_attrs = VariablesListItemModel()
147163
self.used_attrs_view = VariablesListItemView(
148164
acceptedType=(Orange.data.DiscreteVariable,
149165
Orange.data.ContinuousVariable))
@@ -156,8 +172,8 @@ def dropcompleted(action):
156172
layout.addWidget(box, 0, 2, 1, 1)
157173

158174
box = gui.vBox(self.controlArea, "Target Variable", addToLayout=False)
159-
self.class_attrs = ClassVarListItemModel(enable_dnd=True)
160-
self.class_attrs_view = ClassVariableItemView(
175+
self.class_attrs = VariablesListItemModel()
176+
self.class_attrs_view = VariablesListItemView(
161177
acceptedType=(Orange.data.DiscreteVariable,
162178
Orange.data.ContinuousVariable))
163179
self.class_attrs_view.setModel(self.class_attrs)
@@ -169,7 +185,7 @@ def dropcompleted(action):
169185
layout.addWidget(box, 1, 2, 1, 1)
170186

171187
box = gui.vBox(self.controlArea, "Meta Attributes", addToLayout=False)
172-
self.meta_attrs = VariableListModel(enable_dnd=True)
188+
self.meta_attrs = VariablesListItemModel()
173189
self.meta_attrs_view = VariablesListItemView(
174190
acceptedType=Orange.data.Variable)
175191
self.meta_attrs_view.setModel(self.meta_attrs)

Orange/widgets/data/tests/test_owselectcolumns.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
from unittest import TestCase
22
from unittest.mock import Mock
3+
4+
from AnyQt.QtCore import Qt
5+
36
from Orange.data import Table, ContinuousVariable, DiscreteVariable, Domain
47
from Orange.widgets.data.contexthandlers import \
58
SelectAttributesDomainContextHandler
69
from Orange.widgets.settings import ContextSetting
710
from Orange.widgets.utils import vartype
811
from Orange.widgets.tests.base import WidgetTest
912
from Orange.widgets.data.owselectcolumns \
10-
import OWSelectAttributes
13+
import OWSelectAttributes, VariablesListItemModel
1114

1215
Continuous = vartype(ContinuousVariable())
1316
Discrete = vartype(DiscreteVariable())
@@ -98,6 +101,28 @@ def test_open_context_with_no_match(self):
98101
self.assertEqual(widget.domain_role_hints, {})
99102

100103

104+
class TestModel(TestCase):
105+
def test_drop_mime(self):
106+
iris = Table("iris")
107+
m = VariablesListItemModel(iris.domain.variables)
108+
mime = m.mimeData([m.index(1, 0)])
109+
self.assertTrue(mime.hasFormat(VariablesListItemModel.MIME_TYPE))
110+
assert m.dropMimeData(mime, Qt.MoveAction, 5, 0, m.index(-1, -1))
111+
self.assertIs(m[5], m[1])
112+
assert m.dropMimeData(mime, Qt.MoveAction, -1, -1, m.index(-1, -1))
113+
self.assertIs(m[6], m[1])
114+
115+
def test_flags(self):
116+
m = VariablesListItemModel([ContinuousVariable("X")])
117+
index = m.index(0)
118+
flags = m.flags(m.index(0))
119+
self.assertTrue(flags & Qt.ItemIsDragEnabled)
120+
self.assertFalse(flags & Qt.ItemIsDropEnabled)
121+
# 'invalid' index is drop enabled -> indicates insertion capability
122+
flags = m.flags(m.index(-1, -1))
123+
self.assertTrue(flags & Qt.ItemIsDropEnabled)
124+
125+
101126
class SimpleWidget:
102127
domain_role_hints = ContextSetting({})
103128
required = ContextSetting("", required=ContextSetting.REQUIRED)
@@ -108,6 +133,7 @@ def retrieveSpecificSettings(self):
108133
def storeSpecificSettings(self):
109134
pass
110135

136+
111137
class TestOWSelectAttributes(WidgetTest):
112138
def setUp(self):
113139
self.widget = self.create_widget(OWSelectAttributes)

Orange/widgets/utils/itemmodels.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from AnyQt.QtCore import (
1313
Qt, QObject, QAbstractListModel, QAbstractTableModel, QModelIndex,
14-
QItemSelectionModel, QT_VERSION
14+
QItemSelectionModel, QMimeData, QT_VERSION
1515
)
1616
from AnyQt.QtCore import pyqtSignal as Signal
1717
from AnyQt.QtGui import QColor
@@ -582,6 +582,9 @@ def setData(self, index, value, role=Qt.EditRole):
582582

583583
def setItemData(self, index, data):
584584
data = dict(data)
585+
if not data:
586+
return True # pragma: no cover
587+
585588
with signal_blocking(self):
586589
for role, value in data.items():
587590
if role == Qt.EditRole and \
@@ -752,35 +755,43 @@ def supportedDropActions(self):
752755
return self._supportedDropActions
753756

754757
def mimeTypes(self):
755-
return [self.MIME_TYPE] + list(QAbstractListModel.mimeTypes(self))
758+
return [self.MIME_TYPE]
756759

757760
def mimeData(self, indexlist):
758761
if len(indexlist) <= 0:
759762
return None
760763

764+
def itemData(row):
765+
# type: (int) -> Dict[int, Any]
766+
if row < len(self._other_data):
767+
return {key: val for key, val in self._other_data[row].items()
768+
if isinstance(key, int)}
769+
else:
770+
return {} # pragma: no cover
771+
761772
items = [self[i.row()] for i in indexlist]
762-
itemdata = [self.itemData(i) for i in indexlist]
763-
mime = QAbstractListModel.mimeData(self, indexlist)
773+
itemdata = [itemData(i.row()) for i in indexlist]
774+
mime = QMimeData()
764775
mime.setData(self.MIME_TYPE, b'see properties: _items, _itemdata')
765776
mime.setProperty('_items', items)
766777
mime.setProperty('_itemdata', itemdata)
767778
return mime
768779

769780
def dropMimeData(self, mime, action, row, column, parent):
770781
if action == Qt.IgnoreAction:
771-
return True
782+
return True # pragma: no cover
772783

773784
if not mime.hasFormat(self.MIME_TYPE):
774-
return False
785+
return False # pragma: no cover
775786

776787
items = mime.property('_items')
777788
itemdata = mime.property('_itemdata')
778789

779790
if not items:
780-
return False
791+
return False # pragma: no cover
781792

782793
if row == -1:
783-
row = len(self)
794+
row = len(self) # pragma: no cover
784795

785796
self[row:row] = items
786797
for i, data in enumerate(itemdata):

Orange/widgets/utils/tests/test_itemmodels.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,25 @@ def test_itemData(self):
234234

235235
self.assertEqual(model.itemData(model.index(5)), {})
236236

237+
def test_mimeData(self):
238+
model = PyListModel([1, 2])
239+
model._other_data[:] = [{Qt.UserRole: "a"}, {}]
240+
mime = model.mimeData([model.index(0), model.index(1)])
241+
self.assertTrue(mime.hasFormat(PyListModel.MIME_TYPE))
242+
243+
def test_dropMimeData(self):
244+
model = PyListModel([1, 2])
245+
model.setData(model.index(0), "a", Qt.UserRole)
246+
mime = model.mimeData([model.index(0)])
247+
self.assertTrue(
248+
model.dropMimeData(mime, Qt.CopyAction, 2, -1, model.index(-1, -1))
249+
)
250+
self.assertEqual(len(model), 3)
251+
self.assertEqual(
252+
model.itemData(model.index(2)),
253+
{Qt.DisplayRole: 1, Qt.EditRole: 1, Qt.UserRole: "a"}
254+
)
255+
237256
def test_parent(self):
238257
self.assertFalse(self.model.parent(self.model.index(2)).isValid())
239258

@@ -673,5 +692,6 @@ def test_read_only(self):
673692
self.assertRaises(TypeError, model.insertRows, 0, 0)
674693
self.assertRaises(TypeError, model.removeRows, 0, 0)
675694

695+
676696
if __name__ == "__main__":
677697
unittest.main()

0 commit comments

Comments
 (0)