blocking_input.py 11 KB

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