FIXED: Support for drag and drop from main GUI window to Figure
[oscopy.git] / oscopy / ui.py
blob927fe85f34265af33bd60a0e8a66f3dd5033955f
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):
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 = get_ipython()
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)
418 def destroy(self, num):
419 if not num.isdigit() or int(num) > len(self._ctxt.figures):
420 return
421 else:
422 fignum = int(num)
423 action = self._uimanager.get_action('/MenuBar/Windows/Figure %d' %
424 fignum)
425 if action is not None:
426 self._actiongroup.remove_action(action)
427 self._uimanager.remove_ui(self._fignum_to_merge_id[fignum])
428 self._fignum_to_windows[fignum].destroy()
430 # Search algorithm from pygtk tutorial
431 def _match_func(self, row, data):
432 column, key = data
433 return row[column] == key
435 def _search(self, rows, func, data):
436 if not rows: return None
437 for row in rows:
438 if func(row, data):
439 return row
440 result = self._search(row.iterchildren(), func, data)
441 if result: return result
442 return None
444 def freeze(self, signals):
445 for signal in signals.split(','):
446 match_row = self._search(self._store, self._match_func,\
447 (0, signal.strip()))
448 if match_row is not None:
449 match_row[2] = match_row[1].freeze
450 parent = self._store.iter_parent(match_row.iter)
451 iter = self._store.iter_children(parent)
452 freeze = match_row[2]
453 while iter:
454 if not self._store.get_value(iter, 2) == freeze:
455 break
456 iter = self._store.iter_next(iter)
457 if iter == None:
458 # All row at the same freeze value,
459 # set freeze for the reader
460 self._store.set_value(parent, 2, freeze)
461 else:
462 # Set reader freeze to false
463 self._store.set_value(parent, 2, False)
465 def add_file(self, filename):
466 if filename.strip() in self._ctxt.readers:
467 it = self._store.append(None, (filename.strip(), None, False))
468 for name, sig in self._ctxt.readers[filename.strip()]\
469 .signals.iteritems():
470 self._store.append(it, (name, sig, sig.freeze))
473 # Callbacks for drag and drop
475 def _drag_data_received_main_cb(self, widget, drag_context, x, y, selection,
476 target_type, time):
477 name = selection.data
478 if type(name) == str and name.startswith('file://'):
479 print name[7:].strip()
480 self._app_exec('%%oread %s' % name[7:].strip())
482 def _drag_data_get_cb(self, widget, drag_context, selection, target_type,\
483 time):
484 if target_type == self._TARGET_TYPE_SIGNAL:
485 tv = widget
486 sel = tv.get_selection()
487 (model, pathlist) = sel.get_selected_rows()
488 iter = self._store.get_iter(pathlist[0])
489 # Use the path list stored while button 1 has been pressed
490 # See self._treeview_button_press()
491 data = ' '.join(map(lambda x:self._store[x][1].name, self._rows_for_drag))
492 selection.set(selection.target, 8, data)
493 return True
495 # Configuration-file related functions
497 def _init_config(self):
498 # initialize configuration stuff
499 path = BaseDirectory.save_config_path('oscopy')
500 self.config_file = os.path.join(path, 'gui')
501 self.hist_file = os.path.join(path, 'history')
502 section = App.SECTION
503 self.config = ConfigParser.RawConfigParser()
504 self.config.add_section(section)
505 # defaults
506 self.config.set(section, App.OPT_NETLISTER_COMMANDS, '')
507 self.config.set(section, App.OPT_SIMULATOR_COMMANDS, '')
508 self.config.set(section, App.OPT_RUN_DIRECTORY, '.')
510 def _sanitize_list(self, lst):
511 return filter(lambda x: len(x) > 0, map(lambda x: x.strip(), lst))
513 def _actions_from_config(self, config):
514 section = App.SECTION
515 netlister_commands = config.get(section, App.OPT_NETLISTER_COMMANDS)
516 netlister_commands = self._sanitize_list(netlister_commands.split(';'))
517 simulator_commands = config.get(section, App.OPT_SIMULATOR_COMMANDS)
518 simulator_commands = self._sanitize_list(simulator_commands.split(';'))
519 actions = {
520 'run_netlister': (True, netlister_commands),
521 'run_simulator': (True, simulator_commands),
522 'update': True,
523 'run_from': config.get(section, App.OPT_RUN_DIRECTORY)}
524 return actions
526 def _actions_to_config(self, actions, config):
527 section = App.SECTION
528 netlister_commands = ';'.join(actions['run_netlister'][1])
529 simulator_commands = ';'.join(actions['run_simulator'][1])
530 config.set(section, App.OPT_NETLISTER_COMMANDS, netlister_commands)
531 config.set(section, App.OPT_SIMULATOR_COMMANDS, simulator_commands)
532 config.set(section, App.OPT_RUN_DIRECTORY, actions['run_from'])
534 def _read_config(self):
535 self.config.read(self.config_file)
536 self._actions = self._actions_from_config(self.config)
538 def _write_config(self):
539 self._actions_to_config(self._actions, self.config)
540 with open(self.config_file, 'w') as f:
541 self.config.write(f)
543 # DBus routines
544 @dbus.service.method('org.freedesktop.OscopyIFace')
545 def dbus_update(self):
546 gobject.idle_add(self._activate_net_and_sim)
548 @dbus.service.method('org.freedesktop.OscopyIFace')
549 def dbus_running(self):
550 return
552 # Misc functions
553 def update_from_usr1(self):
554 self._ctxt.update()
556 def update_from_usr2(self):
557 gobject.idle_add(self._activate_net_and_sim)
559 def _activate_net_and_sim(self):
560 if self._actiongroup is not None:
561 action = self._actiongroup.get_action("Run netlister and simulate...")
562 action.activate()
564 def _run_ext_command(self, cmd, run_dir):
565 old_dir = os.getcwd()
566 os.chdir(run_dir)
567 try:
568 status, output = commands.getstatusoutput(cmd)
569 if status:
570 msg = _("Executing command '%s' failed.") % cmd
571 report_error(self._mainwindow, msg)
572 return status == 0
573 finally:
574 os.chdir(old_dir)
576 def _app_exec(self, line):
577 first = line.split()[0]
578 if first.startswith('%') or first.split()[0] in self.shell.lsmagic():
579 self.shell.magic(line)
580 else:
581 self.shell.ex(line)
583 def usr1_handler(signum, frame):
584 app.update_from_usr1()
586 def usr2_handler(signum, frame):
587 app.update_from_usr2()