# GNU Enterprise Forms - GF Object Hierarchy - Navigable Objects
#
# Copyright 2001-2009 Free Software Foundation
#
# This file is part of GNU Enterprise
#
# GNU Enterprise is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation; either
# version 3, or (at your option) any later version.
#
# GNU Enterprise is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with program; see the file COPYING. If not,
# write to the Free Software Foundation, Inc., 59 Temple Place
# - Suite 330, Boston, MA 02111-1307, USA.
#
# $Id: GFTabStop.py 10044 2009-11-18 15:36:46Z reinhard $

"""
Base class for all objects that can receive the keyboard focus on the UI.
"""

from gnue.common import events
from gnue.common.definitions import GParser

from gnue.forms.input import displayHandlers
from gnue.forms.GFObjects.GFObj import GFObj

__all__ = ['GFTabStop', 'FieldNotFoundError']

# =============================================================================
# Base class for navigable controls
# =============================================================================

class GFTabStop (GFObj):
    """
    A base class for all GFObjects that can receive focus on the UI.

    @cvar _navigableInQuery_: If True the object can recieve the keyboard focus
      in query mode, otherwise not
    """

    # -------------------------------------------------------------------------
    # Attributes
    # -------------------------------------------------------------------------
  
    navigable = False

    # -------------------------------------------------------------------------
    # Class variables
    # -------------------------------------------------------------------------

    _navigableInQuery_ = True


    # -------------------------------------------------------------------------
    # Constructor
    # -------------------------------------------------------------------------

    def __init__(self, parent, object_type):

        GFObj.__init__(self, parent, object_type)

        # The sub-event handler handles the events that are passed from the
        # GFInstance. This is the event handler that display handlers 
        self.subEventHandler = events.EventController()

        self._page = None
        self.__grid_link = None

        self._rows = 1
        self._gap = 0

        self.__first_visible_record = 0
        self.__last_enabled_row = None
        self.__current_row_enabled = True


    # -------------------------------------------------------------------------
    # Implementation of virtual methods
    # -------------------------------------------------------------------------

    def _phase_1_init_(self):
        """
        On phase 1 initialization find the owning GFPage-instance.
        This instance is then available through self._page.
        """

        GFObj._phase_1_init_(self)

        self._page = self.findParentOfType('GFPage')
        self._page._entryList.append(self)

        if hasattr(self, 'grid_link') and self.grid_link:
            for grid in self._form.findChildrenOfType('GFGrid',
                    includeSelf=False, allowAllChildren=True):
                if grid.name == self.grid_link:
                    self.__grid_link = grid
                    break

        if not self.__grid_link:
            self.__grid_link = self.findParentOfType('GFGrid')

        if self.__grid_link:
            self._block = self.__grid_link.get_block()

        if self._block:
            self._block._entryList.append(self)


    # -------------------------------------------------------------------------
    # UI events (called from UIEntry/UIButton)
    # -------------------------------------------------------------------------

    def _event_set_focus(self, index):
        """
        Notify the object that the user has set the focus to this object with a
        mouse click.

        This method makes sure that the logical focus follows the physical
        focus.

        In case the current focus widget vetoes the focus loss, this method
        beats the focus back to the old widget.

        In fact, this method only calls GFForm._event_focus_changed() with a
        target of this object.
        """

        # Most UIs issue a set_focus event also when the focus moves from
        # another window to this one. We don't need to do anything in this
        # case.
        if self._form.get_focus_object() is self \
                and index == self._visibleIndex:
            return

        self._form._event_focus_changed(self, index - self._visibleIndex)

    # -------------------------------------------------------------------------

    def _event_line_up(self):
        """
        The user has pressed the cursor up key in an entry which doesn't
        otherwise handle that key.

        If this object is part of a grid, move up one record.
        """

        if self.__grid_link:
            self.__grid_link.line_up()

    # -------------------------------------------------------------------------

    def _event_line_down(self):
        """
        The user has pressed the cursor down key in an entry which doesn't
        otherwise handle that key.

        If this object is part of a grid, move down one record.
        """

        if self.__grid_link:
            self.__grid_link.line_down()

    # -------------------------------------------------------------------------

    def _event_page_up(self):
        """
        The user has pressed the page up key in an entry which doesn't
        otherwise handle that key.

        If this object is part of a grid, move up one screen page.
        """

        if self.__grid_link:
            self.__grid_link.page_up()

    # -------------------------------------------------------------------------

    def _event_page_down(self):
        """
        The user has pressed the page down key in an entry which doesn't
        otherwise handle that key.

        If this object is part of a grid, move down one screen page.
        """

        if self.__grid_link:
            self.__grid_link.page_down()


    # -------------------------------------------------------------------------
    # Number of rows of this widgets has changed
    # -------------------------------------------------------------------------

    def rows_changed(self, new_rows):
        """
        Notify the widget that the number of visible rows has changed.

        This can happen if this widget is part of a grid that has been resized.

        This function takes care about filling the new rows with data.
        """

        old_rows = self._rows
        self._rows = new_rows

        if isinstance(self, GFFieldBound) and new_rows > old_rows:
            self.refresh_ui(old_rows, new_rows - 1)


    # -------------------------------------------------------------------------
    # Recalculate the visible index of an object
    # -------------------------------------------------------------------------

    def recalculate_visible(self, adjustment, cur_record, rec_count, refresh):
        """
        Process a record pointer movement or a result set change for this
        entry.

        This function sets the C{_visibleIndex} property of this entry. It also
        takes care of disabling rows of this entry that are outside the actual
        number of available records, and it redisplays the contents of the
        entry as needed.

        @param adjustment: value to change the visible index, e.g. 1 or -1
        @param cur_record: the currently active record, or -1 if there is no
            record active currently.
        @param rec_count: the number of records available at all
        @param refresh: 'none' if no records have to be redisplayed (i.e. only
            the cursor position has changed), 'current' if all records starting
            with the current record have to be redisplayed (i.e. a new record
            has been inserted), or 'all' if all records have to be redisplayed
            (i.e. the complete resultset has changed); if the records scroll
            within this entry, everything is redisplayed anyway
        """

        if self.hidden:
            return

        if self._form.get_focus_object() is self:
            self.ui_focus_out()
        try:
            old_visible_index = self._visibleIndex

            index = min(max(self._visibleIndex + adjustment, 0),
                        int(self._rows)-1)

            # Don't let the index pass the number of records
            lowestVisible = max(cur_record - index, 0)
            if lowestVisible + index > rec_count:
                index = index -1

            # If the current record has rolled around from the top to the
            # bottom then reset the counter.
            if cur_record == 0:
                index = 0

            self._visibleIndex = index

            if self.uiWidget is not None:
                if self.__last_enabled_row is None:
                    # if running for the first time, all rows are enabled
                    # because widgets are created enabled by default
                    self.__last_enabled_row = self._rows - 1

                # Re-enable old current row if it was disabled
                if not self.__current_row_enabled:
                    self.uiWidget._ui_enable_(old_visible_index)
                    self.__current_row_enabled = True

                last_enabled_row = self._visibleIndex + \
                        (rec_count - cur_record) - 1
                last_enabled_row = min(last_enabled_row, self._rows - 1)

                # Disable rows if necessary
                for i in range(last_enabled_row+1, self.__last_enabled_row+1):
                    self.uiWidget._ui_disable_(i)

                # Enable rows if necessary
                for i in range(self.__last_enabled_row+1, last_enabled_row+1):
                    self.uiWidget._ui_enable_(i)

                self.__last_enabled_row = last_enabled_row

                # Disable current row if current record is -1
                if cur_record == -1:
                    self.uiWidget._ui_disable_(self._visibleIndex)
                    self.__current_row_enabled = False

                first_visible_record = cur_record - self._visibleIndex
                if first_visible_record != self.__first_visible_record:
                    # If we have scrolled, redisplay all records
                    refresh_start = 0
                    self.__first_visible_record = first_visible_record
                else:
                    if refresh == 'all':
                        refresh_start = 0
                    elif refresh == 'current':
                        refresh_start = max(old_visible_index, self._visibleIndex)
                    else:
                        refresh_start = None

                if isinstance(self, GFFieldBound) and refresh_start is not None:
                    self.refresh_ui(refresh_start, self._rows - 1)

                    # Set widgets to editable or non-editable
                    self.__update_editable(refresh_start)

                # Let the marker for the current row in grids follow.
                self.ui_set_current_row()

        finally:
            # If this was the currently focused widget, move the focus along
            if self._form.get_focus_object() is self:
                self.ui_focus_in()
                self.ui_set_focus()
                if hasattr(self, '_displayHandler') \
                        and self._displayHandler.editing:
                    self._displayHandler.generateRefreshEvent()


    # -------------------------------------------------------------------------
    # Set the widgets to editable or non-editable
    # -------------------------------------------------------------------------

    def update_editable(self):
        """
        Update the editable state of this entry.

        This function is called by the field if the editable state of the field
        is changed by a trigger.
        """
        self.__update_editable(0)

    # -------------------------------------------------------------------------

    def __update_editable(self, refresh_start):

        for index in range(refresh_start, self.__last_enabled_row + 1):
            offset = index - self._visibleIndex
            self.uiWidget._ui_set_editable_(index,
                    self._field.is_editable(offset))


    # -------------------------------------------------------------------------
    # Find out whether this control is navigable
    # -------------------------------------------------------------------------

    def is_navigable(self, mode):
        """
        Return True if the user should be able to tab to this control, False
        otherwise.
        """

        # Hidden control: not navigable.
        if self.hidden:
            return False

        # Current row not enabled (i.e. no records in this block): not
        # navigable.
        if not self.__current_row_enabled:
            return False

        # Attached field is not editable in current mode: not navigable.
        if isinstance(self, GFFieldBound) and not self._field.is_editable():
            return False

        # Control not navigable in query mode.
        if mode == 'query' and not self._navigableInQuery_:
            return False

        # Normal case: user setting.
        return self.navigable


    # -------------------------------------------------------------------------
    # Change the focus to this object
    # -------------------------------------------------------------------------

    def set_focus(self, index):
        """
        Move the UI and GF focus to this object.
        """

        self._form.change_focus(self, index - self._visibleIndex)


    # -------------------------------------------------------------------------
    # Focus handling
    # -------------------------------------------------------------------------

    def focus_in(self):
        """
        Notify the object that it has received the focus.
        """

        self.ui_focus_in()

        self.processTrigger('PRE-FOCUSIN')
        self.processTrigger('POST-FOCUSIN')

        # Update tip
        if self.get_option('tip'):
            tip = self.get_option('tip')
        elif isinstance(self, GFFieldBound) and self._field.get_option('tip'):
            tip = self._field.get_option('tip')
        elif hasattr(self, "_displayHandler"):
            tip = self._displayHandler.get_tip()
        else:
            tip = ""

        self._form.update_tip(tip)

    # -------------------------------------------------------------------------

    def validate(self):
        """
        Validate the object to decide whether the focus can be moved away from
        it.

        This function can raise an exception, in which case the focus change
        will be prevented.
        """

        self.processTrigger('PRE-FOCUSOUT', ignoreAbort=False)

    # -------------------------------------------------------------------------

    def focus_out(self):
        """
        Notify the object that it is going to lose the focus.

        The focus change is already decided at this moment, there is no way to
        stop the focus from changing now.
        """

        self.processTrigger('POST-FOCUSOUT')

        self.ui_focus_out()


    # -------------------------------------------------------------------------
    # UI focus movement
    # -------------------------------------------------------------------------

    def ui_set_focus(self):
        """
        Set the focus to this widget on the UI layer.

        This function is only called when the focus is set from the GF layer.
        If the user changes the focus with a mouse click, this function is not
        called because the UI focus already is on the target widget.

        So the purpose of this function is to make the UI focus follow the GF
        focus.
        """

        self.uiWidget._ui_set_focus_(self._visibleIndex)

    # -------------------------------------------------------------------------

    def ui_focus_in(self):
        """
        Notify the UI widget that is is going to receive the focus.

        This function is always called, no matter whether the user requested
        the focus change via mouse click, keypress, or trigger function.

        The purpose of this function is to allow the UI widget to do things
        that always must be done when it gets the focus, like changing the
        color of the current widget, or activating the current entry in the
        grid.
        """

        self.uiWidget._ui_focus_in_(self._visibleIndex)

    # -------------------------------------------------------------------------

    def ui_focus_out(self):
        """
        Notify the UI widget that it has lost the focus.

        This function is always called, no matter whether the user requested
        the focus change via mouse click, keypress, or trigger function.

        The purpose of this function is to allow the UI widget to do things
        that always must be done when it loses the focus, like changing the
        color of the formerly current widget back to normal, or deactivating
        the no-longer-current entry in the grid.

        This function works better than the KILL-FOCUS event of the UI, because
        KILL-FOCUS runs too often, for example also when the dropdown is opened
        (and the focus moves from the dropdown entry to the dropdown list).
        """

        self.uiWidget._ui_focus_out_(self._visibleIndex)

    # -------------------------------------------------------------------------

    def ui_set_current_row(self):
        """
        Notify the UI widget about the row on screen containing the current
        record.
        """

        if self.__current_row_enabled:
            self.uiWidget._ui_set_current_row_(self._visibleIndex)
        else:
            self.uiWidget._ui_set_current_row_(-1)


# =============================================================================
# Base class for all widgets bound to a field
# =============================================================================

class GFFieldBound(GFTabStop):

    # -------------------------------------------------------------------------
    # Phase 1 init
    # -------------------------------------------------------------------------

    def _phase_1_init_(self):

        GFTabStop._phase_1_init_(self)

        if not self.field in self._bound_block._fieldMap:
            raise FieldNotFoundError, self
        self._field = self._bound_block._fieldMap[self.field]
        self._field._entryList.append(self)

        self._formatmask  = ""
        self._inputmask   = getattr(self, 'inputmask', '')
        self._displaymask = getattr(self, 'displaymask', '')

        # Associate a display handler with this instance
        self._displayHandler = displayHandlers.factory(self,
                             self._form._instance.eventController,
                             self.subEventHandler,
                             self._displaymask,
                             self._inputmask)

        # Row settings
        grid = self.findParentOfType('GFGrid')
        if grid:
            self._rows = int(getattr(grid, 'rows', 1))
        else:
            self._rows = getattr(self, 'rows', self._field._rows)
            self._gap  = getattr(self, 'rowSpacer', self._field._gap)


    # -------------------------------------------------------------------------
    # UI events (called from UIEntry)
    # -------------------------------------------------------------------------

    def _event_jump_records(self, index):
        """
        Move the database cursor as a result of the user clicking on a part of
        the grid which can't receive the focus.

        If the user clicks on a part of the grid which can receive the focus,
        the database cursor is moved implicitly throug the set_focus magic.
        """

        self._block.jump_records(index - self._visibleIndex)


    # -------------------------------------------------------------------------
    # Get the field this object is bound to
    # -------------------------------------------------------------------------

    def get_field(self):
        """
        Returns the objects' field from the blocks' field mapping

        @returns: GFField instance or None if the object does not support fields
        @raises FieldNotFoundError: if the field is not available through the
          block
        """

        return self._field


    # -------------------------------------------------------------------------
    # Clipboard and selection
    # -------------------------------------------------------------------------

    def cut(self):

        if self.uiWidget is not None:
            self.uiWidget._ui_cut_(self._visibleIndex)

    # -------------------------------------------------------------------------

    def copy(self):

        if self.uiWidget is not None:
            self.uiWidget._ui_copy_(self._visibleIndex)

    # -------------------------------------------------------------------------

    def paste(self):

        if self.uiWidget is not None:
            self.uiWidget._ui_paste_(self._visibleIndex)

    # -------------------------------------------------------------------------

    def select_all(self):

        if self.uiWidget is not None:
            self.uiWidget._ui_select_all_(self._visibleIndex)

    # -------------------------------------------------------------------------
    # Refresh the user interface with the current field data for current row
    # -------------------------------------------------------------------------

    def refresh_ui_current(self):

        self.refresh_ui(self._visibleIndex, self._visibleIndex)


    # -------------------------------------------------------------------------
    # Refresh the user interface with the current field data for all rows
    # -------------------------------------------------------------------------

    def refresh_ui_all(self):

        self.refresh_ui(0, self._rows - 1)


    # -------------------------------------------------------------------------
    # Refresh the user interface with the current field data
    # -------------------------------------------------------------------------

    def refresh_ui(self, from_index, to_index):

        if self.hidden:
            return

        for index in range (from_index, to_index + 1):
            # Do not execute if we were editing - would overwrite unsaved change
            if not (index == self._visibleIndex \
                    and self._displayHandler.editing):
                try:
                    value = self._field.get_value(index - self._visibleIndex)
                except Exception:               # invalid value
                    value = None
                display = self._displayHandler.build_display(value, False)
                self.uiWidget._ui_set_value_(index, display)


    # -------------------------------------------------------------------------
    # Update the available list of choices for all uiWidgets
    # -------------------------------------------------------------------------

    def refresh_ui_choices(self, choices):

        for index in range(self._rows):
            self.uiWidget._ui_set_choices_(index, choices)


# =============================================================================
# Exceptions
# =============================================================================

class FieldNotFoundError(GParser.MarkupError):
    """ Element refernces non-existent field """
    def __init__(self, item):
        itemType = item._type[2:]
        msg = u_("%(item)s '%(name)s' references non-existent "
                 "field '%(field)s'") \
              % {'item': itemType, 'name': item.name, 'field': item.field}
        GParser.MarkupError.__init__(self, msg, item._url, item._lineNumber)

