7 class Counter(Pmw.MegaWidget):
9 def __init__(self, parent = None, **kw):
11 # Define the megawidget options.
14 ('autorepeat', 1, None),
15 ('buttonaspect', 1.0, INITOPT),
16 ('datatype', 'numeric', self._datatype),
17 ('increment', 1, None),
18 ('initwait', 300, None),
19 ('labelmargin', 0, INITOPT),
20 ('labelpos', None, INITOPT),
21 ('orient', 'horizontal', INITOPT),
24 ('repeatrate', 50, None),
25 ('sticky', 'ew', INITOPT),
27 self.defineoptions(kw, optiondefs)
29 # Initialise the base class (after defining the options).
30 Pmw.MegaWidget.__init__(self, parent)
32 # Initialise instance variables.
34 self._normalRelief = None
36 # Create the components.
37 interior = self.interior()
39 # If there is no label, put the arrows and the entry directly
40 # into the interior, otherwise create a frame for them. In
41 # either case the border around the arrows and the entry will
42 # be raised (but not around the label).
43 if self['labelpos'] is None:
45 if not kw.has_key('hull_relief'):
46 frame.configure(relief = 'raised')
47 if not kw.has_key('hull_borderwidth'):
48 frame.configure(borderwidth = 1)
50 frame = self.createcomponent('frame',
52 Tkinter.Frame, (interior,),
53 relief = 'raised', borderwidth = 1)
54 frame.grid(column=2, row=2, sticky=self['sticky'])
55 interior.grid_columnconfigure(2, weight=1)
56 interior.grid_rowconfigure(2, weight=1)
58 # Create the down arrow.
59 self._downArrowBtn = self.createcomponent('downarrow',
61 Tkinter.Canvas, (frame,),
62 width = 16, height = 16, relief = 'raised', borderwidth = 2)
64 # Create the entry field.
65 self._counterEntry = self.createcomponent('entryfield',
66 (('entry', 'entryfield_entry'),), None,
67 Pmw.EntryField, (frame,))
69 # Create the up arrow.
70 self._upArrowBtn = self.createcomponent('uparrow',
72 Tkinter.Canvas, (frame,),
73 width = 16, height = 16, relief = 'raised', borderwidth = 2)
77 orient = self['orient']
78 if orient == 'horizontal':
79 self._downArrowBtn.grid(column = 0, row = 0)
80 self._counterEntry.grid(column = 1, row = 0,
81 sticky = self['sticky'])
82 self._upArrowBtn.grid(column = 2, row = 0)
83 frame.grid_columnconfigure(1, weight = 1)
84 frame.grid_rowconfigure(0, weight = 1)
85 if Tkinter.TkVersion >= 4.2:
86 frame.grid_columnconfigure(0, pad = padx)
87 frame.grid_columnconfigure(2, pad = padx)
88 frame.grid_rowconfigure(0, pad = pady)
89 elif orient == 'vertical':
90 self._upArrowBtn.grid(column = 0, row = 0, sticky = 's')
91 self._counterEntry.grid(column = 0, row = 1,
92 sticky = self['sticky'])
93 self._downArrowBtn.grid(column = 0, row = 2, sticky = 'n')
94 frame.grid_columnconfigure(0, weight = 1)
95 frame.grid_rowconfigure(0, weight = 1)
96 frame.grid_rowconfigure(2, weight = 1)
97 if Tkinter.TkVersion >= 4.2:
98 frame.grid_rowconfigure(0, pad = pady)
99 frame.grid_rowconfigure(2, pad = pady)
100 frame.grid_columnconfigure(0, pad = padx)
102 raise ValueError, 'bad orient option ' + repr(orient) + \
103 ': must be either \'horizontal\' or \'vertical\''
105 self.createlabel(interior)
107 self._upArrowBtn.bind('<Configure>', self._drawUpArrow)
108 self._upArrowBtn.bind('<1>', self._countUp)
109 self._upArrowBtn.bind('<Any-ButtonRelease-1>', self._stopCounting)
110 self._downArrowBtn.bind('<Configure>', self._drawDownArrow)
111 self._downArrowBtn.bind('<1>', self._countDown)
112 self._downArrowBtn.bind('<Any-ButtonRelease-1>', self._stopCounting)
113 self._counterEntry.bind('<Configure>', self._resizeArrow)
114 entry = self._counterEntry.component('entry')
115 entry.bind('<Down>', lambda event, s = self: s._key_decrement(event))
116 entry.bind('<Up>', lambda event, s = self: s._key_increment(event))
118 # Need to cancel the timer if an arrow button is unmapped (eg:
119 # its toplevel window is withdrawn) while the mouse button is
120 # held down. The canvas will not get the ButtonRelease event
121 # if it is not mapped, since the implicit grab is cancelled.
122 self._upArrowBtn.bind('<Unmap>', self._stopCounting)
123 self._downArrowBtn.bind('<Unmap>', self._stopCounting)
125 # Check keywords and initialise options.
126 self.initialiseoptions()
128 def _resizeArrow(self, event):
129 for btn in (self._upArrowBtn, self._downArrowBtn):
130 bw = (string.atoi(btn['borderwidth']) +
131 string.atoi(btn['highlightthickness']))
132 newHeight = self._counterEntry.winfo_reqheight() - 2 * bw
133 newWidth = int(newHeight * self['buttonaspect'])
134 btn.configure(width=newWidth, height=newHeight)
137 def _drawUpArrow(self, event):
138 self._drawArrow(self._upArrowBtn)
140 def _drawDownArrow(self, event):
141 self._drawArrow(self._downArrowBtn)
143 def _drawArrow(self, arrow):
144 if self['orient'] == 'vertical':
145 if arrow == self._upArrowBtn:
150 if arrow == self._upArrowBtn:
154 Pmw.drawarrow(arrow, self['entry_foreground'], direction, 'arrow')
156 def _stopCounting(self, event = None):
157 if self._timerId is not None:
158 self.after_cancel(self._timerId)
160 if self._normalRelief is not None:
161 button, relief = self._normalRelief
162 button.configure(relief=relief)
163 self._normalRelief = None
165 def _countUp(self, event):
166 self._normalRelief = (self._upArrowBtn, self._upArrowBtn.cget('relief'))
167 self._upArrowBtn.configure(relief='sunken')
168 # Force arrow down (it may come up immediately, if increment fails).
169 self._upArrowBtn.update_idletasks()
172 def _countDown(self, event):
173 self._normalRelief = (self._downArrowBtn, self._downArrowBtn.cget('relief'))
174 self._downArrowBtn.configure(relief='sunken')
175 # Force arrow down (it may come up immediately, if increment fails).
176 self._downArrowBtn.update_idletasks()
185 def _key_increment(self, event):
187 self.update_idletasks()
189 def _key_decrement(self, event):
191 self.update_idletasks()
194 datatype = self['datatype']
196 if type(datatype) is types.DictionaryType:
197 self._counterArgs = datatype.copy()
198 if self._counterArgs.has_key('counter'):
199 datatype = self._counterArgs['counter']
200 del self._counterArgs['counter']
204 self._counterArgs = {}
206 if _counterCommands.has_key(datatype):
207 self._counterCommand = _counterCommands[datatype]
208 elif callable(datatype):
209 self._counterCommand = datatype
211 validValues = _counterCommands.keys()
213 raise ValueError, ('bad datatype value "%s": must be a' +
214 ' function or one of %s') % (datatype, validValues)
216 def _forceCount(self, factor):
221 text = self._counterEntry.get()
223 value = apply(self._counterCommand,
224 (text, factor, self['increment']), self._counterArgs)
229 previousICursor = self._counterEntry.index('insert')
230 if self._counterEntry.setentry(value) == Pmw.OK:
231 self._counterEntry.xview('end')
232 self._counterEntry.icursor(previousICursor)
234 def _count(self, factor, first):
240 origtext = self._counterEntry.get()
242 value = apply(self._counterCommand,
243 (origtext, factor, self['increment']), self._counterArgs)
245 # If text is invalid, stop counting.
250 # If incrementing produces an invalid value, restore previous
251 # text and stop counting.
252 previousICursor = self._counterEntry.index('insert')
253 valid = self._counterEntry.setentry(value)
256 self._counterEntry.setentry(origtext)
257 if valid == Pmw.PARTIAL:
260 self._counterEntry.xview('end')
261 self._counterEntry.icursor(previousICursor)
263 if self['autorepeat']:
265 delay = self['initwait']
267 delay = self['repeatrate']
268 self._timerId = self.after(delay,
269 lambda self=self, factor=factor: self._count(factor, 0))
273 Pmw.MegaWidget.destroy(self)
275 Pmw.forwardmethods(Counter, Pmw.EntryField, '_counterEntry')
277 def _changeNumber(text, factor, increment):
278 value = string.atol(text)
280 value = (value / increment) * increment + increment
282 value = ((value - 1) / increment) * increment
284 # Get rid of the 'L' at the end of longs (in python up to 1.5.2).
291 def _changeReal(text, factor, increment, separator = '.'):
292 value = Pmw.stringtoreal(text, separator)
293 div = value / increment
295 # Compare reals using str() to avoid problems caused by binary
296 # numbers being only approximations to decimal numbers.
297 # For example, if value is -0.3 and increment is 0.1, then
298 # int(value/increment) = -2, not -3 as one would expect.
299 if str(div)[-2:] == '.0':
300 # value is an even multiple of increment.
301 div = round(div) + factor
303 # value is not an even multiple of increment.
310 value = div * increment
314 index = string.find(text, '.')
316 text = text[:index] + separator + text[index + 1:]
319 def _changeDate(value, factor, increment, format = 'ymd',
320 separator = '/', yyyy = 0):
322 jdn = Pmw.datestringtojdn(value, format, separator) + factor * increment
324 y, m, d = Pmw.jdntoymd(jdn)
326 for index in range(3):
328 result = result + separator
332 result = result + '%02d' % y
334 result = result + '%02d' % (y % 100)
336 result = result + '%02d' % m
338 result = result + '%02d' % d
342 _SECSPERDAY = 24 * 60 * 60
343 def _changeTime(value, factor, increment, separator = ':', time24 = 0):
344 unixTime = Pmw.timestringtoseconds(value, separator)
346 chunks = unixTime / increment + 1
348 chunks = (unixTime - 1) / increment
349 unixTime = chunks * increment
352 unixTime = unixTime + _SECSPERDAY
353 while unixTime >= _SECSPERDAY:
354 unixTime = unixTime - _SECSPERDAY
361 unixTime = unixTime / 60
363 hours = unixTime / 60
364 return '%s%02d%s%02d%s%02d' % (sign, hours, separator, mins, separator, secs)
366 # hexadecimal, alphabetic, alphanumeric not implemented
368 'numeric' : _changeNumber, # } integer
369 'integer' : _changeNumber, # } these two use the same function
370 'real' : _changeReal, # real number
371 'time' : _changeTime,
372 'date' : _changeDate,