1313from functools import singledispatch , partial
1414from typing import (
1515 Tuple , List , Any , Optional , Union , Dict , Sequence , Iterable , NamedTuple ,
16- FrozenSet , Type , Callable , TypeVar , Mapping , Hashable
16+ FrozenSet , Type , Callable , TypeVar , Mapping , Hashable , cast
1717)
1818
1919import numpy as np
2323 QLineEdit , QAction , QActionGroup , QGroupBox ,
2424 QStyledItemDelegate , QStyleOptionViewItem , QStyle , QSizePolicy ,
2525 QDialogButtonBox , QPushButton , QCheckBox , QComboBox , QStackedLayout ,
26- QDialog , QRadioButton , QGridLayout , QLabel , QSpinBox , QDoubleSpinBox )
26+ QDialog , QRadioButton , QGridLayout , QLabel , QSpinBox , QDoubleSpinBox ,
27+ QAbstractItemView , QMenu
28+ )
2729from AnyQt .QtGui import QStandardItemModel , QStandardItem , QKeySequence , QIcon
2830from AnyQt .QtCore import (
29- Qt , QSize , QModelIndex , QAbstractItemModel , QPersistentModelIndex
31+ Qt , QSize , QModelIndex , QAbstractItemModel , QPersistentModelIndex , QRect ,
32+ QPoint ,
3033)
3134from AnyQt .QtCore import pyqtSignal as Signal , pyqtSlot as Slot
3235
3639from Orange .widgets import widget , gui , settings
3740from Orange .widgets .utils import itemmodels
3841from Orange .widgets .utils .buttons import FixedSizeButton
42+ from Orange .widgets .utils .itemmodels import signal_blocking
3943from Orange .widgets .utils .widgetpreview import WidgetPreview
4044from Orange .widgets .utils .state_summary import format_summary_details
4145from Orange .widgets .widget import Input , Output
@@ -466,8 +470,6 @@ def get_dict(self):
466470 return rval
467471
468472
469-
470-
471473class VariableEditor (QWidget ):
472474 """
473475 An editor widget for a variable.
@@ -997,6 +999,14 @@ def keyRoles(self): # type: () -> FrozenSet[int]
997999 return frozenset ({Qt .EditRole , EditStateRole })
9981000
9991001
1002+ def mapRectTo (widget : QWidget , parent : QWidget , rect : QRect ) -> QRect : # pylint: disable=redefined-outer-name
1003+ return QRect (widget .mapTo (parent , rect .topLeft ()), rect .size ())
1004+
1005+
1006+ def mapRectToGlobal (widget : QWidget , rect : QRect ) -> QRect : # pylint: disable=redefined-outer-name
1007+ return QRect (widget .mapToGlobal (rect .topLeft ()), rect .size ())
1008+
1009+
10001010class CategoriesEditDelegate (QStyledItemDelegate ):
10011011 """
10021012 Display delegate for editing categories.
@@ -1028,6 +1038,64 @@ def initStyleOption(self, option, index):
10281038 text = text + " " + suffix
10291039 option .text = text
10301040
1041+ class CatEditComboBox (QComboBox ):
1042+ prows : List [QPersistentModelIndex ]
1043+
1044+ def createEditor (
1045+ self , parent : QWidget , option : 'QStyleOptionViewItem' ,
1046+ index : QModelIndex
1047+ ) -> QWidget :
1048+ view = option .widget
1049+ assert isinstance (view , QAbstractItemView )
1050+ selmodel = view .selectionModel ()
1051+ rows = selmodel .selectedRows (0 )
1052+ if len (rows ) < 2 :
1053+ return super ().createEditor (parent , option , index )
1054+ # edit multiple selection
1055+ cb = CategoriesEditDelegate .CatEditComboBox (
1056+ editable = True , insertPolicy = QComboBox .InsertAtBottom )
1057+ cb .setParent (view , Qt .Popup )
1058+ cb .addItems (
1059+ list (unique (str (row .data (Qt .EditRole )) for row in rows )))
1060+ prows = [QPersistentModelIndex (row ) for row in rows ]
1061+ cb .prows = prows
1062+ return cb
1063+
1064+ def updateEditorGeometry (
1065+ self , editor : QWidget , option : 'QStyleOptionViewItem' ,
1066+ index : QModelIndex
1067+ ) -> None :
1068+ if isinstance (editor , CategoriesEditDelegate .CatEditComboBox ):
1069+ view = cast (QAbstractItemView , option .widget )
1070+ view .scrollTo (index )
1071+ vport = view .viewport ()
1072+ vrect = view .visualRect (index )
1073+ vrect = mapRectTo (vport , view , vrect )
1074+ vrect = vrect .intersected (vport .geometry ())
1075+ vrect = mapRectToGlobal (vport , vrect )
1076+ size = editor .sizeHint ().expandedTo (vrect .size ())
1077+ editor .resize (size )
1078+ editor .move (vrect .topLeft ())
1079+ else :
1080+ super ().updateEditorGeometry (editor , option , index )
1081+
1082+ def setModelData (
1083+ self , editor : QWidget , model : QAbstractItemModel , index : QModelIndex
1084+ ) -> None :
1085+ if isinstance (editor , CategoriesEditDelegate .CatEditComboBox ):
1086+ text = editor .currentText ()
1087+ with signal_blocking (model ):
1088+ for prow in editor .prows :
1089+ if prow .isValid ():
1090+ model .setData (QModelIndex (prow ), text , Qt .EditRole )
1091+ # this could be better
1092+ model .dataChanged .emit (
1093+ model .index (0 , 0 ), model .index (model .rowCount () - 1 , 0 ),
1094+ (Qt .EditRole ,)
1095+ )
1096+ else :
1097+ super ().setModelData (editor , model , index )
1098+
10311099
10321100class DiscreteVariableEditor (VariableEditor ):
10331101 """An editor widget for editing a discrete variable.
@@ -1069,7 +1137,8 @@ def __init__(self, *args, **kwargs):
10691137 self , objectName = "action-group-categories" , enabled = False
10701138 )
10711139 self .move_value_up = QAction (
1072- "\N{UPWARDS ARROW} " , group ,
1140+ "Move up" , group ,
1141+ iconText = "\N{UPWARDS ARROW} " ,
10731142 toolTip = "Move the selected item up." ,
10741143 shortcut = QKeySequence (Qt .ControlModifier | Qt .AltModifier |
10751144 Qt .Key_BracketLeft ),
@@ -1078,7 +1147,8 @@ def __init__(self, *args, **kwargs):
10781147 self .move_value_up .triggered .connect (self .move_up )
10791148
10801149 self .move_value_down = QAction (
1081- "\N{DOWNWARDS ARROW} " , group ,
1150+ "Move down" , group ,
1151+ iconText = "\N{DOWNWARDS ARROW} " ,
10821152 toolTip = "Move the selected item down." ,
10831153 shortcut = QKeySequence (Qt .ControlModifier | Qt .AltModifier |
10841154 Qt .Key_BracketRight ),
@@ -1087,29 +1157,41 @@ def __init__(self, *args, **kwargs):
10871157 self .move_value_down .triggered .connect (self .move_down )
10881158
10891159 self .add_new_item = QAction (
1090- "+" , group ,
1160+ "Add" , group ,
1161+ iconText = "+" ,
10911162 objectName = "action-add-item" ,
10921163 toolTip = "Append a new item." ,
10931164 shortcut = QKeySequence (QKeySequence .New ),
10941165 shortcutContext = Qt .WidgetShortcut ,
10951166 )
10961167 self .remove_item = QAction (
1097- "\N{MINUS SIGN} " , group ,
1168+ "Remove item" , group ,
1169+ iconText = "\N{MINUS SIGN} " ,
10981170 objectName = "action-remove-item" ,
10991171 toolTip = "Delete the selected item." ,
11001172 shortcut = QKeySequence (QKeySequence .Delete ),
11011173 shortcutContext = Qt .WidgetShortcut ,
11021174 )
1103- self .merge_items = QAction (
1104- "M" , group ,
1105- objectName = "action-merge-item" ,
1106- toolTip = "Merge selected items." ,
1175+ self .rename_selected_items = QAction (
1176+ "Rename selected items" , group ,
1177+ iconText = "=" ,
1178+ objectName = "action-rename-selected-items" ,
1179+ toolTip = "Rename selected items." ,
11071180 shortcut = QKeySequence (Qt .ControlModifier | Qt .Key_Equal ),
1181+ shortcutContext = Qt .WidgetShortcut ,
1182+ )
1183+ self .merge_items = QAction (
1184+ "Merge" , group ,
1185+ iconText = "M" ,
1186+ objectName = "action-activate-merge-dialog" ,
1187+ toolTip = "Merge infrequent items." ,
1188+ shortcut = QKeySequence (Qt .ControlModifier | Qt .MetaModifier | Qt .Key_Equal ),
11081189 shortcutContext = Qt .WidgetShortcut
11091190 )
11101191
11111192 self .add_new_item .triggered .connect (self ._add_category )
11121193 self .remove_item .triggered .connect (self ._remove_category )
1194+ self .rename_selected_items .triggered .connect (self ._rename_selected_categories )
11131195 self .merge_items .triggered .connect (self ._merge_categories )
11141196
11151197 button1 = FixedSizeButton (
@@ -1129,18 +1211,37 @@ def __init__(self, *args, **kwargs):
11291211 accessibleName = "Remove"
11301212 )
11311213 button5 = FixedSizeButton (
1214+ self , defaultAction = self .rename_selected_items ,
1215+ accessibleName = "Merge selected items"
1216+ )
1217+ button6 = FixedSizeButton (
11321218 self , defaultAction = self .merge_items ,
1133- accessibleName = "Merge" ,
1219+ accessibleName = "Merge infrequent " ,
11341220 )
1135- self .values_edit .addActions ([self .move_value_up , self .move_value_down ,
1136- self .add_new_item , self .remove_item ])
1221+
1222+ self .values_edit .addActions ([
1223+ self .move_value_up , self .move_value_down ,
1224+ self .add_new_item , self .remove_item , self .rename_selected_items
1225+ ])
1226+ self .values_edit .setContextMenuPolicy (Qt .CustomContextMenu )
1227+
1228+ def context_menu (pos : QPoint ):
1229+ viewport = self .values_edit .viewport ()
1230+ menu = QMenu (self .values_edit )
1231+ menu .setAttribute (Qt .WA_DeleteOnClose )
1232+ menu .addActions ([self .rename_selected_items , self .remove_item ])
1233+ menu .popup (viewport .mapToGlobal (pos ))
1234+ self .values_edit .customContextMenuRequested .connect (context_menu )
1235+
11371236 hlayout .addWidget (button1 )
11381237 hlayout .addWidget (button2 )
11391238 hlayout .addSpacing (3 )
11401239 hlayout .addWidget (button3 )
11411240 hlayout .addWidget (button4 )
11421241 hlayout .addSpacing (3 )
11431242 hlayout .addWidget (button5 )
1243+ hlayout .addWidget (button6 )
1244+
11441245 hlayout .addStretch (10 )
11451246 vlayout .addLayout (hlayout )
11461247
@@ -1151,6 +1252,8 @@ def __init__(self, *args, **kwargs):
11511252 QWidget .setTabOrder (button1 , button2 )
11521253 QWidget .setTabOrder (button2 , button3 )
11531254 QWidget .setTabOrder (button3 , button4 )
1255+ QWidget .setTabOrder (button4 , button5 )
1256+ QWidget .setTabOrder (button5 , button6 )
11541257
11551258 def set_data (self , var , transform = ()):
11561259 raise NotImplementedError
@@ -1293,13 +1396,6 @@ def on_values_changed(self):
12931396 @Slot ()
12941397 def on_value_selection_changed (self ):
12951398 rows = self .values_edit .selectionModel ().selectedRows ()
1296- # enable merge if at least 2 selected items and the selection must not
1297- # contain any added/dropped items.
1298- enable_merge = \
1299- len (rows ) > 1 and \
1300- not any (index .data (EditStateRole ) != ItemEditState .NoState
1301- for index in rows )
1302-
13031399 if len (rows ) == 1 :
13041400 i = rows [0 ].row ()
13051401 self .move_value_up .setEnabled (i != 0 )
@@ -1422,6 +1518,22 @@ def complete_merge(text, merge_attributes):
14221518 dlg .get_merged_value_name (), dlg .get_merge_attributes ()
14231519 )
14241520
1521+ def _rename_selected_categories (self ):
1522+ """
1523+ Rename selected categories and merging them.
1524+
1525+ Popup an editable combo box for selection/edit of a new value.
1526+ """
1527+ view = self .values_edit
1528+ selmodel = view .selectionModel ()
1529+ index = view .currentIndex ()
1530+ if not selmodel .isSelected (index ):
1531+ indices = selmodel .selectedRows (0 )
1532+ if indices :
1533+ index = indices [0 ]
1534+ # delegate to the CategoriesEditDelegate
1535+ view .edit (index )
1536+
14251537
14261538class ContinuousVariableEditor (VariableEditor ):
14271539 # TODO: enable editing of display format...
0 commit comments