From 3dd0a3e16db8f5640a674276a2582dc021df64b3 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 15 Jan 2014 19:45:01 -0500 Subject: [PATCH 1/9] Navigation, Toolbar and Tool in place --- examples/user_interfaces/navigation.py | 10 + lib/matplotlib/backend_bases.py | 279 ++++++++++++++++++++---- lib/matplotlib/backends/backend_gtk3.py | 80 ++++++- 3 files changed, 325 insertions(+), 44 deletions(-) create mode 100644 examples/user_interfaces/navigation.py diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py new file mode 100644 index 000000000000..4c3b8b467a2c --- /dev/null +++ b/examples/user_interfaces/navigation.py @@ -0,0 +1,10 @@ +import matplotlib +matplotlib.use('GTK3AGG') +import matplotlib.pyplot as plt + +fig = plt.figure() +ax = fig.add_subplot(111) +ax.plot([1, 2, 3]) + +fig.canvas.manager.navigation.list_tools() +plt.show() \ No newline at end of file diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index c9212939b1a7..29827952f369 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2416,38 +2416,38 @@ def key_press_handler(event, canvas, toolbar=None): """ # these bindings happen whether you are over an axes or not - if event.key is None: - return +# if event.key is None: +# return # Load key-mappings from your matplotlibrc file. - fullscreen_keys = rcParams['keymap.fullscreen'] - home_keys = rcParams['keymap.home'] +# fullscreen_keys = rcParams['keymap.fullscreen'] +# home_keys = rcParams['keymap.home'] back_keys = rcParams['keymap.back'] forward_keys = rcParams['keymap.forward'] - pan_keys = rcParams['keymap.pan'] - zoom_keys = rcParams['keymap.zoom'] +# pan_keys = rcParams['keymap.pan'] +# zoom_keys = rcParams['keymap.zoom'] save_keys = rcParams['keymap.save'] - quit_keys = rcParams['keymap.quit'] - grid_keys = rcParams['keymap.grid'] +# quit_keys = rcParams['keymap.quit'] +# grid_keys = rcParams['keymap.grid'] toggle_yscale_keys = rcParams['keymap.yscale'] toggle_xscale_keys = rcParams['keymap.xscale'] all = rcParams['keymap.all_axes'] - # toggle fullscreen mode (default key 'f') - if event.key in fullscreen_keys: - canvas.manager.full_screen_toggle() +# # toggle fullscreen mode (default key 'f') +# if event.key in fullscreen_keys: +# canvas.manager.full_screen_toggle() # quit the figure (defaut key 'ctrl+w') - if event.key in quit_keys: - Gcf.destroy_fig(canvas.figure) +# if event.key in quit_keys: +# Gcf.destroy_fig(canvas.figure) if toolbar is not None: # home or reset mnemonic (default key 'h', 'home' and 'r') - if event.key in home_keys: - toolbar.home() +# if event.key in home_keys: +# toolbar.home() # forward / backward keys to enable left handed quick navigation # (default key for backward: 'left', 'backspace' and 'c') - elif event.key in back_keys: + if event.key in back_keys: toolbar.back() # (default key for forward: 'right' and 'v') elif event.key in forward_keys: @@ -2468,9 +2468,9 @@ def key_press_handler(event, canvas, toolbar=None): # these bindings require the mouse to be over an axes to trigger # switching on/off a grid in current axes (default key 'g') - if event.key in grid_keys: - event.inaxes.grid() - canvas.draw() +# if event.key in grid_keys: +# event.inaxes.grid() +# canvas.draw() # toggle scaling of y-axes between 'log and 'linear' (default key 'l') elif event.key in toggle_yscale_keys: ax = event.inaxes @@ -2513,6 +2513,192 @@ class NonGuiException(Exception): pass +class ToolBase(object): + keymap = None + position = None + description = None + name = None + image = None + toggle = False # Change the status (take control of the events) + + def __init__(self): + pass + + def action(self, figure, event): + print('Without action:', self.name, self.description) + + +class ToolQuit(ToolBase): + description = 'Quit the figure' + keymap = rcParams['keymap.quit'] + + def action(self, figure, event): + Gcf.destroy_fig(figure) + + +class ToolToggleGrid(ToolBase): + description = 'Toogle Grid' + keymap = rcParams['keymap.grid'] + + def action(self, figure, event): + if event.inaxes is None: + return + event.inaxes.grid() + figure.canvas.draw() + + +class ToolToggleFullScreen(ToolBase): + description = 'Toogle Fullscreen mode' + keymap = rcParams['keymap.fullscreen'] + + def action(self, figure, event): + figure.canvas.manager.full_screen_toggle() + + +class ToolHome(ToolBase): + description = 'Reset original view' + name = 'Home' + image = 'home' + keymap = rcParams['keymap.home'] + position = 0 + + +class ToolToggleBase(ToolBase): + toggle = True + capture_keypress = False + status = False + + def key_press(self, figure, event): + pass + +# def action(self, figure, event): +# self.status = not self.status + + +class ToolZoom(ToolToggleBase): + description = 'Zoom to rectangle' + name = 'Zoom' + image = 'zoom_to_rect' + position = 1 + keymap = rcParams['keymap.zoom'] + + +class ToolPan(ToolToggleBase): + keymap = rcParams['keymap.pan'] + name = 'Pan' + description = 'Pan axes with left mouse, zoom with right' + image = 'move' + position = 2 + + +class NavigationBase(object): + tools = [ToolToggleGrid, + ToolToggleFullScreen, + ToolQuit, + ToolHome, + ToolZoom, + ToolPan] + + 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._tools = {} + self._keys = {} + self._toggled = None + self._toolitems = {} # Toolbar items + + for tool in self.tools: + self.add_tool(tool) + + def _get_toolbar(self, toolbar, canvas): + # must be inited after the window, drawingArea and figure + # attrs are set + if rcParams['toolbar'] == 'toolbar2' and toolbar is not None: + toolbar = toolbar(canvas.manager) + else: + toolbar = None + return toolbar + + def add_tool(self, tool): + from matplotlib.rcsetup import validate_stringlist + instance = tool() + id_ = id(instance) + self._tools[id_] = instance + if instance.keymap is not None: + for k in validate_stringlist(instance.keymap): + self._keys[k] = id_ + + if self.toolbar and tool.position is not None: + basedir = os.path.join(rcParams['datapath'], 'images') + fname = os.path.join(basedir, tool.image + '.png') + toolitem = self.toolbar.add_toolitem(tool.name, tool.description, + fname, + tool.position, + tool.toggle, + id_) + self._toolitems[id_] = toolitem + + def key_press(self, event): + """ + Implement the default mpl key bindings defined at + :ref:`key-event-handling` + """ + + if event.key is None: + return + + if self._toggled and self._tools[self._toggled].capture_keypress: + self._tools[self._toggled].key_press(self.canvas.figure, event) + return + + id_ = self._keys.get(event.key, False) + if id_: + if id_ in self._toolitems: + #For toolbar items, it is safer to pass by the toolbar + #so no back and forth for toggling + self.toolbar.click(self._toolitems[id_], id_) + else: + self._tools[id_].action(self.canvas.figure, event) + + def toolbar_callback(self, tool_id): + self._tools[tool_id].action(self.canvas.figure, None) + + def list_tools(self): + print ("{0:40} {1}".format('Tool description', 'Keymap')) + print ('_' * 50, '\n') + for id_, tool in self._tools.items(): + keys = [k for k, i in self._keys.items() if i == id_] + print ("{0:40} {1}".format(tool.description, ', '.join(keys))) + + def update(self, fig): + """Reset the axes stack""" + pass +# self._views.clear() +# self._positions.clear() +# self.set_history_buttons() + + def mouse_move(self, event): + 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 len(self.mode): +# self.set_message('%s, %s' % (self.mode, s)) +# else: + self.toolbar.set_message(s) + + class FigureManagerBase: """ Helper class for pyplot mode, wraps everything up into a neat bundle @@ -2530,8 +2716,8 @@ 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) +# 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`. @@ -2562,12 +2748,13 @@ def resize(self, w, h): """"For gui backends, resize the window (in pixels).""" pass - 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) +# def key_press(self, event): +# """ +# Implement the default mpl key bindings defined at +# :ref:`key-event-handling` +# """ +# print ('key press') +# key_press_handler(event, self.canvas, self.canvas.toolbar) def show_popup(self, msg): """ @@ -2596,6 +2783,22 @@ class Cursors: cursors = Cursors() +class ToolbarBase(object): + def __init__(self, manager): + pass + + def add_toolitem(self, name, description, image_file, position, + toggle, tool_id): + raise NotImplementedError + + def add_separator(self, pos): + pass + + def set_message(self, s): + """Display a message on toolbar or in status bar""" + pass + + class NavigationToolbar2(object): """ Base class for the navigation cursor, version 2 @@ -2672,8 +2875,8 @@ def __init__(self, canvas): self._active = None self._lastCursor = None self._init_toolbar() - self._idDrag = self.canvas.mpl_connect( - 'motion_notify_event', self.mouse_move) +# self._idDrag = self.canvas.mpl_connect( +# 'motion_notify_event', self.mouse_move) self._ids_zoom = [] self._zoom_mode = None @@ -2684,9 +2887,9 @@ def __init__(self, canvas): self.mode = '' # a mode string for the status bar self.set_history_buttons() - def set_message(self, s): - """Display a message on toolbar or in status bar""" - pass +# def set_message(self, s): +# """Display a message on toolbar or in status bar""" +# pass def back(self, *args): """move back up the view lim stack""" @@ -2709,12 +2912,12 @@ def forward(self, *args): self.set_history_buttons() self._update_view() - def home(self, *args): - """Restore the original view""" - self._views.home() - self._positions.home() - self.set_history_buttons() - self._update_view() +# def home(self, *args): +# """Restore the original view""" +# self._views.home() +# self._positions.home() +# self.set_history_buttons() +# self._update_view() def _init_toolbar(self): """ diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index dc285df80b9a..767cbff198e7 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -29,7 +29,8 @@ def fn_name(): return sys._getframe(1).f_code.co_name import matplotlib from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \ - FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase + FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, \ + TimerBase, NavigationBase, ToolbarBase from matplotlib.backend_bases import ShowBase from matplotlib.cbook import is_string_like, is_writable_file_like @@ -375,8 +376,8 @@ class FigureManagerGTK3(FigureManagerBase): def __init__(self, canvas, num): if _debug: print('FigureManagerGTK3.%s' % fn_name()) FigureManagerBase.__init__(self, canvas, num) - self.window = Gtk.Window() + self.navigation = NavigationGTK3(canvas, ToolbarGTK3) self.set_window_title("Figure %d" % num) try: self.window.set_icon_from_file(window_icon) @@ -399,7 +400,8 @@ def __init__(self, canvas, num): self.vbox.pack_start(self.canvas, True, True, 0) - self.toolbar = self._get_toolbar(canvas) +# self.toolbar = self._get_toolbar(canvas) + self.toolbar = self.navigation.toolbar # calculate size for window w = int (self.canvas.figure.bbox.width) @@ -422,7 +424,8 @@ 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.toolbar is not None: self.toolbar.update() + self.navigation.update(fig) self.canvas.figure.add_axobserver(notify_axes_change) self.canvas.grab_focus() @@ -476,6 +479,71 @@ def resize(self, width, height): self.window.resize(width, height) +class NavigationGTK3(NavigationBase): + pass + + +class ToolbarGTK3(ToolbarBase, Gtk.Box,): + def __init__(self, manager): + self.manager = manager +# self.win = manager.window + Gtk.Box.__init__(self) + self.set_property("orientation", Gtk.Orientation.VERTICAL) + ToolbarBase.__init__(self, manager) + self._toolbar = Gtk.Toolbar() + self._toolbar.set_style(Gtk.ToolbarStyle.ICONS) + self.pack_start(self._toolbar, False, False, 0) + self._toolbar.show_all() + + 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, text, tooltip_text, image_file, position, + toggle, tool_id): + image = Gtk.Image() + image.set_from_file(image_file) + if toggle: + tbutton = Gtk.ToggleToolButton() + else: + tbutton = Gtk.ToolButton() + tbutton.set_label(text) + tbutton.set_icon_widget(image) + self._toolbar.insert(tbutton, position) + tbutton.connect('clicked', self._call_tool, tool_id) + tbutton.set_tooltip_text(tooltip_text) + tbutton.show_all() + return tbutton +# self.show_all() + + def _call_tool(self, btn, tool_id): + self.manager.navigation.toolbar_callback(tool_id) + + def set_message(self, s): + self.message.set_label(s) + + def click(self, toolitem, tool_id): + if isinstance(toolitem, Gtk.ToggleToolButton): + status = toolitem.get_active() + toolitem.set_active(not status) + else: + self._call_tool(toolitem, tool_id) + + class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar): def __init__(self, canvas, window): self.win = window @@ -483,8 +551,8 @@ def __init__(self, canvas, window): NavigationToolbar2.__init__(self, canvas) self.ctx = None - def set_message(self, s): - self.message.set_label(s) +# def set_message(self, s): +# self.message.set_label(s) def set_cursor(self, cursor): self.canvas.get_property("window").set_cursor(cursord[cursor]) From 128f1a7f78a4bfe6b981d801b4b260e4e36da527 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 16 Jan 2014 18:40:10 -0500 Subject: [PATCH 2/9] stuff moved to new locations --- lib/matplotlib/backend_bases.py | 1285 +++++++++++------------ lib/matplotlib/backends/backend_gtk3.py | 320 +++--- 2 files changed, 780 insertions(+), 825 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 29827952f369..e69fe1a46337 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -50,6 +50,8 @@ from matplotlib import get_backend from matplotlib._pylab_helpers import Gcf +from matplotlib.rcsetup import validate_stringlist + from matplotlib.transforms import Bbox, TransformedBbox, Affine2D import matplotlib.tight_bbox as tight_bbox @@ -2414,84 +2416,9 @@ def key_press_handler(event, canvas, toolbar=None): a :class:`NavigationToolbar2` instance """ - # these bindings happen whether you are over an axes or not - -# if event.key is None: -# return - - # Load key-mappings from your matplotlibrc file. -# fullscreen_keys = rcParams['keymap.fullscreen'] -# home_keys = rcParams['keymap.home'] - back_keys = rcParams['keymap.back'] - forward_keys = rcParams['keymap.forward'] -# pan_keys = rcParams['keymap.pan'] -# zoom_keys = rcParams['keymap.zoom'] - save_keys = rcParams['keymap.save'] -# quit_keys = rcParams['keymap.quit'] -# grid_keys = rcParams['keymap.grid'] - toggle_yscale_keys = rcParams['keymap.yscale'] - toggle_xscale_keys = rcParams['keymap.xscale'] - all = rcParams['keymap.all_axes'] - -# # toggle fullscreen mode (default key 'f') -# if event.key in fullscreen_keys: -# canvas.manager.full_screen_toggle() - - # quit the figure (defaut key 'ctrl+w') -# if event.key in quit_keys: -# Gcf.destroy_fig(canvas.figure) - - if toolbar is not None: - # home or reset mnemonic (default key 'h', 'home' and 'r') -# if event.key in home_keys: -# toolbar.home() - # forward / backward keys to enable left handed quick navigation - # (default key for backward: 'left', 'backspace' and 'c') - if event.key in back_keys: - toolbar.back() - # (default key for forward: 'right' and 'v') - elif event.key in forward_keys: - toolbar.forward() - # pan mnemonic (default key 'p') - elif event.key in pan_keys: - toolbar.pan() - # zoom mnemonic (default key 'o') - elif event.key in zoom_keys: - toolbar.zoom() - # saving current figure (default key 's') - elif event.key in save_keys: - toolbar.save_figure() - if event.inaxes is None: return - # these bindings require the mouse to be over an axes to trigger - - # switching on/off a grid in current axes (default key 'g') -# if event.key in grid_keys: -# event.inaxes.grid() -# canvas.draw() - # toggle scaling of y-axes between 'log and 'linear' (default key 'l') - elif event.key in toggle_yscale_keys: - ax = event.inaxes - 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() - # toggle scaling of x-axes between 'log and 'linear' (default key 'k') - elif event.key in toggle_xscale_keys: - ax = event.inaxes - 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() - elif (event.key.isdigit() and event.key != '0') or event.key in all: # keys in list 'all' enables all axes (default key 'a'), # otherwise if key is a number only enable this particular axes @@ -2513,6 +2440,12 @@ class NonGuiException(Exception): pass +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 @@ -2520,567 +2453,281 @@ class ToolBase(object): name = None image = None toggle = False # Change the status (take control of the events) + persistent = False - def __init__(self): - pass + def __init__(self, figure, event=None): + self.figure = figure + self.navigation = figure.canvas.manager.navigation + self.activate(event) - def action(self, figure, event): + def activate(self, event): print('Without action:', self.name, self.description) class ToolQuit(ToolBase): + name = 'Quit' description = 'Quit the figure' keymap = rcParams['keymap.quit'] - def action(self, figure, event): - Gcf.destroy_fig(figure) + def activate(self, event): + Gcf.destroy_fig(self.figure) -class ToolToggleGrid(ToolBase): - description = 'Toogle Grid' - keymap = rcParams['keymap.grid'] +class ToolEnableAllNavigation(ToolBase): + name = 'EnableAll' + description = 'Enables all axes navigation' + keymap = rcParams['keymap.all_axes'] - def action(self, figure, event): + def activate(self, event): if event.inaxes is None: return - event.inaxes.grid() - figure.canvas.draw() - - -class ToolToggleFullScreen(ToolBase): - description = 'Toogle Fullscreen mode' - keymap = rcParams['keymap.fullscreen'] - - def action(self, figure, event): - figure.canvas.manager.full_screen_toggle() - - -class ToolHome(ToolBase): - description = 'Reset original view' - name = 'Home' - image = 'home' - keymap = rcParams['keymap.home'] - position = 0 - - -class ToolToggleBase(ToolBase): - toggle = True - capture_keypress = False - status = False - def key_press(self, figure, event): - pass + 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) -# def action(self, figure, event): -# self.status = not self.status +#FIXME: use a function instead of string for enable navigation +class ToolEnableNavigation(ToolBase): + name = 'EnableOne' + description = 'Enables one axes navigation' + keymap = range(1, 5) -class ToolZoom(ToolToggleBase): - description = 'Zoom to rectangle' - name = 'Zoom' - image = 'zoom_to_rect' - position = 1 - keymap = rcParams['keymap.zoom'] + 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 ToolPan(ToolToggleBase): - keymap = rcParams['keymap.pan'] - name = 'Pan' - description = 'Pan axes with left mouse, zoom with right' - image = 'move' - position = 2 +class ToolToggleGrid(ToolBase): + name = 'Grid' + description = 'Toogle Grid' + keymap = rcParams['keymap.grid'] -class NavigationBase(object): - tools = [ToolToggleGrid, - ToolToggleFullScreen, - ToolQuit, - ToolHome, - ToolZoom, - ToolPan] + def activate(self, event): + if event.inaxes is None: + return + event.inaxes.grid() + self.figure.canvas.draw() - 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) +class ToolToggleFullScreen(ToolBase): + name = 'Fullscreen' + description = 'Toogle Fullscreen mode' + keymap = rcParams['keymap.fullscreen'] - self._idDrag = self.canvas.mpl_connect('motion_notify_event', - self.mouse_move) - self._tools = {} - self._keys = {} - self._toggled = None - self._toolitems = {} # Toolbar items + def activate(self, event): + self.figure.canvas.manager.full_screen_toggle() - for tool in self.tools: - self.add_tool(tool) - def _get_toolbar(self, toolbar, canvas): - # must be inited after the window, drawingArea and figure - # attrs are set - if rcParams['toolbar'] == 'toolbar2' and toolbar is not None: - toolbar = toolbar(canvas.manager) - else: - toolbar = None - return toolbar +class ToolToggleYScale(ToolBase): + name = 'YScale' + description = 'Toogle Scale Y axis' + keymap = rcParams['keymap.yscale'] - def add_tool(self, tool): - from matplotlib.rcsetup import validate_stringlist - instance = tool() - id_ = id(instance) - self._tools[id_] = instance - if instance.keymap is not None: - for k in validate_stringlist(instance.keymap): - self._keys[k] = id_ + def activate(self, event): + ax = event.inaxes + if ax is None: + return - if self.toolbar and tool.position is not None: - basedir = os.path.join(rcParams['datapath'], 'images') - fname = os.path.join(basedir, tool.image + '.png') - toolitem = self.toolbar.add_toolitem(tool.name, tool.description, - fname, - tool.position, - tool.toggle, - id_) - self._toolitems[id_] = toolitem + 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() - def key_press(self, event): - """ - Implement the default mpl key bindings defined at - :ref:`key-event-handling` - """ - if event.key is None: - return +class ToolToggleXScale(ToolBase): + name = 'XScale' + description = 'Toogle Scale X axis' + keymap = rcParams['keymap.xscale'] - if self._toggled and self._tools[self._toggled].capture_keypress: - self._tools[self._toggled].key_press(self.canvas.figure, event) + def activate(self, event): + ax = event.inaxes + if ax is None: return - id_ = self._keys.get(event.key, False) - if id_: - if id_ in self._toolitems: - #For toolbar items, it is safer to pass by the toolbar - #so no back and forth for toggling - self.toolbar.click(self._toolitems[id_], id_) - else: - self._tools[id_].action(self.canvas.figure, event) + 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 toolbar_callback(self, tool_id): - self._tools[tool_id].action(self.canvas.figure, None) - def list_tools(self): - print ("{0:40} {1}".format('Tool description', 'Keymap')) - print ('_' * 50, '\n') - for id_, tool in self._tools.items(): - keys = [k for k, i in self._keys.items() if i == id_] - print ("{0:40} {1}".format(tool.description, ', '.join(keys))) +class ToolHome(ToolBase): + description = 'Reset original view' + name = 'Home' + image = 'home' + keymap = rcParams['keymap.home'] + position = -1 - def update(self, fig): - """Reset the axes stack""" - pass -# self._views.clear() -# self._positions.clear() + def activate(self, *args): + """Restore the original view""" + self.navigation.views.home() + self.navigation.positions.home() + self.navigation.update_view() # self.set_history_buttons() - def mouse_move(self, event): - 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 len(self.mode): -# self.set_message('%s, %s' % (self.mode, s)) -# else: - self.toolbar.set_message(s) +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 FigureManagerBase: - """ - Helper class for pyplot mode, wraps everything up into a neat bundle - Public attibutes: +class ToolForward(ToolBase): + description = 'Forward to next view' + name = 'Forward' + image = 'forward' + keymap = rcParams['keymap.forward'] + position = -1 - *canvas* - A :class:`FigureCanvasBase` instance + 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() - *num* - The figure number - """ - def __init__(self, canvas, num): - self.canvas = canvas - 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) - """ - The returned id from connecting the default key handler via - :meth:`FigureCanvasBase.mpl_connnect`. +class ToolPersistentBase(ToolBase): + persistent = True - To disable default key press handling:: + def __init__(self, figure, event=None): + self.figure = figure + self.navigation = figure.canvas.manager.navigation + #persistent tools don't call activate a at instantiation - manager, canvas = figure.canvas.manager, figure.canvas - canvas.mpl_disconnect(manager.key_press_handler_id) + def unregister(self, *args): + #call this to unregister from navigation + self.navigation.unregister(self.name) - """ - def show(self): - """ - For GUI backends, show the figure window and redraw. - For non-GUI backends, raise an exception to be caught - by :meth:`~matplotlib.figure.Figure.show`, for an - optional warning. - """ - raise NonGuiException() +class ConfigureSubplotsBase(ToolPersistentBase): + description = 'Configure subplots' + name = 'Subplots' + image = 'subplots' + position = -1 - def destroy(self): - pass - def full_screen_toggle(self): - pass +class SaveFigureBase(ToolBase): + description = 'Save the figure' + name = 'Save' + image = 'filesave' + position = -1 + keymap = rcParams['keymap.save'] - def resize(self, w, h): - """"For gui backends, resize the window (in pixels).""" - pass -# def key_press(self, event): -# """ -# Implement the default mpl key bindings defined at -# :ref:`key-event-handling` -# """ -# print ('key press') -# key_press_handler(event, self.canvas, self.canvas.toolbar) +class ToolToggleBase(ToolPersistentBase): + toggle = True + cursor = None + capture_keypress = False + capture_move = False + capture_press = False + capture_release = False + lock_drawing = False - def show_popup(self, msg): - """ - Display message in a popup -- GUI only - """ + def mouse_move(self, event): pass - def get_window_title(self): - """ - Get the title text of the window containing the figure. - Return None for non-GUI backends (eg, a PS backend). - """ - return 'image' - - def set_window_title(self, title): - """ - Set the title text of the window containing the figure. Note that - this has no effect for non-GUI backends (eg, a PS backend). - """ + def press(self, event): pass - -class Cursors: - # this class is only used as a simple namespace - HAND, POINTER, SELECT_REGION, MOVE = list(range(4)) -cursors = Cursors() - - -class ToolbarBase(object): - def __init__(self, manager): + def release(self, event): pass - def add_toolitem(self, name, description, image_file, position, - toggle, tool_id): - raise NotImplementedError - - def add_separator(self, pos): + def deactivate(self, event=None): pass - def set_message(self, s): - """Display a message on toolbar or in status bar""" + def key_press(self, event): pass -class NavigationToolbar2(object): - """ - Base class for the navigation cursor, version 2 +class ToolZoom(ToolToggleBase): + description = 'Zoom to rectangle' + name = 'Zoom' + image = 'zoom_to_rect' + position = -1 + keymap = rcParams['keymap.zoom'] - backends must implement a canvas that handles connections for - 'button_press_event' and 'button_release_event'. See - :meth:`FigureCanvasBase.mpl_connect` for more information + cursor = cursors.SELECT_REGION + capture_press = True + capture_release = True + def __init__(self, *args): + ToolToggleBase.__init__(self, *args) + self._ids_zoom = [] + self._button_pressed = None + self._xypress = None - They must also define + 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.capture_move = False + 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 - :meth:`save_figure` - save the current figure + if event.button == 1: + self._button_pressed = 1 + elif event.button == 3: + self._button_pressed = 3 + else: + self._button_pressed = None + return - :meth:`set_cursor` - if you want the pointer icon to change + x, y = event.x, event.y - :meth:`_init_toolbar` - create your toolbar widget - - :meth:`draw_rubberband` (optional) - draw the zoom to rect "rubberband" rectangle - - :meth:`press` (optional) - whenever a mouse button is pressed, you'll be notified with - the event - - :meth:`release` (optional) - whenever a mouse button is released, you'll be notified with - the event - - :meth:`dynamic_update` (optional) - dynamically update the window while navigating - - :meth:`set_message` (optional) - display message - - :meth:`set_history_buttons` (optional) - you can change the history back / forward buttons to - indicate disabled / enabled state. - - That's it, we'll do the rest! - """ - - # list of toolitems to add to the toolbar, format is: - # ( - # text, # the text of the button (often not visible to users) - # tooltip_text, # the tooltip shown on hover (where possible) - # image_file, # name of the image for the button (without the extension) - # name_of_method, # name of the method in NavigationToolbar2 to call - # ) - toolitems = ( - ('Home', 'Reset original view', 'home', 'home'), - ('Back', 'Back to previous view', 'back', 'back'), - ('Forward', 'Forward to next view', 'forward', 'forward'), - (None, None, None, None), - ('Pan', 'Pan axes with left mouse, zoom with right', 'move', 'pan'), - ('Zoom', 'Zoom to rectangle', 'zoom_to_rect', 'zoom'), - (None, None, None, None), - ('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'), - ('Save', 'Save the figure', 'filesave', 'save_figure'), - ) - - def __init__(self, canvas): - self.canvas = canvas - canvas.toolbar = self - # a dict from axes index to a list of view limits - self._views = cbook.Stack() - self._positions = cbook.Stack() # stack of subplot positions - self._xypress = None # the location and axis info at the time - # of the press - self._idPress = None - self._idRelease = None - self._active = None - self._lastCursor = None - self._init_toolbar() -# self._idDrag = self.canvas.mpl_connect( -# 'motion_notify_event', self.mouse_move) - - self._ids_zoom = [] - self._zoom_mode = None - - self._button_pressed = None # determined by the button pressed - # at start - - self.mode = '' # a mode string for the status bar - self.set_history_buttons() - -# def set_message(self, s): -# """Display a message on toolbar or in status bar""" -# pass - - def back(self, *args): - """move back up the view lim stack""" - self._views.back() - self._positions.back() - self.set_history_buttons() - self._update_view() - - def dynamic_update(self): - pass - - def draw_rubberband(self, event, x0, y0, x1, y1): - """Draw a rectangle rubberband to indicate zoom limits""" - pass - - def forward(self, *args): - """Move forward in the view lim stack""" - self._views.forward() - self._positions.forward() - self.set_history_buttons() - self._update_view() - -# def home(self, *args): -# """Restore the original view""" -# self._views.home() -# self._positions.home() -# self.set_history_buttons() -# self._update_view() - - def _init_toolbar(self): - """ - This is where you actually build the GUI widgets (called by - __init__). The icons ``home.xpm``, ``back.xpm``, ``forward.xpm``, - ``hand.xpm``, ``zoom_to_rect.xpm`` and ``filesave.xpm`` are standard - across backends (there are ppm versions in CVS also). - - You just need to set the callbacks - - home : self.home - back : self.back - forward : self.forward - hand : self.pan - zoom_to_rect : self.zoom - filesave : self.save_figure - - You only need to define the last one - the others are in the base - class implementation. - - """ - raise NotImplementedError - - def mouse_move(self, event): - if not event.inaxes or not self._active: - if self._lastCursor != cursors.POINTER: - self.set_cursor(cursors.POINTER) - self._lastCursor = cursors.POINTER - else: - if self._active == 'ZOOM': - if self._lastCursor != cursors.SELECT_REGION: - self.set_cursor(cursors.SELECT_REGION) - self._lastCursor = cursors.SELECT_REGION - elif (self._active == 'PAN' and - self._lastCursor != cursors.MOVE): - self.set_cursor(cursors.MOVE) - - self._lastCursor = cursors.MOVE - - if event.inaxes and event.inaxes.get_navigate(): - - try: - s = event.inaxes.format_coord(event.xdata, event.ydata) - except (ValueError, OverflowError): - pass - else: - if len(self.mode): - self.set_message('%s, %s' % (self.mode, s)) - else: - self.set_message(s) - else: - self.set_message(self.mode) - - def pan(self, *args): - """Activate the pan/zoom tool. pan with left button, zoom with right""" - # set the pointer icon and button press funcs to the - # appropriate callbacks - - if self._active == 'PAN': - self._active = None - else: - self._active = 'PAN' - if self._idPress is not None: - self._idPress = self.canvas.mpl_disconnect(self._idPress) - self.mode = '' - - if self._idRelease is not None: - self._idRelease = self.canvas.mpl_disconnect(self._idRelease) - self.mode = '' - - if self._active: - self._idPress = self.canvas.mpl_connect( - 'button_press_event', self.press_pan) - self._idRelease = self.canvas.mpl_connect( - 'button_release_event', self.release_pan) - self.mode = 'pan/zoom' - self.canvas.widgetlock(self) - else: - self.canvas.widgetlock.release(self) - - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self._active) - - self.set_message(self.mode) - - def press(self, event): - """Called whenver a mouse button is pressed.""" - pass - - def press_pan(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 - if self._views.empty(): - self.push_current() - - self._xypress = [] - for i, a in enumerate(self.canvas.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.canvas.mpl_disconnect(self._idDrag) - self._idDrag = self.canvas.mpl_connect('motion_notify_event', - self.drag_pan) - - self.press(event) - - def press_zoom(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.canvas.mpl_disconnect(zoom_id) - self.release(event) - self.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 - if self._views.empty(): - self.push_current() + # 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.canvas.figure.get_axes()): + 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())) - id1 = self.canvas.mpl_connect('motion_notify_event', self.drag_zoom) - id2 = self.canvas.mpl_connect('key_press_event', + self.capture_move = True + id2 = self.figure.canvas.mpl_connect('key_press_event', self._switch_on_zoom_mode) - id3 = self.canvas.mpl_connect('key_release_event', + id3 = self.figure.canvas.mpl_connect('key_release_event', self._switch_off_zoom_mode) - self._ids_zoom = id1, id2, id3 + self._ids_zoom = id2, id3 self._zoom_mode = event.key - self.press(event) + self.navigation.press(event) def _switch_on_zoom_mode(self, event): self._zoom_mode = event.key @@ -3090,59 +2737,11 @@ def _switch_off_zoom_mode(self, event): self._zoom_mode = None self.mouse_move(event) - 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 release(self, event): - """this will be called whenever mouse button is released""" - pass - - def release_pan(self, event): - """the release mouse button callback in pan/zoom mode""" - - if self._button_pressed is None: - return - self.canvas.mpl_disconnect(self._idDrag) - self._idDrag = self.canvas.mpl_connect( - 'motion_notify_event', self.mouse_move) - for a, ind in self._xypress: - a.end_pan() - if not self._xypress: - return - self._xypress = [] - self._button_pressed = None - self.push_current() - self.release(event) - self.draw() - - def drag_pan(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.dynamic_update() - - def drag_zoom(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] + lastx, lasty, a, _ind, _lim, _trans = self._xypress[0] # adjust x, last, y, last x1, y1, x2, y2 = a.bbox.extents @@ -3156,12 +2755,13 @@ def drag_zoom(self, event): x1, y1, x2, y2 = a.bbox.extents x, lastx = x1, x2 - self.draw_rubberband(event, x, y, lastx, lasty) + self.navigation.draw_rubberband(event, x, y, lastx, lasty) - def release_zoom(self, event): + def release(self, event): """the release mouse button callback in zoom to rect mode""" + self.capture_move = False for zoom_id in self._ids_zoom: - self.canvas.mpl_disconnect(zoom_id) + self.figure.canvas.mpl_disconnect(zoom_id) self._ids_zoom = [] if not self._xypress: @@ -3171,12 +2771,12 @@ def release_zoom(self, event): for cur_xypress in self._xypress: x, y = event.x, event.y - lastx, lasty, a, ind, lim, trans = cur_xypress + 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.release(event) - self.draw() + self.navigation.release(event) + self.navigation.draw() return x0, y0, x1, y1 = lim.extents @@ -3276,15 +2876,325 @@ def release_zoom(self, event): a.set_xlim((rx1, rx2)) a.set_ylim((ry1, ry2)) - self.draw() + self.navigation.draw() self._xypress = None self._button_pressed = None self._zoom_mode = None - self.push_current() + 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 + capture_press = True + capture_release = True + lock_drawing = True + + def __init__(self, *args): + ToolToggleBase.__init__(self, *args) + self._button_pressed = None + self._xypress = None + + 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.capture_move = True + self.navigation.press(event) + + def release(self, event): + if self._button_pressed is None: + return + + self.capture_move = False + + 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() + + +class NavigationBase(object): + _default_cursor = cursors.POINTER + tools = [ToolToggleGrid, + ToolToggleFullScreen, + ToolQuit, ToolEnableAllNavigation, ToolEnableNavigation, + ToolToggleXScale, ToolToggleYScale, + ToolHome, ToolBack, ToolForward, + ToolZoom, 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 + + for tool in self.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'] == 'toolbar2' and toolbar is not None: + toolbar = toolbar(canvas.manager) + else: + toolbar = None + return toolbar + + #remove persistent instances + def unregister(self, name): + if name in self._instances: + del self._instances[name] + + def add_tool(self, callback_class): + tool = self._get_cls_to_instantiate(callback_class) + name = tool.name + if name is None: + #TODO: add a warning + print ('impossible to add without name') + return + if name in self._tools: + #TODO: add a warning + print ('impossible to add two times with the same name') + 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') + fname = os.path.join(basedir, tool.image + '.png') + 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): + """ + Implement the default mpl key bindings defined at + :ref:`key-event-handling` + """ + + if event.key is None: + return + + #some tools may need to capture keypress, but they need to be toggle + if self._toggled and self._tools[self._toggled].capture_keypress: + self._instances[self._toggled].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) + 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): + #when from keypress 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 + self.canvas.widgetlock(self) + + elif self._toggled == name: + instance.deactivate(None) + self._toggled = None + self.canvas.widgetlock.release(self) + + 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 ("{0:20} {1:40} {2}".format('Name (id)', 'Tool description', + 'Keymap')) + print ('_' * 50, '\n') + for id_, tool in self._tools.items(): + keys = [k for k, i in self._keys.items() if i == id_] + print ("{0:20} {1:40} {2}".format(tool.name, tool.description, + ', '.join(keys))) + + 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 instance.capture_move: + 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 instance.capture_release: + 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 instance.capture_press: + 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(): @@ -3302,15 +3212,25 @@ def draw(self): loc.refresh() self.canvas.draw_idle() - def _update_view(self): + 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() + lims = self.views() if lims is None: return - pos = self._positions() + pos = self.positions() if pos is None: return for i, a in enumerate(self.canvas.figure.get_axes()): @@ -3323,53 +3243,110 @@ def _update_view(self): self.canvas.draw_idle() - def save_figure(self, *args): - """Save the current figure""" - raise NotImplementedError + 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 FigureManagerBase: + """ + Helper class for pyplot mode, wraps everything up into a neat bundle + + Public attibutes: + + *canvas* + A :class:`FigureCanvasBase` instance + + *num* + The figure number + """ + def __init__(self, canvas, num): + self.canvas = canvas + canvas.manager = self # store a pointer to parent + self.num = num - def set_cursor(self, cursor): """ - Set the current cursor to one of the :class:`Cursors` - enums values + The returned id from connecting the default key handler via + :meth:`FigureCanvasBase.mpl_connnect`. + + To disable default key press handling:: + + manager, canvas = figure.canvas.manager, figure.canvas + canvas.mpl_disconnect(manager.key_press_handler_id) + + """ + + def show(self): """ + For GUI backends, show the figure window and redraw. + For non-GUI backends, raise an exception to be caught + by :meth:`~matplotlib.figure.Figure.show`, for an + optional warning. + """ + raise NonGuiException() + + def destroy(self): pass - def update(self): - """Reset the axes stack""" - self._views.clear() - self._positions.clear() - self.set_history_buttons() - - def zoom(self, *args): - """Activate zoom to rect mode""" - if self._active == 'ZOOM': - self._active = None - else: - self._active = 'ZOOM' - - if self._idPress is not None: - self._idPress = self.canvas.mpl_disconnect(self._idPress) - self.mode = '' - - if self._idRelease is not None: - self._idRelease = self.canvas.mpl_disconnect(self._idRelease) - self.mode = '' - - if self._active: - self._idPress = self.canvas.mpl_connect('button_press_event', - self.press_zoom) - self._idRelease = self.canvas.mpl_connect('button_release_event', - self.release_zoom) - self.mode = 'zoom rect' - self.canvas.widgetlock(self) - else: - self.canvas.widgetlock.release(self) + def full_screen_toggle(self): + pass - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self._active) + def resize(self, w, h): + """"For gui backends, resize the window (in pixels).""" + pass + + def show_popup(self, msg): + """ + Display message in a popup -- GUI only + """ + pass + + def get_window_title(self): + """ + Get the title text of the window containing the figure. + Return None for non-GUI backends (eg, a PS backend). + """ + return 'image' + + def set_window_title(self, title): + """ + Set the title text of the window containing the figure. Note that + this has no effect for non-GUI backends (eg, a PS backend). + """ + pass - self.set_message(self.mode) - def set_history_buttons(self): - """Enable or disable back/forward button""" +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 diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 767cbff198e7..bbd064988326 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -29,8 +29,8 @@ def fn_name(): return sys._getframe(1).f_code.co_name import matplotlib from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \ - FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, \ - TimerBase, NavigationBase, ToolbarBase + FigureManagerBase, FigureCanvasBase, cursors, \ + TimerBase, NavigationBase, ToolbarBase, ConfigureSubplotsBase, SaveFigureBase from matplotlib.backend_bases import ShowBase from matplotlib.cbook import is_string_like, is_writable_file_like @@ -374,7 +374,8 @@ class FigureManagerGTK3(FigureManagerBase): window : The Gtk.Window (gtk only) """ def __init__(self, canvas, num): - if _debug: print('FigureManagerGTK3.%s' % fn_name()) + if _debug: + print('FigureManagerGTK3.%s' % fn_name()) FigureManagerBase.__init__(self, canvas, num) self.window = Gtk.Window() self.navigation = NavigationGTK3(canvas, ToolbarGTK3) @@ -389,7 +390,8 @@ def __init__(self, canvas, num): # all, so I am not sure how to catch it. I am unhappy # doing a blanket catch here, but am not sure what a # better way is - JDH - verbose.report('Could not load matplotlib icon: %s' % sys.exc_info()[1]) + verbose.report('Could not load matplotlib icon: %s' % + sys.exc_info()[1]) self.vbox = Gtk.Box() self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) @@ -400,12 +402,11 @@ def __init__(self, canvas, num): self.vbox.pack_start(self.canvas, True, True, 0) -# self.toolbar = self._get_toolbar(canvas) self.toolbar = self.navigation.toolbar # calculate size for window - w = int (self.canvas.figure.bbox.width) - h = int (self.canvas.figure.bbox.height) + w = int(self.canvas.figure.bbox.width) + h = int(self.canvas.figure.bbox.height) if self.toolbar is not None: self.toolbar.show() @@ -413,7 +414,7 @@ def __init__(self, canvas, num): size_request = self.toolbar.size_request() h += size_request.height - self.window.set_default_size (w, h) + self.window.set_default_size(w, h) def destroy(*args): Gcf.destroy(num) @@ -424,21 +425,21 @@ 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() - self.navigation.update(fig) + self.navigation.update() self.canvas.figure.add_axobserver(notify_axes_change) self.canvas.grab_focus() def destroy(self, *args): - if _debug: print('FigureManagerGTK3.%s' % fn_name()) + if _debug: + print('FigureManagerGTK3.%s' % fn_name()) self.vbox.destroy() self.window.destroy() self.canvas.destroy() if self.toolbar: self.toolbar.destroy() - if Gcf.get_num_fig_managers()==0 and \ + if Gcf.get_num_fig_managers() == 0 and \ not matplotlib.is_interactive() and \ Gtk.main_level() >= 1: Gtk.main_quit() @@ -447,7 +448,7 @@ def show(self): # show the figure window self.window.show() - def full_screen_toggle (self): + def full_screen_toggle(self): self._full_screen_flag = not self._full_screen_flag if self._full_screen_flag: self.window.fullscreen() @@ -455,16 +456,6 @@ def full_screen_toggle (self): self.window.unfullscreen() _full_screen_flag = False - - def _get_toolbar(self, canvas): - # must be inited after the window, drawingArea and figure - # attrs are set - if rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2GTK3 (canvas, self.window) - else: - toolbar = None - return toolbar - def get_window_title(self): return self.window.get_title() @@ -480,94 +471,16 @@ def resize(self, width, height): class NavigationGTK3(NavigationBase): - pass - - -class ToolbarGTK3(ToolbarBase, Gtk.Box,): - def __init__(self, manager): - self.manager = manager -# self.win = manager.window - Gtk.Box.__init__(self) - self.set_property("orientation", Gtk.Orientation.VERTICAL) - ToolbarBase.__init__(self, manager) - self._toolbar = Gtk.Toolbar() - self._toolbar.set_style(Gtk.ToolbarStyle.ICONS) - self.pack_start(self._toolbar, False, False, 0) - self._toolbar.show_all() - - 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, text, tooltip_text, image_file, position, - toggle, tool_id): - image = Gtk.Image() - image.set_from_file(image_file) - if toggle: - tbutton = Gtk.ToggleToolButton() - else: - tbutton = Gtk.ToolButton() - tbutton.set_label(text) - tbutton.set_icon_widget(image) - self._toolbar.insert(tbutton, position) - tbutton.connect('clicked', self._call_tool, tool_id) - tbutton.set_tooltip_text(tooltip_text) - tbutton.show_all() - return tbutton -# self.show_all() - - def _call_tool(self, btn, tool_id): - self.manager.navigation.toolbar_callback(tool_id) - - def set_message(self, s): - self.message.set_label(s) - - def click(self, toolitem, tool_id): - if isinstance(toolitem, Gtk.ToggleToolButton): - status = toolitem.get_active() - toolitem.set_active(not status) - else: - self._call_tool(toolitem, tool_id) - - -class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar): - def __init__(self, canvas, window): - self.win = window - GObject.GObject.__init__(self) - NavigationToolbar2.__init__(self, canvas) + def __init__(self, *args, **kwargs): + NavigationBase.__init__(self, *args, **kwargs) self.ctx = None -# def set_message(self, s): -# self.message.set_label(s) - def set_cursor(self, cursor): self.canvas.get_property("window").set_cursor(cursord[cursor]) - #self.canvas.set_cursor(cursord[cursor]) - - def release(self, event): - try: del self._pixmapBack - except AttributeError: pass - - def dynamic_update(self): - # legacy method; new method is canvas.draw_idle - self.canvas.draw_idle() def draw_rubberband(self, event, x0, y0, x1, y1): - '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 @@ -579,7 +492,7 @@ def draw_rubberband(self, event, x0, y0, x1, 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)] + 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) @@ -587,96 +500,161 @@ def draw_rubberband(self, event, x0, y0, x1, y1): self.ctx.set_source_rgb(0, 0, 0) self.ctx.stroke() - def _init_toolbar(self): - self.set_style(Gtk.ToolbarStyle.ICONS) - basedir = os.path.join(rcParams['datapath'],'images') - - for text, tooltip_text, image_file, callback in self.toolitems: - if text is None: - self.insert( Gtk.SeparatorToolItem(), -1 ) - continue - fname = os.path.join(basedir, image_file + '.png') - image = Gtk.Image() - image.set_from_file(fname) - tbutton = Gtk.ToolButton() - tbutton.set_label(text) - tbutton.set_icon_widget(image) - self.insert(tbutton, -1) - tbutton.connect('clicked', getattr(self, callback)) - tbutton.set_tooltip_text(tooltip_text) - - toolitem = Gtk.SeparatorToolItem() - self.insert(toolitem, -1) - toolitem.set_draw(False) - toolitem.set_expand(True) - - toolitem = Gtk.ToolItem() - self.insert(toolitem, -1) - self.message = Gtk.Label() - toolitem.add(self.message) + 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 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) - self.show_all() + 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 SaveFigureGTK3(SaveFigureBase): def get_filechooser(self): fc = FileChooserDialog( title='Save the figure', - parent=self.win, + parent=self.figure.canvas.manager.window, path=os.path.expanduser(rcParams.get('savefig.directory', '')), - filetypes=self.canvas.get_supported_filetypes(), - default_filetype=self.canvas.get_default_filetype()) - fc.set_current_name(self.canvas.get_default_filename()) + 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 save_figure(self, *args): + def activate(self, *args): chooser = self.get_filechooser() - fname, format = chooser.get_filename_from_user() + fname, format_ = chooser.get_filename_from_user() chooser.destroy() if fname: - startpath = os.path.expanduser(rcParams.get('savefig.directory', '')) + 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)) + rcParams['savefig.directory'] = os.path.dirname( + six.text_type(fname)) try: - self.canvas.print_figure(fname, format=format) + self.figure.canvas.print_figure(fname, format=format_) except Exception as e: error_msg_gtk(str(e), parent=self) - def configure_subplots(self, button): - toolfig = Figure(figsize=(6,3)) - canvas = self._get_canvas(toolfig) - toolfig.subplots_adjust(top=0.9) - tool = SubplotTool(self.canvas.figure, toolfig) +SaveFigure = SaveFigureGTK3 - w = int (toolfig.bbox.width) - h = int (toolfig.bbox.height) +class ToolbarGTK3(ToolbarBase, Gtk.Box,): + def __init__(self, manager): + ToolbarBase.__init__(self, manager) + Gtk.Box.__init__(self) + self.set_property("orientation", Gtk.Orientation.VERTICAL) - window = Gtk.Window() - try: - 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 - window.set_title("Subplot Configuration Tool") - window.set_default_size(w, h) - vbox = Gtk.Box() - vbox.set_property("orientation", Gtk.Orientation.VERTICAL) - window.add(vbox) - vbox.show() + 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() - canvas.show() - vbox.pack_start(canvas, True, True, 0) - window.show() + 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() - def _get_canvas(self, fig): - return self.canvas.__class__(fig) + 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): + image = Gtk.Image() + image.set_from_file(image_file) + if toggle: + tbutton = Gtk.ToggleToolButton() + else: + tbutton = Gtk.ToolButton() + tbutton.set_label(name) + 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]) class FileChooserDialog(Gtk.FileChooserDialog): From 482e247f28371a60f7a525bd2aed3e193be66b03 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 16 Jan 2014 18:44:39 -0500 Subject: [PATCH 3/9] Removing prints --- lib/matplotlib/backend_bases.py | 35 +-------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index e69fe1a46337..8504e408792a 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2403,39 +2403,6 @@ def stop_event_loop_default(self): self._looping = False -def key_press_handler(event, canvas, toolbar=None): - """ - Implement the default mpl key bindings for the canvas and toolbar - described at :ref:`key-event-handling` - - *event* - a :class:`KeyEvent` instance - *canvas* - a :class:`FigureCanvasBase` instance - *toolbar* - a :class:`NavigationToolbar2` instance - - """ - if event.inaxes is None: - return - - elif (event.key.isdigit() and event.key != '0') or event.key in all: - # keys in list 'all' enables all axes (default key 'a'), - # otherwise if key is a number only enable this particular axes - # if it was the axes, where the event was raised - if not (event.key in all): - n = int(event.key) - 1 - for i, a in enumerate(canvas.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): - if event.key in all: - a.set_navigate(True) - else: - a.set_navigate(i == n) - - class NonGuiException(Exception): pass @@ -2461,7 +2428,7 @@ def __init__(self, figure, event=None): self.activate(event) def activate(self, event): - print('Without action:', self.name, self.description) + pass class ToolQuit(ToolBase): From 72ce737b1c0c402bf8b28edb9734de2437e7e8f7 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 16 Jan 2014 22:49:57 -0500 Subject: [PATCH 4/9] improving the example --- examples/user_interfaces/navigation.py | 26 +++++++++++++++++-- lib/matplotlib/backend_bases.py | 33 ++++++++++++++++++++----- lib/matplotlib/backends/backend_gtk3.py | 8 ++++++ 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 4c3b8b467a2c..d98a820ea2d6 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -4,7 +4,29 @@ fig = plt.figure() ax = fig.add_subplot(111) -ax.plot([1, 2, 3]) +ax.plot([1, 2, 3], label='My First line') +ax.plot([2, 3, 4], label='Second line') -fig.canvas.manager.navigation.list_tools() + + +from matplotlib.backend_bases import ToolBase +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() + +#Add the simple tool to the toolbar +fig.canvas.manager.navigation.add_tool(ListTools) + +#Just for fun, lets remove the back button +fig.canvas.manager.navigation.remove_tool('Back') + plt.show() \ No newline at end of file diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 8504e408792a..cccf3eece9a3 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2973,9 +2973,21 @@ def _get_toolbar(self, toolbar, canvas): #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 @@ -2995,7 +3007,10 @@ def add_tool(self, callback_class): if self.toolbar and tool.position is not None: basedir = os.path.join(rcParams['datapath'], 'images') - fname = os.path.join(basedir, tool.image + '.png') + if tool.image is not None: + fname = os.path.join(basedir, tool.image + '.png') + else: + fname = 'Unknown' self.toolbar.add_toolitem(name, tool.description, fname, tool.position, @@ -3089,13 +3104,16 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): a.set_navigate_mode(self._toggled) def list_tools(self): - print ("{0:20} {1:40} {2}".format('Name (id)', 'Tool description', + print ('_' * 80) + print ("{0:20} {1:50} {2}".format('Name (id)', 'Tool description', 'Keymap')) - print ('_' * 50, '\n') - for id_, tool in self._tools.items(): - keys = [k for k, i in self._keys.items() if i == id_] - print ("{0:20} {1:40} {2}".format(tool.name, tool.description, + 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""" @@ -3317,3 +3335,6 @@ def set_message(self, s): 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 bbd064988326..1d4dc0aeafb6 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -656,6 +656,14 @@ def toggle(self, name, callback=False): 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 FileChooserDialog(Gtk.FileChooserDialog): """GTK+ file selector which remembers the last file/directory From aae701bbad2f2a57807374f076a6d8e751d05417 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 21 Jan 2014 11:09:48 -0500 Subject: [PATCH 5/9] adding locks for events, instead of modifying attributes --- lib/matplotlib/backend_bases.py | 76 ++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index cccf3eece9a3..6fa5fab9f1aa 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2609,11 +2609,6 @@ class SaveFigureBase(ToolBase): class ToolToggleBase(ToolPersistentBase): toggle = True cursor = None - capture_keypress = False - capture_move = False - capture_press = False - capture_release = False - lock_drawing = False def mouse_move(self, event): pass @@ -2637,10 +2632,7 @@ class ToolZoom(ToolToggleBase): image = 'zoom_to_rect' position = -1 keymap = rcParams['keymap.zoom'] - cursor = cursors.SELECT_REGION - capture_press = True - capture_release = True def __init__(self, *args): ToolToggleBase.__init__(self, *args) @@ -2648,12 +2640,22 @@ def __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 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.capture_move = False + self.navigation.movelock.release(self) for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) self.navigation.release(event) @@ -2685,7 +2687,7 @@ def press(self, event): self._xypress.append((x, y, a, i, a.viewLim.frozen(), a.transData.frozen())) - self.capture_move = True + 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', @@ -2726,7 +2728,7 @@ def mouse_move(self, event): def release(self, event): """the release mouse button callback in zoom to rect mode""" - self.capture_move = False + self.navigation.movelock.release(self) for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) self._ids_zoom = [] @@ -2859,17 +2861,23 @@ class ToolPan(ToolToggleBase): description = 'Pan axes with left mouse, zoom with right' image = 'move' position = -1 - cursor = cursors.MOVE - capture_press = True - capture_release = True - lock_drawing = True 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""" @@ -2894,14 +2902,14 @@ def press(self, event): a.get_navigate() and a.can_pan()): a.start_pan(x, y, event.button) self._xypress.append((a, i)) - self.capture_move = True + self.navigation.movelock(self) self.navigation.press(event) def release(self, event): if self._button_pressed is None: return - self.capture_move = False + self.navigation.movelock.release(self) for a, _ind in self._xypress: a.end_pan() @@ -2957,6 +2965,14 @@ def __init__(self, canvas, toolbar=None): 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.tools: self.add_tool(tool) @@ -2991,13 +3007,12 @@ def remove_tool(self, name): def add_tool(self, callback_class): tool = self._get_cls_to_instantiate(callback_class) name = tool.name + if name is None: - #TODO: add a warning - print ('impossible to add without name') + warnings.warn('Tools need a name to be added, it is used as ID') return if name in self._tools: - #TODO: add a warning - print ('impossible to add two times with the same name') + warnings.warn('A tool with the same name already exist, not added') return self._tools[name] = tool @@ -3040,8 +3055,10 @@ def key_press(self, event): return #some tools may need to capture keypress, but they need to be toggle - if self._toggled and self._tools[self._toggled].capture_keypress: - self._instances[self._toggled].key_press(event) + 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) @@ -3062,6 +3079,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 self._instances[name] = instance return self._instances[name] @@ -3077,7 +3095,7 @@ def toolbar_callback(self, name): tool(self.canvas.figure, None) def _handle_toggle(self, name, event=None, from_toolbar=False): - #when from keypress toggle toolbar without callback + #toggle toolbar without callback if not from_toolbar and self.toolbar: self.toolbar.toggle(name, False) @@ -3085,12 +3103,10 @@ def _handle_toggle(self, name, event=None, from_toolbar=False): if self._toggled is None: instance.activate(None) self._toggled = name - self.canvas.widgetlock(self) elif self._toggled == name: instance.deactivate(None) self._toggled = None - self.canvas.widgetlock.release(self) else: if self.toolbar: @@ -3124,7 +3140,7 @@ def update(self): def mouse_move(self, event): if self._toggled: instance = self._instances[self._toggled] - if instance.capture_move: + if self.movelock.isowner(instance): instance.mouse_move(event) return @@ -3159,7 +3175,7 @@ def mouse_move(self, event): def _release(self, event): if self._toggled: instance = self._instances[self._toggled] - if instance.capture_release: + if self.releaselock.isowner(instance): instance.release(event) return self.release(event) @@ -3171,7 +3187,7 @@ def _press(self, event): """Called whenver a mouse button is pressed.""" if self._toggled: instance = self._instances[self._toggled] - if instance.capture_press: + if self.presslock.isowner(instance): instance.press(event) return self.press(event) @@ -3333,7 +3349,7 @@ def set_message(self, s): pass def toggle(self, name, callback=False): - #carefull, callback means to perform or not the callback while toggling + #callback = perform or not the callback while toggling raise NotImplementedError def remove_toolitem(self, name): From e226cc02801fa2908bb776c3a29110d6e6950307 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 21 Jan 2014 14:36:19 -0500 Subject: [PATCH 6/9] moving class attribute to ToolBase --- lib/matplotlib/backend_bases.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 6fa5fab9f1aa..150eef618911 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2421,6 +2421,7 @@ class ToolBase(object): 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 @@ -2608,7 +2609,6 @@ class SaveFigureBase(ToolBase): class ToolToggleBase(ToolPersistentBase): toggle = True - cursor = None def mouse_move(self, event): pass @@ -3007,12 +3007,12 @@ def remove_tool(self, 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 @@ -3349,7 +3349,7 @@ def set_message(self, s): pass def toggle(self, name, callback=False): - #callback = 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): From deb834cc49f026861e64182b806296fffd8c7dd2 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 22 Jan 2014 00:22:10 -0500 Subject: [PATCH 7/9] Reducing the number of public methods --- lib/matplotlib/backend_bases.py | 18 +++++++----------- lib/matplotlib/backends/backend_gtk3.py | 9 ++++++--- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 150eef618911..1f2c46fc31d9 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2933,7 +2933,7 @@ def mouse_move(self, event): class NavigationBase(object): _default_cursor = cursors.POINTER - tools = [ToolToggleGrid, + _default_tools = [ToolToggleGrid, ToolToggleFullScreen, ToolQuit, ToolEnableAllNavigation, ToolEnableNavigation, ToolToggleXScale, ToolToggleYScale, @@ -2946,10 +2946,10 @@ def __init__(self, canvas, toolbar=None): self.toolbar = self._get_toolbar(toolbar, canvas) self._key_press_handler_id = self.canvas.mpl_connect('key_press_event', - self.key_press) + self._key_press) self._idDrag = self.canvas.mpl_connect('motion_notify_event', - self.mouse_move) + self._mouse_move) self._idPress = self.canvas.mpl_connect('button_press_event', self._press) @@ -2973,7 +2973,7 @@ def __init__(self, canvas, toolbar=None): #just to group all the locks in one place self.canvaslock = self.canvas.widgetlock - for tool in self.tools: + for tool in self._default_tools: self.add_tool(tool) self._last_cursor = self._default_cursor @@ -3025,7 +3025,7 @@ def add_tool(self, callback_class): if tool.image is not None: fname = os.path.join(basedir, tool.image + '.png') else: - fname = 'Unknown' + fname = None self.toolbar.add_toolitem(name, tool.description, fname, tool.position, @@ -3045,11 +3045,7 @@ def _get_cls_to_instantiate(self, callback_class): return callback_class - def key_press(self, event): - """ - Implement the default mpl key bindings defined at - :ref:`key-event-handling` - """ + def _key_press(self, event): if event.key is None: return @@ -3137,7 +3133,7 @@ def update(self): self.positions.clear() # self.set_history_buttons() - def mouse_move(self, event): + def _mouse_move(self, event): if self._toggled: instance = self._instances[self._toggled] if self.movelock.isowner(instance): diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 1d4dc0aeafb6..b9af1661bfb4 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -620,14 +620,17 @@ def _add_message(self): def add_toolitem(self, name, tooltip_text, image_file, position, toggle): - image = Gtk.Image() - image.set_from_file(image_file) if toggle: tbutton = Gtk.ToggleToolButton() else: tbutton = Gtk.ToolButton() tbutton.set_label(name) - tbutton.set_icon_widget(image) + + 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) From 8835b9b31eda152e69f8330ae62cda09e37c3512 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 22 Jan 2014 16:15:06 -0500 Subject: [PATCH 8/9] adding copy tool to example --- examples/user_interfaces/navigation.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index d98a820ea2d6..2dd7912bb2c6 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -1,5 +1,5 @@ import matplotlib -matplotlib.use('GTK3AGG') +matplotlib.use('GTK3Cairo') import matplotlib.pyplot as plt fig = plt.figure() @@ -28,5 +28,23 @@ def activate(self, event): #Just for fun, lets remove the back button fig.canvas.manager.navigation.remove_tool('Back') - + +#looking at https://github.com/matplotlib/matplotlib/issues/1987 +#a simple example of copy canvas +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.canvas.manager.navigation.add_tool(CopyTool) + plt.show() \ No newline at end of file From 560868a551645626f64449fbe64b4c9dd3e2fb49 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Wed, 22 Jan 2014 16:54:56 -0500 Subject: [PATCH 9/9] bug fix --- examples/user_interfaces/navigation.py | 1 + lib/matplotlib/backend_bases.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/user_interfaces/navigation.py b/examples/user_interfaces/navigation.py index 2dd7912bb2c6..7fdc88d9bbe0 100644 --- a/examples/user_interfaces/navigation.py +++ b/examples/user_interfaces/navigation.py @@ -1,5 +1,6 @@ import matplotlib matplotlib.use('GTK3Cairo') +#matplotlib.rcParams['toolbar'] = 'None' import matplotlib.pyplot as plt fig = plt.figure() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 1f2c46fc31d9..edd13f637137 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3046,7 +3046,6 @@ def _get_cls_to_instantiate(self, callback_class): return callback_class def _key_press(self, event): - if event.key is None: return @@ -3055,7 +3054,7 @@ def _key_press(self, event): instance = self._get_instance(self._toggled) if self.keypresslock.isowner(instance): instance.key_press(event) - return + return name = self._keys.get(event.key, None) if name is None: