backend_macosx.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import contextlib
  2. import os
  3. import signal
  4. import socket
  5. import matplotlib as mpl
  6. from matplotlib import _api, cbook
  7. from matplotlib._pylab_helpers import Gcf
  8. from . import _macosx
  9. from .backend_agg import FigureCanvasAgg
  10. from matplotlib.backend_bases import (
  11. _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
  12. ResizeEvent, TimerBase)
  13. class TimerMac(_macosx.Timer, TimerBase):
  14. """Subclass of `.TimerBase` using CFRunLoop timer events."""
  15. # completely implemented at the C-level (in _macosx.Timer)
  16. class FigureCanvasMac(FigureCanvasAgg, _macosx.FigureCanvas, FigureCanvasBase):
  17. # docstring inherited
  18. # Ideally this class would be `class FCMacAgg(FCAgg, FCMac)`
  19. # (FC=FigureCanvas) where FCMac would be an ObjC-implemented mac-specific
  20. # class also inheriting from FCBase (this is the approach with other GUI
  21. # toolkits). However, writing an extension type inheriting from a Python
  22. # base class is slightly tricky (the extension type must be a heap type),
  23. # and we can just as well lift the FCBase base up one level, keeping it *at
  24. # the end* to have the right method resolution order.
  25. # Events such as button presses, mouse movements, and key presses are
  26. # handled in C and events (MouseEvent, etc.) are triggered from there.
  27. required_interactive_framework = "macosx"
  28. _timer_cls = TimerMac
  29. manager_class = _api.classproperty(lambda cls: FigureManagerMac)
  30. def __init__(self, figure):
  31. super().__init__(figure=figure)
  32. self._draw_pending = False
  33. self._is_drawing = False
  34. # Keep track of the timers that are alive
  35. self._timers = set()
  36. def draw(self):
  37. """Render the figure and update the macosx canvas."""
  38. # The renderer draw is done here; delaying causes problems with code
  39. # that uses the result of the draw() to update plot elements.
  40. if self._is_drawing:
  41. return
  42. with cbook._setattr_cm(self, _is_drawing=True):
  43. super().draw()
  44. self.update()
  45. def draw_idle(self):
  46. # docstring inherited
  47. if not (getattr(self, '_draw_pending', False) or
  48. getattr(self, '_is_drawing', False)):
  49. self._draw_pending = True
  50. # Add a singleshot timer to the eventloop that will call back
  51. # into the Python method _draw_idle to take care of the draw
  52. self._single_shot_timer(self._draw_idle)
  53. def _single_shot_timer(self, callback):
  54. """Add a single shot timer with the given callback"""
  55. # We need to explicitly stop and remove the timer after
  56. # firing, otherwise segfaults will occur when trying to deallocate
  57. # the singleshot timers.
  58. def callback_func(callback, timer):
  59. callback()
  60. self._timers.remove(timer)
  61. timer.stop()
  62. timer = self.new_timer(interval=0)
  63. timer.single_shot = True
  64. timer.add_callback(callback_func, callback, timer)
  65. self._timers.add(timer)
  66. timer.start()
  67. def _draw_idle(self):
  68. """
  69. Draw method for singleshot timer
  70. This draw method can be added to a singleshot timer, which can
  71. accumulate draws while the eventloop is spinning. This method will
  72. then only draw the first time and short-circuit the others.
  73. """
  74. with self._idle_draw_cntx():
  75. if not self._draw_pending:
  76. # Short-circuit because our draw request has already been
  77. # taken care of
  78. return
  79. self._draw_pending = False
  80. self.draw()
  81. def blit(self, bbox=None):
  82. # docstring inherited
  83. super().blit(bbox)
  84. self.update()
  85. def resize(self, width, height):
  86. # Size from macOS is logical pixels, dpi is physical.
  87. scale = self.figure.dpi / self.device_pixel_ratio
  88. width /= scale
  89. height /= scale
  90. self.figure.set_size_inches(width, height, forward=False)
  91. ResizeEvent("resize_event", self)._process()
  92. self.draw_idle()
  93. def start_event_loop(self, timeout=0):
  94. # docstring inherited
  95. with _maybe_allow_interrupt():
  96. # Call the objc implementation of the event loop after
  97. # setting up the interrupt handling
  98. self._start_event_loop(timeout=timeout)
  99. class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2):
  100. def __init__(self, canvas):
  101. data_path = cbook._get_data_path('images')
  102. _, tooltips, image_names, _ = zip(*NavigationToolbar2.toolitems)
  103. _macosx.NavigationToolbar2.__init__(
  104. self, canvas,
  105. tuple(str(data_path / image_name) + ".pdf"
  106. for image_name in image_names if image_name is not None),
  107. tuple(tooltip for tooltip in tooltips if tooltip is not None))
  108. NavigationToolbar2.__init__(self, canvas)
  109. def draw_rubberband(self, event, x0, y0, x1, y1):
  110. self.canvas.set_rubberband(int(x0), int(y0), int(x1), int(y1))
  111. def remove_rubberband(self):
  112. self.canvas.remove_rubberband()
  113. def save_figure(self, *args):
  114. directory = os.path.expanduser(mpl.rcParams['savefig.directory'])
  115. filename = _macosx.choose_save_file('Save the figure',
  116. directory,
  117. self.canvas.get_default_filename())
  118. if filename is None: # Cancel
  119. return
  120. # Save dir for next time, unless empty str (which means use cwd).
  121. if mpl.rcParams['savefig.directory']:
  122. mpl.rcParams['savefig.directory'] = os.path.dirname(filename)
  123. self.canvas.figure.savefig(filename)
  124. class FigureManagerMac(_macosx.FigureManager, FigureManagerBase):
  125. _toolbar2_class = NavigationToolbar2Mac
  126. def __init__(self, canvas, num):
  127. self._shown = False
  128. _macosx.FigureManager.__init__(self, canvas)
  129. icon_path = str(cbook._get_data_path('images/matplotlib.pdf'))
  130. _macosx.FigureManager.set_icon(icon_path)
  131. FigureManagerBase.__init__(self, canvas, num)
  132. self._set_window_mode(mpl.rcParams["macosx.window_mode"])
  133. if self.toolbar is not None:
  134. self.toolbar.update()
  135. if mpl.is_interactive():
  136. self.show()
  137. self.canvas.draw_idle()
  138. def _close_button_pressed(self):
  139. Gcf.destroy(self)
  140. self.canvas.flush_events()
  141. def destroy(self):
  142. # We need to clear any pending timers that never fired, otherwise
  143. # we get a memory leak from the timer callbacks holding a reference
  144. while self.canvas._timers:
  145. timer = self.canvas._timers.pop()
  146. timer.stop()
  147. super().destroy()
  148. @classmethod
  149. def start_main_loop(cls):
  150. # Set up a SIGINT handler to allow terminating a plot via CTRL-C.
  151. # The logic is largely copied from qt_compat._maybe_allow_interrupt; see its
  152. # docstring for details. Parts are implemented by wake_on_fd_write in ObjC.
  153. with _maybe_allow_interrupt():
  154. _macosx.show()
  155. def show(self):
  156. if not self._shown:
  157. self._show()
  158. self._shown = True
  159. if mpl.rcParams["figure.raise_window"]:
  160. self._raise()
  161. @contextlib.contextmanager
  162. def _maybe_allow_interrupt():
  163. """
  164. This manager allows to terminate a plot by sending a SIGINT. It is
  165. necessary because the running backend prevents Python interpreter to
  166. run and process signals (i.e., to raise KeyboardInterrupt exception). To
  167. solve this one needs to somehow wake up the interpreter and make it close
  168. the plot window. The implementation is taken from qt_compat, see that
  169. docstring for a more detailed description.
  170. """
  171. old_sigint_handler = signal.getsignal(signal.SIGINT)
  172. if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
  173. yield
  174. return
  175. handler_args = None
  176. wsock, rsock = socket.socketpair()
  177. wsock.setblocking(False)
  178. rsock.setblocking(False)
  179. old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
  180. _macosx.wake_on_fd_write(rsock.fileno())
  181. def handle(*args):
  182. nonlocal handler_args
  183. handler_args = args
  184. _macosx.stop()
  185. signal.signal(signal.SIGINT, handle)
  186. try:
  187. yield
  188. finally:
  189. wsock.close()
  190. rsock.close()
  191. signal.set_wakeup_fd(old_wakeup_fd)
  192. signal.signal(signal.SIGINT, old_sigint_handler)
  193. if handler_args is not None:
  194. old_sigint_handler(*handler_args)
  195. @_Backend.export
  196. class _BackendMac(_Backend):
  197. FigureCanvas = FigureCanvasMac
  198. FigureManager = FigureManagerMac
  199. mainloop = FigureManagerMac.start_main_loop