From 8cceed493743e49b3e822213f2e6ecfb913b7489 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 23 Jan 2014 13:35:40 -0500 Subject: [PATCH 01/69] 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 a3dec56759c4..c2a5d6898d48 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 @@ -2570,8 +2572,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`. @@ -2630,10 +2634,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): @@ -3213,3 +3214,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 0600da2fb8a5..a9c025ff1347 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 @@ -414,7 +415,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 @@ -438,7 +439,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() @@ -474,7 +477,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 @@ -702,7 +709,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 1fdf121b32d8..40a24c247872 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 3118a5ad94b7d4e4ebcd7dc44d38199b49c370eb Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 23 Jan 2014 15:46:41 -0500 Subject: [PATCH 02/69] mod keypress in figuremanager --- examples/user_interfaces/navigation.py | 52 +++ lib/matplotlib/backend_bases.py | 3 +- lib/matplotlib/backend_tools.py | 527 +++++++++++++++++++++++++ 3 files changed, 581 insertions(+), 1 deletion(-) 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_bases.py b/lib/matplotlib/backend_bases.py index c2a5d6898d48..3686de934211 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2611,7 +2611,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): """ 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 b4d5fcf0479986ca4c31af3842a7f82294aedaed Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 24 Jan 2014 10:29:05 -0500 Subject: [PATCH 03/69] 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 3686de934211..2a4f8476b0a5 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3228,8 +3228,10 @@ class NavigationBase(object): tools.ToolToggleYScale, tools.ToolHome, tools.ToolBack, tools.ToolForward, + None, tools.ToolZoom, tools.ToolPan, + None, 'ConfigureSubplots', 'SaveFigure'] @@ -3266,7 +3268,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 @@ -3279,7 +3285,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) @@ -3310,6 +3337,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: @@ -3573,3 +3603,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 a9c025ff1347..3a89b82002e2 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -807,8 +807,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() @@ -822,12 +821,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 1e8af47b99265a342b515874110de79eae0e3d53 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 24 Jan 2014 13:23:16 -0500 Subject: [PATCH 04/69] 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 2a4f8476b0a5..60621b7d0f42 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, @@ -3218,6 +3226,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, @@ -3286,13 +3313,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) @@ -3308,12 +3365,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] @@ -3321,37 +3398,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): @@ -3401,7 +3487,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) @@ -3414,7 +3511,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: @@ -3427,7 +3524,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) @@ -3437,6 +3534,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')) @@ -3583,29 +3681,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 3a89b82002e2..62765147f9c6 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -779,7 +779,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() @@ -800,12 +800,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 @@ -819,7 +819,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 622cb95460001e95e4547a0904a1765800fb5dd1 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Sat, 25 Jan 2014 22:06:41 -0500 Subject: [PATCH 05/69] 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 60621b7d0f42..d90b2ce01a2c 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3233,17 +3233,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, @@ -3263,6 +3256,7 @@ class NavigationBase(object): 'SaveFigure'] def __init__(self, canvas, toolbar=None): + """.. automethod:: _toolbar_callback""" self.canvas = canvas self.toolbar = self._get_toolbar(toolbar, canvas) @@ -3312,18 +3306,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 @@ -3375,7 +3371,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: @@ -3686,17 +3683,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 d1a9de49356b6484ed4cafcf790f15e9f8aea9a9 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 27 Jan 2014 16:37:29 -0500 Subject: [PATCH 06/69] simulate click --- lib/matplotlib/backend_bases.py | 71 +++++++++++++++++++-------------- lib/matplotlib/backend_tools.py | 4 +- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d90b2ce01a2c..f48339cdfa83 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3308,16 +3308,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 @@ -3331,7 +3332,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 @@ -3363,6 +3364,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 @@ -3372,8 +3378,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) @@ -3450,24 +3455,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) @@ -3476,6 +3478,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) @@ -3496,14 +3512,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 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 3f89d528a7f13ad1453dce5559cdc50f5ba9577a Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 28 Jan 2014 18:41:19 -0500 Subject: [PATCH 07/69] 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 f48339cdfa83..46cd4489ae25 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3472,7 +3472,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?) @@ -3521,7 +3521,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: @@ -3533,7 +3533,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 62765147f9c6..3ed66ccd5baf 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -859,7 +859,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() @@ -920,7 +920,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 4f3c10b58bfc754127e53769a613ed6b450c3f2a Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 28 Jan 2014 19:20:45 -0500 Subject: [PATCH 08/69] 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 46cd4489ae25..976c6c8281d6 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3455,15 +3455,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) @@ -3490,7 +3490,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: @@ -3512,7 +3512,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 @@ -3525,14 +3525,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 6065daad1f586a2a2e5fc18975e51d6dc04eb037 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 28 Jan 2014 19:36:49 -0500 Subject: [PATCH 09/69] 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 976c6c8281d6..b22d5d9a6165 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3521,22 +3521,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 f6a2f196dc5ea1cf8517fa97d8ed9fe94c9f390f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 29 Jan 2014 12:53:42 -0500 Subject: [PATCH 10/69] reducing number of locks --- lib/matplotlib/backend_bases.py | 91 +++++++---------- lib/matplotlib/backend_tools.py | 128 ++++++++++-------------- lib/matplotlib/backends/backend_gtk3.py | 9 +- 3 files changed, 92 insertions(+), 136 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index b22d5d9a6165..af73656d654c 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3232,11 +3232,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, @@ -3266,11 +3264,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 @@ -3282,11 +3275,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: @@ -3479,16 +3468,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) @@ -3559,12 +3541,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) @@ -3576,7 +3552,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(): @@ -3593,30 +3569,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(): @@ -3681,9 +3633,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..575e279587d0 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,26 +415,25 @@ 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) + 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 @@ -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): + 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() diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 3ed66ccd5baf..f1bb1f9f9857 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -718,7 +718,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() @@ -744,10 +747,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 05db3b62ea43c1a59d7b306d084365b83b8a555f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 4 Feb 2014 09:17:41 -0500 Subject: [PATCH 11/69] 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 af73656d654c..1b7c15c5c06c 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3273,8 +3273,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: @@ -3401,6 +3402,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') @@ -3425,10 +3427,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): @@ -3457,9 +3460,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 c08fe56fb6e8e25c4e0df4c6aa18e477c7cbfbfd Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 4 Feb 2014 09:47:07 -0500 Subject: [PATCH 12/69] 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 1b7c15c5c06c..64213befb2bf 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3450,8 +3450,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) @@ -3475,6 +3474,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 b207a728c7b2bd227252f3408d07ccca9f43fcc9 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 4 Feb 2014 15:56:08 -0500 Subject: [PATCH 13/69] 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 9266447b0de925ea3b6c1d4b62dcfa19e177bd5a Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 6 Feb 2014 09:38:29 -0500 Subject: [PATCH 14/69] classmethods for default tools modification --- examples/user_interfaces/navigation.py | 10 +++++----- lib/matplotlib/backend_bases.py | 22 ++++++++++++++++------ lib/matplotlib/backend_tools.py | 2 +- 3 files changed, 22 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 64213befb2bf..2d683df86e46 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3287,10 +3287,20 @@ 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 - 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 @@ -3324,7 +3334,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): @@ -3340,7 +3350,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] @@ -3385,7 +3395,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] @@ -3434,7 +3444,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] @@ -3533,7 +3543,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 a53419aafe1ae4452f074e53393f832874647a2f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 1 May 2014 19:12:56 -0400 Subject: [PATCH 15/69] 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 2d683df86e46..f1a95166ab73 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3236,6 +3236,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, @@ -3255,6 +3256,7 @@ class NavigationBase(object): def __init__(self, canvas, toolbar=None): """.. automethod:: _toolbar_callback""" + self.canvas = canvas self.toolbar = self._get_toolbar(toolbar, canvas) @@ -3273,9 +3275,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: @@ -3290,11 +3292,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): @@ -3312,6 +3316,7 @@ def active_toggle(self): **string** : Currently toggled tool, or None """ + return self._toggled @property @@ -3320,6 +3325,7 @@ def instances(self): **dictionary** : Contains the active instances that are registered """ + return self._instances def get_tool_keymap(self, name): @@ -3334,6 +3340,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 @@ -3380,6 +3387,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: @@ -3393,6 +3401,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] @@ -3410,6 +3419,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 @@ -3445,7 +3455,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] @@ -3462,6 +3472,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): @@ -3475,8 +3486,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): @@ -3491,7 +3501,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] @@ -3508,24 +3518,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 @@ -3537,6 +3548,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')) @@ -3550,6 +3562,7 @@ def list_tools(self): def update(self): """Reset the axes stack""" + self.views.clear() self.positions.clear() # self.set_history_buttons() @@ -3585,9 +3598,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()) @@ -3595,6 +3610,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() @@ -3608,6 +3626,7 @@ def set_cursor(self, cursor): Set the current cursor to one of the :class:`Cursors` enums values """ + pass def update_view(self): @@ -3633,6 +3652,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(): @@ -3659,6 +3679,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) @@ -3673,6 +3694,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) @@ -3684,12 +3706,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, @@ -3717,6 +3741,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): @@ -3728,10 +3753,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): @@ -3746,7 +3773,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): @@ -3758,6 +3786,7 @@ def _remove_toolitem(self, name): Name of the tool to remove """ + raise NotImplementedError def move_toolitem(self, pos_ini, pos_fin): @@ -3770,6 +3799,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): @@ -3783,4 +3813,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 704c717e02526bac9d0af397c5749763d93e4780 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 08:48:51 -0400 Subject: [PATCH 16/69] 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 f1a95166ab73..d05757f0b0d5 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3618,9 +3618,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 f1bb1f9f9857..b45ab95515c9 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -743,10 +743,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 50567298326effe8ae3ae72a5a1d27988d374629 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 12:22:28 -0400 Subject: [PATCH 17/69] 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 | 194 ++++++++++++++++++++++- 4 files changed, 207 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 d05757f0b0d5..e6781db59411 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3421,6 +3421,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: @@ -3442,9 +3445,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 f652d13412be..7c7acc13bc75 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,23 @@ 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 +886,180 @@ 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): + 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 e6a4e1e282a1f36dd44769fc29f5520ea73282fa Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 6 May 2014 13:56:47 -0400 Subject: [PATCH 18/69] example working with Tk --- examples/user_interfaces/navigation.py | 54 ++++++----- 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, 148 insertions(+), 173 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 705e493919ef..b1f91e7886d6 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -1,33 +1,40 @@ 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 = '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() - - -#A simple example of copy canvas -#ref: at https://github.com/matplotlib/matplotlib/issues/1987 -class CopyTool(ToolBase): + 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' - position = -1 + # It is not added to the toolbar as a button + intoolbar = False def trigger(self, event): from gi.repository import Gtk, Gdk @@ -41,13 +48,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('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') +# 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 e6781db59411..e5ef12ec2c3d 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3230,7 +3230,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 @@ -3238,33 +3238,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() @@ -3280,36 +3266,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 @@ -3381,8 +3346,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. @@ -3411,29 +3375,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 @@ -3444,7 +3406,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') @@ -3453,10 +3415,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): @@ -3505,7 +3468,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 @@ -3551,26 +3514,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: @@ -3667,7 +3627,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 @@ -3719,7 +3678,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, @@ -3776,7 +3735,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 b45ab95515c9..a7380792f33a 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -415,8 +415,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) @@ -472,19 +473,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() @@ -722,8 +727,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 @@ -775,7 +780,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: @@ -787,6 +792,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) @@ -860,14 +867,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: @@ -1107,6 +1114,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 7c7acc13bc75..0e1a66bd5b18 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 8942c47bcb9883da5107733798d9a8b48e3126f0 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Jul 2014 09:45:26 -0400 Subject: [PATCH 19/69] 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 e5ef12ec2c3d..6f188d9ac4d4 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3400,11 +3400,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 022de6fc0d478d4aa7b93341c7d31c930cd5c4ce Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Jul 2014 13:37:09 -0400 Subject: [PATCH 20/69] 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 a7380792f33a..a441179e419f 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -485,7 +485,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 @@ -749,7 +749,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 2c9a1951e59afbf7d1f164688c2133d586593f85 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 24 Jul 2014 16:52:25 -0400 Subject: [PATCH 21/69] 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 6f188d9ac4d4..8f90478109dd 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3252,10 +3252,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 = {} @@ -3522,12 +3518,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: @@ -3557,28 +3547,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` @@ -3587,43 +3555,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 a441179e419f..91688cfc7f29 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 @@ -441,7 +442,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) @@ -761,9 +762,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 0e1a66bd5b18..83e8dc6e97f3 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 cafe668d41d8c09053f9817225925d202c6b95f1 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 25 Jul 2014 10:31:03 -0400 Subject: [PATCH 22/69] 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 91688cfc7f29..26aa57d55396 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 @@ -442,7 +441,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 83e8dc6e97f3..c57ea9bc0378 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 224f745020e944d1a442866a6fc6cabec8244a99 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 25 Jul 2014 10:59:04 -0400 Subject: [PATCH 23/69] 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 26aa57d55396..22f1839053b8 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -488,6 +488,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 c57ea9bc0378..b515450c2020 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 94c711e9b607decf5c4eef6e4f6062b2e780eba6 Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Mon, 28 Jul 2014 19:40:38 +0200 Subject: [PATCH 24/69] 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 8f90478109dd..e7390c54770b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3262,13 +3262,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 @@ -3371,6 +3364,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 :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): """Add tool to `Navigation` diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 1eef95867743..79165e5015e8 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 67257e72f92a29586fb2cde72c6fe35b2782b708 Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Tue, 29 Jul 2014 16:06:38 +0200 Subject: [PATCH 25/69] Moved default_tool initilisation to FigureManagerBase and cleaned. --- lib/matplotlib/backend_bases.py | 10 ++++++++++ lib/matplotlib/backends/backend_gtk3.py | 3 --- lib/matplotlib/backends/backend_tkagg.py | 3 +-- lib/matplotlib/pyplot.py | 4 ---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index e7390c54770b..0733551fcb00 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2595,6 +2595,11 @@ 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. @@ -2642,6 +2647,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 22f1839053b8..e05fb7b4abce 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -416,9 +416,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 b515450c2020..bf1279f9e67c 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 79165e5015e8..1eef95867743 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 ffa65d61f961295dac81ece323726f120beea273 Mon Sep 17 00:00:00 2001 From: Ocean Wolf Date: Tue, 29 Jul 2014 19:10:55 +0200 Subject: [PATCH 26/69] 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 0733551fcb00..e7390c54770b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2595,11 +2595,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. @@ -2647,11 +2642,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 e05fb7b4abce..5532c84bb543 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 @@ -416,6 +416,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 bf1279f9e67c..76d084cd1fb0 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 6739ee03d23dd70200c691f0f19c464ad55bf515 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 3 Sep 2014 10:43:32 -0400 Subject: [PATCH 27/69] 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 e7390c54770b..521cc3a500a0 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3254,7 +3254,6 @@ def __init__(self, manager): self._tools = {} self._keys = {} - self._instances = {} self._toggled = None # to process keypress event @@ -3273,15 +3272,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 @@ -3323,28 +3313,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` @@ -3356,7 +3337,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] @@ -3403,7 +3384,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) @@ -3436,27 +3417,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(): @@ -3467,14 +3444,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` @@ -3495,7 +3464,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 @@ -3507,10 +3476,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) @@ -3534,7 +3503,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 5532c84bb543..c7d35f772456 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -890,10 +890,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 @@ -901,12 +906,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) @@ -917,17 +922,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 76d084cd1fb0..58719b18087e 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 d18206ff1f82769edf790de2fa83898650c043b5 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 4 Sep 2014 16:53:16 -0400 Subject: [PATCH 28/69] 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 521cc3a500a0..2f1a0c4552c0 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3288,6 +3288,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 @@ -3301,9 +3306,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): @@ -3312,21 +3315,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` @@ -3336,15 +3324,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` @@ -3491,7 +3483,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 34a52c860993977df62e6919a817d0c8dd63234a Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 5 Sep 2014 10:21:40 -0400 Subject: [PATCH 29/69] 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 2f1a0c4552c0..ca4a374ddde2 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3476,6 +3476,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`""" @@ -3488,7 +3491,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) @@ -3500,6 +3506,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 c2da483b70a08fc3f19a599517255ca76179d85e Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 15 Oct 2014 19:50:39 -0400 Subject: [PATCH 30/69] 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 44a9b0e2d68fbaa370a45d9fee6945eba94fcf67 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 16 Oct 2014 17:33:25 -0400 Subject: [PATCH 31/69] 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 ca4a374ddde2..7c41359de0db 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3225,13 +3225,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 @@ -3240,11 +3265,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) @@ -3258,11 +3281,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 @@ -3328,12 +3417,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] @@ -3346,14 +3434,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 @@ -3380,20 +3466,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): @@ -3414,18 +3487,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(): @@ -3434,50 +3496,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`""" @@ -3495,6 +3515,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) @@ -3509,9 +3532,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: @@ -3520,15 +3545,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 """ @@ -3575,33 +3598,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) @@ -3611,41 +3693,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 @@ -3656,30 +3709,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 c7d35f772456..5f29c56c9d0b 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -419,6 +419,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 @@ -783,8 +784,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: @@ -798,34 +802,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 @@ -838,20 +837,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 a2ed47febfc8ea548f8ba9a5ed1f9491a2ce2a5e Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 17 Oct 2014 14:12:51 -0400 Subject: [PATCH 32/69] 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 7c41359de0db..b733c082bb77 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 @@ -3225,32 +3226,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 @@ -3262,95 +3237,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): @@ -3421,7 +3329,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] @@ -3448,6 +3358,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 """ @@ -3466,9 +3378,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(): @@ -3487,7 +3429,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(): @@ -3496,8 +3440,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`""" @@ -3512,43 +3455,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` @@ -3599,12 +3523,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') @@ -3614,33 +3579,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 ---------- @@ -3648,22 +3591,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 5f29c56c9d0b..fccf6920b40d 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -419,7 +419,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 0665890ca74f9433fd2c96fece0aaddf729b200a Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 20 Oct 2014 14:50:43 -0400 Subject: [PATCH 33/69] 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 b733c082bb77..af397e8c7198 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 @@ -3226,10 +3225,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 @@ -3247,18 +3270,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): @@ -3329,9 +3385,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] @@ -3352,6 +3408,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 @@ -3374,40 +3435,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 @@ -3424,14 +3483,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(): @@ -3440,7 +3532,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`""" @@ -3454,63 +3546,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): @@ -3523,50 +3567,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""" @@ -3579,11 +3617,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 ---------- @@ -3591,8 +3626,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 @@ -3627,6 +3661,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 fccf6920b40d..263a2ddd22ca 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 @@ -416,8 +417,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) @@ -444,7 +445,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() @@ -720,26 +722,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) @@ -752,6 +752,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): @@ -849,7 +851,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() @@ -868,7 +870,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): @@ -919,12 +928,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 411e6e22a7052634402c94a19df3fc22d602519d Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 20 Oct 2014 17:39:59 -0400 Subject: [PATCH 34/69] 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 af397e8c7198..2ab8583ac5f7 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3546,6 +3546,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 d484ebdde7c74ea64bda4926b7b781b24fb860e0 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 27 Oct 2014 15:25:56 -0400 Subject: [PATCH 35/69] moving set_cursor completely out of navigation --- examples/user_interfaces/navigation.py | 25 +++-- lib/matplotlib/backend_bases.py | 136 ++++++++++++----------- lib/matplotlib/backend_tools.py | 148 +++++++++++++++---------- 3 files changed, 177 insertions(+), 132 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index f4daa9cf3317..bec3e57e2efa 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -4,30 +4,31 @@ 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): - tools = self.navigation.get_tools() - print ('_' * 80) print ("{0:12} {1:45} {2}".format('Name (id)', 'Tool description', 'Keymap')) - print ('_' * 80) + 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) - - + print ('_' * 80) + + # A simple example of copy canvas # ref: at https://github.com/matplotlib/matplotlib/issues/1987 class CopyToolGTK3(ToolBase): @@ -35,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) @@ -52,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') diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 2ab8583ac5f7..02ec5d41dd2b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3234,9 +3234,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 @@ -3254,10 +3254,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): @@ -3270,7 +3270,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() @@ -3279,22 +3279,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 @@ -3305,16 +3307,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): @@ -3369,7 +3371,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 ---------- @@ -3387,7 +3389,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] @@ -3396,9 +3398,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: @@ -3406,23 +3411,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) @@ -3447,7 +3453,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 @@ -3466,8 +3472,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): @@ -3485,7 +3489,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 ---------- @@ -3493,9 +3497,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: @@ -3508,8 +3512,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, name, sender, canvasevent, data) - self.callbacks.process(s, event) + 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): """Trigger on a tool @@ -3534,45 +3538,31 @@ 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 - Parameters: + Parameters ----------- - name: String + name : String Name of the tool """ 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 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): @@ -3596,12 +3586,12 @@ 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""" 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, @@ -3641,6 +3631,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)` @@ -3653,31 +3645,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 30162ad1d072..ccc67ae689f1 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 @@ -143,7 +164,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 `ToolToggleBase` derived tools, and calls + set_cursor 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 +176,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 +218,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 @@ -180,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 @@ -206,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: @@ -303,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 @@ -323,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 @@ -339,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): @@ -354,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: @@ -433,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 @@ -457,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' @@ -466,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' @@ -499,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 @@ -508,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) @@ -515,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) @@ -801,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')]], @@ -841,6 +879,4 @@ def _mouse_move(self, event): (ToolViewsPositions, 'viewpos'), ('ToolSetCursor', 'cursor'), ('ToolRubberband', 'rubberband')]]] - - """Default tools""" From 75bf97b27dab7b6f97a8d80d88ecae55a1d37862 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 10 Nov 2014 10:37:02 -0500 Subject: [PATCH 36/69] 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 02ec5d41dd2b..7f8a0610eaee 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3226,27 +3226,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): @@ -3303,7 +3314,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) """ @@ -3315,7 +3326,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 @@ -3388,7 +3399,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] @@ -3449,28 +3460,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 @@ -3512,7 +3523,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): @@ -3523,7 +3535,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 6cc040b74b9b6f652269b9f091ad126fcabcb8e9 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 10 Nov 2014 10:45:33 -0500 Subject: [PATCH 37/69] 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 7f8a0610eaee..8cedd6edb3fb 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3522,7 +3522,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) @@ -3611,7 +3611,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 0ff5997832981d70273fc6114664e3d1f937ebe6 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 14 Nov 2014 14:19:25 -0500 Subject: [PATCH 38/69] 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 8cedd6edb3fb..b1c6c674de2a 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3280,7 +3280,7 @@ def __init__(self, manager): self._tools = {} self._keys = {} - self._toggled = None + self._toggled = {} self._callbacks = cbook.CallbackRegistry() # to process keypress event @@ -3333,7 +3333,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 @@ -3393,7 +3393,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) @@ -3456,6 +3457,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): @@ -3470,16 +3480,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 af6734f278d3ff0dafed7c2bec38cfdac3766de0 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 28 Nov 2014 15:33:23 -0500 Subject: [PATCH 39/69] scroll to zoom in zoom/pan tools --- lib/matplotlib/backend_tools.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index cabee9acf6e4..3eb51565dd3b 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,40 @@ 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 + # 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 78513d24a9f9895ece7affacd56432b4f2b7fec8 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 5 Dec 2014 10:25:48 -0500 Subject: [PATCH 40/69] remove ToolAddedEvent incorporating the functionality into toolevent --- lib/matplotlib/backend_bases.py | 77 +++++++++++++++------------------ lib/matplotlib/backend_tools.py | 19 ++++---- 2 files changed, 43 insertions(+), 53 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index b1c6c674de2a..ef4360af9667 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3227,32 +3227,24 @@ 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): - """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 @@ -3339,7 +3331,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 ---------- @@ -3360,13 +3352,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: @@ -3440,7 +3432,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) @@ -3457,7 +3449,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 @@ -3470,15 +3462,15 @@ 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): - # 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 @@ -3490,8 +3482,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 @@ -3536,7 +3527,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 @@ -3567,7 +3558,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): @@ -3616,13 +3607,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 @@ -3630,12 +3621,12 @@ 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, - event.group, - event.position, + event.data['group'], + event.data['position'], image, event.tool.description, toggle) @@ -3644,11 +3635,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') @@ -3664,7 +3655,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) @@ -3672,7 +3663,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)` @@ -3680,12 +3671,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 @@ -3723,9 +3714,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 377ff54cd0a960f8d640f2d26cab9c6d58dd7c9f Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 5 Jan 2015 11:30:48 -0500 Subject: [PATCH 41/69] eliminating repeated loop --- lib/matplotlib/backend_bases.py | 10 ++++++---- lib/matplotlib/backend_tools.py | 2 +- lib/matplotlib/backends/backend_gtk3.py | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index ef4360af9667..db2738de37c8 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3292,9 +3292,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) @@ -3347,8 +3350,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/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 diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 263a2ddd22ca..8e02f99eaecd 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -803,6 +803,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 7dbbf58065f4c5ac952ecf7f8bc365afba9f736a Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 21 Jan 2015 15:13:46 -0500 Subject: [PATCH 42/69] 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 dd66b576fd958f506c557d9e66cf77bab4235a75 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 21 Jan 2015 15:31:56 -0500 Subject: [PATCH 43/69] rename mpl_connect --- examples/user_interfaces/navigation.py | 2 -- lib/matplotlib/backend_bases.py | 16 ++++++++-------- lib/matplotlib/backend_tools.py | 4 ++-- 3 files changed, 10 insertions(+), 12 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 diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index db2738de37c8..10eb15be3099 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3279,7 +3279,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 @@ -3304,14 +3304,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) @@ -3603,9 +3603,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): @@ -3633,7 +3633,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 67a414fcbec3b2465a224a3c919ad76b4be7c3d8 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 4 Feb 2015 12:03:36 -0500 Subject: [PATCH 44/69] 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 10eb15be3099..3d2a2635209d 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3263,9 +3263,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) @@ -3599,9 +3598,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 8e02f99eaecd..05333d0f5822 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -483,7 +483,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 @@ -491,7 +491,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 @@ -756,8 +756,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 e415d8d02e95661807b40217dcdd886ae9a57bbd Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Wed, 11 Feb 2015 15:16:09 +0100 Subject: [PATCH 45/69] 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 3d2a2635209d..2ce1b5b80cda 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3383,7 +3383,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 @@ -3534,8 +3534,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: @@ -3544,8 +3544,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): @@ -3553,7 +3552,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) @@ -3578,13 +3577,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 12130867bc7623c6f775b61d56dec948919e3a6c Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Thu, 12 Feb 2015 22:01:04 +0100 Subject: [PATCH 46/69] Refactored Toolbar out of NavigationBase --- lib/matplotlib/backend_bases.py | 63 +++++++++++++------------ lib/matplotlib/backend_tools.py | 41 ++++++++-------- lib/matplotlib/backends/backend_gtk3.py | 3 +- 3 files changed, 54 insertions(+), 53 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 2ce1b5b80cda..884c84d6e79b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3405,17 +3405,15 @@ def add_tools(self, tools): ---------- 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 + [(Tool1, name1), (Tool2, name2) ...] + where Tool1, name1 represent the tool, and the respective name + of the tool which gets used as an id. """ - for group, grouptools in tools: - for position, tool in enumerate(grouptools): - self.add_tool(tool[1], tool[0], group, position) + for tool, name in tools: + self.add_tool(name, tool) - def add_tool(self, name, tool, group=None, position=None): + def add_tool(self, name, tool): """Add tool to `NavigationBase` Add a tool to the tools controlled by Navigation @@ -3430,10 +3428,6 @@ 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 `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 within its group in the toolbar, if None, it goes at the end """ tool_cls = self._get_cls_to_instantiate(tool) @@ -3459,14 +3453,11 @@ def add_tool(self, name, tool, group=None, position=None): else: self._toggled.setdefault(tool_cls.radio_group, None) - self._tool_added_event(self._tools[name], group, position) + self._tool_added_event(self._tools[name]) - def _tool_added_event(self, tool, group, position): + def _tool_added_event(self, tool): s = 'tool_added_event' - event = ToolEvent(s, - self, - tool, - data={'group': group, 'position': position}) + event = ToolEvent(s, self, tool) self._callbacks.process(s, event) def _handle_toggle(self, tool, sender, canvasevent, data): @@ -3606,7 +3597,6 @@ 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) self.navigation.nav_connect('tool_removed_event', self._remove_tool_cbk) @@ -3624,18 +3614,31 @@ def _tool_triggered_cbk(self, event): self.toggle_toolitem(event.tool.name) - def _add_tool_cbk(self, event): - """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, - event.data['group'], - event.data['position'], - image, - event.tool.description, - toggle) + def add_tools(self, tools): + """ Add multiple tools to `Navigation` + + Parameters + ---------- + tools : List + List in the form + [[group1, [name1, 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: + for position, tool in enumerate(grouptools): + self.add_tool(self.navigation.get_tool(tool), group, position) + + def add_tool(self, tool, group, position): + """Adds a tool to the toolbar""" + image = self._get_image_filename(tool.image) + toggle = getattr(tool, 'toggled', None) is not None + self.add_toolitem(tool.name, group, position, image, + tool.description, toggle) if toggle: - self.navigation.nav_connect('tool_trigger_%s' % event.tool.name, + self.navigation.nav_connect('tool_trigger_%s' % 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 97fbc0b680b3..0252657ce75f 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -903,26 +903,23 @@ def _mouse_move(self, event): self.navigation.canvas.draw_idle() -tools = [['navigation', [(ToolHome, 'home'), - (ToolBack, 'back'), - (ToolForward, 'forward')]], - - ['zoompan', [(ToolZoom, 'zoom'), - (ToolPan, 'pan')]], - - ['layout', [('ToolConfigureSubplots', 'subplots'), ]], - - ['io', [('ToolSaveFigure', 'save'), ]], - - [None, [(ToolGrid, 'grid'), - (ToolFullScreen, 'fullscreen'), - (ToolQuit, 'quit'), - (ToolEnableAllNavigation, 'allnav'), - (ToolEnableNavigation, 'nav'), - (ToolXScale, 'xscale'), - (ToolYScale, 'yscale'), - (ToolCursorPosition, 'position'), - (ToolViewsPositions, 'viewpos'), - ('ToolSetCursor', 'cursor'), - ('ToolRubberband', 'rubberband')]]] +tools = [(ToolHome, 'home'), (ToolBack, 'back'), (ToolForward, 'forward'), + (ToolZoom, 'zoom'), (ToolPan, 'pan'), + ('ToolConfigureSubplots', 'subplots'), + ('ToolSaveFigure', 'save'), + (ToolGrid, 'grid'), + (ToolFullScreen, 'fullscreen'), + (ToolQuit, 'quit'), + (ToolEnableAllNavigation, 'allnav'), + (ToolEnableNavigation, 'nav'), + (ToolXScale, 'xscale'), + (ToolYScale, 'yscale'), + (ToolCursorPosition, 'position'), + (ToolViewsPositions, 'viewpos'), + ('ToolSetCursor', 'cursor'), + ('ToolRubberband', 'rubberband')] +toolbar_tools = [['navigation', ['home', 'back', 'forward']], + ['zoompan', ['zoom', 'pan']], + ['layout', ['subplots']], + ['io', ['save']]] """Default tools""" diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 05333d0f5822..9003221d462c 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -32,7 +32,7 @@ def fn_name(): return sys._getframe(1).f_code.co_name FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase from matplotlib.backend_bases import ShowBase, ToolbarBase, NavigationBase from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, \ - tools, SetCursorBase, RubberbandBase + tools, toolbar_tools, SetCursorBase, RubberbandBase from matplotlib.cbook import is_string_like, is_writable_file_like from matplotlib.colors import colorConverter @@ -421,6 +421,7 @@ def __init__(self, canvas, num): self.toolbar = self._get_toolbar() if matplotlib.rcParams['toolbar'] == 'navigation': self.navigation.add_tools(tools) + self.toolbar.add_tools(toolbar_tools) # calculate size for window w = int (self.canvas.figure.bbox.width) From ba61dec6aad410ee6d97cd496e1a420ed2002e94 Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Mon, 16 Feb 2015 18:59:15 +0100 Subject: [PATCH 47/69] Some short cuts for adding tools --- lib/matplotlib/backend_bases.py | 22 ++++++++++++++++++++-- lib/matplotlib/backend_tools.py | 3 +++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 884c84d6e79b..30f33ee7e72a 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3440,7 +3440,13 @@ def add_tool(self, name, tool): 'not added') return - self._tools[name] = tool_cls(self, name) + if isinstance(tool_cls, type): + self._tools[name] = tool_cls(self, name) + else: + tool_cls.set_navigation(self) + tool.name = name + self._tools[name] = tool_cls + if tool_cls.keymap is not None: self.set_tool_keymap(name, tool_cls.keymap) @@ -3455,6 +3461,8 @@ def add_tool(self, name, tool): self._tool_added_event(self._tools[name]) + return self._tools[name] + def _tool_added_event(self, tool): s = 'tool_added_event' event = ToolEvent(s, self, tool) @@ -3629,10 +3637,20 @@ def add_tools(self, tools): for group, grouptools in tools: for position, tool in enumerate(grouptools): - self.add_tool(self.navigation.get_tool(tool), group, position) + self.add_tool(tool, group, position) def add_tool(self, tool, group, position): """Adds a tool to the toolbar""" + t = self.navigation.get_tool(tool) + if t is None: + if isinstance(tool, (list, tuple)): + t = self.navigation.add_tool(tool[0], tool[1]) + elif isinstance(tool, ToolBase): + t = self.navigation.add_tool(tool.name, tool) + else: + warning.warn('Cannot add tool %s'%tool) + return + tool = t image = self._get_image_filename(tool.image) toggle = getattr(tool, 'toggled', None) is not None self.add_toolitem(tool.name, group, position, image, diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 0252657ce75f..8900aa6e9439 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -65,6 +65,9 @@ class ToolBase(object): def __init__(self, navigation, name): self._name = name self.figure = None + self.set_navigation(navigation) + + def set_navigation(self, navigation): self.navigation = navigation self.set_figure(navigation.canvas.figure) From 9f2ee2b8cc5642fb79505a1da7bbf51a8dd9b43c Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Wed, 18 Feb 2015 16:17:04 +0100 Subject: [PATCH 48/69] Lots of fixes --- examples/user_interfaces/navigation.py | 2 +- lib/matplotlib/backend_bases.py | 56 ++++++++++++++----------- lib/matplotlib/backend_tools.py | 3 -- lib/matplotlib/backends/backend_gtk3.py | 22 ++++------ 4 files changed, 42 insertions(+), 41 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 8142082d98fd..4c4d320c176f 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -56,7 +56,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) - +fig.canvas.manager.toolbar.add_tool('zoom', 'foo') # Uncomment to remove the forward button # fig.canvas.manager.navigation.remove_tool('forward') diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 30f33ee7e72a..5926ae3694fc 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3413,7 +3413,7 @@ def add_tools(self, tools): for tool, name in tools: self.add_tool(name, tool) - def add_tool(self, name, tool): + def add_tool(self, name, tool, *args, **kwargs): """Add tool to `NavigationBase` Add a tool to the tools controlled by Navigation @@ -3428,6 +3428,10 @@ def add_tool(self, name, tool): Name of the tool, treated as the ID, has to be unique tool : string or `matplotlib.backend_tools.ToolBase` derived class Reference to find the class of the Tool to be added + + Notes + ----- + args and kwargs get passed directly to the tools constructor. """ tool_cls = self._get_cls_to_instantiate(tool) @@ -3440,12 +3444,7 @@ def add_tool(self, name, tool): 'not added') return - if isinstance(tool_cls, type): - self._tools[name] = tool_cls(self, name) - else: - tool_cls.set_navigation(self) - tool.name = name - self._tools[name] = tool_cls + self._tools[name] = tool_cls(self, name, *args, **kwargs) if tool_cls.keymap is not None: self.set_tool_keymap(name, tool_cls.keymap) @@ -3460,7 +3459,6 @@ def add_tool(self, name, tool): self._toggled.setdefault(tool_cls.radio_group, None) self._tool_added_event(self._tools[name]) - return self._tools[name] def _tool_added_event(self, tool): @@ -3583,7 +3581,7 @@ def get_tool(self, name): name : String, ToolBase Name of the tool, or the tool itself """ - if isinstance(name, tools.ToolBase): + if isinstance(name, tools.ToolBase) and tool.name in self._tools: return name if name not in self._tools: warnings.warn("%s is not a tool controlled by Navigation" % name) @@ -3612,15 +3610,12 @@ def _message_cbk(self, event): """Captures the 'tool_message_event' to set the message on the toolbar""" self.set_message(event.message) - def _tool_triggered_cbk(self, event): + def _tool_toggled_cbk(self, event): """Captures the 'tool-trigger-toolname This only gets used for toggled tools """ - if event.sender is self: - return - - self.toggle_toolitem(event.tool.name) + self.toggle_toolitem(event.tool.name, event.tool.toggled) def add_tools(self, tools): """ Add multiple tools to `Navigation` @@ -3639,17 +3634,30 @@ def add_tools(self, tools): for position, tool in enumerate(grouptools): self.add_tool(tool, group, position) - def add_tool(self, tool, group, position): - """Adds a tool to the toolbar""" + def add_tool(self, tool, group, position=-1, name=None, **kwargs): + """Adds a tool to the toolbar + + Parameters + ---------- + tool : string, tool + The name or the type of tool to add. + group : string + The name of the group to add this tool to. + position : int + the relative position within the group to place this tool. + name : string (optional) + If given, and the above fails, we use this to create a new tool of + type given by tool, and use this as the name of the tool. + """ t = self.navigation.get_tool(tool) if t is None: - if isinstance(tool, (list, tuple)): - t = self.navigation.add_tool(tool[0], tool[1]) - elif isinstance(tool, ToolBase): - t = self.navigation.add_tool(tool.name, tool) - else: - warning.warn('Cannot add tool %s'%tool) - return + if isinstance(tool, type): + tool = tool.__class__ + if name is not None: + t = self.navigation.add_tool(name, tool, **kwargs) + if t is None: + warning.warn('Cannot add tool %s'%tool) + return tool = t image = self._get_image_filename(tool.image) toggle = getattr(tool, 'toggled', None) is not None @@ -3657,7 +3665,7 @@ def add_tool(self, tool, group, position): tool.description, toggle) if toggle: self.navigation.nav_connect('tool_trigger_%s' % tool.name, - self._tool_triggered_cbk) + self._tool_toggled_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 8900aa6e9439..0252657ce75f 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -65,9 +65,6 @@ class ToolBase(object): def __init__(self, navigation, name): self._name = name self.figure = None - self.set_navigation(navigation) - - def set_navigation(self, navigation): self.navigation = navigation self.set_figure(navigation.canvas.figure) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 9003221d462c..cfdcc95d2984 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -767,7 +767,6 @@ def __init__(self, navigation): self.pack_start(self._toolbar, False, False, 0) self._toolbar.show_all() self._toolitems = {} - self._signals = {} self._setup_message_area() def _setup_message_area(self): @@ -788,9 +787,6 @@ def _setup_message_area(self): def add_toolitem(self, name, group, position, image_file, description, toggle): - if group is None: - return - if toggle: tbutton = Gtk.ToggleToolButton() else: @@ -809,8 +805,8 @@ def add_toolitem(self, name, group, position, image_file, description, signal = tbutton.connect('clicked', self._call_tool, name) tbutton.set_tooltip_text(description) tbutton.show_all() - self._toolitems[name] = tbutton - self._signals[name] = signal + self._toolitems.setdefault(name, []) + self._toolitems[name].append((tbutton, signal)) def _call_tool(self, btn, name): self.trigger_tool(name) @@ -818,20 +814,20 @@ def _call_tool(self, btn, name): def set_message(self, s): self.message.set_label(s) - def toggle_toolitem(self, name): + def toggle_toolitem(self, name, toggled): if name not in self._toolitems: return - - status = self._toolitems[name].get_active() - self._toolitems[name].handler_block(self._signals[name]) - self._toolitems[name].set_active(not status) - self._toolitems[name].handler_unblock(self._signals[name]) + for toolitem, signal in self._toolitems[name]: + toolitem.handler_block(signal) + toolitem.set_active(toggled) + toolitem.handler_unblock(signal) def remove_toolitem(self, name): if name not in self._toolitems: self.set_message('%s Not in toolbar' % name) return - self._toolbar.remove(self._toolitems[name]) + for toolitem, signal in self._toolitems[name]: + self._toolbar.remove(toolitem) del self._toolitems[name] def add_separator(self, pos=-1): From 9da2b1324d2304aba39d98a95c0dfe4376c7bc63 Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Wed, 18 Feb 2015 17:01:09 +0100 Subject: [PATCH 49/69] Rename ToolbarBase -> ToolContainerBase --- lib/matplotlib/backend_bases.py | 101 +++++++++++------------- lib/matplotlib/backend_tools.py | 34 ++++---- lib/matplotlib/backends/backend_gtk3.py | 6 +- 3 files changed, 68 insertions(+), 73 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 5926ae3694fc..71d29d880ee3 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -30,7 +30,7 @@ user interaction (key press, toolbar clicks, ..) and the actions in response to the user inputs. -:class:`ToolbarBase` +:class:`ToolContainerBase` The base class for the Toolbar class of each interactive backend. """ @@ -3399,18 +3399,16 @@ def remove_tool(self, name): del self._tools[name] def add_tools(self, tools): - """ Add multiple tools to `Navigation` + """ Add multiple tools to `NavigationBase` Parameters ---------- - tools : List - List in the form - [(Tool1, name1), (Tool2, name2) ...] - where Tool1, name1 represent the tool, and the respective name - of the tool which gets used as an id. + tools : {str: class_like} + The tools to add in a {name: tool} dict, see `add_tool` for more + info. """ - for tool, name in tools: + for name, tool in six.iteritems(tools): self.add_tool(name, tool) def add_tool(self, name, tool, *args, **kwargs): @@ -3424,14 +3422,18 @@ def add_tool(self, name, tool, *args, **kwargs): Parameters ---------- - name : string + name : str Name of the tool, treated as the ID, has to be unique - tool : string or `matplotlib.backend_tools.ToolBase` derived class - Reference to find the class of the Tool to be added + tool : class_like, i.e. str or type + Reference to find the class of the Tool to added. Notes ----- args and kwargs get passed directly to the tools constructor. + + See Also + -------- + matplotlib.backend_tools.ToolBase : The base class for tools. """ tool_cls = self._get_cls_to_instantiate(tool) @@ -3442,7 +3444,7 @@ def add_tool(self, name, tool, *args, **kwargs): if name in self._tools: warnings.warn('A tool_cls with the same name already exist, ' 'not added') - return + return self._tools[name] self._tools[name] = tool_cls(self, name, *args, **kwargs) @@ -3573,30 +3575,32 @@ def tools(self): return self._tools - def get_tool(self, name): + def get_tool(self, name, warn=True): """Return the tool object, also accepts the actual tool for convenience Parameters ----------- - name : String, ToolBase + name : str, ToolBase Name of the tool, or the tool itself + warn : bool + If this method should give warnings. """ - if isinstance(name, tools.ToolBase) and tool.name in self._tools: + if isinstance(name, tools.ToolBase) and name.name in self._tools: return name if name not in self._tools: - warnings.warn("%s is not a tool controlled by Navigation" % name) + if warn: + warnings.warn("Navigation does not control tool %s" % name) return None return self._tools[name] -class ToolbarBase(object): - """Base class for `Toolbar` implementation +class ToolContainerBase(object): + """Base class for all tool containers, e.g. toolbars. Attributes ---------- - manager : `FigureManager` object that integrates this `Toolbar` - navigation : `NavigationBase` object that hold the tools that - this `Toolbar` wants to communicate with + navigation : `NavigationBase` object that holds the tools that + this `ToolContainer` wants to communicate with. """ def __init__(self, navigation): @@ -3618,51 +3622,38 @@ def _tool_toggled_cbk(self, event): self.toggle_toolitem(event.tool.name, event.tool.toggled) def add_tools(self, tools): - """ Add multiple tools to `Navigation` + """ Add multiple tools to the container. Parameters ---------- - tools : List + tools : list List in the form - [[group1, [name1, 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 + [[group1, [tool1, tool2 ...]], [group2, [...]]] + Where the tools given by tool1, and tool2 will display in group1. + See `add_tool` for details. """ for group, grouptools in tools: for position, tool in enumerate(grouptools): self.add_tool(tool, group, position) - def add_tool(self, tool, group, position=-1, name=None, **kwargs): - """Adds a tool to the toolbar + def add_tool(self, tool, group, position=-1): + """Adds a tool to this container Parameters ---------- - tool : string, tool - The name or the type of tool to add. - group : string + tool : tool_like + The tool to add, see `NavigationBase.get_tool`. + group : str The name of the group to add this tool to. - position : int - the relative position within the group to place this tool. - name : string (optional) - If given, and the above fails, we use this to create a new tool of - type given by tool, and use this as the name of the tool. - """ - t = self.navigation.get_tool(tool) - if t is None: - if isinstance(tool, type): - tool = tool.__class__ - if name is not None: - t = self.navigation.add_tool(name, tool, **kwargs) - if t is None: - warning.warn('Cannot add tool %s'%tool) - return - tool = t + position : int (optional) + The position within the group to place this tool. Defaults to end. + """ + tool = self.navigation.get_tool(tool) image = self._get_image_filename(tool.image) toggle = getattr(tool, 'toggled', None) is not None - self.add_toolitem(tool.name, group, position, image, - tool.description, toggle) + self.add_toolitem(tool.name, group, position, + image, tool.description, toggle) if toggle: self.navigation.nav_connect('tool_trigger_%s' % tool.name, self._tool_toggled_cbk) @@ -3688,13 +3679,13 @@ def trigger_tool(self, name): Parameters ---------- name : String - Name(id) of the tool triggered from within the toolbar + Name(id) of the tool triggered from within the container """ self.navigation.tool_trigger_event(name, sender=self) def add_toolitem(self, name, group, position, image, description, toggle): - """Add a toolitem to the toolbar + """Add a toolitem to the container This method must get implemented per backend @@ -3734,18 +3725,20 @@ def set_message(self, s): pass - def toggle_toolitem(self, name): + def toggle_toolitem(self, name, toggled): """Toggle the toolitem without firing event Parameters ---------- name : String Id of the tool to toggle + toggled : bool + Whether to set this tool as toggled or not. """ raise NotImplementedError def remove_toolitem(self, name): - """Remove a toolitem from the `Toolbar` + """Remove a toolitem from the `ToolContainer` This method must get implemented per backend diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 0252657ce75f..16cd67f374f0 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -903,23 +903,25 @@ def _mouse_move(self, event): self.navigation.canvas.draw_idle() -tools = [(ToolHome, 'home'), (ToolBack, 'back'), (ToolForward, 'forward'), - (ToolZoom, 'zoom'), (ToolPan, 'pan'), - ('ToolConfigureSubplots', 'subplots'), - ('ToolSaveFigure', 'save'), - (ToolGrid, 'grid'), - (ToolFullScreen, 'fullscreen'), - (ToolQuit, 'quit'), - (ToolEnableAllNavigation, 'allnav'), - (ToolEnableNavigation, 'nav'), - (ToolXScale, 'xscale'), - (ToolYScale, 'yscale'), - (ToolCursorPosition, 'position'), - (ToolViewsPositions, 'viewpos'), - ('ToolSetCursor', 'cursor'), - ('ToolRubberband', 'rubberband')] +tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward, + 'zoom': ToolZoom, 'pan': ToolPan, + 'subplots': 'ToolConfigureSubplots', + 'save': 'ToolSaveFigure', + 'grid': ToolGrid, + 'fullscreen': ToolFullScreen, + 'quit': ToolQuit, + 'allnav': ToolEnableAllNavigation, + 'nav': ToolEnableNavigation, + 'xscale': ToolXScale, + 'yscale': ToolYScale, + 'position': ToolCursorPosition, + 'viewpos': ToolViewsPositions, + 'cursor': 'ToolSetCursor', + 'rubberband': 'ToolRubberband'} +"""Default tools""" + toolbar_tools = [['navigation', ['home', 'back', 'forward']], ['zoompan', ['zoom', 'pan']], ['layout', ['subplots']], ['io', ['save']]] -"""Default tools""" +"""Default tools in the toolbar""" diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index cfdcc95d2984..1aeef13777c0 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -30,7 +30,7 @@ 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, ToolbarBase, NavigationBase +from matplotlib.backend_bases import ShowBase, ToolContainerBase, NavigationBase from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, \ tools, toolbar_tools, SetCursorBase, RubberbandBase @@ -756,9 +756,9 @@ def draw_rubberband(self, x0, y0, x1, y1): ToolRubberband = RubberbandGTK3 -class ToolbarGTK3(ToolbarBase, Gtk.Box): +class ToolbarGTK3(ToolContainerBase, Gtk.Box): def __init__(self, navigation): - ToolbarBase.__init__(self, navigation) + ToolContainerBase.__init__(self, navigation) Gtk.Box.__init__(self) self.set_property("orientation", Gtk.Orientation.VERTICAL) From 110253f5419fac6dbf56e808773b858de035f03c Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Fri, 20 Feb 2015 16:46:03 +0100 Subject: [PATCH 50/69] Statusbar --- lib/matplotlib/backend_bases.py | 41 +++++++++------- lib/matplotlib/backend_tools.py | 6 +-- lib/matplotlib/backends/backend_gtk3.py | 64 ++++++++++++------------- 3 files changed, 59 insertions(+), 52 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 71d29d880ee3..b20fcf715d6c 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -33,6 +33,8 @@ :class:`ToolContainerBase` The base class for the Toolbar class of each interactive backend. +:class:`StatusbarBase` + The base class for the messaging area. """ from __future__ import (absolute_import, division, print_function, @@ -3605,15 +3607,9 @@ class ToolContainerBase(object): def __init__(self, navigation): self.navigation = navigation - - self.navigation.nav_connect('tool_message_event', self._message_cbk) self.navigation.nav_connect('tool_removed_event', self._remove_tool_cbk) - def _message_cbk(self, event): - """Captures the 'tool_message_event' to set the message on the toolbar""" - self.set_message(event.message) - def _tool_toggled_cbk(self, event): """Captures the 'tool-trigger-toolname @@ -3714,17 +3710,6 @@ def add_toolitem(self, name, group, position, image, description, toggle): raise NotImplementedError - def set_message(self, s): - """Display a message on toolbar or in status bar - - Parameters - ---------- - s : String - Message text - """ - - pass - def toggle_toolitem(self, name, toggled): """Toggle the toolitem without firing event @@ -3752,3 +3737,25 @@ def remove_toolitem(self, name): """ raise NotImplementedError + + +class StatusbarBase(object): + """Base class for the statusbar""" + def __init__(self, navigation): + self.navigation = navigation + self.navigation.nav_connect('tool_message_event', self._message_cbk) + + def _message_cbk(self, event): + """Captures the 'tool_message_event' and set the message""" + self.set_message(event.message) + + def set_message(self, s): + """Display a message on toolbar or in status bar + + Parameters + ---------- + s : str + Message text + """ + + pass diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 16cd67f374f0..1904af98c032 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -921,7 +921,7 @@ def _mouse_move(self, event): """Default tools""" toolbar_tools = [['navigation', ['home', 'back', 'forward']], - ['zoompan', ['zoom', 'pan']], - ['layout', ['subplots']], - ['io', ['save']]] + ['zoompan', ['zoom', 'pan']], + ['layout', ['subplots']], + ['io', ['save']]] """Default tools in the toolbar""" diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 1aeef13777c0..973b9bab61ed 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -30,9 +30,10 @@ 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, ToolContainerBase, NavigationBase -from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, \ - tools, toolbar_tools, SetCursorBase, RubberbandBase +from matplotlib.backend_bases import (ShowBase, ToolContainerBase, + NavigationBase, StatusbarBase) +from matplotlib.backend_tools import (SaveFigureBase, ConfigureSubplotsBase, + tools, toolbar_tools, SetCursorBase, RubberbandBase) from matplotlib.cbook import is_string_like, is_writable_file_like from matplotlib.colors import colorConverter @@ -416,22 +417,30 @@ def __init__(self, canvas, num): self.canvas.show() self.vbox.pack_start(self.canvas, True, True, 0) + # calculate size for window + w = int (self.canvas.figure.bbox.width) + h = int (self.canvas.figure.bbox.height) self.navigation = self._get_navigation() self.toolbar = self._get_toolbar() + self.statusbar = None + + def add_widget(child, expand, fill, padding): + child.show() + self.vbox.pack_end(child, False, False, 0) + size_request = child.size_request() + return size_request.height + if matplotlib.rcParams['toolbar'] == 'navigation': self.navigation.add_tools(tools) self.toolbar.add_tools(toolbar_tools) - - # calculate size for window - w = int (self.canvas.figure.bbox.width) - h = int (self.canvas.figure.bbox.height) + self.statusbar = StatusbarGTK3(self.navigation) + h += add_widget(self.statusbar, False, False, 0) + h += add_widget(Gtk.HSeparator(), False, False, 0) if self.toolbar is not None: self.toolbar.show() - self.vbox.pack_end(self.toolbar, False, False, 0) - size_request = self.toolbar.size_request() - h += size_request.height + h += add_widget(self.toolbar, False, False, 0) self.window.set_default_size (w, h) @@ -767,23 +776,6 @@ def __init__(self, navigation): self.pack_start(self._toolbar, False, False, 0) self._toolbar.show_all() self._toolitems = {} - self._setup_message_area() - - def _setup_message_area(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, group, position, image_file, description, toggle): @@ -811,9 +803,6 @@ def add_toolitem(self, name, group, position, image_file, description, def _call_tool(self, btn, name): self.trigger_tool(name) - def set_message(self, s): - self.message.set_label(s) - def toggle_toolitem(self, name, toggled): if name not in self._toolitems: return @@ -824,9 +813,9 @@ def toggle_toolitem(self, name, toggled): def remove_toolitem(self, name): if name not in self._toolitems: - self.set_message('%s Not in toolbar' % name) + self.navigation.message_event('%s Not in toolbar' % name, self) return - for toolitem, signal in self._toolitems[name]: + for toolitem, _signal in self._toolitems[name]: self._toolbar.remove(toolitem) del self._toolitems[name] @@ -837,6 +826,17 @@ def add_separator(self, pos=-1): return toolitem +class StatusbarGTK3(StatusbarBase, Gtk.Statusbar): + def __init__(self, *args, **kwargs): + StatusbarBase.__init__(self, *args, **kwargs) + Gtk.Statusbar.__init__(self) + self._context = self.get_context_id('message') + + def set_message(self, s): + self.pop(self._context) + self.push(self._context, s) + + class SaveFigureGTK3(SaveFigureBase): def get_filechooser(self): From e2804ea17361934a693a6ed964f96c8caaf39c64 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 26 Feb 2015 14:55:44 -0500 Subject: [PATCH 51/69] tool group position --- examples/user_interfaces/navigation.py | 28 ++++++++++++----- lib/matplotlib/backends/backend_gtk3.py | 42 +++++++++++++++++-------- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 4c4d320c176f..697514c2579a 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -1,13 +1,21 @@ +'''This example demonstrates how the `matplotlib.backend_bases.NavigationBase` +class allows to: +* Modify the Toolbar +* Add tools +* Remove tools +''' + + +from __future__ import print_function import matplotlib 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 class ListTools(ToolBase): + '''List all the tools controlled by `Navigation`''' # keyboard shortcut keymap = 'm' description = 'List Tools' @@ -34,9 +42,9 @@ def trigger(self, *args, **kwargs): print("{0:12} {1:45}").format(group, active) -# A simple example of copy canvas # ref: at https://github.com/matplotlib/matplotlib/issues/1987 class CopyToolGTK3(ToolBase): + '''Copy canvas to clipboard''' keymap = 'ctrl+c' description = 'Copy canvas' @@ -54,10 +62,16 @@ def trigger(self, *args, **kwargs): # 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('copy', CopyToolGTK3) + +# Add an existing tool to new group `foo`. +# It can be added as many times as we want fig.canvas.manager.toolbar.add_tool('zoom', 'foo') -# Uncomment to remove the forward button -# fig.canvas.manager.navigation.remove_tool('forward') + +# Remove the forward button +fig.canvas.manager.navigation.remove_tool('forward') + +# To add a custom tool to the toolbar at specific location +fig.canvas.manager.toolbar.add_tool('List', 'navigation', 1) plt.show() diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 973b9bab61ed..a558ab067422 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -771,14 +771,16 @@ def __init__(self, navigation): 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._toolarea = Gtk.Box() + self._toolarea.set_property('orientation', Gtk.Orientation.HORIZONTAL) + self.pack_start(self._toolarea, False, False, 0) + self._toolarea.show_all() + self._groups = {} self._toolitems = {} def add_toolitem(self, name, group, position, image_file, description, toggle): + if toggle: tbutton = Gtk.ToggleToolButton() else: @@ -792,14 +794,25 @@ 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) + + self._add_button(tbutton, group, position) signal = tbutton.connect('clicked', self._call_tool, name) tbutton.set_tooltip_text(description) tbutton.show_all() self._toolitems.setdefault(name, []) self._toolitems[name].append((tbutton, signal)) + def _add_button(self, button, group, position): + if group not in self._groups: + if self._groups: + self._add_separator() + toolbar = Gtk.Toolbar() + toolbar.set_style(Gtk.ToolbarStyle.ICONS) + self._toolarea.pack_start(toolbar, False, False, 0) + toolbar.show_all() + self._groups[group] = toolbar + self._groups[group].insert(button, position) + def _call_tool(self, btn, name): self.trigger_tool(name) @@ -815,15 +828,18 @@ def remove_toolitem(self, name): if name not in self._toolitems: self.navigation.message_event('%s Not in toolbar' % name, self) return - for toolitem, _signal in self._toolitems[name]: - self._toolbar.remove(toolitem) + + for group in self._groups: + for toolitem, _signal in self._toolitems[name]: + if toolitem in self._groups[group]: + self._groups[group].remove(toolitem) del self._toolitems[name] - def add_separator(self, pos=-1): - toolitem = Gtk.SeparatorToolItem() - self._toolbar.insert(toolitem, pos) - toolitem.show() - return toolitem + def _add_separator(self): + sep = Gtk.Separator() + sep.set_property("orientation", Gtk.Orientation.VERTICAL) + self._toolarea.pack_start(sep, False, True, 0) + sep.show_all() class StatusbarGTK3(StatusbarBase, Gtk.Statusbar): From 9a64b7eaed8cf1210e7759bef974878c2044fc44 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 23 Mar 2015 10:21:25 -0400 Subject: [PATCH 52/69] docstrings and small corrections by WeatherGod --- examples/user_interfaces/navigation.py | 6 +- lib/matplotlib/backend_bases.py | 83 ++++++++++------- lib/matplotlib/backend_tools.py | 119 +++++++++++++++---------- 3 files changed, 126 insertions(+), 82 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 697514c2579a..9f9d96fec3f0 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -12,6 +12,7 @@ class allows to: matplotlib.rcParams['toolbar'] = 'navigation' import matplotlib.pyplot as plt from matplotlib.backend_tools import ToolBase +from gi.repository import Gtk, Gdk class ListTools(ToolBase): @@ -36,10 +37,10 @@ def trigger(self, *args, **kwargs): keys)) print('_' * 80) print("Active Toggle tools") - print("{0:12} {1:45}").format("Group", "Active") + 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) + print("{0:12} {1:45}".format(group, active)) # ref: at https://github.com/matplotlib/matplotlib/issues/1987 @@ -49,7 +50,6 @@ class CopyToolGTK3(ToolBase): description = 'Copy canvas' def trigger(self, *args, **kwargs): - 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() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index b20fcf715d6c..81a8fcd41e94 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3244,7 +3244,8 @@ def __init__(self, name, sender, tool, canvasevent=None, data=None): class NavigationMessageEvent(object): - """Event carrying messages from navigation + """ + Event carrying messages from navigation Messages usually get displayed to the user by the toolbar """ @@ -3255,7 +3256,8 @@ def __init__(self, name, sender, message): 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 ---------- @@ -3281,7 +3283,8 @@ def __init__(self, canvas): self.messagelock = widgets.LockDraw() def nav_connect(self, s, func): - """Connect event with string *s* to *func*. + """ + Connect event with string *s* to *func*. Parameters ----------- @@ -3306,7 +3309,8 @@ def func(event) return self._callbacks.connect(s, func) def nav_disconnect(self, cid): - """Disconnect callback id cid + """ + Disconnect callback id cid Example usage:: @@ -3317,7 +3321,7 @@ def nav_disconnect(self, cid): return self._callbacks.disconnect(cid) def message_event(self, message, sender=None): - """ Emit a tool_message_event event""" + """ Emit a `NavigationMessageEvent`""" if sender is None: sender = self @@ -3327,7 +3331,8 @@ def message_event(self, message, sender=None): @property def active_toggle(self): - """Toggled Tool + """ + Toggled Tool **dict** : Currently toggled tools """ @@ -3335,7 +3340,8 @@ def active_toggle(self): return self._toggled def get_tool_keymap(self, name): - """Get the keymap associated with the specified tool + """ + Get the keymap associated with the specified tool Parameters ---------- @@ -3355,7 +3361,8 @@ def _remove_keys(self, name): del self._keys[k] def set_tool_keymap(self, name, *keys): - """Set the keymap to associate with the specified tool + """ + Set the keymap to associate with the specified tool Parameters ---------- @@ -3365,7 +3372,7 @@ def set_tool_keymap(self, name, *keys): """ if name not in self._tools: - raise AttributeError('%s not in Tools' % name) + raise KeyError('%s not in Tools' % name) self._remove_keys(name) @@ -3377,7 +3384,8 @@ def set_tool_keymap(self, name, *keys): self._keys[k] = name def remove_tool(self, name): - """Remove tool from `Navigation` + """ + Remove tool from `Navigation` Parameters ---------- @@ -3401,7 +3409,8 @@ def remove_tool(self, name): del self._tools[name] def add_tools(self, tools): - """ Add multiple tools to `NavigationBase` + """ + Add multiple tools to `NavigationBase` Parameters ---------- @@ -3414,7 +3423,8 @@ def add_tools(self, tools): self.add_tool(name, tool) def add_tool(self, name, tool, *args, **kwargs): - """Add tool to `NavigationBase` + """ + Add tool to `NavigationBase` Add a tool to the tools controlled by Navigation @@ -3440,11 +3450,10 @@ def add_tool(self, name, tool, *args, **kwargs): tool_cls = self._get_cls_to_instantiate(tool) if tool_cls is False: - warnings.warn('Impossible to find class for %s' % str(tool)) - return + raise ValueError('Impossible to find class for %s' % str(tool)) if name in self._tools: - warnings.warn('A tool_cls with the same name already exist, ' + warnings.warn('A "Tool class" with the same name already exists, ' 'not added') return self._tools[name] @@ -3454,7 +3463,7 @@ def add_tool(self, name, tool, *args, **kwargs): self.set_tool_keymap(name, tool_cls.keymap) # For toggle tools init the radio_group in self._toggled - if getattr(tool_cls, 'toggled', False) is not False: + if isinstance(self._tools[name], tools.ToolToggleBase): # 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: @@ -3471,8 +3480,10 @@ def _tool_added_event(self, tool): self._callbacks.process(s, event) def _handle_toggle(self, tool, sender, canvasevent, data): - # Toggle tools, need to untoggle prior to using other Toggle tool - # 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 @@ -3522,7 +3533,8 @@ def _get_cls_to_instantiate(self, callback_class): def tool_trigger_event(self, name, sender=None, canvasevent=None, data=None): - """Trigger a tool and emit the tool-trigger-[name] event + """ + Trigger a tool and emit the tool_trigger_[name] event Parameters ---------- @@ -3549,7 +3561,8 @@ def tool_trigger_event(self, name, sender=None, canvasevent=None, self._callbacks.process(s, event) def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): - """Trigger on a tool + """ + Trigger on a tool Method to actually trigger the tool """ @@ -3578,7 +3591,8 @@ def tools(self): return self._tools def get_tool(self, name, warn=True): - """Return the tool object, also accepts the actual tool for convenience + """ + Return the tool object, also accepts the actual tool for convenience Parameters ----------- @@ -3597,7 +3611,8 @@ def get_tool(self, name, warn=True): class ToolContainerBase(object): - """Base class for all tool containers, e.g. toolbars. + """ + Base class for all tool containers, e.g. toolbars. Attributes ---------- @@ -3611,14 +3626,16 @@ def __init__(self, navigation): self._remove_tool_cbk) def _tool_toggled_cbk(self, event): - """Captures the 'tool-trigger-toolname + """ + Captures the 'tool_trigger_[name]' This only gets used for toggled tools """ self.toggle_toolitem(event.tool.name, event.tool.toggled) def add_tools(self, tools): - """ Add multiple tools to the container. + """ + Add multiple tools to the container. Parameters ---------- @@ -3634,7 +3651,8 @@ def add_tools(self, tools): self.add_tool(tool, group, position) def add_tool(self, tool, group, position=-1): - """Adds a tool to this container + """ + Adds a tool to this container Parameters ---------- @@ -3670,7 +3688,8 @@ def _get_image_filename(self, image): return fname def trigger_tool(self, name): - """Trigger the tool + """ + Trigger the tool Parameters ---------- @@ -3681,7 +3700,8 @@ def trigger_tool(self, name): self.navigation.tool_trigger_event(name, sender=self) def add_toolitem(self, name, group, position, image, description, toggle): - """Add a toolitem to the container + """ + Add a toolitem to the container This method must get implemented per backend @@ -3711,7 +3731,8 @@ def add_toolitem(self, name, group, position, image, description, toggle): raise NotImplementedError def toggle_toolitem(self, name, toggled): - """Toggle the toolitem without firing event + """ + Toggle the toolitem without firing event Parameters ---------- @@ -3723,7 +3744,8 @@ def toggle_toolitem(self, name, toggled): raise NotImplementedError def remove_toolitem(self, name): - """Remove a toolitem from the `ToolContainer` + """ + Remove a toolitem from the `ToolContainer` This method must get implemented per backend @@ -3750,7 +3772,8 @@ def _message_cbk(self, event): self.set_message(event.message) def set_message(self, s): - """Display a message on toolbar or in status bar + """ + Display a message on toolbar or in status bar Parameters ---------- diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 1904af98c032..2d5c7c064f7e 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -23,9 +23,13 @@ class Cursors(object): HAND, POINTER, SELECT_REGION, MOVE = list(range(4)) cursors = Cursors() +# Views positions tool +_views_positions = 'viewpos' + class ToolBase(object): - """Base tool class + """ + Base tool class A base tool, only implements `trigger` method or not method at all. The tool is instantiated by `matplotlib.backend_bases.NavigationBase` @@ -42,21 +46,24 @@ class ToolBase(object): """ keymap = None - """Keymap to associate with 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 """ description = None - """Description of the Tool + """ + Description of the Tool **String**: If the Tool is included in the Toolbar this text is used as a Tooltip """ image = None - """Filename of the image + """ + Filename of the image **String**: Filename of the image to use in the toolbar. If None, the `name` is used as a label in the toolbar button @@ -64,19 +71,24 @@ class ToolBase(object): def __init__(self, navigation, name): self._name = name - self.figure = None + self._figure = None self.navigation = navigation self.set_figure(navigation.canvas.figure) + @property + def figure(self): + return self._figure + def trigger(self, sender, event, data=None): - """Called when this tool gets used + """ + Called when this tool gets used This method is called by `matplotlib.backend_bases.NavigationBase.tool_trigger_event` Parameters ---------- - event : `Event` + event: `Event` The Canvas event that caused this tool to be called sender: object Object that requested the tool to be triggered @@ -87,16 +99,17 @@ def trigger(self, sender, event, data=None): pass def set_figure(self, figure): - """Set the figure + """ + Set the figure Set the figure to be affected by this tool Parameters ---------- - figure : `Figure` + figure: `Figure` """ - self.figure = figure + self._figure = figure @property def name(self): @@ -104,7 +117,8 @@ def name(self): return self._name def destroy(self): - """Destroy the tool + """ + Destroy the tool This method is called when the tool is removed by `matplotlib.backend_bases.NavigationBase.remove_tool` @@ -113,7 +127,8 @@ def destroy(self): class ToolToggleBase(ToolBase): - """Toggleable tool + """ + Toggleable tool Every time it is triggered, it switches between enable and disable """ @@ -141,7 +156,8 @@ def trigger(self, sender, event, data=None): self._toggled = not self._toggled def enable(self, event=None): - """Enable the toggle tool + """ + Enable the toggle tool `trigger` calls this method when `toggled` is False """ @@ -149,9 +165,10 @@ def enable(self, event=None): pass def disable(self, event=None): - """Disable the toggle tool + """ + Disable the toggle tool - `trigger` call this methond when `toggled` is True. + `trigger` call this method when `toggled` is True. This can happen in different circumstances @@ -171,7 +188,8 @@ 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 `ToolToggleBase` derived tools, and calls set_cursor when a tool gets triggered @@ -197,14 +215,14 @@ def _tool_trigger_cbk(self, event): self._set_cursor_cbk(event.canvasevent) - # If the tool is toggleable, set the cursor when the tool is triggered def _add_tool(self, tool): + """set the cursor when the tool is triggered""" if getattr(tool, 'cursor', None) is not None: self.navigation.nav_connect('tool_trigger_%s' % tool.name, self._tool_trigger_cbk) - # If tool is added, process it def _add_tool_cbk(self, event): + """Process every newly added tool""" if event.tool is self: return @@ -218,23 +236,24 @@ def _set_cursor_cbk(self, event): if self._last_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.set_cursor(cursor) - self._last_cursor = cursor + elif self._cursor: + cursor = self._cursor + if cursor and self._last_cursor != cursor: + self.set_cursor(cursor) + self._last_cursor = cursor def set_cursor(self, cursor): - """Set the cursor + """ + Set the cursor This method has to be implemented per backend """ - pass + raise NotImplementedError 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 """ @@ -272,16 +291,18 @@ def trigger(self, sender, event, data): self.remove_rubberband() def draw_rubberband(self, *data): - """Draw rubberband + """ + Draw rubberband This method must get implemented per backend """ - pass + raise NotImplementedError def remove_rubberband(self): - """Remove rubberband + """ + Remove rubberband - This method must get implemented per backend + This method should get implemented per backend """ pass @@ -307,8 +328,8 @@ def trigger(self, sender, event, data=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): + if (event.x is not None and event.y is not None + and a.in_axes(event)): a.set_navigate(True) @@ -324,11 +345,9 @@ def trigger(self, sender, event, data=None): 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) + if (event.x is not None and event.y is not None + and a.in_axes(event)): + a.set_navigate(i == n) class ToolGrid(ToolToggleBase): @@ -402,7 +421,8 @@ def set_scale(self, ax, scale): class ToolViewsPositions(ToolBase): - """Auxiliary Tool to handle changes in views and positions + """ + Auxiliary Tool to handle changes in views and positions 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. @@ -437,7 +457,8 @@ def clear(self, figure): self.positions[figure].clear() def update_view(self): - """Update the viewlim and position from the view and + """ + Update the viewlim and position from the view and position stack for each axes """ @@ -516,9 +537,9 @@ class ViewsPositionsBase(ToolBase): _on_trigger = 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() + self.navigation.get_tool(_views_positions).add_figure() + getattr(self.navigation.get_tool(_views_positions), self._on_trigger)() + self.navigation.get_tool(_views_positions).update_view() class ToolHome(ViewsPositionsBase): @@ -593,7 +614,7 @@ def disable(self, event): self.figure.canvas.mpl_disconnect(self._idScroll) def trigger(self, sender, event, data=None): - self.navigation.get_tool('viewpos').add_figure() + self.navigation.get_tool(_views_positions).add_figure() ToolToggleBase.trigger(self, sender, event, data) def scroll_zoom(self, event): @@ -642,7 +663,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.navigation.get_tool('viewpos').refresh_locators() + self.navigation.get_tool(_views_positions).refresh_locators() self._xypress = None self._button_pressed = None self._ids_zoom = [] @@ -833,7 +854,7 @@ def _release(self, event): a.set_ylim((ry1, ry2)) self._zoom_mode = None - self.navigation.get_tool('viewpos').push_current() + self.navigation.get_tool(_views_positions).push_current() self._cancel_action() @@ -855,7 +876,7 @@ def _cancel_action(self): self._xypress = [] self.figure.canvas.mpl_disconnect(self._idDrag) self.navigation.messagelock.release(self) - self.navigation.get_tool('viewpos').refresh_locators() + self.navigation.get_tool(_views_positions).refresh_locators() def _press(self, event): if event.button == 1: @@ -892,7 +913,7 @@ def _release(self, event): self._cancel_action() return - self.navigation.get_tool('viewpos').push_current() + self.navigation.get_tool(_views_positions).push_current() self._cancel_action() def _mouse_move(self, event): @@ -915,7 +936,7 @@ def _mouse_move(self, event): 'xscale': ToolXScale, 'yscale': ToolYScale, 'position': ToolCursorPosition, - 'viewpos': ToolViewsPositions, + _views_positions: ToolViewsPositions, 'cursor': 'ToolSetCursor', 'rubberband': 'ToolRubberband'} """Default tools""" From 64f947ff23f276210eddf5920185db9e41b54cd6 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 31 Mar 2015 19:18:29 -0400 Subject: [PATCH 53/69] tkbackend updated --- lib/matplotlib/backends/backend_tkagg.py | 129 +++++++++++++---------- 1 file changed, 74 insertions(+), 55 deletions(-) diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 58719b18087e..75472c963c15 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -20,8 +20,10 @@ 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, ToolbarBase, NavigationBase -from matplotlib.backend_tools import SaveFigureBase, ConfigureSubplotsBase, tools +from matplotlib.backend_bases import (ShowBase, ToolContainerBase, + NavigationBase, StatusbarBase) +from matplotlib.backend_tools import (SaveFigureBase, ConfigureSubplotsBase, + tools, toolbar_tools, SetCursorBase, RubberbandBase) from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure @@ -530,14 +532,19 @@ def __init__(self, canvas, num, 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 ) - else: - self.toolbar = None - if self.toolbar is not None: - self.toolbar.update() self.canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) + self._num = num + + self.navigation = self._get_navigation() + self.toolbar = self._get_toolbar() + self.statusbar = None + + if matplotlib.rcParams['toolbar'] == 'navigation': + self.navigation.add_tools(tools) + self.toolbar.add_tools(toolbar_tools) + self.statusbar = StatusbarTk(self.window, self.navigation) + + self._shown = False def notify_axes_change(fig): @@ -552,7 +559,7 @@ def _get_toolbar(self): if matplotlib.rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2TkAgg(self.canvas, self.window) elif matplotlib.rcParams['toolbar'] == 'navigation': - toolbar = ToolbarTk(self) + toolbar = ToolbarTk(self.navigation, self.window) else: toolbar = None return toolbar @@ -560,7 +567,7 @@ def _get_toolbar(self): def _get_navigation(self): # must be inited after toolbar is setted if rcParams['toolbar'] != 'toolbar2': - navigation = NavigationTk(self) + navigation = NavigationTk(self.canvas) else: navigation = None return navigation @@ -894,16 +901,15 @@ def hidetip(self): class NavigationTk(NavigationBase): - def __init__(self, *args, **kwargs): - NavigationBase.__init__(self, *args, **kwargs) + pass - 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 +class RubberbandTk(RubberbandBase): + def __init__(self, *args, **kwargs): + RubberbandBase.__init__(self, *args, **kwargs) + + def draw_rubberband(self, x0, y0, x1, y1): + height = self.figure.canvas.figure.bbox.height y0 = height - y0 y1 = height - y1 try: @@ -911,36 +917,44 @@ def draw_rubberband(self, event, caller, x0, y0, x1, y1): except AttributeError: pass else: - self.canvas._tkcanvas.delete(self.lastrect) - self.lastrect = self.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1) + self.figure.canvas._tkcanvas.delete(self.lastrect) + self.lastrect = self.figure.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1) - def remove_rubberband(self, event, caller): + def remove_rubberband(self): try: self.lastrect except AttributeError: pass else: - self.canvas._tkcanvas.delete(self.lastrect) + self.figure.canvas._tkcanvas.delete(self.lastrect) del self.lastrect +ToolRubberband = RubberbandTk + + +class SetCursorTk(SetCursorBase): + def set_cursor(self, cursor): + self.figure.canvas.manager.window.configure(cursor=cursord[cursor]) + +ToolSetCursor = SetCursorTk + -class ToolbarTk(ToolbarBase, Tk.Frame): - def __init__(self, manager): - ToolbarBase.__init__(self, manager) - xmin, xmax = self.manager.canvas.figure.bbox.intervalx +class ToolbarTk(ToolContainerBase, Tk.Frame): + def __init__(self, navigation, window): + ToolContainerBase.__init__(self, navigation) + xmin, xmax = self.navigation.canvas.figure.bbox.intervalx height, width = 50, xmax - xmin - Tk.Frame.__init__(self, master=self.manager.window, + Tk.Frame.__init__(self, master=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): + self.pack(side=Tk.TOP, fill=Tk.X) + def add_toolitem(self, name, group, position, image_file, description, + toggle): button = self._Button(name, image_file, toggle) - if tooltip_text is not None: - ToolTip.createToolTip(button, tooltip_text) + if description is not None: + ToolTip.createToolTip(button, description) self._toolitems[name] = button def _Button(self, text, image_file, toggle): @@ -961,31 +975,36 @@ def _Button(self, text, image_file, toggle): return b def _button_click(self, name): - self.manager.navigation._toolbar_callback(name) + self.trigger_tool(name) - def _toggle(self, name, callback=False): + def toggle_toolitem(self, name, toggled): 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) + if toggled: + self._toolitems[name].select() + else: + self._toolitems[name].deselect() - def _remove_toolitem(self, name): + def remove_toolitem(self, name): self._toolitems[name].pack_forget() del self._toolitems[name] - def set_toolitem_visibility(self, name, visible): - pass + +class StatusbarTk(StatusbarBase, Tk.Frame): + def __init__(self, window, *args, **kwargs): + StatusbarBase.__init__(self, *args, **kwargs) + xmin, xmax = self.navigation.canvas.figure.bbox.intervalx + height, width = 50, xmax - xmin + Tk.Frame.__init__(self, master=window, + width=int(width), height=int(height), + borderwidth=2) + 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.TOP, fill=Tk.X) + + def set_message(self, s): + self._message.set(s) class SaveFigureTk(SaveFigureBase): @@ -1046,7 +1065,7 @@ def __init__(self, *args, **kwargs): ConfigureSubplotsBase.__init__(self, *args, **kwargs) self.window = None - def trigger(self, event): + def trigger(self, *args): self.init_window() self.window.lift() @@ -1069,8 +1088,8 @@ def destroy(self, *args, **kwargs): self.window = None -SaveFigure = SaveFigureTk -ConfigureSubplots = ConfigureSubplotsTk +ToolSaveFigure = SaveFigureTk +ToolConfigureSubplots = ConfigureSubplotsTk Toolbar = ToolbarTk Navigation = NavigationTk FigureCanvas = FigureCanvasTkAgg From e8cd5d55ba713da4af761a9cea25df7cecfabeee Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 1 Apr 2015 09:33:46 -0400 Subject: [PATCH 54/69] tacaswell comments aprl 1 --- examples/user_interfaces/navigation.py | 4 +- lib/matplotlib/backend_bases.py | 60 +++++++++++++++----------- lib/matplotlib/backend_tools.py | 28 ++++++------ 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 9f9d96fec3f0..987c838b66bc 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -18,7 +18,7 @@ class allows to: class ListTools(ToolBase): '''List all the tools controlled by `Navigation`''' # keyboard shortcut - keymap = 'm' + default_keymap = 'm' description = 'List Tools' def trigger(self, *args, **kwargs): @@ -46,7 +46,7 @@ def trigger(self, *args, **kwargs): # ref: at https://github.com/matplotlib/matplotlib/issues/1987 class CopyToolGTK3(ToolBase): '''Copy canvas to clipboard''' - keymap = 'ctrl+c' + default_keymap = 'ctrl+c' description = 'Copy canvas' def trigger(self, *args, **kwargs): diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 81a8fcd41e94..9bf2d9e20532 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2586,6 +2586,8 @@ def __init__(self, canvas, num): self.key_press_handler_id = self.canvas.mpl_connect( 'key_press_event', self.key_press) + else: + self.key_press_handler_id = None """ The returned id from connecting the default key handler via :meth:`FigureCanvasBase.mpl_connnect`. @@ -3331,11 +3333,7 @@ def message_event(self, message, sender=None): @property def active_toggle(self): - """ - Toggled Tool - - **dict** : Currently toggled tools - """ + """Currently toggled tools""" return self._toggled @@ -3360,7 +3358,7 @@ def _remove_keys(self, name): for k in self.get_tool_keymap(name): del self._keys[k] - def set_tool_keymap(self, name, *keys): + def update_keymap(self, name, *keys): """ Set the keymap to associate with the specified tool @@ -3449,7 +3447,7 @@ def add_tool(self, name, tool, *args, **kwargs): """ tool_cls = self._get_cls_to_instantiate(tool) - if tool_cls is False: + if not tool_cls: raise ValueError('Impossible to find class for %s' % str(tool)) if name in self._tools: @@ -3457,22 +3455,23 @@ def add_tool(self, name, tool, *args, **kwargs): 'not added') return self._tools[name] - self._tools[name] = tool_cls(self, name, *args, **kwargs) + tool_obj = tool_cls(self, name, *args, **kwargs) + self._tools[name] = tool_obj - if tool_cls.keymap is not None: - self.set_tool_keymap(name, tool_cls.keymap) + if tool_cls.default_keymap is not None: + self.update_keymap(name, tool_cls.default_keymap) # For toggle tools init the radio_group in self._toggled - if isinstance(self._tools[name], tools.ToolToggleBase): + if isinstance(tool_obj, tools.ToolToggleBase): # 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: + if tool_obj.radio_group is None: self._toggled.setdefault(None, set()) else: - self._toggled.setdefault(tool_cls.radio_group, None) + self._toggled.setdefault(tool_obj.radio_group, None) - self._tool_added_event(self._tools[name]) - return self._tools[name] + self._tool_added_event(tool_obj) + return tool_obj def _tool_added_event(self, tool): s = 'tool_added_event' @@ -3483,6 +3482,16 @@ def _handle_toggle(self, tool, sender, canvasevent, data): """ Toggle tools, need to untoggle prior to using other Toggle tool Called from tool_trigger_event + + Parameters + ---------- + tool: Tool object + sender: object + Object that wishes to trigger the tool + canvasevent : Event + Original Canvas event or None + data : Object + Extra data to pass to the tool when triggering """ radio_group = tool.radio_group @@ -3500,7 +3509,7 @@ def _handle_toggle(self, tool, sender, canvasevent, data): toggled = None # If no tool was toggled in the radio_group # toggle it - elif self._toggled.get(radio_group, None) is None: + elif self._toggled[radio_group] is None: toggled = tool.name # Other tool in the radio_group is toggled else: @@ -3521,15 +3530,18 @@ def _get_cls_to_instantiate(self, callback_class): if isinstance(callback_class, six.string_types): # 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) + callback_class = globals()[callback_class] + else: + mod = self.__class__.__module__ + current_module = __import__(mod, + globals(), locals(), [mod], 0) - return getattr(current_module, callback_class, False) + callback_class = getattr(current_module, callback_class, False) - return callback_class + if callable(callback_class): + return callback_class + else: + return None def tool_trigger_event(self, name, sender=None, canvasevent=None, data=None): @@ -3598,7 +3610,7 @@ def get_tool(self, name, warn=True): ----------- name : str, ToolBase Name of the tool, or the tool itself - warn : bool + warn : bool, optional If this method should give warnings. """ if isinstance(name, tools.ToolBase) and name.name in self._tools: diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 2d5c7c064f7e..bd6b288e87f8 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -45,7 +45,7 @@ class ToolBase(object): Navigation """ - keymap = None + default_keymap = None """ Keymap to associate with this tool @@ -311,7 +311,7 @@ class ToolQuit(ToolBase): """Tool to call the figure manager destroy method""" description = 'Quit the figure' - keymap = rcParams['keymap.quit'] + default_keymap = rcParams['keymap.quit'] def trigger(self, sender, event, data=None): Gcf.destroy_fig(self.figure) @@ -321,7 +321,7 @@ class ToolEnableAllNavigation(ToolBase): """Tool to enable all axes for navigation interaction""" description = 'Enables all axes navigation' - keymap = rcParams['keymap.all_axes'] + default_keymap = rcParams['keymap.all_axes'] def trigger(self, sender, event, data=None): if event.inaxes is None: @@ -337,7 +337,7 @@ class ToolEnableNavigation(ToolBase): """Tool to enable a specific axes for navigation interaction""" description = 'Enables one axes navigation' - keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9) + default_keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9) def trigger(self, sender, event, data=None): if event.inaxes is None: @@ -354,7 +354,7 @@ class ToolGrid(ToolToggleBase): """Tool to toggle the grid of the figure""" description = 'Toogle Grid' - keymap = rcParams['keymap.grid'] + default_keymap = rcParams['keymap.grid'] def trigger(self, sender, event, data=None): if event.inaxes is None: @@ -374,7 +374,7 @@ class ToolFullScreen(ToolToggleBase): """Tool to toggle full screen""" description = 'Toogle Fullscreen mode' - keymap = rcParams['keymap.fullscreen'] + default_keymap = rcParams['keymap.fullscreen'] def enable(self, event): self.figure.canvas.manager.full_screen_toggle() @@ -404,7 +404,7 @@ class ToolYScale(AxisScaleBase): """Tool to toggle between linear and logarithmic scales on the Y axis""" description = 'Toogle Scale Y axis' - keymap = rcParams['keymap.yscale'] + default_keymap = rcParams['keymap.yscale'] def set_scale(self, ax, scale): ax.set_yscale(scale) @@ -414,7 +414,7 @@ class ToolXScale(AxisScaleBase): """Tool to toggle between linear and logarithmic scales on the X axis""" description = 'Toogle Scale X axis' - keymap = rcParams['keymap.xscale'] + default_keymap = rcParams['keymap.xscale'] def set_scale(self, ax, scale): ax.set_xscale(scale) @@ -547,7 +547,7 @@ class ToolHome(ViewsPositionsBase): description = 'Reset original view' image = 'home.png' - keymap = rcParams['keymap.home'] + default_keymap = rcParams['keymap.home'] _on_trigger = 'home' @@ -556,7 +556,7 @@ class ToolBack(ViewsPositionsBase): description = 'Back to previous view' image = 'back.png' - keymap = rcParams['keymap.back'] + default_keymap = rcParams['keymap.back'] _on_trigger = 'back' @@ -565,7 +565,7 @@ class ToolForward(ViewsPositionsBase): description = 'Forward to next view' image = 'forward.png' - keymap = rcParams['keymap.forward'] + default_keymap = rcParams['keymap.forward'] _on_trigger = 'forward' @@ -581,7 +581,7 @@ class SaveFigureBase(ToolBase): description = 'Save the figure' image = 'filesave.png' - keymap = rcParams['keymap.save'] + default_keymap = rcParams['keymap.save'] class ZoomPanBase(ToolToggleBase): @@ -651,7 +651,7 @@ class ToolZoom(ZoomPanBase): description = 'Zoom to rectangle' image = 'zoom_to_rect.png' - keymap = rcParams['keymap.zoom'] + default_keymap = rcParams['keymap.zoom'] cursor = cursors.SELECT_REGION radio_group = 'default' @@ -861,7 +861,7 @@ def _release(self, event): class ToolPan(ZoomPanBase): """Pan axes with left mouse, zoom with right""" - keymap = rcParams['keymap.pan'] + default_keymap = rcParams['keymap.pan'] description = 'Pan axes with left mouse, zoom with right' image = 'move.png' cursor = cursors.MOVE From 4bbcf4e7005069eb5f289940f0b745955473da05 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 1 Apr 2015 10:14:07 -0400 Subject: [PATCH 55/69] renaming tool_trigger_event --- lib/matplotlib/backend_bases.py | 14 +++++++------- lib/matplotlib/backend_tools.py | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 9bf2d9e20532..9d51220f9dca 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3396,7 +3396,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) @@ -3481,7 +3481,7 @@ def _tool_added_event(self, tool): def _handle_toggle(self, tool, sender, canvasevent, data): """ Toggle tools, need to untoggle prior to using other Toggle tool - Called from tool_trigger_event + Called from trigger_tool Parameters ---------- @@ -3514,7 +3514,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.trigger_tool(self._toggled[radio_group], self, canvasevent, data) @@ -3543,8 +3543,8 @@ def _get_cls_to_instantiate(self, callback_class): else: return None - 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 @@ -3594,7 +3594,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): @@ -3709,7 +3709,7 @@ def trigger_tool(self, name): Name(id) of the tool triggered from within the container """ - 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): """ diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index bd6b288e87f8..094e71a18ef1 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -84,7 +84,7 @@ 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` + `matplotlib.backend_bases.NavigationBase.trigger_tool` Parameters ---------- @@ -173,7 +173,7 @@ def disable(self, event=None): This can happen in different circumstances * Click on the toolbar tool button - * Call to `matplotlib.backend_bases.NavigationBase.tool_trigger_event` + * Call to `matplotlib.backend_bases.NavigationBase.trigger_tool` * Another `ToolToggleBase` derived tool is triggered (from the same `Navigation`) """ @@ -662,7 +662,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(_views_positions).refresh_locators() self._xypress = None self._button_pressed = None @@ -731,9 +731,9 @@ 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 73a26612def9a9240c1bd5eea759e6c074fd9b3d Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 1 Apr 2015 10:40:15 -0400 Subject: [PATCH 56/69] add_tools moved out of base classes --- lib/matplotlib/backend_bases.py | 37 ++---------------------- lib/matplotlib/backend_tools.py | 34 ++++++++++++++++++++++ lib/matplotlib/backends/backend_gtk3.py | 14 ++++++--- lib/matplotlib/backends/backend_tkagg.py | 13 +++++---- 4 files changed, 55 insertions(+), 43 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 9d51220f9dca..6e39693e7ade 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3406,20 +3406,6 @@ def remove_tool(self, name): del self._tools[name] - def add_tools(self, tools): - """ - Add multiple tools to `NavigationBase` - - Parameters - ---------- - tools : {str: class_like} - The tools to add in a {name: tool} dict, see `add_tool` for more - info. - """ - - for name, tool in six.iteritems(tools): - self.add_tool(name, tool) - def add_tool(self, name, tool, *args, **kwargs): """ Add tool to `NavigationBase` @@ -3515,9 +3501,9 @@ def _handle_toggle(self, tool, sender, canvasevent, data): else: # Untoggle previously toggled tool self.trigger_tool(self._toggled[radio_group], - self, - canvasevent, - data) + self, + canvasevent, + data) toggled = tool.name # Keep track of the toggled tool in the radio_group @@ -3645,23 +3631,6 @@ def _tool_toggled_cbk(self, event): """ self.toggle_toolitem(event.tool.name, event.tool.toggled) - def add_tools(self, tools): - """ - Add multiple tools to the container. - - Parameters - ---------- - tools : list - List in the form - [[group1, [tool1, tool2 ...]], [group2, [...]]] - Where the tools given by tool1, and tool2 will display in group1. - See `add_tool` for details. - """ - - for group, grouptools in tools: - for position, tool in enumerate(grouptools): - self.add_tool(tool, group, position) - def add_tool(self, tool, group, position=-1): """ Adds a tool to this container diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 094e71a18ef1..0ad44e8e2189 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 +import six class Cursors(object): @@ -946,3 +947,36 @@ def _mouse_move(self, event): ['layout', ['subplots']], ['io', ['save']]] """Default tools in the toolbar""" + + +def add_tools_2_navigation(navigation, tools=tools): + """ + Add multiple tools to `Navigation` + + Parameters + ---------- + tools : {str: class_like} + The tools to add in a {name: tool} dict, see `add_tool` for more + info. + """ + + for name, tool in six.iteritems(tools): + navigation.add_tool(name, tool) + + +def add_tools_2_container(container, tools=toolbar_tools): + """ + Add multiple tools to the container. + + Parameters + ---------- + tools : list + List in the form + [[group1, [tool1, tool2 ...]], [group2, [...]]] + Where the tools given by tool1, and tool2 will display in group1. + See `add_tool` for details. + """ + + for group, grouptools in tools: + for position, tool in enumerate(grouptools): + container.add_tool(tool, group, position) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index a558ab067422..4eeba2f0d303 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -32,8 +32,12 @@ def fn_name(): return sys._getframe(1).f_code.co_name FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase from matplotlib.backend_bases import (ShowBase, ToolContainerBase, NavigationBase, StatusbarBase) -from matplotlib.backend_tools import (SaveFigureBase, ConfigureSubplotsBase, - tools, toolbar_tools, SetCursorBase, RubberbandBase) +from matplotlib.backend_tools import (SaveFigureBase, + ConfigureSubplotsBase, + add_tools_2_navigation, + add_tools_2_container, + SetCursorBase, + RubberbandBase) from matplotlib.cbook import is_string_like, is_writable_file_like from matplotlib.colors import colorConverter @@ -432,8 +436,10 @@ def add_widget(child, expand, fill, padding): return size_request.height if matplotlib.rcParams['toolbar'] == 'navigation': - self.navigation.add_tools(tools) - self.toolbar.add_tools(toolbar_tools) + add_tools_2_navigation(self.navigation) +# self.navigation.add_tools(tools) +# self.toolbar.add_tools(toolbar_tools) + add_tools_2_container(self.toolbar) self.statusbar = StatusbarGTK3(self.navigation) h += add_widget(self.statusbar, False, False, 0) h += add_widget(Gtk.HSeparator(), False, False, 0) diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 75472c963c15..b0291beb189c 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -22,8 +22,12 @@ from matplotlib.backend_bases import NavigationToolbar2, cursors, TimerBase from matplotlib.backend_bases import (ShowBase, ToolContainerBase, NavigationBase, StatusbarBase) -from matplotlib.backend_tools import (SaveFigureBase, ConfigureSubplotsBase, - tools, toolbar_tools, SetCursorBase, RubberbandBase) +from matplotlib.backend_tools import (SaveFigureBase, + ConfigureSubplotsBase, + add_tools_2_navigation, + add_tools_2_container, + SetCursorBase, + RubberbandBase) from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure @@ -540,11 +544,10 @@ def __init__(self, canvas, num, window): self.statusbar = None if matplotlib.rcParams['toolbar'] == 'navigation': - self.navigation.add_tools(tools) - self.toolbar.add_tools(toolbar_tools) + add_tools_2_navigation(self.navigation) + add_tools_2_container(self.toolbar) self.statusbar = StatusbarTk(self.window, self.navigation) - self._shown = False def notify_axes_change(fig): From 1b8362807bc8c4b90de17dc779eafe34f47813db Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 1 Apr 2015 12:16:43 -0400 Subject: [PATCH 57/69] figure.setter in tools --- lib/matplotlib/backend_tools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 0ad44e8e2189..9faf1fe7e03a 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -74,7 +74,7 @@ def __init__(self, navigation, name): self._name = name self._figure = None self.navigation = navigation - self.set_figure(navigation.canvas.figure) + self.figure = navigation.canvas.figure @property def figure(self): @@ -99,7 +99,8 @@ def trigger(self, sender, event, data=None): pass - def set_figure(self, figure): + @figure.setter + def figure(self, figure): """ Set the figure From e4edd23499b8d30dac210f34db65853f8c8c0157 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 1 Apr 2015 12:18:26 -0400 Subject: [PATCH 58/69] rename tools to default_tools to avoid confusion --- lib/matplotlib/backend_tools.py | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 9faf1fe7e03a..5485f2745576 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -926,31 +926,31 @@ def _mouse_move(self, event): self.navigation.canvas.draw_idle() -tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward, - 'zoom': ToolZoom, 'pan': ToolPan, - 'subplots': 'ToolConfigureSubplots', - 'save': 'ToolSaveFigure', - 'grid': ToolGrid, - 'fullscreen': ToolFullScreen, - 'quit': ToolQuit, - 'allnav': ToolEnableAllNavigation, - 'nav': ToolEnableNavigation, - 'xscale': ToolXScale, - 'yscale': ToolYScale, - 'position': ToolCursorPosition, - _views_positions: ToolViewsPositions, - 'cursor': 'ToolSetCursor', - 'rubberband': 'ToolRubberband'} +default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward, + 'zoom': ToolZoom, 'pan': ToolPan, + 'subplots': 'ToolConfigureSubplots', + 'save': 'ToolSaveFigure', + 'grid': ToolGrid, + 'fullscreen': ToolFullScreen, + 'quit': ToolQuit, + 'allnav': ToolEnableAllNavigation, + 'nav': ToolEnableNavigation, + 'xscale': ToolXScale, + 'yscale': ToolYScale, + 'position': ToolCursorPosition, + _views_positions: ToolViewsPositions, + 'cursor': 'ToolSetCursor', + 'rubberband': 'ToolRubberband'} """Default tools""" -toolbar_tools = [['navigation', ['home', 'back', 'forward']], - ['zoompan', ['zoom', 'pan']], - ['layout', ['subplots']], - ['io', ['save']]] +default_toolbar_tools = [['navigation', ['home', 'back', 'forward']], + ['zoompan', ['zoom', 'pan']], + ['layout', ['subplots']], + ['io', ['save']]] """Default tools in the toolbar""" -def add_tools_2_navigation(navigation, tools=tools): +def add_tools_2_navigation(navigation, tools=default_tools): """ Add multiple tools to `Navigation` @@ -965,7 +965,7 @@ def add_tools_2_navigation(navigation, tools=tools): navigation.add_tool(name, tool) -def add_tools_2_container(container, tools=toolbar_tools): +def add_tools_2_container(container, tools=default_toolbar_tools): """ Add multiple tools to the container. From d4ac2fb0d17794892a37652d34ac51803874f3f5 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 1 Apr 2015 12:23:45 -0400 Subject: [PATCH 59/69] docstring helper add_tools methods --- lib/matplotlib/backend_tools.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 5485f2745576..ece79409e196 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -956,7 +956,9 @@ def add_tools_2_navigation(navigation, tools=default_tools): Parameters ---------- - tools : {str: class_like} + navigation: Navigation + `backend_bases.NavigationBase` object that will get the tools added + tools : {str: class_like}, optional The tools to add in a {name: tool} dict, see `add_tool` for more info. """ @@ -971,7 +973,9 @@ def add_tools_2_container(container, tools=default_toolbar_tools): Parameters ---------- - tools : list + container: Container + `backend_bases.ToolContainerBase` object that will get the tools added + tools : list, optional List in the form [[group1, [tool1, tool2 ...]], [group2, [...]]] Where the tools given by tool1, and tool2 will display in group1. From a7640efbdb6cac5ce51b565a532411df221c5943 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 2 Apr 2015 14:37:43 -0400 Subject: [PATCH 60/69] rename navigation to toolmanager --- .../{navigation.py => toolmanager.py} | 17 +- lib/matplotlib/backend_bases.py | 424 +----------------- lib/matplotlib/backend_managers.py | 411 +++++++++++++++++ lib/matplotlib/backend_tools.py | 95 ++-- lib/matplotlib/backends/backend_gtk3.py | 68 ++- lib/matplotlib/backends/backend_tkagg.py | 66 ++- lib/matplotlib/rcsetup.py | 2 +- 7 files changed, 542 insertions(+), 541 deletions(-) rename examples/user_interfaces/{navigation.py => toolmanager.py} (81%) create mode 100644 lib/matplotlib/backend_managers.py diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/toolmanager.py similarity index 81% rename from examples/user_interfaces/navigation.py rename to examples/user_interfaces/toolmanager.py index 987c838b66bc..88fcb73a6842 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/toolmanager.py @@ -8,8 +8,9 @@ class allows to: from __future__ import print_function import matplotlib -matplotlib.use('GTK3Cairo') -matplotlib.rcParams['toolbar'] = 'navigation' +# matplotlib.use('GTK3Cairo') +matplotlib.use('Tkagg') +matplotlib.rcParams['toolbar'] = 'toolmanager' import matplotlib.pyplot as plt from matplotlib.backend_tools import ToolBase from gi.repository import Gtk, Gdk @@ -27,11 +28,11 @@ def trigger(self, *args, **kwargs): 'Tool description', 'Keymap')) print('-' * 80) - tools = self.navigation.tools + tools = self.toolmanager.tools for name in sorted(tools.keys()): if not tools[name].description: continue - keys = ', '.join(sorted(self.navigation.get_tool_keymap(name))) + keys = ', '.join(sorted(self.toolmanager.get_tool_keymap(name))) print("{0:12} {1:45} {2}".format(name, tools[name].description, keys)) @@ -39,7 +40,7 @@ def trigger(self, *args, **kwargs): print("Active Toggle tools") print("{0:12} {1:45}".format("Group", "Active")) print('-' * 80) - for group, active in self.navigation.active_toggle.items(): + for group, active in self.toolmanager.active_toggle.items(): print("{0:12} {1:45}".format(group, active)) @@ -61,15 +62,15 @@ def trigger(self, *args, **kwargs): plt.plot([1, 2, 3]) # Add the custom tools that we created -fig.canvas.manager.navigation.add_tool('List', ListTools) -fig.canvas.manager.navigation.add_tool('copy', CopyToolGTK3) +fig.canvas.manager.toolmanager.add_tool('List', ListTools) +# fig.canvas.manager.toolmanager.add_tool('copy', CopyToolGTK3) # Add an existing tool to new group `foo`. # It can be added as many times as we want fig.canvas.manager.toolbar.add_tool('zoom', 'foo') # Remove the forward button -fig.canvas.manager.navigation.remove_tool('forward') +fig.canvas.manager.toolmanager.remove_tool('forward') # To add a custom tool to the toolbar at specific location fig.canvas.manager.toolbar.add_tool('List', 'navigation', 1) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 6e39693e7ade..efebdd618e1e 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -24,17 +24,6 @@ The base class for the Show class of each interactive backend; 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:`ToolContainerBase` - The base class for the Toolbar class of each interactive backend. - -:class:`StatusbarBase` - The base class for the messaging area. """ from __future__ import (absolute_import, division, print_function, @@ -56,7 +45,6 @@ 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 @@ -2582,7 +2570,7 @@ def __init__(self, canvas, num): canvas.manager = self # store a pointer to parent self.num = num - if rcParams['toolbar'] != 'navigation': + if rcParams['toolbar'] != 'toolmanager': self.key_press_handler_id = self.canvas.mpl_connect( 'key_press_event', self.key_press) @@ -2623,7 +2611,7 @@ def key_press(self, event): Implement the default mpl key bindings defined at :ref:`key-event-handling` """ - if rcParams['toolbar'] != 'navigation': + if rcParams['toolbar'] != 'toolmanager': key_press_handler(event, self.canvas, self.canvas.toolbar) def show_popup(self, msg): @@ -3229,399 +3217,20 @@ def set_history_buttons(self): pass -class ToolEvent(object): - """Event for tool manipulation (add/remove)""" - 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, data) - self.canvasevent = canvasevent - - -class NavigationMessageEvent(object): - """ - Event carrying messages from navigation - - Messages usually get 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): - """ - Helper class that groups all the user interactions for a FigureManager - - 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 - """ - - def __init__(self, canvas): - self.canvas = canvas - - self._key_press_handler_id = self.canvas.mpl_connect( - 'key_press_event', self._key_press) - - self._tools = {} - self._keys = {} - self._toggled = {} - self._callbacks = cbook.CallbackRegistry() - - # to process keypress event - self.keypresslock = widgets.LockDraw() - self.messagelock = widgets.LockDraw() - - def nav_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_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 - Function to be called with signature - def func(event) - """ - return self._callbacks.connect(s, func) - - def nav_disconnect(self, cid): - """ - Disconnect callback id cid - - Example usage:: - - cid = navigation.nav_connect('tool_trigger_zoom', on_press) - #...later - navigation.nav_disconnect(cid) - """ - return self._callbacks.disconnect(cid) - - def message_event(self, message, sender=None): - """ Emit a `NavigationMessageEvent`""" - if sender is None: - sender = self - - s = 'tool_message_event' - event = NavigationMessageEvent(s, sender, message) - self._callbacks.process(s, event) - - @property - def active_toggle(self): - """Currently toggled tools""" - - return self._toggled - - def get_tool_keymap(self, name): - """ - Get the keymap associated with the specified tool - - Parameters - ---------- - name : string - Name of the Tool - - Returns - ---------- - list : list of keys associated with the Tool - """ - - keys = [k for k, i in six.iteritems(self._keys) if i == name] - return keys - - def _remove_keys(self, name): - for k in self.get_tool_keymap(name): - del self._keys[k] - - def update_keymap(self, name, *keys): - """ - Set the keymap to associate with the specified tool - - Parameters - ---------- - name : string - Name of the Tool - keys : keys to associate with the Tool - """ - - if name not in self._tools: - raise KeyError('%s not in Tools' % name) - - self._remove_keys(name) - - 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 remove_tool(self, name): - """ - Remove tool from `Navigation` - - Parameters - ---------- - name : string - Name of the Tool - """ - - tool = self.get_tool(name) - tool.destroy() - - # If is a toggle tool and toggled, untoggle - if getattr(tool, 'toggled', False): - self.trigger_tool(tool, 'navigation') - - self._remove_keys(name) - - s = 'tool_removed_event' - event = ToolEvent(s, self, tool) - self._callbacks.process(s, event) - - del self._tools[name] - - def add_tool(self, name, tool, *args, **kwargs): - """ - 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 - the tool is triggered. - - Parameters - ---------- - name : str - Name of the tool, treated as the ID, has to be unique - tool : class_like, i.e. str or type - Reference to find the class of the Tool to added. - - Notes - ----- - args and kwargs get passed directly to the tools constructor. - - See Also - -------- - matplotlib.backend_tools.ToolBase : The base class for tools. - """ - - tool_cls = self._get_cls_to_instantiate(tool) - if not tool_cls: - raise ValueError('Impossible to find class for %s' % str(tool)) - - if name in self._tools: - warnings.warn('A "Tool class" with the same name already exists, ' - 'not added') - return self._tools[name] - - tool_obj = tool_cls(self, name, *args, **kwargs) - self._tools[name] = tool_obj - - if tool_cls.default_keymap is not None: - self.update_keymap(name, tool_cls.default_keymap) - - # For toggle tools init the radio_group in self._toggled - if isinstance(tool_obj, tools.ToolToggleBase): - # None group is not mutually exclusive, a set is used to keep track - # of all toggled tools in this group - if tool_obj.radio_group is None: - self._toggled.setdefault(None, set()) - else: - self._toggled.setdefault(tool_obj.radio_group, None) - - self._tool_added_event(tool_obj) - return tool_obj - - def _tool_added_event(self, tool): - s = 'tool_added_event' - event = ToolEvent(s, self, tool) - self._callbacks.process(s, event) - - def _handle_toggle(self, tool, sender, canvasevent, data): - """ - Toggle tools, need to untoggle prior to using other Toggle tool - Called from trigger_tool - - Parameters - ---------- - tool: Tool object - sender: object - Object that wishes to trigger the tool - canvasevent : Event - Original Canvas event or None - data : Object - Extra data to pass to the tool when triggering - """ - - 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 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 - # toggle it - elif self._toggled[radio_group] is None: - toggled = tool.name - # Other tool in the radio_group is toggled - else: - # Untoggle previously toggled tool - self.trigger_tool(self._toggled[radio_group], - self, - canvasevent, - data) - toggled = tool.name - - # 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) - - 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(): - callback_class = globals()[callback_class] - else: - mod = self.__class__.__module__ - current_module = __import__(mod, - globals(), locals(), [mod], 0) - - callback_class = getattr(current_module, callback_class, False) - - if callable(callback_class): - return callback_class - else: - return None - - def trigger_tool(self, name, sender=None, canvasevent=None, - data=None): - """ - Trigger a tool and emit the tool_trigger_[name] event - - Parameters - ---------- - name : string - Name of the tool - sender: object - Object that wishes to trigger the tool - canvasevent : Event - Original Canvas event or None - data : Object - Extra data to pass to the tool when triggering - """ - tool = self.get_tool(name) - if tool is None: - return - - if sender is None: - sender = self - - 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 - - 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) - - def _key_press(self, event): - if event.key is None or self.keypresslock.locked(): - return - - name = self._keys.get(event.key, None) - if name is None: - return - self.trigger_tool(name, canvasevent=event) - - @property - def tools(self): - """Return the tools controlled by `Navigation`""" - - return self._tools - - def get_tool(self, name, warn=True): - """ - Return the tool object, also accepts the actual tool for convenience - - Parameters - ----------- - name : str, ToolBase - Name of the tool, or the tool itself - warn : bool, optional - If this method should give warnings. - """ - if isinstance(name, tools.ToolBase) and name.name in self._tools: - return name - if name not in self._tools: - if warn: - warnings.warn("Navigation does not control tool %s" % name) - return None - return self._tools[name] - - class ToolContainerBase(object): """ Base class for all tool containers, e.g. toolbars. Attributes ---------- - navigation : `NavigationBase` object that holds the tools that + toolmanager : `ToolManager` object that holds the tools that this `ToolContainer` wants to communicate with. """ - def __init__(self, navigation): - self.navigation = navigation - self.navigation.nav_connect('tool_removed_event', - self._remove_tool_cbk) + def __init__(self, toolmanager): + self.toolmanager = toolmanager + self.toolmanager.toolmanager_connect('tool_removed_event', + self._remove_tool_cbk) def _tool_toggled_cbk(self, event): """ @@ -3638,20 +3247,20 @@ def add_tool(self, tool, group, position=-1): Parameters ---------- tool : tool_like - The tool to add, see `NavigationBase.get_tool`. + The tool to add, see `ToolManager.get_tool`. group : str The name of the group to add this tool to. position : int (optional) The position within the group to place this tool. Defaults to end. """ - tool = self.navigation.get_tool(tool) + tool = self.toolmanager.get_tool(tool) image = self._get_image_filename(tool.image) toggle = getattr(tool, 'toggled', None) is not None self.add_toolitem(tool.name, group, position, image, tool.description, toggle) if toggle: - self.navigation.nav_connect('tool_trigger_%s' % tool.name, - self._tool_toggled_cbk) + self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name, + self._tool_toggled_cbk) def _remove_tool_cbk(self, event): """Captures the 'tool_removed_event' signal and removes the tool""" @@ -3678,7 +3287,7 @@ def trigger_tool(self, name): Name(id) of the tool triggered from within the container """ - self.navigation.trigger_tool(name, sender=self) + self.toolmanager.trigger_tool(name, sender=self) def add_toolitem(self, name, group, position, image, description, toggle): """ @@ -3730,7 +3339,7 @@ def remove_toolitem(self, name): This method must get implemented per backend - Called when `NavigationBase` emits a `tool_removed_event` + Called when `ToolManager` emits a `tool_removed_event` Parameters ---------- @@ -3744,9 +3353,10 @@ def remove_toolitem(self, name): class StatusbarBase(object): """Base class for the statusbar""" - def __init__(self, navigation): - self.navigation = navigation - self.navigation.nav_connect('tool_message_event', self._message_cbk) + def __init__(self, toolmanager): + self.toolmanager = toolmanager + self.toolmanager.toolmanager_connect('tool_message_event', + self._message_cbk) def _message_cbk(self, event): """Captures the 'tool_message_event' and set the message""" diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py new file mode 100644 index 000000000000..8e27b7644e1e --- /dev/null +++ b/lib/matplotlib/backend_managers.py @@ -0,0 +1,411 @@ +""" +: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:`ToolContainerBase` + The base class for the Toolbar class of each interactive backend. + +:class:`StatusbarBase` + The base class for the messaging area. +""" + +from __future__ import (absolute_import, division, print_function, + unicode_literals) +import six +import warnings +import sys + +import matplotlib.cbook as cbook +import matplotlib.widgets as widgets +from matplotlib.rcsetup import validate_stringlist +import matplotlib.backend_tools as tools + +try: + from importlib import import_module +except: + # simple python 2.6 implementation (no relative imports) + def import_module(name): + __import__(name) + return sys.modules[name] + + +class ToolEvent(object): + """Event for tool manipulation (add/remove)""" + 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, data) + self.canvasevent = canvasevent + + +class ToolManagerMessageEvent(object): + """ + Event carrying messages from toolmanager + + Messages usually get displayed to the user by the toolbar + """ + def __init__(self, name, sender, message): + self.name = name + self.sender = sender + self.message = message + + +class ToolManager(object): + """ + Helper class that groups all the user interactions for a FigureManager + + Attributes + ---------- + manager: `FigureManager` + keypresslock: `widgets.LockDraw` + `LockDraw` object to know if the `canvas` key_press_event is locked + messagelock: `widgets.LockDraw` + `LockDraw` object to know if the message is available to write + """ + + def __init__(self, canvas): + self.canvas = canvas + + self._key_press_handler_id = self.canvas.mpl_connect( + 'key_press_event', self._key_press) + + self._tools = {} + self._keys = {} + self._toggled = {} + self._callbacks = cbook.CallbackRegistry() + + # to process keypress event + self.keypresslock = widgets.LockDraw() + self.messagelock = widgets.LockDraw() + + def toolmanager_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_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 + Function to be called with signature + def func(event) + """ + return self._callbacks.connect(s, func) + + def toolmanager_disconnect(self, cid): + """ + Disconnect callback id cid + + Example usage:: + + cid = toolmanager.toolmanager_connect('tool_trigger_zoom', + on_press) + #...later + toolmanager.toolmanager_disconnect(cid) + """ + return self._callbacks.disconnect(cid) + + def message_event(self, message, sender=None): + """ Emit a `ToolManagerMessageEvent`""" + if sender is None: + sender = self + + s = 'tool_message_event' + event = ToolManagerMessageEvent(s, sender, message) + self._callbacks.process(s, event) + + @property + def active_toggle(self): + """Currently toggled tools""" + + return self._toggled + + def get_tool_keymap(self, name): + """ + Get the keymap associated with the specified tool + + Parameters + ---------- + name : string + Name of the Tool + + Returns + ---------- + list : list of keys associated with the Tool + """ + + keys = [k for k, i in six.iteritems(self._keys) if i == name] + return keys + + def _remove_keys(self, name): + for k in self.get_tool_keymap(name): + del self._keys[k] + + def update_keymap(self, name, *keys): + """ + Set the keymap to associate with the specified tool + + Parameters + ---------- + name : string + Name of the Tool + keys : keys to associate with the Tool + """ + + if name not in self._tools: + raise KeyError('%s not in Tools' % name) + + self._remove_keys(name) + + 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 remove_tool(self, name): + """ + Remove tool from `ToolManager` + + Parameters + ---------- + name : string + Name of the Tool + """ + + tool = self.get_tool(name) + tool.destroy() + + # If is a toggle tool and toggled, untoggle + if getattr(tool, 'toggled', False): + self.trigger_tool(tool, 'toolmanager') + + self._remove_keys(name) + + s = 'tool_removed_event' + event = ToolEvent(s, self, tool) + self._callbacks.process(s, event) + + del self._tools[name] + + def add_tool(self, name, tool, *args, **kwargs): + """ + Add tool to `ToolManager` + + Add a tool to the tools controlled by ToolManager + + 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 : str + Name of the tool, treated as the ID, has to be unique + tool : class_like, i.e. str or type + Reference to find the class of the Tool to added. + + Notes + ----- + args and kwargs get passed directly to the tools constructor. + + See Also + -------- + matplotlib.backend_tools.ToolBase : The base class for tools. + """ + + tool_cls = self._get_cls_to_instantiate(tool) + if not tool_cls: + raise ValueError('Impossible to find class for %s' % str(tool)) + + if name in self._tools: + warnings.warn('A "Tool class" with the same name already exists, ' + 'not added') + return self._tools[name] + + tool_obj = tool_cls(self, name, *args, **kwargs) + self._tools[name] = tool_obj + + if tool_cls.default_keymap is not None: + self.update_keymap(name, tool_cls.default_keymap) + + # For toggle tools init the radio_group in self._toggled + if isinstance(tool_obj, tools.ToolToggleBase): + # None group is not mutually exclusive, a set is used to keep track + # of all toggled tools in this group + if tool_obj.radio_group is None: + self._toggled.setdefault(None, set()) + else: + self._toggled.setdefault(tool_obj.radio_group, None) + + self._tool_added_event(tool_obj) + return tool_obj + + def _tool_added_event(self, tool): + s = 'tool_added_event' + event = ToolEvent(s, self, tool) + self._callbacks.process(s, event) + + def _handle_toggle(self, tool, sender, canvasevent, data): + """ + Toggle tools, need to untoggle prior to using other Toggle tool + Called from trigger_tool + + Parameters + ---------- + tool: Tool object + sender: object + Object that wishes to trigger the tool + canvasevent : Event + Original Canvas event or None + data : Object + Extra data to pass to the tool when triggering + """ + + 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 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 + # toggle it + elif self._toggled[radio_group] is None: + toggled = tool.name + # Other tool in the radio_group is toggled + else: + # Untoggle previously toggled tool + self.trigger_tool(self._toggled[radio_group], + self, + canvasevent, + data) + toggled = tool.name + + # 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) + + 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(): + callback_class = globals()[callback_class] + else: + mod = 'backend_tools' + current_module = __import__(mod, + globals(), locals(), [mod], -1) + + callback_class = getattr(current_module, callback_class, False) + if callable(callback_class): + return callback_class + else: + return None + + def trigger_tool(self, name, sender=None, canvasevent=None, + data=None): + """ + Trigger a tool and emit the tool_trigger_[name] event + + Parameters + ---------- + name : string + Name of the tool + sender: object + Object that wishes to trigger the tool + canvasevent : Event + Original Canvas event or None + data : Object + Extra data to pass to the tool when triggering + """ + tool = self.get_tool(name) + if tool is None: + return + + if sender is None: + sender = self + + 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 + + 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) + + def _key_press(self, event): + if event.key is None or self.keypresslock.locked(): + return + + name = self._keys.get(event.key, None) + if name is None: + return + self.trigger_tool(name, canvasevent=event) + + @property + def tools(self): + """Return the tools controlled by `ToolManager`""" + + return self._tools + + def get_tool(self, name, warn=True): + """ + Return the tool object, also accepts the actual tool for convenience + + Parameters + ----------- + name : str, ToolBase + Name of the tool, or the tool itself + warn : bool, optional + If this method should give warnings. + """ + if isinstance(name, tools.ToolBase) and name.name in self._tools: + return name + if name not in self._tools: + if warn: + warnings.warn("ToolManager does not control tool %s" % name) + return None + return self._tools[name] diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index ece79409e196..adc00ff0395d 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -1,13 +1,13 @@ """ Abstract base classes define the primitives for Tools. -These tools are used by `NavigationBase` +These tools are used by `ToolManager` :class:`ToolBase` Simple stateless tool :class:`ToolToggleBase` Tool that has two states, only one Toggle tool can be - active at any given time for the same `Navigation` + active at any given time for the same `ToolManager` """ @@ -33,17 +33,17 @@ 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` + The tool is instantiated by `matplotlib.backend_managers.ToolManager` Attributes ---------- - navigation: `matplotlib.backend_bases.NavigationBase` - Navigation that controls this Tool + toolmanager: `matplotlib.backend_managers.ToolManager` + ToolManager that controls this Tool 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 + ToolManager """ default_keymap = None @@ -70,11 +70,11 @@ class ToolBase(object): `name` is used as a label in the toolbar button """ - def __init__(self, navigation, name): + def __init__(self, toolmanager, name): self._name = name self._figure = None - self.navigation = navigation - self.figure = navigation.canvas.figure + self.toolmanager = toolmanager + self.figure = toolmanager.canvas.figure @property def figure(self): @@ -85,7 +85,7 @@ def trigger(self, sender, event, data=None): Called when this tool gets used This method is called by - `matplotlib.backend_bases.NavigationBase.trigger_tool` + `matplotlib.backend_managers.ToolManager.trigger_tool` Parameters ---------- @@ -123,7 +123,7 @@ def destroy(self): Destroy the tool This method is called when the tool is removed by - `matplotlib.backend_bases.NavigationBase.remove_tool` + `matplotlib.backend_managers.ToolManager.remove_tool` """ pass @@ -175,9 +175,9 @@ def disable(self, event=None): This can happen in different circumstances * Click on the toolbar tool button - * Call to `matplotlib.backend_bases.NavigationBase.trigger_tool` + * Call to `matplotlib.backend_managers.ToolManager.trigger_tool` * Another `ToolToggleBase` derived tool is triggered - (from the same `Navigation`) + (from the same `ToolManager`) """ pass @@ -203,10 +203,11 @@ def __init__(self, *args, **kwargs): self._cursor = None self._default_cursor = cursors.POINTER self._last_cursor = self._default_cursor - self.navigation.nav_connect('tool_added_event', self._add_tool_cbk) + self.toolmanager.toolmanager_connect('tool_added_event', + self._add_tool_cbk) # process current tools - for tool in self.navigation.tools.values(): + for tool in self.toolmanager.tools.values(): self._add_tool(tool) def _tool_trigger_cbk(self, event): @@ -220,8 +221,8 @@ def _tool_trigger_cbk(self, event): def _add_tool(self, tool): """set the cursor when the tool is triggered""" if getattr(tool, 'cursor', None) is not None: - self.navigation.nav_connect('tool_trigger_%s' % tool.name, - self._tool_trigger_cbk) + self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name, + self._tool_trigger_cbk) def _add_tool_cbk(self, event): """Process every newly added tool""" @@ -265,8 +266,8 @@ def __init__(self, *args, **kwargs): 'motion_notify_event', self.send_message) def send_message(self, event): - """Call `matplotlib.backend_bases.NavigationBase.message_event`""" - if self.navigation.messagelock.locked(): + """Call `matplotlib.backend_managers.ToolManager.message_event`""" + if self.toolmanager.messagelock.locked(): return message = ' ' @@ -278,7 +279,7 @@ def send_message(self, event): pass else: message = s - self.navigation.message_event(message, self) + self.toolmanager.message_event(message, self) class RubberbandBase(ToolBase): @@ -320,9 +321,9 @@ def trigger(self, sender, event, data=None): class ToolEnableAllNavigation(ToolBase): - """Tool to enable all axes for navigation interaction""" + """Tool to enable all axes for toolmanager interaction""" - description = 'Enables all axes navigation' + description = 'Enables all axes toolmanager' default_keymap = rcParams['keymap.all_axes'] def trigger(self, sender, event, data=None): @@ -336,9 +337,9 @@ def trigger(self, sender, event, data=None): class ToolEnableNavigation(ToolBase): - """Tool to enable a specific axes for navigation interaction""" + """Tool to enable a specific axes for toolmanager interaction""" - description = 'Enables one axes navigation' + description = 'Enables one axes toolmanager' default_keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9) def trigger(self, sender, event, data=None): @@ -539,9 +540,10 @@ class ViewsPositionsBase(ToolBase): _on_trigger = None def trigger(self, sender, event, data=None): - self.navigation.get_tool(_views_positions).add_figure() - getattr(self.navigation.get_tool(_views_positions), self._on_trigger)() - self.navigation.get_tool(_views_positions).update_view() + self.toolmanager.get_tool(_views_positions).add_figure() + getattr(self.toolmanager.get_tool(_views_positions), + self._on_trigger)() + self.toolmanager.get_tool(_views_positions).update_view() class ToolHome(ViewsPositionsBase): @@ -616,7 +618,7 @@ def disable(self, event): self.figure.canvas.mpl_disconnect(self._idScroll) def trigger(self, sender, event, data=None): - self.navigation.get_tool(_views_positions).add_figure() + self.toolmanager.get_tool(_views_positions).add_figure() ToolToggleBase.trigger(self, sender, event, data) def scroll_zoom(self, event): @@ -664,8 +666,8 @@ def __init__(self, *args): def _cancel_action(self): for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) - self.navigation.trigger_tool('rubberband', self) - self.navigation.get_tool(_views_positions).refresh_locators() + self.toolmanager.trigger_tool('rubberband', self) + self.toolmanager.get_tool(_views_positions).refresh_locators() self._xypress = None self._button_pressed = None self._ids_zoom = [] @@ -733,9 +735,9 @@ def _mouse_move(self, event): x1, y1, x2, y2 = a.bbox.extents x, lastx = x1, x2 - self.navigation.trigger_tool('rubberband', - self, - data=(x, y, lastx, lasty)) + self.toolmanager.trigger_tool('rubberband', + self, + data=(x, y, lastx, lasty)) def _release(self, event): """the release mouse button callback in zoom to rect mode""" @@ -856,7 +858,7 @@ def _release(self, event): a.set_ylim((ry1, ry2)) self._zoom_mode = None - self.navigation.get_tool(_views_positions).push_current() + self.toolmanager.get_tool(_views_positions).push_current() self._cancel_action() @@ -877,8 +879,8 @@ def _cancel_action(self): self._button_pressed = None self._xypress = [] self.figure.canvas.mpl_disconnect(self._idDrag) - self.navigation.messagelock.release(self) - self.navigation.get_tool(_views_positions).refresh_locators() + self.toolmanager.messagelock.release(self) + self.toolmanager.get_tool(_views_positions).refresh_locators() def _press(self, event): if event.button == 1: @@ -897,7 +899,7 @@ 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.messagelock(self) + self.toolmanager.messagelock(self) self._idDrag = self.figure.canvas.mpl_connect( 'motion_notify_event', self._mouse_move) @@ -907,7 +909,7 @@ def _release(self, event): return self.figure.canvas.mpl_disconnect(self._idDrag) - self.navigation.messagelock.release(self) + self.toolmanager.messagelock.release(self) for a, _ind in self._xypress: a.end_pan() @@ -915,7 +917,7 @@ def _release(self, event): self._cancel_action() return - self.navigation.get_tool(_views_positions).push_current() + self.toolmanager.get_tool(_views_positions).push_current() self._cancel_action() def _mouse_move(self, event): @@ -923,7 +925,7 @@ 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.canvas.draw_idle() + self.toolmanager.canvas.draw_idle() default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward, @@ -940,7 +942,8 @@ def _mouse_move(self, event): 'position': ToolCursorPosition, _views_positions: ToolViewsPositions, 'cursor': 'ToolSetCursor', - 'rubberband': 'ToolRubberband'} + 'rubberband': 'ToolRubberband', + } """Default tools""" default_toolbar_tools = [['navigation', ['home', 'back', 'forward']], @@ -950,21 +953,21 @@ def _mouse_move(self, event): """Default tools in the toolbar""" -def add_tools_2_navigation(navigation, tools=default_tools): +def add_tools_2_toolmanager(toolmanager, tools=default_tools): """ - Add multiple tools to `Navigation` + Add multiple tools to `ToolManager` Parameters ---------- - navigation: Navigation - `backend_bases.NavigationBase` object that will get the tools added + toolmanager: ToolManager + `backend_managers.ToolManager` object that will get the tools added tools : {str: class_like}, optional The tools to add in a {name: tool} dict, see `add_tool` for more info. """ for name, tool in six.iteritems(tools): - navigation.add_tool(name, tool) + toolmanager.add_tool(name, tool) def add_tools_2_container(container, tools=default_toolbar_tools): diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 4eeba2f0d303..e2729082a94f 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -31,13 +31,9 @@ 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, ToolContainerBase, - NavigationBase, StatusbarBase) -from matplotlib.backend_tools import (SaveFigureBase, - ConfigureSubplotsBase, - add_tools_2_navigation, - add_tools_2_container, - SetCursorBase, - RubberbandBase) + StatusbarBase) +from matplotlib.backend_managers import ToolManager +import matplotlib.backend_tools as tools from matplotlib.cbook import is_string_like, is_writable_file_like from matplotlib.colors import colorConverter @@ -425,7 +421,7 @@ def __init__(self, canvas, num): w = int (self.canvas.figure.bbox.width) h = int (self.canvas.figure.bbox.height) - self.navigation = self._get_navigation() + self.toolmanager = self._get_toolmanager() self.toolbar = self._get_toolbar() self.statusbar = None @@ -435,12 +431,10 @@ def add_widget(child, expand, fill, padding): size_request = child.size_request() return size_request.height - if matplotlib.rcParams['toolbar'] == 'navigation': - add_tools_2_navigation(self.navigation) -# self.navigation.add_tools(tools) -# self.toolbar.add_tools(toolbar_tools) - add_tools_2_container(self.toolbar) - self.statusbar = StatusbarGTK3(self.navigation) + if matplotlib.rcParams['toolbar'] == 'toolmanager': + tools.add_tools_2_toolmanager(self.toolmanager) + tools.add_tools_2_container(self.toolbar) + self.statusbar = StatusbarGTK3(self.toolmanager) h += add_widget(self.statusbar, False, False, 0) h += add_widget(Gtk.HSeparator(), False, False, 0) @@ -459,7 +453,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: + if self.toolmanager is not None: pass elif self.toolbar is not None: self.toolbar.update() @@ -498,19 +492,19 @@ def _get_toolbar(self): # attrs are set if rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2GTK3 (self.canvas, self.window) - elif rcParams['toolbar'] == 'navigation': - toolbar = ToolbarGTK3(self.navigation) + elif rcParams['toolbar'] == 'toolmanager': + toolbar = ToolbarGTK3(self.toolmanager) else: toolbar = None return toolbar - def _get_navigation(self): + def _get_toolmanager(self): # must be initialised after toolbar has been setted if rcParams['toolbar'] != 'toolbar2': - navigation = NavigationGTK3(self.canvas) + toolmanager = ToolManager(self.canvas) else: - navigation = None - return navigation + toolmanager = None + return toolmanager def get_window_title(self): return self.window.get_title() @@ -737,13 +731,9 @@ def get_filename_from_user (self): return filename, self.ext -class NavigationGTK3(NavigationBase): - pass - - -class RubberbandGTK3(RubberbandBase): +class RubberbandGTK3(tools.RubberbandBase): def __init__(self, *args, **kwargs): - RubberbandBase.__init__(self, *args, **kwargs) + tools.RubberbandBase.__init__(self, *args, **kwargs) self.ctx = None def draw_rubberband(self, x0, y0, x1, y1): @@ -768,12 +758,12 @@ def draw_rubberband(self, x0, y0, x1, y1): self.ctx.set_source_rgb(0, 0, 0) self.ctx.stroke() -ToolRubberband = RubberbandGTK3 +tools.ToolRubberband = RubberbandGTK3 class ToolbarGTK3(ToolContainerBase, Gtk.Box): - def __init__(self, navigation): - ToolContainerBase.__init__(self, navigation) + def __init__(self, toolmanager): + ToolContainerBase.__init__(self, toolmanager) Gtk.Box.__init__(self) self.set_property("orientation", Gtk.Orientation.VERTICAL) @@ -786,7 +776,6 @@ def __init__(self, navigation): def add_toolitem(self, name, group, position, image_file, description, toggle): - if toggle: tbutton = Gtk.ToggleToolButton() else: @@ -832,7 +821,7 @@ def toggle_toolitem(self, name, toggled): def remove_toolitem(self, name): if name not in self._toolitems: - self.navigation.message_event('%s Not in toolbar' % name, self) + self.toolmanager.message_event('%s Not in toolbar' % name, self) return for group in self._groups: @@ -859,7 +848,7 @@ def set_message(self, s): self.push(self._context, s) -class SaveFigureGTK3(SaveFigureBase): +class SaveFigureGTK3(tools.SaveFigureBase): def get_filechooser(self): fc = FileChooserDialog( @@ -890,19 +879,19 @@ def trigger(self, *args, **kwargs): except Exception as e: error_msg_gtk(str(e), parent=self) -ToolSaveFigure = SaveFigureGTK3 +tools.ToolSaveFigure = SaveFigureGTK3 -class SetCursorGTK3(SetCursorBase): +class SetCursorGTK3(tools.SetCursorBase): def set_cursor(self, cursor): self.figure.canvas.get_property("window").set_cursor(cursord[cursor]) -ToolSetCursor = SetCursorGTK3 +tools.ToolSetCursor = SetCursorGTK3 -class ConfigureSubplotsGTK3(ConfigureSubplotsBase, Gtk.Window): +class ConfigureSubplotsGTK3(tools.ConfigureSubplotsBase, Gtk.Window): def __init__(self, *args, **kwargs): - ConfigureSubplotsBase.__init__(self, *args, **kwargs) + tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs) self.window = None def init_window(self): @@ -953,7 +942,7 @@ def trigger(self, sender, event, data=None): self.window.present() -ToolConfigureSubplots = ConfigureSubplotsGTK3 +tools.ToolConfigureSubplots = ConfigureSubplotsGTK3 class DialogLineprops: @@ -1142,6 +1131,5 @@ def error_msg_gtk(msg, parent=None): 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 b0291beb189c..9ec851d2c90b 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -21,13 +21,9 @@ from matplotlib.backend_bases import FigureManagerBase, FigureCanvasBase from matplotlib.backend_bases import NavigationToolbar2, cursors, TimerBase from matplotlib.backend_bases import (ShowBase, ToolContainerBase, - NavigationBase, StatusbarBase) -from matplotlib.backend_tools import (SaveFigureBase, - ConfigureSubplotsBase, - add_tools_2_navigation, - add_tools_2_container, - SetCursorBase, - RubberbandBase) + StatusbarBase) +from matplotlib.backend_managers import ToolManager +import matplotlib.backend_tools as tools from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure @@ -539,20 +535,20 @@ def __init__(self, canvas, num, window): self.canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) self._num = num - self.navigation = self._get_navigation() + self.toolmanager = self._get_toolmanager() self.toolbar = self._get_toolbar() self.statusbar = None - if matplotlib.rcParams['toolbar'] == 'navigation': - add_tools_2_navigation(self.navigation) - add_tools_2_container(self.toolbar) - self.statusbar = StatusbarTk(self.window, self.navigation) + if matplotlib.rcParams['toolbar'] == 'toolmanager': + tools.add_tools_2_toolmanager(self.toolmanager) + tools.add_tools_2_container(self.toolbar) + self.statusbar = StatusbarTk(self.window, self.toolmanager) self._shown = False def notify_axes_change(fig): 'this will be called whenever the current axes is changed' - if self.navigation is not None: + if self.toolmanager is not None: pass elif self.toolbar is not None: self.toolbar.update() @@ -561,19 +557,18 @@ def notify_axes_change(fig): def _get_toolbar(self): if matplotlib.rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2TkAgg(self.canvas, self.window) - elif matplotlib.rcParams['toolbar'] == 'navigation': - toolbar = ToolbarTk(self.navigation, self.window) + elif matplotlib.rcParams['toolbar'] == 'toolmanager': + toolbar = ToolbarTk(self.toolmanager, self.window) else: toolbar = None return toolbar - def _get_navigation(self): - # must be inited after toolbar is setted + def _get_toolmanager(self): if rcParams['toolbar'] != 'toolbar2': - navigation = NavigationTk(self.canvas) + toolmanager = ToolManager(self.canvas) else: - navigation = None - return navigation + toolmanager = None + return toolmanager def resize(self, width, height=None): # before 09-12-22, the resize method takes a single *event* @@ -903,13 +898,9 @@ def hidetip(self): tw.destroy() -class NavigationTk(NavigationBase): - pass - - -class RubberbandTk(RubberbandBase): +class RubberbandTk(tools.RubberbandBase): def __init__(self, *args, **kwargs): - RubberbandBase.__init__(self, *args, **kwargs) + tools.RubberbandBase.__init__(self, *args, **kwargs) def draw_rubberband(self, x0, y0, x1, y1): height = self.figure.canvas.figure.bbox.height @@ -932,20 +923,16 @@ def remove_rubberband(self): self.figure.canvas._tkcanvas.delete(self.lastrect) del self.lastrect -ToolRubberband = RubberbandTk - -class SetCursorTk(SetCursorBase): +class SetCursorTk(tools.SetCursorBase): def set_cursor(self, cursor): self.figure.canvas.manager.window.configure(cursor=cursord[cursor]) -ToolSetCursor = SetCursorTk - class ToolbarTk(ToolContainerBase, Tk.Frame): def __init__(self, navigation, window): ToolContainerBase.__init__(self, navigation) - xmin, xmax = self.navigation.canvas.figure.bbox.intervalx + xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx height, width = 50, xmax - xmin Tk.Frame.__init__(self, master=window, width=int(width), height=int(height), @@ -996,7 +983,7 @@ def remove_toolitem(self, name): class StatusbarTk(StatusbarBase, Tk.Frame): def __init__(self, window, *args, **kwargs): StatusbarBase.__init__(self, *args, **kwargs) - xmin, xmax = self.navigation.canvas.figure.bbox.intervalx + xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx height, width = 50, xmax - xmin Tk.Frame.__init__(self, master=window, width=int(width), height=int(height), @@ -1010,7 +997,7 @@ def set_message(self, s): self._message.set(s) -class SaveFigureTk(SaveFigureBase): +class SaveFigureTk(tools.SaveFigureBase): def trigger(self, *args): from six.moves import tkinter_tkfiledialog, tkinter_messagebox filetypes = self.figure.canvas.get_supported_filetypes().copy() @@ -1063,9 +1050,9 @@ def trigger(self, *args): tkinter_messagebox.showerror("Error saving file", str(e)) -class ConfigureSubplotsTk(ConfigureSubplotsBase): +class ConfigureSubplotsTk(tools.ConfigureSubplotsBase): def __init__(self, *args, **kwargs): - ConfigureSubplotsBase.__init__(self, *args, **kwargs) + tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs) self.window = None def trigger(self, *args): @@ -1091,9 +1078,10 @@ def destroy(self, *args, **kwargs): self.window = None -ToolSaveFigure = SaveFigureTk -ToolConfigureSubplots = ConfigureSubplotsTk +tools.ToolSaveFigure = SaveFigureTk +tools.ToolConfigureSubplots = ConfigureSubplotsTk +tools.ToolSetCursor = SetCursorTk +tools.ToolRubberband = RubberbandTk Toolbar = ToolbarTk -Navigation = NavigationTk FigureCanvas = FigureCanvasTkAgg FigureManager = FigureManagerTkAgg diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 40a24c247872..f7263ca7e383 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', 'navigation'], + ['None', 'toolbar2', 'toolmanager'], ignorecase=True) return validator(s) From 48a6971def9757fea339458af884b552239bbea9 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 2 Apr 2015 15:08:18 -0400 Subject: [PATCH 61/69] tkagg updated for toolmanager and tool groups --- examples/user_interfaces/toolmanager.py | 5 ++- lib/matplotlib/backends/backend_tkagg.py | 40 +++++++++++++++++------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/examples/user_interfaces/toolmanager.py b/examples/user_interfaces/toolmanager.py index 88fcb73a6842..eaedf1832319 100644 --- a/examples/user_interfaces/toolmanager.py +++ b/examples/user_interfaces/toolmanager.py @@ -8,8 +8,7 @@ class allows to: from __future__ import print_function import matplotlib -# matplotlib.use('GTK3Cairo') -matplotlib.use('Tkagg') +matplotlib.use('GTK3Cairo') matplotlib.rcParams['toolbar'] = 'toolmanager' import matplotlib.pyplot as plt from matplotlib.backend_tools import ToolBase @@ -63,7 +62,7 @@ def trigger(self, *args, **kwargs): # Add the custom tools that we created fig.canvas.manager.toolmanager.add_tool('List', ListTools) -# fig.canvas.manager.toolmanager.add_tool('copy', CopyToolGTK3) +fig.canvas.manager.toolmanager.add_tool('copy', CopyToolGTK3) # Add an existing tool to new group `foo`. # It can be added as many times as we want diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 9ec851d2c90b..614100f04109 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -939,25 +939,41 @@ def __init__(self, navigation, window): borderwidth=2) self._toolitems = {} self.pack(side=Tk.TOP, fill=Tk.X) + self._groups = {} def add_toolitem(self, name, group, position, image_file, description, toggle): - button = self._Button(name, image_file, toggle) + frame = self._get_groupframe(group) + button = self._Button(name, image_file, toggle, frame) if description is not None: ToolTip.createToolTip(button, description) - self._toolitems[name] = button - - def _Button(self, text, image_file, toggle): + self._toolitems.setdefault(name, []) + self._toolitems[name].append(button) + + def _get_groupframe(self, group): + if group not in self._groups: + if self._groups: + self._add_separator() + frame = Tk.Frame(master=self, borderwidth=0) + frame.pack(side=Tk.LEFT, fill=Tk.Y) + self._groups[group] = frame + return self._groups[group] + + def _add_separator(self): + separator = Tk.Frame(master=self, bd=5, width=1, bg='black') + separator.pack(side=Tk.LEFT, fill=Tk.Y, padx=2) + + def _Button(self, text, image_file, toggle, frame): 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=frame, 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, + b = Tk.Checkbutton(master=frame, text=text, padx=2, pady=2, image=im, indicatoron=False, command=lambda: self._button_click(text)) b._ntimage = im @@ -970,13 +986,15 @@ def _button_click(self, name): def toggle_toolitem(self, name, toggled): if name not in self._toolitems: return - if toggled: - self._toolitems[name].select() - else: - self._toolitems[name].deselect() + for toolitem in self._toolitems[name]: + if toggled: + toolitem.select() + else: + toolitem.deselect() def remove_toolitem(self, name): - self._toolitems[name].pack_forget() + for toolitem in self._toolitems[name]: + toolitem.pack_forget() del self._toolitems[name] From 8dafe09e035fff4341e27fff940c88e9a5cf8ccb Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 2 Apr 2015 15:59:52 -0400 Subject: [PATCH 62/69] doc and minor code organization --- doc/api/backend_managers_api.rst | 8 ++++++++ doc/api/index_backend_api.rst | 1 + lib/matplotlib/backend_bases.py | 6 ++++++ lib/matplotlib/backend_managers.py | 22 +++------------------- lib/matplotlib/backend_tools.py | 5 +++-- lib/matplotlib/backends/backend_gtk3.py | 15 ++++++--------- 6 files changed, 27 insertions(+), 30 deletions(-) create mode 100644 doc/api/backend_managers_api.rst diff --git a/doc/api/backend_managers_api.rst b/doc/api/backend_managers_api.rst new file mode 100644 index 000000000000..86d1c383b966 --- /dev/null +++ b/doc/api/backend_managers_api.rst @@ -0,0 +1,8 @@ + +:mod:`matplotlib.backend_managers` +=================================== + +.. automodule:: matplotlib.backend_managers + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/index_backend_api.rst b/doc/api/index_backend_api.rst index 6e419ac2156f..f02901f04f83 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_managers_api.rst backend_tools_api.rst backend_gtkagg_api.rst backend_qt4agg_api.rst diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index efebdd618e1e..1d49cd732ec4 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -24,6 +24,12 @@ The base class for the Show class of each interactive backend; the 'show' callable is then set to Show.__call__, inherited from ShowBase. + +:class:`ToolContainerBase` + The base class for the Toolbar class of each interactive backend. + +:class:`StatusbarBase` + The base class for the messaging area. """ from __future__ import (absolute_import, division, print_function, diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py index 8e27b7644e1e..e99135bc55e3 100644 --- a/lib/matplotlib/backend_managers.py +++ b/lib/matplotlib/backend_managers.py @@ -1,35 +1,19 @@ """ -: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:`ToolContainerBase` - The base class for the Toolbar class of each interactive backend. - -:class:`StatusbarBase` - The base class for the messaging area. +`ToolManager` + Class that makes the bridge between user interaction (key press, + toolbar clicks, ..) and the actions in response to the user inputs. """ from __future__ import (absolute_import, division, print_function, unicode_literals) import six import warnings -import sys import matplotlib.cbook as cbook import matplotlib.widgets as widgets from matplotlib.rcsetup import validate_stringlist import matplotlib.backend_tools as tools -try: - from importlib import import_module -except: - # simple python 2.6 implementation (no relative imports) - def import_module(name): - __import__(name) - return sys.modules[name] - class ToolEvent(object): """Event for tool manipulation (add/remove)""" diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index adc00ff0395d..90dda9b93f73 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -1,13 +1,14 @@ """ Abstract base classes define the primitives for Tools. -These tools are used by `ToolManager` +These tools are used by `matplotlib.backend_managers.ToolManager` :class:`ToolBase` Simple stateless tool :class:`ToolToggleBase` Tool that has two states, only one Toggle tool can be - active at any given time for the same `ToolManager` + active at any given time for the same + `matplotlib.backend_managers.ToolManager` """ diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index e2729082a94f..1fe6d6dfe759 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -758,8 +758,6 @@ def draw_rubberband(self, x0, y0, x1, y1): self.ctx.set_source_rgb(0, 0, 0) self.ctx.stroke() -tools.ToolRubberband = RubberbandGTK3 - class ToolbarGTK3(ToolContainerBase, Gtk.Box): def __init__(self, toolmanager): @@ -879,15 +877,11 @@ def trigger(self, *args, **kwargs): except Exception as e: error_msg_gtk(str(e), parent=self) -tools.ToolSaveFigure = SaveFigureGTK3 - class SetCursorGTK3(tools.SetCursorBase): def set_cursor(self, cursor): self.figure.canvas.get_property("window").set_cursor(cursord[cursor]) -tools.ToolSetCursor = SetCursorGTK3 - class ConfigureSubplotsGTK3(tools.ConfigureSubplotsBase, Gtk.Window): def __init__(self, *args, **kwargs): @@ -942,9 +936,6 @@ def trigger(self, sender, event, data=None): self.window.present() -tools.ToolConfigureSubplots = ConfigureSubplotsGTK3 - - class DialogLineprops: """ A GUI dialog for controlling lineprops @@ -1130,6 +1121,12 @@ def error_msg_gtk(msg, parent=None): dialog.run() dialog.destroy() + +tools.ToolSaveFigure = SaveFigureGTK3 +tools.ToolConfigureSubplots = ConfigureSubplotsGTK3 +tools.ToolSetCursor = SetCursorGTK3 +tools.ToolRubberband = RubberbandGTK3 + Toolbar = ToolbarGTK3 FigureCanvas = FigureCanvasGTK3 FigureManager = FigureManagerGTK3 From a0695d06fb025edcf5aa25d3fd0e137742583bae Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 3 Apr 2015 10:31:58 -0400 Subject: [PATCH 63/69] whats new --- doc/users/whats_new/rcparams.rst | 8 +++ doc/users/whats_new/toolmanager.rst | 68 +++++++++++++++++++++++++ examples/user_interfaces/toolmanager.py | 5 +- lib/matplotlib/backend_bases.py | 2 +- 4 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 doc/users/whats_new/toolmanager.rst diff --git a/doc/users/whats_new/rcparams.rst b/doc/users/whats_new/rcparams.rst index 4e7d367c46cb..d7641c76b83f 100644 --- a/doc/users/whats_new/rcparams.rst +++ b/doc/users/whats_new/rcparams.rst @@ -15,9 +15,17 @@ Added "figure.titlesize" and "figure.titleweight" keys to rcParams Two new keys were added to rcParams to control the default font size and weight used by the figure title (as emitted by ``pyplot.suptitle()``). + ``image.composite_image`` added to rcParams ``````````````````````````````````````````` Controls whether vector graphics backends (i.e. PDF, PS, and SVG) combine multiple images on a set of axes into a single composite image. Saving each image individually can be useful if you generate vector graphics files in matplotlib and then edit the files further in Inkscape or other programs. + + +Added "toolmanager" to "toolbar" possible values +```````````````````````````````````````````````` + +The new value enables the use of ``ToolManager`` + diff --git a/doc/users/whats_new/toolmanager.rst b/doc/users/whats_new/toolmanager.rst new file mode 100644 index 000000000000..889333e6bace --- /dev/null +++ b/doc/users/whats_new/toolmanager.rst @@ -0,0 +1,68 @@ +ToolManager +----------- + +Federico Ariza wrote the new `matplotlib.backend_managers.ToolManager` that comes as replacement for `NavigationToolbar2` + +`ToolManager` offers a new way of looking at the user interactions with the figures. +Before we had the `NavigationToolbar2` with its own tools like `zoom/pan/home/save/...` and also we had the shortcuts like +`yscale/grid/quit/....` +`Toolmanager` relocate all those actions as `Tools` (located in `matplotlib.backend_tools`), and defines a way to `access/trigger/reconfigure` them. + +The `Toolbars` are replaced for `ToolContainers` that are just GUI interfaces to `trigger` the tools. But don't worry the default backends include a `ToolContainer` called `toolbar` + + +.. note:: + For the moment the `ToolManager` is working only with `GTK3` and `Tk` backends. + Make sure you are using one of those. + Port for the rest of the backends is comming soon. + + To activate the `ToolManager` include the following at the top of your file: + + >>> matplotlib.rcParams['toolbar'] = 'toolmanager' + + +Interact with the ToolContainer +``````````````````````````````` + +The most important feature is the ability to easily reconfigure the ToolContainer (aka toolbar). +For example, if we want to remove the "forward" button we would just do. + + >>> fig.canvas.manager.toolmanager.remove_tool('forward') + +Now if you want to programmatically trigger the "home" button + + >>> fig.canvas.manager.toolmanager.trigger_tool('home') + + +New Tools +````````` + +It is possible to add new tools to the ToolManager + +A very simple tool that prints "You're awesome" would be:: + + from matplotlib.backend_tools import ToolBase + class AwesomeTool(ToolBase): + def trigger(self, *args, **kwargs): + print("You're awesome") + + +To add this tool to `ToolManager` + + >>> fig.canvas.manager.toolmanager.add_tool('Awesome', AwesomeTool) + +If we want to add a shortcut ("d") for the tool + + >>> fig.canvas.manager.toolmanager.update_keymap('Awesome', 'd') + + +To add it to the toolbar inside the group 'foo' + + >>> fig.canvas.manager.toolbar.add_tool('Awesome', 'foo') + + +There is a second class of tools, "Toggleable Tools", this are almost the same as our basic tools, just that belong to a group, and are mutually exclusive inside that group. +For tools derived from `ToolToggleBase` there are two basic methods `enable` and `disable` that are called automatically whenever it is toggled. + + +A full example is located in :ref:`user_interfaces-toolmanager` diff --git a/examples/user_interfaces/toolmanager.py b/examples/user_interfaces/toolmanager.py index eaedf1832319..80e286f8b7f5 100644 --- a/examples/user_interfaces/toolmanager.py +++ b/examples/user_interfaces/toolmanager.py @@ -1,8 +1,9 @@ -'''This example demonstrates how the `matplotlib.backend_bases.NavigationBase` -class allows to: +'''This example demonstrates how to: * Modify the Toolbar +* Create tools * Add tools * Remove tools +Using `matplotlib.backend_managers.ToolManager` ''' diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 1d49cd732ec4..7ef777540a52 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3227,7 +3227,7 @@ class ToolContainerBase(object): """ Base class for all tool containers, e.g. toolbars. - Attributes + Attributes ---------- toolmanager : `ToolManager` object that holds the tools that this `ToolContainer` wants to communicate with. From 328b1692815490e29510514c55cddb706b1bf57c Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 3 Apr 2015 14:58:34 -0400 Subject: [PATCH 64/69] missing object from class declaration --- examples/user_interfaces/toolmanager.py | 5 +++-- lib/matplotlib/backend_managers.py | 6 ++---- lib/matplotlib/backends/backend_gtk3.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/user_interfaces/toolmanager.py b/examples/user_interfaces/toolmanager.py index 80e286f8b7f5..9d3e79e38348 100644 --- a/examples/user_interfaces/toolmanager.py +++ b/examples/user_interfaces/toolmanager.py @@ -17,7 +17,7 @@ class ListTools(ToolBase): - '''List all the tools controlled by `Navigation`''' + '''List all the tools controlled by the `ToolManager`''' # keyboard shortcut default_keymap = 'm' description = 'List Tools' @@ -72,7 +72,8 @@ def trigger(self, *args, **kwargs): # Remove the forward button fig.canvas.manager.toolmanager.remove_tool('forward') -# To add a custom tool to the toolbar at specific location +# To add a custom tool to the toolbar at specific location inside +# the navigation group fig.canvas.manager.toolbar.add_tool('List', 'navigation', 1) plt.show() diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py index e99135bc55e3..cfa73bcacbcf 100644 --- a/lib/matplotlib/backend_managers.py +++ b/lib/matplotlib/backend_managers.py @@ -99,7 +99,7 @@ def func(event) def toolmanager_disconnect(self, cid): """ - Disconnect callback id cid + Disconnect callback id *cid* Example usage:: @@ -196,9 +196,7 @@ def remove_tool(self, name): def add_tool(self, name, tool, *args, **kwargs): """ - Add tool to `ToolManager` - - Add a tool to the tools controlled by ToolManager + Add *tool* to `ToolManager` If successful adds a new event `tool_trigger_name` where **name** is the **name** of the tool, this event is fired everytime diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 1fe6d6dfe759..83f3bf310e85 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -936,7 +936,7 @@ def trigger(self, sender, event, data=None): self.window.present() -class DialogLineprops: +class DialogLineprops(object): """ A GUI dialog for controlling lineprops """ From aac4744aab88f1ac770f8c6c847b1edcea313ae2 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 3 Apr 2015 15:28:29 -0400 Subject: [PATCH 65/69] remove comments and docstrings --- lib/matplotlib/backend_managers.py | 2 -- lib/matplotlib/backends/backend_tkagg.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py index cfa73bcacbcf..41d80c96a9d1 100644 --- a/lib/matplotlib/backend_managers.py +++ b/lib/matplotlib/backend_managers.py @@ -294,8 +294,6 @@ def _handle_toggle(self, tool, sender, canvasevent, data): # 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) def _get_cls_to_instantiate(self, callback_class): # Find the class that corresponds to the tool diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 614100f04109..020b768222e3 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -930,8 +930,8 @@ def set_cursor(self, cursor): class ToolbarTk(ToolContainerBase, Tk.Frame): - def __init__(self, navigation, window): - ToolContainerBase.__init__(self, navigation) + def __init__(self, toolmanager, window): + ToolContainerBase.__init__(self, toolmanager) xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx height, width = 50, xmax - xmin Tk.Frame.__init__(self, master=window, From f09b9ef7c4af22c66bcf8edf9d28ee7577b742d1 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Fri, 3 Apr 2015 15:36:42 -0400 Subject: [PATCH 66/69] import with original name backend_tools --- lib/matplotlib/backends/backend_gtk3.py | 26 ++++++++++++------------ lib/matplotlib/backends/backend_tkagg.py | 26 ++++++++++++------------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 83f3bf310e85..63eaf0389e37 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -33,7 +33,7 @@ def fn_name(): return sys._getframe(1).f_code.co_name from matplotlib.backend_bases import (ShowBase, ToolContainerBase, StatusbarBase) from matplotlib.backend_managers import ToolManager -import matplotlib.backend_tools as tools +from matplotlib import backend_tools from matplotlib.cbook import is_string_like, is_writable_file_like from matplotlib.colors import colorConverter @@ -432,8 +432,8 @@ def add_widget(child, expand, fill, padding): return size_request.height if matplotlib.rcParams['toolbar'] == 'toolmanager': - tools.add_tools_2_toolmanager(self.toolmanager) - tools.add_tools_2_container(self.toolbar) + backend_tools.add_tools_2_toolmanager(self.toolmanager) + backend_tools.add_tools_2_container(self.toolbar) self.statusbar = StatusbarGTK3(self.toolmanager) h += add_widget(self.statusbar, False, False, 0) h += add_widget(Gtk.HSeparator(), False, False, 0) @@ -731,9 +731,9 @@ def get_filename_from_user (self): return filename, self.ext -class RubberbandGTK3(tools.RubberbandBase): +class RubberbandGTK3(backend_tools.RubberbandBase): def __init__(self, *args, **kwargs): - tools.RubberbandBase.__init__(self, *args, **kwargs) + backend_tools.RubberbandBase.__init__(self, *args, **kwargs) self.ctx = None def draw_rubberband(self, x0, y0, x1, y1): @@ -846,7 +846,7 @@ def set_message(self, s): self.push(self._context, s) -class SaveFigureGTK3(tools.SaveFigureBase): +class SaveFigureGTK3(backend_tools.SaveFigureBase): def get_filechooser(self): fc = FileChooserDialog( @@ -878,14 +878,14 @@ def trigger(self, *args, **kwargs): error_msg_gtk(str(e), parent=self) -class SetCursorGTK3(tools.SetCursorBase): +class SetCursorGTK3(backend_tools.SetCursorBase): def set_cursor(self, cursor): self.figure.canvas.get_property("window").set_cursor(cursord[cursor]) -class ConfigureSubplotsGTK3(tools.ConfigureSubplotsBase, Gtk.Window): +class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window): def __init__(self, *args, **kwargs): - tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs) + backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs) self.window = None def init_window(self): @@ -1122,10 +1122,10 @@ def error_msg_gtk(msg, parent=None): dialog.destroy() -tools.ToolSaveFigure = SaveFigureGTK3 -tools.ToolConfigureSubplots = ConfigureSubplotsGTK3 -tools.ToolSetCursor = SetCursorGTK3 -tools.ToolRubberband = RubberbandGTK3 +backend_tools.ToolSaveFigure = SaveFigureGTK3 +backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3 +backend_tools.ToolSetCursor = SetCursorGTK3 +backend_tools.ToolRubberband = RubberbandGTK3 Toolbar = ToolbarGTK3 FigureCanvas = FigureCanvasGTK3 diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 020b768222e3..8f29e0d3e51d 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -23,7 +23,7 @@ from matplotlib.backend_bases import (ShowBase, ToolContainerBase, StatusbarBase) from matplotlib.backend_managers import ToolManager -import matplotlib.backend_tools as tools +from matplotlib import backend_tools from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure @@ -540,8 +540,8 @@ def __init__(self, canvas, num, window): self.statusbar = None if matplotlib.rcParams['toolbar'] == 'toolmanager': - tools.add_tools_2_toolmanager(self.toolmanager) - tools.add_tools_2_container(self.toolbar) + backend_tools.add_tools_2_toolmanager(self.toolmanager) + backend_tools.add_tools_2_container(self.toolbar) self.statusbar = StatusbarTk(self.window, self.toolmanager) self._shown = False @@ -898,9 +898,9 @@ def hidetip(self): tw.destroy() -class RubberbandTk(tools.RubberbandBase): +class RubberbandTk(backend_tools.RubberbandBase): def __init__(self, *args, **kwargs): - tools.RubberbandBase.__init__(self, *args, **kwargs) + backend_tools.RubberbandBase.__init__(self, *args, **kwargs) def draw_rubberband(self, x0, y0, x1, y1): height = self.figure.canvas.figure.bbox.height @@ -924,7 +924,7 @@ def remove_rubberband(self): del self.lastrect -class SetCursorTk(tools.SetCursorBase): +class SetCursorTk(backend_tools.SetCursorBase): def set_cursor(self, cursor): self.figure.canvas.manager.window.configure(cursor=cursord[cursor]) @@ -1015,7 +1015,7 @@ def set_message(self, s): self._message.set(s) -class SaveFigureTk(tools.SaveFigureBase): +class SaveFigureTk(backend_tools.SaveFigureBase): def trigger(self, *args): from six.moves import tkinter_tkfiledialog, tkinter_messagebox filetypes = self.figure.canvas.get_supported_filetypes().copy() @@ -1068,9 +1068,9 @@ def trigger(self, *args): tkinter_messagebox.showerror("Error saving file", str(e)) -class ConfigureSubplotsTk(tools.ConfigureSubplotsBase): +class ConfigureSubplotsTk(backend_tools.ConfigureSubplotsBase): def __init__(self, *args, **kwargs): - tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs) + backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs) self.window = None def trigger(self, *args): @@ -1096,10 +1096,10 @@ def destroy(self, *args, **kwargs): self.window = None -tools.ToolSaveFigure = SaveFigureTk -tools.ToolConfigureSubplots = ConfigureSubplotsTk -tools.ToolSetCursor = SetCursorTk -tools.ToolRubberband = RubberbandTk +backend_tools.ToolSaveFigure = SaveFigureTk +backend_tools.ToolConfigureSubplots = ConfigureSubplotsTk +backend_tools.ToolSetCursor = SetCursorTk +backend_tools.ToolRubberband = RubberbandTk Toolbar = ToolbarTk FigureCanvas = FigureCanvasTkAgg FigureManager = FigureManagerTkAgg From def3a5227706818d546ed25b2f2651031bb55145 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 7 Apr 2015 06:35:49 -0400 Subject: [PATCH 67/69] rename 2 -> to, example without gtk only code --- examples/user_interfaces/toolmanager.py | 43 +++++++++++++++--------- lib/matplotlib/backend_tools.py | 4 +-- lib/matplotlib/backends/backend_gtk3.py | 4 +-- lib/matplotlib/backends/backend_tkagg.py | 4 +-- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/examples/user_interfaces/toolmanager.py b/examples/user_interfaces/toolmanager.py index 9d3e79e38348..5240bab239c2 100644 --- a/examples/user_interfaces/toolmanager.py +++ b/examples/user_interfaces/toolmanager.py @@ -12,7 +12,7 @@ matplotlib.use('GTK3Cairo') matplotlib.rcParams['toolbar'] = 'toolmanager' import matplotlib.pyplot as plt -from matplotlib.backend_tools import ToolBase +from matplotlib.backend_tools import ToolBase, ToolToggleBase from gi.repository import Gtk, Gdk @@ -44,26 +44,39 @@ def trigger(self, *args, **kwargs): print("{0:12} {1:45}".format(group, active)) -# ref: at https://github.com/matplotlib/matplotlib/issues/1987 -class CopyToolGTK3(ToolBase): - '''Copy canvas to clipboard''' - default_keymap = 'ctrl+c' - description = 'Copy canvas' +class GroupHideTool(ToolToggleBase): + '''Hide lines with a given gid''' + default_keymap = 'G' + description = 'Hide by gid' - def trigger(self, *args, **kwargs): - 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) + def __init__(self, *args, **kwargs): + self.gid = kwargs.pop('gid') + ToolToggleBase.__init__(self, *args, **kwargs) + + def enable(self, *args): + self.set_lines_visibility(False) + + def disable(self, *args): + self.set_lines_visibility(True) + + def set_lines_visibility(self, state): + gr_lines = [] + for ax in self.figure.get_axes(): + for line in ax.get_lines(): + if line.get_gid() == self.gid: + line.set_visible(state) + self.figure.canvas.draw() fig = plt.figure() -plt.plot([1, 2, 3]) +plt.plot([1, 2, 3], gid='mygroup') +plt.plot([2, 3, 4], gid='unknown') +plt.plot([3, 2, 1], gid='mygroup') # Add the custom tools that we created fig.canvas.manager.toolmanager.add_tool('List', ListTools) -fig.canvas.manager.toolmanager.add_tool('copy', CopyToolGTK3) +fig.canvas.manager.toolmanager.add_tool('Hide', GroupHideTool, gid='mygroup') + # Add an existing tool to new group `foo`. # It can be added as many times as we want @@ -74,6 +87,6 @@ def trigger(self, *args, **kwargs): # To add a custom tool to the toolbar at specific location inside # the navigation group -fig.canvas.manager.toolbar.add_tool('List', 'navigation', 1) +fig.canvas.manager.toolbar.add_tool('Hide', 'navigation', 1) plt.show() diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 90dda9b93f73..a420dc5cff6c 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -954,7 +954,7 @@ def _mouse_move(self, event): """Default tools in the toolbar""" -def add_tools_2_toolmanager(toolmanager, tools=default_tools): +def add_tools_to_manager(toolmanager, tools=default_tools): """ Add multiple tools to `ToolManager` @@ -971,7 +971,7 @@ def add_tools_2_toolmanager(toolmanager, tools=default_tools): toolmanager.add_tool(name, tool) -def add_tools_2_container(container, tools=default_toolbar_tools): +def add_tools_to_container(container, tools=default_toolbar_tools): """ Add multiple tools to the container. diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 63eaf0389e37..1f27c1961dce 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -432,8 +432,8 @@ def add_widget(child, expand, fill, padding): return size_request.height if matplotlib.rcParams['toolbar'] == 'toolmanager': - backend_tools.add_tools_2_toolmanager(self.toolmanager) - backend_tools.add_tools_2_container(self.toolbar) + backend_tools.add_tools_to_manager(self.toolmanager) + backend_tools.add_tools_to_container(self.toolbar) self.statusbar = StatusbarGTK3(self.toolmanager) h += add_widget(self.statusbar, False, False, 0) h += add_widget(Gtk.HSeparator(), False, False, 0) diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 8f29e0d3e51d..c5d7b779facc 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -540,8 +540,8 @@ def __init__(self, canvas, num, window): self.statusbar = None if matplotlib.rcParams['toolbar'] == 'toolmanager': - backend_tools.add_tools_2_toolmanager(self.toolmanager) - backend_tools.add_tools_2_container(self.toolbar) + backend_tools.add_tools_to_manager(self.toolmanager) + backend_tools.add_tools_to_container(self.toolbar) self.statusbar = StatusbarTk(self.window, self.toolmanager) self._shown = False From 9ee7e256da9ea5039cfe2b9df2e303d620a04009 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 7 Apr 2015 10:44:23 -0400 Subject: [PATCH 68/69] zoom pan buttons order --- 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 a420dc5cff6c..1d952f2417da 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -948,7 +948,7 @@ def _mouse_move(self, event): """Default tools""" default_toolbar_tools = [['navigation', ['home', 'back', 'forward']], - ['zoompan', ['zoom', 'pan']], + ['zoompan', ['pan', 'zoom']], ['layout', ['subplots']], ['io', ['save']]] """Default tools in the toolbar""" From 5eae4e12c657616e007c1ef4336a01f695cf3a43 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 7 Apr 2015 12:12:55 -0400 Subject: [PATCH 69/69] matplotlib.rcParams['toolbar'] == 'None' starts toolmanager but not toolbar --- lib/matplotlib/backends/backend_gtk3.py | 11 ++++++----- lib/matplotlib/backends/backend_tkagg.py | 7 ++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 1f27c1961dce..32d15fb4fbcc 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -431,12 +431,13 @@ def add_widget(child, expand, fill, padding): size_request = child.size_request() return size_request.height - if matplotlib.rcParams['toolbar'] == 'toolmanager': + if self.toolmanager: backend_tools.add_tools_to_manager(self.toolmanager) - backend_tools.add_tools_to_container(self.toolbar) - self.statusbar = StatusbarGTK3(self.toolmanager) - h += add_widget(self.statusbar, False, False, 0) - h += add_widget(Gtk.HSeparator(), False, False, 0) + if self.toolbar: + backend_tools.add_tools_to_container(self.toolbar) + self.statusbar = StatusbarGTK3(self.toolmanager) + h += add_widget(self.statusbar, False, False, 0) + h += add_widget(Gtk.HSeparator(), False, False, 0) if self.toolbar is not None: self.toolbar.show() diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index c5d7b779facc..8ffc8a85318e 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -539,10 +539,11 @@ def __init__(self, canvas, num, window): self.toolbar = self._get_toolbar() self.statusbar = None - if matplotlib.rcParams['toolbar'] == 'toolmanager': + if self.toolmanager: backend_tools.add_tools_to_manager(self.toolmanager) - backend_tools.add_tools_to_container(self.toolbar) - self.statusbar = StatusbarTk(self.window, self.toolmanager) + if self.toolbar: + backend_tools.add_tools_to_container(self.toolbar) + self.statusbar = StatusbarTk(self.window, self.toolmanager) self._shown = False