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