Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 808dbe4

Browse filesBrowse files
authored
Merge pull request #21790 from greglucas/macosx-draw-refactor
FIX: Update blitting and drawing on the macosx backend
2 parents 396a010 + 5415418 commit 808dbe4
Copy full SHA for 808dbe4

File tree

Expand file treeCollapse file tree

3 files changed

+125
-27
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+125
-27
lines changed

‎lib/matplotlib/backends/backend_macosx.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_macosx.py
+49-12Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,64 @@ def __init__(self, figure):
3030
FigureCanvasBase.__init__(self, figure)
3131
width, height = self.get_width_height()
3232
_macosx.FigureCanvas.__init__(self, width, height)
33+
self._draw_pending = False
34+
self._is_drawing = False
3335

3436
def set_cursor(self, cursor):
3537
# docstring inherited
3638
_macosx.set_cursor(cursor)
3739

38-
def _draw(self):
39-
renderer = self.get_renderer()
40-
if self.figure.stale:
41-
renderer.clear()
42-
self.figure.draw(renderer)
43-
return renderer
44-
4540
def draw(self):
46-
# docstring inherited
47-
self._draw()
48-
self.flush_events()
41+
"""Render the figure and update the macosx canvas."""
42+
# The renderer draw is done here; delaying causes problems with code
43+
# that uses the result of the draw() to update plot elements.
44+
if self._is_drawing:
45+
return
46+
with cbook._setattr_cm(self, _is_drawing=True):
47+
super().draw()
48+
self.update()
4949

50-
# draw_idle is provided by _macosx.FigureCanvas
50+
def draw_idle(self):
51+
# docstring inherited
52+
if not (getattr(self, '_draw_pending', False) or
53+
getattr(self, '_is_drawing', False)):
54+
self._draw_pending = True
55+
# Add a singleshot timer to the eventloop that will call back
56+
# into the Python method _draw_idle to take care of the draw
57+
self._single_shot_timer(self._draw_idle)
58+
59+
def _single_shot_timer(self, callback):
60+
"""Add a single shot timer with the given callback"""
61+
# We need to explicitly stop (called from delete) the timer after
62+
# firing, otherwise segfaults will occur when trying to deallocate
63+
# the singleshot timers.
64+
def callback_func(callback, timer):
65+
callback()
66+
del timer
67+
timer = self.new_timer(interval=0)
68+
timer.add_callback(callback_func, callback, timer)
69+
timer.start()
70+
71+
def _draw_idle(self):
72+
"""
73+
Draw method for singleshot timer
74+
75+
This draw method can be added to a singleshot timer, which can
76+
accumulate draws while the eventloop is spinning. This method will
77+
then only draw the first time and short-circuit the others.
78+
"""
79+
with self._idle_draw_cntx():
80+
if not self._draw_pending:
81+
# Short-circuit because our draw request has already been
82+
# taken care of
83+
return
84+
self._draw_pending = False
85+
self.draw()
5186

5287
def blit(self, bbox=None):
53-
self.draw_idle()
88+
# docstring inherited
89+
super().blit(bbox)
90+
self.update()
5491

5592
def resize(self, width, height):
5693
# Size from macOS is logical pixels, dpi is physical.

‎lib/matplotlib/tests/test_backends_interactive.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_backends_interactive.py
+69Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,72 @@ def _lazy_headless():
413413
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
414414
def test_lazy_linux_headless():
415415
proc = _run_helper(_lazy_headless, timeout=_test_timeout, MPLBACKEND="")
416+
417+
418+
def _test_number_of_draws_script():
419+
import matplotlib.pyplot as plt
420+
421+
fig, ax = plt.subplots()
422+
423+
# animated=True tells matplotlib to only draw the artist when we
424+
# explicitly request it
425+
ln, = ax.plot([0, 1], [1, 2], animated=True)
426+
427+
# make sure the window is raised, but the script keeps going
428+
plt.show(block=False)
429+
plt.pause(0.3)
430+
# Connect to draw_event to count the occurrences
431+
fig.canvas.mpl_connect('draw_event', print)
432+
433+
# get copy of entire figure (everything inside fig.bbox)
434+
# sans animated artist
435+
bg = fig.canvas.copy_from_bbox(fig.bbox)
436+
# draw the animated artist, this uses a cached renderer
437+
ax.draw_artist(ln)
438+
# show the result to the screen
439+
fig.canvas.blit(fig.bbox)
440+
441+
for j in range(10):
442+
# reset the background back in the canvas state, screen unchanged
443+
fig.canvas.restore_region(bg)
444+
# Create a **new** artist here, this is poor usage of blitting
445+
# but good for testing to make sure that this doesn't create
446+
# excessive draws
447+
ln, = ax.plot([0, 1], [1, 2])
448+
# render the artist, updating the canvas state, but not the screen
449+
ax.draw_artist(ln)
450+
# copy the image to the GUI state, but screen might not changed yet
451+
fig.canvas.blit(fig.bbox)
452+
# flush any pending GUI events, re-painting the screen if needed
453+
fig.canvas.flush_events()
454+
455+
# Let the event loop process everything before leaving
456+
plt.pause(0.1)
457+
458+
459+
_blit_backends = _get_testable_interactive_backends()
460+
for param in _blit_backends:
461+
backend = param.values[0]["MPLBACKEND"]
462+
if backend == "gtk3cairo":
463+
# copy_from_bbox only works when rendering to an ImageSurface
464+
param.marks.append(
465+
pytest.mark.skip("gtk3cairo does not support blitting"))
466+
elif backend == "wx":
467+
param.marks.append(
468+
pytest.mark.skip("wx does not support blitting"))
469+
470+
471+
@pytest.mark.parametrize("env", _blit_backends)
472+
# subprocesses can struggle to get the display, so rerun a few times
473+
@pytest.mark.flaky(reruns=4)
474+
def test_blitting_events(env):
475+
proc = _run_helper(_test_number_of_draws_script,
476+
timeout=_test_timeout,
477+
**env)
478+
479+
# Count the number of draw_events we got. We could count some initial
480+
# canvas draws (which vary in number by backend), but the critical
481+
# check here is that it isn't 10 draws, which would be called if
482+
# blitting is not properly implemented
483+
ndraws = proc.stdout.count("DrawEvent")
484+
assert 0 < ndraws < 5

‎src/_macosx.m

Copy file name to clipboardExpand all lines: src/_macosx.m
+7-15Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -345,14 +345,7 @@ static CGFloat _get_device_scale(CGContextRef cr)
345345
}
346346

347347
static PyObject*
348-
FigureCanvas_draw(FigureCanvas* self)
349-
{
350-
[self->view display];
351-
Py_RETURN_NONE;
352-
}
353-
354-
static PyObject*
355-
FigureCanvas_draw_idle(FigureCanvas* self)
348+
FigureCanvas_update(FigureCanvas* self)
356349
{
357350
[self->view setNeedsDisplay: YES];
358351
Py_RETURN_NONE;
@@ -361,6 +354,9 @@ static CGFloat _get_device_scale(CGContextRef cr)
361354
static PyObject*
362355
FigureCanvas_flush_events(FigureCanvas* self)
363356
{
357+
// We need to allow the runloop to run very briefly
358+
// to allow the view to be displayed when used in a fast updating animation
359+
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0]];
364360
[self->view displayIfNeeded];
365361
Py_RETURN_NONE;
366362
}
@@ -485,12 +481,8 @@ static CGFloat _get_device_scale(CGContextRef cr)
485481
.tp_new = (newfunc)FigureCanvas_new,
486482
.tp_doc = "A FigureCanvas object wraps a Cocoa NSView object.",
487483
.tp_methods = (PyMethodDef[]){
488-
{"draw",
489-
(PyCFunction)FigureCanvas_draw,
490-
METH_NOARGS,
491-
NULL}, // docstring inherited
492-
{"draw_idle",
493-
(PyCFunction)FigureCanvas_draw_idle,
484+
{"update",
485+
(PyCFunction)FigureCanvas_update,
494486
METH_NOARGS,
495487
NULL}, // docstring inherited
496488
{"flush_events",
@@ -1263,7 +1255,7 @@ -(void)drawRect:(NSRect)rect
12631255

12641256
CGContextRef cr = [[NSGraphicsContext currentContext] CGContext];
12651257

1266-
if (!(renderer = PyObject_CallMethod(canvas, "_draw", ""))
1258+
if (!(renderer = PyObject_CallMethod(canvas, "get_renderer", ""))
12671259
|| !(renderer_buffer = PyObject_GetAttrString(renderer, "_renderer"))) {
12681260
PyErr_Print();
12691261
goto exit;

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.