Skip to content

Commit c7094a3

Browse files
committed
FIX: overhaul backend class management + fix Tk backend
Not clear that the semantics of `FigureManager._active_managers` is correct. It is currently only non-None when in a blocking show (rather than when letting the input hook handle things).
1 parent c1411b8 commit c7094a3

File tree

4 files changed

+106
-23
lines changed

4 files changed

+106
-23
lines changed

mpl_gui/__init__.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
from itertools import count
1818

1919
from matplotlib.backend_bases import FigureCanvasBase as _FigureCanvasBase
20-
from matplotlib.cbook import _setattr_cm
2120

2221
from ._figure import Figure # noqa: F401
2322

2423
from ._manage_interactive import ion, ioff, is_interactive # noqa: F401
25-
from ._manage_backend import select_gui_toolkit, current_backend_module as _cbm
24+
from ._manage_backend import select_gui_toolkit # noqa: F401
25+
from ._manage_backend import current_backend_module as _cbm
2626
from ._promotion import promote_figure as promote_figure
2727
from ._creation import figure, subplots, subplot_mosaic # noqa: F401
2828

@@ -70,17 +70,12 @@ def show(figs, *, block=None, timeout=0):
7070
else:
7171
managers.append(promote_figure(fig))
7272

73-
for manager in managers:
74-
manager.show()
75-
manager.canvas.draw_idle()
76-
7773
if block is None:
7874
block = not is_interactive()
7975

8076
if block and len(managers):
8177
if timeout == 0:
82-
with _setattr_cm(backend, get_active_managers=lambda: managers):
83-
backend.mainloop()
78+
backend.show_managers(managers=managers, block=block)
8479
elif len(managers):
8580
manager, *_ = managers
8681
manager.canvas.start_event_loop(timeout=timeout)

mpl_gui/_manage_backend.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import importlib
22
import sys
33
import logging
4+
import types
45

56
from matplotlib import cbook, rcsetup
67
from matplotlib import rcParams, rcParamsDefault
@@ -96,24 +97,48 @@ def select_gui_toolkit(newbackend=None):
9697

9798
mod = importlib.import_module(backend_name)
9899
if hasattr(mod, "Backend"):
99-
backend_mod = mod.Backend
100+
orig_class = mod.Backend
101+
100102
else:
101-
class backend_mod(matplotlib.backend_bases._Backend):
103+
104+
class orig_class(matplotlib.backend_bases._Backend):
102105
locals().update(vars(mod))
103106

104107
@classmethod
105108
def mainloop(cls):
106109
return mod.Show().mainloop()
107110

111+
class BackendClass(orig_class):
112+
@classmethod
113+
def show_managers(cls, *, managers, block):
114+
if not managers:
115+
return
116+
for manager in managers:
117+
manager.show() # Emits a warning for non-interactive backend
118+
manager.canvas.draw_idle()
119+
if cls.mainloop is None:
120+
return
121+
if block:
122+
try:
123+
cls.FigureManager._active_managers = managers
124+
cls.mainloop()
125+
finally:
126+
cls.FigureManager._active_managers = None
127+
128+
if not hasattr(BackendClass.FigureManager, "_active_managers"):
129+
BackendClass.FigureManager._active_managers = None
108130
rc_params_string = newbackend
109131

110132
else:
111-
backend_mod = newbackend
112-
rc_params_string = f"module://_backend_mod_{id(backend_mod)}"
113-
sys.modules[rc_params_string] = backend_mod
133+
BackendClass = newbackend
134+
mod_name = f"_backend_mod_{id(BackendClass)}"
135+
rc_params_string = f"module://{mod_name}"
136+
mod = types.ModuleType(mod_name)
137+
mod.Backend = BackendClass
138+
sys.modules[mod_name] = mod
114139

115140
required_framework = getattr(
116-
backend_mod.FigureCanvas, "required_interactive_framework", None
141+
BackendClass.FigureCanvas, "required_interactive_framework", None
117142
)
118143
if required_framework is not None:
119144
current_framework = cbook._get_running_interactive_framework()
@@ -129,10 +154,11 @@ def mainloop(cls):
129154
)
130155
)
131156

132-
_log.debug("Loaded backend %s version %s.", newbackend, backend_mod.backend_version)
157+
_log.debug(
158+
"Loaded backend %s version %s.", newbackend, BackendClass.backend_version
159+
)
133160

134161
rcParams["backend"] = rcParamsDefault["backend"] = rc_params_string
135-
_backend_mod = backend_mod
136162

137163
# is IPython imported?
138164
mod_ipython = sys.modules.get("IPython")
@@ -141,4 +167,7 @@ def mainloop(cls):
141167
ip = mod_ipython.get_ipython()
142168
if ip:
143169
ip.enable_gui(required_framework)
144-
return _backend_mod
170+
171+
# remember to set the global variable
172+
_backend_mod = BackendClass
173+
return BackendClass

mpl_gui/_patched_backends/tkagg.py

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,65 @@
1-
from matplotlib.backends.backend_tkagg import _BackendTkAgg
1+
from contextlib import contextmanager
2+
3+
import matplotlib as mpl
4+
from matplotlib import _c_internal_utils
5+
from matplotlib.backends.backend_tkagg import (
6+
_BackendTkAgg,
7+
FigureManagerTk as _FigureManagerTk,
8+
)
9+
10+
11+
@contextmanager
12+
def _restore_foreground_window_at_end():
13+
foreground = _c_internal_utils.Win32_GetForegroundWindow()
14+
try:
15+
yield
16+
finally:
17+
if mpl.rcParams["tk.window_focus"]:
18+
_c_internal_utils.Win32_SetForegroundWindow(foreground)
19+
20+
21+
class FigureManagerTk(_FigureManagerTk):
22+
_active_managers = None
23+
24+
def show(self):
25+
with _restore_foreground_window_at_end():
26+
if not self._shown:
27+
self.window.protocol("WM_DELETE_WINDOW", self.destroy)
28+
self.window.deiconify()
29+
self.canvas._tkcanvas.focus_set()
30+
else:
31+
self.canvas.draw_idle()
32+
if mpl.rcParams["figure.raise_window"]:
33+
self.canvas.manager.window.attributes("-topmost", 1)
34+
self.canvas.manager.window.attributes("-topmost", 0)
35+
self._shown = True
36+
37+
def destroy(self, *args):
38+
if self.canvas._idle_draw_id:
39+
self.canvas._tkcanvas.after_cancel(self.canvas._idle_draw_id)
40+
if self.canvas._event_loop_id:
41+
self.canvas._tkcanvas.after_cancel(self.canvas._event_loop_id)
42+
43+
# NOTE: events need to be flushed before issuing destroy (GH #9956),
44+
# however, self.window.update() can break user code. This is the
45+
# safest way to achieve a complete draining of the event queue,
46+
# but it may require users to update() on their own to execute the
47+
# completion in obscure corner cases.
48+
def delayed_destroy():
49+
self.window.destroy()
50+
51+
if self._owns_mainloop and not self._active_managers:
52+
self.window.quit()
53+
54+
# "after idle after 0" avoids Tcl error/race (GH #19940)
55+
self.window.after_idle(self.window.after, 0, delayed_destroy)
256

357

458
@_BackendTkAgg.export
559
class _PatchedBackendTkAgg(_BackendTkAgg):
6-
@staticmethod
7-
def get_active_managers():
8-
raise RuntimeError("This method should never actually be called")
9-
1060
@classmethod
1161
def mainloop(cls):
12-
managers = cls.get_active_managers()
62+
managers = cls.FigureManager._active_managers
1363
if managers:
1464
first_manager = managers[0]
1565
manager_class = type(first_manager)
@@ -21,5 +71,7 @@ def mainloop(cls):
2171
finally:
2272
manager_class._owns_mainloop = False
2373

74+
FigureManager = FigureManagerTk
75+
2476

2577
Backend = _PatchedBackendTkAgg

mpl_gui/tests/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ def start_event_loop(self, timeout=0):
2222

2323

2424
class TestManger(FigureManagerBase):
25+
_active_managers = None
26+
2527
def __init__(self, *args, **kwargs):
2628
super().__init__(*args, **kwargs)
2729
self.call_info = {}
@@ -47,5 +49,10 @@ class TestingBackend(_Backend):
4749
def mainloop(cls):
4850
...
4951

52+
@classmethod
53+
def show_managers(cls, *, managers, block):
54+
for m in managers:
55+
m.show()
56+
5057

5158
mpl_gui.select_gui_toolkit(TestingBackend)

0 commit comments

Comments
 (0)