Package teamwork :: Package widgets :: Module pmfScale
[hide private]
[frames] | no frames]

Source Code for Module teamwork.widgets.pmfScale

  1  from Tkinter import * 
  2  import Pmw 
  3  import tkMessageBox 
  4  import threading 
  5  from teamwork.widgets.images import loadImages 
  6   
7 -class PMFScale(Pmw.MegaWidget):
8 """ 9 Widget for displaying a probability mass function (PMF) 10 @ivar map: mapping from row index to distribution element 11 @type map: dict 12 @ivar lock: a C{Lock} used to avoid asynchronyous update problems 13 @type lock 14 @cvar epsilon: threshold for determining zero probabilites 15 @type epsilon: float 16 """ 17 epsilon = 1e-10 18
19 - def __init__(self,parent=None,**kw):
20 self.map = {} 21 self.lock = threading.Lock() 22 optiondefs = ( 23 ('distribution', {}, self.setDistribution), 24 ('state','normal',self.setState), 25 ('viewprobs',False,self.setView), 26 ('command',None,None), # lambda <my name>: ... 27 ('fg',None,self.setColor), 28 ('bg',None,self.setColor), 29 ('floatdomain',True,Pmw.INITOPT), 30 # Callbacks for expand/collapse options 31 ('expand',None,Pmw.INITOPT), 32 ('collapse',None,Pmw.INITOPT), 33 ) 34 self.defineoptions(kw,optiondefs) 35 Pmw.MegaWidget.__init__(self,parent) 36 if self['expand']: 37 self.interior().grid_columnconfigure(0,weight=0) 38 self.images = loadImages({'minus': 'icons/minus.png', 39 'plus': 'icons/plus.png',}) 40 self.start = 1 41 else: 42 self.images = {} 43 self.start = 0 44 if self['floatdomain']: 45 self.interior().grid_columnconfigure(self.start+0,weight=0) 46 else: 47 self.interior().grid_columnconfigure(self.start+0,weight=1) 48 self.interior().grid_columnconfigure(self.start+1,weight=2) 49 self.interior().grid_columnconfigure(self.start+2,weight=0) 50 self.initialiseoptions(PMFScale)
51
52 - def makeRow(self,row,element):
53 """Creates the widgets in the given row of the scale 54 @param row: the row to put the widgets on 55 @type row: int 56 @param element: the element for this row (in whatever form it is) 57 """ 58 offset = 0 59 if self['expand']: 60 for other in range(row): 61 if self.isExpanded(other): 62 offset += 1 63 if self['expand']: 64 # Create expandable bit 65 button = self.createcomponent('view%d' % (row),(),'element', 66 Label,(self.interior(),)) 67 if self.images.has_key('plus'): 68 button.configure(image=self.images['plus']) 69 else: 70 button.configure(text='+') 71 button.bind('<ButtonRelease-1>',self.expand) 72 button.grid(row=offset+row,column=0) 73 # Create entry for element value 74 self.map[row] = element 75 cmd = lambda s=self,r=row: s.setElement(r) 76 widget = self.createcomponent('elem%d' % (row),(),'element', 77 Pmw.EntryField, 78 (self.interior(),), 79 hull_bg=self['bg'], 80 entry_fg=self['fg'], 81 entry_bg=self['bg'], 82 command=cmd) 83 if self['floatdomain']: 84 widget.configure(entry_width=4) 85 widget.grid(row=offset+row,column=self.start+0) 86 else: 87 widget.configure(entry_state='readonly',entry_bd=0) 88 widget.grid(row=offset+row,column=self.start+0,sticky='ew') 89 label = getLabel(element) 90 if widget.get() != label: 91 widget.setvalue(label) 92 # Create scale for element value 93 cmd = lambda value,s=self,r=row: s.update(r,value) 94 widget = self.createcomponent('scal%d' % (row),(),'element', 95 Scale,(self.interior(),), 96 orient='horizontal', 97 fg=self['fg'],bg=self['bg'], 98 resolution=0.01,command=cmd, 99 to=1.,showvalue=False) 100 if self['viewprobs']: 101 widget.configure(from_=0.) 102 self.setSlider(row,self['distribution'][element]) 103 else: 104 widget.configure(from_=-1.) 105 self.setSlider(row,element) 106 widget.grid(row=offset+row,column=self.start+1,sticky='ew') 107 # Create probability indicator 108 cmd = lambda s=self,r=row: s.setProbability(r) 109 widget = self.createcomponent('prob%d' % (row),(),'element', 110 Pmw.EntryField, 111 (self.interior(),), 112 validate={'min':0,'max':100, 113 'validator':'integer'}, 114 labelpos='e',label_text='%', 115 label_bg=self['bg'], 116 label_fg=self['fg'], 117 hull_bg=self['bg'], 118 entry_fg=self['fg'], 119 entry_bg=self['bg'], 120 entry_justify='right', 121 entry_width=3,command=cmd) 122 widget.setvalue('%d' % (100*self['distribution'][element])) 123 # Enable probability slider iff more than one element in distribution 124 if len(self['distribution']) > 1 and self['state'] == 'normal': 125 widget.configure(entry_state='normal') 126 else: 127 widget.configure(entry_state='disabled') 128 widget.grid(row=offset+row,column=self.start+2)
129
130 - def setDistribution(self):
131 """Updates the scales to reflect the current distribution 132 """ 133 self.map.clear() 134 elements = self['distribution'].domain() 135 elements.sort() 136 # Hide unneeded widgets 137 for name in self.components(): 138 if self.componentgroup(name) == 'element': 139 if int(name[4:]) >= len(elements): 140 self.component(name).grid_forget() 141 offset = 0 142 for row in range(len(elements)): 143 # Check whether this row already exists; otherwise, create it 144 try: 145 widget = self.component('elem%d' % (row)) 146 except KeyError: 147 if self.lock.acquire(): 148 self.makeRow(row,elements[row]) 149 self.lock.release() 150 if self['expand']: 151 self.component('view%d' % (row)).grid(row=offset+row,column=0) 152 # Create entry for element value 153 element = elements[row] 154 self.map[row] = element 155 widget = self.component('elem%d' % (row)) 156 if self['floatdomain']: 157 widget.grid(row=offset+row,column=self.start+0) 158 else: 159 widget.grid(row=offset+row,column=self.start+0,sticky='ew') 160 label = getLabel(element) 161 if widget.get() != label: 162 widget.setvalue(label) 163 # Create scale for element value 164 widget = self.component('scal%d' % (row)) 165 if self['viewprobs']: 166 self.setSlider(row,self['distribution'][element]) 167 else: 168 self.setSlider(row,element) 169 widget.grid(row=offset+row,column=self.start+1,sticky='ew') 170 # Create probability indicator 171 widget = self.component('prob%d' % (row)) 172 widget.setvalue('%d' % (100*self['distribution'][element])) 173 # Enable probability slider iff more than one element in distribution 174 if len(self['distribution']) > 1 and self['state'] == 'normal': 175 widget.configure(entry_state='normal') 176 else: 177 widget.configure(entry_state='disabled') 178 widget.grid(row=offset+row,column=self.start+2) 179 if self['expand'] and self.isExpanded(row): 180 offset += 1 181 self.component('pane%d' % (row)).grid(row=offset+row,column=1, 182 columnspan=3,sticky='ewns')
183
184 - def setTroughColor(self,row,value):
185 """Sets the trough color of the given slider for the given value 186 """ 187 widget = self.component('scal%d' % (row)) 188 if self['viewprobs']: 189 percent = value 190 lo,hi = '#ffffff','#000000' 191 else: 192 percent = (float(value)+1.)/2. 193 lo,hi = '#ff0000','#00ff00' 194 widget.configure(troughcolor=blend(lo,hi,percent))
195
196 - def setSlider(self,row,value):
197 """Sets the given slider to the given value 198 """ 199 self.setTroughColor(row,value) 200 self.component('scal%d' % (row)).set(value)
201
202 - def addElement(self):
203 """Add a new element to distribution 204 """ 205 new = 0. 206 while new in self['distribution'].domain(): 207 if new > self.epsilon: 208 new = -new 209 else: 210 new += 0.01 211 self['distribution'][new] = 0. 212 self.setDistribution()
213
214 - def setView(self):
215 """Switch sliders to show elements or probabilities as appropriate 216 """ 217 row = 0 218 while True: 219 try: 220 widget = self.component('scal%d' % (row)) 221 except KeyError: 222 break 223 if self['viewprobs']: 224 self.setSlider(row,self['distribution'][self.map[row]]) 225 widget.configure(from_=0.) 226 else: 227 widget.configure(from_=-1.) 228 self.setSlider(row,float(self.map[row])) 229 row += 1
230
231 - def setElement(self,row):
232 """Callback for element entry field 233 """ 234 widget = self.component('elem%d' % (row)) 235 new = float(widget.getvalue()) 236 if self['viewprobs']: 237 # Not viewing element, but need to update behind the scenes 238 self.updateElement(row,new) 239 if self['command'] and self.lock.acquire(False): 240 self['command'](self) 241 self.lock.release() 242 else: 243 # Update the visible scale and let the callback handle the rest 244 self.component('scal%d' % (row)).set(float(new))
245
246 - def setProbability(self,row):
247 """Callback for probability entry field 248 """ 249 widget = self.component('prob%d' % (row)) 250 new = float(widget.getvalue())/100. 251 if self['viewprobs']: 252 # Update the visible scale and let the callback handle the rest 253 if self.lock.acquire(False): 254 self.component('scal%d' % (row)).set(float(new)) 255 self.lock.release() 256 else: 257 # Not viewing probability, but need to update behind the scenes 258 self.updateProbability(row,new) 259 if self['command'] and self.lock.acquire(False): 260 self['command'](self) 261 self.lock.release()
262
263 - def update(self,row,new=None):
264 """Slider callback""" 265 new = float(new) 266 if self['viewprobs']: 267 if self.lock.acquire(False): 268 change = self.updateProbability(row,new) 269 if change and new < self.epsilon: 270 # Handle deletion of element if zero probability 271 element = self.map[row] 272 if self['floatdomain']: 273 msg = 'Would you like to delete element %5.2f?' % (element) 274 else: 275 msg = 'Would you like to delete element %s?' % (element) 276 if tkMessageBox.askyesno('Delete?',msg): 277 del self['distribution'][element] 278 self.setDistribution() 279 self.lock.release() 280 else: 281 # Change the element for this scale 282 if self.updateElement(row,new): 283 self.component('elem%d' % (row)).setvalue(getLabel(new)) 284 self.setTroughColor(row,new) 285 # Do callback (only once) 286 if self['command'] and self.lock.acquire(False): 287 self['command'](self) 288 self.lock.release()
289
290 - def updateElement(self,row,new):
291 """Updates an element in the distribution based on a change 292 (slider or entry) 293 @return: C{True} if there is any change; otherwise, C{False} 294 @rtype: bool 295 """ 296 old = self.map[row] 297 if old != new: 298 if new in self['distribution'].domain(): 299 # Duplicate! Run for the hills! 300 tkMessageBox.showwarning('Duplicate!','Duplicate elements are not allowed.') 301 return False 302 else: 303 prob = self['distribution'][old] 304 del self['distribution'][old] 305 self['distribution'][new] = prob 306 self.map[row] = new 307 return True 308 else: 309 return False
310
311 - def updateProbability(self,row,new):
312 """Updates an probability in the distribution based on a change 313 (slider or entry) 314 @return: C{True} if there is any change; otherwise, C{False} 315 @rtype: bool 316 """ 317 element = self.map[row] 318 # Figure out much probability mass must be re-distributed 319 try: 320 delta = (self['distribution'][element] - new)\ 321 / float(len(self['distribution'])-1) 322 except ZeroDivisionError: 323 # Uh, maybe we're OK? 324 return 325 if abs(delta) < self.epsilon: 326 return False 327 for otherRow,otherElement in self.map.items(): 328 if otherRow == row: 329 self['distribution'][otherElement] = new 330 if self['viewprobs']: 331 self.setTroughColor(row,new) 332 else: 333 self['distribution'][otherElement] += delta 334 if self['viewprobs']: 335 self.setSlider(otherRow,self['distribution'][otherElement]) 336 text = '%d' % (100*self['distribution'][otherElement]) 337 self.component('prob%d' % (otherRow)).setvalue(text) 338 return True
339
340 - def setState(self):
341 row = 0 342 while True: 343 try: 344 widget = self.component('elem%d' % (row)) 345 except KeyError: 346 # Already done all of the rows 347 break 348 if self['floatdomain']: 349 widget.configure(entry_state=self['state']) 350 self.component('scal%d' % (row)).configure(state=self['state']) 351 self.component('prob%d_entry' % (row)).configure(state=self['state']) 352 row += 1
353
354 - def expand(self,event):
355 for name in self.components(): 356 if self.component(name) is event.widget: 357 break 358 else: 359 raise NameError,'Unable to find widget' 360 row = int(name[4:]) 361 if self.isExpanded(row): 362 # Collapse 363 if self['collapse']: 364 self['collapse'](self.map[row]) 365 self.destroycomponent('pane%d' % (row)) 366 event.widget.configure(image=self.images['plus']) 367 else: 368 # Expand 369 frame = self.createcomponent('pane%d' % (row),(),None,Frame, 370 (self.interior(),),bd=1,relief='groove') 371 self['expand'](self.map[row],frame) 372 event.widget.configure(image=self.images['minus']) 373 self.setDistribution()
374
375 - def isExpanded(self,row):
376 """ 377 @param row: the row of interest 378 @type row: int 379 @return: C{True} iff the given row's details pane is expanded 380 @rtype: bool 381 """ 382 widget = self.component('view%d' % (row)) 383 return str(widget.cget('image')) == str(self.images['minus'])
384
385 - def setColor(self):
386 """Updates the foreground and background colors for all component widgets 387 """ 388 self.interior().configure(bg=self['bg']) 389 row = 0 390 while True: 391 try: 392 widget = self.component('elem%d' % (row)) 393 except KeyError: 394 # Already done all of the rows 395 break 396 widget.component('entry').configure(fg=self['fg'],bg=self['bg']) 397 widget.component('hull').configure(bg=self['bg']) 398 self.component('scal%d' % (row)).configure(fg=self['fg'],bg=self['bg']) 399 self.component('prob%d_entry' % (row)).configure(fg=self['fg'], 400 bg=self['bg']) 401 self.component('prob%d_label' % (row)).configure(fg=self['fg'], 402 bg=self['bg']) 403 self.component('prob%d_hull' % (row)).configure(bg=self['bg']) 404 row += 1
405
406 -def blend(color1,color2,percentage):
407 """ 408 Generates a color a given point between two extremes. If the percentage is less than 0, then color1 is returned. If over 1, then color2 is returned. 409 @param color1,color2: the two colors representing the opposite ends of the spectrum 410 @type color1,color2: str 411 @param percentage: the percentage of the spectrum between the two where this point is (0. represents color1, 1. represents color2) 412 @type percentage: float 413 @return: string RGB blending two colors (string RGB) by float percent 414 @rtype: str 415 """ 416 red1 = int(color1[1:3],16) 417 red2 = int(color2[1:3],16) 418 green1 = int(color1[3:5],16) 419 green2 = int(color2[3:5],16) 420 blue1 = int(color1[5:7],16) 421 blue2 = int(color2[5:7],16) 422 hi = {'r':max(red1,red2),'g':max(green1,green2),'b':max(blue1,blue2)} 423 lo = {'r':min(red1,red2),'g':min(green1,green2),'b':min(blue1,blue2)} 424 red = (1.-percentage)*float(red1) + percentage*float(red2) 425 red = min(max(red,lo['r']),hi['r']) 426 green = (1.-percentage)*float(green1) + percentage*float(green2) 427 green = min(max(green,lo['g']),hi['g']) 428 blue = (1.-percentage)*float(blue1) + percentage*float(blue2) 429 blue = min(max(blue,lo['b']),hi['b']) 430 return '#%02x%02x%02x' % (red,green,blue)
431
432 -def getLabel(element):
433 """Generates a canonical string representation of an element 434 @rtype: str 435 """ 436 if isinstance(element,float): 437 return '%5.2f' % (element) 438 elif isinstance(element,int): 439 return '%d' % (element) 440 else: 441 return str(element)
442