Search code examples
pythonenthoughttraitsuichaco

Can I force my enable Container to redraw from a traitsui Handler?


I have employed a traitsui.api.Handler to catch and handle events for a traitsui.api.View, that view includes a button, whose behavior is to remove a plot from a container containing multiple plots. The container's components list is accessed when the remove button is used, the pop() method is called, and the plot is removed. However, the view does not redraw, and so that plot appears to remain in place. Resizing with window through dragging a corner will force the redraw, confirming the pop()

The question is: How can I force the redraw programmatically ?

It seems to me that the right place to do so would be in the Handler's setattr method, just after pop()-ing the plot.

# Major library imports

from numpy import linspace
from scipy.special import jn

# Enthought library imports
from enable.api import Container, ComponentEditor
from traits.api import HasTraits, Instance, Button, Int, Str
from traitsui.api import Item, HGroup, View, VSplit, UItem, InstanceEditor, Handler

# Chaco imports
from chaco.api import ArrayPlotData, GridContainer, Plot

# ===============================================================================
# Attributes to use for the plot view.
size = (1000, 800)

COLOR_PALETTE = [
    (0.65098039, 0.80784314, 0.89019608, 1.0),
    (0.12156863, 0.47058824, 0.70588235, 1.0),
    (0.69803922, 0.8745098, 0.54117647, 1.0),
    (0.2, 0.62745098, 0.17254902, 1.0),
    (0.98431373, 0.60392157, 0.6, 1.0),
    (0.89019608, 0.10196078, 0.10980392, 1.0),
    (0.99215686, 0.74901961, 0.43529412, 1.0),
    (1., 0.49803922, 0., 1.0),
    (0.79215686, 0.69803922, 0.83921569, 1.0),
]


class InstanceUItem(UItem):
    """Convenience class for including an Instance in a View"""
    style = Str('custom')
    editor = Instance(InstanceEditor, ())



# ===============================================================================
# # ManagerHandler will be the View's handler
#===============================================================================
class ManagerHandler(Handler):
    def setattr(self, info, object, name, value):
        Handler.setattr(self, info, object, name, value)
        info.ui.context['pgrid'].plots_container.components.pop()
        #At this point, the container does not redraw, and so, while it no longer
        #contains the last plot in its components collection, that plot is still
        # visible

# ===============================================================================
# # PlotsGrid class that is used by the demo
#===============================================================================
class PlotsGrid(HasTraits):
    plots_container = Instance(Container)
    rows = Int(3)
    cols = Int(3)

    #===============================================================================
    # # Create the plots, this is adapted from the chaco GridContainer demo
    #===============================================================================
    def _plots_container_default(self):
        # Create a GridContainer to hold all of our plots
        container = GridContainer(padding=20, fill_padding=True,
                                  bgcolor="lightgray", use_backbuffer=True,
                                  shape=(self.rows, self.cols), spacing=(20, 20))

        # Create the initial series of data
        x = linspace(-5, 15.0, 100)
        pd = ArrayPlotData(index=x)

        # Plot some bessel functions and add the plots to our container
        for i in range(self.rows * self.cols):
            pd.set_data("y" + str(i), jn(i, x))
            plot = Plot(pd)
            plot.plot(("index", "y" + str(i)),
                      color=tuple(COLOR_PALETTE[i]), line_width=2.0,
                      bgcolor="white", border_visible=True)

            container.add(plot)
        return container



# ===============================================================================
# # Controls HasTraits provides a button used to wire in the desired behavior
#===============================================================================
class Controls(HasTraits):
    rem_plot = Button("remove ...")

    def _rem_plot_changed(self):
        print "rem plot changed"



# ===============================================================================
# # manager_view provides the View and defines its layout
#===============================================================================
manager_view = View(
    VSplit(
        HGroup(
            Item('controls.rem_plot', height=32)
        ),
        Item('pgrid.plots_container', editor=ComponentEditor(size=size), show_label=False),
        show_border=True
    ),
    handler=ManagerHandler(),
    resizable=True
)

grid = PlotsGrid()
ctrl = Controls()

if __name__ == "__main__":
    ctrl.configure_traits(view=manager_view, context={'pgrid': grid, 'controls': ctrl})

Solution

  • The simplest way to get this working is to call invalidate_and_redraw on your plot container after popping the plot. In that case, you could modify your call to pop to look something like:

    plots_container = info.ui.context['pgrid'].plots_container
    plots_container.components.pop()
    plots_container.invalidate_and_redraw()
    

    Longer discussion:

    Ideally, this would be handled by Chaco. Part of the problem is that the container wasn't designed to have the components list modified directly. Instead, (I'm guessing) the intention was to have the user call plots_container.remove on an item in the components list.

    That said, that doesn't seem to work either. It turns out that remove invalidates the current state, but doesn't request a redraw. (My guess is that chaco/enable doesn't automatically redraw since there may be many cache-invalidating operations in a row; e.g. if you were to remove all plots from this container, you'd only want to redraw after all the calls to remove, not after each one.)

    So to actually use this alternative method, you would write something like:

    plot = plots_container.components[-1]
    plots_container.remove(plot)
    plots_container.request_redraw()