diff --git a/examples/user_interfaces/multifigure_backend_gtk3.py b/examples/user_interfaces/multifigure_backend_gtk3.py
new file mode 100644
index 000000000000..7bc59fada293
--- /dev/null
+++ b/examples/user_interfaces/multifigure_backend_gtk3.py
@@ -0,0 +1,56 @@
+import matplotlib
+matplotlib.use('GTK3Agg')
+matplotlib.rcParams['backend.single_window'] = True
+import matplotlib.pyplot as plt
+
+
+import numpy as np
+
+x = np.arange(100)
+
+#Create 4 figures
+fig1 = plt.figure()
+ax1 = fig1.add_subplot(111)
+ax1.plot(x, x)
+
+fig2 = plt.figure()
+ax2 = fig2.add_subplot(111)
+ax2.plot(x, np.sqrt(x))
+
+
+fig3 = plt.figure()
+ax3 = fig3.add_subplot(111)
+ax3.plot(x, x ** 2)
+
+fig4 = plt.figure()
+ax4 = fig4.add_subplot(111)
+ax4.plot(x, x ** 3)
+
+
+###################
+#Figure management
+#Change the figure1 tab label
+fig1.canvas.manager.set_window_title('Just a line')
+
+#Change the figure manager window title
+fig1.canvas.manager.set_mainwindow_title('The powerful window manager')
+
+#Detach figure3 from the rest
+fig3.canvas.manager.detach()
+
+#Put the figure4 in the same manager as fig3
+fig4.canvas.manager.reparent(fig3)
+
+#Control the parent from the figure instantiation with the parent argument
+#To place it in the same parent as fig1 we have several options
+#parent=fig1
+#parent=fig1.canvas.manager
+#parent=fig2.canvas.manager.parent
+fig5 = plt.figure(parent=fig1)
+ax5 = fig5.add_subplot(111)
+ax5.plot(x, x**4)
+#if we want it in a separate window
+#parent=False
+
+
+plt.show()
diff --git a/examples/user_interfaces/reconfigurable_toolbar_gtk3.py b/examples/user_interfaces/reconfigurable_toolbar_gtk3.py
new file mode 100644
index 000000000000..266b2665361c
--- /dev/null
+++ b/examples/user_interfaces/reconfigurable_toolbar_gtk3.py
@@ -0,0 +1,38 @@
+import matplotlib
+matplotlib.use('GTK3Agg')
+matplotlib.rcParams['backend.single_window'] = True
+import matplotlib.pyplot as plt
+from matplotlib.backend_bases import ToolBase
+import numpy as np
+
+x = np.arange(100)
+#Create 4 figures
+fig1 = plt.figure()
+ax1 = fig1.add_subplot(111)
+ax1.plot(x, x)
+
+
+###################
+#Toolbar management
+
+#Lets reorder the buttons in the fig3-fig4 toolbar
+#Back? who needs back? my mom always told me, don't look back,
+fig1.canvas.manager.toolbar.remove_tool(1)
+
+#Move home somewhere nicer
+fig1.canvas.manager.toolbar.move_tool(0, 8)
+
+
+class SampleNonGuiTool(ToolBase):
+ text = 'Stats'
+
+ def set_figures(self, *figures):
+ #stupid routine that says how many axes are in each figure
+ for figure in figures:
+ title = figure.canvas.get_window_title()
+ print('Figure "%s": Has %d axes' % (title, len(figure.axes)))
+
+#Add simple SampleNonGuiTool to the toolbar of fig1-fig2
+fig1.canvas.manager.toolbar.add_tool(SampleNonGuiTool)
+
+plt.show()
diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py
index 39489fb1ae5b..e0330d668344 100644
--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -25,6 +25,28 @@
the 'show' callable is then set to Show.__call__, inherited from
ShowBase.
+The following classes, are the classes that define a multi-figure-manager, this is a variant
+of figure manager, that allows to have multiple figures under the same GUI interface
+
+:class:`ChildFigureManager`
+ The class that initializes the gui and the Navigation (toolbar state),
+ this is the class that the canvas sees as the manager
+
+:class:`MultiFigureManagerBase`
+ The base class for gui interface that allows to have several canvas groupped
+ under the control of the same window and same toolbar
+
+:class:`Navigation`
+ Class that holds the navigation state (or toolbar state) for one specific canvas.
+ This class is attached to `ChildFigureManager.navigation`
+
+:class:`MultiFigureToolbarBase`
+ The base class that defines the GUI interface for the toolbar of the `MultiFigureManagerBase`
+ It allows to swtich the control from one canvas to another.
+
+:class:`ToolBase`
+ The base class for tools that can be added to a derivate of `MultiFigureToolbarBase`
+
"""
from __future__ import (absolute_import, division, print_function,
@@ -37,6 +59,7 @@
import warnings
import time
import io
+import weakref
import numpy as np
import matplotlib.cbook as cbook
@@ -3207,3 +3230,1212 @@ def zoom(self, *args):
def set_history_buttons(self):
"""Enable or disable back/forward button"""
pass
+
+
+class ChildFigureManager(FigureManagerBase):
+ """Entry point for multi-figure-manager backend
+
+ Extension of `FigureManagerBase` to allow the canvas, to be attached and detached from its GUI interface
+
+ The `parent` is the GUI interface, that is responsible for everything related to display and external controls
+ This `ChildFigureManager` is responsible for parent instantiation and assignment.
+
+ Attributes
+ ----------
+ parent : MultiFigureManager
+ Instance derivate of `MultiFigureManagerBase` that serves as container for several canvas
+ navigation : `Navigation`
+ The navigation state of the `canvas`
+ canvas : FigureManagerCanvas
+ Canvas that is managed by this `ChildFigureManager`
+
+ Examples
+ ----------
+ To access this instance from a figure instance use
+
+ >>> figure.canvas.manager
+
+ In Gtk3 the interaction with this class is limited to
+
+ >>> class FigureManagerGTK3(ChildFigureManager):
+ >>> parent_class = MultiFigureManagerGTK3
+
+ Notes
+ ----------
+ To change the figure manager functionality, subclass `MultiFigureManagerBase`
+
+ In general it is not necessary to overrride this class.
+ """
+ _parent = None
+ parent_class = None
+ """multi-figure-manager class that will holds this child"""
+
+ navigation_class = None
+ """Navigation class that will be instantiated as navigation for this child"""
+
+ @classmethod
+ def get_parent(cls, parent=None):
+ """Get the parent instance
+
+ Parameters
+ ----------
+ parent: None (defatult), False, `Figure`, `ChildFigureManager`, `MultiFigureManagerBase`
+ Used to determine wich parent to set and if necessary instantiate
+
+ Notes
+ ----------
+ if `parent` is:
+ - False: `parent_class` is instantiated every time (default in rcParams)
+ - None or True: `parent_class` is instantiated the first time
+ and reused everytime after
+ - `Figure`, `ChildFigureManager`, `MultiFigureManagerBase`: Try to extract the parent from
+ the given instance
+ """
+
+ #Force new parent for the child
+ if parent is False:
+ new_parent = cls.parent_class()
+
+ #New parent only if there is no previous parent
+ elif parent in (None, True):
+ if cls._parent is None or cls._parent() is None:
+ new_parent = cls.parent_class()
+ else:
+ new_parent = cls._parent()
+ #fig2 = plt.figure(parent=fig1.canvas.manager)
+ elif isinstance(parent, ChildFigureManager):
+ new_parent = parent.parent
+
+ #fig2 = plt.figure(parent=fig1.canvas.manager.parent)
+ elif isinstance(parent, MultiFigureManagerBase):
+ new_parent = parent
+
+ else:
+ #fig2 = plt.figure(parent=fig1)
+ try:
+ parent = parent.canvas.manager.parent
+ except AttributeError:
+ raise AttributeError('%s is not a Figure, ChildFigureManager or a MultiFigureManager' % parent)
+ else:
+ new_parent = parent
+
+ #keep the reference only if there are children with this parent
+ cls._parent = weakref.ref(new_parent)
+ return new_parent
+
+ def __init__(self, canvas, num, parent=None):
+ self.parent = self.get_parent(parent)
+ FigureManagerBase.__init__(self, canvas, num)
+
+ if self.navigation_class is None:
+ self.navigation_class = Navigation
+ self.navigation = self.navigation_class(self.canvas)
+ self.navigation.set_toolbar(self.parent.toolbar)
+ self.parent.add_child(self)
+ self.canvas.show()
+
+ def notify_axes_change(fig):
+ 'this will be called whenever the current axes is changed'
+ if self.navigation is not None: self.navigation.update()
+ canvas.figure.add_axobserver(notify_axes_change)
+
+ def reparent(self, parent):
+ """Change the multi-figure-manager controlling this child
+
+ Change the control and visual location of the manager from one multi-figure-manager
+ to another
+
+ Parameters
+ ----------
+ parent:
+ Instance from where to extract the parent
+
+ See Also
+ --------
+ get_parent: Used to get the new parent
+
+ Examples
+ ----------
+ To reparent (group) fig2 in the same parent of fig1
+
+ >>> fig2.canvas.manager.reparent(fig1)
+
+ Notes
+ ----------
+ Not supported by all backends (tk,...)
+ """
+
+ self.navigation.detach()
+ self.parent.remove_child(self)
+
+ self.parent = self.get_parent(parent)
+ self.navigation.set_toolbar(self.parent.toolbar)
+ self.parent.add_child(self)
+ self.canvas.show()
+
+ def detach(self):
+ """Remove this child from current parent instantiating a new(empty) one
+
+ Notes
+ ----------
+ Not supported by all backends (tk,...)
+
+ Examples
+ ----------
+ To detach a figure instance
+
+ >>> figure.canvas.manager.detach()
+
+ """
+
+ parent = self.get_parent(parent=False)
+ self.reparent(parent)
+ self.parent.show()
+
+ def show(self):
+ """Ask `parent` to show this child
+ """
+
+ self.parent.show_child(self)
+
+ def destroy(self):
+ """Remove from parent and from toolbar, and destroy the canvas
+
+ Notes:
+ ----------
+ This method is called from Gcf.destroy(num)
+ """
+
+ self.navigation.detach()
+ self.parent.remove_child(self)
+ del self.parent
+ del self.navigation
+ #For some reason there is not destroy in canvas base
+ try:
+ self.canvas.destroy()
+ except AttributeError:
+ pass
+
+ def resize(self, w, h):
+ """Ask the `parent` to resize the space available for this canvas
+ """
+
+ self.parent.resize_child(self, w, h)
+
+ def show_popup(self, msg):
+ """Ask `parent` to Pop up a message to the user
+
+ Parameters
+ ----------
+ msg : string
+ Text to show
+ """
+ self.parent.show_popup(self, msg)
+
+ def get_window_title(self):
+ """Get the title of the window/tab/... containing this canvas
+ """
+ return self.parent.get_child_title(self)
+
+ def set_window_title(self, title):
+ """Set the title of the window/tab/... containing this canvas
+ """
+ self.parent.set_child_title(self, title)
+
+ def get_mainwindow_title(self):
+ """Get the title of the `parent` window
+ """
+ return self.parent.get_window_title()
+
+ def set_mainwindow_title(self, title):
+ """Set the title of the `parent` window
+ """
+ self.parent.set_window_title(title)
+
+ def __getattr__(self, name):
+ #There are some parent attributes that we want to reflect as ours
+ if name in ('toolbar', 'window', 'full_screen_toggle'):
+ return getattr(self.parent, name)
+ raise AttributeError('Unknown attribute %s' % name)
+
+
+class MultiFigureManagerBase(object):
+ """Base class for the multi-figure-manager
+
+ This class defines the basic methods that the backend specific GUI interface implementation
+ has to have.
+
+ .. note:: This class is instantiated automatically by `ChildFigureManager.get_parent` and does not
+ passes any argument
+
+ .. warning:: The `__init__` method should not receive any argument
+
+ Notes
+ ----------
+ The mandatory methods for a specific backend are
+
+ - `__init__` : Creation of window, notebooks, etc.. and addition of multi-figure-toolbar if relevant
+ - `destroy`
+ - `add_child`
+ - `remove_child`
+ """
+ def __init__(self):
+ raise NotImplementedError
+
+ def switch_child(self, child):
+ """Method to inform the toolbar that the active canvas has changed
+
+ Parameters
+ ----------
+ child : `ChildFigureManager`
+ Instance of `ChildFigureManager` to set as active in the toolbar
+
+ Notes
+ ----------
+ There is no need to override this method, just to make sure to invoke it when
+ changing the active child
+
+ Examples
+ ----------
+ In the gtk3 backend, this is called when the user selects a new tab after finding the new selected tab
+
+ >>> self.notebook.connect('switch-page', self._on_switch_page)
+ >>> ...
+ >>> def _on_switch_page(self, notebook, pointer, num):
+ >>> canvas = self.notebook.get_nth_page(num)
+ >>> self.switch_child(canvas.manager)
+ """
+
+ #Here we invoke switch_navigation with child.canvas.toolbar instead os child.navigation
+ #because for canvas, navigation is the toolbar
+ if self.toolbar is None:
+ return
+ self.toolbar.switch_navigation(child.canvas.toolbar)
+
+ def destroy(self):
+ """Destroy all the gui stuff
+ """
+ pass
+
+ def add_child(self, child):
+ """Add child
+
+ Add a child to this multi-figure-manager
+
+ Parameters
+ ----------
+ child : `ChildFigureManager`
+ Instance of `ChildFigureManager` that will be controlled by this instance
+
+ Notes
+ ----------
+ This method involves adding the canvas to a container (notebook tab, tree branch, panning window, etc...),
+ providing individual close and detach buttons
+
+ - close button : should call Gcf.destroy(num)
+ - detach button : should call `child.detach` on the child parameter
+
+ This method is called from `ChildFigureManager.__init__` and `ChildFigureManager.reparent`
+
+ This method is not called as answer to a user interaction with the GUI
+
+ """
+ raise NotImplementedError
+
+ def remove_child(self, child):
+ """Remove child
+
+ Remove the child from the control of this multi-figure-manager without destroying it.
+
+ Parameters
+ ----------
+ child : `ChildFigureManager`
+ Instance of `ChildFigureManager` that will be remove from its control
+
+ Notes
+ ----------
+ This method involves removing the container that holds the child
+
+ .. warning:: Do not call destroy on the child, it may be relocated to another parent
+ """
+ #Remove the child from the control of this multi-figure-manager
+ #visually and logically
+ #do not destroy the child
+ raise NotImplementedError
+
+ def show_child(self, child):
+ """Find the appropiate child container and show it"""
+ pass
+
+ def set_child_title(self, child, title):
+ """
+ Set the title text of the container (notebook tab/tree branch name/etc...) containing the figure.
+ """
+ pass
+
+ def get_child_title(self, child):
+ """
+ Get the title text of the container (notebook tab/tree branch name/etc...) containing the figure
+ """
+ pass
+
+ def set_window_title(self, title):
+ """
+ Set the title text of the multi-figure-manager window.
+ """
+ pass
+
+ def get_window_title(self):
+ """
+ Get the title text of the multi-figure-manager window.
+ """
+ pass
+
+ def show(self):
+ """Show the multi-figure-manager"""
+ pass
+
+ def full_screen_toggle(self):
+ """Toggle full screen mode"""
+ pass
+
+
+class MultiFigureToolbarBase(object):
+ """Base class for the multi-figure-manager-toolbar
+
+ This class defines the basic methods that the backend specific implementation
+ has to have.
+
+ Notes
+ ----------
+ The mandatory methods for a specific backend are
+
+ - `add_toolitem`
+ - `connect_toolitem`
+ - `init_toolbar`
+ - `save_figure`
+ - `save_all_figures`
+
+ The suggested methods to implement are
+
+ - `remove_tool`
+ - `move_tool`
+ - `set_visible_tool`
+
+
+ Each implementation defines it's own system of coordinates, that use the `pos`
+ argument (used in different methods) to refer to the exact placement of each toolitem
+
+ Examples
+ ----------
+ To access this instance from a figure isntance
+
+ >>> figure.canvas.toolbar.toolbar
+
+ Some undefined attributes of `Navigation` call this class via
+ `Navigation.__getattr__`, most of the time it can be accesed directly with
+
+ >>> figure.canvas.toolbar
+ """
+ toolitems = ({'text': 'Home',
+ 'tooltip_text': 'Reset original view',
+ 'image': 'home',
+ 'callback': 'home'},
+
+ {'text': 'Back',
+ 'tooltip_text': 'Back to previous view',
+ 'image': 'back',
+ 'callback': 'back'},
+
+ {'text': 'Forward',
+ 'tooltip_text': 'Forward to next view',
+ 'image': 'forward',
+ 'callback': 'forward'},
+
+ None,
+
+ {'text': 'Pan',
+ 'tooltip_text': 'Pan axes with left mouse, zoom with right',
+ 'image': 'move',
+ 'callback': 'pan'},
+
+ {'text': 'Zoom',
+ 'tooltip_text': 'Zoom to rectangle',
+ 'image': 'zoom_to_rect',
+ 'callback': 'zoom'},
+
+ {'text': 'Save',
+ 'tooltip_text': 'Save the figure',
+ 'image': 'filesave',
+ 'callback': 'save_figure'},
+
+ {'text': 'SaveAll',
+ 'tooltip_text': 'Save all figures',
+ 'image': 'saveall',
+ 'callback': 'save_all_figures'},
+
+ None,
+ )
+ """toolitems=({})
+
+ List of Dictionnaries containing the default toolitems to add to the toolbar
+
+ Each dict element of contains
+ - text : Text or name for the tool
+ - tooltip_text : Tooltip text
+ - image : Image to use
+ - callback : Function callback definied in this class or derivates
+ """
+ external_toolitems = ()
+ """List of Dictionnaries containing external tools to add to the toolbar
+
+ Each item has the same structure of `toolitems` items but with callback being a
+ string or class pointing to a `ToolBase` derivate
+
+ Examples
+ ----------
+ In Gtk3 backend
+
+ >>> external_toolitems = ({'text': 'Subplots',
+ >>> 'tooltip_text': 'Configure subplots',
+ >>> 'image': 'subplots',
+ >>> 'callback': 'ConfigureSubplotsGTK3'},
+ >>> {'callback': 'LinesProperties'},
+ >>> {'callback': 'AxesProperties'}
+ >>> )
+
+ """
+
+ def __init__(self):
+ self._external_instances = {}
+ self._navigations = []
+ self.init_toolbar()
+ self.add_message()
+
+ for pos, item in enumerate(self.toolitems):
+ if item is None:
+ self.add_separator(pos=pos)
+ continue
+ btn = item.copy()
+ callback = btn.pop('callback')
+ tbutton = self.add_toolitem(pos=pos, **btn)
+ if tbutton:
+ self.connect_toolitem(tbutton, callback)
+ #we need this reference to hide it when only one figure
+ if btn['text'] == 'SaveAll':
+ self.__save_all_toolitem = tbutton
+
+ for pos, item in enumerate(self.external_toolitems):
+ btn = item.copy()
+ callback = btn.pop('callback')
+ i_pos = pos + len(self.toolitems)
+ self.add_tool(callback, pos=i_pos, **btn)
+
+ self.add_separator(len(self.external_toolitems) + len(self.toolitems))
+
+ self._current = None
+
+ def init_toolbar(self):
+ """Initialized the toolbar
+
+ Creates the frame to place the toolitems
+ """
+ raise NotImplementedError
+
+ def add_tool(self, callback, **kwargs):
+ """Add toolitem to the toolbar and connect it to the callback
+
+ The optional arguments are the same strcture as the elements of `external_toolitems`
+
+ .. note:: It is not recommended to override this method when implementing a
+ specifc backend toolbar
+
+ This method calls `add_toolitem` to place the item in the toolbar and `connect_toolitem`
+ to handle the callback
+
+ Parameters
+ ----------
+ callback : String or class that is a derivate of `ToolBase`
+
+ Examples
+ ----------
+ If `SampleTool` is defined (derivate of `ToolBase`)
+
+ >>> fig2.canvas.toolbar.add_tool(SampleTool, text='Stats')
+
+ Will add the `SampleTool` to the toolbar
+
+ Notes
+ ----------
+ The first time this tool is activated it will instantiate the callback class and call set_figures,
+ if activated again, will call the show method of the callback class
+
+ If the active figure changes (switch the active figure from the manager)
+ the set_figures method of the callback class is invoked again.
+
+ """
+
+ cls = self._get_cls_to_instantiate(callback)
+ if not cls:
+ self.set_message('%s Not found' % callback)
+ return
+
+ #if not passed directly from the call, look for them in the class
+ text = kwargs.pop('text', cls.text)
+ tooltip_text = kwargs.pop('tooltip_text', cls.tooltip_text)
+ pos = kwargs.pop('pos', cls.pos)
+ image = kwargs.pop('image', cls.image)
+
+ tbutton = self.add_toolitem(pos=pos, text=text,
+ tooltip_text=tooltip_text,
+ image=image)
+ if not tbutton:
+ return
+
+ self.connect_toolitem(tbutton, '_external_callback', cls, **kwargs)
+
+ def remove_tool(self, pos):
+ """Remove the tool located at given position
+
+ .. note:: It is recommended to implement this method
+
+ Parameters
+ ----------
+ pos : backend specific
+ Position (coordinates) where the tool to remove is located
+ """
+ #remote item from the toolbar,
+ pass
+
+ def set_visible_tool(self, toolitem, visible):
+ """Toggle the visibility of a toolitem
+
+ Parameters
+ ----------
+ toolitem: backend specific
+ toolitem returned by `add_toolitem`
+ visible: bool
+ if true set visible,
+ if false set invisible
+
+ Notes
+ ----------
+ This method is used to automatically hide save_all button when
+ there is only one figure. It is called from `add_navigation` and
+ `remove_navigation`
+ """
+
+ pass
+
+ def move_tool(self, pos_ini, pos_fin):
+ """Move the tool between to positions
+
+ .. note:: It is recommended to implement this method
+
+ Parameters
+ ----------
+ pos_ini : backend specific
+ Position (coordinates) where the tool to is located
+ pos_fin : backend specific
+ New position (coordinates) where the tool will reside
+ """
+ #move item in the toolbar
+ pass
+
+ def connect_toolitem(self, toolitem, callback, *args, **kwargs):
+ """Connect the tooitem to the callback
+
+ This is backend specific, takes the arguments and connect the added tool to
+ the callback passing *args and **kwargs to the callback
+
+ The action is the 'clicked' or whatever name in the backend for the activation of the tool
+
+ Parameters
+ ----------
+ toolitem : backend specific
+ Toolitem returned by `add_toolitem`
+ callback : method
+ Method that will be called when the toolitem is activated
+
+ Examples
+ ----------
+ In Gtk3 this method is implemented as
+
+ >>> def connect_toolitem(self, button, callback, *args, **kwargs):
+ >>> def mcallback(btn, cb, args, kwargs):
+ >>> getattr(self, cb)(*args, **kwargs)
+ >>>
+ >>> button.connect('clicked', mcallback, callback, args, kwargs)
+
+ Notes
+ ----------
+ The need for this method is to get rid of all the backend specific signal handling
+ """
+
+ raise NotImplementedError
+
+ def _external_callback(self, callback, **kwargs):
+ #This handles the invocation of external classes
+ #this callback class should take only *figures as arguments
+ #and preform its work on those figures
+ #the instance of this callback is added to _external_instances
+ #to inform them of the switch and destroy
+
+ id_ = id(callback)
+
+ if id_ in self._external_instances:
+ self._external_instances[id_].show()
+ return
+
+ figures = self.get_figures()
+
+ external_instance = callback(*figures, **kwargs)
+ if external_instance.register:
+# print('register', id_)
+ external_instance.unregister = lambda *a, **kw: self.unregister_external(id_)
+ self._external_instances[id_] = external_instance
+
+ def unregister_external(self, id_):
+ """Unregister an external tool instance from the toolbar
+
+ Notes
+ ----------
+ It is not recommended to override this method when implementing a
+ specifc backend toolbar
+
+ When registering an external tool, this method replaces the external the method
+ `ToolBase.unregister` and it is called during `ToolBase.destroy`
+
+ Parameters
+ ----------
+ id_ : int
+ Id of the callback class for the external tool
+ """
+ if id_ in self._external_instances:
+# print ('unregister', id_)
+ del self._external_instances[id_]
+
+ 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 __getattr__(self, name):
+ #The callbacks are handled directly by navigation
+ #A general getattr from _current may get caught in an infinite loop
+ #Navigation has a getattr poiting to his class
+ cbs = [it['callback'] for it in self.toolitems if it is not None]
+ if name in cbs:
+ return getattr(self._current, name)
+ raise AttributeError('Unknown attribute %s' % name)
+
+ def add_navigation(self, navigation):
+ """Add the `Navigation` under the control of this toolbar
+
+ .. note:: It is not recommended to override this method when implementing a
+ specifc backend toolbar
+
+ Parameters
+ ----------
+ navigation : `Navigation`
+ Instance of `Navigation` to add
+
+ Notes
+ ----------
+ This method is called from the child `Navigation.set_toolbar`, during creation and reasignment
+ """
+
+ self._navigations.append(navigation)
+ self._current = navigation
+ state = len(self._navigations) > 1
+ self.set_visible_tool(self.__save_all_toolitem, state)
+
+ def remove_navigation(self, navigation):
+ """Remove the `Navigation` from the control of this toolbar
+
+ .. note:: It is not recommended to override this method when implementing a
+ specifc backend toolbar
+
+ Parameters
+ ----------
+ child : `Navigation`
+ Instance of `Navigation` to remove
+
+ Notes
+ ----------
+ This method is called from `Navigation.detach`
+ """
+
+ self._navigations.remove(navigation)
+ if navigation is self._current:
+ self._current = None
+
+ state = len(self._navigations) > 1
+ self.set_visible_tool(self.__save_all_toolitem, state)
+
+ def get_figures(self):
+ """Return the figures under the control of this toolbar
+
+ The firsrt figure in the list is the active figure
+
+ .. note:: It is not recommended to override this method when implementing a
+ specifc backend toolbar
+
+ Returns
+ ----------
+ list
+ List of figures that are controlled by this toolbar
+ """
+
+ figures = []
+ if self._current:
+ figures = [self._current.canvas.figure]
+ others = [navigation.canvas.figure for navigation in self._navigations if navigation is not self._current]
+ figures.extend(others)
+ return figures
+
+ def add_toolitem(self, text='_', pos=-1,
+ tooltip_text='', image=None):
+
+ """Add toolitem to the toolbar
+
+ Parameters
+ ----------
+ pos : backend specific, optional
+ Position to add the tool, depends on the specific backend how the position is handled
+ it can be an int, dict, etc...
+ text : string, optional
+ Text for the tool
+ tooltip_text : string, optional
+ Text for the tooltip
+ image : string, optional
+ Reference to an image file to be used to represent the tool
+
+ Returns
+ -------
+ toolitem: Toolitem created, backend specific
+
+ Notes
+ ----------
+ There is no need to call this method directly, it is called from `add_tool`
+ """
+
+ raise NotImplementedError
+
+ def add_separator(self, pos=-1):
+ """Add a separator to the toolbar
+
+ Separator is a 'generic' word to describe any kind of item other than toolitem that will be added
+ to the toolbar, for example an extra container to acomodate more tools
+
+ Parameters
+ ----------
+ pos : backend specific, optional
+ Position to add the separator, depends on the specific backend how the position is handled
+ it can be an int, dict, etc...
+
+ """
+ pass
+
+ def switch_navigation(self, navigation):
+ """Switch the current navigation under control
+
+ .. note:: It is not recommended to override this method when implementing a
+ specifc backend toolbar
+
+ Parameters
+ ----------
+ navigation : `Navigation`
+ Navigation that will be controlled
+
+ Notes
+ ----------
+ When the multi-figure-manager switches child, this toolbar needs to switch too, so it controls
+ the correct figure
+
+ If there are external instances (tools) inform them of the switch
+ by invoking instance.set_figures(*figures)
+ """
+
+ #when multi-figure-manager switches child (figure)
+ #this toolbar needs to switch to, so it controls the correct one
+ #if there are external instances (tools) inform them of the switch
+ #by invoking instance.set_figures(*figures)
+
+ if navigation not in self._navigations:
+ raise AttributeError('This container does not control the given child')
+
+ # For these two actions we have to unselect and reselect
+ if self._current and self._current._active in ('PAN', 'ZOOM'):
+ action = self._current._active.lower()
+ getattr(self._current, action)()
+ getattr(navigation, action)()
+ self._current = navigation
+
+ figures = self.get_figures()
+ for v in self._external_instances.values():
+ v.set_figures(*figures)
+
+ def set_navigation_message(self, navigation, text):
+ """Set the message from the child
+
+ Parameters
+ ----------
+ navigation : `ChildNavigationToolbar`
+ Navigation that emits the message
+ text : string
+ Text to be displayed
+
+ Notes
+ ----------
+ In general the message from the navigation are displayed the
+ same as message from the toolbar via `set_message`, overwritting this method
+ the message can be displayed otherwise
+ """
+
+ self.set_message(text)
+
+ def set_navigation_cursor(self, navigation, cursor):
+ """Set the cursor for the navigation
+
+ Parameters
+ ----------
+ navigation : `Navigation`
+ Navigation that will get the new cursor
+ cursor : backend specific
+ Cursor to be used with this navigation
+
+ Notes
+ ----------
+ Called from `Navigation.set_cursor`
+ """
+ pass
+
+ def set_message(self, text):
+ """Set message
+
+ Parameters
+ ----------
+ text : string
+ Text to be displayed
+
+ Notes
+ ----------
+ The message is displayed in the container created by `add_message`
+ """
+ pass
+
+ def add_message(self):
+ """Add message container
+
+ The message in this container will be setted by `set_message` and `set_navigation_message`
+ """
+ pass
+
+ def save_all_figures(self):
+ """Save all figures"""
+
+ raise NotImplementedError
+
+
+class Navigation(NavigationToolbar2):
+ """Holder for navigation information
+
+ In a multi-figure-manager backend, the canvas navigation information is stored here and
+ the controls belongs to a derivate of `MultiFigureToolbarBase`.
+
+ In general it is not necessary to overrride this class. If you need to change the toolbar
+ change the backend derivate of `MultiFigureToolbarBase`
+
+ The `toolbar` is responsible for everything related to external controls,
+ and this is responsible for parent assignment and holding navigation state information.
+
+ There is no need to instantiate this class, this will be done automatically from
+ `ChildFigureManager`
+
+ Attributes
+ ----------
+ toolbar : MultiFigureToolbar
+ Instance derivate of `MultiFigureToolbarBase` that serves as container for several `Navigation`
+
+ Examples
+ ----------
+ To access this instance from a figure instance use
+
+ >>> figure.canvas.toolbar
+
+ Notes
+ ----------
+ Every call to this toolbar that is not defined in `NavigationToolbar2` or here will be passed to
+ `toolbar` via `__getattr__`
+
+ For the canvas there is no concept of navigation, so when it calls the toolbar it pass
+ throught this class first
+ """
+
+ def __init__(self, canvas):
+ self.toolbar = None
+ NavigationToolbar2.__init__(self, canvas)
+
+ #The method should be called _init_navigation but...
+ def _init_toolbar(self):
+ self.ctx = None
+
+ def set_toolbar(self, toolbar):
+ """Add itself to the given toolbar
+
+ Parameters
+ ----------
+ toolbar: MultiFigureToolbar
+ Derivate of `MultiFigureToolbarBase`
+
+ """
+ if self.toolbar is not None:
+ self.detach()
+ self.toolbar = toolbar
+ if toolbar is not None:
+ self.toolbar.add_navigation(self)
+
+ def detach(self):
+ """Remove this instance from the control of `toolbar`
+
+ Notes
+ ----------
+ This method is called from `ChildFigureManager.destroy`, `ChildFigureManager.reparent`
+ and `ChildFigureManager.detach`
+ """
+ #called by ChildFigureManager.destroy method
+ if self.toolbar is not None:
+ self.toolbar.remove_navigation(self)
+ self.toolbar = None
+
+ def set_message(self, s):
+ """Display message from this child
+
+ Parameters
+ ----------
+ s: string
+ Message to be displayed
+ """
+ if self.toolbar is not None:
+ self.toolbar.set_navigation_message(self, s)
+
+ def set_cursor(self, cursor):
+ """Set the cursor to display
+
+ Parameters
+ ----------
+ cursor: cursor
+ """
+ if self.toolbar is not None:
+ self.toolbar.set_navigation_cursor(self, cursor)
+# self.canvas.get_property("window").set_cursor(cursord[cursor])
+
+ def release(self, event):
+ """See: `NavigationToolbar2.release`"""
+ try: del self._pixmapBack
+ except AttributeError: pass
+
+ def dynamic_update(self):
+ """See: `NavigationToolbar2.dynamic_update`"""
+ # legacy method; new method is canvas.draw_idle
+ self.canvas.draw_idle()
+
+ def draw_rubberband(self, event, x0, y0, x1, y1):
+ """
+ See: `NavigationToolbar2.draw_rubberband`
+
+ Notes
+ ----------
+ Adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/189744
+ """
+
+ self.ctx = self.canvas.get_property("window").cairo_create()
+
+ # todo: instead of redrawing the entire figure, copy the part of
+ # the figure that was covered by the previous rubberband rectangle
+ self.canvas.draw()
+
+ height = self.canvas.figure.bbox.height
+ y1 = height - y1
+ y0 = height - y0
+ w = abs(x1 - x0)
+ h = abs(y1 - y0)
+ rect = [int(val) for val in (min(x0, x1), min(y0, y1), w, h)]
+
+ self.ctx.new_path()
+ self.ctx.set_line_width(0.5)
+ self.ctx.rectangle(rect[0], rect[1], rect[2], rect[3])
+ self.ctx.set_source_rgb(0, 0, 0)
+ self.ctx.stroke()
+
+ def __getattr__(self, name):
+ #we suposse everything else that we want from this child
+ #belongs into the toolbar
+ if self.toolbar is not None:
+ return getattr(self.toolbar, name)
+ raise AttributeError('Unknown %s attribute' % name)
+
+
+class ToolBase(object):
+ """Base class for tools that can be added to a multi-figure-toolbar
+
+ Establish the basic frame for tools
+
+ The only mandatory method is `set_figures`
+
+ The optional methods are
+ - `init_tool`
+ - `destroy`
+ - `show`
+
+ Attributes
+ ----------
+ `image`: string
+ `register`: bool
+ `pos`: int (backend specific)
+ `tooltip_text`: string
+ `text`: string
+
+ Examples
+ ----------
+ To define a New Tool called SampleNonGuiTool that just prints the number of
+ lines and axes per figure
+
+ >>> from matplotlib.backend_bases import ToolBase
+ class SampleNonGuiTool(ToolBase):
+ text = 'stats'
+ def set_figures(self, *figures):
+ for figure in figures:
+ title = figure.canvas.get_window_title()
+ print(title)
+ lines = [line for ax in figure.axes for line in ax.lines]
+ print('Axes: %d Lines: %d' % (len(figure.axes), len(lines)))
+
+ To call this Tool on two figure instances
+
+ >>> SampleNonGuiTool(fig3, fig2)
+
+ To add this tool to the toolbar
+
+ >>> fig.canvas.toolbar.add_tool(SampleNonGuiTool)
+ """
+
+ pos = -1 #: Position (coordinates) for the tool in the toolbar
+ text = '_' #: Text for tool in the toolbar
+ tooltip_text = '' #: Tooltip text for the tool in the toolbar
+ image = None #: Image to be used for the tool in the toolbar
+ register = False
+ """Register the tool with the toolbar
+
+ Set to True if this tool is registered by the toolbar and updated at each
+ figure switch, the toolbar overwrites the `unregister` method to be called at destroy
+ """
+
+ def __init__(self, *figures, **kwargs):
+ """
+ Parameters
+ ----------
+ *figures : list, optional
+ List of figures that are going to be used by this tool
+ **kwargs : optional
+ Optional arguments that are going to be passed directly to `init_tool`
+ """
+
+ self.init_tool(**kwargs)
+
+ if figures:
+ self.set_figures(*figures)
+
+ def init_tool(self, **kwargs):
+ """Perform the tool creation
+
+ Do some initialization work as create windows and stuff
+
+ Parameters
+ ----------
+ **kwargs : optional
+ keyword arguments to be consumed during the creation of the tool
+ If the tool is added after toolbar creation, pass this arguments during the call
+ to `MultiFigureToolbarBase.add_tool`
+
+ Examples
+ ----------
+ If wanting to add the `backends.backend_gtk3.LinesProperties`
+
+ >>> from matplotlib.backends.backend_gtk3 import LinesProperties
+ >>> fig.canvas.toolbar.add_tool(LinesProperties, pick=False)
+
+ This pick argument is used in the `backends.backend_gtk3.LinesProperties.init_tool`
+ to prevent the tool to connect to the pick event
+ """
+
+ #do some initialization work as create windows and stuff
+ #kwargs are the keyword paramters given by the user
+ if kwargs:
+ raise TypeError('init_tool() got an unexpected keyword arguments %s' % str(kwargs))
+
+ def set_figures(self, *figures):
+ """Set the figures to be used by the tool
+
+ .. warning:: Do not modify the signature of this method
+
+ Parameters
+ ----------
+ *figures : list of figures
+
+ Notes
+ ----------
+ This is the main work, many non gui tools use only this method.
+
+ Make sure it receives an array *figures. The toolbar caller
+ always sends an array with all the figures
+
+ The first figure of the array is the current figure (from the toolbar point of view)
+ if it uses only the fisrt one, use it as figure = figures[0]
+ """
+
+ raise NotImplementedError
+
+ def destroy(self, *args):
+ """Destroy the tool
+
+ Perform the destroy action of the tool,
+
+ .. note:: This method should call `unregister`
+
+ """
+ self.unregister()
+
+ def show(self):
+ """Bring to focus the tool
+
+ Examples
+ ----------
+ In Gtk3 this is normally implented as
+
+ >>> self.window.show_all()
+ >>> self.window.present()
+ """
+ pass
+
+ def unregister(self, *args):
+ """Unregister the tool with the toolbar
+
+ .. warning:: Never override this method
+
+ Notes
+ ----------
+ This method is overriden by `MultiFigureToolbarBase` derivate during the initialization of
+ this tool
+ """
+ pass
diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py
index 740d8bb0e872..cdd3d51c6eb6 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, \
+ MultiFigureManagerBase, MultiFigureToolbarBase, ToolBase, ChildFigureManager
from matplotlib.backend_bases import ShowBase
from matplotlib.cbook import is_string_like, is_writable_file_like
@@ -38,6 +39,7 @@ def fn_name(): return sys._getframe(1).f_code.co_name
from matplotlib.widgets import SubplotTool
from matplotlib import lines
+from matplotlib import markers
from matplotlib import cbook
from matplotlib import verbose
from matplotlib import rcParams
@@ -361,22 +363,20 @@ def stop_event_loop(self):
FigureCanvas = FigureCanvasGTK3
-class FigureManagerGTK3(FigureManagerBase):
- """
- Public attributes
- canvas : The FigureCanvas instance
- num : The Figure number
- toolbar : The Gtk.Toolbar (gtk only)
- vbox : The Gtk.VBox containing the canvas and toolbar (gtk only)
- window : The Gtk.Window (gtk only)
- """
- def __init__(self, canvas, num):
- if _debug: print('FigureManagerGTK3.%s' % fn_name())
- FigureManagerBase.__init__(self, canvas, num)
+class MultiFigureManagerGTK3(MultiFigureManagerBase):
+ #to acces from figure instance
+ #figure.canvas.manager.parent!!!!!
+
+ def __init__(self, *args):
+ self._children = []
+ self._labels = {}
+ self._w_min = 0
+ self._h_min = 0
+ if _debug: print('%s.%s' % (self.__class__.__name__, fn_name()))
self.window = Gtk.Window()
- self.set_window_title("Figure %d" % num)
+ self.window.set_title("MultiFiguremanager")
try:
self.window.set_icon_from_file(window_icon)
except (SystemExit, KeyboardInterrupt):
@@ -391,224 +391,301 @@ def __init__(self, canvas, num):
self.vbox = Gtk.Box()
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
- self.window.add(self.vbox)
- self.vbox.show()
- self.canvas.show()
+ self.notebook = Gtk.Notebook()
- self.vbox.pack_start(self.canvas, True, True, 0)
+ self.notebook.set_scrollable(True)
- self.toolbar = self._get_toolbar(canvas)
+ self.notebook.connect('switch-page', self._on_switch_page)
+ self.notebook.set_show_tabs(False)
- # calculate size for window
- w = int (self.canvas.figure.bbox.width)
- h = int (self.canvas.figure.bbox.height)
+ self.vbox.pack_start(self.notebook, True, True, 0)
+ self.window.add(self.vbox)
+
+ self.toolbar = self._get_toolbar()
if self.toolbar is not None:
- self.toolbar.show()
+ self.toolbar.show_all()
self.vbox.pack_end(self.toolbar, False, False, 0)
- size_request = self.toolbar.size_request()
- h += size_request.height
- self.window.set_default_size (w, h)
+ def destroy_window(*args):
+ nums = [manager.num for manager in self._children]
+ for num in nums:
+ Gcf.destroy(num)
+ self.window.connect("destroy", destroy_window)
+ self.window.connect("delete_event", destroy_window)
+
+ self.vbox.show_all()
- def destroy(*args):
- Gcf.destroy(num)
- self.window.connect("destroy", destroy)
- self.window.connect("delete_event", destroy)
if matplotlib.is_interactive():
self.window.show()
- 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.canvas.figure.add_axobserver(notify_axes_change)
+ def _on_switch_page(self, notebook, pointer, num):
+ canvas = self.notebook.get_nth_page(num)
+ self.switch_child(canvas.manager)
- self.canvas.grab_focus()
+ def destroy(self):
+ if _debug: print('%s.%s' % (self.__class__.__name__, fn_name()))
- def destroy(self, *args):
- if _debug: print('FigureManagerGTK3.%s' % fn_name())
self.vbox.destroy()
self.window.destroy()
- self.canvas.destroy()
if self.toolbar:
self.toolbar.destroy()
- self.__dict__.clear() #Is this needed? Other backends don't have it.
- if Gcf.get_num_fig_managers()==0 and \
- not matplotlib.is_interactive() and \
- Gtk.main_level() >= 1:
+ if Gcf.get_num_fig_managers() == 0 and \
+ not matplotlib.is_interactive() and \
+ Gtk.main_level() >= 1:
Gtk.main_quit()
- def show(self):
- # show the figure window
- self.window.show()
+ def remove_child(self, child):
+ '''Remove the child from the multi figure, if it was the last one, destroy itself'''
+ if _debug: print('%s.%s' % (self.__class__.__name__, fn_name()))
+ if child not in self._children:
+ raise AttributeError('This container does not control the given figure child')
+ canvas = child.canvas
+ id_ = self.notebook.page_num(canvas)
+ if id_ > -1:
+ del self._labels[child.num]
+ self.notebook.remove_page(id_)
+ self._children.remove(child)
+
+ if self.notebook.get_n_pages() == 0:
+ self.destroy()
+
+ self._tabs_changed()
+
+ def _tabs_changed(self):
+ #Everytime we change the tabs configuration (add/remove)
+ #we have to check to hide tabs and saveall button(if less than two)
+ #we have to resize because the space used by tabs is not 0
+
+ #hide tabs and saveall button if only one tab
+ if self.notebook.get_n_pages() < 2:
+ self.notebook.set_show_tabs(False)
+ notebook_w = 0
+ notebook_h = 0
+ else:
+ self.notebook.set_show_tabs(True)
+ size_request = self.notebook.size_request()
+ notebook_h = size_request.height
+ notebook_w = size_request.width
+
+ #if there are no children max will fail, so try/except
+ try:
+ canvas_w = max([int(manager.canvas.figure.bbox.width) for manager in self._children])
+ canvas_h = max([int(manager.canvas.figure.bbox.height) for manager in self._children])
+ except ValueError:
+ canvas_w = 0
+ canvas_h = 0
- def full_screen_toggle (self):
- self._full_screen_flag = not self._full_screen_flag
- if self._full_screen_flag:
- self.window.fullscreen()
+ if self.toolbar is not None:
+ size_request = self.toolbar.size_request()
+ toolbar_h = size_request.height
+ toolbar_w = size_request.width
else:
- self.window.unfullscreen()
- _full_screen_flag = False
+ toolbar_h = 0
+ toolbar_w = 0
+
+ w = max(canvas_w, notebook_w, toolbar_w)
+ h = canvas_h + notebook_h + toolbar_h
+ if w and h:
+ self.window.resize(w, h)
+
+ def set_child_title(self, child, title):
+ self._labels[child.num].set_text(title)
+
+ def get_child_title(self, child):
+ return self._labels[child.num].get_text()
+ def set_window_title(self, title):
+ self.window.set_title(title)
+
+ def get_window_title(self):
+ return self.window.get_title()
- 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 = MultiFigureNavigationToolbar2GTK3(self.window)
else:
toolbar = None
return toolbar
- def get_window_title(self):
- return self.window.get_title()
+ def add_child(self, child):
+ if _debug: print('%s.%s' % (self.__class__.__name__, fn_name()))
+ if child in self._children:
+ raise AttributeError('Impossible to add two times the same child')
+ canvas = child.canvas
+ num = child.num
+
+ title = 'Fig %d' % num
+ box = Gtk.Box()
+ box.set_orientation(Gtk.Orientation.HORIZONTAL)
+ box.set_spacing(5)
+
+ label = Gtk.Label(title)
+ self._labels[num] = label
+ self._children.append(child)
+
+ box.pack_start(label, True, True, 0)
+
+ # close button
+ button = Gtk.Button()
+ button.set_tooltip_text('Close')
+ button.set_relief(Gtk.ReliefStyle.NONE)
+ button.set_focus_on_click(False)
+ button.add(Gtk.Image.new_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU))
+ box.pack_end(button, False, False, 0)
+
+ def _remove(btn):
+ Gcf.destroy(num)
- def set_window_title(self, title):
- self.window.set_title(title)
+ button.connect("clicked", _remove)
+
+ # Detach button
+ button = Gtk.Button()
+ button.set_tooltip_text('Detach')
+ button.set_relief(Gtk.ReliefStyle.NONE)
+ button.set_focus_on_click(False)
+ button.add(Gtk.Image.new_from_stock(Gtk.STOCK_JUMP_TO, Gtk.IconSize.MENU))
+ box.pack_end(button, False, False, 0)
- def resize(self, width, height):
- 'set the canvas size in pixels'
- #_, _, cw, ch = self.canvas.allocation
- #_, _, ww, wh = self.window.allocation
- #self.window.resize (width-cw+ww, height-ch+wh)
- self.window.resize(width, height)
+ def _detach(btn):
+ child.detach()
+ button.connect("clicked", _detach)
+ box.show_all()
+ canvas.show()
+
+ self.notebook.append_page(canvas, box)
+ self._tabs_changed()
+ self.show_child(child)
+
+ def show_child(self, child):
+ if _debug: print('%s.%s' % (self.__class__.__name__, fn_name()))
+ self.show()
+ canvas = child.canvas
+ id_ = self.notebook.page_num(canvas)
+ self.notebook.set_current_page(id_)
+
+ def show(self):
+ if _debug: print('%s.%s' % (self.__class__.__name__, fn_name()))
+# self.window.show_all()
+ self.window.show()
-class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar):
- def __init__(self, canvas, window):
+
+class FigureManagerGTK3(ChildFigureManager):
+ parent_class = MultiFigureManagerGTK3
+
+
+class MultiFigureNavigationToolbar2GTK3(Gtk.Box, MultiFigureToolbarBase):
+ external_toolitems = ({'text': 'Subplots',
+ 'tooltip_text': 'Configure subplots',
+ 'image': 'subplots',
+ 'callback': 'ConfigureSubplotsGTK3'},
+ {'callback': 'LinesProperties'},
+ {'callback': 'AxesProperties'}
+ )
+
+ def __init__(self, window):
self.win = window
- GObject.GObject.__init__(self)
- NavigationToolbar2.__init__(self, canvas)
- 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'
- self.ctx = self.canvas.get_property("window").cairo_create()
-
- # todo: instead of redrawing the entire figure, copy the part of
- # the figure that was covered by the previous rubberband rectangle
- self.canvas.draw()
-
- height = self.canvas.figure.bbox.height
- y1 = height - y1
- y0 = height - y0
- w = abs(x1 - x0)
- h = abs(y1 - y0)
- rect = [int(val) for val in (min(x0,x1), min(y0, y1), w, h)]
-
- self.ctx.new_path()
- self.ctx.set_line_width(0.5)
- self.ctx.rectangle(rect[0], rect[1], rect[2], rect[3])
- self.ctx.set_source_rgb(0, 0, 0)
- self.ctx.stroke()
-
- def _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)
+ MultiFigureToolbarBase.__init__(self)
- toolitem = Gtk.SeparatorToolItem()
- self.insert(toolitem, -1)
- toolitem.set_draw(False)
- toolitem.set_expand(True)
+ def set_visible_tool(self, toolitem, visible):
+ toolitem.set_visible(visible)
- toolitem = Gtk.ToolItem()
- self.insert(toolitem, -1)
- self.message = Gtk.Label()
- toolitem.add(self.message)
+ def connect_toolitem(self, button, callback, *args, **kwargs):
+ def mcallback(btn, cb, args, kwargs):
+ getattr(self, cb)(*args, **kwargs)
- self.show_all()
+ button.connect('clicked', mcallback, callback, args, kwargs)
- def get_filechooser(self):
- fc = FileChooserDialog(
- title='Save the figure',
- parent=self.win,
- 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())
- return fc
+ def add_toolitem(self, text='_', pos=-1,
+ tooltip_text='', image=None):
+ timage = None
+ if image:
+ timage = Gtk.Image()
- def save_figure(self, *args):
- chooser = self.get_filechooser()
- fname, format = chooser.get_filename_from_user()
- chooser.destroy()
- if fname:
- startpath = os.path.expanduser(rcParams.get('savefig.directory', ''))
- if startpath == '':
- # explicitly missing key or empty str signals to use cwd
- rcParams['savefig.directory'] = startpath
+ if os.path.isfile(image):
+ timage.set_from_file(image)
else:
- # save dir for next time
- rcParams['savefig.directory'] = os.path.dirname(six.text_type(fname))
- try:
- self.canvas.print_figure(fname, format=format)
- except Exception as e:
- error_msg_gtk(str(e), parent=self)
+ basedir = os.path.join(rcParams['datapath'], 'images')
+ fname = os.path.join(basedir, image + '.png')
+ if os.path.isfile(fname):
+ timage.set_from_file(fname)
+ else:
+ #TODO: Add the right mechanics to pass the image from string
+# from gi.repository import GdkPixbuf
+# pixbuf = GdkPixbuf.Pixbuf.new_from_inline(image, False)
+ timage = False
+
+ tbutton = Gtk.ToolButton()
+
+ tbutton.set_label(text)
+ if timage:
+ tbutton.set_icon_widget(timage)
+ tbutton.set_tooltip_text(tooltip_text)
+ self._toolbar.insert(tbutton, pos)
+ tbutton.show()
+ return tbutton
+
+ def remove_tool(self, pos):
+ widget = self._toolbar.get_nth_item(pos)
+ if not widget:
+ self.set_message('Impossible to remove tool %d' % pos)
+ return
+ self._toolbar.remove(widget)
- 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)
+ def move_tool(self, pos_ini, pos_fin):
+ widget = self._toolbar.get_nth_item(pos_ini)
+ if not widget:
+ self.set_message('Impossible to remove tool %d' % pos_ini)
+ return
+ self._toolbar.remove(widget)
+ self._toolbar.insert(widget, pos_fin)
- w = int (toolfig.bbox.width)
- h = int (toolfig.bbox.height)
+ def add_separator(self, pos=-1):
+ toolitem = Gtk.SeparatorToolItem()
+ self._toolbar.insert(toolitem, pos)
+ return toolitem
+ def init_toolbar(self):
+ Gtk.Box.__init__(self)
+ self.set_property("orientation", Gtk.Orientation.VERTICAL)
+ self._toolbar = Gtk.Toolbar()
+ self._toolbar.set_style(Gtk.ToolbarStyle.ICONS)
+ self.pack_start(self._toolbar, False, False, 0)
- 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.show_all()
- 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)
+ box.show_all()
+ self.pack_end(box, False, False, 5)
- 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)
+ self.show_all()
+
+ def save_figure(self, *args):
+ SaveFiguresDialogGTK3(self.get_figures()[0])
+
+ def save_all_figures(self, *args):
+ SaveFiguresDialogGTK3(*self.get_figures())
+
+ def set_message(self, text):
+ self.message.set_label(text)
+
+ def set_navigation_cursor(self, navigation, cursor):
+ navigation.canvas.get_property("window").set_cursor(cursord[cursor])
class FileChooserDialog(Gtk.FileChooserDialog):
@@ -686,165 +763,750 @@ def get_filename_from_user (self):
return filename, self.ext
-class DialogLineprops:
- """
- A GUI dialog for controlling lineprops
- """
- signals = (
- 'on_combobox_lineprops_changed',
- 'on_combobox_linestyle_changed',
- 'on_combobox_marker_changed',
- 'on_colorbutton_linestyle_color_set',
- 'on_colorbutton_markerface_color_set',
- 'on_dialog_lineprops_okbutton_clicked',
- 'on_dialog_lineprops_cancelbutton_clicked',
- )
-
- linestyles = [ls for ls in lines.Line2D.lineStyles if ls.strip()]
- linestyled = dict([ (s,i) for i,s in enumerate(linestyles)])
+class ConfigureSubplotsGTK3(ToolBase):
+ register = True
+
+ def init_tool(self):
+ self.window = Gtk.Window()
+
+ 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.window.set_title("Subplot Configuration Tool")
+ 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)
- markers = [m for m in lines.Line2D.markers if cbook.is_string_like(m)]
-
- markerd = dict([(s,i) for i,s in enumerate(markers)])
-
- def __init__(self, lines):
- import Gtk.glade
-
- datadir = matplotlib.get_data_path()
- gladefile = os.path.join(datadir, 'lineprops.glade')
- if not os.path.exists(gladefile):
- raise IOError('Could not find gladefile lineprops.glade in %s'%datadir)
+ def reset(self, *args):
+ children = self.vbox.get_children()
+ for child in children:
+ self.vbox.remove(child)
+ del children
- self._inited = False
- self._updateson = True # suppress updates when setting widgets manually
- self.wtree = Gtk.glade.XML(gladefile, 'dialog_lineprops')
- self.wtree.signal_autoconnect(dict([(s, getattr(self, s)) for s in self.signals]))
+ def set_figures(self, *figures):
+ self.reset()
+ figure = figures[0]
+ toolfig = Figure(figsize=(6, 3))
+ canvas = figure.canvas.__class__(toolfig)
- self.dlg = self.wtree.get_widget('dialog_lineprops')
+ toolfig.subplots_adjust(top=0.9)
+ SubplotTool(figure, toolfig)
- self.lines = lines
+ w = int(toolfig.bbox.width)
+ h = int(toolfig.bbox.height)
- cbox = self.wtree.get_widget('combobox_lineprops')
- cbox.set_active(0)
- self.cbox_lineprops = cbox
+ self.window.set_default_size(w, h)
- cbox = self.wtree.get_widget('combobox_linestyles')
- for ls in self.linestyles:
- cbox.append_text(ls)
- cbox.set_active(0)
- self.cbox_linestyles = cbox
+ canvas.show()
+ self.vbox.pack_start(canvas, True, True, 0)
+ self.window.show()
- cbox = self.wtree.get_widget('combobox_markers')
- for m in self.markers:
- cbox.append_text(m)
- cbox.set_active(0)
- self.cbox_markers = cbox
- self._lastcnt = 0
- self._inited = True
+ def show(self):
+ self.window.present()
+
+
+class SaveFiguresDialogGTK3(ToolBase):
+ def set_figures(self, *figs):
+ ref_figure = figs[0]
+ self.figures = figs
+
+ self.ref_canvas = ref_figure.canvas
+ self.current_name = self.ref_canvas.get_default_filename()
+ self.title = 'Save %d Figures' % len(figs)
+
+ if len(figs) > 1:
+ fname_end = '.' + self.ref_canvas.get_default_filetype()
+ self.current_name = self.current_name[:-len(fname_end)]
+
+ chooser = self._get_filechooser()
+ fname, format_ = chooser.get_filename_from_user()
+ chooser.destroy()
+ if not fname:
+ return
+ self._save_figures(fname, format_)
+
+ def _save_figures(self, basename, format_):
+ figs = self.figures
+ 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(basename))
+
+ # Get rid of the extension including the point
+ extension = '.' + format_
+ if basename.endswith(extension):
+ basename = basename[:-len(extension)]
+
+ # In the case of multiple figures, we have to insert a
+ # "figure identifier" in the filename name
+ n = len(figs)
+ if n == 1:
+ figure_identifier = ('',)
+ else:
+ figure_identifier = [str('_%.3d' % figs[i].canvas.manager.num) for i in range(n)]
+ for i in range(n):
+ canvas = figs[i].canvas
+ fname = str('%s%s%s' % (basename, figure_identifier[i], extension))
+ try:
+ canvas.print_figure(fname, format=format_)
+ except Exception as e:
+ error_msg_gtk(str(e), parent=canvas.manager.window)
+ def _get_filechooser(self):
+ fc = FileChooserDialog(
+ title=self.title,
+ parent=self.ref_canvas.manager.window,
+ path=os.path.expanduser(rcParams.get('savefig.directory', '')),
+ filetypes=self.ref_canvas.get_supported_filetypes(),
+ default_filetype=self.ref_canvas.get_default_filetype())
+ fc.set_current_name(self.current_name)
+ return fc
+
+
+class LinesProperties(ToolBase):
+ text = 'Lines'
+ tooltip_text = 'Change line properties'
+ register = True
+ image = 'line_editor'
+
+ _linestyle = [(k, ' '.join(v.split('_')[2:])) for k, v in lines.Line2D.lineStyles.items() if k.strip()]
+ _drawstyle = [(k, ' '.join(v.split('_')[2:])) for k, v in lines.Line2D.drawStyles.items()]
+ _marker = [(k, v) for k, v in markers.MarkerStyle.markers.items() if (k not in (None, '', ' '))]
+
+ _pick_event = None
+
def show(self):
- 'populate the combo box'
- self._updateson = False
- # flush the old
- cbox = self.cbox_lineprops
- for i in range(self._lastcnt-1,-1,-1):
- cbox.remove_text(i)
-
- # add the new
- for line in self.lines:
- cbox.append_text(line.get_label())
- cbox.set_active(0)
-
- self._updateson = True
- self._lastcnt = len(self.lines)
- self.dlg.show()
-
- def get_active_line(self):
- 'get the active line'
- ind = self.cbox_lineprops.get_active()
- line = self.lines[ind]
- return line
-
- def get_active_linestyle(self):
- 'get the active lineinestyle'
- ind = self.cbox_linestyles.get_active()
- ls = self.linestyles[ind]
- return ls
-
- def get_active_marker(self):
- 'get the active lineinestyle'
- ind = self.cbox_markers.get_active()
- m = self.markers[ind]
- return m
-
- def _update(self):
- 'update the active line props from the widgets'
- if not self._inited or not self._updateson: return
- line = self.get_active_line()
- ls = self.get_active_linestyle()
- marker = self.get_active_marker()
- line.set_linestyle(ls)
- line.set_marker(marker)
-
- button = self.wtree.get_widget('colorbutton_linestyle')
- color = button.get_color()
- r, g, b = [val/65535. for val in (color.red, color.green, color.blue)]
- line.set_color((r,g,b))
-
- button = self.wtree.get_widget('colorbutton_markerface')
+ self.window.show_all()
+ self.window.present()
+
+ def init_tool(self, pick=True):
+ self._line = None
+ self._pick = pick
+
+ self.window = Gtk.Window(title='Line properties handler')
+
+ try:
+ self.window.set_icon_from_file(window_icon)
+ except (SystemExit, KeyboardInterrupt):
+ # re-raise exit type Exceptions
+ raise
+ except:
+ pass
+
+ self.window.connect('destroy', self.destroy)
+
+ vbox = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL,
+ column_spacing=5, row_spacing=10, border_width=10)
+
+ self._lines_store = Gtk.ListStore(int, str)
+ self.line_combo = Gtk.ComboBox.new_with_model(self._lines_store)
+ renderer_text = Gtk.CellRendererText()
+ self.line_combo.pack_start(renderer_text, True)
+ self.line_combo.add_attribute(renderer_text, "text", 1)
+ self.line_combo.connect("changed", self._on_line_changed)
+ vbox.attach(self.line_combo, 0, 0, 2, 1)
+
+ vbox.attach_next_to(Gtk.HSeparator(), self.line_combo, Gtk.PositionType.BOTTOM, 2, 1)
+
+ self._visible = Gtk.CheckButton()
+ self._visible.connect("toggled", self._on_visible_toggled)
+
+ visible = Gtk.Label('Visible ')
+ vbox.add(visible)
+ vbox.attach_next_to(self._visible, visible, Gtk.PositionType.RIGHT, 1, 1)
+
+ self.label = Gtk.Entry()
+ self.label.connect('activate', self._on_label_activate)
+
+ label = Gtk.Label('Label')
+ vbox.add(label)
+ vbox.attach_next_to(self.label, label, Gtk.PositionType.RIGHT, 1, 1)
+
+ vbox.attach_next_to(Gtk.HSeparator(), label, Gtk.PositionType.BOTTOM, 2, 1)
+ vbox.add(Gtk.Label('Line', use_markup=True))
+
+ style = Gtk.Label('Style')
+ vbox.add(style)
+
+ drawstyle = Gtk.Label('Draw Style')
+ vbox.add(drawstyle)
+
+ linewidth = Gtk.Label('Width')
+ vbox.add(linewidth)
+
+ color = Gtk.Label('Color')
+ vbox.add(color)
+
+ vbox.attach_next_to(Gtk.HSeparator(), color, Gtk.PositionType.BOTTOM, 2, 1)
+ vbox.add(Gtk.Label('Marker', use_markup=True))
+
+ marker = Gtk.Label('Style')
+ vbox.add(marker)
+
+ markersize = Gtk.Label('Size')
+ vbox.add(markersize)
+
+ markerfacecolor = Gtk.Label('Face Color')
+ vbox.add(markerfacecolor)
+
+ markeredgecolor = Gtk.Label('Edge Color')
+ vbox.add(markeredgecolor)
+
+ for attr, pos in (('linewidth', linewidth), ('markersize', markersize)):
+ button = Gtk.SpinButton(numeric=True, digits=1)
+ adjustment = Gtk.Adjustment(0, 0, 100, 0.1, 10, 0)
+ button.set_adjustment(adjustment)
+ button.connect('value-changed', self._on_size_changed, attr)
+ vbox.attach_next_to(button, pos, Gtk.PositionType.RIGHT, 1, 1)
+ setattr(self, attr, button)
+
+ for attr, pos in (('color', color),
+ ('markerfacecolor', markerfacecolor),
+ ('markeredgecolor', markeredgecolor)):
+ button = Gtk.ColorButton()
+ button.connect('color-set', self._on_color_set, attr)
+ vbox.attach_next_to(button, pos, Gtk.PositionType.RIGHT, 1, 1)
+ setattr(self, attr, button)
+
+ for attr, pos in (('linestyle', style),
+ ('marker', marker),
+ ('drawstyle', drawstyle)):
+ store = Gtk.ListStore(int, str)
+ for i, v in enumerate(getattr(self, '_' + attr)):
+ store.append([i, v[1]])
+ combo = Gtk.ComboBox.new_with_model(store)
+ renderer_text = Gtk.CellRendererText()
+ combo.pack_start(renderer_text, True)
+ combo.add_attribute(renderer_text, "text", 1)
+ combo.connect("changed", self._on_combo_changed, attr)
+ vbox.attach_next_to(combo, pos, Gtk.PositionType.RIGHT, 1, 1)
+ setattr(self, attr + '_combo', combo)
+
+ self.window.add(vbox)
+ self.window.show_all()
+
+ def _on_combo_changed(self, combo, attr):
+ if not self._line:
+ return
+
+ tree_iter = combo.get_active_iter()
+ if tree_iter is None:
+ return
+ store = combo.get_model()
+ id_ = store[tree_iter][0]
+ getattr(self._line, 'set_' + attr)(getattr(self, '_' + attr)[id_][0])
+ self._redraw()
+
+ def _on_size_changed(self, button, attr):
+ if not self._line:
+ return
+
+ getattr(self._line, 'set_' + attr)(getattr(self, attr).get_value())
+ self._redraw()
+
+ def _on_color_set(self, button, attr):
+ if not self._line:
+ return
+
color = button.get_color()
- r, g, b = [val/65535. for val in (color.red, color.green, color.blue)]
- line.set_markerfacecolor((r,g,b))
-
- line.figure.canvas.draw()
-
- def on_combobox_lineprops_changed(self, item):
- 'update the widgets from the active line'
- if not self._inited: return
- self._updateson = False
- line = self.get_active_line()
-
- ls = line.get_linestyle()
- if ls is None: ls = 'None'
- self.cbox_linestyles.set_active(self.linestyled[ls])
-
- marker = line.get_marker()
- if marker is None: marker = 'None'
- self.cbox_markers.set_active(self.markerd[marker])
-
- r,g,b = colorConverter.to_rgb(line.get_color())
- color = Gdk.Color(*[int(val*65535) for val in (r,g,b)])
- button = self.wtree.get_widget('colorbutton_linestyle')
- button.set_color(color)
-
- r,g,b = colorConverter.to_rgb(line.get_markerfacecolor())
- color = Gdk.Color(*[int(val*65535) for val in (r,g,b)])
- button = self.wtree.get_widget('colorbutton_markerface')
- button.set_color(color)
- self._updateson = True
-
- def on_combobox_linestyle_changed(self, item):
- self._update()
-
- def on_combobox_marker_changed(self, item):
- self._update()
-
- def on_colorbutton_linestyle_color_set(self, button):
- self._update()
-
- def on_colorbutton_markerface_color_set(self, button):
- 'called colorbutton marker clicked'
- self._update()
-
- def on_dialog_lineprops_okbutton_clicked(self, button):
- self._update()
- self.dlg.hide()
-
- def on_dialog_lineprops_cancelbutton_clicked(self, button):
- self.dlg.hide()
+ r, g, b = [val / 65535. for val in (color.red, color.green, color.blue)]
+ getattr(self._line, 'set_' + attr)((r, g, b))
+ self._redraw()
+
+ def _on_label_activate(self, *args):
+ if not self._line:
+ return
+ self._line.set_label(self.label.get_text())
+ self._redraw()
+
+ def _on_line_changed(self, combo):
+ tree_iter = combo.get_active_iter()
+ if tree_iter is None:
+ self.line = None
+ return
+
+ id_ = self._lines_store[tree_iter][0]
+ line = self.lines[id_]
+ self._fill(line)
+
+ def _on_visible_toggled(self, *args):
+ if self._line:
+ self._line.set_visible(self._visible.get_active())
+ self._redraw()
+
+ def set_figures(self, *figures):
+ self._line = None
+ self.figure = figures[0]
+ self.lines = self._get_lines()
+
+ self._lines_store.clear()
+
+ for i, l in enumerate(self.lines):
+ self._lines_store.append([i, l.get_label()])
+
+ if self._pick:
+ if self._pick_event:
+ self.figure.canvas.mpl_disconnect(self._pick_event)
+ self._pick_event = self.figure.canvas.mpl_connect('pick_event', self._on_pick)
+
+ def _on_pick(self, event):
+ artist = event.artist
+ if not isinstance(artist, matplotlib.lines.Line2D):
+ return
+
+ try:
+ i = self.lines.index(artist)
+ except ValueError:
+ return
+ self.line_combo.set_active(i)
+
+ def _get_lines(self):
+ lines = set()
+ for ax in self.figure.get_axes():
+ for line in ax.get_lines():
+ lines.add(line)
+
+ #It is easier to find the lines if they are ordered by label
+ lines = list(lines)
+ labels = [line.get_label() for line in lines]
+ a = [line for (_label, line) in sorted(zip(labels, lines))]
+ return a
+
+ def _fill(self, line=None):
+ self._line = line
+ if line is None:
+ return
+
+ self._visible.set_active(line.get_visible())
+ self.label.set_text(line.get_label())
+
+ for attr in ('linewidth', 'markersize'):
+ getattr(self, attr).set_value(getattr(line, 'get_' + attr)())
+
+ for attr in ('linestyle', 'marker', 'drawstyle'):
+ v = getattr(line, 'get_' + attr)()
+ for i, val in enumerate(getattr(self, '_' + attr)):
+ if val[0] == v:
+ getattr(self, attr + '_combo').set_active(i)
+ break
+
+ for attr in ('color', 'markerfacecolor', 'markeredgecolor'):
+ r, g, b = colorConverter.to_rgb(getattr(line, 'get_' + attr)())
+ color = Gdk.Color(*[int(val * 65535) for val in (r, g, b)])
+ getattr(self, attr).set_color(color)
+
+ def _redraw(self):
+ if self._line:
+ self._line.figure.canvas.draw()
+
+ def destroy(self, *args):
+ if self._pick_event:
+ self.figure.canvas.mpl_disconnect(self._pick_event)
+
+ self.unregister()
+
+
+class AxesProperties(ToolBase):
+ """Manage the axes properties
+
+ Subclass of `ToolBase` for axes management
+ """
+
+
+ text = 'Axes'
+ tooltip_text = 'Change axes properties'
+ register = True
+ image = 'axes_editor'
+
+ _release_event = None
+
+ def show(self):
+ self.window.show_all()
+ self.window.present()
+
+ def init_tool(self, release=True):
+ self._line = None
+ self._release = release
+
+ self.window = Gtk.Window(title='Line properties handler')
+
+ try:
+ self.window.set_icon_from_file(window_icon)
+ except (SystemExit, KeyboardInterrupt):
+ # re-raise exit type Exceptions
+ raise
+ except:
+ pass
+
+ self.window.connect('destroy', self.destroy)
+
+ vbox = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL,
+ column_spacing=5, row_spacing=10, border_width=10)
+
+ l = Gtk.Label('Subplots', use_markup=True)
+ vbox.add(l)
+
+ self._subplot_store = Gtk.ListStore(int, str)
+ self._subplot_combo = Gtk.ComboBox.new_with_model(self._subplot_store)
+ renderer_text = Gtk.CellRendererText()
+ self._subplot_combo.pack_start(renderer_text, True)
+ self._subplot_combo.add_attribute(renderer_text, "text", 1)
+ self._subplot_combo.connect("changed", self._on_subplot_changed)
+ vbox.attach_next_to(self._subplot_combo, l, Gtk.PositionType.BOTTOM, 2, 1)
+
+ vbox.attach_next_to(Gtk.HSeparator(), self._subplot_combo, Gtk.PositionType.BOTTOM, 2, 1)
+ l = Gtk.Label('Axes', use_markup=True)
+ vbox.add(l)
+# vbox.attach_next_to(Gtk.HSeparator(), l, Gtk.PositionType.TOP, 2, 1)
+
+ self._axes_store = Gtk.ListStore(int, str)
+ self._axes_combo = Gtk.ComboBox.new_with_model(self._axes_store)
+ renderer_text = Gtk.CellRendererText()
+ self._axes_combo.pack_start(renderer_text, True)
+ self._axes_combo.add_attribute(renderer_text, "text", 1)
+ self._axes_combo.connect("changed", self._on_axes_changed)
+ vbox.attach_next_to(self._axes_combo, l, Gtk.PositionType.BOTTOM, 2, 1)
+
+ self._title = Gtk.Entry()
+ self._title.connect('activate', self._on_title_activate)
+ title = Gtk.Label('Title')
+ vbox.add(title)
+ vbox.attach_next_to(self._title, title, Gtk.PositionType.RIGHT, 1, 1)
+
+ self._legend = Gtk.CheckButton()
+ self._legend.connect("toggled", self._on_legend_toggled)
+
+ legend = Gtk.Label('Legend')
+ vbox.add(legend)
+ vbox.attach_next_to(self._legend, legend, Gtk.PositionType.RIGHT, 1, 1)
+
+ vbox.attach_next_to(Gtk.HSeparator(), legend, Gtk.PositionType.BOTTOM, 2, 1)
+ l = Gtk.Label('X', use_markup=True)
+ vbox.add(l)
+
+ xaxis = Gtk.Label('Visible')
+ vbox.add(xaxis)
+
+ xlabel = Gtk.Label('Label')
+ vbox.add(xlabel)
+
+ xmin = Gtk.Label('Min')
+ vbox.add(xmin)
+
+ xmax = Gtk.Label('Max')
+ vbox.add(xmax)
+
+ xscale = Gtk.Label('Scale')
+ vbox.add(xscale)
+
+ xgrid = Gtk.Label('Grid')
+ vbox.add(xgrid)
+
+ vbox.attach_next_to(Gtk.HSeparator(), xgrid, Gtk.PositionType.BOTTOM, 2, 1)
+ l = Gtk.Label('Y', use_markup=True)
+ vbox.add(l)
+
+ yaxis = Gtk.Label('Visible')
+ vbox.add(yaxis)
+
+ ylabel = Gtk.Label('Label')
+ vbox.add(ylabel)
+
+ ymin = Gtk.Label('Min')
+ vbox.add(ymin)
+
+ ymax = Gtk.Label('Max')
+ vbox.add(ymax)
+
+ yscale = Gtk.Label('Scale')
+ vbox.add(yscale)
+
+ ygrid = Gtk.Label('Grid')
+ vbox.add(ygrid)
+
+ for attr, pos in (('xaxis', xaxis), ('yaxis', yaxis)):
+ checkbox = Gtk.CheckButton()
+ checkbox.connect("toggled", self._on_axis_visible, attr)
+ vbox.attach_next_to(checkbox, pos, Gtk.PositionType.RIGHT, 1, 1)
+ setattr(self, '_' + attr, checkbox)
+
+ for attr, pos in (('xlabel', xlabel), ('ylabel', ylabel)):
+ entry = Gtk.Entry()
+ entry.connect('activate', self._on_label_activate, attr)
+ vbox.attach_next_to(entry, pos, Gtk.PositionType.RIGHT, 1, 1)
+ setattr(self, '_' + attr, entry)
+
+ for attr, pos in (('x_min', xmin,), ('x_max', xmax), ('y_min', ymin), ('y_max', ymax)):
+ entry = Gtk.Entry()
+ entry.connect('activate', self._on_limit_activate, attr)
+ vbox.attach_next_to(entry, pos, Gtk.PositionType.RIGHT, 1, 1)
+ setattr(self, '_' + attr, entry)
+
+ for attr, pos in (('xscale', xscale), ('yscale', yscale)):
+ hbox = Gtk.Box(spacing=6)
+ log_ = Gtk.RadioButton.new_with_label_from_widget(None, "Log")
+ lin_ = Gtk.RadioButton.new_with_label_from_widget(log_, "Linear")
+ log_.connect("toggled", self._on_scale_toggled, attr, "log")
+ lin_.connect("toggled", self._on_scale_toggled, attr, "linear")
+
+ hbox.pack_start(log_, False, False, 0)
+ hbox.pack_start(lin_, False, False, 0)
+ vbox.attach_next_to(hbox, pos, Gtk.PositionType.RIGHT, 1, 1)
+ setattr(self, '_' + attr, {'log': log_, 'linear': lin_})
+
+ for attr, pos in (('x', xgrid), ('y', ygrid)):
+ combo = Gtk.ComboBoxText()
+ for k in ('None', 'Major', 'Minor', 'Both'):
+ combo.append_text(k)
+ vbox.attach_next_to(combo, pos, Gtk.PositionType.RIGHT, 1, 1)
+ combo.connect("changed", self._on_grid_changed, attr)
+ setattr(self, '_' + attr + 'grid', combo)
+
+ self.window.add(vbox)
+ self.window.show_all()
+
+ def _on_grid_changed(self, combo, attr):
+ if self._ax is None:
+ return
+
+ marker = combo.get_active_text()
+ self._ax.grid(False, axis=attr, which='both')
+
+ if marker != 'None':
+ self._ax.grid(False, axis=attr, which='both')
+ self._ax.grid(True, axis=attr, which=marker)
+
+ self._redraw()
+
+ def _on_scale_toggled(self, button, attr, scale):
+ if self._ax is None:
+ return
+
+ getattr(self._ax, 'set_' + attr)(scale)
+ self._redraw()
+
+ def _on_limit_activate(self, entry, attr):
+ if self._ax is None:
+ return
+
+ direction = attr.split('_')[0]
+ min_ = getattr(self, '_' + direction + '_min').get_text()
+ max_ = getattr(self, '_' + direction + '_max').get_text()
+
+ try:
+ min_ = float(min_)
+ max_ = float(max_)
+ except:
+ min_, max_ = getattr(self._ax, 'get_' + direction + 'lim')()
+ getattr(self, '_' + direction + '_min').set_text(str(min_))
+ getattr(self, '_' + direction + '_max').set_text(str(max_))
+ return
+
+ getattr(self._ax, 'set_' + direction + 'lim')(min_, max_)
+ self._redraw()
+
+ def _on_axis_visible(self, button, attr):
+ if self._ax is None:
+ return
+
+ axis = getattr(self._ax, 'get_' + attr)()
+ axis.set_visible(getattr(self, '_' + attr).get_active())
+ self._redraw()
+
+ def _on_label_activate(self, entry, attr):
+ if self._ax is None:
+ return
+
+ getattr(self._ax, 'set_' + attr)(getattr(self, '_' + attr).get_text())
+ self._redraw()
+
+ def _on_legend_toggled(self, *args):
+ if self._ax is None:
+ return
+
+ legend = self._ax.get_legend()
+ if not legend:
+ legend = self._ax.legend(loc='best', shadow=True)
+
+ if legend:
+ legend.set_visible(self._legend.get_active())
+ #Put the legend always draggable,
+ #Maybe a bad idea, but fix the problem of possition
+ try:
+ legend.draggable(True)
+ except:
+ pass
+
+ self._redraw()
+
+ def _on_title_activate(self, *args):
+ if self._ax is None:
+ return
+ self._ax.set_title(self._title.get_text())
+ self._redraw()
+
+ def _on_axes_changed(self, combo):
+ self._ax = None
+ if self._axes is None:
+ return
+ tree_iter = combo.get_active_iter()
+ if tree_iter is None:
+ return
+
+ id_ = self._axes_store[tree_iter][0]
+ ax = self._axes[id_]
+
+ self._fill(ax)
+
+ def _fill(self, ax=None):
+ if ax is None:
+ self._ax = None
+ return
+
+ self._title.set_text(ax.get_title())
+
+ self._legend.set_active(bool(ax.get_legend()) and ax.get_legend().get_visible())
+
+ for attr in ('xlabel', 'ylabel'):
+ t = getattr(ax, 'get_' + attr)()
+ getattr(self, '_' + attr).set_text(t)
+
+ for attr in ('xaxis', 'yaxis'):
+ axis = getattr(ax, 'get_' + attr)()
+ getattr(self, '_' + attr).set_active(axis.get_visible())
+
+ for attr in ('x', 'y'):
+ min_, max_ = getattr(ax, 'get_' + attr + 'lim')()
+ getattr(self, '_' + attr + '_min').set_text(str(min_))
+ getattr(self, '_' + attr + '_max').set_text(str(max_))
+
+ for attr in ('xscale', 'yscale'):
+ scale = getattr(ax, 'get_' + attr)()
+ getattr(self, '_' + attr)[scale].set_active(True)
+
+ for attr in ('x', 'y'):
+ axis = getattr(ax, 'get_' + attr + 'axis')()
+ if axis._gridOnMajor and not axis._gridOnMinor:
+ gridon = 'Major'
+ elif not axis._gridOnMajor and axis._gridOnMinor:
+ gridon = 'Minor'
+ elif axis._gridOnMajor and axis._gridOnMinor:
+ gridon = 'Both'
+ else:
+ gridon = 'None'
+
+ combo = getattr(self, '_' + attr + 'grid')
+ model = combo.get_model()
+ for i in range(len(model)):
+ if model[i][0] == gridon:
+ combo.set_active(i)
+ break
+ self._ax = ax
+
+ def _on_subplot_changed(self, combo):
+ self._axes = None
+ self._ax = None
+ self._axes_store.clear()
+
+ tree_iter = combo.get_active_iter()
+ if tree_iter is None:
+ return
+
+ id_ = self._subplot_store[tree_iter][0]
+ self._axes = self._subplots[id_][1]
+
+ for i in range(len(self._axes)):
+ self._axes_store.append([i, 'Axes %d' % i])
+
+ self._axes_combo.set_active(0)
+
+ def set_figures(self, *figures):
+ self._ax = None
+ self.figure = figures[0]
+ self._subplots = self._get_subplots()
+
+ self._subplot_store.clear()
+
+ for i, l in enumerate(self._subplots):
+ self._subplot_store.append([i, str(l[0])])
+
+ self._subplot_combo.set_active(0)
+
+ if self._release:
+ if self._release_event:
+ self.figure.canvas.mpl_disconnect(self._release_event)
+ self._release_event = self.figure.canvas.mpl_connect('button_release_event', self._on_release)
+
+ def _on_release(self, event):
+ try:
+ ax = event.inaxes.axes
+ except:
+ return
+
+ ax_subplot = [subplot[0] for subplot in self._subplots if ax in subplot[1]][0]
+ current_subplot = [subplot[0] for subplot in self._subplots if self._ax in subplot[1]][0]
+ if ax_subplot == current_subplot:
+ return
+
+ for i, subplot in enumerate(self._subplots):
+ if subplot[0] == ax_subplot:
+ self._subplot_combo.set_active(i)
+ break
+
+ def _get_subplots(self):
+ axes = {}
+ alone = []
+ rem = []
+ for ax in self.figure.get_axes():
+ try:
+ axes.setdefault(ax.get_geometry(), []).append(ax)
+ except AttributeError:
+ alone.append(ax)
+
+ #try to find if share something with one of the axes with geometry
+ for ax in alone:
+ for ax2 in [i for sl in axes.values() for i in sl]:
+ if ((ax in ax2.get_shared_x_axes().get_siblings(ax2)) or
+ (ax in ax2.get_shared_y_axes().get_siblings(ax2))):
+ axes[ax2.get_geometry()].append(ax)
+ rem.append(ax)
+
+ for ax in rem:
+ alone.remove(ax)
+
+ for i, ax in enumerate(alone):
+ axes[i] = [ax, ]
+
+ return [(k, axes[k]) for k in sorted(axes.keys())]
+# return axes
+
+ def destroy(self, *args):
+ if self._release_event:
+ self.figure.canvas.mpl_disconnect(self._release_event)
+
+ self.unregister()
+
+ def _redraw(self):
+ if self._ax:
+ self._ax.figure.canvas.draw()
+
+
+
+
# Define the file to use as the GTk icon
if sys.platform == 'win32':
diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py
index 0c10426c14df..a0dfebbfaa85 100644
--- a/lib/matplotlib/backends/backend_gtk3agg.py
+++ b/lib/matplotlib/backends/backend_gtk3agg.py
@@ -11,7 +11,7 @@
from . import backend_agg
from . import backend_gtk3
from matplotlib.figure import Figure
-from matplotlib import transforms
+from matplotlib import transforms, rcParams
if six.PY3:
warnings.warn("The Gtk3Agg backend is not known to work on Python 3.x.")
@@ -94,16 +94,17 @@ def new_figure_manager(num, *args, **kwargs):
Create a new figure manager instance
"""
FigureClass = kwargs.pop('FigureClass', Figure)
+ parent = kwargs.pop('parent', rcParams['backend.single_window'])
thisFig = FigureClass(*args, **kwargs)
- return new_figure_manager_given_figure(num, thisFig)
+ return new_figure_manager_given_figure(num, thisFig, parent)
-def new_figure_manager_given_figure(num, figure):
+def new_figure_manager_given_figure(num, figure, parent):
"""
Create a new figure manager instance for the given figure.
"""
canvas = FigureCanvasGTK3Agg(figure)
- manager = FigureManagerGTK3Agg(canvas, num)
+ manager = FigureManagerGTK3Agg(canvas, num, parent)
return manager
diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py
index 4421cd0e2fd4..64e8f5174a2d 100644
--- a/lib/matplotlib/backends/backend_gtk3cairo.py
+++ b/lib/matplotlib/backends/backend_gtk3cairo.py
@@ -6,6 +6,8 @@
from . import backend_gtk3
from . import backend_cairo
from matplotlib.figure import Figure
+from matplotlib import rcParams
+
class RendererGTK3Cairo(backend_cairo.RendererCairo):
def set_context(self, ctx):
@@ -22,8 +24,8 @@ def _renderer_init(self):
self._renderer = RendererGTK3Cairo(self.figure.dpi)
def _render_figure(self, width, height):
- self._renderer.set_width_height (width, height)
- self.figure.draw (self._renderer)
+ self._renderer.set_width_height(width, height)
+ self.figure.draw(self._renderer)
def on_draw_event(self, widget, ctx):
""" GtkDrawable draw event, like expose_event in GTK 2.X
@@ -33,7 +35,8 @@ def on_draw_event(self, widget, ctx):
#if self._need_redraw:
self._renderer.set_context(ctx)
allocation = self.get_allocation()
- x, y, w, h = allocation.x, allocation.y, allocation.width, allocation.height
+ x, y = allocation.x, allocation.y
+ w, h = allocation.width, allocation.height
self._render_figure(w, h)
#self._need_redraw = False
@@ -49,16 +52,17 @@ def new_figure_manager(num, *args, **kwargs):
Create a new figure manager instance
"""
FigureClass = kwargs.pop('FigureClass', Figure)
+ parent = kwargs.pop('parent', rcParams['backend.single_window'])
thisFig = FigureClass(*args, **kwargs)
- return new_figure_manager_given_figure(num, thisFig)
+ return new_figure_manager_given_figure(num, thisFig, parent)
-def new_figure_manager_given_figure(num, figure):
+def new_figure_manager_given_figure(num, figure, parent):
"""
Create a new figure manager instance for the given figure.
"""
canvas = FigureCanvasGTK3Cairo(figure)
- manager = FigureManagerGTK3Cairo(canvas, num)
+ manager = FigureManagerGTK3Cairo(canvas, num, parent)
return manager
diff --git a/lib/matplotlib/mpl-data/images/axes_editor.png b/lib/matplotlib/mpl-data/images/axes_editor.png
new file mode 100644
index 000000000000..c97e1a5936c7
Binary files /dev/null and b/lib/matplotlib/mpl-data/images/axes_editor.png differ
diff --git a/lib/matplotlib/mpl-data/images/axes_editor.svg b/lib/matplotlib/mpl-data/images/axes_editor.svg
new file mode 100644
index 000000000000..f505f41169ce
--- /dev/null
+++ b/lib/matplotlib/mpl-data/images/axes_editor.svg
@@ -0,0 +1,644 @@
+
+
+
+
diff --git a/lib/matplotlib/mpl-data/images/axes_editor.xpm b/lib/matplotlib/mpl-data/images/axes_editor.xpm
new file mode 100644
index 000000000000..0f4ad1bfb3a0
--- /dev/null
+++ b/lib/matplotlib/mpl-data/images/axes_editor.xpm
@@ -0,0 +1,39 @@
+/* XPM */
+static char *axes_editor[] = {
+/* columns rows colors chars-per-pixel */
+"24 24 9 1 ",
+" c black",
+". c #7B4D43",
+"X c #BE5516",
+"o c #9E5632",
+"O c #AD5421",
+"+ c #AF5829",
+"@ c #B95B24",
+"# c #0043A5",
+"$ c None",
+/* pixels */
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$.$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$O$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$#$$$o$$$$",
+"$$$$$$ $$$$+$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$@$$$$X$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$ $$ $$ $$ $$",
+" ",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$",
+"$$$$$$ $$$$$$$$$$$$$$$$$"
+};
diff --git a/lib/matplotlib/mpl-data/images/line_editor.png b/lib/matplotlib/mpl-data/images/line_editor.png
new file mode 100644
index 000000000000..1b4b97144dbb
Binary files /dev/null and b/lib/matplotlib/mpl-data/images/line_editor.png differ
diff --git a/lib/matplotlib/mpl-data/images/line_editor.svg b/lib/matplotlib/mpl-data/images/line_editor.svg
new file mode 100644
index 000000000000..a9214997d730
--- /dev/null
+++ b/lib/matplotlib/mpl-data/images/line_editor.svg
@@ -0,0 +1,1589 @@
+
+
+
+
diff --git a/lib/matplotlib/mpl-data/images/line_editor.xpm b/lib/matplotlib/mpl-data/images/line_editor.xpm
new file mode 100644
index 000000000000..f690468bd832
--- /dev/null
+++ b/lib/matplotlib/mpl-data/images/line_editor.xpm
@@ -0,0 +1,187 @@
+/* XPM */
+static char *line_editor[] = {
+/* columns rows colors chars-per-pixel */
+"24 24 157 2 ",
+" c #615C48",
+". c #686665",
+"X c #6E6C6A",
+"o c #6E6C6B",
+"O c #787573",
+"+ c #787877",
+"@ c #860000",
+"# c #8C0000",
+"$ c #920000",
+"% c #B90000",
+"& c #BD0E0E",
+"* c #A11919",
+"= c #922222",
+"- c #952828",
+"; c #C01B1B",
+": c #C11D1D",
+"> c red",
+", c #C32424",
+"< c #C32626",
+"1 c #AE4141",
+"2 c #A64848",
+"3 c #B75959",
+"4 c #B65A5A",
+"5 c #B75F5F",
+"6 c #85787D",
+"7 c #B76464",
+"8 c #C06060",
+"9 c #C86767",
+"0 c #C96767",
+"q c #D16E6E",
+"w c #D26E6E",
+"e c #C27777",
+"r c #C77E7E",
+"t c #C87D7D",
+"y c #DA7575",
+"u c #DB7575",
+"i c #E37C7C",
+"p c #00CC00",
+"a c #00CD00",
+"s c #02CC02",
+"d c #0BCB0B",
+"f c #0CCB0C",
+"g c #FF8B00",
+"h c DarkOrange",
+"j c #FF8D00",
+"k c #FF9905",
+"l c #FF9906",
+"z c #FF9B07",
+"x c #FF9C07",
+"c c #FF9C08",
+"v c #FF9C0A",
+"b c #FF9D0C",
+"n c #FFA700",
+"m c #FFA60C",
+"M c #FFA60D",
+"N c #FFA60E",
+"B c #FFA80F",
+"V c #FFA810",
+"C c #FFA811",
+"Z c #FFA812",
+"A c #FFAC1D",
+"S c #F9B43C",
+"D c #98FF00",
+"F c #99FF00",
+"G c #9AFF00",
+"H c #FFDA26",
+"J c #FFDA28",
+"K c #FFDA2A",
+"L c #FFDB2B",
+"P c #FFDB2D",
+"I c #FCD92E",
+"U c #FFDB2F",
+"Y c #F9C146",
+"T c #FFCB40",
+"R c #FFCB41",
+"E c #FFCB42",
+"W c #FFCC42",
+"Q c #FFCC43",
+"! c #FFCC44",
+"~ c #FFCD44",
+"^ c #FFCD45",
+"/ c #FFCF4F",
+"( c #FFDD40",
+") c #FFE54E",
+"_ c #FFE75A",
+"` c #FFE85E",
+"' c #FFE860",
+"] c #FFE863",
+"[ c #FFE967",
+"{ c #FFE969",
+"} c #FFEA6D",
+"| c #FFE071",
+" . c #FFE072",
+".. c #FFE073",
+"X. c #FFE074",
+"o. c #FFE175",
+"O. c #FFE179",
+"+. c #330098",
+"@. c #340099",
+"#. c #360499",
+"$. c #3D0E9A",
+"%. c #0033CC",
+"&. c #1341C9",
+"*. c #1341CA",
+"=. c #1541CA",
+"-. c #1B47C8",
+";. c #D1138E",
+":. c #CD0A97",
+">. c #CC0098",
+",. c #CD0099",
+"<. c #009898",
+"1. c #009999",
+"2. c #0B9A9A",
+"3. c #0A9B9B",
+"4. c #0F9C9C",
+"5. c #119D9D",
+"6. c #878483",
+"7. c #918180",
+"8. c #939090",
+"9. c #979694",
+"0. c #A38A89",
+"q. c #B7A980",
+"w. c #AAA9A8",
+"e. c #ADABAA",
+"r. c #CF8A8A",
+"t. c #CC908F",
+"y. c #D19191",
+"u. c #D59797",
+"i. c #D79B9A",
+"p. c #EC8383",
+"a. c #F58A8A",
+"s. c #F58B8B",
+"d. c #FD9191",
+"f. c #C7BDA6",
+"g. c #EBAFAD",
+"h. c #E6B0B0",
+"j. c #FFE391",
+"k. c #FCEA91",
+"l. c #FFE69F",
+"z. c #FFE7A2",
+"x. c #FFEEA1",
+"c. c #FFEFA2",
+"v. c #FFEFA3",
+"b. c #FFEFA4",
+"n. c #FFEFA5",
+"m. c #F9E9B0",
+"M. c #FFEBB1",
+"N. c #FFEBB2",
+"B. c #C9C9C8",
+"V. c #CBC9C9",
+"C. c #FFEFC1",
+"Z. c #FFEFC2",
+"A. c #FFF3D1",
+"S. c #FFF7E1",
+"D. c #FFF7E2",
+"F. c #FFFBF1",
+"G. c None",
+/* pixels */
+"G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.d.a.G.G.G.",
+"G.G.G.G.G.G.G.G.%.%.G.G.G.@.+.G.G.G.d.a.p.i G.G.",
+"G.G.G.G.G.G.G.G.%.%.=.G.G.$.#.+.G.0.g.p.i u w G.",
+"G.G.G.G.G.G.G.G.*.-.*.G.G.+.+.G.6.e.V.h.u w 9 5 ",
+"G.G.G.G.1.<.G.G.G.G.G.G.G.G.G.I k.B.w.9.i.0 8 4 ",
+"G.G.G.1.<.2.5.G.G.G.G.G.G.G.H } c.m.8.X O t.4 G.",
+"G.G.G.G.2.4.G.G.G.G.G.G.G.J { c. .T Y X . 7.G.G.",
+"G.G.G.G.G.G.G.G.G.G.G.G.J [ c. .T M n S 6 G.G.G.",
+"G.p p f G.G.G.G.G.G.G.P ] n. .W M n g v :.,.G.G.",
+"G.p p f G.G.G.G.G.G.P ` v. .W M n g v ;.>.,.G.G.",
+"G.p p p G.G.G.G.G.I ` n.o.W M n g v G.G.>.,.G.G.",
+"G.G.G.G.G.G.G.G.U _ n.o.! V n j c G.G.G.G.G.G.G.",
+"G.G.G.G.G.G.G.( ) n.o.^ V n j l G.G.G.G.G.G.G.G.",
+"G.G D G G.G.G.F.S.O.^ V n j l G.G.G.G.G.> > G.G.",
+"G.D D D G.G.G.S.A./ V n j l G.G.G.G.G.> > > G.G.",
+"G.G D G G.G.S.A.C.M.l.v l G.G.G.G.G.G.> > > G.G.",
+"G.G.G.G.G.G.f.C.M.z.j.A G.G.G.G.G.G.G.G.G.G.G.G.",
+"G.G.G.G.G.G.+ q.z.j.G.G.G.G.G.G.G.G.G.G.G.G.G.G.",
+"G.G.G.G.G.G. G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.",
+"G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.",
+"G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.",
+"G.G.e r r.u.y.r = G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.",
+"# % & : < , ; & % @ G.G.G.G.G.G.G.G.G.G.G.G.G.G.",
+"G.$ * 1 5 7 2 - G.G.G.G.G.G.G.G.G.G.G.G.G.G.G.G."
+};
diff --git a/lib/matplotlib/mpl-data/images/saveall.png b/lib/matplotlib/mpl-data/images/saveall.png
new file mode 100644
index 000000000000..fcdfd4f11294
Binary files /dev/null and b/lib/matplotlib/mpl-data/images/saveall.png differ
diff --git a/lib/matplotlib/mpl-data/images/saveall.ppm b/lib/matplotlib/mpl-data/images/saveall.ppm
new file mode 100644
index 000000000000..7ef7ced9c9cd
Binary files /dev/null and b/lib/matplotlib/mpl-data/images/saveall.ppm differ
diff --git a/lib/matplotlib/mpl-data/images/saveall.svg b/lib/matplotlib/mpl-data/images/saveall.svg
new file mode 100644
index 000000000000..66260e8f6b18
--- /dev/null
+++ b/lib/matplotlib/mpl-data/images/saveall.svg
@@ -0,0 +1,32873 @@
+
+
+
+
diff --git a/lib/matplotlib/mpl-data/images/saveall.xpm b/lib/matplotlib/mpl-data/images/saveall.xpm
new file mode 100644
index 000000000000..d3b03d7028d1
--- /dev/null
+++ b/lib/matplotlib/mpl-data/images/saveall.xpm
@@ -0,0 +1,168 @@
+/* XPM */
+static char *saveall[] = {
+/* columns rows colors chars-per-pixel */
+"24 24 138 2 ",
+" c #1D2A43",
+". c #3B495E",
+"X c #004468",
+"o c #0C4768",
+"O c #034B6E",
+"+ c #194C6B",
+"@ c #075375",
+"# c #085476",
+"$ c #0A5678",
+"% c #0B5A7B",
+"& c #165777",
+"* c #185676",
+"= c #1E5876",
+"- c #155778",
+"; c #155878",
+": c #1C5B7A",
+"> c #294962",
+", c #3E4D62",
+"< c #34526A",
+"1 c #39556B",
+"2 c #3D596E",
+"3 c #2F5670",
+"4 c #265A77",
+"5 c #295874",
+"6 c #205D7C",
+"7 c #2C5D78",
+"8 c #315A75",
+"9 c #3E5B71",
+"0 c #325F7A",
+"q c #25607F",
+"w c #3F647A",
+"e c #495457",
+"r c #515454",
+"t c #65564C",
+"y c #675A4C",
+"u c #695B4D",
+"i c #746452",
+"p c #786551",
+"a c #414F64",
+"s c #425E74",
+"d c #4E656E",
+"f c #42667B",
+"g c #526273",
+"h c #6E6663",
+"j c #84654F",
+"k c #906D53",
+"l c #A66F49",
+"z c #D7A463",
+"x c #D9A663",
+"c c #DDAA65",
+"v c #E0AE65",
+"b c #266280",
+"n c #2A6583",
+"m c #2D6986",
+"M c #2B6C89",
+"N c #336E8A",
+"B c #33718C",
+"V c #3A748D",
+"C c #357591",
+"Z c #3D7691",
+"A c #3C7B95",
+"S c #446C84",
+"D c #4D7086",
+"F c #487389",
+"G c #52748A",
+"H c #5A7A8E",
+"J c #427A94",
+"K c #487F99",
+"L c #537F95",
+"P c #627284",
+"I c #6C7D8C",
+"U c #44809A",
+"Y c #4B839C",
+"T c #588097",
+"R c #5C849A",
+"E c #56899E",
+"W c #75828E",
+"Q c #668799",
+"! c #6C8C9D",
+"~ c #748592",
+"^ c #768892",
+"/ c #7A8D97",
+"( c #7B8C9A",
+") c #7F919B",
+"_ c #5B8DA2",
+"` c #6F90A0",
+"' c #7493A3",
+"] c #7A96A5",
+"[ c #7998A7",
+"{ c #7C9BAB",
+"} c #6E9EB1",
+"| c #8A888F",
+" . c #8D8D93",
+".. c #908F96",
+"X. c #82959F",
+"o. c #8D929A",
+"O. c #93939B",
+"+. c #99979D",
+"@. c #8397A1",
+"#. c #8699A3",
+"$. c #899CA6",
+"%. c #809AA8",
+"&. c #8B9EA9",
+"*. c #9397A0",
+"=. c #9699A1",
+"-. c #9B9CA2",
+";. c #A09DA2",
+":. c #83A0AF",
+">. c #8EA2AC",
+",. c #91A3AD",
+"<. c #9DA0A8",
+"1. c #83A2B0",
+"2. c #8FAAB8",
+"3. c #92A6B2",
+"4. c #95A9B3",
+"5. c #98ACB6",
+"6. c #9CAEB9",
+"7. c #9EB1BD",
+"8. c #A3A0A5",
+"9. c #A3A4AA",
+"0. c #A6A8AF",
+"q. c #ACA8AC",
+"w. c #AAACB3",
+"e. c #AEB0B6",
+"r. c #A0B3BD",
+"t. c #B2B4BA",
+"y. c #A3B7C1",
+"u. c #AABAC4",
+"i. c #ACBFC8",
+"p. c #AFC2CC",
+"a. c #B2C5CE",
+"s. c #B6C7D1",
+"d. c #B7C9D2",
+"f. c #BACCD5",
+"g. c #C0D1D9",
+"h. c #D6DADF",
+"j. c #E9EBED",
+"k. c None",
+/* pixels */
+"k.k.k.k.% % % r u u u u u u u u u y y y e # # k.",
+"k.k.k.k.% 7 ; k v v v v v c c c c c c c j + 8 $ ",
+"k.k.k.k.+ h.F H q.0.;.;.-.+.+.O... . .| f G u.# ",
+"k.k.k.k.$ N : _ f.d.a.1.{ { [ ] ] ' ' %.Z o + # ",
+"} A A d i i p p i i i i i i i i t M B Y F X X # ",
+"U R R h c c c c c x z x x x z z l = S q E X X @ ",
+"A 6.3.T t.e.w.0.9.<.-.=.*.*.o.o.W 4 j.0 _ X X @ ",
+"A R L Y g.f.d.2.{ { [ ' ` ` ! ` ] : 5 * _ X X @ ",
+"J Y Y Y f.d.s.D < < < < < < < w [ : * - Y X X @ ",
+"A U J Y d.s.p.%.' ` ! ! ! Q Q ! ] : * ; J X X # ",
+"C J Z Y s.p.i.D 1 < < < < < < w ] : * - J X X # ",
+"C Z V U p.p.u.y.7.7.6.4.3.3.&.&.] : * - J O X # ",
+"C V N N J J Z V V V V V V N N N N : * - J X X @ ",
+"B M m n b 6 : * * * * * * * * = * * * - J X X # ",
+"B m n q : : * * * * * * * * * * * * - - J X X # ",
+"M q : : * * * * * * * * * * * * * * * - J X X @ ",
+"M : : * * + + + + + + + + + + + * * * - J X X # ",
+"M : * * * < &.I P I / / ^ I I > * * * - J X X # ",
+"M * * * * 3 i.< a 5.3.,.$.@.> * * * ; J X O % ",
+"M * * * * < u., a 4.,.&.$.X.> * * * ; K # % k.",
+"M * * * * < y., , ,.&.#.X.X.> * * * - } k.k.k.",
+"M * * * * < 7., , >.$.@.) / > * * - - } k.k.k.",
+"Y ; * * * < 6.g . g $.#.) / ^ > * * - C k.k.k.k.",
+"k.K M M M 8 s 9 9 9 2 2 1 1 < 3 B B A k.k.k.k.k."
+};
diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py
index 60d6e3c12e13..ed5ac9f1a90d 100644
--- a/lib/matplotlib/rcsetup.py
+++ b/lib/matplotlib/rcsetup.py
@@ -475,7 +475,7 @@ def __call__(self, s):
# a map from key -> value, converter
defaultParams = {
'backend': ['Agg', validate_backend], # agg is certainly
- # present
+ 'backend.single_window': [False, validate_bool], # present
'backend_fallback': [True, validate_bool], # agg is certainly present
'backend.qt4': ['PyQt4', validate_qt4],
'webagg.port': [8988, validate_int],
diff --git a/lib/matplotlib/tests/test_coding_standards.py b/lib/matplotlib/tests/test_coding_standards.py
index 58f95c758eac..4edb3974467a 100644
--- a/lib/matplotlib/tests/test_coding_standards.py
+++ b/lib/matplotlib/tests/test_coding_standards.py
@@ -106,7 +106,6 @@
'*/matplotlib/backends/backend_gdk.py',
'*/matplotlib/backends/backend_gtk.py',
'*/matplotlib/backends/backend_gtk3.py',
- '*/matplotlib/backends/backend_gtk3cairo.py',
'*/matplotlib/backends/backend_gtkagg.py',
'*/matplotlib/backends/backend_gtkcairo.py',
'*/matplotlib/backends/backend_macosx.py',
diff --git a/matplotlibrc.template b/matplotlibrc.template
index 44f94fdfd95d..09a3267b85ac 100644
--- a/matplotlibrc.template
+++ b/matplotlibrc.template
@@ -36,6 +36,11 @@ backend : %(backend)s
# the underlying Qt4 toolkit.
#backend.qt4 : PyQt4 # PyQt4 | PySide
+# If you are using one of the GTK3 backends (GTK3Agg or GTK3Cairo)
+# you can set to use only one window with tabbed figures instead of
+# multiple windows one for each figure
+#backend.single_window : True
+
# Note that this can be overridden by the environment variable
# QT_API used by Enthought Tool Suite (ETS); valid values are
# "pyqt" and "pyside". The "pyqt" setting has the side effect of