blocking_input.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. """
  2. Classes used for blocking interaction with figure windows:
  3. `BlockingInput`
  4. Creates a callable object to retrieve events in a blocking way for
  5. interactive sessions. Base class of the other classes listed here.
  6. `BlockingKeyMouseInput`
  7. Creates a callable object to retrieve key or mouse clicks in a blocking
  8. way for interactive sessions. Used by `~.Figure.waitforbuttonpress`.
  9. `BlockingMouseInput`
  10. Creates a callable object to retrieve mouse clicks in a blocking way for
  11. interactive sessions. Used by `~.Figure.ginput`.
  12. `BlockingContourLabeler`
  13. Creates a callable object to retrieve mouse clicks in a blocking way that
  14. will then be used to place labels on a `.ContourSet`. Used by
  15. `~.Axes.clabel`.
  16. """
  17. import logging
  18. from numbers import Integral
  19. from matplotlib import cbook
  20. from matplotlib.backend_bases import MouseButton
  21. import matplotlib.lines as mlines
  22. _log = logging.getLogger(__name__)
  23. class BlockingInput:
  24. """Callable for retrieving events in a blocking way."""
  25. def __init__(self, fig, eventslist=()):
  26. self.fig = fig
  27. self.eventslist = eventslist
  28. def on_event(self, event):
  29. """
  30. Event handler; will be passed to the current figure to retrieve events.
  31. """
  32. # Add a new event to list - using a separate function is overkill for
  33. # the base class, but this is consistent with subclasses.
  34. self.add_event(event)
  35. _log.info("Event %i", len(self.events))
  36. # This will extract info from events.
  37. self.post_event()
  38. # Check if we have enough events already.
  39. if len(self.events) >= self.n > 0:
  40. self.fig.canvas.stop_event_loop()
  41. def post_event(self):
  42. """For baseclass, do nothing but collect events."""
  43. def cleanup(self):
  44. """Disconnect all callbacks."""
  45. for cb in self.callbacks:
  46. self.fig.canvas.mpl_disconnect(cb)
  47. self.callbacks = []
  48. def add_event(self, event):
  49. """For base class, this just appends an event to events."""
  50. self.events.append(event)
  51. def pop_event(self, index=-1):
  52. """
  53. Remove an event from the event list -- by default, the last.
  54. Note that this does not check that there are events, much like the
  55. normal pop method. If no events exist, this will throw an exception.
  56. """
  57. self.events.pop(index)
  58. pop = pop_event
  59. def __call__(self, n=1, timeout=30):
  60. """Blocking call to retrieve *n* events."""
  61. cbook._check_isinstance(Integral, n=n)
  62. self.n = n
  63. self.events = []
  64. if hasattr(self.fig.canvas, "manager"):
  65. # Ensure that the figure is shown, if we are managing it.
  66. self.fig.show()
  67. # Connect the events to the on_event function call.
  68. self.callbacks = [self.fig.canvas.mpl_connect(name, self.on_event)
  69. for name in self.eventslist]
  70. try:
  71. # Start event loop.
  72. self.fig.canvas.start_event_loop(timeout=timeout)
  73. finally: # Run even on exception like ctrl-c.
  74. # Disconnect the callbacks.
  75. self.cleanup()
  76. # Return the events in this case.
  77. return self.events
  78. class BlockingMouseInput(BlockingInput):
  79. """
  80. Callable for retrieving mouse clicks in a blocking way.
  81. This class will also retrieve keypresses and map them to mouse clicks:
  82. delete and backspace are a right click, enter is like a middle click,
  83. and all others are like a left click.
  84. """
  85. button_add = MouseButton.LEFT
  86. button_pop = MouseButton.RIGHT
  87. button_stop = MouseButton.MIDDLE
  88. def __init__(self, fig,
  89. mouse_add=MouseButton.LEFT,
  90. mouse_pop=MouseButton.RIGHT,
  91. mouse_stop=MouseButton.MIDDLE):
  92. BlockingInput.__init__(self, fig=fig,
  93. eventslist=('button_press_event',
  94. 'key_press_event'))
  95. self.button_add = mouse_add
  96. self.button_pop = mouse_pop
  97. self.button_stop = mouse_stop
  98. def post_event(self):
  99. """Process an event."""
  100. if len(self.events) == 0:
  101. _log.warning("No events yet")
  102. elif self.events[-1].name == 'key_press_event':
  103. self.key_event()
  104. else:
  105. self.mouse_event()
  106. def mouse_event(self):
  107. """Process a mouse click event."""
  108. event = self.events[-1]
  109. button = event.button
  110. if button == self.button_pop:
  111. self.mouse_event_pop(event)
  112. elif button == self.button_stop:
  113. self.mouse_event_stop(event)
  114. elif button == self.button_add:
  115. self.mouse_event_add(event)
  116. def key_event(self):
  117. """
  118. Process a key press event, mapping keys to appropriate mouse clicks.
  119. """
  120. event = self.events[-1]
  121. if event.key is None:
  122. # At least in OSX gtk backend some keys return None.
  123. return
  124. key = event.key.lower()
  125. if key in ['backspace', 'delete']:
  126. self.mouse_event_pop(event)
  127. elif key in ['escape', 'enter']:
  128. self.mouse_event_stop(event)
  129. else:
  130. self.mouse_event_add(event)
  131. def mouse_event_add(self, event):
  132. """
  133. Process an button-1 event (add a click if inside axes).
  134. Parameters
  135. ----------
  136. event : `~.backend_bases.MouseEvent`
  137. """
  138. if event.inaxes:
  139. self.add_click(event)
  140. else: # If not a valid click, remove from event list.
  141. BlockingInput.pop(self)
  142. def mouse_event_stop(self, event):
  143. """
  144. Process an button-2 event (end blocking input).
  145. Parameters
  146. ----------
  147. event : `~.backend_bases.MouseEvent`
  148. """
  149. # Remove last event just for cleanliness.
  150. BlockingInput.pop(self)
  151. # This will exit even if not in infinite mode. This is consistent with
  152. # MATLAB and sometimes quite useful, but will require the user to test
  153. # how many points were actually returned before using data.
  154. self.fig.canvas.stop_event_loop()
  155. def mouse_event_pop(self, event):
  156. """
  157. Process an button-3 event (remove the last click).
  158. Parameters
  159. ----------
  160. event : `~.backend_bases.MouseEvent`
  161. """
  162. # Remove this last event.
  163. BlockingInput.pop(self)
  164. # Now remove any existing clicks if possible.
  165. if self.events:
  166. self.pop(event)
  167. def add_click(self, event):
  168. """
  169. Add the coordinates of an event to the list of clicks.
  170. Parameters
  171. ----------
  172. event : `~.backend_bases.MouseEvent`
  173. """
  174. self.clicks.append((event.xdata, event.ydata))
  175. _log.info("input %i: %f, %f",
  176. len(self.clicks), event.xdata, event.ydata)
  177. # If desired, plot up click.
  178. if self.show_clicks:
  179. line = mlines.Line2D([event.xdata], [event.ydata],
  180. marker='+', color='r')
  181. event.inaxes.add_line(line)
  182. self.marks.append(line)
  183. self.fig.canvas.draw()
  184. def pop_click(self, event, index=-1):
  185. """
  186. Remove a click (by default, the last) from the list of clicks.
  187. Parameters
  188. ----------
  189. event : `~.backend_bases.MouseEvent`
  190. """
  191. self.clicks.pop(index)
  192. if self.show_clicks:
  193. self.marks.pop(index).remove()
  194. self.fig.canvas.draw()
  195. def pop(self, event, index=-1):
  196. """
  197. Remove a click and the associated event from the list of clicks.
  198. Defaults to the last click.
  199. """
  200. self.pop_click(event, index)
  201. BlockingInput.pop(self, index)
  202. def cleanup(self, event=None):
  203. """
  204. Parameters
  205. ----------
  206. event : `~.backend_bases.MouseEvent`, optional
  207. Not used
  208. """
  209. # Clean the figure.
  210. if self.show_clicks:
  211. for mark in self.marks:
  212. mark.remove()
  213. self.marks = []
  214. self.fig.canvas.draw()
  215. # Call base class to remove callbacks.
  216. BlockingInput.cleanup(self)
  217. def __call__(self, n=1, timeout=30, show_clicks=True):
  218. """
  219. Blocking call to retrieve *n* coordinate pairs through mouse clicks.
  220. """
  221. self.show_clicks = show_clicks
  222. self.clicks = []
  223. self.marks = []
  224. BlockingInput.__call__(self, n=n, timeout=timeout)
  225. return self.clicks
  226. class BlockingContourLabeler(BlockingMouseInput):
  227. """
  228. Callable for retrieving mouse clicks and key presses in a blocking way.
  229. Used to place contour labels.
  230. """
  231. def __init__(self, cs):
  232. self.cs = cs
  233. BlockingMouseInput.__init__(self, fig=cs.ax.figure)
  234. def add_click(self, event):
  235. self.button1(event)
  236. def pop_click(self, event, index=-1):
  237. self.button3(event)
  238. def button1(self, event):
  239. """
  240. Process an button-1 event (add a label to a contour).
  241. Parameters
  242. ----------
  243. event : `~.backend_bases.MouseEvent`
  244. """
  245. # Shorthand
  246. if event.inaxes == self.cs.ax:
  247. self.cs.add_label_near(event.x, event.y, self.inline,
  248. inline_spacing=self.inline_spacing,
  249. transform=False)
  250. self.fig.canvas.draw()
  251. else: # Remove event if not valid
  252. BlockingInput.pop(self)
  253. def button3(self, event):
  254. """
  255. Process an button-3 event (remove a label if not in inline mode).
  256. Unfortunately, if one is doing inline labels, then there is currently
  257. no way to fix the broken contour - once humpty-dumpty is broken, he
  258. can't be put back together. In inline mode, this does nothing.
  259. Parameters
  260. ----------
  261. event : `~.backend_bases.MouseEvent`
  262. """
  263. if self.inline:
  264. pass
  265. else:
  266. self.cs.pop_label()
  267. self.cs.ax.figure.canvas.draw()
  268. def __call__(self, inline, inline_spacing=5, n=-1, timeout=-1):
  269. self.inline = inline
  270. self.inline_spacing = inline_spacing
  271. BlockingMouseInput.__call__(self, n=n, timeout=timeout,
  272. show_clicks=False)
  273. class BlockingKeyMouseInput(BlockingInput):
  274. """
  275. Callable for retrieving mouse clicks and key presses in a blocking way.
  276. """
  277. def __init__(self, fig):
  278. BlockingInput.__init__(self, fig=fig, eventslist=(
  279. 'button_press_event', 'key_press_event'))
  280. def post_event(self):
  281. """Determine if it is a key event."""
  282. if self.events:
  283. self.keyormouse = self.events[-1].name == 'key_press_event'
  284. else:
  285. _log.warning("No events yet.")
  286. def __call__(self, timeout=30):
  287. """
  288. Blocking call to retrieve a single mouse click or key press.
  289. Returns ``True`` if key press, ``False`` if mouse click, or ``None`` if
  290. timed out.
  291. """
  292. self.keyormouse = None
  293. BlockingInput.__call__(self, n=1, timeout=timeout)
  294. return self.keyormouse