Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

536

537

538

539

540

541

542

543

544

545

546

547

548

549

550

551

552

553

554

555

556

557

558

559

560

561

562

563

564

565

566

567

568

569

570

571

572

573

574

575

576

577

578

579

580

581

582

583

584

585

586

587

588

589

590

591

592

593

594

595

596

597

598

599

600

601

602

603

604

605

606

607

608

609

610

611

612

613

614

615

616

617

618

619

620

621

622

623

624

625

626

627

628

629

630

631

632

633

634

635

636

637

638

639

640

641

642

643

644

645

646

647

648

649

650

651

652

653

654

655

656

657

658

659

660

661

662

663

664

665

666

667

668

669

670

671

672

673

674

675

676

677

678

679

680

681

682

683

684

685

686

687

688

689

690

691

692

693

694

695

696

697

698

699

700

701

702

703

704

705

706

707

708

709

710

711

712

713

714

715

716

717

718

719

720

721

722

723

724

725

726

727

728

729

730

731

732

733

734

735

736

737

738

739

740

741

742

743

744

745

746

747

748

749

750

751

752

753

754

755

756

757

758

759

760

761

762

763

764

765

766

767

768

769

770

771

772

773

774

775

776

777

778

779

780

781

782

783

784

785

786

787

788

789

790

791

792

793

794

795

796

797

798

799

800

801

802

803

804

805

806

807

808

809

810

811

812

813

814

815

816

817

818

819

820

821

822

823

824

825

826

827

828

829

830

831

832

833

834

835

836

837

838

839

840

841

842

843

844

845

846

847

848

849

850

851

852

853

854

855

856

857

858

859

860

861

862

863

864

865

866

867

868

869

870

871

872

873

874

875

876

877

878

879

880

881

882

883

884

885

886

887

888

889

890

891

892

893

894

895

896

897

898

899

900

901

902

903

904

905

906

907

908

909

910

911

912

913

914

915

916

917

918

919

920

921

922

923

924

925

926

927

928

929

930

931

932

933

934

935

936

937

938

939

940

941

942

943

944

945

946

947

948

949

950

951

952

953

954

955

956

957

958

959

960

961

962

963

964

965

966

967

968

969

970

971

972

973

974

975

976

977

978

979

980

981

982

983

984

985

986

987

988

989

990

991

992

993

994

995

996

997

998

999

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

1015

1016

1017

1018

1019

1020

1021

1022

1023

1024

1025

1026

1027

1028

1029

1030

1031

1032

1033

1034

1035

1036

1037

1038

1039

1040

1041

1042

1043

1044

1045

1046

1047

1048

1049

1050

1051

1052

1053

1054

1055

1056

1057

1058

1059

1060

1061

1062

1063

1064

1065

1066

1067

1068

1069

1070

1071

1072

1073

1074

1075

1076

1077

1078

1079

1080

1081

1082

1083

1084

1085

1086

1087

1088

1089

# Natural Language Toolkit: Table widget 

# 

# Copyright (C) 2001-2012 NLTK Project 

# Author: Edward Loper <edloper@gradient.cis.upenn.edu> 

# URL: <http://www.nltk.org/> 

# For license information, see LICENSE.TXT 

 

""" 

Tkinter widgets for displaying multi-column listboxes and tables. 

""" 

from __future__ import print_function 

import operator 

 

from Tkinter import (Frame, Label, Listbox, Scrollbar, Tk) 

 

 

###################################################################### 

# Multi-Column Listbox 

###################################################################### 

 

class MultiListbox(Frame): 

    """ 

    A multi-column listbox, where the current selection applies to an 

    entire row.  Based on the MultiListbox Tkinter widget 

    recipe from the Python Cookbook (http://code.activestate.com/recipes/52266/) 

 

    For the most part, ``MultiListbox`` methods delegate to its 

    contained listboxes.  For any methods that do not have docstrings, 

    see ``Tkinter.Listbox`` for a description of what that method does. 

    """ 

    #///////////////////////////////////////////////////////////////// 

    # Configuration 

    #///////////////////////////////////////////////////////////////// 

 

    #: Default configuration values for the frame. 

    FRAME_CONFIG = dict(background='#888', 

                        takefocus=True, 

                        highlightthickness=1) 

 

    #: Default configurations for the column labels. 

    LABEL_CONFIG = dict(borderwidth=1, relief='raised', 

                        font='helvetica -16 bold', 

                      background='#444', foreground='white') 

 

    #: Default configuration for the column listboxes. 

    LISTBOX_CONFIG = dict(borderwidth=1, 

                          selectborderwidth=0, 

                          highlightthickness=0, 

                          exportselection=False, 

                          selectbackground='#888', 

                          activestyle='none', 

                          takefocus=False) 

 

    #///////////////////////////////////////////////////////////////// 

    # Constructor 

    #///////////////////////////////////////////////////////////////// 

 

    def __init__(self, master, columns, column_weights=None, cnf={}, **kw): 

        """ 

        Construct a new multi-column listbox widget. 

 

        :param master: The widget that should contain the new 

            multi-column listbox. 

 

        :param columns: Specifies what columns should be included in 

            the new multi-column listbox.  If ``columns`` is an integer, 

            the it is the number of columns to include.  If it is 

            a list, then its length indicates the number of columns 

            to include; and each element of the list will be used as 

            a label for the corresponding column. 

 

        :param cnf, kw: Configuration parameters for this widget. 

            Use ``label_*`` to configure all labels; and ``listbox_*`` 

            to configure all listboxes.  E.g.: 

 

                >>> mlb = MultiListbox(master, 5, label_foreground='red') 

        """ 

        # If columns was specified as an int, convert it to a list. 

        if isinstance(columns, int): 

            columns = list(range(columns)) 

            include_labels = False 

        else: 

            include_labels = True 

 

        if len(columns) == 0: 

            raise ValueError("Expected at least one column") 

 

        # Instance variables 

        self._column_names = tuple(columns) 

        self._listboxes = [] 

        self._labels = [] 

 

        # Pick a default value for column_weights, if none was specified. 

        if column_weights is None: 

            column_weights = [1] * len(columns) 

        elif len(column_weights) != len(columns): 

            raise ValueError('Expected one column_weight for each column') 

        self._column_weights = column_weights 

 

        # Configure our widgets. 

        Frame.__init__(self, master, **self.FRAME_CONFIG) 

        self.grid_rowconfigure(1, weight=1) 

        for i, label in enumerate(self._column_names): 

            self.grid_columnconfigure(i, weight=column_weights[i]) 

 

            # Create a label for the column 

            if include_labels: 

                l = Label(self, text=label, **self.LABEL_CONFIG) 

                self._labels.append(l) 

                l.grid(column=i, row=0, sticky='news', padx=0, pady=0) 

                l.column_index = i 

 

            # Create a listbox for the column 

            lb = Listbox(self, **self.LISTBOX_CONFIG) 

            self._listboxes.append(lb) 

            lb.grid(column=i, row=1, sticky='news', padx=0, pady=0) 

            lb.column_index = i 

 

            # Clicking or dragging selects: 

            lb.bind('<Button-1>', self._select) 

            lb.bind('<B1-Motion>', self._select) 

            # Scroll whell scrolls: 

            lb.bind('<Button-4>', lambda e: self._scroll(-1)) 

            lb.bind('<Button-5>', lambda e: self._scroll(+1)) 

            lb.bind('<MouseWheel>', lambda e: self._scroll(e.delta)) 

            # Button 2 can be used to scan: 

            lb.bind('<Button-2>', lambda e: self.scan_mark(e.x, e.y)) 

            lb.bind('<B2-Motion>', lambda e: self.scan_dragto(e.x, e.y)) 

            # Dragging outside the window has no effect (diable 

            # the default listbox behavior, which scrolls): 

            lb.bind('<B1-Leave>', lambda e: 'break') 

            # Columns can be resized by dragging them: 

            l.bind('<Button-1>', self._resize_column) 

 

        # Columns can be resized by dragging them.  (This binding is 

        # used if they click on the grid between columns:) 

        self.bind('<Button-1>', self._resize_column) 

 

        # Set up key bindings for the widget: 

        self.bind('<Up>', lambda e: self.select(delta=-1)) 

        self.bind('<Down>', lambda e: self.select(delta=1)) 

        self.bind('<Prior>', lambda e: self.select(delta=-self._pagesize())) 

        self.bind('<Next>', lambda e: self.select(delta=self._pagesize())) 

 

        # Configuration customizations 

        self.configure(cnf, **kw) 

 

    #///////////////////////////////////////////////////////////////// 

    # Column Resizing 

    #///////////////////////////////////////////////////////////////// 

 

    def _resize_column(self, event): 

        """ 

        Callback used to resize a column of the table.  Return ``True`` 

        if the column is actually getting resized (if the user clicked 

        on the far left or far right 5 pixels of a label); and 

        ``False`` otherwies. 

        """ 

        # If we're already waiting for a button release, then ignore 

        # the new button press. 

        if event.widget.bind('<ButtonRelease>'): 

            return False 

 

        # Decide which column (if any) to resize. 

        self._resize_column_index = None 

        if event.widget is self: 

            for i, lb in enumerate(self._listboxes): 

                if abs(event.x-(lb.winfo_x()+lb.winfo_width())) < 10: 

                    self._resize_column_index = i 

        elif event.x > (event.widget.winfo_width()-5): 

            self._resize_column_index = event.widget.column_index 

        elif event.x < 5 and event.widget.column_index != 0: 

            self._resize_column_index = event.widget.column_index-1 

 

        # Bind callbacks that are used to resize it. 

        if self._resize_column_index is not None: 

            event.widget.bind('<Motion>', self._resize_column_motion_cb) 

            event.widget.bind('<ButtonRelease-%d>' % event.num, 

                              self._resize_column_buttonrelease_cb) 

            return True 

        else: 

            return False 

 

    def _resize_column_motion_cb(self, event): 

        lb = self._listboxes[self._resize_column_index] 

        charwidth = lb.winfo_width() / float(lb['width']) 

 

        x1 = event.x + event.widget.winfo_x() 

        x2 = lb.winfo_x() + lb.winfo_width() 

 

        lb['width'] = max(3, lb['width'] + int((x1-x2)/charwidth)) 

 

    def _resize_column_buttonrelease_cb(self, event): 

        event.widget.unbind('<ButtonRelease-%d>' % event.num) 

        event.widget.unbind('<Motion>') 

 

    #///////////////////////////////////////////////////////////////// 

    # Properties 

    #///////////////////////////////////////////////////////////////// 

 

    @property 

    def column_names(self): 

        """ 

        A tuple containing the names of the columns used by this 

        multi-column listbox. 

        """ 

        return self._column_names 

 

    @property 

    def column_labels(self): 

        """ 

        A tuple containing the ``Tkinter.Label`` widgets used to 

        display the label of each column.  If this multi-column 

        listbox was created without labels, then this will be an empty 

        tuple.  These widgets will all be augmented with a 

        ``column_index`` attribute, which can be used to determine 

        which column they correspond to.  This can be convenient, 

        e.g., when defining callbacks for bound events. 

        """ 

        return tuple(self._labels) 

 

    @property 

    def listboxes(self): 

        """ 

        A tuple containing the ``Tkinter.Listbox`` widgets used to 

        display individual columns.  These widgets will all be 

        augmented with a ``column_index`` attribute, which can be used 

        to determine which column they correspond to.  This can be 

        convenient, e.g., when defining callbacks for bound events. 

        """ 

        return tuple(self._listboxes) 

 

    #///////////////////////////////////////////////////////////////// 

    # Mouse & Keyboard Callback Functions 

    #///////////////////////////////////////////////////////////////// 

 

    def _select(self, e): 

        i = e.widget.nearest(e.y) 

        self.selection_clear(0, 'end') 

        self.selection_set(i) 

        self.activate(i) 

        self.focus() 

 

    def _scroll(self, delta): 

        for lb in self._listboxes: 

            lb.yview_scroll(delta, 'unit') 

        return 'break' 

 

    def _pagesize(self): 

        """:return: The number of rows that makes up one page""" 

        return int(self.index('@0,1000000')) - int(self.index('@0,0')) 

 

    #///////////////////////////////////////////////////////////////// 

    # Row selection 

    #///////////////////////////////////////////////////////////////// 

 

    def select(self, index=None, delta=None, see=True): 

        """ 

        Set the selected row.  If ``index`` is specified, then select 

        row ``index``.  Otherwise, if ``delta`` is specified, then move 

        the current selection by ``delta`` (negative numbers for up, 

        positive numbers for down).  This will not move the selection 

        past the top or the bottom of the list. 

 

        :param see: If true, then call ``self.see()`` with the newly 

            selected index, to ensure that it is visible. 

        """ 

        if (index is not None) and (delta is not None): 

            raise ValueError('specify index or delta, but not both') 

 

        # If delta was given, then calculate index. 

        if delta is not None: 

            if len(self.curselection()) == 0: 

                index = -1 + delta 

            else: 

                index = int(self.curselection()[0]) + delta 

 

        # Clear all selected rows. 

        self.selection_clear(0, 'end') 

 

        # Select the specified index 

        if index is not None: 

            index = min(max(index, 0), self.size()-1) 

            #self.activate(index) 

            self.selection_set(index) 

            if see: self.see(index) 

 

    #///////////////////////////////////////////////////////////////// 

    # Configuration 

    #///////////////////////////////////////////////////////////////// 

 

    def configure(self, cnf={}, **kw): 

        """ 

        Configure this widget.  Use ``label_*`` to configure all 

        labels; and ``listbox_*`` to configure all listboxes.  E.g.: 

 

                >>> mlb = MultiListbox(master, 5) 

                >>> mlb.configure(label_foreground='red') 

                >>> mlb.configure(listbox_foreground='red') 

        """ 

        cnf = dict(cnf.items() + kw.items()) 

        for (key, val) in cnf.items(): 

            if key.startswith('label_') or key.startswith('label-'): 

                for label in self._labels: 

                    label.configure({key[6:]: val}) 

            elif key.startswith('listbox_') or key.startswith('listbox-'): 

                for listbox in self._listboxes: 

                    listbox.configure({key[8:]: val}) 

            else: 

                Frame.configure(self, {key:val}) 

 

    def __setitem__(self, key, val): 

        """ 

        Configure this widget.  This is equivalent to 

        ``self.configure({key,val``)}.  See ``configure()``. 

        """ 

        self.configure({key:val}) 

 

    def rowconfigure(self, row_index, cnf={}, **kw): 

        """ 

        Configure all table cells in the given row.  Valid keyword 

        arguments are: ``background``, ``bg``, ``foreground``, ``fg``, 

        ``selectbackground``, ``selectforeground``. 

        """ 

        for lb in self._listboxes: lb.itemconfigure(row_index, cnf, **kw) 

 

    def columnconfigure(self, col_index, cnf={}, **kw): 

        """ 

        Configure all table cells in the given column.  Valid keyword 

        arguments are: ``background``, ``bg``, ``foreground``, ``fg``, 

        ``selectbackground``, ``selectforeground``. 

        """ 

        lb = self._listboxes[col_index] 

 

        cnf = dict(cnf.items() + kw.items()) 

        for (key, val) in cnf.items(): 

            if key in ('background', 'bg', 'foreground', 'fg', 

                       'selectbackground', 'selectforeground'): 

                for i in range(lb.size()): lb.itemconfigure(i, {key:val}) 

            else: 

                lb.configure({key:val}) 

 

    def itemconfigure(self, row_index, col_index, cnf=None, **kw): 

        """ 

        Configure the table cell at the given row and column.  Valid 

        keyword arguments are: ``background``, ``bg``, ``foreground``, 

        ``fg``, ``selectbackground``, ``selectforeground``. 

        """ 

        lb = self._listboxes[col_index] 

        return lb.itemconfigure(row_index, cnf, **kw) 

 

    #///////////////////////////////////////////////////////////////// 

    # Value Access 

    #///////////////////////////////////////////////////////////////// 

 

    def insert(self, index, *rows): 

        """ 

        Insert the given row or rows into the table, at the given 

        index.  Each row value should be a tuple of cell values, one 

        for each column in the row.  Index may be an integer or any of 

        the special strings (such as ``'end'``) accepted by 

        ``Tkinter.Listbox``. 

        """ 

        for elt in rows: 

            if len(elt) != len(self._column_names): 

                raise ValueError('rows should be tuples whose length ' 

                                 'is equal to the number of columns') 

        for (lb,elts) in zip(self._listboxes, zip(*rows)): 

            lb.insert(index, *elts) 

 

    def get(self, first, last=None): 

        """ 

        Return the value(s) of the specified row(s).  If ``last`` is 

        not specified, then return a single row value; otherwise, 

        return a list of row values.  Each row value is a tuple of 

        cell values, one for each column in the row. 

        """ 

        values = [lb.get(first, last) for lb in self._listboxes] 

        if last: 

            return [tuple(row) for row in zip(*values)] 

        else: 

            return tuple(values) 

 

    def bbox(self, row, col): 

        """ 

        Return the bounding box for the given table cell, relative to 

        this widget's top-left corner.  The bounding box is a tuple 

        of integers ``(left, top, width, height)``. 

        """ 

        dx, dy, _, _ = self.grid_bbox(row=0, column=col) 

        x, y, w, h = self._listboxes[col].bbox(row) 

        return int(x)+int(dx), int(y)+int(dy), int(w), int(h) 

 

    #///////////////////////////////////////////////////////////////// 

    # Hide/Show Columns 

    #///////////////////////////////////////////////////////////////// 

 

    def hide_column(self, col_index): 

        """ 

        Hide the given column.  The column's state is still 

        maintained: its values will still be returned by ``get()``, and 

        you must supply its values when calling ``insert()``.  It is 

        safe to call this on a column that is already hidden. 

 

        :see: ``show_column()`` 

        """ 

        if self._labels: 

            self._labels[col_index].grid_forget() 

        self.listboxes[col_index].grid_forget() 

        self.grid_columnconfigure(col_index, weight=0) 

 

    def show_column(self, col_index): 

        """ 

        Display a column that has been hidden using ``hide_column()``. 

        It is safe to call this on a column that is not hidden. 

        """ 

        weight = self._column_weights[col_index] 

        if self._labels: 

            self._labels[col_index].grid(column=col_index, row=0, 

                                         sticky='news', padx=0, pady=0) 

        self._listboxes[col_index].grid(column=col_index, row=1, 

                                        sticky='news', padx=0, pady=0) 

        self.grid_columnconfigure(col_index, weight=weight) 

 

    #///////////////////////////////////////////////////////////////// 

    # Binding Methods 

    #///////////////////////////////////////////////////////////////// 

 

    def bind_to_labels(self, sequence=None, func=None, add=None): 

        """ 

        Add a binding to each ``Tkinter.Label`` widget in this 

        mult-column listbox that will call ``func`` in response to the 

        event sequence. 

 

        :return: A list of the identifiers of replaced binding 

            functions (if any), allowing for their deletion (to 

            prevent a memory leak). 

        """ 

        return [label.bind(sequence, func, add) 

                for label in self.column_labels] 

 

    def bind_to_listboxes(self, sequence=None, func=None, add=None): 

        """ 

        Add a binding to each ``Tkinter.Listbox`` widget in this 

        mult-column listbox that will call ``func`` in response to the 

        event sequence. 

 

        :return: A list of the identifiers of replaced binding 

            functions (if any), allowing for their deletion (to 

            prevent a memory leak). 

        """ 

        for listbox in self.listboxes: 

            listbox.bind(sequence, func, add) 

 

    def bind_to_columns(self, sequence=None, func=None, add=None): 

        """ 

        Add a binding to each ``Tkinter.Label`` and ``Tkinter.Listbox`` 

        widget in this mult-column listbox that will call ``func`` in 

        response to the event sequence. 

 

        :return: A list of the identifiers of replaced binding 

            functions (if any), allowing for their deletion (to 

            prevent a memory leak). 

        """ 

        return (self.bind_to_labels(sequence, func, add) + 

                self.bind_to_listboxes(sequence, func, add)) 

 

    #///////////////////////////////////////////////////////////////// 

    # Simple Delegation 

    #///////////////////////////////////////////////////////////////// 

 

    # These methods delegate to the first listbox: 

    def curselection(self, *args, **kwargs): 

        return self._listboxes[0].curselection(*args, **kwargs) 

    def selection_includes(self, *args, **kwargs): 

        return self._listboxes[0].selection_includes(*args, **kwargs) 

    def itemcget(self, *args, **kwargs): 

        return self._listboxes[0].itemcget(*args, **kwargs) 

    def size(self, *args, **kwargs): 

        return self._listboxes[0].size(*args, **kwargs) 

    def index(self, *args, **kwargs): 

        return self._listboxes[0].index(*args, **kwargs) 

    def nearest(self, *args, **kwargs): 

        return self._listboxes[0].nearest(*args, **kwargs) 

 

    # These methods delegate to each listbox (and return None): 

    def activate(self, *args, **kwargs): 

        for lb in self._listboxes: lb.activate(*args, **kwargs) 

    def delete(self, *args, **kwargs): 

        for lb in self._listboxes: lb.delete(*args, **kwargs) 

    def scan_mark(self, *args, **kwargs): 

        for lb in self._listboxes: lb.scan_mark(*args, **kwargs) 

    def scan_dragto(self, *args, **kwargs): 

        for lb in self._listboxes: lb.scan_dragto(*args, **kwargs) 

    def see(self, *args, **kwargs): 

        for lb in self._listboxes: lb.see(*args, **kwargs) 

    def selection_anchor(self, *args, **kwargs): 

        for lb in self._listboxes: lb.selection_anchor(*args, **kwargs) 

    def selection_clear(self, *args, **kwargs): 

        for lb in self._listboxes: lb.selection_clear(*args, **kwargs) 

    def selection_set(self, *args, **kwargs): 

        for lb in self._listboxes: lb.selection_set(*args, **kwargs) 

    def yview(self, *args, **kwargs): 

        for lb in self._listboxes: v = lb.yview(*args, **kwargs) 

        return v # if called with no arguments 

    def yview_moveto(self, *args, **kwargs): 

        for lb in self._listboxes: lb.yview_moveto(*args, **kwargs) 

    def yview_scroll(self, *args, **kwargs): 

        for lb in self._listboxes: lb.yview_scroll(*args, **kwargs) 

 

    #///////////////////////////////////////////////////////////////// 

    # Aliases 

    #///////////////////////////////////////////////////////////////// 

 

    itemconfig = itemconfigure 

    rowconfig = rowconfigure 

    columnconfig = columnconfigure 

    select_anchor = selection_anchor 

    select_clear = selection_clear 

    select_includes = selection_includes 

    select_set = selection_set 

 

    #///////////////////////////////////////////////////////////////// 

    # These listbox methods are not defined for multi-listbox 

    #///////////////////////////////////////////////////////////////// 

    # def xview(self, *what): pass 

    # def xview_moveto(self, fraction): pass 

    # def xview_scroll(self, number, what): pass 

 

###################################################################### 

# Table 

###################################################################### 

 

class Table(object): 

    """ 

    A display widget for a table of values, based on a ``MultiListbox`` 

    widget.  For many purposes, ``Table`` can be treated as a 

    list-of-lists.  E.g., table[i] is a list of the values for row i; 

    and table.append(row) adds a new row with the given lits of 

    values.  Individual cells can be accessed using table[i,j], which 

    refers to the j-th column of the i-th row.  This can be used to 

    both read and write values from the table.  E.g.: 

 

        >>> table[i,j] = 'hello' 

 

    The column (j) can be given either as an index number, or as a 

    column name.  E.g., the following prints the value in the 3rd row 

    for the 'First Name' column: 

 

        >>> print(table[3, 'First Name']) 

        John 

 

    You can configure the colors for individual rows, columns, or 

    cells using ``rowconfig()``, ``columnconfig()``, and ``itemconfig()``. 

    The color configuration for each row will be preserved if the 

    table is modified; however, when new rows are added, any color 

    configurations that have been made for *columns* will not be 

    applied to the new row. 

 

    Note: Although ``Table`` acts like a widget in some ways (e.g., it 

    defines ``grid()``, ``pack()``, and ``bind()``), it is not itself a 

    widget; it just contains one.  This is because widgets need to 

    define ``__getitem__()``, ``__setitem__()``, and ``__nonzero__()`` in 

    a way that's incompatible with the fact that ``Table`` behaves as a 

    list-of-lists. 

 

    :ivar _mlb: The multi-column listbox used to display this table's data. 

    :ivar _rows: A list-of-lists used to hold the cell values of this 

        table.  Each element of _rows is a row value, i.e., a list of 

        cell values, one for each column in the row. 

    """ 

    def __init__(self, master, column_names, rows=None, 

                 column_weights=None, 

                 scrollbar=True, click_to_sort=True, 

                 reprfunc=None, cnf={}, **kw): 

        """ 

        Construct a new Table widget. 

 

        :type master: Tkinter.Widget 

        :param master: The widget that should contain the new table. 

        :type column_names: list(str) 

        :param column_names: A list of names for the columns; these 

            names will be used to create labels for each column; 

            and can be used as an index when reading or writing 

            cell values from the table. 

        :type rows: list(list) 

        :param rows: A list of row values used to initialze the table. 

            Each row value should be a tuple of cell values, one for 

            each column in the row. 

        :type scrollbar: bool 

        :param scrollbar: If true, then create a scrollbar for the 

            new table widget. 

        :type click_to_sort: bool 

        :param click_to_sort: If true, then create bindings that will 

            sort the table's rows by a given column's values if the 

            user clicks on that colum's label. 

        :type reprfunc: function 

        :param reprfunc: If specified, then use this function to 

            convert each table cell value to a string suitable for 

            display.  ``reprfunc`` has the following signature: 

            reprfunc(row_index, col_index, cell_value) -> str 

            (Note that the column is specified by index, not by name.) 

        :param cnf, kw: Configuration parameters for this widget's 

            contained ``MultiListbox``.  See ``MultiListbox.__init__()`` 

            for details. 

        """ 

        self._num_columns = len(column_names) 

        self._reprfunc = reprfunc 

        self._frame = Frame(master) 

 

        self._column_name_to_index = dict((c,i) for (i,c) in 

                                          enumerate(column_names)) 

 

        # Make a copy of the rows & check that it's valid. 

        if rows is None: self._rows = [] 

        else: self._rows = [[v for v in row] for row in rows] 

        for row in self._rows: self._checkrow(row) 

 

        # Create our multi-list box. 

        self._mlb = MultiListbox(self._frame, column_names, 

                                 column_weights, cnf, **kw) 

        self._mlb.pack(side='left', expand=True, fill='both') 

 

        # Optional scrollbar 

        if scrollbar: 

            sb = Scrollbar(self._frame, orient='vertical', 

                           command=self._mlb.yview) 

            self._mlb.listboxes[0]['yscrollcommand'] = sb.set 

            #for listbox in self._mlb.listboxes: 

            #    listbox['yscrollcommand'] = sb.set 

            sb.pack(side='right', fill='y') 

            self._scrollbar = sb 

 

        # Set up sorting 

        self._sortkey = None 

        if click_to_sort: 

            for i, l in enumerate(self._mlb.column_labels): 

                l.bind('<Button-1>', self._sort) 

 

        # Fill in our multi-list box. 

        self._fill_table() 

 

    #///////////////////////////////////////////////////////////////// 

    #{ Widget-like Methods 

    #///////////////////////////////////////////////////////////////// 

    # These all just delegate to either our frame or our MLB. 

 

    def pack(self, *args, **kwargs): 

        """Position this table's main frame widget in its parent 

        widget.  See ``Tkinter.Frame.pack()`` for more info.""" 

        self._frame.pack(*args, **kwargs) 

 

    def grid(self, *args, **kwargs): 

        """Position this table's main frame widget in its parent 

        widget.  See ``Tkinter.Frame.grid()`` for more info.""" 

        self._frame.grid(*args, **kwargs) 

 

    def focus(self): 

        """Direct (keyboard) input foxus to this widget.""" 

        self._mlb.focus() 

 

    def bind(self, sequence=None, func=None, add=None): 

        """Add a binding to this table's main frame that will call 

        ``func`` in response to the event sequence.""" 

        self._mlb.bind(sequence, func, add) 

 

    def rowconfigure(self, row_index, cnf={}, **kw): 

        """:see: ``MultiListbox.rowconfigure()``""" 

        self._mlb.rowconfigure(row_index, cnf, **kw) 

 

    def columnconfigure(self, col_index, cnf={}, **kw): 

        """:see: ``MultiListbox.columnconfigure()``""" 

        col_index = self.column_index(col_index) 

        self._mlb.columnconfigure(col_index, cnf, **kw) 

 

    def itemconfigure(self, row_index, col_index, cnf=None, **kw): 

        """:see: ``MultiListbox.itemconfigure()``""" 

        col_index = self.column_index(col_index) 

        return self._mlb.itemconfigure(row_index, col_index, cnf, **kw) 

 

    def bind_to_labels(self, sequence=None, func=None, add=None): 

        """:see: ``MultiListbox.bind_to_labels()``""" 

        return self._mlb.bind_to_labels(sequence, func, add) 

 

    def bind_to_listboxes(self, sequence=None, func=None, add=None): 

        """:see: ``MultiListbox.bind_to_listboxes()``""" 

        return self._mlb.bind_to_listboxes(sequence, func, add) 

 

    def bind_to_columns(self, sequence=None, func=None, add=None): 

        """:see: ``MultiListbox.bind_to_columns()``""" 

        return self._mlb.bind_to_columns(sequence, func, add) 

 

    rowconfig = rowconfigure 

    columnconfig = columnconfigure 

    itemconfig = itemconfigure 

 

    #///////////////////////////////////////////////////////////////// 

    #{ Table as list-of-lists 

    #///////////////////////////////////////////////////////////////// 

 

    def insert(self, row_index, rowvalue): 

        """ 

        Insert a new row into the table, so that its row index will be 

        ``row_index``.  If the table contains any rows whose row index 

        is greater than or equal to ``row_index``, then they will be 

        shifted down. 

 

        :param rowvalue: A tuple of cell values, one for each column 

            in the new row. 

        """ 

        self._checkrow(rowvalue) 

        self._rows.insert(row_index, rowvalue) 

        if self._reprfunc is not None: 

            rowvalue = [self._reprfunc(row_index,j,v) 

                        for (j,v) in enumerate(rowvalue)] 

        self._mlb.insert(row_index, rowvalue) 

        if self._DEBUG: self._check_table_vs_mlb() 

 

    def extend(self, rowvalues): 

        """ 

        Add new rows at the end of the table. 

 

        :param rowvalues: A list of row values used to initialze the 

            table.  Each row value should be a tuple of cell values, 

            one for each column in the row. 

        """ 

        for rowvalue in rowvalues: self.append(rowvalue) 

        if self._DEBUG: self._check_table_vs_mlb() 

 

    def append(self, rowvalue): 

        """ 

        Add a new row to the end of the table. 

 

        :param rowvalue: A tuple of cell values, one for each column 

            in the new row. 

        """ 

        self.insert(len(self._rows), rowvalue) 

        if self._DEBUG: self._check_table_vs_mlb() 

 

    def clear(self): 

        """ 

        Delete all rows in this table. 

        """ 

        self._rows = [] 

        self._mlb.delete(0, 'end') 

        if self._DEBUG: self._check_table_vs_mlb() 

 

    def __getitem__(self, index): 

        """ 

        Return the value of a row or a cell in this table.  If 

        ``index`` is an integer, then the row value for the ``index``th 

        row.  This row value consists of a tuple of cell values, one 

        for each column in the row.  If ``index`` is a tuple of two 

        integers, ``(i,j)``, then return the value of the cell in the 

        ``i``th row and the ``j``th column. 

        """ 

        if isinstance(index, slice): 

            raise ValueError('Slicing not supported') 

        elif isinstance(index, tuple) and len(index)==2: 

            return self._rows[index[0]][self.column_index(index[1])] 

        else: 

            return tuple(self._rows[index]) 

 

    def __setitem__(self, index, val): 

        """ 

        Replace the value of a row or a cell in this table with 

        ``val``. 

 

        If ``index`` is an integer, then ``val`` should be a row value 

        (i.e., a tuple of cell values, one for each column).  In this 

        case, the values of the ``index``th row of the table will be 

        replaced with the values in ``val``. 

 

        If ``index`` is a tuple of integers, ``(i,j)``, then replace the 

        value of the cell in the ``i``th row and ``j``th column with 

        ``val``. 

        """ 

        if isinstance(index, slice): 

            raise ValueError('Slicing not supported') 

 

 

        # table[i,j] = val 

        elif isinstance(index, tuple) and len(index)==2: 

            i, j = index[0], self.column_index(index[1]) 

            config_cookie = self._save_config_info([i]) 

            self._rows[i][j] = val 

            if self._reprfunc is not None: 

                val = self._reprfunc(i, j, val) 

            self._mlb.listboxes[j].insert(i, val) 

            self._mlb.listboxes[j].delete(i+1) 

            self._restore_config_info(config_cookie) 

 

        # table[i] = val 

        else: 

            config_cookie = self._save_config_info([index]) 

            self._checkrow(val) 

            self._rows[index] = list(val) 

            if self._reprfunc is not None: 

                val = [self._reprfunc(index,j,v) for (j,v) in enumerate(val)] 

            self._mlb.insert(index, val) 

            self._mlb.delete(index+1) 

            self._restore_config_info(config_cookie) 

 

    def __delitem__(self, row_index): 

        """ 

        Delete the ``row_index``th row from this table. 

        """ 

        if isinstance(index, slice): 

            raise ValueError('Slicing not supported') 

        if isinstance(row_index, tuple) and len(row_index)==2: 

            raise ValueError('Cannot delete a single cell!') 

        del self._rows[row_index] 

        self._mlb.delete(row_index) 

        if self._DEBUG: self._check_table_vs_mlb() 

 

    def __len__(self): 

        """ 

        :return: the number of rows in this table. 

        """ 

        return len(self._rows) 

 

    def _checkrow(self, rowvalue): 

        """ 

        Helper function: check that a given row value has the correct 

        number of elements; and if not, raise an exception. 

        """ 

        if len(rowvalue) != self._num_columns: 

            raise ValueError('Row %r has %d columns; expected %d' % 

                             (rowvalue, len(rowvalue), self._num_columns)) 

 

    #///////////////////////////////////////////////////////////////// 

    # Columns 

    #///////////////////////////////////////////////////////////////// 

 

    @property 

    def column_names(self): 

        """A list of the names of the columns in this table.""" 

        return self._mlb.column_names 

 

    def column_index(self, i): 

        """ 

        If ``i`` is a valid column index integer, then return it as is. 

        Otherwise, check if ``i`` is used as the name for any column; 

        if so, return that column's index.  Otherwise, raise a 

        ``KeyError`` exception. 

        """ 

        if isinstance(i, int) and 0 <= i < self._num_columns: 

            return i 

        else: 

            # This raises a key error if the column is not found. 

            return self._column_name_to_index[i] 

 

    def hide_column(self, column_index): 

        """:see: ``MultiListbox.hide_column()``""" 

        self._mlb.hide_column(self.column_index(column_index)) 

 

    def show_column(self, column_index): 

        """:see: ``MultiListbox.show_column()``""" 

        self._mlb.show_column(self.column_index(column_index)) 

 

    #///////////////////////////////////////////////////////////////// 

    # Selection 

    #///////////////////////////////////////////////////////////////// 

 

    def selected_row(self): 

        """ 

        Return the index of the currently selected row, or None if 

        no row is selected.  To get the row value itself, use 

        ``table[table.selected_row()]``. 

        """ 

        sel = self._mlb.curselection() 

        if sel: return int(sel[0]) 

        else: return None 

 

    def select(self, index=None, delta=None, see=True): 

        """:see: ``MultiListbox.select()``""" 

        self._mlb.select(index, delta, see) 

 

    #///////////////////////////////////////////////////////////////// 

    # Sorting 

    #///////////////////////////////////////////////////////////////// 

 

    def sort_by(self, column_index, order='toggle'): 

        """ 

        Sort the rows in this table, using the specified column's 

        values as a sort key. 

 

        :param column_index: Specifies which column to sort, using 

            either a column index (int) or a column's label name 

            (str). 

 

        :param order: Specifies whether to sort the values in 

            ascending or descending order: 

 

              - ``'ascending'``: Sort from least to greatest. 

              - ``'descending'``: Sort from greatest to least. 

              - ``'toggle'``: If the most recent call to ``sort_by()`` 

                sorted the table by the same column (``column_index``), 

                then reverse the rows; otherwise sort in ascending 

                order. 

        """ 

        if order not in ('ascending', 'descending', 'toggle'): 

            raise ValueError('sort_by(): order should be "ascending", ' 

                             '"descending", or "toggle".') 

        column_index = self.column_index(column_index) 

        config_cookie = self._save_config_info(index_by_id=True) 

 

        # Sort the rows. 

        if order == 'toggle' and column_index == self._sortkey: 

            self._rows.reverse() 

        else: 

            self._rows.sort(key=operator.itemgetter(column_index), 

                            reverse=(order=='descending')) 

            self._sortkey = column_index 

 

        # Redraw the table. 

        self._fill_table() 

        self._restore_config_info(config_cookie, index_by_id=True, see=True) 

        if self._DEBUG: self._check_table_vs_mlb() 

 

    def _sort(self, event): 

        """Event handler for clicking on a column label -- sort by 

        that column.""" 

        column_index = event.widget.column_index 

 

        # If they click on the far-left of far-right of a column's 

        # label, then resize rather than sorting. 

        if self._mlb._resize_column(event): 

            return 'continue' 

 

        # Otherwise, sort. 

        else: 

            self.sort_by(column_index) 

            return 'continue' 

 

    #///////////////////////////////////////////////////////////////// 

    #{ Table Drawing Helpers 

    #///////////////////////////////////////////////////////////////// 

 

    def _fill_table(self, save_config=True): 

        """ 

        Re-draw the table from scratch, by clearing out the table's 

        multi-column listbox; and then filling it in with values from 

        ``self._rows``.  Note that any cell-, row-, or column-specific 

        color configuration that has been done will be lost.  The 

        selection will also be lost -- i.e., no row will be selected 

        after this call completes. 

        """ 

        self._mlb.delete(0, 'end') 

        for i, row in enumerate(self._rows): 

            if self._reprfunc is not None: 

                row = [self._reprfunc(i,j,v) for (j,v) in enumerate(row)] 

            self._mlb.insert('end', row) 

 

    def _get_itemconfig(self, r, c): 

        return dict( (k, self._mlb.itemconfig(r, c, k)[-1]) 

                     for k in ('foreground', 'selectforeground', 

                               'background', 'selectbackground') ) 

 

    def _save_config_info(self, row_indices=None, index_by_id=False): 

        """ 

        Return a 'cookie' containing information about which row is 

        selected, and what color configurations have been applied. 

        this information can the be re-applied to the table (after 

        making modifications) using ``_restore_config_info()``.  Color 

        configuration information will be saved for any rows in 

        ``row_indices``, or in the entire table, if 

        ``row_indices=None``.  If ``index_by_id=True``, the the cookie 

        will associate rows with their configuration information based 

        on the rows' python id.  This is useful when performing 

        operations that re-arrange the rows (e.g. ``sort``).  If 

        ``index_by_id=False``, then it is assumed that all rows will be 

        in the same order when ``_restore_config_info()`` is called. 

        """ 

        # Default value for row_indices is all rows. 

        if row_indices is None: 

            row_indices = list(range(len(self._rows))) 

 

        # Look up our current selection. 

        selection = self.selected_row() 

        if index_by_id and selection is not None: 

            selection = id(self._rows[selection]) 

 

        # Look up the color configuration info for each row. 

        if index_by_id: 

            config = dict((id(self._rows[r]), [self._get_itemconfig(r, c) 

                                        for c in range(self._num_columns)]) 

                          for r in row_indices) 

        else: 

            config = dict((r, [self._get_itemconfig(r, c) 

                               for c in range(self._num_columns)]) 

                          for r in row_indices) 

 

 

        return selection, config 

 

    def _restore_config_info(self, cookie, index_by_id=False, see=False): 

        """ 

        Restore selection & color configuration information that was 

        saved using ``_save_config_info``. 

        """ 

        selection, config = cookie 

 

        # Clear the selection. 

        if selection is None: 

            self._mlb.selection_clear(0, 'end') 

 

        # Restore selection & color config 

        if index_by_id: 

            for r, row in enumerate(self._rows): 

                if id(row) in config: 

                    for c in range(self._num_columns): 

                        self._mlb.itemconfigure(r, c, config[id(row)][c]) 

                if id(row) == selection: 

                    self._mlb.select(r, see=see) 

        else: 

            if selection is not None: 

                self._mlb.select(selection, see=see) 

            for r in config: 

                for c in range(self._num_columns): 

                    self._mlb.itemconfigure(r, c, config[r][c]) 

 

    #///////////////////////////////////////////////////////////////// 

    # Debugging (Invariant Checker) 

    #///////////////////////////////////////////////////////////////// 

 

    _DEBUG = False 

    """If true, then run ``_check_table_vs_mlb()`` after any operation 

       that modifies the table.""" 

 

    def _check_table_vs_mlb(self): 

        """ 

        Verify that the contents of the table's ``_rows`` variable match 

        the contents of its multi-listbox (``_mlb``).  This is just 

        included for debugging purposes, to make sure that the 

        list-modifying operations are working correctly. 

        """ 

        for col in self._mlb.listboxes: 

            assert len(self) == col.size() 

        for row in self: 

            assert len(row) == self._num_columns 

        assert self._num_columns == len(self._mlb.column_names) 

        #assert self._column_names == self._mlb.column_names 

        for i, row in enumerate(self): 

            for j, cell in enumerate(row): 

                if self._reprfunc is not None: 

                    cell = self._reprfunc(i, j, cell) 

                assert self._mlb.get(i)[j] == cell 

 

###################################################################### 

# Demo/Test Function 

###################################################################### 

 

# update this to use new WordNet API 

def demo(): 

    root = Tk() 

    root.bind('<Control-q>', lambda e: root.destroy()) 

 

    table = Table(root, 'Word Synset Hypernym Hyponym'.split(), 

                  column_weights=[0, 1, 1, 1], 

                  reprfunc=(lambda i,j,s: '  %s' % s)) 

    table.pack(expand=True, fill='both') 

 

    from nltk.corpus import wordnet 

    from nltk.corpus import brown 

    for word, pos in sorted(set(brown.tagged_words()[:500])): 

        if pos[0] != 'N': continue 

        word = word.lower() 

        for synset in wordnet.synsets(word): 

            hyper = (synset.hypernyms()+[''])[0] 

            hypo = (synset.hyponyms()+[''])[0] 

            table.append([word, 

                          getattr(synset, 'definition', '*none*'), 

                          getattr(hyper, 'definition', '*none*'), 

                          getattr(hypo, 'definition', '*none*')]) 

 

    table.columnconfig('Word', background='#afa') 

    table.columnconfig('Synset', background='#efe') 

    table.columnconfig('Hypernym', background='#fee') 

    table.columnconfig('Hyponym', background='#ffe') 

    for row in range(len(table)): 

        for column in ('Hypernym', 'Hyponym'): 

            if table[row, column] == '*none*': 

                table.itemconfig(row, column, foreground='#666', 

                                 selectforeground='#666') 

    root.mainloop() 

 

if __name__ == '__main__': 

    demo()