From dda0cdc8f6b41078a3c205507f8f56a5d017300c Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 23 Jan 2014 13:35:40 -0500 Subject: [PATCH 01/41] navigation and toolbar coexistence --- lib/matplotlib/backend_bases.py | 371 +++++++++++++++++++++++- lib/matplotlib/backends/backend_gtk3.py | 212 +++++++++++++- lib/matplotlib/rcsetup.py | 2 +- 3 files changed, 575 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index c7a10904f37e..e334151d856e 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -46,6 +46,7 @@ import matplotlib.widgets as widgets #import matplotlib.path as path from matplotlib import rcParams +from matplotlib.rcsetup import validate_stringlist from matplotlib import is_interactive from matplotlib import get_backend from matplotlib._pylab_helpers import Gcf @@ -56,6 +57,7 @@ import matplotlib.textpath as textpath from matplotlib.path import Path from matplotlib.cbook import mplDeprecation +import matplotlib.backend_tools as tools try: from importlib import import_module @@ -2531,8 +2533,10 @@ def __init__(self, canvas, num): canvas.manager = self # store a pointer to parent self.num = num - self.key_press_handler_id = self.canvas.mpl_connect('key_press_event', - self.key_press) + if rcParams['toolbar'] != 'navigation': + self.key_press_handler_id = self.canvas.mpl_connect( + 'key_press_event', + self.key_press) """ The returned id from connecting the default key handler via :meth:`FigureCanvasBase.mpl_connnect`. @@ -2591,10 +2595,7 @@ def set_window_title(self, title): pass -class Cursors: - # this class is only used as a simple namespace - HAND, POINTER, SELECT_REGION, MOVE = list(range(4)) -cursors = Cursors() +cursors = tools.cursors class NavigationToolbar2(object): @@ -3171,3 +3172,361 @@ def zoom(self, *args): def set_history_buttons(self): """Enable or disable back/forward button""" pass + + +class NavigationBase(object): + _default_cursor = cursors.POINTER + _default_tools = [tools.ToolToggleGrid, + tools.ToolToggleFullScreen, + tools.ToolQuit, + tools.ToolEnableAllNavigation, + tools.ToolEnableNavigation, + tools.ToolToggleXScale, + tools.ToolToggleYScale, + tools.ToolHome, tools.ToolBack, + tools.ToolForward, + tools.ToolZoom, + tools.ToolPan, + 'ConfigureSubplots', + 'SaveFigure'] + + def __init__(self, canvas, toolbar=None): + self.canvas = canvas + self.toolbar = self._get_toolbar(toolbar, canvas) + + self._key_press_handler_id = self.canvas.mpl_connect('key_press_event', + self._key_press) + + self._idDrag = self.canvas.mpl_connect('motion_notify_event', + self._mouse_move) + + self._idPress = self.canvas.mpl_connect('button_press_event', + self._press) + self._idRelease = self.canvas.mpl_connect('button_release_event', + self._release) + + # a dict from axes index to a list of view limits + self.views = cbook.Stack() + self.positions = cbook.Stack() # stack of subplot positions + + self._tools = {} + self._keys = {} + self._instances = {} + self._toggled = None + + #to communicate with tools and redirect events + self.keypresslock = widgets.LockDraw() + self.movelock = widgets.LockDraw() + self.presslock = widgets.LockDraw() + self.releaselock = widgets.LockDraw() + #just to group all the locks in one place + self.canvaslock = self.canvas.widgetlock + + for tool in self._default_tools: + self.add_tool(tool) + + self._last_cursor = self._default_cursor + + def _get_toolbar(self, toolbar, canvas): + # must be inited after the window, drawingArea and figure + # attrs are set + if rcParams['toolbar'] == 'navigation' and toolbar is not None: + toolbar = toolbar(canvas.manager) + else: + toolbar = None + return toolbar + + #remove persistent instances + def unregister(self, name): + if self._toggled == name: + self._handle_toggle(name, from_toolbar=False) + if name in self._instances: + del self._instances[name] + + def remove_tool(self, name): + self.unregister(name) + del self._tools[name] + keys = [k for k, v in self._keys.items() if v == name] + for k in keys: + del self._keys[k] + + if self.toolbar: + self.toolbar.remove_toolitem(name) + + def add_tool(self, callback_class): + tool = self._get_cls_to_instantiate(callback_class) + name = tool.name + if name is None: + warnings.warn('Tools need a name to be added, it is used as ID') + return + if name in self._tools: + warnings.warn('A tool with the same name already exist, not added') + + return + + self._tools[name] = tool + if tool.keymap is not None: + for k in validate_stringlist(tool.keymap): + self._keys[k] = name + + if self.toolbar and tool.position is not None: + basedir = os.path.join(rcParams['datapath'], 'images') + if tool.image is not None: + fname = os.path.join(basedir, tool.image + '.png') + else: + fname = None + self.toolbar.add_toolitem(name, tool.description, + fname, + tool.position, + tool.toggle) + + def _get_cls_to_instantiate(self, callback_class): + if isinstance(callback_class, basestring): + #FIXME: make more complete searching structure + if callback_class in globals(): + return globals()[callback_class] + + mod = self.__class__.__module__ + current_module = __import__(mod, + globals(), locals(), [mod], 0) + + return getattr(current_module, callback_class, False) + + return callback_class + + def _key_press(self, event): + if event.key is None: + return + + #some tools may need to capture keypress, but they need to be toggle + if self._toggled: + instance = self._get_instance(self._toggled) + if self.keypresslock.isowner(instance): + instance.key_press(event) + return + + name = self._keys.get(event.key, None) + if name is None: + return + + tool = self._tools[name] + if tool.toggle: + self._handle_toggle(name, event=event) + elif tool.persistent: + instance = self._get_instance(name) + instance.activate(event) + else: + #Non persistent tools, are + #instantiated and forgotten (reminds me an exgirlfriend?) + tool(self.canvas.figure, event) + + def _get_instance(self, name): + if name not in self._instances: + instance = self._tools[name](self.canvas.figure) + #register instance + self._instances[name] = instance + + return self._instances[name] + + def toolbar_callback(self, name): + tool = self._tools[name] + if tool.toggle: + self._handle_toggle(name, from_toolbar=True) + elif tool.persistent: + instance = self._get_instance(name) + instance.activate(None) + else: + tool(self.canvas.figure, None) + + def _handle_toggle(self, name, event=None, from_toolbar=False): + #toggle toolbar without callback + if not from_toolbar and self.toolbar: + self.toolbar.toggle(name, False) + + instance = self._get_instance(name) + if self._toggled is None: + instance.activate(None) + self._toggled = name + + elif self._toggled == name: + instance.deactivate(None) + self._toggled = None + + else: + if self.toolbar: + self.toolbar.toggle(self._toggled, False) + + self._get_instance(self._toggled).deactivate(None) + instance.activate(None) + self._toggled = name + + for a in self.canvas.figure.get_axes(): + a.set_navigate_mode(self._toggled) + + def list_tools(self): + print ('_' * 80) + print ("{0:20} {1:50} {2}".format('Name (id)', 'Tool description', + 'Keymap')) + print ('_' * 80) + for name in sorted(self._tools.keys()): + tool = self._tools[name] + keys = [k for k, i in self._keys.items() if i == name] + print ("{0:20} {1:50} {2}".format(tool.name, tool.description, + ', '.join(keys))) + print ('_' * 80, '\n') + + def update(self): + """Reset the axes stack""" + self.views.clear() + self.positions.clear() +# self.set_history_buttons() + + def _mouse_move(self, event): + if self._toggled: + instance = self._instances[self._toggled] + if self.movelock.isowner(instance): + instance.mouse_move(event) + return + + if not event.inaxes or not self._toggled: + if self._last_cursor != self._default_cursor: + self.set_cursor(self._default_cursor) + self._last_cursor = self._default_cursor + else: + if self._toggled: + cursor = self._instances[self._toggled].cursor + if cursor and self._last_cursor != cursor: + self.set_cursor(cursor) + self._last_cursor = cursor + + if self.toolbar is None: + return + + if event.inaxes and event.inaxes.get_navigate(): + + try: + s = event.inaxes.format_coord(event.xdata, event.ydata) + except (ValueError, OverflowError): + pass + else: + if self._toggled: + self.toolbar.set_message('%s, %s' % (self._toggled, s)) + else: + self.toolbar.set_message(s) + else: + self.toolbar.set_message('') + + def _release(self, event): + if self._toggled: + instance = self._instances[self._toggled] + if self.releaselock.isowner(instance): + instance.release(event) + return + self.release(event) + + def release(self, event): + pass + + def _press(self, event): + """Called whenver a mouse button is pressed.""" + if self._toggled: + instance = self._instances[self._toggled] + if self.presslock.isowner(instance): + instance.press(event) + return + self.press(event) + + def press(self, event): + """Called whenver a mouse button is pressed.""" + pass + + def draw(self): + """Redraw the canvases, update the locators""" + for a in self.canvas.figure.get_axes(): + xaxis = getattr(a, 'xaxis', None) + yaxis = getattr(a, 'yaxis', None) + locators = [] + if xaxis is not None: + locators.append(xaxis.get_major_locator()) + locators.append(xaxis.get_minor_locator()) + if yaxis is not None: + locators.append(yaxis.get_major_locator()) + locators.append(yaxis.get_minor_locator()) + + for loc in locators: + loc.refresh() + self.canvas.draw_idle() + + def dynamic_update(self): + pass + + def set_cursor(self, cursor): + """ + Set the current cursor to one of the :class:`Cursors` + enums values + """ + pass + + def update_view(self): + """Update the viewlim and position from the view and + position stack for each axes + """ + + lims = self.views() + if lims is None: + return + pos = self.positions() + if pos is None: + return + for i, a in enumerate(self.canvas.figure.get_axes()): + xmin, xmax, ymin, ymax = lims[i] + a.set_xlim((xmin, xmax)) + a.set_ylim((ymin, ymax)) + # Restore both the original and modified positions + a.set_position(pos[i][0], 'original') + a.set_position(pos[i][1], 'active') + + self.canvas.draw_idle() + + def push_current(self): + """push the current view limits and position onto the stack""" + lims = [] + pos = [] + for a in self.canvas.figure.get_axes(): + xmin, xmax = a.get_xlim() + ymin, ymax = a.get_ylim() + lims.append((xmin, xmax, ymin, ymax)) + # Store both the original and modified positions + pos.append(( + a.get_position(True).frozen(), + a.get_position().frozen())) + self.views.push(lims) + self.positions.push(pos) +# self.set_history_buttons() + + def draw_rubberband(self, event, x0, y0, x1, y1): + """Draw a rectangle rubberband to indicate zoom limits""" + pass + + +class ToolbarBase(object): + def __init__(self, manager): + self.manager = manager + + def add_toolitem(self, name, description, image_file, position, + toggle): + raise NotImplementedError + + def add_separator(self, pos): + pass + + def set_message(self, s): + """Display a message on toolbar or in status bar""" + pass + + def toggle(self, name, callback=False): + #carefull, callback means to perform or not the callback while toggling + raise NotImplementedError + + def remove_toolitem(self, name): + pass diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index eab6564a2667..2532b870742d 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -30,7 +30,8 @@ def fn_name(): return sys._getframe(1).f_code.co_name from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \ FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase -from matplotlib.backend_bases import ShowBase +from matplotlib.backend_bases import ShowBase, ToolbarBase, NavigationBase +from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase from matplotlib.cbook import is_string_like, is_writable_file_like from matplotlib.colors import colorConverter @@ -411,7 +412,7 @@ def __init__(self, canvas, num): self.canvas.show() self.vbox.pack_start(self.canvas, True, True, 0) - + self.navigation = None self.toolbar = self._get_toolbar(canvas) # calculate size for window @@ -435,7 +436,9 @@ def destroy(*args): def notify_axes_change(fig): 'this will be called whenever the current axes is changed' - if self.toolbar is not None: self.toolbar.update() + if self.navigation is not None: + self.navigation.update() + elif self.toolbar is not None: self.toolbar.update() self.canvas.figure.add_axobserver(notify_axes_change) self.canvas.grab_focus() @@ -471,7 +474,11 @@ def _get_toolbar(self, canvas): # attrs are set if rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2GTK3 (canvas, self.window) + elif rcParams['toolbar'] == 'navigation': + self.navigation = NavigationGTK3(canvas, ToolbarGTK3) + toolbar = self.navigation.toolbar else: + self.navigation = NavigationGTK3(canvas, None) toolbar = None return toolbar @@ -699,6 +706,205 @@ def get_filename_from_user (self): return filename, self.ext +class NavigationGTK3(NavigationBase): + def __init__(self, *args, **kwargs): + NavigationBase.__init__(self, *args, **kwargs) + self.ctx = None + + def set_cursor(self, cursor): + self.canvas.get_property("window").set_cursor(cursord[cursor]) + + def draw_rubberband(self, event, x0, y0, x1, y1): + #'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/ + #Recipe/189744' + self.ctx = self.canvas.get_property("window").cairo_create() + + # todo: instead of redrawing the entire figure, copy the part of + # the figure that was covered by the previous rubberband rectangle + self.canvas.draw() + + height = self.canvas.figure.bbox.height + y1 = height - y1 + y0 = height - y0 + w = abs(x1 - x0) + h = abs(y1 - y0) + rect = [int(val) for val in (min(x0, x1), min(y0, y1), w, h)] + + self.ctx.new_path() + self.ctx.set_line_width(0.5) + self.ctx.rectangle(rect[0], rect[1], rect[2], rect[3]) + self.ctx.set_source_rgb(0, 0, 0) + self.ctx.stroke() + + def dynamic_update(self): + # legacy method; new method is canvas.draw_idle + self.canvas.draw_idle() + +# def release(self, event): +# try: del self._pixmapBack +# except AttributeError: pass + + +class ToolbarGTK3(ToolbarBase, Gtk.Box,): + def __init__(self, manager): + ToolbarBase.__init__(self, manager) + Gtk.Box.__init__(self) + self.set_property("orientation", Gtk.Orientation.VERTICAL) + + self._toolbar = Gtk.Toolbar() + self._toolbar.set_style(Gtk.ToolbarStyle.ICONS) + self.pack_start(self._toolbar, False, False, 0) + self._toolbar.show_all() + self._toolitems = {} + self._signals = {} + self._add_message() + + def _add_message(self): + box = Gtk.Box() + box.set_property("orientation", Gtk.Orientation.HORIZONTAL) + sep = Gtk.Separator() + sep.set_property("orientation", Gtk.Orientation.VERTICAL) + box.pack_start(sep, False, True, 0) + self.message = Gtk.Label() + box.pack_end(self.message, False, False, 0) + self.pack_end(box, False, False, 5) + box.show_all() + + sep = Gtk.Separator() + sep.set_property("orientation", Gtk.Orientation.HORIZONTAL) + self.pack_end(sep, False, True, 0) + sep.show_all() + + def add_toolitem(self, name, tooltip_text, image_file, position, + toggle): + if toggle: + tbutton = Gtk.ToggleToolButton() + else: + tbutton = Gtk.ToolButton() + tbutton.set_label(name) + + if image_file is not None: + image = Gtk.Image() + image.set_from_file(image_file) + tbutton.set_icon_widget(image) + + self._toolbar.insert(tbutton, position) + signal = tbutton.connect('clicked', self._call_tool, name) + tbutton.set_tooltip_text(tooltip_text) + tbutton.show_all() + self._toolitems[name] = tbutton + self._signals[name] = signal + + def _call_tool(self, btn, name): + self.manager.navigation.toolbar_callback(name) + + def set_message(self, s): + self.message.set_label(s) + + def toggle(self, name, callback=False): + if name not in self._toolitems: + # TODO: raise a warning + print('Not in toolbar', name) + return + + status = self._toolitems[name].get_active() + if not callback: + self._toolitems[name].handler_block(self._signals[name]) + + self._toolitems[name].set_active(not status) + + if not callback: + self._toolitems[name].handler_unblock(self._signals[name]) + + def remove_toolitem(self, name): + if name not in self._toolitems: + #TODO: raise warning + print('Not in toolbar', name) + return + self._toolbar.remove(self._toolitems[name]) + del self._toolitems[name] + + +class SaveFigureGTK3(SaveFigureBase): + + def get_filechooser(self): + fc = FileChooserDialog( + title='Save the figure', + parent=self.figure.canvas.manager.window, + path=os.path.expanduser(rcParams.get('savefig.directory', '')), + filetypes=self.figure.canvas.get_supported_filetypes(), + default_filetype=self.figure.canvas.get_default_filetype()) + fc.set_current_name(self.figure.canvas.get_default_filename()) + return fc + + def activate(self, *args): + chooser = self.get_filechooser() + fname, format_ = chooser.get_filename_from_user() + chooser.destroy() + if fname: + startpath = os.path.expanduser( + rcParams.get('savefig.directory', '')) + if startpath == '': + # explicitly missing key or empty str signals to use cwd + rcParams['savefig.directory'] = startpath + else: + # save dir for next time + rcParams['savefig.directory'] = os.path.dirname( + six.text_type(fname)) + try: + self.figure.canvas.print_figure(fname, format=format_) + except Exception as e: + error_msg_gtk(str(e), parent=self) + +SaveFigure = SaveFigureGTK3 + + +class ConfigureSubplotsGTK3(ConfigureSubplotsBase, Gtk.Window): + def __init__(self, *args, **kwargs): + ConfigureSubplotsBase.__init__(self, *args, **kwargs) + Gtk.Window.__init__(self) + + try: + self.window.set_icon_from_file(window_icon) + except (SystemExit, KeyboardInterrupt): + # re-raise exit type Exceptions + raise + except: + # we presumably already logged a message on the + # failure of the main plot, don't keep reporting + pass + self.set_title("Subplot Configuration Tool") + self.vbox = Gtk.Box() + self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) + self.add(self.vbox) + self.vbox.show() + self.connect('destroy', self.unregister) + + toolfig = Figure(figsize=(6, 3)) + canvas = self.figure.canvas.__class__(toolfig) + + toolfig.subplots_adjust(top=0.9) + SubplotTool(self.figure, toolfig) + + w = int(toolfig.bbox.width) + h = int(toolfig.bbox.height) + + self.set_default_size(w, h) + + canvas.show() + self.vbox.pack_start(canvas, True, True, 0) + self.show() + + def _get_canvas(self, fig): + return self.canvas.__class__(fig) + + def activate(self, event): + self.present() + + +ConfigureSubplots = ConfigureSubplotsGTK3 + + class DialogLineprops: """ A GUI dialog for controlling lineprops diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index bb228d7d2de3..5e6af2a788d2 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -154,7 +154,7 @@ def validate_backend(s): def validate_toolbar(s): validator = ValidateInStrings( 'toolbar', - ['None', 'toolbar2'], + ['None', 'toolbar2', 'navigation'], ignorecase=True) return validator(s) From 10f5dc7a815c602568644820074f7ac683a68eb1 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 23 Jan 2014 15:46:41 -0500 Subject: [PATCH 02/41] mod keypress in figuremanager --- lib/matplotlib/backend_bases.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index e334151d856e..500870020233 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2572,7 +2572,8 @@ def key_press(self, event): Implement the default mpl key bindings defined at :ref:`key-event-handling` """ - key_press_handler(event, self.canvas, self.canvas.toolbar) + if rcParams['toolbar'] != 'navigation': + key_press_handler(event, self.canvas, self.canvas.toolbar) def show_popup(self, msg): """ From 08a6020e4e1356886a83a81691beca1a39f3b2d2 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 23 Jan 2014 15:51:06 -0500 Subject: [PATCH 03/41] extra files --- examples/user_interfaces/navigation.py | 52 +++ lib/matplotlib/backend_tools.py | 527 +++++++++++++++++++++++++ 2 files changed, 579 insertions(+) create mode 100644 examples/user_interfaces/navigation.py create mode 100644 lib/matplotlib/backend_tools.py diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py new file mode 100644 index 000000000000..dd2ab12bbb61 --- /dev/null +++ b/examples/user_interfaces/navigation.py @@ -0,0 +1,52 @@ +import matplotlib +matplotlib.use('GTK3Cairo') +matplotlib.rcParams['toolbar'] = 'navigation' +import matplotlib.pyplot as plt +from matplotlib.backend_tools import ToolBase + + +#Create a simple tool to list all the tools +class ListTools(ToolBase): + #keyboard shortcut + keymap = 'm' + #Name used as id, must be unique between tools of the same navigation + name = 'List' + description = 'List Tools' + #Where to put it in the toolbar, -1 = at the end, None = Not in toolbar + position = -1 + + def activate(self, event): + #The most important attributes are navigation and figure + self.navigation.list_tools() + + +#A simple example of copy canvas +#ref: at https://github.com/matplotlib/matplotlib/issues/1987 +class CopyTool(ToolBase): + keymap = 'ctrl+c' + name = 'Copy' + description = 'Copy canvas' + position = -1 + + def activate(self, event): + from gi.repository import Gtk, Gdk, GdkPixbuf + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + window = self.figure.canvas.get_window() + x, y, width, height = window.get_geometry() + pb = Gdk.pixbuf_get_from_window(window, x, y, width, height) + clipboard.set_image(pb) + + +fig = plt.figure() +plt.plot([1, 2, 3]) + +#If we are in the old toolbar, don't try to modify it +if matplotlib.rcParams['toolbar'] in ('navigation', 'None'): + ##Add the custom tools that we created + fig.canvas.manager.navigation.add_tool(ListTools) + fig.canvas.manager.navigation.add_tool(CopyTool) + + ##Just for fun, lets remove the back button + fig.canvas.manager.navigation.remove_tool('Back') + +plt.show() diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py new file mode 100644 index 000000000000..8535d8d87a21 --- /dev/null +++ b/lib/matplotlib/backend_tools.py @@ -0,0 +1,527 @@ +from matplotlib import rcParams +from matplotlib._pylab_helpers import Gcf +import numpy as np + + +class Cursors: + # this class is only used as a simple namespace + HAND, POINTER, SELECT_REGION, MOVE = list(range(4)) +cursors = Cursors() + + +class ToolBase(object): + keymap = None + position = None + description = None + name = None + image = None + toggle = False # Change the status (take control of the events) + persistent = False + cursor = None + + def __init__(self, figure, event=None): + self.figure = figure + self.navigation = figure.canvas.manager.navigation + self.activate(event) + + def activate(self, event): + pass + + +class ToolPersistentBase(ToolBase): + persistent = True + + def __init__(self, figure, event=None): + self.figure = figure + self.navigation = figure.canvas.manager.navigation + #persistent tools don't call activate a at instantiation + + def unregister(self, *args): + #call this to unregister from navigation + self.navigation.unregister(self.name) + + +class ToolToggleBase(ToolPersistentBase): + toggle = True + + def mouse_move(self, event): + pass + + def press(self, event): + pass + + def release(self, event): + pass + + def deactivate(self, event=None): + pass + + def key_press(self, event): + pass + + +class ToolQuit(ToolBase): + name = 'Quit' + description = 'Quit the figure' + keymap = rcParams['keymap.quit'] + + def activate(self, event): + Gcf.destroy_fig(self.figure) + + +class ToolEnableAllNavigation(ToolBase): + name = 'EnableAll' + description = 'Enables all axes navigation' + keymap = rcParams['keymap.all_axes'] + + def activate(self, event): + if event.inaxes is None: + return + + for a in self.figure.get_axes(): + if event.x is not None and event.y is not None \ + and a.in_axes(event): + a.set_navigate(True) + + +#FIXME: use a function instead of string for enable navigation +class ToolEnableNavigation(ToolBase): + name = 'EnableOne' + description = 'Enables one axes navigation' + keymap = range(1, 5) + + def activate(self, event): + if event.inaxes is None: + return + + n = int(event.key) - 1 + for i, a in enumerate(self.figure.get_axes()): + # consider axes, in which the event was raised + # FIXME: Why only this axes? + if event.x is not None and event.y is not None \ + and a.in_axes(event): + a.set_navigate(i == n) + + +class ToolToggleGrid(ToolBase): + name = 'Grid' + description = 'Toogle Grid' + keymap = rcParams['keymap.grid'] + + def activate(self, event): + if event.inaxes is None: + return + event.inaxes.grid() + self.figure.canvas.draw() + + +class ToolToggleFullScreen(ToolBase): + name = 'Fullscreen' + description = 'Toogle Fullscreen mode' + keymap = rcParams['keymap.fullscreen'] + + def activate(self, event): + self.figure.canvas.manager.full_screen_toggle() + + +class ToolToggleYScale(ToolBase): + name = 'YScale' + description = 'Toogle Scale Y axis' + keymap = rcParams['keymap.yscale'] + + def activate(self, event): + ax = event.inaxes + if ax is None: + return + + scale = ax.get_yscale() + if scale == 'log': + ax.set_yscale('linear') + ax.figure.canvas.draw() + elif scale == 'linear': + ax.set_yscale('log') + ax.figure.canvas.draw() + + +class ToolToggleXScale(ToolBase): + name = 'XScale' + description = 'Toogle Scale X axis' + keymap = rcParams['keymap.xscale'] + + def activate(self, event): + ax = event.inaxes + if ax is None: + return + + scalex = ax.get_xscale() + if scalex == 'log': + ax.set_xscale('linear') + ax.figure.canvas.draw() + elif scalex == 'linear': + ax.set_xscale('log') + ax.figure.canvas.draw() + + +class ToolHome(ToolBase): + description = 'Reset original view' + name = 'Home' + image = 'home' + keymap = rcParams['keymap.home'] + position = -1 + + def activate(self, *args): + """Restore the original view""" + self.navigation.views.home() + self.navigation.positions.home() + self.navigation.update_view() +# self.set_history_buttons() + + +class ToolBack(ToolBase): + description = 'Back to previous view' + name = 'Back' + image = 'back' + keymap = rcParams['keymap.back'] + position = -1 + + def activate(self, *args): + """move back up the view lim stack""" + self.navigation.views.back() + self.navigation.positions.back() +# self.set_history_buttons() + self.navigation.update_view() + + +class ToolForward(ToolBase): + description = 'Forward to next view' + name = 'Forward' + image = 'forward' + keymap = rcParams['keymap.forward'] + position = -1 + + def activate(self, *args): + """Move forward in the view lim stack""" + self.navigation.views.forward() + self.navigation.positions.forward() +# self.set_history_buttons() + self.navigation.update_view() + + +class ConfigureSubplotsBase(ToolPersistentBase): + description = 'Configure subplots' + name = 'Subplots' + image = 'subplots' + position = -1 + + +class SaveFigureBase(ToolBase): + description = 'Save the figure' + name = 'Save' + image = 'filesave' + position = -1 + keymap = rcParams['keymap.save'] + + +class ToolZoom(ToolToggleBase): + description = 'Zoom to rectangle' + name = 'Zoom' + image = 'zoom_to_rect' + position = -1 + keymap = rcParams['keymap.zoom'] + cursor = cursors.SELECT_REGION + + def __init__(self, *args): + ToolToggleBase.__init__(self, *args) + self._ids_zoom = [] + self._button_pressed = None + self._xypress = None + + def activate(self, event): + self.navigation.canvaslock(self) + self.navigation.presslock(self) + self.navigation.releaselock(self) + + def deactivate(self, event): + self.navigation.canvaslock.release(self) + self.navigation.presslock.release(self) + self.navigation.releaselock.release(self) + + def press(self, event): + """the press mouse button in zoom to rect mode callback""" + # If we're already in the middle of a zoom, pressing another + # button works to "cancel" + if self._ids_zoom != []: + self.navigation.movelock.release(self) + for zoom_id in self._ids_zoom: + self.figure.canvas.mpl_disconnect(zoom_id) + self.navigation.release(event) + self.navigation.draw() + self._xypress = None + self._button_pressed = None + self._ids_zoom = [] + return + + if event.button == 1: + self._button_pressed = 1 + elif event.button == 3: + self._button_pressed = 3 + else: + self._button_pressed = None + return + + x, y = event.x, event.y + + # push the current view to define home if stack is empty + # TODO: add a set home in navigation + if self.navigation.views.empty(): + self.navigation.push_current() + + self._xypress = [] + for i, a in enumerate(self.figure.get_axes()): + if (x is not None and y is not None and a.in_axes(event) and + a.get_navigate() and a.can_zoom()): + self._xypress.append((x, y, a, i, a.viewLim.frozen(), + a.transData.frozen())) + + self.navigation.movelock(self) + id2 = self.figure.canvas.mpl_connect('key_press_event', + self._switch_on_zoom_mode) + id3 = self.figure.canvas.mpl_connect('key_release_event', + self._switch_off_zoom_mode) + + self._ids_zoom = id2, id3 + self._zoom_mode = event.key + + self.navigation.press(event) + + def _switch_on_zoom_mode(self, event): + self._zoom_mode = event.key + self.mouse_move(event) + + def _switch_off_zoom_mode(self, event): + self._zoom_mode = None + self.mouse_move(event) + + def mouse_move(self, event): + """the drag callback in zoom mode""" + if self._xypress: + x, y = event.x, event.y + lastx, lasty, a, _ind, _lim, _trans = self._xypress[0] + + # adjust x, last, y, last + x1, y1, x2, y2 = a.bbox.extents + x, lastx = max(min(x, lastx), x1), min(max(x, lastx), x2) + y, lasty = max(min(y, lasty), y1), min(max(y, lasty), y2) + + if self._zoom_mode == "x": + x1, y1, x2, y2 = a.bbox.extents + y, lasty = y1, y2 + elif self._zoom_mode == "y": + x1, y1, x2, y2 = a.bbox.extents + x, lastx = x1, x2 + + self.navigation.draw_rubberband(event, x, y, lastx, lasty) + + def release(self, event): + """the release mouse button callback in zoom to rect mode""" + self.navigation.movelock.release(self) + for zoom_id in self._ids_zoom: + self.figure.canvas.mpl_disconnect(zoom_id) + self._ids_zoom = [] + + if not self._xypress: + return + + last_a = [] + + for cur_xypress in self._xypress: + x, y = event.x, event.y + lastx, lasty, a, _ind, lim, _trans = cur_xypress + # ignore singular clicks - 5 pixels is a threshold + if abs(x - lastx) < 5 or abs(y - lasty) < 5: + self._xypress = None + self.navigation.release(event) + self.navigation.draw() + return + + x0, y0, x1, y1 = lim.extents + + # zoom to rect + inverse = a.transData.inverted() + lastx, lasty = inverse.transform_point((lastx, lasty)) + x, y = inverse.transform_point((x, y)) + Xmin, Xmax = a.get_xlim() + Ymin, Ymax = a.get_ylim() + + # detect twinx,y axes and avoid double zooming + twinx, twiny = False, False + if last_a: + for la in last_a: + if a.get_shared_x_axes().joined(a, la): + twinx = True + if a.get_shared_y_axes().joined(a, la): + twiny = True + last_a.append(a) + + if twinx: + x0, x1 = Xmin, Xmax + else: + if Xmin < Xmax: + if x < lastx: + x0, x1 = x, lastx + else: + x0, x1 = lastx, x + if x0 < Xmin: + x0 = Xmin + if x1 > Xmax: + x1 = Xmax + else: + if x > lastx: + x0, x1 = x, lastx + else: + x0, x1 = lastx, x + if x0 > Xmin: + x0 = Xmin + if x1 < Xmax: + x1 = Xmax + + if twiny: + y0, y1 = Ymin, Ymax + else: + if Ymin < Ymax: + if y < lasty: + y0, y1 = y, lasty + else: + y0, y1 = lasty, y + if y0 < Ymin: + y0 = Ymin + if y1 > Ymax: + y1 = Ymax + else: + if y > lasty: + y0, y1 = y, lasty + else: + y0, y1 = lasty, y + if y0 > Ymin: + y0 = Ymin + if y1 < Ymax: + y1 = Ymax + + if self._button_pressed == 1: + if self._zoom_mode == "x": + a.set_xlim((x0, x1)) + elif self._zoom_mode == "y": + a.set_ylim((y0, y1)) + else: + a.set_xlim((x0, x1)) + a.set_ylim((y0, y1)) + elif self._button_pressed == 3: + if a.get_xscale() == 'log': + alpha = np.log(Xmax / Xmin) / np.log(x1 / x0) + rx1 = pow(Xmin / x0, alpha) * Xmin + rx2 = pow(Xmax / x0, alpha) * Xmin + else: + alpha = (Xmax - Xmin) / (x1 - x0) + rx1 = alpha * (Xmin - x0) + Xmin + rx2 = alpha * (Xmax - x0) + Xmin + if a.get_yscale() == 'log': + alpha = np.log(Ymax / Ymin) / np.log(y1 / y0) + ry1 = pow(Ymin / y0, alpha) * Ymin + ry2 = pow(Ymax / y0, alpha) * Ymin + else: + alpha = (Ymax - Ymin) / (y1 - y0) + ry1 = alpha * (Ymin - y0) + Ymin + ry2 = alpha * (Ymax - y0) + Ymin + + if self._zoom_mode == "x": + a.set_xlim((rx1, rx2)) + elif self._zoom_mode == "y": + a.set_ylim((ry1, ry2)) + else: + a.set_xlim((rx1, rx2)) + a.set_ylim((ry1, ry2)) + + self.navigation.draw() + self._xypress = None + self._button_pressed = None + + self._zoom_mode = None + + self.navigation.push_current() + self.navigation.release(event) + + +class ToolPan(ToolToggleBase): + keymap = rcParams['keymap.pan'] + name = 'Pan' + description = 'Pan axes with left mouse, zoom with right' + image = 'move' + position = -1 + cursor = cursors.MOVE + + def __init__(self, *args): + ToolToggleBase.__init__(self, *args) + self._button_pressed = None + self._xypress = None + + def activate(self, event): + self.navigation.canvaslock(self) + self.navigation.presslock(self) + self.navigation.releaselock(self) + + def deactivate(self, event): + self.navigation.canvaslock.release(self) + self.navigation.presslock.release(self) + self.navigation.releaselock.release(self) + + def press(self, event): + """the press mouse button in pan/zoom mode callback""" + + if event.button == 1: + self._button_pressed = 1 + elif event.button == 3: + self._button_pressed = 3 + else: + self._button_pressed = None + return + + x, y = event.x, event.y + + # push the current view to define home if stack is empty + #TODO: add define_home in navigation + if self.navigation.views.empty(): + self.navigation.push_current() + + self._xypress = [] + for i, a in enumerate(self.figure.get_axes()): + if (x is not None and y is not None and a.in_axes(event) and + a.get_navigate() and a.can_pan()): + a.start_pan(x, y, event.button) + self._xypress.append((a, i)) + self.navigation.movelock(self) + self.navigation.press(event) + + def release(self, event): + if self._button_pressed is None: + return + + self.navigation.movelock.release(self) + + for a, _ind in self._xypress: + a.end_pan() + if not self._xypress: + return + self._xypress = [] + self._button_pressed = None + self.navigation.push_current() + self.navigation.release(event) + self.navigation.draw() + + def mouse_move(self, event): + """the drag callback in pan/zoom mode""" + + for a, _ind in self._xypress: + #safer to use the recorded button at the press than current button: + #multiple button can get pressed during motion... + a.drag_pan(self._button_pressed, event.key, event.x, event.y) + self.navigation.dynamic_update() From c6c0ad3540158f84acbb52e095678d21946d3bc3 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 24 Jan 2014 10:29:05 -0500 Subject: [PATCH 04/41] helper methods in toolbar and navigation --- lib/matplotlib/backend_bases.py | 40 +++++++++++++++++++++++-- lib/matplotlib/backend_tools.py | 2 +- lib/matplotlib/backends/backend_gtk3.py | 27 ++++++++++++++--- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 500870020233..ffed00b6e7f9 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3186,8 +3186,10 @@ class NavigationBase(object): tools.ToolToggleYScale, tools.ToolHome, tools.ToolBack, tools.ToolForward, + None, tools.ToolZoom, tools.ToolPan, + None, 'ConfigureSubplots', 'SaveFigure'] @@ -3224,7 +3226,11 @@ def __init__(self, canvas, toolbar=None): self.canvaslock = self.canvas.widgetlock for tool in self._default_tools: - self.add_tool(tool) + if tool is None: + if self.toolbar is not None: + self.toolbar.add_separator(-1) + else: + self.add_tool(tool) self._last_cursor = self._default_cursor @@ -3237,7 +3243,28 @@ def _get_toolbar(self, toolbar, canvas): toolbar = None return toolbar - #remove persistent instances + def get_active(self): + return {'toggled': self._toggled, 'instances': self._instances.keys()} + + def get_tool_keymap(self, name): + keys = [k for k, i in self._keys.items() if i == name] + return keys + + def set_tool_keymap(self, name, *keys): + if name not in self._tools: + raise AttributeError('%s not in Tools' % name) + + active_keys = [k for k, i in self._keys.items() if i == name] + for k in active_keys: + del self._keys[k] + + for key in keys: + for k in validate_stringlist(key): + if k in self._keys: + warnings.warn('Key %s changed from %s to %s' % + (k, self._keys[k], name)) + self._keys[k] = name + def unregister(self, name): if self._toggled == name: self._handle_toggle(name, from_toolbar=False) @@ -3268,6 +3295,9 @@ def add_tool(self, callback_class): self._tools[name] = tool if tool.keymap is not None: for k in validate_stringlist(tool.keymap): + if k in self._keys: + warnings.warn('Key %s changed from %s to %s' % + (k, self._keys[k], name)) self._keys[k] = name if self.toolbar and tool.position is not None: @@ -3531,3 +3561,9 @@ def toggle(self, name, callback=False): def remove_toolitem(self, name): pass + + def move_toolitem(self, pos_ini, pos_fin): + pass + + def set_toolitem_visibility(self, name, visible): + pass diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 8535d8d87a21..9911b0d40709 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -88,7 +88,7 @@ def activate(self, event): class ToolEnableNavigation(ToolBase): name = 'EnableOne' description = 'Enables one axes navigation' - keymap = range(1, 5) + keymap = range(1, 10) def activate(self, event): if event.inaxes is None: diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 2532b870742d..19e2c1ac91c3 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -706,6 +706,7 @@ def get_filename_from_user (self): return filename, self.ext + class NavigationGTK3(NavigationBase): def __init__(self, *args, **kwargs): NavigationBase.__init__(self, *args, **kwargs) @@ -803,8 +804,7 @@ def set_message(self, s): def toggle(self, name, callback=False): if name not in self._toolitems: - # TODO: raise a warning - print('Not in toolbar', name) + self.set_message('%s Not in toolbar' % name) return status = self._toolitems[name].get_active() @@ -818,12 +818,31 @@ def toggle(self, name, callback=False): def remove_toolitem(self, name): if name not in self._toolitems: - #TODO: raise warning - print('Not in toolbar', name) + self.set_message('%s Not in toolbar' % name) return self._toolbar.remove(self._toolitems[name]) del self._toolitems[name] + def add_separator(self, pos=-1): + toolitem = Gtk.SeparatorToolItem() + self._toolbar.insert(toolitem, pos) + toolitem.show() + return toolitem + + def move_toolitem(self, pos_ini, pos_fin): + widget = self._toolbar.get_nth_item(pos_ini) + if not widget: + self.set_message('Impossible to remove tool %d' % pos_ini) + return + self._toolbar.remove(widget) + self._toolbar.insert(widget, pos_fin) + + def set_toolitem_visibility(self, name, visible): + if name not in self._toolitems: + self.set_message('%s Not in toolbar' % name) + return + self._toolitems[name].set_visible(visible) + class SaveFigureGTK3(SaveFigureBase): From 1fc29fa6c5bcaecf894fb32b2e19599b4e6a21ce Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 24 Jan 2014 13:23:16 -0500 Subject: [PATCH 05/41] Adding doc to base methods --- doc/api/backend_tools_api.rst | 8 + doc/api/index_backend_api.rst | 1 + lib/matplotlib/backend_bases.py | 226 +++++++++++++++++++++--- lib/matplotlib/backend_tools.py | 153 +++++++++++++++- lib/matplotlib/backends/backend_gtk3.py | 8 +- 5 files changed, 363 insertions(+), 33 deletions(-) create mode 100644 doc/api/backend_tools_api.rst diff --git a/doc/api/backend_tools_api.rst b/doc/api/backend_tools_api.rst new file mode 100644 index 000000000000..32babd5844b0 --- /dev/null +++ b/doc/api/backend_tools_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backend_tools` +================================ + +.. automodule:: matplotlib.backend_tools + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/index_backend_api.rst b/doc/api/index_backend_api.rst index 5ca377150e2f..295976cbfcab 100644 --- a/doc/api/index_backend_api.rst +++ b/doc/api/index_backend_api.rst @@ -5,6 +5,7 @@ backends .. toctree:: backend_bases_api.rst + backend_tools_api.rst backend_gtkagg_api.rst backend_qt4agg_api.rst backend_wxagg_api.rst diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index ffed00b6e7f9..f5aed235ea25 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -25,6 +25,14 @@ the 'show' callable is then set to Show.__call__, inherited from ShowBase. +:class:`NavigationBase` + The base class for the Navigation class that makes the bridge between + user interaction (key press, toolbar clicks, ..) and the actions in + response to the user inputs. + +:class:`ToolbarBase` + The base class for the Toolbar class of each interactive backend. + """ from __future__ import (absolute_import, division, print_function, @@ -3176,6 +3184,25 @@ def set_history_buttons(self): class NavigationBase(object): + """ Helper class that groups all the user interactions for a FigureManager + + Attributes + ---------- + canvas : `FigureCanvas` instance + toolbar : `Toolbar` instance that is controlled by this `Navigation` + keypresslock : `LockDraw` to direct the `canvas` key_press_event + movelock: `LockDraw` to direct the `canvas` motion_notify_event + presslock: `LockDraw` to direct the `canvas` button_press_event + releaselock: `LockDraw` to direct the `canvas` button_release_event + canvaslock: shortcut to `canvas.widgetlock` + + Notes + --------_ + The following methos are for implementation pourposes and not for user use + For these reason they are defined as **_methodname** (private) + + .. automethod:: _toolbar_callback + """ _default_cursor = cursors.POINTER _default_tools = [tools.ToolToggleGrid, tools.ToolToggleFullScreen, @@ -3244,13 +3271,43 @@ def _get_toolbar(self, toolbar, canvas): return toolbar def get_active(self): + """Get the active tools + + Returns + ---------- + A dictionary with the following elements + * `toggled`: The currently toggled Tool or None + * `instances`: List of the currently active tool instances + that are registered with Navigation + + """ return {'toggled': self._toggled, 'instances': self._instances.keys()} def get_tool_keymap(self, name): + """Get the keymap associated with a tool + + Parameters + ---------- + name : string + Name of the Tool + + Returns + ---------- + Keymap : list of keys associated with the Tool + """ keys = [k for k, i in self._keys.items() if i == name] return keys def set_tool_keymap(self, name, *keys): + """Set the keymap associated with a tool + + Parameters + ---------- + name : string + Name of the Tool + keys : keys to associated with the Tool + """ + if name not in self._tools: raise AttributeError('%s not in Tools' % name) @@ -3266,12 +3323,32 @@ def set_tool_keymap(self, name, *keys): self._keys[k] = name def unregister(self, name): + """Unregister the tool from the active instances + + Notes + ----- + This method is used by `PersistentTools` to remove the reference kept + by `Navigation`. + + It is usually called by the `deactivate` method or during + destroy if it is a graphical Tool. + + If called, next time the `Tool` is used it will be reinstantiated instead + of using the existing instance. + """ if self._toggled == name: self._handle_toggle(name, from_toolbar=False) if name in self._instances: del self._instances[name] def remove_tool(self, name): + """Remove tool from the `Navigation` + + Parameters + ---------- + name : string + Name of the Tool + """ self.unregister(name) del self._tools[name] keys = [k for k, v in self._keys.items() if v == name] @@ -3279,37 +3356,46 @@ def remove_tool(self, name): del self._keys[k] if self.toolbar: - self.toolbar.remove_toolitem(name) + self.toolbar._remove_toolitem(name) + + def add_tool(self, tool): + """Add tool to `Navigation` - def add_tool(self, callback_class): - tool = self._get_cls_to_instantiate(callback_class) - name = tool.name + Parameters + ---------- + tool : string or `Tool` class + Reference to find the class of the Tool to be added + """ + tool_cls = self._get_cls_to_instantiate(tool) + name = tool_cls.name if name is None: - warnings.warn('Tools need a name to be added, it is used as ID') + warnings.warn('tool_clss need a name to be added, it is used ' + 'as ID') return if name in self._tools: - warnings.warn('A tool with the same name already exist, not added') + warnings.warn('A tool_cls with the same name already exist, ' + 'not added') return - self._tools[name] = tool - if tool.keymap is not None: - for k in validate_stringlist(tool.keymap): + self._tools[name] = tool_cls + if tool_cls.keymap is not None: + for k in validate_stringlist(tool_cls.keymap): if k in self._keys: warnings.warn('Key %s changed from %s to %s' % (k, self._keys[k], name)) self._keys[k] = name - if self.toolbar and tool.position is not None: + if self.toolbar and tool_cls.position is not None: basedir = os.path.join(rcParams['datapath'], 'images') - if tool.image is not None: - fname = os.path.join(basedir, tool.image + '.png') + if tool_cls.image is not None: + fname = os.path.join(basedir, tool_cls.image + '.png') else: fname = None - self.toolbar.add_toolitem(name, tool.description, + self.toolbar._add_toolitem(name, tool_cls.description, fname, - tool.position, - tool.toggle) + tool_cls.position, + tool_cls.toggle) def _get_cls_to_instantiate(self, callback_class): if isinstance(callback_class, basestring): @@ -3359,7 +3445,18 @@ def _get_instance(self, name): return self._instances[name] - def toolbar_callback(self, name): + def _toolbar_callback(self, name): + """Callback for the `Toolbar` + + All Toolbar implementations have to call this method to signal that a + toolitem was clicked on + + Parameters + ---------- + name : string + Name of the tool that was activated (click) by the user using the + toolbar + """ tool = self._tools[name] if tool.toggle: self._handle_toggle(name, from_toolbar=True) @@ -3372,7 +3469,7 @@ def toolbar_callback(self, name): def _handle_toggle(self, name, event=None, from_toolbar=False): #toggle toolbar without callback if not from_toolbar and self.toolbar: - self.toolbar.toggle(name, False) + self.toolbar._toggle(name, False) instance = self._get_instance(name) if self._toggled is None: @@ -3385,7 +3482,7 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): else: if self.toolbar: - self.toolbar.toggle(self._toggled, False) + self.toolbar._toggle(self._toggled, False) self._get_instance(self._toggled).deactivate(None) instance.activate(None) @@ -3395,6 +3492,7 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): a.set_navigate_mode(self._toggled) def list_tools(self): + """Print the list the tools controlled by `Navigation`""" print ('_' * 80) print ("{0:20} {1:50} {2}".format('Name (id)', 'Tool description', 'Keymap')) @@ -3541,29 +3639,113 @@ def draw_rubberband(self, event, x0, y0, x1, y1): class ToolbarBase(object): + """Base class for `Toolbar` implementation + + Attributes + ---------- + manager : `FigureManager` instance that integrates this `Toolbar` + + Notes + ----- + The following methos are for implementation pourposes and not for user use. + For these reason they are defined as **_methodname** (private) + + .. automethod:: _toggle + .. automethod:: _add_toolitem + .. automethod:: _remove_toolitem + """ def __init__(self, manager): self.manager = manager - def add_toolitem(self, name, description, image_file, position, + def _add_toolitem(self, name, description, image_file, position, toggle): + """Add a toolitem to the toolbar + + The callback associated with the button click event, + must be **EXACTLY** `self.manager.navigation._toolbar_callback(name)` + + Parameters + ---------- + name : string + Name of the tool to add, this is used as ID and as default label + of the buttons + description : string + Description of the tool, used for the tooltips + image_file : string + Filename of the image for the button or `None` + position : integer + Position of the toolitem within the other toolitems + if -1 at the End + toggle : bool + * `True` : The button is a toggle (change the pressed/unpressed + state between consecutive clicks) + * `False` : The button is a normal button (returns to unpressed + state after release) + """ raise NotImplementedError def add_separator(self, pos): + """Add a separator + + Parameters + ---------- + pos : integer + Position where to add the separator within the toolitems + if -1 at the end + """ pass def set_message(self, s): """Display a message on toolbar or in status bar""" pass - def toggle(self, name, callback=False): + def _toggle(self, name, callback=False): + """Toogle a button + + Parameters + ---------- + name : string + Name of the button to toggle + callback : bool + * `True`: call the button callback during toggle + * `False`: toggle the button without calling the callback + + """ #carefull, callback means to perform or not the callback while toggling raise NotImplementedError - def remove_toolitem(self, name): - pass + def _remove_toolitem(self, name): + """Remove a toolitem from the `Toolbar` + + Parameters + ---------- + name : string + Name of the tool to remove + + """ + raise NotImplementedError def move_toolitem(self, pos_ini, pos_fin): + """Change the position of a toolitem + + Parameters + ---------- + pos_ini : integer + Initial position of the toolitem to move + pos_fin : integer + Final position of the toolitem + """ pass def set_toolitem_visibility(self, name, visible): + """Change the visibility of a toolitem + + Parameters + ---------- + name : string + Name of the `Tool` + visible : bool + * `True`: set the toolitem visible + * `False`: set the toolitem invisible + """ pass diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 9911b0d40709..e060884eee3d 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -1,3 +1,19 @@ +""" +Abstract base classes define the primitives for Tools. +These tools are used by `NavigationBase` + +:class:`ToolBase` + Simple tool that is instantiated every time it is used + +:class:`ToolPersistentBase` + Tool which instance is registered within `Navigation` + +:class:`ToolToggleBase` + PersistentTool that has two states, only one Toggle tool can be + active at any given time for the same `Navigation` +""" + + from matplotlib import rcParams from matplotlib._pylab_helpers import Gcf import numpy as np @@ -10,14 +26,73 @@ class Cursors: class ToolBase(object): + """Base tool class + + Attributes + ---------- + navigation : `NavigationBase` + Navigation that controls this Tool + figure : `FigureCanvas` + Figure instance that is affected by this Tool + """ + keymap = None + """Keymap to associate this tool + + **string**: List of comma separated keys that will be used to call this + tool when the keypress event of *self.figure.canvas* is emited + """ + position = None + """Where to put the tool in the *Toolbar* + + * **integer** : Position within the Toolbar + * **None** : Do not put in the Toolbar + * **-1**: At the end of the Toolbar + + """ + description = None + """Description of the Tool + + **string**: If the Tool is included in the Toolbar this text is used + as Tooltip + """ + name = None + """Name of the Tool + + **string**: Used as ID for the tool, must be unique + """ + image = None + """Filename of the image + + **string**: Filename of the image to use in the toolbar. If None, the + `name` is used as label in the toolbar button + """ + toggle = False # Change the status (take control of the events) + """Is toggleable tool + + **bool**: + + * **True**: The tool is a toogleable tool + * **False**: The tool is not toggleable + + """ + persistent = False + """Is persistent tool + + **bool**: + * `True`: The tool is persistent + * `False`: The tool is not persistent + """ + cursor = None + """Cursor to use when the tool is active + """ def __init__(self, figure, event=None): self.figure = figure @@ -25,10 +100,27 @@ def __init__(self, figure, event=None): self.activate(event) def activate(self, event): + """Called when tool is used + + Parameters + ---------- + event : `Event` + Event that caused this tool to be called + """ pass class ToolPersistentBase(ToolBase): + """Persisten tool + + Persistent Tools are keept alive after their initialization, + a reference of the instance is kept by `navigation`. + + Notes + ----- + The difference with `ToolBase` is that `activate` method + is not called automatically at initialization + """ persistent = True def __init__(self, figure, event=None): @@ -37,30 +129,69 @@ def __init__(self, figure, event=None): #persistent tools don't call activate a at instantiation def unregister(self, *args): + """Unregister the tool from the instances of Navigation + + If the reference in navigation was the last reference + to the instance of the tool, it will be garbage collected + """ #call this to unregister from navigation self.navigation.unregister(self.name) class ToolToggleBase(ToolPersistentBase): + """Toggleable tool + + This tool is a Persistent Tool, that has the ability to capture + the keypress, press and release events, preventing other tools + to use the same events at the same time + """ toggle = True def mouse_move(self, event): + """Mouse move event + + Called when a motion_notify_event is emited by the `FigureCanvas` if + `navigation.movelock(self)` was setted + """ pass def press(self, event): + """Mouse press event + + Called when a button_press_event is emited by the `FigureCanvas` if + `navigation.presslock(self)` was setted + """ pass def release(self, event): + """Mouse release event + + Called when a button_release_event is emited by the `FigureCanvas` if + `navigation.releaselock(self)` was setted + """ pass def deactivate(self, event=None): + """Deactivate the toggle tool + + This method is called when the tool is deactivated (second click on the + toolbar button) or when another toogle tool from the same `navigation` is + activated + """ pass def key_press(self, event): + """Key press event + + Called when a key_press_event is emited by the `FigureCanvas` if + `navigation.keypresslock(self)` was setted + """ pass class ToolQuit(ToolBase): + """Tool to call the figure manager destroy method + """ name = 'Quit' description = 'Quit the figure' keymap = rcParams['keymap.quit'] @@ -70,6 +201,8 @@ def activate(self, event): class ToolEnableAllNavigation(ToolBase): + """Tool to enable all axes for navigation interaction + """ name = 'EnableAll' description = 'Enables all axes navigation' keymap = rcParams['keymap.all_axes'] @@ -86,6 +219,8 @@ def activate(self, event): #FIXME: use a function instead of string for enable navigation class ToolEnableNavigation(ToolBase): + """Tool to enable a specific axes for navigation interaction + """ name = 'EnableOne' description = 'Enables one axes navigation' keymap = range(1, 10) @@ -104,6 +239,7 @@ def activate(self, event): class ToolToggleGrid(ToolBase): + """Tool to toggle the grid of the figure""" name = 'Grid' description = 'Toogle Grid' keymap = rcParams['keymap.grid'] @@ -116,6 +252,7 @@ def activate(self, event): class ToolToggleFullScreen(ToolBase): + """Tool to toggle full screen""" name = 'Fullscreen' description = 'Toogle Fullscreen mode' keymap = rcParams['keymap.fullscreen'] @@ -125,6 +262,7 @@ def activate(self, event): class ToolToggleYScale(ToolBase): + """Tool to toggle between linear and logarithmic the Y axis""" name = 'YScale' description = 'Toogle Scale Y axis' keymap = rcParams['keymap.yscale'] @@ -144,6 +282,7 @@ def activate(self, event): class ToolToggleXScale(ToolBase): + """Tool to toggle between linear and logarithmic the X axis""" name = 'XScale' description = 'Toogle Scale X axis' keymap = rcParams['keymap.xscale'] @@ -163,6 +302,7 @@ def activate(self, event): class ToolHome(ToolBase): + """Restore the original view""" description = 'Reset original view' name = 'Home' image = 'home' @@ -170,7 +310,6 @@ class ToolHome(ToolBase): position = -1 def activate(self, *args): - """Restore the original view""" self.navigation.views.home() self.navigation.positions.home() self.navigation.update_view() @@ -178,6 +317,7 @@ def activate(self, *args): class ToolBack(ToolBase): + """move back up the view lim stack""" description = 'Back to previous view' name = 'Back' image = 'back' @@ -185,7 +325,6 @@ class ToolBack(ToolBase): position = -1 def activate(self, *args): - """move back up the view lim stack""" self.navigation.views.back() self.navigation.positions.back() # self.set_history_buttons() @@ -193,6 +332,7 @@ def activate(self, *args): class ToolForward(ToolBase): + """Move forward in the view lim stack""" description = 'Forward to next view' name = 'Forward' image = 'forward' @@ -200,7 +340,6 @@ class ToolForward(ToolBase): position = -1 def activate(self, *args): - """Move forward in the view lim stack""" self.navigation.views.forward() self.navigation.positions.forward() # self.set_history_buttons() @@ -208,6 +347,7 @@ def activate(self, *args): class ConfigureSubplotsBase(ToolPersistentBase): + """Base tool for the configuration of subplots""" description = 'Configure subplots' name = 'Subplots' image = 'subplots' @@ -215,6 +355,7 @@ class ConfigureSubplotsBase(ToolPersistentBase): class SaveFigureBase(ToolBase): + """Base tool for figure saving""" description = 'Save the figure' name = 'Save' image = 'filesave' @@ -223,6 +364,7 @@ class SaveFigureBase(ToolBase): class ToolZoom(ToolToggleBase): + """Zoom to rectangle""" description = 'Zoom to rectangle' name = 'Zoom' image = 'zoom_to_rect' @@ -452,6 +594,7 @@ def release(self, event): class ToolPan(ToolToggleBase): + """Pan axes with left mouse, zoom with right""" keymap = rcParams['keymap.pan'] name = 'Pan' description = 'Pan axes with left mouse, zoom with right' @@ -475,8 +618,6 @@ def deactivate(self, event): self.navigation.releaselock.release(self) def press(self, event): - """the press mouse button in pan/zoom mode callback""" - if event.button == 1: self._button_pressed = 1 elif event.button == 3: @@ -518,8 +659,6 @@ def release(self, event): self.navigation.draw() def mouse_move(self, event): - """the drag callback in pan/zoom mode""" - for a, _ind in self._xypress: #safer to use the recorded button at the press than current button: #multiple button can get pressed during motion... diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 19e2c1ac91c3..a4a644ebcb51 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -776,7 +776,7 @@ def _add_message(self): self.pack_end(sep, False, True, 0) sep.show_all() - def add_toolitem(self, name, tooltip_text, image_file, position, + def _add_toolitem(self, name, tooltip_text, image_file, position, toggle): if toggle: tbutton = Gtk.ToggleToolButton() @@ -797,12 +797,12 @@ def add_toolitem(self, name, tooltip_text, image_file, position, self._signals[name] = signal def _call_tool(self, btn, name): - self.manager.navigation.toolbar_callback(name) + self.manager.navigation._toolbar_callback(name) def set_message(self, s): self.message.set_label(s) - def toggle(self, name, callback=False): + def _toggle(self, name, callback=False): if name not in self._toolitems: self.set_message('%s Not in toolbar' % name) return @@ -816,7 +816,7 @@ def toggle(self, name, callback=False): if not callback: self._toolitems[name].handler_unblock(self._signals[name]) - def remove_toolitem(self, name): + def _remove_toolitem(self, name): if name not in self._toolitems: self.set_message('%s Not in toolbar' % name) return From 979875e6b2643bbc8a1360aecbceb58cafd00acf Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Sat, 25 Jan 2014 22:06:41 -0500 Subject: [PATCH 06/41] property for active_toggle --- lib/matplotlib/backend_bases.py | 51 ++++++++++++++------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index f5aed235ea25..9582668f6d4f 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3191,17 +3191,10 @@ class NavigationBase(object): canvas : `FigureCanvas` instance toolbar : `Toolbar` instance that is controlled by this `Navigation` keypresslock : `LockDraw` to direct the `canvas` key_press_event - movelock: `LockDraw` to direct the `canvas` motion_notify_event - presslock: `LockDraw` to direct the `canvas` button_press_event - releaselock: `LockDraw` to direct the `canvas` button_release_event - canvaslock: shortcut to `canvas.widgetlock` - - Notes - --------_ - The following methos are for implementation pourposes and not for user use - For these reason they are defined as **_methodname** (private) - - .. automethod:: _toolbar_callback + movelock : `LockDraw` to direct the `canvas` motion_notify_event + presslock : `LockDraw` to direct the `canvas` button_press_event + releaselock : `LockDraw` to direct the `canvas` button_release_event + canvaslock : shortcut to `canvas.widgetlock` """ _default_cursor = cursors.POINTER _default_tools = [tools.ToolToggleGrid, @@ -3221,6 +3214,7 @@ class NavigationBase(object): 'SaveFigure'] def __init__(self, canvas, toolbar=None): + """.. automethod:: _toolbar_callback""" self.canvas = canvas self.toolbar = self._get_toolbar(toolbar, canvas) @@ -3270,18 +3264,20 @@ def _get_toolbar(self, toolbar, canvas): toolbar = None return toolbar - def get_active(self): - """Get the active tools + @property + def active_toggle(self): + """Get the tooggled Tool""" + return self._toggled + + def get_instances(self): + """Get the active tools instgances Returns ---------- - A dictionary with the following elements - * `toggled`: The currently toggled Tool or None - * `instances`: List of the currently active tool instances - that are registered with Navigation - + A dictionary with the active instances that are registered with + Navigation """ - return {'toggled': self._toggled, 'instances': self._instances.keys()} + return self._instances def get_tool_keymap(self, name): """Get the keymap associated with a tool @@ -3333,7 +3329,8 @@ def unregister(self, name): It is usually called by the `deactivate` method or during destroy if it is a graphical Tool. - If called, next time the `Tool` is used it will be reinstantiated instead + If called, next time the `Tool` is used it will be reinstantiated + instead of using the existing instance. """ if self._toggled == name: @@ -3644,17 +3641,13 @@ class ToolbarBase(object): Attributes ---------- manager : `FigureManager` instance that integrates this `Toolbar` - - Notes - ----- - The following methos are for implementation pourposes and not for user use. - For these reason they are defined as **_methodname** (private) - - .. automethod:: _toggle - .. automethod:: _add_toolitem - .. automethod:: _remove_toolitem """ def __init__(self, manager): + """ + .. automethod:: _add_toolitem + .. automethod:: _remove_toolitem + .. automethod:: _toggle + """ self.manager = manager def _add_toolitem(self, name, description, image_file, position, From c5c4f0f185e8c72acb34ab36efca5224ccd642d7 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 27 Jan 2014 16:37:29 -0500 Subject: [PATCH 07/41] simulate click --- lib/matplotlib/backend_bases.py | 71 +++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 9582668f6d4f..2bbbf5b148fe 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3266,16 +3266,17 @@ def _get_toolbar(self, toolbar, canvas): @property def active_toggle(self): - """Get the tooggled Tool""" + """Toggled Tool + + **string** : Currently toggled tool, or None + """ return self._toggled - def get_instances(self): - """Get the active tools instgances + @property + def instances(self): + """Active tools instances - Returns - ---------- - A dictionary with the active instances that are registered with - Navigation + **dictionary** : Contains the active instances that are registered """ return self._instances @@ -3289,7 +3290,7 @@ def get_tool_keymap(self, name): Returns ---------- - Keymap : list of keys associated with the Tool + list : list of keys associated with the Tool """ keys = [k for k, i in self._keys.items() if i == name] return keys @@ -3321,6 +3322,11 @@ def set_tool_keymap(self, name, *keys): def unregister(self, name): """Unregister the tool from the active instances + Parameters + ---------- + name : string + Name of the tool to unregister + Notes ----- This method is used by `PersistentTools` to remove the reference kept @@ -3330,8 +3336,7 @@ def unregister(self, name): destroy if it is a graphical Tool. If called, next time the `Tool` is used it will be reinstantiated - instead - of using the existing instance. + instead of using the existing instance. """ if self._toggled == name: self._handle_toggle(name, from_toolbar=False) @@ -3408,24 +3413,21 @@ def _get_cls_to_instantiate(self, callback_class): return callback_class - def _key_press(self, event): - if event.key is None: - return + def click_tool(self, name): + """Simulate a click on a tool - #some tools may need to capture keypress, but they need to be toggle - if self._toggled: - instance = self._get_instance(self._toggled) - if self.keypresslock.isowner(instance): - instance.key_press(event) - return + This is a convenient method to programatically click on + Tools + """ + self._tool_activate(name, None, False) - name = self._keys.get(event.key, None) - if name is None: - return + def _tool_activate(self, name, event, from_toolbar): + if name not in self._tools: + raise AttributeError('%s not in Tools' % name) tool = self._tools[name] if tool.toggle: - self._handle_toggle(name, event=event) + self._handle_toggle(name, event=event, from_toolbar=from_toolbar) elif tool.persistent: instance = self._get_instance(name) instance.activate(event) @@ -3434,6 +3436,20 @@ def _key_press(self, event): #instantiated and forgotten (reminds me an exgirlfriend?) tool(self.canvas.figure, event) + def _key_press(self, event): + if event.key is None: + return + + #some tools may need to capture keypress, but they need to be toggle + if self._toggled: + instance = self._get_instance(self._toggled) + if self.keypresslock.isowner(instance): + instance.key_press(event) + return + + name = self._keys.get(event.key, None) + self._tool_activate(name, event, False) + def _get_instance(self, name): if name not in self._instances: instance = self._tools[name](self.canvas.figure) @@ -3454,14 +3470,7 @@ def _toolbar_callback(self, name): Name of the tool that was activated (click) by the user using the toolbar """ - tool = self._tools[name] - if tool.toggle: - self._handle_toggle(name, from_toolbar=True) - elif tool.persistent: - instance = self._get_instance(name) - instance.activate(None) - else: - tool(self.canvas.figure, None) + self._tool_activate(name, None, True) def _handle_toggle(self, name, event=None, from_toolbar=False): #toggle toolbar without callback From 97dfda72002e954b4694252bffe0c412af6f7f71 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 27 Jan 2014 16:55:41 -0500 Subject: [PATCH 08/41] pep8 backend_tools --- lib/matplotlib/backend_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index e060884eee3d..2302479cd127 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -175,8 +175,8 @@ def deactivate(self, event=None): """Deactivate the toggle tool This method is called when the tool is deactivated (second click on the - toolbar button) or when another toogle tool from the same `navigation` is - activated + toolbar button) or when another toogle tool from the same `navigation` + is activated """ pass From 6b647ad069306bcf8fde91a7e093c0885b37deb2 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 28 Jan 2014 18:41:19 -0500 Subject: [PATCH 09/41] activate renamed to trigger --- examples/user_interfaces/navigation.py | 4 ++-- lib/matplotlib/backend_bases.py | 6 ++--- lib/matplotlib/backend_tools.py | 32 ++++++++++++------------- lib/matplotlib/backends/backend_gtk3.py | 4 ++-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index dd2ab12bbb61..7e84c9ae8ccf 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -15,7 +15,7 @@ class ListTools(ToolBase): #Where to put it in the toolbar, -1 = at the end, None = Not in toolbar position = -1 - def activate(self, event): + def trigger(self, event): #The most important attributes are navigation and figure self.navigation.list_tools() @@ -28,7 +28,7 @@ class CopyTool(ToolBase): description = 'Copy canvas' position = -1 - def activate(self, event): + def trigger(self, event): from gi.repository import Gtk, Gdk, GdkPixbuf clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) window = self.figure.canvas.get_window() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 2bbbf5b148fe..d5f77568897c 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3430,7 +3430,7 @@ def _tool_activate(self, name, event, from_toolbar): self._handle_toggle(name, event=event, from_toolbar=from_toolbar) elif tool.persistent: instance = self._get_instance(name) - instance.activate(event) + instance.trigger(event) else: #Non persistent tools, are #instantiated and forgotten (reminds me an exgirlfriend?) @@ -3479,7 +3479,7 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): instance = self._get_instance(name) if self._toggled is None: - instance.activate(None) + instance.trigger(None) self._toggled = name elif self._toggled == name: @@ -3491,7 +3491,7 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): self.toolbar._toggle(self._toggled, False) self._get_instance(self._toggled).deactivate(None) - instance.activate(None) + instance.trigger(None) self._toggled = name for a in self.canvas.figure.get_axes(): diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 2302479cd127..2a0f60551e96 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -97,9 +97,9 @@ class ToolBase(object): def __init__(self, figure, event=None): self.figure = figure self.navigation = figure.canvas.manager.navigation - self.activate(event) + self.trigger(event) - def activate(self, event): + def trigger(self, event): """Called when tool is used Parameters @@ -118,7 +118,7 @@ class ToolPersistentBase(ToolBase): Notes ----- - The difference with `ToolBase` is that `activate` method + The difference with `ToolBase` is that `trigger` method is not called automatically at initialization """ persistent = True @@ -126,7 +126,7 @@ class ToolPersistentBase(ToolBase): def __init__(self, figure, event=None): self.figure = figure self.navigation = figure.canvas.manager.navigation - #persistent tools don't call activate a at instantiation + #persistent tools don't call trigger a at instantiation def unregister(self, *args): """Unregister the tool from the instances of Navigation @@ -196,7 +196,7 @@ class ToolQuit(ToolBase): description = 'Quit the figure' keymap = rcParams['keymap.quit'] - def activate(self, event): + def trigger(self, event): Gcf.destroy_fig(self.figure) @@ -207,7 +207,7 @@ class ToolEnableAllNavigation(ToolBase): description = 'Enables all axes navigation' keymap = rcParams['keymap.all_axes'] - def activate(self, event): + def trigger(self, event): if event.inaxes is None: return @@ -225,7 +225,7 @@ class ToolEnableNavigation(ToolBase): description = 'Enables one axes navigation' keymap = range(1, 10) - def activate(self, event): + def trigger(self, event): if event.inaxes is None: return @@ -244,7 +244,7 @@ class ToolToggleGrid(ToolBase): description = 'Toogle Grid' keymap = rcParams['keymap.grid'] - def activate(self, event): + def trigger(self, event): if event.inaxes is None: return event.inaxes.grid() @@ -257,7 +257,7 @@ class ToolToggleFullScreen(ToolBase): description = 'Toogle Fullscreen mode' keymap = rcParams['keymap.fullscreen'] - def activate(self, event): + def trigger(self, event): self.figure.canvas.manager.full_screen_toggle() @@ -267,7 +267,7 @@ class ToolToggleYScale(ToolBase): description = 'Toogle Scale Y axis' keymap = rcParams['keymap.yscale'] - def activate(self, event): + def trigger(self, event): ax = event.inaxes if ax is None: return @@ -287,7 +287,7 @@ class ToolToggleXScale(ToolBase): description = 'Toogle Scale X axis' keymap = rcParams['keymap.xscale'] - def activate(self, event): + def trigger(self, event): ax = event.inaxes if ax is None: return @@ -309,7 +309,7 @@ class ToolHome(ToolBase): keymap = rcParams['keymap.home'] position = -1 - def activate(self, *args): + def trigger(self, *args): self.navigation.views.home() self.navigation.positions.home() self.navigation.update_view() @@ -324,7 +324,7 @@ class ToolBack(ToolBase): keymap = rcParams['keymap.back'] position = -1 - def activate(self, *args): + def trigger(self, *args): self.navigation.views.back() self.navigation.positions.back() # self.set_history_buttons() @@ -339,7 +339,7 @@ class ToolForward(ToolBase): keymap = rcParams['keymap.forward'] position = -1 - def activate(self, *args): + def trigger(self, *args): self.navigation.views.forward() self.navigation.positions.forward() # self.set_history_buttons() @@ -378,7 +378,7 @@ def __init__(self, *args): self._button_pressed = None self._xypress = None - def activate(self, event): + def trigger(self, event): self.navigation.canvaslock(self) self.navigation.presslock(self) self.navigation.releaselock(self) @@ -607,7 +607,7 @@ def __init__(self, *args): self._button_pressed = None self._xypress = None - def activate(self, event): + def trigger(self, event): self.navigation.canvaslock(self) self.navigation.presslock(self) self.navigation.releaselock(self) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index a4a644ebcb51..a5dea34b7885 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -856,7 +856,7 @@ def get_filechooser(self): fc.set_current_name(self.figure.canvas.get_default_filename()) return fc - def activate(self, *args): + def trigger(self, *args): chooser = self.get_filechooser() fname, format_ = chooser.get_filename_from_user() chooser.destroy() @@ -917,7 +917,7 @@ def __init__(self, *args, **kwargs): def _get_canvas(self, fig): return self.canvas.__class__(fig) - def activate(self, event): + def trigger(self, event): self.present() From 99667aad8c67e225fe75d21bcf6fd46f8755fb52 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 28 Jan 2014 19:20:45 -0500 Subject: [PATCH 10/41] toggle tools using enable/disable from its trigger method --- lib/matplotlib/backend_bases.py | 19 +++++++++--------- lib/matplotlib/backend_tools.py | 34 +++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d5f77568897c..54f9ed2032f9 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3413,15 +3413,15 @@ def _get_cls_to_instantiate(self, callback_class): return callback_class - def click_tool(self, name): - """Simulate a click on a tool + def trigger_tool(self, name): + """Trigger on a tool - This is a convenient method to programatically click on + This is a convenient method to programatically "click" on Tools """ - self._tool_activate(name, None, False) + self._trigger_tool(name, None, False) - def _tool_activate(self, name, event, from_toolbar): + def _trigger_tool(self, name, event, from_toolbar): if name not in self._tools: raise AttributeError('%s not in Tools' % name) @@ -3448,7 +3448,7 @@ def _key_press(self, event): return name = self._keys.get(event.key, None) - self._tool_activate(name, event, False) + self._trigger_tool(name, event, False) def _get_instance(self, name): if name not in self._instances: @@ -3470,7 +3470,7 @@ def _toolbar_callback(self, name): Name of the tool that was activated (click) by the user using the toolbar """ - self._tool_activate(name, None, True) + self._trigger_tool(name, None, True) def _handle_toggle(self, name, event=None, from_toolbar=False): #toggle toolbar without callback @@ -3483,14 +3483,15 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): self._toggled = name elif self._toggled == name: - instance.deactivate(None) + instance.trigger(None) self._toggled = None else: if self.toolbar: + #untoggle the previous toggled tool self.toolbar._toggle(self._toggled, False) - self._get_instance(self._toggled).deactivate(None) + self._get_instance(self._toggled).trigger(None) instance.trigger(None) self._toggled = name diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 2a0f60551e96..8ef4c927a45e 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -146,6 +146,7 @@ class ToolToggleBase(ToolPersistentBase): to use the same events at the same time """ toggle = True + _toggled = False def mouse_move(self, event): """Mouse move event @@ -171,12 +172,26 @@ def release(self, event): """ pass - def deactivate(self, event=None): - """Deactivate the toggle tool + def trigger(self, event): + if self._toggled: + self.disable(event) + else: + self.enable(event) + self._toggled = not self._toggled + + def enable(self, event=None): + """Enable the toggle tool - This method is called when the tool is deactivated (second click on the - toolbar button) or when another toogle tool from the same `navigation` - is activated + This method is called when the tool is triggered and not active + """ + pass + + def disable(self, event=None): + """Disable the toggle tool + + This method is called when the tool is triggered and active. + * Second click on the toolbar button + * Another toogle tool is triggered (from the same `navigation`) """ pass @@ -217,7 +232,6 @@ def trigger(self, event): a.set_navigate(True) -#FIXME: use a function instead of string for enable navigation class ToolEnableNavigation(ToolBase): """Tool to enable a specific axes for navigation interaction """ @@ -378,12 +392,12 @@ def __init__(self, *args): self._button_pressed = None self._xypress = None - def trigger(self, event): + def enable(self, event): self.navigation.canvaslock(self) self.navigation.presslock(self) self.navigation.releaselock(self) - def deactivate(self, event): + def disable(self, event): self.navigation.canvaslock.release(self) self.navigation.presslock.release(self) self.navigation.releaselock.release(self) @@ -607,12 +621,12 @@ def __init__(self, *args): self._button_pressed = None self._xypress = None - def trigger(self, event): + def enable(self, event): self.navigation.canvaslock(self) self.navigation.presslock(self) self.navigation.releaselock(self) - def deactivate(self, event): + def disable(self, event): self.navigation.canvaslock.release(self) self.navigation.presslock.release(self) self.navigation.releaselock.release(self) From 6c19579d4faa675353b466add1bb08e71c0fb6e6 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 28 Jan 2014 19:36:49 -0500 Subject: [PATCH 11/41] simplifying _handle_toggle --- lib/matplotlib/backend_bases.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 54f9ed2032f9..9c47ed1e40e6 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3479,22 +3479,21 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): instance = self._get_instance(name) if self._toggled is None: - instance.trigger(None) + #first trigger of tool self._toggled = name - elif self._toggled == name: - instance.trigger(None) + #second trigger of tool self._toggled = None - else: + #other tool is triggered so trigger toggled tool if self.toolbar: #untoggle the previous toggled tool self.toolbar._toggle(self._toggled, False) - - self._get_instance(self._toggled).trigger(None) - instance.trigger(None) + self._get_instance(self._toggled).trigger(event) self._toggled = name + instance.trigger(event) + for a in self.canvas.figure.get_axes(): a.set_navigate_mode(self._toggled) From 0495aac084d459e77ea5413bdffc620d486b6a62 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 29 Jan 2014 12:53:42 -0500 Subject: [PATCH 12/41] reducing number of locks --- lib/matplotlib/backend_bases.py | 91 +++++++----------- lib/matplotlib/backend_tools.py | 118 ++++++++++-------------- lib/matplotlib/backends/backend_gtk3.py | 9 +- 3 files changed, 87 insertions(+), 131 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 9c47ed1e40e6..4b526eca8c7e 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3190,11 +3190,9 @@ class NavigationBase(object): ---------- canvas : `FigureCanvas` instance toolbar : `Toolbar` instance that is controlled by this `Navigation` - keypresslock : `LockDraw` to direct the `canvas` key_press_event - movelock : `LockDraw` to direct the `canvas` motion_notify_event - presslock : `LockDraw` to direct the `canvas` button_press_event - releaselock : `LockDraw` to direct the `canvas` button_release_event - canvaslock : shortcut to `canvas.widgetlock` + keypresslock : `LockDraw` to know if the `canvas` key_press_event is + locked + messagelock : `LockDraw` to know if the message is available to write """ _default_cursor = cursors.POINTER _default_tools = [tools.ToolToggleGrid, @@ -3224,11 +3222,6 @@ def __init__(self, canvas, toolbar=None): self._idDrag = self.canvas.mpl_connect('motion_notify_event', self._mouse_move) - self._idPress = self.canvas.mpl_connect('button_press_event', - self._press) - self._idRelease = self.canvas.mpl_connect('button_release_event', - self._release) - # a dict from axes index to a list of view limits self.views = cbook.Stack() self.positions = cbook.Stack() # stack of subplot positions @@ -3240,11 +3233,7 @@ def __init__(self, canvas, toolbar=None): #to communicate with tools and redirect events self.keypresslock = widgets.LockDraw() - self.movelock = widgets.LockDraw() - self.presslock = widgets.LockDraw() - self.releaselock = widgets.LockDraw() - #just to group all the locks in one place - self.canvaslock = self.canvas.widgetlock + self.messagelock = widgets.LockDraw() for tool in self._default_tools: if tool is None: @@ -3437,16 +3426,9 @@ def _trigger_tool(self, name, event, from_toolbar): tool(self.canvas.figure, event) def _key_press(self, event): - if event.key is None: + if event.key is None or self.keypresslock.locked(): return - #some tools may need to capture keypress, but they need to be toggle - if self._toggled: - instance = self._get_instance(self._toggled) - if self.keypresslock.isowner(instance): - instance.key_press(event) - return - name = self._keys.get(event.key, None) self._trigger_tool(name, event, False) @@ -3517,12 +3499,6 @@ def update(self): # self.set_history_buttons() def _mouse_move(self, event): - if self._toggled: - instance = self._instances[self._toggled] - if self.movelock.isowner(instance): - instance.mouse_move(event) - return - if not event.inaxes or not self._toggled: if self._last_cursor != self._default_cursor: self.set_cursor(self._default_cursor) @@ -3534,7 +3510,7 @@ def _mouse_move(self, event): self.set_cursor(cursor) self._last_cursor = cursor - if self.toolbar is None: + if self.toolbar is None or self.messagelock.locked(): return if event.inaxes and event.inaxes.get_navigate(): @@ -3551,30 +3527,6 @@ def _mouse_move(self, event): else: self.toolbar.set_message('') - def _release(self, event): - if self._toggled: - instance = self._instances[self._toggled] - if self.releaselock.isowner(instance): - instance.release(event) - return - self.release(event) - - def release(self, event): - pass - - def _press(self, event): - """Called whenver a mouse button is pressed.""" - if self._toggled: - instance = self._instances[self._toggled] - if self.presslock.isowner(instance): - instance.press(event) - return - self.press(event) - - def press(self, event): - """Called whenver a mouse button is pressed.""" - pass - def draw(self): """Redraw the canvases, update the locators""" for a in self.canvas.figure.get_axes(): @@ -3639,9 +3591,34 @@ def push_current(self): self.positions.push(pos) # self.set_history_buttons() - def draw_rubberband(self, event, x0, y0, x1, y1): - """Draw a rectangle rubberband to indicate zoom limits""" - pass + def draw_rubberband(self, event, caller, x0, y0, x1, y1): + """Draw a rectangle rubberband to indicate zoom limits + + Draw a rectanlge in the canvas, if + `self.canvas.widgetlock` is available to **caller** + + Parameters + ---------- + event : `FigureCanvas` event + caller : instance trying to draw the rubberband + x0, y0, x1, y1 : coordinates + """ + if not self.canvas.widgetlock.available(caller): + warnings.warn("%s doesn't own the canvas widgetlock" % caller) + + def remove_rubberband(self, event, caller): + """Remove the rubberband + + Remove the rubberband if the `self.canvas.widgetlock` is + available to **caller** + + Parameters + ---------- + event : `FigureCanvas` event + caller : instance trying to remove the rubberband + """ + if not self.canvas.widgetlock.available(caller): + warnings.warn("%s doesn't own the canvas widgetlock" % caller) class ToolbarBase(object): diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 8ef4c927a45e..cbef016b4dbc 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -141,37 +141,13 @@ def unregister(self, *args): class ToolToggleBase(ToolPersistentBase): """Toggleable tool - This tool is a Persistent Tool, that has the ability to capture - the keypress, press and release events, preventing other tools - to use the same events at the same time + This tool is a Persistent Tool that has a toggled state. + Every time it is triggered, it switches between enable and disable + """ toggle = True _toggled = False - def mouse_move(self, event): - """Mouse move event - - Called when a motion_notify_event is emited by the `FigureCanvas` if - `navigation.movelock(self)` was setted - """ - pass - - def press(self, event): - """Mouse press event - - Called when a button_press_event is emited by the `FigureCanvas` if - `navigation.presslock(self)` was setted - """ - pass - - def release(self, event): - """Mouse release event - - Called when a button_release_event is emited by the `FigureCanvas` if - `navigation.releaselock(self)` was setted - """ - pass - def trigger(self, event): if self._toggled: self.disable(event) @@ -182,26 +158,23 @@ def trigger(self, event): def enable(self, event=None): """Enable the toggle tool - This method is called when the tool is triggered and not active + This method is called when the tool is triggered and not toggled """ pass def disable(self, event=None): """Disable the toggle tool - This method is called when the tool is triggered and active. - * Second click on the toolbar button + This method is called when the tool is triggered and toggled. + * Second click on the toolbar tool button * Another toogle tool is triggered (from the same `navigation`) """ pass - def key_press(self, event): - """Key press event - - Called when a key_press_event is emited by the `FigureCanvas` if - `navigation.keypresslock(self)` was setted - """ - pass + @property + def toggled(self): + """State of the toggled tool""" + return self._toggled class ToolQuit(ToolBase): @@ -391,26 +364,29 @@ def __init__(self, *args): self._ids_zoom = [] self._button_pressed = None self._xypress = None + self._idPress = None + self._idRelease = None def enable(self, event): - self.navigation.canvaslock(self) - self.navigation.presslock(self) - self.navigation.releaselock(self) + self.figure.canvas.widgetlock(self) + self._idPress = self.figure.canvas.mpl_connect( + 'button_press_event', self._press) + self._idRelease = self.figure.canvas.mpl_connect( + 'button_release_event', self._release) def disable(self, event): - self.navigation.canvaslock.release(self) - self.navigation.presslock.release(self) - self.navigation.releaselock.release(self) + self.figure.canvas.widgetlock.release(self) + self.figure.canvas.mpl_disconnect(self._idPress) + self.figure.canvas.mpl_disconnect(self._idRelease) - def press(self, event): - """the press mouse button in zoom to rect mode callback""" + def _press(self, event): + """the _press mouse button in zoom to rect mode callback""" # If we're already in the middle of a zoom, pressing another # button works to "cancel" if self._ids_zoom != []: - self.navigation.movelock.release(self) for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) - self.navigation.release(event) + self.navigation.remove_rubberband(event, self) self.navigation.draw() self._xypress = None self._button_pressed = None @@ -439,17 +415,16 @@ def press(self, event): self._xypress.append((x, y, a, i, a.viewLim.frozen(), a.transData.frozen())) - self.navigation.movelock(self) + id1 = self.figure.canvas.mpl_connect( + 'motion_notify_event', self.mouse_move) id2 = self.figure.canvas.mpl_connect('key_press_event', self._switch_on_zoom_mode) id3 = self.figure.canvas.mpl_connect('key_release_event', self._switch_off_zoom_mode) - self._ids_zoom = id2, id3 + self._ids_zoom = id1, id2, id3 self._zoom_mode = event.key - self.navigation.press(event) - def _switch_on_zoom_mode(self, event): self._zoom_mode = event.key self.mouse_move(event) @@ -476,11 +451,10 @@ def mouse_move(self, event): x1, y1, x2, y2 = a.bbox.extents x, lastx = x1, x2 - self.navigation.draw_rubberband(event, x, y, lastx, lasty) + self.navigation.draw_rubberband(event, self, x, y, lastx, lasty) - def release(self, event): + def _release(self, event): """the release mouse button callback in zoom to rect mode""" - self.navigation.movelock.release(self) for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) self._ids_zoom = [] @@ -496,7 +470,7 @@ def release(self, event): # ignore singular clicks - 5 pixels is a threshold if abs(x - lastx) < 5 or abs(y - lasty) < 5: self._xypress = None - self.navigation.release(event) + self.navigation.remove_rubberband(event, self) self.navigation.draw() return @@ -604,7 +578,7 @@ def release(self, event): self._zoom_mode = None self.navigation.push_current() - self.navigation.release(event) + self.navigation.remove_rubberband(event, self) class ToolPan(ToolToggleBase): @@ -620,18 +594,23 @@ def __init__(self, *args): ToolToggleBase.__init__(self, *args) self._button_pressed = None self._xypress = None + self._idPress = None + self._idRelease = None + self._idDrag = None def enable(self, event): - self.navigation.canvaslock(self) - self.navigation.presslock(self) - self.navigation.releaselock(self) + self.figure.canvas.widgetlock(self) + self._idPress = self.figure.canvas.mpl_connect( + 'button_press_event', self._press) + self._idRelease = self.figure.canvas.mpl_connect( + 'button_release_event', self._release) def disable(self, event): - self.navigation.canvaslock.release(self) - self.navigation.presslock.release(self) - self.navigation.releaselock.release(self) + self.figure.canvas.widgetlock.release(self) + self.figure.canvas.mpl_disconnect(self._idPress) + self.figure.canvas.mpl_disconnect(self._idRelease) - def press(self, event): + def _press(self, event): if event.button == 1: self._button_pressed = 1 elif event.button == 3: @@ -653,14 +632,16 @@ def press(self, event): a.get_navigate() and a.can_pan()): a.start_pan(x, y, event.button) self._xypress.append((a, i)) - self.navigation.movelock(self) - self.navigation.press(event) + self.navigation.messagelock(self) + self._idDrag = self.figure.canvas.mpl_connect( + 'motion_notify_event', self.mouse_move) - def release(self, event): + def _release(self, event): if self._button_pressed is None: return - self.navigation.movelock.release(self) + self.figure.canvas.mpl_disconnect(self._idDrag) + self.navigation.messagelock.release(self) for a, _ind in self._xypress: a.end_pan() @@ -669,12 +650,11 @@ def release(self, event): self._xypress = [] self._button_pressed = None self.navigation.push_current() - self.navigation.release(event) self.navigation.draw() def mouse_move(self, event): for a, _ind in self._xypress: - #safer to use the recorded button at the press than current button: + #safer to use the recorded button at the _press than current button: #multiple button can get pressed during motion... a.drag_pan(self._button_pressed, event.key, event.x, event.y) self.navigation.dynamic_update() diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index a5dea34b7885..6254256dda8d 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -715,7 +715,10 @@ def __init__(self, *args, **kwargs): def set_cursor(self, cursor): self.canvas.get_property("window").set_cursor(cursord[cursor]) - def draw_rubberband(self, event, x0, y0, x1, y1): + def draw_rubberband(self, event, caller, x0, y0, x1, y1): + if not self.canvas.widgetlock.available(caller): + return + #'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/ #Recipe/189744' self.ctx = self.canvas.get_property("window").cairo_create() @@ -741,10 +744,6 @@ def dynamic_update(self): # legacy method; new method is canvas.draw_idle self.canvas.draw_idle() -# def release(self, event): -# try: del self._pixmapBack -# except AttributeError: pass - class ToolbarGTK3(ToolbarBase, Gtk.Box,): def __init__(self, manager): From fb46fc1abd57e4945217ff5a0e75daff3aae6674 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 29 Jan 2014 13:24:30 -0500 Subject: [PATCH 13/41] pep8 correction --- lib/matplotlib/backend_tools.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index cbef016b4dbc..575e279587d0 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -416,7 +416,7 @@ def _press(self, event): a.transData.frozen())) id1 = self.figure.canvas.mpl_connect( - 'motion_notify_event', self.mouse_move) + 'motion_notify_event', self._mouse_move) id2 = self.figure.canvas.mpl_connect('key_press_event', self._switch_on_zoom_mode) id3 = self.figure.canvas.mpl_connect('key_release_event', @@ -427,13 +427,13 @@ def _press(self, event): def _switch_on_zoom_mode(self, event): self._zoom_mode = event.key - self.mouse_move(event) + self._mouse_move(event) def _switch_off_zoom_mode(self, event): self._zoom_mode = None - self.mouse_move(event) + self._mouse_move(event) - def mouse_move(self, event): + def _mouse_move(self, event): """the drag callback in zoom mode""" if self._xypress: x, y = event.x, event.y @@ -634,7 +634,7 @@ def _press(self, event): self._xypress.append((a, i)) self.navigation.messagelock(self) self._idDrag = self.figure.canvas.mpl_connect( - 'motion_notify_event', self.mouse_move) + 'motion_notify_event', self._mouse_move) def _release(self, event): if self._button_pressed is None: @@ -652,9 +652,9 @@ def _release(self, event): self.navigation.push_current() self.navigation.draw() - def mouse_move(self, event): + def _mouse_move(self, event): for a, _ind in self._xypress: - #safer to use the recorded button at the _press than current button: - #multiple button can get pressed during motion... + #safer to use the recorded button at the _press than current + #button: #multiple button can get pressed during motion... a.drag_pan(self._button_pressed, event.key, event.x, event.y) self.navigation.dynamic_update() From d49c431f1541cb8cc56b3a151f805284834a1a3d Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 4 Feb 2014 09:17:41 -0500 Subject: [PATCH 14/41] changing toggle and persistent attributes for issubclass --- lib/matplotlib/backend_bases.py | 11 ++++++--- lib/matplotlib/backend_tools.py | 43 +++++++++++++++------------------ 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 4b526eca8c7e..5b766e0670cf 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3231,8 +3231,9 @@ def __init__(self, canvas, toolbar=None): self._instances = {} self._toggled = None - #to communicate with tools and redirect events + #to process keypress event self.keypresslock = widgets.LockDraw() + #to write into toolbar message self.messagelock = widgets.LockDraw() for tool in self._default_tools: @@ -3359,6 +3360,7 @@ def add_tool(self, tool): """ tool_cls = self._get_cls_to_instantiate(tool) name = tool_cls.name + if name is None: warnings.warn('tool_clss need a name to be added, it is used ' 'as ID') @@ -3383,10 +3385,11 @@ def add_tool(self, tool): fname = os.path.join(basedir, tool_cls.image + '.png') else: fname = None + toggle = issubclass(tool_cls, tools.ToolToggleBase) self.toolbar._add_toolitem(name, tool_cls.description, fname, tool_cls.position, - tool_cls.toggle) + toggle) def _get_cls_to_instantiate(self, callback_class): if isinstance(callback_class, basestring): @@ -3415,9 +3418,9 @@ def _trigger_tool(self, name, event, from_toolbar): raise AttributeError('%s not in Tools' % name) tool = self._tools[name] - if tool.toggle: + if issubclass(tool, tools.ToolToggleBase): self._handle_toggle(name, event=event, from_toolbar=from_toolbar) - elif tool.persistent: + elif issubclass(tool, tools.ToolPersistentBase): instance = self._get_instance(name) instance.trigger(event) else: diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 575e279587d0..1cdc25bfdc57 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -72,31 +72,14 @@ class ToolBase(object): `name` is used as label in the toolbar button """ - toggle = False # Change the status (take control of the events) - """Is toggleable tool - - **bool**: - - * **True**: The tool is a toogleable tool - * **False**: The tool is not toggleable - - """ - - persistent = False - """Is persistent tool - - **bool**: - * `True`: The tool is persistent - * `False`: The tool is not persistent - """ - cursor = None """Cursor to use when the tool is active """ def __init__(self, figure, event=None): - self.figure = figure - self.navigation = figure.canvas.manager.navigation + self.figure = None + self.navigation = None + self.set_figure(figure) self.trigger(event) def trigger(self, event): @@ -109,6 +92,18 @@ def trigger(self, event): """ pass + def set_figure(self, figure): + """Set the figure and navigation + + Set the figure to be affected by this tool + + Parameters + ---------- + figure : `Figure` + """ + self.figure = figure + self.navigation = figure.canvas.manager.navigation + class ToolPersistentBase(ToolBase): """Persisten tool @@ -121,12 +116,13 @@ class ToolPersistentBase(ToolBase): The difference with `ToolBase` is that `trigger` method is not called automatically at initialization """ - persistent = True def __init__(self, figure, event=None): - self.figure = figure - self.navigation = figure.canvas.manager.navigation + self.figure = None + self.navigation = None + self.set_figure(figure) #persistent tools don't call trigger a at instantiation + #it will be called by Navigation def unregister(self, *args): """Unregister the tool from the instances of Navigation @@ -145,7 +141,6 @@ class ToolToggleBase(ToolPersistentBase): Every time it is triggered, it switches between enable and disable """ - toggle = True _toggled = False def trigger(self, event): From 5ba6210ca6ca3e0fb3f6defe17b17410558ba4f1 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 4 Feb 2014 09:47:07 -0500 Subject: [PATCH 15/41] bug in combined key press --- lib/matplotlib/backend_bases.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 5b766e0670cf..86316bbd95f8 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3408,8 +3408,7 @@ def _get_cls_to_instantiate(self, callback_class): def trigger_tool(self, name): """Trigger on a tool - This is a convenient method to programatically "click" on - Tools + Method to programatically "click" on Tools """ self._trigger_tool(name, None, False) @@ -3433,6 +3432,8 @@ def _key_press(self, event): return name = self._keys.get(event.key, None) + if name is None: + return self._trigger_tool(name, event, False) def _get_instance(self, name): From 7ca86260909d893f575242efb514e1bfdc22b68f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 4 Feb 2014 15:56:08 -0500 Subject: [PATCH 16/41] untoggle zoom and pan from keypress while toggled --- lib/matplotlib/backend_tools.py | 51 +++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 1cdc25bfdc57..76fbb13c49ca 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -370,30 +370,34 @@ def enable(self, event): 'button_release_event', self._release) def disable(self, event): + self._cancel_zoom() self.figure.canvas.widgetlock.release(self) self.figure.canvas.mpl_disconnect(self._idPress) self.figure.canvas.mpl_disconnect(self._idRelease) + def _cancel_zoom(self): + for zoom_id in self._ids_zoom: + self.figure.canvas.mpl_disconnect(zoom_id) + self.navigation.remove_rubberband(None, self) + self.navigation.draw() + self._xypress = None + self._button_pressed = None + self._ids_zoom = [] + return + def _press(self, event): """the _press mouse button in zoom to rect mode callback""" # If we're already in the middle of a zoom, pressing another # button works to "cancel" if self._ids_zoom != []: - for zoom_id in self._ids_zoom: - self.figure.canvas.mpl_disconnect(zoom_id) - self.navigation.remove_rubberband(event, self) - self.navigation.draw() - self._xypress = None - self._button_pressed = None - self._ids_zoom = [] - return + self._cancel_zoom() if event.button == 1: self._button_pressed = 1 elif event.button == 3: self._button_pressed = 3 else: - self._button_pressed = None + self._cancel_zoom() return x, y = event.x, event.y @@ -455,6 +459,7 @@ def _release(self, event): self._ids_zoom = [] if not self._xypress: + self._cancel_zoom() return last_a = [] @@ -464,9 +469,7 @@ def _release(self, event): lastx, lasty, a, _ind, lim, _trans = cur_xypress # ignore singular clicks - 5 pixels is a threshold if abs(x - lastx) < 5 or abs(y - lasty) < 5: - self._xypress = None - self.navigation.remove_rubberband(event, self) - self.navigation.draw() + self._cancel_zoom() return x0, y0, x1, y1 = lim.extents @@ -566,14 +569,9 @@ def _release(self, event): a.set_xlim((rx1, rx2)) a.set_ylim((ry1, ry2)) - self.navigation.draw() - self._xypress = None - self._button_pressed = None - self._zoom_mode = None - self.navigation.push_current() - self.navigation.remove_rubberband(event, self) + self._cancel_zoom() class ToolPan(ToolToggleBase): @@ -601,17 +599,25 @@ def enable(self, event): 'button_release_event', self._release) def disable(self, event): + self._cancel_pan() self.figure.canvas.widgetlock.release(self) self.figure.canvas.mpl_disconnect(self._idPress) self.figure.canvas.mpl_disconnect(self._idRelease) + def _cancel_pan(self): + self._button_pressed = None + self._xypress = [] + self.figure.canvas.mpl_disconnect(self._idDrag) + self.navigation.messagelock.release(self) + self.navigation.draw() + def _press(self, event): if event.button == 1: self._button_pressed = 1 elif event.button == 3: self._button_pressed = 3 else: - self._button_pressed = None + self._cancel_pan() return x, y = event.x, event.y @@ -633,6 +639,7 @@ def _press(self, event): def _release(self, event): if self._button_pressed is None: + self._cancel_pan() return self.figure.canvas.mpl_disconnect(self._idDrag) @@ -641,11 +648,11 @@ def _release(self, event): for a, _ind in self._xypress: a.end_pan() if not self._xypress: + self._cancel_pan() return - self._xypress = [] - self._button_pressed = None + self.navigation.push_current() - self.navigation.draw() + self._cancel_pan() def _mouse_move(self, event): for a, _ind in self._xypress: From dcc0f1659e96ee36c27ccd8cce73b0488dfd72b0 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 6 Feb 2014 09:38:29 -0500 Subject: [PATCH 17/41] classmethods for default tools modification --- lib/matplotlib/backend_bases.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 86316bbd95f8..32cc06735b4c 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3245,6 +3245,16 @@ def __init__(self, canvas, toolbar=None): self._last_cursor = self._default_cursor + @classmethod + def get_default_tools(cls): + """Get the default tools""" + return cls._default_tools + + @classmethod + def set_default_tools(cls, tools): + """Set default tools""" + cls._default_tools = tools + def _get_toolbar(self, toolbar, canvas): # must be inited after the window, drawingArea and figure # attrs are set From bc703e018f54a921f714c29a923ccb36aa15469c Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Apr 2014 11:50:03 -0400 Subject: [PATCH 18/41] six fixes --- examples/user_interfaces/navigation.py | 10 +++++----- lib/matplotlib/backend_bases.py | 12 ++++++------ lib/matplotlib/backend_tools.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 7e84c9ae8ccf..8d1f56907d46 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -10,11 +10,11 @@ class ListTools(ToolBase): #keyboard shortcut keymap = 'm' #Name used as id, must be unique between tools of the same navigation - name = 'List' - description = 'List Tools' + name = 'List' + description = 'List Tools' #Where to put it in the toolbar, -1 = at the end, None = Not in toolbar position = -1 - + def trigger(self, event): #The most important attributes are navigation and figure self.navigation.list_tools() @@ -29,7 +29,7 @@ class CopyTool(ToolBase): position = -1 def trigger(self, event): - from gi.repository import Gtk, Gdk, GdkPixbuf + from gi.repository import Gtk, Gdk clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) window = self.figure.canvas.get_window() x, y, width, height = window.get_geometry() @@ -46,7 +46,7 @@ def trigger(self, event): fig.canvas.manager.navigation.add_tool(ListTools) fig.canvas.manager.navigation.add_tool(CopyTool) - ##Just for fun, lets remove the back button + ##Just for fun, lets remove the back button fig.canvas.manager.navigation.remove_tool('Back') plt.show() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 32cc06735b4c..1ed3eb394216 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3258,7 +3258,7 @@ def set_default_tools(cls, tools): def _get_toolbar(self, toolbar, canvas): # must be inited after the window, drawingArea and figure # attrs are set - if rcParams['toolbar'] == 'navigation' and toolbar is not None: + if rcParams['toolbar'] == 'navigation' and toolbar is not None: toolbar = toolbar(canvas.manager) else: toolbar = None @@ -3292,7 +3292,7 @@ def get_tool_keymap(self, name): ---------- list : list of keys associated with the Tool """ - keys = [k for k, i in self._keys.items() if i == name] + keys = [k for k, i in six.iteritems(self._keys) if i == name] return keys def set_tool_keymap(self, name, *keys): @@ -3308,7 +3308,7 @@ def set_tool_keymap(self, name, *keys): if name not in self._tools: raise AttributeError('%s not in Tools' % name) - active_keys = [k for k, i in self._keys.items() if i == name] + active_keys = [k for k, i in six.iteritems(self._keys) if i == name] for k in active_keys: del self._keys[k] @@ -3353,7 +3353,7 @@ def remove_tool(self, name): """ self.unregister(name) del self._tools[name] - keys = [k for k, v in self._keys.items() if v == name] + keys = [k for k, v in six.iteritems(self._keys) if v == name] for k in keys: del self._keys[k] @@ -3402,7 +3402,7 @@ def add_tool(self, tool): toggle) def _get_cls_to_instantiate(self, callback_class): - if isinstance(callback_class, basestring): + if isinstance(callback_class, six.string_types): #FIXME: make more complete searching structure if callback_class in globals(): return globals()[callback_class] @@ -3501,7 +3501,7 @@ def list_tools(self): print ('_' * 80) for name in sorted(self._tools.keys()): tool = self._tools[name] - keys = [k for k, i in self._keys.items() if i == name] + keys = [k for k, i in six.iteritems(self._keys) if i == name] print ("{0:20} {1:50} {2}".format(tool.name, tool.description, ', '.join(keys))) print ('_' * 80, '\n') diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 76fbb13c49ca..d54442744504 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -205,7 +205,7 @@ class ToolEnableNavigation(ToolBase): """ name = 'EnableOne' description = 'Enables one axes navigation' - keymap = range(1, 10) + keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9) def trigger(self, event): if event.inaxes is None: From 68dc7114c73c3a6fbffbfffaba64e186951e8f00 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 1 May 2014 19:12:56 -0400 Subject: [PATCH 19/41] adding zaxis and some pep8 --- lib/matplotlib/backend_bases.py | 55 ++++++++++++++++++++++++++------- lib/matplotlib/backend_tools.py | 50 ++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 28 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 1ed3eb394216..068fec5a5ae6 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3194,6 +3194,7 @@ class NavigationBase(object): locked messagelock : `LockDraw` to know if the message is available to write """ + _default_cursor = cursors.POINTER _default_tools = [tools.ToolToggleGrid, tools.ToolToggleFullScreen, @@ -3213,6 +3214,7 @@ class NavigationBase(object): def __init__(self, canvas, toolbar=None): """.. automethod:: _toolbar_callback""" + self.canvas = canvas self.toolbar = self._get_toolbar(toolbar, canvas) @@ -3231,9 +3233,9 @@ def __init__(self, canvas, toolbar=None): self._instances = {} self._toggled = None - #to process keypress event + # to process keypress event self.keypresslock = widgets.LockDraw() - #to write into toolbar message + # to write into toolbar message self.messagelock = widgets.LockDraw() for tool in self._default_tools: @@ -3248,11 +3250,13 @@ def __init__(self, canvas, toolbar=None): @classmethod def get_default_tools(cls): """Get the default tools""" + return cls._default_tools @classmethod def set_default_tools(cls, tools): """Set default tools""" + cls._default_tools = tools def _get_toolbar(self, toolbar, canvas): @@ -3270,6 +3274,7 @@ def active_toggle(self): **string** : Currently toggled tool, or None """ + return self._toggled @property @@ -3278,6 +3283,7 @@ def instances(self): **dictionary** : Contains the active instances that are registered """ + return self._instances def get_tool_keymap(self, name): @@ -3292,6 +3298,7 @@ def get_tool_keymap(self, name): ---------- list : list of keys associated with the Tool """ + keys = [k for k, i in six.iteritems(self._keys) if i == name] return keys @@ -3338,6 +3345,7 @@ def unregister(self, name): If called, next time the `Tool` is used it will be reinstantiated instead of using the existing instance. """ + if self._toggled == name: self._handle_toggle(name, from_toolbar=False) if name in self._instances: @@ -3351,6 +3359,7 @@ def remove_tool(self, name): name : string Name of the Tool """ + self.unregister(name) del self._tools[name] keys = [k for k, v in six.iteritems(self._keys) if v == name] @@ -3368,6 +3377,7 @@ def add_tool(self, tool): tool : string or `Tool` class Reference to find the class of the Tool to be added """ + tool_cls = self._get_cls_to_instantiate(tool) name = tool_cls.name @@ -3403,7 +3413,7 @@ def add_tool(self, tool): def _get_cls_to_instantiate(self, callback_class): if isinstance(callback_class, six.string_types): - #FIXME: make more complete searching structure + # FIXME: make more complete searching structure if callback_class in globals(): return globals()[callback_class] @@ -3420,6 +3430,7 @@ def trigger_tool(self, name): Method to programatically "click" on Tools """ + self._trigger_tool(name, None, False) def _trigger_tool(self, name, event, from_toolbar): @@ -3433,8 +3444,7 @@ def _trigger_tool(self, name, event, from_toolbar): instance = self._get_instance(name) instance.trigger(event) else: - #Non persistent tools, are - #instantiated and forgotten (reminds me an exgirlfriend?) + # Non persistent tools, are instantiated and forgotten tool(self.canvas.figure, event) def _key_press(self, event): @@ -3449,7 +3459,7 @@ def _key_press(self, event): def _get_instance(self, name): if name not in self._instances: instance = self._tools[name](self.canvas.figure) - #register instance + # register instance self._instances[name] = instance return self._instances[name] @@ -3466,24 +3476,25 @@ def _toolbar_callback(self, name): Name of the tool that was activated (click) by the user using the toolbar """ + self._trigger_tool(name, None, True) def _handle_toggle(self, name, event=None, from_toolbar=False): - #toggle toolbar without callback + # toggle toolbar without callback if not from_toolbar and self.toolbar: self.toolbar._toggle(name, False) instance = self._get_instance(name) if self._toggled is None: - #first trigger of tool + # first trigger of tool self._toggled = name elif self._toggled == name: - #second trigger of tool + # second trigger of tool self._toggled = None else: - #other tool is triggered so trigger toggled tool + # other tool is triggered so trigger toggled tool if self.toolbar: - #untoggle the previous toggled tool + # untoggle the previous toggled tool self.toolbar._toggle(self._toggled, False) self._get_instance(self._toggled).trigger(event) self._toggled = name @@ -3495,6 +3506,7 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): def list_tools(self): """Print the list the tools controlled by `Navigation`""" + print ('_' * 80) print ("{0:20} {1:50} {2}".format('Name (id)', 'Tool description', 'Keymap')) @@ -3508,6 +3520,7 @@ def list_tools(self): def update(self): """Reset the axes stack""" + self.views.clear() self.positions.clear() # self.set_history_buttons() @@ -3543,9 +3556,11 @@ def _mouse_move(self, event): def draw(self): """Redraw the canvases, update the locators""" + for a in self.canvas.figure.get_axes(): xaxis = getattr(a, 'xaxis', None) yaxis = getattr(a, 'yaxis', None) + zaxis = getattr(a, 'zaxis', None) locators = [] if xaxis is not None: locators.append(xaxis.get_major_locator()) @@ -3553,6 +3568,9 @@ def draw(self): if yaxis is not None: locators.append(yaxis.get_major_locator()) locators.append(yaxis.get_minor_locator()) + if zaxis is not None: + locators.append(zaxis.get_major_locator()) + locators.append(zaxis.get_minor_locator()) for loc in locators: loc.refresh() @@ -3566,6 +3584,7 @@ def set_cursor(self, cursor): Set the current cursor to one of the :class:`Cursors` enums values """ + pass def update_view(self): @@ -3591,6 +3610,7 @@ def update_view(self): def push_current(self): """push the current view limits and position onto the stack""" + lims = [] pos = [] for a in self.canvas.figure.get_axes(): @@ -3617,6 +3637,7 @@ def draw_rubberband(self, event, caller, x0, y0, x1, y1): caller : instance trying to draw the rubberband x0, y0, x1, y1 : coordinates """ + if not self.canvas.widgetlock.available(caller): warnings.warn("%s doesn't own the canvas widgetlock" % caller) @@ -3631,6 +3652,7 @@ def remove_rubberband(self, event, caller): event : `FigureCanvas` event caller : instance trying to remove the rubberband """ + if not self.canvas.widgetlock.available(caller): warnings.warn("%s doesn't own the canvas widgetlock" % caller) @@ -3642,12 +3664,14 @@ class ToolbarBase(object): ---------- manager : `FigureManager` instance that integrates this `Toolbar` """ + def __init__(self, manager): """ .. automethod:: _add_toolitem .. automethod:: _remove_toolitem .. automethod:: _toggle """ + self.manager = manager def _add_toolitem(self, name, description, image_file, position, @@ -3675,6 +3699,7 @@ def _add_toolitem(self, name, description, image_file, position, * `False` : The button is a normal button (returns to unpressed state after release) """ + raise NotImplementedError def add_separator(self, pos): @@ -3686,10 +3711,12 @@ def add_separator(self, pos): Position where to add the separator within the toolitems if -1 at the end """ + pass def set_message(self, s): """Display a message on toolbar or in status bar""" + pass def _toggle(self, name, callback=False): @@ -3704,7 +3731,8 @@ def _toggle(self, name, callback=False): * `False`: toggle the button without calling the callback """ - #carefull, callback means to perform or not the callback while toggling + + # carefull, callback means to perform or not the callback while toggling raise NotImplementedError def _remove_toolitem(self, name): @@ -3716,6 +3744,7 @@ def _remove_toolitem(self, name): Name of the tool to remove """ + raise NotImplementedError def move_toolitem(self, pos_ini, pos_fin): @@ -3728,6 +3757,7 @@ def move_toolitem(self, pos_ini, pos_fin): pos_fin : integer Final position of the toolitem """ + pass def set_toolitem_visibility(self, name, visible): @@ -3741,4 +3771,5 @@ def set_toolitem_visibility(self, name, visible): * `True`: set the toolitem visible * `False`: set the toolitem invisible """ + pass diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index d54442744504..4624ea3e53f8 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -49,7 +49,6 @@ class ToolBase(object): * **integer** : Position within the Toolbar * **None** : Do not put in the Toolbar * **-1**: At the end of the Toolbar - """ description = None @@ -73,8 +72,7 @@ class ToolBase(object): """ cursor = None - """Cursor to use when the tool is active - """ + """Cursor to use when the tool is active""" def __init__(self, figure, event=None): self.figure = None @@ -90,6 +88,7 @@ def trigger(self, event): event : `Event` Event that caused this tool to be called """ + pass def set_figure(self, figure): @@ -101,6 +100,7 @@ def set_figure(self, figure): ---------- figure : `Figure` """ + self.figure = figure self.navigation = figure.canvas.manager.navigation @@ -121,8 +121,8 @@ def __init__(self, figure, event=None): self.figure = None self.navigation = None self.set_figure(figure) - #persistent tools don't call trigger a at instantiation - #it will be called by Navigation + # persistent tools don't call trigger a at instantiation + # it will be called by Navigation def unregister(self, *args): """Unregister the tool from the instances of Navigation @@ -130,7 +130,8 @@ def unregister(self, *args): If the reference in navigation was the last reference to the instance of the tool, it will be garbage collected """ - #call this to unregister from navigation + + # call this to unregister from navigation self.navigation.unregister(self.name) @@ -139,8 +140,8 @@ class ToolToggleBase(ToolPersistentBase): This tool is a Persistent Tool that has a toggled state. Every time it is triggered, it switches between enable and disable - """ + _toggled = False def trigger(self, event): @@ -155,6 +156,7 @@ def enable(self, event=None): This method is called when the tool is triggered and not toggled """ + pass def disable(self, event=None): @@ -164,17 +166,19 @@ def disable(self, event=None): * Second click on the toolbar tool button * Another toogle tool is triggered (from the same `navigation`) """ + pass @property def toggled(self): """State of the toggled tool""" + return self._toggled class ToolQuit(ToolBase): - """Tool to call the figure manager destroy method - """ + """Tool to call the figure manager destroy method""" + name = 'Quit' description = 'Quit the figure' keymap = rcParams['keymap.quit'] @@ -184,8 +188,8 @@ def trigger(self, event): class ToolEnableAllNavigation(ToolBase): - """Tool to enable all axes for navigation interaction - """ + """Tool to enable all axes for navigation interaction""" + name = 'EnableAll' description = 'Enables all axes navigation' keymap = rcParams['keymap.all_axes'] @@ -201,8 +205,8 @@ def trigger(self, event): class ToolEnableNavigation(ToolBase): - """Tool to enable a specific axes for navigation interaction - """ + """Tool to enable a specific axes for navigation interaction""" + name = 'EnableOne' description = 'Enables one axes navigation' keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9) @@ -222,6 +226,7 @@ def trigger(self, event): class ToolToggleGrid(ToolBase): """Tool to toggle the grid of the figure""" + name = 'Grid' description = 'Toogle Grid' keymap = rcParams['keymap.grid'] @@ -235,6 +240,7 @@ def trigger(self, event): class ToolToggleFullScreen(ToolBase): """Tool to toggle full screen""" + name = 'Fullscreen' description = 'Toogle Fullscreen mode' keymap = rcParams['keymap.fullscreen'] @@ -245,6 +251,7 @@ def trigger(self, event): class ToolToggleYScale(ToolBase): """Tool to toggle between linear and logarithmic the Y axis""" + name = 'YScale' description = 'Toogle Scale Y axis' keymap = rcParams['keymap.yscale'] @@ -265,6 +272,7 @@ def trigger(self, event): class ToolToggleXScale(ToolBase): """Tool to toggle between linear and logarithmic the X axis""" + name = 'XScale' description = 'Toogle Scale X axis' keymap = rcParams['keymap.xscale'] @@ -285,6 +293,7 @@ def trigger(self, event): class ToolHome(ToolBase): """Restore the original view""" + description = 'Reset original view' name = 'Home' image = 'home' @@ -300,6 +309,7 @@ def trigger(self, *args): class ToolBack(ToolBase): """move back up the view lim stack""" + description = 'Back to previous view' name = 'Back' image = 'back' @@ -315,6 +325,7 @@ def trigger(self, *args): class ToolForward(ToolBase): """Move forward in the view lim stack""" + description = 'Forward to next view' name = 'Forward' image = 'forward' @@ -330,6 +341,7 @@ def trigger(self, *args): class ConfigureSubplotsBase(ToolPersistentBase): """Base tool for the configuration of subplots""" + description = 'Configure subplots' name = 'Subplots' image = 'subplots' @@ -338,6 +350,7 @@ class ConfigureSubplotsBase(ToolPersistentBase): class SaveFigureBase(ToolBase): """Base tool for figure saving""" + description = 'Save the figure' name = 'Save' image = 'filesave' @@ -347,6 +360,7 @@ class SaveFigureBase(ToolBase): class ToolZoom(ToolToggleBase): """Zoom to rectangle""" + description = 'Zoom to rectangle' name = 'Zoom' image = 'zoom_to_rect' @@ -387,6 +401,7 @@ def _cancel_zoom(self): def _press(self, event): """the _press mouse button in zoom to rect mode callback""" + # If we're already in the middle of a zoom, pressing another # button works to "cancel" if self._ids_zoom != []: @@ -434,6 +449,7 @@ def _switch_off_zoom_mode(self, event): def _mouse_move(self, event): """the drag callback in zoom mode""" + if self._xypress: x, y = event.x, event.y lastx, lasty, a, _ind, _lim, _trans = self._xypress[0] @@ -454,6 +470,7 @@ def _mouse_move(self, event): def _release(self, event): """the release mouse button callback in zoom to rect mode""" + for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) self._ids_zoom = [] @@ -576,6 +593,7 @@ def _release(self, event): class ToolPan(ToolToggleBase): """Pan axes with left mouse, zoom with right""" + keymap = rcParams['keymap.pan'] name = 'Pan' description = 'Pan axes with left mouse, zoom with right' @@ -623,7 +641,7 @@ def _press(self, event): x, y = event.x, event.y # push the current view to define home if stack is empty - #TODO: add define_home in navigation + # TODO: add define_home in navigation if self.navigation.views.empty(): self.navigation.push_current() @@ -656,7 +674,7 @@ def _release(self, event): def _mouse_move(self, event): for a, _ind in self._xypress: - #safer to use the recorded button at the _press than current - #button: #multiple button can get pressed during motion... + # safer to use the recorded button at the _press than current + # button: # multiple button can get pressed during motion... a.drag_pan(self._button_pressed, event.key, event.x, event.y) self.navigation.dynamic_update() From bb9f1c7cd4bb122864ddf03ae9ace0e87d513f01 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 08:48:51 -0400 Subject: [PATCH 20/41] removing legacy method dynamic update --- lib/matplotlib/backend_bases.py | 3 --- lib/matplotlib/backend_tools.py | 2 +- lib/matplotlib/backends/backend_gtk3.py | 4 ---- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 068fec5a5ae6..07702568fcbf 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3576,9 +3576,6 @@ def draw(self): loc.refresh() self.canvas.draw_idle() - def dynamic_update(self): - pass - def set_cursor(self, cursor): """ Set the current cursor to one of the :class:`Cursors` diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 4624ea3e53f8..7a87b3ad30d8 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -677,4 +677,4 @@ def _mouse_move(self, event): # safer to use the recorded button at the _press than current # button: # multiple button can get pressed during motion... a.drag_pan(self._button_pressed, event.key, event.x, event.y) - self.navigation.dynamic_update() + self.navigation.canvas.draw_idle() diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 6254256dda8d..a9f3545c4b12 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -740,10 +740,6 @@ def draw_rubberband(self, event, caller, x0, y0, x1, y1): self.ctx.set_source_rgb(0, 0, 0) self.ctx.stroke() - def dynamic_update(self): - # legacy method; new method is canvas.draw_idle - self.canvas.draw_idle() - class ToolbarGTK3(ToolbarBase, Gtk.Box,): def __init__(self, manager): From 2c2e649070b707d1aff721c6ff82931e5c4d282c Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 12:22:28 -0400 Subject: [PATCH 21/41] tk backend --- examples/user_interfaces/navigation.py | 3 +- lib/matplotlib/backend_bases.py | 7 +- lib/matplotlib/backend_tools.py | 14 +- lib/matplotlib/backends/backend_tkagg.py | 193 ++++++++++++++++++++++- 4 files changed, 202 insertions(+), 15 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 8d1f56907d46..705e493919ef 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -1,5 +1,6 @@ import matplotlib -matplotlib.use('GTK3Cairo') +# matplotlib.use('GTK3Cairo') +matplotlib.use('TkAGG') matplotlib.rcParams['toolbar'] = 'navigation' import matplotlib.pyplot as plt from matplotlib.backend_tools import ToolBase diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 07702568fcbf..0e6d8193e94b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3379,6 +3379,9 @@ def add_tool(self, tool): """ tool_cls = self._get_cls_to_instantiate(tool) + if tool_cls is False: + warnings.warn('Impossible to find class for %s' % str(tool)) + return name = tool_cls.name if name is None: @@ -3400,9 +3403,11 @@ def add_tool(self, tool): self._keys[k] = name if self.toolbar and tool_cls.position is not None: + # TODO: better search for images, they are not always in the + # datapath basedir = os.path.join(rcParams['datapath'], 'images') if tool_cls.image is not None: - fname = os.path.join(basedir, tool_cls.image + '.png') + fname = os.path.join(basedir, tool_cls.image) else: fname = None toggle = issubclass(tool_cls, tools.ToolToggleBase) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 7a87b3ad30d8..edc66903e3ab 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -296,7 +296,7 @@ class ToolHome(ToolBase): description = 'Reset original view' name = 'Home' - image = 'home' + image = 'home.png' keymap = rcParams['keymap.home'] position = -1 @@ -312,7 +312,7 @@ class ToolBack(ToolBase): description = 'Back to previous view' name = 'Back' - image = 'back' + image = 'back.png' keymap = rcParams['keymap.back'] position = -1 @@ -328,7 +328,7 @@ class ToolForward(ToolBase): description = 'Forward to next view' name = 'Forward' - image = 'forward' + image = 'forward.png' keymap = rcParams['keymap.forward'] position = -1 @@ -344,7 +344,7 @@ class ConfigureSubplotsBase(ToolPersistentBase): description = 'Configure subplots' name = 'Subplots' - image = 'subplots' + image = 'subplots.png' position = -1 @@ -353,7 +353,7 @@ class SaveFigureBase(ToolBase): description = 'Save the figure' name = 'Save' - image = 'filesave' + image = 'filesave.png' position = -1 keymap = rcParams['keymap.save'] @@ -363,7 +363,7 @@ class ToolZoom(ToolToggleBase): description = 'Zoom to rectangle' name = 'Zoom' - image = 'zoom_to_rect' + image = 'zoom_to_rect.png' position = -1 keymap = rcParams['keymap.zoom'] cursor = cursors.SELECT_REGION @@ -597,7 +597,7 @@ class ToolPan(ToolToggleBase): keymap = rcParams['keymap.pan'] name = 'Pan' description = 'Pan axes with left mouse, zoom with right' - image = 'move' + image = 'move.png' position = -1 cursor = cursors.MOVE diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 3625dc5e666f..d31e0ab23e42 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -20,7 +20,8 @@ from matplotlib.backend_bases import RendererBase, GraphicsContextBase from matplotlib.backend_bases import FigureManagerBase, FigureCanvasBase from matplotlib.backend_bases import NavigationToolbar2, cursors, TimerBase -from matplotlib.backend_bases import ShowBase +from matplotlib.backend_bases import ShowBase, ToolbarBase, NavigationBase +from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure @@ -531,13 +532,11 @@ def __init__(self, canvas, num, window): self.set_window_title("Figure %d" % num) self.canvas = canvas self._num = num + self.navigation = None _, _, w, h = canvas.figure.bbox.bounds w, h = int(w), int(h) self.window.minsize(int(w*3/4),int(h*3/4)) - if matplotlib.rcParams['toolbar']=='toolbar2': - self.toolbar = NavigationToolbar2TkAgg( canvas, self.window ) - else: - self.toolbar = None + self.toolbar = self._get_toolbar(canvas) if self.toolbar is not None: self.toolbar.update() self.canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) @@ -545,9 +544,22 @@ def __init__(self, canvas, num, window): def notify_axes_change(fig): 'this will be called whenever the current axes is changed' - if self.toolbar != None: self.toolbar.update() + if self.navigation is not None: + self.navigation.update() + elif self.toolbar is not None: self.toolbar.update() self.canvas.figure.add_axobserver(notify_axes_change) + def _get_toolbar(self, canvas): + if matplotlib.rcParams['toolbar']=='toolbar2': + toolbar = NavigationToolbar2TkAgg( canvas, self.window ) + elif matplotlib.rcParams['toolbar'] == 'navigation': + self.navigation = NavigationTk(canvas, ToolbarTk) + toolbar = self.navigation.toolbar + else: + self.navigation = NavigationTk(canvas, None) + toolbar = None + return toolbar + def resize(self, width, height=None): # before 09-12-22, the resize method takes a single *event* # parameter. On the other hand, the resize method of other @@ -874,5 +886,174 @@ def hidetip(self): if tw: tw.destroy() + +class NavigationTk(NavigationBase): + def __init__(self, *args, **kwargs): + NavigationBase.__init__(self, *args, **kwargs) + + def set_cursor(self, cursor): + self.canvas.manager.window.configure(cursor=cursord[cursor]) + + def draw_rubberband(self, event, caller, x0, y0, x1, y1): + if not self.canvas.widgetlock.available(caller): + return + height = self.canvas.figure.bbox.height + y0 = height-y0 + y1 = height-y1 + try: self.lastrect + except AttributeError: pass + else: self.canvas._tkcanvas.delete(self.lastrect) + self.lastrect = self.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1) + + def remove_rubberband(self, event, caller): + try: self.lastrect + except AttributeError: pass + else: + self.canvas._tkcanvas.delete(self.lastrect) + del self.lastrect + + +class ToolbarTk(ToolbarBase, Tk.Frame): + def __init__(self, manager): + ToolbarBase.__init__(self, manager) + xmin, xmax = self.manager.canvas.figure.bbox.intervalx + height, width = 50, xmax-xmin + Tk.Frame.__init__(self, master=self.manager.window, + width=int(width), height=int(height), + borderwidth=2) + self._toolitems = {} + self._add_message() + + def _add_toolitem(self, name, tooltip_text, image_file, position, + toggle): + + button = self._Button(name, image_file, toggle) + if tooltip_text is not None: + ToolTip.createToolTip(button, tooltip_text) + self._toolitems[name] = button + + def _Button(self, text, file, toggle): + extension='.ppm' + if file is not None: + img_file = os.path.join(rcParams['datapath'], 'images', file ) + im = Tk.PhotoImage(master=self, file=img_file) + else: + im = None + + if not toggle: + b = Tk.Button( + master=self, text=text, padx=2, pady=2, image=im, + command=lambda: self._button_click(text)) + else: + b = Tk.Checkbutton(master=self, text=text, padx=2, pady=2, + image=im, indicatoron=False, + command=lambda: self._button_click(text)) + b._ntimage = im + b.pack(side=Tk.LEFT) + return b + + def _button_click(self, name): + self.manager.navigation._toolbar_callback(name) + + def _toggle(self, name, callback=False): + if name not in self._toolitems: + self.set_message('%s Not in toolbar' % name) + return + self._toolitems[name].toggle() + if callback: + self._button_click(name) + + def _add_message(self): + self.message = Tk.StringVar(master=self) + self._message_label = Tk.Label(master=self, textvariable=self.message) + self._message_label.pack(side=Tk.RIGHT) + self.pack(side=Tk.BOTTOM, fill=Tk.X) + + def set_message(self, s): + self.message.set(s) + + def _remove_toolitem(self, name): + self._toolitems[name].pack_forget() + del self._toolitems[name] + + def set_toolitem_visibility(self, name, visible): + pass + +class SaveFigureTk(SaveFigureBase): + def trigger(self, *args): + from six.moves import tkinter_tkfiledialog, tkinter_messagebox + filetypes = self.figure.canvas.get_supported_filetypes().copy() + default_filetype = self.figure.canvas.get_default_filetype() + + # Tk doesn't provide a way to choose a default filetype, + # so we just have to put it first + default_filetype_name = filetypes[default_filetype] + del filetypes[default_filetype] + + sorted_filetypes = list(six.iteritems(filetypes)) + sorted_filetypes.sort() + sorted_filetypes.insert(0, (default_filetype, default_filetype_name)) + + tk_filetypes = [ + (name, '*.%s' % ext) for (ext, name) in sorted_filetypes] + + # adding a default extension seems to break the + # asksaveasfilename dialog when you choose various save types + # from the dropdown. Passing in the empty string seems to + # work - JDH! + #defaultextension = self.figure.canvas.get_default_filetype() + defaultextension = '' + initialdir = rcParams.get('savefig.directory', '') + initialdir = os.path.expanduser(initialdir) + initialfile = self.figure.canvas.get_default_filename() + fname = tkinter_tkfiledialog.asksaveasfilename( + master=self.figure.canvas.manager.window, + title='Save the figure', + filetypes=tk_filetypes, + defaultextension=defaultextension, + initialdir=initialdir, + initialfile=initialfile, + ) + + if fname == "" or fname == (): + return + else: + if initialdir == '': + # explicitly missing key or empty str signals to use cwd + rcParams['savefig.directory'] = initialdir + else: + # save dir for next time + rcParams['savefig.directory'] = os.path.dirname(six.text_type(fname)) + try: + # This method will handle the delegation to the correct type + self.figure.canvas.print_figure(fname) + except Exception as e: + tkinter_messagebox.showerror("Error saving file", str(e)) + + +class ConfigureSubplotsTk(ConfigureSubplotsBase): + def __init__(self, *args, **kwargs): + ConfigureSubplotsBase.__init__(self, *args, **kwargs) + toolfig = Figure(figsize=(6,3)) + self.window = Tk.Tk() + + canvas = FigureCanvasTkAgg(toolfig, master=self.window) + toolfig.subplots_adjust(top=0.9) + tool = SubplotTool(self.figure, toolfig) + canvas.show() + canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) + self.window.protocol("WM_DELETE_WINDOW", self.destroy) + + def trigger(self, event): + self.window.lift() + + def destroy(self, *args, **kwargs): + self.unregister() + self.window.destroy() + + +SaveFigure = SaveFigureTk +ConfigureSubplots = ConfigureSubplotsTk + FigureCanvas = FigureCanvasTkAgg FigureManager = FigureManagerTkAgg From a99367f48b9a27bbd1bdd38ce9d424a2edd54bc4 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 12:33:00 -0400 Subject: [PATCH 22/41] pep8 --- lib/matplotlib/backends/backend_tkagg.py | 49 ++++++++++++++---------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index d31e0ab23e42..1e562b748450 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -546,12 +546,13 @@ def notify_axes_change(fig): 'this will be called whenever the current axes is changed' if self.navigation is not None: self.navigation.update() - elif self.toolbar is not None: self.toolbar.update() + elif self.toolbar is not None: + self.toolbar.update() self.canvas.figure.add_axobserver(notify_axes_change) def _get_toolbar(self, canvas): - if matplotlib.rcParams['toolbar']=='toolbar2': - toolbar = NavigationToolbar2TkAgg( canvas, self.window ) + if matplotlib.rcParams['toolbar'] == 'toolbar2': + toolbar = NavigationToolbar2TkAgg(canvas, self.window) elif matplotlib.rcParams['toolbar'] == 'navigation': self.navigation = NavigationTk(canvas, ToolbarTk) toolbar = self.navigation.toolbar @@ -898,16 +899,21 @@ def draw_rubberband(self, event, caller, x0, y0, x1, y1): if not self.canvas.widgetlock.available(caller): return height = self.canvas.figure.bbox.height - y0 = height-y0 - y1 = height-y1 - try: self.lastrect - except AttributeError: pass - else: self.canvas._tkcanvas.delete(self.lastrect) + y0 = height - y0 + y1 = height - y1 + try: + self.lastrect + except AttributeError: + pass + else: + self.canvas._tkcanvas.delete(self.lastrect) self.lastrect = self.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1) def remove_rubberband(self, event, caller): - try: self.lastrect - except AttributeError: pass + try: + self.lastrect + except AttributeError: + pass else: self.canvas._tkcanvas.delete(self.lastrect) del self.lastrect @@ -917,7 +923,7 @@ class ToolbarTk(ToolbarBase, Tk.Frame): def __init__(self, manager): ToolbarBase.__init__(self, manager) xmin, xmax = self.manager.canvas.figure.bbox.intervalx - height, width = 50, xmax-xmin + height, width = 50, xmax - xmin Tk.Frame.__init__(self, master=self.manager.window, width=int(width), height=int(height), borderwidth=2) @@ -931,15 +937,14 @@ def _add_toolitem(self, name, tooltip_text, image_file, position, if tooltip_text is not None: ToolTip.createToolTip(button, tooltip_text) self._toolitems[name] = button - + def _Button(self, text, file, toggle): - extension='.ppm' if file is not None: - img_file = os.path.join(rcParams['datapath'], 'images', file ) + img_file = os.path.join(rcParams['datapath'], 'images', file) im = Tk.PhotoImage(master=self, file=img_file) else: im = None - + if not toggle: b = Tk.Button( master=self, text=text, padx=2, pady=2, image=im, @@ -951,7 +956,7 @@ def _Button(self, text, file, toggle): b._ntimage = im b.pack(side=Tk.LEFT) return b - + def _button_click(self, name): self.manager.navigation._toolbar_callback(name) @@ -979,6 +984,7 @@ def _remove_toolitem(self, name): def set_toolitem_visibility(self, name, visible): pass + class SaveFigureTk(SaveFigureBase): def trigger(self, *args): from six.moves import tkinter_tkfiledialog, tkinter_messagebox @@ -1023,7 +1029,8 @@ def trigger(self, *args): rcParams['savefig.directory'] = initialdir else: # save dir for next time - rcParams['savefig.directory'] = os.path.dirname(six.text_type(fname)) + rcParams['savefig.directory'] = os.path.dirname( + six.text_type(fname)) try: # This method will handle the delegation to the correct type self.figure.canvas.print_figure(fname) @@ -1034,16 +1041,16 @@ def trigger(self, *args): class ConfigureSubplotsTk(ConfigureSubplotsBase): def __init__(self, *args, **kwargs): ConfigureSubplotsBase.__init__(self, *args, **kwargs) - toolfig = Figure(figsize=(6,3)) + toolfig = Figure(figsize=(6, 3)) self.window = Tk.Tk() - + canvas = FigureCanvasTkAgg(toolfig, master=self.window) toolfig.subplots_adjust(top=0.9) - tool = SubplotTool(self.figure, toolfig) + _tool = SubplotTool(self.figure, toolfig) canvas.show() canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) self.window.protocol("WM_DELETE_WINDOW", self.destroy) - + def trigger(self, event): self.window.lift() From 3d1be3428fef8ccf1228b39646c19418edf7edb7 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 13:56:47 -0400 Subject: [PATCH 23/41] example working with Tk --- examples/user_interfaces/navigation.py | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 705e493919ef..fe4762e63351 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -1,33 +1,34 @@ import matplotlib -# matplotlib.use('GTK3Cairo') -matplotlib.use('TkAGG') +matplotlib.use('GTK3Cairo') +# matplotlib.use('TkAGG') matplotlib.rcParams['toolbar'] = 'navigation' import matplotlib.pyplot as plt from matplotlib.backend_tools import ToolBase -#Create a simple tool to list all the tools +# Create a simple tool to list all the tools class ListTools(ToolBase): - #keyboard shortcut + # keyboard shortcut keymap = 'm' - #Name used as id, must be unique between tools of the same navigation + # Name used as id, must be unique between tools of the same navigation name = 'List' description = 'List Tools' - #Where to put it in the toolbar, -1 = at the end, None = Not in toolbar + # Where to put it in the toolbar, -1 = at the end, None = Not in toolbar position = -1 def trigger(self, event): - #The most important attributes are navigation and figure + # The most important attributes are navigation and figure self.navigation.list_tools() -#A simple example of copy canvas -#ref: at https://github.com/matplotlib/matplotlib/issues/1987 -class CopyTool(ToolBase): +# A simple example of copy canvas +# ref: at https://github.com/matplotlib/matplotlib/issues/1987 +class CopyToolGTK3(ToolBase): keymap = 'ctrl+c' name = 'Copy' description = 'Copy canvas' - position = -1 + # It is not added to the toolbar as a button + position = None def trigger(self, event): from gi.repository import Gtk, Gdk @@ -41,13 +42,12 @@ def trigger(self, event): fig = plt.figure() plt.plot([1, 2, 3]) -#If we are in the old toolbar, don't try to modify it -if matplotlib.rcParams['toolbar'] in ('navigation', 'None'): - ##Add the custom tools that we created - fig.canvas.manager.navigation.add_tool(ListTools) - fig.canvas.manager.navigation.add_tool(CopyTool) +# Add the custom tools that we created +fig.canvas.manager.navigation.add_tool(ListTools) +if matplotlib.rcParams['backend'] == 'GTK3Cairo': + fig.canvas.manager.navigation.add_tool(CopyToolGTK3) - ##Just for fun, lets remove the back button - fig.canvas.manager.navigation.remove_tool('Back') +# Just for fun, lets remove the back button +fig.canvas.manager.navigation.remove_tool('Back') plt.show() From afdd34c9d9116fec40f71e148f69bc05900c7c76 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 7 May 2014 14:06:20 -0400 Subject: [PATCH 24/41] cleanup --- examples/user_interfaces/navigation.py | 26 +++--- lib/matplotlib/backend_bases.py | 110 ++++++++--------------- lib/matplotlib/backend_tools.py | 92 ++++++++++--------- lib/matplotlib/backends/backend_gtk3.py | 34 ++++--- lib/matplotlib/backends/backend_tkagg.py | 37 ++++---- 5 files changed, 138 insertions(+), 161 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index fe4762e63351..b1f91e7886d6 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -10,25 +10,31 @@ class ListTools(ToolBase): # keyboard shortcut keymap = 'm' - # Name used as id, must be unique between tools of the same navigation - name = 'List' description = 'List Tools' - # Where to put it in the toolbar, -1 = at the end, None = Not in toolbar - position = -1 def trigger(self, event): - # The most important attributes are navigation and figure - self.navigation.list_tools() + tools = self.navigation.get_tools() + + print ('_' * 80) + print ("{0:12} {1:45} {2}".format('Name (id)', + 'Tool description', + 'Keymap')) + print ('_' * 80) + for name in sorted(tools.keys()): + keys = ', '.join(sorted(tools[name]['keymap'])) + print ("{0:12} {1:45} {2}".format(name, + tools[name]['description'], + keys)) + print ('_' * 80) # A simple example of copy canvas # ref: at https://github.com/matplotlib/matplotlib/issues/1987 class CopyToolGTK3(ToolBase): keymap = 'ctrl+c' - name = 'Copy' description = 'Copy canvas' # It is not added to the toolbar as a button - position = None + intoolbar = False def trigger(self, event): from gi.repository import Gtk, Gdk @@ -43,9 +49,9 @@ def trigger(self, event): plt.plot([1, 2, 3]) # Add the custom tools that we created -fig.canvas.manager.navigation.add_tool(ListTools) +fig.canvas.manager.navigation.add_tool('List', ListTools) if matplotlib.rcParams['backend'] == 'GTK3Cairo': - fig.canvas.manager.navigation.add_tool(CopyToolGTK3) + fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) # Just for fun, lets remove the back button fig.canvas.manager.navigation.remove_tool('Back') diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 0e6d8193e94b..39169dc97399 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3188,7 +3188,7 @@ class NavigationBase(object): Attributes ---------- - canvas : `FigureCanvas` instance + manager : `FigureManager` instance toolbar : `Toolbar` instance that is controlled by this `Navigation` keypresslock : `LockDraw` to know if the `canvas` key_press_event is locked @@ -3196,33 +3196,19 @@ class NavigationBase(object): """ _default_cursor = cursors.POINTER - _default_tools = [tools.ToolToggleGrid, - tools.ToolToggleFullScreen, - tools.ToolQuit, - tools.ToolEnableAllNavigation, - tools.ToolEnableNavigation, - tools.ToolToggleXScale, - tools.ToolToggleYScale, - tools.ToolHome, tools.ToolBack, - tools.ToolForward, - None, - tools.ToolZoom, - tools.ToolPan, - None, - 'ConfigureSubplots', - 'SaveFigure'] - - def __init__(self, canvas, toolbar=None): + + def __init__(self, manager): """.. automethod:: _toolbar_callback""" - self.canvas = canvas - self.toolbar = self._get_toolbar(toolbar, canvas) + self.manager = manager + self.canvas = manager.canvas + self.toolbar = manager.toolbar - self._key_press_handler_id = self.canvas.mpl_connect('key_press_event', - self._key_press) + self._key_press_handler_id = self.canvas.mpl_connect( + 'key_press_event', self._key_press) - self._idDrag = self.canvas.mpl_connect('motion_notify_event', - self._mouse_move) + self._idDrag = self.canvas.mpl_connect( + 'motion_notify_event', self._mouse_move) # a dict from axes index to a list of view limits self.views = cbook.Stack() @@ -3238,36 +3224,15 @@ def __init__(self, canvas, toolbar=None): # to write into toolbar message self.messagelock = widgets.LockDraw() - for tool in self._default_tools: + for name, tool in tools.tools: if tool is None: if self.toolbar is not None: self.toolbar.add_separator(-1) else: - self.add_tool(tool) + self.add_tool(name, tool, None) self._last_cursor = self._default_cursor - @classmethod - def get_default_tools(cls): - """Get the default tools""" - - return cls._default_tools - - @classmethod - def set_default_tools(cls, tools): - """Set default tools""" - - cls._default_tools = tools - - def _get_toolbar(self, toolbar, canvas): - # must be inited after the window, drawingArea and figure - # attrs are set - if rcParams['toolbar'] == 'navigation' and toolbar is not None: - toolbar = toolbar(canvas.manager) - else: - toolbar = None - return toolbar - @property def active_toggle(self): """Toggled Tool @@ -3339,8 +3304,7 @@ def unregister(self, name): This method is used by `PersistentTools` to remove the reference kept by `Navigation`. - It is usually called by the `deactivate` method or during - destroy if it is a graphical Tool. + It is usually called by the `unregister` method If called, next time the `Tool` is used it will be reinstantiated instead of using the existing instance. @@ -3369,29 +3333,27 @@ def remove_tool(self, name): if self.toolbar: self.toolbar._remove_toolitem(name) - def add_tool(self, tool): + def add_tool(self, name, tool, position=None): """Add tool to `Navigation` Parameters ---------- + name : string + Name of the tool, treated as the ID, has to be unique tool : string or `Tool` class Reference to find the class of the Tool to be added + position : int or None (default) + Position in the toolbar, if None, is positioned at the end """ tool_cls = self._get_cls_to_instantiate(tool) if tool_cls is False: warnings.warn('Impossible to find class for %s' % str(tool)) return - name = tool_cls.name - if name is None: - warnings.warn('tool_clss need a name to be added, it is used ' - 'as ID') - return if name in self._tools: warnings.warn('A tool_cls with the same name already exist, ' 'not added') - return self._tools[name] = tool_cls @@ -3402,7 +3364,7 @@ def add_tool(self, tool): (k, self._keys[k], name)) self._keys[k] = name - if self.toolbar and tool_cls.position is not None: + if self.toolbar and tool_cls.intoolbar: # TODO: better search for images, they are not always in the # datapath basedir = os.path.join(rcParams['datapath'], 'images') @@ -3411,10 +3373,11 @@ def add_tool(self, tool): else: fname = None toggle = issubclass(tool_cls, tools.ToolToggleBase) - self.toolbar._add_toolitem(name, tool_cls.description, - fname, - tool_cls.position, - toggle) + self.toolbar._add_toolitem(name, + tool_cls.description, + fname, + position, + toggle) def _get_cls_to_instantiate(self, callback_class): if isinstance(callback_class, six.string_types): @@ -3463,7 +3426,7 @@ def _key_press(self, event): def _get_instance(self, name): if name not in self._instances: - instance = self._tools[name](self.canvas.figure) + instance = self._tools[name](self.canvas.figure, name) # register instance self._instances[name] = instance @@ -3509,26 +3472,23 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): for a in self.canvas.figure.get_axes(): a.set_navigate_mode(self._toggled) - def list_tools(self): - """Print the list the tools controlled by `Navigation`""" + def get_tools(self): + """Return the tools controlled by `Navigation`""" - print ('_' * 80) - print ("{0:20} {1:50} {2}".format('Name (id)', 'Tool description', - 'Keymap')) - print ('_' * 80) + d = {} for name in sorted(self._tools.keys()): tool = self._tools[name] keys = [k for k, i in six.iteritems(self._keys) if i == name] - print ("{0:20} {1:50} {2}".format(tool.name, tool.description, - ', '.join(keys))) - print ('_' * 80, '\n') + d[name] = {'cls': tool, + 'description': tool.description, + 'keymap': keys} + return d def update(self): """Reset the axes stack""" self.views.clear() self.positions.clear() -# self.set_history_buttons() def _mouse_move(self, event): if not event.inaxes or not self._toggled: @@ -3625,7 +3585,6 @@ def push_current(self): a.get_position().frozen())) self.views.push(lims) self.positions.push(pos) -# self.set_history_buttons() def draw_rubberband(self, event, caller, x0, y0, x1, y1): """Draw a rectangle rubberband to indicate zoom limits @@ -3677,7 +3636,7 @@ def __init__(self, manager): self.manager = manager def _add_toolitem(self, name, description, image_file, position, - toggle): + toggle): """Add a toolitem to the toolbar The callback associated with the button click event, @@ -3734,7 +3693,8 @@ def _toggle(self, name, callback=False): """ - # carefull, callback means to perform or not the callback while toggling + # carefull, callback means to perform or not the callback while + # toggling raise NotImplementedError def _remove_toolitem(self, name): diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index edc66903e3ab..a662a4f0972a 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -43,14 +43,6 @@ class ToolBase(object): tool when the keypress event of *self.figure.canvas* is emited """ - position = None - """Where to put the tool in the *Toolbar* - - * **integer** : Position within the Toolbar - * **None** : Do not put in the Toolbar - * **-1**: At the end of the Toolbar - """ - description = None """Description of the Tool @@ -58,12 +50,6 @@ class ToolBase(object): as Tooltip """ - name = None - """Name of the Tool - - **string**: Used as ID for the tool, must be unique - """ - image = None """Filename of the image @@ -71,6 +57,9 @@ class ToolBase(object): `name` is used as label in the toolbar button """ + intoolbar = True + """Add the tool to the toolbar""" + cursor = None """Cursor to use when the tool is active""" @@ -117,7 +106,8 @@ class ToolPersistentBase(ToolBase): is not called automatically at initialization """ - def __init__(self, figure, event=None): + def __init__(self, figure, name, event=None): + self._name = name self.figure = None self.navigation = None self.set_figure(figure) @@ -127,12 +117,15 @@ def __init__(self, figure, event=None): def unregister(self, *args): """Unregister the tool from the instances of Navigation + It is usually called by during destroy if it is a + graphical Tool. + If the reference in navigation was the last reference to the instance of the tool, it will be garbage collected """ # call this to unregister from navigation - self.navigation.unregister(self.name) + self.navigation.unregister(self._name) class ToolToggleBase(ToolPersistentBase): @@ -179,7 +172,7 @@ def toggled(self): class ToolQuit(ToolBase): """Tool to call the figure manager destroy method""" - name = 'Quit' + intoolbar = False description = 'Quit the figure' keymap = rcParams['keymap.quit'] @@ -190,7 +183,7 @@ def trigger(self, event): class ToolEnableAllNavigation(ToolBase): """Tool to enable all axes for navigation interaction""" - name = 'EnableAll' + intoolbar = False description = 'Enables all axes navigation' keymap = rcParams['keymap.all_axes'] @@ -207,7 +200,7 @@ def trigger(self, event): class ToolEnableNavigation(ToolBase): """Tool to enable a specific axes for navigation interaction""" - name = 'EnableOne' + intoolbar = False description = 'Enables one axes navigation' keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9) @@ -227,7 +220,7 @@ def trigger(self, event): class ToolToggleGrid(ToolBase): """Tool to toggle the grid of the figure""" - name = 'Grid' + intoolbar = False description = 'Toogle Grid' keymap = rcParams['keymap.grid'] @@ -241,7 +234,7 @@ def trigger(self, event): class ToolToggleFullScreen(ToolBase): """Tool to toggle full screen""" - name = 'Fullscreen' + intoolbar = False description = 'Toogle Fullscreen mode' keymap = rcParams['keymap.fullscreen'] @@ -252,9 +245,9 @@ def trigger(self, event): class ToolToggleYScale(ToolBase): """Tool to toggle between linear and logarithmic the Y axis""" - name = 'YScale' description = 'Toogle Scale Y axis' keymap = rcParams['keymap.yscale'] + intoolbar = False def trigger(self, event): ax = event.inaxes @@ -273,9 +266,9 @@ def trigger(self, event): class ToolToggleXScale(ToolBase): """Tool to toggle between linear and logarithmic the X axis""" - name = 'XScale' description = 'Toogle Scale X axis' keymap = rcParams['keymap.xscale'] + intoolbar = False def trigger(self, event): ax = event.inaxes @@ -295,10 +288,8 @@ class ToolHome(ToolBase): """Restore the original view""" description = 'Reset original view' - name = 'Home' image = 'home.png' keymap = rcParams['keymap.home'] - position = -1 def trigger(self, *args): self.navigation.views.home() @@ -311,10 +302,8 @@ class ToolBack(ToolBase): """move back up the view lim stack""" description = 'Back to previous view' - name = 'Back' image = 'back.png' keymap = rcParams['keymap.back'] - position = -1 def trigger(self, *args): self.navigation.views.back() @@ -327,10 +316,8 @@ class ToolForward(ToolBase): """Move forward in the view lim stack""" description = 'Forward to next view' - name = 'Forward' image = 'forward.png' keymap = rcParams['keymap.forward'] - position = -1 def trigger(self, *args): self.navigation.views.forward() @@ -343,18 +330,14 @@ class ConfigureSubplotsBase(ToolPersistentBase): """Base tool for the configuration of subplots""" description = 'Configure subplots' - name = 'Subplots' image = 'subplots.png' - position = -1 class SaveFigureBase(ToolBase): """Base tool for figure saving""" description = 'Save the figure' - name = 'Save' image = 'filesave.png' - position = -1 keymap = rcParams['keymap.save'] @@ -362,9 +345,7 @@ class ToolZoom(ToolToggleBase): """Zoom to rectangle""" description = 'Zoom to rectangle' - name = 'Zoom' image = 'zoom_to_rect.png' - position = -1 keymap = rcParams['keymap.zoom'] cursor = cursors.SELECT_REGION @@ -379,9 +360,9 @@ def __init__(self, *args): def enable(self, event): self.figure.canvas.widgetlock(self) self._idPress = self.figure.canvas.mpl_connect( - 'button_press_event', self._press) + 'button_press_event', self._press) self._idRelease = self.figure.canvas.mpl_connect( - 'button_release_event', self._release) + 'button_release_event', self._release) def disable(self, event): self._cancel_zoom() @@ -430,11 +411,11 @@ def _press(self, event): a.transData.frozen())) id1 = self.figure.canvas.mpl_connect( - 'motion_notify_event', self._mouse_move) - id2 = self.figure.canvas.mpl_connect('key_press_event', - self._switch_on_zoom_mode) - id3 = self.figure.canvas.mpl_connect('key_release_event', - self._switch_off_zoom_mode) + 'motion_notify_event', self._mouse_move) + id2 = self.figure.canvas.mpl_connect( + 'key_press_event', self._switch_on_zoom_mode) + id3 = self.figure.canvas.mpl_connect( + 'key_release_event', self._switch_off_zoom_mode) self._ids_zoom = id1, id2, id3 self._zoom_mode = event.key @@ -595,10 +576,8 @@ class ToolPan(ToolToggleBase): """Pan axes with left mouse, zoom with right""" keymap = rcParams['keymap.pan'] - name = 'Pan' description = 'Pan axes with left mouse, zoom with right' image = 'move.png' - position = -1 cursor = cursors.MOVE def __init__(self, *args): @@ -612,9 +591,9 @@ def __init__(self, *args): def enable(self, event): self.figure.canvas.widgetlock(self) self._idPress = self.figure.canvas.mpl_connect( - 'button_press_event', self._press) + 'button_press_event', self._press) self._idRelease = self.figure.canvas.mpl_connect( - 'button_release_event', self._release) + 'button_release_event', self._release) def disable(self, event): self._cancel_pan() @@ -653,7 +632,7 @@ def _press(self, event): self._xypress.append((a, i)) self.navigation.messagelock(self) self._idDrag = self.figure.canvas.mpl_connect( - 'motion_notify_event', self._mouse_move) + 'motion_notify_event', self._mouse_move) def _release(self, event): if self._button_pressed is None: @@ -678,3 +657,22 @@ def _mouse_move(self, event): # button: # multiple button can get pressed during motion... a.drag_pan(self._button_pressed, event.key, event.x, event.y) self.navigation.canvas.draw_idle() + + +tools = (('Grid', ToolToggleGrid), + ('Fullscreen', ToolToggleFullScreen), + ('Quit', ToolQuit), + ('EnableAll', ToolEnableAllNavigation), + ('EnableOne', ToolEnableNavigation), + ('XScale', ToolToggleXScale), + ('YScale', ToolToggleYScale), + ('Home', ToolHome), + ('Back', ToolBack), + ('Forward', ToolForward), + ('Spacer1', None), + ('Zoom', ToolZoom), + ('Pan', ToolPan), + ('Spacer2', None), + ('Subplots', 'ConfigureSubplots'), + ('Save', 'SaveFigure')) +"""Default tools""" diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index a9f3545c4b12..d4ce7ff1377a 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -412,8 +412,9 @@ def __init__(self, canvas, num): self.canvas.show() self.vbox.pack_start(self.canvas, True, True, 0) - self.navigation = None - self.toolbar = self._get_toolbar(canvas) + + self.toolbar = self._get_toolbar() + self.navigation = self._get_navigation() # calculate size for window w = int (self.canvas.figure.bbox.width) @@ -469,19 +470,23 @@ def full_screen_toggle (self): _full_screen_flag = False - def _get_toolbar(self, canvas): + def _get_toolbar(self): # must be inited after the window, drawingArea and figure # attrs are set if rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2GTK3 (canvas, self.window) + toolbar = NavigationToolbar2GTK3 (self.canvas, self.window) elif rcParams['toolbar'] == 'navigation': - self.navigation = NavigationGTK3(canvas, ToolbarGTK3) - toolbar = self.navigation.toolbar + toolbar = ToolbarGTK3(self) else: - self.navigation = NavigationGTK3(canvas, None) toolbar = None return toolbar + def _get_navigation(self): + # must be inited after toolbar is setted + if rcParams['toolbar'] != 'toolbar2': + navigation = NavigationGTK3(self) + return navigation + def get_window_title(self): return self.window.get_title() @@ -719,8 +724,8 @@ def draw_rubberband(self, event, caller, x0, y0, x1, y1): if not self.canvas.widgetlock.available(caller): return - #'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/ - #Recipe/189744' + # 'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/ + # Recipe/189744' self.ctx = self.canvas.get_property("window").cairo_create() # todo: instead of redrawing the entire figure, copy the part of @@ -772,7 +777,7 @@ def _add_message(self): sep.show_all() def _add_toolitem(self, name, tooltip_text, image_file, position, - toggle): + toggle): if toggle: tbutton = Gtk.ToggleToolButton() else: @@ -784,6 +789,8 @@ def _add_toolitem(self, name, tooltip_text, image_file, position, image.set_from_file(image_file) tbutton.set_icon_widget(image) + if position is None: + position = -1 self._toolbar.insert(tbutton, position) signal = tbutton.connect('clicked', self._call_tool, name) tbutton.set_tooltip_text(tooltip_text) @@ -857,14 +864,14 @@ def trigger(self, *args): chooser.destroy() if fname: startpath = os.path.expanduser( - rcParams.get('savefig.directory', '')) + rcParams.get('savefig.directory', '')) if startpath == '': # explicitly missing key or empty str signals to use cwd rcParams['savefig.directory'] = startpath else: # save dir for next time rcParams['savefig.directory'] = os.path.dirname( - six.text_type(fname)) + six.text_type(fname)) try: self.figure.canvas.print_figure(fname, format=format_) except Exception as e: @@ -1104,6 +1111,7 @@ def error_msg_gtk(msg, parent=None): dialog.run() dialog.destroy() - +Toolbar = ToolbarGTK3 +Navigation = NavigationGTK3 FigureCanvas = FigureCanvasGTK3 FigureManager = FigureManagerGTK3 diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 1e562b748450..e33c5bb833cb 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -532,11 +532,13 @@ def __init__(self, canvas, num, window): self.set_window_title("Figure %d" % num) self.canvas = canvas self._num = num - self.navigation = None _, _, w, h = canvas.figure.bbox.bounds w, h = int(w), int(h) self.window.minsize(int(w*3/4),int(h*3/4)) - self.toolbar = self._get_toolbar(canvas) + + self.toolbar = self._get_toolbar() + self.navigation = self._get_navigation() + if self.toolbar is not None: self.toolbar.update() self.canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) @@ -550,17 +552,21 @@ def notify_axes_change(fig): self.toolbar.update() self.canvas.figure.add_axobserver(notify_axes_change) - def _get_toolbar(self, canvas): + def _get_toolbar(self): if matplotlib.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2TkAgg(canvas, self.window) + toolbar = NavigationToolbar2TkAgg(self.canvas, self.window) elif matplotlib.rcParams['toolbar'] == 'navigation': - self.navigation = NavigationTk(canvas, ToolbarTk) - toolbar = self.navigation.toolbar + toolbar = ToolbarTk(self) else: - self.navigation = NavigationTk(canvas, None) toolbar = None return toolbar + def _get_navigation(self): + # must be inited after toolbar is setted + if rcParams['toolbar'] != 'toolbar2': + navigation = NavigationTk(self) + return navigation + def resize(self, width, height=None): # before 09-12-22, the resize method takes a single *event* # parameter. On the other hand, the resize method of other @@ -931,23 +937,21 @@ def __init__(self, manager): self._add_message() def _add_toolitem(self, name, tooltip_text, image_file, position, - toggle): + toggle): button = self._Button(name, image_file, toggle) if tooltip_text is not None: ToolTip.createToolTip(button, tooltip_text) self._toolitems[name] = button - def _Button(self, text, file, toggle): - if file is not None: - img_file = os.path.join(rcParams['datapath'], 'images', file) - im = Tk.PhotoImage(master=self, file=img_file) + def _Button(self, text, image_file, toggle): + if image_file is not None: + im = Tk.PhotoImage(master=self, file=image_file) else: im = None if not toggle: - b = Tk.Button( - master=self, text=text, padx=2, pady=2, image=im, + b = Tk.Button(master=self, text=text, padx=2, pady=2, image=im, command=lambda: self._button_click(text)) else: b = Tk.Checkbutton(master=self, text=text, padx=2, pady=2, @@ -1007,7 +1011,7 @@ def trigger(self, *args): # asksaveasfilename dialog when you choose various save types # from the dropdown. Passing in the empty string seems to # work - JDH! - #defaultextension = self.figure.canvas.get_default_filetype() + # defaultextension = self.figure.canvas.get_default_filetype() defaultextension = '' initialdir = rcParams.get('savefig.directory', '') initialdir = os.path.expanduser(initialdir) @@ -1061,6 +1065,7 @@ def destroy(self, *args, **kwargs): SaveFigure = SaveFigureTk ConfigureSubplots = ConfigureSubplotsTk - +Toolbar = ToolbarTk +Navigation = NavigationTk FigureCanvas = FigureCanvasTkAgg FigureManager = FigureManagerTkAgg From 5b49c7a684f8b88aa5d00a8622a6d2b368578975 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Jul 2014 09:45:26 -0400 Subject: [PATCH 25/41] duplicate code in keymap tool initialization --- lib/matplotlib/backend_bases.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 39169dc97399..d9a3f28e9109 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3358,11 +3358,7 @@ def add_tool(self, name, tool, position=None): self._tools[name] = tool_cls if tool_cls.keymap is not None: - for k in validate_stringlist(tool_cls.keymap): - if k in self._keys: - warnings.warn('Key %s changed from %s to %s' % - (k, self._keys[k], name)) - self._keys[k] = name + self.set_tool_keymap(name, tool_cls.keymap) if self.toolbar and tool_cls.intoolbar: # TODO: better search for images, they are not always in the From 773db880d02ad7d0c9fb91a5cae133b9542a7cb1 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Jul 2014 13:37:09 -0400 Subject: [PATCH 26/41] grammar corrections --- lib/matplotlib/backend_tools.py | 14 +++++++------- lib/matplotlib/backends/backend_gtk3.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index a662a4f0972a..aa765f28859d 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -3,10 +3,10 @@ These tools are used by `NavigationBase` :class:`ToolBase` - Simple tool that is instantiated every time it is used + Simple tool that gets instantiated every time it is used :class:`ToolPersistentBase` - Tool which instance is registered within `Navigation` + Tool whose instance gets registered within `Navigation` :class:`ToolToggleBase` PersistentTool that has two states, only one Toggle tool can be @@ -37,7 +37,7 @@ class ToolBase(object): """ keymap = None - """Keymap to associate this tool + """Keymap to associate with this tool **string**: List of comma separated keys that will be used to call this tool when the keypress event of *self.figure.canvas* is emited @@ -47,14 +47,14 @@ class ToolBase(object): """Description of the Tool **string**: If the Tool is included in the Toolbar this text is used - as Tooltip + as a Tooltip """ image = None """Filename of the image **string**: Filename of the image to use in the toolbar. If None, the - `name` is used as label in the toolbar button + `name` is used as a label in the toolbar button """ intoolbar = True @@ -70,12 +70,12 @@ def __init__(self, figure, event=None): self.trigger(event) def trigger(self, event): - """Called when tool is used + """Called when this tool gets used Parameters ---------- event : `Event` - Event that caused this tool to be called + The event that caused this tool to be called """ pass diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index d4ce7ff1377a..8a8dd6c6dca5 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -482,7 +482,7 @@ def _get_toolbar(self): return toolbar def _get_navigation(self): - # must be inited after toolbar is setted + # must be initialised after toolbar has been setted if rcParams['toolbar'] != 'toolbar2': navigation = NavigationGTK3(self) return navigation @@ -746,7 +746,7 @@ def draw_rubberband(self, event, caller, x0, y0, x1, y1): self.ctx.stroke() -class ToolbarGTK3(ToolbarBase, Gtk.Box,): +class ToolbarGTK3(ToolbarBase, Gtk.Box): def __init__(self, manager): ToolbarBase.__init__(self, manager) Gtk.Box.__init__(self) From 2ca69266029386eb3557c129d0d36b451b16c987 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Jul 2014 16:52:25 -0400 Subject: [PATCH 27/41] moving views and positions to tools --- lib/matplotlib/backend_bases.py | 69 ------- lib/matplotlib/backend_tools.py | 224 +++++++++++++++-------- lib/matplotlib/backends/backend_gtk3.py | 9 +- lib/matplotlib/backends/backend_tkagg.py | 5 +- 4 files changed, 157 insertions(+), 150 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d9a3f28e9109..0f47c4653e1a 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3210,10 +3210,6 @@ def __init__(self, manager): self._idDrag = self.canvas.mpl_connect( 'motion_notify_event', self._mouse_move) - # a dict from axes index to a list of view limits - self.views = cbook.Stack() - self.positions = cbook.Stack() # stack of subplot positions - self._tools = {} self._keys = {} self._instances = {} @@ -3480,12 +3476,6 @@ def get_tools(self): 'keymap': keys} return d - def update(self): - """Reset the axes stack""" - - self.views.clear() - self.positions.clear() - def _mouse_move(self, event): if not event.inaxes or not self._toggled: if self._last_cursor != self._default_cursor: @@ -3515,28 +3505,6 @@ def _mouse_move(self, event): else: self.toolbar.set_message('') - def draw(self): - """Redraw the canvases, update the locators""" - - for a in self.canvas.figure.get_axes(): - xaxis = getattr(a, 'xaxis', None) - yaxis = getattr(a, 'yaxis', None) - zaxis = getattr(a, 'zaxis', None) - locators = [] - if xaxis is not None: - locators.append(xaxis.get_major_locator()) - locators.append(xaxis.get_minor_locator()) - if yaxis is not None: - locators.append(yaxis.get_major_locator()) - locators.append(yaxis.get_minor_locator()) - if zaxis is not None: - locators.append(zaxis.get_major_locator()) - locators.append(zaxis.get_minor_locator()) - - for loc in locators: - loc.refresh() - self.canvas.draw_idle() - def set_cursor(self, cursor): """ Set the current cursor to one of the :class:`Cursors` @@ -3545,43 +3513,6 @@ def set_cursor(self, cursor): pass - def update_view(self): - """Update the viewlim and position from the view and - position stack for each axes - """ - - lims = self.views() - if lims is None: - return - pos = self.positions() - if pos is None: - return - for i, a in enumerate(self.canvas.figure.get_axes()): - xmin, xmax, ymin, ymax = lims[i] - a.set_xlim((xmin, xmax)) - a.set_ylim((ymin, ymax)) - # Restore both the original and modified positions - a.set_position(pos[i][0], 'original') - a.set_position(pos[i][1], 'active') - - self.canvas.draw_idle() - - def push_current(self): - """push the current view limits and position onto the stack""" - - lims = [] - pos = [] - for a in self.canvas.figure.get_axes(): - xmin, xmax = a.get_xlim() - ymin, ymax = a.get_ylim() - lims.append((xmin, xmax, ymin, ymax)) - # Store both the original and modified positions - pos.append(( - a.get_position(True).frozen(), - a.get_position().frozen())) - self.views.push(lims) - self.positions.push(pos) - def draw_rubberband(self, event, caller, x0, y0, x1, y1): """Draw a rectangle rubberband to indicate zoom limits diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index aa765f28859d..bfdc210729dc 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -16,6 +16,8 @@ from matplotlib import rcParams from matplotlib._pylab_helpers import Gcf +import matplotlib.cbook as cbook +from weakref import WeakKeyDictionary import numpy as np @@ -284,46 +286,137 @@ def trigger(self, event): ax.figure.canvas.draw() -class ToolHome(ToolBase): +class ViewsPositionsMixin(object): + views = WeakKeyDictionary() + positions = WeakKeyDictionary() + + def init_vp(self): + if self.figure not in self.views: + self.views[self.figure] = cbook.Stack() + self.positions[self.figure] = cbook.Stack() + # Define Home + self.push_current() + + @classmethod + def clear(cls, figure): + """Reset the axes stack""" + if figure in cls.views: + cls.views[figure].clear() + cls.positions[figure].clear() + + def update_view(self): + """Update the viewlim and position from the view and + position stack for each axes + """ + + lims = self.views[self.figure]() + if lims is None: + return + pos = self.positions[self.figure]() + if pos is None: + return + for i, a in enumerate(self.figure.get_axes()): + xmin, xmax, ymin, ymax = lims[i] + a.set_xlim((xmin, xmax)) + a.set_ylim((ymin, ymax)) + # Restore both the original and modified positions + a.set_position(pos[i][0], 'original') + a.set_position(pos[i][1], 'active') + + self.figure.canvas.draw_idle() + + def push_current(self): + """push the current view limits and position onto the stack""" + + lims = [] + pos = [] + for a in self.figure.get_axes(): + xmin, xmax = a.get_xlim() + ymin, ymax = a.get_ylim() + lims.append((xmin, xmax, ymin, ymax)) + # Store both the original and modified positions + pos.append(( + a.get_position(True).frozen(), + a.get_position().frozen())) + self.views[self.figure].push(lims) + self.positions[self.figure].push(pos) + + def refresh_locators(self): + """Redraw the canvases, update the locators""" + for a in self.figure.get_axes(): + xaxis = getattr(a, 'xaxis', None) + yaxis = getattr(a, 'yaxis', None) + zaxis = getattr(a, 'zaxis', None) + locators = [] + if xaxis is not None: + locators.append(xaxis.get_major_locator()) + locators.append(xaxis.get_minor_locator()) + if yaxis is not None: + locators.append(yaxis.get_major_locator()) + locators.append(yaxis.get_minor_locator()) + if zaxis is not None: + locators.append(zaxis.get_major_locator()) + locators.append(zaxis.get_minor_locator()) + + for loc in locators: + loc.refresh() + self.figure.canvas.draw_idle() + + def home(self): + self.views[self.figure].home() + self.positions[self.figure].home() + + def back(self): + self.views[self.figure].back() + self.positions[self.figure].back() + + def forward(self): + self.views[self.figure].forward() + self.positions[self.figure].forward() + + +def clear_views_positions(figure): + ViewsPositionsMixin.clear(figure) + + +class ViewsPositionsBase(ViewsPositionsMixin, ToolBase): + # Simple base to avoid repeating code on Home, Back and Forward + _on_trigger = None + + def set_figure(self, *args): + ToolBase.set_figure(self, *args) + self.init_vp() + + def trigger(self, *args): + getattr(self, self._on_trigger)() + self.update_view() + + +class ToolHome(ViewsPositionsBase): """Restore the original view""" description = 'Reset original view' image = 'home.png' keymap = rcParams['keymap.home'] + _on_trigger = 'home' - def trigger(self, *args): - self.navigation.views.home() - self.navigation.positions.home() - self.navigation.update_view() -# self.set_history_buttons() - -class ToolBack(ToolBase): +class ToolBack(ViewsPositionsBase): """move back up the view lim stack""" description = 'Back to previous view' image = 'back.png' keymap = rcParams['keymap.back'] - - def trigger(self, *args): - self.navigation.views.back() - self.navigation.positions.back() -# self.set_history_buttons() - self.navigation.update_view() + _on_trigger = 'back' -class ToolForward(ToolBase): +class ToolForward(ViewsPositionsBase): """Move forward in the view lim stack""" description = 'Forward to next view' image = 'forward.png' keymap = rcParams['keymap.forward'] - - def trigger(self, *args): - self.navigation.views.forward() - self.navigation.positions.forward() -# self.set_history_buttons() - self.navigation.update_view() + _on_trigger = 'forward' class ConfigureSubplotsBase(ToolPersistentBase): @@ -341,17 +434,10 @@ class SaveFigureBase(ToolBase): keymap = rcParams['keymap.save'] -class ToolZoom(ToolToggleBase): - """Zoom to rectangle""" - - description = 'Zoom to rectangle' - image = 'zoom_to_rect.png' - keymap = rcParams['keymap.zoom'] - cursor = cursors.SELECT_REGION - +class ZoomPanBase(ViewsPositionsMixin, ToolToggleBase): def __init__(self, *args): ToolToggleBase.__init__(self, *args) - self._ids_zoom = [] + self.init_vp() self._button_pressed = None self._xypress = None self._idPress = None @@ -365,16 +451,29 @@ def enable(self, event): 'button_release_event', self._release) def disable(self, event): - self._cancel_zoom() + self._cancel_action() self.figure.canvas.widgetlock.release(self) self.figure.canvas.mpl_disconnect(self._idPress) self.figure.canvas.mpl_disconnect(self._idRelease) - def _cancel_zoom(self): + +class ToolZoom(ZoomPanBase): + """Zoom to rectangle""" + + description = 'Zoom to rectangle' + image = 'zoom_to_rect.png' + keymap = rcParams['keymap.zoom'] + cursor = cursors.SELECT_REGION + + def __init__(self, *args): + ZoomPanBase.__init__(self, *args) + self._ids_zoom = [] + + def _cancel_action(self): for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) self.navigation.remove_rubberband(None, self) - self.navigation.draw() + self.refresh_locators() self._xypress = None self._button_pressed = None self._ids_zoom = [] @@ -386,23 +485,18 @@ def _press(self, event): # If we're already in the middle of a zoom, pressing another # button works to "cancel" if self._ids_zoom != []: - self._cancel_zoom() + self._cancel_action() if event.button == 1: self._button_pressed = 1 elif event.button == 3: self._button_pressed = 3 else: - self._cancel_zoom() + self._cancel_action() return x, y = event.x, event.y - # push the current view to define home if stack is empty - # TODO: add a set home in navigation - if self.navigation.views.empty(): - self.navigation.push_current() - self._xypress = [] for i, a in enumerate(self.figure.get_axes()): if (x is not None and y is not None and a.in_axes(event) and @@ -457,7 +551,7 @@ def _release(self, event): self._ids_zoom = [] if not self._xypress: - self._cancel_zoom() + self._cancel_action() return last_a = [] @@ -467,7 +561,7 @@ def _release(self, event): lastx, lasty, a, _ind, lim, _trans = cur_xypress # ignore singular clicks - 5 pixels is a threshold if abs(x - lastx) < 5 or abs(y - lasty) < 5: - self._cancel_zoom() + self._cancel_action() return x0, y0, x1, y1 = lim.extents @@ -568,11 +662,11 @@ def _release(self, event): a.set_ylim((ry1, ry2)) self._zoom_mode = None - self.navigation.push_current() - self._cancel_zoom() + self.push_current() + self._cancel_action() -class ToolPan(ToolToggleBase): +class ToolPan(ZoomPanBase): """Pan axes with left mouse, zoom with right""" keymap = rcParams['keymap.pan'] @@ -581,32 +675,16 @@ class ToolPan(ToolToggleBase): cursor = cursors.MOVE def __init__(self, *args): - ToolToggleBase.__init__(self, *args) - self._button_pressed = None - self._xypress = None - self._idPress = None - self._idRelease = None + ZoomPanBase.__init__(self, *args) self._idDrag = None - def enable(self, event): - self.figure.canvas.widgetlock(self) - self._idPress = self.figure.canvas.mpl_connect( - 'button_press_event', self._press) - self._idRelease = self.figure.canvas.mpl_connect( - 'button_release_event', self._release) - - def disable(self, event): - self._cancel_pan() - self.figure.canvas.widgetlock.release(self) - self.figure.canvas.mpl_disconnect(self._idPress) - self.figure.canvas.mpl_disconnect(self._idRelease) - - def _cancel_pan(self): + def _cancel_action(self): self._button_pressed = None self._xypress = [] self.figure.canvas.mpl_disconnect(self._idDrag) self.navigation.messagelock.release(self) - self.navigation.draw() +# self.navigation.draw() + self.refresh_locators() def _press(self, event): if event.button == 1: @@ -614,16 +692,11 @@ def _press(self, event): elif event.button == 3: self._button_pressed = 3 else: - self._cancel_pan() + self._cancel_action() return x, y = event.x, event.y - # push the current view to define home if stack is empty - # TODO: add define_home in navigation - if self.navigation.views.empty(): - self.navigation.push_current() - self._xypress = [] for i, a in enumerate(self.figure.get_axes()): if (x is not None and y is not None and a.in_axes(event) and @@ -636,7 +709,7 @@ def _press(self, event): def _release(self, event): if self._button_pressed is None: - self._cancel_pan() + self._cancel_action() return self.figure.canvas.mpl_disconnect(self._idDrag) @@ -645,11 +718,12 @@ def _release(self, event): for a, _ind in self._xypress: a.end_pan() if not self._xypress: - self._cancel_pan() + self._cancel_action() return - self.navigation.push_current() - self._cancel_pan() +# self.navigation.push_current() + self.push_current() + self._cancel_action() def _mouse_move(self, event): for a, _ind in self._xypress: diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 8a8dd6c6dca5..6d4fd882c1af 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -31,7 +31,8 @@ def fn_name(): return sys._getframe(1).f_code.co_name from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \ FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase from matplotlib.backend_bases import ShowBase, ToolbarBase, NavigationBase -from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase +from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, \ + clear_views_positions from matplotlib.cbook import is_string_like, is_writable_file_like from matplotlib.colors import colorConverter @@ -438,7 +439,7 @@ def destroy(*args): def notify_axes_change(fig): 'this will be called whenever the current axes is changed' if self.navigation is not None: - self.navigation.update() + clear_views_positions(fig) elif self.toolbar is not None: self.toolbar.update() self.canvas.figure.add_axobserver(notify_axes_change) @@ -758,9 +759,9 @@ def __init__(self, manager): self._toolbar.show_all() self._toolitems = {} self._signals = {} - self._add_message() + self._setup_message_area() - def _add_message(self): + def _setup_message_area(self): box = Gtk.Box() box.set_property("orientation", Gtk.Orientation.HORIZONTAL) sep = Gtk.Separator() diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index e33c5bb833cb..1bb9248527bf 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -21,7 +21,8 @@ from matplotlib.backend_bases import FigureManagerBase, FigureCanvasBase from matplotlib.backend_bases import NavigationToolbar2, cursors, TimerBase from matplotlib.backend_bases import ShowBase, ToolbarBase, NavigationBase -from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase +from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, \ + clear_views_positions from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure @@ -547,7 +548,7 @@ def __init__(self, canvas, num, window): def notify_axes_change(fig): 'this will be called whenever the current axes is changed' if self.navigation is not None: - self.navigation.update() + clear_views_positions(fig) elif self.toolbar is not None: self.toolbar.update() self.canvas.figure.add_axobserver(notify_axes_change) From 661417d949f4132d582e75fe790d1ffecdf7b46f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 25 Jul 2014 10:31:03 -0400 Subject: [PATCH 28/41] The views positions mixin automatically adds the clear as axobserver --- examples/user_interfaces/navigation.py | 6 ++--- lib/matplotlib/backend_tools.py | 32 +++++++++++++++++++----- lib/matplotlib/backends/backend_gtk3.py | 5 ++-- lib/matplotlib/backends/backend_tkagg.py | 5 ++-- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index b1f91e7886d6..aa528389452c 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -1,6 +1,6 @@ import matplotlib -matplotlib.use('GTK3Cairo') -# matplotlib.use('TkAGG') +# matplotlib.use('GTK3Cairo') +matplotlib.use('TkAGG') matplotlib.rcParams['toolbar'] = 'navigation' import matplotlib.pyplot as plt from matplotlib.backend_tools import ToolBase @@ -54,6 +54,6 @@ def trigger(self, event): fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) # Just for fun, lets remove the back button -fig.canvas.manager.navigation.remove_tool('Back') +# fig.canvas.manager.navigation.remove_tool('Back') plt.show() diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index bfdc210729dc..14814d8ed709 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -97,9 +97,9 @@ def set_figure(self, figure): class ToolPersistentBase(ToolBase): - """Persisten tool + """Persistent tool - Persistent Tools are keept alive after their initialization, + Persistent Tools are kept alive after their initialization, a reference of the instance is kept by `navigation`. Notes @@ -287,20 +287,41 @@ def trigger(self, event): class ViewsPositionsMixin(object): + """Mixin to handle changes in views and positions + + Tools that change the views and positions, use this mixin to + keep track of the changes. + """ + views = WeakKeyDictionary() + """Record of views with Figure objects as keys""" + positions = WeakKeyDictionary() + """Record of positions with Figure objects as keys""" def init_vp(self): + """Add a figure to the list of figures handled by this mixin + + To handle the views and positions for a given figure, this method + has to be called at least once before any other method. + + The best way to call it is during the set_figure method of the tools + """ if self.figure not in self.views: self.views[self.figure] = cbook.Stack() self.positions[self.figure] = cbook.Stack() # Define Home self.push_current() + # Adding the clear method as axobserver, removes this burden from + # the backend + self.figure.add_axobserver(self.clear) @classmethod def clear(cls, figure): """Reset the axes stack""" + print('clear') if figure in cls.views: + print('done clear') cls.views[figure].clear() cls.positions[figure].clear() @@ -375,12 +396,9 @@ def forward(self): self.positions[self.figure].forward() -def clear_views_positions(figure): - ViewsPositionsMixin.clear(figure) - - class ViewsPositionsBase(ViewsPositionsMixin, ToolBase): # Simple base to avoid repeating code on Home, Back and Forward + # Not of much use for other tools, so not documented _on_trigger = None def set_figure(self, *args): @@ -435,6 +453,8 @@ class SaveFigureBase(ToolBase): class ZoomPanBase(ViewsPositionsMixin, ToolToggleBase): + # Base class to group common functionality between zoom and pan + # Not of much use for other tools, so not documented def __init__(self, *args): ToolToggleBase.__init__(self, *args) self.init_vp() diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 6d4fd882c1af..28376fd16431 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -31,8 +31,7 @@ def fn_name(): return sys._getframe(1).f_code.co_name from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \ FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase from matplotlib.backend_bases import ShowBase, ToolbarBase, NavigationBase -from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, \ - clear_views_positions +from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase from matplotlib.cbook import is_string_like, is_writable_file_like from matplotlib.colors import colorConverter @@ -439,7 +438,7 @@ def destroy(*args): def notify_axes_change(fig): 'this will be called whenever the current axes is changed' if self.navigation is not None: - clear_views_positions(fig) + pass elif self.toolbar is not None: self.toolbar.update() self.canvas.figure.add_axobserver(notify_axes_change) diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 1bb9248527bf..926109c9b9c8 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -21,8 +21,7 @@ from matplotlib.backend_bases import FigureManagerBase, FigureCanvasBase from matplotlib.backend_bases import NavigationToolbar2, cursors, TimerBase from matplotlib.backend_bases import ShowBase, ToolbarBase, NavigationBase -from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, \ - clear_views_positions +from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure @@ -548,7 +547,7 @@ def __init__(self, canvas, num, window): def notify_axes_change(fig): 'this will be called whenever the current axes is changed' if self.navigation is not None: - clear_views_positions(fig) + pass elif self.toolbar is not None: self.toolbar.update() self.canvas.figure.add_axobserver(notify_axes_change) From 90ab64f07ed65f9e0bea5bdf7a4cebf6da80eb99 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 25 Jul 2014 10:59:04 -0400 Subject: [PATCH 29/41] bug when navigation was not defined --- examples/user_interfaces/navigation.py | 4 ++-- lib/matplotlib/backend_tools.py | 2 -- lib/matplotlib/backends/backend_gtk3.py | 2 ++ lib/matplotlib/backends/backend_tkagg.py | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index aa528389452c..c79c97137092 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -52,8 +52,8 @@ def trigger(self, event): fig.canvas.manager.navigation.add_tool('List', ListTools) if matplotlib.rcParams['backend'] == 'GTK3Cairo': fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) - + # Just for fun, lets remove the back button -# fig.canvas.manager.navigation.remove_tool('Back') +fig.canvas.manager.navigation.remove_tool('Back') plt.show() diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 14814d8ed709..442f9bf6fedb 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -319,9 +319,7 @@ def init_vp(self): @classmethod def clear(cls, figure): """Reset the axes stack""" - print('clear') if figure in cls.views: - print('done clear') cls.views[figure].clear() cls.positions[figure].clear() diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 28376fd16431..dab819fa9193 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -485,6 +485,8 @@ def _get_navigation(self): # must be initialised after toolbar has been setted if rcParams['toolbar'] != 'toolbar2': navigation = NavigationGTK3(self) + else: + navigation = None return navigation def get_window_title(self): diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 926109c9b9c8..a17c0f4c8c13 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -565,6 +565,8 @@ def _get_navigation(self): # must be inited after toolbar is setted if rcParams['toolbar'] != 'toolbar2': navigation = NavigationTk(self) + else: + navigation = None return navigation def resize(self, width, height=None): From 55dd1494f60af6c23cfd0924a41ab303f495d76a Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Mon, 28 Jul 2014 19:40:38 +0200 Subject: [PATCH 30/41] Small refactor so that we first initiate the Navigation (ToolManager), before filling it with tools. Added a nice utility API function, Navigation.addTools. --- lib/matplotlib/backend_bases.py | 23 ++++++++++++++++------- lib/matplotlib/pyplot.py | 4 ++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 0f47c4653e1a..567ff03b2073 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3220,13 +3220,6 @@ def __init__(self, manager): # to write into toolbar message self.messagelock = widgets.LockDraw() - for name, tool in tools.tools: - if tool is None: - if self.toolbar is not None: - self.toolbar.add_separator(-1) - else: - self.add_tool(name, tool, None) - self._last_cursor = self._default_cursor @property @@ -3329,6 +3322,22 @@ def remove_tool(self, name): if self.toolbar: self.toolbar._remove_toolitem(name) + def add_tools(self, tools): + """ Add multiple tools to `Navigation` + + Parameters + ---------- + tools : a list of tuples which contains the id of the tool and + a either a reference to the tool Tool class itself, or None to + insert a spacer. See @add_tool. + """ + for name, tool in tools: + if tool is None: + if self.toolbar is not None: + self.toolbar.add_separator(-1) + else: + self.add_tool(name, tool, None) + def add_tool(self, name, tool, position=None): """Add tool to `Navigation` diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index da553fe212c6..7ca2ef8eef65 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -31,6 +31,7 @@ from matplotlib.cbook import _string_to_bool from matplotlib import docstring from matplotlib.backend_bases import FigureCanvasBase +from matplotlib.backend_tools import tools as default_tools from matplotlib.figure import Figure, figaspect from matplotlib.gridspec import GridSpec from matplotlib.image import imread as _imread @@ -428,6 +429,9 @@ def figure(num=None, # autoincrement if None, else integer from 1-N FigureClass=FigureClass, **kwargs) + if rcParams['toolbar'] == 'navigation': + figManager.navigation.add_tools(default_tools) + if figLabel: figManager.set_window_title(figLabel) figManager.canvas.figure.set_label(figLabel) From 15ac091aab13a149c971ee212339d3f1e3f0d05d Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Mon, 28 Jul 2014 23:57:39 +0200 Subject: [PATCH 31/41] Update for Sphinx documentation --- lib/matplotlib/backend_bases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 567ff03b2073..96adc3160155 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3329,7 +3329,7 @@ def add_tools(self, tools): ---------- tools : a list of tuples which contains the id of the tool and a either a reference to the tool Tool class itself, or None to - insert a spacer. See @add_tool. + insert a spacer. See :func:`add_tool`. """ for name, tool in tools: if tool is None: From 8cd241ce04bfb6f9e8af7c70dc33eb6f4dc7b7db Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Tue, 29 Jul 2014 16:06:38 +0200 Subject: [PATCH 32/41] Moved default_tool initilisation to FigureManagerBase and cleaned. --- lib/matplotlib/backend_bases.py | 9 +++++++++ lib/matplotlib/backends/backend_gtk3.py | 3 --- lib/matplotlib/backends/backend_tkagg.py | 6 +----- lib/matplotlib/pyplot.py | 4 ---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 96adc3160155..d49abd5bd2ab 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2556,6 +2556,10 @@ def __init__(self, canvas, num): """ + self.toolbar = self._get_toolbar() + self.navigation = self._get_navigation() + self.navigation.add_tools(tools.tools) + def show(self): """ For GUI backends, show the figure window and redraw. @@ -2603,6 +2607,11 @@ def set_window_title(self, title): """ pass + def _get_toolbar(self): + return None + + def _get_navigation(self): + return None cursors = tools.cursors diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index dab819fa9193..8559527c44b5 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -413,9 +413,6 @@ def __init__(self, canvas, num): self.vbox.pack_start(self.canvas, True, True, 0) - self.toolbar = self._get_toolbar() - self.navigation = self._get_navigation() - # calculate size for window w = int (self.canvas.figure.bbox.width) h = int (self.canvas.figure.bbox.height) diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index a17c0f4c8c13..2310b9ac2479 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -526,19 +526,15 @@ class FigureManagerTkAgg(FigureManagerBase): window : The tk.Window """ def __init__(self, canvas, num, window): - FigureManagerBase.__init__(self, canvas, num) self.window = window + FigureManagerBase.__init__(self, canvas, num) self.window.withdraw() self.set_window_title("Figure %d" % num) - self.canvas = canvas self._num = num _, _, w, h = canvas.figure.bbox.bounds w, h = int(w), int(h) self.window.minsize(int(w*3/4),int(h*3/4)) - self.toolbar = self._get_toolbar() - self.navigation = self._get_navigation() - if self.toolbar is not None: self.toolbar.update() self.canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 7ca2ef8eef65..da553fe212c6 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -31,7 +31,6 @@ from matplotlib.cbook import _string_to_bool from matplotlib import docstring from matplotlib.backend_bases import FigureCanvasBase -from matplotlib.backend_tools import tools as default_tools from matplotlib.figure import Figure, figaspect from matplotlib.gridspec import GridSpec from matplotlib.image import imread as _imread @@ -429,9 +428,6 @@ def figure(num=None, # autoincrement if None, else integer from 1-N FigureClass=FigureClass, **kwargs) - if rcParams['toolbar'] == 'navigation': - figManager.navigation.add_tools(default_tools) - if figLabel: figManager.set_window_title(figLabel) figManager.canvas.figure.set_label(figLabel) From 39f5b749a597c8457ccb6d930c19a449f7aa92a6 Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Tue, 29 Jul 2014 16:47:16 +0200 Subject: [PATCH 33/41] Fix navigation --- lib/matplotlib/backend_bases.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d49abd5bd2ab..19e3a15151b2 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2558,7 +2558,8 @@ def __init__(self, canvas, num): self.toolbar = self._get_toolbar() self.navigation = self._get_navigation() - self.navigation.add_tools(tools.tools) + if rcParams['toolbar'] == 'navigation': + self.navigation.add_tools(tools.tools) def show(self): """ From b20daded90e0134d5af51c7fdbafae585a80a114 Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Tue, 29 Jul 2014 19:10:55 +0200 Subject: [PATCH 34/41] Temporary fix to backends --- lib/matplotlib/backend_bases.py | 10 ---------- lib/matplotlib/backends/backend_gtk3.py | 7 ++++++- lib/matplotlib/backends/backend_tkagg.py | 10 ++++++++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 19e3a15151b2..96adc3160155 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2556,11 +2556,6 @@ def __init__(self, canvas, num): """ - self.toolbar = self._get_toolbar() - self.navigation = self._get_navigation() - if rcParams['toolbar'] == 'navigation': - self.navigation.add_tools(tools.tools) - def show(self): """ For GUI backends, show the figure window and redraw. @@ -2608,11 +2603,6 @@ def set_window_title(self, title): """ pass - def _get_toolbar(self): - return None - - def _get_navigation(self): - return None cursors = tools.cursors diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 8559527c44b5..df222fa2a520 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -31,7 +31,7 @@ def fn_name(): return sys._getframe(1).f_code.co_name from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \ FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase from matplotlib.backend_bases import ShowBase, ToolbarBase, NavigationBase -from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase +from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, tools from matplotlib.cbook import is_string_like, is_writable_file_like from matplotlib.colors import colorConverter @@ -413,6 +413,11 @@ def __init__(self, canvas, num): self.vbox.pack_start(self.canvas, True, True, 0) + self.toolbar = self._get_toolbar() + self.navigation = self._get_navigation() + if matplotlib.rcParams['toolbar'] == 'navigation': + self.navigation.add_tools(tools) + # calculate size for window w = int (self.canvas.figure.bbox.width) h = int (self.canvas.figure.bbox.height) diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 2310b9ac2479..bbaf35670cf0 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -21,7 +21,7 @@ from matplotlib.backend_bases import FigureManagerBase, FigureCanvasBase from matplotlib.backend_bases import NavigationToolbar2, cursors, TimerBase from matplotlib.backend_bases import ShowBase, ToolbarBase, NavigationBase -from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase +from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, tools from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure @@ -526,15 +526,21 @@ class FigureManagerTkAgg(FigureManagerBase): window : The tk.Window """ def __init__(self, canvas, num, window): - self.window = window FigureManagerBase.__init__(self, canvas, num) + self.window = window self.window.withdraw() self.set_window_title("Figure %d" % num) + self.canvas = canvas self._num = num _, _, w, h = canvas.figure.bbox.bounds w, h = int(w), int(h) self.window.minsize(int(w*3/4),int(h*3/4)) + self.toolbar = self._get_toolbar() + self.navigation = self._get_navigation() + if matplotlib.rcParams['toolbar'] == 'navigation': + self.navigation.add_tools(tools) + if self.toolbar is not None: self.toolbar.update() self.canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) From 2cb4501ee7eabf40b94bc604c16e8c1f8351bbcf Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 3 Sep 2014 10:43:32 -0400 Subject: [PATCH 35/41] removing persistent tools --- examples/user_interfaces/navigation.py | 2 +- lib/matplotlib/backend_bases.py | 59 +++------ lib/matplotlib/backend_tools.py | 150 ++++++++++------------- lib/matplotlib/backends/backend_gtk3.py | 26 ++-- lib/matplotlib/backends/backend_tkagg.py | 15 ++- 5 files changed, 110 insertions(+), 142 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index c79c97137092..ab348db69b8e 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -54,6 +54,6 @@ def trigger(self, event): fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) # Just for fun, lets remove the back button -fig.canvas.manager.navigation.remove_tool('Back') +# fig.canvas.manager.navigation.remove_tool('Back') plt.show() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 96adc3160155..d7ce3017a22b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3212,7 +3212,6 @@ def __init__(self, manager): self._tools = {} self._keys = {} - self._instances = {} self._toggled = None # to process keypress event @@ -3231,15 +3230,6 @@ def active_toggle(self): return self._toggled - @property - def instances(self): - """Active tools instances - - **dictionary** : Contains the active instances that are registered - """ - - return self._instances - def get_tool_keymap(self, name): """Get the keymap associated with a tool @@ -3281,28 +3271,19 @@ def set_tool_keymap(self, name, *keys): self._keys[k] = name def unregister(self, name): - """Unregister the tool from the active instances + """Unregister the tool from Navigation Parameters ---------- name : string Name of the tool to unregister - - Notes - ----- - This method is used by `PersistentTools` to remove the reference kept - by `Navigation`. - - It is usually called by the `unregister` method - - If called, next time the `Tool` is used it will be reinstantiated - instead of using the existing instance. """ if self._toggled == name: self._handle_toggle(name, from_toolbar=False) - if name in self._instances: - del self._instances[name] + if name in self._tools: + self._tools[name].destroy() + del self._tools[name] def remove_tool(self, name): """Remove tool from the `Navigation` @@ -3314,7 +3295,7 @@ def remove_tool(self, name): """ self.unregister(name) - del self._tools[name] + keys = [k for k, v in six.iteritems(self._keys) if v == name] for k in keys: del self._keys[k] @@ -3361,7 +3342,7 @@ def add_tool(self, name, tool, position=None): 'not added') return - self._tools[name] = tool_cls + self._tools[name] = tool_cls(self.canvas.figure, name) if tool_cls.keymap is not None: self.set_tool_keymap(name, tool_cls.keymap) @@ -3394,27 +3375,23 @@ def _get_cls_to_instantiate(self, callback_class): return callback_class - def trigger_tool(self, name): + def trigger_tool(self, name, event=None): """Trigger on a tool Method to programatically "click" on Tools """ - self._trigger_tool(name, None, False) + self._trigger_tool(name, event, False) def _trigger_tool(self, name, event, from_toolbar): if name not in self._tools: raise AttributeError('%s not in Tools' % name) tool = self._tools[name] - if issubclass(tool, tools.ToolToggleBase): + if isinstance(tool, tools.ToolToggleBase): self._handle_toggle(name, event=event, from_toolbar=from_toolbar) - elif issubclass(tool, tools.ToolPersistentBase): - instance = self._get_instance(name) - instance.trigger(event) else: - # Non persistent tools, are instantiated and forgotten - tool(self.canvas.figure, event) + tool.trigger(event) def _key_press(self, event): if event.key is None or self.keypresslock.locked(): @@ -3425,14 +3402,6 @@ def _key_press(self, event): return self._trigger_tool(name, event, False) - def _get_instance(self, name): - if name not in self._instances: - instance = self._tools[name](self.canvas.figure, name) - # register instance - self._instances[name] = instance - - return self._instances[name] - def _toolbar_callback(self, name): """Callback for the `Toolbar` @@ -3453,7 +3422,7 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): if not from_toolbar and self.toolbar: self.toolbar._toggle(name, False) - instance = self._get_instance(name) + tool = self._tools[name] if self._toggled is None: # first trigger of tool self._toggled = name @@ -3465,10 +3434,10 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): if self.toolbar: # untoggle the previous toggled tool self.toolbar._toggle(self._toggled, False) - self._get_instance(self._toggled).trigger(event) + self._tools[self._toggled].trigger(event) self._toggled = name - instance.trigger(event) + tool.trigger(event) for a in self.canvas.figure.get_axes(): a.set_navigate_mode(self._toggled) @@ -3492,7 +3461,7 @@ def _mouse_move(self, event): self._last_cursor = self._default_cursor else: if self._toggled: - cursor = self._instances[self._toggled].cursor + cursor = self._tools[self._toggled].cursor if cursor and self._last_cursor != cursor: self.set_cursor(cursor) self._last_cursor = cursor diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 442f9bf6fedb..93a9c2f54818 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -5,11 +5,8 @@ :class:`ToolBase` Simple tool that gets instantiated every time it is used -:class:`ToolPersistentBase` - Tool whose instance gets registered within `Navigation` - :class:`ToolToggleBase` - PersistentTool that has two states, only one Toggle tool can be + Tool that has two states, only one Toggle tool can be active at any given time for the same `Navigation` """ @@ -65,11 +62,11 @@ class ToolBase(object): cursor = None """Cursor to use when the tool is active""" - def __init__(self, figure, event=None): + def __init__(self, figure, name, event=None): + self._name = name self.figure = None self.navigation = None self.set_figure(figure) - self.trigger(event) def trigger(self, event): """Called when this tool gets used @@ -95,27 +92,6 @@ def set_figure(self, figure): self.figure = figure self.navigation = figure.canvas.manager.navigation - -class ToolPersistentBase(ToolBase): - """Persistent tool - - Persistent Tools are kept alive after their initialization, - a reference of the instance is kept by `navigation`. - - Notes - ----- - The difference with `ToolBase` is that `trigger` method - is not called automatically at initialization - """ - - def __init__(self, figure, name, event=None): - self._name = name - self.figure = None - self.navigation = None - self.set_figure(figure) - # persistent tools don't call trigger a at instantiation - # it will be called by Navigation - def unregister(self, *args): """Unregister the tool from the instances of Navigation @@ -129,11 +105,17 @@ def unregister(self, *args): # call this to unregister from navigation self.navigation.unregister(self._name) + @property + def name(self): + return self._name + + def destroy(self): + pass + -class ToolToggleBase(ToolPersistentBase): +class ToolToggleBase(ToolBase): """Toggleable tool - This tool is a Persistent Tool that has a toggled state. Every time it is triggered, it switches between enable and disable """ @@ -286,12 +268,8 @@ def trigger(self, event): ax.figure.canvas.draw() -class ViewsPositionsMixin(object): - """Mixin to handle changes in views and positions - - Tools that change the views and positions, use this mixin to - keep track of the changes. - """ +class ViewsPositions(object): + """Auxiliary class to handle changes in views and positions""" views = WeakKeyDictionary() """Record of views with Figure objects as keys""" @@ -299,22 +277,17 @@ class ViewsPositionsMixin(object): positions = WeakKeyDictionary() """Record of positions with Figure objects as keys""" - def init_vp(self): - """Add a figure to the list of figures handled by this mixin - - To handle the views and positions for a given figure, this method - has to be called at least once before any other method. - - The best way to call it is during the set_figure method of the tools - """ - if self.figure not in self.views: - self.views[self.figure] = cbook.Stack() - self.positions[self.figure] = cbook.Stack() + @classmethod + def add_figure(cls, figure): + """Add a figure to the list of figures handled by this class""" + if figure not in cls.views: + cls.views[figure] = cbook.Stack() + cls.positions[figure] = cbook.Stack() # Define Home - self.push_current() + cls.push_current(figure) # Adding the clear method as axobserver, removes this burden from # the backend - self.figure.add_axobserver(self.clear) + figure.add_axobserver(cls.clear) @classmethod def clear(cls, figure): @@ -323,18 +296,19 @@ def clear(cls, figure): cls.views[figure].clear() cls.positions[figure].clear() - def update_view(self): + @classmethod + def update_view(cls, figure): """Update the viewlim and position from the view and position stack for each axes """ - lims = self.views[self.figure]() + lims = cls.views[figure]() if lims is None: return - pos = self.positions[self.figure]() + pos = cls.positions[figure]() if pos is None: return - for i, a in enumerate(self.figure.get_axes()): + for i, a in enumerate(figure.get_axes()): xmin, xmax, ymin, ymax = lims[i] a.set_xlim((xmin, xmax)) a.set_ylim((ymin, ymax)) @@ -342,14 +316,15 @@ def update_view(self): a.set_position(pos[i][0], 'original') a.set_position(pos[i][1], 'active') - self.figure.canvas.draw_idle() + figure.canvas.draw_idle() - def push_current(self): + @classmethod + def push_current(cls, figure): """push the current view limits and position onto the stack""" lims = [] pos = [] - for a in self.figure.get_axes(): + for a in figure.get_axes(): xmin, xmax = a.get_xlim() ymin, ymax = a.get_ylim() lims.append((xmin, xmax, ymin, ymax)) @@ -357,12 +332,13 @@ def push_current(self): pos.append(( a.get_position(True).frozen(), a.get_position().frozen())) - self.views[self.figure].push(lims) - self.positions[self.figure].push(pos) + cls.views[figure].push(lims) + cls.positions[figure].push(pos) - def refresh_locators(self): + @classmethod + def refresh_locators(cls, figure): """Redraw the canvases, update the locators""" - for a in self.figure.get_axes(): + for a in figure.get_axes(): xaxis = getattr(a, 'xaxis', None) yaxis = getattr(a, 'yaxis', None) zaxis = getattr(a, 'zaxis', None) @@ -379,33 +355,37 @@ def refresh_locators(self): for loc in locators: loc.refresh() - self.figure.canvas.draw_idle() + figure.canvas.draw_idle() - def home(self): - self.views[self.figure].home() - self.positions[self.figure].home() + @classmethod + def home(cls, figure): + cls.views[figure].home() + cls.positions[figure].home() - def back(self): - self.views[self.figure].back() - self.positions[self.figure].back() + @classmethod + def back(cls, figure): + cls.views[figure].back() + cls.positions[figure].back() - def forward(self): - self.views[self.figure].forward() - self.positions[self.figure].forward() + @classmethod + def forward(cls, figure): + cls.views[figure].forward() + cls.positions[figure].forward() -class ViewsPositionsBase(ViewsPositionsMixin, ToolBase): +class ViewsPositionsBase(ToolBase): # Simple base to avoid repeating code on Home, Back and Forward # Not of much use for other tools, so not documented _on_trigger = None - def set_figure(self, *args): - ToolBase.set_figure(self, *args) - self.init_vp() + def __init__(self, *args, **kwargs): + ToolBase.__init__(self, *args, **kwargs) + self.viewspos = ViewsPositions() def trigger(self, *args): - getattr(self, self._on_trigger)() - self.update_view() + self.viewspos.add_figure(self.figure) + getattr(self.viewspos, self._on_trigger)(self.figure) + self.viewspos.update_view(self.figure) class ToolHome(ViewsPositionsBase): @@ -435,7 +415,7 @@ class ToolForward(ViewsPositionsBase): _on_trigger = 'forward' -class ConfigureSubplotsBase(ToolPersistentBase): +class ConfigureSubplotsBase(ToolBase): """Base tool for the configuration of subplots""" description = 'Configure subplots' @@ -450,16 +430,16 @@ class SaveFigureBase(ToolBase): keymap = rcParams['keymap.save'] -class ZoomPanBase(ViewsPositionsMixin, ToolToggleBase): +class ZoomPanBase(ToolToggleBase): # Base class to group common functionality between zoom and pan # Not of much use for other tools, so not documented def __init__(self, *args): ToolToggleBase.__init__(self, *args) - self.init_vp() self._button_pressed = None self._xypress = None self._idPress = None self._idRelease = None + self.viewspos = ViewsPositions() def enable(self, event): self.figure.canvas.widgetlock(self) @@ -474,6 +454,10 @@ def disable(self, event): self.figure.canvas.mpl_disconnect(self._idPress) self.figure.canvas.mpl_disconnect(self._idRelease) + def trigger(self, *args): + self.viewspos.add_figure(self.figure) + ToolToggleBase.trigger(self, *args) + class ToolZoom(ZoomPanBase): """Zoom to rectangle""" @@ -491,7 +475,7 @@ def _cancel_action(self): for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) self.navigation.remove_rubberband(None, self) - self.refresh_locators() + self.viewspos.refresh_locators(self.figure) self._xypress = None self._button_pressed = None self._ids_zoom = [] @@ -680,7 +664,7 @@ def _release(self, event): a.set_ylim((ry1, ry2)) self._zoom_mode = None - self.push_current() + self.viewspos.push_current(self.figure) self._cancel_action() @@ -701,8 +685,7 @@ def _cancel_action(self): self._xypress = [] self.figure.canvas.mpl_disconnect(self._idDrag) self.navigation.messagelock.release(self) -# self.navigation.draw() - self.refresh_locators() + self.viewspos.refresh_locators(self.figure) def _press(self, event): if event.button == 1: @@ -739,8 +722,7 @@ def _release(self, event): self._cancel_action() return -# self.navigation.push_current() - self.push_current() + self.viewspos.push_current(self.figure) self._cancel_action() def _mouse_move(self, event): diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index df222fa2a520..7428926baf4a 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -887,10 +887,15 @@ def trigger(self, *args): class ConfigureSubplotsGTK3(ConfigureSubplotsBase, Gtk.Window): def __init__(self, *args, **kwargs): ConfigureSubplotsBase.__init__(self, *args, **kwargs) - Gtk.Window.__init__(self) + self.window = None + + def init_window(self): + if self.window: + return + self.window = Gtk.Window(title="Subplot Configuration Tool") try: - self.window.set_icon_from_file(window_icon) + self.window.window.set_icon_from_file(window_icon) except (SystemExit, KeyboardInterrupt): # re-raise exit type Exceptions raise @@ -898,12 +903,12 @@ def __init__(self, *args, **kwargs): # we presumably already logged a message on the # failure of the main plot, don't keep reporting pass - self.set_title("Subplot Configuration Tool") + self.vbox = Gtk.Box() self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) - self.add(self.vbox) + self.window.add(self.vbox) self.vbox.show() - self.connect('destroy', self.unregister) + self.window.connect('destroy', self.destroy) toolfig = Figure(figsize=(6, 3)) canvas = self.figure.canvas.__class__(toolfig) @@ -914,17 +919,22 @@ def __init__(self, *args, **kwargs): w = int(toolfig.bbox.width) h = int(toolfig.bbox.height) - self.set_default_size(w, h) + self.window.set_default_size(w, h) canvas.show() self.vbox.pack_start(canvas, True, True, 0) - self.show() + self.window.show() + + def destroy(self, *args): + self.window.destroy() + self.window = None def _get_canvas(self, fig): return self.canvas.__class__(fig) def trigger(self, event): - self.present() + self.init_window() + self.window.present() ConfigureSubplots = ConfigureSubplotsGTK3 diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index bbaf35670cf0..110ef93aac0b 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -1049,6 +1049,16 @@ def trigger(self, *args): class ConfigureSubplotsTk(ConfigureSubplotsBase): def __init__(self, *args, **kwargs): ConfigureSubplotsBase.__init__(self, *args, **kwargs) + self.window = None + + def trigger(self, event): + self.init_window() + self.window.lift() + + def init_window(self): + if self.window: + return + toolfig = Figure(figsize=(6, 3)) self.window = Tk.Tk() @@ -1059,12 +1069,9 @@ def __init__(self, *args, **kwargs): canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) self.window.protocol("WM_DELETE_WINDOW", self.destroy) - def trigger(self, event): - self.window.lift() - def destroy(self, *args, **kwargs): - self.unregister() self.window.destroy() + self.window = None SaveFigure = SaveFigureTk From 9d3c97796253dd9e152efdf71b52526a7c0c4b81 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 4 Sep 2014 16:53:16 -0400 Subject: [PATCH 36/41] removing unregister --- examples/user_interfaces/navigation.py | 8 +++--- lib/matplotlib/backend_bases.py | 40 +++++++++++--------------- lib/matplotlib/backend_tools.py | 13 --------- 3 files changed, 20 insertions(+), 41 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index ab348db69b8e..42903b03d9c6 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -1,6 +1,6 @@ import matplotlib -# matplotlib.use('GTK3Cairo') -matplotlib.use('TkAGG') +matplotlib.use('GTK3Cairo') +# matplotlib.use('TkAGG') matplotlib.rcParams['toolbar'] = 'navigation' import matplotlib.pyplot as plt from matplotlib.backend_tools import ToolBase @@ -53,7 +53,7 @@ def trigger(self, event): if matplotlib.rcParams['backend'] == 'GTK3Cairo': fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) -# Just for fun, lets remove the back button -# fig.canvas.manager.navigation.remove_tool('Back') +# Just for fun, lets remove the forward button +fig.canvas.manager.navigation.remove_tool('Forward') plt.show() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d7ce3017a22b..b11ea72a9d9d 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3246,6 +3246,11 @@ def get_tool_keymap(self, name): keys = [k for k, i in six.iteritems(self._keys) if i == name] return keys + def _remove_keys(self, name): + keys = [k for k, v in six.iteritems(self._keys) if v == name] + for k in keys: + del self._keys[k] + def set_tool_keymap(self, name, *keys): """Set the keymap associated with a tool @@ -3259,9 +3264,7 @@ def set_tool_keymap(self, name, *keys): if name not in self._tools: raise AttributeError('%s not in Tools' % name) - active_keys = [k for k, i in six.iteritems(self._keys) if i == name] - for k in active_keys: - del self._keys[k] + self._remove_keys(name) for key in keys: for k in validate_stringlist(key): @@ -3270,21 +3273,6 @@ def set_tool_keymap(self, name, *keys): (k, self._keys[k], name)) self._keys[k] = name - def unregister(self, name): - """Unregister the tool from Navigation - - Parameters - ---------- - name : string - Name of the tool to unregister - """ - - if self._toggled == name: - self._handle_toggle(name, from_toolbar=False) - if name in self._tools: - self._tools[name].destroy() - del self._tools[name] - def remove_tool(self, name): """Remove tool from the `Navigation` @@ -3294,15 +3282,19 @@ def remove_tool(self, name): Name of the Tool """ - self.unregister(name) + tool = self._tools[name] + tool.destroy() - keys = [k for k, v in six.iteritems(self._keys) if v == name] - for k in keys: - del self._keys[k] + if self._toggled == name: + self._handle_toggle(name, from_toolbar=False) - if self.toolbar: + self._remove_keys(name) + + if self.toolbar and tool.intoolbar: self.toolbar._remove_toolitem(name) + del self._tools[name] + def add_tools(self, tools): """ Add multiple tools to `Navigation` @@ -3449,7 +3441,7 @@ def get_tools(self): for name in sorted(self._tools.keys()): tool = self._tools[name] keys = [k for k, i in six.iteritems(self._keys) if i == name] - d[name] = {'cls': tool, + d[name] = {'obj': tool, 'description': tool.description, 'keymap': keys} return d diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 93a9c2f54818..7d9b845ef2dd 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -92,19 +92,6 @@ def set_figure(self, figure): self.figure = figure self.navigation = figure.canvas.manager.navigation - def unregister(self, *args): - """Unregister the tool from the instances of Navigation - - It is usually called by during destroy if it is a - graphical Tool. - - If the reference in navigation was the last reference - to the instance of the tool, it will be garbage collected - """ - - # call this to unregister from navigation - self.navigation.unregister(self._name) - @property def name(self): return self._name From 6e0b7e6a12b1d17bb58219ff7102d06a81ed5fc0 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 5 Sep 2014 10:21:40 -0400 Subject: [PATCH 37/41] change cursor inmediately after toggle --- lib/matplotlib/backend_bases.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index b11ea72a9d9d..d7e55beef196 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3434,6 +3434,9 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): for a in self.canvas.figure.get_axes(): a.set_navigate_mode(self._toggled) + # Change the cursor inmediately, don't wait for mouse move + self._set_cursor(event) + def get_tools(self): """Return the tools controlled by `Navigation`""" @@ -3446,7 +3449,10 @@ def get_tools(self): 'keymap': keys} return d - def _mouse_move(self, event): + def _set_cursor(self, event): + """Call the backend specific set_cursor method, + if the pointer is inaxes + """ if not event.inaxes or not self._toggled: if self._last_cursor != self._default_cursor: self.set_cursor(self._default_cursor) @@ -3458,6 +3464,9 @@ def _mouse_move(self, event): self.set_cursor(cursor) self._last_cursor = cursor + def _mouse_move(self, event): + self._set_cursor(event) + if self.toolbar is None or self.messagelock.locked(): return From 206935bbd05d5aee7a534f2422f819a56fbbafcb Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 15 Oct 2014 19:50:39 -0400 Subject: [PATCH 38/41] removing intoolbar --- examples/user_interfaces/navigation.py | 86 +++++++++++++------------- lib/matplotlib/backend_tools.py | 34 +++------- 2 files changed, 51 insertions(+), 69 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 42903b03d9c6..efbbe323d09c 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -6,54 +6,54 @@ from matplotlib.backend_tools import ToolBase -# Create a simple tool to list all the tools -class ListTools(ToolBase): - # keyboard shortcut - keymap = 'm' - description = 'List Tools' - - def trigger(self, event): - tools = self.navigation.get_tools() - - print ('_' * 80) - print ("{0:12} {1:45} {2}".format('Name (id)', - 'Tool description', - 'Keymap')) - print ('_' * 80) - for name in sorted(tools.keys()): - keys = ', '.join(sorted(tools[name]['keymap'])) - print ("{0:12} {1:45} {2}".format(name, - tools[name]['description'], - keys)) - print ('_' * 80) - - -# A simple example of copy canvas -# ref: at https://github.com/matplotlib/matplotlib/issues/1987 -class CopyToolGTK3(ToolBase): - keymap = 'ctrl+c' - description = 'Copy canvas' - # It is not added to the toolbar as a button - intoolbar = False - - def trigger(self, event): - from gi.repository import Gtk, Gdk - clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - window = self.figure.canvas.get_window() - x, y, width, height = window.get_geometry() - pb = Gdk.pixbuf_get_from_window(window, x, y, width, height) - clipboard.set_image(pb) +# # Create a simple tool to list all the tools +# class ListTools(ToolBase): +# # keyboard shortcut +# keymap = 'm' +# description = 'List Tools' +# +# def trigger(self, event): +# tools = self.navigation.get_tools() +# +# print ('_' * 80) +# print ("{0:12} {1:45} {2}".format('Name (id)', +# 'Tool description', +# 'Keymap')) +# print ('_' * 80) +# for name in sorted(tools.keys()): +# keys = ', '.join(sorted(tools[name]['keymap'])) +# print ("{0:12} {1:45} {2}".format(name, +# tools[name]['description'], +# keys)) +# print ('_' * 80) +# +# +# # A simple example of copy canvas +# # ref: at https://github.com/matplotlib/matplotlib/issues/1987 +# class CopyToolGTK3(ToolBase): +# keymap = 'ctrl+c' +# description = 'Copy canvas' +# # It is not added to the toolbar as a button +# intoolbar = False +# +# def trigger(self, event): +# from gi.repository import Gtk, Gdk +# clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) +# window = self.figure.canvas.get_window() +# x, y, width, height = window.get_geometry() +# pb = Gdk.pixbuf_get_from_window(window, x, y, width, height) +# clipboard.set_image(pb) fig = plt.figure() plt.plot([1, 2, 3]) # Add the custom tools that we created -fig.canvas.manager.navigation.add_tool('List', ListTools) -if matplotlib.rcParams['backend'] == 'GTK3Cairo': - fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) - -# Just for fun, lets remove the forward button -fig.canvas.manager.navigation.remove_tool('Forward') +# fig.canvas.manager.navigation.add_tool('List', ListTools) +# if matplotlib.rcParams['backend'] == 'GTK3Cairo': +# fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) +# +# # Just for fun, lets remove the forward button +# fig.canvas.manager.navigation.remove_tool('Forward') plt.show() diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 7d9b845ef2dd..ec06d1d3111c 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -56,9 +56,6 @@ class ToolBase(object): `name` is used as a label in the toolbar button """ - intoolbar = True - """Add the tool to the toolbar""" - cursor = None """Cursor to use when the tool is active""" @@ -143,7 +140,6 @@ def toggled(self): class ToolQuit(ToolBase): """Tool to call the figure manager destroy method""" - intoolbar = False description = 'Quit the figure' keymap = rcParams['keymap.quit'] @@ -154,7 +150,6 @@ def trigger(self, event): class ToolEnableAllNavigation(ToolBase): """Tool to enable all axes for navigation interaction""" - intoolbar = False description = 'Enables all axes navigation' keymap = rcParams['keymap.all_axes'] @@ -171,7 +166,6 @@ def trigger(self, event): class ToolEnableNavigation(ToolBase): """Tool to enable a specific axes for navigation interaction""" - intoolbar = False description = 'Enables one axes navigation' keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9) @@ -191,7 +185,6 @@ def trigger(self, event): class ToolToggleGrid(ToolBase): """Tool to toggle the grid of the figure""" - intoolbar = False description = 'Toogle Grid' keymap = rcParams['keymap.grid'] @@ -205,7 +198,6 @@ def trigger(self, event): class ToolToggleFullScreen(ToolBase): """Tool to toggle full screen""" - intoolbar = False description = 'Toogle Fullscreen mode' keymap = rcParams['keymap.fullscreen'] @@ -218,7 +210,6 @@ class ToolToggleYScale(ToolBase): description = 'Toogle Scale Y axis' keymap = rcParams['keymap.yscale'] - intoolbar = False def trigger(self, event): ax = event.inaxes @@ -239,7 +230,6 @@ class ToolToggleXScale(ToolBase): description = 'Toogle Scale X axis' keymap = rcParams['keymap.xscale'] - intoolbar = False def trigger(self, event): ax = event.inaxes @@ -720,20 +710,12 @@ def _mouse_move(self, event): self.navigation.canvas.draw_idle() -tools = (('Grid', ToolToggleGrid), - ('Fullscreen', ToolToggleFullScreen), - ('Quit', ToolQuit), - ('EnableAll', ToolEnableAllNavigation), - ('EnableOne', ToolEnableNavigation), - ('XScale', ToolToggleXScale), - ('YScale', ToolToggleYScale), - ('Home', ToolHome), - ('Back', ToolBack), - ('Forward', ToolForward), - ('Spacer1', None), - ('Zoom', ToolZoom), - ('Pan', ToolPan), - ('Spacer2', None), - ('Subplots', 'ConfigureSubplots'), - ('Save', 'SaveFigure')) +tools = {'navigation': [ToolHome, ToolBack, ToolForward], + 'zoompan': [ToolZoom, ToolPan], + 'layout': ['ConfigureSubplots', ], + 'io': ['SaveFigure', ], + None: [ToolToggleGrid, ToolToggleFullScreen, ToolQuit, + ToolEnableAllNavigation, ToolEnableNavigation, + ToolToggleXScale, ToolToggleYScale]} + """Default tools""" From 30dcf3b46499286b645e9caac7755392bff47607 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 16 Oct 2014 17:33:25 -0400 Subject: [PATCH 39/41] events working --- examples/user_interfaces/navigation.py | 84 +++--- lib/matplotlib/backend_bases.py | 348 +++++++++++++----------- lib/matplotlib/backend_tools.py | 52 +++- lib/matplotlib/backends/backend_gtk3.py | 41 +-- 4 files changed, 283 insertions(+), 242 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index efbbe323d09c..282e551379fc 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -6,54 +6,54 @@ from matplotlib.backend_tools import ToolBase -# # Create a simple tool to list all the tools -# class ListTools(ToolBase): -# # keyboard shortcut -# keymap = 'm' -# description = 'List Tools' -# -# def trigger(self, event): -# tools = self.navigation.get_tools() -# -# print ('_' * 80) -# print ("{0:12} {1:45} {2}".format('Name (id)', -# 'Tool description', -# 'Keymap')) -# print ('_' * 80) -# for name in sorted(tools.keys()): -# keys = ', '.join(sorted(tools[name]['keymap'])) -# print ("{0:12} {1:45} {2}".format(name, -# tools[name]['description'], -# keys)) -# print ('_' * 80) -# -# -# # A simple example of copy canvas -# # ref: at https://github.com/matplotlib/matplotlib/issues/1987 -# class CopyToolGTK3(ToolBase): -# keymap = 'ctrl+c' -# description = 'Copy canvas' -# # It is not added to the toolbar as a button -# intoolbar = False -# -# def trigger(self, event): -# from gi.repository import Gtk, Gdk -# clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) -# window = self.figure.canvas.get_window() -# x, y, width, height = window.get_geometry() -# pb = Gdk.pixbuf_get_from_window(window, x, y, width, height) -# clipboard.set_image(pb) +# Create a simple tool to list all the tools +class ListTools(ToolBase): + # keyboard shortcut + keymap = 'm' + description = 'List Tools' + + def trigger(self, event): + tools = self.navigation.get_tools() + + print ('_' * 80) + print ("{0:12} {1:45} {2}".format('Name (id)', + 'Tool description', + 'Keymap')) + print ('_' * 80) + for name in sorted(tools.keys()): + keys = ', '.join(sorted(tools[name]['keymap'])) + print ("{0:12} {1:45} {2}".format(name, + tools[name]['description'], + keys)) + print ('_' * 80) + + +# A simple example of copy canvas +# ref: at https://github.com/matplotlib/matplotlib/issues/1987 +class CopyToolGTK3(ToolBase): + keymap = 'ctrl+c' + description = 'Copy canvas' + # It is not added to the toolbar as a button + intoolbar = False + + def trigger(self, event): + from gi.repository import Gtk, Gdk + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + window = self.figure.canvas.get_window() + x, y, width, height = window.get_geometry() + pb = Gdk.pixbuf_get_from_window(window, x, y, width, height) + clipboard.set_image(pb) fig = plt.figure() plt.plot([1, 2, 3]) # Add the custom tools that we created -# fig.canvas.manager.navigation.add_tool('List', ListTools) -# if matplotlib.rcParams['backend'] == 'GTK3Cairo': -# fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) +fig.canvas.manager.navigation.add_tool('List', ListTools) +if matplotlib.rcParams['backend'] == 'GTK3Cairo': + fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) # -# # Just for fun, lets remove the forward button -# fig.canvas.manager.navigation.remove_tool('Forward') +# Just for fun, lets remove the forward button +fig.canvas.manager.navigation.remove_tool('forward') plt.show() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d7e55beef196..95be7b5130dd 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3183,13 +3183,38 @@ def set_history_buttons(self): pass +class NavigationEvent(object): + """"A Navigation Event ('tool_add_event', + 'tool_remove_event', + 'tool_trigger_event', + 'navigation_message_event'). + Attributes + ---------- + name: String + Name of the event + tool: ToolInstance + data: Extra data + source: String + Name of the object responsible for emiting the event + ('toolbar', 'navigation', 'keypress', etc...) + event: Event + Original event that causes navigation to emit this event + """ + + def __init__(self, name, tool, source, data=None, event=None): + self.name = name + self.tool = tool + self.data = data + self.source = source + self.event = event + + class NavigationBase(object): """ Helper class that groups all the user interactions for a FigureManager Attributes ---------- manager : `FigureManager` instance - toolbar : `Toolbar` instance that is controlled by this `Navigation` keypresslock : `LockDraw` to know if the `canvas` key_press_event is locked messagelock : `LockDraw` to know if the message is available to write @@ -3198,11 +3223,9 @@ class NavigationBase(object): _default_cursor = cursors.POINTER def __init__(self, manager): - """.. automethod:: _toolbar_callback""" - self.manager = manager self.canvas = manager.canvas - self.toolbar = manager.toolbar + self.callbacks = cbook.CallbackRegistry() self._key_press_handler_id = self.canvas.mpl_connect( 'key_press_event', self._key_press) @@ -3216,11 +3239,77 @@ def __init__(self, manager): # to process keypress event self.keypresslock = widgets.LockDraw() - # to write into toolbar message + # To prevent the firing of 'navigation_message_event' self.messagelock = widgets.LockDraw() self._last_cursor = self._default_cursor + def mpl_connect(self, s, func): + return self.callbacks.connect(s, func) + + def mpl_disconnect(self, cid): + return self.callbacks.disconnect(cid) + + def tool_add_event(self, tool, group, position): + """ + This method will call all functions connected to the + 'tool_add_event' with a :class:`NavigationEvent` + """ + s = 'tool_add_event' + data = {'group': group, + 'position': position} + event = NavigationEvent(s, tool, 'navigation', data) + self.callbacks.process(s, event) + + def tool_remove_event(self, tool): + """ + This method will call all functions connected to the + 'tool_remove_event' with a :class:`NavigationEvent` + """ + s = 'tool_remove_event' + event = NavigationEvent(s, tool, 'navigation') + self.callbacks.process(s, event) + + def tool_trigger_event(self, name, source, originalevent=None): + """ + This method will call all functions connected to the + 'tool_trigger_event' with a :class:`NavigationEvent` + """ + if name not in self._tools: + raise AttributeError('%s not in Tools' % name) + + tool = self._tools[name] + + if isinstance(tool, tools.ToolToggleBase): + if self._toggled == name: + self._toggled = None + elif self._toggled is not None: + self.tool_trigger_event(self._toggled, 'navigation', + originalevent) + self._toggled = name + else: + self._toggled = name + + tool.trigger(originalevent) + + s = 'tool_trigger_event' + event = NavigationEvent(s, tool, source, originalevent) + self.callbacks.process(s, event) + + for a in self.canvas.figure.get_axes(): + a.set_navigate_mode(self._toggled) + + self._set_cursor(originalevent) + + def message_event(self, message, source='navigation'): + """ + This method will call all functions connected to the + 'navigation_message_event' with a :class:`NavigationEvent` + """ + s = 'navigation_message_event' + event = NavigationEvent(s, None, source, data=message) + self.callbacks.process(s, event) + @property def active_toggle(self): """Toggled Tool @@ -3286,12 +3375,11 @@ def remove_tool(self, name): tool.destroy() if self._toggled == name: - self._handle_toggle(name, from_toolbar=False) + self.tool_trigger_event(tool, 'navigation') self._remove_keys(name) - if self.toolbar and tool.intoolbar: - self.toolbar._remove_toolitem(name) + self.tool_remove_event(tool) del self._tools[name] @@ -3304,14 +3392,12 @@ def add_tools(self, tools): a either a reference to the tool Tool class itself, or None to insert a spacer. See :func:`add_tool`. """ - for name, tool in tools: - if tool is None: - if self.toolbar is not None: - self.toolbar.add_separator(-1) - else: - self.add_tool(name, tool, None) - def add_tool(self, name, tool, position=None): + for group, grouptools in tools: + for position, tool in enumerate(grouptools): + self.add_tool(tool[1], tool[0], group, position) + + def add_tool(self, name, tool, group=None, position=None): """Add tool to `Navigation` Parameters @@ -3338,20 +3424,7 @@ def add_tool(self, name, tool, position=None): if tool_cls.keymap is not None: self.set_tool_keymap(name, tool_cls.keymap) - if self.toolbar and tool_cls.intoolbar: - # TODO: better search for images, they are not always in the - # datapath - basedir = os.path.join(rcParams['datapath'], 'images') - if tool_cls.image is not None: - fname = os.path.join(basedir, tool_cls.image) - else: - fname = None - toggle = issubclass(tool_cls, tools.ToolToggleBase) - self.toolbar._add_toolitem(name, - tool_cls.description, - fname, - position, - toggle) + self.tool_add_event(self._tools[name], group, position) def _get_cls_to_instantiate(self, callback_class): if isinstance(callback_class, six.string_types): @@ -3372,18 +3445,7 @@ def trigger_tool(self, name, event=None): Method to programatically "click" on Tools """ - - self._trigger_tool(name, event, False) - - def _trigger_tool(self, name, event, from_toolbar): - if name not in self._tools: - raise AttributeError('%s not in Tools' % name) - - tool = self._tools[name] - if isinstance(tool, tools.ToolToggleBase): - self._handle_toggle(name, event=event, from_toolbar=from_toolbar) - else: - tool.trigger(event) + self.tool_trigger_event(name, 'navigation', event) def _key_press(self, event): if event.key is None or self.keypresslock.locked(): @@ -3392,50 +3454,8 @@ def _key_press(self, event): name = self._keys.get(event.key, None) if name is None: return - self._trigger_tool(name, event, False) - - def _toolbar_callback(self, name): - """Callback for the `Toolbar` - - All Toolbar implementations have to call this method to signal that a - toolitem was clicked on - - Parameters - ---------- - name : string - Name of the tool that was activated (click) by the user using the - toolbar - """ - - self._trigger_tool(name, None, True) - - def _handle_toggle(self, name, event=None, from_toolbar=False): - # toggle toolbar without callback - if not from_toolbar and self.toolbar: - self.toolbar._toggle(name, False) - - tool = self._tools[name] - if self._toggled is None: - # first trigger of tool - self._toggled = name - elif self._toggled == name: - # second trigger of tool - self._toggled = None - else: - # other tool is triggered so trigger toggled tool - if self.toolbar: - # untoggle the previous toggled tool - self.toolbar._toggle(self._toggled, False) - self._tools[self._toggled].trigger(event) - self._toggled = name - - tool.trigger(event) - - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self._toggled) - # Change the cursor inmediately, don't wait for mouse move - self._set_cursor(event) + self.tool_trigger_event(name, 'keypress', event) def get_tools(self): """Return the tools controlled by `Navigation`""" @@ -3453,6 +3473,9 @@ def _set_cursor(self, event): """Call the backend specific set_cursor method, if the pointer is inaxes """ + if not event: + return + if not event.inaxes or not self._toggled: if self._last_cursor != self._default_cursor: self.set_cursor(self._default_cursor) @@ -3467,9 +3490,11 @@ def _set_cursor(self, event): def _mouse_move(self, event): self._set_cursor(event) - if self.toolbar is None or self.messagelock.locked(): + if self.messagelock.locked(): return + message = ' ' + if event.inaxes and event.inaxes.get_navigate(): try: @@ -3478,15 +3503,13 @@ def _mouse_move(self, event): pass else: if self._toggled: - self.toolbar.set_message('%s, %s' % (self._toggled, s)) + message = '%s, %s' % (self._toggled, s) else: - self.toolbar.set_message(s) - else: - self.toolbar.set_message('') + message = s + self.message_event(message) def set_cursor(self, cursor): - """ - Set the current cursor to one of the :class:`Cursors` + """Set the current cursor to one of the :class:`Cursors` enums values """ @@ -3533,33 +3556,92 @@ class ToolbarBase(object): """ def __init__(self, manager): + self.manager = manager + self._tool_trigger_id = None + self._add_tool_id = None + self._remove_tool_id = None + self._navigation = None + + def _get_image_filename(self, image): + # TODO: better search for images, they are not always in the + # datapath + basedir = os.path.join(rcParams['datapath'], 'images') + if image is not None: + fname = os.path.join(basedir, image) + else: + fname = None + return fname + + def _add_tool_callback(self, event): + name = event.tool.name + group = event.data['group'] + position = event.data['position'] + image = self._get_image_filename(event.tool.image) + description = event.tool.description + toggle = isinstance(event.tool, tools.ToolToggleBase) + self.add_toolitem(name, group, position, image, description, toggle) + + def _remove_tool_callback(self, event): + self.remove_toolitem(event.tool.name) + + def _tool_trigger_callback(self, event): + if event.source == 'toolbar': + return + + if isinstance(event.tool, tools.ToolToggleBase): + self.toggle_toolitem(event.tool.name) + + def _message_event_callback(self, event): + self.set_message(event.data) + + def trigger_tool(self, name): + """Inform navigation of a toolbar event + + Uses the navigation method to emit a 'tool_trigger_event' + with 'navigation' as the source + + Parameters + ---------- + name : String + Name(id) of the tool that was triggered in the toolbar + """ - .. automethod:: _add_toolitem - .. automethod:: _remove_toolitem - .. automethod:: _toggle - """ + self._navigation.tool_trigger_event(name, 'toolbar') - self.manager = manager + def set_navigation(self, navigation): + """Initialize the callbacks for navigation events""" + self._navigation = navigation + self._add_tool_id = self._navigation.mpl_connect( + 'tool_add_event', self._add_tool_callback) + + self._tool_trigger_id = self._navigation.mpl_connect( + 'tool_trigger_event', self._tool_trigger_callback) + + self._message_id = self._navigation.mpl_connect( + 'navigation_message_event', self._message_event_callback) - def _add_toolitem(self, name, description, image_file, position, - toggle): + self._remove_tool_id = self._navigation.mpl_connect( + 'tool_remove_event', self._remove_tool_callback) + + def add_toolitem(self, name, group, position, image, description, toggle): """Add a toolitem to the toolbar The callback associated with the button click event, - must be **EXACTLY** `self.manager.navigation._toolbar_callback(name)` + must be **EXACTLY** `self.trigger_tool(name)` Parameters ---------- name : string Name of the tool to add, this is used as ID and as default label of the buttons - description : string - Description of the tool, used for the tooltips + group : String + Name of the group that the tool belongs to + position : Int + Position of the tool whthin its group if -1 at the End image_file : string Filename of the image for the button or `None` - position : integer - Position of the toolitem within the other toolitems - if -1 at the End + description : string + Description of the tool, used for the tooltips toggle : bool * `True` : The button is a toggle (change the pressed/unpressed state between consecutive clicks) @@ -3569,41 +3651,12 @@ def _add_toolitem(self, name, description, image_file, position, raise NotImplementedError - def add_separator(self, pos): - """Add a separator - - Parameters - ---------- - pos : integer - Position where to add the separator within the toolitems - if -1 at the end - """ - - pass - def set_message(self, s): """Display a message on toolbar or in status bar""" pass - def _toggle(self, name, callback=False): - """Toogle a button - - Parameters - ---------- - name : string - Name of the button to toggle - callback : bool - * `True`: call the button callback during toggle - * `False`: toggle the button without calling the callback - - """ - - # carefull, callback means to perform or not the callback while - # toggling - raise NotImplementedError - - def _remove_toolitem(self, name): + def remove_toolitem(self, name): """Remove a toolitem from the `Toolbar` Parameters @@ -3614,30 +3667,3 @@ def _remove_toolitem(self, name): """ raise NotImplementedError - - def move_toolitem(self, pos_ini, pos_fin): - """Change the position of a toolitem - - Parameters - ---------- - pos_ini : integer - Initial position of the toolitem to move - pos_fin : integer - Final position of the toolitem - """ - - pass - - def set_toolitem_visibility(self, name, visible): - """Change the visibility of a toolitem - - Parameters - ---------- - name : string - Name of the `Tool` - visible : bool - * `True`: set the toolitem visible - * `False`: set the toolitem invisible - """ - - pass diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index ec06d1d3111c..3b03f58442ca 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -182,7 +182,7 @@ def trigger(self, event): a.set_navigate(i == n) -class ToolToggleGrid(ToolBase): +class ToolGrid(ToolBase): """Tool to toggle the grid of the figure""" description = 'Toogle Grid' @@ -195,7 +195,7 @@ def trigger(self, event): self.figure.canvas.draw() -class ToolToggleFullScreen(ToolBase): +class ToolFullScreen(ToolBase): """Tool to toggle full screen""" description = 'Toogle Fullscreen mode' @@ -205,7 +205,7 @@ def trigger(self, event): self.figure.canvas.manager.full_screen_toggle() -class ToolToggleYScale(ToolBase): +class ToolYScale(ToolBase): """Tool to toggle between linear and logarithmic the Y axis""" description = 'Toogle Scale Y axis' @@ -225,7 +225,7 @@ def trigger(self, event): ax.figure.canvas.draw() -class ToolToggleXScale(ToolBase): +class ToolXScale(ToolBase): """Tool to toggle between linear and logarithmic the X axis""" description = 'Toogle Scale X axis' @@ -710,12 +710,42 @@ def _mouse_move(self, event): self.navigation.canvas.draw_idle() -tools = {'navigation': [ToolHome, ToolBack, ToolForward], - 'zoompan': [ToolZoom, ToolPan], - 'layout': ['ConfigureSubplots', ], - 'io': ['SaveFigure', ], - None: [ToolToggleGrid, ToolToggleFullScreen, ToolQuit, - ToolEnableAllNavigation, ToolEnableNavigation, - ToolToggleXScale, ToolToggleYScale]} +# Not so nice, extra order need for groups +# tools = {'home': {'cls': ToolHome, 'group': 'navigation', 'pos': 0}, +# 'back': {'cls': ToolBack, 'group': 'navigation', 'pos': 1}, +# 'forward': {'cls': ToolForward, 'group': 'navigation', 'pos': 2}, +# 'zoom': {'cls': ToolZoom, 'group': 'zoompan', 'pos': 0}, +# 'pan': {'cls': ToolPan, 'group': 'zoompan', 'pos': 1}, +# 'subplots': {'cls': 'ConfigureSubplots', 'group': 'layout'}, +# 'save': {'cls': 'SaveFigure', 'group': 'io'}, +# 'grid': {'cls': ToolGrid}, +# 'fullscreen': {'cls': ToolFullScreen}, +# 'quit': {'cls': ToolQuit}, +# 'allnavigation': {'cls': ToolEnableAllNavigation}, +# 'navigation': {'cls': ToolEnableNavigation}, +# 'xscale': {'cls': ToolXScale}, +# 'yscale': {'cls': ToolYScale} +# } + +# Horrible with implicit order +tools = [['navigation', [(ToolHome, 'home'), + (ToolBack, 'back'), + (ToolForward, 'forward')]], + + ['zoompan', [(ToolZoom, 'zoom'), + (ToolPan, 'pan')]], + + ['layout', [('ConfigureSubplots', 'subplots'), ]], + + ['io', [('SaveFigure', 'save'), ]], + + [None, [(ToolGrid, 'grid'), + (ToolFullScreen, 'fullscreen'), + (ToolQuit, 'quit'), + (ToolEnableAllNavigation, 'allnav'), + (ToolEnableNavigation, 'nav'), + (ToolXScale, 'xscale'), + (ToolYScale, 'yscale')]]] + """Default tools""" diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 7428926baf4a..c5fd918b1114 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -416,6 +416,7 @@ def __init__(self, canvas, num): self.toolbar = self._get_toolbar() self.navigation = self._get_navigation() if matplotlib.rcParams['toolbar'] == 'navigation': + self.toolbar.set_navigation(self.navigation) self.navigation.add_tools(tools) # calculate size for window @@ -780,8 +781,11 @@ def _setup_message_area(self): self.pack_end(sep, False, True, 0) sep.show_all() - def _add_toolitem(self, name, tooltip_text, image_file, position, - toggle): + def add_toolitem(self, name, group, position, image_file, description, + toggle): + if group is None: + return + if toggle: tbutton = Gtk.ToggleToolButton() else: @@ -795,34 +799,29 @@ def _add_toolitem(self, name, tooltip_text, image_file, position, if position is None: position = -1 - self._toolbar.insert(tbutton, position) + self._toolbar.insert(tbutton, -1) signal = tbutton.connect('clicked', self._call_tool, name) - tbutton.set_tooltip_text(tooltip_text) + tbutton.set_tooltip_text(description) tbutton.show_all() self._toolitems[name] = tbutton self._signals[name] = signal def _call_tool(self, btn, name): - self.manager.navigation._toolbar_callback(name) + self.trigger_tool(name) def set_message(self, s): self.message.set_label(s) - def _toggle(self, name, callback=False): + def toggle_toolitem(self, name): if name not in self._toolitems: - self.set_message('%s Not in toolbar' % name) return status = self._toolitems[name].get_active() - if not callback: - self._toolitems[name].handler_block(self._signals[name]) - + self._toolitems[name].handler_block(self._signals[name]) self._toolitems[name].set_active(not status) + self._toolitems[name].handler_unblock(self._signals[name]) - if not callback: - self._toolitems[name].handler_unblock(self._signals[name]) - - def _remove_toolitem(self, name): + def remove_toolitem(self, name): if name not in self._toolitems: self.set_message('%s Not in toolbar' % name) return @@ -835,20 +834,6 @@ def add_separator(self, pos=-1): toolitem.show() return toolitem - def move_toolitem(self, pos_ini, pos_fin): - widget = self._toolbar.get_nth_item(pos_ini) - if not widget: - self.set_message('Impossible to remove tool %d' % pos_ini) - return - self._toolbar.remove(widget) - self._toolbar.insert(widget, pos_fin) - - def set_toolitem_visibility(self, name, visible): - if name not in self._toolitems: - self.set_message('%s Not in toolbar' % name) - return - self._toolitems[name].set_visible(visible) - class SaveFigureGTK3(SaveFigureBase): From d8e90df31bdd717830ec7a20dbe1ca5ea11db35c Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 17 Oct 2014 14:12:51 -0400 Subject: [PATCH 40/41] using pydispatch --- examples/user_interfaces/navigation.py | 16 +- lib/matplotlib/backend_bases.py | 299 +++++++++--------------- lib/matplotlib/backend_tools.py | 69 +++++- lib/matplotlib/backends/backend_gtk3.py | 1 - 4 files changed, 192 insertions(+), 193 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 282e551379fc..76feee3fc177 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -4,7 +4,7 @@ matplotlib.rcParams['toolbar'] = 'navigation' import matplotlib.pyplot as plt from matplotlib.backend_tools import ToolBase - +from pydispatch import dispatcher # Create a simple tool to list all the tools class ListTools(ToolBase): @@ -14,7 +14,7 @@ class ListTools(ToolBase): def trigger(self, event): tools = self.navigation.get_tools() - + print ('_' * 80) print ("{0:12} {1:45} {2}".format('Name (id)', 'Tool description', @@ -25,7 +25,7 @@ def trigger(self, event): print ("{0:12} {1:45} {2}".format(name, tools[name]['description'], keys)) - print ('_' * 80) + print ('_' * 80) # A simple example of copy canvas @@ -45,6 +45,9 @@ def trigger(self, event): clipboard.set_image(pb) + + + fig = plt.figure() plt.plot([1, 2, 3]) @@ -52,8 +55,9 @@ def trigger(self, event): fig.canvas.manager.navigation.add_tool('List', ListTools) if matplotlib.rcParams['backend'] == 'GTK3Cairo': fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) -# -# Just for fun, lets remove the forward button -fig.canvas.manager.navigation.remove_tool('forward') + +# # Just for fun, lets remove the forward button +# fig.canvas.manager.navigation.remove_tool('forward') + plt.show() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 95be7b5130dd..bc4a321b4c4c 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -46,6 +46,7 @@ import warnings import time import io +from pydispatch import dispatcher import numpy as np import matplotlib.cbook as cbook @@ -3183,32 +3184,6 @@ def set_history_buttons(self): pass -class NavigationEvent(object): - """"A Navigation Event ('tool_add_event', - 'tool_remove_event', - 'tool_trigger_event', - 'navigation_message_event'). - Attributes - ---------- - name: String - Name of the event - tool: ToolInstance - data: Extra data - source: String - Name of the object responsible for emiting the event - ('toolbar', 'navigation', 'keypress', etc...) - event: Event - Original event that causes navigation to emit this event - """ - - def __init__(self, name, tool, source, data=None, event=None): - self.name = name - self.tool = tool - self.data = data - self.source = source - self.event = event - - class NavigationBase(object): """ Helper class that groups all the user interactions for a FigureManager @@ -3220,95 +3195,28 @@ class NavigationBase(object): messagelock : `LockDraw` to know if the message is available to write """ - _default_cursor = cursors.POINTER - def __init__(self, manager): self.manager = manager self.canvas = manager.canvas - self.callbacks = cbook.CallbackRegistry() self._key_press_handler_id = self.canvas.mpl_connect( 'key_press_event', self._key_press) - self._idDrag = self.canvas.mpl_connect( - 'motion_notify_event', self._mouse_move) - self._tools = {} self._keys = {} self._toggled = None # to process keypress event self.keypresslock = widgets.LockDraw() - # To prevent the firing of 'navigation_message_event' self.messagelock = widgets.LockDraw() - self._last_cursor = self._default_cursor - - def mpl_connect(self, s, func): - return self.callbacks.connect(s, func) - - def mpl_disconnect(self, cid): - return self.callbacks.disconnect(cid) - - def tool_add_event(self, tool, group, position): - """ - This method will call all functions connected to the - 'tool_add_event' with a :class:`NavigationEvent` - """ - s = 'tool_add_event' - data = {'group': group, - 'position': position} - event = NavigationEvent(s, tool, 'navigation', data) - self.callbacks.process(s, event) - - def tool_remove_event(self, tool): - """ - This method will call all functions connected to the - 'tool_remove_event' with a :class:`NavigationEvent` - """ - s = 'tool_remove_event' - event = NavigationEvent(s, tool, 'navigation') - self.callbacks.process(s, event) - - def tool_trigger_event(self, name, source, originalevent=None): - """ - This method will call all functions connected to the - 'tool_trigger_event' with a :class:`NavigationEvent` - """ - if name not in self._tools: - raise AttributeError('%s not in Tools' % name) - - tool = self._tools[name] - - if isinstance(tool, tools.ToolToggleBase): - if self._toggled == name: - self._toggled = None - elif self._toggled is not None: - self.tool_trigger_event(self._toggled, 'navigation', - originalevent) - self._toggled = name - else: - self._toggled = name - - tool.trigger(originalevent) - - s = 'tool_trigger_event' - event = NavigationEvent(s, tool, source, originalevent) - self.callbacks.process(s, event) - - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self._toggled) - - self._set_cursor(originalevent) - - def message_event(self, message, source='navigation'): - """ - This method will call all functions connected to the - 'navigation_message_event' with a :class:`NavigationEvent` - """ - s = 'navigation_message_event' - event = NavigationEvent(s, None, source, data=message) - self.callbacks.process(s, event) + def send_message(self, message, sender=None): + """ Send a navigation-message event""" + if sender is None: + sender = self + dispatcher.send(signal='navigation-message', + sender=sender, + message=message) @property def active_toggle(self): @@ -3379,7 +3287,9 @@ def remove_tool(self, name): self._remove_keys(name) - self.tool_remove_event(tool) + dispatcher.send(signal='navigation-tool-removed', + sender=self, + tool=tool) del self._tools[name] @@ -3406,6 +3316,8 @@ def add_tool(self, name, tool, group=None, position=None): Name of the tool, treated as the ID, has to be unique tool : string or `Tool` class Reference to find the class of the Tool to be added + group: String + Group to position the tool in position : int or None (default) Position in the toolbar, if None, is positioned at the end """ @@ -3424,9 +3336,39 @@ def add_tool(self, name, tool, group=None, position=None): if tool_cls.keymap is not None: self.set_tool_keymap(name, tool_cls.keymap) - self.tool_add_event(self._tools[name], group, position) + dispatcher.send(signal='navigation-tool-added', + sender=self, + tool=self._tools[name], + group=group, + position=position) + + if isinstance(self._tools[name], tools.ToolToggleBase): + dispatcher.connect(self._handle_toggle, + 'tool-pre-trigger-%s' % name, + sender=dispatcher.Any) + + def _handle_toggle(self, signal, sender, event=None): + # Toggle tools, need to be untoggled before other Toggle tool is used + # This is connected to the 'tool-pre-trigger-toolname' signal + name = '-'.join(signal.split('-')[3:]) + if self._toggled == name: + toggled = None + elif self._toggled is None: + toggled = name + else: + # untoggle currently toggled tool + dispatcher.send(signal='tool-trigger-%s' % self._toggled, + sender=self) + toggled = name + + self._toggled = toggled + for a in self.canvas.figure.get_axes(): + a.set_navigate_mode(self._toggled) + + self._set_cursor(event) def _get_cls_to_instantiate(self, callback_class): + # Find the class that corresponds to the tool if isinstance(callback_class, six.string_types): # FIXME: make more complete searching structure if callback_class in globals(): @@ -3445,7 +3387,9 @@ def trigger_tool(self, name, event=None): Method to programatically "click" on Tools """ - self.tool_trigger_event(name, 'navigation', event) + dispatcher.send(signal='tool-trigger-%s' % name, + sender=self, + event=event) def _key_press(self, event): if event.key is None or self.keypresslock.locked(): @@ -3454,8 +3398,7 @@ def _key_press(self, event): name = self._keys.get(event.key, None) if name is None: return - - self.tool_trigger_event(name, 'keypress', event) + self.trigger_tool(name, event) def get_tools(self): """Return the tools controlled by `Navigation`""" @@ -3470,43 +3413,24 @@ def get_tools(self): return d def _set_cursor(self, event): - """Call the backend specific set_cursor method, - if the pointer is inaxes - """ - if not event: - return + """Fire the tool-trigger-cursor event, - if not event.inaxes or not self._toggled: - if self._last_cursor != self._default_cursor: - self.set_cursor(self._default_cursor) - self._last_cursor = self._default_cursor + This event set the current cursor + in the tool ToolSetCursor + """ + if event is None: + class dummy(object): + cursor = None + event = dummy() + if self._toggled: + cursor = self._tools[self._toggled].cursor else: - if self._toggled: - cursor = self._tools[self._toggled].cursor - if cursor and self._last_cursor != cursor: - self.set_cursor(cursor) - self._last_cursor = cursor - - def _mouse_move(self, event): - self._set_cursor(event) - - if self.messagelock.locked(): - return - - message = ' ' - - if event.inaxes and event.inaxes.get_navigate(): - - try: - s = event.inaxes.format_coord(event.xdata, event.ydata) - except (ValueError, OverflowError): - pass - else: - if self._toggled: - message = '%s, %s' % (self._toggled, s) - else: - message = s - self.message_event(message) + cursor = None + setattr(event, 'cursor', cursor) +# event.cursor = cursor + dispatcher.send(signal='tool-trigger-cursor', + sender=self, + event=event) def set_cursor(self, cursor): """Set the current cursor to one of the :class:`Cursors` @@ -3557,12 +3481,53 @@ class ToolbarBase(object): def __init__(self, manager): self.manager = manager - self._tool_trigger_id = None - self._add_tool_id = None - self._remove_tool_id = None - self._navigation = None + + dispatcher.connect(self._add_tool_cbk, + signal='navigation-tool-added', + sender=dispatcher.Any) + + dispatcher.connect(self._remove_tool_cbk, + signal='navigation-tool-removed', + sender=dispatcher.Any) + + dispatcher.connect(self._message_cbk, + signal='navigation-message', + sender=dispatcher.Any) + + def _message_cbk(self, signal, sender, message): + """Captures the 'navigation-message to set message on the toolbar""" + self.set_message(message) + + def _tool_triggered_cbk(self, signal, sender): + """Captures the 'tool-trigger-toolname + + This is only used for toggled tools + If the sender is not the toolbar itself, just untoggle the toggled tool + """ + if sender is self: + return + + name = '-'.join(signal.split('-')[2:]) + self.toggle_toolitem(name) + + def _add_tool_cbk(self, tool, group, position, signal, sender): + """Captures 'navigation-tool-added' and add the tool to the toolbar""" + name = tool.name + image = self._get_image_filename(tool.image) + description = tool.description + toggle = isinstance(tool, tools.ToolToggleBase) + self.add_toolitem(name, group, position, image, description, toggle) + if toggle: + dispatcher.connect(self._tool_triggered_cbk, + signal='tool-trigger-%s' % name, + sender=dispatcher.Any) + + def _remove_tool_cbk(self, tool, signal, sender): + """Captures the 'navigation-tool-removed' signal and remove the tool""" + self.remove_toolitem(tool.name) def _get_image_filename(self, image): + """"Base on the image name find the corresponding image""" # TODO: better search for images, they are not always in the # datapath basedir = os.path.join(rcParams['datapath'], 'images') @@ -3572,33 +3537,11 @@ def _get_image_filename(self, image): fname = None return fname - def _add_tool_callback(self, event): - name = event.tool.name - group = event.data['group'] - position = event.data['position'] - image = self._get_image_filename(event.tool.image) - description = event.tool.description - toggle = isinstance(event.tool, tools.ToolToggleBase) - self.add_toolitem(name, group, position, image, description, toggle) - - def _remove_tool_callback(self, event): - self.remove_toolitem(event.tool.name) - - def _tool_trigger_callback(self, event): - if event.source == 'toolbar': - return - - if isinstance(event.tool, tools.ToolToggleBase): - self.toggle_toolitem(event.tool.name) - - def _message_event_callback(self, event): - self.set_message(event.data) +# def _message_event_callback(self, event): +# self.set_message(event.data) def trigger_tool(self, name): - """Inform navigation of a toolbar event - - Uses the navigation method to emit a 'tool_trigger_event' - with 'navigation' as the source + """fire the 'tool-trigger-toolname' signal Parameters ---------- @@ -3606,22 +3549,8 @@ def trigger_tool(self, name): Name(id) of the tool that was triggered in the toolbar """ - self._navigation.tool_trigger_event(name, 'toolbar') - - def set_navigation(self, navigation): - """Initialize the callbacks for navigation events""" - self._navigation = navigation - self._add_tool_id = self._navigation.mpl_connect( - 'tool_add_event', self._add_tool_callback) - - self._tool_trigger_id = self._navigation.mpl_connect( - 'tool_trigger_event', self._tool_trigger_callback) - - self._message_id = self._navigation.mpl_connect( - 'navigation_message_event', self._message_event_callback) - - self._remove_tool_id = self._navigation.mpl_connect( - 'tool_remove_event', self._remove_tool_callback) + dispatcher.send(signal='tool-trigger-%s' % name, + sender=self) def add_toolitem(self, name, group, position, image, description, toggle): """Add a toolitem to the toolbar diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 3b03f58442ca..3bce393cecd0 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -16,6 +16,7 @@ import matplotlib.cbook as cbook from weakref import WeakKeyDictionary import numpy as np +from pydispatch import dispatcher class Cursors: @@ -64,6 +65,18 @@ def __init__(self, figure, name, event=None): self.figure = None self.navigation = None self.set_figure(figure) + dispatcher.connect(self._trigger_cbk, + signal='tool-trigger-%s' % self.name, + sender=dispatcher.Any) + + def _trigger_cbk(self, signal, sender, event=None): + # Inform the rest of the world that we are going to trigger + # Used mainly to untoggle other tools + dispatcher.send(signal='tool-pre-trigger-%s' % self.name, + sender=sender, + event=event) + + self.trigger(event) def trigger(self, event): """Called when this tool gets used @@ -137,6 +150,58 @@ def toggled(self): return self._toggled +class ToolSetCursor(ToolBase): + def __init__(self, *args, **kwargs): + ToolBase.__init__(self, *args, **kwargs) + self._idDrag = self.figure.canvas.mpl_connect( + 'motion_notify_event', self.set_cursor) + self._cursor = None + self._default_cursor = cursors.POINTER + self._last_cursor = self._default_cursor + + def set_cursor(self, event): + if not event: + return + + if not getattr(event, 'inaxes', False) or not self._cursor: + if self._last_cursor != self._default_cursor: + self.navigation.set_cursor(self._default_cursor) + self._last_cursor = self._default_cursor + else: + if self._cursor: + cursor = self._cursor + if cursor and self._last_cursor != cursor: + self.navigation.set_cursor(cursor) + self._last_cursor = cursor + + def trigger(self, event): + self._cursor = event.cursor + self.set_cursor(event) + + +class ToolCursorPosition(ToolBase): + def __init__(self, *args, **kwargs): + ToolBase.__init__(self, *args, **kwargs) + self._idDrag = self.figure.canvas.mpl_connect( + 'motion_notify_event', self.send_message) + + def send_message(self, event): + if self.navigation.messagelock.locked(): + return + + message = ' ' + + if event.inaxes and event.inaxes.get_navigate(): + + try: + s = event.inaxes.format_coord(event.xdata, event.ydata) + except (ValueError, OverflowError): + pass + else: + message = s + self.navigation.send_message(message, self) + + class ToolQuit(ToolBase): """Tool to call the figure manager destroy method""" @@ -745,7 +810,9 @@ def _mouse_move(self, event): (ToolEnableAllNavigation, 'allnav'), (ToolEnableNavigation, 'nav'), (ToolXScale, 'xscale'), - (ToolYScale, 'yscale')]]] + (ToolYScale, 'yscale'), + (ToolCursorPosition, 'position'), + (ToolSetCursor, 'cursor')]]] """Default tools""" diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index c5fd918b1114..15a0f1c68305 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -416,7 +416,6 @@ def __init__(self, canvas, num): self.toolbar = self._get_toolbar() self.navigation = self._get_navigation() if matplotlib.rcParams['toolbar'] == 'navigation': - self.toolbar.set_navigation(self.navigation) self.navigation.add_tools(tools) # calculate size for window From 00c8f590154f99bb19907d359f5bb481ef646bb4 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Sat, 18 Oct 2014 00:14:55 -0400 Subject: [PATCH 41/41] pydispatch in travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cae44a2980fa..2284a2151642 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ python: - 3.3 install: - - pip install -q --use-mirrors nose python-dateutil numpy pep8 pyparsing pillow + - pip install -q --use-mirrors nose python-dateutil numpy pep8 pyparsing pillow pydispatch - sudo apt-get update && sudo apt-get -qq install inkscape libav-tools - python setup.py install