From 7ae3c1daf3737fe5b33b4fb47889030a7f70ec59 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 19 Nov 2021 20:36:08 +0100 Subject: [PATCH] Bind subplot_tool more closely to target figure. - More the implementation to backend_bases and have pyplot read it, rather than having backend_bases fetch it from pyplot, which was a bit weird. - Attach the subplot_tool to the toolbar, which avoids having to keep a reference to it (when calling plt.subplot_tool), and prevents opening two subplot_tools for the same figure. On most backends, closing the subplot_tool deletes the reference, because reopening a closed figure seems not guaranteed to work; on Qt, which has its own subplot_tool, reopening the closed QDialog does work, so just reuse it. - Ensure that closing the main figure also closes the subplot_tool. --- lib/matplotlib/backend_bases.py | 19 +++++++++++++++++-- lib/matplotlib/backends/backend_qt.py | 23 ++++++++++++++++------- lib/matplotlib/pyplot.py | 24 ++++++++++++------------ 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 24dc65b81d34..7dbca2e64b60 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3258,9 +3258,24 @@ def _update_view(self): self.canvas.draw_idle() def configure_subplots(self, *args): + if hasattr(self, "subplot_tool"): + self.subplot_tool.figure.canvas.manager.show() + return plt = _safe_pyplot_import() - self.subplot_tool = plt.subplot_tool(self.canvas.figure) - self.subplot_tool.figure.canvas.manager.show() + with mpl.rc_context({"toolbar": "none"}): # No navbar for the toolfig. + # Use new_figure_manager() instead of figure() so that the figure + # doesn't get registered with pyplot. + manager = plt.new_figure_manager(-1, (6, 3)) + manager.set_window_title("Subplot configuration tool") + tool_fig = manager.canvas.figure + tool_fig.subplots_adjust(top=0.9) + self.subplot_tool = widgets.SubplotTool(self.canvas.figure, tool_fig) + tool_fig.canvas.mpl_connect( + "close_event", lambda e: delattr(self, "subplot_tool")) + self.canvas.mpl_connect( + "close_event", lambda e: manager.destroy()) + manager.show() + return self.subplot_tool def save_figure(self, *args): """Save the current figure.""" diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index d836ca84c356..3f377929837d 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -750,11 +750,14 @@ def remove_rubberband(self): self.canvas.drawRectangle(None) def configure_subplots(self): - image = str(cbook._get_data_path('images/matplotlib.png')) - self._subplot_dialog = SubplotToolQt( - self.canvas.figure, self.canvas.parent()) - self._subplot_dialog.setWindowIcon(QtGui.QIcon(image)) + if self._subplot_dialog is None: + self._subplot_dialog = SubplotToolQt( + self.canvas.figure, self.canvas.parent()) + self.canvas.mpl_connect( + "close_event", lambda e: self._subplot_dialog.reject()) + self._subplot_dialog.update_from_current_subplotpars() self._subplot_dialog.show() + return self._subplot_dialog def save_figure(self, *args): filetypes = self.canvas.get_supported_filetypes_grouped() @@ -799,6 +802,8 @@ def set_history_buttons(self): class SubplotToolQt(QtWidgets.QDialog): def __init__(self, targetfig, parent): super().__init__() + self.setWindowIcon(QtGui.QIcon( + str(cbook._get_data_path("images/matplotlib.png")))) self.setObjectName("SubplotTool") self._spinboxes = {} main_layout = QtWidgets.QHBoxLayout() @@ -819,7 +824,6 @@ def __init__(self, targetfig, parent): inner = QtWidgets.QFormLayout(box) for name in spinboxes: self._spinboxes[name] = spinbox = QtWidgets.QDoubleSpinBox() - spinbox.setValue(getattr(targetfig.subplotpars, name)) spinbox.setRange(0, 1) spinbox.setDecimals(3) spinbox.setSingleStep(0.005) @@ -836,9 +840,14 @@ def __init__(self, targetfig, parent): if name == "Close": button.setFocus() self._figure = targetfig - self._defaults = {spinbox: vars(self._figure.subplotpars)[attr] - for attr, spinbox in self._spinboxes.items()} + self._defaults = {} self._export_values_dialog = None + self.update_from_current_subplotpars() + + def update_from_current_subplotpars(self): + self._defaults = {spinbox: getattr(self._figure.subplotpars, name) + for name, spinbox in self._spinboxes.items()} + self._reset() # Set spinbox current values without triggering signals. def _export_values(self): # Explicitly round to 3 decimals (which is also the spinbox precision) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 3da54a06a529..f6ea82ba2cca 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -74,7 +74,7 @@ from matplotlib.lines import Line2D from matplotlib.text import Text, Annotation from matplotlib.patches import Polygon, Rectangle, Circle, Arrow -from matplotlib.widgets import SubplotTool, Button, Slider, Widget +from matplotlib.widgets import Button, Slider, Widget from .ticker import ( TickHelper, Formatter, FixedFormatter, NullFormatter, FuncFormatter, @@ -1627,20 +1627,20 @@ def subplot_tool(targetfig=None): """ Launch a subplot tool window for a figure. - A `matplotlib.widgets.SubplotTool` instance is returned. You must maintain - a reference to the instance to keep the associated callbacks alive. + Returns + ------- + `matplotlib.widgets.SubplotTool` """ if targetfig is None: targetfig = gcf() - with rc_context({"toolbar": "none"}): # No navbar for the toolfig. - # Use new_figure_manager() instead of figure() so that the figure - # doesn't get registered with pyplot. - manager = new_figure_manager(-1, (6, 3)) - manager.set_window_title("Subplot configuration tool") - tool_fig = manager.canvas.figure - tool_fig.subplots_adjust(top=0.9) - manager.show() - return SubplotTool(targetfig, tool_fig) + tb = targetfig.canvas.manager.toolbar + if hasattr(tb, "configure_subplots"): # toolbar2 + return tb.configure_subplots() + elif hasattr(tb, "trigger_tool"): # toolmanager + return tb.trigger_tool("subplots") + else: + raise ValueError("subplot_tool can only be launched for figures with " + "an associated toolbar") def box(on=None):