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"