1 from Tkinter import *
2 import Pmw
3 import tkMessageBox
4 import threading
5 from teamwork.widgets.images import loadImages
6
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
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),
27 ('fg',None,self.setColor),
28 ('bg',None,self.setColor),
29 ('floatdomain',True,Pmw.INITOPT),
30
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
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
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
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
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
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
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
131 """Updates the scales to reflect the current distribution
132 """
133 self.map.clear()
134 elements = self['distribution'].domain()
135 elements.sort()
136
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
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
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
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
171 widget = self.component('prob%d' % (row))
172 widget.setvalue('%d' % (100*self['distribution'][element]))
173
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
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
197 """Sets the given slider to the given value
198 """
199 self.setTroughColor(row,value)
200 self.component('scal%d' % (row)).set(value)
201
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
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
232 """Callback for element entry field
233 """
234 widget = self.component('elem%d' % (row))
235 new = float(widget.getvalue())
236 if self['viewprobs']:
237
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
244 self.component('scal%d' % (row)).set(float(new))
245
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
253 if self.lock.acquire(False):
254 self.component('scal%d' % (row)).set(float(new))
255 self.lock.release()
256 else:
257
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
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
282 if self.updateElement(row,new):
283 self.component('elem%d' % (row)).setvalue(getLabel(new))
284 self.setTroughColor(row,new)
285
286 if self['command'] and self.lock.acquire(False):
287 self['command'](self)
288 self.lock.release()
289
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
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
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
319 try:
320 delta = (self['distribution'][element] - new)\
321 / float(len(self['distribution'])-1)
322 except ZeroDivisionError:
323
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
341 row = 0
342 while True:
343 try:
344 widget = self.component('elem%d' % (row))
345 except KeyError:
346
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
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
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
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
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
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
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
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