ui: rework unit window
[oscopy/ivan.git] / oscopy_ui
blob513b91e3e9e8038564fb89e40771aca323b2d87a
1 #!/usr/bin/python
3 import gobject
4 import gtk
5 import signal
6 import os
7 import readline
8 import commands
9 import ConfigParser
10 from xdg import BaseDirectory
12 import oscopy
14 from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
15 from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
16 import gui
18 def report_error(parent, msg):
19 dlg = gtk.MessageDialog(parent,
20 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
21 gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, msg)
22 dlg.set_title(parent.get_title())
23 dlg.run()
24 dlg.destroy()
26 class OscopyAppUI(oscopy.OscopyApp):
27 def __init__(self, context):
28 oscopy.OscopyApp.__init__(self, context)
29 self._callbacks = {}
30 self._autorefresh = True
32 def connect(self, event, func, data):
33 if not isinstance(event, str):
34 return
35 if hasattr(self, 'do_'+event):
36 self._callbacks[event] = {func: data}
38 def postcmd(self, stop, line):
39 oscopy.OscopyApp.postcmd(self, stop, line)
40 if not line:
41 return
42 if len(line.split()) > 1:
43 event = line.split()[0].strip()
44 args = line.split(' ', 1)[1].strip()
45 else:
46 event = ''
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="Show terminal"/>
97 <menuitem action="Quit"/>
98 </menu>
99 </menubar>
100 </ui>'''
102 def __init__(self):
103 self._scale_to_str = {'lin': 'Linear', 'logx': 'LogX', 'logy': 'LogY',\
104 'loglog': 'Loglog'}
105 self._configfile = "gui"
106 self._figcount = 0
107 self._windows_to_figures = {}
108 self._current_graph = None
109 self._current_figure = None
110 self._term_win = None
111 self._prompt = "oscopy-ui>"
112 # History file
113 self.hist_file = None
115 self._TARGET_TYPE_SIGNAL = 10354
116 self._from_signal_list = [("oscopy-signals", gtk.TARGET_SAME_APP,\
117 self._TARGET_TYPE_SIGNAL)]
118 self._to_figure = [("oscopy-signals", gtk.TARGET_SAME_APP,\
119 self._TARGET_TYPE_SIGNAL)]
120 self._resource = "oscopy"
121 self._read_config()
123 self._ctxt = oscopy.Context()
124 self._app = OscopyAppUI(self._ctxt)
125 self._app.connect('read', self._add_file, None)
126 self._app.connect('math', self._add_file, None)
127 self._app.connect('freeze', self._freeze, None)
128 self._app.connect('unfreeze', self._freeze, None)
129 self._app.connect('create', self._create, None)
130 self._store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
131 gobject.TYPE_BOOLEAN)
132 self._create_widgets()
133 #self._app_exec('read demo/irf540.dat')
134 #self._app_exec('read demo/ac.dat')
135 #self._add_file('demo/res.dat')
137 def _add_file(self, event, filename, data=None):
138 it = self._store.append(None, (filename.strip(), None, False))
139 for name, sig in self._ctxt.readers[filename.strip()]\
140 .signals.iteritems():
141 self._store.append(it, (name, sig, sig.freeze))
143 def _action_add_file(self, action):
144 dlg = gtk.FileChooserDialog('Add file', parent=self._mainwindow,
145 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
146 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
147 resp = dlg.run()
148 filename = dlg.get_filename()
149 dlg.destroy()
150 if resp == gtk.RESPONSE_ACCEPT:
151 self._app_exec("read " + filename)
153 def _app_exec(self, line):
154 line = self._app.precmd(line)
155 stop = self._app.onecmd(line)
156 self._app.postcmd(stop, line)
158 def _action_update(self, action):
159 self._ctxt.update()
161 def _action_new_math(self, action):
162 dlg = gtk.Dialog('New math signal', parent=self._mainwindow,
163 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
164 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
166 # Label and entry
167 hbox = gtk.HBox()
168 label = gtk.Label('Expression:')
169 hbox.pack_start(label)
170 entry = gtk.Entry()
171 hbox.pack_start(entry)
172 dlg.vbox.pack_start(hbox)
174 dlg.show_all()
175 resp = dlg.run()
176 if resp == gtk.RESPONSE_ACCEPT:
177 expr = entry.get_text()
178 self._app_exec('%s' % expr)
180 dlg.destroy()
182 def _action_show_terminal(self, action):
183 self._create_terminal_window()
185 def _action_execute_script(self, action):
186 dlg = gtk.FileChooserDialog('Execute script', parent=self._mainwindow,
187 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
188 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
189 resp = dlg.run()
190 filename = dlg.get_filename()
191 dlg.destroy()
192 if resp == gtk.RESPONSE_ACCEPT:
193 self._app_exec("exec " + filename)
195 def _action_quit(self, action):
196 self._write_config()
197 readline.write_history_file(self.hist_file)
198 main_loop.quit()
200 def _create_menubar(self):
201 # tuple format:
202 # (name, stock-id, label, accelerator, tooltip, callback)
203 actions = [
204 ('File', None, '_File'),
205 ('Add file', gtk.STOCK_ADD, '_Add file', None, None,
206 self._action_add_file),
207 ('Update files', gtk.STOCK_REFRESH, '_Update', None, None,
208 self._action_update),
209 ('Execute script...', gtk.STOCK_MEDIA_PLAY, '_Execute script...',
210 None, None, self._action_execute_script),
211 ("New Math Signal...", gtk.STOCK_NEW, '_New Math Signal', None,
212 None, self._action_new_math),
213 ("Run netlister and simulate...", gtk.STOCK_MEDIA_FORWARD,\
214 "_Run netlister and simulate...", None, None,\
215 self._action_netlist_and_simulate),
216 ("Show terminal", None, "_Show terminal", None, None,
217 self._action_show_terminal),
218 ('Quit', gtk.STOCK_QUIT, '_Quit', None, None,
219 self._action_quit),
221 actiongroup = gtk.ActionGroup('App')
222 actiongroup.add_actions(actions)
224 uimanager = gtk.UIManager()
225 uimanager.add_ui_from_string(self.__ui)
226 uimanager.insert_action_group(actiongroup, 0)
227 return uimanager.get_accel_group(), uimanager.get_widget('/MenuBar')
229 def _create_treeview(self):
230 celltext = gtk.CellRendererText()
231 col = gtk.TreeViewColumn('Signal', celltext, text=0)
232 tv = gtk.TreeView()
233 col.set_cell_data_func(celltext, self._reader_name_in_bold)
234 tv.append_column(col)
235 tv.set_model(self._store)
236 tv.connect('row-activated', self._row_activated)
237 tv.connect('drag_data_get', self._drag_data_get_cb)
238 tv.drag_source_set(gtk.gdk.BUTTON1_MASK,\
239 self._from_signal_list,\
240 gtk.gdk.ACTION_COPY)
241 self._togglecell = gtk.CellRendererToggle()
242 self._togglecell.set_property('activatable', True)
243 self._togglecell.connect('toggled', self._cell_toggled, None)
244 colfreeze = gtk.TreeViewColumn('Freeze', self._togglecell)
245 colfreeze.add_attribute(self._togglecell, 'active', 2)
246 tv.append_column(colfreeze)
247 tv.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
248 return tv
250 def _reader_name_in_bold(self, column, cell, model, iter, data=None):
251 if len(model.get_path(iter)) == 1:
252 cell.set_property('markup', "<b>" + model.get_value(iter, 0) +\
253 "</b>")
254 else:
255 cell.set_property('text', model.get_value(iter, 0))
257 # Search algorithm from pygtk tutorial
258 def _match_func(self, row, data):
259 column, key = data
260 return row[column] == key
262 def _search(self, rows, func, data):
263 if not rows: return None
264 for row in rows:
265 if func(row, data):
266 return row
267 result = self._search(row.iterchildren(), func, data)
268 if result: return result
269 return None
271 def _freeze(self, event, signals, data=None):
272 for signal in signals.split(','):
273 match_row = self._search(self._store, self._match_func,\
274 (0, signal.strip()))
275 if match_row is not None:
276 match_row[2] = match_row[1].freeze
277 parent = self._store.iter_parent(match_row.iter)
278 iter = self._store.iter_children(parent)
279 freeze = match_row[2]
280 while iter:
281 if not self._store.get_value(iter, 2) == freeze:
282 break
283 iter = self._store.iter_next(iter)
284 if iter == None:
285 # All row at the same freeze value,
286 # set freeze for the reader
287 self._store.set_value(parent, 2, freeze)
288 else:
289 # Set reader freeze to false
290 self._store.set_value(parent, 2, False)
292 def _cell_toggled(self, cellrenderer, path, data):
293 if len(path) == 3:
294 # Single signal
295 if self._store[path][1].freeze:
296 cmd = 'unfreeze'
297 else:
298 cmd = 'freeze'
299 self._app_exec('%s %s' % (cmd, self._store[path][0]))
300 elif len(path) == 1:
301 # Whole reader
302 parent = self._store.get_iter(path)
303 freeze = not self._store.get_value(parent, 2)
304 if self._store[path][2]:
305 cmd = 'unfreeze'
306 else:
307 cmd = 'freeze'
308 self._store.set_value(parent, 2, freeze)
309 iter = self._store.iter_children(parent)
310 while iter:
311 self._app_exec('%s %s' % (cmd, self._store.get_value(iter, 0)))
312 iter = self._store.iter_next(iter)
314 def _create_widgets(self):
315 accel_group, self._menubar = self._create_menubar()
316 self._treeview = self._create_treeview()
317 self._create_terminal_window()
319 sw = gtk.ScrolledWindow()
320 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
321 sw.add(self._treeview)
323 vbox = gtk.VBox()
324 vbox.pack_start(self._menubar, False)
325 vbox.pack_start(sw)
327 w = self._mainwindow = gtk.Window(gtk.WINDOW_TOPLEVEL)
328 w.set_title('Oscopy GUI')
329 w.add(vbox)
330 w.add_accel_group(accel_group)
331 w.connect('destroy', lambda *x: self._action_quit(None))
332 w.set_default_size(400, 300)
333 w.show_all()
335 def _create_terminal_window(self):
336 if self._term_win is None:
337 self._term_win = gui.dialogs.TerminalWindow(self._prompt,
338 self._app.intro,
339 self.hist_file,
340 self._app_exec)
341 if not self._term_win.is_there:
342 self._term_win.create()
343 pass
345 def _create_figure_popup_menu(self, figure, graph):
346 figmenu = gui.menus.FigureMenu()
347 return figmenu.create_menu(self._store, figure, graph, self._app_exec)
349 def _treeview_button_press(self, widget, event):
350 if event.button == 3:
351 tv = widget
352 path, tvc, x, y = tv.get_path_at_pos(int(event.x), int(event.y))
353 if len(path) == 1:
354 return
355 tv.set_cursor(path)
356 row = self._store[path]
357 signals = {row[0]: row[1]}
358 menu = self._create_treeview_popup_menu(signals, path)
359 menu.show_all()
360 menu.popup(None, None, None, event.button, event.time)
362 def _button_press(self, event):
363 if event.button == 3:
364 menu = self._create_figure_popup_menu(event.canvas.figure, event.inaxes)
365 menu.show_all()
366 menu.popup(None, None, None, event.button, event.guiEvent.time)
368 #TODO: _windows_to_figures consistency...
369 # think of a better way to map events to Figure objects
370 def _row_activated(self, widget, path, col):
371 if len(path) == 1:
372 return
374 row = self._store[path]
375 self._app_exec('create %s' % row[0])
377 def _create(self, event, signals, data=None):
378 fig = self._ctxt.figures[len(self._ctxt.figures) - 1]
380 w = gtk.Window()
381 self._figcount += 1
382 self._windows_to_figures[w] = fig
383 w.set_title('Figure %d' % self._figcount)
384 vbox = gtk.VBox()
385 w.add(vbox)
386 canvas = FigureCanvas(fig)
387 canvas.mpl_connect('button_press_event', self._button_press)
388 canvas.mpl_connect('axes_enter_event', self._axes_enter)
389 canvas.mpl_connect('axes_leave_event', self._axes_leave)
390 canvas.mpl_connect('figure_enter_event', self._figure_enter)
391 canvas.mpl_connect('figure_leave_event', self._figure_leave)
392 w.connect("drag_data_received", self._drag_data_received_cb)
393 w.drag_dest_set(gtk.DEST_DEFAULT_MOTION |\
394 gtk.DEST_DEFAULT_HIGHLIGHT |\
395 gtk.DEST_DEFAULT_DROP,
396 self._to_figure, gtk.gdk.ACTION_COPY)
397 vbox.pack_start(canvas)
398 toolbar = NavigationToolbar(canvas, w)
399 vbox.pack_start(toolbar, False, False)
400 w.resize(400, 300)
401 w.show_all()
402 self._app_exec('select %d-1' % len(self._ctxt.figures))
404 def _axes_enter(self, event):
405 self._current_graph = event.inaxes
406 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
407 fig_num = self._ctxt.figures.index(self._current_figure) + 1
408 self._app_exec('select %d-%d' % (fig_num, axes_num))
410 def _axes_leave(self, event):
411 # Unused for better user interaction
412 # self._current_graph = None
413 pass
415 def _figure_enter(self, event):
416 self._current_figure = event.canvas.figure
417 if hasattr(event, 'inaxes') and event.inaxes is not None:
418 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
419 else:
420 axes_num = 1
421 fig_num = self._ctxt.figures.index(self._current_figure) + 1
422 self._app_exec('select %d-%d' % (fig_num, axes_num))
424 def _figure_leave(self, event):
425 # self._current_figure = None
426 pass
428 def update_from_usr1(self):
429 self._ctxt.update()
431 def _action_netlist_and_simulate(self, action):
432 netnsimdlg = gui.dialogs.Run_Netlister_and_Simulate_Dialog()
433 netnsimdlg.display(self._actions)
434 actions = netnsimdlg.run()
435 if actions is not None:
436 self._actions = actions
437 old_dir = os.getcwd()
438 os.chdir(self._actions['run_from'])
439 if self._actions['run_netlister'][0]:
440 res = commands.getstatusoutput(self._actions['run_netlister'][1])
441 if res[0]:
442 report_error(self._mainwindow, res[1])
443 print res[1]
444 if self._actions['run_simulator'][0]:
445 res = commands.getstatusoutput(self._actions['run_simulator'][1])
446 if res[0]:
447 report_error(self._mainwindow, res[1])
448 print res[1]
449 os.chdir(old_dir)
450 if self._actions['update']:
451 self._ctxt.update()
453 def _drag_data_get_cb(self, widget, drag_context, selection, target_type,\
454 time):
455 if target_type == self._TARGET_TYPE_SIGNAL:
456 tv = widget
457 sel = tv.get_selection()
458 (model, pathlist) = sel.get_selected_rows()
459 iter = self._store.get_iter(pathlist[0])
460 data = " ".join(map(lambda x:self._store[x][1].name, pathlist))
461 selection.set(selection.target, 8, data)
462 # The multiple selection do work, but how to select signals
463 # that are not neighbours in the list? Ctrl+left do not do
464 # anything, neither alt+left or shift+left!
466 def _drag_data_received_cb(self, widget, drag_context, x, y, selection,\
467 target_type, time):
468 if target_type == self._TARGET_TYPE_SIGNAL:
469 if self._current_graph is not None:
470 signals = {}
471 for name in selection.data.split():
472 signals[name] = self._ctxt.signals[name]
473 self._current_graph.insert(signals)
474 if self._current_figure.canvas is not None:
475 self._current_figure.canvas.draw()
477 def _read_config(self):
478 cmdsec = 'Commands'
479 netlopt = 'Netlister'
480 simopt = 'Simulator'
481 runfrom = "RunFrom"
482 history = 'history'
483 self._confparse = ConfigParser.SafeConfigParser()
484 self._confparse.add_section(cmdsec)
485 self._confparse.set(cmdsec, netlopt, '')
486 self._confparse.set(cmdsec, simopt, '')
487 self._confparse.set(cmdsec, runfrom, '.')
488 path = BaseDirectory.load_first_config(self._resource)
489 if path is not None:
490 res = self._confparse.read('/'.join((path, self._configfile)))
491 else:
492 path = BaseDirectory.save_config_path(self._resource)
493 self.hist_file = '/'.join((path, history))
494 self._actions = {'run_netlister': (True,
495 self._confparse.get(cmdsec, netlopt)),
496 'run_simulator': (True,
497 self._confparse.get(cmdsec, simopt)),
498 'update': True,
499 'run_from': self._confparse.get(cmdsec, runfrom)}
501 def _write_config(self):
502 cmdsec = 'Commands'
503 netlopt = 'Netlister'
504 simopt = 'Simulator'
505 runfrom = "RunFrom"
506 self._confparse.set(cmdsec, netlopt, self._actions['run_netlister'][1])
507 self._confparse.set(cmdsec, simopt, self._actions['run_simulator'][1])
508 self._confparse.set(cmdsec, runfrom, self._actions['run_from'])
509 path = BaseDirectory.save_config_path(self._resource)
510 f = open('/'.join((path, self._configfile)), 'w')
511 res = self._confparse.write(f)
512 f.close()
515 def usr1_handler(signum, frame):
516 app.update_from_usr1()
518 if __name__ == '__main__':
519 app = App()
520 main_loop = gobject.MainLoop()
521 signal.signal(signal.SIGUSR1, usr1_handler)
522 main_loop.run()