Use find_magic and run_line_magic for calling magics
[oscopy.git] / src / oscopy_ipython / ui.py
blob9767bd43e8e9cd3552e1422f8bb1a1a7988736cb
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 math import log10, sqrt
14 from xdg import BaseDirectory
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
23 from gtk_figure import IOscopy_GTK_Figure
25 IOSCOPY_COL_TEXT = 0
26 IOSCOPY_COL_X10 = 1
27 IOSCOPY_COL_VIS = 2 # Text in graphs combobox visible
29 # Note: for crosshair, see gtk.gdk.GC / function = gtk.gdk.XOR
31 def report_error(parent, msg):
32 dlg = gtk.MessageDialog(parent,
33 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
34 gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, msg)
35 dlg.set_title(parent.get_title())
36 dlg.run()
37 dlg.destroy()
39 class App(dbus.service.Object):
40 __ui = '''<ui>
41 <menubar name="MenuBar">
42 <menu action="File">
43 <menuitem action="Add file(s)..."/>
44 <menuitem action="Update files"/>
45 <menuitem action="Execute script..."/>
46 <menuitem action="New Math Signal..."/>
47 <menuitem action="Run netlister and simulate..."/>
48 <menuitem action="Quit"/>
49 </menu>
50 <menu action="Windows">
51 </menu>
52 </menubar>
53 </ui>'''
55 def __init__(self, bus_name, object_path='/org/freedesktop/Oscopy', ctxt=None, ip=None):
56 if bus_name is not None:
57 dbus.service.Object.__init__(self, bus_name, object_path)
58 self._scale_to_str = {'lin': _('Linear'), 'logx': _('LogX'), 'logy': _('LogY'),\
59 'loglog': _('Loglog')}
60 self._windows_to_figures = {}
61 self._fignum_to_windows = {}
62 self._fignum_to_merge_id = {}
63 self._current_graph = None
64 self._current_figure = None
65 self._prompt = "oscopy-ui>"
66 self._init_config()
67 self._read_config()
69 # Might be moved to a dedicated app_figure class one day...
70 self._btns = {}
71 self._cbxs = {}
72 self._cbx_stores = {}
74 self._TARGET_TYPE_SIGNAL = 10354
75 self._from_signal_list = [("oscopy-signals", gtk.TARGET_SAME_APP,\
76 self._TARGET_TYPE_SIGNAL)]
77 self._to_figure = [("oscopy-signals", gtk.TARGET_SAME_APP,\
78 self._TARGET_TYPE_SIGNAL)]
79 self._to_main_win = [("text/plain", 0,
80 self._TARGET_TYPE_SIGNAL),
81 ('STRING', 0,
82 self._TARGET_TYPE_SIGNAL),
83 ('application/octet-stream', 0,
84 self._TARGET_TYPE_SIGNAL),
85 # For '*.raw' formats
86 ('application/x-panasonic-raw', 0,
87 self._TARGET_TYPE_SIGNAL),
88 # For '*.ts' formats
89 ('video/mp2t', 0,
90 self._TARGET_TYPE_SIGNAL),
93 if ctxt is None:
94 self._ctxt = oscopy.Context()
95 else:
96 self._ctxt = ctxt
98 self._store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
99 gobject.TYPE_BOOLEAN)
100 self._create_widgets()
101 #self._app_exec('read demo/irf540.dat')
102 #self._app_exec('read demo/ac.dat')
103 #self._add_file('demo/res.dat')
105 # From IPython/demo.py
106 self.shell = ip
108 SECTION = 'oscopy_ui'
109 OPT_NETLISTER_COMMANDS = 'netlister_commands'
110 OPT_SIMULATOR_COMMANDS = 'simulator_commands'
111 OPT_RUN_DIRECTORY = 'run_directory'
114 # Actions
116 def _action_add_file(self, action):
117 dlg = gtk.FileChooserDialog(_('Add file(s)'), parent=self._mainwindow,
118 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
119 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
120 dlg.set_select_multiple(True)
121 resp = dlg.run()
122 if resp == gtk.RESPONSE_ACCEPT:
123 for filename in dlg.get_filenames():
124 self._app_exec('oread ' + filename)
125 dlg.destroy()
127 def _action_update(self, action):
128 self._ctxt.update()
130 def _action_new_math(self, action):
131 dlg = gtk.Dialog(_('New math signal'), parent=self._mainwindow,
132 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
133 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
135 # Label and entry
136 hbox = gtk.HBox()
137 label = gtk.Label(_('Expression:'))
138 hbox.pack_start(label)
139 entry = gtk.Entry()
140 hbox.pack_start(entry)
141 dlg.vbox.pack_start(hbox)
143 dlg.show_all()
144 resp = dlg.run()
145 if resp == gtk.RESPONSE_ACCEPT:
146 expr = entry.get_text()
147 self._app_exec('%s' % expr)
148 self._app_exec('oimport %s' % expr.split('=')[0].strip())
149 dlg.destroy()
151 def _action_execute_script(self, action):
152 dlg = gtk.FileChooserDialog(_('Execute script'), parent=self._mainwindow,
153 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
154 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
155 resp = dlg.run()
156 filename = dlg.get_filename()
157 dlg.destroy()
158 if resp == gtk.RESPONSE_ACCEPT:
159 self._app_exec('oexec ' + filename)
161 def _action_netlist_and_simulate(self, action):
162 dlg = gui.dialogs.Run_Netlister_and_Simulate_Dialog()
163 dlg.display(self._actions)
164 actions = dlg.run()
165 if actions is None:
166 return
167 self._actions = actions
168 run_dir = actions['run_from']
169 if actions['run_netlister'][0]:
170 if not self._run_ext_command(actions['run_netlister'][1][0], run_dir):
171 return
172 if actions['run_simulator'][0]:
173 if not self._run_ext_command(actions['run_simulator'][1][0], run_dir):
174 return
175 if actions['update']:
176 self._ctxt.update()
178 def _action_quit(self, action):
179 self._write_config()
180 readline.write_history_file(self.hist_file)
181 gtk.main_quit()
182 sys.exit()
184 def _action_figure(self, action, w, fignum):
185 if not (w.flags() & gtk.VISIBLE):
186 w.show()
187 else:
188 w.window.show()
189 self._app_exec('%%oselect %d-1' % fignum)
192 # UI Creation functions
194 def _create_menubar(self):
195 # tuple format:
196 # (name, stock-id, label, accelerator, tooltip, callback)
197 actions = [
198 ('File', None, _('_File')),
199 ('Add file(s)...', gtk.STOCK_ADD, _('_Add file(s)...'), None, None,
200 self._action_add_file),
201 ('Update files', gtk.STOCK_REFRESH, _('_Update'), None, None,
202 self._action_update),
203 ('Execute script...', gtk.STOCK_MEDIA_PLAY, _('_Execute script...'),
204 None, None, self._action_execute_script),
205 ("New Math Signal...", gtk.STOCK_NEW, _('_New Math Signal'), None,
206 None, self._action_new_math),
207 ("Run netlister and simulate...", gtk.STOCK_MEDIA_FORWARD,\
208 _("_Run netlister and simulate..."), None, None,\
209 self._action_netlist_and_simulate),
210 ('Windows', None, _('_Windows')),
211 ('Quit', gtk.STOCK_QUIT, _('_Quit'), None, None,
212 self._action_quit),
215 actiongroup = self._actiongroup = gtk.ActionGroup('App')
216 actiongroup.add_actions(actions)
218 uimanager = self._uimanager = gtk.UIManager()
219 uimanager.add_ui_from_string(self.__ui)
220 uimanager.insert_action_group(actiongroup, 0)
221 return uimanager.get_accel_group(), uimanager.get_widget('/MenuBar')
223 def _create_treeview(self):
224 celltext = gtk.CellRendererText()
225 col = gtk.TreeViewColumn(_('Signal'), celltext, text=0)
226 tv = gtk.TreeView()
227 col.set_cell_data_func(celltext, self._reader_name_in_bold)
228 col.set_expand(True)
229 tv.append_column(col)
230 tv.set_model(self._store)
231 tv.connect('row-activated', self._row_activated)
232 tv.connect('drag_data_get', self._drag_data_get_cb)
233 tv.connect('button-press-event', self._treeview_button_press)
234 tv.drag_source_set(gtk.gdk.BUTTON1_MASK,\
235 self._from_signal_list,\
236 gtk.gdk.ACTION_COPY)
237 self._togglecell = gtk.CellRendererToggle()
238 self._togglecell.set_property('activatable', True)
239 self._togglecell.connect('toggled', self._cell_toggled, None)
240 colfreeze = gtk.TreeViewColumn(_('Freeze'), self._togglecell)
241 colfreeze.add_attribute(self._togglecell, 'active', 2)
242 tv.append_column(colfreeze)
243 tv.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
244 return tv
246 def _reader_name_in_bold(self, column, cell, model, iter, data=None):
247 if len(model.get_path(iter)) == 1:
248 cell.set_property('markup', "<b>" + model.get_value(iter, 0) +\
249 "</b>")
250 else:
251 cell.set_property('text', model.get_value(iter, 0))
253 def _create_widgets(self):
254 accel_group, self._menubar = self._create_menubar()
255 self._treeview = self._create_treeview()
257 sw = gtk.ScrolledWindow()
258 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
259 sw.add(self._treeview)
261 vbox = gtk.VBox()
262 vbox.pack_start(self._menubar, False)
263 vbox.pack_start(sw)
265 w = self._mainwindow = gtk.Window(gtk.WINDOW_TOPLEVEL)
266 w.set_title(_('Oscopy GUI'))
267 w.add(vbox)
268 w.add_accel_group(accel_group)
269 w.connect('destroy', lambda w, e: w.hide() or True)
270 w.connect('delete-event', lambda w, e: w.hide() or True)
271 w.set_default_size(400, 300)
272 w.show_all()
273 w.drag_dest_set(gtk.DEST_DEFAULT_MOTION |\
274 gtk.DEST_DEFAULT_HIGHLIGHT |\
275 gtk.DEST_DEFAULT_DROP,
276 self._to_main_win, gtk.gdk.ACTION_COPY)
277 w.connect('drag_data_received', self._drag_data_received_main_cb)
279 def _create_figure_popup_menu(self, figure, graph):
280 figmenu = gui.menus.FigureMenu()
281 return figmenu.create_menu(figure, graph, self._app_exec)
283 def show_all(self):
284 self._mainwindow.show()
287 # Event-triggered functions
289 def _treeview_button_press(self, widget, event):
290 if event.button == 3:
291 tv = widget
292 ret = tv.get_path_at_pos(int(event.x), int(event.y))
293 if ret is None: return True
294 path, tvc, x, y = ret
295 if len(path) == 1:
296 # Not supported to add a full file
297 return True
298 sel = tv.get_selection()
299 if path not in sel.get_selected_rows()[1]:
300 # Click in another path than the one selected
301 sel.unselect_all()
302 sel.select_path(path)
303 signals = {}
304 def add_sig_func(tm, p, iter):
305 name = tm.get_value(iter, 0)
306 signals[name] = self._ctxt.signals[name]
307 sel.selected_foreach(add_sig_func)
308 tvmenu = gui.menus.TreeviewMenu(self.create)
309 menu = tvmenu.make_menu(self._ctxt.figures, signals)
310 menu.show_all()
311 menu.popup(None, None, None, event.button, event.time)
312 return True
313 if event.button == 1:
314 # It is not _that_ trivial to keep the selection when user start
315 # to drag. The default handler reset the selection when button 1
316 # is pressed. So we use this handler to store the selection
317 # until drag has been recognized.
318 tv = widget
319 sel = tv.get_selection()
320 rows = sel.get_selected_rows()[1]
321 self._rows_for_drag = rows
322 return False
324 def _row_activated(self, widget, path, col):
325 if len(path) == 1:
326 return
328 row = self._store[path]
329 self._app_exec('ocreate %s' % row[0])
331 def _axes_enter(self, event):
332 self._figure_enter(event)
333 self._current_graph = event.inaxes
335 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
336 fig_num = self._ctxt.figures.index(self._current_figure) + 1
337 self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
339 def _axes_leave(self, event):
340 # Unused for better user interaction
341 # self._current_graph = None
342 pass
344 def _figure_enter(self, event):
345 self._current_figure = event.canvas.figure
346 if hasattr(event, 'inaxes') and event.inaxes is not None:
347 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
348 else:
349 axes_num = 1
350 fig_num = self._ctxt.figures.index(self._current_figure) + 1
351 self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
353 def _figure_leave(self, event):
354 # self._current_figure = None
355 pass
357 def _cell_toggled(self, cellrenderer, path, data):
358 if len(path) == 3:
359 # Single signal
360 if self._store[path][1].freeze:
361 cmd = 'ounfreeze'
362 else:
363 cmd = 'ofreeze'
364 self._app_exec('%s %s' % (cmd, self._store[path][0]))
365 elif len(path) == 1:
366 # Whole reader
367 parent = self._store.get_iter(path)
368 freeze = not self._store.get_value(parent, 2)
369 if self._store[path][2]:
370 cmd = 'ounfreeze'
371 else:
372 cmd = 'ofreeze'
373 self._store.set_value(parent, 2, freeze)
374 iter = self._store.iter_children(parent)
375 while iter:
376 self._app_exec('%s %s' % (cmd, self._store.get_value(iter, 0)))
377 iter = self._store.iter_next(iter)
380 # Callbacks for App
382 def create(self, sigs):
383 """ Instanciate the window widget with the figure inside, set the
384 relevant events and add it to the 'Windows' menu.
385 Finally, select the first graph of this figure.
387 The figure has been instanciated by the application
388 and is assumed to be the last one in Context's figure list
391 fignum = len(self._ctxt.figures) + 1
392 fig = IOscopy_GTK_Figure(sigs, None,
393 _('Figure %d') % fignum)
394 self._ctxt.create(fig)
396 fig.window.connect('drag_data_received', fig.drag_data_received_cb,
397 self._ctxt.signals)
398 fig.canvas.mpl_connect('axes_enter_event', self._axes_enter)
399 fig.canvas.mpl_connect('axes_leave_event', self._axes_leave)
400 fig.canvas.mpl_connect('figure_enter_event', self._figure_enter)
401 fig.canvas.mpl_connect('figure_leave_event', self._figure_leave)
403 # Add it to the 'Windows' menu
404 actions = [('Figure %d' % fignum, None, _('Figure %d') % fignum,
405 None, None, self._action_figure)]
406 self._actiongroup.add_actions(actions, (fig.window, fignum))
407 ui = "<ui>\
408 <menubar name=\"MenuBar\">\
409 <menu action=\"Windows\">\
410 <menuitem action=\"Figure %d\"/>\
411 </menu>\
412 </menubar>\
413 </ui>" % fignum
414 merge_id = self._uimanager.add_ui_from_string(ui)
415 self._fignum_to_merge_id[fignum] = merge_id
416 self._app_exec('%%oselect %d-1' % fignum)
417 return fig
419 def destroy(self, num):
420 if not num.isdigit() or int(num) > len(self._ctxt.figures):
421 return
422 else:
423 fignum = int(num)
424 action = self._uimanager.get_action('/MenuBar/Windows/Figure %d' %
425 fignum)
426 if action is not None:
427 self._actiongroup.remove_action(action)
428 self._uimanager.remove_ui(self._fignum_to_merge_id[fignum])
429 self._fignum_to_windows[fignum].destroy()
431 # Search algorithm from pygtk tutorial
432 def _match_func(self, row, data):
433 column, key = data
434 return row[column] == key
436 def _search(self, rows, func, data):
437 if not rows: return None
438 for row in rows:
439 if func(row, data):
440 return row
441 result = self._search(row.iterchildren(), func, data)
442 if result: return result
443 return None
445 def freeze(self, signals):
446 for signal in signals.split(','):
447 match_row = self._search(self._store, self._match_func,\
448 (0, signal.strip()))
449 if match_row is not None:
450 match_row[2] = match_row[1].freeze
451 parent = self._store.iter_parent(match_row.iter)
452 iter = self._store.iter_children(parent)
453 freeze = match_row[2]
454 while iter:
455 if not self._store.get_value(iter, 2) == freeze:
456 break
457 iter = self._store.iter_next(iter)
458 if iter == None:
459 # All row at the same freeze value,
460 # set freeze for the reader
461 self._store.set_value(parent, 2, freeze)
462 else:
463 # Set reader freeze to false
464 self._store.set_value(parent, 2, False)
466 def add_file(self, filename):
467 if filename.strip() in self._ctxt.readers:
468 it = self._store.append(None, (filename.strip(), None, False))
469 for name, sig in self._ctxt.readers[filename.strip()]\
470 .signals.iteritems():
471 self._store.append(it, (name, sig, sig.freeze))
474 # Callbacks for drag and drop
476 def _drag_data_received_main_cb(self, widget, drag_context, x, y, selection,
477 target_type, time):
478 name = selection.data
479 if type(name) == str and name.startswith('file://'):
480 print name[7:].strip()
481 self._app_exec('%%oread %s' % name[7:].strip())
483 def _drag_data_get_cb(self, widget, drag_context, selection, target_type,\
484 time):
485 if target_type == self._TARGET_TYPE_SIGNAL:
486 tv = widget
487 sel = tv.get_selection()
488 (model, pathlist) = sel.get_selected_rows()
489 iter = self._store.get_iter(pathlist[0])
490 # Use the path list stored while button 1 has been pressed
491 # See self._treeview_button_press()
492 data = ' '.join(map(lambda x:self._store[x][1].name, self._rows_for_drag))
493 selection.set(selection.target, 8, data)
494 return True
496 # Configuration-file related functions
498 def _init_config(self):
499 # initialize configuration stuff
500 path = BaseDirectory.save_config_path('oscopy')
501 self.config_file = os.path.join(path, 'gui')
502 self.hist_file = os.path.join(path, 'history')
503 section = App.SECTION
504 self.config = ConfigParser.RawConfigParser()
505 self.config.add_section(section)
506 # defaults
507 self.config.set(section, App.OPT_NETLISTER_COMMANDS, '')
508 self.config.set(section, App.OPT_SIMULATOR_COMMANDS, '')
509 self.config.set(section, App.OPT_RUN_DIRECTORY, '.')
511 def _sanitize_list(self, lst):
512 return filter(lambda x: len(x) > 0, map(lambda x: x.strip(), lst))
514 def _actions_from_config(self, config):
515 section = App.SECTION
516 netlister_commands = config.get(section, App.OPT_NETLISTER_COMMANDS)
517 netlister_commands = self._sanitize_list(netlister_commands.split(';'))
518 simulator_commands = config.get(section, App.OPT_SIMULATOR_COMMANDS)
519 simulator_commands = self._sanitize_list(simulator_commands.split(';'))
520 actions = {
521 'run_netlister': (True, netlister_commands),
522 'run_simulator': (True, simulator_commands),
523 'update': True,
524 'run_from': config.get(section, App.OPT_RUN_DIRECTORY)}
525 return actions
527 def _actions_to_config(self, actions, config):
528 section = App.SECTION
529 netlister_commands = ';'.join(actions['run_netlister'][1])
530 simulator_commands = ';'.join(actions['run_simulator'][1])
531 config.set(section, App.OPT_NETLISTER_COMMANDS, netlister_commands)
532 config.set(section, App.OPT_SIMULATOR_COMMANDS, simulator_commands)
533 config.set(section, App.OPT_RUN_DIRECTORY, actions['run_from'])
535 def _read_config(self):
536 self.config.read(self.config_file)
537 self._actions = self._actions_from_config(self.config)
539 def _write_config(self):
540 self._actions_to_config(self._actions, self.config)
541 with open(self.config_file, 'w') as f:
542 self.config.write(f)
544 # DBus routines
545 @dbus.service.method('org.freedesktop.OscopyIFace')
546 def dbus_update(self):
547 gobject.idle_add(self._activate_net_and_sim)
549 @dbus.service.method('org.freedesktop.OscopyIFace')
550 def dbus_running(self):
551 return
553 # Misc functions
554 def update_from_usr1(self):
555 self._ctxt.update()
557 def update_from_usr2(self):
558 gobject.idle_add(self._activate_net_and_sim)
560 def _activate_net_and_sim(self):
561 if self._actiongroup is not None:
562 action = self._actiongroup.get_action("Run netlister and simulate...")
563 action.activate()
565 def _run_ext_command(self, cmd, run_dir):
566 old_dir = os.getcwd()
567 os.chdir(run_dir)
568 try:
569 status, output = commands.getstatusoutput(cmd)
570 if status:
571 msg = _("Executing command '%s' failed.") % cmd
572 report_error(self._mainwindow, msg)
573 return status == 0
574 finally:
575 os.chdir(old_dir)
577 def _app_exec(self, line):
578 (first, last) = line.split(' ', 1)
579 if first.startswith('%') or self.shell.find_magic(first.split()[0]) is not None:
580 name = first.lstrip('%')
581 self.shell.run_line_magic(name, last.strip())
582 else:
583 self.shell.ex(line)
585 def usr1_handler(signum, frame):
586 app.update_from_usr1()
588 def usr2_handler(signum, frame):
589 app.update_from_usr2()