From 4347da02ebc8748b381ec6f503c32d772df3e20a Mon Sep 17 00:00:00 2001 From: Martin Fitzpatrick Date: Sun, 18 May 2014 13:44:07 +0100 Subject: [PATCH 1/5] Implement backend for PyQt5 + modify Qt4 backend to Qt5 structure. A backend for PyQt5 based on intial work by @badders, modified to fix Qt5 mouse event handling, then re-structured to implement as a wrapper over the existing Qt4 code. Following discussions on this pull request: https://github.com/matplotlib/matplotlib/pull/2471 The code has been restructured to implement PyQt5 backend as a first-class implementation, with other Qt backends (PyQt4, PyQt4v2, PySide (Qt v4)) wrapping it and modifying as required. The issues of objects being moved around in the Qt namespace (many QtGui objects now in QtWidgets) QtWidgets is simply assigned as a copy of QtGui if not available. This achieves the intended outcome with the minimum code. PySide required re-ordering of import on FigureCanvasQTAgg or paintEvent function would not being called on FigureCanvasQTAggBase resulting in black window. A number of indentation, import and other fixes. --- lib/matplotlib/backends/backend_qt4.py | 777 +--------------- lib/matplotlib/backends/backend_qt4agg.py | 114 +-- lib/matplotlib/backends/backend_qt5.py | 848 ++++++++++++++++++ lib/matplotlib/backends/backend_qt5agg.py | 210 +++++ .../backends/{qt4_compat.py => qt_compat.py} | 61 +- .../{qt4_editor => qt_editor}/__init__.py | 0 .../figureoptions.py | 4 +- .../{qt4_editor => qt_editor}/formlayout.py | 102 +-- .../formsubplottool.py | 87 +- lib/matplotlib/pyplot.py | 5 + lib/matplotlib/rcsetup.py | 4 +- lib/matplotlib/tests/test_backend_qt4.py | 2 +- lib/matplotlib/tests/test_backend_qt5.py | 160 ++++ lib/matplotlib/tests/test_coding_standards.py | 3 +- setup.py | 1 + setupext.py | 18 +- 16 files changed, 1405 insertions(+), 991 deletions(-) create mode 100644 lib/matplotlib/backends/backend_qt5.py create mode 100644 lib/matplotlib/backends/backend_qt5agg.py rename lib/matplotlib/backends/{qt4_compat.py => qt_compat.py} (64%) rename lib/matplotlib/backends/{qt4_editor => qt_editor}/__init__.py (100%) rename lib/matplotlib/backends/{qt4_editor => qt_editor}/figureoptions.py (97%) rename lib/matplotlib/backends/{qt4_editor => qt_editor}/formlayout.py (85%) rename lib/matplotlib/backends/{qt4_editor => qt_editor}/formsubplottool.py (73%) create mode 100644 lib/matplotlib/tests/test_backend_qt5.py diff --git a/lib/matplotlib/backends/backend_qt4.py b/lib/matplotlib/backends/backend_qt4.py index 70152aac9f47..2ab88c96d45a 100644 --- a/lib/matplotlib/backends/backend_qt4.py +++ b/lib/matplotlib/backends/backend_qt4.py @@ -25,131 +25,23 @@ from matplotlib.widgets import SubplotTool try: - import matplotlib.backends.qt4_editor.figureoptions as figureoptions + import matplotlib.backends.qt_editor.figureoptions as figureoptions except ImportError: figureoptions = None -from .qt4_compat import QtCore, QtGui, _getSaveFileName, __version__ -from matplotlib.backends.qt4_editor.formsubplottool import UiSubplotTool +from .qt_compat import QtCore, QtWidgets, _getSaveFileName, __version__ +from matplotlib.backends.qt_editor.formsubplottool import UiSubplotTool -backend_version = __version__ +from .backend_qt5 import (backend_version, SPECIAL_KEYS, SUPER, ALT, CTRL, + SHIFT, MODIFIER_KEYS, fn_name, cursord, + draw_if_interactive, _create_qApp, show, TimerQT, + MainWindow, FigureManagerQT, NavigationToolbar2QT, + SubplotToolQt, error_msg_qt, exception_handler) -# SPECIAL_KEYS are keys that do *not* return their unicode name -# instead they have manually specified names -SPECIAL_KEYS = {QtCore.Qt.Key_Control: 'control', - QtCore.Qt.Key_Shift: 'shift', - QtCore.Qt.Key_Alt: 'alt', - QtCore.Qt.Key_Meta: 'super', - QtCore.Qt.Key_Return: 'enter', - QtCore.Qt.Key_Left: 'left', - QtCore.Qt.Key_Up: 'up', - QtCore.Qt.Key_Right: 'right', - QtCore.Qt.Key_Down: 'down', - QtCore.Qt.Key_Escape: 'escape', - QtCore.Qt.Key_F1: 'f1', - QtCore.Qt.Key_F2: 'f2', - QtCore.Qt.Key_F3: 'f3', - QtCore.Qt.Key_F4: 'f4', - QtCore.Qt.Key_F5: 'f5', - QtCore.Qt.Key_F6: 'f6', - QtCore.Qt.Key_F7: 'f7', - QtCore.Qt.Key_F8: 'f8', - QtCore.Qt.Key_F9: 'f9', - QtCore.Qt.Key_F10: 'f10', - QtCore.Qt.Key_F11: 'f11', - QtCore.Qt.Key_F12: 'f12', - QtCore.Qt.Key_Home: 'home', - QtCore.Qt.Key_End: 'end', - QtCore.Qt.Key_PageUp: 'pageup', - QtCore.Qt.Key_PageDown: 'pagedown', - QtCore.Qt.Key_Tab: 'tab', - QtCore.Qt.Key_Backspace: 'backspace', - QtCore.Qt.Key_Enter: 'enter', - QtCore.Qt.Key_Insert: 'insert', - QtCore.Qt.Key_Delete: 'delete', - QtCore.Qt.Key_Pause: 'pause', - QtCore.Qt.Key_SysReq: 'sysreq', - QtCore.Qt.Key_Clear: 'clear', } - -# define which modifier keys are collected on keyboard events. -# elements are (mpl names, Modifier Flag, Qt Key) tuples -SUPER = 0 -ALT = 1 -CTRL = 2 -SHIFT = 3 -MODIFIER_KEYS = [('super', QtCore.Qt.MetaModifier, QtCore.Qt.Key_Meta), - ('alt', QtCore.Qt.AltModifier, QtCore.Qt.Key_Alt), - ('ctrl', QtCore.Qt.ControlModifier, QtCore.Qt.Key_Control), - ('shift', QtCore.Qt.ShiftModifier, QtCore.Qt.Key_Shift), - ] - -if sys.platform == 'darwin': - # in OSX, the control and super (aka cmd/apple) keys are switched, so - # switch them back. - SPECIAL_KEYS.update({QtCore.Qt.Key_Control: 'super', # cmd/apple key - QtCore.Qt.Key_Meta: 'control', - }) - MODIFIER_KEYS[0] = ('super', QtCore.Qt.ControlModifier, - QtCore.Qt.Key_Control) - MODIFIER_KEYS[2] = ('ctrl', QtCore.Qt.MetaModifier, - QtCore.Qt.Key_Meta) - - -def fn_name(): - return sys._getframe(1).f_code.co_name +from .backend_qt5 import FigureCanvasQT as FigureCanvasQT5 DEBUG = False -cursord = { - cursors.MOVE: QtCore.Qt.SizeAllCursor, - cursors.HAND: QtCore.Qt.PointingHandCursor, - cursors.POINTER: QtCore.Qt.ArrowCursor, - cursors.SELECT_REGION: QtCore.Qt.CrossCursor, - } - - -def draw_if_interactive(): - """ - Is called after every pylab drawing command - """ - if matplotlib.is_interactive(): - figManager = Gcf.get_active() - if figManager is not None: - figManager.canvas.draw_idle() - - -def _create_qApp(): - """ - Only one qApp can exist at a time, so check before creating one. - """ - if QtGui.QApplication.startingUp(): - if DEBUG: - print("Starting up QApplication") - global qApp - app = QtGui.QApplication.instance() - if app is None: - - # check for DISPLAY env variable on X11 build of Qt - if hasattr(QtGui, "QX11Info"): - display = os.environ.get('DISPLAY') - if display is None or not re.search(':\d', display): - raise RuntimeError('Invalid DISPLAY variable') - - qApp = QtGui.QApplication([str(" ")]) - qApp.lastWindowClosed.connect(qApp.quit) - else: - qApp = app - - -class Show(ShowBase): - def mainloop(self): - # allow KeyboardInterrupt exceptions to close the plot window. - signal.signal(signal.SIGINT, signal.SIG_DFL) - - QtGui.qApp.exec_() -show = Show() - - def new_figure_manager(num, *args, **kwargs): """ Create a new figure manager instance @@ -157,7 +49,6 @@ def new_figure_manager(num, *args, **kwargs): thisFig = Figure(*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. @@ -166,68 +57,15 @@ def new_figure_manager_given_figure(num, figure): manager = FigureManagerQT(canvas, num) return manager - -class TimerQT(TimerBase): - ''' - Subclass of :class:`backend_bases.TimerBase` that uses Qt4 timer events. - - Attributes: - * interval: The time between timer events in milliseconds. Default - is 1000 ms. - * single_shot: Boolean flag indicating whether this timer should - operate as single shot (run once and then stop). Defaults to False. - * callbacks: Stores list of (func, args) tuples that will be called - upon timer events. This list can be manipulated directly, or the - functions add_callback and remove_callback can be used. - ''' - def __init__(self, *args, **kwargs): - TimerBase.__init__(self, *args, **kwargs) - - # Create a new timer and connect the timeout() signal to the - # _on_timer method. - self._timer = QtCore.QTimer() - self._timer.timeout.connect(self._on_timer) - self._timer_set_interval() - - def __del__(self): - # Probably not necessary in practice, but is good behavior to - # disconnect - try: - TimerBase.__del__(self) - self._timer.timeout.disconnect(self._on_timer) - except RuntimeError: - # Timer C++ object already deleted - pass - - def _timer_set_single_shot(self): - self._timer.setSingleShot(self._single) - - def _timer_set_interval(self): - self._timer.setInterval(self._interval) - - def _timer_start(self): - self._timer.start() - - def _timer_stop(self): - self._timer.stop() - - -class FigureCanvasQT(QtGui.QWidget, FigureCanvasBase): - - # map Qt button codes to MouseEvent's ones: - buttond = {QtCore.Qt.LeftButton: 1, - QtCore.Qt.MidButton: 2, - QtCore.Qt.RightButton: 3, - # QtCore.Qt.XButton1: None, - # QtCore.Qt.XButton2: None, - } +class FigureCanvasQT(FigureCanvasQT5): def __init__(self, figure): if DEBUG: - print('FigureCanvasQt: ', figure) + print('FigureCanvasQt qt4: ', figure) _create_qApp() - QtGui.QWidget.__init__(self) + # Note different super-calling style to backend_qt5 + QtWidgets.QWidget.__init__(self) FigureCanvasBase.__init__(self, figure) self.figure = figure self.setMouseTracking(True) @@ -237,55 +75,6 @@ def __init__(self, figure): w, h = self.get_width_height() self.resize(w, h) - def __timerEvent(self, event): - # hide until we can test and fix - self.mpl_idle_event(event) - - def enterEvent(self, event): - FigureCanvasBase.enter_notify_event(self, event) - - def leaveEvent(self, event): - QtGui.QApplication.restoreOverrideCursor() - FigureCanvasBase.leave_notify_event(self, event) - - def mousePressEvent(self, event): - x = event.pos().x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.pos().y() - button = self.buttond.get(event.button()) - if button is not None: - FigureCanvasBase.button_press_event(self, x, y, button) - if DEBUG: - print('button pressed:', event.button()) - - def mouseDoubleClickEvent(self, event): - x = event.pos().x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.pos().y() - button = self.buttond.get(event.button()) - if button is not None: - FigureCanvasBase.button_press_event(self, x, y, - button, dblclick=True) - if DEBUG: - print('button doubleclicked:', event.button()) - - def mouseMoveEvent(self, event): - x = event.x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y() - FigureCanvasBase.motion_notify_event(self, x, y) - #if DEBUG: print('mouse move') - - def mouseReleaseEvent(self, event): - x = event.x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y() - button = self.buttond.get(event.button()) - if button is not None: - FigureCanvasBase.button_release_event(self, x, y, button) - if DEBUG: - print('button released') - def wheelEvent(self, event): x = event.x() # flipy so y=0 is bottom of canvas @@ -298,546 +87,6 @@ def wheelEvent(self, event): print('scroll event: delta = %i, ' 'steps = %i ' % (event.delta(), steps)) - def keyPressEvent(self, event): - key = self._get_key(event) - if key is None: - return - FigureCanvasBase.key_press_event(self, key) - if DEBUG: - print('key press', key) - - def keyReleaseEvent(self, event): - key = self._get_key(event) - if key is None: - return - FigureCanvasBase.key_release_event(self, key) - if DEBUG: - print('key release', key) - - def resizeEvent(self, event): - w = event.size().width() - h = event.size().height() - if DEBUG: - print('resize (%d x %d)' % (w, h)) - print("FigureCanvasQt.resizeEvent(%d, %d)" % (w, h)) - dpival = self.figure.dpi - winch = w/dpival - hinch = h/dpival - self.figure.set_size_inches(winch, hinch) - FigureCanvasBase.resize_event(self) - self.draw() - self.update() - QtGui.QWidget.resizeEvent(self, event) - - def sizeHint(self): - w, h = self.get_width_height() - return QtCore.QSize(w, h) - - def minumumSizeHint(self): - return QtCore.QSize(10, 10) - - def _get_key(self, event): - if event.isAutoRepeat(): - return None - - event_key = event.key() - event_mods = int(event.modifiers()) # actually a bitmask - - # get names of the pressed modifier keys - # bit twiddling to pick out modifier keys from event_mods bitmask, - # if event_key is a MODIFIER, it should not be duplicated in mods - mods = [name for name, mod_key, qt_key in MODIFIER_KEYS - if event_key != qt_key and (event_mods & mod_key) == mod_key] - try: - # for certain keys (enter, left, backspace, etc) use a word for the - # key, rather than unicode - key = SPECIAL_KEYS[event_key] - except KeyError: - # unicode defines code points up to 0x0010ffff - # QT will use Key_Codes larger than that for keyboard keys that are - # are not unicode characters (like multimedia keys) - # skip these - # if you really want them, you should add them to SPECIAL_KEYS - MAX_UNICODE = 0x10ffff - if event_key > MAX_UNICODE: - return None - - key = unichr(event_key) - # qt delivers capitalized letters. fix capitalization - # note that capslock is ignored - if 'shift' in mods: - mods.remove('shift') - else: - key = key.lower() - - mods.reverse() - return '+'.join(mods + [key]) - - def new_timer(self, *args, **kwargs): - """ - Creates a new backend-specific subclass of - :class:`backend_bases.Timer`. This is useful for getting - periodic events through the backend's native event - loop. Implemented only for backends with GUIs. - - optional arguments: - - *interval* - Timer interval in milliseconds - - *callbacks* - Sequence of (func, args, kwargs) where func(*args, **kwargs) - will be executed by the timer every *interval*. - - """ - return TimerQT(*args, **kwargs) - - def flush_events(self): - QtGui.qApp.processEvents() - - def start_event_loop(self, timeout): - FigureCanvasBase.start_event_loop_default(self, timeout) - - start_event_loop.__doc__ = \ - FigureCanvasBase.start_event_loop_default.__doc__ - - def stop_event_loop(self): - FigureCanvasBase.stop_event_loop_default(self) - - stop_event_loop.__doc__ = FigureCanvasBase.stop_event_loop_default.__doc__ - - def draw_idle(self): - 'update drawing area only if idle' - d = self._idle - self._idle = False - - def idle_draw(*args): - try: - self.draw() - finally: - self._idle = True - if d: - QtCore.QTimer.singleShot(0, idle_draw) - - -class MainWindow(QtGui.QMainWindow): - closing = QtCore.Signal() - - def closeEvent(self, event): - self.closing.emit() - QtGui.QMainWindow.closeEvent(self, event) - - -class FigureManagerQT(FigureManagerBase): - """ - Public attributes - - canvas : The FigureCanvas instance - num : The Figure number - toolbar : The qt.QToolBar - window : The qt.QMainWindow - """ - - def __init__(self, canvas, num): - if DEBUG: - print('FigureManagerQT.%s' % fn_name()) - FigureManagerBase.__init__(self, canvas, num) - self.canvas = canvas - self.window = MainWindow() - self.window.closing.connect(canvas.close_event) - self.window.closing.connect(self._widgetclosed) - - self.window.setWindowTitle("Figure %d" % num) - image = os.path.join(matplotlib.rcParams['datapath'], - 'images', 'matplotlib.png') - self.window.setWindowIcon(QtGui.QIcon(image)) - - # 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 - self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus) - self.canvas.setFocus() - - self.window._destroying = False - - self.toolbar = self._get_toolbar(self.canvas, self.window) - if self.toolbar is not None: - self.window.addToolBar(self.toolbar) - self.toolbar.message.connect(self._show_message) - tbs_height = self.toolbar.sizeHint().height() - else: - tbs_height = 0 - - # resize the main window so it will display the canvas with the - # requested size: - cs = canvas.sizeHint() - sbs = self.window.statusBar().sizeHint() - self._status_and_tool_height = tbs_height + sbs.height() - height = cs.height() + self._status_and_tool_height - self.window.resize(cs.width(), height) - - self.window.setCentralWidget(self.canvas) - - if matplotlib.is_interactive(): - self.window.show() - - def notify_axes_change(fig): - # This will be called whenever the current axes is changed - if self.toolbar is not None: - self.toolbar.update() - self.canvas.figure.add_axobserver(notify_axes_change) - - @QtCore.Slot() - def _show_message(self, s): - # Fixes a PySide segfault. - self.window.statusBar().showMessage(s) - - def full_screen_toggle(self): - if self.window.isFullScreen(): - self.window.showNormal() - else: - self.window.showFullScreen() - - def _widgetclosed(self): - if self.window._destroying: - return - self.window._destroying = True - try: - Gcf.destroy(self.num) - except AttributeError: - pass - # It seems that when the python session is killed, - # Gcf can get destroyed before the Gcf.destroy - # line is run, leading to a useless AttributeError. - - def _get_toolbar(self, canvas, parent): - # must be inited after the window, drawingArea and figure - # attrs are set - if matplotlib.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2QT(canvas, parent, False) - else: - toolbar = None - return toolbar - - def resize(self, width, height): - 'set the canvas size in pixels' - self.window.resize(width, height + self._status_and_tool_height) - - def show(self): - self.window.show() - - def destroy(self, *args): - # check for qApp first, as PySide deletes it in its atexit handler - if QtGui.QApplication.instance() is None: - return - if self.window._destroying: - return - self.window._destroying = True - self.window.destroyed.connect(self._widgetclosed) - - if self.toolbar: - self.toolbar.destroy() - if DEBUG: - print("destroy figure manager") - self.window.close() - - def get_window_title(self): - return str(self.window.windowTitle()) - - def set_window_title(self, title): - self.window.setWindowTitle(title) - - -class NavigationToolbar2QT(NavigationToolbar2, QtGui.QToolBar): - message = QtCore.Signal(str) - - def __init__(self, canvas, parent, coordinates=True): - """ coordinates: should we show the coordinates on the right? """ - self.canvas = canvas - self.parent = parent - self.coordinates = coordinates - self._actions = {} - """A mapping of toolitem method names to their QActions""" - - QtGui.QToolBar.__init__(self, parent) - NavigationToolbar2.__init__(self, canvas) - - def _icon(self, name): - return QtGui.QIcon(os.path.join(self.basedir, name)) - - def _init_toolbar(self): - self.basedir = os.path.join(matplotlib.rcParams['datapath'], 'images') - - for text, tooltip_text, image_file, callback in self.toolitems: - if text is None: - self.addSeparator() - else: - a = self.addAction(self._icon(image_file + '.png'), - text, getattr(self, callback)) - self._actions[callback] = a - if callback in ['zoom', 'pan']: - a.setCheckable(True) - if tooltip_text is not None: - a.setToolTip(tooltip_text) - - if figureoptions is not None: - a = self.addAction(self._icon("qt4_editor_options.png"), - 'Customize', self.edit_parameters) - a.setToolTip('Edit curves line and axes parameters') - - self.buttons = {} - - # Add the x,y location widget at the right side of the toolbar - # The stretch factor is 1 which means any resizing of the toolbar - # will resize this label instead of the buttons. - if self.coordinates: - self.locLabel = QtGui.QLabel("", self) - self.locLabel.setAlignment( - QtCore.Qt.AlignRight | QtCore.Qt.AlignTop) - self.locLabel.setSizePolicy( - QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, - QtGui.QSizePolicy.Ignored)) - labelAction = self.addWidget(self.locLabel) - labelAction.setVisible(True) - - # reference holder for subplots_adjust window - self.adj_window = None - - if figureoptions is not None: - def edit_parameters(self): - allaxes = self.canvas.figure.get_axes() - if len(allaxes) == 1: - axes = allaxes[0] - else: - titles = [] - for axes in allaxes: - title = axes.get_title() - ylabel = axes.get_ylabel() - label = axes.get_label() - if title: - fmt = "%(title)s" - if ylabel: - fmt += ": %(ylabel)s" - fmt += " (%(axes_repr)s)" - elif ylabel: - fmt = "%(axes_repr)s (%(ylabel)s)" - elif label: - fmt = "%(axes_repr)s (%(label)s)" - else: - fmt = "%(axes_repr)s" - titles.append(fmt % dict(title=title, - ylabel=ylabel, label=label, - axes_repr=repr(axes))) - item, ok = QtGui.QInputDialog.getItem( - self.parent, 'Customize', 'Select axes:', titles, 0, False) - if ok: - axes = allaxes[titles.index(six.text_type(item))] - else: - return - - figureoptions.figure_edit(axes, self) - - def _update_buttons_checked(self): - #sync button checkstates to match active mode - self._actions['pan'].setChecked(self._active == 'PAN') - self._actions['zoom'].setChecked(self._active == 'ZOOM') - - def pan(self, *args): - super(NavigationToolbar2QT, self).pan(*args) - self._update_buttons_checked() - - def zoom(self, *args): - super(NavigationToolbar2QT, self).zoom(*args) - self._update_buttons_checked() - - def dynamic_update(self): - self.canvas.draw() - - def set_message(self, s): - self.message.emit(s) - if self.coordinates: - self.locLabel.setText(s.replace(', ', '\n')) - - def set_cursor(self, cursor): - if DEBUG: - print('Set cursor', cursor) - self.canvas.setCursor(cursord[cursor]) - - def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height - y1 = height - y1 - y0 = height - y0 - - w = abs(x1 - x0) - h = abs(y1 - y0) - - rect = [int(val)for val in (min(x0, x1), min(y0, y1), w, h)] - self.canvas.drawRectangle(rect) - - def configure_subplots(self): - image = os.path.join(matplotlib.rcParams['datapath'], - 'images', 'matplotlib.png') - dia = SubplotToolQt(self.canvas.figure, self.parent) - dia.setWindowIcon(QtGui.QIcon(image)) - dia.exec_() - - def save_figure(self, *args): - filetypes = self.canvas.get_supported_filetypes_grouped() - sorted_filetypes = list(six.iteritems(filetypes)) - sorted_filetypes.sort() - default_filetype = self.canvas.get_default_filetype() - - startpath = matplotlib.rcParams.get('savefig.directory', '') - startpath = os.path.expanduser(startpath) - start = os.path.join(startpath, self.canvas.get_default_filename()) - filters = [] - selectedFilter = None - for name, exts in sorted_filetypes: - exts_list = " ".join(['*.%s' % ext for ext in exts]) - filter = '%s (%s)' % (name, exts_list) - if default_filetype in exts: - selectedFilter = filter - filters.append(filter) - filters = ';;'.join(filters) - - fname = _getSaveFileName(self.parent, "Choose a filename to save to", - start, filters, selectedFilter) - if fname: - if startpath == '': - # explicitly missing key or empty str signals to use cwd - matplotlib.rcParams['savefig.directory'] = startpath - else: - # save dir for next time - savefig_dir = os.path.dirname(six.text_type(fname)) - matplotlib.rcParams['savefig.directory'] = savefig_dir - try: - self.canvas.print_figure(six.text_type(fname)) - except Exception as e: - QtGui.QMessageBox.critical( - self, "Error saving file", str(e), - QtGui.QMessageBox.Ok, QtGui.QMessageBox.NoButton) - - -class SubplotToolQt(SubplotTool, UiSubplotTool): - def __init__(self, targetfig, parent): - UiSubplotTool.__init__(self, None) - - self.targetfig = targetfig - self.parent = parent - self.donebutton.clicked.connect(self.close) - self.resetbutton.clicked.connect(self.reset) - self.tightlayout.clicked.connect(self.functight) - - # constraints - self.sliderleft.valueChanged.connect(self.sliderright.setMinimum) - self.sliderright.valueChanged.connect(self.sliderleft.setMaximum) - self.sliderbottom.valueChanged.connect(self.slidertop.setMinimum) - self.slidertop.valueChanged.connect(self.sliderbottom.setMaximum) - - self.defaults = {} - for attr in ('left', 'bottom', 'right', 'top', 'wspace', 'hspace',): - self.defaults[attr] = getattr(self.targetfig.subplotpars, attr) - slider = getattr(self, 'slider' + attr) - slider.setMinimum(0) - slider.setMaximum(1000) - slider.setSingleStep(5) - slider.valueChanged.connect(getattr(self, 'func' + attr)) - - self._setSliderPositions() - - def _setSliderPositions(self): - for attr in ('left', 'bottom', 'right', 'top', 'wspace', 'hspace',): - slider = getattr(self, 'slider' + attr) - slider.setSliderPosition(int(self.defaults[attr] * 1000)) - - def funcleft(self, val): - if val == self.sliderright.value(): - val -= 1 - val /= 1000. - self.targetfig.subplots_adjust(left=val) - self.leftvalue.setText("%.2f" % val) - if self.drawon: - self.targetfig.canvas.draw() - - def funcright(self, val): - if val == self.sliderleft.value(): - val += 1 - val /= 1000. - self.targetfig.subplots_adjust(right=val) - self.rightvalue.setText("%.2f" % val) - if self.drawon: - self.targetfig.canvas.draw() - - def funcbottom(self, val): - if val == self.slidertop.value(): - val -= 1 - val /= 1000. - self.targetfig.subplots_adjust(bottom=val) - self.bottomvalue.setText("%.2f" % val) - if self.drawon: - self.targetfig.canvas.draw() - - def functop(self, val): - if val == self.sliderbottom.value(): - val += 1 - val /= 1000. - self.targetfig.subplots_adjust(top=val) - self.topvalue.setText("%.2f" % val) - if self.drawon: - self.targetfig.canvas.draw() - - def funcwspace(self, val): - val /= 1000. - self.targetfig.subplots_adjust(wspace=val) - self.wspacevalue.setText("%.2f" % val) - if self.drawon: - self.targetfig.canvas.draw() - - def funchspace(self, val): - val /= 1000. - self.targetfig.subplots_adjust(hspace=val) - self.hspacevalue.setText("%.2f" % val) - if self.drawon: - self.targetfig.canvas.draw() - - def functight(self): - self.targetfig.tight_layout() - self._setSliderPositions() - self.targetfig.canvas.draw() - - def reset(self): - self.targetfig.subplots_adjust(**self.defaults) - self._setSliderPositions() - self.targetfig.canvas.draw() - - -def error_msg_qt(msg, parent=None): - if not is_string_like(msg): - msg = ','.join(map(str, msg)) - - QtGui.QMessageBox.warning(None, "Matplotlib", msg, QtGui.QMessageBox.Ok) - - -def exception_handler(type, value, tb): - """Handle uncaught exceptions - It does not catch SystemExit - """ - msg = '' - # get the filename attribute if available (for IOError) - if hasattr(value, 'filename') and value.filename is not None: - msg = value.filename + ': ' - if hasattr(value, 'strerror') and value.strerror is not None: - msg += value.strerror - else: - msg += str(value) - - if len(msg): - error_msg_qt(msg) - FigureCanvas = FigureCanvasQT FigureManager = FigureManagerQT diff --git a/lib/matplotlib/backends/backend_qt4agg.py b/lib/matplotlib/backends/backend_qt4agg.py index addfa75b6a31..3727d921830d 100644 --- a/lib/matplotlib/backends/backend_qt4agg.py +++ b/lib/matplotlib/backends/backend_qt4agg.py @@ -14,9 +14,11 @@ import matplotlib from matplotlib.figure import Figure +from .backend_qt5agg import new_figure_manager, NavigationToolbar2QTAgg +from .backend_qt5agg import FigureCanvasQTAggBase + from .backend_agg import FigureCanvasAgg from .backend_qt4 import QtCore -from .backend_qt4 import QtGui from .backend_qt4 import FigureManagerQT from .backend_qt4 import FigureCanvasQT from .backend_qt4 import NavigationToolbar2QT @@ -39,12 +41,11 @@ def new_figure_manager(num, *args, **kwargs): Create a new figure manager instance """ if DEBUG: - print('backend_qtagg.new_figure_manager') + print('backend_qt4agg.new_figure_manager') 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. @@ -52,8 +53,7 @@ def new_figure_manager_given_figure(num, figure): canvas = FigureCanvasQTAgg(figure) return FigureManagerQT(canvas, num) - -class FigureCanvasQTAgg(FigureCanvasQT, FigureCanvasAgg): +class FigureCanvasQTAgg(FigureCanvasQTAggBase, FigureCanvasQT, FigureCanvasAgg): """ The canvas the figure renders into. Calls the draw and print fig methods, creates the renderers, etc... @@ -88,110 +88,6 @@ def __init__(self, figure): else: self._priv_update = self.update - def drawRectangle(self, rect): - self._drawRect = rect - self.repaint() - - def paintEvent(self, e): - """ - Copy the image from the Agg canvas to the qt.drawable. - In Qt, all drawing should be done inside of here when a widget is - shown onscreen. - """ - - #FigureCanvasQT.paintEvent(self, e) - if DEBUG: - print('FigureCanvasQtAgg.paintEvent: ', self, - self.get_width_height()) - - if self.blitbox is None: - # matplotlib is in rgba byte order. QImage wants to put the bytes - # into argb format and is in a 4 byte unsigned int. Little endian - # system is LSB first and expects the bytes in reverse order - # (bgra). - if QtCore.QSysInfo.ByteOrder == QtCore.QSysInfo.LittleEndian: - stringBuffer = self.renderer._renderer.tostring_bgra() - else: - stringBuffer = self.renderer._renderer.tostring_argb() - - refcnt = sys.getrefcount(stringBuffer) - - # convert the Agg rendered image -> qImage - qImage = QtGui.QImage(stringBuffer, self.renderer.width, - self.renderer.height, - QtGui.QImage.Format_ARGB32) - # get the rectangle for the image - rect = qImage.rect() - p = QtGui.QPainter(self) - # reset the image area of the canvas to be the back-ground color - p.eraseRect(rect) - # draw the rendered image on to the canvas - p.drawPixmap(QtCore.QPoint(0, 0), QtGui.QPixmap.fromImage(qImage)) - - # draw the zoom rectangle to the QPainter - if self._drawRect is not None: - p.setPen(QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.DotLine)) - x, y, w, h = self._drawRect - p.drawRect(x, y, w, h) - p.end() - - # This works around a bug in PySide 1.1.2 on Python 3.x, - # where the reference count of stringBuffer is incremented - # but never decremented by QImage. - # TODO: revert PR #1323 once the issue is fixed in PySide. - del qImage - if refcnt != sys.getrefcount(stringBuffer): - _decref(stringBuffer) - else: - bbox = self.blitbox - l, b, r, t = bbox.extents - w = int(r) - int(l) - h = int(t) - int(b) - t = int(b) + h - reg = self.copy_from_bbox(bbox) - stringBuffer = reg.to_string_argb() - qImage = QtGui.QImage(stringBuffer, w, h, - QtGui.QImage.Format_ARGB32) - pixmap = QtGui.QPixmap.fromImage(qImage) - p = QtGui.QPainter(self) - p.drawPixmap(QtCore.QPoint(l, self.renderer.height-t), pixmap) - p.end() - self.blitbox = None - self._drawRect = None - - def draw(self): - """ - Draw the figure with Agg, and queue a request - for a Qt draw. - """ - # The Agg draw is done here; delaying it until the paintEvent - # causes problems with code that uses the result of the - # draw() to update plot elements. - FigureCanvasAgg.draw(self) - self._priv_update() - - def blit(self, bbox=None): - """ - Blit the region in bbox - """ - self.blitbox = bbox - l, b, w, h = bbox.bounds - t = b + h - self.repaint(l, self.renderer.height-t, w, h) - - def print_figure(self, *args, **kwargs): - FigureCanvasAgg.print_figure(self, *args, **kwargs) - self.draw() - - -class NavigationToolbar2QTAgg(NavigationToolbar2QT): - def __init__(*args, **kwargs): - warnings.warn('This class has been deprecated in 1.4 ' + - 'as it has no additional functionality over ' + - '`NavigationToolbar2QT`. Please change your code to ' - 'use `NavigationToolbar2QT` instead', - mplDeprecation) - NavigationToolbar2QT.__init__(*args, **kwargs) FigureCanvas = FigureCanvasQTAgg diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py new file mode 100644 index 000000000000..e3ca79747b37 --- /dev/null +++ b/lib/matplotlib/backends/backend_qt5.py @@ -0,0 +1,848 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) +import six + +import os +import re +import signal +import sys + +import matplotlib + +from matplotlib.cbook import is_string_like +from matplotlib.backend_bases import 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._pylab_helpers import Gcf +from matplotlib.figure import Figure + + +from matplotlib.widgets import SubplotTool +try: + import matplotlib.backends.qt_editor.figureoptions as figureoptions +except ImportError: + figureoptions = None + +from .qt_compat import QtCore, QtGui, QtWidgets, _getSaveFileName, __version__ +from matplotlib.backends.qt_editor.formsubplottool import UiSubplotTool + +backend_version = __version__ + +# SPECIAL_KEYS are keys that do *not* return their unicode name +# instead they have manually specified names +SPECIAL_KEYS = {QtCore.Qt.Key_Control: 'control', + QtCore.Qt.Key_Shift: 'shift', + QtCore.Qt.Key_Alt: 'alt', + QtCore.Qt.Key_Meta: 'super', + QtCore.Qt.Key_Return: 'enter', + QtCore.Qt.Key_Left: 'left', + QtCore.Qt.Key_Up: 'up', + QtCore.Qt.Key_Right: 'right', + QtCore.Qt.Key_Down: 'down', + QtCore.Qt.Key_Escape: 'escape', + QtCore.Qt.Key_F1: 'f1', + QtCore.Qt.Key_F2: 'f2', + QtCore.Qt.Key_F3: 'f3', + QtCore.Qt.Key_F4: 'f4', + QtCore.Qt.Key_F5: 'f5', + QtCore.Qt.Key_F6: 'f6', + QtCore.Qt.Key_F7: 'f7', + QtCore.Qt.Key_F8: 'f8', + QtCore.Qt.Key_F9: 'f9', + QtCore.Qt.Key_F10: 'f10', + QtCore.Qt.Key_F11: 'f11', + QtCore.Qt.Key_F12: 'f12', + QtCore.Qt.Key_Home: 'home', + QtCore.Qt.Key_End: 'end', + QtCore.Qt.Key_PageUp: 'pageup', + QtCore.Qt.Key_PageDown: 'pagedown', + QtCore.Qt.Key_Tab: 'tab', + QtCore.Qt.Key_Backspace: 'backspace', + QtCore.Qt.Key_Enter: 'enter', + QtCore.Qt.Key_Insert: 'insert', + QtCore.Qt.Key_Delete: 'delete', + QtCore.Qt.Key_Pause: 'pause', + QtCore.Qt.Key_SysReq: 'sysreq', + QtCore.Qt.Key_Clear: 'clear', } + +# define which modifier keys are collected on keyboard events. +# elements are (mpl names, Modifier Flag, Qt Key) tuples +SUPER = 0 +ALT = 1 +CTRL = 2 +SHIFT = 3 +MODIFIER_KEYS = [('super', QtCore.Qt.MetaModifier, QtCore.Qt.Key_Meta), + ('alt', QtCore.Qt.AltModifier, QtCore.Qt.Key_Alt), + ('ctrl', QtCore.Qt.ControlModifier, QtCore.Qt.Key_Control), + ('shift', QtCore.Qt.ShiftModifier, QtCore.Qt.Key_Shift), + ] + +if sys.platform == 'darwin': + # in OSX, the control and super (aka cmd/apple) keys are switched, so + # switch them back. + SPECIAL_KEYS.update({QtCore.Qt.Key_Control: 'super', # cmd/apple key + QtCore.Qt.Key_Meta: 'control', + }) + MODIFIER_KEYS[0] = ('super', QtCore.Qt.ControlModifier, + QtCore.Qt.Key_Control) + MODIFIER_KEYS[2] = ('ctrl', QtCore.Qt.MetaModifier, + QtCore.Qt.Key_Meta) + + +def fn_name(): + return sys._getframe(1).f_code.co_name + +DEBUG = False + +cursord = { + cursors.MOVE: QtCore.Qt.SizeAllCursor, + cursors.HAND: QtCore.Qt.PointingHandCursor, + cursors.POINTER: QtCore.Qt.ArrowCursor, + cursors.SELECT_REGION: QtCore.Qt.CrossCursor, + } + + +def draw_if_interactive(): + """ + Is called after every pylab drawing command + """ + if matplotlib.is_interactive(): + figManager = Gcf.get_active() + if figManager is not None: + figManager.canvas.draw_idle() + + +def _create_qApp(): + """ + Only one qApp can exist at a time, so check before creating one. + """ + if QtWidgets.QApplication.startingUp(): + if DEBUG: + print("Starting up QApplication") + global qApp + app = QtWidgets.QApplication.instance() + if app is None: + + # check for DISPLAY env variable on X11 build of Qt + if hasattr(QtGui, "QX11Info"): + display = os.environ.get('DISPLAY') + if display is None or not re.search(':\d', display): + raise RuntimeError('Invalid DISPLAY variable') + + qApp = QtWidgets.QApplication([str(" ")]) + qApp.lastWindowClosed.connect(qApp.quit) + else: + qApp = app + + +class Show(ShowBase): + def mainloop(self): + # allow KeyboardInterrupt exceptions to close the plot window. + signal.signal(signal.SIGINT, signal.SIG_DFL) + global qApp + qApp.exec_() + +show = Show() + + +def new_figure_manager(num, *args, **kwargs): + """ + Create a new figure manager instance + """ + thisFig = Figure(*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 = FigureCanvasQT(figure) + manager = FigureManagerQT(canvas, num) + return manager + + +class TimerQT(TimerBase): + ''' + Subclass of :class:`backend_bases.TimerBase` that uses Qt4 timer events. + + Attributes: + * interval: The time between timer events in milliseconds. Default + is 1000 ms. + * single_shot: Boolean flag indicating whether this timer should + operate as single shot (run once and then stop). Defaults to False. + * callbacks: Stores list of (func, args) tuples that will be called + upon timer events. This list can be manipulated directly, or the + functions add_callback and remove_callback can be used. + ''' + def __init__(self, *args, **kwargs): + TimerBase.__init__(self, *args, **kwargs) + + # Create a new timer and connect the timeout() signal to the + # _on_timer method. + self._timer = QtCore.QTimer() + self._timer.timeout.connect(self._on_timer) + self._timer_set_interval() + + def __del__(self): + # Probably not necessary in practice, but is good behavior to + # disconnect + try: + TimerBase.__del__(self) + self._timer.timeout.disconnect(self._on_timer) + except RuntimeError: + # Timer C++ object already deleted + pass + + def _timer_set_single_shot(self): + self._timer.setSingleShot(self._single) + + def _timer_set_interval(self): + self._timer.setInterval(self._interval) + + def _timer_start(self): + self._timer.start() + + def _timer_stop(self): + self._timer.stop() + + +class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase): + + # map Qt button codes to MouseEvent's ones: + buttond = {QtCore.Qt.LeftButton: 1, + QtCore.Qt.MidButton: 2, + QtCore.Qt.RightButton: 3, + # QtCore.Qt.XButton1: None, + # QtCore.Qt.XButton2: None, + } + + def __init__(self, figure): + 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) + self.figure = figure + self.setMouseTracking(True) + self._idle = True + # hide until we can test and fix + #self.startTimer(backend_IdleEvent.milliseconds) + w, h = self.get_width_height() + self.resize(w, h) + + def __timerEvent(self, event): + # hide until we can test and fix + self.mpl_idle_event(event) + + def enterEvent(self, event): + FigureCanvasBase.enter_notify_event(self, event) + + def leaveEvent(self, event): + QtWidgets.QApplication.restoreOverrideCursor() + FigureCanvasBase.leave_notify_event(self, event) + + def mousePressEvent(self, event): + x = event.pos().x() + # flipy so y=0 is bottom of canvas + y = self.figure.bbox.height - event.pos().y() + button = self.buttond.get(event.button()) + if button is not None: + FigureCanvasBase.button_press_event(self, x, y, button) + if DEBUG: + print('button pressed:', event.button()) + + def mouseDoubleClickEvent(self, event): + x = event.pos().x() + # flipy so y=0 is bottom of canvas + y = self.figure.bbox.height - event.pos().y() + button = self.buttond.get(event.button()) + if button is not None: + FigureCanvasBase.button_press_event(self, x, y, + button, dblclick=True) + if DEBUG: + print('button doubleclicked:', event.button()) + + def mouseMoveEvent(self, event): + x = event.x() + # flipy so y=0 is bottom of canvas + y = self.figure.bbox.height - event.y() + FigureCanvasBase.motion_notify_event(self, x, y) + #if DEBUG: print('mouse move') + + def mouseReleaseEvent(self, event): + x = event.x() + # flipy so y=0 is bottom of canvas + y = self.figure.bbox.height - event.y() + button = self.buttond.get(event.button()) + if button is not None: + FigureCanvasBase.button_release_event(self, x, y, button) + if DEBUG: + print('button released') + + def wheelEvent(self, event): + x = event.x() + # flipy so y=0 is bottom of canvas + y = self.figure.bbox.height - event.y() + # from QWheelEvent::delta doc + if event.pixelDelta().x() == 0 and event.pixelDelta().y() == 0: + steps = event.angleDelta().y() / 120 + else: + steps = event.pixelDelta().y() + + if steps != 0: + FigureCanvasBase.scroll_event(self, x, y, steps) + if DEBUG: + print('scroll event: delta = %i, ' + 'steps = %i ' % (event.delta(), steps)) + + def keyPressEvent(self, event): + key = self._get_key(event) + if key is None: + return + FigureCanvasBase.key_press_event(self, key) + if DEBUG: + print('key press', key) + + def keyReleaseEvent(self, event): + key = self._get_key(event) + if key is None: + return + FigureCanvasBase.key_release_event(self, key) + if DEBUG: + print('key release', key) + + def resizeEvent(self, event): + w = event.size().width() + h = event.size().height() + if DEBUG: + print('resize (%d x %d)' % (w, h)) + print("FigureCanvasQt.resizeEvent(%d, %d)" % (w, h)) + dpival = self.figure.dpi + winch = w/dpival + hinch = h/dpival + self.figure.set_size_inches(winch, hinch) + FigureCanvasBase.resize_event(self) + self.draw() + self.update() + QtWidgets.QWidget.resizeEvent(self, event) + + def sizeHint(self): + w, h = self.get_width_height() + return QtCore.QSize(w, h) + + def minumumSizeHint(self): + return QtCore.QSize(10, 10) + + def _get_key(self, event): + if event.isAutoRepeat(): + return None + + event_key = event.key() + event_mods = int(event.modifiers()) # actually a bitmask + + # get names of the pressed modifier keys + # bit twiddling to pick out modifier keys from event_mods bitmask, + # if event_key is a MODIFIER, it should not be duplicated in mods + mods = [name for name, mod_key, qt_key in MODIFIER_KEYS + if event_key != qt_key and (event_mods & mod_key) == mod_key] + try: + # for certain keys (enter, left, backspace, etc) use a word for the + # key, rather than unicode + key = SPECIAL_KEYS[event_key] + except KeyError: + # unicode defines code points up to 0x0010ffff + # QT will use Key_Codes larger than that for keyboard keys that are + # are not unicode characters (like multimedia keys) + # skip these + # if you really want them, you should add them to SPECIAL_KEYS + MAX_UNICODE = 0x10ffff + if event_key > MAX_UNICODE: + return None + + key = unichr(event_key) + # qt delivers capitalized letters. fix capitalization + # note that capslock is ignored + if 'shift' in mods: + mods.remove('shift') + else: + key = key.lower() + + mods.reverse() + return u'+'.join(mods + [key]) + + def new_timer(self, *args, **kwargs): + """ + Creates a new backend-specific subclass of + :class:`backend_bases.Timer`. This is useful for getting + periodic events through the backend's native event + loop. Implemented only for backends with GUIs. + + optional arguments: + + *interval* + Timer interval in milliseconds + + *callbacks* + Sequence of (func, args, kwargs) where func(*args, **kwargs) + will be executed by the timer every *interval*. + + """ + return TimerQT(*args, **kwargs) + + def flush_events(self): + global qApp + qApp.processEvents() + + def start_event_loop(self, timeout): + FigureCanvasBase.start_event_loop_default(self, timeout) + + start_event_loop.__doc__ = \ + FigureCanvasBase.start_event_loop_default.__doc__ + + def stop_event_loop(self): + FigureCanvasBase.stop_event_loop_default(self) + + stop_event_loop.__doc__ = FigureCanvasBase.stop_event_loop_default.__doc__ + + def draw_idle(self): + 'update drawing area only if idle' + d = self._idle + self._idle = False + + def idle_draw(*args): + try: + self.draw() + finally: + self._idle = True + if d: + QtCore.QTimer.singleShot(0, idle_draw) + + +class MainWindow(QtWidgets.QMainWindow): + closing = QtCore.Signal() + + def closeEvent(self, event): + self.closing.emit() + QtWidgets.QMainWindow.closeEvent(self, event) + + +class FigureManagerQT(FigureManagerBase): + """ + Public attributes + + canvas : The FigureCanvas instance + num : The Figure number + toolbar : The qt.QToolBar + window : The qt.QMainWindow + """ + + def __init__(self, canvas, num): + if DEBUG: + print('FigureManagerQT.%s' % fn_name()) + FigureManagerBase.__init__(self, canvas, num) + self.canvas = canvas + self.window = MainWindow() + self.window.closing.connect(canvas.close_event) + self.window.closing.connect(self._widgetclosed) + + self.window.setWindowTitle("Figure %d" % num) + image = os.path.join(matplotlib.rcParams['datapath'], + 'images', 'matplotlib.png') + self.window.setWindowIcon(QtGui.QIcon(image)) + + # 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 + self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus) + self.canvas.setFocus() + + self.window._destroying = False + + self.toolbar = self._get_toolbar(self.canvas, self.window) + if self.toolbar is not None: + self.window.addToolBar(self.toolbar) + self.toolbar.message.connect(self._show_message) + tbs_height = self.toolbar.sizeHint().height() + else: + tbs_height = 0 + + # resize the main window so it will display the canvas with the + # requested size: + cs = canvas.sizeHint() + sbs = self.window.statusBar().sizeHint() + self._status_and_tool_height = tbs_height + sbs.height() + height = cs.height() + self._status_and_tool_height + self.window.resize(cs.width(), height) + + self.window.setCentralWidget(self.canvas) + + if matplotlib.is_interactive(): + self.window.show() + + def notify_axes_change(fig): + # This will be called whenever the current axes is changed + if self.toolbar is not None: + self.toolbar.update() + self.canvas.figure.add_axobserver(notify_axes_change) + + @QtCore.Slot() + def _show_message(self, s): + # Fixes a PySide segfault. + self.window.statusBar().showMessage(s) + + def full_screen_toggle(self): + if self.window.isFullScreen(): + self.window.showNormal() + else: + self.window.showFullScreen() + + def _widgetclosed(self): + if self.window._destroying: + return + self.window._destroying = True + try: + Gcf.destroy(self.num) + except AttributeError: + pass + # It seems that when the python session is killed, + # Gcf can get destroyed before the Gcf.destroy + # line is run, leading to a useless AttributeError. + + def _get_toolbar(self, canvas, parent): + # must be inited after the window, drawingArea and figure + # attrs are set + if matplotlib.rcParams['toolbar'] == 'toolbar2': + toolbar = NavigationToolbar2QT(canvas, parent, False) + else: + toolbar = None + return toolbar + + def resize(self, width, height): + 'set the canvas size in pixels' + self.window.resize(width, height + self._status_and_tool_height) + + def show(self): + self.window.show() + + def destroy(self, *args): + # check for qApp first, as PySide deletes it in its atexit handler + if QtWidgets.QApplication.instance() is None: + return + if self.window._destroying: + return + self.window._destroying = True + self.window.destroyed.connect(self._widgetclosed) + + if self.toolbar: + self.toolbar.destroy() + if DEBUG: + print("destroy figure manager") + self.window.close() + + def get_window_title(self): + return str(self.window.windowTitle()) + + def set_window_title(self, title): + self.window.setWindowTitle(title) + + +class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar): + message = QtCore.Signal(str) + + def __init__(self, canvas, parent, coordinates=True): + """ coordinates: should we show the coordinates on the right? """ + self.canvas = canvas + self.parent = parent + self.coordinates = coordinates + self._actions = {} + """A mapping of toolitem method names to their QActions""" + + QtWidgets.QToolBar.__init__(self, parent) + NavigationToolbar2.__init__(self, canvas) + + def _icon(self, name): + return QtGui.QIcon(os.path.join(self.basedir, name)) + + def _init_toolbar(self): + self.basedir = os.path.join(matplotlib.rcParams['datapath'], 'images') + + for text, tooltip_text, image_file, callback in self.toolitems: + if text is None: + self.addSeparator() + else: + a = self.addAction(self._icon(image_file + '.png'), + text, getattr(self, callback)) + self._actions[callback] = a + if callback in ['zoom', 'pan']: + a.setCheckable(True) + if tooltip_text is not None: + a.setToolTip(tooltip_text) + + if figureoptions is not None: + a = self.addAction(self._icon("qt4_editor_options.png"), + 'Customize', self.edit_parameters) + a.setToolTip('Edit curves line and axes parameters') + + self.buttons = {} + + # Add the x,y location widget at the right side of the toolbar + # The stretch factor is 1 which means any resizing of the toolbar + # will resize this label instead of the buttons. + if self.coordinates: + self.locLabel = QtWidgets.QLabel("", self) + self.locLabel.setAlignment( + QtCore.Qt.AlignRight | QtCore.Qt.AlignTop) + self.locLabel.setSizePolicy( + QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, + QtGui.QSizePolicy.Ignored)) + labelAction = self.addWidget(self.locLabel) + labelAction.setVisible(True) + + # reference holder for subplots_adjust window + self.adj_window = None + + if figureoptions is not None: + def edit_parameters(self): + allaxes = self.canvas.figure.get_axes() + if len(allaxes) == 1: + axes = allaxes[0] + else: + titles = [] + for axes in allaxes: + title = axes.get_title() + ylabel = axes.get_ylabel() + label = axes.get_label() + if title: + fmt = "%(title)s" + if ylabel: + fmt += ": %(ylabel)s" + fmt += " (%(axes_repr)s)" + elif ylabel: + fmt = "%(axes_repr)s (%(ylabel)s)" + elif label: + fmt = "%(axes_repr)s (%(label)s)" + else: + fmt = "%(axes_repr)s" + titles.append(fmt % dict(title=title, + ylabel=ylabel, label=label, + axes_repr=repr(axes))) + item, ok = QtWidgets.QInputDialog.getItem( + self.parent, 'Customize', 'Select axes:', titles, 0, False) + if ok: + axes = allaxes[titles.index(six.text_type(item))] + else: + return + + figureoptions.figure_edit(axes, self) + + def _update_buttons_checked(self): + #sync button checkstates to match active mode + self._actions['pan'].setChecked(self._active == 'PAN') + self._actions['zoom'].setChecked(self._active == 'ZOOM') + + def pan(self, *args): + super(NavigationToolbar2QT, self).pan(*args) + self._update_buttons_checked() + + def zoom(self, *args): + super(NavigationToolbar2QT, self).zoom(*args) + self._update_buttons_checked() + + def dynamic_update(self): + self.canvas.draw() + + def set_message(self, s): + self.message.emit(s) + if self.coordinates: + self.locLabel.setText(s.replace(', ', '\n')) + + def set_cursor(self, cursor): + if DEBUG: + print('Set cursor', cursor) + self.canvas.setCursor(cursord[cursor]) + + def draw_rubberband(self, event, x0, y0, x1, y1): + height = self.canvas.figure.bbox.height + y1 = height - y1 + y0 = height - y0 + + w = abs(x1 - x0) + h = abs(y1 - y0) + + rect = [int(val)for val in (min(x0, x1), min(y0, y1), w, h)] + self.canvas.drawRectangle(rect) + + def configure_subplots(self): + image = os.path.join(matplotlib.rcParams['datapath'], + 'images', 'matplotlib.png') + dia = SubplotToolQt(self.canvas.figure, self.parent) + dia.setWindowIcon(QtGui.QIcon(image)) + dia.exec_() + + def save_figure(self, *args): + filetypes = self.canvas.get_supported_filetypes_grouped() + sorted_filetypes = list(six.iteritems(filetypes)) + sorted_filetypes.sort() + default_filetype = self.canvas.get_default_filetype() + + startpath = matplotlib.rcParams.get('savefig.directory', '') + startpath = os.path.expanduser(startpath) + start = os.path.join(startpath, self.canvas.get_default_filename()) + filters = [] + selectedFilter = None + for name, exts in sorted_filetypes: + exts_list = " ".join(['*.%s' % ext for ext in exts]) + filter = '%s (%s)' % (name, exts_list) + if default_filetype in exts: + selectedFilter = filter + filters.append(filter) + filters = ';;'.join(filters) + + fname = _getSaveFileName(self.parent, "Choose a filename to save to", + start, filters, selectedFilter) + if fname: + if startpath == '': + # explicitly missing key or empty str signals to use cwd + matplotlib.rcParams['savefig.directory'] = startpath + else: + # save dir for next time + savefig_dir = os.path.dirname(six.text_type(fname)) + matplotlib.rcParams['savefig.directory'] = savefig_dir + try: + self.canvas.print_figure(six.text_type(fname)) + except Exception as e: + QtWidgets.QMessageBox.critical( + self, "Error saving file", str(e), + QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.NoButton) + + +class SubplotToolQt(SubplotTool, UiSubplotTool): + def __init__(self, targetfig, parent): + UiSubplotTool.__init__(self, None) + + self.targetfig = targetfig + self.parent = parent + self.donebutton.clicked.connect(self.close) + self.resetbutton.clicked.connect(self.reset) + self.tightlayout.clicked.connect(self.functight) + + # constraints + self.sliderleft.valueChanged.connect(self.sliderright.setMinimum) + self.sliderright.valueChanged.connect(self.sliderleft.setMaximum) + self.sliderbottom.valueChanged.connect(self.slidertop.setMinimum) + self.slidertop.valueChanged.connect(self.sliderbottom.setMaximum) + + self.defaults = {} + for attr in ('left', 'bottom', 'right', 'top', 'wspace', 'hspace',): + self.defaults[attr] = getattr(self.targetfig.subplotpars, attr) + slider = getattr(self, 'slider' + attr) + slider.setMinimum(0) + slider.setMaximum(1000) + slider.setSingleStep(5) + slider.valueChanged.connect(getattr(self, 'func' + attr)) + + self._setSliderPositions() + + def _setSliderPositions(self): + for attr in ('left', 'bottom', 'right', 'top', 'wspace', 'hspace',): + slider = getattr(self, 'slider' + attr) + slider.setSliderPosition(int(self.defaults[attr] * 1000)) + + def funcleft(self, val): + if val == self.sliderright.value(): + val -= 1 + val /= 1000. + self.targetfig.subplots_adjust(left=val) + self.leftvalue.setText("%.2f" % val) + if self.drawon: + self.targetfig.canvas.draw() + + def funcright(self, val): + if val == self.sliderleft.value(): + val += 1 + val /= 1000. + self.targetfig.subplots_adjust(right=val) + self.rightvalue.setText("%.2f" % val) + if self.drawon: + self.targetfig.canvas.draw() + + def funcbottom(self, val): + if val == self.slidertop.value(): + val -= 1 + val /= 1000. + self.targetfig.subplots_adjust(bottom=val) + self.bottomvalue.setText("%.2f" % val) + if self.drawon: + self.targetfig.canvas.draw() + + def functop(self, val): + if val == self.sliderbottom.value(): + val += 1 + val /= 1000. + self.targetfig.subplots_adjust(top=val) + self.topvalue.setText("%.2f" % val) + if self.drawon: + self.targetfig.canvas.draw() + + def funcwspace(self, val): + val /= 1000. + self.targetfig.subplots_adjust(wspace=val) + self.wspacevalue.setText("%.2f" % val) + if self.drawon: + self.targetfig.canvas.draw() + + def funchspace(self, val): + val /= 1000. + self.targetfig.subplots_adjust(hspace=val) + self.hspacevalue.setText("%.2f" % val) + if self.drawon: + self.targetfig.canvas.draw() + + def functight(self): + self.targetfig.tight_layout() + self._setSliderPositions() + self.targetfig.canvas.draw() + + def reset(self): + self.targetfig.subplots_adjust(**self.defaults) + self._setSliderPositions() + self.targetfig.canvas.draw() + + +def error_msg_qt(msg, parent=None): + if not is_string_like(msg): + msg = ','.join(map(str, msg)) + + QtWidgets.QMessageBox.warning(None, "Matplotlib", msg, QtGui.QMessageBox.Ok) + + +def exception_handler(type, value, tb): + """Handle uncaught exceptions + It does not catch SystemExit + """ + msg = '' + # get the filename attribute if available (for IOError) + if hasattr(value, 'filename') and value.filename is not None: + msg = value.filename + ': ' + if hasattr(value, 'strerror') and value.strerror is not None: + msg += value.strerror + else: + msg += str(value) + + if len(msg): + error_msg_qt(msg) + + +FigureCanvas = FigureCanvasQT +FigureManager = FigureManagerQT diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py new file mode 100644 index 000000000000..2079d39b9b0f --- /dev/null +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -0,0 +1,210 @@ +""" +Render to qt from agg +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +import os # not used +import sys +import ctypes +import warnings + +import matplotlib +from matplotlib.figure import Figure + +from .backend_agg import FigureCanvasAgg +from .backend_qt5 import QtCore +from .backend_qt5 import QtGui +from .backend_qt5 import 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 draw_if_interactive +from .backend_qt5 import backend_version +###### + + +from matplotlib.cbook import mplDeprecation + +DEBUG = False + +_decref = ctypes.pythonapi.Py_DecRef +_decref.argtypes = [ctypes.py_object] +_decref.restype = None + +def new_figure_manager(num, *args, **kwargs): + """ + Create a new figure manager instance + """ + if DEBUG: + print('backend_qt5agg.new_figure_manager') + 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 = FigureCanvasQTAgg(figure) + return FigureManagerQT(canvas, num) + +class FigureCanvasQTAggBase(object): + """ + The canvas the figure renders into. Calls the draw and print fig + methods, creates the renderers, etc... + + Public attribute + + figure - A Figure instance + """ + + def drawRectangle(self, rect): + self._drawRect = rect + self.repaint() + + def paintEvent(self, e): + """ + Copy the image from the Agg canvas to the qt.drawable. + In Qt, all drawing should be done inside of here when a widget is + shown onscreen. + """ + + #FigureCanvasQT.paintEvent(self, e) + if DEBUG: + print('FigureCanvasQtAgg.paintEvent: ', self, + self.get_width_height()) + + if self.blitbox is None: + # matplotlib is in rgba byte order. QImage wants to put the bytes + # into argb format and is in a 4 byte unsigned int. Little endian + # system is LSB first and expects the bytes in reverse order + # (bgra). + if QtCore.QSysInfo.ByteOrder == QtCore.QSysInfo.LittleEndian: + stringBuffer = self.renderer._renderer.tostring_bgra() + else: + stringBuffer = self.renderer._renderer.tostring_argb() + + refcnt = sys.getrefcount(stringBuffer) + + # convert the Agg rendered image -> qImage + qImage = QtGui.QImage(stringBuffer, self.renderer.width, + self.renderer.height, + QtGui.QImage.Format_ARGB32) + # get the rectangle for the image + rect = qImage.rect() + p = QtGui.QPainter(self) + # reset the image area of the canvas to be the back-ground color + p.eraseRect(rect) + # draw the rendered image on to the canvas + p.drawPixmap(QtCore.QPoint(0, 0), QtGui.QPixmap.fromImage(qImage)) + + # draw the zoom rectangle to the QPainter + if self._drawRect is not None: + p.setPen(QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.DotLine)) + x, y, w, h = self._drawRect + p.drawRect(x, y, w, h) + p.end() + + # This works around a bug in PySide 1.1.2 on Python 3.x, + # where the reference count of stringBuffer is incremented + # but never decremented by QImage. + # TODO: revert PR #1323 once the issue is fixed in PySide. + del qImage + if refcnt != sys.getrefcount(stringBuffer): + _decref(stringBuffer) + else: + bbox = self.blitbox + l, b, r, t = bbox.extents + w = int(r) - int(l) + h = int(t) - int(b) + t = int(b) + h + reg = self.copy_from_bbox(bbox) + stringBuffer = reg.to_string_argb() + qImage = QtGui.QImage(stringBuffer, w, h, + QtGui.QImage.Format_ARGB32) + pixmap = QtGui.QPixmap.fromImage(qImage) + p = QtGui.QPainter(self) + p.drawPixmap(QtCore.QPoint(l, self.renderer.height-t), pixmap) + p.end() + self.blitbox = None + self._drawRect = None + + def draw(self): + """ + Draw the figure with Agg, and queue a request + for a Qt draw. + """ + # The Agg draw is done here; delaying it until the paintEvent + # causes problems with code that uses the result of the + # draw() to update plot elements. + FigureCanvasAgg.draw(self) + self.update() + + def blit(self, bbox=None): + """ + Blit the region in bbox + """ + self.blitbox = bbox + l, b, w, h = bbox.bounds + t = b + h + self.repaint(l, self.renderer.height-t, w, h) + + def print_figure(self, *args, **kwargs): + FigureCanvasAgg.print_figure(self, *args, **kwargs) + self.draw() + +class FigureCanvasQTAgg(FigureCanvasQTAggBase, FigureCanvasQT, FigureCanvasAgg): + """ + The canvas the figure renders into. Calls the draw and print fig + methods, creates the renderers, etc. + + Modified to import from Qt5 backend for new-style mouse events. + + Public attribute + + figure - A Figure instance + """ + + def __init__(self, figure): + if DEBUG: + print('FigureCanvasQtAgg: ', figure) + FigureCanvasQT.__init__(self, figure) + FigureCanvasAgg.__init__(self, figure) + self._drawRect = None + self.blitbox = None + self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) + # it has been reported that Qt is semi-broken in a windows + # environment. If `self.draw()` uses `update` to trigger a + # system-level window repaint (as is explicitly advised in the + # Qt documentation) the figure responds very slowly to mouse + # input. The work around is to directly use `repaint` + # (against the advice of the Qt documentation). The + # difference between `update` and repaint is that `update` + # schedules a `repaint` for the next time the system is idle, + # where as `repaint` repaints the window immediately. The + # risk is if `self.draw` gets called with in another `repaint` + # method there will be an infinite recursion. Thus, we only + # expose windows users to this risk. + if sys.platform.startswith('win'): + self._priv_update = self.repaint + else: + self._priv_update = self.update + +class NavigationToolbar2QTAgg(NavigationToolbar2QT): + def __init__(*args, **kwargs): + warnings.warn('This class has been deprecated in 1.4 ' + + 'as it has no additional functionality over ' + + '`NavigationToolbar2QT`. Please change your code to ' + 'use `NavigationToolbar2QT` instead', + mplDeprecation) + NavigationToolbar2QT.__init__(*args, **kwargs) + + + +FigureCanvas = FigureCanvasQTAgg +FigureManager = FigureManagerQT diff --git a/lib/matplotlib/backends/qt4_compat.py b/lib/matplotlib/backends/qt_compat.py similarity index 64% rename from lib/matplotlib/backends/qt4_compat.py rename to lib/matplotlib/backends/qt_compat.py index 2ec0819c9ac2..974cf3cf16a7 100644 --- a/lib/matplotlib/backends/qt4_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -12,8 +12,9 @@ QT_API_PYQT = 'PyQt4' # API is not set here; Python 2.x default is V 1 QT_API_PYQTv2 = 'PyQt4v2' # forced to Version 2 API QT_API_PYSIDE = 'PySide' # only supports Version 2 API +QT_API_PYQT5 = 'PyQt5' # use PyQt5 API; Version 2 with module shim -ETS = dict(pyqt=QT_API_PYQTv2, pyside=QT_API_PYSIDE) +ETS = dict(pyqt=QT_API_PYQTv2, pyside=QT_API_PYSIDE, pyqt5=QT_API_PYQT5) # If the ETS QT_API environment variable is set, use it. Note that # ETS requires the version 2 of PyQt4, which is not the platform @@ -26,10 +27,13 @@ except KeyError: raise RuntimeError( 'Unrecognized environment variable %r, valid values are: %r or %r' % - (QT_API_ENV, 'pyqt', 'pyside')) + (QT_API_ENV, 'pyqt', 'pyside', 'pyqt5')) else: # No ETS environment, so use rcParams. - QT_API = rcParams['backend.qt4'] + if rcParams['backend'] == 'Qt5Agg': + QT_API = rcParams['backend.qt5'] + else: + QT_API = rcParams['backend.qt4'] # We will define an appropriate wrapper for the differing versions # of file dialog. @@ -39,7 +43,7 @@ _sip_imported = False # Now perform the imports. -if QT_API in (QT_API_PYQT, QT_API_PYQTv2): +if QT_API in (QT_API_PYQT, QT_API_PYQTv2, QT_API_PYQT5): try: import sip _sip_imported = True @@ -66,9 +70,32 @@ except: res = 'QVariant API v2 specification failed. Defaulting to v1.' verbose.report(cond+res, 'helpful') + + if QT_API in [QT_API_PYQT, QT_API_PYQTv2]: # PyQt4 API - from PyQt4 import QtCore, QtGui + from PyQt4 import QtCore, QtGui + try: + if sip.getapi("QString") > 1: + # Use new getSaveFileNameAndFilter() + _get_save = QtGui.QFileDialog.getSaveFileNameAndFilter + else: + # Use old getSaveFileName() + _getSaveFileName = QtGui.QFileDialog.getSaveFileName + except (AttributeError, KeyError): + # call to getapi() can fail in older versions of sip + _getSaveFileName = QtGui.QFileDialog.getSaveFileName + + + else: # PyQt5 API + + from PyQt5 import QtCore, QtGui, QtWidgets + + # Additional PyQt5 shimming to make it appear as for PyQt4 + + _get_save = QtWidgets.QFileDialog.getSaveFileName + _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName + # Alias PyQt-specific functions for PySide compatibility. QtCore.Signal = QtCore.pyqtSignal try: @@ -79,16 +106,7 @@ QtCore.Property = QtCore.pyqtProperty __version__ = QtCore.PYQT_VERSION_STR - try: - if sip.getapi("QString") > 1: - # Use new getSaveFileNameAndFilter() - _get_save = QtGui.QFileDialog.getSaveFileNameAndFilter - else: - # Use old getSaveFileName() - _getSaveFileName = QtGui.QFileDialog.getSaveFileName - except (AttributeError, KeyError): - # call to getapi() can fail in older versions of sip - _getSaveFileName = QtGui.QFileDialog.getSaveFileName + else: # try importing pyside from PySide import QtCore, QtGui, __version__, __version_info__ @@ -98,8 +116,17 @@ _get_save = QtGui.QFileDialog.getSaveFileName - if _getSaveFileName is None: - def _getSaveFileName(self, msg, start, filters, selectedFilter): return _get_save(self, msg, start, filters, selectedFilter)[0] + +# Apply shim to Qt4 APIs to make them look like Qt5 +if QT_API in (QT_API_PYQT, QT_API_PYQTv2, QT_API_PYSIDE): + ''' + Import all used QtGui objects into QtWidgets + + Here I've opted to simple copy QtGui into QtWidgets as that achieves the same result + as copying over the objects, and will continue to work if other objects are used. + ''' + QtWidgets = QtGui + diff --git a/lib/matplotlib/backends/qt4_editor/__init__.py b/lib/matplotlib/backends/qt_editor/__init__.py similarity index 100% rename from lib/matplotlib/backends/qt4_editor/__init__.py rename to lib/matplotlib/backends/qt_editor/__init__.py diff --git a/lib/matplotlib/backends/qt4_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py similarity index 97% rename from lib/matplotlib/backends/qt4_editor/figureoptions.py rename to lib/matplotlib/backends/qt_editor/figureoptions.py index 2e65e4fdaa7b..76920a7e62b0 100644 --- a/lib/matplotlib/backends/qt4_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -14,8 +14,8 @@ import os.path as osp -import matplotlib.backends.qt4_editor.formlayout as formlayout -from matplotlib.backends.qt4_compat import QtGui +import matplotlib.backends.qt_editor.formlayout as formlayout +from matplotlib.backends.qt_compat import QtGui from matplotlib import markers diff --git a/lib/matplotlib/backends/qt4_editor/formlayout.py b/lib/matplotlib/backends/qt_editor/formlayout.py similarity index 85% rename from lib/matplotlib/backends/qt4_editor/formlayout.py rename to lib/matplotlib/backends/qt_editor/formlayout.py index edf4a368e9d1..78836720b629 100644 --- a/lib/matplotlib/backends/qt4_editor/formlayout.py +++ b/lib/matplotlib/backends/qt_editor/formlayout.py @@ -55,8 +55,8 @@ from matplotlib.colors import rgb2hex from matplotlib.colors import colorConverter -from matplotlib.backends.qt4_compat import QtGui, QtCore -if not hasattr(QtGui, 'QFormLayout'): +from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore +if not hasattr(QtWidgets, 'QFormLayout'): raise ImportError("Warning: formlayout requires PyQt4 >v4.3 or PySide") import datetime @@ -67,21 +67,21 @@ def col2hex(color): return rgb2hex(colorConverter.to_rgb(color)) -class ColorButton(QtGui.QPushButton): +class ColorButton(QtWidgets.QPushButton): """ Color choosing push button """ colorChanged = QtCore.Signal(QtGui.QColor) def __init__(self, parent=None): - QtGui.QPushButton.__init__(self, parent) + QtWidgets.QPushButton.__init__(self, parent) self.setFixedSize(20, 20) self.setIconSize(QtCore.QSize(12, 12)) self.clicked.connect(self.choose_color) self._color = QtGui.QColor() def choose_color(self): - color = QtGui.QColorDialog.getColor(self._color, self.parentWidget(), '') + color = QtWidgets.QColorDialog.getColor(self._color, self.parentWidget(), '') if color.isValid(): self.set_color(color) @@ -116,12 +116,12 @@ def to_qcolor(color): return qcolor # return valid QColor -class ColorLayout(QtGui.QHBoxLayout): +class ColorLayout(QtWidgets.QHBoxLayout): """Color-specialized QLineEdit layout""" def __init__(self, color, parent=None): - QtGui.QHBoxLayout.__init__(self) + QtWidgets.QHBoxLayout.__init__(self) assert isinstance(color, QtGui.QColor) - self.lineedit = QtGui.QLineEdit(color.name(), parent) + self.lineedit = QtWidgets.QLineEdit(color.name(), parent) self.lineedit.editingFinished.connect(self.update_color) self.addWidget(self.lineedit) self.colorbtn = ColorButton(parent) @@ -172,20 +172,20 @@ def qfont_to_tuple(font): font.italic(), font.bold()) -class FontLayout(QtGui.QGridLayout): +class FontLayout(QtWidgets.QGridLayout): """Font selection""" def __init__(self, value, parent=None): - QtGui.QGridLayout.__init__(self) + QtWidgets.QGridLayout.__init__(self) font = tuple_to_qfont(value) assert font is not None # Font family - self.family = QtGui.QFontComboBox(parent) + self.family = QtWidgets.QFontComboBox(parent) self.family.setCurrentFont(font) self.addWidget(self.family, 0, 0, 1, -1) # Font size - self.size = QtGui.QComboBox(parent) + self.size = QtWidgets.QComboBox(parent) self.size.setEditable(True) sizelist = list(xrange(6, 12)) + list(xrange(12, 30, 2)) + [36, 48, 72] size = font.pointSize() @@ -197,12 +197,12 @@ def __init__(self, value, parent=None): self.addWidget(self.size, 1, 0) # Italic or not - self.italic = QtGui.QCheckBox(self.tr("Italic"), parent) + self.italic = QtWidgets.QCheckBox(self.tr("Italic"), parent) self.italic.setChecked(font.italic()) self.addWidget(self.italic, 1, 1) # Bold or not - self.bold = QtGui.QCheckBox(self.tr("Bold"), parent) + self.bold = QtWidgets.QCheckBox(self.tr("Bold"), parent) self.bold.setChecked(font.bold()) self.addWidget(self.bold, 1, 2) @@ -221,17 +221,17 @@ def is_edit_valid(edit): return state == QtGui.QDoubleValidator.Acceptable -class FormWidget(QtGui.QWidget): +class FormWidget(QtWidgets.QWidget): update_buttons = QtCore.Signal() def __init__(self, data, comment="", parent=None): - QtGui.QWidget.__init__(self, parent) + QtWidgets.QWidget.__init__(self, parent) from copy import deepcopy self.data = deepcopy(data) self.widgets = [] - self.formlayout = QtGui.QFormLayout(self) + self.formlayout = QtWidgets.QFormLayout(self) if comment: - self.formlayout.addRow(QtGui.QLabel(comment)) - self.formlayout.addRow(QtGui.QLabel(" ")) + self.formlayout.addRow(QtWidgets.QLabel(comment)) + self.formlayout.addRow(QtWidgets.QLabel(" ")) if DEBUG: print("\n"+("*"*80)) print("DATA:", self.data) @@ -242,7 +242,7 @@ def __init__(self, data, comment="", parent=None): def get_dialog(self): """Return FormDialog instance""" dialog = self.parent() - while not isinstance(dialog, QtGui.QDialog): + while not isinstance(dialog, QtWidgets.QDialog): dialog = dialog.parent() return dialog @@ -252,12 +252,12 @@ def setup(self): print("value:", value) if label is None and value is None: # Separator: (None, None) - self.formlayout.addRow(QtGui.QLabel(" "), QtGui.QLabel(" ")) + self.formlayout.addRow(QtWidgets.QLabel(" "), QtWidgets.QLabel(" ")) self.widgets.append(None) continue elif label is None: # Comment - self.formlayout.addRow(QtGui.QLabel(value)) + self.formlayout.addRow(QtWidgets.QLabel(value)) self.widgets.append(None) continue elif tuple_to_qfont(value) is not None: @@ -265,12 +265,12 @@ def setup(self): elif is_color_like(value): field = ColorLayout(to_qcolor(value), self) elif isinstance(value, six.string_types): - field = QtGui.QLineEdit(value, self) + field = QtWidgets.QLineEdit(value, self) elif isinstance(value, (list, tuple)): if isinstance(value, tuple): value = list(value) selindex = value.pop(0) - field = QtGui.QComboBox(self) + field = QtWidgets.QComboBox(self) if isinstance(value[0], (list, tuple)): keys = [key for key, _val in value] value = [val for _key, val in value] @@ -287,29 +287,29 @@ def setup(self): selindex = 0 field.setCurrentIndex(selindex) elif isinstance(value, bool): - field = QtGui.QCheckBox(self) + field = QtWidgets.QCheckBox(self) if value: field.setCheckState(QtCore.Qt.Checked) else: field.setCheckState(QtCore.Qt.Unchecked) elif isinstance(value, float): - field = QtGui.QLineEdit(repr(value), self) + field = QtWidgets.QLineEdit(repr(value), self) field.setValidator(QtGui.QDoubleValidator(field)) dialog = self.get_dialog() dialog.register_float_field(field) field.textChanged.connect(lambda text: dialog.update_buttons()) elif isinstance(value, int): - field = QtGui.QSpinBox(self) + field = QtWidgets.QSpinBox(self) field.setRange(-1e9, 1e9) field.setValue(value) elif isinstance(value, datetime.datetime): - field = QtGui.QDateTimeEdit(self) + field = QtWidgets.QDateTimeEdit(self) field.setDateTime(value) elif isinstance(value, datetime.date): - field = QtGui.QDateEdit(self) + field = QtWidgets.QDateEdit(self) field.setDate(value) else: - field = QtGui.QLineEdit(repr(value), self) + field = QtWidgets.QLineEdit(repr(value), self) self.formlayout.addRow(label, field) self.widgets.append(field) @@ -346,17 +346,17 @@ def get(self): return valuelist -class FormComboWidget(QtGui.QWidget): +class FormComboWidget(QtWidgets.QWidget): update_buttons = QtCore.Signal() def __init__(self, datalist, comment="", parent=None): - QtGui.QWidget.__init__(self, parent) - layout = QtGui.QVBoxLayout() + QtWidgets.QWidget.__init__(self, parent) + layout = QtWidgets.QVBoxLayout() self.setLayout(layout) - self.combobox = QtGui.QComboBox() + self.combobox = QtWidgets.QComboBox() layout.addWidget(self.combobox) - self.stackwidget = QtGui.QStackedWidget(self) + self.stackwidget = QtWidgets.QStackedWidget(self) layout.addWidget(self.stackwidget) self.combobox.currentIndexChanged.connect(self.stackwidget.setCurrentIndex) @@ -375,13 +375,13 @@ def get(self): return [widget.get() for widget in self.widgetlist] -class FormTabWidget(QtGui.QWidget): +class FormTabWidget(QtWidgets.QWidget): update_buttons = QtCore.Signal() def __init__(self, datalist, comment="", parent=None): - QtGui.QWidget.__init__(self, parent) - layout = QtGui.QVBoxLayout() - self.tabwidget = QtGui.QTabWidget() + QtWidgets.QWidget.__init__(self, parent) + layout = QtWidgets.QVBoxLayout() + self.tabwidget = QtWidgets.QTabWidget() layout.addWidget(self.tabwidget) self.setLayout(layout) self.widgetlist = [] @@ -402,11 +402,11 @@ def get(self): return [widget.get() for widget in self.widgetlist] -class FormDialog(QtGui.QDialog): +class FormDialog(QtWidgets.QDialog): """Form Dialog""" def __init__(self, data, title="", comment="", icon=None, parent=None, apply=None): - QtGui.QDialog.__init__(self, parent) + QtWidgets.QDialog.__init__(self, parent) self.apply_callback = apply @@ -420,18 +420,18 @@ def __init__(self, data, title="", comment="", else: self.formwidget = FormWidget(data, comment=comment, parent=self) - layout = QtGui.QVBoxLayout() + layout = QtWidgets.QVBoxLayout() layout.addWidget(self.formwidget) self.float_fields = [] self.formwidget.setup() # Button box - self.bbox = bbox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok - | QtGui.QDialogButtonBox.Cancel) + self.bbox = bbox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok + | QtWidgets.QDialogButtonBox.Cancel) self.formwidget.update_buttons.connect(self.update_buttons) if self.apply_callback is not None: - apply_btn = bbox.addButton(QtGui.QDialogButtonBox.Apply) + apply_btn = bbox.addButton(QtWidgets.QDialogButtonBox.Apply) apply_btn.clicked.connect(self.apply) bbox.accepted.connect(self.accept) @@ -442,7 +442,7 @@ def __init__(self, data, title="", comment="", self.setWindowTitle(title) if not isinstance(icon, QtGui.QIcon): - icon = QtGui.QWidget().style().standardIcon(QtGui.QStyle.SP_MessageBoxQuestion) + icon = QtWidgets.QWidget().style().standardIcon(QtWidgets.QStyle.SP_MessageBoxQuestion) self.setWindowIcon(icon) def register_float_field(self, field): @@ -453,18 +453,18 @@ def update_buttons(self): for field in self.float_fields: if not is_edit_valid(field): valid = False - for btn_type in (QtGui.QDialogButtonBox.Ok, QtGui.QDialogButtonBox.Apply): + for btn_type in (QtWidgets.QDialogButtonBox.Ok, QtWidgets.QDialogButtonBox.Apply): btn = self.bbox.button(btn_type) if btn is not None: btn.setEnabled(valid) def accept(self): self.data = self.formwidget.get() - QtGui.QDialog.accept(self) + QtWidgets.QDialog.accept(self) def reject(self): self.data = None - QtGui.QDialog.reject(self) + QtWidgets.QDialog.reject(self) def apply(self): self.apply_callback(self.formwidget.get()) @@ -505,8 +505,8 @@ def fedit(data, title="", comment="", icon=None, parent=None, apply=None): # Create a QApplication instance if no instance currently exists # (e.g., if the module is used directly from the interpreter) - if QtGui.QApplication.startingUp(): - _app = QtGui.QApplication([]) + if QtWidgets.QApplication.startingUp(): + _app = QtWidgets.QApplication([]) dialog = FormDialog(data, title, comment, icon, parent, apply) if dialog.exec_(): return dialog.get() diff --git a/lib/matplotlib/backends/qt4_editor/formsubplottool.py b/lib/matplotlib/backends/qt_editor/formsubplottool.py similarity index 73% rename from lib/matplotlib/backends/qt4_editor/formsubplottool.py rename to lib/matplotlib/backends/qt_editor/formsubplottool.py index c0c61025dca1..ef434da39714 100644 --- a/lib/matplotlib/backends/qt4_editor/formsubplottool.py +++ b/lib/matplotlib/backends/qt_editor/formsubplottool.py @@ -8,44 +8,43 @@ __author__ = 'rudolf.hoefler@gmail.com' -from matplotlib.backends.qt4_compat import QtCore, QtGui +from matplotlib.backends.qt_compat import QtCore, QtGui, QtWidgets - -class UiSubplotTool(QtGui.QDialog): +class UiSubplotTool(QtWidgets.QDialog): def __init__(self, *args, **kwargs): super(UiSubplotTool, self).__init__(*args, **kwargs) self.setObjectName('SubplotTool') self.resize(450, 265) - gbox = QtGui.QGridLayout(self) + gbox = QtWidgets.QGridLayout(self) self.setLayout(gbox) # groupbox borders - groupbox = QtGui.QGroupBox('Borders', self) + groupbox = QtWidgets.QGroupBox('Borders', self) gbox.addWidget(groupbox, 6, 0, 1, 1) - self.verticalLayout = QtGui.QVBoxLayout(groupbox) + self.verticalLayout = QtWidgets.QVBoxLayout(groupbox) self.verticalLayout.setSpacing(0) # slider top - self.hboxtop = QtGui.QHBoxLayout() - self.labeltop = QtGui.QLabel('top', self) + self.hboxtop = QtWidgets.QHBoxLayout() + self.labeltop = QtWidgets.QLabel('top', self) self.labeltop.setMinimumSize(QtCore.QSize(50, 0)) self.labeltop.setAlignment( QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) - self.slidertop = QtGui.QSlider(self) + self.slidertop = QtWidgets.QSlider(self) self.slidertop.setMouseTracking(False) self.slidertop.setProperty("value", 0) self.slidertop.setOrientation(QtCore.Qt.Horizontal) self.slidertop.setInvertedAppearance(False) self.slidertop.setInvertedControls(False) - self.slidertop.setTickPosition(QtGui.QSlider.TicksAbove) + self.slidertop.setTickPosition(QtWidgets.QSlider.TicksAbove) self.slidertop.setTickInterval(100) - self.topvalue = QtGui.QLabel('0', self) + self.topvalue = QtWidgets.QLabel('0', self) self.topvalue.setMinimumSize(QtCore.QSize(30, 0)) self.topvalue.setAlignment( QtCore.Qt.AlignRight | @@ -58,24 +57,24 @@ def __init__(self, *args, **kwargs): self.hboxtop.addWidget(self.topvalue) # slider bottom - hboxbottom = QtGui.QHBoxLayout() - labelbottom = QtGui.QLabel('bottom', self) + hboxbottom = QtWidgets.QHBoxLayout() + labelbottom = QtWidgets.QLabel('bottom', self) labelbottom.setMinimumSize(QtCore.QSize(50, 0)) labelbottom.setAlignment( QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) - self.sliderbottom = QtGui.QSlider(self) + self.sliderbottom = QtWidgets.QSlider(self) self.sliderbottom.setMouseTracking(False) self.sliderbottom.setProperty("value", 0) self.sliderbottom.setOrientation(QtCore.Qt.Horizontal) self.sliderbottom.setInvertedAppearance(False) self.sliderbottom.setInvertedControls(False) - self.sliderbottom.setTickPosition(QtGui.QSlider.TicksAbove) + self.sliderbottom.setTickPosition(QtWidgets.QSlider.TicksAbove) self.sliderbottom.setTickInterval(100) - self.bottomvalue = QtGui.QLabel('0', self) + self.bottomvalue = QtWidgets.QLabel('0', self) self.bottomvalue.setMinimumSize(QtCore.QSize(30, 0)) self.bottomvalue.setAlignment( QtCore.Qt.AlignRight | @@ -88,24 +87,24 @@ def __init__(self, *args, **kwargs): hboxbottom.addWidget(self.bottomvalue) # slider left - hboxleft = QtGui.QHBoxLayout() - labelleft = QtGui.QLabel('left', self) + hboxleft = QtWidgets.QHBoxLayout() + labelleft = QtWidgets.QLabel('left', self) labelleft.setMinimumSize(QtCore.QSize(50, 0)) labelleft.setAlignment( QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) - self.sliderleft = QtGui.QSlider(self) + self.sliderleft = QtWidgets.QSlider(self) self.sliderleft.setMouseTracking(False) self.sliderleft.setProperty("value", 0) self.sliderleft.setOrientation(QtCore.Qt.Horizontal) self.sliderleft.setInvertedAppearance(False) self.sliderleft.setInvertedControls(False) - self.sliderleft.setTickPosition(QtGui.QSlider.TicksAbove) + self.sliderleft.setTickPosition(QtWidgets.QSlider.TicksAbove) self.sliderleft.setTickInterval(100) - self.leftvalue = QtGui.QLabel('0', self) + self.leftvalue = QtWidgets.QLabel('0', self) self.leftvalue.setMinimumSize(QtCore.QSize(30, 0)) self.leftvalue.setAlignment( QtCore.Qt.AlignRight | @@ -118,24 +117,24 @@ def __init__(self, *args, **kwargs): hboxleft.addWidget(self.leftvalue) # slider right - hboxright = QtGui.QHBoxLayout() - self.labelright = QtGui.QLabel('right', self) + hboxright = QtWidgets.QHBoxLayout() + self.labelright = QtWidgets.QLabel('right', self) self.labelright.setMinimumSize(QtCore.QSize(50, 0)) self.labelright.setAlignment( QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) - self.sliderright = QtGui.QSlider(self) + self.sliderright = QtWidgets.QSlider(self) self.sliderright.setMouseTracking(False) self.sliderright.setProperty("value", 0) self.sliderright.setOrientation(QtCore.Qt.Horizontal) self.sliderright.setInvertedAppearance(False) self.sliderright.setInvertedControls(False) - self.sliderright.setTickPosition(QtGui.QSlider.TicksAbove) + self.sliderright.setTickPosition(QtWidgets.QSlider.TicksAbove) self.sliderright.setTickInterval(100) - self.rightvalue = QtGui.QLabel('0', self) + self.rightvalue = QtWidgets.QLabel('0', self) self.rightvalue.setMinimumSize(QtCore.QSize(30, 0)) self.rightvalue.setAlignment( QtCore.Qt.AlignRight | @@ -148,13 +147,13 @@ def __init__(self, *args, **kwargs): hboxright.addWidget(self.rightvalue) # groupbox spacings - groupbox = QtGui.QGroupBox('Spacings', self) + groupbox = QtWidgets.QGroupBox('Spacings', self) gbox.addWidget(groupbox, 7, 0, 1, 1) - self.verticalLayout = QtGui.QVBoxLayout(groupbox) + self.verticalLayout = QtWidgets.QVBoxLayout(groupbox) self.verticalLayout.setSpacing(0) # slider hspace - hboxhspace = QtGui.QHBoxLayout() + hboxhspace = QtWidgets.QHBoxLayout() self.labelhspace = QtGui.QLabel('hspace', self) self.labelhspace.setMinimumSize(QtCore.QSize(50, 0)) self.labelhspace.setAlignment( @@ -162,16 +161,16 @@ def __init__(self, *args, **kwargs): QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) - self.sliderhspace = QtGui.QSlider(self) + self.sliderhspace = QtWidgets.QSlider(self) self.sliderhspace.setMouseTracking(False) self.sliderhspace.setProperty("value", 0) self.sliderhspace.setOrientation(QtCore.Qt.Horizontal) self.sliderhspace.setInvertedAppearance(False) self.sliderhspace.setInvertedControls(False) - self.sliderhspace.setTickPosition(QtGui.QSlider.TicksAbove) + self.sliderhspace.setTickPosition(QtWidgets.QSlider.TicksAbove) self.sliderhspace.setTickInterval(100) - self.hspacevalue = QtGui.QLabel('0', self) + self.hspacevalue = QtWidgets.QLabel('0', self) self.hspacevalue.setMinimumSize(QtCore.QSize(30, 0)) self.hspacevalue.setAlignment( QtCore.Qt.AlignRight | @@ -184,24 +183,24 @@ def __init__(self, *args, **kwargs): hboxhspace.addWidget(self.hspacevalue) # slider hspace # slider wspace - hboxwspace = QtGui.QHBoxLayout() - self.labelwspace = QtGui.QLabel('wspace', self) + hboxwspace = QtWidgets.QHBoxLayout() + self.labelwspace = QtWidgets.QLabel('wspace', self) self.labelwspace.setMinimumSize(QtCore.QSize(50, 0)) self.labelwspace.setAlignment( QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) - self.sliderwspace = QtGui.QSlider(self) + self.sliderwspace = QtWidgets.QSlider(self) self.sliderwspace.setMouseTracking(False) self.sliderwspace.setProperty("value", 0) self.sliderwspace.setOrientation(QtCore.Qt.Horizontal) self.sliderwspace.setInvertedAppearance(False) self.sliderwspace.setInvertedControls(False) - self.sliderwspace.setTickPosition(QtGui.QSlider.TicksAbove) + self.sliderwspace.setTickPosition(QtWidgets.QSlider.TicksAbove) self.sliderwspace.setTickInterval(100) - self.wspacevalue = QtGui.QLabel('0', self) + self.wspacevalue = QtWidgets.QLabel('0', self) self.wspacevalue.setMinimumSize(QtCore.QSize(30, 0)) self.wspacevalue.setAlignment( QtCore.Qt.AlignRight | @@ -214,14 +213,14 @@ def __init__(self, *args, **kwargs): hboxwspace.addWidget(self.wspacevalue) # button bar - hbox2 = QtGui.QHBoxLayout() + hbox2 = QtWidgets.QHBoxLayout() gbox.addLayout(hbox2, 8, 0, 1, 1) - self.tightlayout = QtGui.QPushButton('Tight Layout', self) - spacer = QtGui.QSpacerItem( - 5, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.resetbutton = QtGui.QPushButton('Reset', self) - self.donebutton = QtGui.QPushButton('Close', self) - self.donebutton.setFocus(True) + self.tightlayout = QtWidgets.QPushButton('Tight Layout', self) + spacer = QtWidgets.QSpacerItem( + 5, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.resetbutton = QtWidgets.QPushButton('Reset', self) + self.donebutton = QtWidgets.QPushButton('Close', self) + self.donebutton.setFocus() hbox2.addWidget(self.tightlayout) hbox2.addItem(spacer) hbox2.addWidget(self.resetbutton) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index da553fe212c6..4c6618587361 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -86,6 +86,11 @@ def _backend_selection(): if not PyQt4.QtGui.qApp.startingUp(): # The mainloop is running. rcParams['backend'] = 'qt4Agg' + elif 'PyQt5.QtCore' in sys.modules and not backend == 'Qt5Agg': + import PyQt5.QtGui + if not PyQt5.QtGui.qApp.startingUp(): + # The mainloop is running. + rcParams['backend'] = 'qt5Agg' elif 'gtk' in sys.modules and not backend in ('GTK', 'GTKAgg', 'GTKCairo'): import gobject diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index bb228d7d2de3..7932784e5329 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -29,7 +29,7 @@ # change for later versions. interactive_bk = ['GTK', 'GTKAgg', 'GTKCairo', 'MacOSX', - 'Qt4Agg', 'TkAgg', 'WX', 'WXAgg', 'CocoaAgg', + 'Qt4Agg', 'Qt5Agg', 'TkAgg', 'WX', 'WXAgg', 'CocoaAgg', 'GTK3Cairo', 'GTK3Agg', 'WebAgg'] @@ -149,6 +149,7 @@ def validate_backend(s): return _validate_standard_backends(s) validate_qt4 = ValidateInStrings('backend.qt4', ['PyQt4', 'PySide']) +validate_qt5 = ValidateInStrings('backend.qt5', ['PyQt5']) def validate_toolbar(s): @@ -479,6 +480,7 @@ def __call__(self, s): # present 'backend_fallback': [True, validate_bool], # agg is certainly present 'backend.qt4': ['PyQt4', validate_qt4], + 'backend.qt5': ['PyQt5', validate_qt5], 'webagg.port': [8988, validate_int], 'webagg.open_in_browser': [True, validate_bool], 'webagg.port_retries': [50, validate_int], diff --git a/lib/matplotlib/tests/test_backend_qt4.py b/lib/matplotlib/tests/test_backend_qt4.py index 93e2c657a133..711dff8a6a16 100644 --- a/lib/matplotlib/tests/test_backend_qt4.py +++ b/lib/matplotlib/tests/test_backend_qt4.py @@ -16,7 +16,7 @@ import mock try: - from matplotlib.backends.qt4_compat import QtCore + from matplotlib.backends.qt_compat import QtCore from matplotlib.backends.backend_qt4 import (MODIFIER_KEYS, SUPER, ALT, CTRL, SHIFT) diff --git a/lib/matplotlib/tests/test_backend_qt5.py b/lib/matplotlib/tests/test_backend_qt5.py new file mode 100644 index 000000000000..e11debab963d --- /dev/null +++ b/lib/matplotlib/tests/test_backend_qt5.py @@ -0,0 +1,160 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +from matplotlib import pyplot as plt +from matplotlib.testing.decorators import cleanup +from matplotlib.testing.decorators import knownfailureif +from matplotlib._pylab_helpers import Gcf +import copy + +try: + # mock in python 3.3+ + from unittest import mock +except ImportError: + import mock + +try: + from matplotlib.backends.qt_compat import QtCore + from matplotlib.backends.backend_qt5 import (MODIFIER_KEYS, + SUPER, ALT, CTRL, SHIFT) + + _, ControlModifier, ControlKey = MODIFIER_KEYS[CTRL] + _, AltModifier, AltKey = MODIFIER_KEYS[ALT] + _, SuperModifier, SuperKey = MODIFIER_KEYS[SUPER] + _, ShiftModifier, ShiftKey = MODIFIER_KEYS[SHIFT] + HAS_QT = True +except ImportError: + HAS_QT = False + + +@cleanup +@knownfailureif(not HAS_QT) +def test_fig_close(): + # force switch to the Qt4 backend + plt.switch_backend('Qt5Agg') + + #save the state of Gcf.figs + init_figs = copy.copy(Gcf.figs) + + # make a figure using pyplot interface + fig = plt.figure() + + # simulate user clicking the close button by reaching in + # and calling close on the underlying Qt object + fig.canvas.manager.window.close() + + # assert that we have removed the reference to the FigureManager + # that got added by plt.figure() + assert(init_figs == Gcf.figs) + + +def assert_correct_key(qt_key, qt_mods, answer): + """ + Make a figure + Send a key_press_event event (using non-public, qt4 backend specific api) + Catch the event + Assert sent and caught keys are the same + """ + plt.switch_backend('Qt5Agg') + qt_canvas = plt.figure().canvas + + event = mock.Mock() + event.isAutoRepeat.return_value = False + event.key.return_value = qt_key + event.modifiers.return_value = qt_mods + + def receive(event): + assert event.key == answer + + qt_canvas.mpl_connect('key_press_event', receive) + qt_canvas.keyPressEvent(event) + + +@cleanup +@knownfailureif(not HAS_QT) +def test_shift(): + assert_correct_key(QtCore.Qt.Key_A, + ShiftModifier, + 'A') + + +@cleanup +@knownfailureif(not HAS_QT) +def test_lower(): + assert_correct_key(QtCore.Qt.Key_A, + QtCore.Qt.NoModifier, + 'a') + + +@cleanup +@knownfailureif(not HAS_QT) +def test_control(): + assert_correct_key(QtCore.Qt.Key_A, + ControlModifier, + 'ctrl+a') + + +@cleanup +@knownfailureif(not HAS_QT) +def test_unicode_upper(): + assert_correct_key(QtCore.Qt.Key_Aacute, + ShiftModifier, + unichr(193)) + + +@cleanup +@knownfailureif(not HAS_QT) +def test_unicode_lower(): + assert_correct_key(QtCore.Qt.Key_Aacute, + QtCore.Qt.NoModifier, + unichr(225)) + + +@cleanup +@knownfailureif(not HAS_QT) +def test_alt_control(): + assert_correct_key(ControlKey, + AltModifier, + 'alt+control') + + +@cleanup +@knownfailureif(not HAS_QT) +def test_control_alt(): + assert_correct_key(AltKey, + ControlModifier, + 'ctrl+alt') + + +@cleanup +@knownfailureif(not HAS_QT) +def test_modifier_order(): + assert_correct_key(QtCore.Qt.Key_Aacute, + (ControlModifier | AltModifier | SuperModifier), + 'ctrl+alt+super+' + unichr(225)) + + +@cleanup +@knownfailureif(not HAS_QT) +def test_backspace(): + assert_correct_key(QtCore.Qt.Key_Backspace, + QtCore.Qt.NoModifier, + 'backspace') + + +@cleanup +@knownfailureif(not HAS_QT) +def test_backspace_mod(): + assert_correct_key(QtCore.Qt.Key_Backspace, + ControlModifier, + 'ctrl+backspace') + + +@cleanup +@knownfailureif(not HAS_QT) +def test_non_unicode_key(): + assert_correct_key(QtCore.Qt.Key_Play, + QtCore.Qt.NoModifier, + None) diff --git a/lib/matplotlib/tests/test_coding_standards.py b/lib/matplotlib/tests/test_coding_standards.py index 45d1be620b5d..14bc7000d4e8 100644 --- a/lib/matplotlib/tests/test_coding_standards.py +++ b/lib/matplotlib/tests/test_coding_standards.py @@ -134,7 +134,8 @@ '*/matplotlib/backends/qt4_compat.py', '*/matplotlib/backends/tkagg.py', '*/matplotlib/backends/windowing.py', - '*/matplotlib/backends/qt4_editor/formlayout.py', + '*/matplotlib/backends/qt_editor/figureoptions.py', + '*/matplotlib/backends/qt_editor/formlayout.py', '*/matplotlib/sphinxext/ipython_console_highlighting.py', '*/matplotlib/sphinxext/ipython_directive.py', '*/matplotlib/sphinxext/mathmpl.py', diff --git a/setup.py b/setup.py index a14e10a6da92..5b1f587cc7c2 100644 --- a/setup.py +++ b/setup.py @@ -91,6 +91,7 @@ # work will be selected as the default backend. setupext.BackendMacOSX(), setupext.BackendQt4(), + setupext.BackendQt5(), setupext.BackendGtk3Agg(), setupext.BackendGtk3Cairo(), setupext.BackendGtkAgg(), diff --git a/setupext.py b/setupext.py index f99b6f19e383..6d6f09ed2d57 100755 --- a/setupext.py +++ b/setupext.py @@ -545,7 +545,7 @@ def get_packages(self): return [ 'matplotlib', 'matplotlib.backends', - 'matplotlib.backends.qt4_editor', + 'matplotlib.backends.qt_editor', 'matplotlib.compat', 'matplotlib.projections', 'matplotlib.axes', @@ -1880,6 +1880,22 @@ def check_requirements(self): qt_version), pyqt_version_str)) +class BackendQt5(BackendQt4): + name = "qt5agg" + + def check_requirements(self): + try: + from PyQt5 import QtCore + except ImportError: + raise CheckFailed("PyQt5 not found") + # Import may still be broken for our python + try: + qtconfig = QtCore.PYQT_CONFIGURATION + except AttributeError: + raise CheckFailed('PyQt5 not correctly imported') + BackendAgg.force = True + # FIXME: How to return correct version information? + return ("Qt: 5, PyQt5: %s" % (QtCore.PYQT_VERSION_STR) ) class BackendPySide(OptionalBackendPackage): name = "pyside" From c9bf9eca198504cc3841d581f5bed271b39c7fbc Mon Sep 17 00:00:00 2001 From: Martin Fitzpatrick Date: Mon, 19 May 2014 23:49:51 +0100 Subject: [PATCH 2/5] Fix problems identified in pull-request review This commit fixes the following issues identified in comments: - Errant unicode space in backend_qt5agg.py (bad editor) - Update should occur via via self._priv_update in FigureCanvasQTAggBase.draw() In addition I noticed: - A stray QtGui.QLabel in formsubplottool.py --- lib/matplotlib/backends/backend_qt5agg.py | 4 ++-- lib/matplotlib/backends/qt_editor/formsubplottool.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 2079d39b9b0f..c1c2266378b3 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -19,7 +19,7 @@ from .backend_qt5 import QtGui from .backend_qt5 import FigureManagerQT from .backend_qt5 import NavigationToolbar2QT -##### Modified Qt5 backend import +##### Modified Qt5 backend import from .backend_qt5 import FigureCanvasQT ##### not used from .backend_qt5 import show @@ -143,7 +143,7 @@ def draw(self): # causes problems with code that uses the result of the # draw() to update plot elements. FigureCanvasAgg.draw(self) - self.update() + self._priv_update() def blit(self, bbox=None): """ diff --git a/lib/matplotlib/backends/qt_editor/formsubplottool.py b/lib/matplotlib/backends/qt_editor/formsubplottool.py index ef434da39714..096711b06891 100644 --- a/lib/matplotlib/backends/qt_editor/formsubplottool.py +++ b/lib/matplotlib/backends/qt_editor/formsubplottool.py @@ -154,7 +154,7 @@ def __init__(self, *args, **kwargs): # slider hspace hboxhspace = QtWidgets.QHBoxLayout() - self.labelhspace = QtGui.QLabel('hspace', self) + self.labelhspace = QtWidgets.QLabel('hspace', self) self.labelhspace.setMinimumSize(QtCore.QSize(50, 0)) self.labelhspace.setAlignment( QtCore.Qt.AlignRight | From c98406da768ac03d50c8582efe8b21ee1e7e4e5f Mon Sep 17 00:00:00 2001 From: Martin Fitzpatrick Date: Tue, 20 May 2014 17:12:57 +0100 Subject: [PATCH 3/5] Fix setupext.py for the case where both PyQt4 and PyQt5 are installed If both PyQt4 and PyQt5 are installed in the same Python the setup check for the modules will cause a RuntimeError as they are both imported. The Gtk module uses a multiprocessing pool to perform the imports, and this approach is used here. However, if we import PyQt4 normally, then PyQt5 using multiprocessing the import is incomplete (no version information). Instead, we default to multiprocessing for both PyQt4 and PyQt5 and only fall to normal imports if that is not available. In the case of a RuntimeError on a normal-style import the error is raised as `CheckFailed("Could not import: are PyQt4 & PyQt5 both installed?")` to give a hint as to the problem. --- setupext.py | 105 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 30 deletions(-) diff --git a/setupext.py b/setupext.py index 6d6f09ed2d57..dad33905103c 100755 --- a/setupext.py +++ b/setupext.py @@ -1852,8 +1852,7 @@ def get_extension(self): return ext -class BackendQt4(OptionalBackendPackage): - name = "qt4agg" +class BackendQtBase(OptionalBackendPackage): def convert_qt_version(self, version): version = '%x' % version @@ -1864,38 +1863,84 @@ def convert_qt_version(self, version): return '.'.join(temp) def check_requirements(self): + ''' + If PyQt4/PyQt5 is already imported, importing PyQt5/PyQt4 will fail + so we need to test in a subprocess (as for Gtk3). + ''' try: - from PyQt4 import QtCore - except ImportError: - raise CheckFailed("PyQt4 not found") - # Import may still be broken for our python - try: - qt_version = QtCore.QT_VERSION - pyqt_version_str = QtCore.PYQT_VERSION_STR - except AttributeError: - raise CheckFailed('PyQt4 not correctly imported') - BackendAgg.force = True - return ("Qt: %s, PyQt4: %s" % - (self.convert_qt_version( - qt_version), - pyqt_version_str)) + p = multiprocessing.Pool() + + except: + # Can't do multiprocessing, fall back to normal approach ( this will fail if importing both PyQt4 and PyQt5 ) + try: + # Try in-process + msg = self.callback(self) + + except RuntimeError: + raise CheckFailed("Could not import: are PyQt4 & PyQt5 both installed?") + + except: + # Raise any other exceptions + raise + + else: + # Multiprocessing OK + try: + msg = p.map(self.callback, [self])[0] + except: + # If we hit an error on multiprocessing raise it + raise + finally: + # Tidy up multiprocessing + p.close() + p.join() + + return msg + + +def backend_qt4_internal_check(self): + try: + from PyQt4 import QtCore + except ImportError: + raise CheckFailed("PyQt4 not found") + try: + qt_version = QtCore.QT_VERSION + pyqt_version_str = QtCore.QT_VERSION_STR + except AttributeError: + raise CheckFailed('PyQt4 not correctly imported') + + return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) + -class BackendQt5(BackendQt4): +class BackendQt4(BackendQtBase): + name = "qt4agg" + + def __init__(self, *args, **kwargs): + BackendQtBase.__init__(self, *args, **kwargs) + self.callback = backend_qt4_internal_check + + +def backend_qt5_internal_check(self): + try: + from PyQt5 import QtCore + except ImportError: + raise CheckFailed("PyQt5 not found") + try: + qt_version = QtCore.QT_VERSION + pyqt_version_str = QtCore.QT_VERSION_STR + except AttributeError: + raise CheckFailed('PyQt5 not correctly imported') + + return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) + + +class BackendQt5(BackendQtBase): name = "qt5agg" - def check_requirements(self): - try: - from PyQt5 import QtCore - except ImportError: - raise CheckFailed("PyQt5 not found") - # Import may still be broken for our python - try: - qtconfig = QtCore.PYQT_CONFIGURATION - except AttributeError: - raise CheckFailed('PyQt5 not correctly imported') - BackendAgg.force = True - # FIXME: How to return correct version information? - return ("Qt: 5, PyQt5: %s" % (QtCore.PYQT_VERSION_STR) ) + def __init__(self, *args, **kwargs): + BackendQtBase.__init__(self, *args, **kwargs) + self.callback = backend_qt5_internal_check + class BackendPySide(OptionalBackendPackage): name = "pyside" From 170028248b97c0ca8599826feb7fa7cd87765ea4 Mon Sep 17 00:00:00 2001 From: Martin Fitzpatrick Date: Tue, 20 May 2014 17:26:27 +0100 Subject: [PATCH 4/5] BackendPySide using multiprocessing + added to setup.py BackendPySide is now implemented using the same approach as for PyQt4 and PyQt5. The calls in setup.py have been updated to include the 3 Qt interfaces: setupext.BackendQt5(), setupext.BackendQt4(), setupext.BackendPySide(), --- setup.py | 3 ++- setupext.py | 39 ++++++++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/setup.py b/setup.py index 5b1f587cc7c2..970b9927bf5a 100644 --- a/setup.py +++ b/setup.py @@ -90,8 +90,9 @@ # being the most preferred. The first one that looks like it will # work will be selected as the default backend. setupext.BackendMacOSX(), - setupext.BackendQt4(), setupext.BackendQt5(), + setupext.BackendQt4(), + setupext.BackendPySide(), setupext.BackendGtk3Agg(), setupext.BackendGtk3Cairo(), setupext.BackendGtkAgg(), diff --git a/setupext.py b/setupext.py index dad33905103c..d1d6e937282e 100755 --- a/setupext.py +++ b/setupext.py @@ -1903,13 +1903,15 @@ def backend_qt4_internal_check(self): from PyQt4 import QtCore except ImportError: raise CheckFailed("PyQt4 not found") + try: qt_version = QtCore.QT_VERSION pyqt_version_str = QtCore.QT_VERSION_STR except AttributeError: raise CheckFailed('PyQt4 not correctly imported') - - return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) + else: + BackendAgg.force = True + return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) class BackendQt4(BackendQtBase): @@ -1925,13 +1927,15 @@ def backend_qt5_internal_check(self): from PyQt5 import QtCore except ImportError: raise CheckFailed("PyQt5 not found") + try: qt_version = QtCore.QT_VERSION pyqt_version_str = QtCore.QT_VERSION_STR except AttributeError: raise CheckFailed('PyQt5 not correctly imported') - - return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) + else: + BackendAgg.force = True + return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) class BackendQt5(BackendQtBase): @@ -1942,20 +1946,25 @@ def __init__(self, *args, **kwargs): self.callback = backend_qt5_internal_check -class BackendPySide(OptionalBackendPackage): +def backend_pyside_internal_check(self): + try: + from PySide import __version__ + from PySide import QtCore + except ImportError: + raise CheckFailed("PySide not found") + else: + BackendAgg.force = True + return ("Qt: %s, PySide: %s" % + (QtCore.__version__, __version__)) + + +class BackendPySide(BackendQtBase): name = "pyside" - def check_requirements(self): - try: - from PySide import __version__ - from PySide import QtCore - except ImportError: - raise CheckFailed("PySide not found") - else: - BackendAgg.force = True + def __init__(self, *args, **kwargs): + BackendQtBase.__init__(self, *args, **kwargs) + self.callback = backend_pyside_internal_check - return ("Qt: %s, PySide: %s" % - (QtCore.__version__, __version__)) class BackendCairo(OptionalBackendPackage): From 3779fab61ea0d30b3fe953bc6e187b20691e9060 Mon Sep 17 00:00:00 2001 From: Martin Fitzpatrick Date: Mon, 23 Jun 2014 19:41:17 +0100 Subject: [PATCH 5/5] Fix error on pyplot initialisation in IPython There was a bug on the pyplot initialisation that was wrongly looking for qApp in PyQt5.QtGui rather than PyQt5.QtWidgets. I've updated the init to do this correctly, which hopefully fixes the bug reported by @tacaswell --- lib/matplotlib/pyplot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 4c6618587361..f03827c2aa24 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -87,8 +87,8 @@ def _backend_selection(): # The mainloop is running. rcParams['backend'] = 'qt4Agg' elif 'PyQt5.QtCore' in sys.modules and not backend == 'Qt5Agg': - import PyQt5.QtGui - if not PyQt5.QtGui.qApp.startingUp(): + import PyQt5.QtWidgets + if not PyQt5.QtWidgets.qApp.startingUp(): # The mainloop is running. rcParams['backend'] = 'qt5Agg' elif 'gtk' in sys.modules and not backend in ('GTK', 'GTKAgg',