ui: DND support for multiple signals selections
[oscopy/ivan.git] / oscopy_app
blob67bf16b65c7ae67b9b73b54eac0f41968423c532
1 #!/usr/bin/python
2 """
3 Sample command line application
4 """
5 import gobject
6 import gtk
8 import re
9 import readline
10 import os.path
11 import time
12 from optparse import OptionParser
13 import oscopy
14 from oscopy.readers.reader import ReadError
16 from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
17 from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
19 class OscopyApp(object):
20 """ Analyse command arguments and call function from Cmd
21 See Cmd for more help
22 """
23 def __init__(self):
24 self.cmds = oscopy.Context()
25 # Prompt
26 p = "oscopy> "
27 # History file
28 self.hist_file = ".oscopy_hist"
30 # Readline configuration
31 if not os.path.exists(self.hist_file):
32 f = open(self.hist_file, "w")
33 f.write("figlist")
34 f.close()
35 readline.read_history_file(self.hist_file)
37 # Parse command line arguments
38 # Current options:
39 # -b : batch mode, read commands from file
40 # -i : interactive mode, do not quit at the end of batch file
41 # -q : do not display startup message
42 parser = OptionParser()
43 parser.add_option("-b", "--batch", dest="fn",\
44 help="Execute commands in FILE then exit",\
45 metavar="FILE")
46 parser.add_option("-i", "--interactive", action="store_true",\
47 dest="inter",\
48 help="Go to interactive mode after executing batch file")
49 parser.add_option("-q", "--quiet", action="store_true",\
50 dest="quiet",\
51 help="Do not display startup message")
52 (options, args) = parser.parse_args()
53 if options.fn is None:
54 f = None
55 else:
56 try:
57 f = open(options.fn, 'r')
58 except IOError, e:
59 print "Unable to access batch file:", e
60 f = None
61 if options.inter == True:
62 batch = False
63 else:
64 batch = True
66 # Startup message
67 if options.quiet is None:
68 print "This is oscopy, a program to view electrical simulation results\n\
69 Copyright (C) 2009 Arnaud Gardelein.\n\
70 This is free software, you are invited to redistribute it \n\
71 under certain conditions.\n\
72 There is ABSOLUTELY NO WARRANTY; not even for MERCHANTIBILITY or\n\
73 FITNESS FOR A PARTICULAR PURPOSE."
75 # Current graph and current figure
76 self._current_figure = None
77 self._current_graph = None
78 self._figcount = 0
80 # Start main loop
81 self.loop(p, f, batch)
83 def create(self, args):
84 if args == "help":
85 print "Usage : create [SIG [, SIG [, SIG]...]]"
86 print " Create a new figure, set_ it as current, add the signals"
87 return
88 self.cmds.create(self.get_signames(args))
89 self._current_figure = self.cmds.figures[len(self.cmds.figures) - 1]
90 if self._current_figure.graphs:
91 self._current_graph =\
92 self._current_figure.graphs[len(\
93 self._current_figure.graphs) - 1]
94 else:
95 self._current_graph = None
97 def destroy(self, args):
98 if args == "help":
99 print "Usage : destroy FIG#"
100 print " Destroy a figure"
101 return
102 self.cmds.destroy(eval(args))
103 # Go back to the first graph of the first figure or None
104 if len(self.cmds.figures):
105 self._current_figure = self.cmds.figures[0]
106 if self._current_figure.graphs:
107 self._current_graph = self._current_figure.graphs[0]
108 else:
109 self._current_graph = None
110 else:
111 self._current_figure = None
112 self._current_graph = None
114 def select(self, args):
115 if args == "help":
116 print "Usage: select FIG#-GRAPH#"
117 print " Select the current figure and the current graph"
118 return
119 s = args.split('-')
120 num = eval(s[0])
121 if len(s) > 1:
122 gn = eval(s[1])
123 else:
124 print "Usage: select FIG#-GRAPH#"
125 return
126 # self.cmds.current = num, gn
127 self._current_figure = self.cmds.figures[num - 1]
128 self._current_graph = self._current_figure.graphs[gn - 1]
130 def layout(self, args):
131 if args == "help":
132 print "Usage : layout horiz|vert|quad"
133 print " Define the layout of the current figure"
134 return
135 if self._current_figure is not None:
136 self._current_figure.layout = args
138 def figlist(self, args):
139 if args == "help":
140 print "Usage : figlist"
141 print " Print the list of figures"
142 return
144 SEPARATOR = " "
145 for fn, f in enumerate(self.cmds.figures):
146 print "%s Figure %d: %s" %\
147 ([" ", "*"][f == self._current_figure],\
148 fn + 1, f.layout)
149 for gn, g in enumerate(f.graphs):
150 print " %s Graph %d : (%s) %s" %\
151 ([" ","*"][g == self._current_graph],\
152 gn + 1, g.type,\
153 SEPARATOR.join(g.signals.keys()))
155 def plot(self, args):
156 if args == "help":
157 print "Usage : plot"
158 print " Draw and show the figures"
159 return
160 if self._figcount == len(self.cmds.figures):
161 self._main_loop.run()
162 else:
163 # Create a window for each figure, add navigation toolbar
164 self._figcount = 0
165 for i, f in enumerate(self.cmds.figures):
166 # fig = plt.figure(i + 1)
167 w = gtk.Window()
168 self._figcount += 1
169 w.set_title('Figure %d' % self._figcount)
170 vbox = gtk.VBox()
171 w.add(vbox)
172 canvas = FigureCanvas(f)
173 canvas.connect('destroy', self._window_destroy)
174 vbox.pack_start(canvas)
175 toolbar = NavigationToolbar(canvas, w)
176 vbox.pack_start(toolbar, False, False)
177 w.resize(640, 480)
178 w.show_all()
179 self._main_loop = gobject.MainLoop()
180 self._main_loop.run()
182 def _window_destroy(self, arg):
183 self._figcount = self._figcount - 1
184 if not self._figcount:
185 self._main_loop.quit()
186 return False
188 def read(self, args):
189 if args == "help":
190 print "Usage : load DATAFILE"
191 print " Load signal file"
192 return
193 fn = args
194 try:
195 self.cmds.read(fn)
196 except ReadError, e:
197 print "Failed to read %s:" % fn, e
198 return
199 except NotImplementedError:
200 print "File format not supported"
201 return
203 def write(self, args):
204 if args == "help":
205 print "Usage: write format [(OPTIONS)] FILE SIG [, SIG [, SIG]...]"
206 print " Write signals to file"
207 return
208 # Extract format, options and signal list
209 tmp = re.search(r'(?P<fmt>\w+)\s*(?P<opts>\([^\)]*\))?\s+(?P<fn>[\w\./]+)\s+(?P<sigs>\w+(\s*,\s*\w+)*)', args)
211 if tmp is None:
212 print "What format ? Where ? Which signals ?"
213 return
214 fmt = tmp.group('fmt')
215 fn = tmp.group('fn')
216 opt = tmp.group('opts')
217 sns = self.get_signames(tmp.group('sigs'))
218 opts = {}
219 if opt is not None:
220 for on in opt.strip('()').split(','):
221 tmp = on.split(':', 1)
222 if len(tmp) == 2:
223 opts[tmp[0]] = tmp[1]
224 try:
225 self.cmds.write(fn, fmt, sns, opts)
226 except WriteError, e:
227 print "Write error:", e
228 return
229 except NotImplementedError:
230 print "File format not supported"
231 return
233 def update(self, args):
234 if args == "help":
235 print "Usage: update"
236 print " Reread data files"
237 return
238 if not args:
239 self.cmds.update()
240 else:
241 if self.cmds.readers.has_key(args):
242 self.cmds.update(self.cmds.readers[args])
243 else:
244 print "%s not found in readers" % args
246 def add(self, args):
247 if args == "help":
248 print "Usage : add SIG [, SIG [, SIG]...]"
249 print " Add a graph to the current figure"
250 return
251 if self._current_figure is not None:
252 self._current_figure.add(self.cmds.names_to_signals(\
253 self.get_signames(args)))
254 self._current_graph =\
255 self._current_figure.graphs[len(\
256 self._current_figure.graphs) - 1]
257 else:
258 self.cmds.create(self.get_signames(args))
259 if self.cmds.figures:
260 self._current_figure = self.cmds.figures[0]
261 else:
262 print "Create failed"
264 def delete(self, args):
265 if args == "help":
266 print "Usage : delete GRAPH#"
267 print " Delete a graph from the current figure"
268 return
269 if self._current_figure is not None:
270 self._current_figure.delete(args)
271 if self._current_figure.graphs:
272 self._current_graph = self._current_figure.graphs[0]
273 else:
274 self._current_graph = None
276 def mode(self, args):
277 if args == "help":
278 print "Usage: mode MODE"
279 print " Set the type of the current graph of the current figure"
280 print "Available modes :\n\
281 lin Linear graph\n"
282 # fft Fast Fourier Transform (FFT) of signals\n\
283 # ifft Inverse FFT of signals"
284 return
285 if self._current_graph is None:
286 return
287 idx = self._current_figure.graphs.index(self._current_graph)
288 self._current_figure.mode = self._current_graph, mode
289 self._current_graph = self._current_figure.graphs[idx]
291 def scale(self, args):
292 if args == "help":
293 print "Usage: scale [lin|logx|logy|loglog]"
294 print " Set the axis scale"
295 return
296 if self._current_graph is None:
297 return
298 self._current_graph.scale = args
300 def range(self, args):
301 if args == "help":
302 print "Usage: range [x|y min max]|[xmin xmax ymin ymax]|[reset]"
303 print " Set the axis range of the current graph of the current figure"
304 return
305 if self._current_graph is None:
306 return
308 range = args.split()
309 if len(range) == 1:
310 if range[0] == "reset":
311 self._current_graph.range = range[0]
312 elif len(range) == 3:
313 if range[0] == 'x' or range[0] == 'y':
314 self._current_graph.range = range[0],\
315 [float(range[1]), float(range[2])]
316 elif len(range) == 4:
317 self._current_graph.range = [float(range[0]), float(range[1]),\
318 float(range[2]), float(range[3])]
320 def unit(self, args):
321 if args == "help":
322 print "Usage: unit [XUNIT,] YUNIT"
323 print " Set the unit to be displayed on graph axis"
324 return
326 units = args.split(",", 1)
327 if len(units) < 1 or self._current_graph is None:
328 return
329 elif len(units) == 1:
330 self._current_graph.unit = units[0].strip(),
331 elif len(units) == 2:
332 self._current_graph.unit = units[0].strip(), units[1].strip()
333 else:
334 return
336 def insert(self, args):
337 if args == "help":
338 print "Usage: insert SIG [, SIG [, SIG]...]"
339 print " Insert a list of signals into the current graph"
340 return
341 if self._current_graph is None:
342 return
343 self._current_graph.insert(self.cmds.names_to_signals(\
344 self.get_signames(args)))
346 def remove(self, args):
347 if args == "help":
348 print "Usage: remove SIG [, SIG [, SIG]...]"
349 print " Delete a list of signals into from current graph"
350 return
351 if self._current_graph is None:
352 return
353 self._current_graph.remove(self.cmds.names_to_signals(\
354 self.get_signames(args)))
356 def freeze(self, args):
357 if args == "help":
358 print "Usage: freeze SIG [, SIG [, SIG]...]"
359 print " Do not consider signal for subsequent updates"
360 self.cmds.freeze(self.get_signames(args))
362 def unfreeze(self, args):
363 if args == "help":
364 print "Usage: unfreeze SIG [, SIG [, SIG]...]"
365 print " Consider signal for subsequent updates"
366 self.cmds.unfreeze(self.get_signames(args))
368 def siglist(self, args):
369 if args == "help":
370 print "Usage : siglist"
371 print " List loaded signals"
372 return
373 SEPARATOR = "\t"
374 HEADER=["Name", "Unit", "Ref", "Reader","Last updated (sec)"]
375 print SEPARATOR.join(HEADER)
376 t = time.time()
377 for reader_name, reader in self.cmds.readers.iteritems():
378 for signal_name, signal in reader.signals.iteritems():
379 print SEPARATOR.join((signal_name, \
380 signal.unit,\
381 signal.ref.name,\
382 reader_name,\
383 str(int(t - reader.info['last_update']))))
385 def math(self, inp):
386 if inp == "help":
387 print "Usage: destsig=mathexpr"
388 print " Define a new signal destsig using mathematical expression"
389 return
390 try:
391 self.cmds.math(inp)
392 except ReadError, e:
393 print "Error creating signal from math expression", e
394 return
396 def get_signames(self, args):
397 """ Return the signal names list extracted from the commandline
398 The list must be a coma separated list of signal names.
399 If no signals are loaded of no signal are found, return []
401 sns = []
402 if args == "":
403 sns = []
404 else:
405 for sn in args.split(","):
406 sns.append(sn.strip())
407 return sns
409 def help(self, args):
410 """ Display help messages
412 if args == "":
413 print "\
414 Commands related to figures:\n\
415 create create a new figure\n\
416 destroy delete a figure\n\
417 select define the current figure and the current graph\n\
418 layout set_ the layout (either horiz, vert or quad)\n\
419 figlist list the existing figures\n\
420 plot draw and show the figures\n\
421 Commands related to graphs:\n\
422 add add a graph to the current figure\n\
423 delete delete a graph from the current figure\n\
424 mode set_ the mode of the current graph of the current figure\n\
425 unit set_ the units of the current graph of the current figure\n\
426 scale set_ the scale of the current graph of the current figure\n\
427 range set_ the axis range of the current graph of the current figure\n\
428 Commands related to signals:\n\
429 read read signals from file\n\
430 write write signals to file\n\
431 update reread signals from file(s)\n\
432 insert add a signal to the current graph of the current figure\n\
433 remove delete a signal from the current graph of the current figure\n\
434 (un)freeze toggle signal update\n\
435 siglist list the signals\n\
436 Misc commands:\n\
437 echo print a message\n\
438 pause wait for the user to press enter\n\
439 quit, exit exit the program\n\
440 help display this help message\n\
442 During plot:\n\
443 1, 2 Toggle first and second vertical cursors\n\
444 3, 4 Toggle first and second horizontal cursors\n\
445 Maths:\n\
446 SIG = EXPR Compute a signal from mathematical expression\n\
447 SIG = [i]fft(SIG)\n\
448 Compute Fast Fourier Transform (FFT) or inverse FFT from SIG\n\
450 Help for individual command can be obtained with 'help COMMAND'\
452 else:
453 if args in dir(cmds):
454 eval("self." + args + "(\"help\")")
455 else:
456 print "Unknown command", args
458 def echo(self, args):
459 if args == "help":
460 print "Usage: echo [TEXT]"
461 print " Print text"
462 return
463 print args
465 def pause(self, args):
466 if args == "help":
467 print "Usage: pause"
468 print " Wait for the user to press enter"
469 return
470 inp = raw_input("Press enter")
472 def loop(self, p, f = None, batch = False):
473 # Main loop
474 while True:
475 try:
476 if f is None:
477 inp = raw_input(p)
478 else:
479 try:
480 inp = f.readline().rstrip("\n")
481 if inp == "":
482 if batch == True:
483 # Batch mode, exit at the end of script
484 break
485 else:
486 # Interactive mode, continue with command line
487 f = None
488 continue
489 except IOError, e:
490 print "Script error:", e
491 f.close()
492 f = None
493 # Check if line is a comment
494 if inp.lstrip().startswith("#"):
495 continue
496 # Check if command is assignment
497 if inp.find("=") >= 0:
498 self.math(inp)
499 continue
501 # Separate command from args
502 if inp.find(" ") >= 0:
503 st = inp.lstrip().split(' ', 1)
504 cmd = st[0]
505 args = st[1]
506 # print "cmd:", cmd, "args:", args
507 else:
508 cmd = inp
509 args = ""
511 # End of program
512 if cmd == "exit" or cmd == "quit":
513 break
515 # Evaluate the command
516 if cmd in dir(self):
517 eval("self." + cmd + "(args)")
518 else:
519 print cmd, "not supported"
520 continue
522 except EOFError:
523 break
525 # except AttributeError, e:
526 # print "Unknown command:", e.message
527 # continue
529 # except NameError, e:
530 # print "Unknown command", e.message
531 # continue
533 except SyntaxError, e:
534 print "Syntax Error", e.message
535 continue
537 except ReadError, e:
538 print "Error in read :", e
540 readline.write_history_file(self.hist_file)
542 if __name__ == "__main__":
543 o = OscopyApp()