Repository: biolab/orange3

Issue:
Pythagorean Tree & Tree Viewer Have Static Output Signals 
<!--
This is an issue template. Please fill in the relevant details in the
sections below.

Wrap code and verbatim terminal window output into triple backticks, see:
https://help.github.com/articles/basic-writing-and-formatting-syntax/#quoting-code

If you're raising an issue about an add-on (e.g. installed via
Options > Add-ons), raise an issue on the relevant add-on's issue
tracker instead. See: https://github.com/biolab?q=orange3
-->

##### Orange version
<!-- From menu _Help→About→Version_ or code `Orange.version.full_version` -->
3.8.0.dev0+a3b4f8b

##### Expected behavior
Pythagorean Tree & Tree Viewer would have dynamic signals as most Orange widgets now do.


##### Actual behavior
Pythagorean Tree & Tree Viewer seem to have static signals which causes problems for addons. For example, in Text that uses Corpuses instead of Tables, the outputs from these two widgets cannot be passes back to widgets accepting `Corpus` on input.


##### Steps to reproduce the behavior
In the scheme below, one cannot connet the output of Pythagorean Tree or Tree Viewer back to Corpus Viewer:
![screen shot 2017-11-27 at 17 51 37](https://user-images.githubusercontent.com/713026/33278369-a1c660ac-d39b-11e7-8e94-1a0412fc2e6c.png)



##### Additional info (worksheets, data, screenshots, ...)



Discussion:

Resolving patch:
diff --git a/Orange/widgets/visualize/owpythagorastree.py b/Orange/widgets/visualize/owpythagorastree.py
--- a/Orange/widgets/visualize/owpythagorastree.py
+++ b/Orange/widgets/visualize/owpythagorastree.py
@@ -8,11 +8,12 @@
 
 from Orange.base import TreeModel, SklModel
 from Orange.data.table import Table
-from Orange.widgets import gui, settings, widget
+from Orange.widgets import gui, settings
 from Orange.widgets.utils.annotated_data import (
     create_annotated_table,
     ANNOTATED_DATA_SIGNAL_NAME
 )
+from Orange.widgets.utils.signals import Input, Output
 from Orange.widgets.visualize.pythagorastreeviewer import (
     PythagorasTreeViewer,
     SquareGraphicsItem,
@@ -43,9 +44,12 @@ class OWPythagorasTree(OWWidget):
 
     priority = 1000
 
-    inputs = [('Tree', TreeModel, 'set_tree')]
-    outputs = [('Selected Data', Table, widget.Default),
-               (ANNOTATED_DATA_SIGNAL_NAME, Table)]
+    class Inputs:
+        tree = Input("Tree", TreeModel)
+
+    class Outputs:
+        selected_data = Output("Selected Data", Table, default=True)
+        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)
 
     # Enable the save as feature
     graph_name = 'scene'
@@ -136,6 +140,7 @@ def __init__(self):
         # Clear the widget to correctly set the intial values
         self.clear()
 
+    @Inputs.tree
     def set_tree(self, model=None):
         """When a different tree is given."""
         self.clear()
@@ -185,8 +190,7 @@ def set_tree(self, model=None):
             #     self.depth_limit = model.meta_depth_limit
             #     self.update_depth()
 
-        self.send(ANNOTATED_DATA_SIGNAL_NAME,
-                  create_annotated_table(self.instances, None))
+        self.Outputs.annotated_data.send(create_annotated_table(self.instances, None))
 
     def clear(self):
         """Clear all relevant data from the widget."""
@@ -296,16 +300,15 @@ def onDeleteWidget(self):
     def commit(self):
         """Commit the selected data to output."""
         if self.instances is None:
-            self.send('Selected Data', None)
-            self.send(ANNOTATED_DATA_SIGNAL_NAME, None)
+            self.Outputs.selected_data.send(None)
+            self.Outputs.annotated_data.send(None)
             return
         nodes = [i.tree_node.label for i in self.scene.selectedItems()
                  if isinstance(i, SquareGraphicsItem)]
         data = self.tree_adapter.get_instances_in_nodes(nodes)
-        self.send('Selected Data', data)
+        self.Outputs.selected_data.send(data)
         selected_indices = self.tree_adapter.get_indices(nodes)
-        self.send(ANNOTATED_DATA_SIGNAL_NAME,
-                  create_annotated_table(self.instances, selected_indices))
+        self.Outputs.annotated_data.send(create_annotated_table(self.instances, selected_indices))
 
     def send_report(self):
         """Send report."""
diff --git a/Orange/widgets/visualize/owpythagoreanforest.py b/Orange/widgets/visualize/owpythagoreanforest.py
--- a/Orange/widgets/visualize/owpythagoreanforest.py
+++ b/Orange/widgets/visualize/owpythagoreanforest.py
@@ -9,6 +9,7 @@
 from Orange.base import RandomForestModel, TreeModel
 from Orange.data import Table
 from Orange.widgets import gui, settings
+from Orange.widgets.utils.signals import Input, Output
 from Orange.widgets.visualize.pythagorastreeviewer import (
     PythagorasTreeViewer,
     ContinuousTreeNode,
@@ -30,8 +31,11 @@ class OWPythagoreanForest(OWWidget):
 
     priority = 1001
 
-    inputs = [('Random forest', RandomForestModel, 'set_rf')]
-    outputs = [('Tree', TreeModel)]
+    class Inputs:
+        random_forest = Input("Random forest", RandomForestModel)
+
+    class Outputs:
+        tree = Output("Tree", TreeModel)
 
     # Enable the save as feature
     graph_name = 'scene'
@@ -110,6 +114,7 @@ def __init__(self):
 
         self.clear()
 
+    @Inputs.random_forest
     def set_rf(self, model=None):
         """When a different forest is given."""
         self.clear()
@@ -273,7 +278,7 @@ def commit(self):
             return
 
         if not self.scene.selectedItems():
-            self.send('Tree', None)
+            self.Outputs.tree.send(None)
             # The selected tree index should only reset when model changes
             if self.model is None:
                 self.selected_tree_index = -1
@@ -287,7 +292,7 @@ def commit(self):
         tree.meta_size_calc_idx = self.size_calc_idx
         tree.meta_depth_limit = self.depth_limit
 
-        self.send('Tree', tree)
+        self.Outputs.tree.send(tree)
 
     def send_report(self):
         """Send report."""
diff --git a/Orange/widgets/visualize/owtreeviewer.py b/Orange/widgets/visualize/owtreeviewer.py
--- a/Orange/widgets/visualize/owtreeviewer.py
+++ b/Orange/widgets/visualize/owtreeviewer.py
@@ -9,6 +9,7 @@
 from AnyQt.QtCore import Qt, QPointF, QSizeF, QRectF
 
 from Orange.base import TreeModel, SklModel
+from Orange.widgets.utils.signals import Input, Output
 from Orange.widgets.visualize.owtreeviewer2d import \
     GraphicsNode, GraphicsEdge, OWTreeViewer2D
 from Orange.widgets.utils import to_html
@@ -16,7 +17,7 @@
 
 from Orange.widgets.settings import ContextSetting, ClassValuesContextHandler, \
     Setting
-from Orange.widgets import gui, widget
+from Orange.widgets import gui
 from Orange.widgets.utils.colorpalette import ContinuousPaletteGenerator
 from Orange.widgets.utils.annotated_data import (create_annotated_table,
                                                  ANNOTATED_DATA_SIGNAL_NAME)
@@ -158,21 +159,15 @@ class OWTreeGraph(OWTreeViewer2D):
     name = "Tree Viewer"
     icon = "icons/TreeViewer.svg"
     priority = 35
-    inputs = [
-        widget.InputSignal(
-            "Tree", TreeModel, "ctree",
-            # Had different input names before merging from
-            # Classification/Regression tree variants
-            replaces=["Classification Tree", "Regression Tree"])
-    ]
-    outputs = [
-        widget.OutputSignal(
-            "Selected Data", Table, widget.Default, id="selected-data",
-        ),
-        widget.OutputSignal(
-            ANNOTATED_DATA_SIGNAL_NAME, Table, id="annotated-data")
-    ]
 
+    class Inputs:
+        # Had different input names before merging from
+        # Classification/Regression tree variants
+        tree = Input("Tree", TreeModel, replaces=["Classification Tree", "Regression Tree"])
+
+    class Outputs:
+        selected_data = Output("Selected Data", Table, default=True, id="selected-data")
+        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table, id="annotated-data")
 
     settingsHandler = ClassValuesContextHandler()
     target_class_index = ContextSetting(0)
@@ -257,6 +252,7 @@ def toggle_color_reg(self):
         self.set_node_info()
         self.scene.update()
 
+    @Inputs.tree
     def ctree(self, model=None):
         """Input signal handler"""
         self.clear_scene()
@@ -297,9 +293,8 @@ def ctree(self, model=None):
                 self.tree_adapter.num_nodes,
                 len(self.tree_adapter.leaves(self.tree_adapter.root))))
         self.setup_scene()
-        self.send("Selected Data", None)
-        self.send(ANNOTATED_DATA_SIGNAL_NAME,
-                  create_annotated_table(self.dataset, []))
+        self.Outputs.selected_data.send(None)
+        self.Outputs.annotated_data.send(create_annotated_table(self.dataset, []))
 
     def walkcreate(self, node, parent=None):
         """Create a structure of tree nodes from the given model"""
@@ -324,10 +319,9 @@ def update_selection(self):
         nodes = [item.node_inst for item in self.scene.selectedItems()
                  if isinstance(item, TreeNode)]
         data = self.tree_adapter.get_instances_in_nodes(nodes)
-        self.send("Selected Data", data)
-        self.send(ANNOTATED_DATA_SIGNAL_NAME, create_annotated_table(
-            self.dataset,
-            self.tree_adapter.get_indices(nodes)))
+        self.Outputs.selected_data.send(data)
+        self.Outputs.annotated_data.send(create_annotated_table(
+            self.dataset, self.tree_adapter.get_indices(nodes)))
 
     def send_report(self):
         if not self.model:


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owpythagorastree.py b/Orange/widgets/visualize/tests/test_owpythagorastree.py
--- a/Orange/widgets/visualize/tests/test_owpythagorastree.py
+++ b/Orange/widgets/visualize/tests/test_owpythagorastree.py
@@ -134,7 +134,7 @@ def get_tree_nodes(self):
         return (sq.tree_node for sq in self.get_squares())
 
     def test_tree_is_drawn(self):
-        self.send_signal('Tree', self.housing)
+        self.send_signal(self.widget.Inputs.tree, self.housing)
         self.assertTrue(len(self.get_squares()) > 0)
 
     def _check_all_same(self, items):
@@ -162,13 +162,14 @@ def _callback():
             self.assertTrue(any(x is False for x in squares_same),
                             'Colors did not change for %s data' % data_type)
 
-        self.send_signal('Tree', self.titanic)
+        w = self.widget
+        self.send_signal(w.Inputs.tree, self.titanic)
         _test('classification')
-        self.send_signal('Tree', self.housing)
+        self.send_signal(w.Inputs.tree, self.housing)
         _test('regression')
 
     def test_changing_size_adjustment_changes_sizes(self):
-        self.send_signal('Tree', self.titanic)
+        self.send_signal(self.widget.Inputs.tree, self.titanic)
         squares = []
 
         def _callback():
@@ -187,7 +188,7 @@ def test_log_scale_slider(self):
         self.assertFalse(self.widget.log_scale_box.isEnabled(),
                          'Should be disabled with no tree')
 
-        self.send_signal('Tree', self.titanic)
+        self.send_signal(self.widget.Inputs.tree, self.titanic)
         # No size adjustment
         simulate.combobox_activate_item(self.widget.size_calc_combo, 'Normal')
         self.assertFalse(self.widget.log_scale_box.isEnabled(),
@@ -216,95 +217,96 @@ def test_log_scale_slider(self):
 
     def test_legend(self):
         """Test legend behaviour."""
-        self.widget.cb_show_legend.setChecked(True)
+        w = self.widget
+        w.cb_show_legend.setChecked(True)
 
-        self.send_signal('Tree', self.titanic)
+        self.send_signal(w.Inputs.tree, self.titanic)
         self.assertIsInstance(self.widget.legend, OWDiscreteLegend)
         self.assertTrue(self.widget.legend.isVisible())
 
         # The legend should be invisible when regression coloring is none
-        self.send_signal('Tree', self.housing)
-        self.assertIsInstance(self.widget.legend, OWContinuousLegend)
-        self.assertFalse(self.widget.legend.isVisible())
+        self.send_signal(w.Inputs.tree, self.housing)
+        self.assertIsInstance(w.legend, OWContinuousLegend)
+        self.assertFalse(w.legend.isVisible())
 
         # The legend should appear when there is a coloring (2 is mean coloring)
         index = 2
-        simulate.combobox_activate_index(self.widget.target_class_combo, index)
-        self.assertIsInstance(self.widget.legend, OWContinuousLegend)
-        self.assertTrue(self.widget.legend.isVisible())
+        simulate.combobox_activate_index(w.target_class_combo, index)
+        self.assertIsInstance(w.legend, OWContinuousLegend)
+        self.assertTrue(w.legend.isVisible())
 
         # Check that switching back to a discrete target class works
-        self.send_signal('Tree', self.titanic)
-        self.assertIsInstance(self.widget.legend, OWDiscreteLegend)
-        self.assertTrue(self.widget.legend.isVisible())
+        self.send_signal(w.Inputs.tree, self.titanic)
+        self.assertIsInstance(w.legend, OWDiscreteLegend)
+        self.assertTrue(w.legend.isVisible())
 
     def test_checking_legend_checkbox_shows_and_hides_legend(self):
-        self.send_signal('Tree', self.titanic)
+        w = self.widget
+        self.send_signal(w.Inputs.tree, self.titanic)
         # Hide the legend
-        self.widget.cb_show_legend.setChecked(False)
-        self.assertFalse(self.widget.legend.isVisible(), 'Hiding legend failed')
+        w.cb_show_legend.setChecked(False)
+        self.assertFalse(w.legend.isVisible(), 'Hiding legend failed')
         # Show the legend
-        self.widget.cb_show_legend.setChecked(True)
-        self.assertTrue(self.widget.legend.isVisible(), 'Showing legend failed')
+        w.cb_show_legend.setChecked(True)
+        self.assertTrue(w.legend.isVisible(), 'Showing legend failed')
 
     def test_tooltip_changes_for_classification_target_class(self):
         """Tooltips should change when a target class is specified with a
         discrete target class."""
-        self.widget.cb_show_tooltips.setChecked(True)
-        self.send_signal('Tree', self.titanic)
+        w = self.widget
+        w.cb_show_tooltips.setChecked(True)
+        self.send_signal(w.Inputs.tree, self.titanic)
         tooltips = []
 
         def _callback():
             tooltips.append(self.get_visible_squares()[2].toolTip())
 
-        simulate.combobox_run_through_all(
-            self.widget.target_class_combo, callback=_callback)
+        simulate.combobox_run_through_all(w.target_class_combo, callback=_callback)
 
         self.assertFalse(self._check_all_same(tooltips))
 
     def test_checking_tooltip_shows_and_hides_tooltips(self):
-        self.send_signal('Tree', self.titanic)
+        w = self.widget
+        self.send_signal(w.Inputs.tree, self.titanic)
         square = self.get_squares()[0]
         # Hide tooltips
-        self.widget.cb_show_tooltips.setChecked(False)
+        w.cb_show_tooltips.setChecked(False)
         self.assertEqual(square.toolTip(), '', 'Hiding tooltips failed')
         # Show tooltips
-        self.widget.cb_show_tooltips.setChecked(True)
+        w.cb_show_tooltips.setChecked(True)
         self.assertNotEqual(square.toolTip(), '', 'Showing tooltips failed')
 
     def test_changing_max_depth_slider(self):
-        self.send_signal('Tree', self.titanic)
+        w = self.widget
+        self.send_signal(w.Inputs.tree, self.titanic)
 
-        max_depth = self.widget.tree_adapter.max_depth
+        max_depth = w.tree_adapter.max_depth
         num_squares_full = len(self.get_visible_squares())
-        self.assertEqual(self.widget.depth_limit, max_depth,
-                         'Full tree should be drawn initially')
+        self.assertEqual(w.depth_limit, max_depth, 'Full tree should be drawn initially')
 
         self.widget.depth_slider.setValue(max_depth - 1)
         num_squares_less = len(self.get_visible_squares())
         self.assertLess(num_squares_less, num_squares_full,
                         'Lowering tree depth limit did not hide squares')
 
-        self.widget.depth_slider.setValue(max_depth + 1)
+        w.depth_slider.setValue(max_depth + 1)
         self.assertGreater(len(self.get_visible_squares()), num_squares_less,
                            'Increasing tree depth limit did not show squares')
 
     def test_label_on_tree_connect_and_disconnect(self):
+        w = self.widget
         regex = r'Nodes:(.+)\s*Depth:(.+)'
         # Should contain no info by default
         self.assertNotRegex(
             self.widget.info.text(), regex,
             'Initial info should not contain node or depth info')
         # Test info label for tree
-        self.send_signal('Tree', self.titanic)
-        self.assertRegex(
-            self.widget.info.text(), regex,
-            'Valid tree does not update info')
+        self.send_signal(w.Inputs.tree, self.titanic)
+        self.assertRegex(w.info.text(), regex, 'Valid tree does not update info')
         # Remove tree from input
-        self.send_signal('Tree', None)
+        self.send_signal(w.Inputs.tree, None)
         self.assertNotRegex(
-            self.widget.info.text(), regex,
-            'Initial info should not contain node or depth info')
+            w.info.text(), regex, 'Initial info should not contain node or depth info')
 
     def test_tree_determinism(self):
         """Check that the tree is drawn identically upon receiving the same
@@ -313,7 +315,7 @@ def test_tree_determinism(self):
         # Make sure the tree are deterministic for iris
         scene_nodes = []
         for _ in range(n_tries):
-            self.send_signal(self.signal_name, self.signal_data)
+            self.send_signal(self.widget.Inputs.tree, self.signal_data)
             scene_nodes.append([n.pos() for n in self.get_visible_squares()])
         for node_row in zip(*scene_nodes):
             self.assertTrue(
@@ -330,7 +332,7 @@ def test_tree_determinism(self):
         data_same_entropy = TreeLearner()(data_same_entropy)
         scene_nodes = []
         for _ in range(n_tries):
-            self.send_signal(self.signal_name, data_same_entropy)
+            self.send_signal(self.widget.Inputs.tree, data_same_entropy)
             scene_nodes.append([n.pos() for n in self.get_visible_squares()])
         for node_row in zip(*scene_nodes):
             self.assertTrue(
@@ -342,14 +344,14 @@ def test_tree_determinism(self):
 
     def test_keep_colors_on_sizing_change(self):
         """The color should be the same after a full recompute of the tree."""
-        self.send_signal('Tree', self.titanic)
+        w = self.widget
+        self.send_signal(w.Inputs.tree, self.titanic)
         colors = []
 
         def _callback():
             colors.append([sq.brush().color() for sq in self.get_visible_squares()])
 
-        simulate.combobox_run_through_all(
-            self.widget.size_calc_combo, callback=_callback)
+        simulate.combobox_run_through_all(w.size_calc_combo, callback=_callback)
 
         # Check that individual squares all have the same color
         colors_same = [self._check_all_same(x) for x in zip(*colors)]
diff --git a/Orange/widgets/visualize/tests/test_owpythagoreanforest.py b/Orange/widgets/visualize/tests/test_owpythagoreanforest.py
--- a/Orange/widgets/visualize/tests/test_owpythagoreanforest.py
+++ b/Orange/widgets/visualize/tests/test_owpythagoreanforest.py
@@ -39,47 +39,48 @@ def get_grid_items(self):
                 if isinstance(x, GridItem)]
 
     def test_sending_rf_draws_trees(self):
+        w = self.widget
         # No trees by default
         self.assertEqual(len(self.get_tree_widgets()), 0,
                          'No trees should be drawn when no forest on input')
 
         # Draw trees for classification rf
-        self.send_signal('Random forest', self.titanic)
+        self.send_signal(w.Inputs.random_forest, self.titanic)
         self.assertEqual(len(self.get_tree_widgets()), 3,
                          'Incorrect number of trees when forest on input')
 
         # Clear trees when None
-        self.send_signal('Random forest', None)
+        self.send_signal(w.Inputs.random_forest, None)
         self.assertEqual(len(self.get_tree_widgets()), 0,
                          'Trees are cleared when forest is disconnected')
 
         # Draw trees for regression rf
-        self.send_signal('Random forest', self.housing)
+        self.send_signal(w.Inputs.random_forest, self.housing)
         self.assertEqual(len(self.get_tree_widgets()), 3,
                          'Incorrect number of trees when forest on input')
 
     def test_info_label(self):
+        w = self.widget
         regex = r'Trees:(.+)'
         # If no forest on input, display a message saying that
-        self.assertNotRegex(self.widget.ui_info.text(), regex,
+        self.assertNotRegex(w.ui_info.text(), regex,
                             'Initial info should not contain info on trees')
 
-        self.send_signal('Random forest', self.titanic)
-        self.assertRegex(self.widget.ui_info.text(), regex,
-                         'Valid RF does not update info')
+        self.send_signal(w.Inputs.random_forest, self.titanic)
+        self.assertRegex(self.widget.ui_info.text(), regex, 'Valid RF does not update info')
 
-        self.send_signal('Random forest', None)
-        self.assertNotRegex(self.widget.ui_info.text(), regex,
-                            'Removing RF does not clear info box')
+        self.send_signal(w.Inputs.random_forest, None)
+        self.assertNotRegex(w.ui_info.text(), regex, 'Removing RF does not clear info box')
 
     def test_depth_slider(self):
-        self.send_signal('Random forest', self.titanic)
+        w = self.widget
+        self.send_signal(w.Inputs.random_forest, self.titanic)
 
         trees = self.get_tree_widgets()
         for tree in trees:
             tree.set_depth_limit = Mock()
 
-        self.widget.ui_depth_slider.setValue(0)
+        w.ui_depth_slider.setValue(0)
         for tree in trees:
             tree.set_depth_limit.assert_called_once_with(0)
 
@@ -106,14 +107,14 @@ def _check_all_same(self, items):
 
     def test_changing_target_class_changes_coloring(self):
         """Changing the `Target class` combo box should update colors."""
+        w = self.widget
         def _test(data_type):
             colors, tree = [], self._pick_random_tree()
 
             def _callback():
                 colors.append([sq.brush().color() for sq in self._get_visible_squares(tree)])
 
-            simulate.combobox_run_through_all(
-                self.widget.ui_target_class_combo, callback=_callback)
+            simulate.combobox_run_through_all(w.ui_target_class_combo, callback=_callback)
 
             # Check that individual squares all have different colors
             squares_same = [self._check_all_same(x) for x in zip(*colors)]
@@ -121,24 +122,24 @@ def _callback():
             self.assertTrue(any(x is False for x in squares_same),
                             'Colors did not change for %s data' % data_type)
 
-        self.send_signal('Random forest', self.titanic)
+        self.send_signal(w.Inputs.random_forest, self.titanic)
         _test('classification')
-        self.send_signal('Random forest', self.housing)
+        self.send_signal(w.Inputs.random_forest, self.housing)
         _test('regression')
 
     def test_changing_size_adjustment_changes_sizes(self):
-        self.send_signal('Random forest', self.titanic)
+        w = self.widget
+        self.send_signal(w.Inputs.random_forest, self.titanic)
         squares = []
         # We have to get the same tree with an index on the grid items since
         # the tree objects are deleted and recreated with every invalidation
-        tree_index = self.widget.grid_items.index(random.choice(self.get_grid_items()))
+        tree_index = w.grid_items.index(random.choice(self.get_grid_items()))
 
         def _callback():
             squares.append([sq.rect() for sq in self._get_visible_squares(
                 self.get_tree_widgets()[tree_index])])
 
-        simulate.combobox_run_through_all(
-            self.widget.ui_size_calc_combo, callback=_callback)
+        simulate.combobox_run_through_all(w.ui_size_calc_combo, callback=_callback)
 
         # Check that individual squares are in different position
         squares_same = [self._check_all_same(x) for x in zip(*squares)]
@@ -146,9 +147,10 @@ def _callback():
         self.assertTrue(any(x is False for x in squares_same))
 
     def test_zoom(self):
-        self.send_signal('Random forest', self.titanic)
+        w = self.widget
+        self.send_signal(w.Inputs.random_forest, self.titanic)
 
-        grid_item, zoom = self.get_grid_items()[0], self.widget.zoom
+        grid_item, zoom = self.get_grid_items()[0], w.zoom
 
         def _destructure_rectf(r):
             return r.width(), r.height()
@@ -156,27 +158,27 @@ def _destructure_rectf(r):
         iw, ih = _destructure_rectf(grid_item.boundingRect())
 
         # Increase the size of grid item
-        self.widget.ui_zoom_slider.setValue(zoom + 1)
+        w.ui_zoom_slider.setValue(zoom + 1)
         lw, lh = _destructure_rectf(grid_item.boundingRect())
         self.assertTrue(iw < lw and ih < lh)
 
         # Decrease the size of grid item
-        self.widget.ui_zoom_slider.setValue(zoom - 1)
+        w.ui_zoom_slider.setValue(zoom - 1)
         lw, lh = _destructure_rectf(grid_item.boundingRect())
         self.assertTrue(iw > lw and ih > lh)
 
     def test_keep_colors_on_sizing_change(self):
         """The color should be the same after a full recompute of the tree."""
-        self.send_signal('Random forest', self.titanic)
+        w = self.widget
+        self.send_signal(w.Inputs.random_forest, self.titanic)
         colors = []
-        tree_index = self.widget.grid_items.index(random.choice(self.get_grid_items()))
+        tree_index = w.grid_items.index(random.choice(self.get_grid_items()))
 
         def _callback():
             colors.append([sq.brush().color() for sq in self._get_visible_squares(
                 self.get_tree_widgets()[tree_index])])
 
-        simulate.combobox_run_through_all(
-            self.widget.ui_size_calc_combo, callback=_callback)
+        simulate.combobox_run_through_all(w.ui_size_calc_combo, callback=_callback)
 
         # Check that individual squares all have the same color
         colors_same = [self._check_all_same(x) for x in zip(*colors)]
diff --git a/Orange/widgets/visualize/tests/test_owtreegraph.py b/Orange/widgets/visualize/tests/test_owtreegraph.py
--- a/Orange/widgets/visualize/tests/test_owtreegraph.py
+++ b/Orange/widgets/visualize/tests/test_owtreegraph.py
@@ -38,16 +38,18 @@ def _select_data(self):
 
     def test_target_class_changed(self):
         """Check if node content has changed after selecting target class"""
-        self.send_signal(self.signal_name, self.signal_data)
-        nodes = self.widget.scene.nodes()
+        w = self.widget
+        self.send_signal(w.Inputs.tree, self.signal_data)
+        nodes = w.scene.nodes()
         text = nodes[0].toPlainText()
-        self.widget.color_combo.activated.emit(1)
-        self.widget.color_combo.setCurrentIndex(1)
+        w.color_combo.activated.emit(1)
+        w.color_combo.setCurrentIndex(1)
         self.assertNotEqual(nodes[0].toPlainText(), text)
 
     def test_tree_determinism(self):
         """Check that the tree is drawn identically upon receiving the same
         dataset with no parameter changes."""
+        w = self.widget
         n_tries = 10
 
         def _check_all_same(data):
@@ -62,8 +64,8 @@ def _check_all_same(data):
         # Make sure the tree are deterministic for iris
         scene_nodes = []
         for _ in range(n_tries):
-            self.send_signal(self.signal_name, self.signal_data)
-            scene_nodes.append([n.pos() for n in self.widget.scene.nodes()])
+            self.send_signal(w.Inputs.tree, self.signal_data)
+            scene_nodes.append([n.pos() for n in w.scene.nodes()])
         for node_row in zip(*scene_nodes):
             self.assertTrue(
                 _check_all_same(node_row),
@@ -75,8 +77,8 @@ def _check_all_same(data):
         # the same entropy
         scene_nodes = []
         for _ in range(n_tries):
-            self.send_signal(self.signal_name, self.data_same_entropy)
-            scene_nodes.append([n.pos() for n in self.widget.scene.nodes()])
+            self.send_signal(w.Inputs.tree, self.data_same_entropy)
+            scene_nodes.append([n.pos() for n in w.scene.nodes()])
         for node_row in zip(*scene_nodes):
             self.assertTrue(
                 _check_all_same(node_row),