# (C) Copyright 2005-2021 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!

""" A grid control with a model/ui architecture. """


import sys
import wx
import wx.lib.gridmovers as grid_movers
from os.path import abspath, exists
from wx.grid import Grid as wxGrid
from wx.grid import GridCellAttr, GridCellBoolRenderer, GridTableBase
from wx.grid import (
    GridTableMessage,
    GRIDTABLE_NOTIFY_ROWS_APPENDED,
    GRIDTABLE_NOTIFY_ROWS_DELETED,
    GRIDTABLE_NOTIFY_ROWS_INSERTED,
    GRIDTABLE_NOTIFY_COLS_APPENDED,
    GRIDTABLE_NOTIFY_COLS_DELETED,
    GRIDTABLE_NOTIFY_COLS_INSERTED,
    GRIDTABLE_REQUEST_VIEW_GET_VALUES,
    GRID_VALUE_STRING,
)
from wx import TheClipboard


from pyface.api import Widget
from pyface.timer.api import do_later
from traits.api import (
    Bool,
    Color,
    Enum,
    Event,
    Font,
    Instance,
    Int,
    Trait,
    Undefined,
)
from pyface.wx.drag_and_drop import (
    PythonDropSource,
    PythonDropTarget,
    PythonObject,
)
from pyface.wx.drag_and_drop import clipboard as enClipboard, FileDropSource

from .grid_model import GridModel

# Is this code running on MS Windows?
is_win32 = sys.platform == "win32"

ASCII_C = 67


class Grid(Widget):
    """ A grid control with a model/ui architecture. """

    # 'Grid' interface -----------------------------------------------------

    # The model that provides the data for the grid.
    model = Instance(GridModel, ())

    # Should grid lines be shown on the table?
    enable_lines = Bool(True)

    # The color to show gridlines in
    grid_line_color = Color("blue")

    # Show row headers?
    show_row_headers = Bool(True)

    # Show column headers?
    show_column_headers = Bool(True)

    # The default font to use for text in labels
    default_label_font = Font(None)

    # The default background color for labels
    default_label_bg_color = Color(wx.Colour(236, 233, 216))

    # The default text color for labels
    default_label_text_color = Color("black")

    # The color to use for a selection background
    selection_bg_color = Trait(wx.Colour(49, 106, 197), None, Color)

    # The color to use for a selection foreground/text
    selection_text_color = Trait(wx.Colour(255, 255, 255), None, Color)

    # The default font to use for text in cells
    default_cell_font = Font(None)

    # The default text color to use for text in cells
    default_cell_text_color = Color("black")

    # The default background color to use for editable cells
    default_cell_bg_color = Color("white")

    # The default background color to use for read-only cells
    # default_cell_read_only_color = Trait(Color("linen"), None, Color)
    default_cell_read_only_color = Color(wx.Colour(248, 247, 241))

    # Should the grid be read-only? If this is set to false, individual
    # cells can still declare themselves read-only.
    read_only = Bool(False)

    # Selection mode.
    selection_mode = Enum("cell", "rows", "cols", "")

    # Sort data when a column header is clicked?
    allow_column_sort = Bool(True)

    # Sort data when a row header is clicked?
    allow_row_sort = Bool(False)

    # pixel height of column labels
    column_label_height = Int(32)

    # pixel width of row labels
    row_label_width = Int(82)

    # auto-size columns and rows?
    autosize = Bool(False)

    # Allow single-click access to cell-editors?
    edit_on_first_click = Bool(True)

    # Events ----

    # A cell has been activated (ie. double-clicked).
    cell_activated = Event()

    # The current selection has changed.
    selection_changed = Event()

    # A drag operation was started on a cell.
    cell_begin_drag = Event()

    # A left-click occurred on a cell.
    cell_left_clicked = Event()

    # A left-click occurred on a cell at specific location
    # Useful if the cell contains multiple controls though the hit test
    # is left to the consumer of the event
    cell_left_clicked_location = Event()

    # A right-click occurred on a cell.
    cell_right_clicked = Event()

    # protected variables to store the location of the clicked event
    _x_clicked = Int()
    _y_clicked = Int()

    # ------------------------------------------------------------------------
    # 'object' interface.
    # ------------------------------------------------------------------------
    def __init__(self, parent, **traits):
        """ Creates a new grid.

        'parent' is the toolkit-specific control that is the grid's parent.

        """

        # Base class constructors.
        super(Grid, self).__init__(**traits)

        # Flag set when columns are resizing:
        self._user_col_size = False

        # Create the toolkit-specific control.
        self.control = self._grid = grid = wxGrid(parent, -1)
        grid.grid = self

        self._moveTo = None
        self._edit = False
        grid.Bind(wx.EVT_IDLE, self._on_idle)
        # Set when moving edit cursor:
        grid._no_reset_col = False
        grid._no_reset_row = False

        # initialize the current selection
        self.__current_selection = ()

        self.__initialize_counts(self.model)
        self.__initialize_sort_state()

        # Don't display any extra space around the rows and columns.
        grid.SetMargins(0, 0)

        # Provides more accurate scrolling behavior without creating large
        # margins on the bottom and right. The down side is that it makes
        # scrolling using the scroll bar buttons painfully slow:
        grid.SetScrollLineX(1)
        grid.SetScrollLineY(1)

        # Tell the grid to get its data from the model.
        #
        # N.B The terminology used in the wxPython API is a little confusing!
        # --- The 'SetTable' method is actually setting the model used by
        #     the grid (which is the view)!
        #
        # The second parameter to 'SetTable' tells the grid to take ownership
        # of the model and to destroy it when it is done.  Otherwise you would
        # need to keep a reference to the model and manually destroy it later
        # (by calling it's Destroy method).
        #
        # fixme: We should create a default model if one is not supplied.

        # The wx virtual table hook.

        self._grid_table_base = _GridTableBase(self.model, self)

        # keep the onership of the table in this class
        grid.SetTable(self._grid_table_base, takeOwnership=False)

        # Enable column and row moving:
        grid_movers.GridColMover(grid)
        grid_movers.GridRowMover(grid)
        grid.Bind(grid_movers.EVT_GRID_COL_MOVE, self._on_col_move)
        grid.Bind(grid_movers.EVT_GRID_ROW_MOVE, self._on_row_move)

        self.model.observe(self._on_model_content_changed, "content_changed")
        self.model.observe(
            self._on_model_structure_changed, "structure_changed"
        )
        self.model.observe(self._on_row_sort, "row_sorted")
        self.model.observe(self._on_column_sort, "column_sorted")
        self.observe(self._on_new_model, "model")

        # hook up style trait handlers - note that we have to use
        # dynamic notification hook-ups because these handlers should
        # not be called until after the control object is initialized.
        # static trait notifiers get called when the object inits.
        self.observe(self._on_enable_lines_changed, "enable_lines")
        self.observe(self._on_grid_line_color_changed, "grid_line_color")
        self.observe(self._on_default_label_font_changed, "default_label_font")
        self.observe(
            self._on_default_label_bg_color_changed, "default_label_bg_color"
        )
        self.observe(
            self._on_default_label_text_color_changed,
            "default_label_text_color",
        )
        self.observe(self._on_selection_bg_color_changed, "selection_bg_color")
        self.observe(
            self._on_selection_text_color_changed, "selection_text_color"
        )
        self.observe(self._on_default_cell_font_changed, "default_cell_font")
        self.observe(
            self._on_default_cell_text_color_changed, "default_cell_text_color"
        )
        self.observe(
            self._on_default_cell_bg_color_changed, "default_cell_bg_color"
        )
        self.observe(self._on_read_only_changed, "read_only_changed")
        self.observe(self._on_selection_mode_changed, "selection_mode")
        self.observe(
            self._on_column_label_height_changed, "column_label_height"
        )
        self.observe(self._on_row_label_width_changed, "row_label_width")
        self.observe(
            self._on_show_column_headers_changed, "show_column_headers"
        )
        self.observe(self._on_show_row_headers_changed, "show_row_headers")

        # Initialize wx handlers:
        self._notify_select = True
        grid.Bind(wx.grid.EVT_GRID_SELECT_CELL, self._on_select_cell)
        grid.Bind(wx.grid.EVT_GRID_RANGE_SELECT, self._on_range_select)
        grid.Bind(wx.grid.EVT_GRID_COL_SIZE, self._on_col_size)
        grid.Bind(wx.grid.EVT_GRID_ROW_SIZE, self._on_row_size)

        # This starts the cell editor on a double-click as well as on a second
        # click:
        grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self._on_cell_left_dclick)

        # Notify when cells are clicked on:
        grid.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self._on_cell_right_click)
        grid.Bind(
            wx.grid.EVT_GRID_CELL_RIGHT_DCLICK, self._on_cell_right_dclick
        )

        grid.Bind(
            wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self._on_label_right_click
        )
        grid.Bind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self._on_label_left_click)

        if is_win32:
            grid.Bind(wx.grid.EVT_GRID_EDITOR_HIDDEN, self._on_editor_hidden)

        # We handle key presses to change the behavior of the <Enter> and
        # <Tab> keys to make manual data entry smoother.
        grid.Bind(wx.EVT_KEY_DOWN, self._on_key_down)

        # We handle control resize events to adjust column widths
        grid.Bind(wx.EVT_SIZE, self._on_size)

        # Handle drags:
        self._corner_window = grid.GetGridCornerLabelWindow()
        self._grid_window = gw = grid.GetGridWindow()
        self._row_window = rw = grid.GetGridRowLabelWindow()
        self._col_window = cw = grid.GetGridColLabelWindow()

        # Handle mouse button state changes:
        self._ignore = False
        for window in (gw, rw, cw):
            window.Bind(wx.EVT_MOTION, self._on_grid_motion)
            window.Bind(wx.EVT_LEFT_DOWN, self._on_left_down)
            window.Bind(wx.EVT_LEFT_UP, self._on_left_up)

        # Initialize the row and column models:
        self.__initialize_rows(self.model)
        self.__initialize_columns(self.model)
        self.__initialize_fonts()

        # Handle trait style settings:
        self.__initialize_style_settings()

        # Enable the grid as a drag and drop target:
        self._grid.SetDropTarget(PythonDropTarget(self))

        self.__autosize()

        self._edit = False
        grid.Bind(wx.EVT_IDLE, self._on_idle)

    def dispose(self):
        # Remove all wx handlers:
        grid = self._grid
        if grid is not None:
            grid.Unbind(wx.grid.EVT_GRID_SELECT_CELL)
            grid.Unbind(wx.grid.EVT_GRID_RANGE_SELECT)
            grid.Unbind(wx.grid.EVT_GRID_COL_SIZE)
            grid.Unbind(wx.grid.EVT_GRID_ROW_SIZE)
            grid.Unbind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK)
            grid.Unbind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK)
            grid.Unbind(wx.grid.EVT_GRID_CELL_RIGHT_DCLICK)
            grid.Unbind(wx.grid.EVT_GRID_LABEL_RIGHT_CLICK)
            grid.Unbind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK)
            grid.Unbind(wx.grid.EVT_GRID_EDITOR_CREATED)
            if is_win32:
                grid.Unbind(wx.grid.EVT_GRID_EDITOR_HIDDEN)
            grid.Unbind(wx.EVT_KEY_DOWN)
            grid.Unbind(wx.EVT_SIZE)

        self._grid_window.Unbind(wx.EVT_PAINT)

        for window in (self._grid_window, self._row_window, self._col_window):
            window.Unbind(wx.EVT_MOTION)
            window.Unbind(wx.EVT_LEFT_DOWN)
            window.Unbind(wx.EVT_LEFT_UP)

        self.model.observe(
            self._on_model_content_changed, "content_changed", remove=True
        )
        self.model.observe(
            self._on_model_structure_changed, "structure_changed", remove=True
        )
        self.model.observe(self._on_row_sort, "row_sorted", remove=True)
        self.model.observe(self._on_column_sort, "column_sorted", remove=True)
        self.observe(self._on_new_model, "model", remove=True)

        self.observe(
            self._on_enable_lines_changed, "enable_lines", remove=True
        )
        self.observe(
            self._on_grid_line_color_changed, "grid_line_color", remove=True
        )
        self.observe(
            self._on_default_label_font_changed,
            "default_label_font",
            remove=True,
        )
        self.observe(
            self._on_default_label_bg_color_changed,
            "default_label_bg_color",
            remove=True,
        )
        self.observe(
            self._on_default_label_text_color_changed,
            "default_label_text_color",
            remove=True,
        )
        self.observe(
            self._on_selection_bg_color_changed,
            "selection_bg_color",
            remove=True,
        )
        self.observe(
            self._on_selection_text_color_changed,
            "selection_text_color",
            remove=True,
        )
        self.observe(
            self._on_default_cell_font_changed,
            "default_cell_font",
            remove=True,
        )
        self.observe(
            self._on_default_cell_text_color_changed,
            "default_cell_text_color",
            remove=True,
        )
        self.observe(
            self._on_default_cell_bg_color_changed,
            "default_cell_bg_color",
            remove=True,
        )
        self.observe(
            self._on_read_only_changed, "read_only_changed", remove=True
        )
        self.observe(
            self._on_selection_mode_changed, "selection_mode", remove=True
        )
        self.observe(
            self._on_column_label_height_changed,
            "column_label_height",
            remove=True,
        )
        self.observe(
            self._on_row_label_width_changed, "row_label_width", remove=True
        )
        self.observe(
            self._on_show_column_headers_changed,
            "show_column_headers",
            remove=True,
        )
        self.observe(
            self._on_show_row_headers_changed, "show_row_headers", remove=True
        )

        self._grid_table_base.dispose()
        self._grid = None

    # ------------------------------------------------------------------------
    # Trait event handlers.
    # ------------------------------------------------------------------------

    def _on_new_model(self, event):
        """ When we get a new model reinitialize grid match to that model. """

        self._grid_table_base.model = self.model

        self.__initialize_counts(self.model)

        self._on_model_changed()

        if self.autosize:
            # Note that we don't call AutoSize() here, because autosizing
            # the rows looks like crap.
            self._grid.AutoSizeColumns(False)

    def _on_model_content_changed(self, event):
        """ A notification method called when the data in the underlying
            model changes. """
        self._grid.ForceRefresh()

    def _on_model_structure_changed(self, event=None):
        """ A notification method called when the underlying model has
        changed. Responsible for making sure the view object updates
        correctly. """

        # Disable any active editors in order to prevent a wx crash bug:
        self._edit = False
        grid = self._grid

        # Make sure any current active editor has been disabled:
        grid.DisableCellEditControl()

        # More wacky fun with wx. We have to manufacture the appropriate
        # grid messages and send them off to make sure the grid updates
        # correctly:
        should_autosize = False

        # First check to see if rows have been added or deleted:
        row_count = self.model.get_row_count()
        delta = row_count - self._row_count
        self._row_count = row_count

        if delta > 0:
            # Rows were added:
            msg = GridTableMessage(
                self._grid_table_base, GRIDTABLE_NOTIFY_ROWS_APPENDED, delta
            )
            grid.ProcessTableMessage(msg)
            should_autosize = True
        elif delta < 0:
            # Rows were deleted:
            msg = GridTableMessage(
                self._grid_table_base,
                GRIDTABLE_NOTIFY_ROWS_DELETED,
                row_count,
                -delta,
            )
            grid.ProcessTableMessage(msg)
            should_autosize = True

        # Now check for column changes:
        col_count = self.model.get_column_count()
        delta = col_count - self._col_count
        self._col_count = col_count

        if delta > 0:
            # Columns were added:
            msg = GridTableMessage(
                self._grid_table_base, GRIDTABLE_NOTIFY_COLS_APPENDED, delta
            )
            grid.ProcessTableMessage(msg)
            should_autosize = True
        elif delta < 0:
            # Columns were deleted:
            msg = GridTableMessage(
                self._grid_table_base,
                GRIDTABLE_NOTIFY_COLS_DELETED,
                col_count,
                -delta,
            )
            grid.ProcessTableMessage(msg)
            should_autosize = True

        # Finally make sure we update for any new values in the table:
        msg = GridTableMessage(
            self._grid_table_base, GRIDTABLE_REQUEST_VIEW_GET_VALUES
        )
        grid.ProcessTableMessage(msg)

        if should_autosize:
            self.__autosize()

        # We have to make sure the editor/renderer cache in the GridTableBase
        # object is cleaned out:
        self._grid_table_base._clear_cache()

        grid.AdjustScrollbars()
        self._refresh()

    def _on_row_sort(self, event):
        """ Handles a row_sorted event from the underlying model. """

        # First grab the new data out of the event:
        if event.new.index < 0:
            self._current_sorted_row = None
        else:
            self._current_sorted_row = event.new.index

        self._row_sort_reversed = event.new.reversed

        # Since the label may have changed we may need to autosize again:
        # fixme: when we change how we represent the sorted column
        #        this should go away.
        self.__autosize()

        # Make sure everything updates to reflect the changes:
        self._on_model_structure_changed()

    def _on_column_sort(self, event):
        """ Handles a column_sorted event from the underlying model. """

        # first grab the new data out of the event
        if event.new.index < 0:
            self._current_sorted_col = None
        else:
            self._current_sorted_col = event.new.index

        self._col_sort_reversed = event.new.reversed

        # since the label may have changed we may need to autosize again
        # fixme: when we change how we represent the sorted column
        #        this should go away.
        self.__autosize()

        # make sure everything updates to reflect the changes
        self._on_model_structure_changed()

    def _on_enable_lines_changed(self, event=None):
        """ Handle a change to the enable_lines trait. """
        self._grid.EnableGridLines(self.enable_lines)

    def _on_grid_line_color_changed(self, event=None):
        """ Handle a change to the enable_lines trait. """
        self._grid.SetGridLineColour(self.grid_line_color)

    def _on_default_label_font_changed(self, event=None):
        """ Handle a change to the default_label_font trait. """

        font = self.default_label_font
        if font is None:
            font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
            font.SetWeight(wx.BOLD)

        self._grid.SetLabelFont(font)

    def _on_default_label_text_color_changed(self, event=None):
        """ Handle a change to the default_cell_text_color trait. """

        if self.default_label_text_color is not None:
            color = self.default_label_text_color
            self._grid.SetLabelTextColour(color)
            self._grid.ForceRefresh()

    def _on_default_label_bg_color_changed(self, event=None):
        """ Handle a change to the default_cell_text_color trait. """

        if self.default_label_bg_color is not None:
            color = self.default_label_bg_color
            self._grid.SetLabelBackgroundColour(color)
            self._grid.ForceRefresh()

    def _on_selection_bg_color_changed(self, event=None):
        """ Handle a change to the selection_bg_color trait. """
        if self.selection_bg_color is not None:
            self._grid.SetSelectionBackground(self.selection_bg_color)

    def _on_selection_text_color_changed(self, event=None):
        """ Handle a change to the selection_text_color trait. """
        if self.selection_text_color is not None:
            self._grid.SetSelectionForeground(self.selection_text_color)

    def _on_default_cell_font_changed(self, event=None):
        """ Handle a change to the default_cell_font trait. """

        if self.default_cell_font is not None:
            self._grid.SetDefaultCellFont(self.default_cell_font)
            self._grid.ForceRefresh()

    def _on_default_cell_text_color_changed(self, event=None):
        """ Handle a change to the default_cell_text_color trait. """

        if self.default_cell_text_color is not None:
            color = self.default_cell_text_color
            self._grid.SetDefaultCellTextColour(color)
            self._grid.ForceRefresh()

    def _on_default_cell_bg_color_changed(self, event=None):
        """ Handle a change to the default_cell_bg_color trait. """

        if self.default_cell_bg_color is not None:
            color = self.default_cell_bg_color
            self._grid.SetDefaultCellBackgroundColour(color)
            self._grid.ForceRefresh()

    def _on_read_only_changed(self, event=None):
        """ Handle a change to the read_only trait. """

        # should the whole grid be read-only?
        if self.read_only:
            self._grid.EnableEditing(False)
        else:
            self._grid.EnableEditing(True)

    def _on_selection_mode_changed(self, event=None):
        """ Handle a change to the selection_mode trait. """

        # should we allow individual cells to be selected or only rows
        # or only columns
        if self.selection_mode == "cell":
            self._grid.SetSelectionMode(wxGrid.SelectCells)
        elif self.selection_mode == "rows":
            self._grid.SetSelectionMode(wxGrid.SelectRows)
        elif self.selection_mode == "cols":
            self._grid.SetSelectionMode(wxGrid.SelectColumns)

    def _on_column_label_height_changed(self, event=None):
        """ Handle a change to the column_label_height trait. """

        # handle setting for height of column labels
        if self.column_label_height is not None:
            self._grid.SetColLabelSize(self.column_label_height)

    def _on_row_label_width_changed(self, event=None):
        """ Handle a change to the row_label_width trait. """
        # handle setting for width of row labels
        if self.row_label_width is not None:
            self._grid.SetRowLabelSize(self.row_label_width)

    def _on_show_column_headers_changed(self, event=None):
        """ Handle a change to the show_column_headers trait. """

        if not self.show_column_headers:
            self._grid.SetColLabelSize(0)
        else:
            self._grid.SetColLabelSize(self.column_label_height)

    def _on_show_row_headers_changed(self, event=None):
        """ Handle a change to the show_row_headers trait. """

        if not self.show_row_headers:
            self._grid.SetRowLabelSize(0)
        else:
            self._grid.SetRowLabelSize(self.row_label_width)

    # ------------------------------------------------------------------------
    # 'Grid' interface.
    # ------------------------------------------------------------------------

    def get_selection(self):
        """ Return a list of the currently selected objects. """

        return self.__get_selection()

    def set_selection(self, selection_list, notify=True):
        """ Set the current selection to the objects listed in selection_list.
        Note that these objects are model-specific, as the grid depends on the
        underlying model to translate these objects into grid coordinates.
        A ValueError will be raised if the objects are not in the proper format
        for the underlying model. """

        # Set the 'should we notify the model of the selection change' flag:
        self._notify_select = notify

        # First use the model to resolve the object list into a set of
        # grid coordinates
        cells = self.model.resolve_selection(selection_list)

        # Now make sure all those grid coordinates get set properly:
        grid = self._grid
        grid.BeginBatch()

        grid.ClearSelection()

        mode = self.selection_mode
        if mode == "rows":
            self._select_rows(cells)
        elif mode != "":
            for selection in cells:
                row, col = max(0, selection[0]), max(0, selection[1])
                grid.SelectBlock(row, col, row, col, True)

        grid.EndBatch()

        # Indicate that the selection has been changed:
        if notify:
            self.__fire_selection_changed()

        self._notify_select = True

    def stop_editing_indices(self, indices):
        """ If editing is occuring in a row in 'indices', stop editing. """

        if self._grid.GetGridCursorRow() in indices:
            self._grid.DisableCellEditControl()

    # ------------------------------------------------------------------------
    # wx event handlers.
    # ------------------------------------------------------------------------

    def _on_size(self, evt):
        """ Called when the grid is resized. """
        self.__autosize()
        evt.Skip()

    def _on_editor_hidden(self, evt):
        # This is a hack to make clicking on a window button while a grid
        # editor is active work correctly under Windows. Normally, when the
        # user clicks on the button to exit grid cell editing and perform the
        # button function, only the grid cell editing is terminated under
        # Windows. A second click on the button is required to actually
        # trigger the button functionality. We circumvent this problem by
        # generating a 'fake' button click event. Technically this solution
        # is not correct since the button click should be generated on mouse
        # up, but I'm not sure if we will get the mouse up event, so we do it
        # here instead. Note that this handler is only set-up when the OS is
        # Windows.
        control = wx.FindWindowAtPointer()
        if isinstance(control, wx.Button):
            do_later(
                wx.PostEvent,
                control,
                wx.CommandEvent(
                    wx.wxEVT_COMMAND_BUTTON_CLICKED, control.GetId()
                ),
            )

    def _on_left_down(self, evt):
        """ Called when the left mouse button is pressed.
        """
        grid = self._grid
        self._x_clicked = evt.GetX()
        self._y_clicked = evt.GetY()
        self._ignore = (grid.XToEdgeOfCol(evt.GetX()) != wx.NOT_FOUND) or (
            grid.YToEdgeOfRow(evt.GetY()) != wx.NOT_FOUND
        )
        evt.Skip()

    def _on_left_up(self, evt):
        """ Called when the left mouse button is released.
        """
        self._ignore = False
        evt.Skip()

    def _on_motion(self, evt):
        """ Called when the mouse moves. """

        evt.Skip()
        if evt.Dragging() and not evt.ControlDown():
            data = self.__get_drag_value()
            if isinstance(data, str):
                file = abspath(data)
                if exists(file):
                    FileDropSource(self._grid, file)
                    return

            PythonDropSource(self._grid, data)

    def _on_grid_motion(self, evt):
        if evt.GetEventObject() is self._grid_window:
            x, y = self._grid.CalcUnscrolledPosition(evt.GetPosition().Get())
            row = self._grid.YToRow(y)
            col = self._grid.XToCol(x)

            # Notify the model that the mouse has moved into the cell at row,col,
            # only if the row and col are valid.
            if (row >= 0) and (col >= 0):
                self.model.mouse_cell = (row, col)

            # If we are not ignoring mouse events, call _on_motion.
            if not self._ignore:
                self._on_motion(evt)

        evt.Skip()

    def _on_select_cell(self, evt):
        """ Called when the user has moved to another cell. """
        row, col = evt.GetRow(), evt.GetCol()
        self._moveTo = (row, col)
        self.cell_left_clicked = self.model.click = (row, col)

        # Try to find a renderer for this cell:
        renderer = self.model.get_cell_renderer(row, col)

        # If the renderer has defined a handler then call it:
        result = False
        if renderer is not None:
            result = renderer.on_left_click(self, row, col)

        # print("self._grid.GetParent()", self._grid.GetParent().GetParent().GetParent())
        # if the handler didn't tell us to stop further handling then skip
        if not result:
            if (self.selection_mode != "") or (not self.edit_on_first_click):
                self._grid.SelectBlock(row, col, row, col, evt.ControlDown())

            self._edit = True
            evt.Skip()

    def _on_range_select(self, evt):
        if evt.Selecting():
            if (self.selection_mode == "cell") and evt.ControlDown():
                self._grid.SelectBlock(
                    evt.GetTopRow(),
                    evt.GetLeftCol(),
                    evt.GetBottomRow(),
                    evt.GetRightCol(),
                    True,
                )

        if self._notify_select:
            self.__fire_selection_changed()

    def _on_col_size(self, evt):
        """ Called when the user changes a column's width. """
        self.__autosize()

        evt.Skip()

    def _on_row_size(self, evt):
        """ Called when the user changes a row's height. """
        self._grid.AdjustScrollbars()

        evt.Skip()

    def _on_idle(self, evt):
        """ Immediately jumps into editing mode, bypassing the
            usual select mode of a spreadsheet. See also self.OnSelectCell().
        """
        if self._edit == True and self.edit_on_first_click:
            if self._grid.CanEnableCellControl():
                self._grid.EnableCellEditControl()
            self._edit = False

        evt.Skip()

    def _on_cell_left_dclick(self, evt):
        """ Called when the left mouse button was double-clicked.

        From the wxPython demo code:-

        'I do this because I don't like the default behaviour of not starting
        the cell editor on double clicks, but only a second click.'

        Fair enuff!

        """
        row, col = evt.GetRow(), evt.GetCol()
        data = self.model.get_value(row, col)
        self.cell_activated = data

        # Tell the model that a cell was double-clicked on:
        self.model.dclick = (row, col)

        # Try to find a renderer for this cell
        renderer = self.model.get_cell_renderer(row, col)

        # If the renderer has defined a handler then call it
        if renderer is not None:
            renderer.on_left_dclick(self, row, col)

    def _on_cell_right_dclick(self, evt):
        """ Called when the right mouse button was double-clicked.

        From the wxPython demo code:-

        'I do this because I don't like the default behaviour of not starting
        the cell editor on double clicks, but only a second click.'

        Fair enuff!

        """
        row, col = evt.GetRow(), evt.GetCol()

        # try to find a renderer for this cell
        renderer = self.model.get_cell_renderer(row, col)

        # if the renderer has defined a handler then call it
        if renderer is not None:
            renderer.on_right_dclick(self, row, col)

    def _on_cell_right_click(self, evt):
        """ Called when a right click occurred in a cell. """
        row, col = evt.GetRow(), evt.GetCol()

        # try to find a renderer for this cell
        renderer = self.model.get_cell_renderer(row, col)

        # if the renderer has defined a handler then call it
        result = False
        if renderer is not None:
            result = renderer.on_right_click(self, row, col)

        # if the handler didn't tell us to stop further handling then skip
        if not result:
            # ask model for the appropriate context menu
            menu_manager = self.model.get_cell_context_menu(row, col)
            # get the underlying menu object
            if menu_manager is not None:
                controller = None
                if type(menu_manager) is tuple:
                    menu_manager, controller = menu_manager
                menu = menu_manager.create_menu(self._grid, controller)
                # if it has anything in it pop it up
                if menu.GetMenuItemCount() > 0:
                    # Popup the menu (if an action is selected it will be
                    # performed before before 'PopupMenu' returns).
                    x, y = evt.GetPosition().Get()
                    self._grid.PopupMenu(menu, x - 10, y - 10)

            self.cell_right_clicked = (row, col)

            evt.Skip()

    def _on_label_right_click(self, evt):
        """ Called when a right click occurred on a label. """

        row, col = evt.GetRow(), evt.GetCol()

        # a row value of -1 means this click happened on a column.
        # vice versa, a col value of -1 means a row click.
        menu_manager = None
        if row == -1:
            menu_manager = self.model.get_column_context_menu(col)
        else:
            menu_manager = self.model.get_row_context_menu(row)

        if menu_manager is not None:
            # get the underlying menu object
            menu = menu_manager.create_menu(self._grid)
            # if it has anything in it pop it up
            if menu.GetMenuItemCount() > 0:
                # Popup the menu (if an action is selected it will be performed
                # before before 'PopupMenu' returns).
                self._grid.PopupMenu(menu, evt.GetPosition().Get())
        elif col >= 0:
            cws = getattr(self, "_cached_widths", None)
            if (cws is not None) and (0 <= col < len(cws)):
                cws[col] = None
                self.__autosize()

        evt.Skip()

    def _on_label_left_click(self, evt):
        """ Called when a left click occurred on a label. """

        row, col = evt.GetRow(), evt.GetCol()

        # A row value of -1 means this click happened on a column.
        # vice versa, a col value of -1 means a row click.
        if (row == -1) and self.allow_column_sort and evt.ControlDown():
            self._column_sort(col)

        elif (col == -1) and self.allow_row_sort and evt.ControlDown():
            self._row_sort(row)

        evt.Skip()

    def _column_sort(self, col):
        """ Sorts the data on the specified column **col**.
        """
        self._grid.Freeze()

        if (col == self._current_sorted_col) and (not self._col_sort_reversed):
            # If this column is currently sorted on then reverse it:
            self.model.sort_by_column(col, True)
        elif (col == self._current_sorted_col) and self._col_sort_reversed:
            # If this column is currently reverse-sorted then unsort it:
            try:
                self.model.no_column_sort()
            except NotImplementedError:
                # If an unsort function is not implemented then just
                # reverse the sort:
                self.model.sort_by_column(col, False)
        else:
            # Sort the data:
            self.model.sort_by_column(col, False)

        self._grid.Thaw()

    def _row_sort(self, row):
        self._grid.Freeze()

        if (row == self._current_sorted_row) and (not self._row_sort_reversed):
            # If this row is currently sorted on then reverse it:
            self.model.sort_by_row(row, True)
        elif row == self._current_sorted_row and self._row_sort_reversed:
            # If this row is currently reverse-sorted then unsort it:
            try:
                self.model.no_row_sort()
            except NotImplementedError:
                # If an unsort function is not implemented then just
                # reverse the sort:
                self.model.sort_by_row(row, False)
        else:
            # Sort the data:
            self.model.sort_by_row(row, False)

        self._grid.Thaw()

    def _on_key_down(self, evt):
        """ Called when a key is pressed. """
        # This changes the behaviour of the <Enter> and <Tab> keys to make
        # manual data entry smoother!
        #
        # Don't change the behavior if the <Control> key is pressed as this
        # has meaning to the edit control.
        key_code = evt.GetKeyCode()

        evt.Skip()

    def _move_to_next_cell(self, expandSelection=False):
        """ Move to the 'next' cell. """

        # Complete the edit on the current cell.
        self._grid.DisableCellEditControl()

        # Try to move to the next column.
        success = self._grid.MoveCursorRight(expandSelection)

        # If the move failed then we must be at the end of a row.
        if not success:
            # Move to the first column in the next row.
            newRow = self._grid.GetGridCursorRow() + 1
            if newRow < self._grid.GetNumberRows():
                self._grid.SetGridCursor(newRow, 0)
                self._grid.MakeCellVisible(newRow, 0)

            else:
                # This would be a good place to add a new row if your app
                # needs to do that.
                pass

        return success

    def _move_to_previous_cell(self, expandSelection=False):
        """ Move to the 'previous' cell. """

        # Complete the edit on the current cell.
        self._grid.DisableCellEditControl()

        # Try to move to the previous column (without expanding the current
        # selection).
        success = self._grid.MoveCursorLeft(expandSelection)

        # If the move failed then we must be at the start of a row.
        if not success:
            # Move to the last column in the previous row.
            newRow = self._grid.GetGridCursorRow() - 1
            if newRow >= 0:
                self._grid.SetGridCursor(
                    newRow, self._grid.GetNumberCols() - 1
                )
                self._grid.MakeCellVisible(
                    newRow, self._grid.GetNumberCols() - 1
                )

    def _refresh(self):
        self._grid.GetParent().Layout()

    def _on_col_move(self, evt):
        """ Called when a column move is taking place.
        """
        self._ignore = True

        # Get the column being moved:
        frm = evt.GetMoveColumn()

        # Get the column to insert it before:
        to = evt.GetBeforeColumn()

        # Tell the model to update its columns:
        if self.model._move_column(frm, to):

            # Modify the grid:
            grid = self._grid
            cols = grid.GetNumberCols()
            widths = [grid.GetColSize(i) for i in range(cols)]
            width = widths[frm]
            del widths[frm]
            to -= frm < to
            widths.insert(to, width)

            grid.BeginBatch()

            grid.ProcessTableMessage(
                GridTableMessage(
                    self._grid_table_base,
                    GRIDTABLE_NOTIFY_COLS_DELETED,
                    frm,
                    1,
                )
            )

            grid.ProcessTableMessage(
                GridTableMessage(
                    self._grid_table_base,
                    GRIDTABLE_NOTIFY_COLS_INSERTED,
                    to,
                    1,
                )
            )

            [grid.SetColSize(i, widths[i]) for i in range(min(frm, to), cols)]

            grid.EndBatch()

    def _on_row_move(self, evt):
        """ Called when a row move is taking place.
        """
        self._ignore = True

        # Get the column being moved:
        frm = evt.GetMoveRow()

        # Get the column to insert it before:
        to = evt.GetBeforeRow()

        # Tell the model to update its rows:
        if self.model._move_row(frm, to):

            # Notify the grid:
            grid = self._grid
            rows = grid.GetNumberRows()
            heights = [grid.GetRowSize(i) for i in range(rows)]
            height = heights[frm]
            del heights[frm]
            to -= frm < to
            heights.insert(to, height)

            grid.BeginBatch()

            grid.ProcessTableMessage(
                GridTableMessage(
                    self._grid_table_base,
                    GRIDTABLE_NOTIFY_ROWS_DELETED,
                    frm,
                    1,
                )
            )

            grid.ProcessTableMessage(
                GridTableMessage(
                    self._grid_table_base,
                    GRIDTABLE_NOTIFY_ROWS_INSERTED,
                    to,
                    1,
                )
            )

            [grid.SetRowSize(i, heights[i]) for i in range(min(frm, to), rows)]

            grid.EndBatch()

    # ------------------------------------------------------------------------
    # PythonDropTarget interface.
    # ------------------------------------------------------------------------
    def wx_dropped_on(self, x, y, drag_object, drag_result):

        # first resolve the x/y coords into a grid row/col
        row, col = self.__resolve_grid_coords(x, y)

        result = wx.DragNone
        if row != -1 and col != -1:
            # now ask the model if the target cell can accept this object
            valid_target = self.model.is_valid_cell_value(
                row, col, drag_object
            )
            # if this is a valid target then attempt to set the value
            if valid_target:
                # find the data
                data = drag_object
                # sometimes a 'node' attribute on the clipboard gets set
                # to a binding. if this happens we want to use it, otherwise
                # we want to just use the drag_object passed to us
                if (
                    hasattr(enClipboard, "node")
                    and enClipboard.node is not None
                ):
                    data = enClipboard.node

                # now make sure the value gets set in the model
                self.model.set_value(row, col, data)
                result = drag_result

        return result

    def wx_drag_over(self, x, y, drag_object, drag_result):

        # first resolve the x/y coords into a grid row/col
        row, col = self.__resolve_grid_coords(x, y)

        result = wx.DragNone
        if row != -1 and col != -1:
            # now ask the model if the target cell can accept this object
            valid_target = self.model.is_valid_cell_value(
                row, col, drag_object
            )
            if valid_target:
                result = drag_result

        return result

    # ------------------------------------------------------------------------
    # private interface.
    # ------------------------------------------------------------------------

    def __initialize_fonts(self):
        """ Initialize the label fonts. """

        self._on_default_label_font_changed()
        self._on_default_cell_font_changed()
        self._on_default_cell_text_color_changed()
        self._on_grid_line_color_changed()

        self._grid.SetColLabelAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
        self._grid.SetRowLabelAlignment(wx.ALIGN_RIGHT, wx.ALIGN_CENTRE)

    def __initialize_rows(self, model):
        """ Initialize the row headers. """

        # should we really be doing this?
        for row in range(model.get_row_count()):
            if model.is_row_read_only(row):
                attr = wx.grid.GridCellAttr()
                attr.SetReadOnly()
                # attr.SetRenderer(None)
                # attr.SetBackgroundColour('linen')
                self._grid.SetRowAttr(row, attr)

    def __initialize_columns(self, model):
        """ Initialize the column headers. """

        # should we really be doing this?
        for column in range(model.get_column_count()):
            if model.is_column_read_only(column):
                attr = wx.grid.GridCellAttr()
                attr.SetReadOnly()
                # attr.SetRenderer(None)
                # attr.SetBackgroundColour('linen')
                self._grid.SetColAttr(column, attr)

    def __initialize_counts(self, model):
        """ Initializes the row and column counts. """

        if model is not None:
            self._row_count = model.get_row_count()
        else:
            self._row_count = 0

        if model is not None:
            self._col_count = model.get_column_count()
        else:
            self._col_count = 0

    def __initialize_sort_state(self):
        """ Initializes the row and column counts. """

        self._current_sorted_col = None
        self._current_sorted_row = None
        self._col_sort_reversed = False
        self._row_sort_reversed = False

    def __initialize_style_settings(self):

        # make sure all the handlers for traits defining styles get called
        self._on_enable_lines_changed()
        self._on_read_only_changed()
        self._on_selection_mode_changed()
        self._on_column_label_height_changed()
        self._on_row_label_width_changed()
        self._on_show_column_headers_changed()
        self._on_show_row_headers_changed()
        self._on_default_cell_bg_color_changed()
        self._on_default_label_bg_color_changed()
        self._on_default_label_text_color_changed()
        self._on_selection_bg_color_changed()
        self._on_selection_text_color_changed()

    def __get_drag_value(self):
        """ Calculates the drag value based on the current selection. """
        return self.model.get_cell_drag_value(
            self._grid.GetGridCursorRow(), self._grid.GetGridCursorCol()
        )

    def __get_selection(self):
        """ Returns a list of values for the current selection. """

        rows, cols = self.__get_selected_rows_and_cols()

        if len(rows) > 0:
            rows.sort()
            value = self.model.get_rows_selection_value(rows)
        elif len(cols) > 0:
            cols.sort()
            value = self.model.get_cols_selection_value(cols)
        else:
            # our final option -- grab the cell that the cursor is currently in
            row = self._grid.GetGridCursorRow()
            col = self._grid.GetGridCursorCol()
            value = self.model.get_cell_selection_value(row, col)
            if value is not None:
                value = [value]

        if value is None:
            value = []

        return value

    def __get_selected_rows_and_cols(self):
        """ Return lists of the selected rows and the selected columns. """

        # note: because the wx grid control is so screwy, we have limited
        # the selection behavior. we only allow single cells to be selected,
        # or whole row, or whole columns.

        rows = self._grid.GetSelectedRows()
        cols = self._grid.GetSelectedCols()

        # because of wx we have to check this as well
        # note that all this malarkey is working on the assumption that
        # only entire rows or entire columns or single cells are selected.
        top_left = self._grid.GetSelectionBlockTopLeft()
        bottom_right = self._grid.GetSelectionBlockBottomRight()
        selection_mode = self._grid.GetSelectionMode()

        if selection_mode == wxGrid.SelectRows:
            # handle rows differently. figure out which rows were
            # selected. turns out that in this case, wx adds a "block"
            # per row, so we have to cycle over the list returned by
            # the GetSelectionBlock routines
            for i in range(len(top_left)):
                top_point = top_left[i]
                bottom_point = bottom_right[i]
                for row_index in range(top_point[0], bottom_point[0] + 1):
                    rows.append(row_index)
        elif selection_mode == wxGrid.SelectColumns:
            # again, in this case we know that only whole columns can be
            # selected
            for i in range(len(top_left)):
                top_point = top_left[i]
                bottom_point = bottom_right[i]
                for col_index in range(top_point[1], bottom_point[1] + 1):
                    cols.append(col_index)
        elif selection_mode == wxGrid.SelectCells:
            # this is the case where the selection_mode is cell, which also
            # allows complete columns or complete rows to be selected.

            # first find the size of the grid
            row_size = self.model.get_row_count()
            col_size = self.model.get_column_count()

            for i in range(len(top_left)):
                top_point = top_left[i]
                bottom_point = bottom_right[i]
                # precalculate whether this is a row or column select
                row_select = (
                    top_point[1] == 0 and bottom_point[1] == col_size - 1
                )
                col_select = (
                    top_point[0] == 0 and bottom_point[0] == row_size - 1
                )

                if row_select:
                    for row_index in range(top_point[0], bottom_point[0] + 1):
                        rows.append(row_index)
                if col_select:
                    for col_index in range(top_point[1], bottom_point[1] + 1):
                        cols.append(top_point[0])

        return (rows, cols)

    def __fire_selection_changed(self):
        self.selection_changed = True

    def __autosize(self):
        """ Autosize the grid with appropriate flags. """

        model = self.model
        grid = self._grid
        if grid is not None and self.autosize:
            grid.AutoSizeColumns(False)
            grid.AutoSizeRows(False)

        # Whenever we size the grid we need to take in to account any
        # explicitly set column sizes:

        grid.BeginBatch()

        dx, dy = grid.GetClientSize().Get()
        n = model.get_column_count()
        pdx = 0
        wdx = 0.0
        widths = []
        cached = getattr(self, "_cached_widths", None)
        current = [grid.GetColSize(i) for i in range(n)]
        if (cached is None) or (len(cached) != n):
            self._cached_widths = cached = [None] * n

        for i in range(n):
            cw = cached[i]
            if (
                (cw is None)
                or (-cw == current[i])
                or
                # hack: For some reason wx always seems to adjust column 0 by
                # 1 pixel from what we set it to (at least on Windows), so we
                # need to add a little fudge factor just for this column:
                ((i == 0) and (abs(current[i] + cw) <= 1))
            ):
                width = model.get_column_size(i)
                if width is None:
                    width = 0.0
                if width <= 0.0:
                    width = 0.1
                if width <= 1.0:
                    wdx += width
                    cached[i] = -1
                else:
                    width = int(width)
                    pdx += width
                    if cw is None:
                        cached[i] = width
            else:
                cached[i] = width = current[i]
                pdx += width

            widths.append(width)

        # The '-1' below adjusts for an off by 1 error in the way the wx.Grid
        # control determines whether or not it needs a horizontal scroll bar:
        adx = max(0, dx - pdx - 1)

        for i in range(n):
            width = cached[i]
            if width < 0:
                width = widths[i]
                if width <= 1.0:
                    w = max(30, int(round((adx * width) / wdx)))
                    wdx -= width
                    width = w
                    adx -= width
                    cached[i] = -w

            grid.SetColSize(i, width)

        grid.AdjustScrollbars()
        grid.EndBatch()
        grid.ForceRefresh()

    def __resolve_grid_coords(self, x, y):
        """ Resolve the specified x and y coordinates into row/col
            coordinates. Returns row, col. """

        # the x,y coordinates here are Unscrolled coordinates.
        # They must be changed to scrolled coordinates.
        x, y = self._grid.CalcUnscrolledPosition(x, y)

        # now we need to get the row and column from the grid
        # but we need to first remove the RowLabel and ColumnLabel
        # bounding boxes
        if self.show_row_headers:
            x = x - self._grid.GetGridRowLabelWindow().GetRect().width

        if self.show_column_headers:
            y = y - self._grid.GetGridColLabelWindow().GetRect().height

        return (self._grid.YToRow(y), self._grid.XToCol(x))

    def _select_rows(self, cells):
        """ Selects all of the rows specified by a list of (row,column) pairs.
        """
        # For a large set of rows, simply calling 'SelectBlock' on the Grid
        # object for each row is very inefficient, so we first make a pass over
        # all of the cells to merge them into contiguous ranges as much as
        # possible:
        sb = self._grid.SelectBlock

        # Extract the rows and sort them:
        rows = [row for row, column in cells]
        rows.sort()

        # Now find contiguous ranges of rows, and select the current range
        # whenever a break in the sequence is found:
        first = last = -999
        for row in rows:
            if row == (last + 1):
                last = row
            else:
                if first >= 0:
                    sb(first, 0, last, 0, True)
                first = last = row

        # Handle the last pending range of lines to be selected:
        if first >= 0:
            sb(first, 0, last, 0, True)


class _GridTableBase(GridTableBase):
    """ A private adapter for the underlying wx grid implementation. """

    # ------------------------------------------------------------------------
    # 'object' interface.
    # ------------------------------------------------------------------------

    def __init__(self, model, grid):
        """ Creates a new table base. """

        # Base class constructor.
        GridTableBase.__init__(self)

        # The Pyface model that provides the data.
        self.model = model
        self._grid = grid

        # hacky state variables so we can identify when rows have been
        # added or deleted.
        self._row_count = -1
        self._col_count = -1

        # caches for editors and renderers
        self._editor_cache = {}
        self._renderer_cache = {}

    def dispose(self):

        # Make sure dispose gets called on all traits editors:
        for editor in self._editor_cache.values():
            editor.dispose()
        self._editor_cache = {}

        for renderer in self._renderer_cache.values():
            renderer.dispose()
        self._renderer_cache = {}

    # ------------------------------------------------------------------------
    # 'wxGridTableBase' interface.
    # ------------------------------------------------------------------------

    def GetNumberRows(self):
        """ Return the number of rows in the model. """

        # because wx is such a wack job we have to store away the row
        # and column counts so we can figure out when rows or cols have
        # been appended or deleted. lacking a better place to do this
        # we just set the local variable every time GetNumberRows is called.
        self._row_count = self.model.get_row_count()
        return self._row_count

    def GetNumberCols(self):
        """ Return the number of columns in the model. """

        # see comment in GetNumberRows for logic here
        self._col_count = self.model.get_column_count()
        return self._col_count

    def IsEmptyCell(self, row, col):
        """ Is the specified cell empty? """

        return self.model.is_cell_empty(row, col)

    def GetValue(self, row, col):
        """ Get the value at the specified row and column. """

        return self.model.get_value(row, col)

    def SetValue(self, row, col, value):
        """ Set the value at the specified row and column. """
        return self.model.set_value(row, col, value)

    def GetRowLabelValue(self, row):
        """ Called when the grid needs to display a row label. """

        label = self.model.get_row_name(row)

        if row == self._grid._current_sorted_row:
            if self._grid._row_sort_reversed:
                if is_win32:
                    ulabel = str(label, "ascii") + "  \u00ab"
                    label = ulabel.encode("latin-1")
                else:
                    label += "  <<"
            elif is_win32:
                ulabel = str(label, "ascii") + "  \u00bb"
                label = ulabel.encode("latin-1")
            else:
                label += "  >>"

        return label

    def GetColLabelValue(self, col):
        """ Called when the grid needs to display a column label. """

        label = self.model.get_column_name(col)

        if col == self._grid._current_sorted_col:
            if self._grid._col_sort_reversed:
                if is_win32:
                    ulabel = str(label, "ascii") + "  \u00ab"
                    label = ulabel.encode("latin-1")
                else:
                    label += "  <<"
            elif is_win32:
                ulabel = str(label, "ascii") + "  \u00bb"
                label = ulabel.encode("latin-1")
            else:
                label += "  >>"

        return label

    def GetTypeName(self, row, col):
        """ Called to determine the kind of editor/renderer to use.

        This doesn't necessarily have to be the same type used natively by the
        editor/renderer if they know how to convert.

        """
        typename = None
        try:
            typename = self.model.get_type(row, col)
        except NotImplementedError:
            pass

        if typename is None:
            typename = GRID_VALUE_STRING

        return typename

    def DeleteRows(self, pos, num_rows):
        """ Called when the view is deleting rows. """

        # clear the cache
        self._clear_cache()
        return self.model.delete_rows(pos, num_rows)

    def InsertRows(self, pos, num_rows):
        """ Called when the view is inserting rows. """
        # clear the cache
        self._clear_cache()
        return self.model.insert_rows(pos, num_rows)

    def AppendRows(self, num_rows):
        """ Called when the view is inserting rows. """
        # clear the cache
        self._clear_cache()
        pos = self.model.get_row_count()
        return self.model.insert_rows(pos, num_rows)

    def DeleteCols(self, pos, num_cols):
        """ Called when the view is deleting columns. """

        # clear the cache
        self._clear_cache()
        return self.model.delete_columns(pos, num_cols)

    def InsertCols(self, pos, num_cols):
        """ Called when the view is inserting columns. """
        # clear the cache
        self._clear_cache()
        return self.model.insert_columns(pos, num_cols)

    def AppendCols(self, num_cols):
        """ Called when the view is inserting columns. """
        # clear the cache
        self._clear_cache()
        pos = self.model.get_column_count()
        return self.model.insert_columns(pos, num_cols)

    def GetAttr(self, row, col, kind):
        """ Retrieve the cell attribute object for the specified cell. """

        result = GridCellAttr()
        # we only handle cell requests, for other delegate to the supa
        if kind != GridCellAttr.Cell and kind != GridCellAttr.Any:
            return result

        rows = self.model.get_row_count()
        cols = self.model.get_column_count()

        # First look in the cache for the editor:
        editor = self._editor_cache.get((row, col))
        if editor is None:
            if (row >= rows) or (col >= cols):
                editor = DummyGridCellEditor()
            else:
                # Ask the underlying model for an editor for this cell:
                editor = self.model.get_cell_editor(row, col)
                if editor is not None:
                    self._editor_cache[(row, col)] = editor
                    editor._grid_info = (self._grid._grid, row, col)

        if False:  # editor is not None:
            # Note: We have to increment the reference to keep the
            #       underlying code from destroying our object.
            editor.IncRef()
            result.SetEditor(editor)

        # try to find a renderer for this cell
        renderer = None
        if row < rows and col < cols:
            renderer = self.model.get_cell_renderer(row, col)

        if renderer is not None and renderer.renderer is not None:
            renderer.renderer.IncRef()
            result.SetRenderer(renderer.renderer)

        # look to see if this cell is editable
        read_only = False
        if row < rows and col < cols:
            read_only = (
                self.model.is_cell_read_only(row, col)
                or self.model.is_row_read_only(row)
                or self.model.is_column_read_only(col)
            )

        result.SetReadOnly(read_only)
        if read_only:
            read_only_color = self._grid.default_cell_read_only_color
            if (
                read_only_color is not None
                and read_only_color is not Undefined
            ):
                result.SetBackgroundColour(read_only_color)

        # check to see if colors or fonts are specified for this cell
        bgcolor = None
        if row < rows and col < cols:
            bgcolor = self.model.get_cell_bg_color(row, col)
        else:
            bgcolor = self._grid.default_cell_bg_color

        if bgcolor is not None:
            result.SetBackgroundColour(bgcolor)

        text_color = None
        if row < rows and col < cols:
            text_color = self.model.get_cell_text_color(row, col)
        else:
            text_color = self._grid.default_cell_text_color
        if text_color is not None:
            result.SetTextColour(text_color)

        cell_font = None
        if row < rows and col < cols:
            cell_font = self.model.get_cell_font(row, col)
        else:
            cell_font = self._grid.default_cell_font
        if cell_font is not None:
            result.SetFont(cell_font)

        # check for alignment definition for this cell
        halignment = valignment = None
        if row < rows and col < cols:
            halignment = self.model.get_cell_halignment(row, col)
            valignment = self.model.get_cell_valignment(row, col)
        if halignment is not None and valignment is not None:
            if halignment == "center":
                h = wx.ALIGN_CENTRE
            elif halignment == "right":
                h = wx.ALIGN_RIGHT
            else:
                h = wx.ALIGN_LEFT

            if valignment == "top":
                v = wx.ALIGN_TOP
            elif valignment == "bottom":
                v = wx.ALIGN_BOTTOM
            else:
                v = wx.ALIGN_CENTRE

            result.SetAlignment(h, v)

        return result

    # ------------------------------------------------------------------------
    # private interface.
    # ------------------------------------------------------------------------
    def _clear_cache(self):
        """ Clean out the editor/renderer cache. """

        # Dispose of the editors in the cache after a brief delay, so as
        # to allow completion of the current event:
        do_later(self._editor_dispose, list(self._editor_cache.values()))

        self._editor_cache = {}
        self._renderer_cache = {}

    def _editor_dispose(self, editors):
        for editor in editors:
            editor.dispose()


from wx.grid import GridCellEditor


class DummyGridCellEditor(GridCellEditor):
    def Show(self, show, attr):
        return
