diff --git a/Orange/widgets/data/owselectcolumns.py b/Orange/widgets/data/owselectcolumns.py index 4f4dbce0e0e..6926396bfad 100644 --- a/Orange/widgets/data/owselectcolumns.py +++ b/Orange/widgets/data/owselectcolumns.py @@ -5,10 +5,10 @@ from AnyQt.QtWidgets import QWidget, QGridLayout from AnyQt.QtWidgets import QListView # pylint: disable=unused-import from AnyQt.QtCore import ( - Qt, QTimer, QSortFilterProxyModel, QItemSelection, QItemSelectionModel + Qt, QTimer, QSortFilterProxyModel, QItemSelection, QItemSelectionModel, + QMimeData ) -from Orange.util import deprecated from Orange.widgets import gui, widget from Orange.widgets.data.contexthandlers import \ SelectAttributesDomainContextHandler @@ -41,43 +41,59 @@ def source_indexes(indexes, view): return indexes -# owloadcorpus in orange3-text used this -@deprecated('Orange.widgets.utils.itemmodels.VariableListModel') -def VariablesListItemModel(*args, **kwargs): - return VariableListModel(*args, enable_dnd=True, **kwargs) +class VariablesListItemModel(VariableListModel): + """ + An Variable list item model specialized for Drag and Drop. + """ + MIME_TYPE = "application/x-Orange-VariableListModelData" + def flags(self, index): + flags = super().flags(index) + if index.isValid(): + flags |= Qt.ItemIsDragEnabled + else: + flags |= Qt.ItemIsDropEnabled + return flags -class ClassVarListItemModel(VariableListModel): - def dropMimeData(self, mime, action, row, column, parent): - """ Ensure only one variable can be dropped onto the view. - """ - vars = mime.property('_items') - if vars is None: - return False - if action == Qt.IgnoreAction: - return True - return VariableListModel.dropMimeData( - self, mime, action, row, column, parent) + def supportedDropActions(self): + return Qt.MoveAction # pragma: no cover + def supportedDragActions(self): + return Qt.MoveAction # pragma: no cover -class ClassVariableItemView(VariablesListItemView): - def __init__(self, parent=None, acceptedType=Orange.data.Variable): - VariablesListItemView.__init__(self, parent, acceptedType) - self.setDropIndicatorShown(False) + def mimeTypes(self): + return [self.MIME_TYPE] - def acceptsDropEvent(self, event): + def mimeData(self, indexlist): """ - Reimplemented + Reimplemented. - Ensure only one variable is in the model. + For efficiency reasons only the variable instances are set on the + mime data (under `'_items'` property) """ - accepts = super().acceptsDropEvent(event) - mime = event.mimeData() - vars = mime.property('_items') - if vars is None: - return False + items = [self[index.row()] for index in indexlist] + mime = QMimeData() + # the encoded 'data' is empty, variables are passed by properties + mime.setData(self.MIME_TYPE, b'') + mime.setProperty("_items", items) + return mime + + def dropMimeData(self, mime, action, row, column, parent): + """ + Reimplemented. + """ + if action == Qt.IgnoreAction: + return True # pragma: no cover + if not mime.hasFormat(self.MIME_TYPE): + return False # pragma: no cover + variables = mime.property("_items") + if variables is None: + return False # pragma: no cover + if row < 0: + row = self.rowCount() - return accepts + self[row:row] = variables + return True class OWSelectAttributes(widget.OWWidget): @@ -126,7 +142,7 @@ def update_on_change(view): box = gui.vBox(self.controlArea, "Available Variables", addToLayout=False) - self.available_attrs = VariableListModel(enable_dnd=True) + self.available_attrs = VariablesListItemModel() filter_edit, self.available_attrs_view = variables_filter( parent=self, model=self.available_attrs) box.layout().addWidget(filter_edit) @@ -143,7 +159,7 @@ def dropcompleted(action): layout.addWidget(box, 0, 0, 3, 1) box = gui.vBox(self.controlArea, "Features", addToLayout=False) - self.used_attrs = VariableListModel(enable_dnd=True) + self.used_attrs = VariablesListItemModel() self.used_attrs_view = VariablesListItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) @@ -156,8 +172,8 @@ def dropcompleted(action): layout.addWidget(box, 0, 2, 1, 1) box = gui.vBox(self.controlArea, "Target Variable", addToLayout=False) - self.class_attrs = ClassVarListItemModel(enable_dnd=True) - self.class_attrs_view = ClassVariableItemView( + self.class_attrs = VariablesListItemModel() + self.class_attrs_view = VariablesListItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.class_attrs_view.setModel(self.class_attrs) @@ -169,7 +185,7 @@ def dropcompleted(action): layout.addWidget(box, 1, 2, 1, 1) box = gui.vBox(self.controlArea, "Meta Attributes", addToLayout=False) - self.meta_attrs = VariableListModel(enable_dnd=True) + self.meta_attrs = VariablesListItemModel() self.meta_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.meta_attrs_view.setModel(self.meta_attrs) diff --git a/Orange/widgets/data/tests/test_owselectcolumns.py b/Orange/widgets/data/tests/test_owselectcolumns.py index 77072b1b8d5..ab4daeafc69 100644 --- a/Orange/widgets/data/tests/test_owselectcolumns.py +++ b/Orange/widgets/data/tests/test_owselectcolumns.py @@ -1,5 +1,8 @@ from unittest import TestCase from unittest.mock import Mock + +from AnyQt.QtCore import Qt + from Orange.data import Table, ContinuousVariable, DiscreteVariable, Domain from Orange.widgets.data.contexthandlers import \ SelectAttributesDomainContextHandler @@ -7,7 +10,7 @@ from Orange.widgets.utils import vartype from Orange.widgets.tests.base import WidgetTest from Orange.widgets.data.owselectcolumns \ - import OWSelectAttributes + import OWSelectAttributes, VariablesListItemModel Continuous = vartype(ContinuousVariable()) Discrete = vartype(DiscreteVariable()) @@ -98,6 +101,28 @@ def test_open_context_with_no_match(self): self.assertEqual(widget.domain_role_hints, {}) +class TestModel(TestCase): + def test_drop_mime(self): + iris = Table("iris") + m = VariablesListItemModel(iris.domain.variables) + mime = m.mimeData([m.index(1, 0)]) + self.assertTrue(mime.hasFormat(VariablesListItemModel.MIME_TYPE)) + assert m.dropMimeData(mime, Qt.MoveAction, 5, 0, m.index(-1, -1)) + self.assertIs(m[5], m[1]) + assert m.dropMimeData(mime, Qt.MoveAction, -1, -1, m.index(-1, -1)) + self.assertIs(m[6], m[1]) + + def test_flags(self): + m = VariablesListItemModel([ContinuousVariable("X")]) + index = m.index(0) + flags = m.flags(m.index(0)) + self.assertTrue(flags & Qt.ItemIsDragEnabled) + self.assertFalse(flags & Qt.ItemIsDropEnabled) + # 'invalid' index is drop enabled -> indicates insertion capability + flags = m.flags(m.index(-1, -1)) + self.assertTrue(flags & Qt.ItemIsDropEnabled) + + class SimpleWidget: domain_role_hints = ContextSetting({}) required = ContextSetting("", required=ContextSetting.REQUIRED) @@ -108,6 +133,7 @@ def retrieveSpecificSettings(self): def storeSpecificSettings(self): pass + class TestOWSelectAttributes(WidgetTest): def setUp(self): self.widget = self.create_widget(OWSelectAttributes) diff --git a/Orange/widgets/utils/itemmodels.py b/Orange/widgets/utils/itemmodels.py index 0404b7ddf8d..2f6fa188ba1 100644 --- a/Orange/widgets/utils/itemmodels.py +++ b/Orange/widgets/utils/itemmodels.py @@ -11,7 +11,7 @@ from AnyQt.QtCore import ( Qt, QObject, QAbstractListModel, QAbstractTableModel, QModelIndex, - QItemSelectionModel, QT_VERSION + QItemSelectionModel, QMimeData, QT_VERSION ) from AnyQt.QtCore import pyqtSignal as Signal from AnyQt.QtGui import QColor @@ -582,6 +582,9 @@ def setData(self, index, value, role=Qt.EditRole): def setItemData(self, index, data): data = dict(data) + if not data: + return True # pragma: no cover + with signal_blocking(self): for role, value in data.items(): if role == Qt.EditRole and \ @@ -752,15 +755,23 @@ def supportedDropActions(self): return self._supportedDropActions def mimeTypes(self): - return [self.MIME_TYPE] + list(QAbstractListModel.mimeTypes(self)) + return [self.MIME_TYPE] def mimeData(self, indexlist): if len(indexlist) <= 0: return None + def itemData(row): + # type: (int) -> Dict[int, Any] + if row < len(self._other_data): + return {key: val for key, val in self._other_data[row].items() + if isinstance(key, int)} + else: + return {} # pragma: no cover + items = [self[i.row()] for i in indexlist] - itemdata = [self.itemData(i) for i in indexlist] - mime = QAbstractListModel.mimeData(self, indexlist) + itemdata = [itemData(i.row()) for i in indexlist] + mime = QMimeData() mime.setData(self.MIME_TYPE, b'see properties: _items, _itemdata') mime.setProperty('_items', items) mime.setProperty('_itemdata', itemdata) @@ -768,19 +779,19 @@ def mimeData(self, indexlist): def dropMimeData(self, mime, action, row, column, parent): if action == Qt.IgnoreAction: - return True + return True # pragma: no cover if not mime.hasFormat(self.MIME_TYPE): - return False + return False # pragma: no cover items = mime.property('_items') itemdata = mime.property('_itemdata') if not items: - return False + return False # pragma: no cover if row == -1: - row = len(self) + row = len(self) # pragma: no cover self[row:row] = items for i, data in enumerate(itemdata): diff --git a/Orange/widgets/utils/tests/test_itemmodels.py b/Orange/widgets/utils/tests/test_itemmodels.py index f644cbc9dd8..f4119c752e5 100644 --- a/Orange/widgets/utils/tests/test_itemmodels.py +++ b/Orange/widgets/utils/tests/test_itemmodels.py @@ -234,6 +234,25 @@ def test_itemData(self): self.assertEqual(model.itemData(model.index(5)), {}) + def test_mimeData(self): + model = PyListModel([1, 2]) + model._other_data[:] = [{Qt.UserRole: "a"}, {}] + mime = model.mimeData([model.index(0), model.index(1)]) + self.assertTrue(mime.hasFormat(PyListModel.MIME_TYPE)) + + def test_dropMimeData(self): + model = PyListModel([1, 2]) + model.setData(model.index(0), "a", Qt.UserRole) + mime = model.mimeData([model.index(0)]) + self.assertTrue( + model.dropMimeData(mime, Qt.CopyAction, 2, -1, model.index(-1, -1)) + ) + self.assertEqual(len(model), 3) + self.assertEqual( + model.itemData(model.index(2)), + {Qt.DisplayRole: 1, Qt.EditRole: 1, Qt.UserRole: "a"} + ) + def test_parent(self): self.assertFalse(self.model.parent(self.model.index(2)).isValid()) @@ -673,5 +692,6 @@ def test_read_only(self): self.assertRaises(TypeError, model.insertRows, 0, 0) self.assertRaises(TypeError, model.removeRows, 0, 0) + if __name__ == "__main__": unittest.main()