app: more tests in do_select
[oscopy/ivan.git] / oscopy_ui.py
blob5e08e1548a4bb1491cdee47ea9b566a8a3a6b692
1 #!/usr/bin/python
2 from __future__ import with_statement
4 import gobject
5 import gtk
6 import signal
7 import os
8 import readline
9 import commands
10 import ConfigParser
11 import dbus, dbus.service, dbus.glib
12 from xdg import BaseDirectory
14 import oscopy
16 from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
17 from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
18 import oscopy_gui
20 # Note: for crosshair, see gtk.gdk.GC / function = gtk.gdk.XOR
22 def report_error(parent, msg):
23 dlg = gtk.MessageDialog(parent,
24 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
25 gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, msg)
26 dlg.set_title(parent.get_title())
27 dlg.run()
28 dlg.destroy()
30 class OscopyAppUI(oscopy.OscopyApp):
31 def __init__(self, context):
32 oscopy.OscopyApp.__init__(self, context)
33 self._callbacks = {}
34 self._autorefresh = True
36 def connect(self, event, func, data):
37 if not isinstance(event, str):
38 return
39 if hasattr(self, 'do_'+event):
40 self._callbacks[event] = {func: data}
42 def postcmd(self, stop, line):
43 oscopy.OscopyApp.postcmd(self, stop, line)
44 if not line.strip():
45 return
46 event = line.split()[0].strip()
47 if len(line.split()) > 1:
48 args = line.split(' ', 1)[1].strip()
49 else:
50 args = ''
51 if self._callbacks.has_key(event):
52 for func, data in self._callbacks[event].iteritems():
53 func(event, args, data)
54 if self._autorefresh and self._current_figure is not None and\
55 self._current_figure.canvas is not None:
56 self._current_figure.canvas.draw()
58 def help_refresh(self):
59 print 'refresh FIG#|on|off|current|all'
60 print ' on|off toggle auto refresh of current figure'
61 print ' current|all refresh either current figure or all'
62 print ' FIG# figure to refresh'
63 print 'without arguments refresh current figure'
64 def do_refresh(self, args):
65 if args == 'on':
66 self._autorefresh = True
67 elif args == 'off':
68 self._autorefresh = False
69 elif args == 'current' or args == '':
70 if self._current_figure is not None and\
71 self._current_figure.canvas is not None:
72 self._current_figure.canvas.draw()
73 elif args == 'all':
74 for fig in self._ctxt.figures:
75 if fig.canvas is not None:
76 fig.canvas.draw()
77 elif args.isdigit():
78 fignum = int(args) - 1
79 if fignum >= 0 and fignum < len(self._ctxt.figures):
80 if self._ctxt.figures[fignum].canvas is not None:
81 print 'refreshing'
82 self._ctxt.figures[fignum].canvas.draw()
84 def do_pause(self, args):
85 print "Pause command disabled in UI"
87 def do_plot(self, line):
88 print "Plot command disabled in UI"
90 class App(dbus.service.Object):
91 __ui = '''<ui>
92 <menubar name="MenuBar">
93 <menu action="File">
94 <menuitem action="Add file(s)..."/>
95 <menuitem action="Update files"/>
96 <menuitem action="Execute script..."/>
97 <menuitem action="New Math Signal..."/>
98 <menuitem action="Run netlister and simulate..."/>
99 <menuitem action="Quit"/>
100 </menu>
101 <menu action="Windows">
102 <menuitem action="Show terminal"/>
103 </menu>
104 </menubar>
105 </ui>'''
107 def __init__(self, bus_name, object_path='/org/freedesktop/Oscopy'):
108 dbus.service.Object.__init__(self, bus_name, object_path)
109 self._scale_to_str = {'lin': 'Linear', 'logx': 'LogX', 'logy': 'LogY',\
110 'loglog': 'Loglog'}
111 self._windows_to_figures = {}
112 self._fignum_to_windows = {}
113 self._fignum_to_merge_id = {}
114 self._current_graph = None
115 self._current_figure = None
116 self._term_win = None
117 self._prompt = "oscopy-ui>"
118 self._init_config()
119 self._read_config()
121 self._TARGET_TYPE_SIGNAL = 10354
122 self._from_signal_list = [("oscopy-signals", gtk.TARGET_SAME_APP,\
123 self._TARGET_TYPE_SIGNAL)]
124 self._to_figure = [("oscopy-signals", gtk.TARGET_SAME_APP,\
125 self._TARGET_TYPE_SIGNAL)]
127 self._ctxt = oscopy.Context()
128 self._app = OscopyAppUI(self._ctxt)
129 self._app.connect('read', self._add_file, None)
130 self._app.connect('math', self._add_file, None)
131 self._app.connect('freeze', self._freeze, None)
132 self._app.connect('unfreeze', self._freeze, None)
133 self._app.connect('create', self._create, None)
134 self._app.connect('destroy', self._destroy, None)
135 self._app.connect('quit', lambda e, s, d: self._action_quit(None), None)
136 self._app.connect('exit', lambda e, s, d: self._action_quit(None), None)
137 self._store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
138 gobject.TYPE_BOOLEAN)
139 self._create_widgets()
140 #self._app_exec('read demo/irf540.dat')
141 #self._app_exec('read demo/ac.dat')
142 #self._add_file('demo/res.dat')
144 SECTION = 'oscopy_ui'
145 OPT_NETLISTER_COMMANDS = 'netlister_commands'
146 OPT_SIMULATOR_COMMANDS = 'simulator_commands'
147 OPT_RUN_DIRECTORY = 'run_directory'
150 # Actions
152 def _action_add_file(self, action):
153 dlg = gtk.FileChooserDialog('Add file(s)', parent=self._mainwindow,
154 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
155 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
156 dlg.set_select_multiple(True)
157 resp = dlg.run()
158 if resp == gtk.RESPONSE_ACCEPT:
159 for filename in dlg.get_filenames():
160 self._app_exec("read " + filename)
161 dlg.destroy()
163 def _action_update(self, action):
164 self._ctxt.update()
166 def _action_new_math(self, action):
167 dlg = gtk.Dialog('New math signal', parent=self._mainwindow,
168 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
169 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
171 # Label and entry
172 hbox = gtk.HBox()
173 label = gtk.Label('Expression:')
174 hbox.pack_start(label)
175 entry = gtk.Entry()
176 hbox.pack_start(entry)
177 dlg.vbox.pack_start(hbox)
179 dlg.show_all()
180 resp = dlg.run()
181 if resp == gtk.RESPONSE_ACCEPT:
182 expr = entry.get_text()
183 self._app_exec('%s' % expr)
185 dlg.destroy()
187 def _action_show_terminal(self, action):
188 if self._term_win.flags() & gtk.VISIBLE:
189 self._term_win.hide()
190 else:
191 self._term_win.show()
193 def _action_execute_script(self, action):
194 dlg = gtk.FileChooserDialog('Execute script', parent=self._mainwindow,
195 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
196 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
197 resp = dlg.run()
198 filename = dlg.get_filename()
199 dlg.destroy()
200 if resp == gtk.RESPONSE_ACCEPT:
201 self._app_exec("exec " + filename)
203 def _action_netlist_and_simulate(self, action):
204 dlg = oscopy_gui.dialogs.Run_Netlister_and_Simulate_Dialog()
205 dlg.display(self._actions)
206 actions = dlg.run()
207 if actions is None:
208 return
209 self._actions = actions
210 run_dir = actions['run_from']
211 if actions['run_netlister'][0]:
212 if not self._run_ext_command(actions['run_netlister'][1][0], run_dir):
213 return
214 if actions['run_simulator'][0]:
215 if not self._run_ext_command(actions['run_simulator'][1][0], run_dir):
216 return
217 if actions['update']:
218 self._ctxt.update()
220 def _action_quit(self, action):
221 self._write_config()
222 readline.write_history_file(self.hist_file)
223 main_loop.quit()
225 def _action_figure(self, action, w, fignum):
226 if not (w.flags() & gtk.VISIBLE):
227 w.show()
228 else:
229 w.window.show()
230 self._app_exec('select %d-1' % fignum)
233 # UI Creation functions
235 def _create_menubar(self):
236 # tuple format:
237 # (name, stock-id, label, accelerator, tooltip, callback)
238 actions = [
239 ('File', None, '_File'),
240 ('Add file(s)...', gtk.STOCK_ADD, '_Add file(s)...', None, None,
241 self._action_add_file),
242 ('Update files', gtk.STOCK_REFRESH, '_Update', None, None,
243 self._action_update),
244 ('Execute script...', gtk.STOCK_MEDIA_PLAY, '_Execute script...',
245 None, None, self._action_execute_script),
246 ("New Math Signal...", gtk.STOCK_NEW, '_New Math Signal', None,
247 None, self._action_new_math),
248 ("Run netlister and simulate...", gtk.STOCK_MEDIA_FORWARD,\
249 "_Run netlister and simulate...", None, None,\
250 self._action_netlist_and_simulate),
251 ('Windows', None, '_Windows'),
252 ('Quit', gtk.STOCK_QUIT, '_Quit', None, None,
253 self._action_quit),
256 actiongroup = self._actiongroup = gtk.ActionGroup('App')
257 actiongroup.add_actions(actions)
259 ta = gtk.ToggleAction('Show terminal', '_Show terminal', None, None)
260 ta.set_active(True)
261 ta.connect('activate', self._action_show_terminal)
262 actiongroup.add_action(ta)
264 uimanager = self._uimanager = gtk.UIManager()
265 uimanager.add_ui_from_string(self.__ui)
266 uimanager.insert_action_group(actiongroup, 0)
267 return uimanager.get_accel_group(), uimanager.get_widget('/MenuBar')
269 def _create_treeview(self):
270 celltext = gtk.CellRendererText()
271 col = gtk.TreeViewColumn('Signal', celltext, text=0)
272 tv = gtk.TreeView()
273 col.set_cell_data_func(celltext, self._reader_name_in_bold)
274 col.set_expand(True)
275 tv.append_column(col)
276 tv.set_model(self._store)
277 tv.connect('row-activated', self._row_activated)
278 tv.connect('drag_data_get', self._drag_data_get_cb)
279 tv.drag_source_set(gtk.gdk.BUTTON1_MASK,\
280 self._from_signal_list,\
281 gtk.gdk.ACTION_COPY)
282 self._togglecell = gtk.CellRendererToggle()
283 self._togglecell.set_property('activatable', True)
284 self._togglecell.connect('toggled', self._cell_toggled, None)
285 colfreeze = gtk.TreeViewColumn('Freeze', self._togglecell)
286 colfreeze.add_attribute(self._togglecell, 'active', 2)
287 tv.append_column(colfreeze)
288 tv.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
289 return tv
291 def _reader_name_in_bold(self, column, cell, model, iter, data=None):
292 if len(model.get_path(iter)) == 1:
293 cell.set_property('markup', "<b>" + model.get_value(iter, 0) +\
294 "</b>")
295 else:
296 cell.set_property('text', model.get_value(iter, 0))
298 def _create_widgets(self):
299 accel_group, self._menubar = self._create_menubar()
300 self._treeview = self._create_treeview()
301 self._create_terminal_window()
303 sw = gtk.ScrolledWindow()
304 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
305 sw.add(self._treeview)
307 vbox = gtk.VBox()
308 vbox.pack_start(self._menubar, False)
309 vbox.pack_start(sw)
311 w = self._mainwindow = gtk.Window(gtk.WINDOW_TOPLEVEL)
312 w.set_title('Oscopy GUI')
313 w.add(vbox)
314 w.add_accel_group(accel_group)
315 w.connect('destroy', lambda *x: self._action_quit(None))
316 w.set_default_size(400, 300)
317 w.show_all()
319 def _create_terminal_window(self):
320 if self._term_win is None:
321 self._term_win = oscopy_gui.dialogs.TerminalWindow(self._prompt,
322 self._app.intro,
323 self.hist_file,
324 self._app_exec)
325 self._term_win.create()
326 self._term_win.connect('delete-event', lambda w, e: w.hide() or True)
327 if not (self._term_win.flags() & gtk.VISIBLE):
328 self._term_win.show_all()
330 def _create_figure_popup_menu(self, figure, graph):
331 figmenu = oscopy_gui.menus.FigureMenu()
332 return figmenu.create_menu(self._store, figure, graph, self._app_exec)
335 # Event-triggered functions
337 def _treeview_button_press(self, widget, event):
338 if event.button == 3:
339 tv = widget
340 path, tvc, x, y = tv.get_path_at_pos(int(event.x), int(event.y))
341 if len(path) == 1:
342 return
343 tv.set_cursor(path)
344 row = self._store[path]
345 signals = {row[0]: row[1]}
346 menu = self._create_treeview_popup_menu(signals, path)
347 menu.show_all()
348 menu.popup(None, None, None, event.button, event.time)
350 def _button_press(self, event):
351 if event.button == 3:
352 menu = self._create_figure_popup_menu(event.canvas.figure, event.inaxes)
353 menu.show_all()
354 menu.popup(None, None, None, event.button, event.guiEvent.time)
356 #TODO: _windows_to_figures consistency...
357 # think of a better way to map events to Figure objects
358 def _row_activated(self, widget, path, col):
359 if len(path) == 1:
360 return
362 row = self._store[path]
363 self._app_exec('create %s' % row[0])
365 def _axes_enter(self, event):
366 self._figure_enter(event)
367 self._current_graph = event.inaxes
368 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
369 fig_num = self._ctxt.figures.index(self._current_figure) + 1
370 self._app_exec('select %d-%d' % (fig_num, axes_num))
372 def _axes_leave(self, event):
373 # Unused for better user interaction
374 # self._current_graph = None
375 pass
377 def _figure_enter(self, event):
378 self._current_figure = event.canvas.figure
379 if hasattr(event, 'inaxes') and event.inaxes is not None:
380 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
381 else:
382 axes_num = 1
383 fig_num = self._ctxt.figures.index(self._current_figure) + 1
384 self._app_exec('select %d-%d' % (fig_num, axes_num))
386 def _figure_leave(self, event):
387 # self._current_figure = None
388 pass
390 def _cell_toggled(self, cellrenderer, path, data):
391 if len(path) == 3:
392 # Single signal
393 if self._store[path][1].freeze:
394 cmd = 'unfreeze'
395 else:
396 cmd = 'freeze'
397 self._app_exec('%s %s' % (cmd, self._store[path][0]))
398 elif len(path) == 1:
399 # Whole reader
400 parent = self._store.get_iter(path)
401 freeze = not self._store.get_value(parent, 2)
402 if self._store[path][2]:
403 cmd = 'unfreeze'
404 else:
405 cmd = 'freeze'
406 self._store.set_value(parent, 2, freeze)
407 iter = self._store.iter_children(parent)
408 while iter:
409 self._app_exec('%s %s' % (cmd, self._store.get_value(iter, 0)))
410 iter = self._store.iter_next(iter)
413 # Callbacks for App
415 def _create(self, event, signals, data=None):
416 fig = self._ctxt.figures[len(self._ctxt.figures) - 1]
417 fignum = len(self._ctxt.figures)
419 w = gtk.Window()
420 self._windows_to_figures[w] = fig
421 self._fignum_to_windows[fignum] = w
422 w.set_title('Figure %d' % fignum)
423 vbox = gtk.VBox()
424 w.add(vbox)
425 canvas = FigureCanvas(fig)
426 canvas.mpl_connect('button_press_event', self._button_press)
427 canvas.mpl_connect('axes_enter_event', self._axes_enter)
428 canvas.mpl_connect('axes_leave_event', self._axes_leave)
429 canvas.mpl_connect('figure_enter_event', self._figure_enter)
430 canvas.mpl_connect('figure_leave_event', self._figure_leave)
431 w.connect("drag_data_received", self._drag_data_received_cb)
432 w.connect('delete-event', lambda w, e: w.hide() or True)
433 w.drag_dest_set(gtk.DEST_DEFAULT_MOTION |\
434 gtk.DEST_DEFAULT_HIGHLIGHT |\
435 gtk.DEST_DEFAULT_DROP,
436 self._to_figure, gtk.gdk.ACTION_COPY)
437 vbox.pack_start(canvas)
438 toolbar = NavigationToolbar(canvas, w)
439 vbox.pack_start(toolbar, False, False)
440 w.resize(400, 300)
441 w.show_all()
443 # Add it to the 'Windows' menu
444 actions = [('Figure %d' % fignum, None, 'Figure %d' % fignum,
445 None, None, self._action_figure)]
446 self._actiongroup.add_actions(actions, (w, fignum))
447 ui = "<ui>\
448 <menubar name=\"MenuBar\">\
449 <menu action=\"Windows\">\
450 <menuitem action=\"Figure %d\"/>\
451 </menu>\
452 </menubar>\
453 </ui>" % fignum
454 merge_id = self._uimanager.add_ui_from_string(ui)
455 self._fignum_to_merge_id[fignum] = merge_id
456 self._app_exec('select %d-1' % fignum)
458 def _destroy(self, event, num, data=None):
459 if not num.isdigit() or int(num) > len(self._ctxt.figures):
460 return
461 else:
462 fignum = int(num)
463 action = self._uimanager.get_action('/MenuBar/Windows/Figure %d' %
464 fignum)
465 if action is not None:
466 self._actiongroup.remove_action(action)
467 self._uimanager.remove_ui(self._fignum_to_merge_id[fignum])
468 self._fignum_to_windows[fignum].destroy()
470 # Search algorithm from pygtk tutorial
471 def _match_func(self, row, data):
472 column, key = data
473 return row[column] == key
475 def _search(self, rows, func, data):
476 if not rows: return None
477 for row in rows:
478 if func(row, data):
479 return row
480 result = self._search(row.iterchildren(), func, data)
481 if result: return result
482 return None
484 def _freeze(self, event, signals, data=None):
485 for signal in signals.split(','):
486 match_row = self._search(self._store, self._match_func,\
487 (0, signal.strip()))
488 if match_row is not None:
489 match_row[2] = match_row[1].freeze
490 parent = self._store.iter_parent(match_row.iter)
491 iter = self._store.iter_children(parent)
492 freeze = match_row[2]
493 while iter:
494 if not self._store.get_value(iter, 2) == freeze:
495 break
496 iter = self._store.iter_next(iter)
497 if iter == None:
498 # All row at the same freeze value,
499 # set freeze for the reader
500 self._store.set_value(parent, 2, freeze)
501 else:
502 # Set reader freeze to false
503 self._store.set_value(parent, 2, False)
505 def _add_file(self, event, filename, data=None):
506 it = self._store.append(None, (filename.strip(), None, False))
507 for name, sig in self._ctxt.readers[filename.strip()]\
508 .signals.iteritems():
509 self._store.append(it, (name, sig, sig.freeze))
512 # Callbacks for drag and drop
514 def _drag_data_get_cb(self, widget, drag_context, selection, target_type,\
515 time):
516 if target_type == self._TARGET_TYPE_SIGNAL:
517 tv = widget
518 sel = tv.get_selection()
519 (model, pathlist) = sel.get_selected_rows()
520 iter = self._store.get_iter(pathlist[0])
521 data = " ".join(map(lambda x:self._store[x][1].name, pathlist))
522 selection.set(selection.target, 8, data)
523 # The multiple selection do work, but how to select signals
524 # that are not neighbours in the list? Ctrl+left do not do
525 # anything, neither alt+left or shift+left!
527 def _drag_data_received_cb(self, widget, drag_context, x, y, selection,\
528 target_type, time):
529 if target_type == self._TARGET_TYPE_SIGNAL:
530 if self._current_graph is not None:
531 signals = {}
532 for name in selection.data.split():
533 signals[name] = self._ctxt.signals[name]
534 self._current_graph.insert(signals)
535 if self._current_figure.canvas is not None:
536 self._current_figure.canvas.draw()
539 # Configuration-file related functions
541 def _init_config(self):
542 # initialize configuration stuff
543 path = BaseDirectory.load_first_config('oscopy')
544 self.config_file = os.path.join(path, 'gui')
545 self.hist_file = os.path.join(path, 'history')
546 section = App.SECTION
547 self.config = ConfigParser.RawConfigParser()
548 self.config.add_section(section)
549 # defaults
550 self.config.set(section, App.OPT_NETLISTER_COMMANDS, '')
551 self.config.set(section, App.OPT_SIMULATOR_COMMANDS, '')
552 self.config.set(section, App.OPT_RUN_DIRECTORY, '.')
554 def _sanitize_list(self, lst):
555 return filter(lambda x: len(x) > 0, map(lambda x: x.strip(), lst))
557 def _actions_from_config(self, config):
558 section = App.SECTION
559 netlister_commands = config.get(section, App.OPT_NETLISTER_COMMANDS)
560 netlister_commands = self._sanitize_list(netlister_commands.split(';'))
561 simulator_commands = config.get(section, App.OPT_SIMULATOR_COMMANDS)
562 simulator_commands = self._sanitize_list(simulator_commands.split(';'))
563 actions = {
564 'run_netlister': (True, netlister_commands),
565 'run_simulator': (True, simulator_commands),
566 'update': True,
567 'run_from': config.get(section, App.OPT_RUN_DIRECTORY)}
568 return actions
570 def _actions_to_config(self, actions, config):
571 section = App.SECTION
572 netlister_commands = ';'.join(actions['run_netlister'][1])
573 simulator_commands = ';'.join(actions['run_simulator'][1])
574 config.set(section, App.OPT_NETLISTER_COMMANDS, netlister_commands)
575 config.set(section, App.OPT_SIMULATOR_COMMANDS, simulator_commands)
576 config.set(section, App.OPT_RUN_DIRECTORY, actions['run_from'])
578 def _read_config(self):
579 self.config.read(self.config_file)
580 self._actions = self._actions_from_config(self.config)
582 def _write_config(self):
583 self._actions_to_config(self._actions, self.config)
584 with open(self.config_file, 'w') as f:
585 self.config.write(f)
587 # DBus routines
588 @dbus.service.method('org.freedesktop.OscopyIFace')
589 def dbus_update(self):
590 gobject.idle_add(self._activate_net_and_sim)
592 @dbus.service.method('org.freedesktop.OscopyIFace')
593 def dbus_running(self):
594 return
596 # Misc functions
597 def update_from_usr1(self):
598 self._ctxt.update()
600 def update_from_usr2(self):
601 gobject.idle_add(self._activate_net_and_sim)
603 def _activate_net_and_sim(self):
604 if self._actiongroup is not None:
605 action = self._actiongroup.get_action("Run netlister and simulate...")
606 action.activate()
608 def _run_ext_command(self, cmd, run_dir):
609 old_dir = os.getcwd()
610 os.chdir(run_dir)
611 try:
612 status, output = commands.getstatusoutput(cmd)
613 if status:
614 msg = "Executing command '%s' failed." % cmd
615 report_error(self._mainwindow, msg)
616 return status == 0
617 finally:
618 os.chdir(old_dir)
620 def _app_exec(self, line):
621 line = self._app.precmd(line)
622 stop = self._app.onecmd(line)
623 self._app.postcmd(stop, line)
625 def usr1_handler(signum, frame):
626 app.update_from_usr1()
628 def usr2_handler(signum, frame):
629 app.update_from_usr2()
631 if __name__ == '__main__':
632 session_bus = dbus.SessionBus()
633 bus_name = dbus.service.BusName('org.freedesktop.Oscopy', bus=session_bus)
634 app = App(bus_name)
635 main_loop = gobject.MainLoop()
636 signal.signal(signal.SIGUSR1, usr1_handler)
637 signal.signal(signal.SIGUSR2, usr2_handler)
638 main_loop.run()