Repository: biolab/orange3

Issue:
Clean up Manifold Learner UX
- Use QFormLayout,
- probably replace spins with line edits,
- double colons...



Discussion:

Resolving patch:
diff --git a/Orange/widgets/unsupervised/owmanifoldlearning.py b/Orange/widgets/unsupervised/owmanifoldlearning.py
--- a/Orange/widgets/unsupervised/owmanifoldlearning.py
+++ b/Orange/widgets/unsupervised/owmanifoldlearning.py
@@ -3,7 +3,8 @@
 
 import numpy as np
 
-from AnyQt.QtWidgets import QWidget, QVBoxLayout
+from AnyQt.QtWidgets import QWidget, QFormLayout
+from AnyQt.QtGui import QFontMetrics
 from AnyQt.QtCore import Qt
 
 from Orange.data import Table, Domain, ContinuousVariable
@@ -25,11 +26,10 @@ def __init__(self, parent):
         self.parameters = {}
         self.parent_callback = parent.settings_changed
 
-        # GUI
-        self.setMinimumWidth(221)
-        self.setLayout(QVBoxLayout())
-        self.layout().setContentsMargins(0, 0, 0, 0)
-        self.main_area = gui.vBox(self, spacing=0)
+        layout = QFormLayout()
+        self.setLayout(layout)
+        layout.setVerticalSpacing(4)
+        layout.setContentsMargins(0, 0, 0, 0)
 
     def get_parameters(self):
         return self.parameters
@@ -40,11 +40,14 @@ def __parameter_changed(self, update_parameter, parameter_name):
 
     def _create_spin_parameter(self, name, minv, maxv, label):
         self.__spin_parameter_update(name)
-        return gui.spin(
-            self.main_area, self, name, minv, maxv, label=label,
+        width = QFontMetrics(self.font()).width("0" * 10)
+        control = gui.spin(
+            self, self, name, minv, maxv,
             alignment=Qt.AlignRight, callbackOnReturn=True,
+            addToLayout=False, controlWidth=width,
             callback=lambda f=self.__spin_parameter_update,
                             p=name: self.__parameter_changed(f, p))
+        self.layout().addRow(label, control)
 
     def __spin_parameter_update(self, name):
         self.parameters[name] = getattr(self, name)
@@ -52,11 +55,12 @@ def __spin_parameter_update(self, name):
     def _create_combo_parameter(self, name, label):
         self.__combo_parameter_update(name)
         items = (x[1] for x in getattr(self, name + "_values"))
-        return gui.comboBox(
-            self.main_area, self, name + "_index", label=label, items=items,
-            orientation=Qt.Horizontal,
+        control = gui.comboBox(
+            None, self, name + "_index", items=items,
             callback=lambda f=self.__combo_parameter_update,
-                            p=name: self.__parameter_changed(f, p))
+                            p=name: self.__parameter_changed(f, p)
+        )
+        self.layout().addRow(label, control)
 
     def __combo_parameter_update(self, name):
         index = getattr(self, name + "_index")
@@ -79,16 +83,17 @@ def __check_parameter_update(self, name):
     def _create_radio_parameter(self, name, label):
         self.__radio_parameter_update(name)
         values = (x[1] for x in getattr(self, name + "_values"))
-        gui.separator(self.main_area)
-        box = gui.hBox(self.main_area)
-        lbl = gui.label(box, self, label + ":")
+        space = QWidget()
+        space.setFixedHeight(4)
+        self.layout().addRow(space)
         rbt = gui.radioButtons(
-            box, self, name + "_index", btnLabels=values,
+            None, self, name + "_index", btnLabels=values,
             callback=lambda f=self.__radio_parameter_update,
                             p=name: self.__parameter_changed(f, p))
-        rbt.layout().setAlignment(Qt.AlignTop)
-        lbl.setAlignment(Qt.AlignTop)
-        return rbt
+        labox = gui.vBox(None)
+        gui.widgetLabel(labox, label)
+        gui.rubber(labox)
+        self.layout().addRow(labox, rbt)
 
     def __radio_parameter_update(self, name):
         index = getattr(self, name + "_index")
@@ -111,19 +116,13 @@ class TSNEParametersEditor(ManifoldParametersEditor):
 
     def __init__(self, parent):
         super().__init__(parent)
-
-        self.metric_combo = self._create_combo_parameter(
-            "metric", "Metric:")
-        self.perplexity_spin = self._create_spin_parameter(
-            "perplexity", 1, 100, "Perplexity:")
-        self.early_exaggeration_spin = self._create_spin_parameter(
-            "early_exaggeration", 1, 100, "Early exaggeration:")
-        self.lr_spin = self._create_spin_parameter(
-            "learning_rate", 1, 1000, "Learning rate:")
-        self.n_iter_spin = self._create_spin_parameter(
-            "n_iter", 250, 1e5, "Max iterations:")
-        self.init_radio = self._create_radio_parameter(
-            "initialization", "Initialization:")
+        self._create_combo_parameter("metric", "Metric:")
+        self._create_spin_parameter("perplexity", 1, 100, "Perplexity:")
+        self._create_spin_parameter("early_exaggeration", 1, 100,
+                                    "Early exaggeration:")
+        self._create_spin_parameter("learning_rate", 1, 1000, "Learning rate:")
+        self._create_spin_parameter("n_iter", 250, 1e5, "Max iterations:")
+        self._create_radio_parameter("initialization", "Initialization:")
 
 
 class MDSParametersEditor(ManifoldParametersEditor):
@@ -134,10 +133,8 @@ class MDSParametersEditor(ManifoldParametersEditor):
 
     def __init__(self, parent):
         super().__init__(parent)
-        self.max_iter_spin = self._create_spin_parameter(
-            "max_iter", 10, 10 ** 4, "Max iterations:")
-        self.random_state_radio = self._create_radio_parameter(
-            "init_type", "Initialization")
+        self._create_spin_parameter("max_iter", 10, 10 ** 4, "Max iterations:")
+        self._create_radio_parameter("init_type", "Initialization:")
 
     def get_parameters(self):
         par = super().get_parameters()
@@ -150,8 +147,7 @@ class IsomapParametersEditor(ManifoldParametersEditor):
 
     def __init__(self, parent):
         super().__init__(parent)
-        self.n_neighbors_spin = self._create_spin_parameter(
-            "n_neighbors", 1, 10 ** 2, "Neighbors:")
+        self._create_spin_parameter("n_neighbors", 1, 10 ** 2, "Neighbors:")
 
 
 class LocallyLinearEmbeddingParametersEditor(ManifoldParametersEditor):
@@ -165,12 +161,9 @@ class LocallyLinearEmbeddingParametersEditor(ManifoldParametersEditor):
 
     def __init__(self, parent):
         super().__init__(parent)
-        self.method_combo = self._create_combo_parameter(
-            "method", "Method:")
-        self.n_neighbors_spin = self._create_spin_parameter(
-            "n_neighbors", 1, 10 ** 2, "Neighbors:")
-        self.max_iter_spin = self._create_spin_parameter(
-            "max_iter", 10, 10 ** 4, "Max iterations:")
+        self._create_combo_parameter("method", "Method:")
+        self._create_spin_parameter("n_neighbors", 1, 10 ** 2, "Neighbors:")
+        self._create_spin_parameter("max_iter", 10, 10 ** 4, "Max iterations:")
 
 
 class SpectralEmbeddingParametersEditor(ManifoldParametersEditor):
@@ -180,8 +173,7 @@ class SpectralEmbeddingParametersEditor(ManifoldParametersEditor):
 
     def __init__(self, parent):
         super().__init__(parent)
-        self.affinity_combo = self._create_combo_parameter(
-            "affinity", "Affinity:")
+        self._create_combo_parameter("affinity", "Affinity:")
 
 
 class OWManifoldLearning(OWWidget):
@@ -253,7 +245,7 @@ def __init__(self):
         self._set_input_summary()
         self._set_output_summary(None)
 
-        self.params_box = gui.vBox(self.controlArea, "Parameters")
+        self.params_box = gui.vBox(method_box)
 
         self.tsne_editor = TSNEParametersEditor(self)
         self.mds_editor = MDSParametersEditor(self)
@@ -273,8 +265,10 @@ def __init__(self):
         output_box = gui.vBox(self.controlArea, "Output")
         self.n_components_spin = gui.spin(
             output_box, self, "n_components", 1, 10, label="Components:",
+            controlWidth=QFontMetrics(self.font()).width("0" * 10),
             alignment=Qt.AlignRight, callbackOnReturn=True,
             callback=self.settings_changed)
+        gui.rubber(self.n_components_spin.box)
         self.apply_button = gui.auto_apply(self.controlArea, self, box=False, commit=self.apply)
 
     def manifold_method_changed(self):


Test patch:
diff --git a/Orange/widgets/unsupervised/tests/test_owmanifoldlearning.py b/Orange/widgets/unsupervised/tests/test_owmanifoldlearning.py
--- a/Orange/widgets/unsupervised/tests/test_owmanifoldlearning.py
+++ b/Orange/widgets/unsupervised/tests/test_owmanifoldlearning.py
@@ -102,7 +102,7 @@ def __callback():
             self.assertFalse(self.widget.Error.manifold_error.is_shown())
 
         simulate.combobox_run_through_all(
-            self.widget.tsne_editor.metric_combo, callback=__callback,
+            self.widget.tsne_editor.controls.metric_index, callback=__callback,
         )
 
     def test_unique_domain(self):