From a3b049f3427dee9f9d51cdf2a43b1a4d378bc6e6 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Mon, 4 Dec 2017 19:32:53 -0500 Subject: [PATCH 1/3] MEP22 implementation for QT backend New Toolbar and QT specific tools --- doc/users/next_whats_new/qt-toolmanager.rst | 17 ++ .../user_interfaces/toolmanager_sgskip.py | 4 +- lib/matplotlib/backends/backend_qt5.py | 177 +++++++++++++++++- 3 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 doc/users/next_whats_new/qt-toolmanager.rst diff --git a/doc/users/next_whats_new/qt-toolmanager.rst b/doc/users/next_whats_new/qt-toolmanager.rst new file mode 100644 index 000000000000..2304b3f8efef --- /dev/null +++ b/doc/users/next_whats_new/qt-toolmanager.rst @@ -0,0 +1,17 @@ +Added support for QT in new ToolManager +======================================= + +Now it is possible to use the ToolManager with Qt5 +For example + + import matplotlib + + matplotlib.use('QT5AGG') + matplotlib.rcParams['toolbar'] = 'toolmanager' + import matplotlib.pyplot as plt + + plt.plot([1,2,3]) + plt.show() + +The main example `examples/user_interfaces/toolmanager_sgskip.py` shows more +details, just adjust the header to use QT instead of GTK3 diff --git a/examples/user_interfaces/toolmanager_sgskip.py b/examples/user_interfaces/toolmanager_sgskip.py index 0c77fe55e699..247997f6e2e2 100644 --- a/examples/user_interfaces/toolmanager_sgskip.py +++ b/examples/user_interfaces/toolmanager_sgskip.py @@ -16,11 +16,13 @@ from __future__ import print_function import matplotlib +# Change to the desired backend matplotlib.use('GTK3Cairo') +# matplotlib.use('TkAgg') +# matplotlib.use('QT5Agg') matplotlib.rcParams['toolbar'] = 'toolmanager' import matplotlib.pyplot as plt from matplotlib.backend_tools import ToolBase, ToolToggleBase -from gi.repository import Gtk, Gdk class ListTools(ToolBase): diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 9110cfce440b..6c665836533d 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -14,10 +14,12 @@ from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - TimerBase, cursors) + TimerBase, cursors, ToolContainerBase, StatusbarBase) import matplotlib.backends.qt_editor.figureoptions as figureoptions from matplotlib.backends.qt_editor.formsubplottool import UiSubplotTool from matplotlib.figure import Figure +from matplotlib.backend_managers import ToolManager +from matplotlib import backend_tools from .qt_compat import ( QtCore, QtGui, QtWidgets, _getSaveFileName, is_pyqt5, __version__, QT_API) @@ -489,14 +491,23 @@ def __init__(self, canvas, num): self.window._destroying = False - # add text label to status bar - self.statusbar_label = QtWidgets.QLabel() - self.window.statusBar().addWidget(self.statusbar_label) - + self.toolmanager = self._get_toolmanager() self.toolbar = self._get_toolbar(self.canvas, self.window) + self.statusbar = None + + if self.toolmanager: + backend_tools.add_tools_to_manager(self.toolmanager) + if self.toolbar: + backend_tools.add_tools_to_container(self.toolbar) + self.statusbar = StatusbarQt(self.window, self.toolmanager) + if self.toolbar is not None: self.window.addToolBar(self.toolbar) - self.toolbar.message.connect(self.statusbar_label.setText) + if not self.toolmanager: + # add text label to status bar + statusbar_label = QtWidgets.QLabel() + self.window.statusBar().addWidget(statusbar_label) + self.toolbar.message.connect(statusbar_label.setText) tbs_height = self.toolbar.sizeHint().height() else: tbs_height = 0 @@ -545,10 +556,19 @@ def _get_toolbar(self, canvas, parent): # attrs are set if matplotlib.rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2QT(canvas, parent, False) + elif matplotlib.rcParams['toolbar'] == 'toolmanager': + toolbar = ToolbarQt(self.toolmanager, self.window) else: toolbar = None return toolbar + def _get_toolmanager(self): + if matplotlib.rcParams['toolbar'] == 'toolmanager': + toolmanager = ToolManager(self.canvas.figure) + else: + toolmanager = None + return toolmanager + def resize(self, width, height): 'set the canvas size in pixels' self.window.resize(width, height + self._status_and_tool_height) @@ -818,6 +838,151 @@ def _reset(self): self._widgets[attr].setValue(value) +class ToolbarQt(ToolContainerBase, QtWidgets.QToolBar): + def __init__(self, toolmanager, parent): + ToolContainerBase.__init__(self, toolmanager) + QtWidgets.QToolBar.__init__(self, parent) + self._toolitems = {} + self._groups = {} + self._last = None + + @property + def _icon_extension(self): + if is_pyqt5(): + return '_large.png' + return '.png' + + def add_toolitem( + self, name, group, position, image_file, description, toggle): + + button = QtWidgets.QToolButton(self) + button.setIcon(self._icon(image_file)) + button.setText(name) + if description: + button.setToolTip(description) + + def handler(): + self.trigger_tool(name) + if toggle: + button.setCheckable(True) + button.toggled.connect(handler) + else: + button.clicked.connect(handler) + + self._last = button + self._toolitems.setdefault(name, []) + self._add_to_group(group, name, button, position) + self._toolitems[name].append((button, handler)) + + def _add_to_group(self, group, name, button, position): + gr = self._groups.get(group, []) + if not gr: + sep = self.addSeparator() + gr.append(sep) + before = gr[position] + widget = self.insertWidget(before, button) + gr.insert(position, widget) + self._groups[group] = gr + + def _icon(self, name): + pm = QtGui.QPixmap(name) + if hasattr(pm, 'setDevicePixelRatio'): + pm.setDevicePixelRatio(self.canvas._dpi_ratio) + return QtGui.QIcon(pm) + + def toggle_toolitem(self, name, toggled): + if name not in self._toolitems: + return + for button, handler in self._toolitems[name]: + button.toggled.disconnect(handler) + button.setChecked(toggled) + button.toggled.connect(handler) + + def remove_toolitem(self, name): + for button, handler in self._toolitems[name]: + button.setParent(None) + del self._toolitems[name] + + +class StatusbarQt(StatusbarBase, QtWidgets.QLabel): + def __init__(self, window, *args, **kwargs): + StatusbarBase.__init__(self, *args, **kwargs) + QtWidgets.QLabel.__init__(self) + window.statusBar().addWidget(self) + + def set_message(self, s): + self.setText(s) + + +class ConfigureSubplotsQt(backend_tools.ConfigureSubplotsBase): + def trigger(self, *args): + image = os.path.join(matplotlib.rcParams['datapath'], + 'images', 'matplotlib.png') + parent = self.canvas.manager.window + dia = SubplotToolQt(self.figure, parent) + dia.setWindowIcon(QtGui.QIcon(image)) + dia.exec_() + + +class SaveFigureQt(backend_tools.SaveFigureBase): + def trigger(self, *args): + filetypes = self.canvas.get_supported_filetypes_grouped() + sorted_filetypes = sorted(six.iteritems(filetypes)) + default_filetype = self.canvas.get_default_filetype() + + startpath = os.path.expanduser( + matplotlib.rcParams['savefig.directory']) + 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) + + parent = self.canvas.manager.window + fname, filter = _getSaveFileName(parent, + "Choose a filename to save to", + start, filters, selectedFilter) + if fname: + # Save dir for next time, unless empty str (i.e., use cwd). + if startpath != "": + matplotlib.rcParams['savefig.directory'] = ( + os.path.dirname(six.text_type(fname))) + try: + self.canvas.figure.savefig(six.text_type(fname)) + except Exception as e: + QtWidgets.QMessageBox.critical( + self, "Error saving file", six.text_type(e), + QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.NoButton) + + +class SetCursorQt(backend_tools.SetCursorBase): + def set_cursor(self, cursor): + self.canvas.setCursor(cursord[cursor]) + + +class RubberbandQt(backend_tools.RubberbandBase): + def draw_rubberband(self, x0, y0, x1, y1): + height = self.canvas.figure.bbox.height + y1 = height - y1 + y0 = height - y0 + rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] + self.canvas.drawRectangle(rect) + + def remove_rubberband(self): + self.canvas.drawRectangle(None) + + +backend_tools.ToolSaveFigure = SaveFigureQt +backend_tools.ToolConfigureSubplots = ConfigureSubplotsQt +backend_tools.ToolSetCursor = SetCursorQt +backend_tools.ToolRubberband = RubberbandQt + + def error_msg_qt(msg, parent=None): if not isinstance(msg, six.string_types): msg = ','.join(map(str, msg)) From 958b512aac92ef78c4388fe384b2bf23bb96bc5d Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Tue, 5 Dec 2017 16:23:33 -0500 Subject: [PATCH 2/3] fix qt5 --- lib/matplotlib/backends/backend_qt5.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 6c665836533d..fc577dd71def 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -887,7 +887,7 @@ def _add_to_group(self, group, name, button, position): def _icon(self, name): pm = QtGui.QPixmap(name) if hasattr(pm, 'setDevicePixelRatio'): - pm.setDevicePixelRatio(self.canvas._dpi_ratio) + pm.setDevicePixelRatio(self.toolmanager.canvas._dpi_ratio) return QtGui.QIcon(pm) def toggle_toolitem(self, name, toggled): From 943c40dfc20aa2249428804c94c00caa06b35594 Mon Sep 17 00:00:00 2001 From: Federico Ariza Date: Thu, 1 Feb 2018 08:38:29 -0500 Subject: [PATCH 3/3] warning in whats new --- doc/users/next_whats_new/qt-toolmanager.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/users/next_whats_new/qt-toolmanager.rst b/doc/users/next_whats_new/qt-toolmanager.rst index 2304b3f8efef..db61617dca47 100644 --- a/doc/users/next_whats_new/qt-toolmanager.rst +++ b/doc/users/next_whats_new/qt-toolmanager.rst @@ -13,5 +13,8 @@ For example plt.plot([1,2,3]) plt.show() + +Treat the new Tool classes experimental for now, the API will likely change and perhaps the rcParam as well + The main example `examples/user_interfaces/toolmanager_sgskip.py` shows more details, just adjust the header to use QT instead of GTK3