Use bboxes and transforms to update scrollbars adjustments
[oscopy.git] / oscopy / gtk_figure.py
blob2ec99fc39a0436e8286e6857856909051c961c77
2 import oscopy
3 import gtk
4 import gobject
5 import gui
6 from math import log10, sqrt
7 from matplotlib.backend_bases import LocationEvent
8 from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
9 from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
10 from matplotlib.widgets import SpanSelector, RectangleSelector
12 IOSCOPY_COL_TEXT = 0 # Combo box text
13 IOSCOPY_COL_X10 = 1 # x10 mode status
14 IOSCOPY_COL_VIS = 2 # Combobox items sensitive
15 IOSCOPY_COL_SPAN = 3 # Span mode status
16 IOSCOPY_COL_HADJ = 4 # Horizontal scrollbar adjustment
17 IOSCOPY_COL_VADJ = 5 # Vertical scrollbar adjustment
19 DEFAULT_ZOOM_FACTOR = 0.8
20 DEFAULT_PAN_FACTOR = 10
22 class MyRectangleSelector(RectangleSelector):
23 """ FIXME: To be removed once upstream has merged PR #658
24 https://github.com/matplotlib/matplotlib/pull/658
25 """
27 def ignore(self, event):
28 'return ``True`` if *event* should be ignored'
29 # If RectangleSelector is not active :
30 if not self.active:
31 return True
33 # If canvas was locked
34 if not self.canvas.widgetlock.available(self):
35 return True
37 # Only do rectangle selection if event was triggered
38 # with a desired button
39 if self.validButtons is not None:
40 if not event.button in self.validButtons:
41 return True
43 # If no button was pressed yet ignore the event if it was out
44 # of the axes
45 if self.eventpress == None:
46 return event.inaxes!= self.ax
48 # If a button was pressed, check if the release-button is the
49 # same. If event is out of axis, limit the data coordinates to axes
50 # boundaries.
51 if event.button == self.eventpress.button and event.inaxes != self.ax:
52 (xdata, ydata) = self.ax.transData.inverted().transform_point((event.x, event.y))
53 x0, x1 = self.ax.get_xbound()
54 y0, y1 = self.ax.get_ybound()
55 xdata = max(x0, xdata)
56 xdata = min(x1, xdata)
57 ydata = max(y0, ydata)
58 ydata = min(y1, ydata)
59 event.xdata = xdata
60 event.ydata = ydata
61 return False
63 # If a button was pressed, check if the release-button is the
64 # same.
65 return (event.inaxes!=self.ax or
66 event.button != self.eventpress.button)
68 class IOscopy_GTK_Figure(oscopy.Figure):
69 def __init__(self, sigs={}, fig=None, title=''):
70 oscopy.Figure.__init__(self, None, fig)
71 self._TARGET_TYPE_SIGNAL = 10354
72 self._to_figure = [("oscopy-signals", gtk.TARGET_SAME_APP,\
73 self._TARGET_TYPE_SIGNAL)]
75 w = gtk.Window()
76 w.set_title(title)
78 hbox1 = gtk.HBox() # The window
79 vbox1 = gtk.VBox() # The Graphs
80 hbox1.pack_start(vbox1)
81 w.add(hbox1)
82 canvas = FigureCanvas(self)
83 canvas.mpl_connect('button_press_event', self._button_press)
84 canvas.mpl_connect('scroll_event', self._mouse_scroll)
85 canvas.mpl_connect('axes_enter_event', self._axes_enter)
86 canvas.mpl_connect('axes_leave_event', self._axes_leave)
87 canvas.mpl_connect('figure_enter_event', self._figure_enter)
88 canvas.mpl_connect('figure_leave_event', self._figure_leave)
89 canvas.mpl_connect('key_press_event', self._key_press)
90 canvas.mpl_connect('draw_event', self._update_scrollbars)
91 w.connect('delete-event', lambda w, e: w.hide() or True)
92 w.drag_dest_set(gtk.DEST_DEFAULT_MOTION |\
93 gtk.DEST_DEFAULT_HIGHLIGHT |\
94 gtk.DEST_DEFAULT_DROP,
95 self._to_figure, gtk.gdk.ACTION_COPY)
97 hbar = gtk.HScrollbar()
98 hbar.set_sensitive(False)
99 self.hbar = hbar
100 vbox1.pack_start(hbar, False, False)
101 vbar = gtk.VScrollbar()
102 vbar.set_sensitive(False)
103 self.vbar = vbar
104 hbox1.pack_start(vbar, False, False)
106 vbox1.pack_start(canvas)
108 toolbar = NavigationToolbar(canvas, w)
109 vbox1.pack_start(toolbar, False, False)
111 vbox2 = gtk.VBox() # The right-side menu
112 store = gtk.ListStore(gobject.TYPE_STRING, # String displayed
113 gobject.TYPE_BOOLEAN, # x10 mode status
114 gobject.TYPE_BOOLEAN, # Combobox item sensitive
115 gobject.TYPE_BOOLEAN, # Span mode status
116 gobject.TYPE_PYOBJECT, # Horizontal Adjustment
117 gobject.TYPE_PYOBJECT, # Vertical Adjustment
119 iter = store.append([_('All Graphs'), False, True, False, gtk.Adjustment(), gtk.Adjustment()])
120 for i in xrange(4):
121 iter = store.append([_('Graph %d') % (i + 1), False, True if i < len(self.graphs) else False, False, gtk.Adjustment(), gtk.Adjustment()])
122 self._cbx_store = store
124 graphs_cbx = gtk.ComboBox(store)
125 cell = gtk.CellRendererText()
126 graphs_cbx.pack_start(cell, True)
127 graphs_cbx.add_attribute(cell, 'text', IOSCOPY_COL_TEXT)
128 graphs_cbx.add_attribute(cell, 'sensitive', IOSCOPY_COL_VIS)
129 graphs_cbx.set_active(0)
130 vbox2.pack_start(graphs_cbx, False, False)
132 x10_toggle_btn = gtk.ToggleButton('x10 mode')
133 x10_toggle_btn.set_mode(True)
134 x10_toggle_btn.connect('toggled', self.x10_toggle_btn_toggled,
135 graphs_cbx, store)
137 span_toggle_btn = gtk.ToggleButton(_('Span'))
138 span_toggle_btn.set_mode(True)
139 span_toggle_btn.connect('toggled', self.span_toggle_btn_toggled,
140 graphs_cbx, store)
142 self._cbx = graphs_cbx
143 self._btn = x10_toggle_btn
145 graphs_cbx.connect('changed', self.graphs_cbx_changed, x10_toggle_btn,
146 span_toggle_btn, store)
147 vbox2.pack_start(x10_toggle_btn, False, False)
148 vbox2.pack_start(span_toggle_btn, False, False)
150 hbox1.pack_start(vbox2, False, False)
152 w.resize(640, 480)
153 w.show_all()
154 self.window = w
155 if sigs:
156 self.add(sigs)
157 # # Update canvas for SpanSelector of Graphs
158 for gr in self.graphs:
159 if hasattr(gr, 'span'):
160 gr.span.new_axes(gr)
162 def set_layout(self, layout='quad'):
163 oscopy.Figure.set_layout(self, layout)
164 iter = self._cbx_store.get_iter_first()
165 for g in self.graphs:
166 iter = self._cbx_store.iter_next(iter)
167 if self._layout == 'horiz':
168 g.span = SpanSelector(g, g.onselect, 'horizontal',
169 useblit=True)
170 g.span.visible = self._cbx_store.get_value(iter, IOSCOPY_COL_SPAN)
171 self.hbar.set_sensitive(True)
172 self.hbar.show()
173 self.vbar.set_sensitive(False)
174 self.vbar.hide()
175 elif self._layout == 'vert':
176 g.span = SpanSelector(g, g.onselect, 'vertical',
177 useblit=True)
178 g.span.visible = self._cbx_store.get_value(iter, IOSCOPY_COL_SPAN)
179 self.hbar.set_sensitive(False)
180 self.hbar.hide()
181 self.vbar.set_sensitive(True)
182 self.vbar.show()
183 elif self._layout == 'quad':
184 g.span = MyRectangleSelector(g, g.onselect, rectprops=dict(facecolor='red', edgecolor = 'black', alpha=0.5, fill=True),
185 useblit=True)
186 g.span.active = self._cbx_store.get_value(iter, IOSCOPY_COL_SPAN)
187 self.hbar.set_sensitive(True)
188 self.hbar.show()
189 self.vbar.set_sensitive(True)
190 self.vbar.show()
192 def graphs_cbx_changed(self, graphs_cbx, x10_toggle_btn, span_toggle_btn, store):
193 iter = graphs_cbx.get_active_iter()
194 if store.get_string_from_iter(iter) == '0':
195 # Do all the graphs have same state?
196 store.iter_next(iter)
197 val = store.get_value(iter, IOSCOPY_COL_X10)
198 while iter is not None:
199 if val != store.get_value(iter, IOSCOPY_COL_X10):
200 # Yes, set the button into inconsistent state
201 x10_toggle_btn.set_inconsistent(True)
202 break
203 iter = store.iter_next(iter)
204 iter = store.get_iter_first()
205 store.iter_next(iter)
206 val = store.get_value(iter, IOSCOPY_COL_SPAN)
207 while iter is not None:
208 if val != store.get_value(iter, IOSCOPY_COL_SPAN):
209 # Yes, set the button into inconsistent state
210 span_toggle_btn.set_inconsistent(True)
211 break
212 iter = store.iter_next(iter)
213 self.hbar.set_adjustment(store.get_value(iter, IOSCOPY_COL_HADJ))
214 self.vbar.set_adjustment(store.get_value(iter, IOSCOPY_COL_VADJ))
215 else:
216 x10_toggle_btn.set_inconsistent(False)
217 x10_toggle_btn.set_active(store.get_value(iter, IOSCOPY_COL_X10))
218 span_toggle_btn.set_inconsistent(False)
219 span_toggle_btn.set_active(store.get_value(iter, IOSCOPY_COL_SPAN))
220 self.hbar.set_adjustment(store.get_value(iter, IOSCOPY_COL_HADJ))
221 self.vbar.set_adjustment(store.get_value(iter, IOSCOPY_COL_VADJ))
223 def x10_toggle_btn_toggled(self, x10_toggle_btn, graphs_cbx, store):
224 center = None
225 iter = graphs_cbx.get_active_iter()
226 a = x10_toggle_btn.get_active()
227 val = -.1 if a else -1 # x10 zoom
228 x10_toggle_btn.set_inconsistent(False)
229 layout = self.layout
230 if iter is not None:
231 store.set_value(iter, IOSCOPY_COL_X10, a)
232 grnum = int(store.get_string_from_iter(iter))
233 if store.get_string_from_iter(iter) == '0':
234 # Set the value for all graphs
235 iter = store.iter_next(iter)
236 while iter is not None:
237 store.set_value(iter, IOSCOPY_COL_X10, a)
238 grnum = int(store.get_string_from_iter(iter))
239 if grnum > len(self.graphs):
240 break
241 g = self.graphs[grnum - 1]
242 self._zoom_x10(g, a)
243 iter = store.iter_next(iter)
244 else:
245 g = self.graphs[grnum - 1]
246 self._zoom_x10(g, a)
247 self.canvas.draw_idle()
249 def _zoom_x10(self, g, a):
250 layout = self._layout
251 result = g.dataLim.frozen()
252 if layout == 'horiz' or layout == 'quad':
253 g.set_xlim(*result.intervalx)
254 if layout == 'vert' or layout == 'quad':
255 g.set_ylim(*result.intervaly)
256 if a:
257 result = g.bbox.expanded(0.1, 0.1).transformed(g.transData.inverted())
258 if layout == 'horiz' or layout == 'quad':
259 g.set_xlim(*result.intervalx)
260 if layout == 'vert' or layout == 'quad':
261 g.set_ylim(*result.intervaly)
263 def span_toggle_btn_toggled(self, span_toggle_btn, graphs_cbx, store):
264 iter = graphs_cbx.get_active_iter()
265 a = span_toggle_btn.get_active()
266 span_toggle_btn.set_inconsistent(False)
267 if iter is not None:
268 store.set_value(iter, IOSCOPY_COL_SPAN, a)
269 grnum = int(store.get_string_from_iter(iter))
270 if store.get_string_from_iter(iter) == '0':
271 # Set the value for all graphs
272 iter = store.iter_next(iter)
273 while iter is not None:
274 store.set_value(iter, IOSCOPY_COL_SPAN, a)
275 grnum = int(store.get_string_from_iter(iter))
276 if grnum > len(self.graphs):
277 break
278 if hasattr(self.graphs[grnum - 1].span, 'active'):
279 self.graphs[grnum - 1].span.active = a
280 elif hasattr(self.graphs[grnum - 1].span, 'visible'):
281 self.graphs[grnum - 1].span.visible = a
282 iter = store.iter_next(iter)
283 else:
284 if hasattr(self.graphs[grnum - 1].span, 'active'):
285 self.graphs[grnum - 1].span.active = a
286 elif hasattr(self.graphs[grnum - 1].span, 'visible'):
287 self.graphs[grnum - 1].span.visible = a
288 self.canvas.draw()
290 def _key_press(self, event):
291 if event.inaxes is not None:
292 g = event.inaxes
293 if event.key == 'z':
294 self._zoom_on_event(event, DEFAULT_ZOOM_FACTOR)
295 self.canvas.draw_idle()
296 elif event.key == 'Z':
297 self._zoom_on_event(event, 1. / DEFAULT_ZOOM_FACTOR)
298 self.canvas.draw_idle()
299 elif event.key == 'l':
300 result = g.bbox.translated(-DEFAULT_PAN_FACTOR, 0).transformed(g.transData.inverted())
301 g.set_xlim(*result.intervalx)
302 g.set_ylim(*result.intervaly)
303 self.canvas.draw_idle()
304 elif event.key == 'r':
305 result = g.bbox.translated(DEFAULT_PAN_FACTOR, 0).transformed(g.transData.inverted())
306 g.set_xlim(*result.intervalx)
307 g.set_ylim(*result.intervaly)
308 self.canvas.draw_idle()
309 return True
311 def _zoom_on_event(self, event, factor):
312 g = event.inaxes
313 if g is None or factor == 1:
314 return
315 layout = self.layout
316 result = g.bbox.expanded(factor, factor).transformed(g.transData.inverted())
317 # Localisation of event.xdata in the new transform
318 if layout == 'horiz' or layout == 'quad':
319 g.set_xlim(*result.intervalx)
320 if layout == 'vert' or layout == 'quad':
321 g.set_ylim(*result.intervaly)
322 # Then place it under cursor
323 b = g.transData.transform_point([event.xdata, event.ydata])
324 result = g.bbox.translated(-(event.x - b[0]), -(event.y - b[1])).transformed(g.transData.inverted())
325 if layout == 'horiz' or layout == 'quad':
326 g.set_xlim(*result.intervalx)
327 if layout == 'vert' or layout == 'quad':
328 g.set_ylim(*result.intervaly)
329 # Limit to data boundaries
330 (dxmin, dxmax) = (g.dataLim.xmin, g.dataLim.xmax)
331 (xmin, xmax) = g.get_xbound()
332 g.set_xbound(max(dxmin, xmin), min(dxmax, xmax))
333 (dymin, dymax) = (g.dataLim.ymin, g.dataLim.ymax)
334 (ymin, ymax) = g.get_ybound()
335 g.set_ybound(max(dymin, ymin), min(dymax, ymax))
337 def drag_data_received_cb(self, widget, drag_context, x, y, selection,\
338 target_type, time, ctxtsignals):
339 # Event handling issue: this drag and drop callback is
340 # processed before matplotlib callback _axes_enter. Therefore
341 # when dropping, self._current_graph is not valid: it contains
342 # the last graph.
343 # The workaround is to retrieve the Graph by creating a Matplotlib
344 # LocationEvent considering inverse 'y' coordinates
345 if target_type == self._TARGET_TYPE_SIGNAL:
346 canvas = self.canvas
347 my_y = canvas.allocation.height - y
348 event = LocationEvent('axes_enter_event', canvas, x, my_y)
349 signals = {}
350 for name in selection.data.split():
351 signals[name] = ctxtsignals[name]
352 if event.inaxes is not None:
353 # Graph not found
354 event.inaxes.insert(signals)
355 self.canvas.draw()
357 def add(self, args):
358 oscopy.Figure.add(self, args)
359 store = self._cbx_store
360 iter = store.get_iter_first()
361 iter = store.iter_next(iter) # First item always sensitive
362 while iter is not None:
363 grnum = int(store.get_string_from_iter(iter))
364 if grnum > len(self.graphs):
365 store.set_value(iter, IOSCOPY_COL_VIS, False)
366 else:
367 store.set_value(iter, IOSCOPY_COL_VIS, True)
368 iter = store.iter_next(iter)
370 def delete(self, args):
371 oscopy.Figure.delete(self, args)
372 store = self._cbx_store
373 iter = store.get_iter_first()
374 iter = store.iter_next(iter) # First item always sensitive
375 while iter is not None:
376 grnum = int(store.get_string_from_iter(iter))
377 if grnum > len(self.graphs):
378 store.set_value(iter, IOSCOPY_COL_VIS, False)
379 else:
380 store.set_value(iter, IOSCOPY_COL_VIS, True)
381 iter = store.iter_next(iter)
383 def _button_press(self, event):
384 if event.button == 3:
385 menu = self._create_figure_popup_menu(event.canvas.figure, event.inaxes)
386 menu.show_all()
387 menu.popup(None, None, None, event.button, event.guiEvent.time)
389 def _mouse_scroll(self, event):
390 if event.button == 'up':
391 if event.inaxes is None:
392 return False
393 self._zoom_on_event(event, DEFAULT_ZOOM_FACTOR)
394 self.canvas.draw_idle()
395 elif event.button == 'down':
396 if event.inaxes is None:
397 return False
398 self._zoom_on_event(event, 1. / DEFAULT_ZOOM_FACTOR)
399 self.canvas.draw_idle()
400 return True
402 def _axes_enter(self, event):
403 # self._figure_enter(event)
404 # self._current_graph = event.inaxes
406 # axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
407 # fig_num = self._ctxt.figures.index(self._current_figure) + 1
408 # self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
409 pass
411 def _axes_leave(self, event):
412 # Unused for better user interaction
413 # self._current_graph = None
414 pass
416 def _figure_enter(self, event):
417 # self._current_figure = event.canvas.figure
418 # if hasattr(event, 'inaxes') and event.inaxes is not None:
419 # axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
420 # else:
421 # axes_num = 1
422 # fig_num = self._ctxt.figures.index(self._current_figure) + 1
423 # self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
424 self.canvas.grab_focus()
425 pass
427 def _figure_leave(self, event):
428 # self._current_figure = None
429 pass
431 def _create_figure_popup_menu(self, figure, graph):
432 figmenu = gui.menus.FigureMenu()
433 return figmenu.create_menu(figure, graph)
435 def _update_scrollbars(self, event):
436 for grnum, gr in enumerate(self.graphs):
437 self._update_graph_adj(grnum, gr)
438 # Then for all graphs...
440 def _update_graph_adj(self, grnum, g):
441 (lower, upper) = (0, 1)
443 hadj = self._cbx_store[grnum + 1][IOSCOPY_COL_HADJ]
444 vadj = self._cbx_store[grnum + 1][IOSCOPY_COL_VADJ]
446 (xdmin, xdmax) = (g.dataLim.xmin, g.dataLim.xmax)
447 (ydmin, ydmax) = (g.dataLim.ymin, g.dataLim.ymax)
448 b0 = g.transData.transform_point([xdmin, ydmin])
449 b1 = g.transData.transform_point([xdmax, ydmax])
451 # Get view bounds in pixels
452 (xvmin, xvmax) = (g.bbox.xmin, g.bbox.xmax)
453 (yvmin, yvmax) = (g.bbox.ymin, g.bbox.ymax)
455 xpage_size = (xvmax - xvmin) / (b1[0] - b0[0])
456 ypage_size = (yvmax - yvmin) / (b1[1] - b0[1])
458 xvalue = (xvmin - b0[0]) / (b1[0] - b0[0])
459 yvalue = (yvmin - b0[1]) / (b1[1] - b0[1])
461 xstep_increment = xpage_size / 10
462 xpage_increment = xpage_size
463 hadj.configure(xvalue, lower, upper,
464 xstep_increment, xpage_increment, xpage_size)
465 ystep_increment = ypage_size / 10
466 ypage_increment = ypage_size
467 vadj.configure(yvalue, lower, upper,
468 ystep_increment, ypage_increment, ypage_size)
470 layout = property(oscopy.Figure.get_layout, set_layout)