Skip to content

Comments

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

Open
alberti42 wants to merge 2 commits intomatplotlib:mainfrom
alberti42:feat-raise-window
Open

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

Conversation

@alberti42
Copy link
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


def raise_window(self):
# docstring inherited
self.window.activateWindow()
Copy link
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
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
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
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
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.

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