refactor: expose raise_window to user by reusing existing internal code#31176
refactor: expose raise_window to user by reusing existing internal code#31176alberti42 wants to merge 9 commits into
Conversation
|
|
||
| def raise_window(self): | ||
| # docstring inherited | ||
| self.window.activateWindow() |
There was a problem hiding this comment.
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.
|
Great point, and worth discussing before we go further. Our primary motivation for That said, you are right that the current Our proposal would be: raise_window(*, with_focus=True)With macOS investigationI 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
ConclusionsOn macOS:
This means the macOS backend's |
An optional parameter is likely reasonable. Alternative would be a separate
The default value of 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. |
|
Hi Tim!
Cool. Probably we do not have to decide it now; we can defer the decision of whether to use a parameter
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:
Yes, I totally see it, and I want to help you. |
0dd867a to
1cf388d
Compare
|
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. |
1cf388d to
2db8f82
Compare
|
Next necessary step: Inverstigate whether raising is possible without setting focus for all GUI toolkits. |
|
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:
for both macos and qtagg. I created a fork of 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. |
2db8f82 to
c44557e
Compare
c44557e to
e922f7d
Compare
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.
e922f7d to
a65e990
Compare
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 toprogrammatically 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 todo so. A common workaround was to toggle
rcParams["figure.raise_window"]andcall
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 eachFigureManagersubclass, and makesshow()delegate to it when the rcParamis set. The base class
FigureManagerBasegets a no-opraise_window()(samepattern as
full_screen_toggle()andresize()), giving non-GUI backends asafe default.
No new native code was written. The change is a pure refactor of existing,
already-tested logic.
Usage example
Files changed
lib/matplotlib/backend_bases.pyraise_window()toFigureManagerBaselib/matplotlib/backends/_backend_gtk.pyraise_window();show()delegateslib/matplotlib/backends/_backend_tk.pyraise_window();show()delegateslib/matplotlib/backends/backend_qt.pyraise_window();show()delegateslib/matplotlib/backends/backend_wx.pyraise_window();show()delegateslib/matplotlib/backends/backend_macosx.pyraise_window()wraps existingself._raise()C extension;show()delegatesPR checklist