1 """
2 2d data display with oscilloscope-inspired controls.
3
4 Copyright 2008-2013 Research Foundation State University of New York
5 This file is part of QUB Express.
6
7 QUB Express is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
11
12 QUB Express is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License,
18 named LICENSE.txt, in the QUB Express program directory. If not, see
19 <http://www.gnu.org/licenses/>.
20
21 """
22
23 import cairo
24 import gc
25 import gtk
26 import gobject
27 import random
28 import traceback
29
30 import qubx.toolspace
31 from qubx.toolspace import ColorInfo
32 if qubx.toolspace.HAVE_OPENGL:
33 try:
34 import gtk.gtkgl
35 import gtk.gdkgl
36 import OpenGL.GL
37 import OpenGL.GLU
38 except:
39 pass
40
41 import qubx.data_types
42 import qubx.table
43
44 from gtk import gdk
45 from gtk import keysyms
46 from itertools import izip, count
47 from math import *
48 from qubx.util_types import *
49 from qubx.accept import *
50 from qubx.GTK import build_menuitem
51
52 SIGNAL_CENTER_HIDDEN = -1e9
53
54
55 TARGET_PIX_PER_DIV = 50.0
57 """Yields an increasing sequence of allowed pixels per div: 2, 4, 6, 10, 12, 14, 16, 20, 22, ..."""
58 base = 0
59 while True:
60 yield base + 2
61 yield base + 4
62 yield base + 6
63 base += 10
64 yield base
65
66 COLOR_BG = ('scope.bg', (0,0,0,1))
67 ColorInfo[COLOR_BG[0]].label = 'Data background'
69 GRID_ALPHA = 0.3
70 COLOR_MEAN_LINE = ('scope.mean.line', MEAN_COLOR(GRID_ALPHA))
71 ColorInfo[COLOR_MEAN_LINE[0]].label = 'Data center line'
72 COLOR_MEAN_VALUE = ('scope.mean.value', MEAN_COLOR(.8))
73 ColorInfo[COLOR_MEAN_VALUE[0]].label = 'Data center number'
74 COLOR_MEAN_HOVER = ('scope.mean.hover', MEAN_COLOR(1))
75 ColorInfo[COLOR_MEAN_HOVER[0]].label = 'Data center number mouseover'
76 COLOR_GRID = ('scope.grid', (0,1,0,GRID_ALPHA))
77 ColorInfo[COLOR_GRID[0]].label = 'Data grid lines'
78 COLOR_UPERD_VALUE = ('scope.uperd.value', (0,1,0,.8))
79 ColorInfo[COLOR_UPERD_VALUE[0]].label = 'Data units-per-division number'
80 COLOR_UPERD_HOVER = ('scope.uperd.hover', (0,1,0,1))
81 ColorInfo[COLOR_UPERD_HOVER[0]].label = 'Data units-per-division number mouseover'
82 COLOR_UNITS_VALUE = ('scope.units.value', (1, 1, 1, .8))
83 ColorInfo[COLOR_UNITS_VALUE[0]].label = 'Data units'
84 COLOR_UNITS_HOVER = ('scope.units.hover', (1, 1, 1, 1))
85 ColorInfo[COLOR_UNITS_HOVER[0]].label = 'Data units mouseover'
86 COLOR_GRAB = ('scope.grab', (.5,.9,1,.8))
87 ColorInfo[COLOR_GRAB[0]].label = 'Data auto-scale "?"'
88 COLOR_GRAB_HOVER = ('scope.grab.hover', (1, .7, .7, 1))
89 ColorInfo[COLOR_GRAB_HOVER[0]].label = 'Data auto-scale "?" mouseover'
90 COLOR_IDEALIZATION = ('scope.idealization', (.8, 0, 0, .8))
91 ColorInfo[COLOR_IDEALIZATION[0]].label = 'Data idealization'
92 COLOR_EXCLUDED = ('scope.excluded', (.5, .5, .5, .4))
93 ColorInfo[COLOR_EXCLUDED[0]].label = 'Data excluded'
94 COLOR_SIGNAL_POPUP = ('scope.signal.popup', (0, 1, 0, .8))
95 ColorInfo[COLOR_SIGNAL_POPUP[0]].label = 'Data signal menus'
96 COLOR_SIGNAL_BORDER = ('scope.signal.border', (1, 1, 1, 1))
97 ColorInfo[COLOR_SIGNAL_BORDER[0]].label = 'Data signal borders'
98 COLOR_SIGNAL_BG = ('scope.signal.bg', (.12, .12, .12, .85))
99 ColorInfo[COLOR_SIGNAL_BG[0]].label = 'Data signal background'
100 COLOR_SIGNAL_HOVER = ('scope.signal.hover', (.1, 1, .1, .9))
101 ColorInfo[COLOR_SIGNAL_HOVER[0]].label = 'Data signal name mouseover'
102 SIGNAL_BORDER = 2
103 COLORS_SIGNAL = {'Current' : ('dataGTK.signals.Current', (1,1,1,1))}
104
105
106
115
116
125
126 SIGNAL_WIDTH = 15
127 SIGNAL_HEIGHT = 3.5
128
129
131 """Returns the optimal number of divs to fill so many pixels."""
132 divv = allowed_divs()
133 divs = divv.next()
134 ppd = pix * 1.0 / divs
135 while True:
136 divs_next = divv.next()
137 ppd_next = pix * 1.0 / divs_next
138 if abs(ppd - target_ppd) < abs(ppd_next - target_ppd):
139 return divs
140 divs, ppd = divs_next, ppd_next
141
142
144 """Returns a function which accepts only n-tuples, in string form, and returns them as tuples."""
145 def accept(x):
146 try:
147 tup = tuple(eval(x))
148 if len(tup) != n:
149 raise Exception()
150 except:
151 raise FieldAcceptException(x, 'must be an %i-tuple'%n)
152 return tup
153 return accept
154
155
156
157
158
159 -class Scope(qubx.toolspace.ToolSpace):
160 """View for sampled data with transparent overlay controls, modeled after an oscilloscope.
161
162 @ivar divs: (x_divs, y_divs) number of divisions (green grid squares)
163 @ivar pix_per_div: (x_per_div, y_per_div)
164 @ivar signals: {qubx.table.Table} of signal Name, Units, Center, and per div
165 @ivar sig_layers: list of L{ScopeSignal}s for time and other signals
166 @ivar controls: L{qubx.toolspace.LayerSet} of all included layers
167 """
168
169 __explore_featured = ['divs', 'pix_per_div', 'signals', 'sig_layers', 'controls',
170 'global_name', 'signal_controls', 'layBlank', 'hide_hidden', 'draw_dim',
171 'line_count', 'onClickGrab', 'onClickUnGrab', 'x2u', 'u2x', 'y2u', 'u2y', 'copy_image',
172 'enable_grid', 'compute_divs']
173
174 - def __init__(self, global_name=None, signal_controls=True):
175
176 qubx.toolspace.ToolSpace.__init__(self, can_focus=True)
177 self.global_name = global_name
178 self.signal_controls = signal_controls
179 self.__ref = Reffer()
180 self.divs = (1, 1)
181 self.__line_count = 1
182 self.__copying = False
183 self.__hide_hidden = False
184 self.__draw_dim = (1, 1)
185 self.pix_per_div = (TARGET_PIX_PER_DIV, TARGET_PIX_PER_DIV)
186 self.sig_layers = []
187 self.layerset = self.controls = qubx.toolspace.LayerSet()
188 self.OnDraw += self.__ref(self.__onDraw)
189 self.OnDrawGL += self.__ref(self.__onDrawGL)
190 self.__signal_layout = qubx.pyenv.DeferredAction(delay_ms=5)
191 self.signals = qubx.table.SimpleTable('Scope')
192 self.signals.add_field('Name', '', acceptString, str, '')
193 self.signals.add_field('Units', '', acceptString, str, '')
194 self.signals.add_field('Center', 0.0, acceptFloat, '%.4g', '')
195 self.signals.add_field('per div', 1.0, acceptFloatNonzero, '%.4g', '')
196 self.signals.default['Group'] = 1
197 self.signals.OnInsert += self.__ref(self.__onInsertSignal)
198 self.signals.OnRemoved += self.__ref(self.__onRemovedSignal)
199 self.signals.OnSet += self.__ref(self.__onSetSignal)
200 self.signals.append({'Name' : 'Time', 'Units' : 's',
201 'Center' : 0.0, 'per div' : 1.0, 'Group' : 0})
202 self.layBlank = qubx.toolspace.Layer(y=6, h=4, w=0.01, cBG=qubx.toolspace.COLOR_CLEAR)
203 self.controls.add_layer(self.layBlank)
204 hide_hidden = property(lambda self: self.__hide_hidden, lambda self, x: self.set_hide_hidden(x))
206 if self.__hide_hidden == x: return
207 self.__hide_hidden = x
208 self.__signal_layout(self, self.__layout_signals)
209 draw_dim = property(lambda self: self.__draw_dim, lambda self, x: self.set_draw_dim(x))
212 line_count = property(lambda self: self.__line_count, lambda self, x: self.set_line_count(x))
214 self.__line_count = x
255 - def x2u(self, x, y, u_mid, u_per_d):
256 """Returns the time corresponding to pixel x."""
257
258
259
260
261 line = int(y / max(1, int(self.dim[1] / self.line_count)))
262 line_offset = (line - self.line_count/2) * u_per_d * self.divs[0]
263 if not (self.line_count % 2):
264 line_offset += u_per_d * self.divs[0] / 2
265 return u_mid + (u_per_d / self.pix_per_div[0]) * (x - self.dim[0]/2.0) + line_offset
266 - def u2x(self, u, u_mid, u_per_d):
267 """Returns the x pixel corresponding to time u."""
268 return ((self.dim[0]/2.0 * (self.line_count%2)) + (self.pix_per_div[0] / (u_per_d or 1.0)) * (u - u_mid)) % self.dim[0]
269 - def y2u(self, y, u_mid, u_per_d):
270 """Returns the value corresponding to pixel y."""
271 h = self.dim[1]
272 return h and (u_mid + (u_per_d / self.pix_per_div[1]) * (h/2.0 - (y % (h / self.line_count)))) or 0.0
273 - def u2y(self, u, line, u_mid, u_per_d):
274 """Returns the pixel y corresponding to value u."""
275 return self.dim[1]/2.0 - (self.pix_per_div[1] / u_per_d) * (u - u_mid) + (line and (line*(self.dim[1] / self.line_count)) or 0.0)
276 - def copy_image(self, w, h, overlays=True, clipboard_name='CLIPBOARD'):
277 """Copies a bitmap of the scope to the specified or default clipboard."""
278 pixmap = gdk.Pixmap(self.window, w, h, -1)
279 ctx = pixmap.cairo_create()
280 self.__copying = True
281 self.draw_to_context(ctx, w, h, overlay=overlays)
282 self.__copying = False
283 del ctx
284 pixbuf = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, w, h)
285 pixbuf.get_from_drawable(pixmap, gdk.colormap_get_system(), 0, 0, 0, 0, -1, -1)
286 clipboard = gtk.clipboard_get(clipboard_name)
287 clipboard.set_image(pixbuf)
288 del pixmap
289 del pixbuf
290 qubx.pyenv.env.gc_collect_on_idle()
291 self.redraw_canvas()
303 - def __onDrawGL(self, gldrawable, glcontext, w, h):
304 if not self.__copying:
305 self.divs = self.compute_divs(w, h)
306 self.pix_per_div = ((float(w) / self.divs[0]) or 1.0, (float(h) / self.divs[1]) or 1.0)
307 self.draw_dim = self._dim
308 OpenGL.GL.glColor4f(*self.appearance.color(COLOR_BG))
309 OpenGL.GL.glBegin(OpenGL.GL.GL_QUADS)
310 OpenGL.GL.glVertex3f(0, 0, 0)
311 OpenGL.GL.glVertex3f(w, 0, 0)
312 OpenGL.GL.glVertex3f(w, h, 0)
313 OpenGL.GL.glVertex3f(0, h, 0)
314 OpenGL.GL.glEnd()
316 if not self.__copying:
317 self.divs = self.compute_divs(w, h)
318 self.pix_per_div = ((float(w) / self.divs[0]) or 1.0, (float(h) / self.divs[1]) or 1.0)
319 self.draw_dim = self._dim
320 context.set_source_rgba(*self.appearance.color(COLOR_BG))
321 context.paint()
346 OpenGL.GL.glLineWidth(1.0)
347 for x in xrange(self.divs[0]):
348 if x == self.divs[0]/2:
349 OpenGL.GL.glColor4f(* self.appearance.color(COLOR_MEAN_LINE))
350 else:
351 OpenGL.GL.glColor4f(* self.appearance.color(COLOR_GRID))
352 px = int(round(x*self.pix_per_div[0] - 0.5)) + 0.5
353 OpenGL.GL.glBegin(OpenGL.GL.GL_LINES)
354 OpenGL.GL.glVertex3f(px, 0, 0)
355 OpenGL.GL.glVertex3f(px, h, 0)
356 OpenGL.GL.glEnd()
357 for y in xrange(self.divs[1]):
358 if y == self.divs[1]/2:
359 OpenGL.GL.glColor4f(* self.appearance.color(COLOR_MEAN_LINE))
360 else:
361 OpenGL.GL.glColor4f(* self.appearance.color(COLOR_GRID))
362 py = int(round(y*self.pix_per_div[1] - 0.5)) + 0.5
363 OpenGL.GL.glBegin(OpenGL.GL.GL_LINES)
364 OpenGL.GL.glVertex3f(0, py, 0)
365 OpenGL.GL.glVertex3f(w, py, 0)
366 OpenGL.GL.glEnd()
367
368
370 """Floating layer controlling the display of one signal (or time).
371
372 @ivar signals: the L{qubx.table.Table} of signals
373 @ivar index: which row of the table to control
374 @ivar OnClickGrab: L{WeakEvent}(ScopeSignal) when the '?' is clicked, so you can adjust Center and per div appropriately
375 @ivar OnClickUnGrab: L{WeakEvent}(ScopeSignal) when the 'x' is clicked, so you can hide the signal
376 """
377 - def __init__(self, signals, index, global_name, *args, **kw):
378 kw['w'] = SIGNAL_WIDTH
379 kw['h'] = SIGNAL_HEIGHT
380 kw['cBG'] = COLOR_SIGNAL_BG
381 kw['border'] = SIGNAL_BORDER
382 kw['cBorder'] = COLOR_SIGNAL_BORDER
383 qubx.toolspace.Layer.__init__(self, *args, **kw)
384 self.OnClickGrab = WeakEvent()
385 self.OnClickUnGrab = WeakEvent()
386 self.__ref = Reffer()
387 self.signals = signals
388 self.signals.OnSet += self.__ref(self.__onSetSignal)
389 self._index = index
390 self.global_name = global_name
391 self.defer_scriptable_scroll = qubx.pyenv.DeferredScriptableScroll()
392 ww = SIGNAL_WIDTH - 1
393 name = signals.get(index, 'Name')
394 self.subName = self.add_sublayer(qubx.toolspace.SubLayer_Label(name, 0, 0, x=0, y=0, w=int(round(.62*ww)), h=1.75,
395 color=COLOR_SIGNAL(name), hover_color=COLOR_SIGNAL_HOVER,
396 action=self.__ref(lambda x,y,e: self.signals.select(self._index, 'Name', self))))
397 self.subUnits = self.add_sublayer(qubx.toolspace.SubLayer_Label(self.format_units(signals.get(index, 'Units')), 0, 0, x=self.subName.rq_w, y=0,
398 w=int(round(.38*ww)), h=1.75,
399 color=COLOR_UNITS_VALUE, hover_color=COLOR_UNITS_HOVER,
400 action=self.__ref(lambda x,y,e: self.signals.select(self._index, 'Units', self))))
401 self.popup = gtk.Menu()
402 build_menuitem(index and 'Center signal' or 'Show entire segment', self.__ref(lambda it: self.OnClickGrab(self)), menu=self.popup)
403 if index:
404 build_menuitem('Hide signal', self.__ref(lambda it: self.OnClickUnGrab(self)), menu=self.popup)
405 build_menuitem('Change color...', self.__ref(lambda it: qubx.toolspace.EditOneColor(qubx.scope.COLOR_SIGNAL(self.subName.label), appearance=self.appearance)),
406 menu=self.popup)
407 self.subMenu = self.add_sublayer(qubx.toolspace.SubLayer_Popup(self.popup, COLOR_SIGNAL_POPUP, x=ww, y=.5, w=1, h=1))
408 self.subMean = self.add_sublayer(qubx.toolspace.SubLayer_Label(SCOPE_FORMAT(signals.get(index, 'Center')), 0, 0, x=0, y=1.75, w=ww/2, h=1.75,
409 color=COLOR_MEAN_VALUE, hover_color=COLOR_MEAN_HOVER,
410 action=self.__ref(lambda x,y,e: self.signals.select(self._index, 'Center', self)),
411 scroll=self.__ref(self._onScrollMean)))
412 self.subUperD = self.add_sublayer(qubx.toolspace.SubLayer_Label(SCOPE_FORMAT(signals.get(index, 'per div')), 0, 0, x=ww/2, y=1.75, w=ww/2, h=1.75,
413 color=COLOR_UPERD_VALUE, hover_color=COLOR_UPERD_HOVER,
414 action=self.__ref(lambda x,y,e: self.signals.select(self._index, 'per div', self)),
415 scroll=self.__ref(self._onScrollUperD)))
416 self.subGrab = self.add_sublayer(qubx.toolspace.SubLayer_Label('?', 0, 0, x=ww, y=1.75, w=1, h=1.75,
417 color=COLOR_GRAB, hover_color=COLOR_GRAB_HOVER,
418 action=self.__ref(self.__onClickGrab), mouse_move=self.__ref(self.__onMouseMoveGrab)))
419 self.subGrab.tooltip = 'Auto-scale'
424 index = property(lambda self: self._index, lambda self, x: self.set_index(x))
458 if (self._index > 0 ) and (e.state & gdk.CONTROL_MASK):
459 self.OnClickUnGrab(self)
460 else:
461 self.OnClickGrab(self)
463 if (self._index > 0 ) and (e.state & gdk.CONTROL_MASK):
464 if self.subGrab.label != 'x':
465 self.subGrab.label = 'x'
466 self.subGrab.tooltip = 'Hide'
467 else:
468 if self.subGrab.label != '?':
469 self.subGrab.label = '?'
470 self.subGrab.tooltip = 'Auto-scale'
471