1# ------------------------------------------------------------------------------ 2# 3# Copyright (c) 2005, Enthought, Inc. 4# All rights reserved. 5# 6# This software is provided without warranty under the terms of the BSD 7# license included in LICENSE.txt and may be redistributed only 8# under the conditions described in the aforementioned license. The license 9# is also available online at http://www.enthought.com/licenses/BSD.txt 10# 11# Thanks for using Enthought open source! 12# 13# Author: David C. Morrill 14# Date: 07/01/2005 15# 16# ------------------------------------------------------------------------------ 17 18""" Defines the table editor for the wxPython user interface toolkit. 19""" 20 21 22from operator import itemgetter 23 24import wx 25 26from pyface.dock.api import ( 27 DockWindow, 28 DockSizer, 29 DockSection, 30 DockRegion, 31 DockControl, 32) 33from pyface.image_resource import ImageResource 34from pyface.timer.api import do_later 35from pyface.ui.wx.grid.api import Grid 36from traits.api import ( 37 Int, 38 List, 39 Instance, 40 Str, 41 Any, 42 Button, 43 Tuple, 44 HasPrivateTraits, 45 Bool, 46 Event, 47 Property, 48) 49 50from traitsui.api import ( 51 View, 52 Item, 53 UI, 54 InstanceEditor, 55 EnumEditor, 56 Handler, 57 SetEditor, 58 ListUndoItem, 59) 60from traitsui.editors.table_editor import BaseTableEditor, customize_filter 61from traitsui.menu import Action, ToolBar 62from traitsui.table_column import TableColumn, ObjectColumn 63from traitsui.table_filter import TableFilter 64from traitsui.ui_traits import SequenceTypes 65 66from .constants import ( 67 TableCellBackgroundColor, 68 TableCellColor, 69 TableLabelBackgroundColor, 70 TableLabelColor, 71 TableReadOnlyBackgroundColor, 72 TableSelectionBackgroundColor, 73 TableSelectionTextColor, 74) 75from .editor import Editor 76from .table_model import TableModel, TraitGridSelection 77from .helper import TraitsUIPanel 78 79 80#: Mapping from TableEditor selection modes to Grid selection modes: 81GridModes = { 82 "row": "rows", 83 "rows": "rows", 84 "column": "cols", 85 "columns": "cols", 86 "cell": "cell", 87 "cells": "cell", 88} 89 90 91def _get_color(color, default_color): 92 """ Return the color if it is not None, otherwise use default. """ 93 if color is not None: 94 return color 95 return default_color 96 97 98class TableEditor(Editor, BaseTableEditor): 99 """ Editor that presents data in a table. Optionally, tables can have 100 a set of filters that reduce the set of data displayed, according to 101 their criteria. 102 """ 103 104 # ------------------------------------------------------------------------- 105 # Trait definitions: 106 # ------------------------------------------------------------------------- 107 108 #: The set of columns currently defined on the editor: 109 columns = List(Instance(TableColumn)) 110 111 #: Index of currently edited (i.e., selected) table item(s): 112 selected_row_index = Int(-1) 113 selected_row_indices = List(Int) 114 selected_indices = Property() 115 116 selected_column_index = Int(-1) 117 selected_column_indices = List(Int) 118 119 selected_cell_index = Tuple(Int, Int) 120 selected_cell_indices = List(Tuple(Int, Int)) 121 122 #: The currently selected table item(s): 123 selected_row = Any() 124 selected_rows = List() 125 selected_items = Property() 126 127 selected_column = Any() 128 selected_columns = List() 129 130 selected_cell = Tuple(Any, Str) 131 selected_cells = List(Tuple(Any, Str)) 132 133 selected_values = Property() 134 135 #: The indices of the table items currently passing the table filter: 136 filtered_indices = List(Int) 137 138 #: The event fired when a cell is clicked on: 139 click = Event() 140 141 #: The event fired when a cell is double-clicked on: 142 dclick = Event() 143 144 #: Is the editor in row mode (i.e. not column or cell mode)? 145 in_row_mode = Property() 146 147 #: Is the editor in column mode (i.e. not row or cell mode)? 148 in_column_mode = Property() 149 150 #: Current filter object (should be a TableFilter or callable or None): 151 filter = Any() 152 153 #: The grid widget associated with the editor: 154 grid = Instance(Grid) 155 156 #: The table model associated with the editor: 157 model = Instance(TableModel) 158 159 #: TableEditorToolbar associated with the editor: 160 toolbar = Any() 161 162 #: The Traits UI associated with the table editor toolbar: 163 toolbar_ui = Instance(UI) 164 165 #: Is the table editor scrollable? This value overrides the default. 166 scrollable = True 167 168 #: Is 'auto_add' mode in effect? (I.e., new rows are automatically added to 169 #: the end of the table when the user modifies current last row.) 170 auto_add = Bool(False) 171 172 def init(self, parent): 173 """ Finishes initializing the editor by creating the underlying toolkit 174 widget. 175 """ 176 177 factory = self.factory 178 self.filter = factory.filter 179 self.auto_add = factory.auto_add and (factory.row_factory is not None) 180 181 columns = factory.columns[:] 182 if (len(columns) == 0) and (len(self.value) > 0): 183 columns = [ 184 ObjectColumn(name=name) 185 for name in self.value[0].editable_traits() 186 ] 187 self.columns = columns 188 189 self.model = model = TableModel(editor=self, reverse=factory.reverse) 190 model.on_trait_change(self._model_sorted, "sorted", dispatch="ui") 191 mode = factory.selection_mode 192 row_mode = mode in ("row", "rows") 193 selected = None 194 items = model.get_filtered_items() 195 if factory.editable and (len(items) > 0): 196 selected = items[0] 197 if (factory.edit_view == " ") or (not row_mode): 198 self.control = panel = TraitsUIPanel(parent, -1) 199 sizer = wx.BoxSizer(wx.VERTICAL) 200 self._create_toolbar(panel, sizer) 201 202 # Create the table (i.e. grid) control: 203 hsizer = wx.BoxSizer(wx.HORIZONTAL) 204 self._create_grid(panel, hsizer) 205 sizer.Add(hsizer, 1, wx.EXPAND) 206 else: 207 item = self.item 208 name = item.get_label(self.ui) 209 theme = factory.dock_theme or item.container.dock_theme 210 self.control = dw = DockWindow(parent, theme=theme).control 211 panel = TraitsUIPanel(dw, -1, size=(300, 300)) 212 sizer = wx.BoxSizer(wx.VERTICAL) 213 dc = DockControl( 214 name=name + " Table", id="table", control=panel, style="fixed" 215 ) 216 contents = [DockRegion(contents=[dc])] 217 self._create_toolbar(panel, sizer) 218 selected = None 219 items = model.get_filtered_items() 220 if factory.editable and (len(items) > 0): 221 selected = items[0] 222 223 # Create the table (i.e. grid) control: 224 hsizer = wx.BoxSizer(wx.HORIZONTAL) 225 self._create_grid(panel, hsizer) 226 sizer.Add(hsizer, 1, wx.EXPAND) 227 228 # Assign the initial object here, so a valid editor will be built 229 # when the 'edit_traits' call is made: 230 self.selected_row = selected 231 self._ui = ui = self.edit_traits( 232 parent=dw, 233 kind="subpanel", 234 view=View( 235 [ 236 Item( 237 "selected_row", 238 style="custom", 239 editor=InstanceEditor( 240 view=factory.edit_view, kind="subpanel" 241 ), 242 resizable=True, 243 width=factory.edit_view_width, 244 height=factory.edit_view_height, 245 ), 246 "|<>", 247 ], 248 resizable=True, 249 handler=factory.edit_view_handler, 250 ), 251 ) 252 253 # Set the parent UI of the new UI to our own UI: 254 ui.parent = self.ui 255 256 # Reset the object so that the sub-sub-view will pick up the 257 # correct history also: 258 self.selected_row = None 259 self.selected_row = selected 260 261 dc.style = item.dock 262 contents.append( 263 DockRegion( 264 contents=[ 265 DockControl( 266 name=name + " Editor", 267 id="editor", 268 control=ui.control, 269 style=item.dock, 270 ) 271 ] 272 ) 273 ) 274 275 # Finish setting up the DockWindow: 276 dw.SetSizer( 277 DockSizer( 278 contents=DockSection( 279 contents=contents, 280 is_row=(factory.orientation == "horizontal"), 281 ) 282 ) 283 ) 284 285 # Set up the required externally synchronized traits (if any): 286 sv = self.sync_value 287 is_list = mode[-1] == "s" 288 sv(factory.click, "click", "to") 289 sv(factory.dclick, "dclick", "to") 290 sv(factory.filter_name, "filter", "from") 291 sv(factory.columns_name, "columns", is_list=True) 292 sv(factory.filtered_indices, "filtered_indices", "to") 293 sv(factory.selected, "selected_%s" % mode, is_list=is_list) 294 if is_list: 295 sv( 296 factory.selected_indices, 297 "selected_%s_indices" % mode[:-1], 298 is_list=True, 299 ) 300 else: 301 sv(factory.selected_indices, "selected_%s_index" % mode) 302 303 # Listen for the selection changing on the grid: 304 self.grid.on_trait_change( 305 getattr(self, "_selection_%s_updated" % mode), 306 "selection_changed", 307 dispatch="ui", 308 ) 309 310 # Set the min height of the grid panel to 0, this will provide 311 # a scrollbar if the window is resized such that only the first row 312 # is visible 313 panel.SetMinSize((-1, 0)) 314 315 # Finish the panel layout setup: 316 panel.SetSizer(sizer) 317 318 def _create_grid(self, parent, sizer): 319 """ Creates the associated grid control used to implement the table. 320 """ 321 factory = self.factory 322 selection_mode = GridModes[factory.selection_mode] 323 if factory.selection_bg_color is None: 324 selection_mode = "" 325 326 cell_color = _get_color(factory.cell_color, TableCellColor) 327 cell_bg_color = _get_color( 328 factory.cell_bg_color, TableCellBackgroundColor 329 ) 330 cell_read_only_bg_color = _get_color( 331 factory.cell_read_only_bg_color, TableReadOnlyBackgroundColor 332 ) 333 label_bg_color = _get_color( 334 factory.label_bg_color, TableLabelBackgroundColor 335 ) 336 label_color = _get_color(factory.label_color, TableLabelColor) 337 selection_text_color = _get_color( 338 factory.selection_color, TableSelectionTextColor 339 ) 340 selection_bg_color = _get_color( 341 factory.selection_bg_color, TableSelectionBackgroundColor 342 ) 343 344 self.grid = grid = Grid( 345 parent, 346 model=self.model, 347 enable_lines=factory.show_lines, 348 grid_line_color=factory.line_color, 349 show_row_headers=factory.show_row_labels, 350 show_column_headers=factory.show_column_labels, 351 default_cell_font=factory.cell_font, 352 default_cell_text_color=cell_color, 353 default_cell_bg_color=cell_bg_color, 354 default_cell_read_only_color=cell_read_only_bg_color, 355 default_label_font=factory.label_font, 356 default_label_text_color=label_color, 357 default_label_bg_color=label_bg_color, 358 selection_bg_color=selection_bg_color, 359 selection_text_color=selection_text_color, 360 autosize=factory.auto_size, 361 read_only=not factory.editable, 362 edit_on_first_click=factory.edit_on_first_click, 363 selection_mode=selection_mode, 364 allow_column_sort=factory.sortable, 365 allow_row_sort=False, 366 column_label_height=factory.column_label_height, 367 row_label_width=factory.row_label_width, 368 ) 369 _grid = grid._grid 370 _grid.SetScrollLineY(factory.scroll_dy) 371 372 # Set the default size for each table row: 373 height = factory.row_height 374 if height <= 0: 375 height = _grid.GetTextExtent("My")[1] + 9 376 _grid.SetDefaultRowSize(height) 377 378 # Allow the table to be resizable if the user did not explicitly 379 # specify a number of rows to display: 380 self.scrollable = factory.rows == 0 381 382 # Calculate a reasonable default size for the table: 383 if len(self.model.get_filtered_items()) > 0: 384 height = _grid.GetRowSize(0) 385 386 max_rows = factory.rows or 15 387 388 min_width = max(150, 80 * len(self.columns)) 389 390 if factory.show_column_labels: 391 min_height = _grid.GetColLabelSize() + (max_rows * height) 392 else: 393 min_height = max_rows * height 394 395 _grid.SetMinSize(wx.Size(min_width, min_height)) 396 397 # On Linux, there is what appears to be a bug in wx in which the 398 # vertical scrollbar will not be sized properly if the TableEditor is 399 # sized to be shorter than the minimum height specified above. Since 400 # this height is only set to ensure that the TableEditor is sized 401 # correctly during the initial UI layout, we un-set it after this takes 402 # place (addresses ticket 1810) 403 def clear_minimum_height(info): 404 min_size = _grid.GetMinSize() 405 min_size.height = 0 406 _grid.SetMinSize(min_size) 407 408 self.ui.add_defined(clear_minimum_height) 409 410 sizer.Add(grid.control, 1, wx.EXPAND) 411 412 return grid.control 413 414 def _create_toolbar(self, parent, sizer): 415 """ Creates the table editing toolbar. 416 """ 417 418 factory = self.factory 419 if not factory.show_toolbar: 420 return 421 422 toolbar = TableEditorToolbar(parent=parent, editor=self) 423 if (toolbar.control is not None) or (len(factory.filters) > 0): 424 tb_sizer = wx.BoxSizer(wx.HORIZONTAL) 425 426 if len(factory.filters) > 0: 427 view = View( 428 [ 429 Item( 430 "filter<250>{View}", editor=factory._filter_editor 431 ), 432 "_", 433 Item( 434 "filter_summary<100>{Results}~", 435 object="model", 436 resizable=False, 437 ), 438 "_", 439 "-", 440 ], 441 resizable=True, 442 ) 443 self.toolbar_ui = ui = view.ui( 444 context={"object": self, "model": self.model}, 445 parent=parent, 446 kind="subpanel", 447 ).trait_set(parent=self.ui) 448 tb_sizer.Add(ui.control, 0) 449 450 if toolbar.control is not None: 451 self.toolbar = toolbar 452 # add padding so the toolbar is right aligned 453 tb_sizer.Add((1, 1), 1, wx.EXPAND) 454 tb_sizer.Add(toolbar.control, 0) 455 456 sizer.Add(tb_sizer, 0, wx.EXPAND) 457 458 def dispose(self): 459 """ Disposes of the contents of an editor. 460 """ 461 if self.toolbar_ui is not None: 462 self.toolbar_ui.dispose() 463 464 if self._ui is not None: 465 self._ui.dispose() 466 467 self.grid.on_trait_change( 468 getattr( 469 self, "_selection_%s_updated" % self.factory.selection_mode 470 ), 471 "selection_changed", 472 remove=True, 473 ) 474 475 self.model.on_trait_change(self._model_sorted, "sorted", remove=True) 476 477 self.grid.dispose() 478 self.model.dispose() 479 480 # Break any links needed to allow garbage collection: 481 self.grid = self.model = self.toolbar = None 482 483 super(TableEditor, self).dispose() 484 485 def update_editor(self): 486 """ Updates the editor when the object trait changes externally to the 487 editor. 488 """ 489 # fixme: Do we need to override this method? 490 pass 491 492 def refresh(self): 493 """ Refreshes the editor control. 494 """ 495 self.grid._grid.Refresh() 496 497 def set_selection(self, objects=[], notify=True): 498 """ Sets the current selection to a set of specified objects. 499 """ 500 if not isinstance(objects, SequenceTypes): 501 objects = [objects] 502 503 self.grid.set_selection( 504 [TraitGridSelection(obj=object) for object in objects], 505 notify=notify, 506 ) 507 508 def set_extended_selection(self, *pairs): 509 """ Sets the current selection to a set of specified object/column 510 pairs. 511 """ 512 if (len(pairs) == 1) and isinstance(pairs[0], list): 513 pairs = pairs[0] 514 515 grid_selections = [ 516 TraitGridSelection(obj=object, name=name) for object, name in pairs 517 ] 518 519 self.grid.set_selection(grid_selections) 520 521 def create_new_row(self): 522 """ Creates a new row object using the provided factory. 523 """ 524 factory = self.factory 525 kw = factory.row_factory_kw.copy() 526 if "__table_editor__" in kw: 527 kw["__table_editor__"] = self 528 529 return self.ui.evaluate( 530 factory.row_factory, *factory.row_factory_args, **kw 531 ) 532 533 def add_row(self, object=None, index=None): 534 """ Adds a specified object as a new row after the specified index. 535 """ 536 filtered_items = self.model.get_filtered_items 537 538 if index is None: 539 indices = self.selected_indices 540 if len(indices) == 0: 541 indices = [len(filtered_items()) - 1] 542 indices.reverse() 543 else: 544 indices = [index] 545 546 if object is None: 547 objects = [] 548 for index in indices: 549 object = self.create_new_row() 550 if object is None: 551 if self.in_row_mode: 552 self.set_selection() 553 return 554 555 objects.append(object) 556 else: 557 objects = [object] 558 559 items = [] 560 insert_item_after = self.model.insert_filtered_item_after 561 in_row_mode = self.in_row_mode 562 for i, index in enumerate(indices): 563 object = objects[i] 564 index, extend = insert_item_after(index, object) 565 566 if in_row_mode and (object in filtered_items()): 567 items.append(object) 568 569 self._add_undo( 570 ListUndoItem( 571 object=self.object, 572 name=self.name, 573 index=index, 574 added=[object], 575 ), 576 extend, 577 ) 578 579 if in_row_mode: 580 self.set_selection(items) 581 582 def move_column(self, from_column, to_column): 583 """ Moves the specified **from_column** from its current position to 584 just preceding the specified **to_column**. 585 """ 586 columns = self.columns 587 frm = columns.index(from_column) 588 if to_column is None: 589 to = len(columns) 590 else: 591 to = columns.index(to_column) 592 del columns[frm] 593 columns.insert(to - (frm < to), from_column) 594 595 return True 596 597 # -- Property Implementations --------------------------------------------- 598 599 def _get_selected_indices(self): 600 sm = self.factory.selection_mode 601 if sm == "rows": 602 return self.selected_row_indices 603 604 elif sm == "row": 605 index = self.selected_row_index 606 if index >= 0: 607 return [index] 608 609 elif sm == "cells": 610 return list({row_col[0] for row_col in self.selected_cell_indices}) 611 612 elif sm == "cell": 613 index = self.selected_cell_index[0] 614 if index >= 0: 615 return [index] 616 617 return [] 618 619 def _get_selected_items(self): 620 sm = self.factory.selection_mode 621 if sm == "rows": 622 return self.selected_rows 623 624 elif sm == "row": 625 item = self.selected_row 626 if item is not None: 627 return [item] 628 629 elif sm == "cells": 630 return list({item_name[0] for item_name in self.selected_cells}) 631 632 elif sm == "cell": 633 item = self.selected_cell[0] 634 if item is not None: 635 return [item] 636 637 return [] 638 639 def _get_selected_values(self): 640 if self.in_row_mode: 641 return [(item, "") for item in self.selected_items] 642 643 if self.in_column_mode: 644 if self.factory.selection_mode == "columns": 645 return [(None, column) for column in self.selected_columns] 646 647 column = self.selected_column 648 if column != "": 649 return [(None, column)] 650 651 return [] 652 653 if self.factory.selection_mode == "cells": 654 return self.selected_cells 655 656 item = self.selected_cell 657 if item[0] is not None: 658 return [item] 659 660 return [] 661 662 def _get_in_row_mode(self): 663 return self.factory.selection_mode in ("row", "rows") 664 665 def _get_in_column_mode(self): 666 return self.factory.selection_mode in ("column", "columns") 667 668 # -- UI preference save/restore interface --------------------------------- 669 670 def restore_prefs(self, prefs): 671 """ Restores any saved user preference information associated with the 672 editor. 673 """ 674 factory = self.factory 675 try: 676 filters = prefs.get("filters", None) 677 if filters is not None: 678 factory.filters = [ 679 f for f in factory.filters if f.template 680 ] + [f for f in filters if not f.template] 681 682 columns = prefs.get("columns") 683 if columns is not None: 684 new_columns = [] 685 all_columns = self.columns + factory.other_columns 686 for column in columns: 687 for column2 in all_columns: 688 if column == column2.get_label(): 689 new_columns.append(column2) 690 break 691 self.columns = new_columns 692 693 # Restore the column sizes if possible: 694 if not factory.auto_size: 695 widths = prefs.get("widths") 696 if widths is not None: 697 # fixme: Talk to Jason about a better way to do this: 698 self.grid._user_col_size = True 699 700 set_col_size = self.grid._grid.SetColSize 701 for i, width in enumerate(widths): 702 if width >= 0: 703 set_col_size(i, width) 704 705 structure = prefs.get("structure") 706 if (structure is not None) and (factory.edit_view != " "): 707 self.control.GetSizer().SetStructure(self.control, structure) 708 except Exception: 709 pass 710 711 def save_prefs(self): 712 """ Returns any user preference information associated with the editor. 713 """ 714 get_col_size = self.grid._grid.GetColSize 715 result = { 716 "filters": [f for f in self.factory.filters if not f.template], 717 "columns": [c.get_label() for c in self.columns], 718 "widths": [get_col_size(i) for i in range(len(self.columns))], 719 } 720 721 if self.factory.edit_view != " ": 722 result["structure"] = self.control.GetSizer().GetStructure() 723 724 return result 725 726 # -- Public Methods ------------------------------------------------------- 727 728 def filter_modified(self): 729 """ Handles updating the selection when some aspect of the current 730 filter has changed. 731 """ 732 values = self.selected_values 733 if len(values) > 0: 734 if self.in_column_mode: 735 self.set_extended_selection(values) 736 else: 737 items = self.model.get_filtered_items() 738 self.set_extended_selection( 739 [item for item in values if item[0] in items] 740 ) 741 742 # -- Event Handlers ------------------------------------------------------- 743 744 def _selection_row_updated(self, event): 745 """ Handles the user selecting items (rows, columns, cells) in the 746 table. 747 """ 748 gfi = self.model.get_filtered_item 749 rio = self.model.raw_index_of 750 tl = self.grid._grid.GetSelectionBlockTopLeft() 751 br = iter(self.grid._grid.GetSelectionBlockBottomRight()) 752 rows = len(self.model.get_filtered_items()) 753 if self.auto_add: 754 rows -= 1 755 756 # Get the row items and indices in the selection: 757 values = [] 758 for row0, col0 in tl: 759 row1, col1 = next(br) 760 for row in range(row0, row1 + 1): 761 if row < rows: 762 values.append((rio(row), gfi(row))) 763 764 if len(values) > 0: 765 # Sort by increasing row index: 766 values.sort(key=itemgetter(0)) 767 index, row = values[0] 768 else: 769 index, row = -1, None 770 771 # Save the new selection information: 772 self.trait_set(selected_row_index=index, trait_change_notify=False) 773 self.setx(selected_row=row) 774 775 # Update the toolbar status: 776 self._update_toolbar(row is not None) 777 778 # Invoke the user 'on_select' handler: 779 self.ui.evaluate(self.factory.on_select, row) 780 781 def _selection_rows_updated(self, event): 782 """ Handles multiple row selection changes. 783 """ 784 gfi = self.model.get_filtered_item 785 rio = self.model.raw_index_of 786 tl = self.grid._grid.GetSelectionBlockTopLeft() 787 br = iter(self.grid._grid.GetSelectionBlockBottomRight()) 788 rows = len(self.model.get_filtered_items()) 789 if self.auto_add: 790 rows -= 1 791 792 # Get the row items and indices in the selection: 793 values = [] 794 for row0, col0 in tl: 795 row1, col1 = next(br) 796 for row in range(row0, row1 + 1): 797 if row < rows: 798 values.append((rio(row), gfi(row))) 799 800 # Sort by increasing row index: 801 values.sort(key=itemgetter(0)) 802 803 # Save the new selection information: 804 self.trait_set( 805 selected_row_indices=[v[0] for v in values], 806 trait_change_notify=False, 807 ) 808 rows = [v[1] for v in values] 809 self.setx(selected_rows=rows) 810 811 # Update the toolbar status: 812 self._update_toolbar(len(values) > 0) 813 814 # Invoke the user 'on_select' handler: 815 self.ui.evaluate(self.factory.on_select, rows) 816 817 def _selection_column_updated(self, event): 818 """ Handles single column selection changes. 819 """ 820 cols = self.columns 821 tl = self.grid._grid.GetSelectionBlockTopLeft() 822 br = iter(self.grid._grid.GetSelectionBlockBottomRight()) 823 824 # Get the column items and indices in the selection: 825 values = [] 826 for row0, col0 in tl: 827 row1, col1 = next(br) 828 for col in range(col0, col1 + 1): 829 values.append((col, cols[col].name)) 830 831 if len(values) > 0: 832 # Sort by increasing column index: 833 values.sort(key=itemgetter(0)) 834 index, column = values[0] 835 else: 836 index, column = -1, "" 837 838 # Save the new selection information: 839 self.trait_set(selected_column_index=index, trait_change_notify=False) 840 self.setx(selected_column=column) 841 842 # Invoke the user 'on_select' handler: 843 self.ui.evaluate(self.factory.on_select, column) 844 845 def _selection_columns_updated(self, event): 846 """ Handles multiple column selection changes. 847 """ 848 cols = self.columns 849 tl = self.grid._grid.GetSelectionBlockTopLeft() 850 br = iter(self.grid._grid.GetSelectionBlockBottomRight()) 851 852 # Get the column items and indices in the selection: 853 values = [] 854 for row0, col0 in tl: 855 row1, col1 = next(br) 856 for col in range(col0, col1 + 1): 857 values.append((col, cols[col].name)) 858 859 # Sort by increasing row index: 860 values.sort(key=itemgetter(0)) 861 862 # Save the new selection information: 863 self.trait_set( 864 selected_column_indices=[v[0] for v in values], 865 trait_change_notify=False, 866 ) 867 columns = [v[1] for v in values] 868 self.setx(selected_columns=columns) 869 870 # Invoke the user 'on_select' handler: 871 self.ui.evaluate(self.factory.on_select, columns) 872 873 def _selection_cell_updated(self, event): 874 """ Handles single cell selection changes. 875 """ 876 tl = self.grid._grid.GetSelectionBlockTopLeft() 877 if len(tl) == 0: 878 return 879 880 gfi = self.model.get_filtered_item 881 rio = self.model.raw_index_of 882 cols = self.columns 883 br = iter(self.grid._grid.GetSelectionBlockBottomRight()) 884 885 # Get the column items and indices in the selection: 886 values = [] 887 for row0, col0 in tl: 888 row1, col1 = next(br) 889 for row in range(row0, row1 + 1): 890 item = gfi(row) 891 for col in range(col0, col1 + 1): 892 values.append(((rio(row), col), (item, cols[col].name))) 893 894 if len(values) > 0: 895 # Sort by increasing row, column index: 896 values.sort(key=itemgetter(0)) 897 index, cell = values[0] 898 else: 899 index, cell = (-1, -1), (None, "") 900 901 # Save the new selection information: 902 self.trait_set(selected_cell_index=index, trait_change_notify=False) 903 self.setx(selected_cell=cell) 904 905 # Update the toolbar status: 906 self._update_toolbar(len(values) > 0) 907 908 # Invoke the user 'on_select' handler: 909 self.ui.evaluate(self.factory.on_select, cell) 910 911 def _selection_cells_updated(self, event): 912 """ Handles multiple cell selection changes. 913 """ 914 gfi = self.model.get_filtered_item 915 rio = self.model.raw_index_of 916 cols = self.columns 917 tl = self.grid._grid.GetSelectionBlockTopLeft() 918 br = iter(self.grid._grid.GetSelectionBlockBottomRight()) 919 920 # Get the column items and indices in the selection: 921 values = [] 922 for row0, col0 in tl: 923 row1, col1 = next(br) 924 for row in range(row0, row1 + 1): 925 item = gfi(row) 926 for col in range(col0, col1 + 1): 927 values.append(((rio(row), col), (item, cols[col].name))) 928 929 # Sort by increasing row, column index: 930 values.sort(key=itemgetter(0)) 931 932 # Save the new selection information: 933 self.setx(selected_cell_indices=[v[0] for v in values]) 934 cells = [v[1] for v in values] 935 self.setx(selected_cells=cells) 936 937 # Update the toolbar status: 938 self._update_toolbar(len(cells) > 0) 939 940 # Invoke the user 'on_select' handler: 941 self.ui.evaluate(self.factory.on_select, cells) 942 943 def _selected_row_changed(self, item): 944 if not self._no_notify: 945 if item is None: 946 self.set_selection(notify=False) 947 else: 948 self.set_selection(item, notify=False) 949 950 def _selected_row_index_changed(self, row): 951 if not self._no_notify: 952 if row < 0: 953 self.set_selection(notify=False) 954 else: 955 self.set_selection(self.value[row], notify=False) 956 957 def _selected_rows_changed(self, items): 958 if not self._no_notify: 959 self.set_selection(items, notify=False) 960 961 def _selected_row_indices_changed(self, indices): 962 if not self._no_notify: 963 value = self.value 964 self.set_selection([value[i] for i in indices], notify=False) 965 966 def _selected_column_changed(self, name): 967 if not self._no_notify: 968 self.set_extended_selection((None, name)) 969 970 def _selected_column_index_changed(self, index): 971 if not self._no_notify: 972 if index < 0: 973 self.set_extended_selection() 974 else: 975 self.set_extended_selection( 976 (None, self.model.get_column_name(index)) 977 ) 978 979 def _selected_columns_changed(self, names): 980 if not self._no_notify: 981 self.set_extended_selection([(None, name) for name in names]) 982 983 def _selected_column_indices_changed(self, indices): 984 if not self._no_notify: 985 gcn = self.model.get_column_name 986 self.set_extended_selection([(None, gcn(i)) for i in indices]) 987 988 def _selected_cell_changed(self, cell): 989 if not self._no_notify: 990 self.set_extended_selection([cell]) 991 992 def _selected_cell_index_changed(self, pair): 993 if not self._no_notify: 994 row, column = pair 995 if (row < 0) or (column < 0): 996 self.set_extended_selection() 997 else: 998 self.set_extended_selection( 999 (self.value[row], self.model.get_column_name(column)) 1000 ) 1001 1002 def _selected_cells_changed(self, cells): 1003 if not self._no_notify: 1004 self.set_extended_selection(cells) 1005 1006 def _selected_cell_indices_changed(self, pairs): 1007 if not self._no_notify: 1008 value = self.value 1009 gcn = self.model.get_column_name 1010 new_selection = [(value[row], gcn(col)) for row, col in pairs] 1011 self.set_extended_selection(new_selection) 1012 1013 def _update_toolbar(self, has_selection): 1014 """ Updates the toolbar after a selection change. 1015 """ 1016 toolbar = self.toolbar 1017 if toolbar is not None: 1018 no_filter = self.filter is None 1019 if has_selection: 1020 indices = self.selected_indices 1021 start = indices[0] 1022 n = len(self.model.get_filtered_items()) - 1 1023 delete = toolbar.delete 1024 if self.auto_add: 1025 n -= 1 1026 delete.enabled = start <= n 1027 else: 1028 delete.enabled = True 1029 1030 deletable = self.factory.deletable 1031 if delete.enabled and callable(deletable): 1032 delete.enabled = all( 1033 deletable(item) for item in self.selected_items 1034 ) 1035 1036 toolbar.add.enabled = toolbar.search.enabled = no_filter 1037 toolbar.move_up.enabled = no_filter and (start > 0) 1038 toolbar.move_down.enabled = no_filter and (indices[-1] < n) 1039 else: 1040 toolbar.add.enabled = toolbar.search.enabled = no_filter 1041 toolbar.delete.enabled = ( 1042 toolbar.move_up.enabled 1043 ) = toolbar.move_down.enabled = False 1044 1045 def _model_sorted(self): 1046 """ Handles the contents of the model being resorted. 1047 """ 1048 if self.toolbar is not None: 1049 self.toolbar.no_sort.enabled = True 1050 1051 values = self.selected_values 1052 if len(values) > 0: 1053 do_later(self.set_extended_selection, values) 1054 1055 def _filter_changed(self, old_filter, new_filter): 1056 """ Handles the current filter being changed. 1057 """ 1058 if new_filter is customize_filter: 1059 do_later(self._customize_filters, old_filter) 1060 1061 elif self.model is not None: 1062 if (new_filter is not None) and ( 1063 not isinstance(new_filter, TableFilter) 1064 ): 1065 new_filter = TableFilter(allowed=new_filter) 1066 self.model.filter = new_filter 1067 self.filter_modified() 1068 1069 def _refresh_filters(self, filters): 1070 factory = self.factory 1071 # hack: The following line forces the 'filters' to be changed... 1072 factory.filters = [] 1073 factory.filters = filters 1074 1075 def _customize_filters(self, filter): 1076 """ Allows the user to customize the current set of table filters. 1077 """ 1078 factory = self.factory 1079 filter_editor = TableFilterEditor(editor=self, filter=filter) 1080 enum_editor = EnumEditor(values=factory.filters[:], mode="list") 1081 ui = filter_editor.edit_traits( 1082 parent=self.control, 1083 view=View( 1084 [ 1085 [ 1086 Item( 1087 "filter<200>@", editor=enum_editor, resizable=True 1088 ), 1089 "|<>", 1090 ], 1091 ["edit:edit", "new", "apply", "delete:delete", "|<>"], 1092 "-", 1093 ], 1094 title="Customize Filters", 1095 kind="livemodal", 1096 height=0.25, 1097 buttons=["OK", "Cancel"], 1098 ), 1099 ) 1100 1101 if ui.result: 1102 self._refresh_filters(enum_editor.values) 1103 self.filter = filter_editor.filter 1104 else: 1105 self.filter = filter 1106 1107 def on_no_sort(self): 1108 """ Handles the user requesting that columns not be sorted. 1109 """ 1110 self.model.no_column_sort() 1111 self.toolbar.no_sort.enabled = False 1112 values = self.selected_values 1113 if len(values) > 0: 1114 self.set_extended_selection(values) 1115 1116 def on_move_up(self): 1117 """ Handles the user requesting to move the current item up one row. 1118 """ 1119 model = self.model 1120 objects = [] 1121 for index in self.selected_indices: 1122 objects.append(model.get_filtered_item(index)) 1123 index -= 1 1124 object = model.get_filtered_item(index) 1125 model.delete_filtered_item_at(index) 1126 model.insert_filtered_item_after(index, object) 1127 1128 if self.in_row_mode: 1129 self.set_selection(objects) 1130 else: 1131 self.set_extended_selection(self.selected_values) 1132 1133 def on_move_down(self): 1134 """ Handles the user requesting to move the current item down one row. 1135 """ 1136 model = self.model 1137 objects = [] 1138 indices = self.selected_indices[:] 1139 indices.reverse() 1140 for index in indices: 1141 object = model.get_filtered_item(index) 1142 objects.append(object) 1143 model.delete_filtered_item_at(index) 1144 model.insert_filtered_item_after(index, object) 1145 1146 if self.in_row_mode: 1147 self.set_selection(objects) 1148 else: 1149 self.set_extended_selection(self.selected_values) 1150 1151 def on_search(self): 1152 """ Handles the user requesting a table search. 1153 """ 1154 self.factory.search.edit_traits( 1155 parent=self.control, 1156 view="searchable_view", 1157 handler=TableSearchHandler(editor=self), 1158 ) 1159 1160 def on_add(self): 1161 """ Handles the user requesting to add a new row to the table. 1162 """ 1163 self.add_row() 1164 1165 def on_delete(self): 1166 """ Handles the user requesting to delete the currently selected items 1167 of the table. 1168 """ 1169 # Get the selected row indices: 1170 indices = self.selected_indices[:] 1171 values = self.selected_values[:] 1172 indices.reverse() 1173 1174 # Make sure that we don't delete any rows while an editor is open in it 1175 self.grid.stop_editing_indices(indices) 1176 1177 # Delete the selected rows: 1178 for i in indices: 1179 index, object = self.model.delete_filtered_item_at(i) 1180 self._add_undo( 1181 ListUndoItem( 1182 object=self.object, 1183 name=self.name, 1184 index=index, 1185 removed=[object], 1186 ) 1187 ) 1188 1189 # Compute the new selection and set it: 1190 items = self.model.get_filtered_items() 1191 n = len(items) - 1 1192 indices.reverse() 1193 for i in range(len(indices) - 1, -1, -1): 1194 if indices[i] > n: 1195 indices[i] = n 1196 if indices[i] < 0: 1197 del indices[i] 1198 del values[i] 1199 1200 n = len(indices) 1201 if n > 0: 1202 if self.in_row_mode: 1203 self.set_selection(list({items[i] for i in indices})) 1204 else: 1205 self.set_extended_selection( 1206 list({(items[indices[i]], values[i][1]) for i in range(n)}) 1207 ) 1208 else: 1209 self._update_toolbar(False) 1210 1211 def on_prefs(self): 1212 """ Handles the user requesting to set the user preference items for the 1213 table. 1214 """ 1215 columns = self.columns[:] 1216 columns.extend( 1217 [ 1218 c 1219 for c in (self.factory.columns + self.factory.other_columns) 1220 if c not in columns 1221 ] 1222 ) 1223 self.edit_traits( 1224 parent=self.control, 1225 view=View( 1226 [ 1227 Item( 1228 "columns", 1229 resizable=True, 1230 editor=SetEditor( 1231 values=columns, ordered=True, can_move_all=False 1232 ), 1233 ), 1234 "|<>", 1235 ], 1236 title="Select and Order Columns", 1237 width=0.3, 1238 height=0.3, 1239 resizable=True, 1240 buttons=["Undo", "OK", "Cancel"], 1241 kind="livemodal", 1242 ), 1243 ) 1244 1245 def prepare_menu(self, row, column): 1246 """ Prepares to have a context menu action called. 1247 """ 1248 object = self.model.get_filtered_item(row) 1249 selection = [x.obj for x in self.grid.get_selection()] 1250 if object not in selection: 1251 self.set_selection(object) 1252 selection = [object] 1253 self.set_menu_context(selection, object, column) 1254 1255 def setx(self, **keywords): 1256 """ Set one or more attributes without notifying the grid model. 1257 """ 1258 self._no_notify = True 1259 1260 for name, value in keywords.items(): 1261 setattr(self, name, value) 1262 1263 self._no_notify = False 1264 1265 # -- Private Methods: ----------------------------------------------------- 1266 1267 def _add_undo(self, undo_item, extend=False): 1268 history = self.ui.history 1269 if history is not None: 1270 history.add(undo_item, extend) 1271 1272 1273class TableFilterEditor(Handler): 1274 """ Editor that manages table filters. 1275 """ 1276 1277 #: TableEditor this editor is associated with 1278 editor = Instance(TableEditor) 1279 1280 #: Current filter 1281 filter = Instance(TableFilter, allow_none=True) 1282 1283 #: Edit the current filter 1284 edit = Button() 1285 1286 #: Create a new filter and edit it 1287 new = Button() 1288 1289 #: Apply the current filter to the editor's table 1290 apply = Button() 1291 1292 #: Delete the current filter 1293 delete = Button() 1294 1295 # ------------------------------------------------------------------------- 1296 # 'Handler' interface: 1297 # ------------------------------------------------------------------------- 1298 1299 def init(self, info): 1300 """ Initializes the controls of a user interface. 1301 """ 1302 # Save both the original filter object reference and its contents: 1303 if self.filter is None: 1304 self.filter = info.filter.factory.values[0] 1305 self._filter = self.filter 1306 self._filter_copy = self.filter.clone_traits() 1307 1308 def closed(self, info, is_ok): 1309 """ Handles a dialog-based user interface being closed by the user. 1310 """ 1311 if not is_ok: 1312 # Restore the contents of the original filter: 1313 self._filter.copy_traits(self._filter_copy) 1314 1315 # ------------------------------------------------------------------------- 1316 # Event handlers: 1317 # ------------------------------------------------------------------------- 1318 1319 def object_filter_changed(self, info): 1320 """ Handles a new filter being selected. 1321 """ 1322 filter = info.object.filter 1323 info.edit.enabled = not filter.template 1324 info.delete.enabled = (not filter.template) and ( 1325 len(info.filter.factory.values) > 1 1326 ) 1327 1328 def object_edit_changed(self, info): 1329 """ Handles the user clicking the **Edit** button. 1330 """ 1331 if info.initialized: 1332 items = self.editor.model.get_filtered_items() 1333 if len(items) > 0: 1334 item = items[0] 1335 else: 1336 item = None 1337 # `item` is now either the first item in the table, or None if 1338 # the table is empty. 1339 ui = self.filter.edit(item) 1340 if ui.result: 1341 self._refresh_filters(info) 1342 1343 def object_new_changed(self, info): 1344 """ Handles the user clicking the **New** button. 1345 """ 1346 if info.initialized: 1347 # Get list of available filters and find the current filter in it: 1348 factory = info.filter.factory 1349 filters = factory.values 1350 filter = self.filter 1351 index = filters.index(filter) + 1 1352 n = len(filters) 1353 while (index < n) and filters[index].template: 1354 index += 1 1355 1356 # Create a new filter based on the current filter: 1357 new_filter = filter.clone_traits() 1358 new_filter.template = False 1359 new_filter.name = new_filter._name = "New filter" 1360 1361 # Add it to the list of filters: 1362 filters.insert(index, new_filter) 1363 self._refresh_filters(info) 1364 1365 # Set up the new filter as the current filter and edit it: 1366 self.filter = new_filter 1367 do_later(self._delayed_edit, info) 1368 1369 def object_apply_changed(self, info): 1370 """ Handles the user clicking the **Apply** button. 1371 """ 1372 if info.initialized: 1373 self.init(info) 1374 self.editor._refresh_filters(info.filter.factory.values) 1375 self.editor.filter = self.filter 1376 1377 def object_delete_changed(self, info): 1378 """ Handles the user clicking the **Delete** button. 1379 """ 1380 # Get the list of available filters: 1381 filters = info.filter.factory.values 1382 1383 if info.initialized: 1384 # Delete the current filter: 1385 index = filters.index(self.filter) 1386 del filters[index] 1387 1388 # Select a new filter: 1389 if index >= len(filters): 1390 index -= 1 1391 self.filter = filters[index] 1392 self._refresh_filters(info) 1393 1394 # ------------------------------------------------------------------------- 1395 # Private methods: 1396 # ------------------------------------------------------------------------- 1397 1398 def _refresh_filters(self, info): 1399 """ Refresh the filter editor's list of filters. 1400 """ 1401 factory = info.filter.factory 1402 values, factory.values = factory.values, [] 1403 factory.values = values 1404 1405 def _delayed_edit(self, info): 1406 """ Edits the current filter, and deletes it if the user cancels the 1407 edit. 1408 """ 1409 ui = self.filter.edit(self.editor.model.get_filtered_item(0)) 1410 if not ui.result: 1411 self.object_delete_changed(info) 1412 else: 1413 self._refresh_filters(info) 1414 1415 # Allow deletion as long as there is more than 1 filter: 1416 if (not self.filter.template) and len(info.filter.factory.values) > 1: 1417 info.delete.enabled = True 1418 1419 1420class TableEditorToolbar(HasPrivateTraits): 1421 """ Toolbar displayed in table editors. 1422 """ 1423 1424 # ------------------------------------------------------------------------- 1425 # Trait definitions: 1426 # ------------------------------------------------------------------------- 1427 1428 #: Do not sort columns: 1429 no_sort = Instance( 1430 Action, 1431 { 1432 "name": "No Sorting", 1433 "tooltip": "Do not sort columns", 1434 "action": "on_no_sort", 1435 "enabled": False, 1436 "image": ImageResource("table_no_sort.png"), 1437 }, 1438 ) 1439 1440 #: Move current object up one row: 1441 move_up = Instance( 1442 Action, 1443 { 1444 "name": "Move Up", 1445 "tooltip": "Move current item up one row", 1446 "action": "on_move_up", 1447 "enabled": False, 1448 "image": ImageResource("table_move_up.png"), 1449 }, 1450 ) 1451 1452 #: Move current object down one row: 1453 move_down = Instance( 1454 Action, 1455 { 1456 "name": "Move Down", 1457 "tooltip": "Move current item down one row", 1458 "action": "on_move_down", 1459 "enabled": False, 1460 "image": ImageResource("table_move_down.png"), 1461 }, 1462 ) 1463 1464 #: Search the table: 1465 search = Instance( 1466 Action, 1467 { 1468 "name": "Search", 1469 "tooltip": "Search table", 1470 "action": "on_search", 1471 "image": ImageResource("table_search.png"), 1472 }, 1473 ) 1474 1475 #: Add a row: 1476 add = Instance( 1477 Action, 1478 { 1479 "name": "Add", 1480 "tooltip": "Insert new item", 1481 "action": "on_add", 1482 "image": ImageResource("table_add.png"), 1483 }, 1484 ) 1485 1486 #: Delete selected row: 1487 delete = Instance( 1488 Action, 1489 { 1490 "name": "Delete", 1491 "tooltip": "Delete current item", 1492 "action": "on_delete", 1493 "enabled": False, 1494 "image": ImageResource("table_delete.png"), 1495 }, 1496 ) 1497 1498 #: Edit the user preferences: 1499 prefs = Instance( 1500 Action, 1501 { 1502 "name": "Preferences", 1503 "tooltip": "Set user preferences for table", 1504 "action": "on_prefs", 1505 "image": ImageResource("table_prefs.png"), 1506 }, 1507 ) 1508 1509 #: The table editor that this is the toolbar for: 1510 editor = Instance(TableEditor) 1511 1512 #: The toolbar control: 1513 control = Any() 1514 1515 def __init__(self, parent=None, **traits): 1516 super(TableEditorToolbar, self).__init__(**traits) 1517 editor = self.editor 1518 factory = editor.factory 1519 actions = [] 1520 1521 if factory.sortable and (not factory.sort_model): 1522 actions.append(self.no_sort) 1523 1524 if (not editor.in_column_mode) and factory.reorderable: 1525 actions.append(self.move_up) 1526 actions.append(self.move_down) 1527 1528 if editor.in_row_mode and (factory.search is not None): 1529 actions.append(self.search) 1530 1531 if factory.editable: 1532 if (factory.row_factory is not None) and (not factory.auto_add): 1533 actions.append(self.add) 1534 1535 if (factory.deletable) and (not editor.in_column_mode): 1536 actions.append(self.delete) 1537 1538 if factory.configurable: 1539 actions.append(self.prefs) 1540 1541 if len(actions) > 0: 1542 toolbar = ToolBar( 1543 image_size=(16, 16), 1544 show_tool_names=False, 1545 show_divider=False, 1546 *actions 1547 ) 1548 self.control = toolbar.create_tool_bar(parent, self) 1549 self.control.SetBackgroundColour(parent.GetBackgroundColour()) 1550 1551 # fixme: Why do we have to explictly set the size of the toolbar? 1552 # Is there some method that needs to be called to do the 1553 # layout? 1554 self.control.SetSize(wx.Size(23 * len(actions), 16)) 1555 1556 # ------------------------------------------------------------------------- 1557 # Pyface/Traits menu/toolbar controller interface: 1558 # ------------------------------------------------------------------------- 1559 1560 def add_to_menu(self, menu_item): 1561 """ Adds a menu item to the menu bar being constructed. 1562 """ 1563 pass 1564 1565 def add_to_toolbar(self, toolbar_item): 1566 """ Adds a toolbar item to the too bar being constructed. 1567 """ 1568 pass 1569 1570 def can_add_to_menu(self, action): 1571 """ Returns whether the action should be defined in the user interface. 1572 """ 1573 return True 1574 1575 def can_add_to_toolbar(self, action): 1576 """ Returns whether the toolbar action should be defined in the user 1577 interface. 1578 """ 1579 return True 1580 1581 def perform(self, action, action_event=None): 1582 """ Performs the action described by a specified Action object. 1583 """ 1584 getattr(self.editor, action.action)() 1585 1586 1587class TableSearchHandler(Handler): 1588 """ Handler for saerching a table. 1589 """ 1590 1591 # ------------------------------------------------------------------------- 1592 # Trait definitions: 1593 # ------------------------------------------------------------------------- 1594 1595 #: The editor that this handler is associated with 1596 editor = Instance(TableEditor) 1597 1598 #: Find next matching item 1599 find_next = Button("Find Next") 1600 1601 #: Find previous matching item 1602 find_previous = Button("Find Previous") 1603 1604 #: Select all matching items 1605 select = Button() 1606 1607 #: The user is finished searching 1608 OK = Button("Close") 1609 1610 #: Search status message: 1611 status = Str() 1612 1613 def handler_find_next_changed(self, info): 1614 """ Handles the user clicking the **Find** button. 1615 """ 1616 if info.initialized: 1617 editor = self.editor 1618 items = editor.model.get_filtered_items() 1619 1620 for i in range(editor.selected_row_index + 1, len(items)): 1621 if info.object.filter(items[i]): 1622 self.status = "Item %d matches" % (i + 1) 1623 editor.set_selection(items[i]) 1624 editor.selected_row_index = i 1625 break 1626 else: 1627 self.status = "No more matches found" 1628 1629 def handler_find_previous_changed(self, info): 1630 """ Handles the user clicking the **Find previous** button. 1631 """ 1632 if info.initialized: 1633 editor = self.editor 1634 items = editor.model.get_filtered_items() 1635 1636 for i in range(editor.selected_row_index - 1, -1, -1): 1637 if info.object.filter(items[i]): 1638 self.status = "Item %d matches" % (i + 1) 1639 editor.set_selection(items[i]) 1640 editor.selected_row_index = i 1641 break 1642 else: 1643 self.status = "No more matches found" 1644 1645 def handler_select_changed(self, info): 1646 """ Handles the user clicking the **Select** button. 1647 """ 1648 if info.initialized: 1649 editor = self.editor 1650 filter = info.object.filter 1651 items = [ 1652 item 1653 for item in editor.model.get_filtered_items() 1654 if filter(item) 1655 ] 1656 editor.set_selection(items) 1657 1658 if len(items) == 1: 1659 self.status = "1 item selected" 1660 else: 1661 self.status = "%d items selected" % len(items) 1662 1663 def handler_OK_changed(self, info): 1664 """ Handles the user clicking the OK button. 1665 """ 1666 if info.initialized: 1667 self.close(info, True) 1668 1669 1670# Define the SimpleEditor class. 1671SimpleEditor = TableEditor 1672 1673 1674# Define the ReadonlyEditor class. 1675ReadonlyEditor = TableEditor 1676