123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- 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"""
- # We need to explicitly stop and remove the timer after
- # firing, otherwise segfaults will occur when trying to deallocate
- # the singleshot timers.
- def callback_func(callback, timer):
- callback()
- self._timers.remove(timer)
- timer.stop()
- 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 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
|