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' pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy