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
19 from matplotlib
.backends
.backend_gtkagg
import FigureCanvasGTKAgg
as FigureCanvas
20 from matplotlib
.backends
.backend_gtkagg
import NavigationToolbar2GTKAgg
as NavigationToolbar
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())
33 class App(dbus
.service
.Object
):
35 <menubar name="MenuBar">
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"/>
44 <menu action="Windows">
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>"
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
)]
70 self
._ctxt
= oscopy
.Context()
74 self
._store
= gtk
.TreeStore(gobject
.TYPE_STRING
, gobject
.TYPE_PYOBJECT
,
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__
85 OPT_NETLISTER_COMMANDS
= 'netlister_commands'
86 OPT_SIMULATOR_COMMANDS
= 'simulator_commands'
87 OPT_RUN_DIRECTORY
= 'run_directory'
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)
98 if resp
== gtk
.RESPONSE_ACCEPT
:
99 for filename
in dlg
.get_filenames():
100 self
._app
_exec
('oread ' + filename
)
103 def _action_update(self
, action
):
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
))
113 label
= gtk
.Label(_('Expression:'))
114 hbox
.pack_start(label
)
116 hbox
.pack_start(entry
)
117 dlg
.vbox
.pack_start(hbox
)
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())
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
))
132 filename
= dlg
.get_filename()
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
)
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
):
148 if actions
['run_simulator'][0]:
149 if not self
._run
_ext
_command
(actions
['run_simulator'][1][0], run_dir
):
151 if actions
['update']:
154 def _action_quit(self
, action
):
156 readline
.write_history_file(self
.hist_file
)
160 def _action_figure(self
, action
, w
, fignum
):
161 if not (w
.flags() & gtk
.VISIBLE
):
165 self
._app
_exec
('%%oselect %d-1' % fignum
)
168 # UI Creation functions
170 def _create_menubar(self
):
172 # (name, stock-id, label, accelerator, tooltip, callback)
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,
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)
203 col
.set_cell_data_func(celltext
, self
._reader
_name
_in
_bold
)
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
,\
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
)
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) +\
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
)
237 vbox
.pack_start(self
._menubar
, False)
240 w
= self
._mainwindow
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
241 w
.set_title(_('Oscopy GUI'))
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)
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
)
254 self
._mainwindow
.show()
257 # Event-triggered functions
259 def _treeview_button_press(self
, widget
, event
):
260 if event
.button
== 3:
262 path
, tvc
, x
, y
= tv
.get_path_at_pos(int(event
.x
), int(event
.y
))
266 row
= self
._store
[path
]
267 signals
= {row
[0]: row
[1]}
268 menu
= self
._create
_treeview
_popup
_menu
(signals
, path
)
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
)
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
):
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
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
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
313 def _cell_toggled(self
, cellrenderer
, path
, data
):
316 if self
._store
[path
][1].freeze
:
320 self
._app
_exec
('%s %s' % (cmd
, self
._store
[path
][0]))
323 parent
= self
._store
.get_iter(path
)
324 freeze
= not self
._store
.get_value(parent
, 2)
325 if self
._store
[path
][2]:
329 self
._store
.set_value(parent
, 2, freeze
)
330 iter = self
._store
.iter_children(parent
)
332 self
._app
_exec
('%s %s' % (cmd
, self
._store
.get_value(iter, 0)))
333 iter = self
._store
.iter_next(iter)
339 fig
= self
._ctxt
.figures
[len(self
._ctxt
.figures
) - 1]
340 fignum
= len(self
._ctxt
.figures
)
343 self
._windows
_to
_figures
[w
] = fig
344 self
._fignum
_to
_windows
[fignum
] = w
345 w
.set_title(_('Figure %d') % fignum
)
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)
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
))
371 <menubar name=\"MenuBar\">\
372 <menu action=\"Windows\">\
373 <menuitem action=\"Figure %d\"/>\
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
):
386 action
= self
._uimanager
.get_action('/MenuBar/Windows/Figure %d' %
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
):
396 return row
[column
] == key
398 def _search(self
, rows
, func
, data
):
399 if not rows
: return None
403 result
= self
._search
(row
.iterchildren(), func
, data
)
404 if result
: return result
407 def freeze(self
, signals
):
408 for signal
in signals
.split(','):
409 match_row
= self
._search
(self
._store
, self
._match
_func
,\
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]
417 if not self
._store
.get_value(iter, 2) == freeze
:
419 iter = self
._store
.iter_next(iter)
421 # All row at the same freeze value,
422 # set freeze for the reader
423 self
._store
.set_value(parent
, 2, freeze
)
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
,\
440 if target_type
== self
._TARGET
_TYPE
_SIGNAL
:
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
,\
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
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
)
464 for name
in selection
.data
.split():
465 signals
[name
] = self
._ctxt
.signals
[name
]
466 if event
.inaxes
is not None:
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
)
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(';'))
497 'run_netlister': (True, netlister_commands
),
498 'run_simulator': (True, simulator_commands
),
500 'run_from': config
.get(section
, App
.OPT_RUN_DIRECTORY
)}
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
:
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
):
530 def update_from_usr1(self
):
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...")
541 def _run_ext_command(self
, cmd
, run_dir
):
542 old_dir
= os
.getcwd()
545 status
, output
= commands
.getstatusoutput(cmd
)
547 msg
= _("Executing command '%s' failed.") % cmd
548 report_error(self
._mainwindow
, msg
)
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()