ui: Add Windows menu and move 'Show Terminal' into
[oscopy.git] / oscopy_ui
blob5749109de8f850dc34701e1f82a5d046f97f3889
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 from xdg import BaseDirectory
13 import oscopy
15 from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
16 from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
17 import gui
19 def report_error(parent, msg):
20 dlg = gtk.MessageDialog(parent,
21 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
22 gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, msg)
23 dlg.set_title(parent.get_title())
24 dlg.run()
25 dlg.destroy()
27 class OscopyAppUI(oscopy.OscopyApp):
28 def __init__(self, context):
29 oscopy.OscopyApp.__init__(self, context)
30 self._callbacks = {}
31 self._autorefresh = True
33 def connect(self, event, func, data):
34 if not isinstance(event, str):
35 return
36 if hasattr(self, 'do_'+event):
37 self._callbacks[event] = {func: data}
39 def postcmd(self, stop, line):
40 oscopy.OscopyApp.postcmd(self, stop, line)
41 if not line.strip():
42 return
43 event = line.split()[0].strip()
44 if len(line.split()) > 1:
45 args = line.split(' ', 1)[1].strip()
46 else:
47 args = ''
48 if self._callbacks.has_key(event):
49 for func, data in self._callbacks[event].iteritems():
50 func(event, args, data)
51 if self._autorefresh and self._current_figure is not None and\
52 self._current_figure.canvas is not None:
53 self._current_figure.canvas.draw()
55 def help_refresh(self):
56 print 'refresh FIG#|on|off|current|all'
57 print ' on|off toggle auto refresh of current figure'
58 print ' current|all refresh either current figure or all'
59 print ' FIG# figure to refresh'
60 print 'without arguments refresh current figure'
61 def do_refresh(self, args):
62 if args == 'on':
63 self._autorefresh = True
64 elif args == 'off':
65 self._autorefresh = False
66 elif args == 'current' or args == '':
67 if self._current_figure is not None and\
68 self._current_figure.canvas is not None:
69 self._current_figure.canvas.draw()
70 elif args == 'all':
71 for fig in self._ctxt.figures:
72 if fig.canvas is not None:
73 fig.canvas.draw()
74 elif args.isdigit():
75 fignum = int(args) - 1
76 if fignum >= 0 and fignum < len(self._ctxt.figures):
77 if self._ctxt.figures[fignum].canvas is not None:
78 print 'refreshing'
79 self._ctxt.figures[fignum].canvas.draw()
81 def do_pause(self, args):
82 print "Pause command disabled in UI"
84 def do_plot(self, line):
85 print "Plot command disabled in UI"
87 class App(object):
88 __ui = '''<ui>
89 <menubar name="MenuBar">
90 <menu action="File">
91 <menuitem action="Add file"/>
92 <menuitem action="Update files"/>
93 <menuitem action="Execute script..."/>
94 <menuitem action="New Math Signal..."/>
95 <menuitem action="Run netlister and simulate..."/>
96 <menuitem action="Quit"/>
97 </menu>
98 <menu action="Windows">
99 <menuitem action="Show terminal"/>
100 </menu>
101 </menubar>
102 </ui>'''
104 def __init__(self):
105 self._scale_to_str = {'lin': 'Linear', 'logx': 'LogX', 'logy': 'LogY',\
106 'loglog': 'Loglog'}
107 self._figcount = 0
108 self._windows_to_figures = {}
109 self._current_graph = None
110 self._current_figure = None
111 self._term_win = None
112 self._prompt = "oscopy-ui>"
113 self._init_config()
114 self._read_config()
116 self._TARGET_TYPE_SIGNAL = 10354
117 self._from_signal_list = [("oscopy-signals", gtk.TARGET_SAME_APP,\
118 self._TARGET_TYPE_SIGNAL)]
119 self._to_figure = [("oscopy-signals", gtk.TARGET_SAME_APP,\
120 self._TARGET_TYPE_SIGNAL)]
122 self._ctxt = oscopy.Context()
123 self._app = OscopyAppUI(self._ctxt)
124 self._app.connect('read', self._add_file, None)
125 self._app.connect('math', self._add_file, None)
126 self._app.connect('freeze', self._freeze, None)
127 self._app.connect('unfreeze', self._freeze, None)
128 self._app.connect('create', self._create, None)
129 self._store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
130 gobject.TYPE_BOOLEAN)
131 self._create_widgets()
132 #self._app_exec('read demo/irf540.dat')
133 #self._app_exec('read demo/ac.dat')
134 #self._add_file('demo/res.dat')
136 SECTION = 'oscopy_ui'
137 OPT_NETLISTER_COMMANDS = 'netlister_commands'
138 OPT_SIMULATOR_COMMANDS = 'simulator_commands'
139 OPT_RUN_DIRECTORY = 'run_directory'
141 def _init_config(self):
142 # initialize configuration stuff
143 path = BaseDirectory.load_first_config('oscopy')
144 self.config_file = os.path.join(path, 'gui')
145 self.hist_file = os.path.join(path, 'history')
146 section = App.SECTION
147 self.config = ConfigParser.RawConfigParser()
148 self.config.add_section(section)
149 # defaults
150 self.config.set(section, App.OPT_NETLISTER_COMMANDS, '')
151 self.config.set(section, App.OPT_SIMULATOR_COMMANDS, '')
152 self.config.set(section, App.OPT_RUN_DIRECTORY, '.')
154 def _add_file(self, event, filename, data=None):
155 it = self._store.append(None, (filename.strip(), None, False))
156 for name, sig in self._ctxt.readers[filename.strip()]\
157 .signals.iteritems():
158 self._store.append(it, (name, sig, sig.freeze))
160 def _action_add_file(self, action):
161 dlg = gtk.FileChooserDialog('Add file', parent=self._mainwindow,
162 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
163 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
164 resp = dlg.run()
165 filename = dlg.get_filename()
166 dlg.destroy()
167 if resp == gtk.RESPONSE_ACCEPT:
168 self._app_exec("read " + filename)
170 def _app_exec(self, line):
171 line = self._app.precmd(line)
172 stop = self._app.onecmd(line)
173 self._app.postcmd(stop, line)
175 def _action_update(self, action):
176 self._ctxt.update()
178 def _action_new_math(self, action):
179 dlg = gtk.Dialog('New math signal', parent=self._mainwindow,
180 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
181 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
183 # Label and entry
184 hbox = gtk.HBox()
185 label = gtk.Label('Expression:')
186 hbox.pack_start(label)
187 entry = gtk.Entry()
188 hbox.pack_start(entry)
189 dlg.vbox.pack_start(hbox)
191 dlg.show_all()
192 resp = dlg.run()
193 if resp == gtk.RESPONSE_ACCEPT:
194 expr = entry.get_text()
195 self._app_exec('%s' % expr)
197 dlg.destroy()
199 def _action_show_terminal(self, action):
200 self._create_terminal_window()
202 def _action_execute_script(self, action):
203 dlg = gtk.FileChooserDialog('Execute script', parent=self._mainwindow,
204 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
205 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
206 resp = dlg.run()
207 filename = dlg.get_filename()
208 dlg.destroy()
209 if resp == gtk.RESPONSE_ACCEPT:
210 self._app_exec("exec " + filename)
212 def _action_quit(self, action):
213 self._write_config()
214 readline.write_history_file(self.hist_file)
215 main_loop.quit()
217 def _create_menubar(self):
218 # tuple format:
219 # (name, stock-id, label, accelerator, tooltip, callback)
220 actions = [
221 ('File', None, '_File'),
222 ('Add file', gtk.STOCK_ADD, '_Add file', None, None,
223 self._action_add_file),
224 ('Update files', gtk.STOCK_REFRESH, '_Update', None, None,
225 self._action_update),
226 ('Execute script...', gtk.STOCK_MEDIA_PLAY, '_Execute script...',
227 None, None, self._action_execute_script),
228 ("New Math Signal...", gtk.STOCK_NEW, '_New Math Signal', None,
229 None, self._action_new_math),
230 ("Run netlister and simulate...", gtk.STOCK_MEDIA_FORWARD,\
231 "_Run netlister and simulate...", None, None,\
232 self._action_netlist_and_simulate),
233 ('Windows', None, '_Windows'),
234 ("Show terminal", None, "_Show terminal", None, None,
235 self._action_show_terminal),
236 ('Quit', gtk.STOCK_QUIT, '_Quit', None, None,
237 self._action_quit),
239 actiongroup = gtk.ActionGroup('App')
240 actiongroup.add_actions(actions)
242 uimanager = gtk.UIManager()
243 uimanager.add_ui_from_string(self.__ui)
244 uimanager.insert_action_group(actiongroup, 0)
245 return uimanager.get_accel_group(), uimanager.get_widget('/MenuBar')
247 def _create_treeview(self):
248 celltext = gtk.CellRendererText()
249 col = gtk.TreeViewColumn('Signal', celltext, text=0)
250 tv = gtk.TreeView()
251 col.set_cell_data_func(celltext, self._reader_name_in_bold)
252 col.set_expand(True)
253 tv.append_column(col)
254 tv.set_model(self._store)
255 tv.connect('row-activated', self._row_activated)
256 tv.connect('drag_data_get', self._drag_data_get_cb)
257 tv.drag_source_set(gtk.gdk.BUTTON1_MASK,\
258 self._from_signal_list,\
259 gtk.gdk.ACTION_COPY)
260 self._togglecell = gtk.CellRendererToggle()
261 self._togglecell.set_property('activatable', True)
262 self._togglecell.connect('toggled', self._cell_toggled, None)
263 colfreeze = gtk.TreeViewColumn('Freeze', self._togglecell)
264 colfreeze.add_attribute(self._togglecell, 'active', 2)
265 tv.append_column(colfreeze)
266 tv.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
267 return tv
269 def _reader_name_in_bold(self, column, cell, model, iter, data=None):
270 if len(model.get_path(iter)) == 1:
271 cell.set_property('markup', "<b>" + model.get_value(iter, 0) +\
272 "</b>")
273 else:
274 cell.set_property('text', model.get_value(iter, 0))
276 # Search algorithm from pygtk tutorial
277 def _match_func(self, row, data):
278 column, key = data
279 return row[column] == key
281 def _search(self, rows, func, data):
282 if not rows: return None
283 for row in rows:
284 if func(row, data):
285 return row
286 result = self._search(row.iterchildren(), func, data)
287 if result: return result
288 return None
290 def _freeze(self, event, signals, data=None):
291 for signal in signals.split(','):
292 match_row = self._search(self._store, self._match_func,\
293 (0, signal.strip()))
294 if match_row is not None:
295 match_row[2] = match_row[1].freeze
296 parent = self._store.iter_parent(match_row.iter)
297 iter = self._store.iter_children(parent)
298 freeze = match_row[2]
299 while iter:
300 if not self._store.get_value(iter, 2) == freeze:
301 break
302 iter = self._store.iter_next(iter)
303 if iter == None:
304 # All row at the same freeze value,
305 # set freeze for the reader
306 self._store.set_value(parent, 2, freeze)
307 else:
308 # Set reader freeze to false
309 self._store.set_value(parent, 2, False)
311 def _cell_toggled(self, cellrenderer, path, data):
312 if len(path) == 3:
313 # Single signal
314 if self._store[path][1].freeze:
315 cmd = 'unfreeze'
316 else:
317 cmd = 'freeze'
318 self._app_exec('%s %s' % (cmd, self._store[path][0]))
319 elif len(path) == 1:
320 # Whole reader
321 parent = self._store.get_iter(path)
322 freeze = not self._store.get_value(parent, 2)
323 if self._store[path][2]:
324 cmd = 'unfreeze'
325 else:
326 cmd = 'freeze'
327 self._store.set_value(parent, 2, freeze)
328 iter = self._store.iter_children(parent)
329 while iter:
330 self._app_exec('%s %s' % (cmd, self._store.get_value(iter, 0)))
331 iter = self._store.iter_next(iter)
333 def _create_widgets(self):
334 accel_group, self._menubar = self._create_menubar()
335 self._treeview = self._create_treeview()
336 self._create_terminal_window()
338 sw = gtk.ScrolledWindow()
339 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
340 sw.add(self._treeview)
342 vbox = gtk.VBox()
343 vbox.pack_start(self._menubar, False)
344 vbox.pack_start(sw)
346 w = self._mainwindow = gtk.Window(gtk.WINDOW_TOPLEVEL)
347 w.set_title('Oscopy GUI')
348 w.add(vbox)
349 w.add_accel_group(accel_group)
350 w.connect('destroy', lambda *x: self._action_quit(None))
351 w.set_default_size(400, 300)
352 w.show_all()
354 def _create_terminal_window(self):
355 if self._term_win is None:
356 self._term_win = gui.dialogs.TerminalWindow(self._prompt,
357 self._app.intro,
358 self.hist_file,
359 self._app_exec)
360 self._term_win.create()
361 self._term_win.connect('delete-event', lambda w, e: w.hide() or True)
362 if not (self._term_win.flags() & gtk.VISIBLE):
363 self._term_win.show_all()
365 def _create_figure_popup_menu(self, figure, graph):
366 figmenu = gui.menus.FigureMenu()
367 return figmenu.create_menu(self._store, figure, graph, self._app_exec)
369 def _treeview_button_press(self, widget, event):
370 if event.button == 3:
371 tv = widget
372 path, tvc, x, y = tv.get_path_at_pos(int(event.x), int(event.y))
373 if len(path) == 1:
374 return
375 tv.set_cursor(path)
376 row = self._store[path]
377 signals = {row[0]: row[1]}
378 menu = self._create_treeview_popup_menu(signals, path)
379 menu.show_all()
380 menu.popup(None, None, None, event.button, event.time)
382 def _button_press(self, event):
383 if event.button == 3:
384 menu = self._create_figure_popup_menu(event.canvas.figure, event.inaxes)
385 menu.show_all()
386 menu.popup(None, None, None, event.button, event.guiEvent.time)
388 #TODO: _windows_to_figures consistency...
389 # think of a better way to map events to Figure objects
390 def _row_activated(self, widget, path, col):
391 if len(path) == 1:
392 return
394 row = self._store[path]
395 self._app_exec('create %s' % row[0])
397 def _create(self, event, signals, data=None):
398 fig = self._ctxt.figures[len(self._ctxt.figures) - 1]
400 w = gtk.Window()
401 self._figcount += 1
402 self._windows_to_figures[w] = fig
403 w.set_title('Figure %d' % self._figcount)
404 vbox = gtk.VBox()
405 w.add(vbox)
406 canvas = FigureCanvas(fig)
407 canvas.mpl_connect('button_press_event', self._button_press)
408 canvas.mpl_connect('axes_enter_event', self._axes_enter)
409 canvas.mpl_connect('axes_leave_event', self._axes_leave)
410 canvas.mpl_connect('figure_enter_event', self._figure_enter)
411 canvas.mpl_connect('figure_leave_event', self._figure_leave)
412 w.connect("drag_data_received", self._drag_data_received_cb)
413 w.connect('delete-event', lambda w, e: w.hide() or True)
414 w.drag_dest_set(gtk.DEST_DEFAULT_MOTION |\
415 gtk.DEST_DEFAULT_HIGHLIGHT |\
416 gtk.DEST_DEFAULT_DROP,
417 self._to_figure, gtk.gdk.ACTION_COPY)
418 vbox.pack_start(canvas)
419 toolbar = NavigationToolbar(canvas, w)
420 vbox.pack_start(toolbar, False, False)
421 w.resize(400, 300)
422 w.show_all()
423 self._app_exec('select %d-1' % len(self._ctxt.figures))
425 def _axes_enter(self, event):
426 self._current_graph = event.inaxes
427 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
428 fig_num = self._ctxt.figures.index(self._current_figure) + 1
429 self._app_exec('select %d-%d' % (fig_num, axes_num))
431 def _axes_leave(self, event):
432 # Unused for better user interaction
433 # self._current_graph = None
434 pass
436 def _figure_enter(self, event):
437 self._current_figure = event.canvas.figure
438 if hasattr(event, 'inaxes') and event.inaxes is not None:
439 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
440 else:
441 axes_num = 1
442 fig_num = self._ctxt.figures.index(self._current_figure) + 1
443 self._app_exec('select %d-%d' % (fig_num, axes_num))
445 def _figure_leave(self, event):
446 # self._current_figure = None
447 pass
449 def update_from_usr1(self):
450 self._ctxt.update()
452 def _run_ext_command(self, cmd, run_dir):
453 old_dir = os.getcwd()
454 os.chdir(run_dir)
455 try:
456 status, output = commands.getstatusoutput(cmd)
457 if status:
458 msg = "Executing command '%s' failed." % cmd
459 report_error(self._mainwindow, msg)
460 return status == 0
461 finally:
462 os.chdir(old_dir)
464 def _action_netlist_and_simulate(self, action):
465 dlg = gui.dialogs.Run_Netlister_and_Simulate_Dialog()
466 dlg.display(self._actions)
467 actions = dlg.run()
468 if actions is None:
469 return
470 self._actions = actions
471 run_dir = actions['run_from']
472 if actions['run_netlister'][0]:
473 if not self._run_ext_command(actions['run_netlister'][1][0], run_dir):
474 return
475 if actions['run_simulator'][0]:
476 if not self._run_ext_command(actions['run_simulator'][1][0], run_dir):
477 return
478 if actions['update']:
479 self._ctxt.update()
481 def _drag_data_get_cb(self, widget, drag_context, selection, target_type,\
482 time):
483 if target_type == self._TARGET_TYPE_SIGNAL:
484 tv = widget
485 sel = tv.get_selection()
486 (model, pathlist) = sel.get_selected_rows()
487 iter = self._store.get_iter(pathlist[0])
488 data = " ".join(map(lambda x:self._store[x][1].name, pathlist))
489 selection.set(selection.target, 8, data)
490 # The multiple selection do work, but how to select signals
491 # that are not neighbours in the list? Ctrl+left do not do
492 # anything, neither alt+left or shift+left!
494 def _drag_data_received_cb(self, widget, drag_context, x, y, selection,\
495 target_type, time):
496 if target_type == self._TARGET_TYPE_SIGNAL:
497 if self._current_graph is not None:
498 signals = {}
499 for name in selection.data.split():
500 signals[name] = self._ctxt.signals[name]
501 self._current_graph.insert(signals)
502 if self._current_figure.canvas is not None:
503 self._current_figure.canvas.draw()
505 def _sanitize_list(self, lst):
506 return filter(lambda x: len(x) > 0, map(lambda x: x.strip(), lst))
508 def _actions_from_config(self, config):
509 section = App.SECTION
510 netlister_commands = config.get(section, App.OPT_NETLISTER_COMMANDS)
511 netlister_commands = self._sanitize_list(netlister_commands.split(';'))
512 simulator_commands = config.get(section, App.OPT_SIMULATOR_COMMANDS)
513 simulator_commands = self._sanitize_list(simulator_commands.split(';'))
514 actions = {
515 'run_netlister': (True, netlister_commands),
516 'run_simulator': (True, simulator_commands),
517 'update': True,
518 'run_from': config.get(section, App.OPT_RUN_DIRECTORY)}
519 return actions
521 def _actions_to_config(self, actions, config):
522 section = App.SECTION
523 netlister_commands = ';'.join(actions['run_netlister'][1])
524 simulator_commands = ';'.join(actions['run_simulator'][1])
525 config.set(section, App.OPT_NETLISTER_COMMANDS, netlister_commands)
526 config.set(section, App.OPT_SIMULATOR_COMMANDS, simulator_commands)
527 config.set(section, App.OPT_RUN_DIRECTORY, actions['run_from'])
529 def _read_config(self):
530 self.config.read(self.config_file)
531 self._actions = self._actions_from_config(self.config)
533 def _write_config(self):
534 self._actions_to_config(self._actions, self.config)
535 with open(self.config_file, 'w') as f:
536 self.config.write(f)
539 def usr1_handler(signum, frame):
540 app.update_from_usr1()
542 if __name__ == '__main__':
543 app = App()
544 main_loop = gobject.MainLoop()
545 signal.signal(signal.SIGUSR1, usr1_handler)
546 main_loop.run()