#------------------------------------------------------------------------------
#
#  Copyright (c) 2006, Enthought, Inc.
#  All rights reserved.
#
#  This software is provided without warranty under the terms of the BSD
#  license included in enthought/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!
#
#  Author: David C. Morrill
#  Date:   01/27/2006
#
#------------------------------------------------------------------------------

""" Defines a source code editor for the wxPython user interface toolkit,
    useful for tools such as debuggers.
"""

#-------------------------------------------------------------------------------
#  Imports:
#-------------------------------------------------------------------------------

import wx
import wx.stc as stc

from traits.api \
    import Str, List, Int, Event, Bool, TraitError, on_trait_change

from traits.trait_base \
    import SequenceTypes

# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward
# compatibility. The class has been moved to the
# traitsui.editors.code_editor file.
from traitsui.editors.code_editor \
    import ToolkitEditorFactory

from pyface.api \
    import PythonEditor

from pyface.util.python_stc \
    import faces

from editor \
    import Editor

from constants \
    import OKColor, ErrorColor

#-------------------------------------------------------------------------------
#  Constants:
#-------------------------------------------------------------------------------

# Marker line constants:

# Marks a marked line
MARK_MARKER = 0

# Marks a line matching the current search
SEARCH_MARKER = 1

# Marks the currently selected line
SELECTED_MARKER = 2

#-------------------------------------------------------------------------------
#  'SourceEditor' class:
#-------------------------------------------------------------------------------

class SourceEditor ( Editor ):
    """ Editor for source code, which displays a PyFace PythonEditor.
    """

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

    # The code editor is scrollable. This value overrides the default.
    scrollable = True

    # Is the editor read only?
    readonly = Bool( False )

    # The currently selected line
    selected_line = Int

    # The currently selected text
    selected_text = Str

    # The list of line numbers to mark
    mark_lines = List( Int )

    # The current line number
    line = Event

    # The current column
    column = Event

    # calltip clicked event
    calltip_clicked = Event

    # The STC lexer use
    lexer = Int

    # The lines to be dimmed
    dim_lines = List(Int)
    dim_color = Str
    _dim_style_number = Int(16) # 0-15 are reserved for the python lexer

    # The lines to have squiggles drawn under them
    squiggle_lines = List(Int)
    squiggle_color = Str

    #---------------------------------------------------------------------------
    #  Finishes initializing the editor by creating the underlying toolkit
    #  widget:
    #---------------------------------------------------------------------------

    def init ( self, parent ):
        """ Finishes initializing the editor by creating the underlying toolkit
            widget.
        """
        factory      = self.factory
        self._editor = editor  = PythonEditor( parent,
                                 show_line_numbers = factory.show_line_numbers )
        self.control = control = editor.control

        # There are a number of events which aren't well documented that look
        # to be useful in future implmentations, below are a subset of the
        # events that look interesting:
        #    EVT_STC_AUTOCOMP_SELECTION
        #    EVT_STC_HOTSPOT_CLICK
        #    EVT_STC_HOTSPOT_DCLICK
        #    EVT_STC_DOUBLECLICK
        #    EVT_STC_MARGINCLICK

        control.SetSize( wx.Size( 300, 124 ) )

        # Clear out the goofy hotkeys for zooming text
        control.CmdKeyClear(ord('B'), stc.STC_SCMOD_CTRL)
        control.CmdKeyClear(ord('N'), stc.STC_SCMOD_CTRL)

        # Set up the events
        wx.EVT_KILL_FOCUS( control, self.wx_update_object )
        stc.EVT_STC_CALLTIP_CLICK( control, control.GetId(),
                                   self._calltip_clicked )

        if factory.auto_scroll and (factory.selected_line != ''):
            wx.EVT_SIZE( control, self._update_selected_line )

        if factory.auto_set:
            editor.on_trait_change( self.update_object, 'changed',
                                    dispatch = 'ui' )

        if factory.key_bindings is not None:
            editor.on_trait_change( self.key_pressed, 'key_pressed',
                                    dispatch = 'ui' )

        if self.readonly:
            control.SetReadOnly( True )

        # Set up the lexer
        control.SetLexer(stc.STC_LEX_CONTAINER)
        control.Bind(stc.EVT_STC_STYLENEEDED, self._style_needed)
        try:
            self.lexer = getattr(stc, 'STC_LEX_' + self.factory.lexer.upper())
        except AttributeError:
            self.lexer = stc.STC_LEX_NULL

        # Define the markers we use:
        control.MarkerDefine( MARK_MARKER, stc.STC_MARK_BACKGROUND,
                              background = factory.mark_color_ )
        control.MarkerDefine( SEARCH_MARKER, stc.STC_MARK_BACKGROUND,
                              background = factory.search_color_ )
        control.MarkerDefine( SELECTED_MARKER, stc.STC_MARK_BACKGROUND,
                              background = factory.selected_color_ )

        # Make sure the editor has been initialized:
        self.update_editor()

        # Set up any event listeners:
        self.sync_value( factory.mark_lines, 'mark_lines', 'from',
                         is_list = True )
        self.sync_value( factory.selected_line, 'selected_line', 'from' )
        self.sync_value( factory.selected_text, 'selected_text', 'to' )
        self.sync_value( factory.line, 'line' )
        self.sync_value( factory.column, 'column' )
        self.sync_value( factory.calltip_clicked, 'calltip_clicked')

        self.sync_value(factory.dim_lines, 'dim_lines', 'from', is_list=True)
        if self.factory.dim_color == '':
            self.dim_color = 'dark grey'
        else:
            self.sync_value(factory.dim_color, 'dim_color', 'from')

        self.sync_value(factory.squiggle_lines, 'squiggle_lines', 'from',
                        is_list=True)
        if factory.squiggle_color == '':
            self.squiggle_color = 'red'
        else:
            self.sync_value(factory.squiggle_color, 'squiggle_color', 'from')

        # Check if we need to monitor the line or column position being changed:
        if (factory.line != '') or (factory.column != '') or \
                (factory.selected_text != ''):
            stc.EVT_STC_UPDATEUI( control, control.GetId(),
                                  self._position_changed )
        self.set_tooltip()

    #---------------------------------------------------------------------------
    #  Handles the user entering input data in the edit control:
    #---------------------------------------------------------------------------

    def wx_update_object ( self, event ):
        """ Handles the user entering input data in the edit control.
        """
        self.update_object()
        event.Skip()

    def update_object ( self ):
        """ Handles the user entering input data in the edit control.
        """
        if not self._locked:
            try:
                value = self.control.GetText()
                if isinstance( self.value, SequenceTypes ):
                    value = value.split()
                self.value = value
                self.control.SetBackgroundColour( OKColor )
                self.control.Refresh()
            except TraitError, excp:
                pass

    #---------------------------------------------------------------------------
    #  Updates the editor when the object trait changes external to the editor:
    #---------------------------------------------------------------------------

    def update_editor ( self ):
        """ Updates the editor when the object trait changes externally to the
            editor.
        """
        self._locked = True
        new_value    = self.value
        if isinstance( new_value, SequenceTypes ):
            new_value = '\n'.join( [ line.rstrip() for line in new_value ] )
        control = self.control
        if control.GetText() != new_value:
            readonly = control.GetReadOnly()
            control.SetReadOnly( False )
            l1  = control.GetFirstVisibleLine()
            pos = control.GetCurrentPos()
            control.SetText( new_value )
            control.GotoPos( pos )
            control.ScrollToLine( l1 )
            control.SetReadOnly( readonly )
            self._mark_lines_changed()
            self._selected_line_changed()
            self._style_document()

        self._locked = False

    #---------------------------------------------------------------------------
    #  Handles the calltip being clicked:
    #---------------------------------------------------------------------------

    def _calltip_clicked ( self, event ):
        self.calltip_clicked = True

    #---------------------------------------------------------------------------
    #  Handles the set of 'marked lines' being changed:
    #---------------------------------------------------------------------------

    def _mark_lines_changed ( self ):
        """ Handles the set of marked lines being changed.
        """
        lines   = self.mark_lines
        control = self.control
        lc      = control.GetLineCount()
        control.MarkerDeleteAll( MARK_MARKER )

        for line in lines:
            if 0 < line <= lc:
                control.MarkerAdd( line - 1, MARK_MARKER )

        control.Refresh()

    def _mark_lines_items_changed ( self ):
        self._mark_lines_changed()

    #---------------------------------------------------------------------------
    #  Handles the currently 'selected line' being changed:
    #---------------------------------------------------------------------------

    def _selected_line_changed ( self ):
        """ Handles a change in which line is currently selected.
        """
        line    = self.selected_line
        control = self.control
        line    = max( 1, min( control.GetLineCount(), line ) ) - 1
        control.MarkerDeleteAll( SELECTED_MARKER )
        control.MarkerAdd( line, SELECTED_MARKER )
        control.GotoLine( line )
        if self.factory.auto_scroll:
            control.ScrollToLine( line - (control.LinesOnScreen() / 2) )

        control.Refresh()

    #---------------------------------------------------------------------------
    #  Handles the 'line' trait being changed:
    #---------------------------------------------------------------------------

    def _line_changed ( self, line ):
        if not self._locked:
            self.control.GotoLine( line - 1 )

    #---------------------------------------------------------------------------
    #  Handles the 'column' trait being changed:
    #---------------------------------------------------------------------------

    def _column_changed ( self, column ):
        if not self._locked:
            control = self.control
            line    = control.LineFromPosition( control.GetCurrentPos() )
            control.GotoPos( control.PositionFromLine( line ) + column - 1 )

    #---------------------------------------------------------------------------
    #  Handles the cursor position being changed:
    #---------------------------------------------------------------------------

    def _position_changed ( self, event ):
        """ Handles the cursor position being changed.
        """
        control      = self.control
        pos          = control.GetCurrentPos()
        line         = control.LineFromPosition( pos )
        self._locked = True
        self.line    = line + 1
        self.column  = pos - control.PositionFromLine( line ) + 1
        self._locked = False
        self.selected_text = control.GetSelectedText()

    #---------------------------------------------------------------------------
    #  Handles a key being pressed within the editor:
    #---------------------------------------------------------------------------

    def key_pressed ( self, event ):
        """ Handles a key being pressed within the editor.
        """
        self.factory.key_bindings.do( event.event, self.ui.handler,
                                      self.ui.info )

    #---------------------------------------------------------------------------
    #  Handles the styling of the editor:
    #---------------------------------------------------------------------------

    def _dim_color_changed(self):
        self.control.StyleSetForeground(self._dim_style_number, self.dim_color)
        self.control.StyleSetFaceName(self._dim_style_number, "courier new")
        self.control.StyleSetSize(self._dim_style_number, faces['size'])
        self.control.Refresh()

    def _squiggle_color_changed(self):
        self.control.IndicatorSetStyle(2, stc.STC_INDIC_SQUIGGLE)
        self.control.IndicatorSetForeground(2, self.squiggle_color)
        self.control.Refresh()

    @on_trait_change('dim_lines, squiggle_lines')
    def _style_document(self):
        """ Force the STC to fire a STC_STYLENEEDED event for the entire
            document.
        """
        self.control.ClearDocumentStyle()
        self.control.Colourise(0, -1)
        self.control.Refresh()

    def _style_needed(self, event):
        """ Handles an STC request for styling for some area.
        """
        position = self.control.GetEndStyled()
        start_line = self.control.LineFromPosition(position)
        end = event.GetPosition()
        end_line = self.control.LineFromPosition(end)

        # Fixes a strange a bug with the STC widget where creating a new line
        # after a dimmed line causes it to mysteriously lose its styling
        if start_line in self.dim_lines:
            start_line -= 1

        # Trying to Colourise only the lines that we want does not seem to work
        # so we do the whole area and then override the styling on certain lines
        if self.lexer != stc.STC_LEX_NULL:
            self.control.SetLexer(self.lexer)
            self.control.Colourise(position, end)
            self.control.SetLexer(stc.STC_LEX_CONTAINER)

        for line in xrange(start_line, end_line+1):
            # We don't use LineLength here because it includes newline
            # characters. Styling these leads to strange behavior.
            position = self.control.PositionFromLine(line)
            style_length = self.control.GetLineEndPosition(line) - position

            if line+1 in self.dim_lines:
                # Set styling mask to only style text bits, not indicator bits
                self.control.StartStyling(position, 0x1f)
                self.control.SetStyling(style_length, self._dim_style_number)
            elif self.lexer == stc.STC_LEX_NULL:
                self.control.StartStyling(position, 0x1f)
                self.control.SetStyling(style_length, stc.STC_STYLE_DEFAULT)

            if line+1 in self.squiggle_lines:
                self.control.StartStyling(position, stc.STC_INDIC2_MASK)
                self.control.SetStyling(style_length, stc.STC_INDIC2_MASK)
            else:
                self.control.StartStyling(position, stc.STC_INDIC2_MASK)
                self.control.SetStyling(style_length, stc.STC_STYLE_DEFAULT)

    #---------------------------------------------------------------------------
    #  Handles an error that occurs while setting the object's trait value:
    #---------------------------------------------------------------------------

    def error ( self, excp ):
        """ Handles an error that occurs while setting the object's trait value.
        """
        self.control.SetBackgroundColour( ErrorColor )
        self.control.Refresh()

    #---------------------------------------------------------------------------
    #  Disposes of the contents of an editor:
    #---------------------------------------------------------------------------

    def dispose ( self ):
        """ Disposes of the contents of an editor.
        """
        if self.factory.auto_set:
            self._editor.on_trait_change( self.update_object, 'changed',
                                          remove = True )
        if self.factory.key_bindings is not None:
            self._editor.on_trait_change( self.key_pressed, 'key_pressed',
                                          remove = True )

        wx.EVT_KILL_FOCUS( self.control, None )

        super( SourceEditor, self ).dispose()

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

    #---------------------------------------------------------------------------
    #  Restores any saved user preference information associated with the
    #  editor:
    #---------------------------------------------------------------------------

    def restore_prefs ( self, prefs ):
        """ Restores any saved user preference information associated with the
            editor.
        """
        if self.factory.key_bindings is not None:
            key_bindings = prefs.get( 'key_bindings' )
            if key_bindings is not None:
                self.factory.key_bindings.merge( key_bindings )

    #---------------------------------------------------------------------------
    #  Returns any user preference information associated with the editor:
    #---------------------------------------------------------------------------

    def save_prefs ( self ):
        """ Returns any user preference information associated with the editor.
        """
        return { 'key_bindings': self.factory.key_bindings }

# Define the simple, custom, text and readonly editors, which will be accessed
# by the editor factory for code editors.

CustomEditor = SimpleEditor = TextEditor = SourceEditor

class ReadonlyEditor(SourceEditor):

    # Set the value of the readonly trait.
    readonly = True

### EOF ########################################################################
