Skip to content

Commit 9696fe9

Browse files
committed
oweditdomain: Move multi categories edit to the delegate
1 parent 6ab4a42 commit 9696fe9

File tree

2 files changed

+101
-64
lines changed

2 files changed

+101
-64
lines changed

Orange/widgets/data/oweditdomain.py

Lines changed: 76 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from functools import singledispatch, partial
1414
from 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

1919
import numpy as np
@@ -24,7 +24,7 @@
2424
QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy, QToolTip,
2525
QDialogButtonBox, QPushButton, QCheckBox, QComboBox, QStackedLayout,
2626
QDialog, QRadioButton, QGridLayout, QLabel, QSpinBox, QDoubleSpinBox,
27-
QShortcut
27+
QShortcut, QAbstractItemView
2828
)
2929
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QKeySequence, QIcon
3030
from AnyQt.QtCore import (
@@ -38,6 +38,7 @@
3838
from Orange.preprocess.transformation import Transformation, Identity, Lookup
3939
from Orange.widgets import widget, gui, settings
4040
from Orange.widgets.utils import itemmodels
41+
from Orange.widgets.utils.itemmodels import signal_blocking
4142
from Orange.widgets.utils.widgetpreview import WidgetPreview
4243
from Orange.widgets.utils.state_summary import format_summary_details
4344
from Orange.widgets.widget import Input, Output
@@ -469,7 +470,6 @@ def get_dict(self):
469470

470471

471472
class FixedSizeButton(QToolButton):
472-
473473
def __init__(self, *args, defaultAction=None, **kwargs):
474474
super().__init__(*args, **kwargs)
475475
sh = self.sizePolicy()
@@ -1033,6 +1033,22 @@ def keyRoles(self): # type: () -> FrozenSet[int]
10331033
return frozenset({Qt.EditRole, EditStateRole})
10341034

10351035

1036+
def mapRectTo(widget, parent, rect):
1037+
# type: (QWidget, QWidget, QRect) -> QRect
1038+
return QRect(
1039+
widget.mapTo(parent, rect.topLeft()),
1040+
rect.size(),
1041+
)
1042+
1043+
1044+
def mapRectToGlobal(widget, rect):
1045+
# type: (QWidget, QRect) -> QRect
1046+
return QRect(
1047+
widget.mapToGlobal(rect.topLeft()),
1048+
rect.size(),
1049+
)
1050+
1051+
10361052
class CategoriesEditDelegate(QStyledItemDelegate):
10371053
"""
10381054
Display delegate for editing categories.
@@ -1064,6 +1080,55 @@ def initStyleOption(self, option, index):
10641080
text = text + " " + suffix
10651081
option.text = text
10661082

1083+
class CatEditComboBox(QComboBox):
1084+
prows: List[QPersistentModelIndex]
1085+
1086+
def createEditor(self, parent: QWidget, option: 'QStyleOptionViewItem', index: QModelIndex) -> QWidget:
1087+
view = option.widget
1088+
assert isinstance(view, QAbstractItemView)
1089+
selmodel = view.selectionModel()
1090+
rows = selmodel.selectedRows(0)
1091+
if len(rows) < 2:
1092+
return super().createEditor(parent, option, index)
1093+
# edit multiple selection
1094+
cb = CategoriesEditDelegate.CatEditComboBox(
1095+
editable=True, insertPolicy=QComboBox.InsertAtBottom)
1096+
cb.setParent(view, Qt.Popup)
1097+
cb.addItems(
1098+
list(unique(str(row.data(Qt.EditRole)) for row in rows)))
1099+
prows = [QPersistentModelIndex(row) for row in rows]
1100+
cb.prows = prows
1101+
return cb
1102+
1103+
def updateEditorGeometry(self, editor: QWidget, option: 'QStyleOptionViewItem', index: QModelIndex) -> None:
1104+
if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):
1105+
view = cast(QAbstractItemView, option.widget)
1106+
view.scrollTo(index)
1107+
vport = view.viewport()
1108+
vrect = view.visualRect(index)
1109+
vrect = mapRectTo(vport, view, vrect)
1110+
vrect = vrect.intersected(vport.geometry())
1111+
vrect = mapRectToGlobal(vport, vrect)
1112+
size = editor.sizeHint().expandedTo(vrect.size())
1113+
editor.resize(size)
1114+
editor.move(vrect.topLeft())
1115+
else:
1116+
super().updateEditorGeometry(editor, option, index)
1117+
1118+
def setModelData(self, editor: QWidget, model: QAbstractItemModel, index: QModelIndex) -> None:
1119+
if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):
1120+
text = editor.currentText()
1121+
with signal_blocking(model):
1122+
for prow in editor.prows:
1123+
if prow.isValid():
1124+
model.setData(QModelIndex(prow), text, Qt.EditRole)
1125+
# this could be better
1126+
model.dataChanged.emit(
1127+
model.index(0, 0),
1128+
model.index(model.rowCount() - 1, 0)), (Qt.EditRole, )
1129+
else:
1130+
super().setModelData(editor, model, index)
1131+
10671132

10681133
class DiscreteVariableEditor(VariableEditor):
10691134
"""An editor widget for editing a discrete variable.
@@ -1142,7 +1207,6 @@ def __init__(self, *args, **kwargs):
11421207
toolTip="Merge selected items.",
11431208
shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Equal),
11441209
shortcutContext=Qt.WidgetShortcut,
1145-
enabled=False,
11461210
)
11471211
self.merge_items = QAction(
11481212
"ƒM", group,
@@ -1349,14 +1413,6 @@ def on_values_changed(self):
13491413
@Slot()
13501414
def on_value_selection_changed(self):
13511415
rows = self.values_edit.selectionModel().selectedRows()
1352-
# enable merge if at least 2 selected items and the selection must not
1353-
# contain any added/dropped items.
1354-
enable_merge = \
1355-
len(rows) > 1 and \
1356-
not any(index.data(EditStateRole) != ItemEditState.NoState
1357-
for index in rows)
1358-
self.merge_selected_items.setEnabled(enable_merge)
1359-
13601416
if len(rows) == 1:
13611417
i = rows[0].row()
13621418
self.move_value_up.setEnabled(i != 0)
@@ -1486,58 +1542,14 @@ def _merge_selected_categories(self):
14861542
Popup an editable combo box for selection/edit of a new value.
14871543
"""
14881544
view = self.values_edit
1489-
model = view.model() # type: QAbstractItemModel
1490-
rows = view.selectedIndexes() # type: List[QModelIndex]
1491-
if not len(rows) >= 2:
1492-
return # pragma: no cover
1493-
first_row = rows[0]
1494-
1495-
def mapRectTo(widget, parent, rect):
1496-
# type: (QWidget, QWidget, QRect) -> QRect
1497-
return QRect(
1498-
widget.mapTo(parent, rect.topLeft()),
1499-
rect.size(),
1500-
)
1501-
1502-
def mapRectToGlobal(widget, rect):
1503-
# type: (QWidget, QRect) -> QRect
1504-
return QRect(
1505-
widget.mapToGlobal(rect.topLeft()),
1506-
rect.size(),
1507-
)
1508-
view.scrollTo(first_row)
1509-
vport = view.viewport()
1510-
vrect = view.visualRect(first_row)
1511-
vrect = mapRectTo(vport, view, vrect)
1512-
vrect = vrect.intersected(vport.geometry())
1513-
vrect = mapRectToGlobal(vport, vrect)
1514-
1515-
cb = QComboBox(editable=True, insertPolicy=QComboBox.InsertAtBottom)
1516-
cb.setAttribute(Qt.WA_DeleteOnClose)
1517-
sh = QShortcut(QKeySequence(QKeySequence.Cancel), cb)
1518-
sh.activated.connect(cb.close)
1519-
cb.setParent(self, Qt.Popup)
1520-
cb.move(vrect.topLeft())
1521-
1522-
cb.addItems(
1523-
list(unique(str(row.data(Qt.EditRole)) for row in rows)))
1524-
prows = [QPersistentModelIndex(row) for row in rows]
1525-
1526-
def complete_merge(text):
1527-
# write the new text for edit role in all rows
1528-
with disconnected(model.dataChanged, self.on_values_changed):
1529-
for prow in prows:
1530-
if prow.isValid():
1531-
model.setData(QModelIndex(prow), text, Qt.EditRole)
1532-
cb.close()
1533-
self.variable_changed.emit()
1534-
1535-
cb.activated[str].connect(complete_merge)
1536-
size = cb.sizeHint().expandedTo(vrect.size())
1537-
cb.resize(size)
1538-
cb.show()
1539-
cb.raise_()
1540-
cb.setFocus(Qt.PopupFocusReason)
1545+
selmodel = view.selectionModel()
1546+
index = view.currentIndex()
1547+
if not selmodel.isSelected(index):
1548+
indices = selmodel.selectedRows(0)
1549+
if indices:
1550+
index = indices[0]
1551+
# delegate to the CategoriesEditDelegate
1552+
view.edit(index)
15411553

15421554

15431555
class ContinuousVariableEditor(VariableEditor):

Orange/widgets/data/tests/test_oweditdomain.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,31 @@ def test_discrete_editor_merge_action(self):
517517
self.assertEqual(model.index(0, 0).data(Qt.EditRole), "other")
518518
self.assertEqual(model.index(1, 0).data(Qt.EditRole), "other")
519519

520+
def test_discrete_editor_rename_selected_items_action(self):
521+
w = DiscreteVariableEditor()
522+
v = Categorical("C", ("a", "b", "c"),
523+
(("A", "1"), ("B", "b")), False)
524+
w.set_data_categorical(v, [])
525+
action = w.rename_selected_items
526+
view = w.values_edit
527+
model = view.model()
528+
selmodel = view.selectionModel() # type: QItemSelectionModel
529+
selmodel.select(
530+
QItemSelection(model.index(0, 0), model.index(1, 0)),
531+
QItemSelectionModel.ClearAndSelect
532+
)
533+
# trigger the action, then find the active popup, and simulate entry
534+
spy = QSignalSpy(w.variable_changed)
535+
action.trigger()
536+
cb = view.findChild(QComboBox)
537+
cb.setCurrentText("BA")
538+
view.commitData(cb)
539+
self.assertEqual(model.index(0, 0).data(Qt.EditRole), "BA")
540+
self.assertEqual(model.index(1, 0).data(Qt.EditRole), "BA")
541+
self.assertSequenceEqual(
542+
list(spy), [[]], 'variable_changed should emit exactly once'
543+
)
544+
520545
def test_time_editor(self):
521546
w = TimeVariableEditor()
522547
self.assertEqual(w.get_data(), (None, []))

0 commit comments

Comments
 (0)