235 lines
8.4 KiB
Python
235 lines
8.4 KiB
Python
import contextlib
|
|
import os
|
|
import signal
|
|
import socket
|
|
|
|
import matplotlib as mpl
|
|
from matplotlib import _api, cbook
|
|
from matplotlib._pylab_helpers import Gcf
|
|
from . import _macosx
|
|
from .backend_agg import FigureCanvasAgg
|
|
from matplotlib.backend_bases import (
|
|
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
|
|
ResizeEvent, TimerBase)
|
|
|
|
|
|
class TimerMac(_macosx.Timer, TimerBase):
|
|
"""Subclass of `.TimerBase` using CFRunLoop timer events."""
|
|
# completely implemented at the C-level (in _macosx.Timer)
|
|
|
|
|
|
class FigureCanvasMac(FigureCanvasAgg, _macosx.FigureCanvas, FigureCanvasBase):
|
|
# docstring inherited
|
|
|
|
# Ideally this class would be `class FCMacAgg(FCAgg, FCMac)`
|
|
# (FC=FigureCanvas) where FCMac would be an ObjC-implemented mac-specific
|
|
# class also inheriting from FCBase (this is the approach with other GUI
|
|
# toolkits). However, writing an extension type inheriting from a Python
|
|
# base class is slightly tricky (the extension type must be a heap type),
|
|
# and we can just as well lift the FCBase base up one level, keeping it *at
|
|
# the end* to have the right method resolution order.
|
|
|
|
# Events such as button presses, mouse movements, and key presses are
|
|
# handled in C and events (MouseEvent, etc.) are triggered from there.
|
|
|
|
required_interactive_framework = "macosx"
|
|
_timer_cls = TimerMac
|
|
manager_class = _api.classproperty(lambda cls: FigureManagerMac)
|
|
|
|
def __init__(self, figure):
|
|
super().__init__(figure=figure)
|
|
self._draw_pending = False
|
|
self._is_drawing = False
|
|
# Keep track of the timers that are alive
|
|
self._timers = set()
|
|
|
|
def draw(self):
|
|
"""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()
|
|
|
|
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"""
|
|
def callback_func(callback, timer):
|
|
callback()
|
|
self._timers.remove(timer)
|
|
timer = self.new_timer(interval=0)
|
|
timer.single_shot = True
|
|
timer.add_callback(callback_func, callback, timer)
|
|
self._timers.add(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):
|
|
# docstring inherited
|
|
super().blit(bbox)
|
|
self.update()
|
|
|
|
def resize(self, width, height):
|
|
# Size from macOS is logical pixels, dpi is physical.
|
|
scale = self.figure.dpi / self.device_pixel_ratio
|
|
width /= scale
|
|
height /= scale
|
|
self.figure.set_size_inches(width, height, forward=False)
|
|
ResizeEvent("resize_event", self)._process()
|
|
self.draw_idle()
|
|
|
|
def start_event_loop(self, timeout=0):
|
|
# docstring inherited
|
|
with _maybe_allow_interrupt():
|
|
# Call the objc implementation of the event loop after
|
|
# setting up the interrupt handling
|
|
self._start_event_loop(timeout=timeout)
|
|
|
|
|
|
class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2):
|
|
|
|
def __init__(self, canvas):
|
|
data_path = cbook._get_data_path('images')
|
|
_, tooltips, image_names, _ = zip(*NavigationToolbar2.toolitems)
|
|
_macosx.NavigationToolbar2.__init__(
|
|
self, canvas,
|
|
tuple(str(data_path / image_name) + ".pdf"
|
|
for image_name in image_names if image_name is not None),
|
|
tuple(tooltip for tooltip in tooltips if tooltip is not None))
|
|
NavigationToolbar2.__init__(self, canvas)
|
|
|
|
def draw_rubberband(self, event, x0, y0, x1, y1):
|
|
self.canvas.set_rubberband(int(x0), int(y0), int(x1), int(y1))
|
|
|
|
def remove_rubberband(self):
|
|
self.canvas.remove_rubberband()
|
|
|
|
def save_figure(self, *args):
|
|
directory = os.path.expanduser(mpl.rcParams['savefig.directory'])
|
|
filename = _macosx.choose_save_file('Save the figure',
|
|
directory,
|
|
self.canvas.get_default_filename())
|
|
if filename is None: # Cancel
|
|
return
|
|
# Save dir for next time, unless empty str (which means use cwd).
|
|
if mpl.rcParams['savefig.directory']:
|
|
mpl.rcParams['savefig.directory'] = os.path.dirname(filename)
|
|
self.canvas.figure.savefig(filename)
|
|
|
|
|
|
class FigureManagerMac(_macosx.FigureManager, FigureManagerBase):
|
|
_toolbar2_class = NavigationToolbar2Mac
|
|
|
|
def __init__(self, canvas, num):
|
|
self._shown = False
|
|
_macosx.FigureManager.__init__(self, canvas)
|
|
icon_path = str(cbook._get_data_path('images/matplotlib.pdf'))
|
|
_macosx.FigureManager.set_icon(icon_path)
|
|
FigureManagerBase.__init__(self, canvas, num)
|
|
self._set_window_mode(mpl.rcParams["macosx.window_mode"])
|
|
if self.toolbar is not None:
|
|
self.toolbar.update()
|
|
if mpl.is_interactive():
|
|
self.show()
|
|
self.canvas.draw_idle()
|
|
|
|
def _close_button_pressed(self):
|
|
Gcf.destroy(self)
|
|
self.canvas.flush_events()
|
|
|
|
def destroy(self):
|
|
# We need to clear any pending timers that never fired, otherwise
|
|
# we get a memory leak from the timer callbacks holding a reference
|
|
while self.canvas._timers:
|
|
timer = self.canvas._timers.pop()
|
|
timer.stop()
|
|
super().destroy()
|
|
|
|
@classmethod
|
|
def start_main_loop(cls):
|
|
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
|
|
# The logic is largely copied from qt_compat._maybe_allow_interrupt; see its
|
|
# docstring for details. Parts are implemented by wake_on_fd_write in ObjC.
|
|
with _maybe_allow_interrupt():
|
|
_macosx.show()
|
|
|
|
def show(self):
|
|
if self.canvas.figure.stale:
|
|
self.canvas.draw_idle()
|
|
if not self._shown:
|
|
self._show()
|
|
self._shown = True
|
|
if mpl.rcParams["figure.raise_window"]:
|
|
self._raise()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _maybe_allow_interrupt():
|
|
"""
|
|
This manager allows to terminate a plot by sending a SIGINT. It is
|
|
necessary because the running backend prevents Python interpreter to
|
|
run and process signals (i.e., to raise KeyboardInterrupt exception). To
|
|
solve this one needs to somehow wake up the interpreter and make it close
|
|
the plot window. The implementation is taken from qt_compat, see that
|
|
docstring for a more detailed description.
|
|
"""
|
|
old_sigint_handler = signal.getsignal(signal.SIGINT)
|
|
if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
|
|
yield
|
|
return
|
|
|
|
handler_args = None
|
|
wsock, rsock = socket.socketpair()
|
|
wsock.setblocking(False)
|
|
rsock.setblocking(False)
|
|
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
|
|
_macosx.wake_on_fd_write(rsock.fileno())
|
|
|
|
def handle(*args):
|
|
nonlocal handler_args
|
|
handler_args = args
|
|
_macosx.stop()
|
|
|
|
signal.signal(signal.SIGINT, handle)
|
|
try:
|
|
yield
|
|
finally:
|
|
wsock.close()
|
|
rsock.close()
|
|
signal.set_wakeup_fd(old_wakeup_fd)
|
|
signal.signal(signal.SIGINT, old_sigint_handler)
|
|
if handler_args is not None:
|
|
old_sigint_handler(*handler_args)
|
|
|
|
|
|
@_Backend.export
|
|
class _BackendMac(_Backend):
|
|
FigureCanvas = FigureCanvasMac
|
|
FigureManager = FigureManagerMac
|
|
mainloop = FigureManagerMac.start_main_loop
|