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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Open Clip Art Library + + + + 2011-07-26T19:16:26 + + http://openclipart.org/detail/152029/vector-x-by-gblas.ivan + + + gblas.ivan + + + + + algebra + axis + calculus + clip art + clipart + formula + mathematics + plot + plotting + vector + x + y + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Open Clip Art Library + + + + 2007-02-22T17:59:32 + A pencil icon. + http://openclipart.org/detail/3297/pencil-by-barretr + + + barretr + + + + + clip art + clipart + color + drawing + icon + office + pencil + school + writing + yellow + + + + + + + + + + + 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 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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