diff --git a/Orange/widgets/unsupervised/owsom.py b/Orange/widgets/unsupervised/owsom.py index a6423734d16..391aabdb7e1 100644 --- a/Orange/widgets/unsupervised/owsom.py +++ b/Orange/widgets/unsupervised/owsom.py @@ -1,4 +1,5 @@ from collections import defaultdict, namedtuple +from contextlib import contextmanager from typing import Optional from xml.sax.saxutils import escape @@ -171,6 +172,15 @@ def paint(self, painter, _option, _index): painter.restore() +@contextmanager +def disconnected_spin(spin): + spin.blockSignals(True) + try: + yield + finally: + spin.blockSignals(False) + + N_ITERATIONS = 200 @@ -209,6 +219,12 @@ class Outputs: ("shape", "auto_dim", "spin_x", "spin_y", "initialization", "start") ) + class Information(OWWidget.Information): + modified = Msg( + 'The parameter settings have been changed. Press "Start" to ' + "rerun with the new settings." + ) + class Warning(OWWidget.Warning): ignoring_disc_variables = Msg("SOM ignores categorical variables.") missing_colors = \ @@ -243,6 +259,7 @@ def __init__(self): shape = gui.comboBox( box, self, "", items=("Hexagonal grid", "Square grid")) shape.setCurrentIndex(1 - self.hexagonal) + shape.currentIndexChanged.connect(self.on_parameter_change) box2 = gui.indentedBox(box, 10) auto_dim = gui.checkBox( @@ -250,27 +267,40 @@ def __init__(self): callback=self.on_auto_dimension_changed) self.manual_box = box3 = gui.hBox(box2) spinargs = dict( - value="", widget=box3, master=self, minv=5, maxv=100, step=5, - alignment=Qt.AlignRight) - spin_x = gui.spin(**spinargs) - spin_x.setValue(self.size_x) + value="", + widget=box3, + master=self, + minv=5, + maxv=100, + step=5, + alignment=Qt.AlignRight, + callback=self.on_parameter_change, + ) + self.spin_x = gui.spin(**spinargs) + with disconnected_spin(self.spin_x): + self.spin_x.setValue(self.size_x) gui.widgetLabel(box3, "×") - spin_y = gui.spin(**spinargs) - spin_y.setValue(self.size_y) + self.spin_y = gui.spin(**spinargs) + with disconnected_spin(self.spin_y): + self.spin_y.setValue(self.size_y) gui.rubber(box3) self.manual_box.setEnabled(not self.auto_dimension) initialization = gui.comboBox( - box, self, "initialization", - items=("Initialize with PCA", "Random initialization", - "Replicable random")) + box, + self, + "initialization", + items=("Initialize with PCA", "Random initialization", "Replicable random"), + callback=self.on_parameter_change, + ) start = gui.button( box, self, "Restart", callback=self.restart_som_pressed, sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)) self.opt_controls = self.OptControls( - shape, auto_dim, spin_x, spin_y, initialization, start) + shape, auto_dim, self.spin_x, self.spin_y, initialization, start + ) box = gui.vBox(self.controlArea, "Color") gui.comboBox( @@ -366,7 +396,8 @@ def set_warnings(): self.set_color_bins() self.create_legend() if invalidated: - self.recompute_dimensions() + with disconnected_spin(self.spin_x), disconnected_spin(self.spin_y): + self.recompute_dimensions() self.start_som() else: self._redraw() @@ -399,6 +430,7 @@ def on_auto_dimension_changed(self): dimy = int(5 * np.round(spin_y.value() / 5)) spin_x.setValue(dimx) spin_y.setValue(dimy) + self.on_parameter_change() def on_attr_color_change(self): self.controls.pie_charts.setEnabled(self.attr_color is not None) @@ -413,6 +445,9 @@ def on_attr_size_change(self): def on_pie_chart_change(self): self._redraw() + def on_parameter_change(self): + self.Information.modified() + def clear_selection(self): self.selection = None self.redraw_selection() @@ -498,6 +533,7 @@ def redraw_selection(self, marks=None): cell.setZValue(marked or sel_group) def restart_som_pressed(self): + self.Information.modified.clear() if self._optimizer_thread is not None: self.stop_optimization = True self._optimizer.stop_optimization = True diff --git a/Orange/widgets/unsupervised/tests/test_owsom.py b/Orange/widgets/unsupervised/tests/test_owsom.py index 71e618ba822..9401db19ac7 100644 --- a/Orange/widgets/unsupervised/tests/test_owsom.py +++ b/Orange/widgets/unsupervised/tests/test_owsom.py @@ -5,6 +5,8 @@ import numpy as np import scipy.sparse as sp +from AnyQt.QtWidgets import QComboBox, QPushButton, QCheckBox +from AnyQt.QtCore import Qt from Orange.data import Table, Domain from Orange.widgets.tests.base import WidgetTest @@ -646,6 +648,42 @@ def test_invalidated(self): self.send_signal(self.widget.Inputs.data, heart_with_less_features) self.widget._recompute_som.assert_called_once() + def test_modified_info(self): + w = self.widget + self.assertFalse(w.Information.modified.is_shown()) + self.send_signal(w.Inputs.data, self.iris) + self.assertFalse(w.Information.modified.is_shown()) + restart_button = w.controlArea.findChild(QPushButton) + + # modify grid + simulate.combobox_activate_index(w.controlArea.findChild(QComboBox), 1) + self.assertTrue(w.Information.modified.is_shown()) + restart_button.click() + self.assertFalse(w.Information.modified.is_shown()) + + # modify set dimensions automatically + w.controlArea.findChild(QCheckBox).setCheckState(Qt.Unchecked) + self.assertTrue(w.Information.modified.is_shown()) + restart_button.click() + self.assertFalse(w.Information.modified.is_shown()) + + # modify dimension spins + w.spin_x.setValue(7) + self.assertTrue(w.Information.modified.is_shown()) + restart_button.click() + self.assertFalse(w.Information.modified.is_shown()) + + w.spin_y.setValue(7) + self.assertTrue(w.Information.modified.is_shown()) + restart_button.click() + self.assertFalse(w.Information.modified.is_shown()) + + # modify initialization + simulate.combobox_activate_index(w.controlArea.findChildren(QComboBox)[1], 1) + self.assertTrue(w.Information.modified.is_shown()) + restart_button.click() + self.assertFalse(w.Information.modified.is_shown()) + if __name__ == "__main__": unittest.main()