Skip to content

Commit 15697ea

Browse files
authored
Merge pull request matplotlib#30591 from timhoffm/safe-blit
FIX: Make widget blitting compatible with swapped canvas
2 parents bbf01d4 + bec8b2b commit 15697ea

File tree

3 files changed

+125
-35
lines changed

3 files changed

+125
-35
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,6 +1740,10 @@ class FigureCanvasBase:
17401740

17411741
filetypes = _default_filetypes
17421742

1743+
# global counter to assign unique ids to blit backgrounds
1744+
# see _get_blit_background_id()
1745+
_last_blit_background_id = 0
1746+
17431747
@_api.classproperty
17441748
def supports_blit(cls):
17451749
"""If this Canvas sub-class supports blitting."""
@@ -1765,6 +1769,7 @@ def __init__(self, figure=None):
17651769
# We don't want to scale up the figure DPI more than once.
17661770
figure._original_dpi = getattr(figure, '_original_dpi', figure.dpi)
17671771
self._device_pixel_ratio = 1
1772+
self._blit_backgrounds = {}
17681773
super().__init__() # Typically the GUI widget init (if any).
17691774

17701775
callbacks = property(lambda self: self.figure._canvas_callbacks)
@@ -1840,6 +1845,51 @@ def is_saving(self):
18401845
def blit(self, bbox=None):
18411846
"""Blit the canvas in bbox (default entire canvas)."""
18421847

1848+
@classmethod
1849+
def _get_blit_background_id(cls):
1850+
"""
1851+
Get a globally unique id that can be used to store a blit background.
1852+
1853+
Blitting support is canvas-dependent, so blitting mechanisms should
1854+
store their backgrounds in the canvas, more precisely in
1855+
``canvas._blit_backgrounds[id]``. The id must be obtained via this
1856+
function to ensure it is globally unique.
1857+
1858+
The content of ``canvas._blit_backgrounds[id]`` is not specified.
1859+
We leave this freedom to the blitting mechanism.
1860+
1861+
Blitting mechanisms must not expect that a background that they
1862+
have stored is still there at a later time. The canvas may have
1863+
been switched out, or we may add other mechanisms later that
1864+
invalidate blit backgrounds (e.g. dpi changes).
1865+
Therefore, always query as `_blit_backgrounds.get(id)` and be
1866+
prepared for a None return value.
1867+
1868+
Note: The blit background API is still experimental and may change
1869+
in the future without warning.
1870+
"""
1871+
cls._last_blit_background_id += 1
1872+
return cls._last_blit_background_id
1873+
1874+
def _release_blit_background_id(self, bb_id):
1875+
"""
1876+
Release a blit background id that is no longer needed.
1877+
1878+
This removes the respective entry from the internal storage, i.e.
1879+
the ``canvas._blit_backgrounds`` dict, and thus allows to free the
1880+
associated memory.
1881+
1882+
After releasing the id you must not use it anymore.
1883+
1884+
It is safe to release an id that has not been used with the canvas
1885+
or that has already been released.
1886+
1887+
Note: The blit background API is still experimental and may change
1888+
in the future without warning.
1889+
"""
1890+
if bb_id in self._blit_backgrounds:
1891+
del self._blit_backgrounds[bb_id]
1892+
18431893
def inaxes(self, xy):
18441894
"""
18451895
Return the topmost visible `~.axes.Axes` containing the point *xy*.

lib/matplotlib/widgets.py

Lines changed: 71 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ class AxesWidget(Widget):
118118
def __init__(self, ax):
119119
self.ax = ax
120120
self._cids = []
121+
self._blit_background_id = None
122+
123+
def __del__(self):
124+
if self._blit_background_id is not None:
125+
self.canvas._release_blit_background_id(self._blit_background_id)
121126

122127
canvas = property(
123128
lambda self: getattr(self.ax.get_figure(root=True), 'canvas', None)
@@ -146,6 +151,26 @@ def _set_cursor(self, cursor):
146151
"""Update the canvas cursor."""
147152
self.ax.get_figure(root=True).canvas.set_cursor(cursor)
148153

154+
def _save_blit_background(self, background):
155+
"""
156+
Save a blit background.
157+
158+
The background is stored on the canvas in a uniquely identifiable way.
159+
It should be read back via `._load_blit_background`. Be prepared that
160+
some events may invalidate the background, in which case
161+
`._load_blit_background` will return None.
162+
163+
This currently allows at most one background per widget, which is
164+
good enough for all existing widgets.
165+
"""
166+
if self._blit_background_id is None:
167+
self._blit_background_id = self.canvas._get_blit_background_id()
168+
self.canvas._blit_backgrounds[self._blit_background_id] = background
169+
170+
def _load_blit_background(self):
171+
"""Load a blit background; may be None at any time."""
172+
return self.canvas._blit_backgrounds.get(self._blit_background_id)
173+
149174

150175
def _call_with_reparented_event(func):
151176
"""
@@ -223,7 +248,7 @@ def __init__(self, ax, label, image=None,
223248
horizontalalignment='center',
224249
transform=ax.transAxes)
225250

226-
self._useblit = useblit and self.canvas.supports_blit
251+
self._useblit = useblit
227252

228253
self._observers = cbook.CallbackRegistry(signals=["clicked"])
229254

@@ -260,7 +285,7 @@ def _motion(self, event):
260285
if not colors.same_color(c, self.ax.get_facecolor()):
261286
self.ax.set_facecolor(c)
262287
if self.drawon:
263-
if self._useblit:
288+
if self._useblit and self.canvas.supports_blit:
264289
self.ax.draw_artist(self.ax)
265290
self.canvas.blit(self.ax.bbox)
266291
else:
@@ -1083,8 +1108,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
10831108
if actives is None:
10841109
actives = [False] * len(labels)
10851110

1086-
self._useblit = useblit and self.canvas.supports_blit
1087-
self._background = None
1111+
self._useblit = useblit and self.canvas.supports_blit # TODO: make dynamic
10881112

10891113
ys = np.linspace(1, 0, len(labels)+2)[1:-1]
10901114

@@ -1131,7 +1155,7 @@ def _clear(self, event):
11311155
"""Internal event handler to clear the buttons."""
11321156
if self.ignore(event) or self.canvas.is_saving():
11331157
return
1134-
self._background = self.canvas.copy_from_bbox(self.ax.bbox)
1158+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
11351159
self.ax.draw_artist(self._checks)
11361160

11371161
@_call_with_reparented_event
@@ -1237,8 +1261,9 @@ def set_active(self, index, state=None):
12371261

12381262
if self.drawon:
12391263
if self._useblit:
1240-
if self._background is not None:
1241-
self.canvas.restore_region(self._background)
1264+
background = self._load_blit_background()
1265+
if background is not None:
1266+
self.canvas.restore_region(background)
12421267
self.ax.draw_artist(self._checks)
12431268
self.canvas.blit(self.ax.bbox)
12441269
else:
@@ -1676,8 +1701,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16761701

16771702
ys = np.linspace(1, 0, len(labels) + 2)[1:-1]
16781703

1679-
self._useblit = useblit and self.canvas.supports_blit
1680-
self._background = None
1704+
self._useblit = useblit and self.canvas.supports_blit # TODO: make dynamic
16811705

16821706
label_props = _expand_text_props(label_props)
16831707
self.labels = [
@@ -1719,7 +1743,7 @@ def _clear(self, event):
17191743
"""Internal event handler to clear the buttons."""
17201744
if self.ignore(event) or self.canvas.is_saving():
17211745
return
1722-
self._background = self.canvas.copy_from_bbox(self.ax.bbox)
1746+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
17231747
self.ax.draw_artist(self._buttons)
17241748

17251749
@_call_with_reparented_event
@@ -1813,8 +1837,9 @@ def set_active(self, index):
18131837

18141838
if self.drawon:
18151839
if self._useblit:
1816-
if self._background is not None:
1817-
self.canvas.restore_region(self._background)
1840+
background = self._load_blit_background()
1841+
if background is not None:
1842+
self.canvas.restore_region(background)
18181843
self.ax.draw_artist(self._buttons)
18191844
self.canvas.blit(self.ax.bbox)
18201845
else:
@@ -1963,22 +1988,21 @@ def __init__(self, ax, *, horizOn=True, vertOn=True, useblit=False,
19631988
self.visible = True
19641989
self.horizOn = horizOn
19651990
self.vertOn = vertOn
1966-
self.useblit = useblit and self.canvas.supports_blit
1991+
self.useblit = useblit and self.canvas.supports_blit # TODO: make dynamic
19671992

19681993
if self.useblit:
19691994
lineprops['animated'] = True
19701995
self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops)
19711996
self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops)
19721997

1973-
self.background = None
19741998
self.needclear = False
19751999

19762000
def clear(self, event):
19772001
"""Internal event handler to clear the cursor."""
19782002
if self.ignore(event) or self.canvas.is_saving():
19792003
return
19802004
if self.useblit:
1981-
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
2005+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
19822006

19832007
@_call_with_reparented_event
19842008
def onmove(self, event):
@@ -2003,8 +2027,9 @@ def onmove(self, event):
20032027
return
20042028
# Redraw.
20052029
if self.useblit:
2006-
if self.background is not None:
2007-
self.canvas.restore_region(self.background)
2030+
background = self._load_blit_background()
2031+
if background is not None:
2032+
self.canvas.restore_region(background)
20082033
self.ax.draw_artist(self.linev)
20092034
self.ax.draw_artist(self.lineh)
20102035
self.canvas.blit(self.ax.bbox)
@@ -2093,6 +2118,7 @@ def __init__(self, *args, useblit=True, horizOn=False, vertOn=True,
20932118
self.useblit = (
20942119
useblit
20952120
and all(canvas.supports_blit for canvas in self._canvas_infos))
2121+
# TODO: make dynamic
20962122

20972123
if self.useblit:
20982124
lineprops['animated'] = True
@@ -2177,7 +2203,7 @@ def __init__(self, ax, onselect=None, useblit=False, button=None,
21772203
self.onselect = lambda *args: None
21782204
else:
21792205
self.onselect = onselect
2180-
self.useblit = useblit and self.canvas.supports_blit
2206+
self._useblit = useblit
21812207
self.connect_default_events()
21822208

21832209
self._state_modifier_keys = dict(move=' ', clear='escape',
@@ -2186,8 +2212,6 @@ def __init__(self, ax, onselect=None, useblit=False, button=None,
21862212
self._state_modifier_keys.update(state_modifier_keys or {})
21872213
self._use_data_coordinates = use_data_coordinates
21882214

2189-
self.background = None
2190-
21912215
if isinstance(button, Integral):
21922216
self.validButtons = [button]
21932217
else:
@@ -2203,6 +2227,11 @@ def __init__(self, ax, onselect=None, useblit=False, button=None,
22032227
self._prev_event = None
22042228
self._state = set()
22052229

2230+
@property
2231+
def useblit(self):
2232+
"""Return whether blitting is used (requested and supported by canvas)."""
2233+
return self._useblit and self.canvas.supports_blit
2234+
22062235
def set_active(self, active):
22072236
super().set_active(active)
22082237
if active:
@@ -2243,7 +2272,7 @@ def update_background(self, event):
22432272
for artist in artists:
22442273
stack.enter_context(artist._cm_set(visible=False))
22452274
self.canvas.draw()
2246-
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
2275+
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
22472276
if needs_redraw:
22482277
for artist in artists:
22492278
self.ax.draw_artist(artist)
@@ -2290,8 +2319,9 @@ def update(self):
22902319
self.ax.get_figure(root=True)._get_renderer() is None):
22912320
return
22922321
if self.useblit:
2293-
if self.background is not None:
2294-
self.canvas.restore_region(self.background)
2322+
background = self._load_blit_background()
2323+
if background is not None:
2324+
self.canvas.restore_region(background)
22952325
else:
22962326
self.update_background(None)
22972327
# We need to draw all artists, which are not included in the
@@ -2629,7 +2659,14 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False,
26292659
if props is None:
26302660
props = dict(facecolor='red', alpha=0.5)
26312661

2632-
props['animated'] = self.useblit
2662+
# Note: We set this based on the user setting during ínitialization,
2663+
# not on the actual capability of blitting. But the value is
2664+
# irrelevant if the backend does not support blitting, so that
2665+
# we don't have to dynamically update this on the backend.
2666+
# This relies on the current behavior that the request for
2667+
# useblit is fixed during initialization and cannot be changed
2668+
# afterwards.
2669+
props['animated'] = self._useblit
26332670

26342671
self.direction = direction
26352672
self._extents_on_press = None
@@ -2695,7 +2732,7 @@ def _setup_edge_handles(self, props):
26952732
self._edge_handles = ToolLineHandles(self.ax, positions,
26962733
direction=self.direction,
26972734
line_props=props,
2698-
useblit=self.useblit)
2735+
useblit=self._useblit)
26992736

27002737
@property
27012738
def _handles_artists(self):
@@ -3269,7 +3306,7 @@ def __init__(self, ax, onselect=None, *, minspanx=0,
32693306
if props is None:
32703307
props = dict(facecolor='red', edgecolor='black',
32713308
alpha=0.2, fill=True)
3272-
props = {**props, 'animated': self.useblit}
3309+
props = {**props, 'animated': self._useblit}
32733310
self._visible = props.pop('visible', self._visible)
32743311
to_draw = self._init_shape(**props)
32753312
self.ax.add_patch(to_draw)
@@ -3294,18 +3331,18 @@ def __init__(self, ax, onselect=None, *, minspanx=0,
32943331
xc, yc = self.corners
32953332
self._corner_handles = ToolHandles(self.ax, xc, yc,
32963333
marker_props=self._handle_props,
3297-
useblit=self.useblit)
3334+
useblit=self._useblit)
32983335

32993336
self._edge_order = ['W', 'S', 'E', 'N']
33003337
xe, ye = self.edge_centers
33013338
self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s',
33023339
marker_props=self._handle_props,
3303-
useblit=self.useblit)
3340+
useblit=self._useblit)
33043341

33053342
xc, yc = self.center
33063343
self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s',
33073344
marker_props=self._handle_props,
3308-
useblit=self.useblit)
3345+
useblit=self._useblit)
33093346

33103347
self._active_handle = None
33113348

@@ -3812,7 +3849,7 @@ def __init__(self, ax, onselect=None, *, useblit=True, props=None, button=None):
38123849
**(props if props is not None else {}),
38133850
# Note that self.useblit may be != useblit, if the canvas doesn't
38143851
# support blitting.
3815-
'animated': self.useblit, 'visible': False,
3852+
'animated': self._useblit, 'visible': False,
38163853
}
38173854
line = Line2D([], [], **props)
38183855
self.ax.add_line(line)
@@ -3937,7 +3974,7 @@ def __init__(self, ax, onselect=None, *, useblit=False,
39373974

39383975
if props is None:
39393976
props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5)
3940-
props = {**props, 'animated': self.useblit}
3977+
props = {**props, 'animated': self._useblit}
39413978
self._selection_artist = line = Line2D([], [], **props)
39423979
self.ax.add_line(line)
39433980

@@ -3946,7 +3983,7 @@ def __init__(self, ax, onselect=None, *, useblit=False,
39463983
markerfacecolor=props.get('color', 'k'))
39473984
self._handle_props = handle_props
39483985
self._polygon_handles = ToolHandles(self.ax, [], [],
3949-
useblit=self.useblit,
3986+
useblit=self._useblit,
39503987
marker_props=self._handle_props)
39513988

39523989
self._active_handle_idx = -1
@@ -3966,7 +4003,7 @@ def _get_bbox(self):
39664003

39674004
def _add_box(self):
39684005
self._box = RectangleSelector(self.ax,
3969-
useblit=self.useblit,
4006+
useblit=self._useblit,
39704007
grab_range=self.grab_range,
39714008
handle_props=self._box_handle_props,
39724009
props=self._box_props,
@@ -4248,7 +4285,7 @@ class Lasso(AxesWidget):
42484285
def __init__(self, ax, xy, callback, *, useblit=True, props=None):
42494286
super().__init__(ax)
42504287

4251-
self.useblit = useblit and self.canvas.supports_blit
4288+
self.useblit = useblit and self.canvas.supports_blit # TODO: Make dynamic
42524289
if self.useblit:
42534290
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
42544291

0 commit comments

Comments
 (0)