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