diff --git a/bin/curvecalc b/bin/curvecalc --- a/bin/curvecalc +++ b/bin/curvecalc @@ -17,7 +17,7 @@ try: except ImportError: import louie as dispatcher from twisted.internet import reactor,tksupport -import jsonlib, restkit +import json, restkit import twisted from twisted.web.client import Agent from twisted.internet.protocol import Protocol @@ -35,7 +35,8 @@ import run_local from light9 import Submaster, dmxclient, networking, showconfig, prof, Patch from light9.TLUtility import make_attributes_from_args from light9.zoomcontrol import Zoomcontrol -from light9.curve import Curveset, Curvesetview +from light9.curve import Curveset +from light9.curveview import Curvesetview from light9.wavelength import wavelength from light9.uihelpers import toplevelat from light9.namespaces import L9 @@ -52,7 +53,7 @@ class GatherJson(Protocol): self.buf += bytes def connectionLost(self, reason): - self.finished.callback(jsonlib.read(self.buf, use_float=True)) + self.finished.callback(json.loads(self.buf)) class StringProducer(object): # http://twistedmatrix.com/documents/current/web/howto/client.html @@ -99,7 +100,7 @@ class Music: def seekplay_or_pause(self,t): d = self.player.request("POST", networking.musicPlayer.path("seekPlayOrPause"), - bodyProducer=StringProducer(jsonlib.write({"t" : t}))) + bodyProducer=StringProducer(json.dumps({"t" : t}))) class Expr(object): """singleton, provides functions for use in subterm expressions, @@ -500,7 +501,8 @@ def createHelpLines(root): def currentlyPlayingSong(): """ask the music player what song it's on""" - t = jsonlib.read(restkit.Resource(networking.musicPlayer.url).get("time").body) + player = restkit.Resource(networking.musicPlayer.url) + t = json.loads(player.get("time").body_string()) if t['song'] is None: raise ValueError("music player is not playing any song") return URIRef(t['song']) diff --git a/buildout.cfg b/buildout.cfg --- a/buildout.cfg +++ b/buildout.cfg @@ -11,6 +11,7 @@ eggs = web.py==0.36 cyclone==1.0-rc4 rdflib==3.2.1 restkit==4.1.2 + Louie==1.1 python = python interpreter = python diff --git a/light9/curve.py b/light9/curve.py --- a/light9/curve.py +++ b/light9/curve.py @@ -1,16 +1,8 @@ from __future__ import division -import math, glob, time, logging +import glob, time, logging from bisect import bisect_left,bisect -import Tix as tk -try: - from dispatch import dispatcher -except ImportError: - import louie as dispatcher +import louie as dispatcher -import run_local -from light9 import cursors -from light9.dmxchanedit import gradient -from light9.zoomcontrol import RegionZoom from bcf2000 import BCF2000 log = logging.getLogger() @@ -101,541 +93,11 @@ class Curve(object): return leftidx -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)) - def slope(p1,p2): if p2[0] == p1[0]: return 0 return (p2[1] - p1[1]) / (p2[0] - p1[0]) -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(tk.Canvas): - def __init__(self, master, curve, knobEnabled=False, isMusic=False, **kw): - """knobEnabled=True highlights the previous key and ties it to a - hardware knob""" - self.redrawsEnabled = False - self.curve = curve - self.knobEnabled = knobEnabled - self._isMusic = isMusic - self._time = 0 - self.last_mouse_world = None - tk.Canvas.__init__(self,master,width=10,height=10, - relief='sunken',bd=1, - closeenough=5,takefocus=1, **kw) - self.selected_points=[] # idx of points being dragged - # self.bind("",self.focus) - 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.bind("",self.update_curve) - 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("" % x, add_kb_marker_point) - - - for butnum,factor in (5, 1.5),(4, 1/1.5): - def onMouseWheel(ev,factor=factor): - dispatcher.send("zoom about mouse", - t=self.world_from_screen(ev.x,0)[0], - factor=factor) - # this is supposed to make the canvases redraw more - # visibly, so we don't waste line redraws that never - # get seen. I'm not sure if it works. - self.update() - self.bind("" % butnum, onMouseWheel) - self.bind("", lambda ev: - dispatcher.send("see time", - t=self.current_time())) - self.bind("", lambda ev: - dispatcher.send("see time until end", - t=self.current_time())) - self.bind("", lambda ev: dispatcher.send("show all")) - self.bind("", lambda ev: - dispatcher.send("music seek", - t=self.world_from_screen(ev.x,0)[0])) - - self.bind("", - self.dotmotion, add=True) - self.bind("", - self.dotrelease, add=True) - - - # this binds on c-a-b1, etc - self.regionzoom = RegionZoom(self, self.world_from_screen, - self.screen_from_world) - - self.sketch = None # an in-progress sketch - self.bind("", self.sketch_press) - self.bind("", self.sketch_motion) - self.bind("", self.sketch_release) - - - self.dragging_dots = False - self.selecting = False - self.bind("",#"", - self.select_press, add=True) - self.bind("", self.select_motion, add=True) - self.bind("", #"", - self.select_release, add=True) - - self.bind("", self.check_deselect, add=True) - - self.bind("", lambda *args: self.curve.toggleMute()) - self.bind("", lambda *args: dispatcher.send('toggle collapse', - sender=self.curve)) - - 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 0: - 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): - start,end = self.zoom - ht = self.height - return (p[0]-start)/(end-start)*self.width, (ht-5)-p[1]*(ht-10) - - def world_from_screen(self,x,y): - start,end = self.zoom - ht = self.height - return x/self.width*(end-start)+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 - pts = self.screen_from_world((val,0))+self.screen_from_world((val,1)) - self.delete('timecursor') - self.create_line(*pts,**dict(width=2,fill='red',tags=('timecursor',))) - self.have_time_line = True - 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,*args): - if not self.redrawsEnabled: - return - self.width, self.height = self.winfo_width(), self.winfo_height() - - self.zoom = dispatcher.send("zoom area")[0][1] - cp = self.curve.points - - visible_x = (self.world_from_screen(0,0)[0], - self.world_from_screen(self.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] - - self.delete('curve') - - if self.curve.muted: - self['bg'] = 'grey20' - else: - self['bg'] = 'black' - - if self.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): - t1 = time.time() - gradient_res = 6 if self.is_music() else 3 - startX = startColor = None - rects = 0 - for x in range(0, self.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.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.height - if not 0 <= x < self.winfo_width(): - return - x = max(5, x) # cheat left-edge stuff onscreen - self.create_line(x, ht, - x, ht - 20, - fill='white', tags=('curve',)) - self.create_text(x, ht-20, text=label, anchor='s', fill='white', - font="arial 7", tags=('curve',)) - - - def _draw_line(self,visible_points): - linepts=[] - step=1 - linewidth=2 - # 800? maybe this should be related to self.width - if len(visible_points)>800: - step = int(len(visible_points)/800) - linewidth=1 - for p in visible_points[::step]: - linepts.extend(self.screen_from_world(p)) - if len(linepts)<4: - return - if self.curve.muted: - fill = 'grey34' - else: - fill = 'white' - kwargs = dict(width=linewidth, tags='curve', fill=fill) - line = self.create_line(*linepts, **kwargs) - - # canvas doesnt have keyboard focus, so i can't easily change the - # cursor when ctrl is pressed - # def curs(ev): - # print ev.state - # self.bind("",curs) - # self.bind("",lambda ev: curs(0)) - self.tag_bind(line,"",self.new_point_at_mouse) - - - 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 = self.create_rectangle(p[0]-rad,p[1]-rad,p[0]+rad,p[1]+rad, - outline='black',fill='blue', - tags=('curve','point', 'handle%d' % i)) - if worldp[1] == 0: - rad += 3 - dot2 = self.create_oval(p[0]-rad,p[1]-rad, - p[0]+rad,p[1]+rad, - outline='darkgreen', - tags=('curve','point', 'handle%d' % i)) - self.tag_bind('handle%d' % i,"", - lambda ev,i=i: self.dotpress(ev,i)) - #self.tag_bind('handle%d' % i, "", - # lambda ev, i=i: self.remove_point_idx(i)) - - self.dots[i]=dot - - def delpoint(ev): - # had a hard time tag_binding to the points, so i trap at - # the widget level (which might be nice anyway when there - # are multiple pts selected) - if self.selected_points: - self.remove_point_idx(*self.selected_points) - self.bind("", delpoint) - - 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: - self.itemconfigure(d,fill='red') - else: - self.itemconfigure(d,fill='blue') - - def dotpress(self,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 dotmotion(self,ev): - if not self.dragging_dots: - return - if not ev.state & 256: - return # not lmb-down - cp = self.curve.points - moved=0 - - cur = self.world_from_screen(ev.x, ev.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= 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 dotrelease(self,ev): - self.print_state("dotrelease") - if not self.dragging_dots: - return - self.last_mouse_world = None - self.dragging_dots = False class Sliders(BCF2000): def __init__(self, cb, knobCallback, knobButtonCallback): @@ -782,138 +244,3 @@ class Curveset: except KeyError: return self.sliders.valueOut("knob%s" % num, value * 127) - -class CurveRow(tk.Frame): - """ - one of the repeating curve rows (including widgets on the left) - """ - def __init__(self, master, name, curve, slider, knobEnabled): - tk.Frame.__init__(self, master, relief='raised', bd=1) - - self.collapsed = tk.IntVar() - self.muted = tk.IntVar() - - labelFont = "arial 8" - - leftside = tk.Frame(self) - leftside.pack(side='left') - - self.curveView = Curveview(self, curve, knobEnabled=knobEnabled, - isMusic=name in ['music', 'smooth_music']) - self.curveView.pack(side='left', fill='both', expand=True) - self.curveView.config(height=100) - - txt = "curve '%s'" % name - if len(name) > 7: - txt = name - curve_name_label = tk.Label(leftside, text=txt, font=labelFont,width=15) - curve_name_label.pack(side='top') - - bools = tk.Frame(leftside) - bools.pack(side='top') - collapsed_cb = tk.Checkbutton(bools, text="C", - font=labelFont, variable=self.collapsed) - collapsed_cb.pack(side='left') - self.collapsed.trace('w', self.update_ui_to_collapsed_state) - dispatcher.connect(self.toggleCollapsed, "toggle collapse", - sender=curve) - - self.default_bg = leftside['bg'] - muted_cb = tk.Checkbutton(bools, text="M", font=labelFont, - variable=self.muted) - muted_cb.pack(side='left') - 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 = tk.Label(leftside, text="Slider %s" % slider, - fg='#800000', font=labelFont) - self.sliderLabel.pack(side='top') - - # 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 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(tk.ScrolledWindow): - def __init__(self, master, curveset, **kw): - self.curveset = curveset - self.allCurveRows = set() - tk.ScrolledWindow.__init__(self,master,**kw) - - f = tk.Frame(self.window,relief='raised',bd=1) - f.pack(side='top',fill='x') - tk.Label(f, text="new curve named: (C-N)").pack(side='left') - - self.newcurvename = tk.StringVar() - - entry = tk.Entry(f, textvariable=self.newcurvename) - entry.pack(side='left', fill='x',exp=1) - entry.bind("", self.new_curve) - - def focus_entry(): - entry.focus() - - dispatcher.connect(self.add_curve, "add_curve", sender=self.curveset) - dispatcher.connect(focus_entry, "focus new curve", weak=False) - - 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(self.window, name, curve, slider, knobEnabled) - f.pack(side='top', fill='both') - self.allCurveRows.add(f) - f.curveView.goLive() - - 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() - diff --git a/light9/curveview.py b/light9/curveview.py new file mode 100644 --- /dev/null +++ b/light9/curveview.py @@ -0,0 +1,675 @@ +from __future__ import division +import math, time, logging +import Tix as tk +import louie as dispatcher +from light9.zoomcontrol import RegionZoom +from light9 import cursors +from light9.curve import introPad, postPad +from light9.dmxchanedit import gradient + +log = logging.getLogger() + +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(tk.Canvas): + def __init__(self, master, curve, knobEnabled=False, isMusic=False, **kw): + """knobEnabled=True highlights the previous key and ties it to a + hardware knob""" + self.redrawsEnabled = False + self.curve = curve + self.knobEnabled = knobEnabled + self._isMusic = isMusic + self._time = 0 + self.last_mouse_world = None + tk.Canvas.__init__(self,master,width=10,height=10, + relief='sunken',bd=1, + closeenough=5,takefocus=1, **kw) + self.selected_points=[] # idx of points being dragged + # self.bind("",self.focus) + 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.bind("",self.update_curve) + 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("" % x, add_kb_marker_point) + + + for butnum,factor in (5, 1.5),(4, 1/1.5): + def onMouseWheel(ev,factor=factor): + dispatcher.send("zoom about mouse", + t=self.world_from_screen(ev.x,0)[0], + factor=factor) + # this is supposed to make the canvases redraw more + # visibly, so we don't waste line redraws that never + # get seen. I'm not sure if it works. + self.update() + self.bind("" % butnum, onMouseWheel) + self.bind("", lambda ev: + dispatcher.send("see time", + t=self.current_time())) + self.bind("", lambda ev: + dispatcher.send("see time until end", + t=self.current_time())) + self.bind("", lambda ev: dispatcher.send("show all")) + self.bind("", lambda ev: + dispatcher.send("music seek", + t=self.world_from_screen(ev.x,0)[0])) + + self.bind("", + self.dotmotion, add=True) + self.bind("", + self.dotrelease, add=True) + + + # this binds on c-a-b1, etc + self.regionzoom = RegionZoom(self, self.world_from_screen, + self.screen_from_world) + + self.sketch = None # an in-progress sketch + self.bind("", self.sketch_press) + self.bind("", self.sketch_motion) + self.bind("", self.sketch_release) + + + self.dragging_dots = False + self.selecting = False + self.bind("",#"", + self.select_press, add=True) + self.bind("", self.select_motion, add=True) + self.bind("", #"", + self.select_release, add=True) + + self.bind("", self.check_deselect, add=True) + + self.bind("", lambda *args: self.curve.toggleMute()) + self.bind("", lambda *args: dispatcher.send('toggle collapse', + sender=self.curve)) + + 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 0: + 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): + start,end = self.zoom + ht = self.height + return (p[0]-start)/(end-start)*self.width, (ht-5)-p[1]*(ht-10) + + def world_from_screen(self,x,y): + start,end = self.zoom + ht = self.height + return x/self.width*(end-start)+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 + pts = self.screen_from_world((val,0))+self.screen_from_world((val,1)) + self.delete('timecursor') + self.create_line(*pts,**dict(width=2,fill='red',tags=('timecursor',))) + self.have_time_line = True + 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,*args): + if not self.redrawsEnabled: + return + self.width, self.height = self.winfo_width(), self.winfo_height() + + self.zoom = dispatcher.send("zoom area")[0][1] + cp = self.curve.points + + visible_x = (self.world_from_screen(0,0)[0], + self.world_from_screen(self.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] + + self.delete('curve') + + if self.curve.muted: + self['bg'] = 'grey20' + else: + self['bg'] = 'black' + + if self.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): + t1 = time.time() + gradient_res = 6 if self.is_music() else 3 + startX = startColor = None + rects = 0 + for x in range(0, self.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.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.height + if not 0 <= x < self.winfo_width(): + return + x = max(5, x) # cheat left-edge stuff onscreen + self.create_line(x, ht, + x, ht - 20, + fill='white', tags=('curve',)) + self.create_text(x, ht-20, text=label, anchor='s', fill='white', + font="arial 7", tags=('curve',)) + + + def _draw_line(self,visible_points): + linepts=[] + step=1 + linewidth=2 + # 800? maybe this should be related to self.width + if len(visible_points)>800: + step = int(len(visible_points)/800) + linewidth=1 + for p in visible_points[::step]: + linepts.extend(self.screen_from_world(p)) + if len(linepts)<4: + return + if self.curve.muted: + fill = 'grey34' + else: + fill = 'white' + kwargs = dict(width=linewidth, tags='curve', fill=fill) + line = self.create_line(*linepts, **kwargs) + + # canvas doesnt have keyboard focus, so i can't easily change the + # cursor when ctrl is pressed + # def curs(ev): + # print ev.state + # self.bind("",curs) + # self.bind("",lambda ev: curs(0)) + self.tag_bind(line,"",self.new_point_at_mouse) + + + 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 = self.create_rectangle(p[0]-rad,p[1]-rad,p[0]+rad,p[1]+rad, + outline='black',fill='blue', + tags=('curve','point', 'handle%d' % i)) + if worldp[1] == 0: + rad += 3 + dot2 = self.create_oval(p[0]-rad,p[1]-rad, + p[0]+rad,p[1]+rad, + outline='darkgreen', + tags=('curve','point', 'handle%d' % i)) + self.tag_bind('handle%d' % i,"", + lambda ev,i=i: self.dotpress(ev,i)) + #self.tag_bind('handle%d' % i, "", + # lambda ev, i=i: self.remove_point_idx(i)) + + self.dots[i]=dot + + def delpoint(ev): + # had a hard time tag_binding to the points, so i trap at + # the widget level (which might be nice anyway when there + # are multiple pts selected) + if self.selected_points: + self.remove_point_idx(*self.selected_points) + self.bind("", delpoint) + + 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: + self.itemconfigure(d,fill='red') + else: + self.itemconfigure(d,fill='blue') + + def dotpress(self,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 dotmotion(self,ev): + if not self.dragging_dots: + return + if not ev.state & 256: + return # not lmb-down + cp = self.curve.points + moved=0 + + cur = self.world_from_screen(ev.x, ev.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= 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 dotrelease(self,ev): + self.print_state("dotrelease") + if not self.dragging_dots: + return + self.last_mouse_world = None + self.dragging_dots = False + +class CurveRow(tk.Frame): + """ + one of the repeating curve rows (including widgets on the left) + """ + def __init__(self, master, name, curve, slider, knobEnabled): + tk.Frame.__init__(self, master, relief='raised', bd=1) + + self.collapsed = tk.IntVar() + self.muted = tk.IntVar() + + labelFont = "arial 8" + + leftside = tk.Frame(self) + leftside.pack(side='left') + + self.curveView = Curveview(self, curve, knobEnabled=knobEnabled, + isMusic=name in ['music', 'smooth_music']) + self.curveView.pack(side='left', fill='both', expand=True) + self.curveView.config(height=100) + + txt = "curve '%s'" % name + if len(name) > 7: + txt = name + curve_name_label = tk.Label(leftside, text=txt, font=labelFont,width=15) + curve_name_label.pack(side='top') + + bools = tk.Frame(leftside) + bools.pack(side='top') + collapsed_cb = tk.Checkbutton(bools, text="C", + font=labelFont, variable=self.collapsed) + collapsed_cb.pack(side='left') + self.collapsed.trace('w', self.update_ui_to_collapsed_state) + dispatcher.connect(self.toggleCollapsed, "toggle collapse", + sender=curve) + + self.default_bg = leftside['bg'] + muted_cb = tk.Checkbutton(bools, text="M", font=labelFont, + variable=self.muted) + muted_cb.pack(side='left') + 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 = tk.Label(leftside, text="Slider %s" % slider, + fg='#800000', font=labelFont) + self.sliderLabel.pack(side='top') + + # 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 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(tk.ScrolledWindow): + def __init__(self, master, curveset, **kw): + self.curveset = curveset + self.allCurveRows = set() + tk.ScrolledWindow.__init__(self,master,**kw) + + f = tk.Frame(self.window,relief='raised',bd=1) + f.pack(side='top',fill='x') + tk.Label(f, text="new curve named: (C-N)").pack(side='left') + + self.newcurvename = tk.StringVar() + + entry = tk.Entry(f, textvariable=self.newcurvename) + entry.pack(side='left', fill='x',exp=1) + entry.bind("", self.new_curve) + + def focus_entry(): + entry.focus() + + dispatcher.connect(self.add_curve, "add_curve", sender=self.curveset) + dispatcher.connect(focus_entry, "focus new curve", weak=False) + + 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(self.window, name, curve, slider, knobEnabled) + f.pack(side='top', fill='both') + self.allCurveRows.add(f) + f.curveView.goLive() + + 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() +