FIXED: support for drag and drop
[oscopy.git] / oscopy / ui.py
bloba3c92f0ad53edeff1ab22b28006a678d5b5b52ad
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 xdg import BaseDirectory
14 from matplotlib.backend_bases import LocationEvent
15 import IPython
17 import oscopy
19 from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
20 from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
21 import gui
23 # Note: for crosshair, see gtk.gdk.GC / function = gtk.gdk.XOR
25 def report_error(parent, msg):
26 dlg = gtk.MessageDialog(parent,
27 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
28 gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, msg)
29 dlg.set_title(parent.get_title())
30 dlg.run()
31 dlg.destroy()
33 class App(dbus.service.Object):
34 __ui = '''<ui>
35 <menubar name="MenuBar">
36 <menu action="File">
37 <menuitem action="Add file(s)..."/>
38 <menuitem action="Update files"/>
39 <menuitem action="Execute script..."/>
40 <menuitem action="New Math Signal..."/>
41 <menuitem action="Run netlister and simulate..."/>
42 <menuitem action="Quit"/>
43 </menu>
44 <menu action="Windows">
45 </menu>
46 </menubar>
47 </ui>'''
49 def __init__(self, bus_name, object_path='/org/freedesktop/Oscopy', ctxt=None):
50 if bus_name is not None:
51 dbus.service.Object.__init__(self, bus_name, object_path)
52 self._scale_to_str = {'lin': _('Linear'), 'logx': _('LogX'), 'logy': _('LogY'),\
53 'loglog': _('Loglog')}
54 self._windows_to_figures = {}
55 self._fignum_to_windows = {}
56 self._fignum_to_merge_id = {}
57 self._current_graph = None
58 self._current_figure = None
59 self._prompt = "oscopy-ui>"
60 self._init_config()
61 self._read_config()
63 self._TARGET_TYPE_SIGNAL = 10354
64 self._from_signal_list = [("oscopy-signals", gtk.TARGET_SAME_APP,\
65 self._TARGET_TYPE_SIGNAL)]
66 self._to_figure = [("oscopy-signals", gtk.TARGET_SAME_APP,\
67 self._TARGET_TYPE_SIGNAL)]
69 if ctxt is None:
70 self._ctxt = oscopy.Context()
71 else:
72 self._ctxt = ctxt
74 self._store = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
75 gobject.TYPE_BOOLEAN)
76 self._create_widgets()
77 #self._app_exec('read demo/irf540.dat')
78 #self._app_exec('read demo/ac.dat')
79 #self._add_file('demo/res.dat')
81 # From IPython/demo.py
82 self.shell = __IPYTHON__
84 SECTION = 'oscopy_ui'
85 OPT_NETLISTER_COMMANDS = 'netlister_commands'
86 OPT_SIMULATOR_COMMANDS = 'simulator_commands'
87 OPT_RUN_DIRECTORY = 'run_directory'
90 # Actions
92 def _action_add_file(self, action):
93 dlg = gtk.FileChooserDialog(_('Add file(s)'), parent=self._mainwindow,
94 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
95 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
96 dlg.set_select_multiple(True)
97 resp = dlg.run()
98 if resp == gtk.RESPONSE_ACCEPT:
99 for filename in dlg.get_filenames():
100 self._app_exec('oread ' + filename)
101 dlg.destroy()
103 def _action_update(self, action):
104 self._ctxt.update()
106 def _action_new_math(self, action):
107 dlg = gtk.Dialog(_('New math signal'), parent=self._mainwindow,
108 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
109 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
111 # Label and entry
112 hbox = gtk.HBox()
113 label = gtk.Label(_('Expression:'))
114 hbox.pack_start(label)
115 entry = gtk.Entry()
116 hbox.pack_start(entry)
117 dlg.vbox.pack_start(hbox)
119 dlg.show_all()
120 resp = dlg.run()
121 if resp == gtk.RESPONSE_ACCEPT:
122 expr = entry.get_text()
123 self._app_exec('%s' % expr)
124 self._app_exec('oimport %s' % expr.split('=')[0].strip())
125 dlg.destroy()
127 def _action_execute_script(self, action):
128 dlg = gtk.FileChooserDialog(_('Execute script'), parent=self._mainwindow,
129 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
130 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
131 resp = dlg.run()
132 filename = dlg.get_filename()
133 dlg.destroy()
134 if resp == gtk.RESPONSE_ACCEPT:
135 self._app_exec('oexec ' + filename)
137 def _action_netlist_and_simulate(self, action):
138 dlg = gui.dialogs.Run_Netlister_and_Simulate_Dialog()
139 dlg.display(self._actions)
140 actions = dlg.run()
141 if actions is None:
142 return
143 self._actions = actions
144 run_dir = actions['run_from']
145 if actions['run_netlister'][0]:
146 if not self._run_ext_command(actions['run_netlister'][1][0], run_dir):
147 return
148 if actions['run_simulator'][0]:
149 if not self._run_ext_command(actions['run_simulator'][1][0], run_dir):
150 return
151 if actions['update']:
152 self._ctxt.update()
154 def _action_quit(self, action):
155 self._write_config()
156 readline.write_history_file(self.hist_file)
157 gtk.main_quit()
158 sys.exit()
160 def _action_figure(self, action, w, fignum):
161 if not (w.flags() & gtk.VISIBLE):
162 w.show()
163 else:
164 w.window.show()
165 self._app_exec('%%oselect %d-1' % fignum)
168 # UI Creation functions
170 def _create_menubar(self):
171 # tuple format:
172 # (name, stock-id, label, accelerator, tooltip, callback)
173 actions = [
174 ('File', None, _('_File')),
175 ('Add file(s)...', gtk.STOCK_ADD, _('_Add file(s)...'), None, None,
176 self._action_add_file),
177 ('Update files', gtk.STOCK_REFRESH, _('_Update'), None, None,
178 self._action_update),
179 ('Execute script...', gtk.STOCK_MEDIA_PLAY, _('_Execute script...'),
180 None, None, self._action_execute_script),
181 ("New Math Signal...", gtk.STOCK_NEW, _('_New Math Signal'), None,
182 None, self._action_new_math),
183 ("Run netlister and simulate...", gtk.STOCK_MEDIA_FORWARD,\
184 _("_Run netlister and simulate..."), None, None,\
185 self._action_netlist_and_simulate),
186 ('Windows', None, _('_Windows')),
187 ('Quit', gtk.STOCK_QUIT, _('_Quit'), None, None,
188 self._action_quit),
191 actiongroup = self._actiongroup = gtk.ActionGroup('App')
192 actiongroup.add_actions(actions)
194 uimanager = self._uimanager = gtk.UIManager()
195 uimanager.add_ui_from_string(self.__ui)
196 uimanager.insert_action_group(actiongroup, 0)
197 return uimanager.get_accel_group(), uimanager.get_widget('/MenuBar')
199 def _create_treeview(self):
200 celltext = gtk.CellRendererText()
201 col = gtk.TreeViewColumn(_('Signal'), celltext, text=0)
202 tv = gtk.TreeView()
203 col.set_cell_data_func(celltext, self._reader_name_in_bold)
204 col.set_expand(True)
205 tv.append_column(col)
206 tv.set_model(self._store)
207 tv.connect('row-activated', self._row_activated)
208 tv.connect('drag_data_get', self._drag_data_get_cb)
209 tv.drag_source_set(gtk.gdk.BUTTON1_MASK,\
210 self._from_signal_list,\
211 gtk.gdk.ACTION_COPY)
212 self._togglecell = gtk.CellRendererToggle()
213 self._togglecell.set_property('activatable', True)
214 self._togglecell.connect('toggled', self._cell_toggled, None)
215 colfreeze = gtk.TreeViewColumn(_('Freeze'), self._togglecell)
216 colfreeze.add_attribute(self._togglecell, 'active', 2)
217 tv.append_column(colfreeze)
218 tv.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
219 return tv
221 def _reader_name_in_bold(self, column, cell, model, iter, data=None):
222 if len(model.get_path(iter)) == 1:
223 cell.set_property('markup', "<b>" + model.get_value(iter, 0) +\
224 "</b>")
225 else:
226 cell.set_property('text', model.get_value(iter, 0))
228 def _create_widgets(self):
229 accel_group, self._menubar = self._create_menubar()
230 self._treeview = self._create_treeview()
232 sw = gtk.ScrolledWindow()
233 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
234 sw.add(self._treeview)
236 vbox = gtk.VBox()
237 vbox.pack_start(self._menubar, False)
238 vbox.pack_start(sw)
240 w = self._mainwindow = gtk.Window(gtk.WINDOW_TOPLEVEL)
241 w.set_title(_('Oscopy GUI'))
242 w.add(vbox)
243 w.add_accel_group(accel_group)
244 w.connect('destroy', lambda w, e: w.hide() or True)
245 w.connect('delete-event', lambda w, e: w.hide() or True)
246 w.set_default_size(400, 300)
247 w.show_all()
249 def _create_figure_popup_menu(self, figure, graph):
250 figmenu = gui.menus.FigureMenu()
251 return figmenu.create_menu(self._store, figure, graph, self._app_exec)
253 def show_all(self):
254 self._mainwindow.show()
257 # Event-triggered functions
259 def _treeview_button_press(self, widget, event):
260 if event.button == 3:
261 tv = widget
262 path, tvc, x, y = tv.get_path_at_pos(int(event.x), int(event.y))
263 if len(path) == 1:
264 return
265 tv.set_cursor(path)
266 row = self._store[path]
267 signals = {row[0]: row[1]}
268 menu = self._create_treeview_popup_menu(signals, path)
269 menu.show_all()
270 menu.popup(None, None, None, event.button, event.time)
272 def _button_press(self, event):
273 if event.button == 3:
274 menu = self._create_figure_popup_menu(event.canvas.figure, event.inaxes)
275 menu.show_all()
276 menu.popup(None, None, None, event.button, event.guiEvent.time)
278 #TODO: _windows_to_figures consistency...
279 # think of a better way to map events to Figure objects
280 def _row_activated(self, widget, path, col):
281 if len(path) == 1:
282 return
284 row = self._store[path]
285 self._app_exec('ocreate %s' % row[0])
287 def _axes_enter(self, event):
288 self._figure_enter(event)
289 self._current_graph = event.inaxes
291 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
292 fig_num = self._ctxt.figures.index(self._current_figure) + 1
293 self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
295 def _axes_leave(self, event):
296 # Unused for better user interaction
297 # self._current_graph = None
298 pass
300 def _figure_enter(self, event):
301 self._current_figure = event.canvas.figure
302 if hasattr(event, 'inaxes') and event.inaxes is not None:
303 axes_num = event.canvas.figure.axes.index(event.inaxes) + 1
304 else:
305 axes_num = 1
306 fig_num = self._ctxt.figures.index(self._current_figure) + 1
307 self._app_exec('%%oselect %d-%d' % (fig_num, axes_num))
309 def _figure_leave(self, event):
310 # self._current_figure = None
311 pass
313 def _cell_toggled(self, cellrenderer, path, data):
314 if len(path) == 3:
315 # Single signal
316 if self._store[path][1].freeze:
317 cmd = 'ounfreeze'
318 else:
319 cmd = 'ofreeze'
320 self._app_exec('%s %s' % (cmd, self._store[path][0]))
321 elif len(path) == 1:
322 # Whole reader
323 parent = self._store.get_iter(path)
324 freeze = not self._store.get_value(parent, 2)
325 if self._store[path][2]:
326 cmd = 'ounfreeze'
327 else:
328 cmd = 'ofreeze'
329 self._store.set_value(parent, 2, freeze)
330 iter = self._store.iter_children(parent)
331 while iter:
332 self._app_exec('%s %s' % (cmd, self._store.get_value(iter, 0)))
333 iter = self._store.iter_next(iter)
336 # Callbacks for App
338 def create(self):
339 fig = self._ctxt.figures[len(self._ctxt.figures) - 1]
340 fignum = len(self._ctxt.figures)
342 w = gtk.Window()
343 self._windows_to_figures[w] = fig
344 self._fignum_to_windows[fignum] = w
345 w.set_title(_('Figure %d') % fignum)
346 vbox = gtk.VBox()
347 w.add(vbox)
348 canvas = FigureCanvas(fig)
349 canvas.mpl_connect('button_press_event', self._button_press)
350 canvas.mpl_connect('axes_enter_event', self._axes_enter)
351 canvas.mpl_connect('axes_leave_event', self._axes_leave)
352 canvas.mpl_connect('figure_enter_event', self._figure_enter)
353 canvas.mpl_connect('figure_leave_event', self._figure_leave)
354 w.connect("drag_data_received", self._drag_data_received_cb)
355 w.connect('delete-event', lambda w, e: w.hide() or True)
356 w.drag_dest_set(gtk.DEST_DEFAULT_MOTION |\
357 gtk.DEST_DEFAULT_HIGHLIGHT |\
358 gtk.DEST_DEFAULT_DROP,
359 self._to_figure, gtk.gdk.ACTION_COPY)
360 vbox.pack_start(canvas)
361 toolbar = NavigationToolbar(canvas, w)
362 vbox.pack_start(toolbar, False, False)
363 w.resize(400, 300)
364 w.show_all()
366 # Add it to the 'Windows' menu
367 actions = [(_('Figure %d') % fignum, None, _('Figure %d') % fignum,
368 None, None, self._action_figure)]
369 self._actiongroup.add_actions(actions, (w, fignum))
370 ui = "<ui>\
371 <menubar name=\"MenuBar\">\
372 <menu action=\"Windows\">\
373 <menuitem action=\"Figure %d\"/>\
374 </menu>\
375 </menubar>\
376 </ui>" % fignum
377 merge_id = self._uimanager.add_ui_from_string(ui)
378 self._fignum_to_merge_id[fignum] = merge_id
379 self._app_exec('%%oselect %d-1' % fignum)
381 def destroy(self, num):
382 if not num.isdigit() or int(num) > len(self._ctxt.figures):
383 return
384 else:
385 fignum = int(num)
386 action = self._uimanager.get_action('/MenuBar/Windows/Figure %d' %
387 fignum)
388 if action is not None:
389 self._actiongroup.remove_action(action)
390 self._uimanager.remove_ui(self._fignum_to_merge_id[fignum])
391 self._fignum_to_windows[fignum].destroy()
393 # Search algorithm from pygtk tutorial
394 def _match_func(self, row, data):
395 column, key = data
396 return row[column] == key
398 def _search(self, rows, func, data):
399 if not rows: return None
400 for row in rows:
401 if func(row, data):
402 return row
403 result = self._search(row.iterchildren(), func, data)
404 if result: return result
405 return None
407 def freeze(self, signals):
408 for signal in signals.split(','):
409 match_row = self._search(self._store, self._match_func,\
410 (0, signal.strip()))
411 if match_row is not None:
412 match_row[2] = match_row[1].freeze
413 parent = self._store.iter_parent(match_row.iter)
414 iter = self._store.iter_children(parent)
415 freeze = match_row[2]
416 while iter:
417 if not self._store.get_value(iter, 2) == freeze:
418 break
419 iter = self._store.iter_next(iter)
420 if iter == None:
421 # All row at the same freeze value,
422 # set freeze for the reader
423 self._store.set_value(parent, 2, freeze)
424 else:
425 # Set reader freeze to false
426 self._store.set_value(parent, 2, False)
428 def add_file(self, filename):
429 if filename.strip() in self._ctxt.readers:
430 it = self._store.append(None, (filename.strip(), None, False))
431 for name, sig in self._ctxt.readers[filename.strip()]\
432 .signals.iteritems():
433 self._store.append(it, (name, sig, sig.freeze))
436 # Callbacks for drag and drop
438 def _drag_data_get_cb(self, widget, drag_context, selection, target_type,\
439 time):
440 if target_type == self._TARGET_TYPE_SIGNAL:
441 tv = widget
442 sel = tv.get_selection()
443 (model, pathlist) = sel.get_selected_rows()
444 iter = self._store.get_iter(pathlist[0])
445 data = " ".join(map(lambda x:self._store[x][1].name, pathlist))
446 selection.set(selection.target, 8, data)
447 # The multiple selection do work, but how to select signals
448 # that are not neighbours in the list? Ctrl+left do not do
449 # anything, neither alt+left or shift+left!
451 def _drag_data_received_cb(self, widget, drag_context, x, y, selection,\
452 target_type, time):
453 # Event handling issue: this drag and drop callback is
454 # processed before matplotlib callback _axes_enter. Therefore
455 # when dropping, self._current_graph is not valid: it contains
456 # the last graph.
457 # The workaround is to retrieve the Graph by creating a Matplotlib
458 # LocationEvent considering inverse 'y' coordinates
459 if target_type == self._TARGET_TYPE_SIGNAL:
460 canvas = self._windows_to_figures[widget].canvas
461 my_y = canvas.allocation.height - y
462 event = LocationEvent('axes_enter_event', canvas, x, my_y)
463 signals = {}
464 for name in selection.data.split():
465 signals[name] = self._ctxt.signals[name]
466 if event.inaxes is not None:
467 # Graph not found
468 event.inaxes.insert(signals)
469 self._windows_to_figures[widget].canvas.draw()
472 # Configuration-file related functions
474 def _init_config(self):
475 # initialize configuration stuff
476 path = BaseDirectory.save_config_path('oscopy')
477 self.config_file = os.path.join(path, 'gui')
478 self.hist_file = os.path.join(path, 'history')
479 section = App.SECTION
480 self.config = ConfigParser.RawConfigParser()
481 self.config.add_section(section)
482 # defaults
483 self.config.set(section, App.OPT_NETLISTER_COMMANDS, '')
484 self.config.set(section, App.OPT_SIMULATOR_COMMANDS, '')
485 self.config.set(section, App.OPT_RUN_DIRECTORY, '.')
487 def _sanitize_list(self, lst):
488 return filter(lambda x: len(x) > 0, map(lambda x: x.strip(), lst))
490 def _actions_from_config(self, config):
491 section = App.SECTION
492 netlister_commands = config.get(section, App.OPT_NETLISTER_COMMANDS)
493 netlister_commands = self._sanitize_list(netlister_commands.split(';'))
494 simulator_commands = config.get(section, App.OPT_SIMULATOR_COMMANDS)
495 simulator_commands = self._sanitize_list(simulator_commands.split(';'))
496 actions = {
497 'run_netlister': (True, netlister_commands),
498 'run_simulator': (True, simulator_commands),
499 'update': True,
500 'run_from': config.get(section, App.OPT_RUN_DIRECTORY)}
501 return actions
503 def _actions_to_config(self, actions, config):
504 section = App.SECTION
505 netlister_commands = ';'.join(actions['run_netlister'][1])
506 simulator_commands = ';'.join(actions['run_simulator'][1])
507 config.set(section, App.OPT_NETLISTER_COMMANDS, netlister_commands)
508 config.set(section, App.OPT_SIMULATOR_COMMANDS, simulator_commands)
509 config.set(section, App.OPT_RUN_DIRECTORY, actions['run_from'])
511 def _read_config(self):
512 self.config.read(self.config_file)
513 self._actions = self._actions_from_config(self.config)
515 def _write_config(self):
516 self._actions_to_config(self._actions, self.config)
517 with open(self.config_file, 'w') as f:
518 self.config.write(f)
520 # DBus routines
521 @dbus.service.method('org.freedesktop.OscopyIFace')
522 def dbus_update(self):
523 gobject.idle_add(self._activate_net_and_sim)
525 @dbus.service.method('org.freedesktop.OscopyIFace')
526 def dbus_running(self):
527 return
529 # Misc functions
530 def update_from_usr1(self):
531 self._ctxt.update()
533 def update_from_usr2(self):
534 gobject.idle_add(self._activate_net_and_sim)
536 def _activate_net_and_sim(self):
537 if self._actiongroup is not None:
538 action = self._actiongroup.get_action("Run netlister and simulate...")
539 action.activate()
541 def _run_ext_command(self, cmd, run_dir):
542 old_dir = os.getcwd()
543 os.chdir(run_dir)
544 try:
545 status, output = commands.getstatusoutput(cmd)
546 if status:
547 msg = _("Executing command '%s' failed.") % cmd
548 report_error(self._mainwindow, msg)
549 return status == 0
550 finally:
551 os.chdir(old_dir)
553 def _app_exec(self, line):
554 self.shell.runlines(line)
556 def usr1_handler(signum, frame):
557 app.update_from_usr1()
559 def usr2_handler(signum, frame):
560 app.update_from_usr2()