-
-
Notifications
You must be signed in to change notification settings - Fork 8.1k
FIX: Make widget blitting compatible with swapped canvas #30591
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -117,6 +117,11 @@ class AxesWidget(Widget): | |
| def __init__(self, ax): | ||
| self.ax = ax | ||
| self._cids = [] | ||
| self._blit_background_id = None | ||
|
|
||
| def __del__(self): | ||
| if self._blit_background_id is not None: | ||
| self.canvas._release_blit_background_id(self._blit_background_id) | ||
|
|
||
| canvas = property( | ||
| lambda self: getattr(self.ax.get_figure(root=True), 'canvas', None) | ||
|
|
@@ -155,6 +160,26 @@ def _set_cursor(self, cursor): | |
| """Update the canvas cursor.""" | ||
| self.ax.get_figure(root=True).canvas.set_cursor(cursor) | ||
|
|
||
| def _save_blit_background(self, background): | ||
| """ | ||
| Save a blit background. | ||
|
|
||
| The background is stored on the canvas in a uniquely identifiable way. | ||
| It should be read back via `._load_blit_background`. Be prepared that | ||
| some events may invalidate the background, in which case | ||
| `._load_blit_background` will return None. | ||
|
|
||
| This currently allows at most one background per widget, which is | ||
| good enough for all existing widgets. | ||
| """ | ||
| if self._blit_background_id is None: | ||
| self._blit_background_id = self.canvas._get_blit_background_id() | ||
| self.canvas._blit_backgrounds[self._blit_background_id] = background | ||
tacaswell marked this conversation as resolved.
Show resolved
Hide resolved
tacaswell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def _load_blit_background(self): | ||
| """Load a blit background; may be None at any time.""" | ||
| return self.canvas._blit_backgrounds.get(self._blit_background_id) | ||
tacaswell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| class Button(AxesWidget): | ||
| """ | ||
|
|
@@ -206,7 +231,7 @@ def __init__(self, ax, label, image=None, | |
| horizontalalignment='center', | ||
| transform=ax.transAxes) | ||
|
|
||
| self._useblit = useblit and self.canvas.supports_blit | ||
| self._useblit = useblit | ||
|
|
||
| self._observers = cbook.CallbackRegistry(signals=["clicked"]) | ||
|
|
||
|
|
@@ -240,7 +265,7 @@ def _motion(self, event): | |
| if not colors.same_color(c, self.ax.get_facecolor()): | ||
| self.ax.set_facecolor(c) | ||
| if self.drawon: | ||
| if self._useblit: | ||
| if self._useblit and self.canvas.supports_blit: | ||
| self.ax.draw_artist(self.ax) | ||
| self.canvas.blit(self.ax.bbox) | ||
| else: | ||
|
|
@@ -1062,8 +1087,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True, | |
| if actives is None: | ||
| actives = [False] * len(labels) | ||
|
|
||
| self._useblit = useblit and self.canvas.supports_blit | ||
| self._background = None | ||
| self._useblit = useblit and self.canvas.supports_blit # TODO: make dynamic | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this TODO necessary to complete before 3.11?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a follow-up and can land in 3.11 or later.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @timhoffm are you planning on addressing this TODO in this PR or is it a follow-up?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a follow-up and can land in 3.11 or later. |
||
|
|
||
| ys = np.linspace(1, 0, len(labels)+2)[1:-1] | ||
|
|
||
|
|
@@ -1110,7 +1134,7 @@ def _clear(self, event): | |
| """Internal event handler to clear the buttons.""" | ||
| if self.ignore(event) or self.canvas.is_saving(): | ||
| return | ||
| self._background = self.canvas.copy_from_bbox(self.ax.bbox) | ||
| self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox)) | ||
| self.ax.draw_artist(self._checks) | ||
|
|
||
| def _clicked(self, event): | ||
|
|
@@ -1215,8 +1239,9 @@ def set_active(self, index, state=None): | |
|
|
||
| if self.drawon: | ||
| if self._useblit: | ||
| if self._background is not None: | ||
| self.canvas.restore_region(self._background) | ||
| background = self._load_blit_background() | ||
| if background is not None: | ||
| self.canvas.restore_region(background) | ||
| self.ax.draw_artist(self._checks) | ||
| self.canvas.blit(self.ax.bbox) | ||
| else: | ||
|
|
@@ -1649,8 +1674,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *, | |
|
|
||
| ys = np.linspace(1, 0, len(labels) + 2)[1:-1] | ||
|
|
||
| self._useblit = useblit and self.canvas.supports_blit | ||
| self._background = None | ||
| self._useblit = useblit and self.canvas.supports_blit # TODO: make dynamic | ||
|
|
||
| label_props = _expand_text_props(label_props) | ||
| self.labels = [ | ||
|
|
@@ -1692,7 +1716,7 @@ def _clear(self, event): | |
| """Internal event handler to clear the buttons.""" | ||
| if self.ignore(event) or self.canvas.is_saving(): | ||
| return | ||
| self._background = self.canvas.copy_from_bbox(self.ax.bbox) | ||
| self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox)) | ||
| self.ax.draw_artist(self._buttons) | ||
|
|
||
| def _clicked(self, event): | ||
|
|
@@ -1785,8 +1809,9 @@ def set_active(self, index): | |
|
|
||
| if self.drawon: | ||
| if self._useblit: | ||
| if self._background is not None: | ||
| self.canvas.restore_region(self._background) | ||
| background = self._load_blit_background() | ||
| if background is not None: | ||
| self.canvas.restore_region(background) | ||
| self.ax.draw_artist(self._buttons) | ||
| self.canvas.blit(self.ax.bbox) | ||
| else: | ||
|
|
@@ -1935,22 +1960,21 @@ def __init__(self, ax, *, horizOn=True, vertOn=True, useblit=False, | |
| self.visible = True | ||
| self.horizOn = horizOn | ||
| self.vertOn = vertOn | ||
| self.useblit = useblit and self.canvas.supports_blit | ||
| self.useblit = useblit and self.canvas.supports_blit # TODO: make dynamic | ||
|
|
||
| if self.useblit: | ||
| lineprops['animated'] = True | ||
| self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops) | ||
| self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops) | ||
|
|
||
| self.background = None | ||
| self.needclear = False | ||
|
|
||
| def clear(self, event): | ||
| """Internal event handler to clear the cursor.""" | ||
| if self.ignore(event) or self.canvas.is_saving(): | ||
| return | ||
| if self.useblit: | ||
| self.background = self.canvas.copy_from_bbox(self.ax.bbox) | ||
| self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox)) | ||
|
|
||
| def onmove(self, event): | ||
| """Internal event handler to draw the cursor when the mouse moves.""" | ||
|
|
@@ -1975,8 +1999,9 @@ def onmove(self, event): | |
| return | ||
| # Redraw. | ||
| if self.useblit: | ||
| if self.background is not None: | ||
| self.canvas.restore_region(self.background) | ||
| background = self._load_blit_background() | ||
| if background is not None: | ||
| self.canvas.restore_region(background) | ||
| self.ax.draw_artist(self.linev) | ||
| self.ax.draw_artist(self.lineh) | ||
| self.canvas.blit(self.ax.bbox) | ||
|
|
@@ -2044,6 +2069,7 @@ def __init__(self, canvas, axes, *, useblit=True, horizOn=False, vertOn=True, | |
| self.useblit = ( | ||
| useblit | ||
| and all(canvas.supports_blit for canvas in self._canvas_infos)) | ||
| # TODO: make dynamic | ||
|
|
||
| if self.useblit: | ||
| lineprops['animated'] = True | ||
|
|
@@ -2128,7 +2154,7 @@ def __init__(self, ax, onselect=None, useblit=False, button=None, | |
| self.onselect = lambda *args: None | ||
| else: | ||
| self.onselect = onselect | ||
| self.useblit = useblit and self.canvas.supports_blit | ||
| self._useblit = useblit | ||
| self.connect_default_events() | ||
|
|
||
| self._state_modifier_keys = dict(move=' ', clear='escape', | ||
|
|
@@ -2137,8 +2163,6 @@ def __init__(self, ax, onselect=None, useblit=False, button=None, | |
| self._state_modifier_keys.update(state_modifier_keys or {}) | ||
| self._use_data_coordinates = use_data_coordinates | ||
|
|
||
| self.background = None | ||
|
|
||
| if isinstance(button, Integral): | ||
| self.validButtons = [button] | ||
| else: | ||
|
|
@@ -2154,6 +2178,11 @@ def __init__(self, ax, onselect=None, useblit=False, button=None, | |
| self._prev_event = None | ||
| self._state = set() | ||
|
|
||
| @property | ||
| def useblit(self): | ||
| """Return whether blitting is used (requested and supported by canvas).""" | ||
| return self._useblit and self.canvas.supports_blit | ||
|
|
||
| def set_active(self, active): | ||
| super().set_active(active) | ||
| if active: | ||
|
|
@@ -2194,7 +2223,7 @@ def update_background(self, event): | |
| for artist in artists: | ||
| stack.enter_context(artist._cm_set(visible=False)) | ||
| self.canvas.draw() | ||
| self.background = self.canvas.copy_from_bbox(self.ax.bbox) | ||
| self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox)) | ||
| if needs_redraw: | ||
| for artist in artists: | ||
| self.ax.draw_artist(artist) | ||
|
|
@@ -2241,8 +2270,9 @@ def update(self): | |
| self.ax.get_figure(root=True)._get_renderer() is None): | ||
| return | ||
| if self.useblit: | ||
| if self.background is not None: | ||
| self.canvas.restore_region(self.background) | ||
| background = self._load_blit_background() | ||
| if background is not None: | ||
| self.canvas.restore_region(background) | ||
| else: | ||
| self.update_background(None) | ||
| # We need to draw all artists, which are not included in the | ||
|
|
@@ -2575,7 +2605,14 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False, | |
| if props is None: | ||
| props = dict(facecolor='red', alpha=0.5) | ||
|
|
||
| props['animated'] = self.useblit | ||
| # Note: We set this based on the user setting during ínitialization, | ||
| # not on the actual capability of blitting. But the value is | ||
| # irrelevant if the backend does not support blitting, so that | ||
| # we don't have to dynamically update this on the backend. | ||
| # This relies on the current behavior that the request for | ||
| # useblit is fixed during initialization and cannot be changed | ||
| # afterwards. | ||
| props['animated'] = self._useblit | ||
|
|
||
| self.direction = direction | ||
| self._extents_on_press = None | ||
|
|
@@ -2641,7 +2678,7 @@ def _setup_edge_handles(self, props): | |
| self._edge_handles = ToolLineHandles(self.ax, positions, | ||
| direction=self.direction, | ||
| line_props=props, | ||
| useblit=self.useblit) | ||
| useblit=self._useblit) | ||
|
|
||
| @property | ||
| def _handles_artists(self): | ||
|
|
@@ -3215,7 +3252,7 @@ def __init__(self, ax, onselect=None, *, minspanx=0, | |
| if props is None: | ||
| props = dict(facecolor='red', edgecolor='black', | ||
| alpha=0.2, fill=True) | ||
| props = {**props, 'animated': self.useblit} | ||
| props = {**props, 'animated': self._useblit} | ||
| self._visible = props.pop('visible', self._visible) | ||
| to_draw = self._init_shape(**props) | ||
| self.ax.add_patch(to_draw) | ||
|
|
@@ -3240,18 +3277,18 @@ def __init__(self, ax, onselect=None, *, minspanx=0, | |
| xc, yc = self.corners | ||
| self._corner_handles = ToolHandles(self.ax, xc, yc, | ||
| marker_props=self._handle_props, | ||
| useblit=self.useblit) | ||
| useblit=self._useblit) | ||
|
|
||
| self._edge_order = ['W', 'S', 'E', 'N'] | ||
| xe, ye = self.edge_centers | ||
| self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', | ||
| marker_props=self._handle_props, | ||
| useblit=self.useblit) | ||
| useblit=self._useblit) | ||
|
|
||
| xc, yc = self.center | ||
| self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', | ||
| marker_props=self._handle_props, | ||
| useblit=self.useblit) | ||
| useblit=self._useblit) | ||
|
|
||
| self._active_handle = None | ||
|
|
||
|
|
@@ -3758,7 +3795,7 @@ def __init__(self, ax, onselect=None, *, useblit=True, props=None, button=None): | |
| **(props if props is not None else {}), | ||
| # Note that self.useblit may be != useblit, if the canvas doesn't | ||
| # support blitting. | ||
| 'animated': self.useblit, 'visible': False, | ||
| 'animated': self._useblit, 'visible': False, | ||
| } | ||
| line = Line2D([], [], **props) | ||
| self.ax.add_line(line) | ||
|
|
@@ -3882,7 +3919,7 @@ def __init__(self, ax, onselect=None, *, useblit=False, | |
|
|
||
| if props is None: | ||
| props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5) | ||
| props = {**props, 'animated': self.useblit} | ||
| props = {**props, 'animated': self._useblit} | ||
| self._selection_artist = line = Line2D([], [], **props) | ||
| self.ax.add_line(line) | ||
|
|
||
|
|
@@ -3891,7 +3928,7 @@ def __init__(self, ax, onselect=None, *, useblit=False, | |
| markerfacecolor=props.get('color', 'k')) | ||
| self._handle_props = handle_props | ||
| self._polygon_handles = ToolHandles(self.ax, [], [], | ||
| useblit=self.useblit, | ||
| useblit=self._useblit, | ||
| marker_props=self._handle_props) | ||
|
|
||
| self._active_handle_idx = -1 | ||
|
|
@@ -3911,7 +3948,7 @@ def _get_bbox(self): | |
|
|
||
| def _add_box(self): | ||
| self._box = RectangleSelector(self.ax, | ||
| useblit=self.useblit, | ||
| useblit=self._useblit, | ||
| grab_range=self.grab_range, | ||
| handle_props=self._box_handle_props, | ||
| props=self._box_props, | ||
|
|
@@ -4191,7 +4228,7 @@ class Lasso(AxesWidget): | |
| def __init__(self, ax, xy, callback, *, useblit=True, props=None): | ||
| super().__init__(ax) | ||
|
|
||
| self.useblit = useblit and self.canvas.supports_blit | ||
| self.useblit = useblit and self.canvas.supports_blit # TODO: Make dynamic | ||
| if self.useblit: | ||
| self.background = self.canvas.copy_from_bbox(self.ax.bbox) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to pass the bounding box in instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For symmetry reasons and to keep the changes within the PR minimal, I would leave the complete handling to the individual widgets for now.
They do
and
It may be reasonable to refactor in a follow-up and move that logic to the base class.