backend_macosx.py 7.2 KB

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