ui: pass figure and graph as data to menu functions (continue previous commit)
[oscopy/ivan.git] / ui.py
blob26ef390933f8d3bc4e4bfe9485dd631ea91478bd
1 import gobject
2 import gtk
3 import signal
4 import commands
6 import oscopy
8 from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
9 from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
11 def report_error(parent, msg):
12 dlg = gtk.MessageDialog(parent,
13 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
14 gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, msg)
15 dlg.set_title(parent.get_title())
16 dlg.run()
17 dlg.destroy()
19 class App(object):
20 __ui = '''<ui>
21 <menubar name="MenuBar">
22 <menu action="File">
23 <menuitem action="Add file"/>
24 <menuitem action="Update files"/>
25 <menuitem action="New Math Signal..."/>
26 <menuitem action="Run netlister and simulate..."/>
27 <menuitem action="Quit"/>
28 </menu>
29 </menubar>
30 </ui>'''
32 def __init__(self):
33 self._scale_to_str = {'lin': 'Linear', 'logx': 'LogX', 'logy': 'LogY',\
34 'loglog': 'Loglog'}
35 self._layout_to_str = {'horiz': 'Horizontal', 'vert':'Vertical',\
36 'quad':'Quad'}
37 self._figcount = 0
38 self._windows_to_figures = {}
39 self._current_graph = None
40 self._current_figure = None
41 self._TARGET_TYPE_SIGNAL = 10354
42 self._from_signal_list = [("oscopy-signals", gtk.TARGET_SAME_APP,\
43 self._TARGET_TYPE_SIGNAL)]
44 self._to_figure = [("oscopy-signals", gtk.TARGET_SAME_APP,\
45 self._TARGET_TYPE_SIGNAL)]
46 self._ctxt = oscopy.Context()
47 self._store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT)
48 self._create_widgets()
49 self._add_file('demo/irf540.dat')
50 #self._add_file('demo/ac.dat')
51 #self._add_file('demo/res.dat')
53 def _add_file(self, filename):
54 try:
55 self._ctxt.read(filename)
56 except NotImplementedError:
57 report_error(self._mainwindow,
58 'Could not find a reader for %s' % filename)
59 return
60 it = self._store.append(None, (filename, None))
61 for name, sig in self._ctxt.readers[filename].signals.iteritems():
62 self._store.append(it, (name, sig))
64 def _action_add_file(self, action):
65 dlg = gtk.FileChooserDialog('Add file', parent=self._mainwindow,
66 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
67 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
68 resp = dlg.run()
69 filename = dlg.get_filename()
70 dlg.destroy()
71 if resp == gtk.RESPONSE_ACCEPT:
72 self._add_file(filename)
74 def _action_update(self, action):
75 self._ctxt.update()
77 def _action_new_math(self, action):
78 dlg = gtk.Dialog('New math signal', parent=self._mainwindow,
79 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
80 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
82 # Label and entry
83 hbox = gtk.HBox()
84 label = gtk.Label('Expression:')
85 hbox.pack_start(label)
86 entry = gtk.Entry()
87 hbox.pack_start(entry)
88 dlg.vbox.pack_start(hbox)
90 dlg.show_all()
91 resp = dlg.run()
92 if resp == gtk.RESPONSE_ACCEPT:
93 expr = entry.get_text()
94 try:
95 self._ctxt.math(expr)
96 except ReadError, e:
97 report_error(self._mainwindow,
98 'Could not find a reader for %s' % expr)
99 return
100 it = self._store.append(None, (expr, None))
101 for name, sig in self._ctxt.readers[expr].signals.iteritems():
102 self._store.append(it, (name, sig))
104 dlg.destroy()
107 def _action_quit(self, action):
108 main_loop.quit()
110 def _create_menubar(self):
111 # tuple format:
112 # (name, stock-id, label, accelerator, tooltip, callback)
113 actions = [
114 ('File', None, '_File'),
115 ('Add file', gtk.STOCK_ADD, '_Add file', None, None,
116 self._action_add_file),
117 ('Update files', gtk.STOCK_REFRESH, '_Update', None, None,
118 self._action_update),
119 ("New Math Signal...", gtk.STOCK_NEW, '_New Math Signal', None,
120 None, self._action_new_math),
121 ("Run netlister and simulate...", gtk.STOCK_MEDIA_FORWARD,\
122 "_Run netlister and simulate...", None, None,\
123 self._action_netlist_and_simulate),
124 ('Quit', gtk.STOCK_QUIT, '_Quit', None, None,
125 self._action_quit),
127 actiongroup = gtk.ActionGroup('App')
128 actiongroup.add_actions(actions)
130 uimanager = gtk.UIManager()
131 uimanager.add_ui_from_string(self.__ui)
132 uimanager.insert_action_group(actiongroup, 0)
133 return uimanager.get_accel_group(), uimanager.get_widget('/MenuBar')
135 def _create_treeview(self):
136 col = gtk.TreeViewColumn('Signal', gtk.CellRendererText(), text=0)
137 tv = gtk.TreeView()
138 tv.append_column(col)
139 tv.set_model(self._store)
140 tv.connect('row-activated', self._row_activated)
141 tv.connect('button-press-event', self._treeview_button_press)
142 tv.connect('drag_data_get', self._drag_data_get_cb)
143 tv.drag_source_set(gtk.gdk.BUTTON1_MASK,\
144 self._from_signal_list,\
145 gtk.gdk.ACTION_COPY)
146 return tv
148 def _create_widgets(self):
149 accel_group, self._menubar = self._create_menubar()
150 self._treeview = self._create_treeview()
152 sw = gtk.ScrolledWindow()
153 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
154 sw.add(self._treeview)
156 vbox = gtk.VBox()
157 vbox.pack_start(self._menubar, False)
158 vbox.pack_start(sw)
160 w = self._mainwindow = gtk.Window(gtk.WINDOW_TOPLEVEL)
161 w.set_title('Oscopy GUI')
162 w.add(vbox)
163 w.add_accel_group(accel_group)
164 w.connect('destroy', lambda *x: self._action_quit(None))
165 w.set_default_size(400, 300)
166 w.show_all()
168 def _create_units_window(self, figure, graph):
169 if self._current_graph is None:
170 return
171 dlg = gtk.Dialog('Enter graph units',\
172 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,\
173 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
174 # Label and entry for X axis
175 hbox_x = gtk.HBox()
176 label_xunits = gtk.Label('X axis unit:')
177 hbox_x.pack_start(label_xunits)
178 entry_xunits = gtk.Entry()
179 entry_xunits.set_text(self._current_graph.unit[0])
180 hbox_x.pack_start(entry_xunits)
181 dlg.vbox.pack_start(hbox_x)
183 # Label and entry for Y axis
184 hbox_y = gtk.HBox()
185 label_yunits = gtk.Label('Y axis unit:')
186 hbox_y.pack_start(label_yunits)
187 entry_yunits = gtk.Entry()
188 entry_yunits.set_text(self._current_graph.unit[1])
189 hbox_y.pack_start(entry_yunits)
190 dlg.vbox.pack_start(hbox_y)
192 dlg.show_all()
193 resp = dlg.run()
194 if resp == gtk.RESPONSE_ACCEPT:
195 self._current_graph.set_unit((entry_xunits.get_text(),\
196 entry_yunits.get_text()))
197 if figure.canvas is not None:
198 figure.canvas.draw()
199 dlg.destroy()
201 def _units_menu_item_activated(self, menuitem, user_data):
202 figure, graph = user_data
203 self._create_units_window(figure, graph)
205 def _create_range_window(self, figure, graph):
206 if self._current_graph is None:
207 return
208 dlg = gtk.Dialog('Enter graph range',\
209 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,\
210 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
211 [xmin, xmax], [ymin, ymax] = self._current_graph.get_range()
212 # Label and entry for X axis
213 hbox_x = gtk.HBox()
214 label_xmin = gtk.Label('Xmin:')
215 hbox_x.pack_start(label_xmin)
216 entry_xmin = gtk.Entry()
217 entry_xmin.set_text(str(xmin))
218 hbox_x.pack_start(entry_xmin)
219 label_xmax = gtk.Label('Xmax:')
220 hbox_x.pack_start(label_xmax)
221 entry_xmax = gtk.Entry()
222 entry_xmax.set_text(str(xmax))
223 hbox_x.pack_start(entry_xmax)
224 dlg.vbox.pack_start(hbox_x)
226 # Label and entry for Y axis
227 hbox_y = gtk.HBox()
228 label_ymin = gtk.Label('Ymin:')
229 hbox_y.pack_start(label_ymin)
230 entry_ymin = gtk.Entry()
231 entry_ymin.set_text(str(ymin))
232 hbox_y.pack_start(entry_ymin)
233 label_ymax = gtk.Label('Ymax:')
234 hbox_y.pack_start(label_ymax)
235 entry_ymax = gtk.Entry()
236 entry_ymax.set_text(str(ymax))
237 hbox_y.pack_start(entry_ymax)
238 dlg.vbox.pack_start(hbox_y)
240 dlg.show_all()
241 resp = dlg.run()
242 if resp == gtk.RESPONSE_ACCEPT:
243 r = [float(entry_xmin.get_text()),\
244 float(entry_xmax.get_text()),\
245 float(entry_ymin.get_text()),\
246 float(entry_ymax.get_text())]
247 self._current_graph.set_range(r)
248 if figure.canvas is not None:
249 figure.canvas.draw()
250 dlg.destroy()
252 def _range_menu_item_activated(self, menuitem, user_data):
253 figure, graph = user_data
254 self._create_range_window(figure, graph)
256 def _signals_menu_item_activated(self, menuitem, user_data):
257 fig, parent_it, it = user_data
258 name, sig = self._store.get(it, 0, 1)
259 # print 'Adding signal %s to %s' % (name, fig)
260 if not fig.graphs:
261 fig.add({name:sig})
262 else:
263 if self._current_graph is not None:
264 self._current_graph.insert({name: sig})
265 if fig.canvas is not None:
266 fig.canvas.draw()
268 def _graph_menu_item_activated(self, menuitem, user_data):
269 fig = user_data
270 fig.add()
271 fig.canvas.draw()
273 def _scale_menu_item_activated(self, menuitem, user_data):
274 fig, scale = user_data
275 self._current_graph.set_scale(scale)
276 if fig.canvas is not None:
277 fig.canvas.draw()
279 def _layout_menu_item_activated(self, menuitem, user_data):
280 fig, layout = user_data
281 fig.layout = layout
282 if fig.canvas is not None:
283 fig.canvas.draw()
285 def _delete_graph_menu_item_activated(self, menuitem, user_data):
286 figure, graph = user_data
287 if self._current_graph is not None:
288 idx = figure.graphs.index(self._current_graph)
289 figure.delete(idx + 1)
290 self._current_graph = None
291 if figure.canvas is not None:
292 figure.canvas.draw()
294 def _remove_signal_menu_item_activated(self, menuitem, user_data):
295 figure, graph, signals = user_data
296 if graph is None:
297 return
298 self._current_graph.remove(signals)
299 if figure.canvas is not None:
300 figure.canvas.draw()
302 def _create_scale_menu(self, data):
303 figure, graph = data
304 menu = gtk.Menu()
305 for scale in self._scale_to_str.keys():
306 item = gtk.CheckMenuItem(self._scale_to_str[scale])
307 item.set_active(self._current_graph.scale == scale)
308 item.connect('activate', self._scale_menu_item_activated,
309 (figure, scale))
310 menu.append(item)
311 return menu
313 def _create_layout_menu(self, fig):
314 menu = gtk.Menu()
315 for layout in self._layout_to_str.keys():
316 item = gtk.CheckMenuItem(self._layout_to_str[layout])
317 item.set_active(fig.layout == layout)
318 item.connect('activate', self._layout_menu_item_activated,
319 (fig, layout))
320 menu.append(item)
321 return menu
323 def _create_remove_signal_menu(self, data):
324 figure, graph = data
325 menu = gtk.Menu()
326 if self._current_graph is None:
327 item_nograph = gtk.MenuItem('No graph selected')
328 menu.append(item_nograph)
329 return menu
330 for name, signal in self._current_graph.signals.iteritems():
331 item = gtk.MenuItem(name)
332 item.connect('activate', self._remove_signal_menu_item_activated,
333 (figure, graph, {name: signal}))
334 menu.append(item)
335 return menu
337 def _create_graph_menu(self, figure, graph):
338 menu = gtk.Menu()
339 item_range = gtk.MenuItem('Range...')
340 item_range.connect('activate', self._range_menu_item_activated,\
341 (figure, graph))
342 menu.append(item_range)
343 item_units = gtk.MenuItem('Units...')
344 item_units.connect('activate', self._units_menu_item_activated,\
345 (figure, graph))
346 menu.append(item_units)
347 item_scale = gtk.MenuItem('Scale')
348 item_scale.set_submenu(self._create_scale_menu((figure, graph)))
349 menu.append(item_scale)
350 item_add = gtk.MenuItem('Insert signal')
351 item_add.set_submenu(self._create_filename_menu((figure, graph)))
352 menu.append(item_add)
353 item_remove = gtk.MenuItem('Remove signal')
354 item_remove.set_submenu(self._create_remove_signal_menu((figure, graph)))
355 menu.append(item_remove)
356 return menu
358 def _create_figure_menu(self, fig, graph):
359 menu = gtk.Menu()
360 item_add = gtk.MenuItem('Add graph')
361 item_add.connect('activate', self._graph_menu_item_activated,
362 (fig))
363 menu.append(item_add)
364 item_delete = gtk.MenuItem('Delete graph')
365 item_delete.connect('activate', self._delete_graph_menu_item_activated,
366 (fig, graph))
367 menu.append(item_delete)
368 item_layout = gtk.MenuItem('Layout')
369 item_layout.set_submenu(self._create_layout_menu(fig))
370 menu.append(item_layout)
371 return menu
373 def _create_signals_menu(self, fig, parent_it):
374 menu = gtk.Menu()
375 it = self._store.iter_children(parent_it)
376 while it is not None:
377 name = self._store.get_value(it, 0)
378 item = gtk.MenuItem(name)
379 item.connect('activate', self._signals_menu_item_activated,
380 (fig, parent_it, it))
381 menu.append(item)
382 it = self._store.iter_next(it)
383 return menu
385 def _create_filename_menu(self, data):
386 figure, graph = data
387 it = self._store.get_iter_root()
388 if it is None:
389 return gtk.Menu()
391 menu = gtk.Menu()
392 while it is not None:
393 filename = self._store.get_value(it, 0)
394 item = gtk.MenuItem(filename)
395 item.set_submenu(self._create_signals_menu(figure, it))
396 menu.append(item)
397 it = self._store.iter_next(it)
398 return menu
400 def _create_figure_popup_menu(self, figure, graph):
401 menu = gtk.Menu()
402 if graph is None:
403 item_nograph = gtk.MenuItem('No graph selected')
404 menu.append(item_nograph)
405 return menu
406 item_figure = gtk.MenuItem('Figure')
407 item_figure.set_submenu(self._create_figure_menu(figure, graph))
408 menu.append(item_figure)
409 item_graph = gtk.MenuItem('Graph')
410 item_graph.set_submenu(self._create_graph_menu(figure, graph))
411 menu.append(item_graph)
412 return menu
414 def _create_treeview_popup_menu(self, signals):
415 menu = gtk.Menu()
416 if not signals:
417 item_none = gtk.MenuItem("No signal selected")
418 menu.append(item_none)
419 return menu
420 for name, signal in signals.iteritems():
421 item_freeze = gtk.CheckMenuItem("Freeze %s" % name)
422 item_freeze.set_active(signal.freeze)
423 item_freeze.connect('activate',\
424 self._signal_freeze_menu_item_activated,\
425 (signal))
426 menu.append(item_freeze)
427 return menu
429 def _signal_freeze_menu_item_activated(self, menuitem, signal):
430 signal.freeze = not signal.freeze
431 # Modify also the signal in the treeview
432 # (italic font? gray font color? a freeze column?)
434 def _treeview_button_press(self, widget, event):
435 if event.button == 3:
436 tv = widget
437 path, tvc, x, y = tv.get_path_at_pos(int(event.x), int(event.y))
438 if len(path) == 1:
439 return
440 tv.set_cursor(path)
441 row = self._store[path]
442 signals = {row[0]: row[1]}
443 menu = self._create_treeview_popup_menu(signals)
444 menu.show_all()
445 menu.popup(None, None, None, event.button, event.time)
447 def _button_press(self, event):
448 if event.button == 3:
449 menu = self._create_figure_popup_menu(event.canvas.figure, event.inaxes)
450 menu.show_all()
451 menu.popup(None, None, None, event.button, event.guiEvent.time)
453 #TODO: _windows_to_figures consistency...
454 # think of a better way to map events to Figure objects
455 def _row_activated(self, widget, path, col):
456 if len(path) == 1:
457 return
459 row = self._store[path]
460 self._ctxt.create({row[0]: row[1]})
461 fig = self._ctxt.figures[len(self._ctxt.figures) - 1]
463 w = gtk.Window()
464 self._figcount += 1
465 self._windows_to_figures[w] = fig
466 w.set_title('Figure %d' % self._figcount)
467 vbox = gtk.VBox()
468 w.add(vbox)
469 canvas = FigureCanvas(fig)
470 canvas.mpl_connect('button_press_event', self._button_press)
471 canvas.mpl_connect('axes_enter_event', self._axes_enter)
472 canvas.mpl_connect('axes_leave_event', self._axes_leave)
473 canvas.mpl_connect('figure_enter_event', self._figure_enter)
474 canvas.mpl_connect('figure_leave_event', self._figure_leave)
475 w.connect("drag_data_received", self._drag_data_received_cb)
476 w.drag_dest_set(gtk.DEST_DEFAULT_MOTION |\
477 gtk.DEST_DEFAULT_HIGHLIGHT |\
478 gtk.DEST_DEFAULT_DROP,
479 self._to_figure, gtk.gdk.ACTION_COPY)
480 vbox.pack_start(canvas)
481 toolbar = NavigationToolbar(canvas, w)
482 vbox.pack_start(toolbar, False, False)
483 w.resize(400, 300)
484 w.show_all()
486 def _axes_enter(self, event):
487 self._current_graph = event.inaxes
489 def _axes_leave(self, event):
490 # Unused for better user interaction
491 # self._current_graph = None
492 pass
494 def _figure_enter(self, event):
495 self._current_figure = event.canvas.figure
497 def _figure_leave(self, event):
498 # self._current_figure = None
499 pass
501 def update_from_usr1(self):
502 self._ctxt.update()
504 def _action_netlist_and_simulate(self, action):
505 dialog = gtk.Dialog("Run netlister and simulate",\
506 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,\
507 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
508 vbox_netl = gtk.VBox()
509 ckbutton_netl = gtk.CheckButton("Run netlister")
510 ckbutton_netl.set_active(True)
511 vbox_netl.pack_start(ckbutton_netl)
512 entry_netl = gtk.Entry()
513 entry_netl.set_text("gnetlist -s -o demo.cir -g spice-sdb demo.sch")
514 vbox_netl.pack_start(entry_netl)
515 dialog.vbox.pack_start(vbox_netl)
516 vbox_netl = gtk.VBox()
518 vbox_sim = gtk.VBox()
519 ckbutton_sim = gtk.CheckButton("Run simulator")
520 ckbutton_sim.set_active(True)
521 vbox_sim.pack_start(ckbutton_sim)
522 entry_sim = gtk.Entry()
523 entry_sim.set_text("gnucap -b demo.cir")
524 vbox_sim.pack_start(entry_sim)
525 dialog.vbox.pack_start(vbox_sim)
526 ckbutton_upd = gtk.CheckButton("Update readers")
527 ckbutton_upd.set_active(True)
528 dialog.vbox.pack_start(ckbutton_upd)
529 dialog.show_all()
530 resp = dialog.run()
531 if resp == gtk.RESPONSE_ACCEPT:
532 if ckbutton_netl.get_active():
533 res = commands.getstatusoutput(entry_netl.get_text())
534 if res[0]:
535 report_error(self._mainwindow, res[1])
536 print res[1]
537 if ckbutton_sim.get_active():
538 res = commands.getstatusoutput(entry_sim.get_text())
539 if res[0]:
540 report_error(self._mainwindow, res[1])
541 print res[1]
542 if ckbutton_upd.get_active():
543 self._ctxt.update()
544 dialog.destroy()
546 def _drag_data_get_cb(self, widget, drag_context, selection, target_type,\
547 time):
548 if target_type == self._TARGET_TYPE_SIGNAL:
549 tv = widget
550 (path, col) = tv.get_cursor()
551 row = self._store[path]
552 print row[0]
553 selection.set(selection.target, 8, row[0])
555 def _drag_data_received_cb(self, widget, drag_context, x, y, selection,\
556 target_type, time):
557 if target_type == self._TARGET_TYPE_SIGNAL:
558 if self._current_graph is not None:
559 signals = {selection.data: self._ctxt.signals[selection.data]}
560 self._current_graph.insert(signals)
561 if self._current_figure.canvas is not None:
562 self._current_figure.canvas.draw()
564 def usr1_handler(signum, frame):
565 app.update_from_usr1()
567 if __name__ == '__main__':
568 app = App()
569 main_loop = gobject.MainLoop()
570 signal.signal(signal.SIGUSR1, usr1_handler)
571 main_loop.run()