Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | import string |
2 | import sys | |
3 | import types | |
4 | import Tkinter | |
5 | import Pmw | |
6 | ||
7 | class Counter(Pmw.MegaWidget): | |
8 | ||
9 | def __init__(self, parent = None, **kw): | |
10 | ||
11 | # Define the megawidget options. | |
12 | INITOPT = Pmw.INITOPT | |
13 | optiondefs = ( | |
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), | |
22 | ('padx', 0, INITOPT), | |
23 | ('pady', 0, INITOPT), | |
24 | ('repeatrate', 50, None), | |
25 | ('sticky', 'ew', INITOPT), | |
26 | ) | |
27 | self.defineoptions(kw, optiondefs) | |
28 | ||
29 | # Initialise the base class (after defining the options). | |
30 | Pmw.MegaWidget.__init__(self, parent) | |
31 | ||
32 | # Initialise instance variables. | |
33 | self._timerId = None | |
34 | self._normalRelief = None | |
35 | ||
36 | # Create the components. | |
37 | interior = self.interior() | |
38 | ||
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: | |
44 | frame = interior | |
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) | |
49 | else: | |
50 | frame = self.createcomponent('frame', | |
51 | (), None, | |
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) | |
57 | ||
58 | # Create the down arrow. | |
59 | self._downArrowBtn = self.createcomponent('downarrow', | |
60 | (), 'Arrow', | |
61 | Tkinter.Canvas, (frame,), | |
62 | width = 16, height = 16, relief = 'raised', borderwidth = 2) | |
63 | ||
64 | # Create the entry field. | |
65 | self._counterEntry = self.createcomponent('entryfield', | |
66 | (('entry', 'entryfield_entry'),), None, | |
67 | Pmw.EntryField, (frame,)) | |
68 | ||
69 | # Create the up arrow. | |
70 | self._upArrowBtn = self.createcomponent('uparrow', | |
71 | (), 'Arrow', | |
72 | Tkinter.Canvas, (frame,), | |
73 | width = 16, height = 16, relief = 'raised', borderwidth = 2) | |
74 | ||
75 | padx = self['padx'] | |
76 | pady = self['pady'] | |
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) | |
101 | else: | |
102 | raise ValueError, 'bad orient option ' + repr(orient) + \ | |
103 | ': must be either \'horizontal\' or \'vertical\'' | |
104 | ||
105 | self.createlabel(interior) | |
106 | ||
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)) | |
117 | ||
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) | |
124 | ||
125 | # Check keywords and initialise options. | |
126 | self.initialiseoptions() | |
127 | ||
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) | |
135 | self._drawArrow(btn) | |
136 | ||
137 | def _drawUpArrow(self, event): | |
138 | self._drawArrow(self._upArrowBtn) | |
139 | ||
140 | def _drawDownArrow(self, event): | |
141 | self._drawArrow(self._downArrowBtn) | |
142 | ||
143 | def _drawArrow(self, arrow): | |
144 | if self['orient'] == 'vertical': | |
145 | if arrow == self._upArrowBtn: | |
146 | direction = 'up' | |
147 | else: | |
148 | direction = 'down' | |
149 | else: | |
150 | if arrow == self._upArrowBtn: | |
151 | direction = 'right' | |
152 | else: | |
153 | direction = 'left' | |
154 | Pmw.drawarrow(arrow, self['entry_foreground'], direction, 'arrow') | |
155 | ||
156 | def _stopCounting(self, event = None): | |
157 | if self._timerId is not None: | |
158 | self.after_cancel(self._timerId) | |
159 | self._timerId = None | |
160 | if self._normalRelief is not None: | |
161 | button, relief = self._normalRelief | |
162 | button.configure(relief=relief) | |
163 | self._normalRelief = None | |
164 | ||
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() | |
170 | self._count(1, 1) | |
171 | ||
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() | |
177 | self._count(-1, 1) | |
178 | ||
179 | def increment(self): | |
180 | self._forceCount(1) | |
181 | ||
182 | def decrement(self): | |
183 | self._forceCount(-1) | |
184 | ||
185 | def _key_increment(self, event): | |
186 | self._forceCount(1) | |
187 | self.update_idletasks() | |
188 | ||
189 | def _key_decrement(self, event): | |
190 | self._forceCount(-1) | |
191 | self.update_idletasks() | |
192 | ||
193 | def _datatype(self): | |
194 | datatype = self['datatype'] | |
195 | ||
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'] | |
201 | else: | |
202 | datatype = 'numeric' | |
203 | else: | |
204 | self._counterArgs = {} | |
205 | ||
206 | if _counterCommands.has_key(datatype): | |
207 | self._counterCommand = _counterCommands[datatype] | |
208 | elif callable(datatype): | |
209 | self._counterCommand = datatype | |
210 | else: | |
211 | validValues = _counterCommands.keys() | |
212 | validValues.sort() | |
213 | raise ValueError, ('bad datatype value "%s": must be a' + | |
214 | ' function or one of %s') % (datatype, validValues) | |
215 | ||
216 | def _forceCount(self, factor): | |
217 | if not self.valid(): | |
218 | self.bell() | |
219 | return | |
220 | ||
221 | text = self._counterEntry.get() | |
222 | try: | |
223 | value = apply(self._counterCommand, | |
224 | (text, factor, self['increment']), self._counterArgs) | |
225 | except ValueError: | |
226 | self.bell() | |
227 | return | |
228 | ||
229 | previousICursor = self._counterEntry.index('insert') | |
230 | if self._counterEntry.setentry(value) == Pmw.OK: | |
231 | self._counterEntry.xview('end') | |
232 | self._counterEntry.icursor(previousICursor) | |
233 | ||
234 | def _count(self, factor, first): | |
235 | if not self.valid(): | |
236 | self.bell() | |
237 | return | |
238 | ||
239 | self._timerId = None | |
240 | origtext = self._counterEntry.get() | |
241 | try: | |
242 | value = apply(self._counterCommand, | |
243 | (origtext, factor, self['increment']), self._counterArgs) | |
244 | except ValueError: | |
245 | # If text is invalid, stop counting. | |
246 | self._stopCounting() | |
247 | self.bell() | |
248 | return | |
249 | ||
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) | |
254 | if valid != Pmw.OK: | |
255 | self._stopCounting() | |
256 | self._counterEntry.setentry(origtext) | |
257 | if valid == Pmw.PARTIAL: | |
258 | self.bell() | |
259 | return | |
260 | self._counterEntry.xview('end') | |
261 | self._counterEntry.icursor(previousICursor) | |
262 | ||
263 | if self['autorepeat']: | |
264 | if first: | |
265 | delay = self['initwait'] | |
266 | else: | |
267 | delay = self['repeatrate'] | |
268 | self._timerId = self.after(delay, | |
269 | lambda self=self, factor=factor: self._count(factor, 0)) | |
270 | ||
271 | def destroy(self): | |
272 | self._stopCounting() | |
273 | Pmw.MegaWidget.destroy(self) | |
274 | ||
275 | Pmw.forwardmethods(Counter, Pmw.EntryField, '_counterEntry') | |
276 | ||
277 | def _changeNumber(text, factor, increment): | |
278 | value = string.atol(text) | |
279 | if factor > 0: | |
280 | value = (value / increment) * increment + increment | |
281 | else: | |
282 | value = ((value - 1) / increment) * increment | |
283 | ||
284 | # Get rid of the 'L' at the end of longs (in python up to 1.5.2). | |
285 | rtn = str(value) | |
286 | if rtn[-1] == 'L': | |
287 | return rtn[:-1] | |
288 | else: | |
289 | return rtn | |
290 | ||
291 | def _changeReal(text, factor, increment, separator = '.'): | |
292 | value = Pmw.stringtoreal(text, separator) | |
293 | div = value / increment | |
294 | ||
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 | |
302 | else: | |
303 | # value is not an even multiple of increment. | |
304 | div = int(div) * 1.0 | |
305 | if value < 0: | |
306 | div = div - 1 | |
307 | if factor > 0: | |
308 | div = (div + 1) | |
309 | ||
310 | value = div * increment | |
311 | ||
312 | text = str(value) | |
313 | if separator != '.': | |
314 | index = string.find(text, '.') | |
315 | if index >= 0: | |
316 | text = text[:index] + separator + text[index + 1:] | |
317 | return text | |
318 | ||
319 | def _changeDate(value, factor, increment, format = 'ymd', | |
320 | separator = '/', yyyy = 0): | |
321 | ||
322 | jdn = Pmw.datestringtojdn(value, format, separator) + factor * increment | |
323 | ||
324 | y, m, d = Pmw.jdntoymd(jdn) | |
325 | result = '' | |
326 | for index in range(3): | |
327 | if index > 0: | |
328 | result = result + separator | |
329 | f = format[index] | |
330 | if f == 'y': | |
331 | if yyyy: | |
332 | result = result + '%02d' % y | |
333 | else: | |
334 | result = result + '%02d' % (y % 100) | |
335 | elif f == 'm': | |
336 | result = result + '%02d' % m | |
337 | elif f == 'd': | |
338 | result = result + '%02d' % d | |
339 | ||
340 | return result | |
341 | ||
342 | _SECSPERDAY = 24 * 60 * 60 | |
343 | def _changeTime(value, factor, increment, separator = ':', time24 = 0): | |
344 | unixTime = Pmw.timestringtoseconds(value, separator) | |
345 | if factor > 0: | |
346 | chunks = unixTime / increment + 1 | |
347 | else: | |
348 | chunks = (unixTime - 1) / increment | |
349 | unixTime = chunks * increment | |
350 | if time24: | |
351 | while unixTime < 0: | |
352 | unixTime = unixTime + _SECSPERDAY | |
353 | while unixTime >= _SECSPERDAY: | |
354 | unixTime = unixTime - _SECSPERDAY | |
355 | if unixTime < 0: | |
356 | unixTime = -unixTime | |
357 | sign = '-' | |
358 | else: | |
359 | sign = '' | |
360 | secs = unixTime % 60 | |
361 | unixTime = unixTime / 60 | |
362 | mins = unixTime % 60 | |
363 | hours = unixTime / 60 | |
364 | return '%s%02d%s%02d%s%02d' % (sign, hours, separator, mins, separator, secs) | |
365 | ||
366 | # hexadecimal, alphabetic, alphanumeric not implemented | |
367 | _counterCommands = { | |
368 | 'numeric' : _changeNumber, # } integer | |
369 | 'integer' : _changeNumber, # } these two use the same function | |
370 | 'real' : _changeReal, # real number | |
371 | 'time' : _changeTime, | |
372 | 'date' : _changeDate, | |
373 | } |