Skip to content

Commit

Permalink
OWCreateInstance: Fix controls sizes
Browse files Browse the repository at this point in the history
  • Loading branch information
VesnaT committed Oct 26, 2020
1 parent be7941d commit 0159778
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 45 deletions.
102 changes: 64 additions & 38 deletions Orange/widgets/data/owcreateinstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import numpy as np

from AnyQt.QtCore import Qt, QSortFilterProxyModel, QSize, QDateTime, \
QModelIndex, Signal, QPoint
QModelIndex, Signal, QPoint, QRect
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QIcon, QPainter
from AnyQt.QtWidgets import QLineEdit, QTableView, QSlider, QHeaderView, \
from AnyQt.QtWidgets import QLineEdit, QTableView, QSlider, \
QComboBox, QStyledItemDelegate, QWidget, QDateTimeEdit, QHBoxLayout, \
QDoubleSpinBox, QSizePolicy, QStyleOptionViewItem, QLabel, QMenu, QAction

Expand All @@ -31,29 +31,36 @@ class VariableEditor(QWidget):
def __init__(self, parent: QWidget, callback: Callable):
super().__init__(parent)
layout = QHBoxLayout()
layout.setContentsMargins(4, 0, 4, 0)
layout.setContentsMargins(6, 0, 6, 0)
layout.setAlignment(Qt.AlignLeft)
self.setLayout(layout)
self.value_changed.connect(callback)

@property
def value(self) -> Union[int, float]:
def value(self) -> Union[int, float, str]:
return NotImplemented

@value.setter
def value(self, value: float):
def value(self, value: Union[float, str]):
raise NotImplementedError

def sizeHint(self):
return QSize(super().sizeHint().width(), 40)


class DiscreteVariableEditor(VariableEditor):
value_changed = Signal(int)

def __init__(self, parent: QWidget, items: List[str], callback: Callable):
super().__init__(parent, callback)
self._combo = QComboBox(parent)
self._combo = QComboBox(
parent,
maximumWidth=180,
sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
)
self._combo.addItems(items)
self._combo.currentIndexChanged.connect(self.value_changed)
self.layout().addWidget(self._combo)
self.layout().setContentsMargins(0, 1, 0, 0)

@property
def value(self) -> int:
Expand Down Expand Up @@ -88,9 +95,9 @@ def __init__(self, parent: QWidget, variable: ContinuousVariable,
sp_spin = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
sp_spin.setHorizontalStretch(1)
sp_slider = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
sp_slider.setHorizontalStretch(6)
sp_slider.setHorizontalStretch(5)
sp_edit = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
sp_edit.setHorizontalStretch(2)
sp_edit.setHorizontalStretch(1)

class DoubleSpinBox(QDoubleSpinBox):
def sizeHint(self) -> QSize:
Expand All @@ -113,7 +120,6 @@ def sizeHint(self) -> QSize:
maximum=self.__map_to_slider(self._max_value),
singleStep=1,
orientation=Qt.Horizontal,
minimumWidth=20,
sizePolicy=sp_slider,
)
self._label_min = QLabel(
Expand All @@ -138,19 +144,23 @@ def sizeHint(self) -> QSize:
self.layout().addWidget(self._label_min)
self.layout().addWidget(self._slider)
self.layout().addWidget(self._label_max)
self.setMinimumWidth(200)

self.setFocusProxy(self._spin)

# FIXME: after setting focus proxy to the spin, the text is highlighted

def deselect():
self._spin.lineEdit().deselect()
try:
self._spin.lineEdit().selectionChanged.disconnect(deselect)
except TypeError:
pass

# Invoking self.setFocusProxy(self._spin), causes the
# self._spin.lineEdit()s to have selected texts (focus is set to
# provide keyboard functionality, i.e.: pressing ESC after changing
# spinbox value). Since the spin text is selected only after the
# delegate draws it, it cannot be deselected during initialization.
# Therefore connect the deselect() function to
# self._spin.lineEdit().selectionChanged only for editor creation.
self._spin.lineEdit().selectionChanged.connect(deselect)

@property
Expand All @@ -163,7 +173,11 @@ def value(self, value: float):
self._value = value
self.value_changed.emit(self.value)
self._spin.setValue(self.value)
self._slider.setValue(self.__map_to_slider(self.value))
# prevent emitting self.value_changed again, due to slider change
slider_value = self.__map_to_slider(self.value)
self._value = self.__map_from_slider(slider_value)
self._slider.setValue(slider_value)
self._value = value

def _apply_slider_value(self):
self.value = self.__map_from_slider(self._slider.value())
Expand All @@ -187,11 +201,12 @@ class StringVariableEditor(VariableEditor):

def __init__(self, parent: QWidget, callback: Callable):
super().__init__(parent, callback)
self._edit = QLineEdit(parent)
self._edit = QLineEdit(
parent,
sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
)
self._edit.textChanged.connect(self.value_changed)
self._edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.layout().addWidget(self._edit)
self.layout().setContentsMargins(5, 0, 5, 0)
self.setFocusProxy(self._edit)

@property
Expand Down Expand Up @@ -230,6 +245,7 @@ def sizeHint(self) -> QSize:
parent,
dateTime=self.__map_to_datetime(self._value),
displayFormat=self._format,
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
)
self._edit.dateTimeChanged.connect(self._apply_edit_value)

Expand Down Expand Up @@ -275,19 +291,29 @@ def _commit_data(self):
assert isinstance(editor, VariableEditor)
self.commitData.emit(editor)

@staticmethod
def setEditorData(editor: VariableEditor, index: QModelIndex):
# pylint: disable=no-self-use
def setEditorData(self, editor: VariableEditor, index: QModelIndex):
editor.value = index.model().data(index, ValueRole)

@staticmethod
def setModelData(editor: VariableEditor, model: QSortFilterProxyModel,
index: QModelIndex):
# pylint: disable=no-self-use
def setModelData(self, editor: VariableEditor,
model: QSortFilterProxyModel, index: QModelIndex):
model.setData(index, editor.value, ValueRole)

def sizeHint(self, option: QStyleOptionViewItem,
index: QModelIndex) -> QSize:
sh = super().sizeHint(option, index)
return QSize(sh.width(), 40)
# pylint: disable=no-self-use
def updateEditorGeometry(self, editor: VariableEditor,
option: QStyleOptionViewItem, _: QModelIndex):
rect: QRect = option.rect
if isinstance(editor, ContinuousVariableEditor):
width = editor.sizeHint().width()
if width > rect.width():
rect.setWidth(width)
editor.setGeometry(rect)

# pylint: disable=no-self-use
def sizeHint(self, _: QStyleOptionViewItem, index: QModelIndex) -> QSize:
return _create_editor(index.data(role=VariableRole), np.array([0]),
None, lambda: 1).sizeHint()


@singledispatch
Expand Down Expand Up @@ -335,6 +361,7 @@ def cont_random(values: np.ndarray) -> float:
class VariableItemModel(QStandardItemModel):
dataHasNanColumn = Signal()

# pylint: disable=dangerous-default-value
def set_data(self, data: Table, saved_values={}):
for variable in data.domain.variables + data.domain.metas:
if variable.is_primitive():
Expand All @@ -350,6 +377,7 @@ def _add_row(self, variable: Variable, values: np.ndarray,
saved_value: Optional[Union[int, float, str]]):
var_item = QStandardItem()
var_item.setData(variable.name, Qt.DisplayRole)
var_item.setToolTip(variable.name)
var_item.setIcon(self._variable_icon(variable))
var_item.setEditable(False)

Expand Down Expand Up @@ -438,11 +466,11 @@ def __init__(self):
self.Header.variable, VariableDelegate(self)
)
self.view.verticalHeader().hide()
header: QHeaderView = self.view.horizontalHeader()
header.setStretchLastSection(True)
header.setMaximumSectionSize(300)
self.view.horizontalHeader().setStretchLastSection(True)
self.view.horizontalHeader().setMaximumSectionSize(350)

self.model = VariableItemModel(self)
self.model.setHorizontalHeaderLabels([x for _, x in self.HEADER])
self.model.dataChanged.connect(self.__table_data_changed)
self.model.dataHasNanColumn.connect(self.Information.nans_removed)
self.proxy_model = QSortFilterProxyModel()
Expand Down Expand Up @@ -546,23 +574,21 @@ def _initialize_values(self, fun: str, indices: List[QModelIndex] = None):
def set_data(self, data: Table):
self.data = data
self._set_input_summary()
self._clear()
self._set_model_data()
self.unconditional_commit()

def _clear(self):
self.Information.nans_removed.clear()
self.model.clear()
self.model.setHorizontalHeaderLabels([x for _, x in self.HEADER])

def _set_model_data(self):
self.Information.nans_removed.clear()
self.model.removeRows(0, self.model.rowCount())
if not self.data:
return

self.model.set_data(self.data, self.__pending_values)
self.__pending_values = {}
self.view.horizontalHeader().setStretchLastSection(False)
self.view.resizeColumnsToContents()
self.view.resizeRowsToContents()
self.view.horizontalHeader().setStretchLastSection(True)

@Inputs.reference
def set_reference(self, data: Table):
Expand Down Expand Up @@ -627,10 +653,10 @@ def send_report(self):

@staticmethod
def sizeHint():
return QSize(800, 500)
return QSize(600, 500)


if __name__ == "__main__": # pragma: no cover
table = Table("heart_disease")
table = Table("housing")
WidgetPreview(OWCreateInstance).run(set_data=table,
set_reference=table[::2])
set_reference=table[:1])
88 changes: 81 additions & 7 deletions Orange/widgets/data/tests/test_owcreateinstance.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# pylint: disable=missing-docstring
# pylint: disable=missing-docstring,protected-access
from unittest.mock import Mock

import numpy as np

from AnyQt.QtCore import QDateTime, QDate, QTime
from AnyQt.QtWidgets import QWidget
from AnyQt.QtCore import QDateTime, QDate, QTime, QPoint
from AnyQt.QtWidgets import QWidget, QLineEdit, QStyleOptionViewItem, QMenu

from orangewidget.tests.base import GuiTest
from Orange.data import Table, ContinuousVariable, Domain, DiscreteVariable, \
TimeVariable
from Orange.widgets.data.owcreateinstance import OWCreateInstance, \
DiscreteVariableEditor, ContinuousVariableEditor, StringVariableEditor, \
TimeVariableEditor
TimeVariableEditor, VariableDelegate, VariableItemModel, ValueRole
from Orange.widgets.tests.base import WidgetTest, datasets
from Orange.widgets.utils.state_summary import format_summary_details, \
format_multiple_summaries
Expand Down Expand Up @@ -105,6 +105,11 @@ def test_initialize_buttons(self):
output_random = self.get_output(self.widget.Outputs.data)
self.assert_table_equal(output_random, self.data[9:10])

self.send_signal(self.widget.Inputs.reference, None)
buttons[3].click() # Input
output = self.get_output(self.widget.Outputs.data)
self.assert_table_equal(output_random, output)

def test_initialize_buttons_commit_once(self):
self.widget.commit = self.widget.unconditional_commit = Mock()
self.send_signal(self.widget.Inputs.data, self.data)
Expand Down Expand Up @@ -185,6 +190,19 @@ def test_commit_once(self):
self.send_signal(self.widget.Inputs.data, self.data)
self.widget.commit.assert_called_once()

def test_context_menu(self):
self.send_signal(self.widget.Inputs.data, self.data)
self.send_signal(self.widget.Inputs.reference, self.data[:1])
output1 = self.get_output(self.widget.Outputs.data)
self.widget.view.customContextMenuRequested.emit(QPoint(0, 0))
menu = [w for w in self.widget.children() if isinstance(w, QMenu)][0]
self.assertEqual(len(menu.actions()), 4)

menu.actions()[3].trigger() # Input
output2 = self.get_output(self.widget.Outputs.data)
np.testing.assert_array_equal(output2.X[:, 1:], output1.X[:, 1:])
np.testing.assert_array_equal(output2.X[:, :1], self.data.X[:1, :1])

def test_report(self):
self.widget.send_report()
self.send_signal(self.widget.Inputs.data, self.data)
Expand Down Expand Up @@ -309,10 +327,22 @@ def test_set_value(self):
self.callback.assert_called_once()

def test_missing_values(self):
domain = Domain([ContinuousVariable("var")])
data = Table(domain, np.array([[np.nan], [np.nan]]))
var = ContinuousVariable("var")
self.assertRaises(ValueError, ContinuousVariableEditor, self.parent,
data.domain[0], np.nan, np.nan, Mock())
var, np.nan, np.nan, Mock())

def test_overflow(self):
var = ContinuousVariable("var", number_of_decimals=10)
editor = ContinuousVariableEditor(
self.parent, var, -100000, 1, self.callback
)
self.assertLess(editor._n_decimals, 10)

def test_spin_selection_after_init(self):
edit: QLineEdit = self.editor._spin.lineEdit()
edit.selectAll()
self.assertEqual(edit.selectedText(), "")
self.assertIs(self.editor.focusProxy(), edit.parent())


class TestStringVariableEditor(GuiTest):
Expand Down Expand Up @@ -428,6 +458,50 @@ def test_no_date_no_time(self):
callback.assert_called_once()


class TestVariableDelegate(GuiTest):
def setUp(self):
self.data = Table("iris")
self.model = model = VariableItemModel()
model.set_data(self.data)
widget = OWCreateInstance()
self.delegate = VariableDelegate(widget)
self.parent = QWidget()
self.opt = QStyleOptionViewItem()

def test_create_editor(self):
index = self.model.index(0, 1)
editor = self.delegate.createEditor(self.parent, self.opt, index)
self.assertIsInstance(editor, ContinuousVariableEditor)

index = self.model.index(4, 1)
editor = self.delegate.createEditor(self.parent, self.opt, index)
self.assertIsInstance(editor, DiscreteVariableEditor)

def test_set_editor_data(self):
index = self.model.index(0, 1)
editor = self.delegate.createEditor(self.parent, self.opt, index)
self.delegate.setEditorData(editor, index)
self.assertEqual(editor.value, np.median(self.data.X[:, 0]))

def test_set_model_data(self):
index = self.model.index(0, 1)
editor = self.delegate.createEditor(self.parent, self.opt, index)
editor.value = 7.5
self.delegate.setModelData(editor, self.model, index)
self.assertEqual(self.model.data(index, ValueRole), 7.5)

def test_editor_geometry(self):
index = self.model.index(0, 1)
editor = self.delegate.createEditor(self.parent, self.opt, index)
self.delegate.updateEditorGeometry(editor, self.opt, index)
self.assertGreaterEqual(editor.geometry().width(),
self.opt.rect.width())

size = self.delegate.sizeHint(self.opt, index)
self.assertEqual(size.width(), editor.geometry().width())
self.assertEqual(size.height(), 40)


if __name__ == "__main__":
import unittest
unittest.main()

0 comments on commit 0159778

Please sign in to comment.