1 # Copyright (c) 2008, Guilherme Polo
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are met:
6 #
7 # * Redistributions of source code must retain the above copyright notice,
8 # this list of conditions and the following disclaimer.
9 # * Redistributions in binary form must reproduce the above copyright notice,
10 # this list of conditions and the following disclaimer in the documentation
11 # and/or other materials provided with the distribution.
12 #
13 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
17 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
23 # POSSIBILITY OF SUCH DAMAGE.
24
25 """
26 This contains a wrapper class for the tktable widget as well a class for using
27 tcl arrays that are, in some instances, required by tktable.
28 """
29
30 __author__ = "Guilherme Polo <ggpolo@gmail.com>"
31
32 __all__ = ["ArrayVar", "Table"]
33
34 import os
35 import Tkinter
36
37 def _setup_master(master):
38 if master is None:
39 if Tkinter._support_default_root:
40 master = Tkinter._default_root or Tkinter.Tk()
41 else:
42 raise RuntimeError("No master specified and Tkinter is "
43 "configured to not support default master")
44 return master
45
46 class ArrayVar(Tkinter.Variable):
47 """Class for handling Tcl arrays.
48
49 An array is actually an associative array in Tcl, so this class supports
50 some dict operations.
51 """
52
53 def __init__(self, master=None, name=None):
54 # Tkinter.Variable.__init__ is not called on purpose! I don't wanna
55 # see an ugly _default value in the pretty array.
56 self._master = _setup_master(master)
57 self._tk = self._master.tk
58 if name:
59 self._name = name
60 else:
61 self._name = 'PY_VAR%s' % id(self)
62
63 def __del__(self):
64 if bool(self._tk.call('info', 'exists', self._name)):
65 self._tk.globalunsetvar(self._name)
66
67 def __len__(self):
68 return int(self._tk.call('array', 'size', str(self)))
69
70 def __getitem__(self, key):
71 return self.get(key)
72
73 def __setitem__(self, key, value):
74 self.set(**{str(key): value})
75
76 def names(self):
77 return self._tk.call('array', 'names', self._name)
78
79 def get(self, key=None):
80 if key is None:
81 flatten_pairs = self._tk.call('array', 'get', str(self))
82 return dict(zip(flatten_pairs[::2], flatten_pairs[1::2]))
83
84 return self._tk.globalgetvar(str(self), str(key))
85
86 def set(self, **kw):
87 self._tk.call('array', 'set', str(self), Tkinter._flatten(kw.items()))
88
89 def unset(self, pattern=None):
90 """Unsets all of the elements in the array. If pattern is given, only
91 the elements that match pattern are unset. """
92 self._tk.call('array', 'unset', str(self), pattern)
93
94
95 _TKTABLE_LOADED = False
96
97 class Table(Tkinter.Widget):
98 """Create and manipulate tables."""
99
100 _switches = ('holddimensions', 'holdselection', 'holdtags', 'holdwindows',
101 'keeptitles', '-')
102 _tabsubst_format = ('%c', '%C', '%i', '%r', '%s', '%S', '%W')
103 _tabsubst_commands = ('browsecommand', 'browsecmd', 'command',
104 'selectioncommand', 'selcmd',
105 'validatecommand', 'valcmd')
106
107 def __init__(self, master=None, **kw):
108 master = _setup_master(master)
109 global _TKTABLE_LOADED
110 if not _TKTABLE_LOADED:
111 tktable_lib = os.environ.get('TKTABLE_LIBRARY')
112 if tktable_lib:
113 master.tk.eval('global auto_path; '
114 'lappend auto_path {%s}' % tktable_lib)
115 master.tk.call('package', 'require', 'Tktable')
116 _TKTABLE_LOADED = True
117
118 Tkinter.Widget.__init__(self, master, 'table', kw)
119
120
121 def _options(self, cnf, kw=None):
122 if kw:
123 cnf = Tkinter._cnfmerge((cnf, kw))
124 else:
125 cnf = Tkinter._cnfmerge(cnf)
126
127 res = ()
128 for k, v in cnf.iteritems():
129 if callable(v):
130 if k in self._tabsubst_commands:
131 v = "%s %s" % (self._register(v, self._tabsubst),
132 ' '.join(self._tabsubst_format))
133 else:
134 v = self._register(v)
135 res += ('-%s' % k, v)
136
137 return res
138
139
140 def _tabsubst(self, *args):
141 if len(args) != len(self._tabsubst_format):
142 return args
143
144 tk = self.tk
145 c, C, i, r, s, S, W = args
146 e = Tkinter.Event()
147
148 e.widget = self
149 e.c = tk.getint(c)
150 e.i = tk.getint(i)
151 e.r = tk.getint(r)
152 e.C = "%d,%d" % (e.r, e.c)
153 e.s = s
154 e.S = S
155 try:
156 e.W = self._nametowidget(W)
157 except KeyError:
158 e.W = None
159
160 return (e,)
161
162
163 def _handle_switches(self, args):
164 args = args or ()
165 return tuple(('-%s' % x) for x in args if x in self._switches)
166
167
168 def activate(self, index):
169 """Set the active cell to the one indicated by index."""
170 self.tk.call(self._w, 'activate', index)
171
172
173 def bbox(self, first, last=None):
174 """Return the bounding box for the specified cell (range) as a
175 4-tuple of x, y, width and height in pixels. It clips the box to
176 the visible portion, if any, otherwise an empty tuple is returned."""
177 return self._getints(self.tk.call(self._w, 'bbox', first, last)) or ()
178
179
180 def clear(self, option, first=None, last=None):
181 """This is a convenience routine to clear certain state information
182 managed by the table. first and last represent valid table indices.
183 If neither are specified, then the command operates on the whole
184 table."""
185 self.tk.call(self._w, 'clear', option, first, last)
186
187
188 def clear_cache(self, first=None, last=None):
189 """Clear the specified section of the cache, if the table has been
190 keeping one."""
191 self.clear('cache', first, last)
192
193
194 def clear_sizes(self, first=None, last=None):
195 """Clear the specified row and column areas of specific height/width
196 dimensions. When just one index is specified, for example 2,0, that
197 is interpreted as row 2 and column 0."""
198 self.clear('sizes', first, last)
199
200
201 def clear_tags(self, first=None, last=None):
202 """Clear the specified area of tags (all row, column and cell tags)."""
203 self.clear('tags', first, last)
204
205
206 def clear_all(self, first=None, last=None):
207 """Perform all of the above clear functions on the specified area."""
208 self.clear('all', first, last)
209
210
211 def curselection(self, value=None):
212 """With no arguments, it returns the sorted indices of the currently
213 selected cells. Otherwise it sets all the selected cells to the given
214 value if there is an associated ArrayVar and the state is not
215 disabled."""
216 result = self.tk.call(self._w, 'curselection', value)
217 if value is None:
218 return result
219
220
221 def curvalue(self, value=None):
222 """If no value is given, the value of the cell being edited (indexed
223 by active) is returned, else it is set to the given value. """
224 return self.tk.call(self._w, 'curvalue', value)
225
226
227 def delete_active(self, index1, index2=None):
228 """Deletes text from the active cell. If only one index is given,
229 it deletes the character after that index, otherwise it deletes from
230 the first index to the second. index can be a number, insert or end."""
231 self.tk.call(self._w, 'delete', 'active', index1, index2)
232
233
234 def delete_cols(self, index, count=None, switches=None):
235 args = self._handle_switches(switches) + (index, count)
236 self.tk.call(self._w, 'delete', 'cols', *args)
237
238
239 def delete_rows(self, index, count=None, switches=None):
240 args = self._handle_switches(switches) + (index, count)
241 self.tk.call(self._w, 'delete', 'rows', *args)
242
243
244 def get(self, first, last=None):
245 """Returns the value of the cells specified by the table indices
246 first and (optionally) last."""
247 return self.tk.call(self._w, 'get', first, last)
248
249
250 def height(self, row=None, **kwargs):
251 """If row and kwargs are not given, a list describing all rows for
252 which a width has been set is returned.
253 If row is given, the height of that row is returnd.
254 If kwargs is given, then it sets the key/value pairs, where key is a
255 row and value represents the height for the row."""
256 if row is None and not kwargs:
257 pairs = self.tk.splitlist(self.tk.call(self._w, 'height'))
258 return dict(pair.split() for pair in pairs)
259 elif row:
260 return int(self.tk.call(self._w, 'height', str(row)))
261
262 args = Tkinter._flatten(kwargs.items())
263 self.tk.call(self._w, 'height', *args)
264
265
266 def hidden(self, *args):
267 """When called without args, it returns all the hidden cells (those
268 cells covered by a spanning cell). If one index is specified, it
269 returns the spanning cell covering that index, if any. If multiple
270 indices are specified, it returns 1 if all indices are hidden cells,
271 0 otherwise."""
272 return self.tk.call(self._w, 'hidden', *args)
273
274
275 def icursor(self, arg=None):
276 """If arg is not specified, return the location of the insertion
277 cursor in the active cell. Otherwise, set the cursor to that point in
278 the string.
279
280 0 is before the first character, you can also use insert or end for
281 the current insertion point or the end of the text. If there is no
282 active cell, or the cell or table is disabled, this will return -1."""
283 return self.tk.call(self._w, 'icursor', arg)
284
285
286 def index(self, index, rc=None):
287 """Return the integer cell coordinate that corresponds to index in the
288 form row, col. If rc is specified, it must be either 'row' or 'col' so
289 only the row or column index is returned."""
290 res = self.tk.call(self._w, 'index', index, rc)
291 if rc is None:
292 return res
293 else:
294 return int(res)
295
296
297 def insert_active(self, index, value):
298 """The value is a text string which is inserted at the index postion
299 of the active cell. The cursor is then positioned after the new text.
300 index can be a number, insert or end. """
301 self.tk.call(self._w, 'insert', 'active', index, value)
302
303
304 def insert_cols(self, index, count=None, switches=None):
305 args = self._handle_switches(switches) + (index, count)
306 self.tk.call(self._w, 'insert', 'cols', *args)
307
308
309 def insert_rows(self, index, count=None, switches=None):
310 args = self._handle_switches(switches) + (index, count)
311 self.tk.call(self._w, 'insert', 'rows', *args)
312
313
314 #def postscript(self, **kwargs):
315 # """Skip this command if you are under Windows.
316 #
317 # Accepted options:
318 # colormap, colormode, file, channel, first, fontmap, height,
319 # last, pageanchor, pageheight, pagewidth, pagex, pagey, rotate,
320 # width, x, y
321 # """
322 # args = ()
323 # for key, val in kwargs.iteritems():
324 # args += ('-%s' % key, val)
325 #
326 # return self.tk.call(self._w, 'postscript', *args)
327
328
329 def reread(self):
330 """Rereads the old contents of the cell back into the editing buffer.
331 Useful for a key binding when <Escape> is pressed to abort the edit
332 (a default binding)."""
333 self.tk.call(self._w, 'reread')
334
335
336 def scan_mark(self, x, y):
337 self.tk.call(self._w, 'scan', 'mark', x, y)
338
339
340 def scan_dragto(self, x, y):
341 self.tk.call(self._w, 'scan', 'dragto', x, y)
342
343
344 def see(self, index):
345 self.tk.call(self._w, 'see', index)
346
347
348 def selection_anchor(self, index):
349 self.tk.call(self._w, 'selection', 'anchor', index)
350
351
352 def selection_clear(self, first, last=None):
353 self.tk.call(self._w, 'selection', 'clear', first, last)
354
355
356 def selection_includes(self, index):
357 return self.getboolean(self.tk.call(self._w, 'selection', 'includes',
358 index))
359
360
361 def selection_set(self, first, last=None):
362 self.tk.call(self._w, 'selection', 'set', first, last)
363
364
365 def set(self, rc=None, index=None, *args, **kwargs):
366 """If rc is specified (either 'row' or 'col') then it is assumes that
367 args (if given) represents values which will be set into the
368 subsequent columns (if row is specified) or rows (for col).
369 If index is not None and args is not given, then it will return the
370 value(s) for the cell(s) specified.
371
372 If kwargs is given, assumes that each key in kwargs is a index in this
373 table and sets the specified index to the associated value. Table
374 validation will not be triggered via this method.
375
376 Note that the table must have an associated array (defined through the
377 variable option) in order to this work."""
378 if not args and index is not None:
379 if rc:
380 args = (rc, index)
381 else:
382 args = (index, )
383 return self.tk.call(self._w, 'set', *args)
384
385 if rc is None:
386 args = Tkinter._flatten(kwargs.items())
387 self.tk.call(self._w, 'set', *args)
388 else:
389 self.tk.call(self._w, 'set', rc, index, args)
390
391
392 def spans(self, index=None, **kwargs):
393 """Manipulate row/col spans.
394
395 When called with no arguments, all known spans are returned as a dict.
396 When called with only the index, the span for that index only is
397 returned, if any. Otherwise kwargs is assumed to contain keys/values
398 pairs used to set spans. A span starts at the row,col defined by a key
399 and continues for the specified number of rows,cols specified by
400 its value. A span of 0,0 unsets any span on that cell."""
401 if kwargs:
402 args = Tkinter._flatten(kwargs.items())
403 self.tk.call(self._w, 'spans', *args)
404 else:
405 return self.tk.call(self._w, 'spans', index)
406
407
408 def tag_cell(self, tagname, *args):
409 return self.tk.call(self._w, 'tag', 'cell', tagname, *args)
410
411
412 def tag_cget(self, tagname, option):
413 return self.tk.call(self._w, 'tag', 'cget', tagname, '-%s' % option)
414
415
416 def tag_col(self, tagname, *args):
417 return self.tk.call(self._w, 'tag', 'col', tagname, *args)
418
419
420 def tag_configure(self, tagname, option=None, **kwargs):
421 """Query or modify options associated with the tag given by tagname.
422
423 If no option is specified, a dict describing all of the available
424 options for tagname is returned. If option is specified, then the
425 command returns a list describing the one named option. Lastly, if
426 kwargs is given then it corresponds to option-value pairs that should
427 be modified."""
428 if option is None and not kwargs:
429 split1 = self.tk.splitlist(
430 self.tk.call(self._w, 'tag', 'configure', tagname))
431
432 result = {}
433 for item in split1:
434 res = self.tk.splitlist(item)
435 result[res[0]] = res[1:]
436
437 return result
438
439 elif option:
440 return self.tk.call(self._w, 'tag', 'configure', tagname,
441 '-%s' % option)
442
443 else:
444 args = ()
445 for key, val in kwargs.iteritems():
446 args += ('-%s' % key, val)
447
448 self.tk.call(self._w, 'tag', 'configure', tagname, *args)
449
450
451 def tag_delete(self, tagname):
452 self.tk.call(self._w, 'tag', 'delete', tagname)
453
454
455 def tag_exists(self, tagname):
456 return self.getboolean(self.tk.call(self._w, 'tag', 'exists', tagname))
457
458
459 def tag_includes(self, tagname, index):
460 return self.getboolean(self.tk.call(self._w, 'tag', 'includes',
461 tagname, index))
462
463
464 def tag_lower(self, tagname, belowthis=None):
465 self.tk.call(self._w, 'tag', 'lower', belowthis)
466
467
468 def tag_names(self, pattern=None):
469 return self.tk.call(self._w, 'tag', 'names', pattern)
470
471
472 def tag_raise(self, tagname, abovethis=None):
473 self.tk.call(self._w, 'tag', 'raise', tagname, abovethis)
474
475
476 def tag_row(self, tagname, *args):
477 return self.tk.call(self._w, 'tag', 'row', tagname, *args)
478
479
480 def validate(self, index):
481 """Explicitly validates the specified index based on the current
482 callback set for the validatecommand option. Return 0 or 1 based on
483 whether the cell was validated."""
484 return self.tk.call(self._w, 'validate', index)
485
486
487 @property
488 def version(self):
489 """Return tktable's package version."""
490 return self.tk.call(self._w, 'version')
491
492
493 def width(self, column=None, **kwargs):
494 """If column and kwargs are not given, a dict describing all columns
495 for which a width has been set is returned.
496 If column is given, the width of that column is returnd.
497 If kwargs is given, then it sets the key/value pairs, where key is a
498 column and value represents the width for the column."""
499 if column is None and not kwargs:
500 pairs = self.tk.splitlist(self.tk.call(self._w, 'width'))
501 return dict(pair.split() for pair in pairs)
502 elif column is not None:
503 return int(self.tk.call(self._w, 'width', str(column)))
504
505 args = Tkinter._flatten(kwargs.items())
506 self.tk.call(self._w, 'width', *args)
507
508
509 def window_cget(self, index, option):
510 return self.tk.call(self._w, 'window', 'cget', index, option)
511
512
513 def window_configure(self, index, option=None, **kwargs):
514 """Query or modify options associated with the embedded window given
515 by index. This should also be used to add a new embedded window into
516 the table.
517
518 If no option is specified, a dict describing all of the available
519 options for index is returned. If option is specified, then the
520 command returns a list describing the one named option. Lastly, if
521 kwargs is given then it corresponds to option-value pairs that should
522 be modified."""
523 if option is None and not kwargs:
524 return self.tk.call(self._w, 'window', 'configure', index)
525 elif option:
526 return self.tk.call(self._w, 'window', 'configure', index,
527 '-%s' % option)
528 else:
529 args = ()
530 for key, val in kwargs.iteritems():
531 args += ('-%s' % key, val)
532
533 self.tk.call(self._w, 'window', 'configure', index, *args)
534
535
536 def window_delete(self, *indexes):
537 self.tk.call(self._w, 'window', 'delete', *indexes)
538
539
540 def window_move(self, index_from, index_to):
541 self.tk.call(self._w, 'window', 'move', index_from, index_to)
542
543
544 def window_names(self, pattern=None):
545 return self.tk.call(self._w, 'window', 'names', pattern)
546
547
548 def xview(self, index=None):
549 """If index is not given a tuple containing two fractions is returned,
550 each fraction is between 0 and 1. Together they describe the
551 horizontal span that is visible in the window.
552
553 If index is given the view in the window is adjusted so that the
554 column given by index is displayed at the left edge of the window."""
555 res = self.tk.call(self._w, 'xview', index)
556 if index is None:
557 return self._getdoubles(res)
558
559
560 def xview_moveto(self, fraction):
561 """Adjusts the view in the window so that fraction of the total width
562 of the table text is off-screen to the left. The fraction parameter
563 must be a fraction between 0 and 1."""
564 self.tk.call(self._w, 'xview', 'moveto', fraction)
565
566
567 def xview_scroll(self, *L):
568 #change by frank gao for attach scrollbar 11/11/2010
569 """Shift the view in the window left or right according to number and
570 what. The 'number' parameter must be an integer. The 'what' parameter
571 must be either units or pages or an abbreviation of one of these.
572
573 If 'what' is units, the view adjusts left or right by number cells on
574 the display; if it is pages then the view adjusts by number screenfuls.
575 If 'number' is negative then cells farther to the left become visible;
576 if it is positive then cells farther to the right become visible. """
577 #self.tk.call(self._w, 'xview', 'scroll', number, what)
578 if op=='scroll':
579 units=L[2]
580 self.tk.call(self._w, 'xview', 'scroll',howMany,units)
581 elif op=='moveto':
582 self.tk.call(self._w, 'xview', 'moveto',howMany)
583
584
585 def yview(self, index=None):
586 """If index is not given a tuple containing two fractions is returned,
587 each fraction is between 0 and 1. The first element gives the position
588 of the table element at the top of the window, relative to the table
589 as a whole. The second element gives the position of the table element
590 just after the last one in the window, relative to the table as a
591 whole.
592
593 If index is given the view in the window is adjusted so that the
594 row given by index is displayed at the top of the window."""
595 res = self.tk.call(self._w, 'yview', index)
596 if index is None:
597 return self._getdoubles(res)
598
599
600 def yview_moveto(self, fraction):
601 """Adjusts the view in the window so that the element given by
602 fraction appears at the top of the window. The fraction parameter
603 must be a fraction between 0 and 1."""
604 self.tk.call(self._w, 'yview', 'moveto', fraction)
605
606
607 def yview_scroll(self, *L):
608 #change by frank gao for attach scrollbar 11/11/2010
609 """Adjust the view in the window up or down according to number and
610 what. The 'number' parameter must be an integer. The 'what' parameter
611 must be either units or pages or an abbreviation of one of these.
612
613 If 'what' is units, the view adjusts up or down by number cells; if it
614 is pages then the view adjusts by number screenfuls.
615 If 'number' is negative then earlier elements become visible; if it
616 is positive then later elements become visible. """
617 #self.tk.call(self._w, 'yview', 'scroll', number, what)
618 op,howMany=L[0],L[1]
619 if op=='scroll':
620 units=L[2]
621 self.tk.call(self._w, 'yview', 'scroll',howMany,units)
622 elif op=='moveto':
623 self.tk.call(self._w, 'yview', 'moveto',howMany)
624
625
626 # Sample test taken from tktable cvs, original tktable python wrapper
627 def sample_test():
628 from Tkinter import Tk, Label, Button
629
630 def test_cmd(event):
631 if event.i == 0:
632 return '%i, %i' % (event.r, event.c)
633 else:
634 return 'set'
635
636 def browsecmd(event):
637 print "event:", event.__dict__
638 print "curselection:", test.curselection()
639 print "active cell index:", test.index('active')
640 print "active:", test.index('active', 'row')
641 print "anchor:", test.index('anchor', 'row')
642
643 root = Tk()
644
645 var = ArrayVar(root)
646 for y in range(-1, 4):
647 for x in range(-1, 5):
648 index = "%i,%i" % (y, x)
649 var[index] = index
650
651 label = Label(root, text="Proof-of-existence test for Tktable")
652 label.pack(side = 'top', fill = 'x')
653
654 quit = Button(root, text="QUIT", command=root.destroy)
655 quit.pack(side = 'bottom', fill = 'x')
656
657 test = Table(root,
658 rows=10,
659 cols=5,
660 state='disabled',
661 width=6,
662 height=6,
663 titlerows=1,
664 titlecols=1,
665 roworigin=-1,
666 colorigin=-1,
667 selectmode='browse',
668 selecttype='row',
669 rowstretch='unset',
670 colstretch='last',
671 browsecmd=browsecmd,
672 flashmode='on',
673 variable=var,
674 usecommand=0,
675 command=test_cmd)
676 test.pack(expand=1, fill='both')
677 test.tag_configure('sel', background = 'yellow')
678 test.tag_configure('active', background = 'blue')
679 test.tag_configure('title', anchor='w', bg='red', relief='sunken')
680 root.mainloop()
681
682 if __name__ == '__main__':
683 sample_test()
684