Skip to content

Commit 6ba8ef0

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

File tree

2 files changed

+102
-64
lines changed

2 files changed

+102
-64
lines changed

Orange/widgets/data/oweditdomain.py

Lines changed: 77 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,14 @@ def keyRoles(self): # type: () -> FrozenSet[int]
10331033
return frozenset({Qt.EditRole, EditStateRole})
10341034

10351035

1036+
def mapRectTo(widget: QWidget, parent: QWidget, rect: QRect) -> QRect: # pylint: disable=redefined-outer-name
1037+
return QRect(widget.mapTo(parent, rect.topLeft()), rect.size())
1038+
1039+
1040+
def mapRectToGlobal(widget: QWidget, rect: QRect) -> QRect: # pylint: disable=redefined-outer-name
1041+
return QRect(widget.mapToGlobal(rect.topLeft()), rect.size())
1042+
1043+
10361044
class CategoriesEditDelegate(QStyledItemDelegate):
10371045
"""
10381046
Display delegate for editing categories.
@@ -1064,6 +1072,64 @@ def initStyleOption(self, option, index):
10641072
text = text + " " + suffix
10651073
option.text = text
10661074

1075+
class CatEditComboBox(QComboBox):
1076+
prows: List[QPersistentModelIndex]
1077+
1078+
def createEditor(
1079+
self, parent: QWidget, option: 'QStyleOptionViewItem',
1080+
index: QModelIndex
1081+
) -> QWidget:
1082+
view = option.widget
1083+
assert isinstance(view, QAbstractItemView)
1084+
selmodel = view.selectionModel()
1085+
rows = selmodel.selectedRows(0)
1086+
if len(rows) < 2:
1087+
return super().createEditor(parent, option, index)
1088+
# edit multiple selection
1089+
cb = CategoriesEditDelegate.CatEditComboBox(
1090+
editable=True, insertPolicy=QComboBox.InsertAtBottom)
1091+
cb.setParent(view, Qt.Popup)
1092+
cb.addItems(
1093+
list(unique(str(row.data(Qt.EditRole)) for row in rows)))
1094+
prows = [QPersistentModelIndex(row) for row in rows]
1095+
cb.prows = prows
1096+
return cb
1097+
1098+
def updateEditorGeometry(
1099+
self, editor: QWidget, option: 'QStyleOptionViewItem',
1100+
index: QModelIndex
1101+
) -> None:
1102+
if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):
1103+
view = cast(QAbstractItemView, option.widget)
1104+
view.scrollTo(index)
1105+
vport = view.viewport()
1106+
vrect = view.visualRect(index)
1107+
vrect = mapRectTo(vport, view, vrect)
1108+
vrect = vrect.intersected(vport.geometry())
1109+
vrect = mapRectToGlobal(vport, vrect)
1110+
size = editor.sizeHint().expandedTo(vrect.size())
1111+
editor.resize(size)
1112+
editor.move(vrect.topLeft())
1113+
else:
1114+
super().updateEditorGeometry(editor, option, index)
1115+
1116+
def setModelData(
1117+
self, editor: QWidget, model: QAbstractItemModel, index: QModelIndex
1118+
) -> 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), model.index(model.rowCount() - 1, 0),
1128+
(Qt.EditRole,)
1129+
)
1130+
else:
1131+
super().setModelData(editor, model, index)
1132+
10671133

10681134
class DiscreteVariableEditor(VariableEditor):
10691135
"""An editor widget for editing a discrete variable.
@@ -1142,7 +1208,6 @@ def __init__(self, *args, **kwargs):
11421208
toolTip="Merge selected items.",
11431209
shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Equal),
11441210
shortcutContext=Qt.WidgetShortcut,
1145-
enabled=False,
11461211
)
11471212
self.merge_items = QAction(
11481213
"ƒM", group,
@@ -1349,14 +1414,6 @@ def on_values_changed(self):
13491414
@Slot()
13501415
def on_value_selection_changed(self):
13511416
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-
13601417
if len(rows) == 1:
13611418
i = rows[0].row()
13621419
self.move_value_up.setEnabled(i != 0)
@@ -1486,58 +1543,14 @@ def _merge_selected_categories(self):
14861543
Popup an editable combo box for selection/edit of a new value.
14871544
"""
14881545
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)
1546+
selmodel = view.selectionModel()
1547+
index = view.currentIndex()
1548+
if not selmodel.isSelected(index):
1549+
indices = selmodel.selectedRows(0)
1550+
if indices:
1551+
index = indices[0]
1552+
# delegate to the CategoriesEditDelegate
1553+
view.edit(index)
15411554

15421555

15431556
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)