Skip to content

Commit

Permalink
oweditdomain: Move multi categories edit to the delegate
Browse files Browse the repository at this point in the history
  • Loading branch information
ales-erjavec committed Aug 28, 2020
1 parent 6ab4a42 commit 9696fe9
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 64 deletions.
140 changes: 76 additions & 64 deletions Orange/widgets/data/oweditdomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from functools import singledispatch, partial
from typing import (
Tuple, List, Any, Optional, Union, Dict, Sequence, Iterable, NamedTuple,
FrozenSet, Type, Callable, TypeVar, Mapping, Hashable
FrozenSet, Type, Callable, TypeVar, Mapping, Hashable, cast
)

import numpy as np
Expand All @@ -24,7 +24,7 @@
QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy, QToolTip,
QDialogButtonBox, QPushButton, QCheckBox, QComboBox, QStackedLayout,
QDialog, QRadioButton, QGridLayout, QLabel, QSpinBox, QDoubleSpinBox,
QShortcut
QShortcut, QAbstractItemView
)
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QKeySequence, QIcon
from AnyQt.QtCore import (
Expand All @@ -38,6 +38,7 @@
from Orange.preprocess.transformation import Transformation, Identity, Lookup
from Orange.widgets import widget, gui, settings
from Orange.widgets.utils import itemmodels
from Orange.widgets.utils.itemmodels import signal_blocking
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.state_summary import format_summary_details
from Orange.widgets.widget import Input, Output
Expand Down Expand Up @@ -469,7 +470,6 @@ def get_dict(self):


class FixedSizeButton(QToolButton):

def __init__(self, *args, defaultAction=None, **kwargs):
super().__init__(*args, **kwargs)
sh = self.sizePolicy()
Expand Down Expand Up @@ -1033,6 +1033,22 @@ def keyRoles(self): # type: () -> FrozenSet[int]
return frozenset({Qt.EditRole, EditStateRole})


def mapRectTo(widget, parent, rect):
# type: (QWidget, QWidget, QRect) -> QRect
return QRect(
widget.mapTo(parent, rect.topLeft()),
rect.size(),
)


def mapRectToGlobal(widget, rect):
# type: (QWidget, QRect) -> QRect
return QRect(
widget.mapToGlobal(rect.topLeft()),
rect.size(),
)


class CategoriesEditDelegate(QStyledItemDelegate):
"""
Display delegate for editing categories.
Expand Down Expand Up @@ -1064,6 +1080,55 @@ def initStyleOption(self, option, index):
text = text + " " + suffix
option.text = text

class CatEditComboBox(QComboBox):
prows: List[QPersistentModelIndex]

def createEditor(self, parent: QWidget, option: 'QStyleOptionViewItem', index: QModelIndex) -> QWidget:
view = option.widget
assert isinstance(view, QAbstractItemView)
selmodel = view.selectionModel()
rows = selmodel.selectedRows(0)
if len(rows) < 2:
return super().createEditor(parent, option, index)
# edit multiple selection
cb = CategoriesEditDelegate.CatEditComboBox(
editable=True, insertPolicy=QComboBox.InsertAtBottom)
cb.setParent(view, Qt.Popup)
cb.addItems(
list(unique(str(row.data(Qt.EditRole)) for row in rows)))
prows = [QPersistentModelIndex(row) for row in rows]
cb.prows = prows
return cb

def updateEditorGeometry(self, editor: QWidget, option: 'QStyleOptionViewItem', index: QModelIndex) -> None:
if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):
view = cast(QAbstractItemView, option.widget)
view.scrollTo(index)
vport = view.viewport()
vrect = view.visualRect(index)
vrect = mapRectTo(vport, view, vrect)
vrect = vrect.intersected(vport.geometry())
vrect = mapRectToGlobal(vport, vrect)
size = editor.sizeHint().expandedTo(vrect.size())
editor.resize(size)
editor.move(vrect.topLeft())
else:
super().updateEditorGeometry(editor, option, index)

def setModelData(self, editor: QWidget, model: QAbstractItemModel, index: QModelIndex) -> None:
if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):
text = editor.currentText()
with signal_blocking(model):
for prow in editor.prows:
if prow.isValid():
model.setData(QModelIndex(prow), text, Qt.EditRole)
# this could be better
model.dataChanged.emit(
model.index(0, 0),
model.index(model.rowCount() - 1, 0)), (Qt.EditRole, )
else:
super().setModelData(editor, model, index)


class DiscreteVariableEditor(VariableEditor):
"""An editor widget for editing a discrete variable.
Expand Down Expand Up @@ -1142,7 +1207,6 @@ def __init__(self, *args, **kwargs):
toolTip="Merge selected items.",
shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Equal),
shortcutContext=Qt.WidgetShortcut,
enabled=False,
)
self.merge_items = QAction(
"ƒM", group,
Expand Down Expand Up @@ -1349,14 +1413,6 @@ def on_values_changed(self):
@Slot()
def on_value_selection_changed(self):
rows = self.values_edit.selectionModel().selectedRows()
# enable merge if at least 2 selected items and the selection must not
# contain any added/dropped items.
enable_merge = \
len(rows) > 1 and \
not any(index.data(EditStateRole) != ItemEditState.NoState
for index in rows)
self.merge_selected_items.setEnabled(enable_merge)

if len(rows) == 1:
i = rows[0].row()
self.move_value_up.setEnabled(i != 0)
Expand Down Expand Up @@ -1486,58 +1542,14 @@ def _merge_selected_categories(self):
Popup an editable combo box for selection/edit of a new value.
"""
view = self.values_edit
model = view.model() # type: QAbstractItemModel
rows = view.selectedIndexes() # type: List[QModelIndex]
if not len(rows) >= 2:
return # pragma: no cover
first_row = rows[0]

def mapRectTo(widget, parent, rect):
# type: (QWidget, QWidget, QRect) -> QRect
return QRect(
widget.mapTo(parent, rect.topLeft()),
rect.size(),
)

def mapRectToGlobal(widget, rect):
# type: (QWidget, QRect) -> QRect
return QRect(
widget.mapToGlobal(rect.topLeft()),
rect.size(),
)
view.scrollTo(first_row)
vport = view.viewport()
vrect = view.visualRect(first_row)
vrect = mapRectTo(vport, view, vrect)
vrect = vrect.intersected(vport.geometry())
vrect = mapRectToGlobal(vport, vrect)

cb = QComboBox(editable=True, insertPolicy=QComboBox.InsertAtBottom)
cb.setAttribute(Qt.WA_DeleteOnClose)
sh = QShortcut(QKeySequence(QKeySequence.Cancel), cb)
sh.activated.connect(cb.close)
cb.setParent(self, Qt.Popup)
cb.move(vrect.topLeft())

cb.addItems(
list(unique(str(row.data(Qt.EditRole)) for row in rows)))
prows = [QPersistentModelIndex(row) for row in rows]

def complete_merge(text):
# write the new text for edit role in all rows
with disconnected(model.dataChanged, self.on_values_changed):
for prow in prows:
if prow.isValid():
model.setData(QModelIndex(prow), text, Qt.EditRole)
cb.close()
self.variable_changed.emit()

cb.activated[str].connect(complete_merge)
size = cb.sizeHint().expandedTo(vrect.size())
cb.resize(size)
cb.show()
cb.raise_()
cb.setFocus(Qt.PopupFocusReason)
selmodel = view.selectionModel()
index = view.currentIndex()
if not selmodel.isSelected(index):
indices = selmodel.selectedRows(0)
if indices:
index = indices[0]
# delegate to the CategoriesEditDelegate
view.edit(index)


class ContinuousVariableEditor(VariableEditor):
Expand Down
25 changes: 25 additions & 0 deletions Orange/widgets/data/tests/test_oweditdomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,31 @@ def test_discrete_editor_merge_action(self):
self.assertEqual(model.index(0, 0).data(Qt.EditRole), "other")
self.assertEqual(model.index(1, 0).data(Qt.EditRole), "other")

def test_discrete_editor_rename_selected_items_action(self):
w = DiscreteVariableEditor()
v = Categorical("C", ("a", "b", "c"),
(("A", "1"), ("B", "b")), False)
w.set_data_categorical(v, [])
action = w.rename_selected_items
view = w.values_edit
model = view.model()
selmodel = view.selectionModel() # type: QItemSelectionModel
selmodel.select(
QItemSelection(model.index(0, 0), model.index(1, 0)),
QItemSelectionModel.ClearAndSelect
)
# trigger the action, then find the active popup, and simulate entry
spy = QSignalSpy(w.variable_changed)
action.trigger()
cb = view.findChild(QComboBox)
cb.setCurrentText("BA")
view.commitData(cb)
self.assertEqual(model.index(0, 0).data(Qt.EditRole), "BA")
self.assertEqual(model.index(1, 0).data(Qt.EditRole), "BA")
self.assertSequenceEqual(
list(spy), [[]], 'variable_changed should emit exactly once'
)

def test_time_editor(self):
w = TimeVariableEditor()
self.assertEqual(w.get_data(), (None, []))
Expand Down

0 comments on commit 9696fe9

Please sign in to comment.