Mercurial > code > home > repos > light9
view light9/curvecalc/curveview.py @ 711:e94989da079c
more curve editor bindings. delete signal isn't done
Ignore-this: e81c81134cd87f3633821841c4ec8dac
author | drewp@bigasterisk.com |
---|---|
date | Sun, 10 Jun 2012 21:05:57 +0000 |
parents | 10ee0756a119 |
children | d3dd982c3e32 |
line wrap: on
line source
from __future__ import division import math, time, logging import gtk, goocanvas import louie as dispatcher from light9.curvecalc.zoomcontrol import RegionZoom from light9.curvecalc import cursors from light9.curvecalc.curve import introPad, postPad from light9.dmxchanedit import gradient log = logging.getLogger() print "curveview.py toplevel" def vlen(v): return math.sqrt(v[0]*v[0] + v[1]*v[1]) def angle_between(base, p0, p1): p0 = p0[0] - base[0], p0[1] - base[1] p1 = p1[0] - base[0], p1[1] - base[1] p0 = [x/vlen(p0) for x in p0] p1 = [x/vlen(p1) for x in p1] dot = p0[0]*p1[0]+p0[1]*p1[1] dot = max(-1,min(1,dot)) return math.degrees(math.acos(dot)) class Sketch: """a sketch motion on a curveview, with temporary points while you draw, and simplification when you release""" def __init__(self,curveview,ev): self.curveview = curveview self.pts = [] self.last_x = None def motion(self,ev): p = self.curveview.world_from_screen(ev.x, ev.y) p = p[0], max(0,min(1,p[1])) if self.last_x is not None and abs(ev.x - self.last_x) < 4: return self.last_x = ev.x self.pts.append(p) self.curveview.add_point(p) def release(self,ev): pts = self.pts pts.sort() dx = .01 to_remove = [] for i in range(1,len(pts)-1): x = pts[i][0] p_left = (x - dx, self.curveview.curve(x - dx)) p_right = (x + dx, self.curveview.curve(x + dx)) if angle_between(pts[i], p_left, p_right) > 160: to_remove.append(i) for i in to_remove: self.curveview.curve.points.remove(pts[i]) # the simplified curve may now be too far away from some of # the points, so we'll put them back. this has an unfortunate # bias toward reinserting the earlier points for i in to_remove: p = pts[i] if abs(self.curveview.curve(p[0]) - p[1]) > .1: self.curveview.add_point(p) self.curveview.update_curve() class Curveview(object): """ graphical curve widget only. Please pack .widget """ def __init__(self, curve, knobEnabled=False, isMusic=False, zoomControl=None, **kw): """knobEnabled=True highlights the previous key and ties it to a hardware knob""" self.widget = goocanvas.Canvas() self.widget.set_property("background-color", "black") self.widget.set_size_request(-1, 100) self.size = self.widget.get_allocation() self.root = self.widget.get_root_item() self.redrawsEnabled = False self.curve = curve self.knobEnabled = knobEnabled self._isMusic = isMusic self.zoomControl = zoomControl self._time = 0 self.last_mouse_world = None self.entered = False # is the mouse currently over this widget self.selected_points=[] # idx of points being dragged # self.bind("<Enter>",self.focus) dispatcher.connect(self.playPause, "onPlayPause") dispatcher.connect(self.input_time, "input time") dispatcher.connect(self.update_curve, "zoom changed") dispatcher.connect(self.update_curve, "points changed", sender=self.curve) dispatcher.connect(self.update_curve, "mute changed", sender=self.curve) dispatcher.connect(self.select_between, "select between") if self.knobEnabled: dispatcher.connect(self.knob_in, "knob in") dispatcher.connect(self.slider_in, "set key") self.widget.connect("size-allocate", self.update_curve) self.widget.connect("leave-notify-event", self.onLeave) self.widget.connect("enter-notify-event", self.onEnter) self.widget.connect("motion-notify-event", self.onMotion) self.widget.connect("scroll-event", self.onScroll) self.widget.connect("button-release-event", self.onRelease) self.root.connect("button-press-event", self.onCanvasPress) # todo: hold control to get a [+] cursor # def curs(ev): # print ev.state # self.bind("<KeyPress>",curs) # self.bind("<KeyRelease-Control_L>",lambda ev: curs(0)) if 0: for x in range(1, 6): def add_kb_marker_point(evt, x=x): self.add_point((self.current_time(), (x - 1) / 4.0)) self.bind("<Key-%s>" % x, add_kb_marker_point) # this binds on c-a-b1, etc if 0: self.regionzoom = RegionZoom(self, self.world_from_screen, self.screen_from_world) self.sketch = None # an in-progress sketch self.dragging_dots = False self.selecting = False if 0: self.bind("<ButtonPress-1>",#"<Alt-Key>", self.select_press, add=True) self.bind("<Motion>", self.select_motion, add=True) self.bind("<ButtonRelease-1>", #"<Alt-KeyRelease>", self.select_release, add=True) self.bind("<ButtonPress-1>", self.check_deselect, add=True) self.bind("<Key-m>", lambda *args: self.curve.toggleMute()) self.bind("<Key-c>", lambda *args: dispatcher.send('toggle collapse', sender=self.curve)) def onDelete(self): if self.selected_points: self.remove_point_idx(*self.selected_points) def onCanvasPress(self, item, target_item, event): # when we support multiple curves per canvas, this should find # the close one and add a point to that. Binding to the line # itself is probably too hard to hit. Maybe a background-color # really thick line would be a nice way to allow a sloppier # click if event.get_state() & gtk.gdk.CONTROL_MASK: self.new_point_at_mouse(event) elif event.get_state() & gtk.gdk.SHIFT_MASK: self.sketch_press(event) else: self.select_press(event) def playPause(self): """ user has pressed ctrl-p over a curve view, possibly this one. Returns the time under the mouse if we know it, or else None todo: there should be a faint timecursor line under the mouse so it's more obvious that we use that time for some events. Rt-click should include Ctrl+P as 'play/pause from here' """ # maybe self.widget.get_pointer would be ok for this? i didn't try it if self.entered: t = self.world_from_screen(self.lastMouseX, 0)[0] return t return None def goLive(self): """this is for startup performance only, since the curves were getting redrawn many times. """ self.redrawsEnabled = True self.update_curve() def knob_in(self, curve, value): """user turned a hardware knob, which edits the point to the left of the current time""" if curve != self.curve: return idx = self.curve.index_before(self.current_time()) if idx is not None: pos = self.curve.points[idx] self.curve.points[idx] = (pos[0], value) self.update_curve() def slider_in(self, curve, value=None): """user pushed on a slider. make a new key. if value is None, the value will be the same as the last.""" if curve != self.curve: return if value is None: value = self.curve.eval(self.current_time()) self.curve.insert_pt((self.current_time(), value)) self.update_curve() def print_state(self, msg=""): if 1: print "%s: dragging_dots=%s selecting=%s" % ( msg, self.dragging_dots, self.selecting) def check_deselect(self,ev): try: self.find_index_near(ev.x, ev.y) except ValueError: self.selected_points[:] = [] self.highlight_selected_dots() def select_press(self,ev): # todo: these select_ handlers are getting called on c-a-drag # zooms too. the dispatching should be more selective than # just calling both handlers all the time self.print_state("select_press") if self.dragging_dots: return if not self.selecting: self.selecting = True self.select_start = self.world_from_screen(ev.x,0)[0] #cursors.push(self,"gumby") def select_motion(self,ev): if not self.selecting: return start = self.select_start cur = self.world_from_screen(ev.x, 0)[0] self.select_between(start, cur) def select_release(self,ev): self.print_state("select_release") # dotrelease never gets called, but I can clear that state here self.dragging_dots = False if not self.selecting: return #cursors.pop(self) self.selecting = False self.select_between(self.select_start, self.world_from_screen(ev.x,0)[0]) def sketch_press(self,ev): self.sketch = Sketch(self,ev) def sketch_motion(self,ev): if self.sketch: self.sketch.motion(ev) def sketch_release(self,ev): if self.sketch: self.sketch.release(ev) self.sketch = None def current_time(self): return self._time def screen_from_world(self,p): z = self.zoomControl ht = self.size.height return (p[0]-z.start)/(z.end-z.start)*self.size.width, (ht-5)-p[1]*(ht-10) def world_from_screen(self,x,y): z = self.zoomControl ht = self.size.height return x/self.size.width*(z.end-z.start)+z.start, ((ht-5)-y)/(ht-10) def input_time(self, val, forceUpdate=False): # i tried various things to make this not update like crazy, # but the timeline was always missing at startup, so i got # scared that things were getting built in a funny order. #if self._time == val: # return t=val if not getattr(self, 'timelineLine', None): self.timelineGroup = goocanvas.Group(parent=self.root) self.timelineLine = goocanvas.Polyline( parent=self.timelineGroup, points=goocanvas.Points([(0,0), (0,0)]), line_width=2, stroke_color='red') self.timelineLine.set_property('points', goocanvas.Points([ self.screen_from_world((val,0)), self.screen_from_world((val,1))])) self._time = t if self.knobEnabled: self.delete('knob') prevKey = self.curve.point_before(t) if prevKey is not None: pos = self.screen_from_world(prevKey) self.create_oval(pos[0] - 8, pos[1] - 8, pos[0] + 8, pos[1] + 8, outline='#800000', tags=('knob',)) dispatcher.send("knob out", value=prevKey[1], curve=self.curve) def update_curve(self, _widget=None, _rect=None): if not self.redrawsEnabled: return self.size = self.widget.get_allocation() cp = self.curve.points visible_x = (self.world_from_screen(0,0)[0], self.world_from_screen(self.size.width, 0)[0]) visible_idxs = self.curve.indices_between(visible_x[0], visible_x[1], beyond=1) visible_points = [cp[i] for i in visible_idxs] if getattr(self, 'curveGroup', None): self.curveGroup.remove() self.curveGroup = goocanvas.Group(parent=self.root) # this makes gtk quietly stop working. Getting called too early? #self.widget.set_property("background-color", # "gray20" if self.curve.muted else "black") if self.size.height < .40: self._draw_gradient() else: self._draw_markers(visible_x) self._draw_line(visible_points) self.dots = {} # idx : canvas rectangle if len(visible_points)<50: self._draw_handle_points(visible_idxs,visible_points) def is_music(self): """are we one of the music curves (which might be drawn a bit differently)""" return self._isMusic def _draw_gradient(self): print "no grad" return t1 = time.time() gradient_res = 6 if self.is_music() else 3 startX = startColor = None rects = 0 for x in range(0, self.size.width, gradient_res): wx = self.world_from_screen(x,0)[0] mag = self.curve.eval(wx, allow_muting=False) if self.curve.muted: low = (8, 8, 8) high = (60, 60, 60) else: low = (20, 10, 50) high = (255, 187, 255) color = gradient(mag, low=low, high=high) if color != startColor: if startColor is not None: self._draw_gradient_slice(startX, x, startColor) rects += 1 startX = x startColor = color if startColor is not None: self._draw_gradient_slice(startX, self.size.width, startColor) rects += 1 log.debug("redraw %s rects in %.02f ms", rects, 1000 * (time.time()-t1)) def _draw_gradient_slice(self, x1, x2, color): self.create_rectangle(x1, 0, x2, 40, fill=color, width=0, tags='curve') def _draw_markers(self,visible_x): mark = self._draw_one_marker mark(0, "0") t1,t2=visible_x if t2-t1<30: for t in range(int(t1),int(t2)+1): mark(t,str(t)) mark(introPad, str(introPad)) endtimes = dispatcher.send("get max time") if endtimes: endtime = endtimes[0][1] mark(endtime, "end %.1f"%endtime) mark(endtime - postPad, "post %.1f" % (endtime - postPad)) def _draw_one_marker(self,t,label): x = self.screen_from_world((t,0))[0] ht = self.size.height if not 0 <= x < self.size.width: return x = max(5, x) # cheat left-edge stuff onscreen goocanvas.polyline_new_line(self.curveGroup, x, ht, x, ht - 20, line_width=.5, stroke_color='gray70') goocanvas.Text(parent=self.curveGroup, fill_color="white", anchor=gtk.ANCHOR_SOUTH, font="ubuntu 7", x=x+3, y=ht-20, text=label) def _draw_line(self,visible_points): linepts=[] step=1 linewidth = 1.5 maxPointsToDraw = self.size.width / 2 if len(visible_points) > maxPointsToDraw: step = int(len(visible_points) / maxPointsToDraw) linewidth = .8 for p in visible_points[::step]: x,y = self.screen_from_world(p) linepts.append((int(x) + .5, y)) if self.curve.muted: fill = 'grey34' else: fill = 'white' self.pl = goocanvas.Polyline(parent=self.curveGroup, points=goocanvas.Points(linepts), line_width=linewidth, stroke_color=fill, ) def _draw_handle_points(self,visible_idxs,visible_points): for i,p in zip(visible_idxs,visible_points): rad=3 worldp = p p = self.screen_from_world(p) dot = goocanvas.Rect(parent=self.curveGroup, x=int(p[0] - rad) + .5, y=int(p[1] - rad) + .5, width=rad * 2, height=rad * 2, stroke_color='gray90', fill_color='blue', line_width=1, #tags=('curve','point', 'handle%d' % i) ) if worldp[1] == 0: rad += 3 goocanvas.Ellipse(parent=self.curveGroup, center_x=p[0], center_y=p[1], radius_x=rad, radius_y=rad, line_width=2, stroke_color='#00a000', #tags=('curve','point', 'handle%d' % i) ) dot.connect("button-press-event", self.dotpress, i) #self.tag_bind('handle%d' % i,"<ButtonPress-1>", # lambda ev,i=i: self.dotpress(ev,i)) #self.tag_bind('handle%d' % i, "<Key-d>", # lambda ev, i=i: self.remove_point_idx(i)) self.dots[i]=dot self.highlight_selected_dots() def find_index_near(self,x,y): tags = self.gettags(self.find_closest(x, y)) try: handletags = [t for t in tags if t.startswith('handle')] return int(handletags[0][6:]) except IndexError: raise ValueError("no point found") def new_point_at_mouse(self, ev): p = self.world_from_screen(ev.x,ev.y) x, y = p y = max(0, y) y = min(1, y) p = x, y self.add_point(p) def add_point(self, p): self.unselect() self.curve.insert_pt(p) self.update_curve() def remove_point_idx(self, *idxs): idxs = list(idxs) while idxs: i = idxs.pop() self.curve.points.pop(i) newsel = [] newidxs = [] for si in range(len(self.selected_points)): sp = self.selected_points[si] if sp == i: continue if sp > i: sp -= 1 newsel.append(sp) for ii in range(len(idxs)): if ii > i: ii -= 1 newidxs.append(idxs[ii]) self.selected_points[:] = newsel idxs[:] = newidxs self.update_curve() def highlight_selected_dots(self): for i,d in self.dots.items(): if i in self.selected_points: d.set_property('fill_color', 'red') else: d.set_property('fill_color', 'blue') def dotpress(self, r1, r2, ev, dotidx): self.print_state("dotpress") if dotidx not in self.selected_points: self.selected_points=[dotidx] self.highlight_selected_dots() self.last_mouse_world = self.world_from_screen(ev.x, ev.y) self.dragging_dots = True def select_between(self,start,end): if start > end: start, end = end, start self.selected_points = self.curve.indices_between(start,end) self.highlight_selected_dots() def onEnter(self, widget, event): self.entered = True def onLeave(self, widget, event): self.entered = False def onMotion(self, widget, event): self.lastMouseX = event.x if event.state & gtk.gdk.SHIFT_MASK and 1: # and B1 self.sketch_motion(event) return self.select_motion(event) if not self.dragging_dots: return if not event.state & 256: return # not lmb-down cp = self.curve.points moved=0 cur = self.world_from_screen(event.x, event.y) if self.last_mouse_world: delta = (cur[0] - self.last_mouse_world[0], cur[1] - self.last_mouse_world[1]) else: delta = 0,0 self.last_mouse_world = cur for idx in self.selected_points: newp = [cp[idx][0] + delta[0], cp[idx][1] + delta[1]] newp[1] = max(0,min(1,newp[1])) if idx>0 and newp[0] <= cp[idx-1][0]: continue if idx<len(cp)-1 and newp[0] >= cp[idx+1][0]: continue moved=1 cp[idx] = tuple(newp) if moved: self.update_curve() def unselect(self): self.selected_points=[] self.highlight_selected_dots() def onScroll(self, widget, event): t = self.world_from_screen(event.x, 0)[0] self.zoomControl.zoom_about_mouse( t, factor=1.5 if event.direction == gtk.gdk.SCROLL_DOWN else 1/1.5) def onRelease(self, widget, event): self.print_state("dotrelease") if event.state & gtk.gdk.SHIFT_MASK: # relese-B1 self.sketch_release(event) return self.select_release(event) if not self.dragging_dots: return self.last_mouse_world = None self.dragging_dots = False class CurveRow(object): """ one of the repeating curve rows (including widgets on the left) please pack self.box """ def __init__(self, name, curve, slider, knobEnabled, zoomControl): self.name = name self.box = gtk.HandleBox() self.box.set_border_width(1) cols = gtk.HBox() self.box.add(cols) controls = gtk.Frame() controls.set_size_request(115, -1) controls.set_shadow_type(gtk.SHADOW_OUT) cols.pack_start(controls, expand=False) self.setupControls(controls, name, curve, slider) self.curveView = Curveview(curve, knobEnabled=knobEnabled, isMusic=name in ['music', 'smooth_music'], zoomControl=zoomControl) cols.pack_start(self.curveView.widget, expand=True) def setupControls(self, controls, name, curve, slider): box = gtk.VBox() controls.add(box) txt = "curve '%s'" % name if len(name) > 7: txt = name curve_name_label = gtk.Label(txt) box.pack_start(curve_name_label) # self.collapsed = tk.IntVar() # self.muted = tk.IntVar() bools = gtk.HBox() box.pack_start(bools) collapsed_cb = gtk.CheckButton("C") bools.pack_start(collapsed_cb) #self.collapsed.trace('w', self.update_ui_to_collapsed_state) dispatcher.connect(self.toggleCollapsed, "toggle collapse", sender=curve) muted_cb = gtk.CheckButton("M") bools.pack_start(muted_cb) #self.muted.trace('w', self.sync_mute_to_curve) dispatcher.connect(self.mute_changed, 'mute changed', sender=curve) self.sliderLabel = None if slider is not None: # slider should have a checkbutton, defaults to off for # music tracks self.sliderLabel = gtk.Label("Slider %s" % slider) box.pack_start(self.sliderLabel) # widgets that need recoloring when we tint the row: #self.widgets = [leftside, collapsed_cb, muted_cb, # curve_name_label, self] #if self.sliderLabel: # self.widgets.append(self.sliderLabel) def onDelete(self): self.curveView.onDelete() def toggleCollapsed(self): self.collapsed.set(not self.collapsed.get()) def update_ui_to_collapsed_state(self, *args): if self.collapsed.get(): if self.sliderLabel: self.sliderLabel.pack_forget() self.curveView.config(height=25) else: if self.sliderLabel: self.sliderLabel.pack(side='left') self.curveView.config(height=100) def sync_mute_to_curve(self, *args): """send value from Tk var to the master attribute inside Curve""" new_mute = self.muted.get() old_mute = self.curveView.curve.muted if new_mute == old_mute: return self.curveView.curve.muted = new_mute def update_mute_look(self): """set colors on the widgets in the row according to self.muted.get()""" if self.muted.get(): new_bg = 'grey20' else: new_bg = self.default_bg for widget in self.widgets: widget['bg'] = new_bg def mute_changed(self): """call this if curve.muted changed""" self.muted.set(self.curveView.curve.muted) self.update_mute_look() class Curvesetview(object): """ """ def __init__(self, curvesVBox, zoomControlBox, curveset): self.curvesVBox = curvesVBox self.curveset = curveset self.allCurveRows = set() import light9.curvecalc.zoomcontrol reload(light9.curvecalc.zoomcontrol) self.zoomControl = light9.curvecalc.zoomcontrol.ZoomControl() zoomControlBox.add(self.zoomControl.widget) self.zoomControl.widget.show_all() for c in curveset.curveNamesInOrder(): self.add_curve(c) dispatcher.connect(self.add_curve, "add_curve", sender=self.curveset) self.newcurvename = gtk.EntryBuffer("", 0) def focus_entry(self): self.entry.focus() def new_curve(self, event): self.curveset.new_curve(self.newcurvename.get()) self.newcurvename.set('') def add_curve(self, name, slider=None, knobEnabled=False): curve = self.curveset.curves[name] f = CurveRow(name, curve, slider, knobEnabled, self.zoomControl) self.curvesVBox.pack_end(f.box) f.box.show_all() self.allCurveRows.add(f) f.curveView.goLive() def row(self, name): return [r for r in self.allCurveRows if r.name == name][0] def goLive(self): """for startup performance, none of the curves redraw themselves until this is called once (and then they're normal)""" for cr in self.allCurveRows: cr.curveView.goLive() def onDelete(self): for r in self.allCurveRows: r.onDelete()