Skip to content

Commit 78da743

Browse files
committed
Select Columns: Accept partially correct drops
1 parent b909f89 commit 78da743

File tree

2 files changed

+342
-26
lines changed

2 files changed

+342
-26
lines changed

Orange/widgets/data/owselectcolumns.py

Lines changed: 105 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from functools import partial
22
from typing import Optional, Dict, Tuple
33

4-
from AnyQt.QtWidgets import QWidget, QGridLayout
5-
from AnyQt.QtWidgets import QListView
64
from AnyQt.QtCore import (
75
Qt, QTimer, QSortFilterProxyModel, QItemSelection, QItemSelectionModel,
86
QMimeData, QAbstractItemModel
97
)
8+
from AnyQt.QtGui import QDrag, QDropEvent
9+
from AnyQt.QtWidgets import QWidget, QGridLayout, QListView
1010

1111
from Orange.data import Domain, Variable
1212
from Orange.widgets import gui, widget
@@ -50,6 +50,10 @@ class VariablesListItemModel(VariableListModel):
5050
"""
5151
MIME_TYPE = "application/x-Orange-VariableListModelData"
5252

53+
def __init__(self, *args, primitive=False, **kwargs):
54+
super().__init__(*args, **kwargs)
55+
self.primitive = primitive
56+
5357
def flags(self, index):
5458
flags = super().flags(index)
5559
if index.isValid():
@@ -88,16 +92,88 @@ def dropMimeData(self, mime, action, row, column, parent):
8892
Reimplemented.
8993
"""
9094
if action == Qt.IgnoreAction:
91-
return True # pragma: no cover
95+
return True
9296
if not mime.hasFormat(self.MIME_TYPE):
93-
return False # pragma: no cover
97+
return False
9498
variables = mime.property("_items")
9599
if variables is None:
96-
return False # pragma: no cover
100+
return False
97101
if row < 0:
98102
row = self.rowCount()
99103

104+
if self.primitive and not all(var.is_primitive() for var in variables):
105+
variables = [var for var in variables if var.is_primitive()]
106+
self[row:row] = variables
107+
mime.setProperty("_moved", variables)
108+
return bool(variables)
109+
100110
self[row:row] = variables
111+
mime.setProperty("_moved", True)
112+
return True
113+
114+
115+
class SelectedVarsView(VariablesListItemView):
116+
"""
117+
VariableListItemView that supports partially accepted drags.
118+
119+
Upon finish, the mime data contains a list of variables accepted by the
120+
destination, and removes only those variables from the model.
121+
"""
122+
def startDrag(self, supported_actions):
123+
indexes = self.selectedIndexes()
124+
if len(indexes) == 0:
125+
return
126+
data = self.model().mimeData(indexes)
127+
if not data:
128+
return
129+
drag = QDrag(self)
130+
drag.setMimeData(data)
131+
res = drag.exec(supported_actions, Qt.DropAction.MoveAction)
132+
133+
moved = data.property("_moved")
134+
if moved is None:
135+
return
136+
137+
if moved is True:
138+
# A quicker path if everything is moved.
139+
# When removing rows, private method QAbstractItemView::clearOrRemove
140+
# iterates over ranges and removes them, apparently assuming their
141+
# reverse order. I haven't found any guarantee for this order in
142+
# documentation (nor, actually, in the code that maintains their
143+
# order, so let's sort them.
144+
to_remove = sorted(
145+
((index.top(), index.bottom() + 1)
146+
for index in self.selectionModel().selection()),
147+
reverse=True)
148+
else:
149+
moved = set(moved)
150+
to_remove = reversed(list(slices(
151+
index.row() for index in self.selectionModel().selectedIndexes()
152+
if index.data(gui.TableVariable) in moved)))
153+
154+
for start, end in to_remove:
155+
self.model().removeRows(start, end - start)
156+
157+
self.dragDropActionDidComplete.emit(res)
158+
159+
160+
class PrimitivesView(SelectedVarsView):
161+
"""
162+
A SelectedVarsView that accepts drops events if it contains *any*
163+
primitive variables. This overrides the inherited behaviour that accepts
164+
the event only if *all* variables are primitive.
165+
"""
166+
def acceptsDropEvent(self, event: QDropEvent) -> bool:
167+
if event.source() is not None and \
168+
event.source().window() is not self.window():
169+
return False # pragma: nocover
170+
171+
mime = event.mimeData()
172+
items = mime.property('_items')
173+
if items is None or not any(var.is_primitive() for var in items):
174+
return False
175+
176+
event.accept()
101177
return True
102178

103179

@@ -217,7 +293,9 @@ def update_on_change(view):
217293

218294
self.available_attrs = VariablesListItemModel()
219295
filter_edit, self.available_attrs_view = variables_filter(
220-
parent=self, model=self.available_attrs)
296+
parent=self, model=self.available_attrs,
297+
view_type=SelectedVarsView
298+
)
221299
box.layout().addWidget(filter_edit)
222300
self.view_boxes.append((name, box, self.available_attrs_view))
223301
filter_edit.textChanged.connect(self.__var_counts_update_timer.start)
@@ -236,11 +314,12 @@ def dropcompleted(action):
236314
# 3rd column
237315
name = "Features"
238316
box = gui.vBox(self.controlArea, name, addToLayout=False)
239-
self.used_attrs = VariablesListItemModel()
317+
self.used_attrs = VariablesListItemModel(primitive=True)
240318
filter_edit, self.used_attrs_view = variables_filter(
241319
parent=self, model=self.used_attrs,
242320
accepted_type=(Orange.data.DiscreteVariable,
243-
Orange.data.ContinuousVariable))
321+
Orange.data.ContinuousVariable),
322+
view_type=PrimitivesView)
244323
self.used_attrs.rowsInserted.connect(self.__used_attrs_changed)
245324
self.used_attrs.rowsRemoved.connect(self.__used_attrs_changed)
246325
self.used_attrs_view.selectionModel().selectionChanged.connect(
@@ -262,10 +341,10 @@ def dropcompleted(action):
262341

263342
name = "Target"
264343
box = gui.vBox(self.controlArea, name, addToLayout=False)
265-
self.class_attrs = VariablesListItemModel()
266-
self.class_attrs_view = VariablesListItemView(
344+
self.class_attrs = VariablesListItemModel(primitive=True)
345+
self.class_attrs_view = PrimitivesView(
267346
acceptedType=(Orange.data.DiscreteVariable,
268-
Orange.data.ContinuousVariable)
347+
Orange.data.ContinuousVariable),
269348
)
270349
self.class_attrs_view.setModel(self.class_attrs)
271350
self.class_attrs_view.selectionModel().selectionChanged.connect(
@@ -279,7 +358,7 @@ def dropcompleted(action):
279358
name = "Metas"
280359
box = gui.vBox(self.controlArea, name, addToLayout=False)
281360
self.meta_attrs = VariablesListItemModel()
282-
self.meta_attrs_view = VariablesListItemView(
361+
self.meta_attrs_view = SelectedVarsView(
283362
acceptedType=Orange.data.Variable)
284363
self.meta_attrs_view.setModel(self.meta_attrs)
285364
self.meta_attrs_view.selectionModel().selectionChanged.connect(
@@ -294,15 +373,15 @@ def dropcompleted(action):
294373
self.move_attr_button = gui.button(
295374
bbox, self, ">",
296375
callback=partial(self.move_selected,
297-
self.used_attrs_view)
376+
self.used_attrs_view, primitive=True)
298377
)
299378
layout.addWidget(bbox, 0, 1, 1, 1)
300379

301380
bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0)
302381
self.move_class_button = gui.button(
303382
bbox, self, ">",
304383
callback=partial(self.move_selected,
305-
self.class_attrs_view)
384+
self.class_attrs_view, primitive=True)
306385
)
307386
layout.addWidget(bbox, 1, 1, 1, 1)
308387

@@ -532,14 +611,19 @@ def move_up(self, view: QListView):
532611
def move_down(self, view: QListView):
533612
self.move_rows(view, 1)
534613

535-
def move_selected(self, view):
614+
def move_selected(self, view, *, primitive=False):
536615
if self.selected_rows(view):
537616
self.move_selected_from_to(view, self.available_attrs_view)
538617
elif self.selected_rows(self.available_attrs_view):
539-
self.move_selected_from_to(self.available_attrs_view, view)
618+
self.move_selected_from_to(self.available_attrs_view, view,
619+
primitive)
540620

541-
def move_selected_from_to(self, src, dst):
542-
self.move_from_to(src, dst, self.selected_rows(src))
621+
def move_selected_from_to(self, src, dst, primitive=False):
622+
rows = self.selected_rows(src)
623+
if primitive:
624+
model = src.model().sourceModel()
625+
rows = [row for row in rows if model[row].is_primitive()]
626+
self.move_from_to(src, dst, rows)
543627

544628
def move_from_to(self, src, dst, rows):
545629
src_model = source_model(src)
@@ -589,18 +673,18 @@ def selected_vars(view):
589673
meta_selected = selected_vars(self.meta_attrs_view)
590674

591675
available_types = set(map(type, available_selected))
592-
all_primitive = all(var.is_primitive()
676+
any_primitive = any(var.is_primitive()
593677
for var in available_types)
594678

595679
move_attr_enabled = \
596-
((available_selected and all_primitive) or attrs_selected) and \
680+
((available_selected and any_primitive) or attrs_selected) and \
597681
self.used_attrs_view.isEnabled()
598682

599683
self.move_attr_button.setEnabled(bool(move_attr_enabled))
600684
if move_attr_enabled:
601685
self.move_attr_button.setText(">" if available_selected else "<")
602686

603-
move_class_enabled = bool(all_primitive and available_selected) or class_selected
687+
move_class_enabled = bool(any_primitive and available_selected) or class_selected
604688

605689
self.move_class_button.setEnabled(bool(move_class_enabled))
606690
if move_class_enabled:

0 commit comments

Comments
 (0)