From 75bca6465dce15ad9776333002a77c1a1aaa46a7 Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Sun, 7 Jun 2026 23:14:26 +0200 Subject: [PATCH 1/9] feat: add public raise_window(*, with_focus) to FigureManagerBase Expose a public raise_window() on the base figure manager (no-op default, same pattern as full_screen_toggle/resize) so users can raise a figure window on demand, not only as a side effect of show()/figure.raise_window. Establish a single cross-backend contract: raising a figure window and giving it keyboard focus are independent operations, and raising never steals focus unless with_focus=True is passed. with_focus defaults to False. This makes behavior consistent when porting code across backends. Includes the .pyi stub, a base no-op test, a whats-new note, and an API behavior-change note documenting the (soft, best-effort) focus change. --- .../next_api_changes/behavior/31176-AA.rst | 34 +++++++++++++++++++ doc/release/next_whats_new/raise_window.rst | 20 +++++++++++ lib/matplotlib/backend_bases.py | 25 ++++++++++++++ lib/matplotlib/backend_bases.pyi | 1 + lib/matplotlib/tests/test_backend_bases.py | 10 ++++-- 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/31176-AA.rst create mode 100644 doc/release/next_whats_new/raise_window.rst diff --git a/doc/api/next_api_changes/behavior/31176-AA.rst b/doc/api/next_api_changes/behavior/31176-AA.rst new file mode 100644 index 000000000000..a2449d212f1c --- /dev/null +++ b/doc/api/next_api_changes/behavior/31176-AA.rst @@ -0,0 +1,34 @@ +Raising a figure window no longer steals keyboard focus by default +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Raising a figure window and giving it keyboard focus are now treated as two +independent operations, with a consistent default across all GUI backends: +raising a window no longer transfers keyboard focus unless explicitly +requested. + +Previously this behavior was backend-dependent. Some backends (e.g. Qt and +GTK) activated the window and stole keyboard focus from the active application +when :rc:`figure.raise_window` was set, while others (e.g. macOS) only raised +the window in the stacking order. As a result, the same code could behave +differently after merely switching backends. + +Now, both the new `.FigureManagerBase.raise_window` method and the raise +triggered by `~.pyplot.show` / :rc:`figure.raise_window` raise the window +without stealing focus. To additionally activate the window and give it +keyboard focus, pass ``with_focus=True``:: + + fig.canvas.manager.raise_window(with_focus=True) + +On backends that previously stole focus (e.g. Qt and GTK) this restores the +old behavior; on backends that never did (e.g. macOS) it enables focusing the +window as a new capability. + +This is a soft change: in the worst case a window that used to grab focus no +longer does so, and any code relying on that can opt back in with +``with_focus=True``. + +The behavior is best-effort and platform-dependent. On some toolkits raising +and focusing cannot be fully separated (for example the GTK4 backend, which +can only raise via ``present()`` and may therefore still transfer focus), and +some window managers (notably Wayland compositors) may ignore or modify raise +and focus requests entirely. diff --git a/doc/release/next_whats_new/raise_window.rst b/doc/release/next_whats_new/raise_window.rst new file mode 100644 index 000000000000..ef31f8abbabc --- /dev/null +++ b/doc/release/next_whats_new/raise_window.rst @@ -0,0 +1,20 @@ +Raise a figure window on demand with ``raise_window`` +----------------------------------------------------- + +The figure manager now exposes a public ``raise_window`` method, letting you +bring a figure window to the front at any point in your program rather than +only as a side effect of `~.pyplot.show` and :rc:`figure.raise_window`:: + + fig.canvas.manager.raise_window() + +By default, the window is raised in the stacking order *without* stealing +keyboard focus from the currently active application. This is convenient in +interactive workflows (for example from an IPython session), where you want to +glance at a figure without leaving your terminal. Pass ``with_focus=True`` to +additionally activate the window and give it keyboard focus:: + + fig.canvas.manager.raise_window(with_focus=True) + +Raising and focusing are now treated as independent operations with a +consistent default across the GUI backends, so code that raises a window +behaves the same way regardless of which backend is in use. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 27c3752858a7..2b9341f528ab 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2882,6 +2882,31 @@ def destroy(self): def full_screen_toggle(self): pass + def raise_window(self, *, with_focus=False): + """ + For GUI backends, raise the figure window to the top. + + Parameters + ---------- + with_focus : bool, default: False + Whether to also give the window keyboard focus (activate it). + If False (the default), the window is raised in the stacking + order without stealing keyboard focus from the currently active + application. If True, the window is additionally activated and + given keyboard focus. + + Notes + ----- + Raising and focusing are treated as independent operations, with a + consistent default across backends: raising never steals focus + unless ``with_focus=True`` is passed. + + This is a best-effort operation. Some window managers (notably + Wayland compositors) may ignore or modify raise and focus requests, + so the exact behavior is platform-dependent. + """ + pass + def resize(self, w, h): """For GUI backends, resize the window (in physical pixels).""" diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 94a8522717cd..67aae6c21b1d 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -437,6 +437,7 @@ class FigureManagerBase: def show(self) -> None: ... def destroy(self) -> None: ... def full_screen_toggle(self) -> None: ... + def raise_window(self, *, with_focus: bool = ...) -> None: ... def resize(self, w: int, h: int) -> None: ... def get_window_title(self) -> str: ... def set_window_title(self, title: str) -> None: ... diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index 0205eac42fb3..92ee0f31a632 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -2,8 +2,8 @@ from matplotlib import path, transforms from matplotlib.backend_bases import ( - FigureCanvasBase, KeyEvent, LocationEvent, MouseButton, MouseEvent, - NavigationToolbar2, RendererBase) + FigureCanvasBase, FigureManagerBase, KeyEvent, LocationEvent, MouseButton, + MouseEvent, NavigationToolbar2, RendererBase) from matplotlib.backend_tools import RubberbandBase from matplotlib.figure import Figure from matplotlib.testing._markers import needs_pgf_xelatex @@ -63,6 +63,12 @@ def test_canvas_ctor(): assert isinstance(FigureCanvasBase().figure, Figure) +def test_figure_manager_base_raise_window_noop(): + canvas = FigureCanvasBase(Figure()) + manager = FigureManagerBase(canvas, 1) + assert manager.raise_window() is None + + def test_get_default_filename(): fig = plt.figure() assert fig.canvas.get_default_filename() == "Figure_1.png" From 18b0dbea96acb26c3fe608af84c0ff837b7ee2d5 Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Sun, 7 Jun 2026 23:14:33 +0200 Subject: [PATCH 2/9] feat: add MacOSWindow helper for raising native windows on macOS Add backends/_macos_window.py with MacOSWindow, a small ctypes wrapper around a native NSWindow. The Qt, Tk and wx backends build it from their native handle (from_nsview / from_tk_drawable) and call raise_window(), because on macOS a backgrounded app cannot lift a window above the foreground app, or take focus, through the toolkits' own raise calls. Kept as a class so further NSWindow operations can be added later. --- lib/matplotlib/backends/_macos_window.py | 81 ++++++++++++++++++++++++ lib/matplotlib/backends/meson.build | 1 + 2 files changed, 82 insertions(+) create mode 100644 lib/matplotlib/backends/_macos_window.py diff --git a/lib/matplotlib/backends/_macos_window.py b/lib/matplotlib/backends/_macos_window.py new file mode 100644 index 000000000000..a9e0d410c311 --- /dev/null +++ b/lib/matplotlib/backends/_macos_window.py @@ -0,0 +1,81 @@ +""" +A wrapper around a native macOS NSWindow, for use by the GUI backends. + +On macOS a backgrounded application cannot lift a window above the foreground +application, or take keyboard focus, using a toolkit's normal raise call. The +native NSWindow methods do work. The Qt, Tk and wx backends obtain their +NSWindow in different ways (see the constructors below) and then share the +operations on `MacOSWindow`. Keeping it a class makes it straightforward to add +further NSWindow operations later (e.g. window level or frame queries). +""" +import ctypes +import ctypes.util +import functools + + +@functools.cache +def _objc(): + objc = ctypes.CDLL(ctypes.util.find_library("objc")) + objc.sel_registerName.restype = ctypes.c_void_p + objc.sel_registerName.argtypes = [ctypes.c_char_p] + objc.objc_getClass.restype = ctypes.c_void_p + objc.objc_getClass.argtypes = [ctypes.c_char_p] + return objc + + +def _msg(receiver, selector, *args, argtypes=()): + # Send an Objective-C message. objc_msgSend is variadic, so a prototype is + # built per call signature; restype/argtypes MUST be pointer-width or 64-bit + # object pointers are truncated and crash on arm64. + objc = _objc() + send = ctypes.CFUNCTYPE( + ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, *argtypes)( + ("objc_msgSend", objc)) + return send(ctypes.c_void_p(receiver), objc.sel_registerName(selector), *args) + + +class MacOSWindow: + """A native macOS NSWindow, wrapped for use by the GUI backends.""" + + def __init__(self, nswindow): + # *nswindow* is the NSWindow pointer (int), or None/0 if unavailable. + self._nswindow = nswindow or None + + @classmethod + def from_nsview(cls, nsview): + """Build from an NSView pointer (Qt's ``winId()``, wx's ``GetHandle()``).""" + nswindow = _msg(int(nsview), b"window") if nsview else None + return cls(nswindow) + + @classmethod + def from_tk_drawable(cls, drawable): + """Build from a Tk drawable (a window's ``winfo_id()``).""" + try: + get = ctypes.CDLL(None).Tk_MacOSXGetNSWindowForDrawable + except (OSError, AttributeError): + return cls(None) + get.restype = ctypes.c_void_p + get.argtypes = [ctypes.c_void_p] + return cls(get(ctypes.c_void_p(int(drawable)))) + + def raise_window(self, *, with_focus): + """ + Raise the window to the front. + + If *with_focus*, also bring the application forward and give the window + keyboard focus; otherwise raise it without taking focus. + """ + if not self._nswindow: + return + if with_focus: + # activateIgnoringOtherApps: is deprecated but used deliberately: + # the modern -activate is cooperative and will not foreground a + # background app when called programmatically (see backend_macosx). + nsapp = _msg(_objc().objc_getClass(b"NSApplication"), + b"sharedApplication") + _msg(nsapp, b"activateIgnoringOtherApps:", True, + argtypes=(ctypes.c_bool,)) + _msg(self._nswindow, b"makeKeyAndOrderFront:", None, + argtypes=(ctypes.c_void_p,)) + else: + _msg(self._nswindow, b"orderFrontRegardless") diff --git a/lib/matplotlib/backends/meson.build b/lib/matplotlib/backends/meson.build index 1e3e47c0a915..6b25ba079e66 100644 --- a/lib/matplotlib/backends/meson.build +++ b/lib/matplotlib/backends/meson.build @@ -10,6 +10,7 @@ python_sources = [ 'backend_gtk4agg.py', 'backend_gtk4cairo.py', 'backend_macosx.py', + '_macos_window.py', 'backend_mixed.py', 'backend_nbagg.py', '_backend_pdf_ps.py', From 4d60de1c6a70e940e351945ceec61b0be4ace25b Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Sun, 7 Jun 2026 18:08:34 +0200 Subject: [PATCH 3/9] feat: add raise_window(*, with_focus) to Qt backend Extract the raise logic from show() into a public raise_window() with a with_focus parameter (default False). with_focus=True activates and raises the window (focus); with_focus=False raises without focus. On Linux/Windows raise_() raises without taking focus, as desired. On macOS, Qt's raise_() also activates the application (stealing focus) and there is no Qt API to avoid it, so for with_focus=False we reach the native NSWindow via winId() and call -[NSWindow orderFrontRegardless] -- the same focus-free raise the macosx backend uses -- through a small ctypes Objective-C bridge. show()/figure.raise_window keep the no-focus default. --- lib/matplotlib/backends/backend_qt.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 39450ee32065..686c629270c4 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -664,8 +664,21 @@ def show(self): self.window._destroying = False self.window.show() if mpl.rcParams['figure.raise_window']: + self.raise_window() + + def raise_window(self, *, with_focus=False): + # docstring inherited + if with_focus: self.window.activateWindow() self.window.raise_() + elif sys.platform == "darwin": + # On macOS, Qt's raise_() also activates the app (stealing focus), + # so raise the native NSWindow without focus instead. + from ._macos_window import MacOSWindow + MacOSWindow.from_nsview(self.window.winId()).raise_window( + with_focus=False) + else: + self.window.raise_() def destroy(self, *args): # check for qApp first, as PySide deletes it in its atexit handler From 607fc0ee47c6d4a6773dd32c70f763bdef54aaea Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Sun, 7 Jun 2026 18:09:09 +0200 Subject: [PATCH 4/9] feat: add raise_window(*, with_focus) to macOS backend Extract the raise logic from show() into a public raise_window() and add a with_focus parameter to the _raise() C function. with_focus=False (default) uses orderFrontRegardless (raise without focus); with_focus=True forces the application to the foreground and makes the window key, a new capability on macOS (which never stole focus before) that matches the focus opt-in of the other backends. Activation goes through a small activate_application() helper, shared with the blocking module-level show(). It uses -[NSApplication activateIgnoringOtherApps:] deliberately: the modern -[NSApplication activate] (macOS 14+) only brings the app forward in response to a user action, so called programmatically it does nothing and cannot transfer focus, and Apple offers no non-deprecated forceful equivalent. The deprecation warning is suppressed locally; it is silent at matplotlib's 10.12 deployment target. --- lib/matplotlib/backends/backend_macosx.py | 6 ++- src/_macosx.m | 54 +++++++++++++++++++++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index 6ea437a90ca1..6540a93cfea4 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -186,7 +186,11 @@ def show(self): self._show() self._shown = True if mpl.rcParams["figure.raise_window"]: - self._raise() + self.raise_window() + + def raise_window(self, *, with_focus=False): + # docstring inherited + self._raise(with_focus=with_focus) @_Backend.export diff --git a/src/_macosx.m b/src/_macosx.m index 0de0540018a7..dbdc718c524c 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -781,6 +781,39 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) Py_TYPE(self)->tp_free((PyObject*)self); } +/* Force the application to the foreground so a figure window can take focus. + * + * -[NSApplication activateIgnoringOtherApps:] was deprecated in macOS 14 in + * favor of the argument-less -[NSApplication activate]. We deliberately keep + * the deprecated call: -activate only brings the app forward when the system + * considers it appropriate -- in practice, in response to a user action. When + * called programmatically with no user action behind it (e.g. from a timer or + * a script), it does nothing, leaving the app in the background without + * keyboard focus. -activateIgnoringOtherApps: activates unconditionally, which + * is what with_focus=True requires, and there is no non-deprecated in-process + * API that does the same. + * + * The only non-deprecated alternative is to have another process (e.g. System + * Events, via an Apple event / AppleScript) bring us frontmost. That works, but + * it requires the user to grant an Automation/Accessibility permission and adds + * a fragile scripting dependency -- too heavy to justify while the direct call + * still exists. We will reconsider it only if and when activateIgnoringOtherApps: + * is *removed* (not merely deprecated), at which point it would likely be the + * only remaining option, asking the user for authorization once. + * + * With the pragmas below, the deprecation warning is suppressed; it only fires + * when building with a deployment target >= 14 and is silent at matplotlib's + * 10.12 target anyway. + */ +static void +activate_application(void) +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [NSApp activateIgnoringOtherApps: YES]; +#pragma clang diagnostic pop +} + static PyObject* FigureManager__show(FigureManager* self) { @@ -791,10 +824,23 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) } static PyObject* -FigureManager__raise(FigureManager* self) +FigureManager__raise(FigureManager* self, PyObject* args, PyObject* kwds) { + int with_focus = 0; + static char* kwlist[] = {"with_focus", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|p", kwlist, &with_focus)) { + return NULL; + } BEGIN_OBJC_ENTRY - [self->window orderFrontRegardless]; + if (with_focus) { + // Bring the application forward and give the window keyboard focus, + // matching the focus-stealing behavior of the Qt and GTK backends. + activate_application(); + [self->window makeKeyAndOrderFront: nil]; + } else { + // Raise the window in the stacking order without stealing focus. + [self->window orderFrontRegardless]; + } END_OBJC_ENTRY RETURN_NULL_OR_NONE } @@ -942,7 +988,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) METH_NOARGS}, {"_raise", (PyCFunction)FigureManager__raise, - METH_NOARGS}, + METH_VARARGS | METH_KEYWORDS}, {"destroy", (PyCFunction)FigureManager_destroy, METH_NOARGS}, @@ -1898,7 +1944,7 @@ - (void)flagsChanged:(NSEvent *)event // Iterating over -[NSApp windows] will add the windows to the topmost // autorelease pool, wrap in @autoreleasepool as -[NSApp run] is long-running. @autoreleasepool { - [NSApp activateIgnoringOtherApps: YES]; + activate_application(); NSArray *windowsArray = [NSApp windows]; NSEnumerator *enumerator = [windowsArray objectEnumerator]; NSWindow *window; From 1b49ff45952ac42061e170176be4677c28e9edb2 Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Sun, 7 Jun 2026 18:09:32 +0200 Subject: [PATCH 5/9] feat: add raise_window(*, with_focus) to GTK backend Extract the raise logic from show() into a public raise_window() and add a with_focus parameter. with_focus=True calls present() (raise + activate); with_focus=False does a best-effort raise without focus: on GTK3 it uses the low-level Gdk window raise() (X11 raises without focus, Wayland is a no-op), while GTK4 has no raise-without-activate and falls back to present(), which may still transfer focus. show()/figure.raise_window keep the no-focus default. Documented in the API behavior note. --- lib/matplotlib/backends/_backend_gtk.py | 48 ++++++++++++++++++++----- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py index 0491db40e565..68bcd2e01b5e 100644 --- a/lib/matplotlib/backends/_backend_gtk.py +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -221,15 +221,45 @@ def show(self): self.window.show() self.canvas.draw() if mpl.rcParams["figure.raise_window"]: - meth_name = {3: "get_window", 4: "get_surface"}[self._gtk_ver] - if getattr(self.window, meth_name)(): - self.window.present() - else: - # If this is called by a callback early during init, - # self.window (a GtkWindow) may not have an associated - # low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet, - # and present() would crash. - _api.warn_external("Cannot raise window yet to be setup") + self.raise_window() + + def raise_window(self, *, with_focus=False): + # docstring inherited + # + # Known limitation on macOS: there GTK runs on the Quartz backend, and + # the present()/raise_() calls below do not lift the window above other + # applications or change focus (macOS only does that for the active + # application). Implementing it would require getting the native + # NSWindow from GDK -- gdk_quartz_window_get_nswindow() on GTK3, and + # there is no clear public way to do it on GTK4 -- and then calling + # orderFrontRegardless/activate on it, as the Qt, Tk and wx backends do. + # GTK is primarily a Linux backend and is rarely used on macOS, so this + # is left unimplemented and documented as a known limitation. The calls + # below work on GTK's usual platforms (X11 and, best-effort, Wayland). + meth_name = {3: "get_window", 4: "get_surface"}[self._gtk_ver] + surface = getattr(self.window, meth_name)() + if not surface: + # If this is called by a callback early during init, + # self.window (a GtkWindow) may not have an associated + # low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet, + # and present()/raise_() would crash. + _api.warn_external("Cannot raise window yet to be setup") + return + if with_focus: + # present() raises the window and gives it keyboard focus. On + # Wayland this goes through the xdg-activation protocol and is + # best-effort (the compositor may ignore it without a token). + self.window.present() + elif self._gtk_ver == 3: + # GTK3 exposes a low-level raise that only changes the stacking + # order, without transferring keyboard focus (on X11; on Wayland + # the compositor controls stacking, so this is a no-op). + surface.raise_() + else: + # GTK4 (GdkSurface) has no way to raise without activating, so fall + # back to present(). This may transfer focus; raising and focusing + # cannot be separated on this toolkit. + self.window.present() def full_screen_toggle(self): is_fullscreen = { From af2053b356a0253258431fe111bb14f7baa0710a Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Sun, 7 Jun 2026 18:09:33 +0200 Subject: [PATCH 6/9] feat: add raise_window(*, with_focus) to Tk backend Extract the raise logic from show() into a public raise_window() with a with_focus parameter (default False). with_focus=True calls focus_force() to grab keyboard focus; with_focus=False raises without focus. On X11/Windows the -topmost toggle raises without taking focus. On macOS that toggle does not lift the window above other applications, and Tk's winfo_id() is an internal MacDrawable rather than an NSView, so for with_focus=False we resolve Tk_MacOSXGetNSWindowForDrawable from the loaded Tk library to get the real NSWindow and call -[NSWindow orderFrontRegardless] (the focus-free raise the macosx backend uses) via a small ctypes Objective-C bridge. The Windows-only _restore_foreground_window_at_end() wrapper around show() only affects the show() path (no focus), so a standalone raise_window(with_focus=True) still grabs focus. --- lib/matplotlib/backends/_backend_tk.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index a18a9bd8660f..593d0f5d6642 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -615,10 +615,23 @@ def destroy(*args): else: self.canvas.draw_idle() if mpl.rcParams['figure.raise_window']: - self.canvas.manager.window.attributes('-topmost', 1) - self.canvas.manager.window.attributes('-topmost', 0) + self.raise_window() self._shown = True + def raise_window(self, *, with_focus=False): + # docstring inherited + if not with_focus and sys.platform == "darwin": + # On macOS the -topmost toggle does not lift the window above other + # applications; use the native focus-free raise instead. + from ._macos_window import MacOSWindow + MacOSWindow.from_tk_drawable(self.window.winfo_id()).raise_window( + with_focus=False) + return + self.window.attributes('-topmost', 1) + self.window.attributes('-topmost', 0) + if with_focus: + self.window.focus_force() + def destroy(self, *args): if self.canvas._idle_draw_id: self.canvas._tkcanvas.after_cancel(self.canvas._idle_draw_id) From bf26c6acc9831d15d3e8cbafe16ae8eff61dbce6 Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Sun, 7 Jun 2026 18:09:33 +0200 Subject: [PATCH 7/9] feat: add raise_window(*, with_focus) to wx backend Extract the raise logic from show() into a public raise_window() with a with_focus parameter (default False). On Linux/Windows, Frame.Raise() raises without focus and (for with_focus=True) Frame.SetFocus() grabs focus. On macOS neither lifts a backgrounded app's window above other applications nor activates the app, so reach the native NSWindow (Frame.GetHandle() is the NSView; [view window] gives the NSWindow) and use the macosx backend's calls via a small ctypes Objective-C bridge: orderFrontRegardless for with_focus=False, and activateIgnoringOtherApps: + makeKeyAndOrderFront: for with_focus=True. --- lib/matplotlib/backends/backend_wx.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 930a944b5274..c9064f5e910e 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1002,7 +1002,22 @@ def show(self): self.frame.Show() self.canvas.draw() if mpl.rcParams['figure.raise_window']: - self.frame.Raise() + self.raise_window() + + def raise_window(self, *, with_focus=False): + # docstring inherited + if sys.platform == "darwin": + # wx's Raise()/SetFocus() don't lift above other apps or activate + # on macOS; use the native NSWindow calls instead. + from ._macos_window import MacOSWindow + MacOSWindow.from_nsview(self.frame.GetHandle()).raise_window( + with_focus=with_focus) + return + # Raise() only changes the stacking order (it does not activate the + # window or take keyboard focus); SetFocus() additionally grabs focus. + self.frame.Raise() + if with_focus: + self.frame.SetFocus() def destroy(self, *args): # docstring inherited From d1bf5e648e0ac714adda1e0bcc6fd51bf63fe1d6 Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Sun, 7 Jun 2026 23:33:49 +0200 Subject: [PATCH 8/9] test: base raise_window accepts with_focus --- lib/matplotlib/tests/test_backend_bases.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index 92ee0f31a632..b6ea5d3b95ef 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -67,6 +67,8 @@ def test_figure_manager_base_raise_window_noop(): canvas = FigureCanvasBase(Figure()) manager = FigureManagerBase(canvas, 1) assert manager.raise_window() is None + assert manager.raise_window(with_focus=True) is None + assert manager.raise_window(with_focus=False) is None def test_get_default_filename(): From a65e990ae4c50f871a992cb1270c1367936f85df Mon Sep 17 00:00:00 2001 From: Andrea Alberti Date: Sun, 7 Jun 2026 23:33:49 +0200 Subject: [PATCH 9/9] test: per-backend raise_window smoke --- lib/matplotlib/tests/test_backends_interactive.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 2e2713f15ee1..b38bcf65d2dc 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -211,8 +211,18 @@ def check_alt_backend(alt_backend): import asyncio asyncio.set_event_loop(asyncio.new_event_loop()) + def _quit(*args): + # Smoke-test raise_window on a realized window: every backend must + # accept with_focus and run all three forms without error. (The actual + # raise/focus behaviour is platform-dependent and verified manually.) + manager = fig.canvas.manager + manager.raise_window() + manager.raise_window(with_focus=False) + manager.raise_window(with_focus=True) + KeyEvent("key_press_event", fig.canvas, "q")._process() + timer = fig.canvas.new_timer(1.) # Test that floats are cast to int. - timer.add_callback(KeyEvent("key_press_event", fig.canvas, "q")._process) + timer.add_callback(_quit) # Trigger quitting upon draw. fig.canvas.mpl_connect("draw_event", lambda event: timer.start()) fig.canvas.mpl_connect("close_event", print)