diff --git a/lib/matplotlib/_pylab_helpers.py b/lib/matplotlib/_pylab_helpers.py index 2ead0e88cac8..3f3b00827b9b 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,49 @@ 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 not None: + if block: + manager.mainloop() + return + + 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 get_backend() != '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: + block = False + + if not is_interactive() or get_backend() == 'WebAgg': + manager.mainloop() + @classmethod def destroy(cls, num): """ @@ -134,7 +189,9 @@ def set_active(cls, manager): if m != manager: cls._activeQue.append(m) cls._activeQue.append(manager) - cls.figs[manager.num] = manager + @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 edcc81f95f89..d3bbc843cb83 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -38,6 +38,7 @@ import warnings import time import io +import weakref import numpy as np import matplotlib.cbook as cbook @@ -132,6 +133,33 @@ 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 = {} + 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): + self.begin() + + def __del__(self): + MainLoopBase._instance_count[self.__class__] -= 1 + if (MainLoopBase._instance_count[self.__class__] <= 0 and + not is_interactive()): + self.end() + + class ShowBase(object): """ Simple base class to generate a show() callable in backends. @@ -1664,7 +1692,7 @@ class FigureCanvasBase(object): register_backend('tiff', 'matplotlib.backends.backend_agg', 'Tagged Image File Format') - def __init__(self, figure): + def __init__(self, figure, manager=None): figure.set_canvas(self) self.figure = figure # a dictionary from event name to a dictionary that maps cid->func @@ -1678,6 +1706,7 @@ def __init__(self, figure): self.mouse_grabber = None # the axes currently grabbing mouse self.toolbar = None # NavigationToolbar2 will set me self._is_saving = False + self.manager = manager def is_saving(self): """ @@ -2422,6 +2451,19 @@ 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() + + @manager.setter + def manager(self, manager): + if manager is not None: + self._manager = weakref.ref(manager) + def key_press_handler(event, canvas, toolbar=None): """ @@ -2461,7 +2503,10 @@ def key_press_handler(event, canvas, toolbar=None): # quit the figure (defaut key 'ctrl+w') if event.key in quit_keys: - Gcf.destroy_fig(canvas.figure) + if isinstance(canvas.manager.mainloop, MainLoopBase): # If new no Gcf. + canvas.manager._destroy('window_destroy_event') + else: + Gcf.destroy_fig(canvas.figure) if toolbar is not None: # home or reset mnemonic (default key 'h', 'home' and 'r') @@ -2537,6 +2582,64 @@ class NonGuiException(Exception): pass +class WindowEvent(object): + def __init__(self, name, window): + self.name = name + self.window = window + + +class WindowBase(cbook.EventEmitter): + def __init__(self, title): + cbook.EventEmitter.__init__(self) + + 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): + pass + + def set_fullscreen(self, fullscreen): + pass + + def set_default_size(self, w, h): + self.resize(w, h) + + def resize(self, w, h): + """"For gui backends, resize the window (in pixels).""" + 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). + """ + 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). + """ + pass + + def add_element_to_window(self, element, expand, fill, pad, side='bottom'): + """ Adds a gui widget to the window. + This has no effect for non-GUI backends + """ + pass + + def destroy_event(self, *args): + 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 diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py new file mode 100644 index 000000000000..6a729d060440 --- /dev/null +++ b/lib/matplotlib/backend_managers.py @@ -0,0 +1,134 @@ +from matplotlib import is_interactive +from matplotlib import rcParams +from matplotlib.figure import Figure +from matplotlib import cbook +from matplotlib.backend_bases import key_press_handler +from matplotlib.backends import get_backends +(FigureCanvas, Window, Toolbar2, MainLoop, + old_new_figure_manager) = get_backends() + + +class FigureManagerEvent(object): + def __init__(self, s, fm): + self.name = s + self.figure_manager = fm + + +class FigureManager(cbook.EventEmitter): + def __init__(self, figure, num): + cbook.EventEmitter.__init__(self) + self.num = num + + self.mainloop = MainLoop() + self.window = Window('Figure %d' % num) + self.window.mpl_connect('window_destroy_event', self._destroy) + + self.canvas = FigureCanvas(figure, manager=self) + + self.key_press_handler_id = self.canvas.mpl_connect('key_press_event', + self.key_press) + + w = int(self.canvas.figure.bbox.width) + h = int(self.canvas.figure.bbox.height) + + self.window.add_element_to_window(self.canvas, True, True, 0, 'top') + + self.toolbar = self._get_toolbar() + if self.toolbar is not None: + h += self.window.add_element_to_window(self.toolbar, + False, False, 0, 'bottom') + + self.window.set_default_size(w, h) + + if is_interactive(): + self.window.show() + + def notify_axes_change(fig): + 'this will be called whenever the current axes is changed' + if self.toolbar is not None: + self.toolbar.update() + self.canvas.figure.add_axobserver(notify_axes_change) + + def key_press(self, event): + """ + Implement the default mpl key bindings defined at + :ref:`key-event-handling` + """ + key_press_handler(event, self.canvas, self.canvas.toolbar) + + def _destroy(self, event=None): + # Callback from the when the window wants to destroy itself + s = 'window_destroy_event' + event = FigureManagerEvent(s, self) + self._callbacks.process(s, event) + + def destroy(self, *args): + self.canvas.destroy() + if self.toolbar: + self.toolbar.destroy() + self.window.destroy() + + self.mainloop.__del__() + + def show(self): + self.window.show() + + def full_screen_toggle(self): + self._full_screen_flag = not self._full_screen_flag + self.window.set_fullscreen(self._full_screen_flag) + + def resize(self, w, h): + 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). + """ + self.window.set_window_title(title) + + def _get_toolbar(self): + # must be inited after the window, drawingArea and figure + # attrs are set + if rcParams['toolbar'] == 'toolbar2': + # Short term hack until toolbar2 gets removed. + if 'qt' in str(FigureCanvas): + toolbar = Toolbar2(self.canvas, self.window, False) + else: + toolbar = Toolbar2(self.canvas, self.window) + else: + toolbar = None + return toolbar + + def show_popup(self, msg): + """ + Display message in a popup -- GUI only + """ + pass + + +def new_figure_manager(num, *args, **kwargs): + """ + Create a new figure manager instance + """ + show = kwargs.pop('show', None) + if old_new_figure_manager is None: # Test if we can use the new code + FigureClass = kwargs.pop('FigureClass', Figure) + thisFig = FigureClass(*args, **kwargs) + manager = new_figure_manager_given_figure(num, thisFig) + else: # TODO remove once Gcf removed from backends. Default to old code. + manager = old_new_figure_manager(num, *args, **kwargs) + manager.mainloop = MainLoop + return manager + + +def new_figure_manager_given_figure(num, figure): + manager = FigureManager(figure, num) + return manager diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index cf80dc0f9ff5..f43366d923b3 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -14,18 +14,32 @@ 'new_figure_manager', 'backend_version'] backend = matplotlib.get_backend() # validates, to match all_backends +if backend.startswith('module://'): + backend_name = backend[9:] +else: + backend_name = 'matplotlib.backends.backend_' + backend.lower() + +def get_backends(): + _temp = __import__(backend_name, globals(), locals(), + ['Window', 'Toolbar2', 'FigureCanvas', 'MainLoop', + 'new_figure_manager'], 0) + FigureCanvas = _temp.FigureCanvas + try: + Window = _temp.Window + Toolbar2 = _temp.Toolbar2 + MainLoop = _temp.MainLoop + old_new_figure_manager = None + except AttributeError: + Window = None + Toolbar2 = None + MainLoop = getattr(_temp, 'show', do_nothing_show) + old_new_figure_manager = _temp.new_figure_manager + + return FigureCanvas, Window, Toolbar2, MainLoop, old_new_figure_manager def pylab_setup(): 'return new_figure_manager, draw_if_interactive and show for pylab' # Import the requested backend into a generic module object - - if backend.startswith('module://'): - backend_name = backend[9:] - else: - backend_name = 'backend_'+backend - 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, @@ -37,18 +51,10 @@ def pylab_setup(): # 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 # by the backend, just do nothing - def do_nothing_show(*args, **kwargs): - frame = inspect.currentframe() - fname = frame.f_back.f_code.co_filename - if fname in ('', ''): - warnings.warn(""" -Your currently selected backend, '%s' does not support show(). -Please select a GUI backend in your matplotlibrc file ('%s') -or with matplotlib.use()""" % - (backend, matplotlib.matplotlib_fname())) + def do_nothing(*args, **kwargs): pass backend_version = getattr(backend_mod,'backend_version', 'unknown') - show = getattr(backend_mod, 'show', do_nothing_show) + show = None if hasattr(backend_mod, 'show') else do_nothing_show draw_if_interactive = getattr(backend_mod, 'draw_if_interactive', do_nothing) # Additional imports which only happen for certain backends. This section @@ -60,3 +66,13 @@ def do_nothing(*args, **kwargs): pass matplotlib.verbose.report('backend %s version %s' % (backend,backend_version)) return backend_mod, new_figure_manager, draw_if_interactive, show + +def do_nothing_show(*args, **kwargs): + frame = inspect.currentframe() + fname = frame.f_back.f_code.co_filename + if fname in ('', ''): + warnings.warn(""" +Your currently selected backend, '%s' does not support show(). +Please select a GUI backend in your matplotlibrc file ('%s') +or with matplotlib.use()""" % + (backend, matplotlib.matplotlib_fname())) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 62622715322a..647344322b7e 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -28,8 +28,9 @@ 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 (RendererBase, GraphicsContextBase, + FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, + TimerBase, WindowBase, MainLoopBase) from matplotlib.backend_bases import ShowBase from matplotlib.cbook import is_string_like, is_writable_file_like @@ -67,6 +68,17 @@ def draw_if_interactive(): if figManager is not None: figManager.canvas.draw_idle() + +class MainLoop(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: @@ -181,9 +193,9 @@ class FigureCanvasGTK3 (Gtk.DrawingArea, FigureCanvasBase): Gdk.EventMask.POINTER_MOTION_HINT_MASK| Gdk.EventMask.SCROLL_MASK) - def __init__(self, figure): + def __init__(self, figure, manager=None): if _debug: print('FigureCanvasGTK3.%s' % fn_name()) - FigureCanvasBase.__init__(self, figure) + FigureCanvasBase.__init__(self, figure, manager) GObject.GObject.__init__(self) self._idle_draw_id = 0 @@ -374,6 +386,66 @@ def stop_event_loop(self): FigureCanvasBase.stop_event_loop_default(self) stop_event_loop.__doc__=FigureCanvasBase.stop_event_loop_default.__doc__ +class WindowGTK3(WindowBase, Gtk.Window): + def __init__(self, title): + WindowBase.__init__(self, title) + Gtk.Window.__init__(self) + 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.vbox = Gtk.Box() + self.vbox.set_property('orientation', Gtk.Orientation.VERTICAL) + self.add(self.vbox) + self.vbox.show() + + self.connect('destroy', self.destroy_event) + self.connect('delete_event', self.destroy_event) + + def add_element_to_window(self, element, expand, fill, pad, side='bottom'): + element.show() + if side == 'top': + self.vbox.pack_start(element, expand, fill, pad) + elif side == 'bottom': + self.vbox.pack_end(element, expand, fill, pad) + else: + raise KeyError('Unknown value for side, %s' % side) + size_request = element.size_request() + return 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) + + def destroy(self): + self.vbox.destroy() + 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) + class FigureManagerGTK3(FigureManagerBase): """ diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py index c3eb1da68be3..5bc4bdcf0afd 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, figure, manager=None): + backend_gtk3.FigureCanvasGTK3.__init__(self, figure, manager) self._bbox_queue = [] def _renderer_init(self): @@ -121,4 +121,7 @@ def new_figure_manager_given_figure(num, figure): FigureCanvas = FigureCanvasGTK3Agg FigureManager = FigureManagerGTK3Agg +Window = backend_gtk3.WindowGTK3 +Toolbar2 = backend_gtk3.NavigationToolbar2GTK3 +MainLoop = backend_gtk3.MainLoop show = backend_gtk3.show diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py index da8f099be7f6..d00ebdebd9c6 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, figure, manager=None): + backend_gtk3.FigureCanvasGTK3.__init__(self, figure, manager) def _renderer_init(self): """use cairo renderer""" @@ -72,4 +72,7 @@ def new_figure_manager_given_figure(num, figure): FigureCanvas = FigureCanvasGTK3Cairo FigureManager = FigureManagerGTK3Cairo +Window = backend_gtk3.WindowGTK3 +Toolbar2 = backend_gtk3.NavigationToolbar2GTK3 +MainLoop = backend_gtk3.MainLoop show = backend_gtk3.show diff --git a/lib/matplotlib/backends/backend_qt4.py b/lib/matplotlib/backends/backend_qt4.py index 8298ae368d98..7a9b8ce2baf4 100644 --- a/lib/matplotlib/backends/backend_qt4.py +++ b/lib/matplotlib/backends/backend_qt4.py @@ -36,7 +36,8 @@ SHIFT, MODIFIER_KEYS, fn_name, cursord, draw_if_interactive, _create_qApp, show, TimerQT, MainWindow, FigureManagerQT, NavigationToolbar2QT, - SubplotToolQt, error_msg_qt, exception_handler) + SubplotToolQt, error_msg_qt, exception_handler, + Window, MainLoop) from .backend_qt5 import FigureCanvasQT as FigureCanvasQT5 @@ -62,14 +63,14 @@ def new_figure_manager_given_figure(num, figure): class FigureCanvasQT(FigureCanvasQT5): - def __init__(self, figure): + def __init__(self, figure, manager=None): if DEBUG: print('FigureCanvasQt qt4: ', figure) _create_qApp() # Note different super-calling style to backend_qt5 QtWidgets.QWidget.__init__(self) - FigureCanvasBase.__init__(self, figure) + FigureCanvasBase.__init__(self, figure, manager) self.figure = figure self.setMouseTracking(True) self._idle = True @@ -78,6 +79,17 @@ def __init__(self, figure): w, h = self.get_width_height() self.resize(w, h) + # Give the keyboard focus to the figure instead of the + # manager; StrongFocus accepts both tab and click to focus and + # will enable the canvas to process event w/o clicking. + # ClickFocus only takes the focus is the window has been + # clicked + # on. http://qt-project.org/doc/qt-4.8/qt.html#FocusPolicy-enum or + # http://doc.qt.digia.com/qt/qt.html#FocusPolicy-enum + if manager: + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setFocus() + def wheelEvent(self, event): x = event.x() # flipy so y=0 is bottom of canvas diff --git a/lib/matplotlib/backends/backend_qt4agg.py b/lib/matplotlib/backends/backend_qt4agg.py index fc20578d0282..53300b3f1644 100644 --- a/lib/matplotlib/backends/backend_qt4agg.py +++ b/lib/matplotlib/backends/backend_qt4agg.py @@ -19,11 +19,11 @@ from .backend_agg import FigureCanvasAgg from .backend_qt4 import QtCore -from .backend_qt4 import FigureManagerQT +from .backend_qt4 import FigureManagerQT, Window from .backend_qt4 import FigureCanvasQT from .backend_qt4 import NavigationToolbar2QT ##### not used -from .backend_qt4 import show +from .backend_qt4 import show, MainLoop from .backend_qt4 import draw_if_interactive from .backend_qt4 import backend_version ###### @@ -65,11 +65,11 @@ class FigureCanvasQTAgg(FigureCanvasQTAggBase, figure - A Figure instance """ - def __init__(self, figure): + def __init__(self, figure, manager=None): if DEBUG: print('FigureCanvasQtAgg: ', figure) - FigureCanvasQT.__init__(self, figure) - FigureCanvasAgg.__init__(self, figure) + FigureCanvasQT.__init__(self, figure, manager) + FigureCanvasAgg.__init__(self, figure, manager) self._drawRect = None self.blitbox = None self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) @@ -93,3 +93,4 @@ def __init__(self, figure): FigureCanvas = FigureCanvasQTAgg FigureManager = FigureManagerQT +Toolbar2 = NavigationToolbar2QT diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 9526422a0266..106c0650936c 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -11,13 +11,13 @@ import matplotlib from matplotlib.cbook import is_string_like -from matplotlib.backend_bases import FigureManagerBase +from matplotlib.backend_bases import WindowBase, FigureManagerBase from matplotlib.backend_bases import FigureCanvasBase from matplotlib.backend_bases import NavigationToolbar2 from matplotlib.backend_bases import cursors from matplotlib.backend_bases import TimerBase -from matplotlib.backend_bases import ShowBase +from matplotlib.backend_bases import MainLoopBase, ShowBase from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure @@ -143,6 +143,18 @@ def _create_qApp(): qApp = app +class MainLoop(MainLoopBase): + def __init__(self): + MainLoopBase.__init__(self) + _create_qApp() + + def begin(self): + # allow KeyboardInterrupt exceptions to close the plot window. + signal.signal(signal.SIGINT, signal.SIG_DFL) + global qApp + qApp.exec_() + + class Show(ShowBase): def mainloop(self): # allow KeyboardInterrupt exceptions to close the plot window. @@ -226,15 +238,14 @@ class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase): # QtCore.Qt.XButton2: None, } - def __init__(self, figure): + def __init__(self, figure, manager=None): if DEBUG: print('FigureCanvasQt qt5: ', figure) _create_qApp() - # NB: Using super for this call to avoid a TypeError: # __init__() takes exactly 2 arguments (1 given) on QWidget # PyQt5 - super(FigureCanvasQT, self).__init__(figure=figure) + super(FigureCanvasQT, self).__init__(figure=figure, manager=manager) self.figure = figure self.setMouseTracking(True) self._idle = True @@ -243,6 +254,17 @@ def __init__(self, figure): w, h = self.get_width_height() self.resize(w, h) + # Give the keyboard focus to the figure instead of the + # manager; StrongFocus accepts both tab and click to focus and + # will enable the canvas to process event w/o clicking. + # ClickFocus only takes the focus is the window has been + # clicked + # on. http://qt-project.org/doc/qt-4.8/qt.html#FocusPolicy-enum or + # http://doc.qt.digia.com/qt/qt.html#FocusPolicy-enum + if manager: + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setFocus() + def __timerEvent(self, event): # hide until we can test and fix self.mpl_idle_event(event) @@ -442,6 +464,52 @@ def closeEvent(self, event): QtWidgets.QMainWindow.closeEvent(self, event) +class Window(WindowBase, MainWindow): + def __init__(self, title): + WindowBase.__init__(self, title) + MainWindow.__init__(self) + self.closing.connect(self.destroy_event) + + self.setWindowTitle(title) + image = os.path.join(matplotlib.rcParams['datapath'], + 'images', 'matplotlib.png') + self.setWindowIcon(QtGui.QIcon(image)) + + def add_element_to_window(self, element, expand, fill, pad, side='bottom'): + h = element.sizeHint().height() + # TODO Hack until NavigationToolbar2 becomes obsolete + if type(element) == NavigationToolbar2QT: + self.addToolBar(element) + sb = self.statusBar() + element.message.connect(self._show_message) + return h + sb.sizeHint().height() + elif isinstance(element, FigureCanvasBase): + self.setCentralWidget(element) + return h + + def show(self): + MainWindow.show(self) + + def destroy(self, *args): + self.close() + + def set_fullscreen(self, fullscreen): + if fullscreen: + self.window.showFullScreen() + else: + self.window.showNormal() + + def get_window_title(self): + return str(self.windowTitle()) + + def set_window_title(self, title): + self.setWindowTitle(title) + + @QtCore.Slot(str) + def _show_message(self, s): + # Fixes a PySide segfault. + self.statusBar().showMessage(s) + class FigureManagerQT(FigureManagerBase): """ Public attributes @@ -473,7 +541,7 @@ def __init__(self, canvas, num): # clicked # on. http://qt-project.org/doc/qt-4.8/qt.html#FocusPolicy-enum or # http://doc.qt.digia.com/qt/qt.html#FocusPolicy-enum - self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus) + self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus) # TODO take a look at this self.canvas.setFocus() self.window._destroying = False diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 687f4366182b..acefa4a67182 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -14,12 +14,12 @@ from .backend_agg import FigureCanvasAgg from .backend_qt5 import QtCore from .backend_qt5 import QtGui -from .backend_qt5 import FigureManagerQT +from .backend_qt5 import Window, FigureManagerQT from .backend_qt5 import NavigationToolbar2QT ##### Modified Qt5 backend import from .backend_qt5 import FigureCanvasQT ##### not used -from .backend_qt5 import show +from .backend_qt5 import MainLoop, show from .backend_qt5 import draw_if_interactive from .backend_qt5 import backend_version ###### @@ -170,11 +170,11 @@ class FigureCanvasQTAgg(FigureCanvasQTAggBase, figure - A Figure instance """ - def __init__(self, figure): + def __init__(self, figure, manager=None): if DEBUG: print('FigureCanvasQtAgg: ', figure) - FigureCanvasQT.__init__(self, figure) - FigureCanvasAgg.__init__(self, figure) + FigureCanvasQT.__init__(self, figure, manager) + FigureCanvasAgg.__init__(self, figure, manager) self._drawRect = None self.blitbox = None self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) @@ -198,3 +198,4 @@ def __init__(self, figure): FigureCanvas = FigureCanvasQTAgg FigureManager = FigureManagerQT +Toolbar2 = NavigationToolbar2QT diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index c0f66c9f27cd..fbc020378a15 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -534,6 +534,17 @@ def process(self, s, *args, **kwargs): proxy(*args, **kwargs) +class EventEmitter(object): + def __init__(self): + 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 Scheduler(threading.Thread): """ Base class for timeout and idle scheduling diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 986870c78140..ec3024d4a900 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1358,11 +1358,18 @@ 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 managers.old_new_figure_manager: + mgr = plt._backend_mod.new_figure_manager_given_figure(num, + self) + mgr.mainloop = plt._show + else: + mgr = managers.FigureManager(self, num) # XXX The following is a copy and paste from pyplot. Consider # factoring to pylab_helpers @@ -1377,7 +1384,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 a13d87ee2ba0..00404f4cc026 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -31,6 +31,7 @@ from matplotlib.cbook import _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 @@ -151,7 +152,10 @@ def show(*args, **kw): described above. """ global _show - return _show(*args, **kw) + if _show is None: + return _pylab_helpers.Gcf.show_all(*args, **kw) + else: + _show(*args, **kw) def isinteractive(): @@ -425,13 +429,14 @@ 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) + figManager = backend_managers.new_figure_manager(num, figsize=figsize, + dpi=dpi, + facecolor=facecolor, + edgecolor=edgecolor, + frameon=frameon, + FigureClass=FigureClass, + show=_show, + **kwargs) if figLabel: figManager.set_window_title(figLabel) @@ -444,7 +449,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) figManager.canvas.figure.number = num draw_if_interactive()