Files @ 6540879e336e
Branch filter:

Location: light9/light8/stage.py - annotation

drewp
fixed Stage a lot: ctrl-drag subtracts from selection; ctrl-a/ctrl-A select all/none;
fixed Stage a lot: ctrl-drag subtracts from selection; ctrl-a/ctrl-A select all/none;
rmb adjusts levels
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
45b12307c695
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
119369e60da1
45b12307c695
45b12307c695
45b12307c695
45b12307c695
45b12307c695
43aa1ee8b3a9
43aa1ee8b3a9
43aa1ee8b3a9
43aa1ee8b3a9
43aa1ee8b3a9
6540879e336e
6540879e336e
43aa1ee8b3a9
43aa1ee8b3a9
6540879e336e
6540879e336e
43aa1ee8b3a9
6540879e336e
6540879e336e
6540879e336e
43aa1ee8b3a9
43aa1ee8b3a9
45b12307c695
45b12307c695
45b12307c695
119369e60da1
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
119369e60da1
119369e60da1
119369e60da1
6540879e336e
6540879e336e
119369e60da1
119369e60da1
119369e60da1
1b0266dd233a
119369e60da1
115636cca107
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
115636cca107
115636cca107
115636cca107
119369e60da1
119369e60da1
119369e60da1
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
6540879e336e
119369e60da1
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
1b0266dd233a
119369e60da1
6540879e336e
6540879e336e
119369e60da1
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
119369e60da1
119369e60da1
119369e60da1
6540879e336e
119369e60da1
6540879e336e
119369e60da1
6540879e336e
6540879e336e
6540879e336e
119369e60da1
6540879e336e
119369e60da1
119369e60da1
6540879e336e
6540879e336e
119369e60da1
119369e60da1
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
119369e60da1
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
119369e60da1
6540879e336e
6540879e336e
6540879e336e
119369e60da1
6540879e336e
115636cca107
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
6540879e336e
6540879e336e
1b0266dd233a
6540879e336e
6540879e336e
1b0266dd233a
6540879e336e
6540879e336e
115636cca107
115636cca107
119369e60da1
6540879e336e
119369e60da1
119369e60da1
6540879e336e
6540879e336e
6540879e336e
119369e60da1
6faae180d1c5
6540879e336e
119369e60da1
119369e60da1
6540879e336e
6540879e336e
119369e60da1
6540879e336e
6540879e336e
6540879e336e
119369e60da1
1b0266dd233a
6540879e336e
1b0266dd233a
1b0266dd233a
45b12307c695
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
119369e60da1
115636cca107
6540879e336e
6540879e336e
6540879e336e
6540879e336e
115636cca107
115636cca107
6540879e336e
6540879e336e
6540879e336e
6540879e336e
1b0266dd233a
1b0266dd233a
1b0266dd233a
119369e60da1
119369e60da1
119369e60da1
119369e60da1
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
95ba7e14d15a
1b0266dd233a
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
95ba7e14d15a
6540879e336e
119369e60da1
1b0266dd233a
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
1b0266dd233a
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
6540879e336e
119369e60da1
119369e60da1
119369e60da1
119369e60da1
119369e60da1
6540879e336e
119369e60da1
119369e60da1
1b0266dd233a
1b0266dd233a
1b0266dd233a
1b0266dd233a
119369e60da1
119369e60da1
533ac835083f
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
533ac835083f
119369e60da1
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
6540879e336e
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
95ba7e14d15a
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
6540879e336e
95ba7e14d15a
95ba7e14d15a
from Tkinter import *


def printevent(ev):
    for k in dir(ev):
        if not k.startswith('__'):
            print k,getattr(ev,k)
    print ""


textstyle={'font':'arial 7','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())

    def setsubediting(self,subeditor):
        self.subeditor = subeditor
    #
    # 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:
                # they moved enough, it's a level change
                self.startrectangleselect()
                

        if self.mode=='levelchange':
            delta = (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

    #
    #
    #
            
    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='red',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)
        
        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))



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()