Repository: enthought/chaco

Issue:
Deal with _anytrait_changed handlers
**Problem Description**
In many cases in chaco we have `_anytrait_changed` methods which define a list of traits of actual interest, checks if the changed trait is in that list, and then calls a certain method.  For example:
https://github.com/enthought/chaco/blob/c758a65222a2caef9a3d2c074ae9dd5775a3a77e/chaco/axis.py#L781-L816

This is a case where we would want to use metadata in the trait definition and listen based on that.  Similar to the recently added `requires_redraw` (see PR #585 and issue #595)



Discussion:

Resolving patch:
diff --git a/chaco/axis.py b/chaco/axis.py
--- a/chaco/axis.py
+++ b/chaco/axis.py
@@ -33,6 +33,7 @@
     Enum,
     Callable,
     ArrayOrNone,
+    observe
 )
 
 # Local relative imports
@@ -778,42 +779,38 @@ def _title_changed(self):
         if self.component:
             self.component.invalidate_draw()
 
-    def _anytrait_changed(self, name, old, new):
-        """For every trait that defines a visual attribute
-        we just call _invalidate() when a change is made.
-        """
-        invalidate_traits = [
-            "title_font",
-            "title_spacing",
-            "title_color",
-            "title_angle",
-            "tick_weight",
-            "tick_color",
-            "tick_label_font",
-            "tick_label_color",
-            "tick_label_rotate_angle",
-            "tick_label_alignment",
-            "tick_label_margin",
-            "tick_label_offset",
-            "tick_label_position",
-            "tick_label_formatter",
-            "tick_in",
-            "tick_out",
-            "tick_visible",
-            "tick_interval",
-            "tick_generator",
-            "orientation",
-            "origin",
-            "axis_line_visible",
-            "axis_line_color",
-            "axis_line_weight",
-            "axis_line_style",
-            "small_haxis_style",
-            "ensure_labels_bounded",
-            "ensure_ticks_bounded",
-        ]
-        if name in invalidate_traits:
-            self._invalidate()
+    @observe([
+        "title_font",
+        "title_spacing",
+        "title_color",
+        "title_angle",
+        "tick_weight",
+        "tick_color",
+        "tick_label_font",
+        "tick_label_color",
+        "tick_label_rotate_angle",
+        "tick_label_alignment",
+        "tick_label_margin",
+        "tick_label_offset",
+        "tick_label_position",
+        "tick_label_formatter",
+        "tick_in",
+        "tick_out",
+        "tick_visible",
+        "tick_interval",
+        "tick_generator",
+        "orientation",
+        "origin",
+        "axis_line_visible",
+        "axis_line_color",
+        "axis_line_weight",
+        "axis_line_style",
+        "small_haxis_style",
+        "ensure_labels_bounded",
+        "ensure_ticks_bounded",
+    ])
+    def _invalidate_on_changed_visual_attr(self, event):
+        self._invalidate()
 
     # ------------------------------------------------------------------------
     # Initialization-related methods
diff --git a/chaco/legend.py b/chaco/legend.py
--- a/chaco/legend.py
+++ b/chaco/legend.py
@@ -19,6 +19,7 @@
     Instance,
     Int,
     List,
+    observe,
     Str,
 )
 
@@ -487,29 +488,27 @@ def _composite_icon_renderer_default(self):
         return CompositeIconRenderer()
 
     # -- trait handlers --------------------------------------------------------
-    def _anytrait_changed(self, name, old, new):
-        if name in (
-            "font",
-            "border_padding",
-            "padding",
-            "line_spacing",
-            "icon_bounds",
-            "icon_spacing",
-            "labels",
-            "plots",
-            "plots_items",
-            "labels_items",
-            "border_width",
-            "align",
-            "position",
-            "position_items",
-            "bounds",
-            "bounds_items",
-            "label_at_top",
-        ):
-            self._layout_needed = True
-        if name == "color":
-            self.get_preferred_size()
+    @observe([
+        "font",
+        "border_padding",
+        "padding",
+        "line_spacing",
+        "icon_bounds",
+        "icon_spacing",
+        "labels.items",
+        "plots.items",
+        "border_width",
+        "align",
+        "position.items",
+        "bounds.items",
+        "title_at_top",
+    ])
+    def _invalidate_existing_layout(self, event):
+        self._layout_needed = True
+    
+    @observe("color")
+    def _update_caches(self, event):
+        self.get_preferred_size()
 
     def _plots_changed(self):
         """Invalidate the caches."""
diff --git a/examples/demo/advanced/scalar_image_function_inspector.py b/examples/demo/advanced/scalar_image_function_inspector.py
--- a/examples/demo/advanced/scalar_image_function_inspector.py
+++ b/examples/demo/advanced/scalar_image_function_inspector.py
@@ -154,17 +154,11 @@ def compute_model(self):
             self.model_changed = True
             self._function = self.function
 
-    def _anytrait_changed(self, name, value):
-        if name in [
-            "function",
-            "npts_x",
-            "npts_y",
-            "min_x",
-            "max_x",
-            "min_y",
-            "max_y",
-        ]:
-            self.compute_model()
+    @observe(
+        ["function", "npts_x", "npts_y", "min_x", "max_x", "min_y", "max_y"]
+    )
+    def _compute_model_on_trait_update(self, event):
+        self.compute_model()
 
 
 class PlotUI(HasTraits):


Test patch: