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