|
13 | 13 | from functools import singledispatch, partial |
14 | 14 | from typing import ( |
15 | 15 | Tuple, List, Any, Optional, Union, Dict, Sequence, Iterable, NamedTuple, |
16 | | - FrozenSet, Type, Callable, TypeVar, Mapping, Hashable |
| 16 | + FrozenSet, Type, Callable, TypeVar, Mapping, Hashable, cast |
17 | 17 | ) |
18 | 18 |
|
19 | 19 | import numpy as np |
|
24 | 24 | QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy, QToolTip, |
25 | 25 | QDialogButtonBox, QPushButton, QCheckBox, QComboBox, QStackedLayout, |
26 | 26 | QDialog, QRadioButton, QGridLayout, QLabel, QSpinBox, QDoubleSpinBox, |
27 | | - QShortcut |
| 27 | + QShortcut, QAbstractItemView |
28 | 28 | ) |
29 | 29 | from AnyQt.QtGui import QStandardItemModel, QStandardItem, QKeySequence, QIcon |
30 | 30 | from AnyQt.QtCore import ( |
|
38 | 38 | from Orange.preprocess.transformation import Transformation, Identity, Lookup |
39 | 39 | from Orange.widgets import widget, gui, settings |
40 | 40 | from Orange.widgets.utils import itemmodels |
| 41 | +from Orange.widgets.utils.itemmodels import signal_blocking |
41 | 42 | from Orange.widgets.utils.widgetpreview import WidgetPreview |
42 | 43 | from Orange.widgets.utils.state_summary import format_summary_details |
43 | 44 | from Orange.widgets.widget import Input, Output |
@@ -469,7 +470,6 @@ def get_dict(self): |
469 | 470 |
|
470 | 471 |
|
471 | 472 | class FixedSizeButton(QToolButton): |
472 | | - |
473 | 473 | def __init__(self, *args, defaultAction=None, **kwargs): |
474 | 474 | super().__init__(*args, **kwargs) |
475 | 475 | sh = self.sizePolicy() |
@@ -1033,6 +1033,22 @@ def keyRoles(self): # type: () -> FrozenSet[int] |
1033 | 1033 | return frozenset({Qt.EditRole, EditStateRole}) |
1034 | 1034 |
|
1035 | 1035 |
|
| 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 | + |
1036 | 1052 | class CategoriesEditDelegate(QStyledItemDelegate): |
1037 | 1053 | """ |
1038 | 1054 | Display delegate for editing categories. |
@@ -1064,6 +1080,55 @@ def initStyleOption(self, option, index): |
1064 | 1080 | text = text + " " + suffix |
1065 | 1081 | option.text = text |
1066 | 1082 |
|
| 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 | + |
1067 | 1132 |
|
1068 | 1133 | class DiscreteVariableEditor(VariableEditor): |
1069 | 1134 | """An editor widget for editing a discrete variable. |
@@ -1142,7 +1207,6 @@ def __init__(self, *args, **kwargs): |
1142 | 1207 | toolTip="Merge selected items.", |
1143 | 1208 | shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Equal), |
1144 | 1209 | shortcutContext=Qt.WidgetShortcut, |
1145 | | - enabled=False, |
1146 | 1210 | ) |
1147 | 1211 | self.merge_items = QAction( |
1148 | 1212 | "ƒM", group, |
@@ -1349,14 +1413,6 @@ def on_values_changed(self): |
1349 | 1413 | @Slot() |
1350 | 1414 | def on_value_selection_changed(self): |
1351 | 1415 | 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 | | - |
1360 | 1416 | if len(rows) == 1: |
1361 | 1417 | i = rows[0].row() |
1362 | 1418 | self.move_value_up.setEnabled(i != 0) |
@@ -1486,58 +1542,14 @@ def _merge_selected_categories(self): |
1486 | 1542 | Popup an editable combo box for selection/edit of a new value. |
1487 | 1543 | """ |
1488 | 1544 | 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) |
1541 | 1553 |
|
1542 | 1554 |
|
1543 | 1555 | class ContinuousVariableEditor(VariableEditor): |
|
0 commit comments