Package nltk :: Package draw :: Module table
[hide private]
[frames] | no frames]

Source Code for Module nltk.draw.table

   1  # Natural Language Toolkit: Table widget 
   2  # 
   3  # Copyright (C) 2001-2008 NLTK Project 
   4  # Author: Edward Loper <[email protected]> 
   5  # URL: <http://nltk.org> 
   6  # For license information, see LICENSE.TXT 
   7  # 
   8  # $Id:$ 
   9   
  10  """ 
  11  Tkinter widgets for displaying multi-column listboxes and tables. 
  12  """ 
  13   
  14  from Tkinter import * 
  15  import operator 
  16   
  17  ###################################################################### 
  18  # Multi-Column Listbox 
  19  ###################################################################### 
  20   
21 -class MultiListbox(Frame):
22 """ 23 A multi-column listbox, where the current selection applies to an 24 entire row. Based on the 'U{MultiListbox Tkinter widget 25 <http://code.activestate.com/recipes/52266/>}' recipe from the 26 Python Cookbook. 27 28 For the most part, C{MultiListbox}'s methods just delegate to its 29 contained listboxes. For any methods that do not have docstrings, 30 see C{Tkinter.Listbox} for a description of what that method does. 31 """ 32 #///////////////////////////////////////////////////////////////// 33 # Configuration 34 #///////////////////////////////////////////////////////////////// 35 36 #: Default configuration values for the frame. 37 FRAME_CONFIG = dict(background='#888', 38 takefocus=True, 39 highlightthickness=1) 40 41 #: Default configurations for the column labels. 42 LABEL_CONFIG = dict(borderwidth=1, relief='raised', 43 font='helvetica -16 bold', 44 background='#444', foreground='white') 45 46 #: Default configuration for the column listboxes. 47 LISTBOX_CONFIG = dict(borderwidth=1, 48 selectborderwidth=0, 49 highlightthickness=0, 50 exportselection=False, 51 selectbackground='#888', 52 activestyle='none', 53 takefocus=False) 54 55 #///////////////////////////////////////////////////////////////// 56 # Constructor 57 #///////////////////////////////////////////////////////////////// 58
59 - def __init__(self, master, columns, column_weights=None, cnf={}, **kw):
60 """ 61 Construct a new multi-column listbox widget. 62 63 @param master: The widget that should contain the new 64 multi-column listbox. 65 66 @param columns: Specifies what columns should be included in 67 the new multi-column listbox. If C{columns} is an integer, 68 the it is the number of columns to include. If it is 69 a list, then its length indicates the number of columns 70 to include; and each element of the list will be used as 71 a label for the corresponding column. 72 73 @param cnf, kw: Configuration parameters for this widget. 74 Use C{label_*} to configure all labels; and C{listbox_*} 75 to configure all listboxes. E.g.: 76 77 >>> mlb = MultiListbox(master, 5, label_foreground='red') 78 """ 79 # If columns was specified as an int, convert it to a list. 80 if isinstance(columns, int): 81 columns = range(columns) 82 include_labels = False 83 else: 84 include_labels = True 85 86 if len(columns) == 0: 87 raise ValuError("Expected at least one column") 88 89 # Instance variables 90 self._column_names = tuple(columns) 91 self._listboxes = [] 92 self._labels = [] 93 94 # Pick a default value for column_weights, if none was specified. 95 if column_weights is None: 96 column_weights = [1] * len(columns) 97 elif len(column_weights) != len(columns): 98 raise ValueError('Expected one column_weight for each column') 99 self._column_weights = column_weights 100 101 # Configure our widgets. 102 Frame.__init__(self, master, **self.FRAME_CONFIG) 103 self.grid_rowconfigure(1, weight=1) 104 for i, label in enumerate(self._column_names): 105 self.grid_columnconfigure(i, weight=column_weights[i]) 106 107 # Create a label for the column 108 if include_labels: 109 l = Label(self, text=label, **self.LABEL_CONFIG) 110 self._labels.append(l) 111 l.grid(column=i, row=0, sticky='news', padx=0, pady=0) 112 l.column_index = i 113 114 # Create a listbox for the column 115 lb = Listbox(self, **self.LISTBOX_CONFIG) 116 self._listboxes.append(lb) 117 lb.grid(column=i, row=1, sticky='news', padx=0, pady=0) 118 lb.column_index = i 119 120 # Clicking or dragging selects: 121 lb.bind('<Button-1>', self._select) 122 lb.bind('<B1-Motion>', self._select) 123 # Scroll whell scrolls: 124 lb.bind('<Button-4>', lambda e: self._scroll(-1)) 125 lb.bind('<Button-5>', lambda e: self._scroll(+1)) 126 lb.bind('<MouseWheel>', lambda e: self._scroll(e.delta)) 127 # Button 2 can be used to scan: 128 lb.bind('<Button-2>', lambda e: self.scan_mark(e.x, e.y)) 129 lb.bind('<B2-Motion>', lambda e: self.scan_dragto(e.x, e.y)) 130 # Dragging outside the window has no effect (diable 131 # the default listbox behavior, which scrolls): 132 lb.bind('<B1-Leave>', lambda e: 'break') 133 # Columns can be resized by dragging them: 134 l.bind('<Button-1>', self._resize_column) 135 136 # Columns can be resized by dragging them. (This binding is 137 # used if they click on the grid between columns:) 138 self.bind('<Button-1>', self._resize_column) 139 140 # Set up key bindings for the widget: 141 self.bind('<Up>', lambda e: self.select(delta=-1)) 142 self.bind('<Down>', lambda e: self.select(delta=1)) 143 self.bind('<Prior>', lambda e: self.select(delta=-self._pagesize())) 144 self.bind('<Next>', lambda e: self.select(delta=self._pagesize())) 145 146 # Configuration customizations 147 self.configure(cnf, **kw)
148 149 #///////////////////////////////////////////////////////////////// 150 # Column Resizing 151 #///////////////////////////////////////////////////////////////// 152
153 - def _resize_column(self, event):
154 """ 155 Callback used to resize a column of the table. Return C{True} 156 if the column is actually getting resized (if the user clicked 157 on the far left or far right 5 pixels of a label); and 158 C{False} otherwies. 159 """ 160 # If we're already waiting for a button release, then ignore 161 # the new button press. 162 if event.widget.bind('<ButtonRelease>'): 163 return False 164 165 # Decide which column (if any) to resize. 166 self._resize_column_index = None 167 if event.widget is self: 168 for i, lb in enumerate(self._listboxes): 169 if abs(event.x-(lb.winfo_x()+lb.winfo_width())) < 10: 170 self._resize_column_index = i 171 elif event.x > (event.widget.winfo_width()-5): 172 self._resize_column_index = event.widget.column_index 173 elif event.x < 5 and event.widget.column_index != 0: 174 self._resize_column_index = event.widget.column_index-1 175 176 # Bind callbacks that are used to resize it. 177 if self._resize_column_index is not None: 178 event.widget.bind('<Motion>', self._resize_column_motion_cb) 179 event.widget.bind('<ButtonRelease-%d>' % event.num, 180 self._resize_column_buttonrelease_cb) 181 return True 182 else: 183 return False
184
185 - def _resize_column_motion_cb(self, event):
186 lb = self._listboxes[self._resize_column_index] 187 charwidth = lb.winfo_width() / float(lb['width']) 188 189 x1 = event.x + event.widget.winfo_x() 190 x2 = lb.winfo_x() + lb.winfo_width() 191 192 lb['width'] = max(3, lb['width'] + int((x1-x2)/charwidth))
193
194 - def _resize_column_buttonrelease_cb(self, event):
195 event.widget.unbind('<ButtonRelease-%d>' % event.num) 196 event.widget.unbind('<Motion>')
197 198 #///////////////////////////////////////////////////////////////// 199 # Properties 200 #///////////////////////////////////////////////////////////////// 201 202 column_names = property(lambda self: self._column_names, doc=""" 203 A tuple containing the names of the columns used by this 204 multi-column listbox.""") 205 206 column_labels = property(lambda self: tuple(self._labels), doc=""" 207 A tuple containing the C{Tkinter.Label} widgets used to 208 display the label of each column. If this multi-column 209 listbox was created without labels, then this will be an empty 210 tuple. These widgets will all be augmented with a 211 C{column_index} attribute, which can be used to determine 212 which column they correspond to. This can be convenient, 213 e.g., when defining callbacks for bound events.""") 214 215 listboxes = property(lambda self: tuple(self._listboxes), doc=""" 216 A tuple containing the C{Tkinter.Listbox} widgets used to 217 display individual columns. These widgets will all be 218 augmented with a C{column_index} attribute, which can be used 219 to determine which column they correspond to. This can be 220 convenient, e.g., when defining callbacks for bound events.""") 221 222 #///////////////////////////////////////////////////////////////// 223 # Mouse & Keyboard Callback Functions 224 #///////////////////////////////////////////////////////////////// 225
226 - def _select(self, e):
227 i = e.widget.nearest(e.y) 228 self.selection_clear(0, 'end') 229 self.selection_set(i) 230 self.activate(i) 231 self.focus()
232
233 - def _scroll(self, delta):
234 for lb in self._listboxes: 235 lb.yview_scroll(delta, 'unit') 236 return 'break'
237
238 - def _pagesize(self):
239 """@return: The number of rows that makes up one page""" 240 return int(self.index('@0,1000000')) - int(self.index('@0,0'))
241 242 #///////////////////////////////////////////////////////////////// 243 # Row selection 244 #///////////////////////////////////////////////////////////////// 245
246 - def select(self, index=None, delta=None, see=True):
247 """ 248 Set the selected row. If C{index} is specified, then select 249 row C{index}. Otherwise, if C{delta} is specified, then move 250 the current selection by C{delta} (negative numbers for up, 251 positive numbers for down). This will not move the selection 252 past the top or the bottom of the list. 253 254 @param see: If true, then call C{self.see()} with the newly 255 selected index, to ensure that it is visible. 256 """ 257 if (index is not None) and (delta is not None): 258 raise ValueError('specify index or delta, but not both') 259 260 # If delta was given, then calculate index. 261 if delta is not None: 262 if len(self.curselection()) == 0: 263 index = -1 + delta 264 else: 265 index = int(self.curselection()[0]) + delta 266 267 # Clear all selected rows. 268 self.selection_clear(0, 'end') 269 270 # Select the specified index 271 if index is not None: 272 index = min(max(index, 0), self.size()-1) 273 #self.activate(index) 274 self.selection_set(index) 275 if see: self.see(index)
276 277 #///////////////////////////////////////////////////////////////// 278 # Configuration 279 #///////////////////////////////////////////////////////////////// 280
281 - def configure(self, cnf={}, **kw):
282 """ 283 Configure this widget. Use C{label_*} to configure all 284 labels; and C{listbox_*} to configure all listboxes. E.g.: 285 286 >>> mlb = MultiListbox(master, 5) 287 >>> mlb.configure(label_foreground='red') 288 >>> mlb.configure(listbox_foreground='red') 289 """ 290 cnf = dict(cnf.items() + kw.items()) 291 for (key, val) in cnf.items(): 292 if key.startswith('label_') or key.startswith('label-'): 293 for label in self._labels: 294 label.configure({key[6:]: val}) 295 elif key.startswith('listbox_') or key.startswith('listbox-'): 296 for listbox in self._listboxes: 297 listbox.configure({key[8:]: val}) 298 else: 299 Frame.configure(self, {key:val})
300
301 - def __setitem__(self, key, val):
302 """ 303 Configure this widget. This is equivalent to 304 C{self.configure({key,val})}. See L{configure()}. 305 """ 306 self.configure({key:val})
307
308 - def rowconfigure(self, row_index, cnf={}, **kw):
309 """ 310 Configure all table cells in the given row. Valid keyword 311 arguments are: C{background}, C{bg}, C{foreground}, C{fg}, 312 C{selectbackground}, C{selectforeground}. 313 """ 314 for lb in self._listboxes: lb.itemconfigure(row_index, cnf, **kw)
315
316 - def columnconfigure(self, col_index, cnf={}, **kw):
317 """ 318 Configure all table cells in the given column. Valid keyword 319 arguments are: C{background}, C{bg}, C{foreground}, C{fg}, 320 C{selectbackground}, C{selectforeground}. 321 """ 322 lb = self._listboxes[col_index] 323 324 cnf = dict(cnf.items() + kw.items()) 325 for (key, val) in cnf.items(): 326 if key in ('background', 'bg', 'foreground', 'fg', 327 'selectbackground', 'selectforeground'): 328 for i in range(lb.size()): lb.itemconfigure(i, {key:val}) 329 else: 330 lb.configure({key:val})
331
332 - def itemconfigure(self, row_index, col_index, cnf=None, **kw):
333 """ 334 Configure the table cell at the given row and column. Valid 335 keyword arguments are: C{background}, C{bg}, C{foreground}, 336 C{fg}, C{selectbackground}, C{selectforeground}. 337 """ 338 lb = self._listboxes[col_index] 339 return lb.itemconfigure(row_index, cnf, **kw)
340 341 #///////////////////////////////////////////////////////////////// 342 # Value Access 343 #///////////////////////////////////////////////////////////////// 344
345 - def insert(self, index, *rows):
346 """ 347 Insert the given row or rows into the table, at the given 348 index. Each row value should be a tuple of cell values, one 349 for each column in the row. Index may be an integer or any of 350 the special strings (such as C{'end'}) accepted by 351 C{Tkinter.Listbox}. 352 """ 353 for elt in rows: 354 if len(elt) != len(self._column_names): 355 raise ValueError('rows should be tuples whose length ' 356 'is equal to the number of columns') 357 for (lb,elts) in zip(self._listboxes, zip(*rows)): 358 lb.insert(index, *elts)
359
360 - def get(self, first, last=None):
361 """ 362 Return the value(s) of the specified row(s). If C{last} is 363 not specified, then return a single row value; otherwise, 364 return a list of row values. Each row value is a tuple of 365 cell values, one for each column in the row. 366 """ 367 values = [lb.get(first, last) for lb in self._listboxes] 368 if last: 369 return [tuple(row) for row in zip(*values)] 370 else: 371 return tuple(values)
372
373 - def bbox(self, row, col):
374 """ 375 Return the bounding box for the given table cell, relative to 376 this widget's top-left corner. The bounding box is a tuple 377 of integers C{(left, top, width, height)}. 378 """ 379 dx, dy, _, _ = self.grid_bbox(row=0, column=col) 380 x, y, w, h = self._listboxes[col].bbox(row) 381 return int(x)+int(dx), int(y)+int(dy), int(w), int(h)
382 383 #///////////////////////////////////////////////////////////////// 384 # Hide/Show Columns 385 #///////////////////////////////////////////////////////////////// 386
387 - def hide_column(self, col_index):
388 """ 389 Hide the given column. The column's state is still 390 maintained: its values will still be returned by L{get()}, and 391 you must supply its values when calling L{insert()}. It is 392 safe to call this on a column that is already hidden. 393 394 @see: L{show_column()} 395 """ 396 if self._labels: 397 self._labels[col_index].grid_forget() 398 self.listboxes[col_index].grid_forget() 399 self.grid_columnconfigure(col_index, weight=0)
400
401 - def show_column(self, col_index):
402 """ 403 Display a column that has been hidden using L{hide_column()}. 404 It is safe to call this on a column that is not hidden. 405 """ 406 weight = self._column_weights[col_index] 407 if self._labels: 408 self._labels[col_index].grid(column=col_index, row=0, 409 sticky='news', padx=0, pady=0) 410 self._listboxes[col_index].grid(column=col_index, row=1, 411 sticky='news', padx=0, pady=0) 412 self.grid_columnconfigure(col_index, weight=weight)
413 414 #///////////////////////////////////////////////////////////////// 415 # Binding Methods 416 #///////////////////////////////////////////////////////////////// 417
418 - def bind_to_labels(self, sequence=None, func=None, add=None):
419 """ 420 Add a binding to each C{Tkinter.Label} widget in this 421 mult-column listbox that will call C{func} in response to the 422 event C{sequence}. 423 424 @return: A list of the identifiers of replaced binding 425 functions (if any), allowing for their deletion (to 426 prevent a memory leak). 427 """ 428 return [label.bind(sequence, func, add) 429 for label in self.column_labels]
430
431 - def bind_to_listboxes(self, sequence=None, func=None, add=None):
432 """ 433 Add a binding to each C{Tkinter.Listbox} widget in this 434 mult-column listbox that will call C{func} in response to the 435 event C{sequence}. 436 437 @return: A list of the identifiers of replaced binding 438 functions (if any), allowing for their deletion (to 439 prevent a memory leak). 440 """ 441 for listbox in self.listboxes: 442 listbox.bind(sequence, func, add)
443
444 - def bind_to_columns(self, sequence=None, func=None, add=None):
445 """ 446 Add a binding to each C{Tkinter.Label} and C{Tkinter.Listbox} 447 widget in this mult-column listbox that will call C{func} in 448 response to the event C{sequence}. 449 450 @return: A list of the identifiers of replaced binding 451 functions (if any), allowing for their deletion (to 452 prevent a memory leak). 453 """ 454 return (self.bind_to_labels(sequence, func, add) + 455 self.bind_to_listboxes(sequence, func, add))
456 457 #///////////////////////////////////////////////////////////////// 458 # Simple Delegation 459 #///////////////////////////////////////////////////////////////// 460 461 # These methods delegate to the first listbox:
462 - def curselection(self, *args, **kwargs):
463 return self._listboxes[0].curselection(*args, **kwargs)
464 - def selection_includes(self, *args, **kwargs):
465 return self._listboxes[0].selection_includes(*args, **kwargs)
466 - def itemcget(self, *args, **kwargs):
467 return self._listboxes[0].itemcget(*args, **kwargs)
468 - def size(self, *args, **kwargs):
469 return self._listboxes[0].size(*args, **kwargs)
470 - def index(self, *args, **kwargs):
471 return self._listboxes[0].index(*args, **kwargs)
472 - def nearest(self, *args, **kwargs):
473 return self._listboxes[0].nearest(*args, **kwargs)
474 475 # These methods delegate to each listbox (and return None):
476 - def activate(self, *args, **kwargs):
477 for lb in self._listboxes: lb.activate(*args, **kwargs)
478 - def delete(self, *args, **kwargs):
479 for lb in self._listboxes: lb.delete(*args, **kwargs)
480 - def scan_mark(self, *args, **kwargs):
481 for lb in self._listboxes: lb.scan_mark(*args, **kwargs)
482 - def scan_dragto(self, *args, **kwargs):
483 for lb in self._listboxes: lb.scan_dragto(*args, **kwargs)
484 - def see(self, *args, **kwargs):
485 for lb in self._listboxes: lb.see(*args, **kwargs)
486 - def selection_anchor(self, *args, **kwargs):
487 for lb in self._listboxes: lb.selection_anchor(*args, **kwargs)
488 - def selection_clear(self, *args, **kwargs):
489 for lb in self._listboxes: lb.selection_clear(*args, **kwargs)
490 - def selection_set(self, *args, **kwargs):
491 for lb in self._listboxes: lb.selection_set(*args, **kwargs)
492 - def yview(self, *args, **kwargs):
493 for lb in self._listboxes: lb.yview(*args, **kwargs)
494 - def yview_moveto(self, *args, **kwargs):
495 for lb in self._listboxes: lb.yview_moveto(*args, **kwargs)
496 - def yview_scroll(self, *args, **kwargs):
497 for lb in self._listboxes: lb.yview_scroll(*args, **kwargs)
498 499 #///////////////////////////////////////////////////////////////// 500 # Aliases 501 #///////////////////////////////////////////////////////////////// 502 503 itemconfig = itemconfigure 504 rowconfig = rowconfigure 505 columnconfig = columnconfigure 506 select_anchor = selection_anchor 507 select_clear = selection_clear 508 select_includes = selection_includes 509 select_set = selection_set
510 511 #///////////////////////////////////////////////////////////////// 512 # These listbox methods are not defined for multi-listbox 513 #///////////////////////////////////////////////////////////////// 514 # def xview(self, *what): pass 515 # def xview_moveto(self, fraction): pass 516 # def xview_scroll(self, number, what): pass 517 518 ###################################################################### 519 # Table 520 ###################################################################### 521
522 -class Table(object):
523 """ 524 A display widget for a table of values, based on a L{MultiListbox} 525 widget. For many purposes, C{Table} can be treated as a 526 list-of-lists. E.g., table[i] is a list of the values for row i; 527 and table.append(row) adds a new row with the given lits of 528 values. Individual cells can be accessed using table[i,j], which 529 refers to the j-th column of the i-th row. This can be used to 530 both read and write values from the table. E.g.: 531 532 >>> table[i,j] = 'hello' 533 534 The column (j) can be given either as an index number, or as a 535 column name. E.g., the following prints the value in the 3rd row 536 for the 'First Name' column: 537 538 >>> print table[3, 'First Name'] 539 John 540 541 You can configure the colors for individual rows, columns, or 542 cells using L{rowconfig()}, L{columnconfig()}, and L{itemconfig()}. 543 The color configuration for each row will be preserved if the 544 table is modified; however, when new rows are added, any color 545 configurations that have been made for I{columns} will not be 546 applied to the new row. 547 548 Note: Although C{Table} acts like a widget in some ways (e.g., it 549 defines L{grid()}, L{pack()}, and L{bind()}), it is not itself a 550 widget; it just contains one. This is because widgets need to 551 define C{__getitem__()}, C{__setitem__()}, and C{__nonzero__()} in 552 a way that's incompatible with the fact that C{Table} behaves as a 553 list-of-lists. 554 555 @ivar _mlb: The multi-column listbox used to display this table's data. 556 @ivar _rows: A list-of-lists used to hold the cell values of this 557 table. Each element of _rows is a row value, i.e., a list of 558 cell values, one for each column in the row. 559 """
560 - def __init__(self, master, column_names, rows=None, 561 column_weights=None, 562 scrollbar=True, click_to_sort=True, 563 reprfunc=None, cnf={}, **kw):
564 """ 565 Construct a new Table widget. 566 567 @type master: C{Tkinter.Widget} 568 @param master: The widget that should contain the new table. 569 570 @type column_names: C{list} of C{str} 571 @param column_names: A list of names for the columns; these 572 names will be used to create labels for each column; 573 and can be used as an index when reading or writing 574 cell values from the table. 575 576 @type rows: C{list} of C{list} 577 @param rows: A list of row values used to initialze the table. 578 Each row value should be a tuple of cell values, one for 579 each column in the row. 580 581 @type scrollbar: C{bool} 582 @param scrollbar: If true, then create a scrollbar for the 583 new table widget. 584 585 @type click_to_sort: C{bool} 586 @param click_to_sort: If true, then create bindings that will 587 sort the table's rows by a given column's values if the 588 user clicks on that colum's label. 589 590 @type reprfunc: C{function} 591 @param reprfunc: If specified, then use this function to 592 convert each table cell value to a string suitable for 593 display. C{reprfunc} has the following signature: 594 595 >>> reprfunc(row_index, col_index, cell_value) -> str 596 597 (Note that the column is specified by index, not by name.) 598 599 @param cnf, kw: Configuration parameters for this widget's 600 contained C{MultiListbox}. See L{MultiListbox.__init__()} 601 for details. 602 """ 603 self._num_columns = len(column_names) 604 self._reprfunc = reprfunc 605 self._frame = Frame(master) 606 607 self._column_name_to_index = dict((c,i) for (i,c) in 608 enumerate(column_names)) 609 610 # Make a copy of the rows & check that it's valid. 611 if rows is None: self._rows = [] 612 else: self._rows = [[v for v in row] for row in rows] 613 for row in self._rows: self._checkrow(row) 614 615 # Create our multi-list box. 616 self._mlb = MultiListbox(self._frame, column_names, 617 column_weights, cnf, **kw) 618 self._mlb.pack(side='left', expand=True, fill='both') 619 620 # Optional scrollbar 621 if scrollbar: 622 sb = Scrollbar(self._frame, orient='vertical', 623 command=self._mlb.yview) 624 sb.pack(side='right', fill='y') 625 for listbox in self._mlb.listboxes: 626 listbox['yscrollcommand'] = sb.set 627 628 # Set up sorting 629 self._sortkey = None 630 if click_to_sort: 631 for i, l in enumerate(self._mlb.column_labels): 632 l.bind('<Button-1>', self._sort) 633 634 # Fill in our multi-list box. 635 self._fill_table()
636 637 #///////////////////////////////////////////////////////////////// 638 #{ Widget-like Methods 639 #///////////////////////////////////////////////////////////////// 640 # These all just delegate to either our frame or our MLB. 641
642 - def pack(self, *args, **kwargs):
643 """Position this table's main frame widget in its parent 644 widget. See C{Tkinter.Frame.pack()} for more info.""" 645 self._frame.pack(*args, **kwargs)
646
647 - def grid(self, *args, **kwargs):
648 """Position this table's main frame widget in its parent 649 widget. See C{Tkinter.Frame.grid()} for more info.""" 650 self._frame.grid(*args, **kwargs)
651
652 - def focus(self):
653 """Direct (keyboard) input foxus to this widget.""" 654 self._mlb.focus()
655
656 - def bind(self, sequence=None, func=None, add=None):
657 """Add a binding to this table's main frame that will call 658 C{func} in response to the event C{sequence}.""" 659 self._mlb.bind(sequence, func, add)
660
661 - def rowconfigure(self, row_index, cnf={}, **kw):
662 """@see: L{MultiListbox.rowconfigure()}""" 663 self._mlb.rowconfigure(row_index, cnf, **kw)
664
665 - def columnconfigure(self, col_index, cnf={}, **kw):
666 """@see: L{MultiListbox.columnconfigure()}""" 667 col_index = self.column_index(col_index) 668 self._mlb.columnconfigure(col_index, cnf, **kw)
669
670 - def itemconfigure(self, row_index, col_index, cnf=None, **kw):
671 """@see: L{MultiListbox.itemconfigure()}""" 672 col_index = self.column_index(col_index) 673 return self._mlb.itemconfigure(row_index, col_index, cnf, **kw)
674
675 - def bind_to_labels(self, sequence=None, func=None, add=None):
676 """@see: L{MultiListbox.bind_to_labels()}""" 677 return self._mlb.bind_to_labels(sequence, func, add)
678
679 - def bind_to_listboxes(self, sequence=None, func=None, add=None):
680 """@see: L{MultiListbox.bind_to_listboxes()}""" 681 return self._mlb.bind_to_listboxes(sequence, func, add)
682
683 - def bind_to_columns(self, sequence=None, func=None, add=None):
684 """@see: L{MultiListbox.bind_to_columns()}""" 685 return self._mlb.bind_to_columns(sequence, func, add)
686 687 rowconfig = rowconfigure 688 columnconfig = columnconfigure 689 itemconfig = itemconfigure 690 691 #///////////////////////////////////////////////////////////////// 692 #{ Table as list-of-lists 693 #///////////////////////////////////////////////////////////////// 694
695 - def insert(self, row_index, rowvalue):
696 """ 697 Insert a new row into the table, so that its row index will be 698 C{row_index}. If the table contains any rows whose row index 699 is greater than or equal to C{row_index}, then they will be 700 shifted down. 701 702 @param rowvalue: A tuple of cell values, one for each column 703 in the new row. 704 """ 705 self._checkrow(rowvalue) 706 self._rows.insert(row_index, rowvalue) 707 if self._reprfunc is not None: 708 rowvalue = [self._reprfunc(row_index,j,v) 709 for (j,v) in enumerate(rowvalue)] 710 self._mlb.insert(row_index, rowvalue) 711 if self._DEBUG: self._check_table_vs_mlb()
712
713 - def extend(self, rowvalues):
714 """ 715 Add new rows at the end of the table. 716 717 @param rowvalues: A list of row values used to initialze the 718 table. Each row value should be a tuple of cell values, 719 one for each column in the row. 720 """ 721 for rowvalue in rowvalues: self.append(rowvalue) 722 if self._DEBUG: self._check_table_vs_mlb()
723
724 - def append(self, rowvalue):
725 """ 726 Add a new row to the end of the table. 727 728 @param rowvalue: A tuple of cell values, one for each column 729 in the new row. 730 """ 731 self.insert(len(self._rows), rowvalue) 732 if self._DEBUG: self._check_table_vs_mlb()
733
734 - def clear(self):
735 """ 736 Delete all rows in this table. 737 """ 738 self._rows = [] 739 self._mlb.delete(0, 'end') 740 if self._DEBUG: self._check_table_vs_mlb()
741
742 - def __getitem__(self, index):
743 """ 744 Return the value of a row or a cell in this table. If 745 C{index} is an integer, then the row value for the C{index}th 746 row. This row value consists of a tuple of cell values, one 747 for each column in the row. If C{index} is a tuple of two 748 integers, C{(i,j)}, then return the value of the cell in the 749 C{i}th row and the C{j}th column. 750 """ 751 if isinstance(index, slice): 752 raise ValueError('Slicing not supported') 753 elif isinstance(index, tuple) and len(index)==2: 754 return self._rows[index[0]][self.column_index(index[1])] 755 else: 756 return tuple(self._rows[index])
757
758 - def __setitem__(self, index, val):
759 """ 760 Replace the value of a row or a cell in this table with 761 C{val}. 762 763 If C{index} is an integer, then C{val} should be a row value 764 (i.e., a tuple of cell values, one for each column). In this 765 case, the values of the C{index}th row of the table will be 766 replaced with the values in C{val}. 767 768 If C{index} is a tuple of integers, C{(i,j)}, then replace the 769 value of the cell in the C{i}th row and C{j}th column with 770 C{val}. 771 """ 772 if isinstance(index, slice): 773 raise ValueError('Slicing not supported') 774 775 776 # table[i,j] = val 777 elif isinstance(index, tuple) and len(index)==2: 778 i, j = index[0], self.column_index(index[1]) 779 config_cookie = self._save_config_info([i]) 780 self._rows[i][j] = val 781 if self._reprfunc is not None: 782 val = self._reprfunc(i, j, val) 783 self._mlb.listboxes[j].insert(i, val) 784 self._mlb.listboxes[j].delete(i+1) 785 self._restore_config_info(config_cookie) 786 787 # table[i] = val 788 else: 789 config_cookie = self._save_config_info([index]) 790 self._checkrow(val) 791 self._rows[index] = list(val) 792 if self._reprfunc is not None: 793 val = [self._reprfunc(index,j,v) for (j,v) in enumerate(val)] 794 self._mlb.insert(index, val) 795 self._mlb.delete(index+1) 796 self._restore_config_info(config_cookie)
797
798 - def __delitem__(self, row_index):
799 """ 800 Delete the C{row_index}th row from this table. 801 """ 802 if isinstance(index, slice): 803 raise ValueError('Slicing not supported') 804 if isinstance(row_index, tuple) and len(row_index)==2: 805 raise ValueError('Cannot delete a single cell!') 806 del self._rows[row_index] 807 self._mlb.delete(row_index) 808 if self._DEBUG: self._check_table_vs_mlb()
809
810 - def __len__(self):
811 """ 812 @return: the number of rows in this table. 813 """ 814 return len(self._rows)
815
816 - def _checkrow(self, rowvalue):
817 """ 818 Helper function: check that a given row value has the correct 819 number of elements; and if not, raise an exception. 820 """ 821 if len(rowvalue) != self._num_columns: 822 raise ValueError('Row %r has %d columns; expected %d' % 823 (rowvalue, len(rowvalue), self._num_columns))
824 825 #///////////////////////////////////////////////////////////////// 826 # Columns 827 #///////////////////////////////////////////////////////////////// 828 829 column_names = property(lambda self: self._mlb.column_names, doc=""" 830 A list of the names of the columns in this table.""") 831
832 - def column_index(self, i):
833 """ 834 If C{i} is a valid column index integer, then return it as is. 835 Otherwise, check if C{i} is used as the name for any column; 836 if so, return that column's index. Otherwise, raise a 837 C{KeyError} exception. 838 """ 839 if isinstance(i, int) and 0 <= i < self._num_columns: 840 return i 841 else: 842 # This raises a key error if the column is not found. 843 return self._column_name_to_index[i]
844
845 - def hide_column(self, column_index):
846 """@see: L{MultiListbox.hide_column()}""" 847 self._mlb.hide_column(self.column_index(column_index))
848
849 - def show_column(self, column_index):
850 """@see: L{MultiListbox.show_column()}""" 851 self._mlb.show_column(self.column_index(column_index))
852 853 #///////////////////////////////////////////////////////////////// 854 # Selection 855 #///////////////////////////////////////////////////////////////// 856
857 - def selected_row(self):
858 """ 859 Return the index of the currently selected row, or C{None} if 860 no row is selected. To get the row value itself, use 861 C{table[table.selected_row()]}. 862 """ 863 sel = self._mlb.curselection() 864 if sel: return int(sel[0]) 865 else: return None
866
867 - def select(self, index=None, delta=None, see=True):
868 """@see: L{MultiListbox.select()}""" 869 self._mlb.select(index, delta, see)
870 871 #///////////////////////////////////////////////////////////////// 872 # Sorting 873 #///////////////////////////////////////////////////////////////// 874
875 - def sort_by(self, column_index, order='toggle'):
876 """ 877 Sort the rows in this table, using the specified column's 878 values as a sort key. 879 880 @param column_index: Specifies which column to sort, using 881 either a column index (C{int}) or a column's label name 882 (C{str}). 883 884 @param order: Specifies whether to sort the values in 885 ascending or descending order: 886 887 - C{'ascending'}: Sort from least to greatest. 888 - C{'descending'}: Sort from greatest to least. 889 - C{'toggle'}: If the most recent call to C{sort_by()} 890 sorted the table by the same column C({column_index}), 891 then reverse the rows; otherwise sort in ascending 892 order. 893 """ 894 if order not in ('ascending', 'descending', 'toggle'): 895 raise ValueError('sort_by(): order should be "ascending", ' 896 '"descending", or "toggle".') 897 column_index = self.column_index(column_index) 898 config_cookie = self._save_config_info(index_by_id=True) 899 900 # Sort the rows. 901 if order == 'toggle' and column_index == self._sortkey: 902 self._rows.reverse() 903 else: 904 self._rows.sort(key=operator.itemgetter(column_index), 905 reverse=(order=='descending')) 906 self._sortkey = column_index 907 908 # Redraw the table. 909 self._fill_table() 910 self._restore_config_info(config_cookie, index_by_id=True, see=True) 911 if self._DEBUG: self._check_table_vs_mlb()
912
913 - def _sort(self, event):
914 """Event handler for clicking on a column label -- sort by 915 that column.""" 916 column_index = event.widget.column_index 917 918 # If they click on the far-left of far-right of a column's 919 # label, then resize rather than sorting. 920 if self._mlb._resize_column(event): 921 return 'continue' 922 923 # Otherwise, sort. 924 else: 925 self.sort_by(column_index) 926 return 'continue'
927 928 #///////////////////////////////////////////////////////////////// 929 #{ Table Drawing Helpers 930 #///////////////////////////////////////////////////////////////// 931
932 - def _fill_table(self, save_config=True):
933 """ 934 Re-draw the table from scratch, by clearing out the table's 935 multi-column listbox; and then filling it in with values from 936 C{self._rows}. Note that any cell-, row-, or column-specific 937 color configuration that has been done will be lost. The 938 selection will also be lost -- i.e., no row will be selected 939 after this call completes. 940 """ 941 self._mlb.delete(0, 'end') 942 for i, row in enumerate(self._rows): 943 if self._reprfunc is not None: 944 row = [self._reprfunc(i,j,v) for (j,v) in enumerate(row)] 945 self._mlb.insert('end', row)
946
947 - def _get_itemconfig(self, r, c):
948 return dict( (k, self._mlb.itemconfig(r, c, k)[-1]) 949 for k in ('foreground', 'selectforeground', 950 'background', 'selectbackground') )
951
952 - def _save_config_info(self, row_indices=None, index_by_id=False):
953 """ 954 Return a 'cookie' containing information about which row is 955 selected, and what color configurations have been applied. 956 this information can the be re-applied to the table (after 957 making modifications) using L{_restore_config_info()}. Color 958 configuration information will be saved for any rows in 959 C{row_indices}, or in the entire table, if 960 C{row_indices=None}. If C{index_by_id=True}, the the cookie 961 will associate rows with their configuration information based 962 on the rows' python id. This is useful when performing 963 operations that re-arrange the rows (e.g. C{sort}). If 964 C{index_by_id=False}, then it is assumed that all rows will be 965 in the same order when C{_restore_config_info()} is called. 966 """ 967 # Default value for row_indices is all rows. 968 if row_indices is None: 969 row_indices = range(len(self._rows)) 970 971 # Look up our current selection. 972 selection = self.selected_row() 973 if index_by_id and selection is not None: 974 selection = id(self._rows[selection]) 975 976 # Look up the color configuration info for each row. 977 if index_by_id: 978 config = dict((id(self._rows[r]), [self._get_itemconfig(r, c) 979 for c in range(self._num_columns)]) 980 for r in row_indices) 981 else: 982 config = dict((r, [self._get_itemconfig(r, c) 983 for c in range(self._num_columns)]) 984 for r in row_indices) 985 986 987 return selection, config
988
989 - def _restore_config_info(self, cookie, index_by_id=False, see=False):
990 """ 991 Restore selection & color configuration information that was 992 saved using L{_save_config_info}. 993 """ 994 selection, config = cookie 995 996 # Clear the selection. 997 if selection is None: 998 self._mlb.selection_clear(0, 'end') 999 1000 # Restore selection & color config 1001 if index_by_id: 1002 for r, row in enumerate(self._rows): 1003 if id(row) in config: 1004 for c in range(self._num_columns): 1005 self._mlb.itemconfigure(r, c, config[id(row)][c]) 1006 if id(row) == selection: 1007 self._mlb.select(r, see=see) 1008 else: 1009 if selection is not None: 1010 self._mlb.select(selection, see=see) 1011 for r in config: 1012 for c in range(self._num_columns): 1013 self._mlb.itemconfigure(r, c, config[r][c])
1014 1015 #///////////////////////////////////////////////////////////////// 1016 # Debugging (Invariant Checker) 1017 #///////////////////////////////////////////////////////////////// 1018 1019 _DEBUG = False 1020 """If true, then run L{_check_table_vs_mlb()} after any operation 1021 that modifies the table.""" 1022
1023 - def _check_table_vs_mlb(self):
1024 """ 1025 Verify that the contents of the table's L{_rows} variable match 1026 the contents of its multi-listbox (L{_mlb}). This is just 1027 included for debugging purposes, to make sure that the 1028 list-modifying operations are working correctly. 1029 """ 1030 for col in self._mlb.listboxes: 1031 assert len(self) == col.size() 1032 for row in self: 1033 assert len(row) == self._num_columns 1034 assert self._num_columns == len(self._mlb.column_names) 1035 #assert self._column_names == self._mlb.column_names 1036 for i, row in enumerate(self): 1037 for j, cell in enumerate(row): 1038 if self._reprfunc is not None: 1039 cell = self._reprfunc(i, j, cell) 1040 assert self._mlb.get(i)[j] == cell
1041 1042 ###################################################################### 1043 # Demo/Test Function 1044 ###################################################################### 1045
1046 -def demo():
1047 root = Tk() 1048 root.bind('<Control-q>', lambda e: root.destroy()) 1049 1050 table = Table(root, 'Word Synset Hypernym Hyponym'.split(), 1051 column_weights=[0, 1, 1, 1], 1052 reprfunc=(lambda i,j,s: ' %s' % s)) 1053 table.pack(expand=True, fill='both') 1054 1055 from nltk import wordnet 1056 from nltk.corpus import brown 1057 for word, pos in sorted(set(brown.tagged_words()[:500])): 1058 if pos[0] != 'N': continue 1059 word = word.lower() 1060 if word not in wordnet.N: continue 1061 for synset in wordnet.N[word].synsets(): 1062 hyper = synset.relations().get(wordnet.HYPERNYM, [''])[0] 1063 hypo = synset.relations().get(wordnet.HYPONYM, [''])[0] 1064 table.append([word, 1065 getattr(synset, 'gloss', '*none*'), 1066 getattr(hyper, 'gloss', '*none*'), 1067 getattr(hypo, 'gloss', '*none*')]) 1068 1069 table.columnconfig('Word', background='#afa') 1070 table.columnconfig('Synset', background='#efe') 1071 table.columnconfig('Hypernym', background='#fee') 1072 table.columnconfig('Hyponym', background='#ffe') 1073 for row in range(len(table)): 1074 for column in ('Hypernym', 'Hyponym'): 1075 if table[row, column] == '*none*': 1076 table.itemconfig(row, column, foreground='#666', 1077 selectforeground='#666') 1078 root.mainloop()
1079 1080 if __name__ == '__main__': 1081 demo() 1082