From 244a3e0097983ab94bf1be1ece98b6a082374447 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 29 May 2017 10:58:16 -0700 Subject: [PATCH 1/2] Simplify and improve Qt borders/spacing tool. Replaced sliders in Qt borders/spacing tool by spinboxes, which 1. should be easier to set to an exact value, and 2. do not continuously trigger redraws unless the user presses enter or uses the arrows to step the values (the redraws can be quite slow when working with a complex plot). The spinbox step size of 0.005 was chosen for consistency with the earlier choice of 5/1000. Greatly simplified the implementation. Attributes on the SubplotToolQt instance are not kept because it is impossible to keep back-compatibility (the sliders simply don't exist anymore). New attributes are all private; only `.default` (which has the same meaning) is kept as is. Tested with PyQt 4.12, PySide 1.2.4, PyQt 5.8. --- lib/matplotlib/backends/backend_qt4.py | 1 - lib/matplotlib/backends/backend_qt5.py | 126 +++------ .../backends/qt_editor/formsubplottool.py | 266 +++--------------- 3 files changed, 81 insertions(+), 312 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt4.py b/lib/matplotlib/backends/backend_qt4.py index ef591900ebad..a10dd19a1788 100644 --- a/lib/matplotlib/backends/backend_qt4.py +++ b/lib/matplotlib/backends/backend_qt4.py @@ -24,7 +24,6 @@ from matplotlib.widgets import SubplotTool from .qt_compat import QtCore, QtWidgets, _getSaveFileName, __version__ -from matplotlib.backends.qt_editor.formsubplottool import UiSubplotTool from .backend_qt5 import (backend_version, SPECIAL_KEYS, SUPER, ALT, CTRL, SHIFT, MODIFIER_KEYS, fn_name, cursord, diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 109f8233ff6a..25a8770f41b1 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -782,97 +782,47 @@ 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', ): - val = getattr(self.targetfig.subplotpars, attr) - self.defaults[attr] = val - slider = getattr(self, 'slider' + attr) - txt = getattr(self, attr + 'value') - slider.setMinimum(0) - slider.setMaximum(1000) - slider.setSingleStep(5) - # do this before hooking up the callbacks - slider.setSliderPosition(int(val * 1000)) - txt.setText("%.2f" % val) - 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_idle() - - 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_idle() - - 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_idle() - - def functop(self, val): - if val == self.sliderbottom.value(): - val += 1 - val /= 1000. - self.targetfig.subplots_adjust(top=val) - self.topvalue.setText("%.2f" % val) + self._figure = targetfig + + for lower, higher in [("bottom", "top"), ("left", "right")]: + self._widgets[lower].valueChanged.connect( + lambda val: self._widgets[higher].setMinimum(val + .001)) + self._widgets[higher].valueChanged.connect( + lambda val: self._widgets[lower].setMaximum(val - .001)) + + self.defaults = { + attr: getattr(self._figure.subplotpars, attr) + for attr in ["left", "bottom", "right", "top", "wspace", "hspace"]} + # Set values after setting the range callbacks, but before setting up + # the redraw callbacks. + self._reset() + + for attr in self.defaults: + self._widgets[attr].valueChanged.connect(self._on_value_changed) + for action, method in [("Tight Layout", self._tight_layout), + ("Reset", self._reset), + ("Close", self.close)]: + self._widgets[action].clicked.connect(method) + + def _on_value_changed(self): + self._figure.subplots_adjust( + **{attr: self._widgets[attr].value() for attr in self.defaults}) if self.drawon: - self.targetfig.canvas.draw_idle() - - def funcwspace(self, val): - val /= 1000. - self.targetfig.subplots_adjust(wspace=val) - self.wspacevalue.setText("%.2f" % val) - if self.drawon: - self.targetfig.canvas.draw_idle() - - def funchspace(self, val): - val /= 1000. - self.targetfig.subplots_adjust(hspace=val) - self.hspacevalue.setText("%.2f" % val) + self._figure.canvas.draw_idle() + + def _tight_layout(self): + self._figure.tight_layout() + for attr in self.defaults: + widget = self._widgets[attr] + widget.blockSignals(True) + widget.setValue(getattr(self._figure.subplotpars, attr)) + widget.blockSignals(False) if self.drawon: - self.targetfig.canvas.draw_idle() - - def functight(self): - self.targetfig.tight_layout() - self._setSliderPositions() - self.targetfig.canvas.draw_idle() + self._figure.canvas.draw_idle() - def reset(self): - self.targetfig.subplots_adjust(**self.defaults) - self._setSliderPositions() - self.targetfig.canvas.draw_idle() + def _reset(self): + for attr in self.defaults: + self._widgets[attr].setValue(self.defaults[attr]) def error_msg_qt(msg, parent=None): diff --git a/lib/matplotlib/backends/qt_editor/formsubplottool.py b/lib/matplotlib/backends/qt_editor/formsubplottool.py index ee92cae41ec7..30ed91e814a9 100644 --- a/lib/matplotlib/backends/qt_editor/formsubplottool.py +++ b/lib/matplotlib/backends/qt_editor/formsubplottool.py @@ -1,230 +1,50 @@ -# -*- coding: utf-8 -*- -""" -formsubplottool.py - -backend.qt4 (PyQt4|PySide) independent form of the subplot tool. - -""" from matplotlib.backends.qt_compat import QtCore, QtGui, QtWidgets -__author__ = 'rudolf.hoefler@gmail.com' - class UiSubplotTool(QtWidgets.QDialog): def __init__(self, *args, **kwargs): super(UiSubplotTool, self).__init__(*args, **kwargs) - self.setObjectName('SubplotTool') - self.resize(450, 265) - - gbox = QtWidgets.QGridLayout(self) - self.setLayout(gbox) - - # groupbox borders - groupbox = QtWidgets.QGroupBox('Borders', self) - gbox.addWidget(groupbox, 6, 0, 1, 1) - self.verticalLayout = QtWidgets.QVBoxLayout(groupbox) - self.verticalLayout.setSpacing(0) - - # slider top - 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 = 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(QtWidgets.QSlider.TicksAbove) - self.slidertop.setTickInterval(100) - - self.topvalue = QtWidgets.QLabel('0', self) - self.topvalue.setMinimumSize(QtCore.QSize(30, 0)) - self.topvalue.setAlignment( - QtCore.Qt.AlignRight | - QtCore.Qt.AlignTrailing | - QtCore.Qt.AlignVCenter) - - self.verticalLayout.addLayout(self.hboxtop) - self.hboxtop.addWidget(self.labeltop) - self.hboxtop.addWidget(self.slidertop) - self.hboxtop.addWidget(self.topvalue) - - # slider bottom - 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 = 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(QtWidgets.QSlider.TicksAbove) - self.sliderbottom.setTickInterval(100) - - self.bottomvalue = QtWidgets.QLabel('0', self) - self.bottomvalue.setMinimumSize(QtCore.QSize(30, 0)) - self.bottomvalue.setAlignment( - QtCore.Qt.AlignRight | - QtCore.Qt.AlignTrailing | - QtCore.Qt.AlignVCenter) - - self.verticalLayout.addLayout(hboxbottom) - hboxbottom.addWidget(labelbottom) - hboxbottom.addWidget(self.sliderbottom) - hboxbottom.addWidget(self.bottomvalue) - - # slider left - 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 = 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(QtWidgets.QSlider.TicksAbove) - self.sliderleft.setTickInterval(100) - - self.leftvalue = QtWidgets.QLabel('0', self) - self.leftvalue.setMinimumSize(QtCore.QSize(30, 0)) - self.leftvalue.setAlignment( - QtCore.Qt.AlignRight | - QtCore.Qt.AlignTrailing | - QtCore.Qt.AlignVCenter) - - self.verticalLayout.addLayout(hboxleft) - hboxleft.addWidget(labelleft) - hboxleft.addWidget(self.sliderleft) - hboxleft.addWidget(self.leftvalue) - - # slider right - 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 = 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(QtWidgets.QSlider.TicksAbove) - self.sliderright.setTickInterval(100) - - self.rightvalue = QtWidgets.QLabel('0', self) - self.rightvalue.setMinimumSize(QtCore.QSize(30, 0)) - self.rightvalue.setAlignment( - QtCore.Qt.AlignRight | - QtCore.Qt.AlignTrailing | - QtCore.Qt.AlignVCenter) - - self.verticalLayout.addLayout(hboxright) - hboxright.addWidget(self.labelright) - hboxright.addWidget(self.sliderright) - hboxright.addWidget(self.rightvalue) - - # groupbox spacings - groupbox = QtWidgets.QGroupBox('Spacings', self) - gbox.addWidget(groupbox, 7, 0, 1, 1) - self.verticalLayout = QtWidgets.QVBoxLayout(groupbox) - self.verticalLayout.setSpacing(0) - - # slider hspace - hboxhspace = QtWidgets.QHBoxLayout() - self.labelhspace = QtWidgets.QLabel('hspace', self) - self.labelhspace.setMinimumSize(QtCore.QSize(50, 0)) - self.labelhspace.setAlignment( - QtCore.Qt.AlignRight | - QtCore.Qt.AlignTrailing | - QtCore.Qt.AlignVCenter) - - 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(QtWidgets.QSlider.TicksAbove) - self.sliderhspace.setTickInterval(100) - - self.hspacevalue = QtWidgets.QLabel('0', self) - self.hspacevalue.setMinimumSize(QtCore.QSize(30, 0)) - self.hspacevalue.setAlignment( - QtCore.Qt.AlignRight | - QtCore.Qt.AlignTrailing | - QtCore.Qt.AlignVCenter) - - self.verticalLayout.addLayout(hboxhspace) - hboxhspace.addWidget(self.labelhspace) - hboxhspace.addWidget(self.sliderhspace) - hboxhspace.addWidget(self.hspacevalue) # slider hspace - - # slider wspace - 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 = 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(QtWidgets.QSlider.TicksAbove) - self.sliderwspace.setTickInterval(100) - - self.wspacevalue = QtWidgets.QLabel('0', self) - self.wspacevalue.setMinimumSize(QtCore.QSize(30, 0)) - self.wspacevalue.setAlignment( - QtCore.Qt.AlignRight | - QtCore.Qt.AlignTrailing | - QtCore.Qt.AlignVCenter) - - self.verticalLayout.addLayout(hboxwspace) - hboxwspace.addWidget(self.labelwspace) - hboxwspace.addWidget(self.sliderwspace) - hboxwspace.addWidget(self.wspacevalue) - - # button bar - hbox2 = QtWidgets.QHBoxLayout() - gbox.addLayout(hbox2, 8, 0, 1, 1) - 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) - hbox2.addWidget(self.donebutton) - - self.donebutton.clicked.connect(self.accept) + self.setObjectName("SubplotTool") + self._widgets = {} + + layout = QtWidgets.QHBoxLayout() + self.setLayout(layout) + + left = QtWidgets.QVBoxLayout() + layout.addLayout(left) + right = QtWidgets.QVBoxLayout() + layout.addLayout(right) + + box = QtWidgets.QGroupBox("Borders") + left.addWidget(box) + inner = QtWidgets.QFormLayout(box) + for side in ["top", "bottom", "left", "right"]: + self._widgets[side] = widget = QtWidgets.QDoubleSpinBox() + widget.setMinimum(0) + widget.setMaximum(1) + widget.setDecimals(3) + widget.setSingleStep(.005) + widget.setKeyboardTracking(False) + inner.addRow(side, widget) + left.addStretch(1) + + box = QtWidgets.QGroupBox("Spacings") + right.addWidget(box) + inner = QtWidgets.QFormLayout(box) + for side in ["hspace", "wspace"]: + self._widgets[side] = widget = QtWidgets.QDoubleSpinBox() + widget.setMinimum(0) + widget.setMaximum(1) + widget.setDecimals(3) + widget.setSingleStep(.005) + widget.setKeyboardTracking(False) + inner.addRow(side, widget) + + for action in ["Tight Layout", "Reset", "Close"]: + self._widgets[action] = widget = QtWidgets.QPushButton(action) + # Don't trigger on , which is used to input values. + widget.setAutoDefault(False) + right.addWidget(widget) + + self._widgets["Close"].setFocus() From 56e9f1b6346390c924c1dc5a21b87c62655e9751 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 29 May 2017 12:49:22 -0700 Subject: [PATCH 2/2] Various improvements. --- lib/matplotlib/backends/backend_qt5.py | 54 +++++++++++++------ .../backends/qt_editor/formsubplottool.py | 10 +++- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 25a8770f41b1..d23bf13f7d76 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -21,7 +21,6 @@ from matplotlib._pylab_helpers import Gcf from matplotlib.figure import Figure -from matplotlib.widgets import SubplotTool import matplotlib.backends.qt_editor.figureoptions as figureoptions from .qt_compat import (QtCore, QtGui, QtWidgets, _getSaveFileName, @@ -778,7 +777,7 @@ def save_figure(self, *args): QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.NoButton) -class SubplotToolQt(SubplotTool, UiSubplotTool): +class SubplotToolQt(UiSubplotTool): def __init__(self, targetfig, parent): UiSubplotTool.__init__(self, None) @@ -790,39 +789,60 @@ def __init__(self, targetfig, parent): self._widgets[higher].valueChanged.connect( lambda val: self._widgets[lower].setMaximum(val - .001)) - self.defaults = { - attr: getattr(self._figure.subplotpars, attr) - for attr in ["left", "bottom", "right", "top", "wspace", "hspace"]} + self._attrs = ["top", "bottom", "left", "right", "hspace", "wspace"] + self._defaults = {attr: vars(self._figure.subplotpars)[attr] + for attr in self._attrs} + # Set values after setting the range callbacks, but before setting up # the redraw callbacks. self._reset() - for attr in self.defaults: + for attr in self._attrs: self._widgets[attr].valueChanged.connect(self._on_value_changed) - for action, method in [("Tight Layout", self._tight_layout), + for action, method in [("Export values", self._export_values), + ("Tight layout", self._tight_layout), ("Reset", self._reset), ("Close", self.close)]: self._widgets[action].clicked.connect(method) + def _export_values(self): + # Explicitly round to 3 decimals (which is also the spinbox precision) + # to avoid numbers of the form 0.100...001. + dialog = QtWidgets.QDialog() + layout = QtWidgets.QVBoxLayout() + dialog.setLayout(layout) + text = QtWidgets.QPlainTextEdit() + text.setReadOnly(True) + layout.addWidget(text) + text.setPlainText( + ",\n".join("{}={:.3}".format(attr, self._widgets[attr].value()) + for attr in self._attrs)) + # Adjust the height of the text widget to fit the whole text, plus + # some padding. + size = text.maximumSize() + size.setHeight( + QtGui.QFontMetrics(text.document().defaultFont()) + .size(0, text.toPlainText()).height() + 20) + text.setMaximumSize(size) + dialog.exec_() + def _on_value_changed(self): - self._figure.subplots_adjust( - **{attr: self._widgets[attr].value() for attr in self.defaults}) - if self.drawon: - self._figure.canvas.draw_idle() + self._figure.subplots_adjust(**{attr: self._widgets[attr].value() + for attr in self._attrs}) + self._figure.canvas.draw_idle() def _tight_layout(self): self._figure.tight_layout() - for attr in self.defaults: + for attr in self._attrs: widget = self._widgets[attr] widget.blockSignals(True) - widget.setValue(getattr(self._figure.subplotpars, attr)) + widget.setValue(vars(self._figure.subplotpars)[attr]) widget.blockSignals(False) - if self.drawon: - self._figure.canvas.draw_idle() + self._figure.canvas.draw_idle() def _reset(self): - for attr in self.defaults: - self._widgets[attr].setValue(self.defaults[attr]) + for attr, value in self._defaults.items(): + self._widgets[attr].setValue(value) def error_msg_qt(msg, parent=None): diff --git a/lib/matplotlib/backends/qt_editor/formsubplottool.py b/lib/matplotlib/backends/qt_editor/formsubplottool.py index 30ed91e814a9..4bce2824707f 100644 --- a/lib/matplotlib/backends/qt_editor/formsubplottool.py +++ b/lib/matplotlib/backends/qt_editor/formsubplottool.py @@ -40,10 +40,16 @@ def __init__(self, *args, **kwargs): widget.setSingleStep(.005) widget.setKeyboardTracking(False) inner.addRow(side, widget) + right.addStretch(1) - for action in ["Tight Layout", "Reset", "Close"]: + widget = QtWidgets.QPushButton("Export values") + self._widgets["Export values"] = widget + # Don't trigger on , which is used to input values. + widget.setAutoDefault(False) + left.addWidget(widget) + + for action in ["Tight layout", "Reset", "Close"]: self._widgets[action] = widget = QtWidgets.QPushButton(action) - # Don't trigger on , which is used to input values. widget.setAutoDefault(False) right.addWidget(widget)