From 37b588637ee975e1484502501c611593c8d60f97 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Thu, 15 Oct 2026 17:29:40 +1000 Subject: [PATCH] feat(base): set_window_title, set the title of a figure's window if it has one A figure on a headless backend, or one not managed by pyplot, has no window; the function then does nothing and returns False. RTB and MVTB each had their own copy with different guards. Co-Authored-By: Claude Sonnet 5.5 --- spatialmath/base/__init__.py | 2 ++ spatialmath/base/graphics.py | 34 ++++++++++++++++++++++++++++++++++ tests/base/test_graphics.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/spatialmath/base/__init__.py b/spatialmath/base/__init__.py index b01e14ba..88f7bec2 100644 --- a/spatialmath/base/__init__.py +++ b/spatialmath/base/__init__.py @@ -42,6 +42,7 @@ "plot_cone": "graphics", "plot_cuboid": "graphics", "axes_logic": "graphics", + "set_window_title": "graphics", "plotvol2": "graphics", "plotvol3": "graphics", "expand_dims": "graphics", @@ -391,6 +392,7 @@ def __getattr__(name): "plot_cone", "plot_cuboid", "axes_logic", + "set_window_title", "expand_dims", "isnotebook", # spatial.base.numeric diff --git a/spatialmath/base/graphics.py b/spatialmath/base/graphics.py index 40503c41..034fd824 100644 --- a/spatialmath/base/graphics.py +++ b/spatialmath/base/graphics.py @@ -1783,6 +1783,37 @@ def expand_dims(dim: ArrayLike = None, nd: int = 2) -> NDArray: else: raise ValueError("nd is 2 or 3") + def set_window_title(fig: plt.Figure, title: str) -> bool: + """ + Set the title of the window containing a figure, if it has one + + :param fig: the figure + :type fig: Figure + :param title: the window title + :type title: str + :return: True if the title was set + :rtype: bool + + A figure has a window only if it is managed by pyplot and the backend + provides one. A figure on a headless backend such as ``Agg``, or one + created directly with ``matplotlib.figure.Figure``, does not, and then + nothing is done and False is returned. + + Example:: + + >>> from spatialmath.base import plotvol2, set_window_title + >>> ax = plotvol2(5) + >>> set_window_title(ax.figure, "my plot") + + .. note:: The title of the window is not the title of the axes, see + :meth:`matplotlib.axes.Axes.set_title`. + """ + manager = getattr(fig.canvas, "manager", None) + if manager is not None and hasattr(manager, "set_window_title"): + manager.set_window_title(title) + return True + return False + def isnotebook() -> bool: """ Determine if code is being run from a Jupyter notebook @@ -1849,3 +1880,6 @@ def plot_cone(*args, **kwargs) -> None: def plot_cylinder(*args, **kwargs) -> None: raise NotImplementedError("Matplotlib is not installed: pip install matplotlib") + + def set_window_title(*args, **kwargs) -> None: + raise NotImplementedError("Matplotlib is not installed: pip install matplotlib") diff --git a/tests/base/test_graphics.py b/tests/base/test_graphics.py index 552ebdb0..2397ff9c 100644 --- a/tests/base/test_graphics.py +++ b/tests/base/test_graphics.py @@ -9,6 +9,39 @@ # TODO check they actually create artists +class TestWindowTitle(unittest.TestCase): + def teardown_method(self, method): + plt.close("all") + + def test_sets_the_title_of_a_managed_figure(self): + from types import SimpleNamespace + from unittest import mock + + manager = mock.Mock() + fig = SimpleNamespace(canvas=SimpleNamespace(manager=manager)) + self.assertTrue(set_window_title(fig, "a title")) + manager.set_window_title.assert_called_once_with("a title") + + def test_pyplot_figure(self): + # the Agg manager has no window, but does accept a title + fig = plt.figure() + self.assertIsInstance(set_window_title(fig, "a title"), bool) + + def test_figure_without_a_manager(self): + from matplotlib.figure import Figure + + self.assertFalse(set_window_title(Figure(), "a title")) + + def test_manager_without_the_method(self): + from types import SimpleNamespace + + class Manager: + pass + + fig = SimpleNamespace(canvas=SimpleNamespace(manager=Manager())) + self.assertFalse(set_window_title(fig, "a title")) + + class TestGraphics(unittest.TestCase): def teardown_method(self, method): plt.close("all")