Esempio n. 1
0
    def __init__(self, *args, **kwds):
        kwds["style"] = wx.TAB_TRAVERSAL
        wx.Panel.__init__(self, *args, **kwds)

        self._config = EditorConfig (Application.config)

        self._enableSpellChecking = True
        self._spellChecker = None

        self.SPELL_ERROR_INDICATOR = 0

        self._spellErrorText = None
        self._spellSuggestList = []

        self._spellMaxSuggest = len (TextEditorMenu.ID_SUGGESTS)
        self._spellStartByteError = -1
        self._spellEndByteError = -1

        # Уже были установлены стили текста (раскраска)
        self._styleSet = False

        self.__stylebytes = None
        self.__indicatorsbytes = None

        # Начинаем раскраску кода не менее чем через это время с момента его изменения
        self._DELAY = timedelta (milliseconds=300)

        # Время последней модификации текста страницы.
        # Используется для замера времени после модификации, чтобы не парсить текст
        # после каждой введенной буквы
        self._lastEdit = datetime.now() - self._DELAY * 2

        self.textCtrl = StyledTextCtrl(self, -1)

        self._popupMenu = TextEditorMenu(self)

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel (self)
        self._searchPanelController = SearchReplaceController (self._searchPanel, self)
        self._searchPanel.setController (self._searchPanelController)

        self.__do_layout()
        self.__createCoders()
        self._helper = TextEditorHelper()

        self.__showlinenumbers = self._config.lineNumbers.value

        self.setDefaultSettings()
        self.__bindEvents()
Esempio n. 2
0
    def __init__(self, parent):
        super(TextEditorBase, self).__init__(parent, style=0)
        self.textCtrl = StyledTextCtrl(self, -1)

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel(self)
        self._searchPanelController = SearchReplaceController(
            self._searchPanel, self)
        self._searchPanel.setController(self._searchPanelController)

        self._do_layout()

        self.__createCoders()
        self._helper = TextEditorHelper()
        self._bind()
        self._setDefaultSettings()
Esempio n. 3
0
    def __init__(self, parent):
        super(TextEditorBase, self).__init__(parent, style=0)
        self.textCtrl = StyledTextCtrl(self, -1)

        # Used to fix text encoding after clipboard pasting
        self._needFixTextEncoding = False

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel(self)
        self._searchPanelController = SearchReplaceController(
            self._searchPanel, self)
        self._searchPanel.setController(self._searchPanelController)

        self._do_layout()

        self._helper = TextEditorHelper()
        self._bind()
        self._setDefaultSettings()
Esempio n. 4
0
    def __init__(self, *args, **kwds):
        kwds["style"] = wx.TAB_TRAVERSAL
        wx.Panel.__init__(self, *args, **kwds)

        self._config = EditorConfig (Application.config)

        self._enableSpellChecking = True
        self._spellChecker = None

        self.SPELL_ERROR_INDICATOR = 0
        self.SPELL_ERROR_INDICATOR_MASK = wx.stc.STC_INDIC0_MASK

        self._spellErrorText = None
        self._spellSuggestList = []

        self._spellMaxSuggest = len (TextEditorMenu.ID_SUGGESTS)
        self._spellStartByteError = -1
        self._spellEndByteError = -1

        # Уже были установлены стили текста (раскраска)
        self._styleSet = False

        self.__stylebytes = None
        self.__indicatorsbytes = None

        # Начинаем раскраску кода не менее чем через это время с момента его изменения
        self._DELAY = timedelta (milliseconds=300)

        # Время последней модификации текста страницы.
        # Используется для замера времени после модификации, чтобы не парсить текст
        # после каждой введенной буквы
        self._lastEdit = datetime.now() - self._DELAY * 2

        self._colorizingThread = None

        self.textCtrl = StyledTextCtrl(self, -1)

        self._popupMenu = TextEditorMenu(self)

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel (self)
        self._searchPanelController = SearchReplaceController (self._searchPanel, self)
        self._searchPanel.setController (self._searchPanelController)

        self.__do_layout()
        self.__createCoders()

        self.__showlinenumbers = self._config.lineNumbers.value

        self.setDefaultSettings()
        self.__bindEvents()
Esempio n. 5
0
    def __init__(self, parent):
        super(TextEditorBase, self).__init__(parent, style=wx.TAB_TRAVERSAL)
        self.textCtrl = StyledTextCtrl(self, -1)

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel(self)
        self._searchPanelController = SearchReplaceController(
            self._searchPanel,
            self)
        self._searchPanel.setController(self._searchPanelController)

        self._do_layout()

        self.__createCoders()
        self._helper = TextEditorHelper()
        self._bind()
        self._setDefaultSettings()
Esempio n. 6
0
class TextEditorBase(wx.Panel):
    def __init__(self, parent):
        super(TextEditorBase, self).__init__(parent, style=0)
        self.textCtrl = StyledTextCtrl(self, -1)

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel(self)
        self._searchPanelController = SearchReplaceController(
            self._searchPanel, self)
        self._searchPanel.setController(self._searchPanelController)

        self._do_layout()

        self.__createCoders()
        self._helper = TextEditorHelper()
        self._bind()
        self._setDefaultSettings()

    def _bind(self):
        self.textCtrl.Bind(wx.EVT_KEY_DOWN, self.__onKeyDown)

    def _do_layout(self):
        mainSizer = wx.FlexGridSizer(rows=2, cols=0, vgap=0, hgap=0)
        mainSizer.AddGrowableRow(0)
        mainSizer.AddGrowableCol(0)

        mainSizer.Add(self.textCtrl, 0, wx.EXPAND, 0)
        mainSizer.Add(self._searchPanel, 0, wx.EXPAND, 0)
        self.SetSizer(mainSizer)

        self._searchPanel.Hide()
        self.Layout()

    def __createCoders(self):
        encoding = outwiker.core.system.getOS().inputEncoding
        self.mbcsEnc = codecs.getencoder(encoding)

    def __onKeyDown(self, event):
        key = event.GetKeyCode()

        if key == wx.WXK_ESCAPE:
            self._searchPanel.Close()

        event.Skip()

    def _setDefaultSettings(self):
        self.textCtrl.SetEndAtLastLine(False)
        self.textCtrl.StyleClearAll()
        self.textCtrl.SetWrapMode(wx.stc.STC_WRAP_WORD)
        self.textCtrl.SetWrapVisualFlags(wx.stc.STC_WRAPVISUALFLAG_END)
        self.textCtrl.SetTabWidth(4)
        self._setDefaultHotKeys()

    def _setDefaultHotKeys(self):
        self.textCtrl.CmdKeyClearAll()

        # Clear Cmd keys for Ubuntu
        for key in list(range(ord('A'),
                              ord('Z') + 1)) + list(
                                  range(ord('0'),
                                        ord('9') + 1)):
            self.textCtrl.CmdKeyClear(
                key, wx.stc.STC_SCMOD_ALT | wx.stc.STC_SCMOD_CTRL)
            self.textCtrl.CmdKeyClear(
                key, wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT
                | wx.stc.STC_SCMOD_CTRL)

        self.textCtrl.CmdKeyClear(wx.stc.STC_KEY_UP, wx.stc.STC_SCMOD_CTRL)
        self.textCtrl.CmdKeyClear(wx.stc.STC_KEY_DOWN, wx.stc.STC_SCMOD_CTRL)

        # Code from Wikidpad sources
        # Default mapping based on Scintilla's "KeyMap.cxx" file
        defaultHotKeys = (
            (wx.stc.STC_KEY_DOWN, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_LINEDOWN),
            (wx.stc.STC_KEY_UP, wx.stc.STC_SCMOD_NORM, wx.stc.STC_CMD_LINEUP),
            # (wx.stc.STC_KEY_DOWN,        wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_LINESCROLLDOWN),
            # (wx.stc.STC_KEY_UP,          wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_LINESCROLLUP),
            (wx.stc.STC_KEY_UP, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_LINEUPEXTEND),
            (wx.stc.STC_KEY_DOWN, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_LINEDOWNEXTEND),
            (ord('['), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_PARAUP),
            (ord('['), wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,
             wx.stc.STC_CMD_PARAUPEXTEND),
            (ord(']'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_PARADOWN),
            (ord(']'), wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,
             wx.stc.STC_CMD_PARADOWNEXTEND),
            (wx.stc.STC_KEY_LEFT, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_CHARLEFT),
            (wx.stc.STC_KEY_LEFT, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_CHARLEFTEXTEND),
            # (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDLEFT),
            # (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDLEFTEXTEND),
            (wx.stc.STC_KEY_RIGHT, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_CHARRIGHT),
            (wx.stc.STC_KEY_RIGHT, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_CHARRIGHTEXTEND),
            # (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDRIGHT),
            # (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDRIGHTEXTEND),
            (ord('/'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_WORDPARTLEFT),
            (ord('/'), wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,
             wx.stc.STC_CMD_WORDPARTLEFTEXTEND),
            (ord('\\'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_WORDPARTRIGHT),
            (ord('\\'), wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,
             wx.stc.STC_CMD_WORDPARTRIGHTEXTEND),
            (wx.stc.STC_KEY_HOME, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_VCHOME),
            (wx.stc.STC_KEY_HOME, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_VCHOMEEXTEND),
            (wx.stc.STC_KEY_HOME, wx.stc.STC_SCMOD_CTRL,
             wx.stc.STC_CMD_DOCUMENTSTART),
            (wx.stc.STC_KEY_HOME,
             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,
             wx.stc.STC_CMD_DOCUMENTSTARTEXTEND),
            (wx.stc.STC_KEY_HOME, wx.stc.STC_SCMOD_ALT,
             wx.stc.STC_CMD_HOMEDISPLAY),
            (wx.stc.STC_KEY_END, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_LINEEND),
            (wx.stc.STC_KEY_END, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_LINEENDEXTEND),
            (wx.stc.STC_KEY_END, wx.stc.STC_SCMOD_CTRL,
             wx.stc.STC_CMD_DOCUMENTEND),
            (wx.stc.STC_KEY_END,
             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,
             wx.stc.STC_CMD_DOCUMENTENDEXTEND),
            (wx.stc.STC_KEY_END, wx.stc.STC_SCMOD_ALT,
             wx.stc.STC_CMD_LINEENDDISPLAY),
            (wx.stc.STC_KEY_PRIOR, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_PAGEUP),
            (wx.stc.STC_KEY_PRIOR, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_PAGEUPEXTEND),
            (wx.stc.STC_KEY_NEXT, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_PAGEDOWN),
            (wx.stc.STC_KEY_NEXT, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_PAGEDOWNEXTEND),
            (wx.stc.STC_KEY_DELETE, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_CLEAR),
            (wx.stc.STC_KEY_INSERT, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_EDITTOGGLEOVERTYPE),
            (wx.stc.STC_KEY_ESCAPE, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_CANCEL),
            (wx.stc.STC_KEY_BACK, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_DELETEBACK),
            (wx.stc.STC_KEY_BACK, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_DELETEBACK),
            (wx.stc.STC_KEY_BACK, wx.stc.STC_SCMOD_ALT, wx.stc.STC_CMD_UNDO),
            (ord('Z'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_UNDO),
            (ord('Y'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_REDO),
            (ord('A'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_SELECTALL),
            (wx.stc.STC_KEY_INSERT,
             wx.stc.STC_SCMOD_CTRL | wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_COPY),
            (wx.stc.STC_KEY_INSERT, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_PASTE),
            (ord('C'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_COPY),
            (ord('X'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_CUT),
            (ord('V'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_PASTE),
            (wx.stc.STC_KEY_TAB, wx.stc.STC_SCMOD_NORM, wx.stc.STC_CMD_TAB),
            (wx.stc.STC_KEY_TAB, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_BACKTAB),
            (wx.stc.STC_KEY_RETURN, wx.stc.STC_SCMOD_NORM,
             wx.stc.STC_CMD_NEWLINE),
            (wx.stc.STC_KEY_RETURN, wx.stc.STC_SCMOD_SHIFT,
             wx.stc.STC_CMD_NEWLINE),
            (wx.stc.STC_KEY_ADD, wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_ZOOMIN),
            (wx.stc.STC_KEY_SUBTRACT, wx.stc.STC_SCMOD_CTRL,
             wx.stc.STC_CMD_ZOOMOUT),
            (wx.stc.STC_KEY_DOWN,
             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,
             wx.stc.STC_CMD_LINEDOWNRECTEXTEND),
            (wx.stc.STC_KEY_UP, wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,
             wx.stc.STC_CMD_LINEUPRECTEXTEND),
            (wx.stc.STC_KEY_LEFT,
             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,
             wx.stc.STC_CMD_CHARLEFTRECTEXTEND),
            (wx.stc.STC_KEY_RIGHT,
             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,
             wx.stc.STC_CMD_CHARRIGHTRECTEXTEND),
            (wx.stc.STC_KEY_HOME,
             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,
             wx.stc.STC_CMD_VCHOMERECTEXTEND),
            (wx.stc.STC_KEY_END, wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,
             wx.stc.STC_CMD_LINEENDRECTEXTEND),
            (wx.stc.STC_KEY_PRIOR,
             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,
             wx.stc.STC_CMD_PAGEUPRECTEXTEND),
            (wx.stc.STC_KEY_NEXT,
             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,
             wx.stc.STC_CMD_PAGEDOWNRECTEXTEND),

            # (wx.stc.STC_KEY_DELETE,    wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_DELLINERIGHT),
            # (wx.stc.STC_KEY_BACK,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_DELLINELEFT),
            # (wx.stc.STC_KEY_BACK,        wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DELWORDLEFT),
            # (wx.stc.STC_KEY_DELETE,     wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_DELWORDRIGHT),
            # (wx.stc.STC_KEY_DIVIDE,    wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_SETZOOM),
            #        (ord('L'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINECUT),
            #        (ord('L'),             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINEDELETE),
            #        (ord('T'),             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINECOPY),
            #        (ord('T'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINETRANSPOSE),
            #        (ord('D'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_SELECTIONDUPLICATE),
            #        (ord('U'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LOWERCASE),
            #        (ord('U'),             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_UPPERCASE),
        )

        [self.textCtrl.CmdKeyAssign(*key) for key in defaultHotKeys]

    @property
    def searchPanel(self):
        """
        Возвращает контроллер панели поиска
        """
        return self._searchPanelController

    def Print(self):
        selectedtext = self.textCtrl.GetSelectedText()
        text = self.textCtrl.GetText()

        printer = TextPrinter(self)
        printer.printout(text if len(selectedtext) == 0 else selectedtext)

    def getPosChar(self, posBytes):
        return len(self.textCtrl.GetTextRange(0, posBytes))

    def AddText(self, text):
        self.textCtrl.AddText(text)

    def replaceText(self, text):
        self.textCtrl.ReplaceSelection(text)

    def toddleLinePrefix(self, line, prefix):
        """
        If line with number "line" starts with prefix, prefix will be removed
        else prefix will be added.
        """
        assert line < self.GetLineCount()
        line_text = self.GetLine(line)
        if line_text.startswith(prefix):
            line_text = line_text[len(prefix):]
        else:
            line_text = prefix + line_text
        self.SetLine(line, line_text)

    def toddleSelectedLinesPrefix(self, prefix):
        """
        Apply toddleLinePrefix method to selected lines
        """
        self.BeginUndoAction()
        old_sel_start = self.GetSelectionStart()
        old_sel_end = self.GetSelectionEnd()

        first_line, last_line = self.GetSelectionLines()
        [
            self.toddleLinePrefix(n, prefix)
            for n in range(first_line, last_line + 1)
        ]

        if old_sel_start != old_sel_end:
            new_sel_start = self.GetLineStartPosition(first_line)
            new_sel_end = self.GetLineEndPosition(last_line)
        else:
            new_sel_start = new_sel_end = self.GetLineEndPosition(last_line)

        self.SetSelection(new_sel_start, new_sel_end)
        self.EndUndoAction()

    def turnText(self, lefttext, righttext):
        selText = self.textCtrl.GetSelectedText()
        newtext = lefttext + selText + righttext
        self.textCtrl.ReplaceSelection(newtext)

        currPos = self.GetSelectionEnd()
        if len(selText) == 0:
            """
            Если не оборачиваем текст, а делаем пустой тег, то поместим каретку до закрывающегося тега
            """
            newpos = currPos - len(righttext)
            self.SetSelection(newpos, newpos)
        else:
            self.SetSelection(currPos - len(selText) - len(righttext),
                              currPos - len(righttext))

    def escapeHtml(self):
        selText = self.textCtrl.GetSelectedText()
        text = html.escape(selText, quote=False)
        self.textCtrl.ReplaceSelection(text)

    def SetReadOnly(self, readonly):
        self.textCtrl.SetReadOnly(readonly)

    def GetReadOnly(self):
        return self.textCtrl.GetReadOnly()

    def GetText(self):
        return self.textCtrl.GetText()

    def SetText(self, text):
        self.textCtrl.SetText(text)

    def EmptyUndoBuffer(self):
        self.textCtrl.EmptyUndoBuffer()

    def GetSelectedText(self):
        return self.textCtrl.GetSelectedText()

    def GetCurrentLine(self):
        return self.textCtrl.GetCurrentLine()

    def ScrollToLine(self, line):
        self.textCtrl.ScrollToLine(line)

    def SetSelection(self, start, end):
        """
        start и end в символах, а не в байтах, в отличие от исходного
        StyledTextCtrl
        """
        startText = self.GetText()[:start]
        endText = self.GetText()[:end]

        firstByte = self._helper.calcByteLen(startText)
        endByte = self._helper.calcByteLen(endText)

        self.textCtrl.SetSelection(firstByte, endByte)

    def GotoPos(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        self.textCtrl.GotoPos(pos_bytes)

    def GetCurrentPosition(self):
        """
        Возвращает номер символа(а не байта), перед которых находится курсор
        """
        return self._calcCharPos(self.textCtrl.GetCurrentPos())

    def GetSelectionStart(self):
        """
        Возвращает позицию начала выбранной области в символах, а не в байтах
        """
        return self._calcCharPos(self.textCtrl.GetSelectionStart())

    def GetSelectionLines(self):
        """
        Return tuple (first selected line, last selected line)
        """
        start_bytes = self.textCtrl.GetSelectionStart()
        end_bytes = self.textCtrl.GetSelectionEnd()
        return (self.textCtrl.LineFromPosition(start_bytes),
                self.textCtrl.LineFromPosition(end_bytes))

    def GetSelectionEnd(self):
        """
        Возвращает позицию конца выбранной области в символах, а не в байтах
        """
        return self._calcCharPos(self.textCtrl.GetSelectionEnd())

    def SetFocus(self):
        self.textCtrl.SetFocus()
        self.textCtrl.SetSTCFocus(True)

    def GetLine(self, line):
        """
        Return line with the "line" number. \n included.
        """
        return self.textCtrl.GetLine(line)

    def SetLine(self, line, newline):
        """
        Replace line with the number "line" newline.
        Newline will be ended with "\n" else line will be joined with next line
        """
        linecount = self.GetLineCount()
        assert line < linecount

        line_start_bytes = self.textCtrl.PositionFromLine(line)
        line_end_bytes = self.textCtrl.PositionFromLine(line + 1)
        self.textCtrl.Replace(line_start_bytes, line_end_bytes, newline)

    def GetLineCount(self):
        return self.textCtrl.GetLineCount()

    def GetLineStartPosition(self, line):
        """
        Retrieve the position at the start of a line in symbols (not bytes)
        """
        return self._calcCharPos(self.textCtrl.PositionFromLine(line))

    def GetLineEndPosition(self, line):
        """
        Get the position after the last visible characters on a line
            in symbols (not bytes)
        """
        return self._calcCharPos(self.textCtrl.GetLineEndPosition(line))

    def MoveSelectedLinesUp(self):
        """
        Move the selected lines up one line,
        shifting the line above after the selection.
        """
        self.textCtrl.MoveSelectedLinesUp()

    def MoveSelectedLinesDown(self):
        """
        Move the selected lines down one line,
        shifting the line below before the selection.
        """
        self.textCtrl.MoveSelectedLinesDown()

    def LineDuplicate(self):
        """
        Duplicate the current line.
        """
        self.textCtrl.LineDuplicate()

    def LineDelete(self):
        """
        Delete the current line.
        """
        self.textCtrl.LineDelete()

    def BeginUndoAction(self):
        self.textCtrl.BeginUndoAction()

    def EndUndoAction(self):
        self.textCtrl.EndUndoAction()

    def JoinLines(self):
        """
        Join selected lines
        """
        first_line, last_line = self.GetSelectionLines()
        if first_line != last_line:
            last_line -= 1

        self.BeginUndoAction()

        for _ in range(first_line, last_line + 1):
            line = self.GetLine(first_line).replace(u'\r\n', u'\n')
            if line.endswith(u'\n'):
                newline = line[:-1]
                self.SetLine(first_line, newline)

        new_sel_pos = self.GetLineEndPosition(first_line)
        self.SetSelection(new_sel_pos, new_sel_pos)

        self.EndUndoAction()

    def DelWordLeft(self):
        self.textCtrl.DelWordLeft()

    def DelWordRight(self):
        self.textCtrl.DelWordRight()

    def DelLineLeft(self):
        """
        Delete back from the current position to the start of the line
        """
        self.textCtrl.DelLineLeft()

    def DelLineRight(self):
        """
        Delete forwards from the current position to the end of the line
        """
        self.textCtrl.DelLineRight()

    def WordLeft(self):
        self.textCtrl.WordLeft()

    def WordRight(self):
        self.textCtrl.WordRight()

    def WordLeftEnd(self):
        self.textCtrl.WordLeftEnd()

    def WordRightEnd(self):
        self.textCtrl.WordRightEnd()

    def WordLeftExtend(self):
        self.textCtrl.WordLeftExtend()

    def WordRightExtend(self):
        self.textCtrl.WordRightExtend()

    def GotoWordStart(self):
        self.WordRight()
        self.WordLeft()

    def GotoWordEnd(self):
        self.WordLeftEnd()
        self.WordRightEnd()

    def ScrollLineToCursor(self):
        maxlines = self.textCtrl.LinesOnScreen()
        line = self.GetCurrentLine()
        if line >= maxlines:
            delta = min(10, maxlines / 3)
            line -= delta
            if line < 0:
                line = 0
            self.ScrollToLine(line)

    def WordStartPosition(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        result_bytes = self.textCtrl.WordStartPosition(pos_bytes, True)
        return self.getPosChar(result_bytes)

    def WordEndPosition(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        result_bytes = self.textCtrl.WordEndPosition(pos_bytes, True)
        return self.getPosChar(result_bytes)

    def GetWord(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        word_start_bytes = self.textCtrl.WordStartPosition(pos_bytes, True)
        word_end_bytes = self.textCtrl.WordEndPosition(pos_bytes, True)
        word = self.textCtrl.GetTextRange(word_start_bytes, word_end_bytes)
        return word

    def _calcCharPos(self, pos_bytes):
        """
        Пересчет позиции в байтах в позицию в символах
        """
        text_left = self.textCtrl.GetTextRange(0, pos_bytes)
        currpos = len(text_left)
        return currpos

    def _getTextForParse(self):
        # Табуляция в редакторе считается за несколько символов
        return self.textCtrl.GetText().replace("\t", " ")
Esempio n. 7
0
class TextEditor(wx.Panel):
    _fontConfigSection = "Font"

    def __init__(self, *args, **kwds):
        kwds["style"] = wx.TAB_TRAVERSAL
        wx.Panel.__init__(self, *args, **kwds)

        self._config = EditorConfig (Application.config)

        self._enableSpellChecking = True
        self._spellChecker = None

        self.SPELL_ERROR_INDICATOR = 0
        self.SPELL_ERROR_INDICATOR_MASK = wx.stc.STC_INDIC0_MASK

        self._spellErrorText = None
        self._spellSuggestList = []

        self._spellMaxSuggest = len (TextEditorMenu.ID_SUGGESTS)
        self._spellStartByteError = -1
        self._spellEndByteError = -1

        # Уже были установлены стили текста (раскраска)
        self._styleSet = False

        self.__stylebytes = None
        self.__indicatorsbytes = None

        # Начинаем раскраску кода не менее чем через это время с момента его изменения
        self._DELAY = timedelta (milliseconds=300)

        # Время последней модификации текста страницы.
        # Используется для замера времени после модификации, чтобы не парсить текст
        # после каждой введенной буквы
        self._lastEdit = datetime.now() - self._DELAY * 2

        self._colorizingThread = None

        self.textCtrl = StyledTextCtrl(self, -1)

        self._popupMenu = TextEditorMenu(self)

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel (self)
        self._searchPanelController = SearchReplaceController (self._searchPanel, self)
        self._searchPanel.setController (self._searchPanelController)

        self.__do_layout()
        self.__createCoders()

        self.__showlinenumbers = self._config.lineNumbers.value

        self.setDefaultSettings()
        self.__bindEvents()


    def __bindEvents (self):
        self.textCtrl.Bind(wx.EVT_MENU, self.__onCopyFromEditor, id = wx.ID_COPY)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onCutFromEditor, id = wx.ID_CUT)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onPasteToEditor, id = wx.ID_PASTE)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onUndo, id = wx.ID_UNDO)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onRedo, id = wx.ID_REDO)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onSelectAll, id = wx.ID_SELECTALL)

        self.textCtrl.Bind(wx.EVT_MENU, self.__onAddWordToDict, id = TextEditorMenu.ID_ADD_WORD)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onAddWordLowerToDict, id = TextEditorMenu.ID_ADD_WORD_LOWER)

        for suggestId in TextEditorMenu.ID_SUGGESTS:
            self.textCtrl.Bind(wx.EVT_MENU, self.__onSpellSuggest, id = suggestId)

        self.textCtrl.Bind (wx.EVT_CHAR, self.__OnChar_ImeWorkaround)
        self.textCtrl.Bind (wx.EVT_KEY_DOWN, self.__onKeyDown)
        self.textCtrl.Bind (wx.EVT_CONTEXT_MENU, self.__onContextMenu)

        # self.textCtrl.Bind (wx.stc.EVT_STC_STYLENEEDED, self._onStyleNeeded)
        self.textCtrl.Bind (wx.EVT_IDLE, self._onStyleNeeded)
        self.Bind (EVT_APPLY_STYLE, self._onApplyStyle)

        # При перехвате этого сообщения в других классах, нужно вызывать event.Skip(),
        # чтобы это сообщение дошло досюда
        self.textCtrl.Bind (wx.stc.EVT_STC_CHANGE, self.__onChange)


    @property
    def config (self):
        return self._config


    @property
    def enableSpellChecking (self):
        return self._enableSpellChecking


    @enableSpellChecking.setter
    def enableSpellChecking (self, value):
        self._enableSpellChecking = value
        self._styleSet = False


    def __onChange (self, event):
        self._styleSet = False
        self._lastEdit = datetime.now()
        self.__setMarginWidth (self.textCtrl)


    @property
    def searchPanel (self):
        """
        Возвращает контроллер панели поиска
        """
        return self._searchPanelController


    def Print (self):
        selectedtext = self.textCtrl.GetSelectedText()
        text = self.textCtrl.GetText()

        printer = TextPrinter (self)
        printer.printout (text if len (selectedtext) == 0 else selectedtext)


    def __onCopyFromEditor (self, event):
        self.textCtrl.Copy()


    def __onCutFromEditor (self, event):
        self.textCtrl.Cut()


    def __onPasteToEditor (self, event):
        self.textCtrl.Paste()


    def __onUndo (self, event):
        self.textCtrl.Undo()


    def __onRedo (self, event):
        self.textCtrl.Redo()


    def __onSelectAll (self, event):
        self.textCtrl.SelectAll()


    def __do_layout(self):
        mainSizer = wx.FlexGridSizer(rows=2)
        mainSizer.AddGrowableRow(0)
        mainSizer.AddGrowableCol(0)

        mainSizer.Add(self.textCtrl, 0, wx.EXPAND, 0)
        mainSizer.Add(self._searchPanel, 0, wx.EXPAND, 0)
        self.SetSizer(mainSizer)

        self._searchPanel.Hide()
        self.Layout()


    def setDefaultSettings (self):
        """
        Установить стили и настройки по умолчанию в контрол StyledTextCtrl
        """
        self._spellChecker = self.getSpellChecker()

        size = self._config.fontSize.value
        faceName = self._config.fontName.value
        isBold = self._config.fontIsBold.value
        isItalic = self._config.fontIsItalic.value
        fontColor = self._config.fontColor.value
        backColor = self._config.backColor.value

        self.__showlinenumbers = self._config.lineNumbers.value
        self.textCtrl.SetEndAtLastLine (False)

        self.textCtrl.StyleSetSize (wx.stc.STC_STYLE_DEFAULT, size)
        self.textCtrl.StyleSetFaceName (wx.stc.STC_STYLE_DEFAULT, faceName)
        self.textCtrl.StyleSetBold (wx.stc.STC_STYLE_DEFAULT, isBold)
        self.textCtrl.StyleSetItalic (wx.stc.STC_STYLE_DEFAULT, isItalic)
        self.textCtrl.StyleSetForeground (wx.stc.STC_STYLE_DEFAULT, fontColor)
        self.textCtrl.StyleSetBackground (wx.stc.STC_STYLE_DEFAULT, backColor)

        self.textCtrl.StyleClearAll()

        self.textCtrl.SetCaretForeground (fontColor)
        self.textCtrl.SetCaretLineBack (backColor)

        # Заблокируем горячую клавишу Ctrl+D, чтобы использовать ее как добавление закладки
        self.textCtrl.CmdKeyClear (ord ("D"), wx.stc.STC_SCMOD_CTRL)
        self.textCtrl.CmdKeyClear (ord ("R"), wx.stc.STC_SCMOD_CTRL | wx.stc.STC_SCMOD_SHIFT)
        self.textCtrl.SetWrapMode (wx.stc.STC_WRAP_WORD)
        self.textCtrl.SetWrapVisualFlags (wx.stc.STC_WRAPVISUALFLAG_END)

        self.__setMarginWidth (self.textCtrl)
        self.textCtrl.SetTabWidth (self._config.tabWidth.value)

        self.enableSpellChecking = self._config.spellEnabled.value
        self._spellChecker.skipWordsWithNumbers = self.config.spellSkipDigits.value

        if self._config.homeEndKeys.value == EditorConfig.HOME_END_OF_LINE:
            # Клавиши Home / End переносят курсор на начало / конец строки
            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_HOME,
                                        0,
                                        wx.stc.STC_CMD_HOMEDISPLAY)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_HOME,
                                        wx.stc.STC_SCMOD_ALT,
                                        wx.stc.STC_CMD_HOME)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_END,
                                        0,
                                        wx.stc.STC_CMD_LINEENDDISPLAY)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_END,
                                        wx.stc.STC_SCMOD_ALT,
                                        wx.stc.STC_CMD_LINEEND)
        else:
            # Клавиши Home / End переносят курсор на начало / конец абзаца
            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_HOME,
                                        0,
                                        wx.stc.STC_CMD_HOME)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_HOME,
                                        wx.stc.STC_SCMOD_ALT,
                                        wx.stc.STC_CMD_HOMEDISPLAY)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_END,
                                        0,
                                        wx.stc.STC_CMD_LINEEND)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_END,
                                        wx.stc.STC_SCMOD_ALT,
                                        wx.stc.STC_CMD_LINEENDDISPLAY)

        self.textCtrl.IndicatorSetStyle(self.SPELL_ERROR_INDICATOR, wx.stc.STC_INDIC_SQUIGGLE)
        self.textCtrl.IndicatorSetForeground(self.SPELL_ERROR_INDICATOR, "red")
        self._styleSet = False


    def __setMarginWidth (self, editor):
        """
        Установить размер левой области, где пишутся номера строк в зависимости от шрифта
        """
        if self.__showlinenumbers:
            editor.SetMarginWidth (0, self.__getMarginWidth())
            editor.SetMarginWidth (1, 5)
        else:
            editor.SetMarginWidth (0, 0)
            editor.SetMarginWidth (1, 8)


    def __getMarginWidth (self):
        """
        Расчет размера серой области с номером строк
        """
        fontSize = self._config.fontSize.value
        linescount = len (self.GetText().split("\n"))

        if linescount == 0:
            width = 10
        else:
            # Количество десятичных цифр в числе строк
            digits = int (math.log10 (linescount) + 1)
            width = int (1.2 * fontSize * digits)

        return width


    def calcByteLen(self, text):
        """Посчитать длину строки в байтах, а не в символах"""
        return len(self.encoder(text)[0])


    def calcBytePos (self, text, pos):
        """Преобразовать позицию в символах в позицию в байтах"""
        return len(self.encoder (text[: pos])[0])


    def getPosChar (self, posBytes):
        return len (self.textCtrl.GetTextRange (0, posBytes))


    def __createCoders (self):
        encoding = outwiker.core.system.getOS().inputEncoding

        self.mbcsEnc = codecs.getencoder(encoding)
        self.encoder = codecs.getencoder("utf-8")


    def __onKeyDown (self, event):
        key = event.GetKeyCode()

        if key == wx.WXK_ESCAPE:
            self._searchPanel.Close()

        event.Skip()


    def __OnChar_ImeWorkaround(self, evt):
        """
        Обработка клавиш вручную, чтобы не было проблем с вводом русских букв в Linux.
        Основа кода взята из Wikidpad (WikiTxtCtrl.py -> OnChar_ImeWorkaround)
        """
        key = evt.GetKeyCode()

        # Return if this doesn't seem to be a real character input
        if evt.ControlDown() or (0 < key < 32):
            evt.Skip()
            return

        if key >= wx.WXK_START and evt.GetUnicodeKey() != key:
            evt.Skip()
            return

        unichar = unichr(evt.GetUnicodeKey())

        self.textCtrl.ReplaceSelection(self.mbcsEnc (unichar, "replace")[0])


    def AddText (self, text):
        self.textCtrl.AddText (text)


    def replaceText (self, text):
        self.textCtrl.ReplaceSelection (text)


    def turnText (self, lefttext, righttext):
        selText = self.textCtrl.GetSelectedText()
        newtext = lefttext + selText + righttext
        self.textCtrl.ReplaceSelection (newtext)

        currPos = self.GetSelectionEnd()
        if len (selText) == 0:
            """
            Если не оборачиваем текст, а делаем пустой тег, то поместим каретку до закрывающегося тега
            """
            newpos = currPos - len (righttext)
            self.SetSelection (newpos, newpos)
        else:
            self.SetSelection (currPos - len (selText) - len (righttext),
                               currPos - len (righttext))


    def escapeHtml (self):
        selText = self.textCtrl.GetSelectedText()
        text = cgi.escape (selText, quote=False)
        self.textCtrl.ReplaceSelection (text)


    def SetReadOnly (self, readonly):
        self.textCtrl.SetReadOnly (readonly)


    def GetReadOnly (self):
        return self.textCtrl.GetReadOnly()


    def GetText(self):
        return self.textCtrl.GetText()


    def SetText (self, text):
        self.textCtrl.SetText (text)


    def EmptyUndoBuffer (self):
        self.textCtrl.EmptyUndoBuffer()


    def GetSelectedText (self):
        return self.textCtrl.GetSelectedText()


    def SetSelection (self, start, end):
        """
        start и end в символах, а не в байтах, в отличие от исходного StyledTextCtrl
        """
        startText = self.GetText()[:start]
        endText = self.GetText()[:end]

        firstByte = self.calcByteLen (startText)
        endByte = self.calcByteLen (endText)

        self.textCtrl.SetSelection (firstByte, endByte)


    def GetCurrentPosition (self):
        """
        Возвращает номер символа (а не байта), перед которых находится курсор
        """
        return self.__calcCharPos (self.textCtrl.GetCurrentPos())


    def GetSelectionStart (self):
        """
        Возвращает позицию начала выбранной области в символах, а не в байтах
        """
        return self.__calcCharPos (self.textCtrl.GetSelectionStart())


    def GetSelectionEnd (self):
        """
        Возвращает позицию конца выбранной области в символах, а не в байтах
        """
        return self.__calcCharPos (self.textCtrl.GetSelectionEnd())


    def SetFocus (self):
        self.textCtrl.SetFocus()
        self.textCtrl.SetSTCFocus(True)


    def __calcCharPos (self, pos_bytes):
        """
        Пересчет позиции в байтах в позицию в символах
        """
        text_left = self.textCtrl.GetTextRange (0, pos_bytes)
        currpos = len (text_left)
        return currpos


    def _getTextForParse (self):
        # Табуляция в редакторе считается за несколько символов
        return self.textCtrl.GetText().replace ("\t", " ")


    def setSpellError (self, stylelist, startpos, endpos):
        """
        Mark positions as error
        startpos, endpos - positions in characters
        """
        text = self._getTextForParse()
        startbytes = self.calcBytePos (text, startpos)
        endbytes = self.calcBytePos (text, endpos)

        self.addStyle (stylelist, self.SPELL_ERROR_INDICATOR_MASK, startbytes, endbytes)


    def addStyle (self, stylelist, styleid, bytepos_start, bytepos_end):
        """
        Добавляет (с помощью операции побитового ИЛИ) стиль с идентификатором styleid к массиву байт stylelist
        """
        style_src = stylelist[bytepos_start: bytepos_end]
        style_new = [style | styleid for style in style_src]

        stylelist[bytepos_start: bytepos_end] = style_new


    def setStyle (self, stylelist, styleid, bytepos_start, bytepos_end):
        """
        Добавляет стиль с идентификатором styleid к массиву байт stylelist
        """
        stylelist[bytepos_start: bytepos_end] = [styleid] * (bytepos_end - bytepos_start)


    def runSpellChecking (self, stylelist, start, end):
        if not self._enableSpellChecking:
            return

        text = self._getTextForParse()[start: end]
        errors = self._spellChecker.findErrors (text)

        for word, err_start, err_end in errors:
            self.setSpellError (stylelist, err_start + start, err_end + start)


    def _onStyleNeeded (self, event):
        if (not self._styleSet and
                datetime.now() - self._lastEdit >= self._DELAY and
                (self._colorizingThread is None or not self._colorizingThread.isAlive())):
            text = self._getTextForParse()
            self._colorizingThread = threading.Thread (None, self._colorizeThreadFunc, args=(text,))
            self._colorizingThread.start()


    def _colorizeThreadFunc (self, text):
        stylebytes = self.getStyleBytes (text)
        indicatorsbytes = self.getIndcatorsStyleBytes (text)
        event = ApplyStyleEvent (text=text,
                                 stylebytes=stylebytes,
                                 indicatorsbytes = indicatorsbytes)
        wx.PostEvent (self, event)


    def _onApplyStyle (self, event):
        if event.text == self._getTextForParse():
            textlength = self.calcByteLen (event.text)
            self.__stylebytes = [0] * textlength

            if event.stylebytes is not None:
                self.__stylebytes = [item1 | item2
                                     for item1, item2
                                     in zip (self.__stylebytes, event.stylebytes)]

            if event.indicatorsbytes is not None:
                self.__stylebytes = [item1 | item2
                                     for item1, item2
                                     in zip (self.__stylebytes, event.indicatorsbytes)]

            stylebytesstr = "".join ([chr(byte) for byte in self.__stylebytes])

            if event.stylebytes is not None:
                self.textCtrl.StartStyling (0, 0xff ^ wx.stc.STC_INDICS_MASK)
                self.textCtrl.SetStyleBytes (len (stylebytesstr), stylebytesstr)

            if event.indicatorsbytes is not None:
                self.textCtrl.StartStyling (0, wx.stc.STC_INDICS_MASK)
                self.textCtrl.SetStyleBytes (len (stylebytesstr), stylebytesstr)

            self._styleSet = True


    def getStyleBytes (self, text):
        """
        Функция должна возвращать список байт, описывающих раскраску (стили)
        для текста text (за исключением индикаторов).
        Функцию нужно переопределить, если используется собственная раскраска текста.
        Если функция возвращает None, то раскраска синтаксиса не применяется.
        """
        return None


    def getIndcatorsStyleBytes (self, text):
        """
        Функция должна возвращать список байт, описывающих раскраску (стили индикаторов) для текста text.
        Функцию нужно переопределить, если используются индикаторы.
        Если функция возвращает None, то раскраска индикаторов не применяется.
        """
        return None


    def getSpellChecker (self):
        langlist = self._getDictsFromConfig()
        spellDirList = outwiker.core.system.getSpellDirList()

        spellChecker = SpellChecker (Application, langlist, spellDirList)
        spellChecker.addCustomDict (os.path.join (spellDirList[-1], CUSTOM_DICT_FILE_NAME))

        return spellChecker


    def _getDictsFromConfig (self):
        dictsStr = self._config.spellCheckerDicts.value
        return [item.strip()
                for item
                in dictsStr.split(',')
                if item.strip()]


    def _getMenu (self):
        """
        pos_byte - nearest text position (in bytes) where was produce a click
        """
        self._popupMenu.RefreshItems ()
        return self._popupMenu


    def __onContextMenu (self, event):
        point = self.textCtrl.ScreenToClient (event.GetPosition())
        pos_byte = self.textCtrl.PositionFromPoint(point)

        popupMenu = self._getMenu ()
        self._appendSpellItems (popupMenu, pos_byte)

        Application.onEditorPopupMenu (
            Application.selectedPage,
            EditorPopupMenuParams (self, popupMenu, point, pos_byte)
        )

        self.textCtrl.PopupMenu(popupMenu)


    def getCachedStyleBytes (self):
        return self.__stylebytes


    def __onAddWordToDict (self, event):
        if self._spellErrorText is not None:
            self.__addWordToDict (self._spellErrorText)


    def __onAddWordLowerToDict (self, event):
        if self._spellErrorText is not None:
            self.__addWordToDict (self._spellErrorText.lower())


    def __addWordToDict (self, word):
        self._spellChecker.addToCustomDict (0, word)
        self._spellErrorText = None
        self._styleSet = False


    def _appendSpellItems (self, menu, pos_byte):
        stylebytes = self.getCachedStyleBytes()
        if stylebytes is None:
            return

        stylebytes_len = len (stylebytes)

        if (stylebytes is None or
                pos_byte >= stylebytes_len or
                stylebytes[pos_byte] & self.SPELL_ERROR_INDICATOR_MASK == 0):
            return

        endSpellError = startSpellError = pos_byte

        while (startSpellError >= 0 and
               stylebytes[startSpellError] & self.SPELL_ERROR_INDICATOR_MASK != 0):
            startSpellError -= 1


        while (endSpellError < stylebytes_len and
               stylebytes[endSpellError] & self.SPELL_ERROR_INDICATOR_MASK != 0):
            endSpellError += 1

        self._spellStartByteError = startSpellError + 1
        self._spellEndByteError = endSpellError
        self._spellErrorText = self.textCtrl.GetTextRange (self._spellStartByteError, self._spellEndByteError)
        self._spellSuggestList = self._spellChecker.getSuggest (self._spellErrorText)[:self._spellMaxSuggest]

        menu.AppendSeparator()
        menu.AppendSpellSubmenu (self._spellErrorText, self._spellSuggestList)


    def __onSpellSuggest (self, event):
        assert event.GetId() in TextEditorMenu.ID_SUGGESTS

        index = TextEditorMenu.ID_SUGGESTS.index (event.GetId())
        word = self._spellSuggestList[index]

        self.textCtrl.SetSelection (self._spellStartByteError, self._spellEndByteError)
        self.textCtrl.ReplaceSelection (word)
Esempio n. 8
0
class TextEditor(wx.Panel):
    _fontConfigSection = "Font"

    def __init__(self, *args, **kwds):
        kwds["style"] = wx.TAB_TRAVERSAL
        wx.Panel.__init__(self, *args, **kwds)

        self._config = EditorConfig (Application.config)

        self._enableSpellChecking = True
        self._spellChecker = None

        self.SPELL_ERROR_INDICATOR = 0

        self._spellErrorText = None
        self._spellSuggestList = []

        self._spellMaxSuggest = len (TextEditorMenu.ID_SUGGESTS)
        self._spellStartByteError = -1
        self._spellEndByteError = -1

        # Уже были установлены стили текста (раскраска)
        self._styleSet = False

        self.__stylebytes = None
        self.__indicatorsbytes = None

        # Начинаем раскраску кода не менее чем через это время с момента его изменения
        self._DELAY = timedelta (milliseconds=300)

        # Время последней модификации текста страницы.
        # Используется для замера времени после модификации, чтобы не парсить текст
        # после каждой введенной буквы
        self._lastEdit = datetime.now() - self._DELAY * 2

        self.textCtrl = StyledTextCtrl(self, -1)

        self._popupMenu = TextEditorMenu(self)

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel (self)
        self._searchPanelController = SearchReplaceController (self._searchPanel, self)
        self._searchPanel.setController (self._searchPanelController)

        self.__do_layout()
        self.__createCoders()
        self._helper = TextEditorHelper()

        self.__showlinenumbers = self._config.lineNumbers.value

        self.setDefaultSettings()
        self.__bindEvents()


    def __bindEvents (self):
        self.textCtrl.Bind(wx.EVT_MENU, self.__onCopyFromEditor, id = wx.ID_COPY)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onCutFromEditor, id = wx.ID_CUT)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onPasteToEditor, id = wx.ID_PASTE)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onUndo, id = wx.ID_UNDO)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onRedo, id = wx.ID_REDO)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onSelectAll, id = wx.ID_SELECTALL)

        self.textCtrl.Bind(wx.EVT_MENU, self.__onAddWordToDict, id = TextEditorMenu.ID_ADD_WORD)
        self.textCtrl.Bind(wx.EVT_MENU, self.__onAddWordLowerToDict, id = TextEditorMenu.ID_ADD_WORD_LOWER)

        for suggestId in TextEditorMenu.ID_SUGGESTS:
            self.textCtrl.Bind(wx.EVT_MENU, self.__onSpellSuggest, id = suggestId)

        self.textCtrl.Bind (wx.EVT_CHAR, self.__OnChar_ImeWorkaround)
        self.textCtrl.Bind (wx.EVT_KEY_DOWN, self.__onKeyDown)
        self.textCtrl.Bind (wx.EVT_CONTEXT_MENU, self.__onContextMenu)

        # self.textCtrl.Bind (wx.stc.EVT_STC_STYLENEEDED, self._onStyleNeeded)
        self.textCtrl.Bind (wx.EVT_IDLE, self._onStyleNeeded)
        self.Bind (EVT_APPLY_STYLE, self._onApplyStyle)

        # При перехвате этого сообщения в других классах, нужно вызывать event.Skip(),
        # чтобы это сообщение дошло досюда
        self.textCtrl.Bind (wx.stc.EVT_STC_CHANGE, self.__onChange)


    @property
    def config (self):
        return self._config


    @property
    def enableSpellChecking (self):
        return self._enableSpellChecking


    @enableSpellChecking.setter
    def enableSpellChecking (self, value):
        self._enableSpellChecking = value
        self._styleSet = False


    def __onChange (self, event):
        self._styleSet = False
        self._lastEdit = datetime.now()
        self.__setMarginWidth (self.textCtrl)


    @property
    def searchPanel (self):
        """
        Возвращает контроллер панели поиска
        """
        return self._searchPanelController


    def Print (self):
        selectedtext = self.textCtrl.GetSelectedText()
        text = self.textCtrl.GetText()

        printer = TextPrinter (self)
        printer.printout (text if len (selectedtext) == 0 else selectedtext)


    def __onCopyFromEditor (self, event):
        self.textCtrl.Copy()


    def __onCutFromEditor (self, event):
        self.textCtrl.Cut()


    def __onPasteToEditor (self, event):
        self.textCtrl.Paste()


    def __onUndo (self, event):
        self.textCtrl.Undo()


    def __onRedo (self, event):
        self.textCtrl.Redo()


    def __onSelectAll (self, event):
        self.textCtrl.SelectAll()


    def __do_layout(self):
        mainSizer = wx.FlexGridSizer(rows=2)
        mainSizer.AddGrowableRow(0)
        mainSizer.AddGrowableCol(0)

        mainSizer.Add(self.textCtrl, 0, wx.EXPAND, 0)
        mainSizer.Add(self._searchPanel, 0, wx.EXPAND, 0)
        self.SetSizer(mainSizer)

        self._searchPanel.Hide()
        self.Layout()


    def setDefaultSettings (self):
        """
        Установить стили и настройки по умолчанию в контрол StyledTextCtrl
        """
        self._spellChecker = self.getSpellChecker()

        size = self._config.fontSize.value
        faceName = self._config.fontName.value
        isBold = self._config.fontIsBold.value
        isItalic = self._config.fontIsItalic.value
        fontColor = self._config.fontColor.value
        backColor = self._config.backColor.value

        self.__showlinenumbers = self._config.lineNumbers.value
        self.textCtrl.SetEndAtLastLine (False)

        self.textCtrl.StyleSetSize (wx.stc.STC_STYLE_DEFAULT, size)
        self.textCtrl.StyleSetFaceName (wx.stc.STC_STYLE_DEFAULT, faceName)
        self.textCtrl.StyleSetBold (wx.stc.STC_STYLE_DEFAULT, isBold)
        self.textCtrl.StyleSetItalic (wx.stc.STC_STYLE_DEFAULT, isItalic)
        self.textCtrl.StyleSetForeground (wx.stc.STC_STYLE_DEFAULT, fontColor)
        self.textCtrl.StyleSetBackground (wx.stc.STC_STYLE_DEFAULT, backColor)

        self.textCtrl.StyleClearAll()

        self.textCtrl.SetCaretForeground (fontColor)
        self.textCtrl.SetCaretLineBack (backColor)

        # Заблокируем горячую клавишу Ctrl+D, чтобы использовать ее как добавление закладки
        self.textCtrl.CmdKeyClear (ord ("D"), wx.stc.STC_SCMOD_CTRL)
        self.textCtrl.CmdKeyClear (ord ("R"), wx.stc.STC_SCMOD_CTRL | wx.stc.STC_SCMOD_SHIFT)
        self.textCtrl.SetWrapMode (wx.stc.STC_WRAP_WORD)
        self.textCtrl.SetWrapVisualFlags (wx.stc.STC_WRAPVISUALFLAG_END)

        self.__setMarginWidth (self.textCtrl)
        self.textCtrl.SetTabWidth (self._config.tabWidth.value)

        self.enableSpellChecking = self._config.spellEnabled.value
        self._spellChecker.skipWordsWithNumbers = self.config.spellSkipDigits.value

        if self._config.homeEndKeys.value == EditorConfig.HOME_END_OF_LINE:
            # Клавиши Home / End переносят курсор на начало / конец строки
            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_HOME,
                                        0,
                                        wx.stc.STC_CMD_HOMEDISPLAY)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_HOME,
                                        wx.stc.STC_SCMOD_ALT,
                                        wx.stc.STC_CMD_HOME)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_END,
                                        0,
                                        wx.stc.STC_CMD_LINEENDDISPLAY)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_END,
                                        wx.stc.STC_SCMOD_ALT,
                                        wx.stc.STC_CMD_LINEEND)
        else:
            # Клавиши Home / End переносят курсор на начало / конец абзаца
            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_HOME,
                                        0,
                                        wx.stc.STC_CMD_HOME)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_HOME,
                                        wx.stc.STC_SCMOD_ALT,
                                        wx.stc.STC_CMD_HOMEDISPLAY)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_END,
                                        0,
                                        wx.stc.STC_CMD_LINEEND)

            self.textCtrl.CmdKeyAssign (wx.stc.STC_KEY_END,
                                        wx.stc.STC_SCMOD_ALT,
                                        wx.stc.STC_CMD_LINEENDDISPLAY)

        self.textCtrl.IndicatorSetStyle(self.SPELL_ERROR_INDICATOR, wx.stc.STC_INDIC_SQUIGGLE)
        self.textCtrl.IndicatorSetForeground(self.SPELL_ERROR_INDICATOR, "red")
        self._styleSet = False


    def __setMarginWidth (self, editor):
        """
        Установить размер левой области, где пишутся номера строк в зависимости от шрифта
        """
        if self.__showlinenumbers:
            editor.SetMarginWidth (0, self.__getMarginWidth())
            editor.SetMarginWidth (1, 5)
        else:
            editor.SetMarginWidth (0, 0)
            editor.SetMarginWidth (1, 8)


    def __getMarginWidth (self):
        """
        Расчет размера серой области с номером строк
        """
        fontSize = self._config.fontSize.value
        linescount = len (self.GetText().split("\n"))

        if linescount == 0:
            width = 10
        else:
            # Количество десятичных цифр в числе строк
            digits = int (math.log10 (linescount) + 1)
            width = int (1.2 * fontSize * digits)

        return width


    def getPosChar (self, posBytes):
        return len (self.textCtrl.GetTextRange (0, posBytes))


    def __createCoders (self):
        encoding = outwiker.core.system.getOS().inputEncoding
        self.mbcsEnc = codecs.getencoder(encoding)


    def __onKeyDown (self, event):
        key = event.GetKeyCode()

        if key == wx.WXK_ESCAPE:
            self._searchPanel.Close()

        event.Skip()


    def __OnChar_ImeWorkaround(self, evt):
        """
        Обработка клавиш вручную, чтобы не было проблем с вводом русских букв в Linux.
        Основа кода взята из Wikidpad (WikiTxtCtrl.py -> OnChar_ImeWorkaround)
        """
        key = evt.GetKeyCode()

        # Return if this doesn't seem to be a real character input
        if evt.ControlDown() or (0 < key < 32):
            evt.Skip()
            return

        if key >= wx.WXK_START and evt.GetUnicodeKey() != key:
            evt.Skip()
            return

        unichar = unichr(evt.GetUnicodeKey())

        self.textCtrl.ReplaceSelection(self.mbcsEnc (unichar, "replace")[0])


    def AddText (self, text):
        self.textCtrl.AddText (text)


    def replaceText (self, text):
        self.textCtrl.ReplaceSelection (text)


    def turnText (self, lefttext, righttext):
        selText = self.textCtrl.GetSelectedText()
        newtext = lefttext + selText + righttext
        self.textCtrl.ReplaceSelection (newtext)

        currPos = self.GetSelectionEnd()
        if len (selText) == 0:
            """
            Если не оборачиваем текст, а делаем пустой тег, то поместим каретку до закрывающегося тега
            """
            newpos = currPos - len (righttext)
            self.SetSelection (newpos, newpos)
        else:
            self.SetSelection (currPos - len (selText) - len (righttext),
                               currPos - len (righttext))


    def escapeHtml (self):
        selText = self.textCtrl.GetSelectedText()
        text = cgi.escape (selText, quote=False)
        self.textCtrl.ReplaceSelection (text)


    def SetReadOnly (self, readonly):
        self.textCtrl.SetReadOnly (readonly)


    def GetReadOnly (self):
        return self.textCtrl.GetReadOnly()


    def GetText(self):
        return self.textCtrl.GetText()


    def SetText (self, text):
        self.textCtrl.SetText (text)


    def EmptyUndoBuffer (self):
        self.textCtrl.EmptyUndoBuffer()


    def GetSelectedText (self):
        return self.textCtrl.GetSelectedText()


    def SetSelection (self, start, end):
        """
        start и end в символах, а не в байтах, в отличие от исходного StyledTextCtrl
        """
        startText = self.GetText()[:start]
        endText = self.GetText()[:end]

        firstByte = self._helper.calcByteLen (startText)
        endByte = self._helper.calcByteLen (endText)

        self.textCtrl.SetSelection (firstByte, endByte)


    def GetCurrentPosition (self):
        """
        Возвращает номер символа (а не байта), перед которых находится курсор
        """
        return self.__calcCharPos (self.textCtrl.GetCurrentPos())


    def GetSelectionStart (self):
        """
        Возвращает позицию начала выбранной области в символах, а не в байтах
        """
        return self.__calcCharPos (self.textCtrl.GetSelectionStart())


    def GetSelectionEnd (self):
        """
        Возвращает позицию конца выбранной области в символах, а не в байтах
        """
        return self.__calcCharPos (self.textCtrl.GetSelectionEnd())


    def SetFocus (self):
        self.textCtrl.SetFocus()
        self.textCtrl.SetSTCFocus(True)


    def __calcCharPos (self, pos_bytes):
        """
        Пересчет позиции в байтах в позицию в символах
        """
        text_left = self.textCtrl.GetTextRange (0, pos_bytes)
        currpos = len (text_left)
        return currpos


    def _getTextForParse (self):
        # Табуляция в редакторе считается за несколько символов
        return self.textCtrl.GetText().replace ("\t", " ")


    def runSpellChecking (self, stylelist, fullText, start, end):
        errors = self._spellChecker.findErrors (fullText[start: end])

        for word, err_start, err_end in errors:
            self._helper.setSpellError (stylelist,
                                        fullText,
                                        err_start + start,
                                        err_end + start)


    def _onStyleNeeded (self, event):
        if (not self._styleSet and
                datetime.now() - self._lastEdit >= self._DELAY):
            page = Application.selectedPage
            text = self._getTextForParse()
            params = EditorStyleNeededParams (self,
                                              text,
                                              self._enableSpellChecking)
            Application.onEditorStyleNeeded (page, params)
            self._styleSet = True


    def _onApplyStyle (self, event):
        if event.text == self._getTextForParse():
            startByte = self._helper.calcBytePos (event.text, event.start)
            endByte = self._helper.calcBytePos (event.text, event.end)
            lenBytes = endByte - startByte

            textlength = self._helper.calcByteLen (event.text)
            self.__stylebytes = [0] * textlength

            if event.stylebytes is not None:
                self.__stylebytes = event.stylebytes

            if event.indicatorsbytes is not None:
                self.__stylebytes = [item1 | item2
                                     for item1, item2
                                     in zip (self.__stylebytes, event.indicatorsbytes)]

            stylebytesstr = "".join ([chr(byte) for byte in self.__stylebytes])


            if event.stylebytes is not None:
                self.textCtrl.StartStyling (startByte, 0xff ^ wx.stc.STC_INDICS_MASK)
                self.textCtrl.SetStyleBytes (lenBytes, stylebytesstr[startByte:endByte])

            if event.indicatorsbytes is not None:
                self.textCtrl.StartStyling (startByte, wx.stc.STC_INDICS_MASK)
                self.textCtrl.SetStyleBytes (lenBytes, stylebytesstr[startByte:endByte])

            self._styleSet = True


    def getSpellChecker (self):
        langlist = self._getDictsFromConfig()
        spellDirList = outwiker.core.system.getSpellDirList()

        spellChecker = SpellChecker (Application, langlist, spellDirList)
        spellChecker.addCustomDict (os.path.join (spellDirList[-1], CUSTOM_DICT_FILE_NAME))

        return spellChecker


    def _getDictsFromConfig (self):
        dictsStr = self._config.spellCheckerDicts.value
        return [item.strip()
                for item
                in dictsStr.split(',')
                if item.strip()]


    def _getMenu (self):
        """
        pos_byte - nearest text position (in bytes) where was produce a click
        """
        self._popupMenu.RefreshItems ()
        return self._popupMenu


    def __onContextMenu (self, event):
        point = self.textCtrl.ScreenToClient (event.GetPosition())
        pos_byte = self.textCtrl.PositionFromPoint(point)

        popupMenu = self._getMenu ()
        self._appendSpellItems (popupMenu, pos_byte)

        Application.onEditorPopupMenu (
            Application.selectedPage,
            EditorPopupMenuParams (self, popupMenu, point, pos_byte)
        )

        self.textCtrl.PopupMenu(popupMenu)


    def getCachedStyleBytes (self):
        return self.__stylebytes


    def __onAddWordToDict (self, event):
        if self._spellErrorText is not None:
            self.__addWordToDict (self._spellErrorText)


    def __onAddWordLowerToDict (self, event):
        if self._spellErrorText is not None:
            self.__addWordToDict (self._spellErrorText.lower())


    def __addWordToDict (self, word):
        self._spellChecker.addToCustomDict (0, word)
        self._spellErrorText = None
        self._styleSet = False


    def _appendSpellItems (self, menu, pos_byte):
        stylebytes = self.getCachedStyleBytes()
        if stylebytes is None:
            return

        stylebytes_len = len (stylebytes)

        if (stylebytes is None or
                pos_byte >= stylebytes_len or
                stylebytes[pos_byte] & self._helper.SPELL_ERROR_INDICATOR_MASK == 0):
            return

        endSpellError = startSpellError = pos_byte

        while (startSpellError >= 0 and
               stylebytes[startSpellError] & self._helper.SPELL_ERROR_INDICATOR_MASK != 0):
            startSpellError -= 1


        while (endSpellError < stylebytes_len and
               stylebytes[endSpellError] & self._helper.SPELL_ERROR_INDICATOR_MASK != 0):
            endSpellError += 1

        self._spellStartByteError = startSpellError + 1
        self._spellEndByteError = endSpellError
        self._spellErrorText = self.textCtrl.GetTextRange (self._spellStartByteError, self._spellEndByteError)
        self._spellSuggestList = self._spellChecker.getSuggest (self._spellErrorText)[:self._spellMaxSuggest]

        menu.AppendSeparator()
        menu.AppendSpellSubmenu (self._spellErrorText, self._spellSuggestList)


    def __onSpellSuggest (self, event):
        assert event.GetId() in TextEditorMenu.ID_SUGGESTS

        index = TextEditorMenu.ID_SUGGESTS.index (event.GetId())
        word = self._spellSuggestList[index]

        self.textCtrl.SetSelection (self._spellStartByteError, self._spellEndByteError)
        self.textCtrl.ReplaceSelection (word)
Esempio n. 9
0
class TextEditorBase(wx.Panel):
    '''
    Added in outwiker.gui 1.3
    '''
    def __init__(self, parent):
        super(TextEditorBase, self).__init__(parent, style=wx.TAB_TRAVERSAL)
        self.textCtrl = StyledTextCtrl(self, -1)

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel(self)
        self._searchPanelController = SearchReplaceController(
            self._searchPanel,
            self)
        self._searchPanel.setController(self._searchPanelController)

        self._do_layout()

        self.__createCoders()
        self._helper = TextEditorHelper()
        self._bind()
        self._setDefaultSettings()

    def _bind(self):
        self.textCtrl.Bind(wx.EVT_KEY_DOWN, self.__onKeyDown)

    def _do_layout(self):
        mainSizer = wx.FlexGridSizer(rows=2)
        mainSizer.AddGrowableRow(0)
        mainSizer.AddGrowableCol(0)

        mainSizer.Add(self.textCtrl, 0, wx.EXPAND, 0)
        mainSizer.Add(self._searchPanel, 0, wx.EXPAND, 0)
        self.SetSizer(mainSizer)

        self._searchPanel.Hide()
        self.Layout()

    def __createCoders(self):
        encoding = outwiker.core.system.getOS().inputEncoding
        self.mbcsEnc = codecs.getencoder(encoding)

    def __onKeyDown(self, event):
        key = event.GetKeyCode()

        if key == wx.WXK_ESCAPE:
            self._searchPanel.Close()

        event.Skip()

    def _setDefaultSettings(self):
        self.textCtrl.SetEndAtLastLine(False)
        self.textCtrl.StyleClearAll()
        self.textCtrl.SetWrapMode(wx.stc.STC_WRAP_WORD)
        self.textCtrl.SetWrapVisualFlags(wx.stc.STC_WRAPVISUALFLAG_END)
        self.textCtrl.SetTabWidth(4)

    @property
    def searchPanel(self):
        """
        Возвращает контроллер панели поиска
        """
        return self._searchPanelController

    def Print(self):
        selectedtext = self.textCtrl.GetSelectedText()
        text = self.textCtrl.GetText()

        printer = TextPrinter(self)
        printer.printout(text if len(selectedtext) == 0 else selectedtext)

    def getPosChar(self, posBytes):
        return len(self.textCtrl.GetTextRange(0, posBytes))

    def AddText(self, text):
        self.textCtrl.AddText(text)

    def replaceText(self, text):
        self.textCtrl.ReplaceSelection(text)

    def toddleLinePrefix(self, line, prefix):
        """
        If line with number "line" starts with prefix, prefix will be removed
        else prefix will be added.

        Added in OutWiker 2.0.0.795.
        """
        assert line < self.GetLineCount()
        line_text = self.GetLine(line)
        if line_text.startswith(prefix):
            line_text = line_text[len(prefix):]
        else:
            line_text = prefix + line_text
        self.SetLine(line, line_text)

    def toddleSelectedLinesPrefix(self, prefix):
        """
        Apply toddleLinePrefix method to selected lines

        Added in OutWiker 2.0.0.795.
        """
        self.BeginUndoAction()
        old_sel_start = self.GetSelectionStart()
        old_sel_end = self.GetSelectionEnd()

        first_line, last_line = self.GetSelectionLines()
        map(lambda n: self.toddleLinePrefix(n, prefix),
            xrange(first_line, last_line + 1))

        if old_sel_start != old_sel_end:
            new_sel_start = self.GetLineStartPosition(first_line)
            new_sel_end = self.GetLineEndPosition(last_line)
        else:
            new_sel_start = new_sel_end = self.GetLineEndPosition(last_line)

        self.SetSelection(new_sel_start, new_sel_end)
        self.EndUndoAction()

    def turnText(self, lefttext, righttext):
        selText = self.textCtrl.GetSelectedText()
        newtext = lefttext + selText + righttext
        self.textCtrl.ReplaceSelection(newtext)

        currPos = self.GetSelectionEnd()
        if len(selText) == 0:
            """
            Если не оборачиваем текст, а делаем пустой тег, то поместим каретку до закрывающегося тега
            """
            newpos = currPos - len(righttext)
            self.SetSelection(newpos, newpos)
        else:
            self.SetSelection(currPos - len(selText) - len(righttext),
                              currPos - len(righttext))

    def escapeHtml(self):
        selText = self.textCtrl.GetSelectedText()
        text = cgi.escape(selText, quote=False)
        self.textCtrl.ReplaceSelection(text)

    def SetReadOnly(self, readonly):
        self.textCtrl.SetReadOnly(readonly)

    def GetReadOnly(self):
        return self.textCtrl.GetReadOnly()

    def GetText(self):
        return self.textCtrl.GetText()

    def SetText(self, text):
        self.textCtrl.SetText(text)

    def EmptyUndoBuffer(self):
        self.textCtrl.EmptyUndoBuffer()

    def GetSelectedText(self):
        return self.textCtrl.GetSelectedText()

    def GetCurrentLine(self):
        return self.textCtrl.GetCurrentLine()

    def ScrollToLine(self, line):
        self.textCtrl.ScrollToLine(line)

    def SetSelection(self, start, end):
        """
        start и end в символах, а не в байтах, в отличие от исходного
        StyledTextCtrl
        """
        startText = self.GetText()[:start]
        endText = self.GetText()[:end]

        firstByte = self._helper.calcByteLen(startText)
        endByte = self._helper.calcByteLen(endText)

        self.textCtrl.SetSelection(firstByte, endByte)

    def GotoPos(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        self.textCtrl.GotoPos(pos_bytes)

    def GetCurrentPosition(self):
        """
        Возвращает номер символа(а не байта), перед которых находится курсор
        """
        return self._calcCharPos(self.textCtrl.GetCurrentPos())

    def GetSelectionStart(self):
        """
        Возвращает позицию начала выбранной области в символах, а не в байтах
        """
        return self._calcCharPos(self.textCtrl.GetSelectionStart())

    def GetSelectionLines(self):
        """
        Return tuple (first selected line, last selected line)

        Added in OutWiker 2.0.0.795.
        """
        start_bytes = self.textCtrl.GetSelectionStart()
        end_bytes = self.textCtrl.GetSelectionEnd()
        return (self.textCtrl.LineFromPosition(start_bytes),
                self.textCtrl.LineFromPosition(end_bytes))

    def GetSelectionEnd(self):
        """
        Возвращает позицию конца выбранной области в символах, а не в байтах
        """
        return self._calcCharPos(self.textCtrl.GetSelectionEnd())

    def SetFocus(self):
        self.textCtrl.SetFocus()
        self.textCtrl.SetSTCFocus(True)

    def GetLine(self, line):
        """
        Return line with the "line" number. \n included.
        Added in OutWiker 2.0.0.795
        """
        return self.textCtrl.GetLine(line)

    def SetLine(self, line, newline):
        """
        Replace line with the number "line" newline.
        Newline will be ended with "\n" else line will be joined with next line

        Added in OutWiker 2.0.0.795
        """
        linecount = self.GetLineCount()
        assert line < linecount

        line_start_bytes = self.textCtrl.PositionFromLine(line)
        line_end_bytes = self.textCtrl.PositionFromLine(line + 1)
        self.textCtrl.Replace(line_start_bytes, line_end_bytes, newline)

    def GetLineCount(self):
        return self.textCtrl.GetLineCount()

    def GetLineStartPosition(self, line):
        """
        Retrieve the position at the start of a line in symbols (not bytes)

        Added in OutWiker 2.0.0.795
        """
        return self._calcCharPos(self.textCtrl.PositionFromLine(line))

    def GetLineEndPosition(self, line):
        """
        Get the position after the last visible characters on a line
            in symbols (not bytes)

        Added in OutWiker 2.0.0.795
        """
        return self._calcCharPos(self.textCtrl.GetLineEndPosition(line))

    def MoveSelectedLinesUp(self):
        """
        Move the selected lines up one line,
        shifting the line above after the selection.

        Added in OutWiker 2.0.0.795
        """
        self.textCtrl.MoveSelectedLinesUp()

    def MoveSelectedLinesDown(self):
        """
        Move the selected lines down one line,
        shifting the line below before the selection.

        Added in OutWiker 2.0.0.795
        """
        self.textCtrl.MoveSelectedLinesDown()

    def LineDuplicate(self):
        """
        Duplicate the current line.

        Added in OutWiker 2.0.0.795
        """
        self.textCtrl.LineDuplicate()

    def LineDelete(self):
        """
        Delete the current line.

        Added in OutWiker 2.0.0.795
        """
        self.textCtrl.LineDelete()

    def BeginUndoAction(self):
        """
        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.BeginUndoAction()

    def EndUndoAction(self):
        """
        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.EndUndoAction()

    def JoinLines(self):
        """
        Join selected lines

        Added in OutWiker 2.0.0.797
        """
        first_line, last_line = self.GetSelectionLines()
        if first_line != last_line:
            last_line -= 1

        self.BeginUndoAction()

        for _ in range(first_line, last_line + 1):
            line = self.GetLine(first_line).replace(u'\r\n', u'\n')
            if line.endswith(u'\n'):
                newline = line[:-1]
                self.SetLine(first_line, newline)

        new_sel_pos = self.GetLineEndPosition(first_line)
        self.SetSelection(new_sel_pos, new_sel_pos)

        self.EndUndoAction()

    def DelWordLeft(self):
        """
        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.DelWordLeft()

    def DelWordRight(self):
        """
        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.DelWordRight()

    def DelLineLeft(self):
        """
        Delete back from the current position to the start of the line

        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.DelLineLeft()

    def DelLineRight(self):
        """
        Delete forwards from the current position to the end of the line

        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.DelLineRight()

    def WordLeft(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordLeft()

    def WordRight(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordRight()

    def WordLeftEnd(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordLeftEnd()

    def WordRightEnd(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordRightEnd()

    def WordLeftExtend(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordLeftExtend()

    def WordRightExtend(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordRightExtend()

    def GotoWordStart(self):
        """
        Added in outwiker.gui 1.2
        """
        self.WordRight()
        self.WordLeft()

    def GotoWordEnd(self):
        """
        Added in outwiker.gui 1.2
        """
        self.WordLeftEnd()
        self.WordRightEnd()

    def ScrollLineToCursor(self):
        """
        Added in outwiker.gui 1.1
        """
        maxlines = self.textCtrl.LinesOnScreen()
        line = self.GetCurrentLine()
        if line >= maxlines:
            delta = min(10, maxlines / 3)
            line -= delta
            if line < 0:
                line = 0
            self.ScrollToLine(line)

    def WordStartPosition(self, pos):
        """
        Added in outwiker.gui 1.2
        """
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        result_bytes = self.textCtrl.WordStartPosition(pos_bytes, True)
        return self.getPosChar(result_bytes)

    def WordEndPosition(self, pos):
        """
        Added in outwiker.gui 1.2
        """
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        result_bytes = self.textCtrl.WordEndPosition(pos_bytes, True)
        return self.getPosChar(result_bytes)

    def GetWord(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        word_start_bytes = self.textCtrl.WordStartPosition(pos_bytes, True)
        word_end_bytes = self.textCtrl.WordEndPosition(pos_bytes, True)
        word = self.textCtrl.GetTextRange(word_start_bytes, word_end_bytes)
        return word

    def _calcCharPos(self, pos_bytes):
        """
        Пересчет позиции в байтах в позицию в символах
        """
        text_left = self.textCtrl.GetTextRange(0, pos_bytes)
        currpos = len(text_left)
        return currpos

    def _getTextForParse(self):
        # Табуляция в редакторе считается за несколько символов
        return self.textCtrl.GetText().replace("\t", " ")
Esempio n. 10
0
class TextEditor(wx.Panel):
    _fontConfigSection = "Font"

    def __init__(self, *args, **kwds):
        kwds["style"] = wx.TAB_TRAVERSAL
        wx.Panel.__init__(self, *args, **kwds)

        self._config = EditorConfig(Application.config)

        self._enableSpellChecking = True
        self._spellChecker = None

        self.SPELL_ERROR_INDICATOR = 0

        self._spellErrorText = None
        self._spellSuggestList = []

        self._spellMaxSuggest = 10
        self._suggestMenuItems = []
        self._spellStartByteError = -1
        self._spellEndByteError = -1

        # Уже были установлены стили текста(раскраска)
        self._styleSet = False

        self.__stylebytes = None
        self.__indicatorsbytes = None

        # Начинаем раскраску кода не менее чем через это время
        # с момента его изменения
        self._DELAY = timedelta(milliseconds=300)

        # Время последней модификации текста страницы.
        # Используется для замера времени после модификации,
        # чтобы не парсить текст после каждой введенной буквы
        self._lastEdit = datetime.now() - self._DELAY * 2

        self.textCtrl = StyledTextCtrl(self, -1)

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel(self)
        self._searchPanelController = SearchReplaceController(
            self._searchPanel, self)
        self._searchPanel.setController(self._searchPanelController)

        self.__do_layout()
        self.__createCoders()
        self._helper = TextEditorHelper()

        self.__showlinenumbers = self._config.lineNumbers.value

        self.setDefaultSettings()
        self.__bindEvents()


    def __bindEvents(self):
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onCopyFromEditor,
                           id=wx.ID_COPY)
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onCutFromEditor,
                           id=wx.ID_CUT)
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onPasteToEditor,
                           id=wx.ID_PASTE)
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onUndo,
                           id=wx.ID_UNDO)
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onRedo,
                           id=wx.ID_REDO)
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onSelectAll,
                           id=wx.ID_SELECTALL)

        self.textCtrl.Bind(wx.EVT_KEY_DOWN, self.__onKeyDown)
        self.textCtrl.Bind(wx.EVT_CONTEXT_MENU, self.__onContextMenu)

        # self.textCtrl.Bind(wx.stc.EVT_STC_STYLENEEDED, self._onStyleNeeded)
        self.textCtrl.Bind(wx.EVT_IDLE, self._onStyleNeeded)
        self.Bind(EVT_APPLY_STYLE, self._onApplyStyle)

        # При перехвате этого сообщения в других классах,
        # нужно вызывать event.Skip(), чтобы это сообщение дошло сюда
        self.textCtrl.Bind(wx.stc.EVT_STC_CHANGE, self.__onChange)

    @property
    def config(self):
        return self._config

    @property
    def enableSpellChecking(self):
        return self._enableSpellChecking

    @enableSpellChecking.setter
    def enableSpellChecking(self, value):
        self._enableSpellChecking = value
        self._styleSet = False

    def __onChange(self, event):
        self._styleSet = False
        self._lastEdit = datetime.now()
        self.__setMarginWidth(self.textCtrl)

    @property
    def searchPanel(self):
        """
        Возвращает контроллер панели поиска
        """
        return self._searchPanelController

    def Print(self):
        selectedtext = self.textCtrl.GetSelectedText()
        text = self.textCtrl.GetText()

        printer = TextPrinter(self)
        printer.printout(text if len(selectedtext) == 0 else selectedtext)

    def __onCopyFromEditor(self, event):
        self.textCtrl.Copy()

    def __onCutFromEditor(self, event):
        self.textCtrl.Cut()

    def __onPasteToEditor(self, event):
        self.textCtrl.Paste()

    def __onUndo(self, event):
        self.textCtrl.Undo()

    def __onRedo(self, event):
        self.textCtrl.Redo()

    def __onSelectAll(self, event):
        self.textCtrl.SelectAll()

    def __do_layout(self):
        mainSizer = wx.FlexGridSizer(rows=2)
        mainSizer.AddGrowableRow(0)
        mainSizer.AddGrowableCol(0)

        mainSizer.Add(self.textCtrl, 0, wx.EXPAND, 0)
        mainSizer.Add(self._searchPanel, 0, wx.EXPAND, 0)
        self.SetSizer(mainSizer)

        self._searchPanel.Hide()
        self.Layout()

    def setDefaultSettings(self):
        """
        Установить стили и настройки по умолчанию в контрол StyledTextCtrl
        """
        self._spellChecker = self.getSpellChecker()

        size = self._config.fontSize.value
        faceName = self._config.fontName.value
        isBold = self._config.fontIsBold.value
        isItalic = self._config.fontIsItalic.value
        fontColor = self._config.fontColor.value
        backColor = self._config.backColor.value

        self.__showlinenumbers = self._config.lineNumbers.value
        self.textCtrl.SetEndAtLastLine(False)

        self.textCtrl.StyleSetSize(wx.stc.STC_STYLE_DEFAULT, size)
        self.textCtrl.StyleSetFaceName(wx.stc.STC_STYLE_DEFAULT, faceName)
        self.textCtrl.StyleSetBold(wx.stc.STC_STYLE_DEFAULT, isBold)
        self.textCtrl.StyleSetItalic(wx.stc.STC_STYLE_DEFAULT, isItalic)
        self.textCtrl.StyleSetForeground(wx.stc.STC_STYLE_DEFAULT, fontColor)
        self.textCtrl.StyleSetBackground(wx.stc.STC_STYLE_DEFAULT, backColor)

        self.textCtrl.StyleClearAll()

        self.textCtrl.SetCaretForeground(fontColor)
        self.textCtrl.SetCaretLineBack(backColor)
        self.textCtrl.SetWrapMode(wx.stc.STC_WRAP_WORD)
        self.textCtrl.SetWrapVisualFlags(wx.stc.STC_WRAPVISUALFLAG_END)

        self._setDefaultHotKeys()

        self.__setMarginWidth(self.textCtrl)
        self.textCtrl.SetTabWidth(self._config.tabWidth.value)

        self.enableSpellChecking = self._config.spellEnabled.value
        self._spellChecker.skipWordsWithNumbers = self.config.spellSkipDigits.value


        self.textCtrl.IndicatorSetStyle(self.SPELL_ERROR_INDICATOR,
                                        wx.stc.STC_INDIC_SQUIGGLE)
        self.textCtrl.IndicatorSetForeground(self.SPELL_ERROR_INDICATOR, "red")
        self._styleSet = False


    def _setDefaultHotKeys(self):
        self.textCtrl.CmdKeyClearAll()

        # Code from Wikidpad sources
        # Default mapping based on Scintilla's "KeyMap.cxx" file
        defaultHotKeys = (
            (wx.stc.STC_KEY_DOWN,        wx.stc.STC_SCMOD_NORM,     wx.stc.STC_CMD_LINEDOWN),
            (wx.stc.STC_KEY_DOWN,        wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_LINEDOWNEXTEND),
            (wx.stc.STC_KEY_DOWN,        wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_LINESCROLLDOWN),
            (wx.stc.STC_KEY_UP,          wx.stc.STC_SCMOD_NORM,     wx.stc.STC_CMD_LINEUP),
            (wx.stc.STC_KEY_UP,          wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_LINEUPEXTEND),
            (wx.stc.STC_KEY_UP,          wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_LINESCROLLUP),
            (ord('['),            wx.stc.STC_SCMOD_CTRL,            wx.stc.STC_CMD_PARAUP),
            (ord('['),            wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_PARAUPEXTEND),
            (ord(']'),            wx.stc.STC_SCMOD_CTRL,        wx.stc.STC_CMD_PARADOWN),
            (ord(']'),            wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_PARADOWNEXTEND),
            (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_CHARLEFT),
            (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_CHARLEFTEXTEND),
            # (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDLEFT),
            # (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDLEFTEXTEND),
            (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_CHARRIGHT),
            (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_CHARRIGHTEXTEND),
            # (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDRIGHT),
            # (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDRIGHTEXTEND),
            (ord('/'),        wx.stc.STC_SCMOD_CTRL,        wx.stc.STC_CMD_WORDPARTLEFT),
            (ord('/'),        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDPARTLEFTEXTEND),
            (ord('\\'),        wx.stc.STC_SCMOD_CTRL,        wx.stc.STC_CMD_WORDPARTRIGHT),
            (ord('\\'),        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDPARTRIGHTEXTEND),
            (wx.stc.STC_KEY_HOME,        wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_VCHOME),
            (wx.stc.STC_KEY_HOME,         wx.stc.STC_SCMOD_SHIFT,     wx.stc.STC_CMD_VCHOMEEXTEND),
            (wx.stc.STC_KEY_HOME,         wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DOCUMENTSTART),
            (wx.stc.STC_KEY_HOME,         wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DOCUMENTSTARTEXTEND),
            (wx.stc.STC_KEY_HOME,         wx.stc.STC_SCMOD_ALT,     wx.stc.STC_CMD_HOMEDISPLAY),
            (wx.stc.STC_KEY_END,         wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_LINEEND),
            (wx.stc.STC_KEY_END,         wx.stc.STC_SCMOD_SHIFT,     wx.stc.STC_CMD_LINEENDEXTEND),
            (wx.stc.STC_KEY_END,         wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DOCUMENTEND),
            (wx.stc.STC_KEY_END,         wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DOCUMENTENDEXTEND),
            (wx.stc.STC_KEY_END,         wx.stc.STC_SCMOD_ALT,     wx.stc.STC_CMD_LINEENDDISPLAY),
            (wx.stc.STC_KEY_PRIOR,        wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_PAGEUP),
            (wx.stc.STC_KEY_PRIOR,        wx.stc.STC_SCMOD_SHIFT,     wx.stc.STC_CMD_PAGEUPEXTEND),
            (wx.stc.STC_KEY_NEXT,         wx.stc.STC_SCMOD_NORM,     wx.stc.STC_CMD_PAGEDOWN),
            (wx.stc.STC_KEY_NEXT,         wx.stc.STC_SCMOD_SHIFT,     wx.stc.STC_CMD_PAGEDOWNEXTEND),
            (wx.stc.STC_KEY_DELETE,     wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_CLEAR),
            (wx.stc.STC_KEY_INSERT,         wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_EDITTOGGLEOVERTYPE),
            (wx.stc.STC_KEY_ESCAPE,      wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_CANCEL),
            (wx.stc.STC_KEY_BACK,        wx.stc.STC_SCMOD_NORM,     wx.stc.STC_CMD_DELETEBACK),
            (wx.stc.STC_KEY_BACK,        wx.stc.STC_SCMOD_SHIFT,     wx.stc.STC_CMD_DELETEBACK),
            (wx.stc.STC_KEY_BACK,         wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_UNDO),
            (ord('Z'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_UNDO),
            (ord('Y'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_REDO),
            (ord('A'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_SELECTALL),
            (wx.stc.STC_KEY_TAB,        wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_TAB),
            (wx.stc.STC_KEY_TAB,        wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_BACKTAB),
            (wx.stc.STC_KEY_RETURN,     wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_NEWLINE),
            (wx.stc.STC_KEY_RETURN,     wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_NEWLINE),
            (wx.stc.STC_KEY_ADD,         wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_ZOOMIN),
            (wx.stc.STC_KEY_SUBTRACT,    wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_ZOOMOUT),
            (wx.stc.STC_KEY_DOWN,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_LINEDOWNRECTEXTEND),
            (wx.stc.STC_KEY_UP,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_LINEUPRECTEXTEND),
            (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_CHARLEFTRECTEXTEND),
            (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_CHARRIGHTRECTEXTEND),
            (wx.stc.STC_KEY_HOME,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_VCHOMERECTEXTEND),
            (wx.stc.STC_KEY_END,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_LINEENDRECTEXTEND),
            (wx.stc.STC_KEY_PRIOR,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_PAGEUPRECTEXTEND),
            (wx.stc.STC_KEY_NEXT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_PAGEDOWNRECTEXTEND),

            # (wx.stc.STC_KEY_DELETE,    wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_DELLINERIGHT),
            # (wx.stc.STC_KEY_BACK,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_DELLINELEFT),
            # (wx.stc.STC_KEY_BACK,        wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DELWORDLEFT),
            # (wx.stc.STC_KEY_DELETE,     wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_DELWORDRIGHT),
            # (wx.stc.STC_KEY_DIVIDE,    wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_SETZOOM),
            #        (ord('L'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINECUT),
            #        (ord('L'),             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINEDELETE),
            #        (ord('T'),             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINECOPY),
            #        (ord('T'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINETRANSPOSE),
            #        (ord('D'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_SELECTIONDUPLICATE),
            #        (ord('U'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LOWERCASE),
            #        (ord('U'),             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_UPPERCASE),
        )

        map(lambda key: self.textCtrl.CmdKeyAssign(key[0], key[1], key[2]),
            defaultHotKeys)

        if self._config.homeEndKeys.value == EditorConfig.HOME_END_OF_LINE:
            # Клавиши Home / End переносят курсор на начало / конец строки
            self.textCtrl.CmdKeyAssign(wx.stc.STC_KEY_HOME,
                                       0,
                                       wx.stc.STC_CMD_HOMEDISPLAY)

            self.textCtrl.CmdKeyAssign(wx.stc.STC_KEY_HOME,
                                       wx.stc.STC_SCMOD_ALT,
                                       wx.stc.STC_CMD_HOME)

            self.textCtrl.CmdKeyAssign(wx.stc.STC_KEY_END,
                                       0,
                                       wx.stc.STC_CMD_LINEENDDISPLAY)

            self.textCtrl.CmdKeyAssign(wx.stc.STC_KEY_END,
                                       wx.stc.STC_SCMOD_ALT,
                                       wx.stc.STC_CMD_LINEEND)
        else:
            # Клавиши Home / End переносят курсор на начало / конец абзаца
            self.textCtrl.CmdKeyAssign(wx.stc.STC_KEY_HOME,
                                       0,
                                       wx.stc.STC_CMD_HOME)

            self.textCtrl.CmdKeyAssign(wx.stc.STC_KEY_HOME,
                                       wx.stc.STC_SCMOD_ALT,
                                       wx.stc.STC_CMD_HOMEDISPLAY)

            self.textCtrl.CmdKeyAssign(wx.stc.STC_KEY_END,
                                       0,
                                       wx.stc.STC_CMD_LINEEND)

            self.textCtrl.CmdKeyAssign(wx.stc.STC_KEY_END,
                                       wx.stc.STC_SCMOD_ALT,
                                       wx.stc.STC_CMD_LINEENDDISPLAY)

    def __setMarginWidth(self, editor):
        """
        Установить размер левой области, где пишутся номера строк в
        зависимости от шрифта
        """
        if self.__showlinenumbers:
            editor.SetMarginWidth(0, self.__getMarginWidth())
            editor.SetMarginWidth(1, 5)
        else:
            editor.SetMarginWidth(0, 0)
            editor.SetMarginWidth(1, 8)

    def __getMarginWidth(self):
        """
        Расчет размера серой области с номером строк
        """
        fontSize = self._config.fontSize.value
        linescount = len(self.GetText().split("\n"))

        if linescount == 0:
            width = 10
        else:
            # Количество десятичных цифр в числе строк
            digits = int(math.log10(linescount) + 1)
            width = int(1.2 * fontSize * digits)

        return width

    def getPosChar(self, posBytes):
        return len(self.textCtrl.GetTextRange(0, posBytes))

    def __createCoders(self):
        encoding = outwiker.core.system.getOS().inputEncoding
        self.mbcsEnc = codecs.getencoder(encoding)

    def __onKeyDown(self, event):
        key = event.GetKeyCode()

        if key == wx.WXK_ESCAPE:
            self._searchPanel.Close()

        event.Skip()

    def AddText(self, text):
        self.textCtrl.AddText(text)

    def replaceText(self, text):
        self.textCtrl.ReplaceSelection(text)

    def toddleLinePrefix(self, line, prefix):
        """
        If line with number "line" starts with prefix, prefix will be removed
        else prefix will be added.

        Added in OutWiker 2.0.0.795.
        """
        assert line < self.GetLineCount()
        line_text = self.GetLine(line)
        if line_text.startswith(prefix):
            line_text = line_text[len(prefix):]
        else:
            line_text = prefix + line_text
        self.SetLine(line, line_text)

    def toddleSelectedLinesPrefix(self, prefix):
        """
        Apply toddleLinePrefix method to selected lines

        Added in OutWiker 2.0.0.795.
        """
        self.BeginUndoAction()
        old_sel_start = self.GetSelectionStart()
        old_sel_end = self.GetSelectionEnd()

        first_line, last_line = self.GetSelectionLines()
        map(lambda n: self.toddleLinePrefix(n, prefix),
            xrange(first_line, last_line + 1))

        if old_sel_start != old_sel_end:
            new_sel_start = self.GetLineStartPosition(first_line)
            new_sel_end = self.GetLineEndPosition(last_line)
        else:
            new_sel_start = new_sel_end = self.GetLineEndPosition(last_line)

        self.SetSelection(new_sel_start, new_sel_end)
        self.EndUndoAction()

    def turnText(self, lefttext, righttext):
        selText = self.textCtrl.GetSelectedText()
        newtext = lefttext + selText + righttext
        self.textCtrl.ReplaceSelection(newtext)

        currPos = self.GetSelectionEnd()
        if len(selText) == 0:
            """
            Если не оборачиваем текст, а делаем пустой тег, то поместим каретку до закрывающегося тега
            """
            newpos = currPos - len(righttext)
            self.SetSelection(newpos, newpos)
        else:
            self.SetSelection(currPos - len(selText) - len(righttext),
                              currPos - len(righttext))

    def escapeHtml(self):
        selText = self.textCtrl.GetSelectedText()
        text = cgi.escape(selText, quote=False)
        self.textCtrl.ReplaceSelection(text)

    def SetReadOnly(self, readonly):
        self.textCtrl.SetReadOnly(readonly)

    def GetReadOnly(self):
        return self.textCtrl.GetReadOnly()

    def GetText(self):
        return self.textCtrl.GetText()

    def SetText(self, text):
        self.textCtrl.SetText(text)

    def EmptyUndoBuffer(self):
        self.textCtrl.EmptyUndoBuffer()

    def GetSelectedText(self):
        return self.textCtrl.GetSelectedText()

    def GetCurrentLine(self):
        return self.textCtrl.GetCurrentLine()

    def ScrollToLine(self, line):
        self.textCtrl.ScrollToLine(line)

    def SetSelection(self, start, end):
        """
        start и end в символах, а не в байтах, в отличие от исходного
        StyledTextCtrl
        """
        startText = self.GetText()[:start]
        endText = self.GetText()[:end]

        firstByte = self._helper.calcByteLen(startText)
        endByte = self._helper.calcByteLen(endText)

        self.textCtrl.SetSelection(firstByte, endByte)

    def GotoPos(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        self.textCtrl.GotoPos(pos_bytes)

    def GetCurrentPosition(self):
        """
        Возвращает номер символа(а не байта), перед которых находится курсор
        """
        return self.__calcCharPos(self.textCtrl.GetCurrentPos())

    def GetSelectionStart(self):
        """
        Возвращает позицию начала выбранной области в символах, а не в байтах
        """
        return self.__calcCharPos(self.textCtrl.GetSelectionStart())

    def GetSelectionLines(self):
        """
        Return tuple (first selected line, last selected line)

        Added in OutWiker 2.0.0.795.
        """
        start_bytes = self.textCtrl.GetSelectionStart()
        end_bytes = self.textCtrl.GetSelectionEnd()
        return (self.textCtrl.LineFromPosition(start_bytes),
                self.textCtrl.LineFromPosition(end_bytes))

    def GetSelectionEnd(self):
        """
        Возвращает позицию конца выбранной области в символах, а не в байтах
        """
        return self.__calcCharPos(self.textCtrl.GetSelectionEnd())

    def SetFocus(self):
        self.textCtrl.SetFocus()
        self.textCtrl.SetSTCFocus(True)

    def GetLine(self, line):
        """
        Return line with the "line" number. \n included.
        Added in OutWiker 2.0.0.795
        """
        return self.textCtrl.GetLine(line)

    def SetLine(self, line, newline):
        """
        Replace line with the number "line" newline.
        Newline will be ended with "\n" else line will be joined with next line

        Added in OutWiker 2.0.0.795
        """
        linecount = self.GetLineCount()
        assert line < linecount

        line_start_bytes = self.textCtrl.PositionFromLine(line)
        line_end_bytes = self.textCtrl.PositionFromLine(line + 1)
        self.textCtrl.Replace(line_start_bytes, line_end_bytes, newline)

    def GetLineCount(self):
        return self.textCtrl.GetLineCount()

    def GetLineStartPosition(self, line):
        """
        Retrieve the position at the start of a line in symbols (not bytes)

        Added in OutWiker 2.0.0.795
        """
        return self.__calcCharPos(self.textCtrl.PositionFromLine(line))

    def GetLineEndPosition(self, line):
        """
        Get the position after the last visible characters on a line
            in symbols (not bytes)

        Added in OutWiker 2.0.0.795
        """
        return self.__calcCharPos(self.textCtrl.GetLineEndPosition(line))

    def MoveSelectedLinesUp(self):
        """
        Move the selected lines up one line,
        shifting the line above after the selection.

        Added in OutWiker 2.0.0.795
        """
        self.textCtrl.MoveSelectedLinesUp()

    def MoveSelectedLinesDown(self):
        """
        Move the selected lines down one line,
        shifting the line below before the selection.

        Added in OutWiker 2.0.0.795
        """
        self.textCtrl.MoveSelectedLinesDown()

    def LineDuplicate(self):
        """
        Duplicate the current line.

        Added in OutWiker 2.0.0.795
        """
        self.textCtrl.LineDuplicate()

    def LineDelete(self):
        """
        Delete the current line.

        Added in OutWiker 2.0.0.795
        """
        self.textCtrl.LineDelete()

    def BeginUndoAction(self):
        """
        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.BeginUndoAction()

    def EndUndoAction(self):
        """
        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.EndUndoAction()

    def JoinLines(self):
        """
        Join selected lines

        Added in OutWiker 2.0.0.797
        """
        first_line, last_line = self.GetSelectionLines()
        if first_line != last_line:
            last_line -= 1

        self.BeginUndoAction()

        for _ in range(first_line, last_line + 1):
            line = self.GetLine(first_line).replace(u'\r\n', u'\n')
            if line.endswith(u'\n'):
                newline = line[:-1]
                self.SetLine(first_line, newline)

        new_sel_pos = self.GetLineEndPosition(first_line)
        self.SetSelection(new_sel_pos, new_sel_pos)

        self.EndUndoAction()

    def DelWordLeft(self):
        """
        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.DelWordLeft()

    def DelWordRight(self):
        """
        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.DelWordRight()

    def DelLineLeft(self):
        """
        Delete back from the current position to the start of the line

        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.DelLineLeft()

    def DelLineRight(self):
        """
        Delete forwards from the current position to the end of the line

        Added in OutWiker 2.0.0.797
        """
        self.textCtrl.DelLineRight()

    def WordLeft(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordLeft()

    def WordRight(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordRight()

    def WordLeftEnd(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordLeftEnd()

    def WordRightEnd(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordRightEnd()

    def WordLeftExtend(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordLeftExtend()

    def WordRightExtend(self):
        """
        Added in outwiker.gui 1.2
        """
        self.textCtrl.WordRightExtend()

    def GotoWordStart(self):
        """
        Added in outwiker.gui 1.2
        """
        self.WordRight()
        self.WordLeft()

    def GotoWordEnd(self):
        """
        Added in outwiker.gui 1.2
        """
        self.WordLeftEnd()
        self.WordRightEnd()

    def ScrollLineToCursor(self):
        """
        Added in outwiker.gui 1.1
        """
        maxlines = self.textCtrl.LinesOnScreen()
        line = self.GetCurrentLine()
        if line >= maxlines:
            delta = min(10, maxlines / 3)
            line -= delta
            if line < 0:
                line = 0
            self.ScrollToLine(line)

    def WordStartPosition(self, pos):
        """
        Added in outwiker.gui 1.2
        """
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        result_bytes = self.textCtrl.WordStartPosition(pos_bytes, True)
        return self.getPosChar(result_bytes)

    def WordEndPosition(self, pos):
        """
        Added in outwiker.gui 1.2
        """
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        result_bytes = self.textCtrl.WordEndPosition(pos_bytes, True)
        return self.getPosChar(result_bytes)

    def GetWord(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        word_start_bytes = self.textCtrl.WordStartPosition(pos_bytes, True)
        word_end_bytes = self.textCtrl.WordEndPosition(pos_bytes, True)
        word = self.textCtrl.GetTextRange(word_start_bytes, word_end_bytes)
        return word

    def __calcCharPos(self, pos_bytes):
        """
        Пересчет позиции в байтах в позицию в символах
        """
        text_left = self.textCtrl.GetTextRange(0, pos_bytes)
        currpos = len(text_left)
        return currpos

    def _getTextForParse(self):
        # Табуляция в редакторе считается за несколько символов
        return self.textCtrl.GetText().replace("\t", " ")

    def runSpellChecking(self, stylelist, fullText, start, end):
        errors = self._spellChecker.findErrors(fullText[start: end])

        for word, err_start, err_end in errors:
            self._helper.setSpellError(stylelist,
                                       fullText,
                                       err_start + start,
                                       err_end + start)

    def _onStyleNeeded(self, event):
        if (not self._styleSet and
                datetime.now() - self._lastEdit >= self._DELAY):
            page = Application.selectedPage
            text = self._getTextForParse()
            params = EditorStyleNeededParams(self,
                                             text,
                                             self._enableSpellChecking)
            Application.onEditorStyleNeeded(page, params)
            self._styleSet = True

    def _onApplyStyle(self, event):
        if event.text == self._getTextForParse():
            startByte = self._helper.calcBytePos(event.text, event.start)
            endByte = self._helper.calcBytePos(event.text, event.end)
            lenBytes = endByte - startByte

            textlength = self._helper.calcByteLen(event.text)
            self.__stylebytes = [0] * textlength

            if event.stylebytes is not None:
                self.__stylebytes = event.stylebytes

            if event.indicatorsbytes is not None:
                self.__stylebytes = [item1 | item2
                                     for item1, item2
                                     in zip(self.__stylebytes,
                                            event.indicatorsbytes)]

            stylebytesstr = "".join([chr(byte) for byte in self.__stylebytes])


            if event.stylebytes is not None:
                self.textCtrl.StartStyling(startByte,
                                           0xff ^ wx.stc.STC_INDICS_MASK)
                self.textCtrl.SetStyleBytes(lenBytes,
                                            stylebytesstr[startByte:endByte])

            if event.indicatorsbytes is not None:
                self.textCtrl.StartStyling(startByte, wx.stc.STC_INDICS_MASK)
                self.textCtrl.SetStyleBytes(lenBytes,
                                            stylebytesstr[startByte:endByte])

            self._styleSet = True

    def getSpellChecker(self):
        langlist = self._getDictsFromConfig()
        spellDirList = outwiker.core.system.getSpellDirList()

        spellChecker = SpellChecker(Application, langlist, spellDirList)
        spellChecker.addCustomDict(os.path.join(spellDirList[-1], CUSTOM_DICT_FILE_NAME))

        return spellChecker

    def _getDictsFromConfig(self):
        dictsStr = self._config.spellCheckerDicts.value
        return [item.strip()
                for item
                in dictsStr.split(',')
                if item.strip()]

    def __onContextMenu(self, event):
        point = self.textCtrl.ScreenToClient(event.GetPosition())
        pos_byte = self.textCtrl.PositionFromPoint(point)

        popupMenu = TextEditorMenu()
        self._appendSpellMenuItems(popupMenu, pos_byte)

        Application.onEditorPopupMenu(
            Application.selectedPage,
            EditorPopupMenuParams(self, popupMenu, point, pos_byte)
        )

        self.textCtrl.PopupMenu(popupMenu)
        popupMenu.Destroy()

    def getCachedStyleBytes(self):
        return self.__stylebytes

    def __onAddWordToDict(self, event):
        if self._spellErrorText is not None:
            self.__addWordToDict(self._spellErrorText)

    def __onAddWordLowerToDict(self, event):
        if self._spellErrorText is not None:
            self.__addWordToDict(self._spellErrorText.lower())

    def __addWordToDict(self, word):
        self._spellChecker.addToCustomDict(0, word)
        self._spellErrorText = None
        self._styleSet = False

    def _appendSpellMenuItems(self, menu, pos_byte):
        stylebytes = self.getCachedStyleBytes()
        if stylebytes is None:
            return

        stylebytes_len = len(stylebytes)

        if (stylebytes is None or
                pos_byte >= stylebytes_len or
                stylebytes[pos_byte] & self._helper.SPELL_ERROR_INDICATOR_MASK == 0):
            return

        endSpellError = startSpellError = pos_byte

        while (startSpellError >= 0 and
               stylebytes[startSpellError] & self._helper.SPELL_ERROR_INDICATOR_MASK != 0):
            startSpellError -= 1


        while (endSpellError < stylebytes_len and
               stylebytes[endSpellError] & self._helper.SPELL_ERROR_INDICATOR_MASK != 0):
            endSpellError += 1

        self._spellStartByteError = startSpellError + 1
        self._spellEndByteError = endSpellError
        self._spellErrorText = self.textCtrl.GetTextRange(
            self._spellStartByteError,
            self._spellEndByteError)

        self._spellSuggestList = self._spellChecker.getSuggest(self._spellErrorText)[:self._spellMaxSuggest]

        menu.AppendSeparator()
        self._suggestMenuItems = menu.AppendSpellSubmenu(self._spellErrorText,
                                                         self._spellSuggestList)

        for menuItem in self._suggestMenuItems:
            self.textCtrl.Bind(wx.EVT_MENU, self.__onSpellSuggest, menuItem)

        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onAddWordToDict,
                           id=menu.ID_ADD_WORD)

        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onAddWordLowerToDict,
                           id=menu.ID_ADD_WORD_LOWER)


    def __onSpellSuggest(self, event):
        word = event.GetEventObject().GetLabelText(event.GetId())

        self.textCtrl.SetSelection(self._spellStartByteError,
                                   self._spellEndByteError)
        self.textCtrl.ReplaceSelection(word)
Esempio n. 11
0
class TextEditorBase(wx.Panel):
    def __init__(self, parent):
        super(TextEditorBase, self).__init__(parent, style=0)
        self.textCtrl = StyledTextCtrl(self, -1)

        # Создание панели поиска и ее контроллера
        self._searchPanel = SearchReplacePanel(self)
        self._searchPanelController = SearchReplaceController(
            self._searchPanel,
            self)
        self._searchPanel.setController(self._searchPanelController)

        self._do_layout()

        self.__createCoders()
        self._helper = TextEditorHelper()
        self._bind()
        self._setDefaultSettings()

    def _bind(self):
        self.textCtrl.Bind(wx.EVT_KEY_DOWN, self.onKeyDown)

    def _do_layout(self):
        mainSizer = wx.FlexGridSizer(rows=2, cols=0, vgap=0, hgap=0)
        mainSizer.AddGrowableRow(0)
        mainSizer.AddGrowableCol(0)

        mainSizer.Add(self.textCtrl, 0, wx.EXPAND, 0)
        mainSizer.Add(self._searchPanel, 0, wx.EXPAND, 0)
        self.SetSizer(mainSizer)

        self._searchPanel.Hide()
        self.Layout()

    def __createCoders(self):
        encoding = outwiker.core.system.getOS().inputEncoding
        self.mbcsEnc = codecs.getencoder(encoding)

    def onKeyDown(self, event):
        key = event.GetKeyCode()

        if key == wx.WXK_ESCAPE:
            self._searchPanel.Close()

        event.Skip()

    def _setDefaultSettings(self):
        self.textCtrl.SetEndAtLastLine(False)
        self.textCtrl.StyleClearAll()
        self.textCtrl.SetWrapMode(wx.stc.STC_WRAP_WORD)
        self.textCtrl.SetWrapVisualFlags(wx.stc.STC_WRAPVISUALFLAG_END)
        self.textCtrl.SetTabWidth(4)
        self._setDefaultHotKeys()

    def _setDefaultHotKeys(self):
        self.textCtrl.CmdKeyClearAll()

        # Clear Cmd keys for Ubuntu
        for key in list(range(ord('A'), ord('Z') + 1)) + list(range(ord('0'), ord('9') + 1)):
            self.textCtrl.CmdKeyClear(key, wx.stc.STC_SCMOD_ALT | wx.stc.STC_SCMOD_CTRL)
            self.textCtrl.CmdKeyClear(key, wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT | wx.stc.STC_SCMOD_CTRL)

        self.textCtrl.CmdKeyClear(wx.stc.STC_KEY_UP, wx.stc.STC_SCMOD_CTRL)
        self.textCtrl.CmdKeyClear(wx.stc.STC_KEY_DOWN, wx.stc.STC_SCMOD_CTRL)

        # Code from Wikidpad sources
        # Default mapping based on Scintilla's "KeyMap.cxx" file
        defaultHotKeys = (
            (wx.stc.STC_KEY_DOWN,        wx.stc.STC_SCMOD_NORM,     wx.stc.STC_CMD_LINEDOWN),
            (wx.stc.STC_KEY_UP,          wx.stc.STC_SCMOD_NORM,     wx.stc.STC_CMD_LINEUP),
            # (wx.stc.STC_KEY_DOWN,        wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_LINESCROLLDOWN),
            # (wx.stc.STC_KEY_UP,          wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_LINESCROLLUP),
            (wx.stc.STC_KEY_UP,          wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_LINEUPEXTEND),
            (wx.stc.STC_KEY_DOWN,        wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_LINEDOWNEXTEND),

            (ord('['),            wx.stc.STC_SCMOD_CTRL,            wx.stc.STC_CMD_PARAUP),
            (ord('['),            wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_PARAUPEXTEND),
            (ord(']'),            wx.stc.STC_SCMOD_CTRL,        wx.stc.STC_CMD_PARADOWN),
            (ord(']'),            wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_PARADOWNEXTEND),
            (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_CHARLEFT),
            (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_CHARLEFTEXTEND),
            # (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDLEFT),
            # (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDLEFTEXTEND),
            (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_CHARRIGHT),
            (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_CHARRIGHTEXTEND),
            # (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDRIGHT),
            # (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDRIGHTEXTEND),
            (ord('/'),        wx.stc.STC_SCMOD_CTRL,        wx.stc.STC_CMD_WORDPARTLEFT),
            (ord('/'),        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDPARTLEFTEXTEND),
            (ord('\\'),        wx.stc.STC_SCMOD_CTRL,        wx.stc.STC_CMD_WORDPARTRIGHT),
            (ord('\\'),        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_WORDPARTRIGHTEXTEND),
            (wx.stc.STC_KEY_HOME,        wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_VCHOME),
            (wx.stc.STC_KEY_HOME,         wx.stc.STC_SCMOD_SHIFT,     wx.stc.STC_CMD_VCHOMEEXTEND),
            (wx.stc.STC_KEY_HOME,         wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DOCUMENTSTART),
            (wx.stc.STC_KEY_HOME,         wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DOCUMENTSTARTEXTEND),
            (wx.stc.STC_KEY_HOME,         wx.stc.STC_SCMOD_ALT,     wx.stc.STC_CMD_HOMEDISPLAY),
            (wx.stc.STC_KEY_END,         wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_LINEEND),
            (wx.stc.STC_KEY_END,         wx.stc.STC_SCMOD_SHIFT,     wx.stc.STC_CMD_LINEENDEXTEND),
            (wx.stc.STC_KEY_END,         wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DOCUMENTEND),
            (wx.stc.STC_KEY_END,         wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DOCUMENTENDEXTEND),
            (wx.stc.STC_KEY_END,         wx.stc.STC_SCMOD_ALT,     wx.stc.STC_CMD_LINEENDDISPLAY),
            (wx.stc.STC_KEY_PRIOR,        wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_PAGEUP),
            (wx.stc.STC_KEY_PRIOR,        wx.stc.STC_SCMOD_SHIFT,     wx.stc.STC_CMD_PAGEUPEXTEND),
            (wx.stc.STC_KEY_NEXT,         wx.stc.STC_SCMOD_NORM,     wx.stc.STC_CMD_PAGEDOWN),
            (wx.stc.STC_KEY_NEXT,         wx.stc.STC_SCMOD_SHIFT,     wx.stc.STC_CMD_PAGEDOWNEXTEND),
            (wx.stc.STC_KEY_DELETE,     wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_CLEAR),
            (wx.stc.STC_KEY_INSERT,         wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_EDITTOGGLEOVERTYPE),
            (wx.stc.STC_KEY_ESCAPE,      wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_CANCEL),
            (wx.stc.STC_KEY_BACK,        wx.stc.STC_SCMOD_NORM,     wx.stc.STC_CMD_DELETEBACK),
            (wx.stc.STC_KEY_BACK,        wx.stc.STC_SCMOD_SHIFT,     wx.stc.STC_CMD_DELETEBACK),
            (wx.stc.STC_KEY_BACK,         wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_UNDO),
            (ord('Z'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_UNDO),
            (ord('Y'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_REDO),
            (ord('A'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_SELECTALL),

            (wx.stc.STC_KEY_INSERT,             wx.stc.STC_SCMOD_CTRL | wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_COPY),
            (wx.stc.STC_KEY_INSERT,             wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_PASTE),
            (ord('C'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_COPY),
            (ord('X'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_CUT),
            (ord('V'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_PASTE),

            (wx.stc.STC_KEY_TAB,        wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_TAB),
            (wx.stc.STC_KEY_TAB,        wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_BACKTAB),
            (wx.stc.STC_KEY_RETURN,     wx.stc.STC_SCMOD_NORM,    wx.stc.STC_CMD_NEWLINE),
            (wx.stc.STC_KEY_RETURN,     wx.stc.STC_SCMOD_SHIFT,    wx.stc.STC_CMD_NEWLINE),
            (wx.stc.STC_KEY_ADD,         wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_ZOOMIN),
            (wx.stc.STC_KEY_SUBTRACT,    wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_ZOOMOUT),
            (wx.stc.STC_KEY_DOWN,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_LINEDOWNRECTEXTEND),
            (wx.stc.STC_KEY_UP,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_LINEUPRECTEXTEND),
            (wx.stc.STC_KEY_LEFT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_CHARLEFTRECTEXTEND),
            (wx.stc.STC_KEY_RIGHT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_CHARRIGHTRECTEXTEND),
            (wx.stc.STC_KEY_HOME,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_VCHOMERECTEXTEND),
            (wx.stc.STC_KEY_END,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_LINEENDRECTEXTEND),
            (wx.stc.STC_KEY_PRIOR,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_PAGEUPRECTEXTEND),
            (wx.stc.STC_KEY_NEXT,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_ALT,    wx.stc.STC_CMD_PAGEDOWNRECTEXTEND),

            # (wx.stc.STC_KEY_DELETE,    wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_DELLINERIGHT),
            # (wx.stc.STC_KEY_BACK,        wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_DELLINELEFT),
            # (wx.stc.STC_KEY_BACK,        wx.stc.STC_SCMOD_CTRL,     wx.stc.STC_CMD_DELWORDLEFT),
            # (wx.stc.STC_KEY_DELETE,     wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_DELWORDRIGHT),
            # (wx.stc.STC_KEY_DIVIDE,    wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_SETZOOM),
            #        (ord('L'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINECUT),
            #        (ord('L'),             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINEDELETE),
            #        (ord('T'),             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINECOPY),
            #        (ord('T'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LINETRANSPOSE),
            #        (ord('D'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_SELECTIONDUPLICATE),
            #        (ord('U'),             wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_LOWERCASE),
            #        (ord('U'),             wx.stc.STC_SCMOD_SHIFT | wx.stc.STC_SCMOD_CTRL,    wx.stc.STC_CMD_UPPERCASE),
        )

        [self.textCtrl.CmdKeyAssign(*key) for key in defaultHotKeys]

    @property
    def searchPanel(self):
        """
        Возвращает контроллер панели поиска
        """
        return self._searchPanelController

    def Print(self):
        selectedtext = self.textCtrl.GetSelectedText()
        text = self.textCtrl.GetText()

        printer = TextPrinter(self)
        printer.printout(text if len(selectedtext) == 0 else selectedtext)

    def getPosChar(self, posBytes):
        return len(self.textCtrl.GetTextRange(0, posBytes))

    def AddText(self, text):
        self.textCtrl.AddText(text)

    def replaceText(self, text):
        self.textCtrl.ReplaceSelection(text)

    def toddleLinePrefix(self, line, prefix):
        """
        If line with number "line" starts with prefix, prefix will be removed
        else prefix will be added.
        """
        assert line < self.GetLineCount()
        line_text = self.GetLine(line)
        if line_text.startswith(prefix):
            line_text = line_text[len(prefix):]
        else:
            line_text = prefix + line_text
        self.SetLine(line, line_text)

    def toddleSelectedLinesPrefix(self, prefix):
        """
        Apply toddleLinePrefix method to selected lines
        """
        self.BeginUndoAction()
        old_sel_start = self.GetSelectionStart()
        old_sel_end = self.GetSelectionEnd()

        first_line, last_line = self.GetSelectionLines()
        [self.toddleLinePrefix(n, prefix)
         for n
         in range(first_line, last_line + 1)]

        if old_sel_start != old_sel_end:
            new_sel_start = self.GetLineStartPosition(first_line)
            new_sel_end = self.GetLineEndPosition(last_line)
        else:
            new_sel_start = new_sel_end = self.GetLineEndPosition(last_line)

        self.SetSelection(new_sel_start, new_sel_end)
        self.EndUndoAction()

    def turnText(self, lefttext, righttext):
        selText = self.textCtrl.GetSelectedText()
        newtext = lefttext + selText + righttext
        self.textCtrl.ReplaceSelection(newtext)

        currPos = self.GetSelectionEnd()
        if len(selText) == 0:
            # Если не оборачиваем текст, а делаем пустой тег,
            # то поместим каретку до закрывающегося тега
            newpos = currPos - len(righttext)
            self.SetSelection(newpos, newpos)
        else:
            self.SetSelection(currPos - len(selText) - len(righttext),
                              currPos - len(righttext))

    def escapeHtml(self):
        selText = self.textCtrl.GetSelectedText()
        text = html.escape(selText, quote=False)
        self.textCtrl.ReplaceSelection(text)

    def SetReadOnly(self, readonly):
        self.textCtrl.SetReadOnly(readonly)

    def GetReadOnly(self) -> bool:
        return self.textCtrl.GetReadOnly()

    def GetText(self) -> str:
        return self.textCtrl.GetText()

    def SetText(self, text: str) -> None:
        self.textCtrl.SetText(text)

    def EmptyUndoBuffer(self):
        self.textCtrl.EmptyUndoBuffer()

    def GetSelectedText(self) -> str:
        return self.textCtrl.GetSelectedText()

    def GetCurrentLine(self) -> int:
        '''
        Returns the line number of the line with the caret.
        '''
        return self.textCtrl.GetCurrentLine()

    def GetCurrentLineText(self) -> str:
        '''
        Retrieve the text of the line containing the caret.
        '''
        return self.textCtrl.GetCurLine()[0]

    def ScrollToLine(self, line):
        self.textCtrl.ScrollToLine(line)

    def SetSelection(self, start, end):
        """
        start и end в символах, а не в байтах, в отличие от исходного
        StyledTextCtrl
        """
        startText = self.GetText()[:start]
        endText = self.GetText()[:end]

        firstByte = self._helper.calcByteLen(startText)
        endByte = self._helper.calcByteLen(endText)

        self.textCtrl.SetSelection(firstByte, endByte)

    def GotoPos(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        self.textCtrl.GotoPos(pos_bytes)

    def GetCurrentPosition(self):
        """
        Возвращает номер символа(а не байта), перед которых находится курсор
        """
        return self._calcCharPos(self.textCtrl.GetCurrentPos())

    def GetSelectionStart(self):
        """
        Возвращает позицию начала выбранной области в символах, а не в байтах
        """
        return self._calcCharPos(self.textCtrl.GetSelectionStart())

    def GetSelectionLines(self):
        """
        Return tuple (first selected line, last selected line)
        """
        start_bytes = self.textCtrl.GetSelectionStart()
        end_bytes = self.textCtrl.GetSelectionEnd()
        return (self.textCtrl.LineFromPosition(start_bytes),
                self.textCtrl.LineFromPosition(end_bytes))

    def GetSelectionEnd(self):
        """
        Возвращает позицию конца выбранной области в символах, а не в байтах
        """
        return self._calcCharPos(self.textCtrl.GetSelectionEnd())

    def SetFocus(self):
        self.textCtrl.SetFocus()
        self.textCtrl.SetSTCFocus(True)

    def GetLine(self, line):
        """
        Return line with the "line" number. \n included.
        """
        return self.textCtrl.GetLine(line)

    def SetLine(self, line, newline):
        """
        Replace line with the number "line" newline.
        Newline will be ended with "\n" else line will be joined with next line
        """
        linecount = self.GetLineCount()
        assert line < linecount

        line_start_bytes = self.textCtrl.PositionFromLine(line)
        line_end_bytes = self.textCtrl.PositionFromLine(line + 1)
        self.textCtrl.Replace(line_start_bytes, line_end_bytes, newline)

    def GetLineCount(self):
        return self.textCtrl.GetLineCount()

    def GetLineStartPosition(self, line):
        """
        Retrieve the position at the start of a line in symbols (not bytes)
        """
        return self._calcCharPos(self.textCtrl.PositionFromLine(line))

    def GetLineEndPosition(self, line: int) -> int:
        """
        Get the position after the last visible characters on a line
            in symbols (not bytes)
        """
        return self._calcCharPos(self.textCtrl.GetLineEndPosition(line))

    def MoveSelectedLinesUp(self):
        """
        Move the selected lines up one line,
        shifting the line above after the selection.
        """
        self.textCtrl.MoveSelectedLinesUp()

    def MoveSelectedLinesDown(self):
        """
        Move the selected lines down one line,
        shifting the line below before the selection.
        """
        self.textCtrl.MoveSelectedLinesDown()

    def LineDuplicate(self):
        """
        Duplicate the current line.
        """
        self.textCtrl.LineDuplicate()

    def LineDelete(self):
        """
        Delete the current line.
        """
        self.textCtrl.LineDelete()

    def BeginUndoAction(self):
        self.textCtrl.BeginUndoAction()

    def EndUndoAction(self):
        self.textCtrl.EndUndoAction()

    def JoinLines(self):
        """
        Join selected lines
        """
        first_line, last_line = self.GetSelectionLines()
        if first_line != last_line:
            last_line -= 1

        self.BeginUndoAction()

        for _ in range(first_line, last_line + 1):
            line = self.GetLine(first_line).replace(u'\r\n', u'\n')
            if line.endswith(u'\n'):
                newline = line[:-1]
                self.SetLine(first_line, newline)

        new_sel_pos = self.GetLineEndPosition(first_line)
        self.SetSelection(new_sel_pos, new_sel_pos)

        self.EndUndoAction()

    def DelWordLeft(self):
        self.textCtrl.DelWordLeft()

    def DelWordRight(self):
        self.textCtrl.DelWordRight()

    def DelLineLeft(self):
        """
        Delete back from the current position to the start of the line
        """
        self.textCtrl.DelLineLeft()

    def DelLineRight(self):
        """
        Delete forwards from the current position to the end of the line
        """
        self.textCtrl.DelLineRight()

    def WordLeft(self):
        self.textCtrl.WordLeft()

    def WordRight(self):
        self.textCtrl.WordRight()

    def WordLeftEnd(self):
        self.textCtrl.WordLeftEnd()

    def WordRightEnd(self):
        self.textCtrl.WordRightEnd()

    def WordLeftExtend(self):
        self.textCtrl.WordLeftExtend()

    def WordRightExtend(self):
        self.textCtrl.WordRightExtend()

    def GotoWordStart(self):
        self.WordRight()
        self.WordLeft()

    def GotoWordEnd(self):
        self.WordLeftEnd()
        self.WordRightEnd()

    def ScrollLineToCursor(self):
        maxlines = self.textCtrl.LinesOnScreen()
        line = self.GetCurrentLine()
        if line >= maxlines:
            delta = min(10, maxlines / 3)
            line -= delta
            if line < 0:
                line = 0
            self.ScrollToLine(line)

    def WordStartPosition(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        result_bytes = self.textCtrl.WordStartPosition(pos_bytes, True)
        return self.getPosChar(result_bytes)

    def WordEndPosition(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        result_bytes = self.textCtrl.WordEndPosition(pos_bytes, True)
        return self.getPosChar(result_bytes)

    def GetWord(self, pos):
        pos_bytes = self._helper.calcBytePos(self.GetText(), pos)
        word_start_bytes = self.textCtrl.WordStartPosition(pos_bytes, True)
        word_end_bytes = self.textCtrl.WordEndPosition(pos_bytes, True)
        word = self.textCtrl.GetTextRange(word_start_bytes, word_end_bytes)
        return word

    def GetLineSelStartPosition(self, line: int) -> int:
        '''
        Retrieve the position of the end of the selection at the given line
        (wx.stc.STC_INVALID_POSITION if no selection on this line).
        '''
        return self.textCtrl.GetLineSelStartPosition(line)

    def GetLineSelEndPosition(self, line: int) -> int:
        '''
        Retrieve the position of the end of the selection at the given line
        (wx.stc.STC_INVALID_POSITION if no selection on this line).
        '''
        return self.textCtrl.GetLineSelEndPosition(line)

    def _calcCharPos(self, pos_bytes):
        """
        Пересчет позиции в байтах в позицию в символах
        """
        text_left = self.textCtrl.GetTextRange(0, pos_bytes)
        currpos = len(text_left)
        return currpos

    def _getTextForParse(self):
        # Табуляция в редакторе считается за несколько символов
        return self.textCtrl.GetText().replace("\t", " ")

    def _bindStandardMenuItems(self):
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onCopyFromEditor,
                           id=wx.ID_COPY)
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onCutFromEditor,
                           id=wx.ID_CUT)
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onPasteToEditor,
                           id=wx.ID_PASTE)
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onUndo,
                           id=wx.ID_UNDO)
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onRedo,
                           id=wx.ID_REDO)
        self.textCtrl.Bind(wx.EVT_MENU,
                           self.__onSelectAll,
                           id=wx.ID_SELECTALL)

    def __onCopyFromEditor(self, event):
        self.textCtrl.Copy()

    def __onCutFromEditor(self, event):
        self.textCtrl.Cut()

    def __onPasteToEditor(self, event):
        self.textCtrl.Paste()

    def __onUndo(self, event):
        self.textCtrl.Undo()

    def __onRedo(self, event):
        self.textCtrl.Redo()

    def __onSelectAll(self, event):
        self.textCtrl.SelectAll()