#----------------------------------------------------------------------------
# Name:         TextEditor.py
# Purpose:      Text Editor for pydocview
#
# Author:       Peter Yared
#
# Created:      8/15/03
# CVS-ID:       $Id$
# Copyright:    (c) 2003-2004 ActiveGrid, Inc.
# License:      wxWindows license
#----------------------------------------------------------------------------
import wx
import wx.lib.docview
import wx.lib.pydocview
import string
import FindService
_ = wx.GetTranslation

class TextDocument(wx.lib.docview.Document):


    def OnSaveDocument(self, filename):
        view = self.GetFirstView()
        if not view.GetTextCtrl().SaveFile(filename):
            return False
        self.Modify(False)
        self.SetDocumentSaved(True)
        #if wx.Platform == "__WXMAC__":
        #    fn = wx.Filename(filename)
        #    fn.MacSetDefaultTypeAndCreator()
        return True


    def OnOpenDocument(self, filename):
        view = self.GetFirstView()
        if not view.GetTextCtrl().LoadFile(filename):
            return False
        self.SetFilename(filename, True)
        self.Modify(False)
        self.UpdateAllViews()
        self._savedYet = True
        return True


    def IsModified(self):
        view = self.GetFirstView()
        if view and view.GetTextCtrl():
            return wx.lib.docview.Document.IsModified(self) or view.GetTextCtrl().IsModified()
        else:
            return wx.lib.docview.Document.IsModified(self)


    def Modify(self, mod):
        view = self.GetFirstView()
        wx.lib.docview.Document.Modify(self, mod)
        if not mod and view and view.GetTextCtrl():
            view.GetTextCtrl().DiscardEdits()


class TextView(wx.lib.docview.View):


    #----------------------------------------------------------------------------
    # Overridden methods
    #----------------------------------------------------------------------------

    def __init__(self):
        wx.lib.docview.View.__init__(self)
        self._textCtrl = None
        self._wordWrap = wx.ConfigBase_Get().ReadInt("TextEditorWordWrap", True)


    def OnCreate(self, doc, flags):
        frame = wx.GetApp().CreateDocumentFrame(self, doc, flags)
        sizer = wx.BoxSizer()
        font, color = self._GetFontAndColorFromConfig()
        self._textCtrl = self._BuildTextCtrl(frame, font, color = color)
        sizer.Add(self._textCtrl, 1, wx.EXPAND, 0)
        frame.SetSizer(sizer)
        frame.Layout()
        frame.Show(True)
        self.Activate()
        return True


    def _BuildTextCtrl(self, parent, font, color = wx.BLACK, value = "", selection = [0, 0]):
        if self._wordWrap:
            wordWrapStyle = wx.TE_WORDWRAP
        else:
            wordWrapStyle = wx.TE_DONTWRAP
        textCtrl = wx.TextCtrl(parent, -1, pos = wx.DefaultPosition, size = parent.GetClientSize(), style = wx.TE_MULTILINE | wordWrapStyle)
        textCtrl.SetFont(font)
        textCtrl.SetForegroundColour(color)
        textCtrl.SetValue(value)
        return textCtrl


    def _GetFontAndColorFromConfig(self):
        font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
        config = wx.ConfigBase_Get()
        fontData = config.Read("TextEditorFont", "")
        if fontData:
            nativeFont = wx.NativeFontInfo()
            nativeFont.FromString(fontData)
            font.SetNativeFontInfo(nativeFont)
        color = wx.BLACK
        colorData = config.Read("TextEditorColor", "")
        if colorData:
            red = int("0x" + colorData[0:2], 16)
            green = int("0x" + colorData[2:4], 16)
            blue = int("0x" + colorData[4:6], 16)
            color = wx.Color(red, green, blue)
        return font, color


    def OnCreateCommandProcessor(self):
        # Don't create a command processor, it has its own
        pass


    def OnActivateView(self, activate, activeView, deactiveView):
        if activate and self._textCtrl:
            # In MDI mode just calling set focus doesn't work and in SDI mode using CallAfter causes an endless loop
            if self.GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI:
                self._textCtrl.SetFocus()
            else:
                def SetFocusToTextCtrl():
                    if self._textCtrl:  # Need to make sure it is there in case we are in the closeall mode of the MDI window
                        self._textCtrl.SetFocus()
                wx.CallAfter(SetFocusToTextCtrl)


    def OnUpdate(self, sender = None, hint = None):
        if hint == "Word Wrap":
            self.SetWordWrap(wx.ConfigBase_Get().ReadInt("TextEditorWordWrap", True))
        elif hint == "Font":
            font, color = self._GetFontAndColorFromConfig()
            self.SetFont(font, color)


    def OnClose(self, deleteWindow = True):
        if not wx.lib.docview.View.OnClose(self, deleteWindow):
            return False
        self.Activate(False)
        if deleteWindow:
            self.GetFrame().Destroy()
        return True


    # Since ProcessEvent is not virtual, we have to trap the relevant events using this pseudo-ProcessEvent instead of EVT_MENU
    def ProcessEvent(self, event):
        id = event.GetId()
        if id == wx.ID_UNDO:
            if not self._textCtrl:
                return False
            self._textCtrl.Undo()
            return True
        elif id == wx.ID_REDO:
            if not self._textCtrl:
                return False
            self._textCtrl.Redo()
            return True
        elif id == wx.ID_CUT:
            if not self._textCtrl:
                return False
            self._textCtrl.Cut()
            return True
        elif id == wx.ID_COPY:
            if not self._textCtrl:
                return False
            self._textCtrl.Copy()
            return True
        elif id == wx.ID_PASTE:
            if not self._textCtrl:
                return False
            self._textCtrl.Paste()
            return True
        elif id == wx.ID_CLEAR:
            if not self._textCtrl:
                return False
            self._textCtrl.Replace(self._textCtrl.GetSelection()[0], self._textCtrl.GetSelection()[1], '')
            return True
        elif id == wx.ID_SELECTALL:
            if not self._textCtrl:
                return False
            self._textCtrl.SetSelection(-1, -1)
            return True
        elif id == TextService.CHOOSE_FONT_ID:
            if not self._textCtrl:
                return False
            self.OnChooseFont(event)
            return True
        elif id == TextService.WORD_WRAP_ID:
            if not self._textCtrl:
                return False
            self.OnWordWrap(event)
            return True
        elif id == FindService.FindService.FIND_ID:
            self.OnFind()
            return True
        elif id == FindService.FindService.FIND_PREVIOUS_ID:
            self.DoFind(forceFindPrevious = True)
            return True
        elif id == FindService.FindService.FIND_NEXT_ID:
            self.DoFind(forceFindNext = True)
            return True
        elif id == FindService.FindService.REPLACE_ID:
            self.OnFind(replace = True)
            return True
        elif id == FindService.FindService.FINDONE_ID:
            self.DoFind()
            return True
        elif id == FindService.FindService.REPLACEONE_ID:
            self.DoFind(replace = True)
            return True
        elif id == FindService.FindService.REPLACEALL_ID:
            self.DoFind(replaceAll = True)
            return True
        elif id == FindService.FindService.GOTO_LINE_ID:
            self.OnGotoLine(event)
            return True
        else:
            return wx.lib.docview.View.ProcessEvent(self, event)


    def ProcessUpdateUIEvent(self, event):
        if not self._textCtrl:
            return False

        hasText = len(self._textCtrl.GetValue()) > 0

        id = event.GetId()
        if id == wx.ID_UNDO:
            event.Enable(self._textCtrl.CanUndo())
            return True
        elif id == wx.ID_REDO:
            event.Enable(self._textCtrl.CanRedo())
            return True
        if id == wx.ID_CUT:
            event.Enable(self._textCtrl.CanCut())
            return True
        elif id == wx.ID_COPY:
            event.Enable(self._textCtrl.CanCopy())
            return True
        elif id == wx.ID_PASTE:
            event.Enable(self._textCtrl.CanPaste())
            return True
        elif id == wx.ID_CLEAR:
            event.Enable(True)  # wxBug: No matter what we do, the wxTextCtrl disables the Clear menu item and the menu item traps the Del key and the wxTextCtrl doesn't get it and can't delete the next character
            return True
        elif id == wx.ID_SELECTALL:
            event.Enable(hasText)
            return True
        elif id == TextService.CHOOSE_FONT_ID:
            event.Enable(True)
            return True
        elif id == TextService.WORD_WRAP_ID:
            event.Enable(True)
            return True
        elif id == FindService.FindService.FIND_ID:
            event.Enable(hasText)
            return True
        elif id == FindService.FindService.FIND_PREVIOUS_ID:
            event.Enable(hasText and
                         self._FindServiceHasString() and
                         self._textCtrl.GetSelection()[0] > 0)
            return True
        elif id == FindService.FindService.FIND_NEXT_ID:
            event.Enable(hasText and
                         self._FindServiceHasString() and
                         self._textCtrl.GetSelection()[0] < len(self._textCtrl.GetValue()))
            return True
        elif id == FindService.FindService.REPLACE_ID:
            event.Enable(hasText)
            return True
        elif id == FindService.FindService.GOTO_LINE_ID:
            event.Enable(True)
            return True
        else:
            return wx.lib.docview.View.ProcessUpdateUIEvent(self, event)


    #----------------------------------------------------------------------------
    # Methods for TextDocument to call
    #----------------------------------------------------------------------------

    def GetTextCtrl(self):
        return self._textCtrl


    #----------------------------------------------------------------------------
    # Format methods
    #----------------------------------------------------------------------------

    def OnChooseFont(self, event):
        data = wx.FontData()
        data.EnableEffects(True)
        data.SetInitialFont(self._textCtrl.GetFont())
        data.SetColour(self._textCtrl.GetForegroundColour())
        fontDialog = wx.FontDialog(self.GetFrame(), data)
        if fontDialog.ShowModal() == wx.ID_OK:
            data = fontDialog.GetFontData()
            self.SetFont(data.GetChosenFont(), data.GetColour())
        fontDialog.Destroy()


    def SetFont(self, font, color):
        self._textCtrl.SetFont(font)
        self._textCtrl.SetForegroundColour(color)
        self._textCtrl.Refresh()
        self._textCtrl.Layout()


    def OnWordWrap(self, event):
        self.SetWordWrap(not self.GetWordWrap())


    def GetWordWrap(self):
        return self._wordWrap


    def SetWordWrap(self, wordWrap = True):
        self._wordWrap = wordWrap
        temp = self._textCtrl
        self._textCtrl = self._BuildTextCtrl(temp.GetParent(),
                                             font = temp.GetFont(),
                                             color = temp.GetForegroundColour(),
                                             value = temp.GetValue(),
                                             selection = temp.GetSelection())
        self.GetDocument().Modify(temp.IsModified())
        temp.Destroy()


    #----------------------------------------------------------------------------
    # Find methods
    #----------------------------------------------------------------------------

    def OnFind(self, replace = False):
        findService = wx.GetApp().GetService(FindService.FindService)
        if findService:
            findService.ShowFindReplaceDialog(findString = self._textCtrl.GetStringSelection(), replace = replace)


    def DoFind(self, forceFindNext = False, forceFindPrevious = False, replace = False, replaceAll = False):
        findService = wx.GetApp().GetService(FindService.FindService)
        if not findService:
            return
        findString = findService.GetFindString()
        if len(findString) == 0:
            return -1
        replaceString = findService.GetReplaceString()
        flags = findService.GetFlags()
        startLoc, endLoc = self._textCtrl.GetSelection()

        wholeWord = flags & wx.FR_WHOLEWORD > 0
        matchCase = flags & wx.FR_MATCHCASE > 0
        regExp = flags & FindService.FindService.FR_REGEXP > 0
        down = flags & wx.FR_DOWN > 0
        wrap = flags & FindService.FindService.FR_WRAP > 0

        if forceFindPrevious:   # this is from function keys, not dialog box
            down = False
            wrap = False        # user would want to know they're at the end of file
        elif forceFindNext:
            down = True
            wrap = False        # user would want to know they're at the end of file

        # On replace dialog operations, user is allowed to replace the currently highlighted text to determine if it should be replaced or not.
        # Typically, it is the text from a previous find operation, but we must check to see if it isn't, user may have moved the cursor or selected some other text accidentally.
        # If the text is a match, then replace it.
        if replace:
            result, start, end, replText = findService.DoFind(findString, replaceString, self._textCtrl.GetStringSelection(), 0, 0, True, matchCase, wholeWord, regExp, replace)
            if result > 0:
                self._textCtrl.Replace(startLoc, endLoc, replaceString)
                self.GetDocument().Modify(True)
                wx.GetApp().GetTopWindow().PushStatusText(_("1 occurrence of \"%s\" replaced") % findString)
                if down:
                    startLoc += len(replText)  # advance start location past replacement string to new text
                endLoc = startLoc

        text = self._textCtrl.GetValue()
        if wx.Platform == "__WXMSW__":
            text = string.replace(text, '\n', '\r\n')

        # Find the next matching text occurance or if it is a ReplaceAll, replace all occurances
        # Even if the user is Replacing, we should replace here, but only select the text and let the user replace it with the next Replace operation
        result, start, end, text = findService.DoFind(findString, replaceString, text, startLoc, endLoc, down, matchCase, wholeWord, regExp, False, replaceAll, wrap)
        if result > 0:
            self._textCtrl.SetValue(text)
            self.GetDocument().Modify(True)
            if result == 1:
                wx.GetApp().GetTopWindow().PushStatusText(_("1 occurrence of \"%s\" replaced") % findString)
            else:
                wx.GetApp().GetTopWindow().PushStatusText(_("%i occurrences of \"%s\" replaced") % (result, findString))
        elif result == 0:
            self._textCtrl.SetSelection(start, end)
            self._textCtrl.SetFocus()
            wx.GetApp().GetTopWindow().PushStatusText(_("Found \"%s\"") % findString)
        else:
            wx.GetApp().GetTopWindow().PushStatusText(_("Can't find \"%s\"") % findString)


    def _FindServiceHasString(self):
        findService = wx.GetApp().GetService(FindService.FindService)
        if not findService or not findService.GetFindString():
            return False
        return True


    def OnGotoLine(self, event):
        findService = wx.GetApp().GetService(FindService.FindService)
        if findService:
            line = findService.GetLineNumber(self.GetDocumentManager().FindSuitableParent())
            if line > -1:
                pos = self._textCtrl.XYToPosition(0, line - 1)
                self._textCtrl.SetSelection(pos, pos)


class TextService(wx.lib.pydocview.DocService):


    WORD_WRAP_ID = wx.NewId()
    CHOOSE_FONT_ID = wx.NewId()


    def __init__(self):
        wx.lib.pydocview.DocService.__init__(self)


    def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None):
        if document and document.GetDocumentTemplate().GetDocumentType() != TextDocument:
            return
        config = wx.ConfigBase_Get()

        formatMenuIndex = menuBar.FindMenu(_("&Format"))
        if formatMenuIndex > -1:
            formatMenu = menuBar.GetMenu(formatMenuIndex)
        else:
            formatMenu = wx.Menu()
        formatMenu = wx.Menu()
        if not menuBar.FindItemById(TextService.WORD_WRAP_ID):
            formatMenu.AppendCheckItem(TextService.WORD_WRAP_ID, _("Word Wrap"), _("Wraps text horizontally when checked"))
            formatMenu.Check(TextService.WORD_WRAP_ID, config.ReadInt("TextEditorWordWrap", True))
            wx.EVT_MENU(frame, TextService.WORD_WRAP_ID, frame.ProcessEvent)
            wx.EVT_UPDATE_UI(frame, TextService.WORD_WRAP_ID, frame.ProcessUpdateUIEvent)
        if not menuBar.FindItemById(TextService.CHOOSE_FONT_ID):
            formatMenu.Append(TextService.CHOOSE_FONT_ID, _("Font..."), _("Sets the font to use"))
            wx.EVT_MENU(frame, TextService.CHOOSE_FONT_ID, frame.ProcessEvent)
            wx.EVT_UPDATE_UI(frame, TextService.CHOOSE_FONT_ID, frame.ProcessUpdateUIEvent)
        if formatMenuIndex == -1:
            viewMenuIndex = menuBar.FindMenu(_("&View"))
            menuBar.Insert(viewMenuIndex + 1, formatMenu, _("&Format"))


    def ProcessUpdateUIEvent(self, event):
        id = event.GetId()
        if id == TextService.CHOOSE_FONT_ID:
            event.Enable(False)
            return True
        elif id == TextService.WORD_WRAP_ID:
            event.Enable(False)
            return True
        else:
            return False


class TextOptionsPanel(wx.Panel):


    def __init__(self, parent, id):
        wx.Panel.__init__(self, parent, id)
        SPACE = 10
        HALF_SPACE   = 5
        config = wx.ConfigBase_Get()
        self._textFont = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
        fontData = config.Read("TextEditorFont", "")
        if fontData:
            nativeFont = wx.NativeFontInfo()
            nativeFont.FromString(fontData)
            self._textFont.SetNativeFontInfo(nativeFont)
        self._originalTextFont = self._textFont
        self._textColor = wx.BLACK
        colorData = config.Read("TextEditorColor", "")
        if colorData:
            red = int("0x" + colorData[0:2], 16)
            green = int("0x" + colorData[2:4], 16)
            blue = int("0x" + colorData[4:6], 16)
            self._textColor = wx.Color(red, green, blue)
        self._originalTextColor = self._textColor
        parent.AddPage(self, _("Text"))
        fontLabel = wx.StaticText(self, -1, _("Font:"))
        self._sampleTextCtrl = wx.TextCtrl(self, -1, "", size = (125, -1))
        self._sampleTextCtrl.SetEditable(False)
        chooseFontButton = wx.Button(self, -1, _("Choose Font..."))
        wx.EVT_BUTTON(self, chooseFontButton.GetId(), self.OnChooseFont)
        self._wordWrapCheckBox = wx.CheckBox(self, -1, _("Wrap words inside text area"))
        self._wordWrapCheckBox.SetValue(wx.ConfigBase_Get().ReadInt("TextEditorWordWrap", True))
        textPanelBorderSizer = wx.BoxSizer(wx.VERTICAL)
        textPanelSizer = wx.BoxSizer(wx.VERTICAL)
        textFontSizer = wx.BoxSizer(wx.HORIZONTAL)
        textFontSizer.Add(fontLabel, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.TOP, HALF_SPACE)
        textFontSizer.Add(self._sampleTextCtrl, 0, wx.ALIGN_LEFT | wx.EXPAND | wx.RIGHT, HALF_SPACE)
        textFontSizer.Add(chooseFontButton, 0, wx.ALIGN_RIGHT | wx.LEFT, HALF_SPACE)
        textPanelSizer.Add(textFontSizer, 0, wx.ALL, HALF_SPACE)
        textPanelSizer.Add(self._wordWrapCheckBox, 0, wx.ALL, HALF_SPACE)
        textPanelBorderSizer.Add(textPanelSizer, 0, wx.ALL, SPACE)
        self.SetSizer(textPanelBorderSizer)
        self.UpdateSampleFont()


    def UpdateSampleFont(self):
        nativeFont = wx.NativeFontInfo()
        nativeFont.FromString(self._textFont.GetNativeFontInfoDesc())
        font = wx.NullFont
        font.SetNativeFontInfo(nativeFont)
        font.SetPointSize(self._sampleTextCtrl.GetFont().GetPointSize())  # Use the standard point size
        self._sampleTextCtrl.SetFont(font)
        self._sampleTextCtrl.SetForegroundColour(self._textColor)
        self._sampleTextCtrl.SetValue(_("%d pt. %s") % (self._textFont.GetPointSize(), self._textFont.GetFaceName()))
        self._sampleTextCtrl.Refresh()
        self.Layout()


    def OnChooseFont(self, event):
        data = wx.FontData()
        data.EnableEffects(True)
        data.SetInitialFont(self._textFont)
        data.SetColour(self._textColor)
        fontDialog = wx.FontDialog(self, data)
        if fontDialog.ShowModal() == wx.ID_OK:
            data = fontDialog.GetFontData()
            self._textFont = data.GetChosenFont()
            self._textColor = data.GetColour()
            self.UpdateSampleFont()
        fontDialog.Destroy()


    def OnOK(self, optionsDialog):
        config = wx.ConfigBase_Get()
        doWordWrapUpdate = config.ReadInt("TextEditorWordWrap", True) != self._wordWrapCheckBox.GetValue()
        config.WriteInt("TextEditorWordWrap", self._wordWrapCheckBox.GetValue())
        doFontUpdate = self._originalTextFont != self._textFont or self._originalTextColor != self._textColor
        config.Write("TextEditorFont", self._textFont.GetNativeFontInfoDesc())
        config.Write("TextEditorColor", "%02x%02x%02x" % (self._textColor.Red(), self._textColor.Green(), self._textColor.Blue()))
        if doWordWrapUpdate or doFontUpdate:
            for document in optionsDialog.GetDocManager().GetDocuments():
                if document.GetDocumentTemplate().GetDocumentType() == TextDocument:
                    if doWordWrapUpdate:
                        document.UpdateAllViews(hint = "Word Wrap")
                    if doFontUpdate:
                        document.UpdateAllViews(hint = "Font")
