# GNU Enterprise Forms - Curses UI Driver - Entry Widget
#
# Copyright 2000-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: entry.py 9956 2009-10-11 18:54:57Z reinhard $

import curses
import sets

from gnue.forms.uidrivers.curses.widgets import _base

__all__ = ['UIEntry']

# =============================================================================
# Entry class
# =============================================================================

class UIEntry(_base.UIHelper):
    """
    Interface implementation for <entry> widgets.
    """

    # -------------------------------------------------------------------------
    # Widget creation
    # -------------------------------------------------------------------------

    def _create_widget_(self, event, spacer):

        _base.UIHelper._create_widget_(self, event, spacer)
        
        style = self._gfObject.style.lower()
        build = getattr(self, '_UIEntry__build_%s' % style, None)
        if not build:
            build = self.__build_default

        return build(spacer * (self._gfObject._gap + 1))


    # -------------------------------------------------------------------------
    # Create the various entry widgets
    # -------------------------------------------------------------------------

    def __build_default(self, row_offset, password=False):

        return TextEntry(self, row_offset, password)

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

    def __build_password(self, row_offset):

        return self.__build_default(row_offset, True)

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

    def __build_multiline(self, row_offset):

        return MultiLineTextEntry(self, row_offset)

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

    def __build_dropdown(self, row_offset):

        return DropDownEntry(self, row_offset)

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

    def __build_checkbox(self, row_offset):

        return Checkbox(self, row_offset)

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

    def __build_label(self, row_offset):

        return StaticText(self, row_offset)

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

    def __build_listbox(self, row_offset):

        return ListBoxEntry(self, row_offset)


    # -------------------------------------------------------------------------
    # Enable/disable this entry
    # -------------------------------------------------------------------------

    def _ui_enable_(self, index):

        self.widgets[index]._ui_enable_()

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

    def _ui_disable_(self, index):

        self.widgets[index]._ui_disable_()


    # -------------------------------------------------------------------------
    # Set "editable" status for this widget
    # -------------------------------------------------------------------------

    def _ui_set_editable_(self, index, editable):

        # Not much we can do here.
        pass


    # -------------------------------------------------------------------------
    # Set value for entry
    # -------------------------------------------------------------------------

    def _ui_set_value_(self, index, value):

        self._call_widget_(index, '_ui_set_value_', value)


    # -------------------------------------------------------------------------
    # Update the list of choices
    # -------------------------------------------------------------------------

    def _ui_set_choices_(self, index, choices):

        self._call_widget_(index, '_ui_set_choices_', choices)


    # -------------------------------------------------------------------------
    # Set cursor position
    # -------------------------------------------------------------------------

    def _ui_set_cursor_position_(self, index, position):

        # If the UI is not ready for painting, remember the requested position
        # for later restoring
        if not self.ready():
            self.last_position = position
        else:
            self.last_position = None
            self._call_widget_(index, '_ui_set_cursor_position_', position)


    # -------------------------------------------------------------------------
    # Set start and end of selection area
    # -------------------------------------------------------------------------

    def _ui_set_selected_area_(self, index, selection1, selection2):

        # If the UI is not ready for painting, remember the requested selection
        # for later restoring
        if not self.ready():
            self.last_selection = (selection1, selection2)
        else:
            self.last_selection = None
            self._call_widget_(index, '_ui_set_selected_area_', selection1,
                selection2)

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

    def _ui_cut_(self, index):
        
        self._call_widget_(index, '_ui_cut_')

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

    def _ui_copy_(self, index):

        self._call_widget_(index, '_ui_copy_')

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

    def _ui_paste_(self, index):
        
        self._call_widget_(index, '_ui_paste_')

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

    def _ui_select_all_(self, index):
        
        self._call_widget_(index, '_ui_select_all_')


    # -------------------------------------------------------------------------
    # Set the size for this entry widget and repaint all controls
    # -------------------------------------------------------------------------

    def set_size_and_fit(self, width, height):

        self.width = width
        self.height = height

        count = min(len(self.widgets), getattr(self._gfObject, '_rows', 0))
        for index in range(count):
            self.widgets[index]._repaint_()


    # -------------------------------------------------------------------------
    # Get the size hints for an entry
    # -------------------------------------------------------------------------

    def get_size_hints(self, vertical=None):

        label = ''
        if self._gfObject.has_label:
            label = getattr(self._gfObject, 'label', '')

        # Only stretch entries if they're in a horizontal container or if they
        # are multiline edits
        if not vertical or self._gfObject.style == 'multiline':
            stretch = self.stretch
        else:
            stretch = 0

        return (self.min_width or 20, self.min_height or 1, len(label), stretch)


# =============================================================================
# Base Entry Widget
# =============================================================================

class BaseEntry(object):
    """
    The base class for widget implementations
    """

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

    def __init__(self, entry, row_offset):

        self.entry = entry
        self.row_offset = row_offset
        self.value = None
        self.choices = []
        self.has_focus = False
        self.enabled = True


    # -------------------------------------------------------------------------
    # Virtual Methods
    # -------------------------------------------------------------------------

    def _repaint_(self):
        """
        Descendants must implement this method to draw the widget onto it's
        parent
        """
        pass


    # -------------------------------------------------------------------------
    # Event Handler
    # -------------------------------------------------------------------------

    def _ui_set_value_(self, value):

        self.value = value
        self._repaint_()

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

    def _ui_set_choices_(self, choices):

        self.choices = choices
        self._repaint_()

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

    def _ui_focus_in_(self):

        self.has_focus = True
        self._repaint_()

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

    def _ui_focus_out_(self):

        self.has_focus = False
        self._repaint_()

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

    def _ui_enable_(self):

        self.enabled = True
        self._repaint_()

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

    def _ui_disable_(self):

        self.enabled = False
        self._repaint_()


    # -------------------------------------------------------------------------
    # Properties
    # -------------------------------------------------------------------------

    def __get_top(self):
        return self.entry.top + self.row_offset

    top = property(__get_top, None, None, """ Get the upper left row """)

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

    def __get_left(self):
        return self.entry.left

    left = property(__get_left, None, None, """ The upper left colum """)

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

    def __get_width(self):
        return self.entry.width

    width = property(__get_width, None, None, """ The widget width """)

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

    def __get_height(self):
        return self.entry.height

    height = property(__get_height, None, None, """ The widget height """)

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

    def __get_attr(self):

        if not self.enabled:
            attr = 'disabled'

        elif self.has_focus:
            attr = 'focusentry'
        else:
            attr = 'entry'

        return self.entry._uiDriver.attr[attr]

    attribute = property(__get_attr, None, None,
            """ The current attribute to use for this widget """)

# =============================================================================
# Static Text style entry
# =============================================================================

class StaticText(BaseEntry):
    """
    A static text entry is an read-only entry.
    """

    # -------------------------------------------------------------------------
    # Draw the static text entry
    # -------------------------------------------------------------------------

    def _repaint_(self):

        text = ("%s" % self.value)[:self.width]
        text += ' ' * (self.width - len(text))
        self.entry._parent.write(self.left, self.top, text,
                self.entry._uiDriver.attr['background'])



# =============================================================================
# Single line text entry
# =============================================================================

class TextEntry(BaseEntry):
    """
    Entry widget implementing single line text or password entries.
    """

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

    def __init__(self, entry, row_offset, password=False):

        BaseEntry.__init__(self, entry, row_offset)
        self.password = password
        self.selection = None
        self.offset = 0


    # -------------------------------------------------------------------------
    # UI event handler
    # -------------------------------------------------------------------------

    def _ui_set_cursor_position_(self, position):
        """
        Set the cursor position.  An already existing selection gets
        unselected.
        """

        # If there is a selection atm, we have to unselected it.  Thus we need
        # a repaint
        need_repaint = self.selection is not None
        self.selection = None

        if not self.entry.ready():
            return

        pos = position - self.offset
        if pos > self.width-1:
            self.offset = position - self.width + 1
            pos = self.width-1
            need_repaint = True

        elif pos < 0:
            self.offset += pos
            pos = 0
            need_repaint = True

        if need_repaint:
            self._repaint_()

        if self.has_focus:
            self.entry._parent.move(self.left + pos, self.top)


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

    def _ui_set_selected_area_(self, selection1, selection2):
        """
        Set the current selection and repaint the widget
        """

        if selection1 == selection2:
            self.selection = None
        else:
            self.selection = (selection1, selection2)
        self._repaint_()
        
    # -------------------------------------------------------------------------

    def _ui_select_all_(self):

        if self.value:
            self.entry._request('SELECTWITHMOUSE', position1=0,
                    position2=len(self.value))

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

    def _ui_focus_out_(self):
        """
        On leaving a widget make sure to remove a selection as well as reset
        the offset.
        """

        self.selection = None
        self.offset = 0

        BaseEntry._ui_focus_out_(self)



    # -------------------------------------------------------------------------
    # Implementation Virtual methods
    # -------------------------------------------------------------------------

    def _repaint_(self):
        """
        Draw the text entry widget
        """

        if self.password:
            vtext = '*' * len(self.value or '')
        else:
            vtext = self.value or ''
        text = vtext[self.offset:self.offset + self.width]
        text += ' ' * (self.width - len(text))

        if self.selection:
            (sel1, sel2) = self.selection
            self.entry._parent.write(self.left, self.top, text[:sel1],
                    self.attribute)
            self.entry._parent.write(self.left + sel1, self.top,
                    text[sel1:sel2], self.attribute + curses.A_STANDOUT)
            self.entry._parent.write(self.left + sel2, self.top, text[sel2:],
                    self.attribute)
        else:
            self.entry._parent.write(self.left, self.top, text, self.attribute)


# =============================================================================
# Multiline text entry
# =============================================================================

class MultiLineTextEntry(TextEntry):

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

    def __init__(self, entry, row_offset):

        BaseEntry.__init__(self, entry, row_offset)

        self.selection = None
        self.__vx = self.__vy = 0            # Virtual cursor position
        self.__hoffs = self.__voffs = 0      # Offset of the top/left cell


    # -------------------------------------------------------------------------
    # Draw the widget
    # -------------------------------------------------------------------------

    def _repaint_(self):
        
        data = [''.ljust(self.width)] * self.height
        text = self.value or ''

        selected = {}
        stream = ''

        if self.selection is not None:
            sel1, sel2 = self.selection
            sel_set = sets.Set(range(sel1, sel2+1))

        for (row, line) in enumerate(text.splitlines()):
            if self.selection is not None:
                left = len(stream)
                right = left + len(line)
                current = sets.Set(range(left, right))
                match = sel_set.intersection(current)
                if match:
                    mins = min(match) - left - self.__hoffs
                    maxs = min(max(match) - left - self.__hoffs+1, self.width)
                    if maxs != len(match):
                        maxs -= 1

                    selected[row] = (mins, maxs)

            stream += line + '\n'

            vrow = row - self.__voffs
            if (vrow >= 0) and (vrow < self.height):
                data[vrow] = line[self.__hoffs:].ljust(self.width)[:self.width]

        for (row, line) in enumerate(data):
            vrow = row + self.__voffs
            if vrow in selected:
                sel1, sel2 = selected[vrow]
                self.entry._parent.write(self.left, self.top + row,
                        line[:sel1], self.attribute)
                self.entry._parent.write(self.left + sel1, self.top + row,
                        line[sel1:sel2], self.attribute + curses.A_STANDOUT)
                self.entry._parent.write(self.left + sel2, self.top + row,
                        line[sel2:], self.attribute)
            else:
                self.entry._parent.write(self.left, self.top + row, line,
                    self.attribute)


    # -------------------------------------------------------------------------
    # UI event handler
    # -------------------------------------------------------------------------

    def _ui_set_cursor_position_(self, position):
        
        # If there is a selection atm, we have to unselected it.  Thus we need
        # a repaint
        need_repaint = self.selection is not None
        self.selection = None

        if not self.entry.ready():
            return

        (row, col) = self.__position_to_coord(position)
        self.__vx = col
        self.__vy = row

        delta_x = col - self.__hoffs
        delta_y = row - self.__voffs

        if (delta_x) > self.width - 1:
            self.__hoffs = col - self.width + 1
            col = self.width - 1
            need_repaint = True

        elif (delta_x) < 0:
            self.__hoffs += delta_x
            col = 0
            need_repaint = True
        else:
            col -= self.__hoffs

        if delta_y > self.height - 1:
            self.__voffs = row - self.height + 1
            row = self.height - 1
            need_repaint = True

        elif delta_y < 0:
            self.__voffs += delta_y
            row = 0
            need_repaint = True
        else:
            row -= self.__voffs

        if need_repaint:
            self._repaint_()

        if self.has_focus:
            self.entry._parent.move(self.left + col, self.top + row)

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

    def _ui_set_selected_area_(self, selection1, selection2):
        """
        Set the current selection and repaint the widget
        """

        if selection1 == selection2:
            self.selection = None
        else:
            self.selection = (selection1, selection2)

        self._repaint_()


    # -------------------------------------------------------------------------
    # Check for navigation keys within text pad
    # -------------------------------------------------------------------------

    def _fkeypress(self, key, shift, ctrl, meta):
        
        result = False

        if key == curses.KEY_UP and self.__vy > 0:
            lines = (self.value or '').splitlines()[:self.__vy]
            if lines:
                last = lines[-1]
                if len(last) > self.__vx:
                    lines[-1] = last[:self.__vx]

            self.entry._request('CURSORMOVE', position = len('\n'.join(lines)))
            result = True

        elif key == curses.KEY_DOWN:
            lines = (self.value or '').splitlines()
            if self.__vy < len(lines):
                lines = lines[:self.__vy+2]
                last = lines[-1]

                if len(last) > self.__vx:
                    lines[-1] = last[:self.__vx]

                self.entry._request('CURSORMOVE',
                        position = len('\n'.join(lines)))
                result = True

        return result


    # -------------------------------------------------------------------------
    # Map a GF position into a row/column tuple suitable for cursor positioning
    # -------------------------------------------------------------------------

    def __position_to_coord(self, position):

        part = (self.value or '')[:position]
        paragraph = part.count('\n')
        offset = position
        if paragraph > 0:
            offset = len(part.split('\n')[-1])

        return (paragraph, offset)



# =============================================================================
# Drodown Entry
# =============================================================================

class DropDownEntry(TextEntry):
    """
    A dropdown entry extends a single line text entry since it has a list of
    allowed values from which the user can choose one.
    """

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

    def __init__(self, entry, row_offset):

        self.__mapping = {}
        TextEntry.__init__(self, entry, row_offset)


    # -------------------------------------------------------------------------
    # UI Event handler
    # -------------------------------------------------------------------------

    def _ui_set_choices_(self, choices):

        TextEntry._ui_set_choices_(self, choices)

        self.__mapping = {}
        for item in self.choices:
            self.__mapping[item] = item


    # -------------------------------------------------------------------------
    # Keypress handler
    # -------------------------------------------------------------------------

    def _keypress(self, key):
        """
        If the key is the lookup key a selection dialog will be displayed.  If
        the user selects an option a REPLACEVALUE event is generated.
        Otherwise the key gets passed back and it should be handled by the
        entry itself.

        @returns: True if the key event has been handled, False otherwise.
        """

        if key == chr(self.entry._uiDriver.lookupKey):
            # We can handle the lookup key here
            choice = self.entry._uiDriver.getOption(u_("Select option"),
                    self.__mapping)
            if choice is not None:
                self.entry._request('REPLACEVALUE', text=choice)

            result = True
        else:
            result = False

        return result


# =============================================================================
# Checkbox widget
# =============================================================================

class Checkbox(BaseEntry):

    # -------------------------------------------------------------------------
    # UI event handler
    # -------------------------------------------------------------------------

    def _ui_focus_in_(self):

        BaseEntry._ui_focus_in_(self)
        self.entry._parent.move(self.left + 1, self.top)


    # -------------------------------------------------------------------------
    # Draw the checkbox in it's current state
    # -------------------------------------------------------------------------

    def _repaint_(self):

        label = '    %s' % self.entry._gfObject.label
        text = label[:self.width]
        text += ' ' * (self.width - len(text))
        self.entry._parent.write(self.left, self.top, text,
                self.entry._uiDriver.attr['background'])

        if self.value is None:
            text = '[-]'
        elif self.value:
            text = '[X]'
        else:
            text = '[ ]'

        self.entry._parent.write(self.left, self.top, text, self.attribute)


# =============================================================================
# A ListBox entry
# =============================================================================

class ListBoxEntry(BaseEntry):

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

    def __init__(self, entry, row_offset):

        BaseEntry.__init__(self, entry, row_offset)
        self.offset = 0
        self.selected = None
        self.display = None
        self.__old_cursor = None


    # -------------------------------------------------------------------------
    # UI event handler
    # -------------------------------------------------------------------------

    def _ui_set_value_(self, value):

        if value <> self.value:
            # If the value has changed from outside (not due to a call of our
            # __move method), we start off with a fresh display-offset.  This
            # means the newly set value will be the first row displayed in the
            # listbox widget.
            if value in self.choices:
                self.selected = self.offset = self.choices.index(value)
            else:
                self.selected = self.offset = 0

            self.display = 1

        BaseEntry._ui_set_value_(self, value)


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

    def _ui_focus_in_(self):

        self.__old_cursor = curses.curs_set(0)
        BaseEntry._ui_focus_in_(self)

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

    def _ui_focus_out_(self):

        curses.curs_set(self.__old_cursor)
        BaseEntry._ui_focus_out_(self)


    # -------------------------------------------------------------------------
    # Draw the listbox widget
    # -------------------------------------------------------------------------

    def _repaint_(self):
        
        lines = self.choices[self.offset:self.offset + self.height]
        if len(lines) < self.height:
            lines.extend([''] * (self.height - len(lines)))

        for (row, value) in enumerate(lines):
            text = value.ljust(self.width)[:self.width]
            attr = self.attribute
            if self.has_focus and row == self.display-1:
                attr += curses.A_STANDOUT

            self.entry._parent.write(self.left, self.top + row, text, attr)


    # -------------------------------------------------------------------------
    # Handle the up- and down-keys here
    # -------------------------------------------------------------------------

    def _fkeypress(self, key, shift, ctrl, meta):

        if key in [curses.KEY_UP, curses.KEY_DOWN]:
            self.__move([1, -1][key == curses.KEY_UP])
            return True
        else:
            return False


    # -------------------------------------------------------------------------
    # Select the prior/next element of the listbox
    # -------------------------------------------------------------------------

    def __move(self, direction):

        self.display += direction
        self.selected += direction

        if self.display > self.height:
            if self.selected < len(self.choices):
                self.offset += direction

        elif self.display < 1:
            self.offset = max(0, self.offset - 1)

        self.selected = max(0, self.selected)
        self.selected = min(len(self.choices) - 1, self.selected)

        self.display = max(1, self.display)
        self.display = min(self.display, self.height)

        # NOTE: in order to keep the display-index accross _ui_set_value_ calls
        # we set it manually here.
        self.value = self.choices[self.selected]
        self.entry._request('REPLACEVALUE', text=self.value)


# =============================================================================
# Configuration data
# =============================================================================

configuration = {
  'baseClass'  : UIEntry,
  'provides'   : 'GFEntry',
  'container'  : 0,
}
