2 from __future__
import with_statement
12 import dbus
, dbus
.service
, dbus
.glib
13 from math
import log10
, sqrt
14 from xdg
import BaseDirectory
15 #from matplotlib.widgets import SpanSelector
20 from matplotlib
.backends
.backend_gtkagg
import FigureCanvasGTKAgg
as FigureCanvas
21 from matplotlib
.backends
.backend_gtkagg
import NavigationToolbar2GTKAgg
as NavigationToolbar
23 from gtk_figure
import IOscopy_GTK_Figure
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())
39 class App(dbus
.service
.Object
):
41 <menubar name="MenuBar">
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"/>
50 <menu action="Windows">
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>"
69 # Might be moved to a dedicated app_figure class one day...
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
),
82 self
._TARGET
_TYPE
_SIGNAL
),
83 ('application/octet-stream', 0,
84 self
._TARGET
_TYPE
_SIGNAL
),
86 ('application/x-panasonic-raw', 0,
87 self
._TARGET
_TYPE
_SIGNAL
),
90 self
._TARGET
_TYPE
_SIGNAL
),
94 self
._ctxt
= oscopy
.Context()
98 self
._store
= gtk
.TreeStore(gobject
.TYPE_STRING
, gobject
.TYPE_PYOBJECT
,
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'
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)
122 if resp
== gtk
.RESPONSE_ACCEPT
:
123 for filename
in dlg
.get_filenames():
124 self
._app
_exec
('oread ' + filename
)
127 def _action_update(self
, action
):
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
))
137 label
= gtk
.Label(_('Expression:'))
138 hbox
.pack_start(label
)
140 hbox
.pack_start(entry
)
141 dlg
.vbox
.pack_start(hbox
)
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())
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
))
156 filename
= dlg
.get_filename()
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
)
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
):
172 if actions
['run_simulator'][0]:
173 if not self
._run
_ext
_command
(actions
['run_simulator'][1][0], run_dir
):
175 if actions
['update']:
178 def _action_quit(self
, action
):
180 readline
.write_history_file(self
.hist_file
)
184 def _action_figure(self
, action
, w
, fignum
):
185 if not (w
.flags() & gtk
.VISIBLE
):
189 self
._app
_exec
('%%oselect %d-1' % fignum
)
192 # UI Creation functions
194 def _create_menubar(self
):
196 # (name, stock-id, label, accelerator, tooltip, callback)
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,
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)
227 col
.set_cell_data_func(celltext
, self
._reader
_name
_in
_bold
)
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
,\
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
)
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) +\
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
)
262 vbox
.pack_start(self
._menubar
, False)
265 w
= self
._mainwindow
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
266 w
.set_title(_('Oscopy GUI'))
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)
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
)
284 self
._mainwindow
.show()
287 # Event-triggered functions
289 def _treeview_button_press(self
, widget
, event
):
290 if event
.button
== 3:
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
296 # Not supported to add a full file
298 sel
= tv
.get_selection()
299 if path
not in sel
.get_selected_rows()[1]:
300 # Click in another path than the one selected
302 sel
.select_path(path
)
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
)
311 menu
.popup(None, None, None, event
.button
, event
.time
)
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.
319 sel
= tv
.get_selection()
320 rows
= sel
.get_selected_rows()[1]
321 self
._rows
_for
_drag
= rows
324 def _row_activated(self
, widget
, path
, col
):
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
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
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
357 def _cell_toggled(self
, cellrenderer
, path
, data
):
360 if self
._store
[path
][1].freeze
:
364 self
._app
_exec
('%s %s' % (cmd
, self
._store
[path
][0]))
367 parent
= self
._store
.get_iter(path
)
368 freeze
= not self
._store
.get_value(parent
, 2)
369 if self
._store
[path
][2]:
373 self
._store
.set_value(parent
, 2, freeze
)
374 iter = self
._store
.iter_children(parent
)
376 self
._app
_exec
('%s %s' % (cmd
, self
._store
.get_value(iter, 0)))
377 iter = self
._store
.iter_next(iter)
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
,
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
))
408 <menubar name=\"MenuBar\">\
409 <menu action=\"Windows\">\
410 <menuitem action=\"Figure %d\"/>\
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
):
423 action
= self
._uimanager
.get_action('/MenuBar/Windows/Figure %d' %
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
):
433 return row
[column
] == key
435 def _search(self
, rows
, func
, data
):
436 if not rows
: return None
440 result
= self
._search
(row
.iterchildren(), func
, data
)
441 if result
: return result
444 def freeze(self
, signals
):
445 for signal
in signals
.split(','):
446 match_row
= self
._search
(self
._store
, self
._match
_func
,\
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]
454 if not self
._store
.get_value(iter, 2) == freeze
:
456 iter = self
._store
.iter_next(iter)
458 # All row at the same freeze value,
459 # set freeze for the reader
460 self
._store
.set_value(parent
, 2, freeze
)
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
,
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
,\
484 if target_type
== self
._TARGET
_TYPE
_SIGNAL
:
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
)
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
)
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(';'))
520 'run_netlister': (True, netlister_commands
),
521 'run_simulator': (True, simulator_commands
),
523 'run_from': config
.get(section
, App
.OPT_RUN_DIRECTORY
)}
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
:
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
):
553 def update_from_usr1(self
):
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...")
564 def _run_ext_command(self
, cmd
, run_dir
):
565 old_dir
= os
.getcwd()
568 status
, output
= commands
.getstatusoutput(cmd
)
570 msg
= _("Executing command '%s' failed.") % cmd
571 report_error(self
._mainwindow
, msg
)
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
)
583 def usr1_handler(signum
, frame
):
584 app
.update_from_usr1()
586 def usr2_handler(signum
, frame
):
587 app
.update_from_usr2()