diff light8/stage.py @ 0:45b12307c695

Initial revision
author drewp
date Wed, 03 Jul 2002 09:37:57 +0000
parents
children a76f775bb635
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light8/stage.py	Wed Jul 03 09:37:57 2002 +0000
@@ -0,0 +1,405 @@
+from Tix import *
+
+def printevent(ev):
+    for k in dir(ev):
+        if not k.startswith('__'):
+            print k,getattr(ev,k)
+    print ""
+
+textstyle={'font':'arial 9','fill':'white'}
+
+class Stage(Canvas):
+    
+    """a fancy widget that shows light locations (and optionally their
+    aim locations on an image of the stage. you can select or
+    multiselect lights and drag them up or down to change their
+    brightness.
+
+    ctrl-a is select all,
+    ctrl-shift-a or clicking on no light deselects all,
+    re-clicking a light with shift key down toggles whether it's in the selection.
+    ctrl-drag-rectangle deselects the lights in the rectangle,
+    shift-drag-rectangle selects the lights in the rectangle,
+    drag-rectangle selects only the lights in the rectangle.
+
+    a light can be selected on its location point, its aim point
+    (which may or may not be present), or its name.
+
+    lights should be able to be interactively 'locked', which blocks
+    them from being selected. 
+
+    API:
+      __init__(parent,**kw)
+        put pass any canvas options you want
+        
+      setimage(stageimage)
+        sets image to given filename (ppm, gif, etc) and resizes the
+        canvas to the image size
+
+      addlight(name, location, aim=None)
+        location and aim are pixel coord tuples. name will be passed
+        back to you in the callback (see below)
+
+      setsubediting(se)
+        give a subediting object to receive the 'startlevelchange' and
+        'levelchange' messages
+      
+
+    """
+    def __init__(self,parent,**kw):
+        Canvas.__init__(self,parent,**kw)
+
+        self.bind("<ButtonPress>", self.press)
+        self.bind("<Motion>", self.motion)
+        self.bind("<ButtonRelease>", self.release)
+        self.bind("<Control-Key-a>", lambda ev: self.selectall())
+        self.bind("<Control-Key-A>", lambda ev: self.clearselection())
+#        self.bind("<Control-Shift-Key-a>",self.handlecontrol_a)
+        
+        self.halo=11 # search radius for clicked items
+
+        self.mode=None # as you perform with the mouse, this goes
+                       # from None to 'pressed','rectangle','levelchange', etc
+
+        self.alllights=[]
+        self.selectedlights=[]
+        self.alllighttags={} # tag: name lookup
+
+        self.subeditor = None
+
+
+    def setimage(self,stageimage):
+        img = Image('photo',file=stageimage)
+        self.img=img # can't lose this!
+        # print img.width()
+#        self.create_image(0,0,anchor='nw',image=img)
+#        self.config(width=img.width(),height=img.height())
+
+        # we had the picture removed for good luck, but we remember
+        # what the dimensions formerly was
+        self.config(width=821,height=681, bg="grey40")
+
+
+    def setsubediting(self,subeditor):
+        self.subeditor = subeditor
+
+    # (17:00:06) drewp: if yes, then self.itemconfigure(tagOrId, text=...)
+    def updatelightlevel(self, name, level):
+        tag = self.nametag(name)
+        self.itemconfigure("level_%s" % tag, text=level)
+
+    #
+    # selection management
+    #
+    def updateselectionboxes(self):
+        "make selection boxes that match self.selectedlights"
+        self.delete("selectbox")
+        for l in self.selectedlights:
+            for c in self.getlightbboxes(l):
+               self.create_rectangle(c[0]-2,c[1]-2,c[2]+2,c[3]+2,
+                                     outline='red',tag="selectbox")
+
+    def selectall(self):
+        self.selectedlights= self.alllights[:]
+        self.updateselectionboxes()
+    def clearselection(self):
+        self.selectedlights=[]
+        self.updateselectionboxes()
+
+    def markfordynselection(self):
+        """call this before calls to replacedynselection"""
+        self.origselection = self.selectedlights[:]
+
+    def replacedynselection(self,newlightnames,subtract=0):
+        """as a dynamic selection changes, keep calling this function
+        with the names of the lights in the dynamic selection. the
+        original selection (at the time of markfordynselection) will
+        be shown along with any new lights. if subtract=1, the selection will
+        be shown MINUS the newlights."""
+        if subtract==0:
+            # orig selection plus any newlights that weren't in the orig 
+            # selection
+            self.selectedlights = self.origselection[:] + \
+                [l for l in newlightnames if l not in self.origselection]
+        else:
+            # orig selection lights except those that are in the newlightnames 
+            # list
+            self.selectedlights = [l for l in self.origselection 
+                if l not in newlightnames]
+        self.updateselectionboxes()
+
+    def select(self,lightname,select=1): # select=0 for deselect
+        """select or deselect (select=0) a light by name"""
+        if select:
+            if lightname not in self.selectedlights:
+                self.selectedlights.append(lightname)
+        elif lightname in self.selectedlights:
+            self.selectedlights.remove(lightname)
+
+        self.updateselectionboxes()
+                
+    #
+    # mouse handling
+    #
+    def press(self,ev):
+        
+        self.mode='pressed'
+        self.mousedownpos=(ev.x,ev.y)
+        print "click at",self.mousedownpos
+
+        button=ev.num
+        shifted=ev.state & 1
+        control=ev.state & 4
+        touching=self.findoverlappinglights((ev.x-self.halo,ev.y-self.halo,
+                                             ev.x+self.halo,ev.y+self.halo))
+        istouching=len(touching)>0
+
+        if button==1:
+            if not istouching:
+                # clicked in space
+                if not shifted and not control and len(self.selectedlights)>0:
+                    # either a deselect (if no motion) or a level change (if motion)
+                    self.mode = 'deselect-or-rectangle'
+                if shifted or control or len(self.selectedlights)==0:
+                    # with shift/control, add/subtract lights to selection
+                    self.startrectangleselect()
+
+            else:
+                # clicked a selectable object
+                # toggle selection
+                if touching[0] in self.selectedlights:
+                    if shifted or control:
+                        # deselect
+                        self.select(touching[0],0)
+                        # and do nothing else
+                        self.mode=None
+                    if not shifted:
+                        # select only this light
+                        self.clearselection()
+                        self.select(touching[0])
+                        # and adjust its level
+                        self.startlevelchange()
+
+                else:
+                    # clicked a light that wasn't selected
+                    if not shifted:
+                        self.clearselection()
+                    self.select(touching[0])
+                    # and adjust levels now
+                    self.startlevelchange()
+
+        if button==3:
+            self.startlevelchange()
+
+    def motion(self,ev):
+
+        coords=(ev.x,ev.y)
+
+        shifted=ev.state & 1
+        control=ev.state & 4
+
+        if self.mode=='deselect-or-rectangle':
+            if (coords[0]-self.mousedownpos[0])**2+(coords[1]-self.mousedownpos[1])**2>self.halo**2:
+                if not shifted and not control:
+                    self.clearselection()
+                # they moved enough, it's a level change
+                self.startrectangleselect()
+                
+
+        if self.mode=='levelchange':
+            delta = 1.5 * (self.mousedownpos[1]-ev.y)
+            if self.subeditor:
+                self.subeditor.levelchange(self.selectedlights,delta)
+
+        if self.mode=='rectangle':
+            sr = self.find_withtag('selectrect')
+            if not sr:
+                # make the selection rectangle
+                sr=self.create_rectangle( self.mousedownpos[0],
+                    self.mousedownpos[1],coords[0],coords[1],
+                    outlinestipple='gray50',outline='yellow',tag='selectrect')
+
+            # move rectangle with mouse
+            self.coords(sr,*(self.mousedownpos+coords))
+
+            # redo the dynselection with the new rectangle
+            self.replacedynselection(self.findoverlappinglights((self.mousedownpos+coords),1),
+                                     subtract=control)
+
+    def release(self,ev):
+        if self.mode:
+            if self.mode=='rectangle':
+                self.delete('selectrect')
+
+            if self.mode=='deselect-or-rectangle':
+                # they didn't move enough to promote the mode to level, so 
+                # it's a deselect click
+                self.clearselection()
+            
+            self.mode=None
+
+    #
+    # subedit type things (correct me if i'm wrong)
+    #
+            
+    def startlevelchange(self):
+        """sets mode to levelchange AND notifies subeditor. this
+        should be done exactly once (per mouse drag), when you first
+        decide the mode is levelchange"""
+        self.mode='levelchange'
+        if self.subeditor:
+            self.subeditor.startlevelchange()
+    def startrectangleselect(self):
+        """sets mode to rectangle AND checkpoints the current selection"""
+        self.mode='rectangle'
+        self.markfordynselection()
+    #
+    # light names vs. canvas object tags
+    #
+    def nametag(self,name):
+        "returns a safe version of the name that won't match other names"
+        return name.replace(" ","__")
+
+    def tagtoname(self,tag):
+        "finds the real light name for a tag written by nametag()"
+        return self.alllighttags[tag]
+
+    #
+    # light methods
+    #
+    def addlight(self,name,location,aim=None):
+        tags='light selectable name_%s' % self.nametag(name)
+        
+        self.create_oval(location[0]-2,location[1]-2,
+                         location[0]+2,location[1]+2,
+                         fill='red',tag=tags+" hotspot")
+        if aim:
+            self.create_oval(aim[0]-2,aim[1]-2,
+                             aim[0]+2,aim[1]+2,
+                             fill='blue',tag=tags+" hotspot")
+            self.create_line(location[0],location[1],aim[0],aim[1],
+                fill='lightblue', arrow='last',arrowshape="9 15 6",tag='light')
+
+        # shadow
+        self.create_text(location[0]-1,location[1]+6,
+                         anchor='n',text=name,fill='black',
+                         tag=tags,**dict([(k,v) 
+                            for k,v in textstyle.items() if k!='fill']))
+        # text
+        self.create_text(location[0],location[1]+5,anchor='n',text=name,
+            tag=tags,**textstyle)
+
+        # level
+        self.create_text(location[0]-2,location[1]+13,anchor='n',
+            text='0', # will be changed later
+            tag='level_%s' % self.nametag(name),**textstyle)
+        
+        self.alllights.append(name)
+        self.alllighttags[self.nametag(name)]=name
+
+    def getlightbboxes(self,tag):
+        """returns a list of bboxes for a light with a given name_ tag. the 
+        selection mechanism draws around these bboxes to show that a light is 
+        selected"""
+        bboxes=[]
+        for o in self.find_withtag("name_%s" % self.nametag(tag)):
+            if 'hotspot' in self.gettags(o):
+                bboxes.append(self.bbox(o))
+        return bboxes
+
+    def findoverlappinglights(self,box,enclosed=0):
+        "returns all the different names for lights that are within (or enclosed by) the box"
+        lights=[]
+        if enclosed:
+            candidates = self.find_enclosed(*box)
+        else:
+            candidates = self.find_overlapping(*box)
+
+        for o in candidates:
+            for t in self.gettags(o):
+                if t.startswith("name_"):
+                    n = self.tagtoname(t[5:])
+                    if n and (n not in lights):
+                        lights.append(n)
+        return lights
+
+
+def createlights(s):
+    s.setimage('guysanddolls.gif')
+    s.addlight('desk1',(46, 659),    aim=(210, 381))
+    s.addlight('marry1',(78, 661),   aim=(398, 428))
+    s.addlight('b13',(110, 661))   
+    s.addlight('hotbox1',(147, 657), aim=(402, 327))
+    s.addlight('edge',(179, 651),    aim=(116, 441))
+    s.addlight('phone',(214, 652),   aim=(651, 417))
+    s.addlight('cuba1',(315, 656),   aim=(559, 407))
+    s.addlight('b22',(347, 661),     aim=(247, 458))
+    s.addlight('b23',(379, 661))  
+    s.addlight('b24',(417, 661))  
+    s.addlight('b25',(455, 658),     aim=(520, 466))
+    s.addlight('desk2',(490, 655),   aim=(237, 375))
+    s.addlight('rock',(571, 655),    aim=(286, 304))
+    s.addlight('b32',(606, 650))  
+    s.addlight('hotbox2',(637, 650), aim=(433, 337))
+    s.addlight('b34',(671, 651))   
+    s.addlight('marry2',(703, 651),  aim=(429, 426))
+    s.addlight('cuba2',(733, 652),   aim=(602, 408))
+
+    s.addlight('sidefill1',(115, 473),aim=(228, 423))
+    s.addlight('sidefill2',(617, 475),aim=(526, 425))
+
+    s.addlight('cycright',(485, 164),(483, 109))
+    s.addlight('cycleft',(330, 154),(333, 108))
+
+    s.addlight('upfill1',(275, 325),(262, 237))
+    s.addlight('upfill2',(333, 326),(330, 229))
+    s.addlight('upfill3',(473, 325),(454, 226))
+    s.addlight('upfill4',(541, 325),(528, 223))
+
+    s.addlight('god',(369,549))
+
+    s.addlight('patio1',(42, 560),(12, 512))
+    s.addlight('patio2',(675, 553),(793, 514))
+
+    s.addlight('hotback',(413, 476),(414, 396))
+
+    s.addlight('main 2',(120, 563) ,aim=(241, 472))
+    s.addlight('main 3',(162, 562) ,aim=(140, 425))
+    s.addlight('main 4',(208, 560) ,aim=(342, 423))
+    s.addlight('main 5',(259, 558) ,aim=(433, 450))
+    s.addlight('main 7',(494, 551) ,aim=(420, 458))
+    s.addlight('main 8',(528, 554) ,aim=(503, 477))
+    s.addlight('main 9',(559, 554) ,aim=(544, 479))
+    s.addlight('main 10',(597, 556),aim=(339, 444))
+    s.addlight('main 11',(636, 556),aim=(449, 409))
+
+    s.addlight('side r', (785, 609))
+    s.addlight('side l', (8, 613))
+
+    s.addlight('cafe1',(174, 476))
+    s.addlight('cafe2',(584, 475))
+    s.addlight('dream',(329, 477),aim=(267, 309))
+    s.addlight('xmas',(555, 56),aim=(438, 284))
+
+    for y,col in ((300,'red'),(333,'blue'),(364,'gree'),(407,'oran')):
+        for i in range(1,5):
+            s.addlight('%s%s' % (col,i),(726+10*i,y))
+
+if __name__=='__main__':
+    root=Tk()
+    root.tk_focusFollowsMouse()
+    root.wm_geometry("+376+330")
+    s=Stage(root)
+    s.setimage('guysanddolls.gif')
+    s.pack()
+
+    createlights(s)
+
+    class subediting_standin:
+        def startlevelchange(self):
+            print "start lev change"
+        def levelchange(self,lights,delta):
+            print "change",lights,delta
+    s.setsubediting(subediting_standin())
+    
+    root.mainloop()