2 from __future__
import with_statement
11 import dbus
, dbus
.service
, dbus
.glib
12 from xdg
import BaseDirectory
16 from matplotlib
.backends
.backend_gtkagg
import FigureCanvasGTKAgg
as FigureCanvas
17 from matplotlib
.backends
.backend_gtkagg
import NavigationToolbar2GTKAgg
as NavigationToolbar
20 # Note: for crosshair, see gtk.gdk.GC / function = gtk.gdk.XOR
22 def report_error(parent
, msg
):
23 dlg
= gtk
.MessageDialog(parent
,
24 gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
,
25 gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_OK
, msg
)
26 dlg
.set_title(parent
.get_title())
30 class OscopyAppUI(oscopy
.OscopyApp
):
31 def __init__(self
, context
):
32 oscopy
.OscopyApp
.__init
__(self
, context
)
34 self
._autorefresh
= True
36 def connect(self
, event
, func
, data
):
37 if not isinstance(event
, str):
39 if hasattr(self
, 'do_'+event
):
40 self
._callbacks
[event
] = {func
: data
}
42 def postcmd(self
, stop
, line
):
43 oscopy
.OscopyApp
.postcmd(self
, stop
, line
)
46 event
= line
.split()[0].strip()
47 if len(line
.split()) > 1:
48 args
= line
.split(' ', 1)[1].strip()
51 if self
._callbacks
.has_key(event
):
52 for func
, data
in self
._callbacks
[event
].iteritems():
53 func(event
, args
, data
)
54 if self
._autorefresh
and self
._current
_figure
is not None and\
55 self
._current
_figure
.canvas
is not None:
56 self
._current
_figure
.canvas
.draw()
58 def help_refresh(self
):
59 print 'refresh FIG#|on|off|current|all'
60 print ' on|off toggle auto refresh of current figure'
61 print ' current|all refresh either current figure or all'
62 print ' FIG# figure to refresh'
63 print 'without arguments refresh current figure'
64 def do_refresh(self
, args
):
66 self
._autorefresh
= True
68 self
._autorefresh
= False
69 elif args
== 'current' or args
== '':
70 if self
._current
_figure
is not None and\
71 self
._current
_figure
.canvas
is not None:
72 self
._current
_figure
.canvas
.draw()
74 for fig
in self
._ctxt
.figures
:
75 if fig
.canvas
is not None:
78 fignum
= int(args
) - 1
79 if fignum
>= 0 and fignum
< len(self
._ctxt
.figures
):
80 if self
._ctxt
.figures
[fignum
].canvas
is not None:
82 self
._ctxt
.figures
[fignum
].canvas
.draw()
84 def do_pause(self
, args
):
85 print "Pause command disabled in UI"
87 def do_plot(self
, line
):
88 print "Plot command disabled in UI"
90 class App(dbus
.service
.Object
):
92 <menubar name="MenuBar">
94 <menuitem action="Add file(s)..."/>
95 <menuitem action="Update files"/>
96 <menuitem action="Execute script..."/>
97 <menuitem action="New Math Signal..."/>
98 <menuitem action="Run netlister and simulate..."/>
99 <menuitem action="Quit"/>
101 <menu action="Windows">
102 <menuitem action="Show terminal"/>
107 def __init__(self
, bus_name
, object_path
='/org/freedesktop/Oscopy'):
108 dbus
.service
.Object
.__init
__(self
, bus_name
, object_path
)
109 self
._scale
_to
_str
= {'lin': 'Linear', 'logx': 'LogX', 'logy': 'LogY',\
111 self
._windows
_to
_figures
= {}
112 self
._fignum
_to
_windows
= {}
113 self
._fignum
_to
_merge
_id
= {}
114 self
._current
_graph
= None
115 self
._current
_figure
= None
116 self
._term
_win
= None
117 self
._prompt
= "oscopy-ui>"
121 self
._TARGET
_TYPE
_SIGNAL
= 10354
122 self
._from
_signal
_list
= [("oscopy-signals", gtk
.TARGET_SAME_APP
,\
123 self
._TARGET
_TYPE
_SIGNAL
)]
124 self
._to
_figure
= [("oscopy-signals", gtk
.TARGET_SAME_APP
,\
125 self
._TARGET
_TYPE
_SIGNAL
)]
127 self
._ctxt
= oscopy
.Context()
128 self
._app
= OscopyAppUI(self
._ctxt
)
129 self
._app
.connect('read', self
._add
_file
, None)
130 self
._app
.connect('math', self
._add
_file
, None)
131 self
._app
.connect('freeze', self
._freeze
, None)
132 self
._app
.connect('unfreeze', self
._freeze
, None)
133 self
._app
.connect('create', self
._create
, None)
134 self
._app
.connect('destroy', self
._destroy
, None)
135 self
._app
.connect('quit', lambda e
, s
, d
: self
._action
_quit
(None), None)
136 self
._app
.connect('exit', lambda e
, s
, d
: self
._action
_quit
(None), None)
137 self
._store
= gtk
.TreeStore(gobject
.TYPE_STRING
, gobject
.TYPE_PYOBJECT
,
138 gobject
.TYPE_BOOLEAN
)
139 self
._create
_widgets
()
140 #self._app_exec('read demo/irf540.dat')
141 #self._app_exec('read demo/ac.dat')
142 #self._add_file('demo/res.dat')
144 SECTION
= 'oscopy_ui'
145 OPT_NETLISTER_COMMANDS
= 'netlister_commands'
146 OPT_SIMULATOR_COMMANDS
= 'simulator_commands'
147 OPT_RUN_DIRECTORY
= 'run_directory'
152 def _action_add_file(self
, action
):
153 dlg
= gtk
.FileChooserDialog('Add file(s)', parent
=self
._mainwindow
,
154 buttons
=(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
155 gtk
.STOCK_OK
, gtk
.RESPONSE_ACCEPT
))
156 dlg
.set_select_multiple(True)
158 if resp
== gtk
.RESPONSE_ACCEPT
:
159 for filename
in dlg
.get_filenames():
160 self
._app
_exec
("read " + filename
)
163 def _action_update(self
, action
):
166 def _action_new_math(self
, action
):
167 dlg
= gtk
.Dialog('New math signal', parent
=self
._mainwindow
,
168 buttons
=(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
169 gtk
.STOCK_OK
, gtk
.RESPONSE_ACCEPT
))
173 label
= gtk
.Label('Expression:')
174 hbox
.pack_start(label
)
176 hbox
.pack_start(entry
)
177 dlg
.vbox
.pack_start(hbox
)
181 if resp
== gtk
.RESPONSE_ACCEPT
:
182 expr
= entry
.get_text()
183 self
._app
_exec
('%s' % expr
)
187 def _action_show_terminal(self
, action
):
188 if self
._term
_win
.flags() & gtk
.VISIBLE
:
189 self
._term
_win
.hide()
191 self
._term
_win
.show()
193 def _action_execute_script(self
, action
):
194 dlg
= gtk
.FileChooserDialog('Execute script', parent
=self
._mainwindow
,
195 buttons
=(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
196 gtk
.STOCK_OK
, gtk
.RESPONSE_ACCEPT
))
198 filename
= dlg
.get_filename()
200 if resp
== gtk
.RESPONSE_ACCEPT
:
201 self
._app
_exec
("exec " + filename
)
203 def _action_netlist_and_simulate(self
, action
):
204 dlg
= oscopy_gui
.dialogs
.Run_Netlister_and_Simulate_Dialog()
205 dlg
.display(self
._actions
)
209 self
._actions
= actions
210 run_dir
= actions
['run_from']
211 if actions
['run_netlister'][0]:
212 if not self
._run
_ext
_command
(actions
['run_netlister'][1][0], run_dir
):
214 if actions
['run_simulator'][0]:
215 if not self
._run
_ext
_command
(actions
['run_simulator'][1][0], run_dir
):
217 if actions
['update']:
220 def _action_quit(self
, action
):
222 readline
.write_history_file(self
.hist_file
)
225 def _action_figure(self
, action
, w
, fignum
):
226 if not (w
.flags() & gtk
.VISIBLE
):
230 self
._app
_exec
('select %d-1' % fignum
)
233 # UI Creation functions
235 def _create_menubar(self
):
237 # (name, stock-id, label, accelerator, tooltip, callback)
239 ('File', None, '_File'),
240 ('Add file(s)...', gtk
.STOCK_ADD
, '_Add file(s)...', None, None,
241 self
._action
_add
_file
),
242 ('Update files', gtk
.STOCK_REFRESH
, '_Update', None, None,
243 self
._action
_update
),
244 ('Execute script...', gtk
.STOCK_MEDIA_PLAY
, '_Execute script...',
245 None, None, self
._action
_execute
_script
),
246 ("New Math Signal...", gtk
.STOCK_NEW
, '_New Math Signal', None,
247 None, self
._action
_new
_math
),
248 ("Run netlister and simulate...", gtk
.STOCK_MEDIA_FORWARD
,\
249 "_Run netlister and simulate...", None, None,\
250 self
._action
_netlist
_and
_simulate
),
251 ('Windows', None, '_Windows'),
252 ('Quit', gtk
.STOCK_QUIT
, '_Quit', None, None,
256 actiongroup
= self
._actiongroup
= gtk
.ActionGroup('App')
257 actiongroup
.add_actions(actions
)
259 ta
= gtk
.ToggleAction('Show terminal', '_Show terminal', None, None)
261 ta
.connect('activate', self
._action
_show
_terminal
)
262 actiongroup
.add_action(ta
)
264 uimanager
= self
._uimanager
= gtk
.UIManager()
265 uimanager
.add_ui_from_string(self
.__ui
)
266 uimanager
.insert_action_group(actiongroup
, 0)
267 return uimanager
.get_accel_group(), uimanager
.get_widget('/MenuBar')
269 def _create_treeview(self
):
270 celltext
= gtk
.CellRendererText()
271 col
= gtk
.TreeViewColumn('Signal', celltext
, text
=0)
273 col
.set_cell_data_func(celltext
, self
._reader
_name
_in
_bold
)
275 tv
.append_column(col
)
276 tv
.set_model(self
._store
)
277 tv
.connect('row-activated', self
._row
_activated
)
278 tv
.connect('drag_data_get', self
._drag
_data
_get
_cb
)
279 tv
.drag_source_set(gtk
.gdk
.BUTTON1_MASK
,\
280 self
._from
_signal
_list
,\
282 self
._togglecell
= gtk
.CellRendererToggle()
283 self
._togglecell
.set_property('activatable', True)
284 self
._togglecell
.connect('toggled', self
._cell
_toggled
, None)
285 colfreeze
= gtk
.TreeViewColumn('Freeze', self
._togglecell
)
286 colfreeze
.add_attribute(self
._togglecell
, 'active', 2)
287 tv
.append_column(colfreeze
)
288 tv
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
291 def _reader_name_in_bold(self
, column
, cell
, model
, iter, data
=None):
292 if len(model
.get_path(iter)) == 1:
293 cell
.set_property('markup', "<b>" + model
.get_value(iter, 0) +\
296 cell
.set_property('text', model
.get_value(iter, 0))
298 def _create_widgets(self
):
299 accel_group
, self
._menubar
= self
._create
_menubar
()
300 self
._treeview
= self
._create
_treeview
()
301 self
._create
_terminal
_window
()
303 sw
= gtk
.ScrolledWindow()
304 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
305 sw
.add(self
._treeview
)
308 vbox
.pack_start(self
._menubar
, False)
311 w
= self
._mainwindow
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
312 w
.set_title('Oscopy GUI')
314 w
.add_accel_group(accel_group
)
315 w
.connect('destroy', lambda *x
: self
._action
_quit
(None))
316 w
.set_default_size(400, 300)
319 def _create_terminal_window(self
):
320 if self
._term
_win
is None:
321 self
._term
_win
= oscopy_gui
.dialogs
.TerminalWindow(self
._prompt
,
325 self
._term
_win
.create()
326 self
._term
_win
.connect('delete-event', lambda w
, e
: w
.hide() or True)
327 if not (self
._term
_win
.flags() & gtk
.VISIBLE
):
328 self
._term
_win
.show_all()
330 def _create_figure_popup_menu(self
, figure
, graph
):
331 figmenu
= oscopy_gui
.menus
.FigureMenu()
332 return figmenu
.create_menu(self
._store
, figure
, graph
, self
._app
_exec
)
335 # Event-triggered functions
337 def _treeview_button_press(self
, widget
, event
):
338 if event
.button
== 3:
340 path
, tvc
, x
, y
= tv
.get_path_at_pos(int(event
.x
), int(event
.y
))
344 row
= self
._store
[path
]
345 signals
= {row
[0]: row
[1]}
346 menu
= self
._create
_treeview
_popup
_menu
(signals
, path
)
348 menu
.popup(None, None, None, event
.button
, event
.time
)
350 def _button_press(self
, event
):
351 if event
.button
== 3:
352 menu
= self
._create
_figure
_popup
_menu
(event
.canvas
.figure
, event
.inaxes
)
354 menu
.popup(None, None, None, event
.button
, event
.guiEvent
.time
)
356 #TODO: _windows_to_figures consistency...
357 # think of a better way to map events to Figure objects
358 def _row_activated(self
, widget
, path
, col
):
362 row
= self
._store
[path
]
363 self
._app
_exec
('create %s' % row
[0])
365 def _axes_enter(self
, event
):
366 self
._figure
_enter
(event
)
367 self
._current
_graph
= event
.inaxes
368 axes_num
= event
.canvas
.figure
.axes
.index(event
.inaxes
) + 1
369 fig_num
= self
._ctxt
.figures
.index(self
._current
_figure
) + 1
370 self
._app
_exec
('select %d-%d' % (fig_num
, axes_num
))
372 def _axes_leave(self
, event
):
373 # Unused for better user interaction
374 # self._current_graph = None
377 def _figure_enter(self
, event
):
378 self
._current
_figure
= event
.canvas
.figure
379 if hasattr(event
, 'inaxes') and event
.inaxes
is not None:
380 axes_num
= event
.canvas
.figure
.axes
.index(event
.inaxes
) + 1
383 fig_num
= self
._ctxt
.figures
.index(self
._current
_figure
) + 1
384 self
._app
_exec
('select %d-%d' % (fig_num
, axes_num
))
386 def _figure_leave(self
, event
):
387 # self._current_figure = None
390 def _cell_toggled(self
, cellrenderer
, path
, data
):
393 if self
._store
[path
][1].freeze
:
397 self
._app
_exec
('%s %s' % (cmd
, self
._store
[path
][0]))
400 parent
= self
._store
.get_iter(path
)
401 freeze
= not self
._store
.get_value(parent
, 2)
402 if self
._store
[path
][2]:
406 self
._store
.set_value(parent
, 2, freeze
)
407 iter = self
._store
.iter_children(parent
)
409 self
._app
_exec
('%s %s' % (cmd
, self
._store
.get_value(iter, 0)))
410 iter = self
._store
.iter_next(iter)
415 def _create(self
, event
, signals
, data
=None):
416 fig
= self
._ctxt
.figures
[len(self
._ctxt
.figures
) - 1]
417 fignum
= len(self
._ctxt
.figures
)
420 self
._windows
_to
_figures
[w
] = fig
421 self
._fignum
_to
_windows
[fignum
] = w
422 w
.set_title('Figure %d' % fignum
)
425 canvas
= FigureCanvas(fig
)
426 canvas
.mpl_connect('button_press_event', self
._button
_press
)
427 canvas
.mpl_connect('axes_enter_event', self
._axes
_enter
)
428 canvas
.mpl_connect('axes_leave_event', self
._axes
_leave
)
429 canvas
.mpl_connect('figure_enter_event', self
._figure
_enter
)
430 canvas
.mpl_connect('figure_leave_event', self
._figure
_leave
)
431 w
.connect("drag_data_received", self
._drag
_data
_received
_cb
)
432 w
.connect('delete-event', lambda w
, e
: w
.hide() or True)
433 w
.drag_dest_set(gtk
.DEST_DEFAULT_MOTION |\
434 gtk
.DEST_DEFAULT_HIGHLIGHT |\
435 gtk
.DEST_DEFAULT_DROP
,
436 self
._to
_figure
, gtk
.gdk
.ACTION_COPY
)
437 vbox
.pack_start(canvas
)
438 toolbar
= NavigationToolbar(canvas
, w
)
439 vbox
.pack_start(toolbar
, False, False)
443 # Add it to the 'Windows' menu
444 actions
= [('Figure %d' % fignum
, None, 'Figure %d' % fignum
,
445 None, None, self
._action
_figure
)]
446 self
._actiongroup
.add_actions(actions
, (w
, fignum
))
448 <menubar name=\"MenuBar\">\
449 <menu action=\"Windows\">\
450 <menuitem action=\"Figure %d\"/>\
454 merge_id
= self
._uimanager
.add_ui_from_string(ui
)
455 self
._fignum
_to
_merge
_id
[fignum
] = merge_id
456 self
._app
_exec
('select %d-1' % fignum
)
458 def _destroy(self
, event
, num
, data
=None):
459 if not num
.isdigit() or int(num
) > len(self
._ctxt
.figures
):
463 action
= self
._uimanager
.get_action('/MenuBar/Windows/Figure %d' %
465 if action
is not None:
466 self
._actiongroup
.remove_action(action
)
467 self
._uimanager
.remove_ui(self
._fignum
_to
_merge
_id
[fignum
])
468 self
._fignum
_to
_windows
[fignum
].destroy()
470 # Search algorithm from pygtk tutorial
471 def _match_func(self
, row
, data
):
473 return row
[column
] == key
475 def _search(self
, rows
, func
, data
):
476 if not rows
: return None
480 result
= self
._search
(row
.iterchildren(), func
, data
)
481 if result
: return result
484 def _freeze(self
, event
, signals
, data
=None):
485 for signal
in signals
.split(','):
486 match_row
= self
._search
(self
._store
, self
._match
_func
,\
488 if match_row
is not None:
489 match_row
[2] = match_row
[1].freeze
490 parent
= self
._store
.iter_parent(match_row
.iter)
491 iter = self
._store
.iter_children(parent
)
492 freeze
= match_row
[2]
494 if not self
._store
.get_value(iter, 2) == freeze
:
496 iter = self
._store
.iter_next(iter)
498 # All row at the same freeze value,
499 # set freeze for the reader
500 self
._store
.set_value(parent
, 2, freeze
)
502 # Set reader freeze to false
503 self
._store
.set_value(parent
, 2, False)
505 def _add_file(self
, event
, filename
, data
=None):
506 it
= self
._store
.append(None, (filename
.strip(), None, False))
507 for name
, sig
in self
._ctxt
.readers
[filename
.strip()]\
508 .signals
.iteritems():
509 self
._store
.append(it
, (name
, sig
, sig
.freeze
))
512 # Callbacks for drag and drop
514 def _drag_data_get_cb(self
, widget
, drag_context
, selection
, target_type
,\
516 if target_type
== self
._TARGET
_TYPE
_SIGNAL
:
518 sel
= tv
.get_selection()
519 (model
, pathlist
) = sel
.get_selected_rows()
520 iter = self
._store
.get_iter(pathlist
[0])
521 data
= " ".join(map(lambda x
:self
._store
[x
][1].name
, pathlist
))
522 selection
.set(selection
.target
, 8, data
)
523 # The multiple selection do work, but how to select signals
524 # that are not neighbours in the list? Ctrl+left do not do
525 # anything, neither alt+left or shift+left!
527 def _drag_data_received_cb(self
, widget
, drag_context
, x
, y
, selection
,\
529 if target_type
== self
._TARGET
_TYPE
_SIGNAL
:
530 if self
._current
_graph
is not None:
532 for name
in selection
.data
.split():
533 signals
[name
] = self
._ctxt
.signals
[name
]
534 self
._current
_graph
.insert(signals
)
535 if self
._current
_figure
.canvas
is not None:
536 self
._current
_figure
.canvas
.draw()
539 # Configuration-file related functions
541 def _init_config(self
):
542 # initialize configuration stuff
543 path
= BaseDirectory
.save_config_path('oscopy')
544 self
.config_file
= os
.path
.join(path
, 'gui')
545 self
.hist_file
= os
.path
.join(path
, 'history')
546 section
= App
.SECTION
547 self
.config
= ConfigParser
.RawConfigParser()
548 self
.config
.add_section(section
)
550 self
.config
.set(section
, App
.OPT_NETLISTER_COMMANDS
, '')
551 self
.config
.set(section
, App
.OPT_SIMULATOR_COMMANDS
, '')
552 self
.config
.set(section
, App
.OPT_RUN_DIRECTORY
, '.')
554 def _sanitize_list(self
, lst
):
555 return filter(lambda x
: len(x
) > 0, map(lambda x
: x
.strip(), lst
))
557 def _actions_from_config(self
, config
):
558 section
= App
.SECTION
559 netlister_commands
= config
.get(section
, App
.OPT_NETLISTER_COMMANDS
)
560 netlister_commands
= self
._sanitize
_list
(netlister_commands
.split(';'))
561 simulator_commands
= config
.get(section
, App
.OPT_SIMULATOR_COMMANDS
)
562 simulator_commands
= self
._sanitize
_list
(simulator_commands
.split(';'))
564 'run_netlister': (True, netlister_commands
),
565 'run_simulator': (True, simulator_commands
),
567 'run_from': config
.get(section
, App
.OPT_RUN_DIRECTORY
)}
570 def _actions_to_config(self
, actions
, config
):
571 section
= App
.SECTION
572 netlister_commands
= ';'.join(actions
['run_netlister'][1])
573 simulator_commands
= ';'.join(actions
['run_simulator'][1])
574 config
.set(section
, App
.OPT_NETLISTER_COMMANDS
, netlister_commands
)
575 config
.set(section
, App
.OPT_SIMULATOR_COMMANDS
, simulator_commands
)
576 config
.set(section
, App
.OPT_RUN_DIRECTORY
, actions
['run_from'])
578 def _read_config(self
):
579 self
.config
.read(self
.config_file
)
580 self
._actions
= self
._actions
_from
_config
(self
.config
)
582 def _write_config(self
):
583 self
._actions
_to
_config
(self
._actions
, self
.config
)
584 with
open(self
.config_file
, 'w') as f
:
588 @dbus.service
.method('org.freedesktop.OscopyIFace')
589 def dbus_update(self
):
590 gobject
.idle_add(self
._activate
_net
_and
_sim
)
592 @dbus.service
.method('org.freedesktop.OscopyIFace')
593 def dbus_running(self
):
597 def update_from_usr1(self
):
600 def update_from_usr2(self
):
601 gobject
.idle_add(self
._activate
_net
_and
_sim
)
603 def _activate_net_and_sim(self
):
604 if self
._actiongroup
is not None:
605 action
= self
._actiongroup
.get_action("Run netlister and simulate...")
608 def _run_ext_command(self
, cmd
, run_dir
):
609 old_dir
= os
.getcwd()
612 status
, output
= commands
.getstatusoutput(cmd
)
614 msg
= "Executing command '%s' failed." % cmd
615 report_error(self
._mainwindow
, msg
)
620 def _app_exec(self
, line
):
621 line
= self
._app
.precmd(line
)
622 stop
= self
._app
.onecmd(line
)
623 self
._app
.postcmd(stop
, line
)
625 def usr1_handler(signum
, frame
):
626 app
.update_from_usr1()
628 def usr2_handler(signum
, frame
):
629 app
.update_from_usr2()
631 if __name__
== '__main__':
632 session_bus
= dbus
.SessionBus()
633 bus_name
= dbus
.service
.BusName('org.freedesktop.Oscopy', bus
=session_bus
)
635 main_loop
= gobject
.MainLoop()
636 signal
.signal(signal
.SIGUSR1
, usr1_handler
)
637 signal
.signal(signal
.SIGUSR2
, usr2_handler
)