Fix Graph range return type
[oscopy.git] / oscopy / ui.py
blobdea902f3798dc7d8550fb2a2f0f8ae2a76d2751e
1 #!/usr/bin/python
2 from __future__ import with_statement
4 import gobject
5 import gtk
6 import signal
7 import os
8 import sys
9 import readline
10 import commands
11 import ConfigParser
12 import dbus, dbus.service, dbus.glib
13 from xdg import BaseDirectory
14 from matplotlib.backend_bases import LocationEvent
15 #from matplotlib.widgets import SpanSelector
16 import IPython
18 import oscopy
20 from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
21 from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
22 import gui
24 # Note: for crosshair, see gtk.gdk.GC / function = gtk.gdk.XOR
26 def report_error(parent, msg):
27 dlg = gtk.MessageDialog(parent,
28 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
29 gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, msg)
30 dlg.set_title(parent.get_title())
31 dlg.run()
32 dlg.destroy()
34 class App(dbus.service.Object):
35 __ui = '''<ui>
36 <menubar name="MenuBar">
37 <menu action="File">
38 <menuitem action="Add file(s)..."/>
39 <menuitem action="Update files"/>
40 <menuitem action="Execute script..."/>
41 <menuitem action="New Math Signal..."/>
42 <menuitem action="Run netlister and simulate..."/>
43 <menuitem action="Quit"/>
44 </menu>
45 <menu action="Windows">
46 </menu>
47 </menubar>
48 </ui>'''
50 def __init__(self, bus_name, object_path='/org/freedesktop/Oscopy', ctxt=None):
51 if bus_name is not None:
52 dbus.service.Object.__init__(self, bus_name, object_path)
53 self._scale_to_str = {'lin': _('Linear'), 'logx': _('LogX'), 'logy': _('LogY'),\
54 'loglog': _('Loglog')}
55 self._windows_to_figures = {}
56 self._fignum_to_windows = {}
57 self._fignum_to_merge_id = {}
58 self._current_graph = None
59 self._current_figure = None
60 self._prompt = "oscopy-ui>"
61 self._init_config()
62 self._read_config()
64 self._TARGET_TYPE_SIGNAL = 10354
65 self._from_signal_list = [("oscopy-signals", gtk.TARGET_SAME_APP,\
66 self._TARGET_TYPE_SIGNAL)]
67 self._to_figure = [("oscopy-signals", gtk.TARGET_SAME_APP,\
68 self._TARGET_TYPE_SIGNAL)]
69 self._to_main_win = [("text/plain", 0,
70 self._TARGET_TYPE_SIGNAL),
71 ('STRING', 0,
72 self._TARGET_TYPE_SIGNAL),
73 ('application/octet-stream', 0,
74 self._TARGET_TYPE_SIGNAL),
75 # For '*.raw' formats
76 ('application/x-panasonic-raw', 0,
77 self._TARGET_TYPE_SIGNAL),
78 # For '*.ts' formats
79 ('video/mp2t', 0,
80 self._TARGET_TYPE_SIGNAL),
83 if ctxt is None:
84 self._ctxt = oscopy.Context()
85 else:
86 self._ctxt = ctxt
88 self._store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
89 gobject.TYPE_BOOLEAN)
90 self._create_widgets()
91 #self._app_exec('read demo/irf540.dat')
92 #self._app_exec('read demo/ac.dat')
93 #self._add_file('demo/res.dat')
95 # From IPython/demo.py
96 self.shell = get_ipython()
98 SECTION = 'oscopy_ui'
99 OPT_NETLISTER_COMMANDS = 'netlister_commands'
100 OPT_SIMULATOR_COMMANDS = 'simulator_commands'
101 OPT_RUN_DIRECTORY = 'run_directory'
104 # Actions
106 def _action_add_file(self, action):
107 dlg = gtk.FileChooserDialog(_('Add file(s)'), parent=self._mainwindow,
108 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
109 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
110 dlg.set_select_multiple(True)
111 resp = dlg.run()
112 if resp == gtk.RESPONSE_ACCEPT:
113 for filename in dlg.get_filenames():
114 self._app_exec('oread ' + filename)
115 dlg.destroy()
117 def _action_update(self, action):
118 self._ctxt.update()
120 def _action_new_math(self, action):
121 dlg = gtk.Dialog(_('New math signal'), parent=self._mainwindow,
122 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
123 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
125 # Label and entry
126 hbox = gtk.HBox()
127 label = gtk.Label(_('Expression:'))
128 hbox.pack_start(label)
129 entry = gtk.Entry()
130 hbox.pack_start(entry)
131 dlg.vbox.pack_start(hbox)
133 dlg.show_all()
134 resp = dlg.run()
135 if resp == gtk.RESPONSE_ACCEPT:
136 expr = entry.get_text()
137 self._app_exec('%s' % expr)
138 self._app_exec('oimport %s' % expr.split('=')[0].strip())
139 dlg.destroy()
141 def _action_execute_script(self, action):
142 dlg = gtk.FileChooserDialog(_('Execute script'), parent=self._mainwindow,
143 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
144 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
145 resp = dlg.run()
146 filename = dlg.get_filename()
147 dlg.destroy()
148 if resp == gtk.RESPONSE_ACCEPT:
149 self._app_exec('oexec ' + filename)
151 def _action_netlist_and_simulate(self, action):
152 dlg = gui.dialogs.Run_Netlister_and_Simulate_Dialog()
153 dlg.display(self._actions)
154 actions = dlg.run()
155 if actions is None:
156 return
157 self._actions = actions
158 run_dir = actions['run_from']
159 if actions['run_netlister'][0]:
160 if not self._run_ext_command(actions['run_netlister'][1][0], run_dir):
161 return
162 if actions['run_simulator'][0]:
163 if not self._run_ext_command(actions['run_simulator'][1][0], run_dir):
164 return
165 if actions['update']:
166 self._ctxt.update()
168 def _action_quit(self, action):
169 self._write_config()
170 readline.write_history_file(self.hist_file)
171 gtk.main_quit()
172 sys.exit()
174 def _action_figure(self, action, w, fignum):
175 if not (w.flags() & gtk.VISIBLE):
176 w.show()
177 else:
178 w.window.show()
179 self._app_exec('%%oselect %d-1' % fignum)
182 # UI Creation functions
184 def _create_menubar(self):
185 # tuple format:
186 # (name, stock-id, label, accelerator, tooltip, callback)
187 actions = [
188 ('File', None, _('_File')),
189 ('Add file(s)...', gtk.STOCK_ADD, _('_Add file(s)...'), None, None,
190 self._action_add_file),
191 ('Update files', gtk.STOCK_REFRESH, _('_Update'), None, None,
192 self._action_update),
193 ('Execute script...', gtk.STOCK_MEDIA_PLAY, _('_Execute script...'),
194 None, None, self._action_execute_script),
195 ("New Math Signal...", gtk.STOCK_NEW, _('_New Math Signal'), None,
196 None, self._action_new_math),
197 ("Run netlister and simulate...", gtk.STOCK_MEDIA_FORWARD,\
198 _("_Run netlister and simulate..."), None, None,\
199 self._action_netlist_and_simulate),
200 ('Windows', None, _('_Windows')),
201 ('Quit', gtk.STOCK_QUIT, _('_Quit'), None, None,
202 self._action_quit),
205 actiongroup = self._actiongroup = gtk.ActionGroup('App')
206 actiongroup.add_actions(actions)
208 uimanager = self._uimanager = gtk.UIManager()
209 uimanager.add_ui_from_string(self.__ui)
210 uimanager.insert_action_group(actiongroup, 0)
211 return uimanager.get_accel_group(), uimanager.get_widget('/MenuBar')
213 def _create_treeview(self):
214 celltext = gtk.CellRendererText()
215 col = gtk.TreeViewColumn(_('Signal'), celltext, text=0)
216 tv = gtk.TreeView()
217 col.set_cell_data_func(celltext, self._reader_name_in_bold)
218 col.set_expand(True)
219 tv.append_column(col)
220 tv.set_model(self._store)
221 tv.connect('row-activated', self._row_activated)
222 tv.connect('drag_data_get', self._drag_data_get_cb)
223 tv.drag_source_set(gtk.gdk.BUTTON1_MASK,\
224 self._from_signal_list,\
225 gtk.gdk.ACTION_COPY)
226 self._togglecell = gtk.CellRendererToggle()
227 self._togglecell.set_property('activatable', True)
228 self._togglecell.connect('toggled', self._cell_toggled, None)
229 colfreeze = gtk.TreeViewColumn(_('Freeze'), self._togglecell)
230 colfreeze.add_attribute(self._togglecell, 'active', 2)
231 tv.append_column(colfreeze)
232 tv.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
233 return tv
235 def _reader_name_in_bold(self, column, cell, model, iter, data=None):
236 if len(model.get_path(iter)) == 1:
237 cell.set_property('markup', "<b>" + model.get_value(iter, 0) +\
238 "</b>")
239 else:
240 cell.set_property('text', model.get_value(iter, 0))
242 def _create_widgets(self):
243 accel_group, self._menubar = self._create_menubar()
244 self._treeview = self._create_treeview()
246 sw = gtk.ScrolledWindow()
247 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
248 sw.add(self._treeview)
250 vbox = gtk.VBox()
251 vbox.pack_start(self._menubar, False)
252 vbox.pack_start(sw)
254 w = self._mainwindow = gtk.Window(gtk.WINDOW_TOPLEVEL)
255 w.set_title(_('Oscopy GUI'))
256 w.add(vbox)
257 w.add_accel_group(accel_group)
258 w.connect('destroy', lambda w, e: w.hide() or True)
259 w.connect('delete-event', lambda w, e: w.hide() or True)
260 w.set_default_size(400, 300)
261 w.show_all()
262 w.drag_dest_set(gtk.DEST_DEFAULT_MOTION |\
263 gtk.DEST_DEFAULT_HIGHLIGHT |\
264 gtk.DEST_DEFAULT_DROP,
265 self._to_main_win, gtk.gdk.ACTION_COPY)
266 w.connect('drag_data_received', self._drag_data_received_main_cb)
268 def _create_figure_popup_menu(self, figure, graph):
269 figmenu = gui.menus.FigureMenu()
270 return figmenu.create_menu(self._store, figure, graph, self._app_exec)
272 def show_all(self):
273 self._mainwindow.show()
276 # Event-triggered functions
278 def _treeview_button_press(self, widget, event):
279 if event.button == 3:
280 tv = widget
281 path, tvc, x, y = tv.get_path_at_pos(int(event.x), int(event.y))
282 if len(path) == 1:
283 return
284 tv.set_cursor(path)
285 row = self._store[path]
286 signals = {row[0]: row[1]}
287 menu = self._create_treeview_popup_menu(signals, path)
288 menu.show_all()
289 menu.popup(None, None, None, event.button, event.time)
291 def _button_press(self, event):
292 if event.button == 3:
293 menu = self._create_figure_popup_menu(event.canvas.figure, event.inaxes)
294 menu.show_all()
295 menu.popup(None, None, None, event.button, event.guiEvent.time)
297 #TODO: _windows_to_figures consistency...
298 # think of a better way to map events to Figure objects
299 def _row_activated(self, widget, path, col):
300 if len(path) == 1:
301 return
303 row = self._store[path]
304 self._app_exec('ocreate %s' % row[0])
306 def _axes_enter(self, event):
307 self._figure_enter(event)
308 self._current_graph = event.inaxes
310 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
311 fig_num = self._ctxt.figures.index(self._current_figure) + 1
312 self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
314 def _axes_leave(self, event):
315 # Unused for better user interaction
316 # self._current_graph = None
317 pass
319 def _figure_enter(self, event):
320 self._current_figure = event.canvas.figure
321 if hasattr(event, 'inaxes') and event.inaxes is not None:
322 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
323 else:
324 axes_num = 1
325 fig_num = self._ctxt.figures.index(self._current_figure) + 1
326 self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
328 def _figure_leave(self, event):
329 # self._current_figure = None
330 pass
332 def _cell_toggled(self, cellrenderer, path, data):
333 if len(path) == 3:
334 # Single signal
335 if self._store[path][1].freeze:
336 cmd = 'ounfreeze'
337 else:
338 cmd = 'ofreeze'
339 self._app_exec('%s %s' % (cmd, self._store[path][0]))
340 elif len(path) == 1:
341 # Whole reader
342 parent = self._store.get_iter(path)
343 freeze = not self._store.get_value(parent, 2)
344 if self._store[path][2]:
345 cmd = 'ounfreeze'
346 else:
347 cmd = 'ofreeze'
348 self._store.set_value(parent, 2, freeze)
349 iter = self._store.iter_children(parent)
350 while iter:
351 self._app_exec('%s %s' % (cmd, self._store.get_value(iter, 0)))
352 iter = self._store.iter_next(iter)
355 # Callbacks for App
357 def create(self):
358 """ Instanciate the window widget with the figure inside, set the
359 relevant events and add it to the 'Windows' menu.
360 Finally, select the first graph of this figure.
362 The figure has been instanciated by the application
363 and is assumed to be the last one in Context's figure list
365 fig = self._ctxt.figures[len(self._ctxt.figures) - 1]
366 fignum = len(self._ctxt.figures)
368 w = gtk.Window()
369 self._windows_to_figures[w] = fig
370 self._fignum_to_windows[fignum] = w
371 w.set_title(_('Figure %d') % fignum)
372 vbox = gtk.VBox()
373 w.add(vbox)
374 canvas = FigureCanvas(fig)
375 canvas.mpl_connect('button_press_event', self._button_press)
376 canvas.mpl_connect('axes_enter_event', self._axes_enter)
377 canvas.mpl_connect('axes_leave_event', self._axes_leave)
378 canvas.mpl_connect('figure_enter_event', self._figure_enter)
379 canvas.mpl_connect('figure_leave_event', self._figure_leave)
380 w.connect("drag_data_received", self._drag_data_received_cb)
381 w.connect('delete-event', lambda w, e: w.hide() or True)
382 w.drag_dest_set(gtk.DEST_DEFAULT_MOTION |\
383 gtk.DEST_DEFAULT_HIGHLIGHT |\
384 gtk.DEST_DEFAULT_DROP,
385 self._to_figure, gtk.gdk.ACTION_COPY)
386 vbox.pack_start(canvas)
387 toolbar = NavigationToolbar(canvas, w)
388 vbox.pack_start(toolbar, False, False)
390 # hscale = gtk.HScrollbar()
391 ## hscale.set_range(0, 10)
392 # #hscale.set_draw_value(False)
393 ## hscale.set_value(5)
394 # adj = gtk.Adjustment(50, 0, 100, 1, 10, 20)
395 # hscale.set_adjustment(adj)
396 # #hscale.set_slider_size_fixed(False)
397 # vbox.pack_start(hscale, False, False)
399 w.resize(640, 480)
400 w.show_all()
402 # # Update canvas for SpanSelector of Graphs
403 # for gr in fig.graphs:
404 # if hasattr(gr, 'span'):
405 # gr.span.new_axes(gr)
407 # Add it to the 'Windows' menu
408 actions = [('Figure %d' % fignum, None, _('Figure %d') % fignum,
409 None, None, self._action_figure)]
410 self._actiongroup.add_actions(actions, (w, fignum))
411 ui = "<ui>\
412 <menubar name=\"MenuBar\">\
413 <menu action=\"Windows\">\
414 <menuitem action=\"Figure %d\"/>\
415 </menu>\
416 </menubar>\
417 </ui>" % fignum
418 merge_id = self._uimanager.add_ui_from_string(ui)
419 self._fignum_to_merge_id[fignum] = merge_id
420 self._app_exec('%%oselect %d-1' % fignum)
422 def destroy(self, num):
423 if not num.isdigit() or int(num) > len(self._ctxt.figures):
424 return
425 else:
426 fignum = int(num)
427 action = self._uimanager.get_action('/MenuBar/Windows/Figure %d' %
428 fignum)
429 if action is not None:
430 self._actiongroup.remove_action(action)
431 self._uimanager.remove_ui(self._fignum_to_merge_id[fignum])
432 self._fignum_to_windows[fignum].destroy()
434 # Search algorithm from pygtk tutorial
435 def _match_func(self, row, data):
436 column, key = data
437 return row[column] == key
439 def _search(self, rows, func, data):
440 if not rows: return None
441 for row in rows:
442 if func(row, data):
443 return row
444 result = self._search(row.iterchildren(), func, data)
445 if result: return result
446 return None
448 def freeze(self, signals):
449 for signal in signals.split(','):
450 match_row = self._search(self._store, self._match_func,\
451 (0, signal.strip()))
452 if match_row is not None:
453 match_row[2] = match_row[1].freeze
454 parent = self._store.iter_parent(match_row.iter)
455 iter = self._store.iter_children(parent)
456 freeze = match_row[2]
457 while iter:
458 if not self._store.get_value(iter, 2) == freeze:
459 break
460 iter = self._store.iter_next(iter)
461 if iter == None:
462 # All row at the same freeze value,
463 # set freeze for the reader
464 self._store.set_value(parent, 2, freeze)
465 else:
466 # Set reader freeze to false
467 self._store.set_value(parent, 2, False)
469 def add_file(self, filename):
470 if filename.strip() in self._ctxt.readers:
471 it = self._store.append(None, (filename.strip(), None, False))
472 for name, sig in self._ctxt.readers[filename.strip()]\
473 .signals.iteritems():
474 self._store.append(it, (name, sig, sig.freeze))
477 # Callbacks for drag and drop
479 def _drag_data_received_main_cb(self, widget, drag_context, x, y, selection,
480 target_type, time):
481 name = selection.data
482 if type(name) == str and name.startswith('file://'):
483 print name[7:].strip()
484 self._app_exec('%%oread %s' % name[7:].strip())
486 def _drag_data_get_cb(self, widget, drag_context, selection, target_type,\
487 time):
488 if target_type == self._TARGET_TYPE_SIGNAL:
489 tv = widget
490 sel = tv.get_selection()
491 (model, pathlist) = sel.get_selected_rows()
492 iter = self._store.get_iter(pathlist[0])
493 data = " ".join(map(lambda x:self._store[x][1].name, pathlist))
494 selection.set(selection.target, 8, data)
495 # The multiple selection do work, but how to select signals
496 # that are not neighbours in the list? Ctrl+left do not do
497 # anything, neither alt+left or shift+left!
499 def _drag_data_received_cb(self, widget, drag_context, x, y, selection,\
500 target_type, time):
501 # Event handling issue: this drag and drop callback is
502 # processed before matplotlib callback _axes_enter. Therefore
503 # when dropping, self._current_graph is not valid: it contains
504 # the last graph.
505 # The workaround is to retrieve the Graph by creating a Matplotlib
506 # LocationEvent considering inverse 'y' coordinates
507 if target_type == self._TARGET_TYPE_SIGNAL:
508 canvas = self._windows_to_figures[widget].canvas
509 my_y = canvas.allocation.height - y
510 event = LocationEvent('axes_enter_event', canvas, x, my_y)
511 signals = {}
512 for name in selection.data.split():
513 signals[name] = self._ctxt.signals[name]
514 if event.inaxes is not None:
515 # Graph not found
516 event.inaxes.insert(signals)
517 self._windows_to_figures[widget].canvas.draw()
520 # Configuration-file related functions
522 def _init_config(self):
523 # initialize configuration stuff
524 path = BaseDirectory.save_config_path('oscopy')
525 self.config_file = os.path.join(path, 'gui')
526 self.hist_file = os.path.join(path, 'history')
527 section = App.SECTION
528 self.config = ConfigParser.RawConfigParser()
529 self.config.add_section(section)
530 # defaults
531 self.config.set(section, App.OPT_NETLISTER_COMMANDS, '')
532 self.config.set(section, App.OPT_SIMULATOR_COMMANDS, '')
533 self.config.set(section, App.OPT_RUN_DIRECTORY, '.')
535 def _sanitize_list(self, lst):
536 return filter(lambda x: len(x) > 0, map(lambda x: x.strip(), lst))
538 def _actions_from_config(self, config):
539 section = App.SECTION
540 netlister_commands = config.get(section, App.OPT_NETLISTER_COMMANDS)
541 netlister_commands = self._sanitize_list(netlister_commands.split(';'))
542 simulator_commands = config.get(section, App.OPT_SIMULATOR_COMMANDS)
543 simulator_commands = self._sanitize_list(simulator_commands.split(';'))
544 actions = {
545 'run_netlister': (True, netlister_commands),
546 'run_simulator': (True, simulator_commands),
547 'update': True,
548 'run_from': config.get(section, App.OPT_RUN_DIRECTORY)}
549 return actions
551 def _actions_to_config(self, actions, config):
552 section = App.SECTION
553 netlister_commands = ';'.join(actions['run_netlister'][1])
554 simulator_commands = ';'.join(actions['run_simulator'][1])
555 config.set(section, App.OPT_NETLISTER_COMMANDS, netlister_commands)
556 config.set(section, App.OPT_SIMULATOR_COMMANDS, simulator_commands)
557 config.set(section, App.OPT_RUN_DIRECTORY, actions['run_from'])
559 def _read_config(self):
560 self.config.read(self.config_file)
561 self._actions = self._actions_from_config(self.config)
563 def _write_config(self):
564 self._actions_to_config(self._actions, self.config)
565 with open(self.config_file, 'w') as f:
566 self.config.write(f)
568 # DBus routines
569 @dbus.service.method('org.freedesktop.OscopyIFace')
570 def dbus_update(self):
571 gobject.idle_add(self._activate_net_and_sim)
573 @dbus.service.method('org.freedesktop.OscopyIFace')
574 def dbus_running(self):
575 return
577 # Misc functions
578 def update_from_usr1(self):
579 self._ctxt.update()
581 def update_from_usr2(self):
582 gobject.idle_add(self._activate_net_and_sim)
584 def _activate_net_and_sim(self):
585 if self._actiongroup is not None:
586 action = self._actiongroup.get_action("Run netlister and simulate...")
587 action.activate()
589 def _run_ext_command(self, cmd, run_dir):
590 old_dir = os.getcwd()
591 os.chdir(run_dir)
592 try:
593 status, output = commands.getstatusoutput(cmd)
594 if status:
595 msg = _("Executing command '%s' failed.") % cmd
596 report_error(self._mainwindow, msg)
597 return status == 0
598 finally:
599 os.chdir(old_dir)
601 def _app_exec(self, line):
602 first = line.split()[0]
603 if first.startswith('%') or first.split()[0] in self.shell.lsmagic():
604 self.shell.magic(line)
605 else:
606 self.shell.ex(line)
608 def usr1_handler(signum, frame):
609 app.update_from_usr1()
611 def usr2_handler(signum, frame):
612 app.update_from_usr2()