comparison light8/stage.py @ 44:6540879e336e

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
author drewp
date Sun, 07 Jul 2002 13:07:31 +0000
parents 115636cca107
children 71489bb71528
comparison
equal deleted inserted replaced
43:2cd759c2b3c7 44:6540879e336e
34 API: 34 API:
35 __init__(parent,**kw) 35 __init__(parent,**kw)
36 put pass any canvas options you want 36 put pass any canvas options you want
37 37
38 setimage(stageimage) 38 setimage(stageimage)
39 sets image to given filename (ppm, gif, etc) and resizes the canvas to the image size 39 sets image to given filename (ppm, gif, etc) and resizes the
40 canvas to the image size
40 41
41 addlight(name, location, aim=None) 42 addlight(name, location, aim=None)
42 location and aim are pixel coord tuples. name will be passed back to you in the callback (see below) 43 location and aim are pixel coord tuples. name will be passed
43 44 back to you in the callback (see below)
44 setlightchangecb(cb) 45
45 give a function which will be called like this: cb(list_of_light_names, delta) 46 setsubediting(se)
47 give a subediting object to receive the 'startlevelchange' and
48 'levelchange' messages
46 49
47 50
48 """ 51 """
49 def __init__(self,parent,**kw): 52 def __init__(self,parent,**kw):
50 Canvas.__init__(self,parent,**kw) 53 Canvas.__init__(self,parent,**kw)
51 54
52 self.bind("<ButtonPress-1>", self.leftpress) 55 self.bind("<ButtonPress>", self.press)
53 self.bind("<B1-Motion>", self.leftmotion) 56 self.bind("<Motion>", self.motion)
54 self.bind("<ButtonRelease-1>", self.leftrelease) 57 self.bind("<ButtonRelease>", self.release)
58 self.bind("<Control-Key-a>", lambda ev: self.selectall())
59 self.bind("<Control-Key-A>", lambda ev: self.clearselection())
60 # self.bind("<Control-Shift-Key-a>",self.handlecontrol_a)
55 61
56 self.halo=11 # search radius for clicked items 62 self.halo=11 # search radius for clicked items
57 63
58 self.lmbstate=None # as you perform with LMB, this goes from None to 'pressed','rectangle','levelchange' 64 self.mode=None # as you perform with the mouse, this goes
65 # from None to 'pressed','rectangle','levelchange', etc
59 66
60 self.alllights=[] 67 self.alllights=[]
61 self.selectedlights=[] 68 self.selectedlights=[]
62 self.alllighttags={} # tag: name lookup 69 self.alllighttags={} # tag: name lookup
63 70
78 def updateselectionboxes(self): 85 def updateselectionboxes(self):
79 "make selection boxes that match self.selectedlights" 86 "make selection boxes that match self.selectedlights"
80 self.delete("selectbox") 87 self.delete("selectbox")
81 for l in self.selectedlights: 88 for l in self.selectedlights:
82 for c in self.getlightbboxes(l): 89 for c in self.getlightbboxes(l):
83 self.create_rectangle(c[0]-2,c[1]-2,c[2]+2,c[3]+2,outline='red',tag="selectbox") 90 self.create_rectangle(c[0]-2,c[1]-2,c[2]+2,c[3]+2,
84 91 outline='red',tag="selectbox")
85 def clearselection(self,dyn=0): 92
93 def selectall(self):
94 self.selectedlights= self.alllights[:]
95 self.updateselectionboxes()
96 def clearselection(self):
86 self.selectedlights=[] 97 self.selectedlights=[]
87 self.updateselectionboxes() 98 self.updateselectionboxes()
88 99
89 def markfordynselection(self): 100 def markfordynselection(self):
90 """call this before calls to replacedynselection""" 101 """call this before calls to replacedynselection"""
91 self.origselection = self.selectedlights 102 self.origselection = self.selectedlights[:]
92 103
93 def replacedynselection(self,newlightnames): 104 def replacedynselection(self,newlightnames,subtract=0):
94 """as a dynamic selection changes, keep calling this function with the 105 """as a dynamic selection changes, keep calling this function
95 names of the lights in the dynamic selection. the original selection (at the time 106 with the names of the lights in the dynamic selection. the
96 of markfordynselection) will be shown along with any new lights""" 107 original selection (at the time of markfordynselection) will
97 self.selectedlights = self.origselection + [l for l in newlightnames if l not in self.origselection] 108 be shown along with any new lights. if subtract=1, the selection will
109 be shown MINUS the newlights."""
110 if subtract==0:
111 # orig selection plus any newlights that weren't in the orig selection
112 self.selectedlights = self.origselection[:] + [l for l in newlightnames if l not in self.origselection]
113 else:
114 # orig selection lights except those that are in the newlightnames list
115 self.selectedlights = [l for l in self.origselection if l not in newlightnames]
98 self.updateselectionboxes() 116 self.updateselectionboxes()
99 117
100 def select(self,lightname,select=1,dyn=0): # select=0 for deselect 118 def select(self,lightname,select=1): # select=0 for deselect
119 """select or deselect (select=0) a light by name"""
101 if select: 120 if select:
102 if lightname not in self.selectedlights: 121 if lightname not in self.selectedlights:
103 self.selectedlights.append(lightname) 122 self.selectedlights.append(lightname)
104 elif lightname in self.selectedlights: 123 elif lightname in self.selectedlights:
105 self.selectedlights.remove(lightname) 124 self.selectedlights.remove(lightname)
106 125
107 self.updateselectionboxes() 126 self.updateselectionboxes()
108 127
109 128
110 # 129 #
111 # LMB click or drag 130 # mouse handling
112 # 131 #
113 def leftpress(self,ev): 132 def press(self,ev):
114 133
115 self.lmbstate='pressed' 134 self.mode='pressed'
116 self.lmbstart=(ev.x,ev.y) 135 self.mousedownpos=(ev.x,ev.y)
117 print "click at",self.lmbstart 136 print "click at",self.mousedownpos
118 137
138 button=ev.num
119 shifted=ev.state & 1 139 shifted=ev.state & 1
120 control=ev.state & 4 140 control=ev.state & 4
121 touching=self.findoverlappinglights((ev.x-self.halo,ev.y-self.halo,ev.x+self.halo,ev.y+self.halo)) 141 touching=self.findoverlappinglights((ev.x-self.halo,ev.y-self.halo,
142 ev.x+self.halo,ev.y+self.halo))
122 istouching=len(touching)>0 143 istouching=len(touching)>0
123 144
124 if not istouching: 145 if button==1:
125 # clicked in space 146 if not istouching:
126 if not shifted and not control and len(self.selectedlights)>0: 147 # clicked in space
127 # either a deselect (if no motion) or a level change (if motion) 148 if not shifted and not control and len(self.selectedlights)>0:
128 self.lmbstate = 'deselect-or-level' 149 # either a deselect (if no motion) or a level change (if motion)
129 if shifted or control or len(self.selectedlights)==0: 150 self.mode = 'deselect-or-rectangle'
130 # with shift/control, add/subtract lights to selection 151 if shifted or control or len(self.selectedlights)==0:
131 self.lmbstate='rectangle' 152 # with shift/control, add/subtract lights to selection
132 153 self.startrectangleselect()
133 else: 154
134 # clicked a selectable object 155 else:
135 # toggle selection 156 # clicked a selectable object
136 if touching[0] in self.selectedlights: 157 # toggle selection
137 if shifted: 158 if touching[0] in self.selectedlights:
138 # deselect 159 if shifted or control:
139 self.select(touching[0],0) 160 # deselect
140 # and do nothing else 161 self.select(touching[0],0)
141 self.lmbstate=None 162 # and do nothing else
163 self.mode=None
164 if not shifted:
165 # select only this light
166 self.clearselection()
167 self.select(touching[0])
168 # and adjust its level
169 self.startlevelchange()
170
142 else: 171 else:
143 # select only this light 172 # clicked a light that wasn't selected
144 self.clearselection() 173 if not shifted:
174 self.clearselection()
145 self.select(touching[0]) 175 self.select(touching[0])
146 # and adjust its level 176 # and adjust levels now
147 self.startlevelchange() 177 self.startlevelchange()
148 178
149 else: 179 if button==3:
150 # clicked a light that wasn't selected 180 self.startlevelchange()
151 if not shifted: 181
152 self.clearselection() 182 def motion(self,ev):
153 self.select(touching[0])
154 # and adjust levels now
155 self.startlevelchange()
156
157 if self.lmbstate=='rectangle':
158 self.markfordynselection()
159 def leftmotion(self,ev):
160 183
161 coords=(ev.x,ev.y) 184 coords=(ev.x,ev.y)
162 185
163 shifted=ev.state & 1 186 shifted=ev.state & 1
164 control=ev.state & 4 187 control=ev.state & 4
165 188
166 if self.lmbstate=='deselect-or-level': 189 if self.mode=='deselect-or-rectangle':
167 if (coords[0]-self.lmbstart[0])**2+(coords[1]-self.lmbstart[1])**2>self.halo**2: 190 if (coords[0]-self.mousedownpos[0])**2+(coords[1]-self.mousedownpos[1])**2>self.halo**2:
168 # they moved enough, it's a level change 191 # they moved enough, it's a level change
169 self.startlevelchange() 192 self.startrectangleselect()
170 193
171 if self.lmbstate=='levelchange': 194
172 delta = (self.lmbstart[1]-ev.y) 195 if self.mode=='levelchange':
196 delta = (self.mousedownpos[1]-ev.y)
173 if self.subeditor: 197 if self.subeditor:
174 self.subeditor.levelchange(self.selectedlights,delta) 198 self.subeditor.levelchange(self.selectedlights,delta)
175 199
176 if self.lmbstate=='rectangle': 200 if self.mode=='rectangle':
177 sr = self.find_withtag('selectrect') 201 sr = self.find_withtag('selectrect')
178 if not sr: 202 if not sr:
179 sr=self.create_rectangle( self.lmbstart[0],self.lmbstart[1],coords[0],coords[1], 203 # make the selection rectangle
180 outlinestipple='gray50',outline='yellow', 204 sr=self.create_rectangle( self.mousedownpos[0],self.mousedownpos[1],coords[0],coords[1],
181 tag='selectrect') 205 outlinestipple='gray50',outline='yellow',tag='selectrect')
182 206
183 # move rectangle with mouse 207 # move rectangle with mouse
184 self.coords(sr,*(self.lmbstart+coords)) 208 self.coords(sr,*(self.mousedownpos+coords))
185 209
186 # redo the dynselection with the new rectangle 210 # redo the dynselection with the new rectangle
187 self.replacedynselection([o for o in self.findoverlappinglights((self.lmbstart+coords),1)]) 211 self.replacedynselection(self.findoverlappinglights((self.mousedownpos+coords),1),
188 212 subtract=control)
189 # need to handle ctrl 213
190 214 def release(self,ev):
191 def leftrelease(self,ev): 215 if self.mode:
192 if self.lmbstate: 216 if self.mode=='rectangle':
193
194 if self.lmbstate=='rectangle':
195 self.delete('selectrect') 217 self.delete('selectrect')
196 218
197 if self.lmbstate=='deselect-or-level': 219 if self.mode=='deselect-or-rectangle':
198 # they didn't move enough to promote the mode to level, so it's a deselect click 220 # they didn't move enough to promote the mode to level, so it's a deselect click
199 self.clearselection() 221 self.clearselection()
200 222
201 # all items that were in dynselection join the selection 223 self.mode=None
202 # self.incorporatedynselection() 224
225 #
226 #
227 #
203 228
204 self.lmbstate=None
205 def startlevelchange(self): 229 def startlevelchange(self):
206 self.lmbstate='levelchange' 230 """sets mode to levelchange AND notifies subeditor. this
231 should be done exactly once (per mouse drag), when you first
232 decide the mode is levelchange"""
233 self.mode='levelchange'
207 if self.subeditor: 234 if self.subeditor:
208 self.subeditor.startlevelchange() 235 self.subeditor.startlevelchange()
209 236 def startrectangleselect(self):
237 """sets mode to rectangle AND checkpoints the current selection"""
238 self.mode='rectangle'
239 self.markfordynselection()
210 # 240 #
211 # light names vs. canvas object tags 241 # light names vs. canvas object tags
212 # 242 #
213 def nametag(self,name): 243 def nametag(self,name):
214 "returns a safe version of the name that won't match other names" 244 "returns a safe version of the name that won't match other names"
231 self.create_oval(aim[0]-2,aim[1]-2, 261 self.create_oval(aim[0]-2,aim[1]-2,
232 aim[0]+2,aim[1]+2, 262 aim[0]+2,aim[1]+2,
233 fill='red',tag=tags+" hotspot") 263 fill='red',tag=tags+" hotspot")
234 self.create_line(location[0],location[1],aim[0],aim[1],fill='lightblue', 264 self.create_line(location[0],location[1],aim[0],aim[1],fill='lightblue',
235 arrow='last',arrowshape="9 15 6",tag='light') 265 arrow='last',arrowshape="9 15 6",tag='light')
236 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'])) 266 # shadow
267 self.create_text(location[0]-1,location[1]+6,
268 anchor='n',text=name,fill='black',
269 tag=tags,**dict([(k,v) for k,v in textstyle.items() if k!='fill']))
270 # text
237 self.create_text(location[0],location[1]+5,anchor='n',text=name,tag=tags,**textstyle) 271 self.create_text(location[0],location[1]+5,anchor='n',text=name,tag=tags,**textstyle)
272
238 self.alllights.append(name) 273 self.alllights.append(name)
239 self.alllighttags[self.nametag(name)]=name 274 self.alllighttags[self.nametag(name)]=name
240 275
241 def getlightbboxes(self,tag): 276 def getlightbboxes(self,tag):
242 """returns a list of bboxes for a light with a given name_ tag. the selection 277 """returns a list of bboxes for a light with a given name_ tag. the selection
246 if 'hotspot' in self.gettags(o): 281 if 'hotspot' in self.gettags(o):
247 bboxes.append(self.bbox(o)) 282 bboxes.append(self.bbox(o))
248 return bboxes 283 return bboxes
249 284
250 def findoverlappinglights(self,box,enclosed=0): 285 def findoverlappinglights(self,box,enclosed=0):
251 "returns all the different name_ tags for lights that are within (or enclosed by) the box" 286 "returns all the different names for lights that are within (or enclosed by) the box"
252 lights=[] 287 lights=[]
253 if enclosed: 288 if enclosed:
254 candidates = self.find_enclosed(*box) 289 candidates = self.find_enclosed(*box)
255 else: 290 else:
256 candidates = self.find_overlapping(*box) 291 candidates = self.find_overlapping(*box)
257 292
258 for o in candidates: 293 for o in candidates:
259 for t in self.gettags(o): 294 for t in self.gettags(o):
260 if t.startswith("name_"): 295 if t.startswith("name_"):
261 n = self.tagtoname(t[5:]) 296 n = self.tagtoname(t[5:])
262 if n and (n not in lights): 297 if n and (n not in lights):
305 340
306 341
307 342
308 if __name__=='__main__': 343 if __name__=='__main__':
309 root=Tk() 344 root=Tk()
345 root.tk_focusFollowsMouse()
310 root.wm_geometry("+376+330") 346 root.wm_geometry("+376+330")
311 s=Stage(root) 347 s=Stage(root)
312 s.setimage('guysanddolls.gif') 348 s.setimage('guysanddolls.gif')
313 s.pack() 349 s.pack()
314 350
315 createlights(s) 351 createlights(s)
352
353 class subediting_standin:
354 def startlevelchange(self):
355 print "start lev change"
356 def levelchange(self,lights,delta):
357 print "change",lights,delta
358 s.setsubediting(subediting_standin())
359
316 root.mainloop() 360 root.mainloop()
317 361