Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
traitsui / wx / table_editor.py
Size: Mime:
# (C) Copyright 2004-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

""" Defines the table editor for the wxPython user interface toolkit.
"""


from operator import itemgetter

import wx

from pyface.dock.api import (
    DockWindow,
    DockSizer,
    DockSection,
    DockRegion,
    DockControl,
)
from pyface.image_resource import ImageResource
from pyface.timer.api import do_later
from pyface.ui.wx.grid.api import Grid
from traits.api import (
    Int,
    List,
    Instance,
    Str,
    Any,
    Button,
    Tuple,
    HasPrivateTraits,
    Bool,
    Event,
    Property,
)

from traitsui.api import (
    View,
    Item,
    UI,
    InstanceEditor,
    EnumEditor,
    Handler,
    SetEditor,
    ListUndoItem,
)
from traitsui.editors.table_editor import BaseTableEditor, customize_filter
from traitsui.menu import Action, ToolBar
from traitsui.table_column import TableColumn, ObjectColumn
from traitsui.table_filter import TableFilter
from traitsui.ui_traits import SequenceTypes

from .constants import (
    TableCellBackgroundColor,
    TableCellColor,
    TableLabelBackgroundColor,
    TableLabelColor,
    TableReadOnlyBackgroundColor,
    TableSelectionBackgroundColor,
    TableSelectionTextColor,
)
from .editor import Editor
from .table_model import TableModel, TraitGridSelection
from .helper import TraitsUIPanel


#: Mapping from TableEditor selection modes to Grid selection modes:
GridModes = {
    "row": "rows",
    "rows": "rows",
    "column": "cols",
    "columns": "cols",
    "cell": "cell",
    "cells": "cell",
}


def _get_color(color, default_color):
    """Return the color if it is not None, otherwise use default."""
    if color is not None:
        return color
    return default_color


class TableEditor(Editor, BaseTableEditor):
    """Editor that presents data in a table. Optionally, tables can have
    a set of filters that reduce the set of data displayed, according to
    their criteria.
    """

    # -------------------------------------------------------------------------
    #  Trait definitions:
    # -------------------------------------------------------------------------

    #: The set of columns currently defined on the editor:
    columns = List(Instance(TableColumn))

    #: Index of currently edited (i.e., selected) table item(s):
    selected_row_index = Int(-1)
    selected_row_indices = List(Int)
    selected_indices = Property()

    selected_column_index = Int(-1)
    selected_column_indices = List(Int)

    selected_cell_index = Tuple(Int, Int)
    selected_cell_indices = List(Tuple(Int, Int))

    #: The currently selected table item(s):
    selected_row = Any()
    selected_rows = List()
    selected_items = Property()

    selected_column = Any()
    selected_columns = List()

    selected_cell = Tuple(Any, Str)
    selected_cells = List(Tuple(Any, Str))

    selected_values = Property()

    #: The indices of the table items currently passing the table filter:
    filtered_indices = List(Int)

    #: The event fired when a cell is clicked on:
    click = Event()

    #: The event fired when a cell is double-clicked on:
    dclick = Event()

    #: Is the editor in row mode (i.e. not column or cell mode)?
    in_row_mode = Property()

    #: Is the editor in column mode (i.e. not row or cell mode)?
    in_column_mode = Property()

    #: Current filter object (should be a TableFilter or callable or None):
    filter = Any()

    #: The grid widget associated with the editor:
    grid = Instance(Grid)

    #: The table model associated with the editor:
    model = Instance(TableModel)

    #: TableEditorToolbar associated with the editor:
    toolbar = Any()

    #: The Traits UI associated with the table editor toolbar:
    toolbar_ui = Instance(UI)

    #: Is the table editor scrollable? This value overrides the default.
    scrollable = True

    #: Is 'auto_add' mode in effect? (I.e., new rows are automatically added to
    #: the end of the table when the user modifies current last row.)
    auto_add = Bool(False)

    def init(self, parent):
        """Finishes initializing the editor by creating the underlying toolkit
        widget.
        """

        factory = self.factory
        self.filter = factory.filter
        self.auto_add = factory.auto_add and (factory.row_factory is not None)

        columns = factory.columns[:]
        if (len(columns) == 0) and (len(self.value) > 0):
            columns = [
                ObjectColumn(name=name)
                for name in self.value[0].editable_traits()
            ]
        self.columns = columns

        self.model = model = TableModel(editor=self, reverse=factory.reverse)
        model.on_trait_change(self._model_sorted, "sorted", dispatch="ui")
        mode = factory.selection_mode
        row_mode = mode in ("row", "rows")
        selected = None
        items = model.get_filtered_items()
        if factory.editable and (len(items) > 0):
            selected = items[0]
        if (factory.edit_view == " ") or (not row_mode):
            self.control = panel = TraitsUIPanel(parent, -1)
            sizer = wx.BoxSizer(wx.VERTICAL)
            self._create_toolbar(panel, sizer)

            # Create the table (i.e. grid) control:
            hsizer = wx.BoxSizer(wx.HORIZONTAL)
            self._create_grid(panel, hsizer)
            sizer.Add(hsizer, 1, wx.EXPAND)
        else:
            item = self.item
            name = item.get_label(self.ui)
            theme = factory.dock_theme or item.container.dock_theme
            self.control = dw = DockWindow(parent, theme=theme).control
            panel = TraitsUIPanel(dw, -1, size=(300, 300))
            sizer = wx.BoxSizer(wx.VERTICAL)
            dc = DockControl(
                name=name + " Table", id="table", control=panel, style="fixed"
            )
            contents = [DockRegion(contents=[dc])]
            self._create_toolbar(panel, sizer)
            selected = None
            items = model.get_filtered_items()
            if factory.editable and (len(items) > 0):
                selected = items[0]

            # Create the table (i.e. grid) control:
            hsizer = wx.BoxSizer(wx.HORIZONTAL)
            self._create_grid(panel, hsizer)
            sizer.Add(hsizer, 1, wx.EXPAND)

            # Assign the initial object here, so a valid editor will be built
            # when the 'edit_traits' call is made:
            self.selected_row = selected
            self._ui = ui = self.edit_traits(
                parent=dw,
                kind="subpanel",
                view=View(
                    [
                        Item(
                            "selected_row",
                            style="custom",
                            editor=InstanceEditor(
                                view=factory.edit_view, kind="subpanel"
                            ),
                            resizable=True,
                            width=factory.edit_view_width,
                            height=factory.edit_view_height,
                        ),
                        "|<>",
                    ],
                    resizable=True,
                    handler=factory.edit_view_handler,
                ),
            )

            # Set the parent UI of the new UI to our own UI:
            ui.parent = self.ui

            # Reset the object so that the sub-sub-view will pick up the
            # correct history also:
            self.selected_row = None
            self.selected_row = selected

            dc.style = item.dock
            contents.append(
                DockRegion(
                    contents=[
                        DockControl(
                            name=name + " Editor",
                            id="editor",
                            control=ui.control,
                            style=item.dock,
                        )
                    ]
                )
            )

            # Finish setting up the DockWindow:
            dw.SetSizer(
                DockSizer(
                    contents=DockSection(
                        contents=contents,
                        is_row=(factory.orientation == "horizontal"),
                    )
                )
            )

        # Set up the required externally synchronized traits (if any):
        sv = self.sync_value
        is_list = mode[-1] == "s"
        sv(factory.click, "click", "to")
        sv(factory.dclick, "dclick", "to")
        sv(factory.filter_name, "filter", "from")
        sv(factory.columns_name, "columns", is_list=True)
        sv(factory.filtered_indices, "filtered_indices", "to")
        sv(factory.selected, "selected_%s" % mode, is_list=is_list)
        if is_list:
            sv(
                factory.selected_indices,
                "selected_%s_indices" % mode[:-1],
                is_list=True,
            )
        else:
            sv(factory.selected_indices, "selected_%s_index" % mode)

        # Listen for the selection changing on the grid:
        self.grid.on_trait_change(
            getattr(self, "_selection_%s_updated" % mode),
            "selection_changed",
            dispatch="ui",
        )

        # Set the min height of the grid panel to 0, this will provide
        # a scrollbar if the window is resized such that only the first row
        # is visible
        panel.SetMinSize((-1, 0))

        # Finish the panel layout setup:
        panel.SetSizer(sizer)

    def _create_grid(self, parent, sizer):
        """Creates the associated grid control used to implement the table."""
        factory = self.factory
        selection_mode = GridModes[factory.selection_mode]
        if factory.selection_bg_color is None:
            selection_mode = ""

        cell_color = _get_color(factory.cell_color, TableCellColor)
        cell_bg_color = _get_color(
            factory.cell_bg_color, TableCellBackgroundColor
        )
        cell_read_only_bg_color = _get_color(
            factory.cell_read_only_bg_color, TableReadOnlyBackgroundColor
        )
        label_bg_color = _get_color(
            factory.label_bg_color, TableLabelBackgroundColor
        )
        label_color = _get_color(factory.label_color, TableLabelColor)
        selection_text_color = _get_color(
            factory.selection_color, TableSelectionTextColor
        )
        selection_bg_color = _get_color(
            factory.selection_bg_color, TableSelectionBackgroundColor
        )

        self.grid = grid = Grid(
            parent,
            model=self.model,
            enable_lines=factory.show_lines,
            grid_line_color=factory.line_color,
            show_row_headers=factory.show_row_labels,
            show_column_headers=factory.show_column_labels,
            default_cell_font=factory.cell_font,
            default_cell_text_color=cell_color,
            default_cell_bg_color=cell_bg_color,
            default_cell_read_only_color=cell_read_only_bg_color,
            default_label_font=factory.label_font,
            default_label_text_color=label_color,
            default_label_bg_color=label_bg_color,
            selection_bg_color=selection_bg_color,
            selection_text_color=selection_text_color,
            autosize=factory.auto_size,
            read_only=not factory.editable,
            edit_on_first_click=factory.edit_on_first_click,
            selection_mode=selection_mode,
            allow_column_sort=factory.sortable,
            allow_row_sort=False,
            column_label_height=factory.column_label_height,
            row_label_width=factory.row_label_width,
        )
        _grid = grid._grid
        _grid.SetScrollLineY(factory.scroll_dy)

        # Set the default size for each table row:
        height = factory.row_height
        if height <= 0:
            height = _grid.GetTextExtent("My")[1] + 9
        _grid.SetDefaultRowSize(height)

        # Allow the table to be resizable if the user did not explicitly
        # specify a number of rows to display:
        self.scrollable = factory.rows == 0

        # Calculate a reasonable default size for the table:
        if len(self.model.get_filtered_items()) > 0:
            height = _grid.GetRowSize(0)

        max_rows = factory.rows or 15

        min_width = max(150, 80 * len(self.columns))

        if factory.show_column_labels:
            min_height = _grid.GetColLabelSize() + (max_rows * height)
        else:
            min_height = max_rows * height

        _grid.SetMinSize(wx.Size(min_width, min_height))

        # On Linux, there is what appears to be a bug in wx in which the
        # vertical scrollbar will not be sized properly if the TableEditor is
        # sized to be shorter than the minimum height specified above. Since
        # this height is only set to ensure that the TableEditor is sized
        # correctly during the initial UI layout, we un-set it after this takes
        # place (addresses ticket 1810)
        def clear_minimum_height(info):
            min_size = _grid.GetMinSize()
            min_size.height = 0
            _grid.SetMinSize(min_size)

        self.ui.add_defined(clear_minimum_height)

        sizer.Add(grid.control, 1, wx.EXPAND)

        return grid.control

    def _create_toolbar(self, parent, sizer):
        """Creates the table editing toolbar."""

        factory = self.factory
        if not factory.show_toolbar:
            return

        toolbar = TableEditorToolbar(parent=parent, editor=self)
        if (toolbar.control is not None) or (len(factory.filters) > 0):
            tb_sizer = wx.BoxSizer(wx.HORIZONTAL)

            if len(factory.filters) > 0:
                view = View(
                    [
                        Item(
                            "filter<250>{View}", editor=factory._filter_editor
                        ),
                        "_",
                        Item(
                            "filter_summary<100>{Results}~",
                            object="model",
                            resizable=False,
                        ),
                        "_",
                        "-",
                    ],
                    resizable=True,
                )
                self.toolbar_ui = ui = view.ui(
                    context={"object": self, "model": self.model},
                    parent=parent,
                    kind="subpanel",
                ).trait_set(parent=self.ui)
                tb_sizer.Add(ui.control, 0)

            if toolbar.control is not None:
                self.toolbar = toolbar
                # add padding so the toolbar is right aligned
                tb_sizer.Add((1, 1), 1, wx.EXPAND)
                tb_sizer.Add(toolbar.control, 0)

            sizer.Add(tb_sizer, 0, wx.EXPAND)

    def dispose(self):
        """Disposes of the contents of an editor."""
        if self.toolbar_ui is not None:
            self.toolbar_ui.dispose()

        if self._ui is not None:
            self._ui.dispose()

        self.grid.on_trait_change(
            getattr(
                self, "_selection_%s_updated" % self.factory.selection_mode
            ),
            "selection_changed",
            remove=True,
        )

        self.model.on_trait_change(self._model_sorted, "sorted", remove=True)

        self.grid.dispose()
        self.model.dispose()

        # Break any links needed to allow garbage collection:
        self.grid = self.model = self.toolbar = None

        super().dispose()

    def update_editor(self):
        """Updates the editor when the object trait changes externally to the
        editor.
        """
        # fixme: Do we need to override this method?
        pass

    def refresh(self):
        """Refreshes the editor control."""
        self.grid._grid.Refresh()

    def set_selection(self, objects=[], notify=True):
        """Sets the current selection to a set of specified objects."""
        if not isinstance(objects, SequenceTypes):
            objects = [objects]

        self.grid.set_selection(
            [TraitGridSelection(obj=object) for object in objects],
            notify=notify,
        )

    def set_extended_selection(self, *pairs):
        """Sets the current selection to a set of specified object/column
        pairs.
        """
        if (len(pairs) == 1) and isinstance(pairs[0], list):
            pairs = pairs[0]

        grid_selections = [
            TraitGridSelection(obj=object, name=name) for object, name in pairs
        ]

        self.grid.set_selection(grid_selections)

    def create_new_row(self):
        """Creates a new row object using the provided factory."""
        factory = self.factory
        kw = factory.row_factory_kw.copy()
        if "__table_editor__" in kw:
            kw["__table_editor__"] = self

        return self.ui.evaluate(
            factory.row_factory, *factory.row_factory_args, **kw
        )

    def add_row(self, object=None, index=None):
        """Adds a specified object as a new row after the specified index."""
        filtered_items = self.model.get_filtered_items

        if index is None:
            indices = self.selected_indices
            if len(indices) == 0:
                indices = [len(filtered_items()) - 1]
            indices.reverse()
        else:
            indices = [index]

        if object is None:
            objects = []
            for index in indices:
                object = self.create_new_row()
                if object is None:
                    if self.in_row_mode:
                        self.set_selection()
                    return

                objects.append(object)
        else:
            objects = [object]

        items = []
        insert_item_after = self.model.insert_filtered_item_after
        in_row_mode = self.in_row_mode
        for i, index in enumerate(indices):
            object = objects[i]
            index, extend = insert_item_after(index, object)

            if in_row_mode and (object in filtered_items()):
                items.append(object)

            self._add_undo(
                ListUndoItem(
                    object=self.object,
                    name=self.name,
                    index=index,
                    added=[object],
                ),
                extend,
            )

        if in_row_mode:
            self.set_selection(items)

    def move_column(self, from_column, to_column):
        """Moves the specified **from_column** from its current position to
        just preceding the specified **to_column**.
        """
        columns = self.columns
        frm = columns.index(from_column)
        if to_column is None:
            to = len(columns)
        else:
            to = columns.index(to_column)
        del columns[frm]
        columns.insert(to - (frm < to), from_column)

        return True

    # -- Property Implementations ---------------------------------------------

    def _get_selected_indices(self):
        sm = self.factory.selection_mode
        if sm == "rows":
            return self.selected_row_indices

        elif sm == "row":
            index = self.selected_row_index
            if index >= 0:
                return [index]

        elif sm == "cells":
            return list({row_col[0] for row_col in self.selected_cell_indices})

        elif sm == "cell":
            index = self.selected_cell_index[0]
            if index >= 0:
                return [index]

        return []

    def _get_selected_items(self):
        sm = self.factory.selection_mode
        if sm == "rows":
            return self.selected_rows

        elif sm == "row":
            item = self.selected_row
            if item is not None:
                return [item]

        elif sm == "cells":
            return list({item_name[0] for item_name in self.selected_cells})

        elif sm == "cell":
            item = self.selected_cell[0]
            if item is not None:
                return [item]

        return []

    def _get_selected_values(self):
        if self.in_row_mode:
            return [(item, "") for item in self.selected_items]

        if self.in_column_mode:
            if self.factory.selection_mode == "columns":
                return [(None, column) for column in self.selected_columns]

            column = self.selected_column
            if column != "":
                return [(None, column)]

            return []

        if self.factory.selection_mode == "cells":
            return self.selected_cells

        item = self.selected_cell
        if item[0] is not None:
            return [item]

        return []

    def _get_in_row_mode(self):
        return self.factory.selection_mode in ("row", "rows")

    def _get_in_column_mode(self):
        return self.factory.selection_mode in ("column", "columns")

    # -- UI preference save/restore interface ---------------------------------

    def restore_prefs(self, prefs):
        """Restores any saved user preference information associated with the
        editor.
        """
        factory = self.factory
        try:
            filters = prefs.get("filters", None)
            if filters is not None:
                factory.filters = [
                    f for f in factory.filters if f.template
                ] + [f for f in filters if not f.template]

            columns = prefs.get("columns")
            if columns is not None:
                new_columns = []
                all_columns = self.columns + factory.other_columns
                for column in columns:
                    for column2 in all_columns:
                        if column == column2.get_label():
                            new_columns.append(column2)
                            break
                self.columns = new_columns

                # Restore the column sizes if possible:
                if not factory.auto_size:
                    widths = prefs.get("widths")
                    if widths is not None:
                        # fixme: Talk to Jason about a better way to do this:
                        self.grid._user_col_size = True

                        set_col_size = self.grid._grid.SetColSize
                        for i, width in enumerate(widths):
                            if width >= 0:
                                set_col_size(i, width)

            structure = prefs.get("structure")
            if (structure is not None) and (factory.edit_view != " "):
                self.control.GetSizer().SetStructure(self.control, structure)
        except Exception:
            pass

    def save_prefs(self):
        """Returns any user preference information associated with the editor."""
        get_col_size = self.grid._grid.GetColSize
        result = {
            "filters": [f for f in self.factory.filters if not f.template],
            "columns": [c.get_label() for c in self.columns],
            "widths": [get_col_size(i) for i in range(len(self.columns))],
        }

        if self.factory.edit_view != " ":
            result["structure"] = self.control.GetSizer().GetStructure()

        return result

    # -- Public Methods -------------------------------------------------------

    def filter_modified(self):
        """Handles updating the selection when some aspect of the current
        filter has changed.
        """
        values = self.selected_values
        if len(values) > 0:
            if self.in_column_mode:
                self.set_extended_selection(values)
            else:
                items = self.model.get_filtered_items()
                self.set_extended_selection(
                    [item for item in values if item[0] in items]
                )

    # -- Event Handlers -------------------------------------------------------

    def _selection_row_updated(self, event):
        """Handles the user selecting items (rows, columns, cells) in the
        table.
        """
        gfi = self.model.get_filtered_item
        rio = self.model.raw_index_of
        tl = self.grid._grid.GetSelectionBlockTopLeft()
        br = iter(self.grid._grid.GetSelectionBlockBottomRight())
        rows = len(self.model.get_filtered_items())
        if self.auto_add:
            rows -= 1

        # Get the row items and indices in the selection:
        values = []
        for row0, col0 in tl:
            row1, col1 = next(br)
            for row in range(row0, row1 + 1):
                if row < rows:
                    values.append((rio(row), gfi(row)))

        if len(values) > 0:
            # Sort by increasing row index:
            values.sort(key=itemgetter(0))
            index, row = values[0]
        else:
            index, row = -1, None

        # Save the new selection information:
        self.trait_set(selected_row_index=index, trait_change_notify=False)
        self.setx(selected_row=row)

        # Update the toolbar status:
        self._update_toolbar(row is not None)

        # Invoke the user 'on_select' handler:
        self.ui.evaluate(self.factory.on_select, row)

    def _selection_rows_updated(self, event):
        """Handles multiple row selection changes."""
        gfi = self.model.get_filtered_item
        rio = self.model.raw_index_of
        tl = self.grid._grid.GetSelectionBlockTopLeft()
        br = iter(self.grid._grid.GetSelectionBlockBottomRight())
        rows = len(self.model.get_filtered_items())
        if self.auto_add:
            rows -= 1

        # Get the row items and indices in the selection:
        values = []
        for row0, col0 in tl:
            row1, col1 = next(br)
            for row in range(row0, row1 + 1):
                if row < rows:
                    values.append((rio(row), gfi(row)))

        # Sort by increasing row index:
        values.sort(key=itemgetter(0))

        # Save the new selection information:
        self.trait_set(
            selected_row_indices=[v[0] for v in values],
            trait_change_notify=False,
        )
        rows = [v[1] for v in values]
        self.setx(selected_rows=rows)

        # Update the toolbar status:
        self._update_toolbar(len(values) > 0)

        # Invoke the user 'on_select' handler:
        self.ui.evaluate(self.factory.on_select, rows)

    def _selection_column_updated(self, event):
        """Handles single column selection changes."""
        cols = self.columns
        tl = self.grid._grid.GetSelectionBlockTopLeft()
        br = iter(self.grid._grid.GetSelectionBlockBottomRight())

        # Get the column items and indices in the selection:
        values = []
        for row0, col0 in tl:
            row1, col1 = next(br)
            for col in range(col0, col1 + 1):
                values.append((col, cols[col].name))

        if len(values) > 0:
            # Sort by increasing column index:
            values.sort(key=itemgetter(0))
            index, column = values[0]
        else:
            index, column = -1, ""

        # Save the new selection information:
        self.trait_set(selected_column_index=index, trait_change_notify=False)
        self.setx(selected_column=column)

        # Invoke the user 'on_select' handler:
        self.ui.evaluate(self.factory.on_select, column)

    def _selection_columns_updated(self, event):
        """Handles multiple column selection changes."""
        cols = self.columns
        tl = self.grid._grid.GetSelectionBlockTopLeft()
        br = iter(self.grid._grid.GetSelectionBlockBottomRight())

        # Get the column items and indices in the selection:
        values = []
        for row0, col0 in tl:
            row1, col1 = next(br)
            for col in range(col0, col1 + 1):
                values.append((col, cols[col].name))

        # Sort by increasing row index:
        values.sort(key=itemgetter(0))

        # Save the new selection information:
        self.trait_set(
            selected_column_indices=[v[0] for v in values],
            trait_change_notify=False,
        )
        columns = [v[1] for v in values]
        self.setx(selected_columns=columns)

        # Invoke the user 'on_select' handler:
        self.ui.evaluate(self.factory.on_select, columns)

    def _selection_cell_updated(self, event):
        """Handles single cell selection changes."""
        tl = self.grid._grid.GetSelectionBlockTopLeft()
        if len(tl) == 0:
            return

        gfi = self.model.get_filtered_item
        rio = self.model.raw_index_of
        cols = self.columns
        br = iter(self.grid._grid.GetSelectionBlockBottomRight())

        # Get the column items and indices in the selection:
        values = []
        for row0, col0 in tl:
            row1, col1 = next(br)
            for row in range(row0, row1 + 1):
                item = gfi(row)
                for col in range(col0, col1 + 1):
                    values.append(((rio(row), col), (item, cols[col].name)))

        if len(values) > 0:
            # Sort by increasing row, column index:
            values.sort(key=itemgetter(0))
            index, cell = values[0]
        else:
            index, cell = (-1, -1), (None, "")

        # Save the new selection information:
        self.trait_set(selected_cell_index=index, trait_change_notify=False)
        self.setx(selected_cell=cell)

        # Update the toolbar status:
        self._update_toolbar(len(values) > 0)

        # Invoke the user 'on_select' handler:
        self.ui.evaluate(self.factory.on_select, cell)

    def _selection_cells_updated(self, event):
        """Handles multiple cell selection changes."""
        gfi = self.model.get_filtered_item
        rio = self.model.raw_index_of
        cols = self.columns
        tl = self.grid._grid.GetSelectionBlockTopLeft()
        br = iter(self.grid._grid.GetSelectionBlockBottomRight())

        # Get the column items and indices in the selection:
        values = []
        for row0, col0 in tl:
            row1, col1 = next(br)
            for row in range(row0, row1 + 1):
                item = gfi(row)
                for col in range(col0, col1 + 1):
                    values.append(((rio(row), col), (item, cols[col].name)))

        # Sort by increasing row, column index:
        values.sort(key=itemgetter(0))

        # Save the new selection information:
        self.setx(selected_cell_indices=[v[0] for v in values])
        cells = [v[1] for v in values]
        self.setx(selected_cells=cells)

        # Update the toolbar status:
        self._update_toolbar(len(cells) > 0)

        # Invoke the user 'on_select' handler:
        self.ui.evaluate(self.factory.on_select, cells)

    def _selected_row_changed(self, item):
        if not self._no_notify:
            if item is None:
                self.set_selection(notify=False)
            else:
                self.set_selection(item, notify=False)

    def _selected_row_index_changed(self, row):
        if not self._no_notify:
            if row < 0:
                self.set_selection(notify=False)
            else:
                self.set_selection(self.value[row], notify=False)

    def _selected_rows_changed(self, items):
        if not self._no_notify:
            self.set_selection(items, notify=False)

    def _selected_row_indices_changed(self, indices):
        if not self._no_notify:
            value = self.value
            self.set_selection([value[i] for i in indices], notify=False)

    def _selected_column_changed(self, name):
        if not self._no_notify:
            self.set_extended_selection((None, name))

    def _selected_column_index_changed(self, index):
        if not self._no_notify:
            if index < 0:
                self.set_extended_selection()
            else:
                self.set_extended_selection(
                    (None, self.model.get_column_name(index))
                )

    def _selected_columns_changed(self, names):
        if not self._no_notify:
            self.set_extended_selection([(None, name) for name in names])

    def _selected_column_indices_changed(self, indices):
        if not self._no_notify:
            gcn = self.model.get_column_name
            self.set_extended_selection([(None, gcn(i)) for i in indices])

    def _selected_cell_changed(self, cell):
        if not self._no_notify:
            self.set_extended_selection([cell])

    def _selected_cell_index_changed(self, pair):
        if not self._no_notify:
            row, column = pair
            if (row < 0) or (column < 0):
                self.set_extended_selection()
            else:
                self.set_extended_selection(
                    (self.value[row], self.model.get_column_name(column))
                )

    def _selected_cells_changed(self, cells):
        if not self._no_notify:
            self.set_extended_selection(cells)

    def _selected_cell_indices_changed(self, pairs):
        if not self._no_notify:
            value = self.value
            gcn = self.model.get_column_name
            new_selection = [(value[row], gcn(col)) for row, col in pairs]
            self.set_extended_selection(new_selection)

    def _update_toolbar(self, has_selection):
        """Updates the toolbar after a selection change."""
        toolbar = self.toolbar
        if toolbar is not None:
            no_filter = self.filter is None
            if has_selection:
                indices = self.selected_indices
                start = indices[0]
                n = len(self.model.get_filtered_items()) - 1
                delete = toolbar.delete
                if self.auto_add:
                    n -= 1
                    delete.enabled = start <= n
                else:
                    delete.enabled = True

                deletable = self.factory.deletable
                if delete.enabled and callable(deletable):
                    delete.enabled = all(
                        deletable(item) for item in self.selected_items
                    )

                toolbar.add.enabled = toolbar.search.enabled = no_filter
                toolbar.move_up.enabled = no_filter and (start > 0)
                toolbar.move_down.enabled = no_filter and (indices[-1] < n)
            else:
                toolbar.add.enabled = toolbar.search.enabled = no_filter
                toolbar.delete.enabled = (
                    toolbar.move_up.enabled
                ) = toolbar.move_down.enabled = False

    def _model_sorted(self):
        """Handles the contents of the model being resorted."""
        if self.toolbar is not None:
            self.toolbar.no_sort.enabled = True

        values = self.selected_values
        if len(values) > 0:
            do_later(self.set_extended_selection, values)

    def _filter_changed(self, old_filter, new_filter):
        """Handles the current filter being changed."""
        if new_filter is customize_filter:
            do_later(self._customize_filters, old_filter)

        elif self.model is not None:
            if (new_filter is not None) and (
                not isinstance(new_filter, TableFilter)
            ):
                new_filter = TableFilter(allowed=new_filter)
            self.model.filter = new_filter
            self.filter_modified()

    def _refresh_filters(self, filters):
        factory = self.factory
        # hack: The following line forces the 'filters' to be changed...
        factory.filters = []
        factory.filters = filters

    def _customize_filters(self, filter):
        """Allows the user to customize the current set of table filters."""
        factory = self.factory
        filter_editor = TableFilterEditor(editor=self, filter=filter)
        enum_editor = EnumEditor(values=factory.filters[:], mode="list")
        ui = filter_editor.edit_traits(
            parent=self.control,
            view=View(
                [
                    [
                        Item(
                            "filter<200>@", editor=enum_editor, resizable=True
                        ),
                        "|<>",
                    ],
                    ["edit:edit", "new", "apply", "delete:delete", "|<>"],
                    "-",
                ],
                title="Customize Filters",
                kind="livemodal",
                height=0.25,
                buttons=["OK", "Cancel"],
            ),
        )

        if ui.result:
            self._refresh_filters(enum_editor.values)
            self.filter = filter_editor.filter
        else:
            self.filter = filter

    def on_no_sort(self):
        """Handles the user requesting that columns not be sorted."""
        self.model.no_column_sort()
        self.toolbar.no_sort.enabled = False
        values = self.selected_values
        if len(values) > 0:
            self.set_extended_selection(values)

    def on_move_up(self):
        """Handles the user requesting to move the current item up one row."""
        model = self.model
        objects = []
        for index in self.selected_indices:
            objects.append(model.get_filtered_item(index))
            index -= 1
            object = model.get_filtered_item(index)
            model.delete_filtered_item_at(index)
            model.insert_filtered_item_after(index, object)

        if self.in_row_mode:
            self.set_selection(objects)
        else:
            self.set_extended_selection(self.selected_values)

    def on_move_down(self):
        """Handles the user requesting to move the current item down one row."""
        model = self.model
        objects = []
        indices = self.selected_indices[:]
        indices.reverse()
        for index in indices:
            object = model.get_filtered_item(index)
            objects.append(object)
            model.delete_filtered_item_at(index)
            model.insert_filtered_item_after(index, object)

        if self.in_row_mode:
            self.set_selection(objects)
        else:
            self.set_extended_selection(self.selected_values)

    def on_search(self):
        """Handles the user requesting a table search."""
        self.factory.search.edit_traits(
            parent=self.control,
            view="searchable_view",
            handler=TableSearchHandler(editor=self),
        )

    def on_add(self):
        """Handles the user requesting to add a new row to the table."""
        self.add_row()

    def on_delete(self):
        """Handles the user requesting to delete the currently selected items
        of the table.
        """
        # Get the selected row indices:
        indices = self.selected_indices[:]
        values = self.selected_values[:]
        indices.reverse()

        # Make sure that we don't delete any rows while an editor is open in it
        self.grid.stop_editing_indices(indices)

        # Delete the selected rows:
        for i in indices:
            index, object = self.model.delete_filtered_item_at(i)
            self._add_undo(
                ListUndoItem(
                    object=self.object,
                    name=self.name,
                    index=index,
                    removed=[object],
                )
            )

        # Compute the new selection and set it:
        items = self.model.get_filtered_items()
        n = len(items) - 1
        indices.reverse()
        for i in range(len(indices) - 1, -1, -1):
            if indices[i] > n:
                indices[i] = n
                if indices[i] < 0:
                    del indices[i]
                    del values[i]

        n = len(indices)
        if n > 0:
            if self.in_row_mode:
                self.set_selection(list({items[i] for i in indices}))
            else:
                self.set_extended_selection(
                    list({(items[indices[i]], values[i][1]) for i in range(n)})
                )
        else:
            self._update_toolbar(False)

    def on_prefs(self):
        """Handles the user requesting to set the user preference items for the
        table.
        """
        columns = self.columns[:]
        columns.extend(
            [
                c
                for c in (self.factory.columns + self.factory.other_columns)
                if c not in columns
            ]
        )
        self.edit_traits(
            parent=self.control,
            view=View(
                [
                    Item(
                        "columns",
                        resizable=True,
                        editor=SetEditor(
                            values=columns, ordered=True, can_move_all=False
                        ),
                    ),
                    "|<>",
                ],
                title="Select and Order Columns",
                width=0.3,
                height=0.3,
                resizable=True,
                buttons=["Undo", "OK", "Cancel"],
                kind="livemodal",
            ),
        )

    def prepare_menu(self, row, column):
        """Prepares to have a context menu action called."""
        object = self.model.get_filtered_item(row)
        selection = [x.obj for x in self.grid.get_selection()]
        if object not in selection:
            self.set_selection(object)
            selection = [object]
        self.set_menu_context(selection, object, column)

    def setx(self, **keywords):
        """Set one or more attributes without notifying the grid model."""
        self._no_notify = True

        for name, value in keywords.items():
            setattr(self, name, value)

        self._no_notify = False

    # -- Private Methods: -----------------------------------------------------

    def _add_undo(self, undo_item, extend=False):
        history = self.ui.history
        if history is not None:
            history.add(undo_item, extend)


class TableFilterEditor(Handler):
    """Editor that manages table filters."""

    #: TableEditor this editor is associated with
    editor = Instance(TableEditor)

    #: Current filter
    filter = Instance(TableFilter, allow_none=True)

    #: Edit the current filter
    edit = Button()

    #: Create a new filter and edit it
    new = Button()

    #: Apply the current filter to the editor's table
    apply = Button()

    #: Delete the current filter
    delete = Button()

    # -------------------------------------------------------------------------
    #  'Handler' interface:
    # -------------------------------------------------------------------------

    def init(self, info):
        """Initializes the controls of a user interface."""
        # Save both the original filter object reference and its contents:
        if self.filter is None:
            self.filter = info.filter.factory.values[0]
        self._filter = self.filter
        self._filter_copy = self.filter.clone_traits()
        return True

    def closed(self, info, is_ok):
        """Handles a dialog-based user interface being closed by the user."""
        if not is_ok:
            # Restore the contents of the original filter:
            self._filter.copy_traits(self._filter_copy)

    # -------------------------------------------------------------------------
    #  Event handlers:
    # -------------------------------------------------------------------------

    def object_filter_changed(self, info):
        """Handles a new filter being selected."""
        filter = info.object.filter
        info.edit.enabled = not filter.template
        info.delete.enabled = (not filter.template) and (
            len(info.filter.factory.values) > 1
        )

    def object_edit_changed(self, info):
        """Handles the user clicking the **Edit** button."""
        if info.initialized:
            items = self.editor.model.get_filtered_items()
            if len(items) > 0:
                item = items[0]
            else:
                item = None
            # `item` is now either the first item in the table, or None if
            # the table is empty.
            ui = self.filter.edit(item)
            if ui.result:
                self._refresh_filters(info)

    def object_new_changed(self, info):
        """Handles the user clicking the **New** button."""
        if info.initialized:
            # Get list of available filters and find the current filter in it:
            factory = info.filter.factory
            filters = factory.values
            filter = self.filter
            index = filters.index(filter) + 1
            n = len(filters)
            while (index < n) and filters[index].template:
                index += 1

            # Create a new filter based on the current filter:
            new_filter = filter.clone_traits()
            new_filter.template = False
            new_filter.name = new_filter._name = "New filter"

            # Add it to the list of filters:
            filters.insert(index, new_filter)
            self._refresh_filters(info)

            # Set up the new filter as the current filter and edit it:
            self.filter = new_filter
            do_later(self._delayed_edit, info)

    def object_apply_changed(self, info):
        """Handles the user clicking the **Apply** button."""
        if info.initialized:
            self.init(info)
            self.editor._refresh_filters(info.filter.factory.values)
            self.editor.filter = self.filter

    def object_delete_changed(self, info):
        """Handles the user clicking the **Delete** button."""
        # Get the list of available filters:
        filters = info.filter.factory.values

        if info.initialized:
            # Delete the current filter:
            index = filters.index(self.filter)
            del filters[index]

            # Select a new filter:
            if index >= len(filters):
                index -= 1
            self.filter = filters[index]
            self._refresh_filters(info)

    # -------------------------------------------------------------------------
    #  Private methods:
    # -------------------------------------------------------------------------

    def _refresh_filters(self, info):
        """Refresh the filter editor's list of filters."""
        factory = info.filter.factory
        values, factory.values = factory.values, []
        factory.values = values

    def _delayed_edit(self, info):
        """Edits the current filter, and deletes it if the user cancels the
        edit.
        """
        ui = self.filter.edit(self.editor.model.get_filtered_item(0))
        if not ui.result:
            self.object_delete_changed(info)
        else:
            self._refresh_filters(info)

        # Allow deletion as long as there is more than 1 filter:
        if (not self.filter.template) and len(info.filter.factory.values) > 1:
            info.delete.enabled = True


class TableEditorToolbar(HasPrivateTraits):
    """Toolbar displayed in table editors."""

    # -------------------------------------------------------------------------
    #  Trait definitions:
    # -------------------------------------------------------------------------

    #: Do not sort columns:
    no_sort = Instance(
        Action,
        {
            "name": "No Sorting",
            "tooltip": "Do not sort columns",
            "action": "on_no_sort",
            "enabled": False,
            "image": ImageResource("table_no_sort.png"),
        },
    )

    #: Move current object up one row:
    move_up = Instance(
        Action,
        {
            "name": "Move Up",
            "tooltip": "Move current item up one row",
            "action": "on_move_up",
            "enabled": False,
            "image": ImageResource("table_move_up.png"),
        },
    )

    #: Move current object down one row:
    move_down = Instance(
        Action,
        {
            "name": "Move Down",
            "tooltip": "Move current item down one row",
            "action": "on_move_down",
            "enabled": False,
            "image": ImageResource("table_move_down.png"),
        },
    )

    #: Search the table:
    search = Instance(
        Action,
        {
            "name": "Search",
            "tooltip": "Search table",
            "action": "on_search",
            "image": ImageResource("table_search.png"),
        },
    )

    #: Add a row:
    add = Instance(
        Action,
        {
            "name": "Add",
            "tooltip": "Insert new item",
            "action": "on_add",
            "image": ImageResource("table_add.png"),
        },
    )

    #: Delete selected row:
    delete = Instance(
        Action,
        {
            "name": "Delete",
            "tooltip": "Delete current item",
            "action": "on_delete",
            "enabled": False,
            "image": ImageResource("table_delete.png"),
        },
    )

    #: Edit the user preferences:
    prefs = Instance(
        Action,
        {
            "name": "Preferences",
            "tooltip": "Set user preferences for table",
            "action": "on_prefs",
            "image": ImageResource("table_prefs.png"),
        },
    )

    #: The table editor that this is the toolbar for:
    editor = Instance(TableEditor)

    #: The toolbar control:
    control = Any()

    def __init__(self, parent=None, **traits):
        super().__init__(**traits)
        editor = self.editor
        factory = editor.factory
        actions = []

        if factory.sortable and (not factory.sort_model):
            actions.append(self.no_sort)

        if (not editor.in_column_mode) and factory.reorderable:
            actions.append(self.move_up)
            actions.append(self.move_down)

        if editor.in_row_mode and (factory.search is not None):
            actions.append(self.search)

        if factory.editable:
            if (factory.row_factory is not None) and (not factory.auto_add):
                actions.append(self.add)

            if (factory.deletable) and (not editor.in_column_mode):
                actions.append(self.delete)

        if factory.configurable:
            actions.append(self.prefs)

        if len(actions) > 0:
            toolbar = ToolBar(
                image_size=(16, 16),
                show_tool_names=False,
                show_divider=False,
                *actions,
            )
            self.control = toolbar.create_tool_bar(parent, self)
            self.control.SetBackgroundColour(parent.GetBackgroundColour())

            # fixme: Why do we have to explictly set the size of the toolbar?
            #        Is there some method that needs to be called to do the
            #        layout?
            self.control.SetSize(wx.Size(23 * len(actions), 16))

    # -------------------------------------------------------------------------
    #  Pyface/Traits menu/toolbar controller interface:
    # -------------------------------------------------------------------------

    def add_to_menu(self, menu_item):
        """Adds a menu item to the menu bar being constructed."""
        pass

    def add_to_toolbar(self, toolbar_item):
        """Adds a toolbar item to the too bar being constructed."""
        pass

    def can_add_to_menu(self, action):
        """Returns whether the action should be defined in the user interface."""
        return True

    def can_add_to_toolbar(self, action):
        """Returns whether the toolbar action should be defined in the user
        interface.
        """
        return True

    def perform(self, action, action_event=None):
        """Performs the action described by a specified Action object."""
        getattr(self.editor, action.action)()


class TableSearchHandler(Handler):
    """Handler for saerching a table."""

    # -------------------------------------------------------------------------
    #  Trait definitions:
    # -------------------------------------------------------------------------

    #: The editor that this handler is associated with
    editor = Instance(TableEditor)

    #: Find next matching item
    find_next = Button("Find Next")

    #: Find previous matching item
    find_previous = Button("Find Previous")

    #: Select all matching items
    select = Button()

    #: The user is finished searching
    OK = Button("Close")

    #: Search status message:
    status = Str()

    def handler_find_next_changed(self, info):
        """Handles the user clicking the **Find** button."""
        if info.initialized:
            editor = self.editor
            items = editor.model.get_filtered_items()

            for i in range(editor.selected_row_index + 1, len(items)):
                if info.object.filter(items[i]):
                    self.status = "Item %d matches" % (i + 1)
                    editor.set_selection(items[i])
                    editor.selected_row_index = i
                    break
            else:
                self.status = "No more matches found"

    def handler_find_previous_changed(self, info):
        """Handles the user clicking the **Find previous** button."""
        if info.initialized:
            editor = self.editor
            items = editor.model.get_filtered_items()

            for i in range(editor.selected_row_index - 1, -1, -1):
                if info.object.filter(items[i]):
                    self.status = "Item %d matches" % (i + 1)
                    editor.set_selection(items[i])
                    editor.selected_row_index = i
                    break
            else:
                self.status = "No more matches found"

    def handler_select_changed(self, info):
        """Handles the user clicking the **Select** button."""
        if info.initialized:
            editor = self.editor
            filter = info.object.filter
            items = [
                item
                for item in editor.model.get_filtered_items()
                if filter(item)
            ]
            editor.set_selection(items)

            if len(items) == 1:
                self.status = "1 item selected"
            else:
                self.status = "%d items selected" % len(items)

    def handler_OK_changed(self, info):
        """Handles the user clicking the OK button."""
        if info.initialized:
            self.close(info, True)


# Define the SimpleEditor class.
SimpleEditor = TableEditor


# Define the ReadonlyEditor class.
ReadonlyEditor = TableEditor