Disable pan/zoom when interacting with draggable artists

I have a plot to which I add images that can be dragged by adding event handlers for button press, release and motion events

My issue is that if Zoom or Pan are selected in the toolbar, then the Zoom/Pan callbacks and the drag callbacks are executed when attempting to drag the image . The result is a confusing interaction for the end user.

You can see this effect by executing the draggable artist demonstration from here matplotlib-draggable-plot/draggable_plot.py at master · yuma-m/matplotlib-draggable-plot · GitHub (Not my code). Click on the plot to create a point, turn on Zoom or Pan and then try to drag the point around. I am not using draggable() but the effect is the same and this is an easy way to show the issue.

I have considered two approaches to solving this. First, attempting to disable the Zoom/Pan function in the NavBar when the draggable artist is selected. Second, preventing the event mouse events from propagating to the NavBar by adding a processed attribute or similar.

I have not found an effective way of temporarily disabling the Navbar although I have tried various combinations of grabbing the widgetlock and changing the NavBar mode, none of my attempts have been successful.

The issue with preventing propagation is that the Navbar callbacks are called first because they were added with mpl_connect first.

Has anyone got a working solution to this issue or a recommended path forward?

Thanks

It’s a little bit tricky where you have to put the locks and releases but I think the following should work (I’ve marked added lines with # NEW):

in _on_click

        if event.button == 1 and event.inaxes in [self._axes]:
            if not self._figure.canvas.widgetlock.available(self): # NEW
                return # NEW
            point = self._find_neighbor_point(event)
            if point:
                self._dragging_point = point
                self._figure.canvas.widgetlock(self) # NEW

add the same return to _on_motion

        if not self._figure.canvas.widgetlock.available(self): # NEW
            return # NEW

and finally make sure to release the lock when you are done with it by modifying _on_release as follows:

        if event.button == 1 and event.inaxes in [self._axes] and self._dragging_point:
            self._dragging_point = None
            self._update_plot()
            self._figure.canvas.widgetlock.release(self) #NEW

with these changes i can use both the zoom and pan buttons as expected without influencing the points, and when they are not activated I can click and drag poitns as expected.

If you want zooming indepdent of clicking (by scrolling) then you might consider using mpl-pan-zoom — mpl-pan-zoom

Thank you @ianhi. I have made the changes you propose but it does not work for me. Perhaps another change that you made that is missing?

In the meantime I have worked out a different approach, which I will post below. If I can get your working it is probably cleaner, but at least I have a working solution for now.

As referenced above, I have found a working solution by subclassing the NavBar as follows:

class NavigationToolbar(NavigationToolbar2Tk):

    def enable(self):
        self.enabled = True
        
    def disable(self):
        self.enabled = False

    #Only pass on events when enabled and delay requests to Navbar so that the graphics overlays can handle them first
    def _zoom_pan_handler(self, event):
        event.requeued = False if not hasattr(event, 'requeued') else True
        if not hasattr(self, 'enabled'):
            self.enabled = True
        if not self.enabled:
            return
        if event.name == 'button_press_event' and not event.requeued:
            event.requeued = True
            self.after(100, self._zoom_pan_handler, event)
        else:
            super()._zoom_pan_handler(event)

This works by taking click events received by the navbar and requeueing the using tk.after. This gives over artists the chance to handle the event first. In turn, those artists can enable or disable the navbar processing in their own event handling.

I actually think a better solution all around would be if mpl_connect accepted a priority parameter and Events had a propagate attribute. Callbacks would then be called in priority order. and any handler could set propagate to False on an event to prevent additional handlers from being called.

maybe you missed adding the widgetlock here? that code works for me with matplotlib version 3.7.1

No. I had that code. Not working for me on 3.8.2