Skip to content

refactor: expose raise_window to user by reusing existing internal code#31176

Open
alberti42 wants to merge 9 commits into
matplotlib:mainfrom
alberti42:feat-raise-window
Open

refactor: expose raise_window to user by reusing existing internal code#31176
alberti42 wants to merge 9 commits into
matplotlib:mainfrom
alberti42:feat-raise-window

Conversation

@alberti42
Copy link
Copy Markdown
Contributor

PR summary

Why is this change necessary?

rcParams["figure.raise_window"] has been supported by all five GUI backends
(GTK, Tk, Qt, Wx, macOS) since at least matplotlib 3.6, meaning the native
machinery to raise a figure window to the foreground is fully implemented
everywhere. However, it was only reachable as a side-effect of calling
show(), controlled by a global rcParam. There was no way for a user to
programmatically raise a specific window on demand.

What problem does it solve?

Users who want to bring a figure window to the foreground at an arbitrary
point in their program — not just at show() time — had no supported API to
do so. A common workaround was to toggle rcParams["figure.raise_window"] and
call show(), which is indirect and may trigger unintended redraws.

What is the reasoning for this implementation?

All the native raise calls already existed inside each backend's show()
method. This PR extracts them into a dedicated raise_window() method on each
FigureManager subclass, and makes show() delegate to it when the rcParam
is set. The base class FigureManagerBase gets a no-op raise_window() (same
pattern as full_screen_toggle() and resize()), giving non-GUI backends a
safe default.

No new native code was written. The change is a pure refactor of existing,
already-tested logic.

Usage example

import matplotlib;
matplotlib.rcParams['figure.raise_window']=False;

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2, 3])
plt.show(block=False)

# ... do some work ...

# Bring the figure window back to the foreground
fig.canvas.manager.raise_window()

Files changed

File Change
lib/matplotlib/backend_bases.py Added no-op raise_window() to FigureManagerBase
lib/matplotlib/backends/_backend_gtk.py Extracted raise logic into raise_window(); show() delegates
lib/matplotlib/backends/_backend_tk.py Extracted raise logic into raise_window(); show() delegates
lib/matplotlib/backends/backend_qt.py Extracted raise logic into raise_window(); show() delegates
lib/matplotlib/backends/backend_wx.py Extracted raise logic into raise_window(); show() delegates
lib/matplotlib/backends/backend_macosx.py raise_window() wraps existing self._raise() C extension; show() delegates

PR checklist

  • "closes #0000" is in the body of the PR description to link the related issue
  • new and changed code is tested
  • Plotting related features are demonstrated in an example
  • New Features and API Changes are noted with a directive and release note
  • Documentation complies with general and docstring guidelines

Comment thread lib/matplotlib/backends/backend_qt.py Outdated

def raise_window(self):
# docstring inherited
self.window.activateWindow()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we haven't specified so far is whether the window has focus. - I think for mpl.rcParams["figure.raise_window"] that should be the case. And that's the reason this line exists. To be checked with the other backends.

But even it that holds, it's not clear whether the standalone raise_window() calls will preserve that behavior. It could be that the previous context of a show() ensured that for some backends, but does not do it generally when called at an arbitrary time.

@alberti42
Copy link
Copy Markdown
Contributor Author

Great point, and worth discussing before we go further.

Our primary motivation for raise_window() is programmatic workflows — e.g. from an IPython session, bringing a figure window into view to inspect a plot, without leaving the terminal (IPython session). In that context, stealing keyboard focus is probably not what the user wants: there isn't much to do with keyboard focus in a matplotlib window anyway, and the user likely wants to stay in their REPL.

That said, you are right that the current rcParams["figure.raise_window"] path has always included focus transfer (at least on Qt and GTK), so changing that silently would be a behaviour regression.

Our proposal would be:

raise_window(*, with_focus=True)

With with_focus=True as the default, the behaviour matches the current rcParams["figure.raise_window"] path, so there is no surprise for existing users. Users who want to bring the window to the front without leaving their REPL can opt out with raise_window(with_focus=False). Internally, show() would simply call raise_window(with_focus=True) to preserve the existing rcParams semantics.


macOS investigation

I ran a first investigation on the macOS backend using the following script:

BACKEND="macosx"
BLOCK=False
FIGURE_RAISE_WINDOW=True

import subprocess
import matplotlib as mpl
mpl.rcParams["figure.raise_window"]=FIGURE_RAISE_WINDOW
mpl.use(BACKEND)
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2, 3])

manager = fig.canvas.manager

# Register focus callbacks so we can observe focus changes
manager.mpl_connect('focus_in_event',  lambda: print(">>> matplotlib window got focus"))
manager.mpl_connect('focus_out_event', lambda: print(">>> matplotlib window lost focus"))

def frontmost_app():
    result = subprocess.run(
        ['osascript', '-e',
         'tell application "System Events" to get name of first application process whose frontmost is true'],
        capture_output=True, text=True
    )
    return result.stdout.strip()

plt.show(block=BLOCK)

# --- After clicking back into the terminal ---

print("Frontmost before:", frontmost_app())
manager.raise_window()
print("Frontmost after: ", frontmost_app())

The four combinations of BLOCK and FIGURE_RAISE_WINDOW were tested:

BLOCK FIGURE_RAISE_WINDOW frontmost_app() changes focus events fired observation
False False no (stays wezterm-gui) none window becomes visible only after manager.raise_window()
False True no (stays wezterm-gui) none window becomes visible directly after show()
True False no (stays python3) focus_in + focus_out blocking show, window clearly visible, focus on window
True True no (stays python3) focus_in + focus_out blocking show, window clearly visible, focus on window

Conclusions

On macOS:

  • [orderFrontRegardless] (the native call behind both show() and raise_window()) brings the window visually to the front without stealing keyboard focus from the calling process. The frontmost app never changes from the terminal (wezterm-gui).
  • Focus events only fire when block=True, because blocking the event loop hands control to the macOS run loop, which naturally activates the window. This is a side-effect of blocking, not of raise_window.
  • rcParams["figure.raise_window"] has no effect on focus on macOS — focus behaviour is entirely determined by whether show() is blocking or not.

This means the macOS backend's raise_window() already implements "raise without stealing focus" — which is the behaviour I would prefer for the standalone raise_window() call. However, this is likely inconsistent with what Qt and GTK currently do via activateWindow() and present(), which do transfer focus. So macOS is already effectively doing with_focus=False, but it diverges from the other backends — which is exactly the cross-platform inconsistency that needs to be resolved.

@timhoffm
Copy link
Copy Markdown
Member

Our proposal would be:

raise_window(*, with_focus=True)

An optional parameter is likely reasonable. Alternative would be a separate focus() method. Viability depends on whether all backends support the opertions separately (or we accept cross-talk on some backends).

With with_focus=True as the default, the behaviour matches the current rcParams["figure.raise_window"] path, so there is no surprise for existing users.

The default value of with_focus=True can be chosen freely. If with_focus=False was better, we would simply call raise_window(with_focus=True) in the existing mpl.rcParams["figure.raise_window"] context and document that in the parameter.

Note: This is a good example why we are reseved on extending the backend. It's a lot of effort to make this right and consistent across all backends.

@alberti42
Copy link
Copy Markdown
Contributor Author

Hi Tim!

An optional parameter is likely reasonable. Alternative would be a separate focus() method. Viability depends on whether all backends support the opertions separately (or we accept cross-talk on some backends).

Cool. Probably we do not have to decide it now; we can defer the decision of whether to use a parameter with_focus or a new function focus() to later. My preference as of today is to use a parameter: focus() has a clear meaning (make it visible and steal focus), but raise_window is not 100% clear what it is supposed to do. So, if we go for raise_window(*, with_focus=True), it provides the user with a simpler mental picture; much less to process in the head.

Viability depends on whether all backends support the opertions separately (or we accept cross-talk on some backends).

I cross my fingers that it is possible to implement it cleanly on all backends, but I never worked with Tk/Gtk3. Do you have a list of preferences in which order I should investigate the other backends: Qt5, Qt4, Gtk3, Tk? Anything else? macosx is already covered. This could take me a while. Is there a minimum subset that is considered a must-have while the rest can be implemented at a later time?

Note: This is a good example why we are reseved on extending the backend. It's a lot of effort to make this right and consistent across all backends.

Yes, I totally see it, and I want to help you.

@story645 story645 assigned alberti42 and unassigned alberti42 Feb 19, 2026
@timhoffm
Copy link
Copy Markdown
Member

For a PoC I'd typically use Qt as it a very capable framework (but maybe I'm biased, because that's the one I'm most familiar with). Tk and Gtk3 and on the lower end of features and good to look at when needing to identify the minimal common feature set.

Note that we do not support Qt4 anymore it's Qt5/6 now.

Not all backends have to be implemented at once. The important point is that we have to make sure there are no surpises from the other backend that would conflict with our design.

For a release, I'd say Qt, Tk and Mac should be covered as these are the morst common ones. But you can make separate PRs if that makes it easier.

@timhoffm
Copy link
Copy Markdown
Member

Next necessary step: Inverstigate whether raising is possible without setting focus for all GUI toolkits.

@alberti42
Copy link
Copy Markdown
Contributor Author

Hey Tim, I did not forget it all. I was only busy. I will come back to it.

On a side note, just if you are curious and you are someone who likes the terminal, I coded in an independent branch (consider it like a demo of possible features; not a proposal of a PR) the necessary functions for:

  • catch resize and move events (only the end event to avoid noise)
  • getting th window frame
  • setting the window frame
  • raise the window
  • set the level to always on top

for both macos and qtagg. I created a fork of matplot lib and recreated the all deployment chain of wheels (published as prelease to avoid being too visible); see https://github.com/alberti42/fork-matplotlib/releases/tag/v3.10.1.post1.

This allowed me to deploy on a few computers in our lab the patched version of matplotlib for testing it.

The changes are used in a new package I wrote https://github.com/alberti42/matplotlib-window-tracker which is meant to be a drop-in extension / enhancement of matplotlib experience.

The idea is to allow people who love the combination IPython + matplotlib to work with persistent windows. All what you need to do is:

track_position_size(mpl_fig, tag="my_interesting_window")

giving a unique name per project. Then this window will always appear on the same spot, regardless of whether you restart IPython.

This is very nice when you start to have many windows and work with multiple monitors and want have an automated workflow, avoiding continuously repositioning the same windows over and over.

I hope this package will find other users. It is so far in a beta version. I am collecting feedback, and we still need to see how it will finally interact with matplotlib. Ideally, I will not want to maintain a fork of matplotlib for this purpose, but rather use the official one.

Anyway, if you think it could be help for you or someone you know, I would love to get feedback.

PS: Sorry for the long side note, only marginally related to this PR.

alberti42 added 9 commits June 7, 2026 23:19
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.
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.
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.
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.
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.
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.
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.
@alberti42 alberti42 force-pushed the feat-raise-window branch from e922f7d to a65e990 Compare June 7, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants