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