diff --git a/.travis.yml b/.travis.yml index fc48b3ee568c..ba31973dcdd3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,6 +47,7 @@ env: - NOSE_ARGS="--processes=$NPROC --process-timeout=300" - PYTHON_ARGS= - DELETE_FONT_CACHE= + - USENEWTOOLMANAGER=false matrix: include: @@ -60,6 +61,10 @@ matrix: env: TEST_ARGS=--pep8 - python: 3.5 env: BUILD_DOCS=true + - python: 2.7 + env: USENEWTOOLMANAGER=true MOCK=mock + - python: 3.4 + env: USENEWTOOLMANAGER=true - python: "nightly" env: PRE=--pre - os: osx @@ -143,7 +148,12 @@ script: # The number of processes is hardcoded, because using too many causes the # Travis VM to run out of memory (since so many copies of inkscape and # ghostscript are running at the same time). + + # Load in the new toolbar rc file for tests - | + if [[ $USENEWTOOLMANAGER == true ]]; then + cp .travis/toolmgr_matplotlibrc matplotlibrc + fi echo Testing import of tkagg backend MPLBACKEND="tkagg" python -c 'import matplotlib.pyplot as plt; print(plt.get_backend())' echo The following args are passed to nose $NOSE_ARGS diff --git a/.travis/toolmgr_matplotlibrc b/.travis/toolmgr_matplotlibrc new file mode 100644 index 000000000000..327b02794023 --- /dev/null +++ b/.travis/toolmgr_matplotlibrc @@ -0,0 +1 @@ +toolbar : toolmanager diff --git a/doc/devel/MEP/MEP27.rst b/doc/devel/MEP/MEP27.rst index 57b0540a4c91..8db8215a6563 100644 --- a/doc/devel/MEP/MEP27.rst +++ b/doc/devel/MEP/MEP27.rst @@ -8,14 +8,16 @@ Status ====== -**Discussion** +**Progress** Branches and Pull requests ========================== Main PR (including GTK3): + + https://github.com/matplotlib/matplotlib/pull/4143 Backend specific branch diffs: + + https://github.com/OceanWolf/matplotlib/compare/backend-refactor...OceanWolf:backend-refactor-tkagg + https://github.com/OceanWolf/matplotlib/compare/backend-refactor...OceanWolf:backend-refactor-qt + https://github.com/OceanWolf/matplotlib/compare/backend-refactor...backend-refactor-wx @@ -79,7 +81,7 @@ The description of this MEP gives us most of the solution: 1. To remove the windowing aspect out of ``FigureManagerBase`` letting it simply wrap this new class along with the other backend classes. Create a new ``WindowBase`` class that can handle this - functionality, with pass-through methods (:arrow_right:) to + functionality, with pass-through methods (->) to ``WindowBase``. Classes that subclass ``WindowBase`` should also subclass the GUI specific window class to ensure backward compatibility (``manager.window == manager.window``). @@ -103,30 +105,30 @@ The description of this MEP gives us most of the solution: |FigureManagerBase(canvas, num) |FigureManager(figure, num) |``WindowBase(title)``|Notes | | | | | | +======================================+==============================+=====================+================================+ -|show | |show | | +|show |-> |show | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ |destroy |calls destroy on all |destroy | | | |components | | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ |full_screen_toggle |handles logic |set_fullscreen | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -|resize | |resize | | +|resize |-> |resize | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -|key_press |key_press | | | +|key_press |key_press |X | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -|show_popup |show_poup | |Not used anywhere in mpl, and | +|show_popup |X |X |Not used anywhere in mpl, and | | | | |does nothing. | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -|get_window_title | |get_window_title | | +|get_window_title |-> |get_window_title | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -|set_window_title | |set_window_title | | +|set_window_title |-> |set_window_title | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -| |_get_toolbar | |A common method to all | +|X |_get_toolbar |X |A common method to all | | | | |subclasses of FigureManagerBase | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -| | |set_default_size | | +|X |X |set_default_size | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ -| | |add_element_to_window| | +|X |X |add_element | | +--------------------------------------+------------------------------+---------------------+--------------------------------+ @@ -135,14 +137,14 @@ The description of this MEP gives us most of the solution: +==========+============+=============+ |mainloop |begin | | +----------+------------+-------------+ -| |end |Gets called | +|X |end |Gets called | | | |automagically| | | |when no more | | | |instances of | | | |the subclass | | | |exist | +----------+------------+-------------+ -|__call__ | |Method moved | +|__call__ |X |Method moved | | | |to | | | |Gcf.show_all | +----------+------------+-------------+ @@ -191,6 +193,8 @@ in the same manner as everything else. | | |window, so this also | | | |breaks BC. | +-------------------------+-------------------------+-------------------------+ +|WebAgg |canvas | | ++-------------------------+-------------------------+-------------------------+ Alternatives diff --git a/examples/user_interfaces/gui_elements.py b/examples/user_interfaces/gui_elements.py new file mode 100644 index 000000000000..6604028bc8e6 --- /dev/null +++ b/examples/user_interfaces/gui_elements.py @@ -0,0 +1,51 @@ +'''This example demonstrates how to: +* Create new toolbars +* Create new windows +Using `matplotlib.backend_managers.ToolManager`, +`matplotlib.backend_bases.WindowBase` and +`matplotlib.backend_bases.ToolContainerBase` +''' + +from __future__ import print_function +import matplotlib +matplotlib.use('GTK3Cairo') +matplotlib.rcParams['toolbar'] = 'toolmanager' +import matplotlib.pyplot as plt + +fig = plt.figure() + +# Shortcuts to FigureManager and ToolManager +manager = fig.canvas.manager +tool_mgr = manager.toolmanager + +# Create a new toolbar +topbar = manager.backend.Toolbar(tool_mgr) + +# Add it to the figure window, we can place it north, east, west and south +manager.window.add_element(topbar, 'north') + +# Remove some tools from the main toolbar and add them to the +# new sidebar +for tool in ('home', 'back', 'forward'): + manager.toolbar.remove_toolitem(tool) + topbar.add_tool(tool, None) + +plt.plot([1, 2, 3]) + +# Add a new window +win = manager.backend.Window('Extra tools') + +# create a sidebar for the new window +sidebar = manager.backend.Toolbar(tool_mgr) + +# add the sidebar to the new window +win.add_element(sidebar, 'west') + +# Add some tools to the new sidebar +for tool in ('home', 'back', 'forward', 'zoom', 'pan'): + sidebar.add_tool(tool, None) + +# show the new window +win.show() + +plt.show() diff --git a/lib/matplotlib/_pylab_helpers.py b/lib/matplotlib/_pylab_helpers.py index c5ea8cc6bb60..4d7cdc6cf6fc 100644 --- a/lib/matplotlib/_pylab_helpers.py +++ b/lib/matplotlib/_pylab_helpers.py @@ -9,6 +9,8 @@ import gc import atexit +from matplotlib import is_interactive + def error_msg(msg): print(msg, file=sys.stderr) @@ -35,6 +37,16 @@ class Gcf(object): _activeQue = [] figs = {} + @classmethod + def add_figure_manager(cls, manager): + cls.figs[manager.num] = manager + try: # TODO remove once all backends converted to use the new manager. + manager.mpl_connect('window_destroy_event', cls.destroy_cbk) + except: + pass + + cls.set_active(manager) + @classmethod def get_fig_manager(cls, num): """ @@ -46,6 +58,56 @@ def get_fig_manager(cls, num): cls.set_active(manager) return manager + @classmethod + def show_all(cls, block=None): + """ + Show all figures. If *block* is not None, then + it is a boolean that overrides all other factors + determining whether show blocks by calling mainloop(). + The other factors are: + it does not block if run inside ipython's "%pylab" mode + it does not block in interactive mode. + """ + + managers = cls.get_all_fig_managers() + if not managers: + return + + for manager in managers: + manager.show() + + if block is True: + # Start the mainloop on the last manager, so we don't have a + # mainloop starting for each manager. Not ideal, but works for now. + manager._mainloop() + return + elif block is False: + return + + # Hack: determine at runtime whether we are + # inside ipython in pylab mode. + from matplotlib import pyplot + try: + ipython_pylab = not pyplot.show._needmain + # IPython versions >= 0.10 tack the _needmain + # attribute onto pyplot.show, and always set + # it to False, when in %pylab mode. + ipython_pylab = ipython_pylab and manager.backend_name != 'webagg' + # TODO: The above is a hack to get the WebAgg backend + # working with ipython's `%pylab` mode until proper + # integration is implemented. + except AttributeError: + ipython_pylab = False + + # Leave the following as a separate step in case we + # want to control this behavior with an rcParam. + if ipython_pylab: + return + + # If not interactive we need to block + if not is_interactive() or manager.backend_name == 'webagg': + manager._mainloop() + @classmethod def destroy(cls, num): """ @@ -68,7 +130,7 @@ def destroy(cls, num): cls._activeQue.append(f) del cls.figs[num] - manager.destroy() + manager.destroy() # Unneeded with MEP27 remove later gc.collect(1) @classmethod @@ -137,7 +199,6 @@ def set_active(cls, manager): if m != manager: cls._activeQue.append(m) cls._activeQue.append(manager) - cls.figs[manager.num] = manager @classmethod def draw_all(cls, force=False): @@ -149,4 +210,8 @@ def draw_all(cls, force=False): if force or f_mgr.canvas.figure.stale: f_mgr.canvas.draw_idle() + @classmethod + def destroy_cbk(cls, event): + cls.destroy(event.figure_manager.num) + atexit.register(Gcf.destroy_all) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d5335dfed6f0..74771208579f 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -20,6 +20,12 @@ pressed, x and y locations in pixel and :class:`~matplotlib.axes.Axes` coordinates. +:class:`WindowBase` + The base class to display a window. + +:class:`MainLoopBase` + The base class to start the GUI's main loop. + :class:`ShowBase` The base class for the Show class of each interactive backend; the 'show' callable is then set to Show.__call__, inherited from @@ -45,6 +51,7 @@ import sys import time import warnings +import weakref import numpy as np import matplotlib.cbook as cbook @@ -133,6 +140,35 @@ def get_registered_canvas_class(format): return backend_class +class MainLoopBase(object): + """This gets used as a key maintaining the event loop. + Backends should only need to override begin and end. + It should not matter if this gets used as a singleton or not due to + clever magic. + """ + _instance_count = {} + _running = False + def __init__(self): + MainLoopBase._instance_count.setdefault(self.__class__, 0) + MainLoopBase._instance_count[self.__class__] += 1 + + def begin(self): + pass + + def end(self): + pass + + def __call__(self): + MainLoopBase._running = True + self.begin() + + def __del__(self): + MainLoopBase._instance_count[self.__class__] -= 1 + if (MainLoopBase._instance_count[self.__class__] <= 0 and + not is_interactive() and MainLoopBase._running): + self.end() + + class ShowBase(object): """ Simple base class to generate a show() callable in backends. @@ -1620,7 +1656,61 @@ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): self.key = key -class FigureCanvasBase(object): +class ExpandableBase(object): + """ + Base class for GUI elements that can expand to fill the area given to them + by the encapsulating container (e.g. the main window). + + At the moment this class does not do anything apart from mark such classes, + but this may well change at a later date, PRs welcome. + """ + pass + +class FlowBase(object): + """ + Base mixin class for all GUI elements that can flow, aka laid out in + different directions. + + The MPL window class deals with the manipulation of this mixin, so users + don't actually need to interact with this class. + + Classes the implement this class must override the _update_flow method. + """ + flow_types = ['horizontal', 'vertical'] + + def __init__(self, flow='horizontal', flow_locked=False, **kwargs): + super(FlowBase, self).__init__(**kwargs) + self.flow_locked = flow_locked + self.flow = flow + + @property + def flow(self): + """ + The direction of flow, one of the strings in `flow_type`. + """ + return FlowBase.flow_types[self._flow] + + @flow.setter + def flow(self, flow): + if self.flow_locked: + return + + try: + self._flow = FlowBase.flow_types.index(flow) + except ValueError: + raise ValueError('Flow (%s), not in list %s' % (flow, FlowBase.flow_types)) + + self._update_flow() + + def _update_flow(self): + """ + Classes that extend FlowBase must override this method. + You can use the internal property self._flow whereby + flow_types[self._flow] gives the current flow. + """ + raise NotImplementedError + +class FigureCanvasBase(ExpandableBase): """ The canvas the figure renders into. @@ -1665,7 +1755,8 @@ class FigureCanvasBase(object): register_backend('tiff', 'matplotlib.backends.backend_agg', 'Tagged Image File Format') - def __init__(self, figure): + def __init__(self, figure, manager=None, backend=None, **kwargs): + self._backend = backend self._is_idle_drawing = True self._is_saving = False figure.set_canvas(self) @@ -1680,6 +1771,7 @@ def __init__(self, figure): self.scroll_pick_id = self.mpl_connect('scroll_event', self.pick) self.mouse_grabber = None # the axes currently grabbing mouse self.toolbar = None # NavigationToolbar2 will set me + self.manager = manager self._is_idle_drawing = False @contextmanager @@ -1688,6 +1780,13 @@ def _idle_draw_cntx(self): yield self._is_idle_drawing = False + @property + def backend(self): + return self._backend + + def focus(self): + pass + def is_saving(self): """ Returns `True` when the renderer is in the process of saving @@ -2251,7 +2350,7 @@ def get_window_title(self): Get the title text of the window containing the figure. Return None if there is no window (e.g., a PS backend). """ - if hasattr(self, "manager"): + if self.manager is not None: return self.manager.get_window_title() def set_window_title(self, title): @@ -2259,7 +2358,7 @@ def set_window_title(self, title): Set the title text of the window containing the figure. Note that this has no effect if there is no window (e.g., a PS backend). """ - if hasattr(self, "manager"): + if self.manager is not None: self.manager.set_window_title(title) def get_default_filename(self): @@ -2453,6 +2552,22 @@ def stop_event_loop_default(self): """ self._looping = False + def destroy(self): + pass + + @property + def manager(self): + if self._manager is not None: + return self._manager() + return None + + @manager.setter + def manager(self, manager): + if manager is not None: + self._manager = weakref.ref(manager) + else: + self._manager = None + def key_press_handler(event, canvas, toolbar=None): """ @@ -2571,6 +2686,120 @@ class NonGuiException(Exception): pass +class WindowEvent(object): + def __init__(self, name, window): + self.name = name + self.window = window + + +class WindowBase(cbook.EventEmitter): + """The base class to show a window on screen. + + Parameters + ---------- + title : str + The title of the window. + """ + + def __init__(self, title, **kwargs): + super(WindowBase, self).__init__(**kwargs) + + def show(self): + """ + For GUI backends, show the figure window and redraw. + For non-GUI backends, raise an exception to be caught + by :meth:`~matplotlib.figure.Figure.show`, for an + optional warning. + """ + raise NonGuiException() + + def destroy(self): + """Destroys the window""" + pass + + def set_fullscreen(self, fullscreen): + """Whether to show the window fullscreen or not, GUI only. + + Parameters + ---------- + fullscreen : bool + True for yes, False for no. + """ + pass + + def set_default_size(self, width, height): + """Sets the default size of the window, defaults to a simple resize. + + Parameters + ---------- + width : int + The default width (in pixels) of the window. + height : int + The default height (in pixels) of the window. + """ + self.resize(width, height) + + def resize(self, width, height): + """"For gui backends, resizes the window. + + Parameters + ---------- + width : int + The new width (in pixels) for the window. + height : int + The new height (in pixels) for the window. + """ + pass + + def get_window_title(self): + """ + Get the title text of the window containing the figure. + Return None for non-GUI backends (e.g., a PS backend). + + Returns + ------- + str : The window's title. + """ + return 'image' + + def set_window_title(self, title): + """ + Set the title text of the window containing the figure. Note that + this has no effect for non-GUI backends (e.g., a PS backend). + + Parameters + ---------- + title : str + The title of the window. + """ + pass + + def add_element(self, element, place): + """ Adds a gui widget to the window. + This has no effect for non-GUI backends and properties only apply + to those backends that support them, or have a suitable workaround. + + Parameters + ---------- + element : A gui element. + The element to add to the window + place : string + The location to place the element, either compass points north, + east, south, west, or center. + """ + pass + + def destroy_event(self, *args): + """Fires this event when the window wants to destroy itself. + + Note this method should hook up to the backend's internal window's + close event. + """ + s = 'window_destroy_event' + event = WindowEvent(s, self) + self._callbacks.process(s, event) + + class FigureManagerBase(object): """ Helper class for pyplot mode, wraps everything up into a neat bundle @@ -2588,12 +2817,8 @@ def __init__(self, canvas, num): canvas.manager = self # store a pointer to parent self.num = num - if rcParams['toolbar'] != 'toolmanager': - self.key_press_handler_id = self.canvas.mpl_connect( - 'key_press_event', - self.key_press) - else: - self.key_press_handler_id = None + self.key_press_handler_id = self.canvas.mpl_connect('key_press_event', + self.key_press) """ The returned id from connecting the default key handler via :meth:`FigureCanvasBase.mpl_connnect`. @@ -3183,7 +3408,8 @@ class ToolContainerBase(object): this `ToolContainer` wants to communicate with. """ - def __init__(self, toolmanager): + def __init__(self, toolmanager, **kwargs): + super(ToolContainerBase, self).__init__(**kwargs) self.toolmanager = toolmanager self.toolmanager.toolmanager_connect('tool_removed_event', self._remove_tool_cbk) @@ -3307,9 +3533,14 @@ def remove_toolitem(self, name): raise NotImplementedError +class ToolbarBase(ToolContainerBase, FlowBase): + pass + + class StatusbarBase(object): """Base class for the statusbar""" - def __init__(self, toolmanager): + def __init__(self, toolmanager, **kwargs): + super(StatusbarBase, self).__init__(**kwargs) self.toolmanager = toolmanager self.toolmanager.toolmanager_connect('tool_message_event', self._message_cbk) diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py index 9fcd6176c07f..5c4b49d63363 100644 --- a/lib/matplotlib/backend_managers.py +++ b/lib/matplotlib/backend_managers.py @@ -1,4 +1,8 @@ """ +`FigureManager` + Class that pulls all of the standard GUI elements together, and manages + the interaction between them. + `ToolManager` Class that makes the bridge between user interaction (key press, toolbar clicks, ..) and the actions in response to the user inputs. @@ -14,6 +18,197 @@ from matplotlib.rcsetup import validate_stringlist import matplotlib.backend_tools as tools +from matplotlib import is_interactive +from matplotlib import rcParams +from matplotlib.figure import Figure +from matplotlib.backends import get_backend, backend as backend_name + + +class FigureManagerEvent(object): + """Event for when something happens to this figure manager. + i.e. the figure it controls gets closed + + Attributes + ---------- + signal : str + The name of the signal. + + figure_manager : FigureManager + The figure manager that fired the event. + """ + def __init__(self, signal, figure_manager): + self.name = signal + self.figure_manager = figure_manager + + +class FigureManager(cbook.EventEmitter): + """ + The FigureManager creates and wraps the necessary components to display a + figure, namely the Window, FigureCanvas and Toolbar. It gets used whenever + you want the figure in a standalone window. + + Parameters + ---------- + figure : `matplotlib.figure.Figure` + The figure to manage. + + num : int + The figure number. + + Attributes + ---------- + + canvas : `matplotlib.backend_bases.FigureCanvasBase` + The GUI element on which we draw. + + figure : `matplotlib.figure.Figure` + The figure that holds the canvas + + toolbar : `matplotlib.backend_bases.ToolbarBase` + The toolbar used for interacting with the figure. + + window : `matplotlib.backend_bases.WindowBase` + The window that holds the canvas and toolbar. + + num : int + The figure number. + """ + def __init__(self, figure, num, **kwargs): + super(FigureManager, self).__init__(**kwargs) + self._backend_name, self._backend = get_backend() + + self.num = num + self.figure = figure + + self._is_gui = hasattr(self._backend, 'Window') + if not self._is_gui: + self.window = None + return + + self._mainloop = self._backend.MainLoop() + self.window = self._backend.Window('Figure %d' % num) + self.window.mpl_connect('window_destroy_event', self.destroy) + + w = int(self.figure.bbox.width) + h = int(self.figure.bbox.height) + + self.window.add_element(self.figure.canvas, 'center') + + self.toolmanager = ToolManager(self.figure) + self.toolbar = self._get_toolbar() + + tools.add_tools_to_manager(self.toolmanager) + if self.toolbar: + tools.add_tools_to_container(self.toolbar) + self.statusbar = self._backend.Statusbar(self.toolmanager) + h += self.window.add_element(self.statusbar, 'south') + + if self.toolbar is not None: + h += self.window.add_element(self.toolbar, 'south') + + self.window.set_default_size(w, h) + self._full_screen_flag = False + + if is_interactive(): + self.window.show() + + def notify_axes_change(fig): + 'this will be called whenever the current axes is changed' + if self.toolmanager is None and self.toolbar is not None: + self.toolbar.update() + self.figure.add_axobserver(notify_axes_change) + + @property + def figure(self): + return self._figure + + @figure.setter + def figure(self, figure): + if hasattr(self, '_figure'): + raise NotImplementedError + + if not figure.canvas: + self._backend.FigureCanvas(figure, manager=self, + backend=self.backend) + self._figure = figure + + @property + def canvas(self): + return self._figure.canvas + + def destroy(self, *args): + """Called to destroy this FigureManager. + """ + + # Make sure we run this routine only once for the FigureManager + # This ensures the nasty __del__ fix below works. + if getattr(self, '_destroying', False) or self._is_gui is False: + return + + self._destroying = True + self.figure.canvas.destroy() + if self.toolbar: + self.toolbar.destroy() + self.window.destroy() + + # Fix as for some reason we have extra references to this# + # i.e. ``del self._mainloop`` doesn't work + self._mainloop.__del__() + + s = 'window_destroy_event' + event = FigureManagerEvent(s, self) + self._callbacks.process(s, event) + + def show(self): + """Shows the figure""" + self.window.show() + self.canvas.focus() + + def full_screen_toggle(self): + """Toggles whether we show fullscreen, alternatively call + `window.fullscreen()`""" + self._full_screen_flag = not self._full_screen_flag + self.window.set_fullscreen(self._full_screen_flag) + + def resize(self, w, h): + """"For gui backends, resize the window (in pixels).""" + self.window.resize(w, h) + + def get_window_title(self): + """ + Get the title text of the window containing the figure. + Return None for non-GUI backends (e.g., a PS backend). + """ + return self.window.get_window_title() + + def set_window_title(self, title): + """ + Set the title text of the window containing the figure. Note that + this has no effect for non-GUI backends (e.g., a PS backend). + """ + if self.window: + self.window.set_window_title(title) + + @property + def backend(self): + return self._backend + + @property + def backend_name(self): + return self._backend_name + + def _get_toolbar(self): + try: + # must be inited after the window, drawingArea and figure + # attrs are set + if rcParams['toolbar'] == 'toolmanager': + toolbar = self._backend.Toolbar(self.toolmanager) + else: + toolbar = None + return toolbar + except: + return None + class ToolEvent(object): """Event for tool manipulation (add/remove)""" @@ -339,14 +534,17 @@ def _handle_toggle(self, tool, sender, canvasevent, data): def _get_cls_to_instantiate(self, callback_class): # Find the class that corresponds to the tool if isinstance(callback_class, six.string_types): + backend = self.canvas.backend + # FIXME: make more complete searching structure - if callback_class in globals(): + if hasattr(backend, callback_class): + callback_class = getattr(backend, callback_class) + elif callback_class in globals(): callback_class = globals()[callback_class] else: mod = 'backend_tools' current_module = __import__(mod, globals(), locals(), [mod], 1) - callback_class = getattr(current_module, callback_class, False) if callable(callback_class): return callback_class diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index c896689bc610..8c5306a6848b 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -13,13 +13,16 @@ from matplotlib import rcParams -from matplotlib._pylab_helpers import Gcf import matplotlib.cbook as cbook +from matplotlib.widgets import SubplotTool + from weakref import WeakKeyDictionary import six import time import warnings +import os + class Cursors(object): """Simple namespace for cursor reference""" @@ -353,7 +356,12 @@ class ToolQuit(ToolBase): default_keymap = rcParams['keymap.quit'] def trigger(self, sender, event, data=None): - Gcf.destroy_fig(self.figure) + try: + manager = self.figure.canvas.manager + except AttributeError: + pass + else: + manager.destroy('window_destroy_event') class ToolQuitAll(ToolBase): @@ -667,6 +675,43 @@ class ConfigureSubplotsBase(ToolBase): image = 'subplots.png' +class ToolConfigureSubplots(ToolBase): + """Tool for the configuration of subplots""" + + description = 'Configure subplots' + image = 'subplots.png' + + def __init__(self, *args, **kwargs): + ToolBase.__init__(self, *args, **kwargs) + self.dialog = None + + def trigger(self, sender, event, data=None): + self.init_dialog() + self.dialog.show() + + def init_dialog(self): + if self.dialog: + return + + from matplotlib.figure import Figure + self.dialog = self.figure.canvas.backend.Window(self.description) + self.dialog.mpl_connect('window_destroy_event', self._window_destroy) + + tool_fig = Figure(figsize=(6, 3)) + self.tool_canvas = self.figure.canvas.__class__(tool_fig) + tool_fig.subplots_adjust(top=0.9) + SubplotTool(self.figure, tool_fig) + + w, h = int(tool_fig.bbox.width), int(tool_fig.bbox.height) + + self.dialog.add_element(self.tool_canvas, 'center') + self.dialog.set_default_size(w, h) + + def _window_destroy(self, *args, **kwargs): + self.tool_canvas.destroy() + self.dialog = None + + class SaveFigureBase(ToolBase): """Base tool for figure saving""" @@ -675,6 +720,43 @@ class SaveFigureBase(ToolBase): default_keymap = rcParams['keymap.save'] +class ToolSaveFigure(ToolBase): + """Saves the figure""" + + description = 'Save the figure' + image = 'filesave.png' + default_keymap = rcParams['keymap.save'] + + def get_filechooser(self): + fc = self.figure.canvas.backend.FileChooserDialog( + title='Save the figure', + parent=self.figure.canvas.manager.window, + path=os.path.expanduser(rcParams.get('savefig.directory', '')), + filetypes=self.figure.canvas.get_supported_filetypes(), + default_filetype=self.figure.canvas.get_default_filetype()) + fc.set_current_name(self.figure.canvas.get_default_filename()) + return fc + + def trigger(self, *args, **kwargs): + chooser = self.get_filechooser() + fname, format_ = chooser.get_filename_from_user() + chooser.destroy() + if fname: + startpath = os.path.expanduser( + rcParams.get('savefig.directory', '')) + if startpath == '': + # explicitly missing key or empty str signals to use cwd + rcParams['savefig.directory'] = startpath + else: + # save dir for next time + rcParams['savefig.directory'] = os.path.dirname( + six.text_type(fname)) + try: + self.figure.canvas.print_figure(fname, format=format_) + except Exception as e: + error_msg_gtk(str(e), parent=self) + + class ZoomPanBase(ToolToggleBase): """Base class for `ToolZoom` and `ToolPan`""" def __init__(self, *args): diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index 68c3a91b1c59..404ea5f3c4f7 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -11,6 +11,36 @@ backend = matplotlib.get_backend() +def get_backend_name(name=None): + '''converts the name of the backend into the module to load + name : str, optional + + Parameters + ---------- + The name of the backend to use. If `None`, falls back to + ``matplotlib.get_backend()`` (which return ``rcParams['backend']``) + ''' + + if name is None: + # validates, to match all_backends + name = matplotlib.get_backend() + if name.startswith('module://'): + backend_name = name[9:] + else: + backend_name = 'matplotlib.backends.backend_' + name.lower() + + return backend_name + + +def get_backend(name=None): + # Import the requested backend into a generic module object + # the last argument is specifies whether to use absolute or relative + # imports. 0 means only perform absolute imports. + backend_name = get_backend_name(name) + return backend_name, __import__(backend_name, globals(), locals(), + [backend_name], 0) + + def pylab_setup(name=None): '''return new_figure_manager, draw_if_interactive and show for pyplot @@ -39,23 +69,10 @@ def pylab_setup(name=None): ''' # Import the requested backend into a generic module object - if name is None: - # validates, to match all_backends - name = matplotlib.get_backend() - if name.startswith('module://'): - backend_name = name[9:] - else: - backend_name = 'backend_' + name - backend_name = backend_name.lower() # until we banish mixed case - backend_name = 'matplotlib.backends.%s' % backend_name.lower() - - # the last argument is specifies whether to use absolute or relative - # imports. 0 means only perform absolute imports. - backend_mod = __import__(backend_name, globals(), locals(), - [backend_name], 0) + backend_mod = get_backend(name)[1] # Things we pull in from all backends - new_figure_manager = backend_mod.new_figure_manager + new_figure_manager = getattr(backend_mod, 'new_figure_manager', None) # image backends like pdf, agg or svg do not need to do anything # for "show" or "draw_if_interactive", so if they are not defined @@ -68,7 +85,7 @@ def do_nothing_show(*args, **kwargs): Your currently selected backend, '%s' does not support show(). Please select a GUI backend in your matplotlibrc file ('%s') or with matplotlib.use()""" % - (name, matplotlib.matplotlib_fname())) + (backend, matplotlib.matplotlib_fname())) def do_nothing(*args, **kwargs): pass diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 1aee5c75f590..ec0ed23fdfd7 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -28,10 +28,10 @@ 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 -from matplotlib.backend_bases import (ShowBase, ToolContainerBase, - StatusbarBase) +from matplotlib.backend_bases import (RendererBase, GraphicsContextBase, + FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, + TimerBase, WindowBase, MainLoopBase, ExpandableBase) +from matplotlib.backend_bases import ShowBase, ToolbarBase, StatusbarBase from matplotlib.backend_managers import ToolManager from matplotlib import backend_tools @@ -66,6 +66,17 @@ def draw_if_interactive(): if figManager is not None: figManager.canvas.draw_idle() + +class MainLoopGTK3(MainLoopBase): + def begin(self): + if Gtk.main_level() == 0: + Gtk.main() + + def end(self): + if Gtk.main_level() >= 1: + Gtk.main_quit() + + class Show(ShowBase): def mainloop(self): if Gtk.main_level() == 0: @@ -115,7 +126,7 @@ def _on_timer(self): self._timer = None return False -class FigureCanvasGTK3 (Gtk.DrawingArea, FigureCanvasBase): +class FigureCanvasGTK3(FigureCanvasBase, Gtk.DrawingArea): keyvald = {65507 : 'control', 65505 : 'shift', 65513 : 'alt', @@ -180,9 +191,9 @@ class FigureCanvasGTK3 (Gtk.DrawingArea, FigureCanvasBase): Gdk.EventMask.POINTER_MOTION_HINT_MASK| Gdk.EventMask.SCROLL_MASK) - def __init__(self, figure): + def __init__(self, *args, **kwargs): if _debug: print('FigureCanvasGTK3.%s' % fn_name()) - FigureCanvasBase.__init__(self, figure) + FigureCanvasBase.__init__(self, *args, **kwargs) GObject.GObject.__init__(self) self._idle_draw_id = 0 @@ -208,6 +219,9 @@ def __init__(self, figure): self._renderer_init() default_context = GLib.main_context_get_thread_default() or GLib.main_context_default() + def focus(self): + self.grab_focus() + def destroy(self): #Gtk.DrawingArea.destroy(self) self.close_event() @@ -374,6 +388,102 @@ def stop_event_loop(self): stop_event_loop.__doc__=FigureCanvasBase.stop_event_loop_default.__doc__ +_flow = [Gtk.Orientation.HORIZONTAL, Gtk.Orientation.VERTICAL] + + +class WindowGTK3(WindowBase, Gtk.Window): + def __init__(self, title, **kwargs): + super(WindowGTK3, self).__init__(title=title, **kwargs) + self.set_window_title(title) + + try: + self.set_icon_from_file(window_icon) + except (SystemExit, KeyboardInterrupt): + # re-raise exit type Exceptions + raise + except: + # some versions of gtk throw a glib.GError but not + # all, so I am not sure how to catch it. I am unhappy + # doing a blanket catch here, but am not sure what a + # better way is - JDH + verbose.report('Could not load matplotlib icon: %s' % sys.exc_info()[1]) + + self._layout = {} + self._setup_box('_outer', Gtk.Orientation.VERTICAL, False, None) + self._setup_box('north', Gtk.Orientation.VERTICAL, False, '_outer') + self._setup_box('_middle', Gtk.Orientation.HORIZONTAL, True, '_outer') + self._setup_box('south', Gtk.Orientation.VERTICAL, False, '_outer') + + self._setup_box('west', Gtk.Orientation.HORIZONTAL, False, '_middle') + self._setup_box('center', Gtk.Orientation.VERTICAL, True, '_middle') + self._setup_box('east', Gtk.Orientation.HORIZONTAL, False, '_middle') + + self.add(self._layout['_outer']) + + self.connect('destroy', self.destroy_event) + self.connect('delete_event', self.destroy_event) + + def _setup_box(self, name, orientation, grow, parent): + self._layout[name] = Gtk.Box(orientation=orientation) + if parent: + self._layout[parent].pack_start(self._layout[name], grow, grow, 0) + self._layout[name].show() + + def add_element(self, element, place): + element.show() + + # Get the flow of the element (the opposite of the container) + flow_index = not _flow.index(self._layout[place].get_orientation()) + flow = _flow[flow_index] + separator = Gtk.Separator(orientation=flow) + separator.show() + + try: + element.flow = element.flow_types[flow_index] + except AttributeError: + pass + + # Determine if this element should fill all the space given to it + expand = isinstance(element, ExpandableBase) + + if place in ['north', 'west', 'center']: + self._layout[place].pack_start(element, expand, expand, 0) + self._layout[place].pack_start(separator, False, False, 0) + elif place in ['south', 'east']: + self._layout[place].pack_end(element, expand, expand, 0) + self._layout[place].pack_end(separator, False, False, 0) + else: + raise KeyError('Unknown value for place, %s' % place) + size_request = element.size_request() + return size_request.height + separator.size_request().height + + def set_default_size(self, width, height): + Gtk.Window.set_default_size(self, width, height) + + def show(self): + # show the figure window + Gtk.Window.show(self) + self.present() + + def destroy(self): + Gtk.Window.destroy(self) + + def set_fullscreen(self, fullscreen): + if fullscreen: + self.fullscreen() + else: + self.unfullscreen() + + def get_window_title(self): + return self.get_title() + + def set_window_title(self, title): + self.set_title(title) + + def resize(self, width, height): + Gtk.Window.resize(self, width, height) + + class FigureManagerGTK3(FigureManagerBase): """ Public attributes @@ -414,9 +524,7 @@ def __init__(self, canvas, num): w = int (self.canvas.figure.bbox.width) h = int (self.canvas.figure.bbox.height) - self.toolmanager = self._get_toolmanager() self.toolbar = self._get_toolbar() - self.statusbar = None def add_widget(child, expand, fill, padding): child.show() @@ -424,14 +532,6 @@ def add_widget(child, expand, fill, padding): size_request = child.size_request() return size_request.height - if self.toolmanager: - backend_tools.add_tools_to_manager(self.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) - self.statusbar = StatusbarGTK3(self.toolmanager) - h += add_widget(self.statusbar, False, False, 0) - h += add_widget(Gtk.HSeparator(), False, False, 0) - if self.toolbar is not None: self.toolbar.show() h += add_widget(self.toolbar, False, False, 0) @@ -448,9 +548,7 @@ def destroy(*args): def notify_axes_change(fig): 'this will be called whenever the current axes is changed' - if self.toolmanager is not None: - pass - elif self.toolbar is not None: + if self.toolbar is not None: self.toolbar.update() self.canvas.figure.add_axobserver(notify_axes_change) @@ -487,20 +585,10 @@ def _get_toolbar(self): # attrs are set if rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2GTK3 (self.canvas, self.window) - elif rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarGTK3(self.toolmanager) else: toolbar = None return toolbar - def _get_toolmanager(self): - # must be initialised after toolbar has been setted - if rcParams['toolbar'] != 'toolbar2': - toolmanager = ToolManager(self.canvas.figure) - else: - toolmanager = None - return toolmanager - def get_window_title(self): return self.window.get_title() @@ -754,14 +842,11 @@ def draw_rubberband(self, x0, y0, x1, y1): self.ctx.stroke() -class ToolbarGTK3(ToolContainerBase, Gtk.Box): - def __init__(self, toolmanager): - ToolContainerBase.__init__(self, toolmanager) - Gtk.Box.__init__(self) - self.set_property("orientation", Gtk.Orientation.VERTICAL) - +class ToolbarGTK3(ToolbarBase, Gtk.Box): + def __init__(self, toolmanager, **kwargs): self._toolarea = Gtk.Box() - self._toolarea.set_property('orientation', Gtk.Orientation.HORIZONTAL) + super(ToolbarGTK3, self).__init__(toolmanager=toolmanager, **kwargs) + self.pack_start(self._toolarea, False, False, 0) self._toolarea.show_all() self._groups = {} @@ -794,7 +879,7 @@ def _add_button(self, button, group, position): if group not in self._groups: if self._groups: self._add_separator() - toolbar = Gtk.Toolbar() + toolbar = Gtk.Toolbar(orientation=_flow[self._flow]) toolbar.set_style(Gtk.ToolbarStyle.ICONS) self._toolarea.pack_start(toolbar, False, False, 0) toolbar.show_all() @@ -823,17 +908,25 @@ def remove_toolitem(self, name): self._groups[group].remove(toolitem) del self._toolitems[name] + def _update_flow(self): + self.set_property("orientation", _flow[not self._flow]) + self._toolarea.set_property('orientation', _flow[self._flow]) + for item in self._toolarea: + if isinstance(item, Gtk.Separator): + item.set_property("orientation", _flow[not self._flow]) + else: + item.set_property("orientation", _flow[self._flow]) + def _add_separator(self): sep = Gtk.Separator() - sep.set_property("orientation", Gtk.Orientation.VERTICAL) + sep.set_property("orientation", _flow[not self._flow]) self._toolarea.pack_start(sep, False, True, 0) sep.show_all() class StatusbarGTK3(StatusbarBase, Gtk.Statusbar): def __init__(self, *args, **kwargs): - StatusbarBase.__init__(self, *args, **kwargs) - Gtk.Statusbar.__init__(self) + super(StatusbarGTK3, self).__init__(*args, **kwargs) self._context = self.get_context_id('message') def set_message(self, s): @@ -841,96 +934,11 @@ def set_message(self, s): self.push(self._context, s) -class SaveFigureGTK3(backend_tools.SaveFigureBase): - - def get_filechooser(self): - fc = FileChooserDialog( - title='Save the figure', - parent=self.figure.canvas.manager.window, - path=os.path.expanduser(rcParams.get('savefig.directory', '')), - filetypes=self.figure.canvas.get_supported_filetypes(), - default_filetype=self.figure.canvas.get_default_filetype()) - fc.set_current_name(self.figure.canvas.get_default_filename()) - return fc - - def trigger(self, *args, **kwargs): - chooser = self.get_filechooser() - fname, format_ = chooser.get_filename_from_user() - chooser.destroy() - if fname: - startpath = os.path.expanduser( - rcParams.get('savefig.directory', '')) - if startpath == '': - # explicitly missing key or empty str signals to use cwd - rcParams['savefig.directory'] = startpath - else: - # save dir for next time - rcParams['savefig.directory'] = os.path.dirname( - six.text_type(fname)) - try: - self.figure.canvas.print_figure(fname, format=format_) - except Exception as e: - error_msg_gtk(str(e), parent=self) - - class SetCursorGTK3(backend_tools.SetCursorBase): def set_cursor(self, cursor): self.figure.canvas.get_property("window").set_cursor(cursord[cursor]) -class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window): - def __init__(self, *args, **kwargs): - backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs) - self.window = None - - def init_window(self): - if self.window: - return - self.window = Gtk.Window(title="Subplot Configuration Tool") - - try: - self.window.window.set_icon_from_file(window_icon) - except (SystemExit, KeyboardInterrupt): - # re-raise exit type Exceptions - raise - except: - # we presumably already logged a message on the - # failure of the main plot, don't keep reporting - pass - - self.vbox = Gtk.Box() - self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) - self.window.add(self.vbox) - self.vbox.show() - self.window.connect('destroy', self.destroy) - - toolfig = Figure(figsize=(6, 3)) - canvas = self.figure.canvas.__class__(toolfig) - - toolfig.subplots_adjust(top=0.9) - SubplotTool(self.figure, toolfig) - - w = int(toolfig.bbox.width) - h = int(toolfig.bbox.height) - - self.window.set_default_size(w, h) - - canvas.show() - self.vbox.pack_start(canvas, True, True, 0) - self.window.show() - - def destroy(self, *args): - self.window.destroy() - self.window = None - - def _get_canvas(self, fig): - return self.canvas.__class__(fig) - - def trigger(self, sender, event, data=None): - self.init_window() - self.window.present() - - # Define the file to use as the GTk icon if sys.platform == 'win32': icon_filename = 'matplotlib.png' @@ -957,11 +965,6 @@ def error_msg_gtk(msg, parent=None): dialog.destroy() -backend_tools.ToolSaveFigure = SaveFigureGTK3 -backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3 -backend_tools.ToolSetCursor = SetCursorGTK3 -backend_tools.ToolRubberband = RubberbandGTK3 - Toolbar = ToolbarGTK3 FigureCanvas = FigureCanvasGTK3 FigureManager = FigureManagerGTK3 diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py index c3eb1da68be3..b770d320a136 100644 --- a/lib/matplotlib/backends/backend_gtk3agg.py +++ b/lib/matplotlib/backends/backend_gtk3agg.py @@ -21,8 +21,8 @@ class FigureCanvasGTK3Agg(backend_gtk3.FigureCanvasGTK3, backend_agg.FigureCanvasAgg): - def __init__(self, figure): - backend_gtk3.FigureCanvasGTK3.__init__(self, figure) + def __init__(self, *args, **kwargs): + backend_gtk3.FigureCanvasGTK3.__init__(self, *args, **kwargs) self._bbox_queue = [] def _renderer_init(self): @@ -121,4 +121,13 @@ def new_figure_manager_given_figure(num, figure): FigureCanvas = FigureCanvasGTK3Agg FigureManager = FigureManagerGTK3Agg +Window = backend_gtk3.WindowGTK3 +Toolbar = backend_gtk3.ToolbarGTK3 +Statusbar = backend_gtk3.StatusbarGTK3 +FileChooserDialog = backend_gtk3.FileChooserDialog +ToolSetCursor = backend_gtk3.SetCursorGTK3 +ToolRubberband = backend_gtk3.RubberbandGTK3 + +Toolbar2 = backend_gtk3.NavigationToolbar2GTK3 +MainLoop = backend_gtk3.MainLoopGTK3 show = backend_gtk3.show diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py index da8f099be7f6..3ee797434596 100644 --- a/lib/matplotlib/backends/backend_gtk3cairo.py +++ b/lib/matplotlib/backends/backend_gtk3cairo.py @@ -22,8 +22,8 @@ def set_context(self, ctx): class FigureCanvasGTK3Cairo(backend_gtk3.FigureCanvasGTK3, backend_cairo.FigureCanvasCairo): - def __init__(self, figure): - backend_gtk3.FigureCanvasGTK3.__init__(self, figure) + def __init__(self, *args, **kwargs): + backend_gtk3.FigureCanvasGTK3.__init__(self, *args, **kwargs) def _renderer_init(self): """use cairo renderer""" @@ -72,4 +72,13 @@ def new_figure_manager_given_figure(num, figure): FigureCanvas = FigureCanvasGTK3Cairo FigureManager = FigureManagerGTK3Cairo +Window = backend_gtk3.WindowGTK3 +Toolbar = backend_gtk3.ToolbarGTK3 +Statusbar = backend_gtk3.StatusbarGTK3 +FileChooserDialog = backend_gtk3.FileChooserDialog +ToolSetCursor = backend_gtk3.SetCursorGTK3 +ToolRubberband = backend_gtk3.RubberbandGTK3 + +Toolbar2 = backend_gtk3.NavigationToolbar2GTK3 +MainLoop = backend_gtk3.MainLoopGTK3 show = backend_gtk3.show diff --git a/lib/matplotlib/backends/backend_template.py b/lib/matplotlib/backends/backend_template.py index f3e483d2f73b..e6c820128dfe 100644 --- a/lib/matplotlib/backends/backend_template.py +++ b/lib/matplotlib/backends/backend_template.py @@ -16,7 +16,7 @@ Copy this to backend_xxx.py and replace all instances of 'template' with 'xxx'. Then implement the class methods and functions below, and add 'xxx' to the switchyard in matplotlib/backends/__init__.py and -'xxx' to the backends list in the validate_backend methon in +'xxx' to the backends list in the validate_backend method in matplotlib/__init__.py and you're off. You can use your backend with:: import matplotlib @@ -25,14 +25,14 @@ plot([1,2,3]) show() -matplotlib also supports external backends, so you can place you can -use any module in your PYTHONPATH with the syntax:: +matplotlib also supports external backends, by placing +any module in your PYTHONPATH and then using the syntax:: import matplotlib matplotlib.use('module://my_backend') where my_backend.py is your module name. This syntax is also -recognized in the rc file and in the -d argument in pylab, e.g.,:: +recognized in the rc file and also with the -d argument in pylab, e.g.,:: python simple_plot.py -dmodule://my_backend @@ -48,6 +48,7 @@ matplotlib/backends/backend_your_backend.py matplotlib/backend_bases.py + matplotlib/backend_managers.py matplotlib/backends/__init__.py matplotlib/__init__.py matplotlib/_pylab_helpers.py @@ -68,9 +69,9 @@ import six import matplotlib -from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\ - FigureManagerBase, FigureCanvasBase + WindowBase, FigureCanvasBase, MainLoopBase, ToolbarBase +from matplotlib import backend_tools from matplotlib.figure import Figure from matplotlib.transforms import Bbox @@ -178,41 +179,6 @@ def draw_if_interactive(): """ pass -def show(): - """ - For image backends - is not required - For GUI backends - show() is usually the last line of a pylab script and - tells the backend that it is time to draw. In interactive mode, this may - be a do nothing func. See the GTK backend for an example of how to handle - interactive versus batch mode - """ - for manager in Gcf.get_all_fig_managers(): - # do something to display the GUI - pass - - -def new_figure_manager(num, *args, **kwargs): - """ - Create a new figure manager instance - """ - # if a main-level app must be created, this (and - # new_figure_manager_given_figure) is the usual place to - # do it -- see backend_wx, backend_wxagg and backend_tkagg for - # examples. Not all GUIs require explicit instantiation of a - # main-level app (egg backend_gtk, backend_gtkagg) for pylab - FigureClass = kwargs.pop('FigureClass', Figure) - thisFig = FigureClass(*args, **kwargs) - return new_figure_manager_given_figure(num, thisFig) - - -def new_figure_manager_given_figure(num, figure): - """ - Create a new figure manager instance for the given figure. - """ - canvas = FigureCanvasTemplate(figure) - manager = FigureManagerTemplate(canvas, num) - return manager - class FigureCanvasTemplate(FigureCanvasBase): """ @@ -256,19 +222,29 @@ def print_foo(self, filename, *args, **kwargs): def get_default_filetype(self): return 'foo' -class FigureManagerTemplate(FigureManagerBase): - """ - Wrap everything up into a window for the pylab interface - For non interactive backends, the base class does all the work - """ +class WindowTemplate(WindowBase): + def show(self): + pass + + +class RubberbandTemplate(backend_tools.RubberbandBase): + pass + + +class SetCursorTemplate(backend_tools.SetCursorBase): pass ######################################################################## # -# Now just provide the standard names that backend.__init__ is expecting +# Now just provide the standard names that backend.__init__ expects # ######################################################################## FigureCanvas = FigureCanvasTemplate -FigureManager = FigureManagerTemplate + +# Needed for a GUI +MainLoop = MainLoopBase +Window = WindowTemplate +ToolRubberband = RubberbandTemplate +ToolSetCursor = SetCursorTemplate diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 0216ccb9c501..cd40c4030f09 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -543,6 +543,18 @@ def process(self, s, *args, **kwargs): self._remove_proxy(proxy) +class EventEmitter(object): + def __init__(self, **kwargs): + super(EventEmitter, self).__init__(**kwargs) # call next class on MRO + self._callbacks = CallbackRegistry() + + def mpl_connect(self, s, func): + return self._callbacks.connect(s, func) + + def mpl_disconnect(self, cid): + return self._callbacks.disconnect(cid) + + class silent_list(list): """ override repr when returning a list of matplotlib artists to diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 89b47984f417..eaa997085266 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1556,11 +1556,17 @@ def __setstate__(self, state): if restore_to_pylab: # lazy import to avoid circularity + # TODO clean on removal of Gcf from backends import matplotlib.pyplot as plt import matplotlib._pylab_helpers as pylab_helpers + import matplotlib.backend_managers as managers allnums = plt.get_fignums() num = max(allnums) + 1 if allnums else 1 - mgr = plt._backend_mod.new_figure_manager_given_figure(num, self) + if rcParams['toolbar'] == 'toolmanager': + mgr = managers.FigureManager(self, num) + else: + mgr = plt._backend_mod.new_figure_manager_given_figure(num, + self) # XXX The following is a copy and paste from pyplot. Consider # factoring to pylab_helpers @@ -1575,7 +1581,7 @@ def make_active(event): mgr._cidgcf = mgr.canvas.mpl_connect('button_press_event', make_active) - pylab_helpers.Gcf.set_active(mgr) + pylab_helpers.Gcf.add_figure_manager(mgr) self.number = num plt.draw_if_interactive() diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 02056a030427..6a4bc09dd98d 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -33,6 +33,7 @@ _string_to_bool) from matplotlib import docstring from matplotlib.backend_bases import FigureCanvasBase +import matplotlib.backend_managers as backend_managers from matplotlib.figure import Figure, figaspect from matplotlib.gridspec import GridSpec from matplotlib.image import imread as _imread @@ -248,7 +249,10 @@ def show(*args, **kw): described above. """ global _show - return _show(*args, **kw) + if rcParams['toolbar'] == 'toolmanager': + return _pylab_helpers.Gcf.show_all(*args, **kw) + else: + return _show(*args, **kw) def isinteractive(): @@ -524,13 +528,18 @@ def figure(num=None, # autoincrement if None, else integer from 1-N if get_backend().lower() == 'ps': dpi = 72 - figManager = new_figure_manager(num, figsize=figsize, - dpi=dpi, - facecolor=facecolor, - edgecolor=edgecolor, - frameon=frameon, - FigureClass=FigureClass, - **kwargs) + if rcParams['toolbar'] == 'toolmanager': + fig = FigureClass(figsize=figsize, dpi=dpi, facecolor=facecolor, + edgecolor=edgecolor, frameon=frameon, **kwargs) + figManager = backend_managers.FigureManager(fig, num) + else: + figManager = new_figure_manager(num, figsize=figsize, + dpi=dpi, + facecolor=facecolor, + edgecolor=edgecolor, + frameon=frameon, + FigureClass=FigureClass, + **kwargs) if figLabel: figManager.set_window_title(figLabel) @@ -543,7 +552,7 @@ def make_active(event): cid = figManager.canvas.mpl_connect('button_press_event', make_active) figManager._cidgcf = cid - _pylab_helpers.Gcf.set_active(figManager) + _pylab_helpers.Gcf.add_figure_manager(figManager) fig = figManager.canvas.figure fig.number = num diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 76de517f18a1..ade123b46cba 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -101,6 +101,10 @@ def setup(): # These settings *must* be hardcoded for running the comparison # tests and are not necessarily the default values as specified in # rcsetup.py + use_new_toolmanager = rcParams['toolbar'] == 'toolmanager' rcdefaults() # Start with all defaults set_font_settings_for_testing() + + if use_new_toolmanager: + rcParams['toolbar'] = 'toolmanager'