From 48866809d651ba6ee34164592b77336e30b70f7a Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 23 Jan 2014 13:35:40 -0500 Subject: [PATCH 01/64] navigation and toolbar coexistence --- lib/matplotlib/backend_bases.py | 371 +++++++++++++++++++++++- lib/matplotlib/backends/backend_gtk3.py | 215 +++++++++++++- lib/matplotlib/rcsetup.py | 2 +- 3 files changed, 577 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 908c05d4362d..8890b77b0598 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 @@ -2552,8 +2554,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`. @@ -2612,10 +2616,7 @@ def set_window_title(self, title): pass -class Cursors(object): - # 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): @@ -3195,3 +3196,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 4d32873f19d7..1b3a8bcaa100 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,7 +706,207 @@ def get_filename_from_user (self): return filename, self.ext -class DialogLineprops(object): + +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 60ab3e1a3fbb..a08c61072285 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -168,7 +168,7 @@ def validate_backend(s): def validate_toolbar(s): validator = ValidateInStrings( 'toolbar', - ['None', 'toolbar2'], + ['None', 'toolbar2', 'navigation'], ignorecase=True) return validator(s) From 268d925d3e8314f4998887ad7229bcf9829ba4b1 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 23 Jan 2014 15:46:41 -0500 Subject: [PATCH 02/64] 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 8890b77b0598..4019a6cbcd91 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2593,7 +2593,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 942cc903f22de39f6f0db2df7c52a4cab767d276 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 23 Jan 2014 15:51:06 -0500 Subject: [PATCH 03/64] 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 814f6bab8a5572115ae394e15160279ea02b086b Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 24 Jan 2014 10:29:05 -0500 Subject: [PATCH 04/64] helper methods in toolbar and navigation --- lib/matplotlib/backend_bases.py | 40 +++++++++++++++++++++++-- lib/matplotlib/backend_tools.py | 2 +- lib/matplotlib/backends/backend_gtk3.py | 26 +++++++++++++--- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 4019a6cbcd91..4dd8c61533cf 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3210,8 +3210,10 @@ class NavigationBase(object): tools.ToolToggleYScale, tools.ToolHome, tools.ToolBack, tools.ToolForward, + None, tools.ToolZoom, tools.ToolPan, + None, 'ConfigureSubplots', 'SaveFigure'] @@ -3248,7 +3250,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 @@ -3261,7 +3267,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) @@ -3292,6 +3319,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: @@ -3555,3 +3585,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 1b3a8bcaa100..19e2c1ac91c3 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -804,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() @@ -819,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 11cf64d9352f174bd0cfb788985af27b955c89aa Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 24 Jan 2014 13:23:16 -0500 Subject: [PATCH 05/64] 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 6dbccb231280..6e419ac2156f 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 4dd8c61533cf..3941c11db2a1 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, @@ -3200,6 +3208,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, @@ -3268,13 +3295,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) @@ -3290,12 +3347,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] @@ -3303,37 +3380,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): @@ -3383,7 +3469,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) @@ -3396,7 +3493,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: @@ -3409,7 +3506,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) @@ -3419,6 +3516,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')) @@ -3565,29 +3663,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 109f129f71a1da1767d1a6df10604448052f82e3 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Sat, 25 Jan 2014 22:06:41 -0500 Subject: [PATCH 06/64] 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 3941c11db2a1..ac7a1ca6da1d 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3215,17 +3215,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, @@ -3245,6 +3238,7 @@ class NavigationBase(object): 'SaveFigure'] def __init__(self, canvas, toolbar=None): + """.. automethod:: _toolbar_callback""" self.canvas = canvas self.toolbar = self._get_toolbar(toolbar, canvas) @@ -3294,18 +3288,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 @@ -3357,7 +3353,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: @@ -3668,17 +3665,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 2991b86ea5ecd1d9a278e932ca941734fd5b7158 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 27 Jan 2014 16:37:29 -0500 Subject: [PATCH 07/64] 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 ac7a1ca6da1d..8030444223a1 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3290,16 +3290,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 @@ -3313,7 +3314,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 @@ -3345,6 +3346,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 @@ -3354,8 +3360,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) @@ -3432,24 +3437,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) @@ -3458,6 +3460,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) @@ -3478,14 +3494,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 24ae298fd9c189cda4a2713e138deab26f96de4f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 27 Jan 2014 16:55:41 -0500 Subject: [PATCH 08/64] 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 bfff069e59c649a43d015bd505f2416f89e9e15d Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 28 Jan 2014 18:41:19 -0500 Subject: [PATCH 09/64] 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 8030444223a1..24c2d9d278e1 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3454,7 +3454,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?) @@ -3503,7 +3503,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: @@ -3515,7 +3515,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 24c733f2cf93891a0d17f49fb5e471f63dbf3b8e Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 28 Jan 2014 19:20:45 -0500 Subject: [PATCH 10/64] 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 24c2d9d278e1..45170fc69cb4 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3437,15 +3437,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) @@ -3472,7 +3472,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: @@ -3494,7 +3494,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 @@ -3507,14 +3507,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 111af35bc5e5bf58a2394ded37a04f50ba829a47 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 28 Jan 2014 19:36:49 -0500 Subject: [PATCH 11/64] 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 45170fc69cb4..52d3036ae09a 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3503,22 +3503,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 69f7e194f8a862dfe876d4985aa96129dde20bea Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 29 Jan 2014 12:53:42 -0500 Subject: [PATCH 12/64] 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 52d3036ae09a..a8f88db6f193 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3214,11 +3214,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, @@ -3248,11 +3246,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 @@ -3264,11 +3257,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: @@ -3461,16 +3450,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) @@ -3541,12 +3523,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) @@ -3558,7 +3534,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(): @@ -3575,30 +3551,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(): @@ -3663,9 +3615,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 890122c18d13e1580d6e1515405d918878ee941f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 29 Jan 2014 13:24:30 -0500 Subject: [PATCH 13/64] 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 fdd6b25a0bf8828d05d1bb25fd00d784cde023b9 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 4 Feb 2014 09:17:41 -0500 Subject: [PATCH 14/64] 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 a8f88db6f193..f39636107355 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3255,8 +3255,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: @@ -3383,6 +3384,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') @@ -3407,10 +3409,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): @@ -3439,9 +3442,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 1b13e5113e0c33870b3551e8b663387991d74f55 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 4 Feb 2014 09:47:07 -0500 Subject: [PATCH 15/64] 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 f39636107355..0c7e7e69b8f1 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3432,8 +3432,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) @@ -3457,6 +3456,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 f7dd1ec56375fbb57b597168336c29b4f92d73ec Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 4 Feb 2014 15:56:08 -0500 Subject: [PATCH 16/64] 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 f4ea69b4f317318fd7f015a01c186d39210f9fc3 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 6 Feb 2014 09:38:29 -0500 Subject: [PATCH 17/64] 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 0c7e7e69b8f1..2dbf4ee62d76 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3269,6 +3269,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 3981f5da3ea07a597162d6c29f6f3a621532a725 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Apr 2014 11:50:03 -0400 Subject: [PATCH 18/64] 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 2dbf4ee62d76..d729eef0f2ba 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3282,7 +3282,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 @@ -3316,7 +3316,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): @@ -3332,7 +3332,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] @@ -3377,7 +3377,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] @@ -3426,7 +3426,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] @@ -3525,7 +3525,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 6672a3969613a426db5a2dc01ddb6943cfb81380 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 1 May 2014 19:12:56 -0400 Subject: [PATCH 19/64] 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 d729eef0f2ba..4bcc1ad004e1 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3218,6 +3218,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, @@ -3237,6 +3238,7 @@ class NavigationBase(object): def __init__(self, canvas, toolbar=None): """.. automethod:: _toolbar_callback""" + self.canvas = canvas self.toolbar = self._get_toolbar(toolbar, canvas) @@ -3255,9 +3257,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: @@ -3272,11 +3274,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): @@ -3294,6 +3298,7 @@ def active_toggle(self): **string** : Currently toggled tool, or None """ + return self._toggled @property @@ -3302,6 +3307,7 @@ def instances(self): **dictionary** : Contains the active instances that are registered """ + return self._instances def get_tool_keymap(self, name): @@ -3316,6 +3322,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 @@ -3362,6 +3369,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: @@ -3375,6 +3383,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] @@ -3392,6 +3401,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 @@ -3427,7 +3437,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] @@ -3444,6 +3454,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): @@ -3457,8 +3468,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): @@ -3473,7 +3483,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] @@ -3490,24 +3500,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 @@ -3519,6 +3530,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')) @@ -3532,6 +3544,7 @@ def list_tools(self): def update(self): """Reset the axes stack""" + self.views.clear() self.positions.clear() # self.set_history_buttons() @@ -3567,9 +3580,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()) @@ -3577,6 +3592,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() @@ -3590,6 +3608,7 @@ def set_cursor(self, cursor): Set the current cursor to one of the :class:`Cursors` enums values """ + pass def update_view(self): @@ -3615,6 +3634,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(): @@ -3641,6 +3661,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) @@ -3655,6 +3676,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) @@ -3666,12 +3688,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, @@ -3699,6 +3723,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): @@ -3710,10 +3735,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): @@ -3728,7 +3755,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): @@ -3740,6 +3768,7 @@ def _remove_toolitem(self, name): Name of the tool to remove """ + raise NotImplementedError def move_toolitem(self, pos_ini, pos_fin): @@ -3752,6 +3781,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): @@ -3765,4 +3795,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 659099f92eca8e15bc4d82fb89990a8c62f8b59f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 08:48:51 -0400 Subject: [PATCH 20/64] 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 4bcc1ad004e1..e0b90e8bfea4 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3600,9 +3600,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 45c16dda19b0f65899cbb68f422b420dba0dcc10 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 12:22:28 -0400 Subject: [PATCH 21/64] 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 | 187 ++++++++++++++++++++++- 4 files changed, 200 insertions(+), 11 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 e0b90e8bfea4..15c6652d0918 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3403,6 +3403,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: @@ -3424,9 +3427,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 9d8368d25abf..20f2ace1f86d 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 @@ -541,9 +542,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 @@ -871,5 +885,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 217098a5eddcb579430feb21b5cec46b7b48fab3 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 12:33:00 -0400 Subject: [PATCH 22/64] 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 20f2ace1f86d..6b60d29fbc9f 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -544,12 +544,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 @@ -897,16 +898,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 @@ -916,7 +922,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) @@ -930,15 +936,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, @@ -950,7 +955,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) @@ -978,6 +983,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 @@ -1022,7 +1028,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) @@ -1033,16 +1040,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 f07dfb2b7215fa996fe9624e5aa208e5a0450be8 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 13:56:47 -0400 Subject: [PATCH 23/64] 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 0009c56b4ab5d8fbc593b61823ed7c2f03c572e6 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 7 May 2014 14:06:20 -0400 Subject: [PATCH 24/64] 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 | 31 ++++--- 5 files changed, 134 insertions(+), 159 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 15c6652d0918..c7b72897c5d1 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3212,7 +3212,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 @@ -3220,33 +3220,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() @@ -3262,36 +3248,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 @@ -3363,8 +3328,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. @@ -3393,29 +3357,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 @@ -3426,7 +3388,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') @@ -3435,10 +3397,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): @@ -3487,7 +3450,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 @@ -3533,26 +3496,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: @@ -3649,7 +3609,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 @@ -3701,7 +3660,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, @@ -3758,7 +3717,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 6b60d29fbc9f..71fb1c5d6b51 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -548,17 +548,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 @@ -930,23 +934,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, @@ -1006,7 +1008,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) @@ -1060,6 +1062,7 @@ def destroy(self, *args, **kwargs): SaveFigure = SaveFigureTk ConfigureSubplots = ConfigureSubplotsTk - +Toolbar = ToolbarTk +Navigation = NavigationTk FigureCanvas = FigureCanvasTkAgg FigureManager = FigureManagerTkAgg From 68ae62d638080b2f4ddecf3adb8d5a3dfbac58de Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Jul 2014 09:45:26 -0400 Subject: [PATCH 25/64] 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 c7b72897c5d1..35cab27750d8 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3382,11 +3382,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 2bf33981623be13f1c282f72e7202c176783692b Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Jul 2014 13:37:09 -0400 Subject: [PATCH 26/64] 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 23f1c583ca128e290c2513e5845e0eee956e03c4 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Jul 2014 16:52:25 -0400 Subject: [PATCH 27/64] 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 35cab27750d8..5c2554c9138b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3234,10 +3234,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 = {} @@ -3504,12 +3500,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: @@ -3539,28 +3529,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` @@ -3569,43 +3537,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 71fb1c5d6b51..6c9b78b88ee3 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 @@ -543,7 +544,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 4c91ac9578c48b7383d72fc91522342259da9cc9 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 25 Jul 2014 10:31:03 -0400 Subject: [PATCH 28/64] 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 6c9b78b88ee3..e32b32ace699 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 @@ -544,7 +543,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 f0081c7f7fd6897e7d8f59afa97e7adc65ea4362 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 25 Jul 2014 10:59:04 -0400 Subject: [PATCH 29/64] 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 e32b32ace699..06d9bf73bcb8 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -561,6 +561,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 84e7a482773a5893eda46c98abec90df7525497b Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Mon, 28 Jul 2014 19:40:38 +0200 Subject: [PATCH 30/64] 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 5c2554c9138b..7f6944e6ac24 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3244,13 +3244,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 @@ -3353,6 +3346,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 029ee9aeda15..7c3f67a2aa80 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 @@ -433,6 +434,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 107b43f82c0147e7eee0c3e904523e7a3b07810b Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Mon, 28 Jul 2014 23:57:39 +0200 Subject: [PATCH 31/64] 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 7f6944e6ac24..7d291880db4a 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3353,7 +3353,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 53b62afdc4b024d96b8b20e964995dbb2f0131b2 Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Tue, 29 Jul 2014 16:06:38 +0200 Subject: [PATCH 32/64] 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 | 3 +-- lib/matplotlib/pyplot.py | 4 ---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 7d291880db4a..c20b2fb90fc8 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2577,6 +2577,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. @@ -2624,6 +2628,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 06d9bf73bcb8..5258c8d461b5 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -525,11 +525,10 @@ 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 if matplotlib.rcParams['toolbar']=='toolbar2': self.toolbar = NavigationToolbar2TkAgg( canvas, self.window ) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 7c3f67a2aa80..029ee9aeda15 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 @@ -434,9 +433,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 0ecd61a4f40f58a7e91dd8fa0c32497d256b7a81 Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Tue, 29 Jul 2014 16:47:16 +0200 Subject: [PATCH 33/64] 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 c20b2fb90fc8..ef64480f44fd 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2579,7 +2579,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 7ec51594d254b629b66656115c086615ce3a2f07 Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Tue, 29 Jul 2014 19:10:55 +0200 Subject: [PATCH 34/64] Temporary fix to backends --- lib/matplotlib/backend_bases.py | 10 ---------- lib/matplotlib/backends/backend_gtk3.py | 7 ++++++- lib/matplotlib/backends/backend_tkagg.py | 5 +++-- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index ef64480f44fd..7d291880db4a 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2577,11 +2577,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. @@ -2629,11 +2624,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 5258c8d461b5..056dbcaaa129 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 @@ -525,10 +525,11 @@ 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 if matplotlib.rcParams['toolbar']=='toolbar2': self.toolbar = NavigationToolbar2TkAgg( canvas, self.window ) From 25656f737147352df3636b54c5d32ee1201736e5 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 3 Sep 2014 10:43:32 -0400 Subject: [PATCH 35/64] 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 7d291880db4a..c4efd67e8bdf 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3236,7 +3236,6 @@ def __init__(self, manager): self._tools = {} self._keys = {} - self._instances = {} self._toggled = None # to process keypress event @@ -3255,15 +3254,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 @@ -3305,28 +3295,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` @@ -3338,7 +3319,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] @@ -3385,7 +3366,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) @@ -3418,27 +3399,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(): @@ -3449,14 +3426,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` @@ -3477,7 +3446,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 @@ -3489,10 +3458,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) @@ -3516,7 +3485,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 056dbcaaa129..2fb9cba059db 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -1044,6 +1044,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() @@ -1054,12 +1064,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 f07b3e1101ef5c13fa40ada563c568ad3c8c2f7d Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 4 Sep 2014 16:53:16 -0400 Subject: [PATCH 36/64] 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 c4efd67e8bdf..84e32ab3e3b9 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3270,6 +3270,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 @@ -3283,9 +3288,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): @@ -3294,21 +3297,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` @@ -3318,15 +3306,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` @@ -3473,7 +3465,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 f60b26c1b9803fa841bbd098aa816ab88df898d2 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 5 Sep 2014 10:21:40 -0400 Subject: [PATCH 37/64] 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 84e32ab3e3b9..a141890e7a44 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3458,6 +3458,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`""" @@ -3470,7 +3473,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) @@ -3482,6 +3488,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 e482cf8c695aea2c4a315e3797e67da21162494f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 15 Oct 2014 19:50:39 -0400 Subject: [PATCH 38/64] 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 d8833822dddb7b0fed71c5deae3f97f2cb213392 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 16 Oct 2014 17:33:25 -0400 Subject: [PATCH 39/64] 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 a141890e7a44..b0e4eb20ecf0 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3207,13 +3207,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 @@ -3222,11 +3247,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) @@ -3240,11 +3263,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 @@ -3310,12 +3399,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] @@ -3328,14 +3416,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 @@ -3362,20 +3448,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): @@ -3396,18 +3469,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(): @@ -3416,50 +3478,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`""" @@ -3477,6 +3497,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) @@ -3491,9 +3514,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: @@ -3502,15 +3527,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 """ @@ -3557,33 +3580,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) @@ -3593,41 +3675,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 @@ -3638,30 +3691,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 e9e942d7478567fec8c6198b0e81019352008633 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 17 Oct 2014 14:12:51 -0400 Subject: [PATCH 40/64] 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 b0e4eb20ecf0..0a0d9fd05778 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 @@ -3207,32 +3208,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 @@ -3244,95 +3219,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): @@ -3403,7 +3311,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] @@ -3430,6 +3340,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 """ @@ -3448,9 +3360,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(): @@ -3469,7 +3411,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(): @@ -3478,8 +3422,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`""" @@ -3494,43 +3437,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` @@ -3581,12 +3505,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') @@ -3596,33 +3561,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 ---------- @@ -3630,22 +3573,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 fcabd2007e36b28c9e50daccea1e8101263ac50f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 20 Oct 2014 14:50:43 -0400 Subject: [PATCH 41/64] using navigation as signal handler --- examples/user_interfaces/navigation.py | 10 +- lib/matplotlib/backend_bases.py | 286 ++++++++++++++---------- lib/matplotlib/backend_tools.py | 129 +++++++---- lib/matplotlib/backends/backend_gtk3.py | 45 ++-- 4 files changed, 272 insertions(+), 198 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 76feee3fc177..f4daa9cf3317 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -12,7 +12,7 @@ class ListTools(ToolBase): keymap = 'm' description = 'List Tools' - def trigger(self, event): + def trigger(self, *args, **kwargs): tools = self.navigation.get_tools() print ('_' * 80) @@ -36,7 +36,7 @@ class CopyToolGTK3(ToolBase): # It is not added to the toolbar as a button intoolbar = False - def trigger(self, event): + def trigger(self, *args, **kwargs): from gi.repository import Gtk, Gdk clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) window = self.figure.canvas.get_window() @@ -45,9 +45,6 @@ def trigger(self, event): clipboard.set_image(pb) - - - fig = plt.figure() plt.plot([1, 2, 3]) @@ -56,8 +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 forward button +# Uncomment to 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 0a0d9fd05778..2cf0298a9941 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -46,7 +46,6 @@ import warnings import time import io -from pydispatch import dispatcher import numpy as np import matplotlib.cbook as cbook @@ -3208,10 +3207,34 @@ def set_history_buttons(self): pass +class ToolEvent(object): + """Base event for tool communication""" + def __init__(self, name, sender): + self.name = name + self.sender = sender + + +class ToolTriggerEvent(ToolEvent): + """Event to inform that a tool has been triggered""" + def __init__(self, name, toolname, sender, canvasevent=None, data=None): + ToolEvent.__init__(self, name, sender) + self.toolname = toolname + self.canvasevent = canvasevent + self.data = data + + +class NavigationEvent(ToolEvent): + """Event for navigation tool management (add/remove/message)""" + def __init__(self, name, sender, **kwargs): + ToolEvent.__init__(self, name, sender) + for key, value in kwargs.items(): + setattr(self, key, value) + + class NavigationBase(object): - """ Helper class that groups all the user interactions for a FigureManager + """Helper class that groups all the user interactions for a FigureManager - Attributes + Attributes ---------- manager : `FigureManager` instance keypresslock : `LockDraw` to know if the `canvas` key_press_event is @@ -3229,18 +3252,51 @@ def __init__(self, manager): self._tools = {} self._keys = {} self._toggled = None + self.callbacks = cbook.CallbackRegistry() # to process keypress event self.keypresslock = widgets.LockDraw() self.messagelock = widgets.LockDraw() - def send_message(self, message, sender=None): - """ Send a navigation-message event""" + def mpl_connect(self, s, func): + """Connect event with string *s* to *func*. + + Parameters: + ----------- + s: String + Name of the event + The following events are recognized + - 'tool_message_even' + - 'tool_removed_event' + - 'tool_added_event' + For every tool added a new event is created + - 'tool_trigger_TOOLNAME + Where TOOLNAME is the id of the tool. + func: function + Function to be called with signature + def func(event) + """ + return self.callbacks.connect(s, func) + + def mpl_disconnect(self, cid): + """Disconnect callback id cid + + Example usage:: + + cid = navigation.mpl_connect('tool-trigger-zoom', on_press) + #...later + navigation.mpl_disconnect(cid) + """ + return self.callbacks.disconnect(cid) + + def message_event(self, message, sender=None): + """ Send a tool_message_event event""" if sender is None: sender = self - dispatcher.send(signal='navigation-message', - sender=sender, - message=message) + + s = 'tool_message_event' + event = NavigationEvent(s, sender, message=message) + self.callbacks.process(s, event) @property def active_toggle(self): @@ -3311,9 +3367,9 @@ def remove_tool(self, name): self._remove_keys(name) - dispatcher.send(signal='navigation-tool-removed', - sender=self, - tool=tool) + s = 'tool_removed_event' + event = NavigationEvent(s, self, tool=tool) + self.callbacks.process(s, event) del self._tools[name] @@ -3334,6 +3390,11 @@ def add_tools(self, tools): def add_tool(self, name, tool, group=None, position=None): """Add tool to `Navigation` + Add a tool to the tools controlled by Navigation + If successful adds a new event `tool_trigger_name` where name is + the name of the tool, this event is fired everytime + the tool is triggered. + Parameters ---------- name : string @@ -3356,40 +3417,38 @@ def add_tool(self, name, tool, group=None, position=None): 'not added') return - self._tools[name] = tool_cls(self.canvas.figure, name) + self._tools[name] = tool_cls(self, name) if tool_cls.keymap is not None: self.set_tool_keymap(name, tool_cls.keymap) - dispatcher.send(signal='navigation-tool-added', - sender=self, - tool=self._tools[name], - group=group, - position=position) + self._tool_added_event(self._tools[name], group, position) - if isinstance(self._tools[name], tools.ToolToggleBase): - dispatcher.connect(self._handle_toggle, - 'tool-pre-trigger-%s' % name, - sender=dispatcher.Any) + def _tool_added_event(self, tool, group, position): + s = 'tool_added_event' + event = NavigationEvent(s, self, + tool=tool, + group=group, + position=position) + self.callbacks.process(s, event) - def _handle_toggle(self, signal, sender, event=None): + def _handle_toggle(self, name, sender, canvasevent, data): # 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:]) + # This is called from tool_trigger_event + 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) + # Untoggle previously toggled tool + self.tool_trigger_event(self._toggled, self, canvasevent, data) toggled = name self._toggled = toggled for a in self.canvas.figure.get_axes(): a.set_navigate_mode(self._toggled) - self._set_cursor(event) + self._set_cursor(canvasevent) def _get_cls_to_instantiate(self, callback_class): # Find the class that corresponds to the tool @@ -3406,14 +3465,47 @@ def _get_cls_to_instantiate(self, callback_class): return callback_class - def trigger_tool(self, name, event=None): + def tool_trigger_event(self, name, sender=None, canvasevent=None, + data=None): + """Trigger a tool and fire the tool-trigger-[name] event + + Parameters + ---------- + name : string + Name of the tool + sender: object + Object that wish to trigger the tool + canvasevent: Event + Original Canvas event or None + data: Object + Extra data to pass to the tool when triggering + """ + if name not in self._tools: + warnings.warn("%s is not a tool controlled by Navigation" % name) + return + + if sender is None: + sender = self + + self._trigger_tool(name, sender, canvasevent, data) + + s = 'tool-trigger-%s' % name + event = ToolTriggerEvent(s, name, sender, canvasevent, data) + self.callbacks.process(s, event) + + def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): """Trigger on a tool - Method to programatically "click" on Tools + Method to actually trigger the tool """ - dispatcher.send(signal='tool-trigger-%s' % name, - sender=self, - event=event) + tool = self._tools[name] + + if isinstance(tool, tools.ToolToggleBase): + self._handle_toggle(name, sender, canvasevent, data) + + # Important!!! + # This is where the Tool object is triggered + tool.trigger(sender, canvasevent, data) def _key_press(self, event): if event.key is None or self.keypresslock.locked(): @@ -3422,7 +3514,7 @@ def _key_press(self, event): name = self._keys.get(event.key, None) if name is None: return - self.trigger_tool(name, event) + self.tool_trigger_event(name, canvasevent=event) def get_tools(self): """Return the tools controlled by `Navigation`""" @@ -3436,63 +3528,15 @@ def get_tools(self): 'keymap': keys} return d - def _set_cursor(self, event): - """Fire the tool-trigger-cursor event, + def _set_cursor(self, canvasevent): + """Sets the current cursor in ToolSetCursor""" - 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: 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` - enums values - """ - - 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) + self.tool_trigger_event('cursor', self, canvasevent, data=cursor) class ToolbarBase(object): @@ -3505,50 +3549,44 @@ class ToolbarBase(object): def __init__(self, manager): self.manager = manager + self.navigation = manager.navigation - 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) + self.navigation.mpl_connect('tool_message_event', self._message_cbk) + self.navigation.mpl_connect('tool_added_event', self._add_tool_cbk) + self.navigation.mpl_connect('tool_removed_event', + self._remove_tool_cbk) - def _message_cbk(self, signal, sender, message): - """Captures the 'navigation-message to set message on the toolbar""" - self.set_message(message) + def _message_cbk(self, event): + """Captures the 'tool_message_event' to set message on the toolbar""" + self.set_message(event.message) - def _tool_triggered_cbk(self, signal, sender): + def _tool_triggered_cbk(self, event): """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: + if event.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) + self.toggle_toolitem(event.toolname) + + def _add_tool_cbk(self, event): + """Captures 'tool_added_event' and add the tool to the toolbar""" + image = self._get_image_filename(event.tool.image) + toggle = isinstance(event.tool, tools.ToolToggleBase) + self.add_toolitem(event.tool.name, + event.group, + event.position, + image, + event.tool.description, + toggle) if toggle: - dispatcher.connect(self._tool_triggered_cbk, - signal='tool-trigger-%s' % name, - sender=dispatcher.Any) + self.navigation.mpl_connect('tool-trigger-%s' % event.tool.name, + self._tool_triggered_cbk) - def _remove_tool_cbk(self, tool, signal, sender): - """Captures the 'navigation-tool-removed' signal and remove the tool""" - self.remove_toolitem(tool.name) + def _remove_tool_cbk(self, event): + """Captures the 'tool_removed_event' signal and remove the tool""" + self.remove_toolitem(event.tool.name) def _get_image_filename(self, image): """"Base on the image name find the corresponding image""" @@ -3561,11 +3599,8 @@ def _get_image_filename(self, image): fname = None return fname -# def _message_event_callback(self, event): -# self.set_message(event.data) - def trigger_tool(self, name): - """fire the 'tool-trigger-toolname' signal + """Trigger the tool Parameters ---------- @@ -3573,8 +3608,7 @@ def trigger_tool(self, name): Name(id) of the tool that was triggered in the toolbar """ - dispatcher.send(signal='tool-trigger-%s' % name, - sender=self) + self.navigation.tool_trigger_event(name, sender=self) def add_toolitem(self, name, group, position, image, description, toggle): """Add a toolitem to the toolbar @@ -3609,6 +3643,10 @@ def set_message(self, s): pass + def toggle_toolitem(self, name): + """Toggle the toolitem without firing event""" + raise NotImplementedError + def remove_toolitem(self, name): """Remove a toolitem from the `Toolbar` diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 3bce393cecd0..59fe433b9a7a 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -16,7 +16,6 @@ import matplotlib.cbook as cbook from weakref import WeakKeyDictionary import numpy as np -from pydispatch import dispatcher class Cursors: @@ -60,37 +59,29 @@ class ToolBase(object): cursor = None """Cursor to use when the tool is active""" - def __init__(self, figure, name, event=None): + def __init__(self, navigation, name, event=None): self._name = name 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): + self.navigation = navigation + self.set_figure(navigation.canvas.figure) + + def trigger(self, sender, event, data=None): """Called when this tool gets used Parameters ---------- event : `Event` - The event that caused this tool to be called + The Canvas event that caused this tool to be called + sender: object + Object that requested the tool to be triggered + data: object + Extra data """ pass def set_figure(self, figure): - """Set the figure and navigation + """Set the figure Set the figure to be affected by this tool @@ -100,7 +91,6 @@ def set_figure(self, figure): """ self.figure = figure - self.navigation = figure.canvas.manager.navigation @property def name(self): @@ -116,9 +106,11 @@ class ToolToggleBase(ToolBase): Every time it is triggered, it switches between enable and disable """ - _toggled = False + def __init__(self, *args, **kwargs): + ToolBase.__init__(self, *args, **kwargs) + self._toggled = False - def trigger(self, event): + def trigger(self, sender, event, data=None): if self._toggled: self.disable(event) else: @@ -150,36 +142,45 @@ def toggled(self): return self._toggled -class ToolSetCursor(ToolBase): +class SetCursorBase(ToolBase): + """Change to the current cursor while inaxes""" def __init__(self, *args, **kwargs): ToolBase.__init__(self, *args, **kwargs) self._idDrag = self.figure.canvas.mpl_connect( - 'motion_notify_event', self.set_cursor) + 'motion_notify_event', self._set_cursor_cbk) self._cursor = None self._default_cursor = cursors.POINTER self._last_cursor = self._default_cursor - def set_cursor(self, event): + def _set_cursor_cbk(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.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.set_cursor(cursor) self._last_cursor = cursor - def trigger(self, event): - self._cursor = event.cursor - self.set_cursor(event) + def trigger(self, sender, event, data): + self._cursor = data + self._set_cursor_cbk(event) + + def set_cursor(self, cursor): + """Set the cursor + + This method has to be implemented per backend + """ + pass class ToolCursorPosition(ToolBase): + """Send message with the current pointer position""" def __init__(self, *args, **kwargs): ToolBase.__init__(self, *args, **kwargs) self._idDrag = self.figure.canvas.mpl_connect( @@ -199,7 +200,32 @@ def send_message(self, event): pass else: message = s - self.navigation.send_message(message, self) + self.navigation.message_event(message, self) + + +class RubberbandBase(ToolBase): + """Draw and remove rubberband""" + def trigger(self, sender, event, data): + if not self.figure.canvas.widgetlock.available(sender): + return + if data is not None: + self.draw_rubberband(*data) + else: + self.remove_rubberband() + + def draw_rubberband(self, *data): + """Draw rubberband + + This method has to be implemented per backend + """ + pass + + def remove_rubberband(self): + """Remove rubberband + + This method has to be implemented per backend + """ + pass class ToolQuit(ToolBase): @@ -208,7 +234,7 @@ class ToolQuit(ToolBase): description = 'Quit the figure' keymap = rcParams['keymap.quit'] - def trigger(self, event): + def trigger(self, sender, event, data=None): Gcf.destroy_fig(self.figure) @@ -218,7 +244,7 @@ class ToolEnableAllNavigation(ToolBase): description = 'Enables all axes navigation' keymap = rcParams['keymap.all_axes'] - def trigger(self, event): + def trigger(self, sender, event, data=None): if event.inaxes is None: return @@ -234,7 +260,7 @@ class ToolEnableNavigation(ToolBase): description = 'Enables one axes navigation' keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9) - def trigger(self, event): + def trigger(self, sender, event, data=None): if event.inaxes is None: return @@ -253,7 +279,7 @@ class ToolGrid(ToolBase): description = 'Toogle Grid' keymap = rcParams['keymap.grid'] - def trigger(self, event): + def trigger(self, sender, event, data=None): if event.inaxes is None: return event.inaxes.grid() @@ -266,7 +292,7 @@ class ToolFullScreen(ToolBase): description = 'Toogle Fullscreen mode' keymap = rcParams['keymap.fullscreen'] - def trigger(self, event): + def trigger(self, sender, event): self.figure.canvas.manager.full_screen_toggle() @@ -276,7 +302,7 @@ class ToolYScale(ToolBase): description = 'Toogle Scale Y axis' keymap = rcParams['keymap.yscale'] - def trigger(self, event): + def trigger(self, sender, event, data=None): ax = event.inaxes if ax is None: return @@ -296,7 +322,7 @@ class ToolXScale(ToolBase): description = 'Toogle Scale X axis' keymap = rcParams['keymap.xscale'] - def trigger(self, event): + def trigger(self, sender, event, data=None): ax = event.inaxes if ax is None: return @@ -416,15 +442,15 @@ def forward(cls, figure): class ViewsPositionsBase(ToolBase): - # Simple base to avoid repeating code on Home, Back and Forward - # Not of much use for other tools, so not documented + """Base class for ToolHome, ToolBack and ToolForward""" + _on_trigger = None def __init__(self, *args, **kwargs): ToolBase.__init__(self, *args, **kwargs) self.viewspos = ViewsPositions() - def trigger(self, *args): + def trigger(self, sender, event, data=None): self.viewspos.add_figure(self.figure) getattr(self.viewspos, self._on_trigger)(self.figure) self.viewspos.update_view(self.figure) @@ -496,9 +522,9 @@ def disable(self, event): self.figure.canvas.mpl_disconnect(self._idPress) self.figure.canvas.mpl_disconnect(self._idRelease) - def trigger(self, *args): + def trigger(self, sender, event, data=None): self.viewspos.add_figure(self.figure) - ToolToggleBase.trigger(self, *args) + ToolToggleBase.trigger(self, sender, event, data) class ToolZoom(ZoomPanBase): @@ -516,7 +542,7 @@ def __init__(self, *args): 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.tool_trigger_event('rubberband', self) self.viewspos.refresh_locators(self.figure) self._xypress = None self._button_pressed = None @@ -585,7 +611,11 @@ def _mouse_move(self, event): x1, y1, x2, y2 = a.bbox.extents x, lastx = x1, x2 - self.navigation.draw_rubberband(event, self, x, y, lastx, lasty) +# self.navigation.draw_rubberband(event, self, x, y, lastx, lasty) +# data = {'x': x, 'y': y, 'lastx': lastx, 'lasty': lasty} + self.navigation.tool_trigger_event('rubberband', + self, + data=(x, y, lastx, lasty)) def _release(self, event): """the release mouse button callback in zoom to rect mode""" @@ -800,9 +830,9 @@ def _mouse_move(self, event): ['zoompan', [(ToolZoom, 'zoom'), (ToolPan, 'pan')]], - ['layout', [('ConfigureSubplots', 'subplots'), ]], + ['layout', [('ToolConfigureSubplots', 'subplots'), ]], - ['io', [('SaveFigure', 'save'), ]], + ['io', [('ToolSaveFigure', 'save'), ]], [None, [(ToolGrid, 'grid'), (ToolFullScreen, 'fullscreen'), @@ -812,7 +842,8 @@ def _mouse_move(self, event): (ToolXScale, 'xscale'), (ToolYScale, 'yscale'), (ToolCursorPosition, 'position'), - (ToolSetCursor, 'cursor')]]] + ('ToolSetCursor', 'cursor'), + ('ToolRubberband', 'rubberband')]]] """Default tools""" diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 15a0f1c68305..5069ce33d4ea 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, tools +from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, \ + tools, SetCursorBase, RubberbandBase from matplotlib.cbook import is_string_like, is_writable_file_like from matplotlib.colors import colorConverter @@ -413,8 +414,8 @@ def __init__(self, canvas, num): self.vbox.pack_start(self.canvas, True, True, 0) - self.toolbar = self._get_toolbar() self.navigation = self._get_navigation() + self.toolbar = self._get_toolbar() if matplotlib.rcParams['toolbar'] == 'navigation': self.navigation.add_tools(tools) @@ -441,7 +442,8 @@ def notify_axes_change(fig): 'this will be called whenever the current axes is changed' if self.navigation is not None: pass - 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) self.canvas.grab_focus() @@ -717,26 +719,24 @@ def get_filename_from_user (self): class NavigationGTK3(NavigationBase): - def __init__(self, *args, **kwargs): - NavigationBase.__init__(self, *args, **kwargs) - self.ctx = None + pass - def set_cursor(self, cursor): - self.canvas.get_property("window").set_cursor(cursord[cursor]) - def draw_rubberband(self, event, caller, x0, y0, x1, y1): - if not self.canvas.widgetlock.available(caller): - return +class RubberbandGTK3(RubberbandBase): + def __init__(self, *args, **kwargs): + RubberbandBase.__init__(self, *args, **kwargs) + self.ctx = None + def draw_rubberband(self, x0, y0, x1, y1): # 'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/ # Recipe/189744' - self.ctx = self.canvas.get_property("window").cairo_create() + self.ctx = self.figure.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() + self.figure.canvas.draw() - height = self.canvas.figure.bbox.height + height = self.figure.bbox.height y1 = height - y1 y0 = height - y0 w = abs(x1 - x0) @@ -749,6 +749,8 @@ def draw_rubberband(self, event, caller, x0, y0, x1, y1): self.ctx.set_source_rgb(0, 0, 0) self.ctx.stroke() +ToolRubberband = RubberbandGTK3 + class ToolbarGTK3(ToolbarBase, Gtk.Box): def __init__(self, manager): @@ -846,7 +848,7 @@ def get_filechooser(self): fc.set_current_name(self.figure.canvas.get_default_filename()) return fc - def trigger(self, *args): + def trigger(self, *args, **kwargs): chooser = self.get_filechooser() fname, format_ = chooser.get_filename_from_user() chooser.destroy() @@ -865,7 +867,14 @@ def trigger(self, *args): except Exception as e: error_msg_gtk(str(e), parent=self) -SaveFigure = SaveFigureGTK3 +ToolSaveFigure = SaveFigureGTK3 + + +class SetCursorGTK3(SetCursorBase): + def set_cursor(self, cursor): + self.figure.canvas.get_property("window").set_cursor(cursord[cursor]) + +ToolSetCursor = SetCursorGTK3 class ConfigureSubplotsGTK3(ConfigureSubplotsBase, Gtk.Window): @@ -916,12 +925,12 @@ def destroy(self, *args): def _get_canvas(self, fig): return self.canvas.__class__(fig) - def trigger(self, event): + def trigger(self, sender, event, data=None): self.init_window() self.window.present() -ConfigureSubplots = ConfigureSubplotsGTK3 +ToolConfigureSubplots = ConfigureSubplotsGTK3 class DialogLineprops: From 86e28302bfd9fb445e3529cdd8af9c4d6f02ba15 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 20 Oct 2014 17:39:59 -0400 Subject: [PATCH 42/64] removing view positions singleton --- lib/matplotlib/backend_bases.py | 10 +++ lib/matplotlib/backend_tools.py | 123 ++++++++++++++++---------------- 2 files changed, 70 insertions(+), 63 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 2cf0298a9941..714f20719c58 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3528,6 +3528,16 @@ def get_tools(self): 'keymap': keys} return d + def get_tool(self, name): + """Return the tool object + + Parameters: + ----------- + name: String + Name of the tool + """ + return self._tools[name] + def _set_cursor(self, canvasevent): """Sets the current cursor in ToolSetCursor""" diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 59fe433b9a7a..30162ad1d072 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -336,47 +336,56 @@ def trigger(self, sender, event, data=None): ax.figure.canvas.draw() -class ViewsPositions(object): - """Auxiliary class to handle changes in views and positions""" +class ToolViewsPositions(ToolBase): + """Auxiliary Tool to handle changes in views and positions + + This tool is accessed by navigation.manipulate_tool + This tool is used by all the tools that need to access the record of + views and positions of the figure + - Zoom + - Pan + - Home + - Back + - Forward + """ - views = WeakKeyDictionary() - """Record of views with Figure objects as keys""" + def __init__(self, *args, **kwargs): + self.views = WeakKeyDictionary() + self.positions = WeakKeyDictionary() + ToolBase.__init__(self, *args, **kwargs) - positions = WeakKeyDictionary() - """Record of positions with Figure objects as keys""" + def set_figure(self, figure): + ToolBase.set_figure(self, figure) - @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() + def add_figure(self): + """Add the current figure to the stack of views and positions""" + if self.figure not in self.views: + self.views[self.figure] = cbook.Stack() + self.positions[self.figure] = cbook.Stack() # Define Home - cls.push_current(figure) + self.push_current() # Adding the clear method as axobserver, removes this burden from # the backend - figure.add_axobserver(cls.clear) + self.figure.add_axobserver(self.clear) - @classmethod - def clear(cls, figure): + def clear(self, figure): """Reset the axes stack""" - if figure in cls.views: - cls.views[figure].clear() - cls.positions[figure].clear() + if figure in self.views: + self.views[figure].clear() + self.positions[figure].clear() - @classmethod - def update_view(cls, figure): + def update_view(self): """Update the viewlim and position from the view and position stack for each axes """ - lims = cls.views[figure]() + lims = self.views[self.figure]() if lims is None: return - pos = cls.positions[figure]() + pos = self.positions[self.figure]() if pos is None: return - for i, a in enumerate(figure.get_axes()): + 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)) @@ -384,15 +393,14 @@ def update_view(cls, figure): a.set_position(pos[i][0], 'original') a.set_position(pos[i][1], 'active') - figure.canvas.draw_idle() + self.figure.canvas.draw_idle() - @classmethod - def push_current(cls, figure): + def push_current(self): """push the current view limits and position onto the stack""" lims = [] pos = [] - for a in figure.get_axes(): + for a in self.figure.get_axes(): xmin, xmax = a.get_xlim() ymin, ymax = a.get_ylim() lims.append((xmin, xmax, ymin, ymax)) @@ -400,13 +408,12 @@ def push_current(cls, figure): pos.append(( a.get_position(True).frozen(), a.get_position().frozen())) - cls.views[figure].push(lims) - cls.positions[figure].push(pos) + self.views[self.figure].push(lims) + self.positions[self.figure].push(pos) - @classmethod - def refresh_locators(cls, figure): + def refresh_locators(self): """Redraw the canvases, update the locators""" - for a in figure.get_axes(): + for a in self.figure.get_axes(): xaxis = getattr(a, 'xaxis', None) yaxis = getattr(a, 'yaxis', None) zaxis = getattr(a, 'zaxis', None) @@ -423,22 +430,19 @@ def refresh_locators(cls, figure): for loc in locators: loc.refresh() - figure.canvas.draw_idle() + self.figure.canvas.draw_idle() - @classmethod - def home(cls, figure): - cls.views[figure].home() - cls.positions[figure].home() + def home(self): + self.views[self.figure].home() + self.positions[self.figure].home() - @classmethod - def back(cls, figure): - cls.views[figure].back() - cls.positions[figure].back() + def back(self): + self.views[self.figure].back() + self.positions[self.figure].back() - @classmethod - def forward(cls, figure): - cls.views[figure].forward() - cls.positions[figure].forward() + def forward(self): + self.views[self.figure].forward() + self.positions[self.figure].forward() class ViewsPositionsBase(ToolBase): @@ -446,14 +450,10 @@ class ViewsPositionsBase(ToolBase): _on_trigger = None - def __init__(self, *args, **kwargs): - ToolBase.__init__(self, *args, **kwargs) - self.viewspos = ViewsPositions() - def trigger(self, sender, event, data=None): - self.viewspos.add_figure(self.figure) - getattr(self.viewspos, self._on_trigger)(self.figure) - self.viewspos.update_view(self.figure) + self.navigation.get_tool('viewpos').add_figure() + getattr(self.navigation.get_tool('viewpos'), self._on_trigger)() + self.navigation.get_tool('viewpos').update_view() class ToolHome(ViewsPositionsBase): @@ -499,15 +499,13 @@ class SaveFigureBase(ToolBase): class ZoomPanBase(ToolToggleBase): - # Base class to group common functionality between zoom and pan - # Not of much use for other tools, so not documented + """Base class for Zoom and Pan tools""" def __init__(self, *args): ToolToggleBase.__init__(self, *args) 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) @@ -523,7 +521,7 @@ def disable(self, event): self.figure.canvas.mpl_disconnect(self._idRelease) def trigger(self, sender, event, data=None): - self.viewspos.add_figure(self.figure) + self.navigation.get_tool('viewpos').add_figure() ToolToggleBase.trigger(self, sender, event, data) @@ -543,7 +541,7 @@ def _cancel_action(self): for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) self.navigation.tool_trigger_event('rubberband', self) - self.viewspos.refresh_locators(self.figure) + self.navigation.get_tool('viewpos').refresh_locators() self._xypress = None self._button_pressed = None self._ids_zoom = [] @@ -611,8 +609,6 @@ def _mouse_move(self, event): x1, y1, x2, y2 = a.bbox.extents x, lastx = x1, x2 -# self.navigation.draw_rubberband(event, self, x, y, lastx, lasty) -# data = {'x': x, 'y': y, 'lastx': lastx, 'lasty': lasty} self.navigation.tool_trigger_event('rubberband', self, data=(x, y, lastx, lasty)) @@ -736,7 +732,7 @@ def _release(self, event): a.set_ylim((ry1, ry2)) self._zoom_mode = None - self.viewspos.push_current(self.figure) + self.navigation.get_tool('viewpos').push_current() self._cancel_action() @@ -757,7 +753,7 @@ def _cancel_action(self): self._xypress = [] self.figure.canvas.mpl_disconnect(self._idDrag) self.navigation.messagelock.release(self) - self.viewspos.refresh_locators(self.figure) + self.navigation.get_tool('viewpos').refresh_locators() def _press(self, event): if event.button == 1: @@ -794,7 +790,7 @@ def _release(self, event): self._cancel_action() return - self.viewspos.push_current(self.figure) + self.navigation.get_tool('viewpos').push_current() self._cancel_action() def _mouse_move(self, event): @@ -842,6 +838,7 @@ def _mouse_move(self, event): (ToolXScale, 'xscale'), (ToolYScale, 'yscale'), (ToolCursorPosition, 'position'), + (ToolViewsPositions, 'viewpos'), ('ToolSetCursor', 'cursor'), ('ToolRubberband', 'rubberband')]]] From a084cb5cce753897e4366646757c2e705a2a8aa4 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 27 Oct 2014 15:25:56 -0400 Subject: [PATCH 43/64] moving set_cursor completely out of navigation --- examples/user_interfaces/navigation.py | 11 ++++---- lib/matplotlib/backend_bases.py | 32 +++++------------------ lib/matplotlib/backend_tools.py | 36 ++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index f4daa9cf3317..6fe91972efb1 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -13,17 +13,18 @@ class ListTools(ToolBase): description = 'List Tools' def trigger(self, *args, **kwargs): - tools = self.navigation.get_tools() - print ('_' * 80) print ("{0:12} {1:45} {2}".format('Name (id)', 'Tool description', 'Keymap')) - print ('_' * 80) + + tools = self.navigation.tools for name in sorted(tools.keys()): - keys = ', '.join(sorted(tools[name]['keymap'])) + if not tools[name].description: + continue + keys = ', '.join(sorted(self.navigation.get_tool_keymap(name))) print ("{0:12} {1:45} {2}".format(name, - tools[name]['description'], + tools[name].description, keys)) print ('_' * 80) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 714f20719c58..778e8bc8f11f 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3216,9 +3216,9 @@ def __init__(self, name, sender): class ToolTriggerEvent(ToolEvent): """Event to inform that a tool has been triggered""" - def __init__(self, name, toolname, sender, canvasevent=None, data=None): + def __init__(self, name, tool, sender, canvasevent=None, data=None): ToolEvent.__init__(self, name, sender) - self.toolname = toolname + self.tool = tool self.canvasevent = canvasevent self.data = data @@ -3448,8 +3448,6 @@ def _handle_toggle(self, name, sender, canvasevent, data): for a in self.canvas.figure.get_axes(): a.set_navigate_mode(self._toggled) - self._set_cursor(canvasevent) - def _get_cls_to_instantiate(self, callback_class): # Find the class that corresponds to the tool if isinstance(callback_class, six.string_types): @@ -3490,7 +3488,7 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, self._trigger_tool(name, sender, canvasevent, data) s = 'tool-trigger-%s' % name - event = ToolTriggerEvent(s, name, sender, canvasevent, data) + event = ToolTriggerEvent(s, self._tools[name], sender, canvasevent, data) self.callbacks.process(s, event) def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): @@ -3516,17 +3514,11 @@ def _key_press(self, event): return self.tool_trigger_event(name, canvasevent=event) - def get_tools(self): + @property + def tools(self): """Return the tools controlled by `Navigation`""" - 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] - d[name] = {'obj': tool, - 'description': tool.description, - 'keymap': keys} - return d + return self._tools def get_tool(self, name): """Return the tool object @@ -3538,16 +3530,6 @@ def get_tool(self, name): """ return self._tools[name] - def _set_cursor(self, canvasevent): - """Sets the current cursor in ToolSetCursor""" - - if self._toggled: - cursor = self._tools[self._toggled].cursor - else: - cursor = None - - self.tool_trigger_event('cursor', self, canvasevent, data=cursor) - class ToolbarBase(object): """Base class for `Toolbar` implementation @@ -3578,7 +3560,7 @@ def _tool_triggered_cbk(self, event): if event.sender is self: return - self.toggle_toolitem(event.toolname) + self.toggle_toolitem(event.tool.name) def _add_tool_cbk(self, event): """Captures 'tool_added_event' and add the tool to the toolbar""" diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 30162ad1d072..da1e49542366 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -143,7 +143,11 @@ def toggled(self): class SetCursorBase(ToolBase): - """Change to the current cursor while inaxes""" + """Change to the current cursor while inaxes + + This tool, keeps track of all "toggleable" tools, and calls + set_cursos when one of these tools is triggered + """ def __init__(self, *args, **kwargs): ToolBase.__init__(self, *args, **kwargs) self._idDrag = self.figure.canvas.mpl_connect( @@ -151,6 +155,32 @@ def __init__(self, *args, **kwargs): self._cursor = None self._default_cursor = cursors.POINTER self._last_cursor = self._default_cursor + self.navigation.mpl_connect('tool_added_event', self._add_tool_cbk) + + # process current tools + for tool in self.navigation.tools.values(): + self._add_tool(tool) + + def _tool_trigger_cbk(self, event): + if event.tool.toggled: + self._cursor = event.tool.cursor + else: + self._cursor = None + + self._set_cursor_cbk(event.canvasevent) + + # If the tool is toggleable, set the cursor when the tool is triggered + def _add_tool(self, tool): + if getattr(tool, 'toggled', None) is not None: + self.navigation.mpl_connect('tool-trigger-%s' % tool.name, + self._tool_trigger_cbk) + + # If tool is added, process it + def _add_tool_cbk(self, event): + if event.tool is self: + return + + self._add_tool(event.tool) def _set_cursor_cbk(self, event): if not event: @@ -167,10 +197,6 @@ def _set_cursor_cbk(self, event): self.set_cursor(cursor) self._last_cursor = cursor - def trigger(self, sender, event, data): - self._cursor = data - self._set_cursor_cbk(event) - def set_cursor(self, cursor): """Set the cursor From f21f1850a1319525907d3e6ffde9fcf23b258839 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 30 Oct 2014 12:03:59 -0400 Subject: [PATCH 44/64] cleanup doc --- examples/user_interfaces/navigation.py | 2 +- lib/matplotlib/backend_bases.py | 104 ++++++++++++++-------- lib/matplotlib/backend_tools.py | 118 ++++++++++++++----------- 3 files changed, 130 insertions(+), 94 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 6fe91972efb1..4a165b3becef 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -17,7 +17,7 @@ def trigger(self, *args, **kwargs): print ("{0:12} {1:45} {2}".format('Name (id)', 'Tool description', 'Keymap')) - + print ('-' * 80) tools = self.navigation.tools for name in sorted(tools.keys()): if not tools[name].description: diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 778e8bc8f11f..14f1bb77586e 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3236,10 +3236,10 @@ class NavigationBase(object): Attributes ---------- - manager : `FigureManager` instance - keypresslock : `LockDraw` to know if the `canvas` key_press_event is - locked - messagelock : `LockDraw` to know if the message is available to write + manager: `FigureManager` instance + keypresslock: `LockDraw` to know if the `canvas` key_press_event is + locked + messagelock: `LockDraw` to know if the message is available to write """ def __init__(self, manager): @@ -3252,7 +3252,7 @@ def __init__(self, manager): self._tools = {} self._keys = {} self._toggled = None - self.callbacks = cbook.CallbackRegistry() + self._callbacks = cbook.CallbackRegistry() # to process keypress event self.keypresslock = widgets.LockDraw() @@ -3261,22 +3261,24 @@ def __init__(self, manager): def mpl_connect(self, s, func): """Connect event with string *s* to *func*. - Parameters: + Parameters ----------- - s: String + s : String Name of the event + The following events are recognized - - 'tool_message_even' + + - 'tool_message_event' - 'tool_removed_event' - 'tool_added_event' For every tool added a new event is created - 'tool_trigger_TOOLNAME Where TOOLNAME is the id of the tool. - func: function + func : function Function to be called with signature def func(event) """ - return self.callbacks.connect(s, func) + return self._callbacks.connect(s, func) def mpl_disconnect(self, cid): """Disconnect callback id cid @@ -3287,16 +3289,16 @@ def mpl_disconnect(self, cid): #...later navigation.mpl_disconnect(cid) """ - return self.callbacks.disconnect(cid) + return self._callbacks.disconnect(cid) def message_event(self, message, sender=None): - """ Send a tool_message_event event""" + """ Emit a tool_message_event event""" if sender is None: sender = self s = 'tool_message_event' event = NavigationEvent(s, sender, message=message) - self.callbacks.process(s, event) + self._callbacks.process(s, event) @property def active_toggle(self): @@ -3351,7 +3353,7 @@ def set_tool_keymap(self, name, *keys): self._keys[k] = name def remove_tool(self, name): - """Remove tool from the `Navigation` + """Remove tool from `Navigation` Parameters ---------- @@ -3369,7 +3371,7 @@ def remove_tool(self, name): s = 'tool_removed_event' event = NavigationEvent(s, self, tool=tool) - self.callbacks.process(s, event) + self._callbacks.process(s, event) del self._tools[name] @@ -3378,9 +3380,12 @@ def add_tools(self, tools): 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 :func:`add_tool`. + tools : List + List in the form + [[group1, [(Tool1, name1), (Tool2, name2) ...]][group2...]] + where group1 is the name of the group where the + Tool1, Tool2... are going to be added, and name1, name2... are the + names of the tools """ for group, grouptools in tools: @@ -3388,23 +3393,24 @@ def add_tools(self, tools): self.add_tool(tool[1], tool[0], group, position) def add_tool(self, name, tool, group=None, position=None): - """Add tool to `Navigation` + """Add tool to `NavigationBase` Add a tool to the tools controlled by Navigation - If successful adds a new event `tool_trigger_name` where name is - the name of the tool, this event is fired everytime + + If successful adds a new event `tool_trigger_name` where **name** is + the **name** of the tool, this event is fired everytime the tool is triggered. Parameters ---------- name : string Name of the tool, treated as the ID, has to be unique - tool : string or `Tool` class + tool : string or `matplotlib.backend_tools.ToolBase` derived 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 + Position within its group in the toolbar, if None, is positioned at the end """ tool_cls = self._get_cls_to_instantiate(tool) @@ -3429,7 +3435,7 @@ def _tool_added_event(self, tool, group, position): tool=tool, group=group, position=position) - self.callbacks.process(s, event) + self._callbacks.process(s, event) def _handle_toggle(self, name, sender, canvasevent, data): # Toggle tools, need to be untoggled before other Toggle tool is used @@ -3465,7 +3471,7 @@ def _get_cls_to_instantiate(self, callback_class): def tool_trigger_event(self, name, sender=None, canvasevent=None, data=None): - """Trigger a tool and fire the tool-trigger-[name] event + """Trigger a tool and emit the tool-trigger-[name] event Parameters ---------- @@ -3473,9 +3479,9 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, Name of the tool sender: object Object that wish to trigger the tool - canvasevent: Event + canvasevent : Event Original Canvas event or None - data: Object + data : Object Extra data to pass to the tool when triggering """ if name not in self._tools: @@ -3489,7 +3495,7 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, s = 'tool-trigger-%s' % name event = ToolTriggerEvent(s, self._tools[name], sender, canvasevent, data) - self.callbacks.process(s, event) + self._callbacks.process(s, event) def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): """Trigger on a tool @@ -3523,9 +3529,9 @@ def tools(self): def get_tool(self, name): """Return the tool object - Parameters: + Parameters ----------- - name: String + name : String Name of the tool """ return self._tools[name] @@ -3536,7 +3542,9 @@ class ToolbarBase(object): Attributes ---------- - manager : `FigureManager` instance that integrates this `Toolbar` + manager : `FigureManager` object that integrates this `Toolbar` + navigation : `NavigationBase` object that hold the tools that + this `Toolbar` wants to communicate with """ def __init__(self, manager): @@ -3565,7 +3573,7 @@ def _tool_triggered_cbk(self, event): def _add_tool_cbk(self, event): """Captures 'tool_added_event' and add the tool to the toolbar""" image = self._get_image_filename(event.tool.image) - toggle = isinstance(event.tool, tools.ToolToggleBase) + toggle = getattr(event.tool, 'toggled', None) is not None self.add_toolitem(event.tool.name, event.group, event.position, @@ -3605,6 +3613,8 @@ def trigger_tool(self, name): def add_toolitem(self, name, group, position, image, description, toggle): """Add a toolitem to the toolbar + This method has to be implemented per backend + The callback associated with the button click event, must be **EXACTLY** `self.trigger_tool(name)` @@ -3617,31 +3627,47 @@ def add_toolitem(self, name, group, position, image, description, toggle): 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 + image_file : String Filename of the image for the button or `None` - description : string + description : String Description of the tool, used for the tooltips - toggle : bool + toggle : Bool * `True` : The button is a toggle (change the pressed/unpressed - state between consecutive clicks) + state between consecutive clicks) * `False` : The button is a normal button (returns to unpressed - state after release) + state after release) """ raise NotImplementedError def set_message(self, s): - """Display a message on toolbar or in status bar""" + """Display a message on toolbar or in status bar + + Parameters + ---------- + s : String + Message text + """ pass def toggle_toolitem(self, name): - """Toggle the toolitem without firing event""" + """Toggle the toolitem without firing event + + Parameters + ---------- + name : String + Id of the tool to toggle + """ raise NotImplementedError def remove_toolitem(self, name): """Remove a toolitem from the `Toolbar` + This method has to be implemented per backend + + Called when `tool_removed_event` is emited by `NavigationBase` + Parameters ---------- name : string diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index da1e49542366..a094a25bd9d8 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -3,7 +3,7 @@ These tools are used by `NavigationBase` :class:`ToolBase` - Simple tool that gets instantiated every time it is used + Simple stateless tool :class:`ToolToggleBase` Tool that has two states, only one Toggle tool can be @@ -19,7 +19,7 @@ class Cursors: - # this class is only used as a simple namespace + """Simple namespace for cursor reference""" HAND, POINTER, SELECT_REGION, MOVE = list(range(4)) cursors = Cursors() @@ -27,39 +27,42 @@ class Cursors: class ToolBase(object): """Base tool class + A base tool, only implements `trigger` method or not method at all. + The tool is instantiated by `matplotlib.backend_bases.NavigationBase` + Attributes ---------- - navigation : `NavigationBase` + navigation: `matplotlib.backend_bases.NavigationBase` Navigation that controls this Tool - figure : `FigureCanvas` + figure: `FigureCanvas` Figure instance that is affected by this Tool + name: String + Used as **Id** of the tool, has to be unique among tools of the same + Navigation """ keymap = None """Keymap to associate with this tool - **string**: List of comma separated keys that will be used to call this + **String**: List of comma separated keys that will be used to call this tool when the keypress event of *self.figure.canvas* is emited """ description = None """Description of the Tool - **string**: If the Tool is included in the Toolbar this text is used + **String**: If the Tool is included in the Toolbar this text is used as a Tooltip """ image = None """Filename of the image - **string**: Filename of the image to use in the toolbar. If None, the + **String**: Filename of the image to use in the toolbar. If None, the `name` is used as a label in the toolbar button """ - cursor = None - """Cursor to use when the tool is active""" - - def __init__(self, navigation, name, event=None): + def __init__(self, navigation, name): self._name = name self.figure = None self.navigation = navigation @@ -68,6 +71,9 @@ def __init__(self, navigation, name, event=None): def trigger(self, sender, event, data=None): """Called when this tool gets used + This method is called by + `matplotlib.backend_bases.NavigationBase.tool_trigger_event` + Parameters ---------- event : `Event` @@ -94,9 +100,15 @@ def set_figure(self, figure): @property def name(self): + """Tool Id""" return self._name def destroy(self): + """Destroy the tool + + This method is called when the tool is removed by + `matplotlib.backend_bases.NavigationBase.remove_tool` + """ pass @@ -106,11 +118,15 @@ class ToolToggleBase(ToolBase): Every time it is triggered, it switches between enable and disable """ + cursor = None + """Cursor to use when the tool is active""" + def __init__(self, *args, **kwargs): ToolBase.__init__(self, *args, **kwargs) self._toggled = False def trigger(self, sender, event, data=None): + """Calls `enable` or `disable` based on `toggled` value""" if self._toggled: self.disable(event) else: @@ -120,7 +136,7 @@ def trigger(self, sender, event, data=None): def enable(self, event=None): """Enable the toggle tool - This method is called when the tool is triggered and not toggled + This method is called dby `trigger` when the `toggled` is False """ pass @@ -128,9 +144,14 @@ def enable(self, event=None): def disable(self, event=None): """Disable the toggle tool - 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`) + This method is called by `trigger` when the `toggled` is True. + + This can happen in different circumstances + + * Click on the toolbar tool button + * Call to `matplotlib.backend_bases.NavigationBase.tool_trigger_event` + * Another `ToolToggleBase` derived tool is triggered + (from the same `Navigation`) """ pass @@ -145,8 +166,8 @@ def toggled(self): class SetCursorBase(ToolBase): """Change to the current cursor while inaxes - This tool, keeps track of all "toggleable" tools, and calls - set_cursos when one of these tools is triggered + This tool, keeps track of all `ToolToggleBase` derived tools, and calls + set_cursor when one of these tools is triggered """ def __init__(self, *args, **kwargs): ToolBase.__init__(self, *args, **kwargs) @@ -206,13 +227,17 @@ def set_cursor(self, cursor): class ToolCursorPosition(ToolBase): - """Send message with the current pointer position""" + """Send message with the current pointer position + + This tool runs in the background reporting the position of the cursor + """ 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): + """Call `matplotlib.backend_bases.NavigationBase.message_event""" if self.navigation.messagelock.locked(): return @@ -232,6 +257,7 @@ def send_message(self, event): class RubberbandBase(ToolBase): """Draw and remove rubberband""" def trigger(self, sender, event, data): + """Call `draw_rubberband` or `remove_rubberband` based on data""" if not self.figure.canvas.widgetlock.available(sender): return if data is not None: @@ -329,6 +355,7 @@ class ToolYScale(ToolBase): keymap = rcParams['keymap.yscale'] def trigger(self, sender, event, data=None): + """Toggle axis scale""" ax = event.inaxes if ax is None: return @@ -349,6 +376,7 @@ class ToolXScale(ToolBase): keymap = rcParams['keymap.xscale'] def trigger(self, sender, event, data=None): + """Toggle axis scale""" ax = event.inaxes if ax is None: return @@ -365,14 +393,14 @@ def trigger(self, sender, event, data=None): class ToolViewsPositions(ToolBase): """Auxiliary Tool to handle changes in views and positions - This tool is accessed by navigation.manipulate_tool - This tool is used by all the tools that need to access the record of - views and positions of the figure - - Zoom - - Pan - - Home - - Back - - Forward + Runs in the background and is used by all the tools that + need to access the record of views and positions of the figure + + * `ToolZoom` + * `ToolPan` + * `ToolHome` + * `ToolBack` + * `ToolForward` """ def __init__(self, *args, **kwargs): @@ -380,9 +408,6 @@ def __init__(self, *args, **kwargs): self.positions = WeakKeyDictionary() ToolBase.__init__(self, *args, **kwargs) - def set_figure(self, figure): - ToolBase.set_figure(self, figure) - def add_figure(self): """Add the current figure to the stack of views and positions""" if self.figure not in self.views: @@ -459,20 +484,23 @@ def refresh_locators(self): self.figure.canvas.draw_idle() def home(self): + """Recall the first view and position from the stack""" self.views[self.figure].home() self.positions[self.figure].home() def back(self): + """Back one step in the stack of views and positions""" self.views[self.figure].back() self.positions[self.figure].back() def forward(self): + """Forward one step in the stack of views and positions""" self.views[self.figure].forward() self.positions[self.figure].forward() class ViewsPositionsBase(ToolBase): - """Base class for ToolHome, ToolBack and ToolForward""" + """Base class for `ToolHome`, `ToolBack` and `ToolForward`""" _on_trigger = None @@ -483,7 +511,7 @@ def trigger(self, sender, event, data=None): class ToolHome(ViewsPositionsBase): - """Restore the original view""" + """Restore the original view lim""" description = 'Reset original view' image = 'home.png' @@ -492,7 +520,7 @@ class ToolHome(ViewsPositionsBase): class ToolBack(ViewsPositionsBase): - """move back up the view lim stack""" + """Move back up the view lim stack""" description = 'Back to previous view' image = 'back.png' @@ -525,7 +553,7 @@ class SaveFigureBase(ToolBase): class ZoomPanBase(ToolToggleBase): - """Base class for Zoom and Pan tools""" + """Base class for `ToolZoom` and `ToolPan`""" def __init__(self, *args): ToolToggleBase.__init__(self, *args) self._button_pressed = None @@ -534,6 +562,7 @@ def __init__(self, *args): self._idRelease = None def enable(self, event): + """Connect press/release events and lock the canvas""" self.figure.canvas.widgetlock(self) self._idPress = self.figure.canvas.mpl_connect( 'button_press_event', self._press) @@ -541,6 +570,7 @@ def enable(self, event): 'button_release_event', self._release) def disable(self, event): + """Release the canvas and disconnect press/release events""" self._cancel_action() self.figure.canvas.widgetlock.release(self) self.figure.canvas.mpl_disconnect(self._idPress) @@ -827,24 +857,6 @@ def _mouse_move(self, event): self.navigation.canvas.draw_idle() -# 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')]], @@ -867,6 +879,4 @@ def _mouse_move(self, event): (ToolViewsPositions, 'viewpos'), ('ToolSetCursor', 'cursor'), ('ToolRubberband', 'rubberband')]]] - - -"""Default tools""" +"""Default tools""" \ No newline at end of file From d66d8ca7a482f4a817f6f0cbd103e0cc3a55275b Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 30 Oct 2014 12:19:06 -0400 Subject: [PATCH 45/64] pep8 error --- lib/matplotlib/backend_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index a094a25bd9d8..ccc67ae689f1 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -879,4 +879,4 @@ def _mouse_move(self, event): (ToolViewsPositions, 'viewpos'), ('ToolSetCursor', 'cursor'), ('ToolRubberband', 'rubberband')]]] -"""Default tools""" \ No newline at end of file +"""Default tools""" From 18b4fec2ab60d075690f65cf56c693167b7fd2c0 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 31 Oct 2014 10:18:18 -0400 Subject: [PATCH 46/64] unused import --- examples/user_interfaces/navigation.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 4a165b3becef..bec3e57e2efa 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -4,14 +4,14 @@ 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): # keyboard shortcut keymap = 'm' description = 'List Tools' - + def trigger(self, *args, **kwargs): print ('_' * 80) print ("{0:12} {1:45} {2}".format('Name (id)', @@ -26,9 +26,9 @@ def trigger(self, *args, **kwargs): print ("{0:12} {1:45} {2}".format(name, tools[name].description, keys)) - print ('_' * 80) - - + print ('_' * 80) + + # A simple example of copy canvas # ref: at https://github.com/matplotlib/matplotlib/issues/1987 class CopyToolGTK3(ToolBase): @@ -36,7 +36,7 @@ class CopyToolGTK3(ToolBase): description = 'Copy canvas' # It is not added to the toolbar as a button intoolbar = False - + def trigger(self, *args, **kwargs): from gi.repository import Gtk, Gdk clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) @@ -53,7 +53,7 @@ def trigger(self, *args, **kwargs): fig.canvas.manager.navigation.add_tool('List', ListTools) if matplotlib.rcParams['backend'] == 'GTK3Cairo': fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) - + # Uncomment to remove the forward button # fig.canvas.manager.navigation.remove_tool('forward') From 316d2660a869769c9869e44357dc3a2e32cf0d35 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 10 Nov 2014 10:37:02 -0500 Subject: [PATCH 47/64] removing unused event class --- lib/matplotlib/backend_bases.py | 64 +++++++++++++++++++-------------- lib/matplotlib/backend_tools.py | 7 ++-- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 14f1bb77586e..eba2a5bb3f0f 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3208,27 +3208,38 @@ def set_history_buttons(self): class ToolEvent(object): - """Base event for tool communication""" - def __init__(self, name, sender): + """Event for tool manipulation (add/remove)""" + def __init__(self, name, sender, tool): self.name = name self.sender = sender + self.tool = tool class ToolTriggerEvent(ToolEvent): """Event to inform that a tool has been triggered""" - def __init__(self, name, tool, sender, canvasevent=None, data=None): - ToolEvent.__init__(self, name, sender) - self.tool = tool + def __init__(self, name, sender, tool, canvasevent=None, data=None): + ToolEvent.__init__(self, name, sender, tool) self.canvasevent = canvasevent self.data = data -class NavigationEvent(ToolEvent): - """Event for navigation tool management (add/remove/message)""" - def __init__(self, name, sender, **kwargs): - ToolEvent.__init__(self, name, sender) - for key, value in kwargs.items(): - setattr(self, key, value) +class ToolAddedEvent(ToolEvent): + """Event triggered when a tool is added""" + def __init__(self, name, sender, tool, group, position): + ToolEvent.__init__(self, name, sender, tool) + self.group = group + self.position = position + + +class NavigationMessageEvent(object): + """Event carring messages from navigation + + Messages are generaly displayed to the user by the toolbar + """ + def __init__(self, name, sender, message): + self.name = name + self.sender = sender + self.message = message class NavigationBase(object): @@ -3285,7 +3296,7 @@ def mpl_disconnect(self, cid): Example usage:: - cid = navigation.mpl_connect('tool-trigger-zoom', on_press) + cid = navigation.mpl_connect('tool_trigger_zoom', on_press) #...later navigation.mpl_disconnect(cid) """ @@ -3297,7 +3308,7 @@ def message_event(self, message, sender=None): sender = self s = 'tool_message_event' - event = NavigationEvent(s, sender, message=message) + event = NavigationMessageEvent(s, sender, message) self._callbacks.process(s, event) @property @@ -3370,7 +3381,7 @@ def remove_tool(self, name): self._remove_keys(name) s = 'tool_removed_event' - event = NavigationEvent(s, self, tool=tool) + event = ToolEvent(s, self, tool) self._callbacks.process(s, event) del self._tools[name] @@ -3431,28 +3442,28 @@ def add_tool(self, name, tool, group=None, position=None): def _tool_added_event(self, tool, group, position): s = 'tool_added_event' - event = NavigationEvent(s, self, - tool=tool, - group=group, - position=position) + event = ToolAddedEvent(s, self, + tool, + group, + position) self._callbacks.process(s, event) - def _handle_toggle(self, name, sender, canvasevent, data): + def _handle_toggle(self, tool, sender, canvasevent, data): # Toggle tools, need to be untoggled before other Toggle tool is used # This is called from tool_trigger_event - if self._toggled == name: + if self._toggled == tool.name: toggled = None elif self._toggled is None: - toggled = name + toggled = tool.name else: # Untoggle previously toggled tool self.tool_trigger_event(self._toggled, self, canvasevent, data) - toggled = name + toggled = tool.name self._toggled = toggled - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self._toggled) +# for a in self.canvas.figure.get_axes(): +# a.set_navigate_mode(self._toggled) def _get_cls_to_instantiate(self, callback_class): # Find the class that corresponds to the tool @@ -3494,7 +3505,8 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, self._trigger_tool(name, sender, canvasevent, data) s = 'tool-trigger-%s' % name - event = ToolTriggerEvent(s, self._tools[name], sender, canvasevent, data) + event = ToolTriggerEvent(s, sender, self._tools[name], canvasevent, + data) self._callbacks.process(s, event) def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): @@ -3505,7 +3517,7 @@ def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): tool = self._tools[name] if isinstance(tool, tools.ToolToggleBase): - self._handle_toggle(name, sender, canvasevent, data) + self._handle_toggle(tool, sender, canvasevent, data) # Important!!! # This is where the Tool object is triggered diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index ccc67ae689f1..08ca4cea80a5 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -18,7 +18,7 @@ import numpy as np -class Cursors: +class Cursors(object): """Simple namespace for cursor reference""" HAND, POINTER, SELECT_REGION, MOVE = list(range(4)) cursors = Cursors() @@ -118,6 +118,9 @@ class ToolToggleBase(ToolBase): Every time it is triggered, it switches between enable and disable """ + radio_group = None + """Attribute to group 'radio' like tools""" + cursor = None """Cursor to use when the tool is active""" @@ -192,7 +195,7 @@ def _tool_trigger_cbk(self, event): # If the tool is toggleable, set the cursor when the tool is triggered def _add_tool(self, tool): - if getattr(tool, 'toggled', None) is not None: + if getattr(tool, 'cursor', None) is not None: self.navigation.mpl_connect('tool-trigger-%s' % tool.name, self._tool_trigger_cbk) From 796d4edd29117ec9617e6bef8908d717c904d43a Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 10 Nov 2014 10:45:33 -0500 Subject: [PATCH 48/64] underscore in tool_trigger-xxx --- lib/matplotlib/backend_bases.py | 4 ++-- lib/matplotlib/backend_tools.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index eba2a5bb3f0f..8947eed9dccb 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3504,7 +3504,7 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, self._trigger_tool(name, sender, canvasevent, data) - s = 'tool-trigger-%s' % name + s = 'tool_trigger_%s' % name event = ToolTriggerEvent(s, sender, self._tools[name], canvasevent, data) self._callbacks.process(s, event) @@ -3593,7 +3593,7 @@ def _add_tool_cbk(self, event): event.tool.description, toggle) if toggle: - self.navigation.mpl_connect('tool-trigger-%s' % event.tool.name, + self.navigation.mpl_connect('tool_trigger_%s' % event.tool.name, self._tool_triggered_cbk) def _remove_tool_cbk(self, event): diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 08ca4cea80a5..fbef18649504 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -196,7 +196,7 @@ def _tool_trigger_cbk(self, event): # If the tool is toggleable, set the cursor when the tool is triggered def _add_tool(self, tool): if getattr(tool, 'cursor', None) is not None: - self.navigation.mpl_connect('tool-trigger-%s' % tool.name, + self.navigation.mpl_connect('tool_trigger_%s' % tool.name, self._tool_trigger_cbk) # If tool is added, process it From 30811ba6bf3a0ea0cdd342cac04fd0bc16393755 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 14 Nov 2014 14:19:25 -0500 Subject: [PATCH 49/64] adding radio_group for toggle tools --- examples/user_interfaces/navigation.py | 23 +++++--- lib/matplotlib/backend_bases.py | 43 +++++++++++--- lib/matplotlib/backend_tools.py | 77 +++++++++++++++----------- 3 files changed, 94 insertions(+), 49 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index bec3e57e2efa..8842e112eb6a 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -13,20 +13,25 @@ class ListTools(ToolBase): description = 'List Tools' def trigger(self, *args, **kwargs): - print ('_' * 80) - print ("{0:12} {1:45} {2}".format('Name (id)', - 'Tool description', - 'Keymap')) - print ('-' * 80) + print('_' * 80) + print("{0:12} {1:45} {2}".format('Name (id)', + 'Tool description', + 'Keymap')) + print('-' * 80) tools = self.navigation.tools for name in sorted(tools.keys()): if not tools[name].description: continue keys = ', '.join(sorted(self.navigation.get_tool_keymap(name))) - print ("{0:12} {1:45} {2}".format(name, - tools[name].description, - keys)) - print ('_' * 80) + print("{0:12} {1:45} {2}".format(name, + tools[name].description, + keys)) + print('_' * 80) + print("Active Toggle tools") + print("{0:12} {1:45}").format("Group", "Active") + print('-' * 80) + for group, active in self.navigation.active_toggle.items(): + print("{0:12} {1:45}").format(group, active) # A simple example of copy canvas diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 8947eed9dccb..6283ce0603f1 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3262,7 +3262,7 @@ def __init__(self, manager): self._tools = {} self._keys = {} - self._toggled = None + self._toggled = {} self._callbacks = cbook.CallbackRegistry() # to process keypress event @@ -3315,7 +3315,7 @@ def message_event(self, message, sender=None): def active_toggle(self): """Toggled Tool - **string** : Currently toggled tool, or None + **dict** : Currently toggled tools """ return self._toggled @@ -3375,7 +3375,8 @@ def remove_tool(self, name): tool = self._tools[name] tool.destroy() - if self._toggled == name: + # If is a toggle tool and toggled, untoggle + if getattr(tool, 'toggled', False): self.tool_trigger_event(tool, 'navigation') self._remove_keys(name) @@ -3438,6 +3439,15 @@ 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) + # For toggle tools init the radio_grop in self._toggled + if getattr(tool_cls, 'toggled', False) is not False: + # None group is not mutually exclusive, a set is used to keep track + # of all toggled tools in this group + if tool_cls.radio_group is None: + self._toggled.setdefault(None, set()) + else: + self._toggled.setdefault(tool_cls.radio_group, None) + self._tool_added_event(self._tools[name], group, position) def _tool_added_event(self, tool, group, position): @@ -3452,16 +3462,35 @@ def _handle_toggle(self, tool, sender, canvasevent, data): # Toggle tools, need to be untoggled before other Toggle tool is used # This is called from tool_trigger_event - if self._toggled == tool.name: + radio_group = tool.radio_group + # radio_group None is not mutually exclusive + # just keep track of toggled tools in this group + if radio_group is None: + if tool.toggled: + self._toggled[None].remove(tool.name) + else: + self._toggled[None].add(tool.name) + return + + # If it is the same tool that is toggled in the radio_group + # untoggle it + if self._toggled[radio_group] == tool.name: toggled = None - elif self._toggled is None: + # If no tool was toggled in the radio_group + # toggle it + elif self._toggled.get(radio_group, None) is None: toggled = tool.name + # Other tool in the radio_group is toggled else: # Untoggle previously toggled tool - self.tool_trigger_event(self._toggled, self, canvasevent, data) + self.tool_trigger_event(self._toggled[radio_group], + self, + canvasevent, + data) toggled = tool.name - self._toggled = toggled + # Keep track of the toggled tool in the radio_group + self._toggled[radio_group] = toggled # for a in self.canvas.figure.get_axes(): # a.set_navigate_mode(self._toggled) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index fbef18649504..cabee9acf6e4 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -119,7 +119,11 @@ class ToolToggleBase(ToolBase): """ radio_group = None - """Attribute to group 'radio' like tools""" + """Attribute to group 'radio' like tools (mutually exclusive) + + **String** that identifies the group or **None** if not belonging to a + group + """ cursor = None """Cursor to use when the tool is active""" @@ -328,7 +332,7 @@ def trigger(self, sender, event, data=None): a.set_navigate(i == n) -class ToolGrid(ToolBase): +class ToolGrid(ToolToggleBase): """Tool to toggle the grid of the figure""" description = 'Toogle Grid' @@ -337,60 +341,65 @@ class ToolGrid(ToolBase): def trigger(self, sender, event, data=None): if event.inaxes is None: return - event.inaxes.grid() + ToolToggleBase.trigger(self, sender, event, data) + + def enable(self, event): + event.inaxes.grid(True) + self.figure.canvas.draw() + + def disable(self, event): + event.inaxes.grid(False) self.figure.canvas.draw() -class ToolFullScreen(ToolBase): +class ToolFullScreen(ToolToggleBase): """Tool to toggle full screen""" description = 'Toogle Fullscreen mode' keymap = rcParams['keymap.fullscreen'] - def trigger(self, sender, event): + def enable(self, event): self.figure.canvas.manager.full_screen_toggle() + def disable(self, event): + self.figure.canvas.manager.full_screen_toggle() -class ToolYScale(ToolBase): - """Tool to toggle between linear and logarithmic the Y axis""" - description = 'Toogle Scale Y axis' - keymap = rcParams['keymap.yscale'] +class AxisScaleBase(ToolToggleBase): + """Base Tool to toggle between linear and logarithmic""" def trigger(self, sender, event, data=None): - """Toggle axis scale""" - ax = event.inaxes - if ax is None: + if event.inaxes is None: return + ToolToggleBase.trigger(self, sender, event, data) + + def enable(self, event): + self.set_scale(event.inaxes, 'log') + self.figure.canvas.draw() + + def disable(self, event): + self.set_scale(event.inaxes, 'linear') + self.figure.canvas.draw() - 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 ToolYScale(AxisScaleBase): + """Tool to toggle between linear and logarithmic the Y axis""" -class ToolXScale(ToolBase): + description = 'Toogle Scale Y axis' + keymap = rcParams['keymap.yscale'] + + def set_scale(self, ax, scale): + ax.set_yscale(scale) + + +class ToolXScale(AxisScaleBase): """Tool to toggle between linear and logarithmic the X axis""" description = 'Toogle Scale X axis' keymap = rcParams['keymap.xscale'] - def trigger(self, sender, event, data=None): - """Toggle axis scale""" - 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() + def set_scale(self, ax, scale): + ax.set_xscale(scale) class ToolViewsPositions(ToolBase): @@ -591,6 +600,7 @@ class ToolZoom(ZoomPanBase): image = 'zoom_to_rect.png' keymap = rcParams['keymap.zoom'] cursor = cursors.SELECT_REGION + radio_group = 'default' def __init__(self, *args): ZoomPanBase.__init__(self, *args) @@ -802,6 +812,7 @@ class ToolPan(ZoomPanBase): description = 'Pan axes with left mouse, zoom with right' image = 'move.png' cursor = cursors.MOVE + radio_group = 'default' def __init__(self, *args): ZoomPanBase.__init__(self, *args) From 6b8cf3fdf5d7a52c7e0c3de40977fa6e53e44b51 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 28 Nov 2014 15:33:23 -0500 Subject: [PATCH 50/64] scroll to zoom in zoom/pan tools --- lib/matplotlib/backend_tools.py | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index cabee9acf6e4..15405d06b783 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -572,6 +572,8 @@ def __init__(self, *args): self._xypress = None self._idPress = None self._idRelease = None + self._idScroll = None + self.base_scale = 2. def enable(self, event): """Connect press/release events and lock the canvas""" @@ -580,6 +582,8 @@ def enable(self, event): 'button_press_event', self._press) self._idRelease = self.figure.canvas.mpl_connect( 'button_release_event', self._release) + self._idScroll = self.figure.canvas.mpl_connect( + 'scroll_event', self.scroll_zoom) def disable(self, event): """Release the canvas and disconnect press/release events""" @@ -587,11 +591,41 @@ def disable(self, event): self.figure.canvas.widgetlock.release(self) self.figure.canvas.mpl_disconnect(self._idPress) self.figure.canvas.mpl_disconnect(self._idRelease) + self.figure.canvas.mpl_disconnect(self._idScroll) def trigger(self, sender, event, data=None): self.navigation.get_tool('viewpos').add_figure() ToolToggleBase.trigger(self, sender, event, data) + def scroll_zoom(self, event): + # https://gist.github.com/tacaswell/3144287 + if event.inaxes is None: + return + ax = event.inaxes + cur_xlim = ax.get_xlim() + cur_ylim = ax.get_ylim() + # set the range + cur_xrange = (cur_xlim[1] - cur_xlim[0])*.5 + cur_yrange = (cur_ylim[1] - cur_ylim[0])*.5 + xdata = event.xdata # get event x location + ydata = event.ydata # get event y location + if event.button == 'up': + # deal with zoom in + scale_factor = 1 / self.base_scale + elif event.button == 'down': + # deal with zoom out + scale_factor = self.base_scale + else: + # deal with something that should never happen + scale_factor = 1 + print event.button + # set new limits + ax.set_xlim([xdata - cur_xrange*scale_factor, + xdata + cur_xrange*scale_factor]) + ax.set_ylim([ydata - cur_yrange*scale_factor, + ydata + cur_yrange*scale_factor]) + self.figure.canvas.draw() # force re-draw + class ToolZoom(ZoomPanBase): """Zoom to rectangle""" From d7d58b86b8726a6113ee44d5d2e8dac1b322464c Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 28 Nov 2014 15:47:04 -0500 Subject: [PATCH 51/64] remove print --- lib/matplotlib/backend_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 15405d06b783..3eb51565dd3b 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -618,7 +618,6 @@ def scroll_zoom(self, event): else: # deal with something that should never happen scale_factor = 1 - print event.button # set new limits ax.set_xlim([xdata - cur_xrange*scale_factor, xdata + cur_xrange*scale_factor]) From c6169ba0abf14793e38b1f3928e54ca54bff6176 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 5 Dec 2014 10:25:48 -0500 Subject: [PATCH 52/64] remove ToolAddedEvent incorporating the functionality into toolevent --- lib/matplotlib/backend_bases.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 6283ce0603f1..227251750efe 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3209,26 +3209,18 @@ def set_history_buttons(self): class ToolEvent(object): """Event for tool manipulation (add/remove)""" - def __init__(self, name, sender, tool): + def __init__(self, name, sender, tool, data=None): self.name = name self.sender = sender self.tool = tool + self.data = data class ToolTriggerEvent(ToolEvent): """Event to inform that a tool has been triggered""" def __init__(self, name, sender, tool, canvasevent=None, data=None): - ToolEvent.__init__(self, name, sender, tool) + ToolEvent.__init__(self, name, sender, tool, data) self.canvasevent = canvasevent - self.data = data - - -class ToolAddedEvent(ToolEvent): - """Event triggered when a tool is added""" - def __init__(self, name, sender, tool, group, position): - ToolEvent.__init__(self, name, sender, tool) - self.group = group - self.position = position class NavigationMessageEvent(object): @@ -3452,10 +3444,10 @@ def add_tool(self, name, tool, group=None, position=None): def _tool_added_event(self, tool, group, position): s = 'tool_added_event' - event = ToolAddedEvent(s, self, - tool, - group, - position) + event = ToolEvent(s, + self, + tool, + data={'group': group, 'position': position}) self._callbacks.process(s, event) def _handle_toggle(self, tool, sender, canvasevent, data): @@ -3616,8 +3608,8 @@ def _add_tool_cbk(self, event): image = self._get_image_filename(event.tool.image) toggle = getattr(event.tool, 'toggled', None) is not None self.add_toolitem(event.tool.name, - event.group, - event.position, + event.data['group'], + event.data['position'], image, event.tool.description, toggle) From b0dc0df9e616bfcc4c0918d199fbbb8fd9480dd4 Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Sat, 3 Jan 2015 20:22:38 +0100 Subject: [PATCH 53/64] Spelling mistakes, and general tidying up of sentences. --- lib/matplotlib/backend_bases.py | 51 ++++++++++++++++----------------- lib/matplotlib/backend_tools.py | 19 ++++++------ 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 227251750efe..1bfed619fec2 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3224,9 +3224,9 @@ def __init__(self, name, sender, tool, canvasevent=None, data=None): class NavigationMessageEvent(object): - """Event carring messages from navigation + """Event carrying messages from navigation - Messages are generaly displayed to the user by the toolbar + Messages usually get displayed to the user by the toolbar """ def __init__(self, name, sender, message): self.name = name @@ -3313,7 +3313,7 @@ def active_toggle(self): return self._toggled def get_tool_keymap(self, name): - """Get the keymap associated with a tool + """Get the keymap associated with the specified tool Parameters ---------- @@ -3334,13 +3334,13 @@ def _remove_keys(self, name): del self._keys[k] def set_tool_keymap(self, name, *keys): - """Set the keymap associated with a tool + """Set the keymap to associate with the specified tool Parameters ---------- name : string Name of the Tool - keys : keys to associated with the Tool + keys : keys to associate with the Tool """ if name not in self._tools: @@ -3414,7 +3414,7 @@ def add_tool(self, name, tool, group=None, position=None): group: String Group to position the tool in position : int or None (default) - Position within its group in the toolbar, if None, is positioned at the end + Position within its group in the toolbar, if None, it goes at the end """ tool_cls = self._get_cls_to_instantiate(tool) @@ -3431,7 +3431,7 @@ 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) - # For toggle tools init the radio_grop in self._toggled + # For toggle tools init the radio_group in self._toggled if getattr(tool_cls, 'toggled', False) is not False: # None group is not mutually exclusive, a set is used to keep track # of all toggled tools in this group @@ -3451,8 +3451,8 @@ def _tool_added_event(self, tool, group, position): self._callbacks.process(s, event) def _handle_toggle(self, tool, sender, canvasevent, data): - # Toggle tools, need to be untoggled before other Toggle tool is used - # This is called from tool_trigger_event + # Toggle tools, need to untoggle prior to using other Toggle tool + # Called from tool_trigger_event radio_group = tool.radio_group # radio_group None is not mutually exclusive @@ -3464,8 +3464,7 @@ def _handle_toggle(self, tool, sender, canvasevent, data): self._toggled[None].add(tool.name) return - # If it is the same tool that is toggled in the radio_group - # untoggle it + # If the tool already has a toggled state, untoggle it if self._toggled[radio_group] == tool.name: toggled = None # If no tool was toggled in the radio_group @@ -3510,7 +3509,7 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, name : string Name of the tool sender: object - Object that wish to trigger the tool + Object that wishes to trigger the tool canvasevent : Event Original Canvas event or None data : Object @@ -3541,7 +3540,7 @@ def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): self._handle_toggle(tool, sender, canvasevent, data) # Important!!! - # This is where the Tool object is triggered + # This is where the Tool object gets triggered tool.trigger(sender, canvasevent, data) def _key_press(self, event): @@ -3590,13 +3589,13 @@ def __init__(self, manager): self._remove_tool_cbk) def _message_cbk(self, event): - """Captures the 'tool_message_event' to set message on the toolbar""" + """Captures the 'tool_message_event' to set the message on the toolbar""" self.set_message(event.message) def _tool_triggered_cbk(self, event): """Captures the 'tool-trigger-toolname - This is only used for toggled tools + This only gets used for toggled tools """ if event.sender is self: return @@ -3604,7 +3603,7 @@ def _tool_triggered_cbk(self, event): self.toggle_toolitem(event.tool.name) def _add_tool_cbk(self, event): - """Captures 'tool_added_event' and add the tool to the toolbar""" + """Captures 'tool_added_event' and adds the tool to the toolbar""" image = self._get_image_filename(event.tool.image) toggle = getattr(event.tool, 'toggled', None) is not None self.add_toolitem(event.tool.name, @@ -3618,11 +3617,11 @@ def _add_tool_cbk(self, event): self._tool_triggered_cbk) def _remove_tool_cbk(self, event): - """Captures the 'tool_removed_event' signal and remove the tool""" + """Captures the 'tool_removed_event' signal and removes the tool""" self.remove_toolitem(event.tool.name) def _get_image_filename(self, image): - """"Base on the image name find the corresponding image""" + """Find the image based on its name""" # TODO: better search for images, they are not always in the # datapath basedir = os.path.join(rcParams['datapath'], 'images') @@ -3638,7 +3637,7 @@ def trigger_tool(self, name): Parameters ---------- name : String - Name(id) of the tool that was triggered in the toolbar + Name(id) of the tool triggered from within the toolbar """ self.navigation.tool_trigger_event(name, sender=self) @@ -3646,7 +3645,7 @@ def trigger_tool(self, name): def add_toolitem(self, name, group, position, image, description, toggle): """Add a toolitem to the toolbar - This method has to be implemented per backend + This method must get implemented per backend The callback associated with the button click event, must be **EXACTLY** `self.trigger_tool(name)` @@ -3654,12 +3653,12 @@ def add_toolitem(self, name, group, position, image, description, toggle): Parameters ---------- name : string - Name of the tool to add, this is used as ID and as default label - of the buttons + Name of the tool to add, this gets used as the tool's ID and as the + default label of the buttons group : String - Name of the group that the tool belongs to + Name of the group that this tool belongs to position : Int - Position of the tool whthin its group if -1 at the End + Position of the tool within its group, if -1 it goes at the End image_file : String Filename of the image for the button or `None` description : String @@ -3697,9 +3696,9 @@ def toggle_toolitem(self, name): def remove_toolitem(self, name): """Remove a toolitem from the `Toolbar` - This method has to be implemented per backend + This method must get implemented per backend - Called when `tool_removed_event` is emited by `NavigationBase` + Called when `NavigationBase` emits a `tool_removed_event` Parameters ---------- diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 3eb51565dd3b..ec3e4eb958e3 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -143,7 +143,7 @@ def trigger(self, sender, event, data=None): def enable(self, event=None): """Enable the toggle tool - This method is called dby `trigger` when the `toggled` is False + `trigger` calls this method when `toggled` is False """ pass @@ -151,7 +151,7 @@ def enable(self, event=None): def disable(self, event=None): """Disable the toggle tool - This method is called by `trigger` when the `toggled` is True. + `trigger` call this methond when `toggled` is True. This can happen in different circumstances @@ -174,7 +174,7 @@ class SetCursorBase(ToolBase): """Change to the current cursor while inaxes This tool, keeps track of all `ToolToggleBase` derived tools, and calls - set_cursor when one of these tools is triggered + set_cursor when a tool gets triggered """ def __init__(self, *args, **kwargs): ToolBase.__init__(self, *args, **kwargs) @@ -251,7 +251,6 @@ def send_message(self, event): message = ' ' if event.inaxes and event.inaxes.get_navigate(): - try: s = event.inaxes.format_coord(event.xdata, event.ydata) except (ValueError, OverflowError): @@ -275,14 +274,14 @@ def trigger(self, sender, event, data): def draw_rubberband(self, *data): """Draw rubberband - This method has to be implemented per backend + This method must get implemented per backend """ pass def remove_rubberband(self): """Remove rubberband - This method has to be implemented per backend + This method must get implemented per backend """ pass @@ -383,7 +382,7 @@ def disable(self, event): class ToolYScale(AxisScaleBase): - """Tool to toggle between linear and logarithmic the Y axis""" + """Tool to toggle between linear and logarithmic scales on the Y axis""" description = 'Toogle Scale Y axis' keymap = rcParams['keymap.yscale'] @@ -393,7 +392,7 @@ def set_scale(self, ax, scale): class ToolXScale(AxisScaleBase): - """Tool to toggle between linear and logarithmic the X axis""" + """Tool to toggle between linear and logarithmic scales on the X axis""" description = 'Toogle Scale X axis' keymap = rcParams['keymap.xscale'] @@ -405,8 +404,8 @@ def set_scale(self, ax, scale): class ToolViewsPositions(ToolBase): """Auxiliary Tool to handle changes in views and positions - Runs in the background and is used by all the tools that - need to access the record of views and positions of the figure + Runs in the background and should get used by all the tools that + need to access the figure's history of views and positions, e.g. * `ToolZoom` * `ToolPan` From 789b48b054dac47b8f7a56d102122dbd35d7dac3 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 5 Jan 2015 11:30:48 -0500 Subject: [PATCH 54/64] eliminating repeated loop --- lib/matplotlib/backend_bases.py | 3 +-- lib/matplotlib/backends/backend_gtk3.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 1bfed619fec2..e23a89e61784 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3329,8 +3329,7 @@ def get_tool_keymap(self, 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: + for k in self.get_tool_keymap(name): del self._keys[k] def set_tool_keymap(self, name, *keys): diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 5069ce33d4ea..cde21901b64e 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -800,6 +800,7 @@ def add_toolitem(self, name, group, position, image_file, description, if position is None: position = -1 + # TODO implement groups positions self._toolbar.insert(tbutton, -1) signal = tbutton.connect('clicked', self._call_tool, name) tbutton.set_tooltip_text(description) From 85bda68444c88cde7f1b8aabcb8a94ffd0e7bcde Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 5 Jan 2015 12:30:51 -0500 Subject: [PATCH 55/64] doc bullet list --- lib/matplotlib/backend_bases.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index e23a89e61784..5cbf291563e2 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3274,9 +3274,12 @@ def mpl_connect(self, s, func): - 'tool_message_event' - 'tool_removed_event' - 'tool_added_event' + For every tool added a new event is created - - 'tool_trigger_TOOLNAME - Where TOOLNAME is the id of the tool. + + - 'tool_trigger_TOOLNAME` + Where TOOLNAME is the id of the tool. + func : function Function to be called with signature def func(event) From 0d39c2da02a96c8ee2e7724ab7bbde5b6fc7259b Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 5 Jan 2015 12:55:21 -0500 Subject: [PATCH 56/64] end single quote missing --- lib/matplotlib/backend_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index ec3e4eb958e3..88140d926e9e 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -244,7 +244,7 @@ def __init__(self, *args, **kwargs): 'motion_notify_event', self.send_message) def send_message(self, event): - """Call `matplotlib.backend_bases.NavigationBase.message_event""" + """Call `matplotlib.backend_bases.NavigationBase.message_event`""" if self.navigation.messagelock.locked(): return From d695dd2ef574e9f7453271a530a757fd6ab70735 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 21 Jan 2015 15:13:46 -0500 Subject: [PATCH 57/64] replace draw by draw_idle in tools --- lib/matplotlib/backend_tools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 88140d926e9e..8949e805cac0 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -344,11 +344,11 @@ def trigger(self, sender, event, data=None): def enable(self, event): event.inaxes.grid(True) - self.figure.canvas.draw() + self.figure.canvas.draw_idle() def disable(self, event): event.inaxes.grid(False) - self.figure.canvas.draw() + self.figure.canvas.draw_idle() class ToolFullScreen(ToolToggleBase): @@ -374,11 +374,11 @@ def trigger(self, sender, event, data=None): def enable(self, event): self.set_scale(event.inaxes, 'log') - self.figure.canvas.draw() + self.figure.canvas.draw_idle() def disable(self, event): self.set_scale(event.inaxes, 'linear') - self.figure.canvas.draw() + self.figure.canvas.draw_idle() class ToolYScale(AxisScaleBase): @@ -622,7 +622,7 @@ def scroll_zoom(self, event): xdata + cur_xrange*scale_factor]) ax.set_ylim([ydata - cur_yrange*scale_factor, ydata + cur_yrange*scale_factor]) - self.figure.canvas.draw() # force re-draw + self.figure.canvas.draw_idle() # force re-draw class ToolZoom(ZoomPanBase): From 7ccb6c23ad1432924cf7942ed10cfe3406c637ac Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 21 Jan 2015 15:31:56 -0500 Subject: [PATCH 58/64] rename mpl_connect --- lib/matplotlib/backend_bases.py | 16 ++++++++-------- lib/matplotlib/backend_tools.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 5cbf291563e2..9752264bd742 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3261,7 +3261,7 @@ def __init__(self, manager): self.keypresslock = widgets.LockDraw() self.messagelock = widgets.LockDraw() - def mpl_connect(self, s, func): + def nav_connect(self, s, func): """Connect event with string *s* to *func*. Parameters @@ -3286,14 +3286,14 @@ def func(event) """ return self._callbacks.connect(s, func) - def mpl_disconnect(self, cid): + def nav_disconnect(self, cid): """Disconnect callback id cid Example usage:: - cid = navigation.mpl_connect('tool_trigger_zoom', on_press) + cid = navigation.nav_connect('tool_trigger_zoom', on_press) #...later - navigation.mpl_disconnect(cid) + navigation.nav_disconnect(cid) """ return self._callbacks.disconnect(cid) @@ -3585,9 +3585,9 @@ def __init__(self, manager): self.manager = manager self.navigation = manager.navigation - self.navigation.mpl_connect('tool_message_event', self._message_cbk) - self.navigation.mpl_connect('tool_added_event', self._add_tool_cbk) - self.navigation.mpl_connect('tool_removed_event', + self.navigation.nav_connect('tool_message_event', self._message_cbk) + self.navigation.nav_connect('tool_added_event', self._add_tool_cbk) + self.navigation.nav_connect('tool_removed_event', self._remove_tool_cbk) def _message_cbk(self, event): @@ -3615,7 +3615,7 @@ def _add_tool_cbk(self, event): event.tool.description, toggle) if toggle: - self.navigation.mpl_connect('tool_trigger_%s' % event.tool.name, + self.navigation.nav_connect('tool_trigger_%s' % event.tool.name, self._tool_triggered_cbk) def _remove_tool_cbk(self, event): diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 8949e805cac0..97fbc0b680b3 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -183,7 +183,7 @@ def __init__(self, *args, **kwargs): self._cursor = None self._default_cursor = cursors.POINTER self._last_cursor = self._default_cursor - self.navigation.mpl_connect('tool_added_event', self._add_tool_cbk) + self.navigation.nav_connect('tool_added_event', self._add_tool_cbk) # process current tools for tool in self.navigation.tools.values(): @@ -200,7 +200,7 @@ def _tool_trigger_cbk(self, event): # If the tool is toggleable, set the cursor when the tool is triggered def _add_tool(self, tool): if getattr(tool, 'cursor', None) is not None: - self.navigation.mpl_connect('tool_trigger_%s' % tool.name, + self.navigation.nav_connect('tool_trigger_%s' % tool.name, self._tool_trigger_cbk) # If tool is added, process it From c46d6d6e9348c83911d38c8c3a38a3cbf2bc045f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 28 Jan 2015 09:53:57 -0500 Subject: [PATCH 59/64] unused attribute example --- examples/user_interfaces/navigation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 8842e112eb6a..8142082d98fd 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -39,8 +39,6 @@ def trigger(self, *args, **kwargs): class CopyToolGTK3(ToolBase): keymap = 'ctrl+c' description = 'Copy canvas' - # It is not added to the toolbar as a button - intoolbar = False def trigger(self, *args, **kwargs): from gi.repository import Gtk, Gdk From aa764579610dd9f6bb702038ea414e1262544302 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 4 Feb 2015 12:03:36 -0500 Subject: [PATCH 60/64] cleaning navigation and toolbar dependencies --- lib/matplotlib/backend_bases.py | 10 ++++------ lib/matplotlib/backends/backend_gtk3.py | 8 ++++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 9752264bd742..920317a891a2 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3245,9 +3245,8 @@ class NavigationBase(object): messagelock: `LockDraw` to know if the message is available to write """ - def __init__(self, manager): - self.manager = manager - self.canvas = manager.canvas + def __init__(self, canvas): + self.canvas = canvas self._key_press_handler_id = self.canvas.mpl_connect( 'key_press_event', self._key_press) @@ -3581,9 +3580,8 @@ class ToolbarBase(object): this `Toolbar` wants to communicate with """ - def __init__(self, manager): - self.manager = manager - self.navigation = manager.navigation + def __init__(self, navigation): + self.navigation = navigation self.navigation.nav_connect('tool_message_event', self._message_cbk) self.navigation.nav_connect('tool_added_event', self._add_tool_cbk) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index cde21901b64e..5aa6474e4ee8 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -480,7 +480,7 @@ def _get_toolbar(self): if rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2GTK3 (self.canvas, self.window) elif rcParams['toolbar'] == 'navigation': - toolbar = ToolbarGTK3(self) + toolbar = ToolbarGTK3(self.navigation) else: toolbar = None return toolbar @@ -488,7 +488,7 @@ def _get_toolbar(self): def _get_navigation(self): # must be initialised after toolbar has been setted if rcParams['toolbar'] != 'toolbar2': - navigation = NavigationGTK3(self) + navigation = NavigationGTK3(self.canvas) else: navigation = None return navigation @@ -753,8 +753,8 @@ def draw_rubberband(self, x0, y0, x1, y1): class ToolbarGTK3(ToolbarBase, Gtk.Box): - def __init__(self, manager): - ToolbarBase.__init__(self, manager) + def __init__(self, navigation): + ToolbarBase.__init__(self, navigation) Gtk.Box.__init__(self) self.set_property("orientation", Gtk.Orientation.VERTICAL) From 91d2f51d28913755a8a1ab6e7fe6f10ecee6a169 Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Wed, 11 Feb 2015 15:16:09 +0100 Subject: [PATCH 61/64] Made NavigationBase.get_tool() more useful. --- lib/matplotlib/backend_bases.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 920317a891a2..9f6f9f364433 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3365,7 +3365,7 @@ def remove_tool(self, name): Name of the Tool """ - tool = self._tools[name] + tool = self.get_tool(name) tool.destroy() # If is a toggle tool and toggled, untoggle @@ -3516,8 +3516,8 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, data : Object Extra data to pass to the tool when triggering """ - if name not in self._tools: - warnings.warn("%s is not a tool controlled by Navigation" % name) + tool = self.get_tool(name) + if tool is None: return if sender is None: @@ -3526,8 +3526,7 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, self._trigger_tool(name, sender, canvasevent, data) s = 'tool_trigger_%s' % name - event = ToolTriggerEvent(s, sender, self._tools[name], canvasevent, - data) + event = ToolTriggerEvent(s, sender, tool, canvasevent, data) self._callbacks.process(s, event) def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): @@ -3535,7 +3534,7 @@ def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): Method to actually trigger the tool """ - tool = self._tools[name] + tool = self.get_tool(name) if isinstance(tool, tools.ToolToggleBase): self._handle_toggle(tool, sender, canvasevent, data) @@ -3560,13 +3559,18 @@ def tools(self): return self._tools def get_tool(self, name): - """Return the tool object + """Return the tool object, also accepts the actual tool for convenience Parameters ----------- - name : String - Name of the tool + name : String, ToolBase + Name of the tool, or the tool itself """ + if isinstance(name, tools.ToolBase): + return name + if name not in self._tools: + warnings.warn("%s is not a tool controlled by Navigation" % name) + return None return self._tools[name] From bf52f5022bba1bf60c2d7c0429eb56922b8715ad Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Sat, 7 Feb 2015 23:02:28 +0100 Subject: [PATCH 62/64] Moved tooltriggerevents into the tool. --- lib/matplotlib/backend_bases.py | 23 +++-------------------- lib/matplotlib/backend_tools.py | 26 ++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 9f6f9f364433..c2e805240468 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3215,14 +3215,6 @@ def __init__(self, name, sender, tool, data=None): self.tool = tool self.data = data - -class ToolTriggerEvent(ToolEvent): - """Event to inform that a tool has been triggered""" - def __init__(self, name, sender, tool, canvasevent=None, data=None): - ToolEvent.__init__(self, name, sender, tool, data) - self.canvasevent = canvasevent - - class NavigationMessageEvent(object): """Event carrying messages from navigation @@ -3274,11 +3266,6 @@ def nav_connect(self, s, func): - 'tool_removed_event' - 'tool_added_event' - For every tool added a new event is created - - - 'tool_trigger_TOOLNAME` - Where TOOLNAME is the id of the tool. - func : function Function to be called with signature def func(event) @@ -3290,7 +3277,7 @@ def nav_disconnect(self, cid): Example usage:: - cid = navigation.nav_connect('tool_trigger_zoom', on_press) + cid = navigation.nav_connect('tool_message_event', on_press) #...later navigation.nav_disconnect(cid) """ @@ -3525,10 +3512,6 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, self._trigger_tool(name, sender, canvasevent, data) - s = 'tool_trigger_%s' % name - event = ToolTriggerEvent(s, sender, tool, canvasevent, data) - self._callbacks.process(s, event) - def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): """Trigger on a tool @@ -3542,6 +3525,7 @@ def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): # Important!!! # This is where the Tool object gets triggered tool.trigger(sender, canvasevent, data) + tool.state_changed_event(sender, canvasevent, data) def _key_press(self, event): if event.key is None or self.keypresslock.locked(): @@ -3617,8 +3601,7 @@ def _add_tool_cbk(self, event): event.tool.description, toggle) if toggle: - self.navigation.nav_connect('tool_trigger_%s' % event.tool.name, - self._tool_triggered_cbk) + event.tool.tool_connect('tool_state_changed', self._tool_triggered_cbk) def _remove_tool_cbk(self, event): """Captures the 'tool_removed_event' signal and removes the tool""" diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 97fbc0b680b3..2920e6a33148 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -23,6 +23,11 @@ class Cursors(object): HAND, POINTER, SELECT_REGION, MOVE = list(range(4)) cursors = Cursors() +class ToolTriggerEvent(object): + """Event to inform that a tool has been triggered""" + def __init__(self, name, sender, tool, canvasevent=None, data=None): + self.name, self.sender, self.tool, self.canvasevent, self.data =\ + name, sender, tool, canvasevent, data class ToolBase(object): """Base tool class @@ -66,6 +71,7 @@ def __init__(self, navigation, name): self._name = name self.figure = None self.navigation = navigation + self._callbacks = cbook.CallbackRegistry() self.set_figure(navigation.canvas.figure) def trigger(self, sender, event, data=None): @@ -111,6 +117,23 @@ def destroy(self): """ pass + def state_changed_event(self, sender, canvasevent, data): + s = 'tool_state_changed' + event = ToolTriggerEvent(s, sender, self, canvasevent, data) + self._callbacks.process(s, event) + + def tool_connect(self, s, func): + ''' + Parameters + ----------- + s : String + Name of the event, i.e. 'tool_state_changed' + + func : function + Function to be called with signature + def func(event) + ''' + return self._callbacks.connect(s, func) class ToolToggleBase(ToolBase): """Toggleable tool @@ -200,8 +223,7 @@ def _tool_trigger_cbk(self, event): # If the tool is toggleable, set the cursor when the tool is triggered def _add_tool(self, tool): if getattr(tool, 'cursor', None) is not None: - self.navigation.nav_connect('tool_trigger_%s' % tool.name, - self._tool_trigger_cbk) + tool.tool_connect('tool_state_changed', self._tool_trigger_cbk) # If tool is added, process it def _add_tool_cbk(self, event): From ea62857b338a5047abce8ae20ddec0fbd720ef1f Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Tue, 10 Feb 2015 14:32:00 +0100 Subject: [PATCH 63/64] Changed name of event from `tool_state_changed` to `tool_triggered` Renamed tool_trigger_event to trigger_tool now that it doesn't fire event. --- lib/matplotlib/backend_bases.py | 29 ++++++++--------------------- lib/matplotlib/backend_tools.py | 15 +++++++-------- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index c2e805240468..edb0007331b9 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3357,7 +3357,7 @@ def remove_tool(self, name): # If is a toggle tool and toggled, untoggle if getattr(tool, 'toggled', False): - self.tool_trigger_event(tool, 'navigation') + self.trigger_tool(tool, 'navigation') self._remove_keys(name) @@ -3438,7 +3438,7 @@ def _tool_added_event(self, tool, group, position): data={'group': group, 'position': position}) self._callbacks.process(s, event) - def _handle_toggle(self, tool, sender, canvasevent, data): + def _handle_toggle(self, tool, sender, *args): # Toggle tools, need to untoggle prior to using other Toggle tool # Called from tool_trigger_event @@ -3462,10 +3462,7 @@ def _handle_toggle(self, tool, sender, canvasevent, data): # Other tool in the radio_group is toggled else: # Untoggle previously toggled tool - self.tool_trigger_event(self._toggled[radio_group], - self, - canvasevent, - data) + self.trigger_tool(self._toggled[radio_group], self, *args) toggled = tool.name # Keep track of the toggled tool in the radio_group @@ -3488,8 +3485,7 @@ def _get_cls_to_instantiate(self, callback_class): return callback_class - def tool_trigger_event(self, name, sender=None, canvasevent=None, - data=None): + def trigger_tool(self, name, sender=None, canvasevent=None, data=None): """Trigger a tool and emit the tool-trigger-[name] event Parameters @@ -3510,22 +3506,13 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, if sender is None: sender = self - self._trigger_tool(name, sender, canvasevent, data) - - def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): - """Trigger on a tool - - Method to actually trigger the tool - """ - tool = self.get_tool(name) - if isinstance(tool, tools.ToolToggleBase): self._handle_toggle(tool, sender, canvasevent, data) # Important!!! # This is where the Tool object gets triggered tool.trigger(sender, canvasevent, data) - tool.state_changed_event(sender, canvasevent, data) + tool.trigger_event(sender, canvasevent, data) def _key_press(self, event): if event.key is None or self.keypresslock.locked(): @@ -3534,7 +3521,7 @@ def _key_press(self, event): name = self._keys.get(event.key, None) if name is None: return - self.tool_trigger_event(name, canvasevent=event) + self.trigger_tool(name, canvasevent=event) @property def tools(self): @@ -3601,7 +3588,7 @@ def _add_tool_cbk(self, event): event.tool.description, toggle) if toggle: - event.tool.tool_connect('tool_state_changed', self._tool_triggered_cbk) + event.tool.tool_connect('tool_triggered', self._tool_triggered_cbk) def _remove_tool_cbk(self, event): """Captures the 'tool_removed_event' signal and removes the tool""" @@ -3627,7 +3614,7 @@ def trigger_tool(self, name): Name(id) of the tool triggered from within the toolbar """ - self.navigation.tool_trigger_event(name, sender=self) + self.navigation.trigger_tool(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 2920e6a33148..a912486e3cea 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -117,8 +117,8 @@ def destroy(self): """ pass - def state_changed_event(self, sender, canvasevent, data): - s = 'tool_state_changed' + def trigger_event(self, sender, canvasevent, data): + s = 'tool_triggered' event = ToolTriggerEvent(s, sender, self, canvasevent, data) self._callbacks.process(s, event) @@ -127,7 +127,7 @@ def tool_connect(self, s, func): Parameters ----------- s : String - Name of the event, i.e. 'tool_state_changed' + Name of the event, i.e. 'tool_triggered' func : function Function to be called with signature @@ -223,7 +223,7 @@ def _tool_trigger_cbk(self, event): # If the tool is toggleable, set the cursor when the tool is triggered def _add_tool(self, tool): if getattr(tool, 'cursor', None) is not None: - tool.tool_connect('tool_state_changed', self._tool_trigger_cbk) + tool.tool_connect('tool_triggered', self._tool_trigger_cbk) # If tool is added, process it def _add_tool_cbk(self, event): @@ -663,7 +663,7 @@ def __init__(self, *args): def _cancel_action(self): for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) - self.navigation.tool_trigger_event('rubberband', self) + self.navigation.trigger_tool('rubberband', self) self.navigation.get_tool('viewpos').refresh_locators() self._xypress = None self._button_pressed = None @@ -732,9 +732,8 @@ def _mouse_move(self, event): x1, y1, x2, y2 = a.bbox.extents x, lastx = x1, x2 - self.navigation.tool_trigger_event('rubberband', - self, - data=(x, y, lastx, lasty)) + self.navigation.trigger_tool('rubberband', self, + data=(x, y, lastx, lasty)) def _release(self, event): """the release mouse button callback in zoom to rect mode""" From 827b00657a9bdbb5e98c8e4174caf42969c7416f Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Wed, 11 Feb 2015 00:23:21 +0100 Subject: [PATCH 64/64] Moved event firing to source, i.e. to the trigger method. Now one must override the _trigger method instead. --- lib/matplotlib/backend_bases.py | 1 - lib/matplotlib/backend_tools.py | 32 ++++++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index edb0007331b9..3d601dbf86a0 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3512,7 +3512,6 @@ def trigger_tool(self, name, sender=None, canvasevent=None, data=None): # Important!!! # This is where the Tool object gets triggered tool.trigger(sender, canvasevent, data) - tool.trigger_event(sender, canvasevent, data) def _key_press(self, event): if event.key is None or self.keypresslock.locked(): diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index a912486e3cea..894c600bc072 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -13,6 +13,7 @@ from matplotlib import rcParams from matplotlib._pylab_helpers import Gcf +from matplotlib import docstring import matplotlib.cbook as cbook from weakref import WeakKeyDictionary import numpy as np @@ -74,6 +75,7 @@ def __init__(self, navigation, name): self._callbacks = cbook.CallbackRegistry() self.set_figure(navigation.canvas.figure) + @docstring.dedent_interpd def trigger(self, sender, event, data=None): """Called when this tool gets used @@ -89,7 +91,13 @@ def trigger(self, sender, event, data=None): data: object Extra data """ + self._trigger(sender, event, data) + self.trigger_event(sender, event, data) + def _trigger(self, *args, **kwargs): + """ Tools should Override this method, not trigger. + %(trigger)s + """ pass def set_figure(self, figure): @@ -155,7 +163,7 @@ def __init__(self, *args, **kwargs): ToolBase.__init__(self, *args, **kwargs) self._toggled = False - def trigger(self, sender, event, data=None): + def _trigger(self, sender, event, data=None): """Calls `enable` or `disable` based on `toggled` value""" if self._toggled: self.disable(event) @@ -284,7 +292,7 @@ def send_message(self, event): class RubberbandBase(ToolBase): """Draw and remove rubberband""" - def trigger(self, sender, event, data): + def _trigger(self, sender, event, data): """Call `draw_rubberband` or `remove_rubberband` based on data""" if not self.figure.canvas.widgetlock.available(sender): return @@ -314,7 +322,7 @@ class ToolQuit(ToolBase): description = 'Quit the figure' keymap = rcParams['keymap.quit'] - def trigger(self, sender, event, data=None): + def _trigger(self, sender, event, data=None): Gcf.destroy_fig(self.figure) @@ -324,7 +332,7 @@ class ToolEnableAllNavigation(ToolBase): description = 'Enables all axes navigation' keymap = rcParams['keymap.all_axes'] - def trigger(self, sender, event, data=None): + def _trigger(self, sender, event, data=None): if event.inaxes is None: return @@ -340,7 +348,7 @@ class ToolEnableNavigation(ToolBase): description = 'Enables one axes navigation' keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9) - def trigger(self, sender, event, data=None): + def _trigger(self, sender, event, data=None): if event.inaxes is None: return @@ -359,10 +367,10 @@ class ToolGrid(ToolToggleBase): description = 'Toogle Grid' keymap = rcParams['keymap.grid'] - def trigger(self, sender, event, data=None): + def _trigger(self, sender, event, data=None): if event.inaxes is None: return - ToolToggleBase.trigger(self, sender, event, data) + ToolToggleBase._trigger(self, sender, event, data) def enable(self, event): event.inaxes.grid(True) @@ -389,10 +397,10 @@ def disable(self, event): class AxisScaleBase(ToolToggleBase): """Base Tool to toggle between linear and logarithmic""" - def trigger(self, sender, event, data=None): + def _trigger(self, sender, event, data=None): if event.inaxes is None: return - ToolToggleBase.trigger(self, sender, event, data) + ToolToggleBase._trigger(self, sender, event, data) def enable(self, event): self.set_scale(event.inaxes, 'log') @@ -537,7 +545,7 @@ class ViewsPositionsBase(ToolBase): _on_trigger = None - def trigger(self, sender, event, data=None): + def _trigger(self, sender, event, data=None): self.navigation.get_tool('viewpos').add_figure() getattr(self.navigation.get_tool('viewpos'), self._on_trigger)() self.navigation.get_tool('viewpos').update_view() @@ -614,9 +622,9 @@ def disable(self, event): self.figure.canvas.mpl_disconnect(self._idRelease) self.figure.canvas.mpl_disconnect(self._idScroll) - def trigger(self, sender, event, data=None): + def _trigger(self, sender, event, data=None): self.navigation.get_tool('viewpos').add_figure() - ToolToggleBase.trigger(self, sender, event, data) + ToolToggleBase._trigger(self, sender, event, data) def scroll_zoom(self, event): # https://gist.github.com/tacaswell/3144287