11from functools import partial
22from typing import Optional , Dict , Tuple
33
4- from AnyQt .QtWidgets import QWidget , QGridLayout
5- from AnyQt .QtWidgets import QListView
64from 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
1111from Orange .data import Domain , Variable
1212from 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