Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] Select Columns: Drag/drop #3032

Merged
merged 5 commits into from
May 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 51 additions & 35 deletions Orange/widgets/data/owselectcolumns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand All @@ -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)
Expand All @@ -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)
Expand Down
28 changes: 27 additions & 1 deletion Orange/widgets/data/tests/test_owselectcolumns.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
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
from Orange.widgets.settings import ContextSetting
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())
Expand Down Expand Up @@ -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)
Expand All @@ -108,6 +133,7 @@ def retrieveSpecificSettings(self):
def storeSpecificSettings(self):
pass


class TestOWSelectAttributes(WidgetTest):
def setUp(self):
self.widget = self.create_widget(OWSelectAttributes)
Expand Down
27 changes: 19 additions & 8 deletions Orange/widgets/utils/itemmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -752,35 +755,43 @@ 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)
return mime

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):
Expand Down
20 changes: 20 additions & 0 deletions Orange/widgets/utils/tests/test_itemmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -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()