diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index 69643e35fe3f..70db18067a7d 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -30,27 +30,64 @@ def __init__(self, figure): FigureCanvasBase.__init__(self, figure) width, height = self.get_width_height() _macosx.FigureCanvas.__init__(self, width, height) + self._draw_pending = False + self._is_drawing = False def set_cursor(self, cursor): # docstring inherited _macosx.set_cursor(cursor) - def _draw(self): - renderer = self.get_renderer() - if self.figure.stale: - renderer.clear() - self.figure.draw(renderer) - return renderer - def draw(self): - # docstring inherited - self._draw() - self.flush_events() + """Render the figure and update the macosx canvas.""" + # The renderer draw is done here; delaying causes problems with code + # that uses the result of the draw() to update plot elements. + if self._is_drawing: + return + with cbook._setattr_cm(self, _is_drawing=True): + super().draw() + self.update() - # draw_idle is provided by _macosx.FigureCanvas + def draw_idle(self): + # docstring inherited + if not (getattr(self, '_draw_pending', False) or + getattr(self, '_is_drawing', False)): + self._draw_pending = True + # Add a singleshot timer to the eventloop that will call back + # into the Python method _draw_idle to take care of the draw + self._single_shot_timer(self._draw_idle) + + def _single_shot_timer(self, callback): + """Add a single shot timer with the given callback""" + # We need to explicitly stop (called from delete) the timer after + # firing, otherwise segfaults will occur when trying to deallocate + # the singleshot timers. + def callback_func(callback, timer): + callback() + del timer + timer = self.new_timer(interval=0) + timer.add_callback(callback_func, callback, timer) + timer.start() + + def _draw_idle(self): + """ + Draw method for singleshot timer + + This draw method can be added to a singleshot timer, which can + accumulate draws while the eventloop is spinning. This method will + then only draw the first time and short-circuit the others. + """ + with self._idle_draw_cntx(): + if not self._draw_pending: + # Short-circuit because our draw request has already been + # taken care of + return + self._draw_pending = False + self.draw() def blit(self, bbox=None): - self.draw_idle() + # docstring inherited + super().blit(bbox) + self.update() def resize(self, width, height): # Size from macOS is logical pixels, dpi is physical. diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 2818f3d21cca..23991410601c 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -413,3 +413,72 @@ def _lazy_headless(): @pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_lazy_linux_headless(): proc = _run_helper(_lazy_headless, timeout=_test_timeout, MPLBACKEND="") + + +def _test_number_of_draws_script(): + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + + # animated=True tells matplotlib to only draw the artist when we + # explicitly request it + ln, = ax.plot([0, 1], [1, 2], animated=True) + + # make sure the window is raised, but the script keeps going + plt.show(block=False) + plt.pause(0.3) + # Connect to draw_event to count the occurrences + fig.canvas.mpl_connect('draw_event', print) + + # get copy of entire figure (everything inside fig.bbox) + # sans animated artist + bg = fig.canvas.copy_from_bbox(fig.bbox) + # draw the animated artist, this uses a cached renderer + ax.draw_artist(ln) + # show the result to the screen + fig.canvas.blit(fig.bbox) + + for j in range(10): + # reset the background back in the canvas state, screen unchanged + fig.canvas.restore_region(bg) + # Create a **new** artist here, this is poor usage of blitting + # but good for testing to make sure that this doesn't create + # excessive draws + ln, = ax.plot([0, 1], [1, 2]) + # render the artist, updating the canvas state, but not the screen + ax.draw_artist(ln) + # copy the image to the GUI state, but screen might not changed yet + fig.canvas.blit(fig.bbox) + # flush any pending GUI events, re-painting the screen if needed + fig.canvas.flush_events() + + # Let the event loop process everything before leaving + plt.pause(0.1) + + +_blit_backends = _get_testable_interactive_backends() +for param in _blit_backends: + backend = param.values[0]["MPLBACKEND"] + if backend == "gtk3cairo": + # copy_from_bbox only works when rendering to an ImageSurface + param.marks.append( + pytest.mark.skip("gtk3cairo does not support blitting")) + elif backend == "wx": + param.marks.append( + pytest.mark.skip("wx does not support blitting")) + + +@pytest.mark.parametrize("env", _blit_backends) +# subprocesses can struggle to get the display, so rerun a few times +@pytest.mark.flaky(reruns=4) +def test_blitting_events(env): + proc = _run_helper(_test_number_of_draws_script, + timeout=_test_timeout, + **env) + + # Count the number of draw_events we got. We could count some initial + # canvas draws (which vary in number by backend), but the critical + # check here is that it isn't 10 draws, which would be called if + # blitting is not properly implemented + ndraws = proc.stdout.count("DrawEvent") + assert 0 < ndraws < 5 diff --git a/src/_macosx.m b/src/_macosx.m index b72d6efd4e0d..7f3b67b40a3d 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -345,14 +345,7 @@ static CGFloat _get_device_scale(CGContextRef cr) } static PyObject* -FigureCanvas_draw(FigureCanvas* self) -{ - [self->view display]; - Py_RETURN_NONE; -} - -static PyObject* -FigureCanvas_draw_idle(FigureCanvas* self) +FigureCanvas_update(FigureCanvas* self) { [self->view setNeedsDisplay: YES]; Py_RETURN_NONE; @@ -361,6 +354,9 @@ static CGFloat _get_device_scale(CGContextRef cr) static PyObject* FigureCanvas_flush_events(FigureCanvas* self) { + // We need to allow the runloop to run very briefly + // to allow the view to be displayed when used in a fast updating animation + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0]]; [self->view displayIfNeeded]; Py_RETURN_NONE; } @@ -485,12 +481,8 @@ static CGFloat _get_device_scale(CGContextRef cr) .tp_new = (newfunc)FigureCanvas_new, .tp_doc = "A FigureCanvas object wraps a Cocoa NSView object.", .tp_methods = (PyMethodDef[]){ - {"draw", - (PyCFunction)FigureCanvas_draw, - METH_NOARGS, - NULL}, // docstring inherited - {"draw_idle", - (PyCFunction)FigureCanvas_draw_idle, + {"update", + (PyCFunction)FigureCanvas_update, METH_NOARGS, NULL}, // docstring inherited {"flush_events", @@ -1263,7 +1255,7 @@ -(void)drawRect:(NSRect)rect CGContextRef cr = [[NSGraphicsContext currentContext] CGContext]; - if (!(renderer = PyObject_CallMethod(canvas, "_draw", "")) + if (!(renderer = PyObject_CallMethod(canvas, "get_renderer", "")) || !(renderer_buffer = PyObject_GetAttrString(renderer, "_renderer"))) { PyErr_Print(); goto exit;
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: