2 from __future__
import with_statement
12 import dbus
, dbus
.service
, dbus
.glib
13 from xdg
import BaseDirectory
14 from matplotlib
.backend_bases
import LocationEvent
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
24 # Note: for crosshair, see gtk.gdk.GC / function = gtk.gdk.XOR
26 def report_error(parent
, msg
):
27 dlg
= gtk
.MessageDialog(parent
,
28 gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
,
29 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_OK
, msg
)
30 dlg
.set_title(parent
.get_title())
34 class App(dbus
.service
.Object
):
36 <menubar name="MenuBar">
38 <menuitem action="Add file(s)..."/>
39 <menuitem action="Update files"/>
40 <menuitem action="Execute script..."/>
41 <menuitem action="New Math Signal..."/>
42 <menuitem action="Run netlister and simulate..."/>
43 <menuitem action="Quit"/>
45 <menu action="Windows">
50 def __init__(self
, bus_name
, object_path
='/org/freedesktop/Oscopy', ctxt
=None):
51 if bus_name
is not None:
52 dbus
.service
.Object
.__init
__(self
, bus_name
, object_path
)
53 self
._scale
_to
_str
= {'lin': _('Linear'), 'logx': _('LogX'), 'logy': _('LogY'),\
54 'loglog': _('Loglog')}
55 self
._windows
_to
_figures
= {}
56 self
._fignum
_to
_windows
= {}
57 self
._fignum
_to
_merge
_id
= {}
58 self
._current
_graph
= None
59 self
._current
_figure
= None
60 self
._prompt
= "oscopy-ui>"
64 self
._TARGET
_TYPE
_SIGNAL
= 10354
65 self
._from
_signal
_list
= [("oscopy-signals", gtk
.TARGET_SAME_APP
,\
66 self
._TARGET
_TYPE
_SIGNAL
)]
67 self
._to
_figure
= [("oscopy-signals", gtk
.TARGET_SAME_APP
,\
68 self
._TARGET
_TYPE
_SIGNAL
)]
69 self
._to
_main
_win
= [("text/plain", 0,
70 self
._TARGET
_TYPE
_SIGNAL
),
72 self
._TARGET
_TYPE
_SIGNAL
),
73 ('application/octet-stream', 0,
74 self
._TARGET
_TYPE
_SIGNAL
),
76 ('application/x-panasonic-raw', 0,
77 self
._TARGET
_TYPE
_SIGNAL
),
80 self
._TARGET
_TYPE
_SIGNAL
),
84 self
._ctxt
= oscopy
.Context()
88 self
._store
= gtk
.TreeStore(gobject
.TYPE_STRING
, gobject
.TYPE_PYOBJECT
,
90 self
._create
_widgets
()
91 #self._app_exec('read demo/irf540.dat')
92 #self._app_exec('read demo/ac.dat')
93 #self._add_file('demo/res.dat')
95 # From IPython/demo.py
96 self
.shell
= get_ipython()
99 OPT_NETLISTER_COMMANDS
= 'netlister_commands'
100 OPT_SIMULATOR_COMMANDS
= 'simulator_commands'
101 OPT_RUN_DIRECTORY
= 'run_directory'
106 def _action_add_file(self
, action
):
107 dlg
= gtk
.FileChooserDialog(_('Add file(s)'), parent
=self
._mainwindow
,
108 buttons
=(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
109 gtk
.STOCK_OK
, gtk
.RESPONSE_ACCEPT
))
110 dlg
.set_select_multiple(True)
112 if resp
== gtk
.RESPONSE_ACCEPT
:
113 for filename
in dlg
.get_filenames():
114 self
._app
_exec
('oread ' + filename
)
117 def _action_update(self
, action
):
120 def _action_new_math(self
, action
):
121 dlg
= gtk
.Dialog(_('New math signal'), parent
=self
._mainwindow
,
122 buttons
=(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
123 gtk
.STOCK_OK
, gtk
.RESPONSE_ACCEPT
))
127 label
= gtk
.Label(_('Expression:'))
128 hbox
.pack_start(label
)
130 hbox
.pack_start(entry
)
131 dlg
.vbox
.pack_start(hbox
)
135 if resp
== gtk
.RESPONSE_ACCEPT
:
136 expr
= entry
.get_text()
137 self
._app
_exec
('%s' % expr
)
138 self
._app
_exec
('oimport %s' % expr
.split('=')[0].strip())
141 def _action_execute_script(self
, action
):
142 dlg
= gtk
.FileChooserDialog(_('Execute script'), parent
=self
._mainwindow
,
143 buttons
=(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
144 gtk
.STOCK_OK
, gtk
.RESPONSE_ACCEPT
))
146 filename
= dlg
.get_filename()
148 if resp
== gtk
.RESPONSE_ACCEPT
:
149 self
._app
_exec
('oexec ' + filename
)
151 def _action_netlist_and_simulate(self
, action
):
152 dlg
= gui
.dialogs
.Run_Netlister_and_Simulate_Dialog()
153 dlg
.display(self
._actions
)
157 self
._actions
= actions
158 run_dir
= actions
['run_from']
159 if actions
['run_netlister'][0]:
160 if not self
._run
_ext
_command
(actions
['run_netlister'][1][0], run_dir
):
162 if actions
['run_simulator'][0]:
163 if not self
._run
_ext
_command
(actions
['run_simulator'][1][0], run_dir
):
165 if actions
['update']:
168 def _action_quit(self
, action
):
170 readline
.write_history_file(self
.hist_file
)
174 def _action_figure(self
, action
, w
, fignum
):
175 if not (w
.flags() & gtk
.VISIBLE
):
179 self
._app
_exec
('%%oselect %d-1' % fignum
)
182 # UI Creation functions
184 def _create_menubar(self
):
186 # (name, stock-id, label, accelerator, tooltip, callback)
188 ('File', None, _('_File')),
189 ('Add file(s)...', gtk
.STOCK_ADD
, _('_Add file(s)...'), None, None,
190 self
._action
_add
_file
),
191 ('Update files', gtk
.STOCK_REFRESH
, _('_Update'), None, None,
192 self
._action
_update
),
193 ('Execute script...', gtk
.STOCK_MEDIA_PLAY
, _('_Execute script...'),
194 None, None, self
._action
_execute
_script
),
195 ("New Math Signal...", gtk
.STOCK_NEW
, _('_New Math Signal'), None,
196 None, self
._action
_new
_math
),
197 ("Run netlister and simulate...", gtk
.STOCK_MEDIA_FORWARD
,\
198 _("_Run netlister and simulate..."), None, None,\
199 self
._action
_netlist
_and
_simulate
),
200 ('Windows', None, _('_Windows')),
201 ('Quit', gtk
.STOCK_QUIT
, _('_Quit'), None, None,
205 actiongroup
= self
._actiongroup
= gtk
.ActionGroup('App')
206 actiongroup
.add_actions(actions
)
208 uimanager
= self
._uimanager
= gtk
.UIManager()
209 uimanager
.add_ui_from_string(self
.__ui
)
210 uimanager
.insert_action_group(actiongroup
, 0)
211 return uimanager
.get_accel_group(), uimanager
.get_widget('/MenuBar')
213 def _create_treeview(self
):
214 celltext
= gtk
.CellRendererText()
215 col
= gtk
.TreeViewColumn(_('Signal'), celltext
, text
=0)
217 col
.set_cell_data_func(celltext
, self
._reader
_name
_in
_bold
)
219 tv
.append_column(col
)
220 tv
.set_model(self
._store
)
221 tv
.connect('row-activated', self
._row
_activated
)
222 tv
.connect('drag_data_get', self
._drag
_data
_get
_cb
)
223 tv
.drag_source_set(gtk
.gdk
.BUTTON1_MASK
,\
224 self
._from
_signal
_list
,\
226 self
._togglecell
= gtk
.CellRendererToggle()
227 self
._togglecell
.set_property('activatable', True)
228 self
._togglecell
.connect('toggled', self
._cell
_toggled
, None)
229 colfreeze
= gtk
.TreeViewColumn(_('Freeze'), self
._togglecell
)
230 colfreeze
.add_attribute(self
._togglecell
, 'active', 2)
231 tv
.append_column(colfreeze
)
232 tv
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
235 def _reader_name_in_bold(self
, column
, cell
, model
, iter, data
=None):
236 if len(model
.get_path(iter)) == 1:
237 cell
.set_property('markup', "<b>" + model
.get_value(iter, 0) +\
240 cell
.set_property('text', model
.get_value(iter, 0))
242 def _create_widgets(self
):
243 accel_group
, self
._menubar
= self
._create
_menubar
()
244 self
._treeview
= self
._create
_treeview
()
246 sw
= gtk
.ScrolledWindow()
247 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
248 sw
.add(self
._treeview
)
251 vbox
.pack_start(self
._menubar
, False)
254 w
= self
._mainwindow
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
255 w
.set_title(_('Oscopy GUI'))
257 w
.add_accel_group(accel_group
)
258 w
.connect('destroy', lambda w
, e
: w
.hide() or True)
259 w
.connect('delete-event', lambda w
, e
: w
.hide() or True)
260 w
.set_default_size(400, 300)
262 w
.drag_dest_set(gtk
.DEST_DEFAULT_MOTION |\
263 gtk
.DEST_DEFAULT_HIGHLIGHT |\
264 gtk
.DEST_DEFAULT_DROP
,
265 self
._to
_main
_win
, gtk
.gdk
.ACTION_COPY
)
266 w
.connect('drag_data_received', self
._drag
_data
_received
_main
_cb
)
268 def _create_figure_popup_menu(self
, figure
, graph
):
269 figmenu
= gui
.menus
.FigureMenu()
270 return figmenu
.create_menu(self
._store
, figure
, graph
, self
._app
_exec
)
273 self
._mainwindow
.show()
276 # Event-triggered functions
278 def _treeview_button_press(self
, widget
, event
):
279 if event
.button
== 3:
281 path
, tvc
, x
, y
= tv
.get_path_at_pos(int(event
.x
), int(event
.y
))
285 row
= self
._store
[path
]
286 signals
= {row
[0]: row
[1]}
287 menu
= self
._create
_treeview
_popup
_menu
(signals
, path
)
289 menu
.popup(None, None, None, event
.button
, event
.time
)
291 def _button_press(self
, event
):
292 if event
.button
== 3:
293 menu
= self
._create
_figure
_popup
_menu
(event
.canvas
.figure
, event
.inaxes
)
295 menu
.popup(None, None, None, event
.button
, event
.guiEvent
.time
)
297 #TODO: _windows_to_figures consistency...
298 # think of a better way to map events to Figure objects
299 def _row_activated(self
, widget
, path
, col
):
303 row
= self
._store
[path
]
304 self
._app
_exec
('ocreate %s' % row
[0])
306 def _axes_enter(self
, event
):
307 self
._figure
_enter
(event
)
308 self
._current
_graph
= event
.inaxes
310 axes_num
= event
.canvas
.figure
.axes
.index(event
.inaxes
) + 1
311 fig_num
= self
._ctxt
.figures
.index(self
._current
_figure
) + 1
312 self
._app
_exec
('%%oselect %d-%d' % (fig_num
, axes_num
))
314 def _axes_leave(self
, event
):
315 # Unused for better user interaction
316 # self._current_graph = None
319 def _figure_enter(self
, event
):
320 self
._current
_figure
= event
.canvas
.figure
321 if hasattr(event
, 'inaxes') and event
.inaxes
is not None:
322 axes_num
= event
.canvas
.figure
.axes
.index(event
.inaxes
) + 1
325 fig_num
= self
._ctxt
.figures
.index(self
._current
_figure
) + 1
326 self
._app
_exec
('%%oselect %d-%d' % (fig_num
, axes_num
))
328 def _figure_leave(self
, event
):
329 # self._current_figure = None
332 def _cell_toggled(self
, cellrenderer
, path
, data
):
335 if self
._store
[path
][1].freeze
:
339 self
._app
_exec
('%s %s' % (cmd
, self
._store
[path
][0]))
342 parent
= self
._store
.get_iter(path
)
343 freeze
= not self
._store
.get_value(parent
, 2)
344 if self
._store
[path
][2]:
348 self
._store
.set_value(parent
, 2, freeze
)
349 iter = self
._store
.iter_children(parent
)
351 self
._app
_exec
('%s %s' % (cmd
, self
._store
.get_value(iter, 0)))
352 iter = self
._store
.iter_next(iter)
358 """ Instanciate the window widget with the figure inside, set the
359 relevant events and add it to the 'Windows' menu.
360 Finally, select the first graph of this figure.
362 The figure has been instanciated by the application
363 and is assumed to be the last one in Context's figure list
365 fig
= self
._ctxt
.figures
[len(self
._ctxt
.figures
) - 1]
366 fignum
= len(self
._ctxt
.figures
)
369 self
._windows
_to
_figures
[w
] = fig
370 self
._fignum
_to
_windows
[fignum
] = w
371 w
.set_title(_('Figure %d') % fignum
)
374 canvas
= FigureCanvas(fig
)
375 canvas
.mpl_connect('button_press_event', self
._button
_press
)
376 canvas
.mpl_connect('axes_enter_event', self
._axes
_enter
)
377 canvas
.mpl_connect('axes_leave_event', self
._axes
_leave
)
378 canvas
.mpl_connect('figure_enter_event', self
._figure
_enter
)
379 canvas
.mpl_connect('figure_leave_event', self
._figure
_leave
)
380 w
.connect("drag_data_received", self
._drag
_data
_received
_cb
)
381 w
.connect('delete-event', lambda w
, e
: w
.hide() or True)
382 w
.drag_dest_set(gtk
.DEST_DEFAULT_MOTION |\
383 gtk
.DEST_DEFAULT_HIGHLIGHT |\
384 gtk
.DEST_DEFAULT_DROP
,
385 self
._to
_figure
, gtk
.gdk
.ACTION_COPY
)
386 vbox
.pack_start(canvas
)
387 toolbar
= NavigationToolbar(canvas
, w
)
388 vbox
.pack_start(toolbar
, False, False)
390 # hscale = gtk.HScrollbar()
391 ## hscale.set_range(0, 10)
392 # #hscale.set_draw_value(False)
393 ## hscale.set_value(5)
394 # adj = gtk.Adjustment(50, 0, 100, 1, 10, 20)
395 # hscale.set_adjustment(adj)
396 # #hscale.set_slider_size_fixed(False)
397 # vbox.pack_start(hscale, False, False)
402 # # Update canvas for SpanSelector of Graphs
403 # for gr in fig.graphs:
404 # if hasattr(gr, 'span'):
405 # gr.span.new_axes(gr)
407 # Add it to the 'Windows' menu
408 actions
= [('Figure %d' % fignum
, None, _('Figure %d') % fignum
,
409 None, None, self
._action
_figure
)]
410 self
._actiongroup
.add_actions(actions
, (w
, fignum
))
412 <menubar name=\"MenuBar\">\
413 <menu action=\"Windows\">\
414 <menuitem action=\"Figure %d\"/>\
418 merge_id
= self
._uimanager
.add_ui_from_string(ui
)
419 self
._fignum
_to
_merge
_id
[fignum
] = merge_id
420 self
._app
_exec
('%%oselect %d-1' % fignum
)
422 def destroy(self
, num
):
423 if not num
.isdigit() or int(num
) > len(self
._ctxt
.figures
):
427 action
= self
._uimanager
.get_action('/MenuBar/Windows/Figure %d' %
429 if action
is not None:
430 self
._actiongroup
.remove_action(action
)
431 self
._uimanager
.remove_ui(self
._fignum
_to
_merge
_id
[fignum
])
432 self
._fignum
_to
_windows
[fignum
].destroy()
434 # Search algorithm from pygtk tutorial
435 def _match_func(self
, row
, data
):
437 return row
[column
] == key
439 def _search(self
, rows
, func
, data
):
440 if not rows
: return None
444 result
= self
._search
(row
.iterchildren(), func
, data
)
445 if result
: return result
448 def freeze(self
, signals
):
449 for signal
in signals
.split(','):
450 match_row
= self
._search
(self
._store
, self
._match
_func
,\
452 if match_row
is not None:
453 match_row
[2] = match_row
[1].freeze
454 parent
= self
._store
.iter_parent(match_row
.iter)
455 iter = self
._store
.iter_children(parent
)
456 freeze
= match_row
[2]
458 if not self
._store
.get_value(iter, 2) == freeze
:
460 iter = self
._store
.iter_next(iter)
462 # All row at the same freeze value,
463 # set freeze for the reader
464 self
._store
.set_value(parent
, 2, freeze
)
466 # Set reader freeze to false
467 self
._store
.set_value(parent
, 2, False)
469 def add_file(self
, filename
):
470 if filename
.strip() in self
._ctxt
.readers
:
471 it
= self
._store
.append(None, (filename
.strip(), None, False))
472 for name
, sig
in self
._ctxt
.readers
[filename
.strip()]\
473 .signals
.iteritems():
474 self
._store
.append(it
, (name
, sig
, sig
.freeze
))
477 # Callbacks for drag and drop
479 def _drag_data_received_main_cb(self
, widget
, drag_context
, x
, y
, selection
,
481 name
= selection
.data
482 if type(name
) == str and name
.startswith('file://'):
483 print name
[7:].strip()
484 self
._app
_exec
('%%oread %s' % name
[7:].strip())
486 def _drag_data_get_cb(self
, widget
, drag_context
, selection
, target_type
,\
488 if target_type
== self
._TARGET
_TYPE
_SIGNAL
:
490 sel
= tv
.get_selection()
491 (model
, pathlist
) = sel
.get_selected_rows()
492 iter = self
._store
.get_iter(pathlist
[0])
493 data
= " ".join(map(lambda x
:self
._store
[x
][1].name
, pathlist
))
494 selection
.set(selection
.target
, 8, data
)
495 # The multiple selection do work, but how to select signals
496 # that are not neighbours in the list? Ctrl+left do not do
497 # anything, neither alt+left or shift+left!
499 def _drag_data_received_cb(self
, widget
, drag_context
, x
, y
, selection
,\
501 # Event handling issue: this drag and drop callback is
502 # processed before matplotlib callback _axes_enter. Therefore
503 # when dropping, self._current_graph is not valid: it contains
505 # The workaround is to retrieve the Graph by creating a Matplotlib
506 # LocationEvent considering inverse 'y' coordinates
507 if target_type
== self
._TARGET
_TYPE
_SIGNAL
:
508 canvas
= self
._windows
_to
_figures
[widget
].canvas
509 my_y
= canvas
.allocation
.height
- y
510 event
= LocationEvent('axes_enter_event', canvas
, x
, my_y
)
512 for name
in selection
.data
.split():
513 signals
[name
] = self
._ctxt
.signals
[name
]
514 if event
.inaxes
is not None:
516 event
.inaxes
.insert(signals
)
517 self
._windows
_to
_figures
[widget
].canvas
.draw()
520 # Configuration-file related functions
522 def _init_config(self
):
523 # initialize configuration stuff
524 path
= BaseDirectory
.save_config_path('oscopy')
525 self
.config_file
= os
.path
.join(path
, 'gui')
526 self
.hist_file
= os
.path
.join(path
, 'history')
527 section
= App
.SECTION
528 self
.config
= ConfigParser
.RawConfigParser()
529 self
.config
.add_section(section
)
531 self
.config
.set(section
, App
.OPT_NETLISTER_COMMANDS
, '')
532 self
.config
.set(section
, App
.OPT_SIMULATOR_COMMANDS
, '')
533 self
.config
.set(section
, App
.OPT_RUN_DIRECTORY
, '.')
535 def _sanitize_list(self
, lst
):
536 return filter(lambda x
: len(x
) > 0, map(lambda x
: x
.strip(), lst
))
538 def _actions_from_config(self
, config
):
539 section
= App
.SECTION
540 netlister_commands
= config
.get(section
, App
.OPT_NETLISTER_COMMANDS
)
541 netlister_commands
= self
._sanitize
_list
(netlister_commands
.split(';'))
542 simulator_commands
= config
.get(section
, App
.OPT_SIMULATOR_COMMANDS
)
543 simulator_commands
= self
._sanitize
_list
(simulator_commands
.split(';'))
545 'run_netlister': (True, netlister_commands
),
546 'run_simulator': (True, simulator_commands
),
548 'run_from': config
.get(section
, App
.OPT_RUN_DIRECTORY
)}
551 def _actions_to_config(self
, actions
, config
):
552 section
= App
.SECTION
553 netlister_commands
= ';'.join(actions
['run_netlister'][1])
554 simulator_commands
= ';'.join(actions
['run_simulator'][1])
555 config
.set(section
, App
.OPT_NETLISTER_COMMANDS
, netlister_commands
)
556 config
.set(section
, App
.OPT_SIMULATOR_COMMANDS
, simulator_commands
)
557 config
.set(section
, App
.OPT_RUN_DIRECTORY
, actions
['run_from'])
559 def _read_config(self
):
560 self
.config
.read(self
.config_file
)
561 self
._actions
= self
._actions
_from
_config
(self
.config
)
563 def _write_config(self
):
564 self
._actions
_to
_config
(self
._actions
, self
.config
)
565 with
open(self
.config_file
, 'w') as f
:
569 @dbus.service
.method('org.freedesktop.OscopyIFace')
570 def dbus_update(self
):
571 gobject
.idle_add(self
._activate
_net
_and
_sim
)
573 @dbus.service
.method('org.freedesktop.OscopyIFace')
574 def dbus_running(self
):
578 def update_from_usr1(self
):
581 def update_from_usr2(self
):
582 gobject
.idle_add(self
._activate
_net
_and
_sim
)
584 def _activate_net_and_sim(self
):
585 if self
._actiongroup
is not None:
586 action
= self
._actiongroup
.get_action("Run netlister and simulate...")
589 def _run_ext_command(self
, cmd
, run_dir
):
590 old_dir
= os
.getcwd()
593 status
, output
= commands
.getstatusoutput(cmd
)
595 msg
= _("Executing command '%s' failed.") % cmd
596 report_error(self
._mainwindow
, msg
)
601 def _app_exec(self
, line
):
602 first
= line
.split()[0]
603 if first
.startswith('%') or first
.split()[0] in self
.shell
.lsmagic():
604 self
.shell
.magic(line
)
608 def usr1_handler(signum
, frame
):
609 app
.update_from_usr1()
611 def usr2_handler(signum
, frame
):
612 app
.update_from_usr2()