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/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..f02901f04f83 100644
--- a/doc/api/index_backend_api.rst
+++ b/doc/api/index_backend_api.rst
@@ -5,6 +5,8 @@ backends
.. toctree::
backend_bases_api.rst
+ backend_managers_api.rst
+ backend_tools_api.rst
backend_gtkagg_api.rst
backend_qt4agg_api.rst
backend_wxagg_api.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
new file mode 100644
index 000000000000..5240bab239c2
--- /dev/null
+++ b/examples/user_interfaces/toolmanager.py
@@ -0,0 +1,92 @@
+'''This example demonstrates how to:
+* Modify the Toolbar
+* Create tools
+* Add tools
+* Remove tools
+Using `matplotlib.backend_managers.ToolManager`
+'''
+
+
+from __future__ import print_function
+import matplotlib
+matplotlib.use('GTK3Cairo')
+matplotlib.rcParams['toolbar'] = 'toolmanager'
+import matplotlib.pyplot as plt
+from matplotlib.backend_tools import ToolBase, ToolToggleBase
+from gi.repository import Gtk, Gdk
+
+
+class ListTools(ToolBase):
+ '''List all the tools controlled by the `ToolManager`'''
+ # keyboard shortcut
+ default_keymap = 'm'
+ description = 'List Tools'
+
+ def trigger(self, *args, **kwargs):
+ print('_' * 80)
+ print("{0:12} {1:45} {2}".format('Name (id)',
+ 'Tool description',
+ 'Keymap'))
+ print('-' * 80)
+ tools = self.toolmanager.tools
+ for name in sorted(tools.keys()):
+ if not tools[name].description:
+ continue
+ keys = ', '.join(sorted(self.toolmanager.get_tool_keymap(name)))
+ 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.toolmanager.active_toggle.items():
+ print("{0:12} {1:45}".format(group, active))
+
+
+class GroupHideTool(ToolToggleBase):
+ '''Hide lines with a given gid'''
+ default_keymap = 'G'
+ description = 'Hide by gid'
+
+ 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], 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('Hide', GroupHideTool, gid='mygroup')
+
+
+# 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.toolmanager.remove_tool('forward')
+
+# To add a custom tool to the toolbar at specific location inside
+# the navigation group
+fig.canvas.manager.toolbar.add_tool('Hide', 'navigation', 1)
+
+plt.show()
diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py
index a3dec56759c4..7ef777540a52 100644
--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -25,6 +25,11 @@
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,
@@ -56,6 +61,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 +2576,12 @@ 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'] != 'toolmanager':
+ 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`.
@@ -2607,7 +2617,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'] != 'toolmanager':
+ key_press_handler(event, self.canvas, self.canvas.toolbar)
def show_popup(self, msg):
"""
@@ -2630,10 +2641,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 +3221,161 @@ def zoom(self, *args):
def set_history_buttons(self):
"""Enable or disable back/forward button"""
pass
+
+
+class ToolContainerBase(object):
+ """
+ Base class for all tool containers, e.g. toolbars.
+
+ Attributes
+ ----------
+ toolmanager : `ToolManager` object that holds the tools that
+ this `ToolContainer` wants to communicate with.
+ """
+
+ def __init__(self, toolmanager):
+ self.toolmanager = toolmanager
+ self.toolmanager.toolmanager_connect('tool_removed_event',
+ self._remove_tool_cbk)
+
+ def _tool_toggled_cbk(self, event):
+ """
+ Captures the 'tool_trigger_[name]'
+
+ This only gets used for toggled tools
+ """
+ self.toggle_toolitem(event.tool.name, event.tool.toggled)
+
+ def add_tool(self, tool, group, position=-1):
+ """
+ Adds a tool to this container
+
+ Parameters
+ ----------
+ tool : tool_like
+ 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.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.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"""
+ self.remove_toolitem(event.tool.name)
+
+ def _get_image_filename(self, 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')
+ if image is not None:
+ fname = os.path.join(basedir, image)
+ else:
+ fname = None
+ return fname
+
+ def trigger_tool(self, name):
+ """
+ Trigger the tool
+
+ Parameters
+ ----------
+ name : String
+ Name(id) of the tool triggered from within the container
+
+ """
+ self.toolmanager.trigger_tool(name, sender=self)
+
+ def add_toolitem(self, name, group, position, image, description, toggle):
+ """
+ Add a toolitem to the container
+
+ This method must get implemented per backend
+
+ The callback associated with the button click event,
+ must be **EXACTLY** `self.trigger_tool(name)`
+
+ Parameters
+ ----------
+ name : string
+ 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 this tool belongs to
+ position : Int
+ 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
+ Description of the tool, used for the tooltips
+ 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 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 `ToolContainer`
+
+ This method must get implemented per backend
+
+ Called when `ToolManager` emits a `tool_removed_event`
+
+ Parameters
+ ----------
+ name : string
+ Name of the tool to remove
+
+ """
+
+ raise NotImplementedError
+
+
+class StatusbarBase(object):
+ """Base class for the statusbar"""
+ 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"""
+ 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_managers.py b/lib/matplotlib/backend_managers.py
new file mode 100644
index 000000000000..41d80c96a9d1
--- /dev/null
+++ b/lib/matplotlib/backend_managers.py
@@ -0,0 +1,391 @@
+"""
+`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 matplotlib.cbook as cbook
+import matplotlib.widgets as widgets
+from matplotlib.rcsetup import validate_stringlist
+import matplotlib.backend_tools as tools
+
+
+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`
+
+ 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
+
+ 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
new file mode 100644
index 000000000000..1d952f2417da
--- /dev/null
+++ b/lib/matplotlib/backend_tools.py
@@ -0,0 +1,991 @@
+"""
+Abstract base classes define the primitives for Tools.
+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
+ `matplotlib.backend_managers.ToolManager`
+"""
+
+
+from matplotlib import rcParams
+from matplotlib._pylab_helpers import Gcf
+import matplotlib.cbook as cbook
+from weakref import WeakKeyDictionary
+import numpy as np
+import six
+
+
+class Cursors(object):
+ """Simple namespace for cursor reference"""
+ HAND, POINTER, SELECT_REGION, MOVE = list(range(4))
+cursors = Cursors()
+
+# Views positions tool
+_views_positions = 'viewpos'
+
+
+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_managers.ToolManager`
+
+ Attributes
+ ----------
+ 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
+ ToolManager
+ """
+
+ default_keymap = None
+ """
+ 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
+
+ **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
+ `name` is used as a label in the toolbar button
+ """
+
+ def __init__(self, toolmanager, name):
+ self._name = name
+ self._figure = None
+ self.toolmanager = toolmanager
+ self.figure = toolmanager.canvas.figure
+
+ @property
+ def figure(self):
+ return self._figure
+
+ def trigger(self, sender, event, data=None):
+ """
+ Called when this tool gets used
+
+ This method is called by
+ `matplotlib.backend_managers.ToolManager.trigger_tool`
+
+ Parameters
+ ----------
+ event: `Event`
+ 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
+
+ @figure.setter
+ def figure(self, figure):
+ """
+ Set the figure
+
+ Set the figure to be affected by this tool
+
+ Parameters
+ ----------
+ figure: `Figure`
+ """
+
+ self._figure = 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_managers.ToolManager.remove_tool`
+ """
+ pass
+
+
+class ToolToggleBase(ToolBase):
+ """
+ Toggleable tool
+
+ Every time it is triggered, it switches between enable and disable
+ """
+
+ radio_group = None
+ """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"""
+
+ 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:
+ self.enable(event)
+ self._toggled = not self._toggled
+
+ def enable(self, event=None):
+ """
+ Enable the toggle tool
+
+ `trigger` calls this method when `toggled` is False
+ """
+
+ pass
+
+ def disable(self, event=None):
+ """
+ Disable the toggle tool
+
+ `trigger` call this method when `toggled` is True.
+
+ This can happen in different circumstances
+
+ * Click on the toolbar tool button
+ * Call to `matplotlib.backend_managers.ToolManager.trigger_tool`
+ * Another `ToolToggleBase` derived tool is triggered
+ (from the same `ToolManager`)
+ """
+
+ pass
+
+ @property
+ def toggled(self):
+ """State of the toggled tool"""
+
+ return self._toggled
+
+
+class SetCursorBase(ToolBase):
+ """
+ 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
+ """
+ def __init__(self, *args, **kwargs):
+ ToolBase.__init__(self, *args, **kwargs)
+ self._idDrag = self.figure.canvas.mpl_connect(
+ 'motion_notify_event', self._set_cursor_cbk)
+ self._cursor = None
+ self._default_cursor = cursors.POINTER
+ self._last_cursor = self._default_cursor
+ self.toolmanager.toolmanager_connect('tool_added_event',
+ self._add_tool_cbk)
+
+ # process current tools
+ for tool in self.toolmanager.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)
+
+ def _add_tool(self, tool):
+ """set the cursor when the tool is triggered"""
+ if getattr(tool, 'cursor', None) is not None:
+ self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name,
+ self._tool_trigger_cbk)
+
+ def _add_tool_cbk(self, event):
+ """Process every newly added tool"""
+ if event.tool is self:
+ return
+
+ self._add_tool(event.tool)
+
+ 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.set_cursor(self._default_cursor)
+ self._last_cursor = self._default_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
+
+ This method has to be implemented per backend
+ """
+ raise NotImplementedError
+
+
+class ToolCursorPosition(ToolBase):
+ """
+ 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_managers.ToolManager.message_event`"""
+ if self.toolmanager.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.toolmanager.message_event(message, self)
+
+
+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:
+ self.draw_rubberband(*data)
+ else:
+ self.remove_rubberband()
+
+ def draw_rubberband(self, *data):
+ """
+ Draw rubberband
+
+ This method must get implemented per backend
+ """
+ raise NotImplementedError
+
+ def remove_rubberband(self):
+ """
+ Remove rubberband
+
+ This method should get implemented per backend
+ """
+ pass
+
+
+class ToolQuit(ToolBase):
+ """Tool to call the figure manager destroy method"""
+
+ description = 'Quit the figure'
+ default_keymap = rcParams['keymap.quit']
+
+ def trigger(self, sender, event, data=None):
+ Gcf.destroy_fig(self.figure)
+
+
+class ToolEnableAllNavigation(ToolBase):
+ """Tool to enable all axes for toolmanager interaction"""
+
+ description = 'Enables all axes toolmanager'
+ default_keymap = rcParams['keymap.all_axes']
+
+ def trigger(self, sender, event, data=None):
+ 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)
+
+
+class ToolEnableNavigation(ToolBase):
+ """Tool to enable a specific axes for toolmanager interaction"""
+
+ description = 'Enables one axes toolmanager'
+ default_keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9)
+
+ def trigger(self, sender, event, data=None):
+ if event.inaxes is None:
+ return
+
+ n = int(event.key) - 1
+ for i, a in enumerate(self.figure.get_axes()):
+ 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):
+ """Tool to toggle the grid of the figure"""
+
+ description = 'Toogle Grid'
+ default_keymap = rcParams['keymap.grid']
+
+ def trigger(self, sender, event, data=None):
+ if event.inaxes is None:
+ return
+ ToolToggleBase.trigger(self, sender, event, data)
+
+ def enable(self, event):
+ event.inaxes.grid(True)
+ self.figure.canvas.draw_idle()
+
+ def disable(self, event):
+ event.inaxes.grid(False)
+ self.figure.canvas.draw_idle()
+
+
+class ToolFullScreen(ToolToggleBase):
+ """Tool to toggle full screen"""
+
+ description = 'Toogle Fullscreen mode'
+ default_keymap = rcParams['keymap.fullscreen']
+
+ def enable(self, event):
+ self.figure.canvas.manager.full_screen_toggle()
+
+ def disable(self, event):
+ self.figure.canvas.manager.full_screen_toggle()
+
+
+class AxisScaleBase(ToolToggleBase):
+ """Base Tool to toggle between linear and logarithmic"""
+
+ def trigger(self, sender, event, data=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_idle()
+
+ def disable(self, event):
+ self.set_scale(event.inaxes, 'linear')
+ self.figure.canvas.draw_idle()
+
+
+class ToolYScale(AxisScaleBase):
+ """Tool to toggle between linear and logarithmic scales on the Y axis"""
+
+ description = 'Toogle Scale Y axis'
+ default_keymap = rcParams['keymap.yscale']
+
+ def set_scale(self, ax, scale):
+ ax.set_yscale(scale)
+
+
+class ToolXScale(AxisScaleBase):
+ """Tool to toggle between linear and logarithmic scales on the X axis"""
+
+ description = 'Toogle Scale X axis'
+ default_keymap = rcParams['keymap.xscale']
+
+ def set_scale(self, ax, scale):
+ ax.set_xscale(scale)
+
+
+class ToolViewsPositions(ToolBase):
+ """
+ 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.
+
+ * `ToolZoom`
+ * `ToolPan`
+ * `ToolHome`
+ * `ToolBack`
+ * `ToolForward`
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.views = WeakKeyDictionary()
+ self.positions = WeakKeyDictionary()
+ ToolBase.__init__(self, *args, **kwargs)
+
+ 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
+ self.push_current()
+ # Adding the clear method as axobserver, removes this burden from
+ # the backend
+ self.figure.add_axobserver(self.clear)
+
+ def clear(self, figure):
+ """Reset the axes stack"""
+ if figure in self.views:
+ self.views[figure].clear()
+ self.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):
+ """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`"""
+
+ _on_trigger = None
+
+ def trigger(self, sender, event, data=None):
+ 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):
+ """Restore the original view lim"""
+
+ description = 'Reset original view'
+ image = 'home.png'
+ default_keymap = rcParams['keymap.home']
+ _on_trigger = 'home'
+
+
+class ToolBack(ViewsPositionsBase):
+ """Move back up the view lim stack"""
+
+ description = 'Back to previous view'
+ image = 'back.png'
+ default_keymap = rcParams['keymap.back']
+ _on_trigger = 'back'
+
+
+class ToolForward(ViewsPositionsBase):
+ """Move forward in the view lim stack"""
+
+ description = 'Forward to next view'
+ image = 'forward.png'
+ default_keymap = rcParams['keymap.forward']
+ _on_trigger = 'forward'
+
+
+class ConfigureSubplotsBase(ToolBase):
+ """Base tool for the configuration of subplots"""
+
+ description = 'Configure subplots'
+ image = 'subplots.png'
+
+
+class SaveFigureBase(ToolBase):
+ """Base tool for figure saving"""
+
+ description = 'Save the figure'
+ image = 'filesave.png'
+ default_keymap = rcParams['keymap.save']
+
+
+class ZoomPanBase(ToolToggleBase):
+ """Base class for `ToolZoom` and `ToolPan`"""
+ def __init__(self, *args):
+ ToolToggleBase.__init__(self, *args)
+ self._button_pressed = None
+ 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"""
+ 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)
+ self._idScroll = self.figure.canvas.mpl_connect(
+ 'scroll_event', self.scroll_zoom)
+
+ 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)
+ self.figure.canvas.mpl_disconnect(self._idRelease)
+ self.figure.canvas.mpl_disconnect(self._idScroll)
+
+ def trigger(self, sender, event, data=None):
+ self.toolmanager.get_tool(_views_positions).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_idle() # force re-draw
+
+
+class ToolZoom(ZoomPanBase):
+ """Zoom to rectangle"""
+
+ description = 'Zoom to rectangle'
+ image = 'zoom_to_rect.png'
+ default_keymap = rcParams['keymap.zoom']
+ cursor = cursors.SELECT_REGION
+ radio_group = 'default'
+
+ 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.toolmanager.trigger_tool('rubberband', self)
+ self.toolmanager.get_tool(_views_positions).refresh_locators()
+ 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 != []:
+ self._cancel_action()
+
+ if event.button == 1:
+ self._button_pressed = 1
+ elif event.button == 3:
+ self._button_pressed = 3
+ else:
+ self._cancel_action()
+ return
+
+ x, y = event.x, event.y
+
+ 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()))
+
+ 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 = id1, id2, id3
+ self._zoom_mode = event.key
+
+ 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.toolmanager.trigger_tool('rubberband',
+ self,
+ data=(x, y, lastx, lasty))
+
+ 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 = []
+
+ if not self._xypress:
+ self._cancel_action()
+ 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._cancel_action()
+ 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._zoom_mode = None
+ self.toolmanager.get_tool(_views_positions).push_current()
+ self._cancel_action()
+
+
+class ToolPan(ZoomPanBase):
+ """Pan axes with left mouse, zoom with right"""
+
+ default_keymap = rcParams['keymap.pan']
+ 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)
+ self._idDrag = None
+
+ def _cancel_action(self):
+ self._button_pressed = None
+ self._xypress = []
+ self.figure.canvas.mpl_disconnect(self._idDrag)
+ self.toolmanager.messagelock.release(self)
+ self.toolmanager.get_tool(_views_positions).refresh_locators()
+
+ def _press(self, event):
+ if event.button == 1:
+ self._button_pressed = 1
+ elif event.button == 3:
+ self._button_pressed = 3
+ else:
+ self._cancel_action()
+ return
+
+ x, y = event.x, event.y
+
+ 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.toolmanager.messagelock(self)
+ self._idDrag = self.figure.canvas.mpl_connect(
+ 'motion_notify_event', self._mouse_move)
+
+ def _release(self, event):
+ if self._button_pressed is None:
+ self._cancel_action()
+ return
+
+ self.figure.canvas.mpl_disconnect(self._idDrag)
+ self.toolmanager.messagelock.release(self)
+
+ for a, _ind in self._xypress:
+ a.end_pan()
+ if not self._xypress:
+ self._cancel_action()
+ return
+
+ self.toolmanager.get_tool(_views_positions).push_current()
+ self._cancel_action()
+
+ 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...
+ a.drag_pan(self._button_pressed, event.key, event.x, event.y)
+ self.toolmanager.canvas.draw_idle()
+
+
+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"""
+
+default_toolbar_tools = [['navigation', ['home', 'back', 'forward']],
+ ['zoompan', ['pan', 'zoom']],
+ ['layout', ['subplots']],
+ ['io', ['save']]]
+"""Default tools in the toolbar"""
+
+
+def add_tools_to_manager(toolmanager, tools=default_tools):
+ """
+ Add multiple tools to `ToolManager`
+
+ Parameters
+ ----------
+ 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):
+ toolmanager.add_tool(name, tool)
+
+
+def add_tools_to_container(container, tools=default_toolbar_tools):
+ """
+ Add multiple tools to the container.
+
+ Parameters
+ ----------
+ 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.
+ 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 0600da2fb8a5..32d15fb4fbcc 100644
--- a/lib/matplotlib/backends/backend_gtk3.py
+++ b/lib/matplotlib/backends/backend_gtk3.py
@@ -30,7 +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
+from matplotlib.backend_bases import (ShowBase, ToolContainerBase,
+ StatusbarBase)
+from matplotlib.backend_managers import ToolManager
+from matplotlib import backend_tools
from matplotlib.cbook import is_string_like, is_writable_file_like
from matplotlib.colors import colorConverter
@@ -414,18 +417,31 @@ def __init__(self, canvas, num):
self.canvas.show()
self.vbox.pack_start(self.canvas, True, True, 0)
-
- self.toolbar = self._get_toolbar(canvas)
-
# calculate size for window
w = int (self.canvas.figure.bbox.width)
h = int (self.canvas.figure.bbox.height)
+ self.toolmanager = self._get_toolmanager()
+ 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 self.toolmanager:
+ backend_tools.add_tools_to_manager(self.toolmanager)
+ 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()
- 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)
@@ -438,7 +454,10 @@ 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.toolmanager is not None:
+ pass
+ elif self.toolbar is not None:
+ self.toolbar.update()
self.canvas.figure.add_axobserver(notify_axes_change)
self.canvas.grab_focus()
@@ -469,15 +488,25 @@ 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'] == 'toolmanager':
+ toolbar = ToolbarGTK3(self.toolmanager)
else:
toolbar = None
return toolbar
+ def _get_toolmanager(self):
+ # must be initialised after toolbar has been setted
+ if rcParams['toolbar'] != 'toolbar2':
+ toolmanager = ToolManager(self.canvas)
+ else:
+ toolmanager = None
+ return toolmanager
+
def get_window_title(self):
return self.window.get_title()
@@ -702,6 +731,212 @@ def get_filename_from_user (self):
return filename, self.ext
+
+class RubberbandGTK3(backend_tools.RubberbandBase):
+ def __init__(self, *args, **kwargs):
+ backend_tools.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.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.figure.canvas.draw()
+
+ height = self.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()
+
+
+class ToolbarGTK3(ToolContainerBase, Gtk.Box):
+ def __init__(self, toolmanager):
+ ToolContainerBase.__init__(self, toolmanager)
+ Gtk.Box.__init__(self)
+ self.set_property("orientation", Gtk.Orientation.VERTICAL)
+
+ 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:
+ 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)
+
+ if position is None:
+ position = -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)
+
+ def toggle_toolitem(self, name, toggled):
+ if name not in self._toolitems:
+ return
+ 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.toolmanager.message_event('%s Not in toolbar' % name, self)
+ return
+
+ 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):
+ 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):
+ 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(backend_tools.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 trigger(self, *args, **kwargs):
+ 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)
+
+
+class SetCursorGTK3(backend_tools.SetCursorBase):
+ def set_cursor(self, cursor):
+ self.figure.canvas.get_property("window").set_cursor(cursord[cursor])
+
+
+class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window):
+ def __init__(self, *args, **kwargs):
+ backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs)
+ self.window = None
+
+ def init_window(self):
+ if self.window:
+ return
+ self.window = Gtk.Window(title="Subplot Configuration Tool")
+
+ try:
+ self.window.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.vbox = Gtk.Box()
+ self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
+ self.window.add(self.vbox)
+ self.vbox.show()
+ self.window.connect('destroy', self.destroy)
+
+ 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.window.set_default_size(w, h)
+
+ canvas.show()
+ self.vbox.pack_start(canvas, True, True, 0)
+ 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, sender, event, data=None):
+ self.init_window()
+ self.window.present()
+
+
class DialogLineprops(object):
"""
A GUI dialog for controlling lineprops
@@ -888,5 +1123,11 @@ def error_msg_gtk(msg, parent=None):
dialog.destroy()
+backend_tools.ToolSaveFigure = SaveFigureGTK3
+backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3
+backend_tools.ToolSetCursor = SetCursorGTK3
+backend_tools.ToolRubberband = RubberbandGTK3
+
+Toolbar = ToolbarGTK3
FigureCanvas = FigureCanvasGTK3
FigureManager = FigureManagerGTK3
diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py
index f652d13412be..8ffc8a85318e 100644
--- a/lib/matplotlib/backends/backend_tkagg.py
+++ b/lib/matplotlib/backends/backend_tkagg.py
@@ -20,7 +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
+from matplotlib.backend_bases import (ShowBase, ToolContainerBase,
+ StatusbarBase)
+from matplotlib.backend_managers import ToolManager
+from matplotlib import backend_tools
from matplotlib._pylab_helpers import Gcf
from matplotlib.figure import Figure
@@ -529,21 +532,45 @@ 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.toolmanager = self._get_toolmanager()
+ self.toolbar = self._get_toolbar()
+ self.statusbar = None
+
+ if self.toolmanager:
+ backend_tools.add_tools_to_manager(self.toolmanager)
+ if self.toolbar:
+ backend_tools.add_tools_to_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.toolbar != None: self.toolbar.update()
+ if self.toolmanager is not None:
+ pass
+ elif self.toolbar is not None:
+ self.toolbar.update()
self.canvas.figure.add_axobserver(notify_axes_change)
+ def _get_toolbar(self):
+ if matplotlib.rcParams['toolbar'] == 'toolbar2':
+ toolbar = NavigationToolbar2TkAgg(self.canvas, self.window)
+ elif matplotlib.rcParams['toolbar'] == 'toolmanager':
+ toolbar = ToolbarTk(self.toolmanager, self.window)
+ else:
+ toolbar = None
+ return toolbar
+
+ def _get_toolmanager(self):
+ if rcParams['toolbar'] != 'toolbar2':
+ toolmanager = ToolManager(self.canvas)
+ else:
+ toolmanager = None
+ return toolmanager
+
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 +898,209 @@ def hidetip(self):
if tw:
tw.destroy()
+
+class RubberbandTk(backend_tools.RubberbandBase):
+ def __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
+ y0 = height - y0
+ y1 = height - y1
+ try:
+ self.lastrect
+ except AttributeError:
+ pass
+ else:
+ self.figure.canvas._tkcanvas.delete(self.lastrect)
+ self.lastrect = self.figure.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1)
+
+ def remove_rubberband(self):
+ try:
+ self.lastrect
+ except AttributeError:
+ pass
+ else:
+ self.figure.canvas._tkcanvas.delete(self.lastrect)
+ del self.lastrect
+
+
+class SetCursorTk(backend_tools.SetCursorBase):
+ def set_cursor(self, cursor):
+ self.figure.canvas.manager.window.configure(cursor=cursord[cursor])
+
+
+class ToolbarTk(ToolContainerBase, Tk.Frame):
+ 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,
+ width=int(width), height=int(height),
+ 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):
+ frame = self._get_groupframe(group)
+ button = self._Button(name, image_file, toggle, frame)
+ if description is not None:
+ ToolTip.createToolTip(button, description)
+ 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=frame, text=text, padx=2, pady=2, image=im,
+ command=lambda: self._button_click(text))
+ else:
+ b = Tk.Checkbutton(master=frame, 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.trigger_tool(name)
+
+ def toggle_toolitem(self, name, toggled):
+ if name not in self._toolitems:
+ return
+ for toolitem in self._toolitems[name]:
+ if toggled:
+ toolitem.select()
+ else:
+ toolitem.deselect()
+
+ def remove_toolitem(self, name):
+ for toolitem in self._toolitems[name]:
+ toolitem.pack_forget()
+ del self._toolitems[name]
+
+
+class StatusbarTk(StatusbarBase, Tk.Frame):
+ def __init__(self, window, *args, **kwargs):
+ StatusbarBase.__init__(self, *args, **kwargs)
+ 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),
+ 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(backend_tools.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(backend_tools.ConfigureSubplotsBase):
+ def __init__(self, *args, **kwargs):
+ backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs)
+ self.window = None
+
+ def trigger(self, *args):
+ self.init_window()
+ self.window.lift()
+
+ def init_window(self):
+ if self.window:
+ return
+
+ 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 destroy(self, *args, **kwargs):
+ self.window.destroy()
+ self.window = None
+
+
+backend_tools.ToolSaveFigure = SaveFigureTk
+backend_tools.ToolConfigureSubplots = ConfigureSubplotsTk
+backend_tools.ToolSetCursor = SetCursorTk
+backend_tools.ToolRubberband = RubberbandTk
+Toolbar = ToolbarTk
FigureCanvas = FigureCanvasTkAgg
FigureManager = FigureManagerTkAgg
diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py
index 1fdf121b32d8..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'],
+ ['None', 'toolbar2', 'toolmanager'],
ignorecase=True)
return validator(s)