Source code for TermTk.TTkWidgets.texedit

# MIT License
#
# Copyright (c) 2021 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

__all__ = ['TTkTextEditView', 'TTkTextEdit']

from math import log10, floor

from TermTk.TTkCore.color import TTkColor
from TermTk.TTkCore.log import TTkLog
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.cfg import TTkCfg
from TermTk.TTkCore.string import TTkString
from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot
from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent
from TermTk.TTkGui.clipboard import TTkClipboard
from TermTk.TTkGui.textwrap1 import TTkTextWrap
from TermTk.TTkGui.textcursor import TTkTextCursor
from TermTk.TTkGui.textdocument import TTkTextDocument
from TermTk.TTkWidgets.widget import TTkWidget
from TermTk.TTkLayouts.gridlayout import TTkGridLayout
from TermTk.TTkAbstract.abstractscrollarea import TTkAbstractScrollArea
from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView, TTkAbstractScrollViewGridLayout

class _TTkTextEditViewLineNumber(TTkAbstractScrollView):
    classStyle = {
                'default':     {
                    'color': TTkColor.fg("#88aaaa")+TTkColor.bg("#333333"),
                    'wrapColor': TTkColor.fg("#888888")+TTkColor.bg("#333333"),
                    'separatorColor': TTkColor.fg("#444444")},
                'disabled':    {
                    'color': TTkColor.fg('#888888'),
                    'wrapColor': TTkColor.fg('#888888'),
                    'separatorColor': TTkColor.fg("#888888")},
            }

    __slots__ = ('_textWrap','_startingNumber')
    def __init__(self, startingNumber=0, **kwargs) -> None:
        self._startingNumber = startingNumber
        self._textWrap = None
        super().__init__(**kwargs)
        self.setMaximumWidth(2)

    def _wrapChanged(self):
        dt = max(1,self._textWrap._lines[-1][0])
        off  = self._startingNumber
        width = 1+max(len(str(int(dt+off))),len(str(int(off))))
        self.setMaximumWidth(width)
        self.update()

    def setTextWrap(self, tw):
        self._textWrap = tw
        tw.wrapChanged.connect(self._wrapChanged)
        self._wrapChanged()

    def viewFullAreaSize(self) -> tuple[int,int]:
        if self._textWrap:
            return 5, self._textWrap.size()
        else:
            return self.size()

    def paintEvent(self, canvas):
        if not self._textWrap: return
        _, oy = self.getViewOffsets()
        w, h = self.size()
        off  = self._startingNumber

        style = self.currentStyle()
        color = style['color']
        wrapColor = style['wrapColor']
        separatorColor = style['separatorColor']

        if self._textWrap:
            for i, (dt, (fr, _)) in enumerate(self._textWrap._lines[oy:oy+h]):
                if fr:
                    canvas.drawText(pos=(0,i), text='<', width=w, color=wrapColor)
                else:
                    canvas.drawText(pos=(0,i), text=f"{dt+off}", width=w, color=color)
                canvas.drawChar(pos=(w-1,i), char='▌', color=separatorColor)
        else:
            for y in range(h):
                canvas.drawText(pos=(0,y), text=f"{y+oy+off}", width=w, color=color)
                canvas.drawChar(pos=(w-1,y), char='▌', color=separatorColor)

[docs] class TTkTextEditView(TTkAbstractScrollView): ''' :py:class:`TTkTextEditView` :: ╔═══════════════════════════════════════════════════════════════════════════════════════╗ ║ 0▌"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor ╥ ║ ║ <▌incididunt ut labore et dolore magna aliqua. ║ ║ ║ 1▌ ║ ║ ║ 2▌Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliqu║ ║ ║ <▌ip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate ║ ║ ║ <▌velit esse cillum dolore eu fugiat nulla pariatur. ║ ║ ║ 3▌ ║ ║ ║ 4▌Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru║ ║ ║ <▌nt mollit anim id est laborum." ╨ ║ ╚═══════════════════════════════════════════════════════════════════════════════════════╝ Demo: `textedit.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/demo/showcase/textedit.py>`_ (`Try Online <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?fileUri=https://raw.githubusercontent.com/ceccopierangiolieugenio/pyTermTk/main/demo/showcase/textedit.py>`__) :ref:`ttkdesigner Tutorial <TextEdit_ttkDesigner-Tutorial_Intro>` ''' currentColorChanged:pyTTkSignal ''' This signal is emitted if the current character color has changed, for example caused by a change of the cursor position. :param color: the new color :type color: :py:class:`TTkColor` ''' undoAvailable:pyTTkSignal ''' This signal is emitted whenever undo operations become available (available is true) or unavailable (available is false). :param available: the availability of undo :type available: bool ''' redoAvailable:pyTTkSignal ''' This signal is emitted whenever redo operations become available (available is true) or unavailable (available is false). :param available: the availability of redo :type available: bool ''' textChanged:pyTTkSignal ''' This signal is emitted whenever the document's content changes; for example, when text is inserted or deleted, or when formatting is applied. ''' classStyle = { 'default': {'color': TTkColor.fg("#dddddd")+TTkColor.bg("#222222"), 'selectedColor': TTkColor.fg("#ffffff")+TTkColor.bg("#008844"), 'lineColor': TTkColor.fg("#444444"), 'wrapLineColor': TTkColor.fg("#888888")+TTkColor.bg("#333333")}, 'disabled': {'color': TTkColor.fg('#888888'), 'selectedColor': TTkColor.bg("#888888"), 'lineColor': TTkColor.fg("#888888"), 'wrapLineColor': TTkColor.fg('#888888')}, 'focus': {'selectedColor': TTkColor.fg("#ffffff")+TTkColor.bg("#008888")}, } __slots__ = ( '_textDocument', '_hsize', '_textCursor', '_cursorParams', '_textWrap', '_lineWrapMode', '_lastWrapUsed', '_replace', '_readOnly', '_multiCursor', '_clipboard', # '_preview', '_previewWidth', '_multiLine', # # Forwarded Methods # 'wrapWidth', 'setWrapWidth', # 'wordWrapMode', 'setWordWrapMode', # Signals 'currentColorChanged', 'undoAvailable', 'redoAvailable', 'textChanged' ) # in order to support the line wrap, I need to divide the full data text in; # _textDocument = the entire text divided in lines, easy to add/remove/append lines # _textWrap._lines = an array of tuples for each displayed line with a pointer to a # specific line and its slice to be shown at this coordinate; # [ (line, (posFrom, posTo)), ... ] # This is required to support the wrap feature def __init__(self, *, readOnly:bool=False, multiLine:bool=True, document:TTkTextDocument=None, **kwargs) -> None: ''' :param lineNumber: Show the line number on the left, defaults to **False** :type lineNumber: bool, optional :param readOnly: In a read-only text edit the user can only navigate through the text and select text; modifying the text is not possible, defaults to **True** :type readOnly: bool, optional :param multiLine: In a multiline text edit the user can split the text in multiple lines, defaults to **True** :type multiLine: bool, optional :param document: If required an external Document can be used in this text editor, this option is useful if multiple editors share the same document as in the `demo <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?fileUri=https://raw.githubusercontent.com/ceccopierangiolieugenio/pyTermTk/main/demo/showcase/textedit.py>`__, defaults to a new Document :type document: :py:class:`TTkTextDocument`, optional ''' self.currentColorChanged = pyTTkSignal(TTkColor) self.undoAvailable = pyTTkSignal(bool) self.redoAvailable = pyTTkSignal(bool) self.textChanged = pyTTkSignal() self._readOnly = readOnly self._multiLine = multiLine self._multiCursor = True self._hsize = 0 self._lastWrapUsed = 0 self._lineWrapMode = TTkK.NoWrap self._replace = False self._cursorParams = None self._textDocument = None self._textCursor = None self._textWrap = None self._clipboard = TTkClipboard() super().__init__(**kwargs) self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus) self.setDocument(document if document else TTkTextDocument()) self.disableWidgetCursor(self._readOnly) self._updateSize() self.viewChanged.connect(self._pushCursor)
[docs] def multiLine(self) -> bool : '''multiline''' return self._multiLine
@pyTTkSlot(bool) def _undoAvailable(self, available): self.undoAvailable.emit(available) @pyTTkSlot(bool) def _redoAvailable(self, available): self.redoAvailable.emit(available) # def toHtml(self, encoding): pass # if self._textDocument: # return self._textDocument.toHtml() # return "" # def toMarkdown(self, features): pass # if self._textDocument: # return self._textDocument.toMarkdown() # return ""
[docs] def toAnsi(self): '''toAnsi''' if self._textDocument: return self._textDocument.toAnsi() return ""
[docs] def toPlainText(self): '''toPlainText''' if self._textDocument: return self._textDocument.toPlainText() return ""
[docs] def toRawText(self): '''toRawText''' if self._textDocument: return self._textDocument.toRawText() return TTkString()
[docs] def isUndoAvailable(self): '''isUndoAvailable''' if self._textDocument: return self._textDocument.isUndoAvailable() return False
[docs] def isRedoAvailable(self): '''isRedoAvailable''' if self._textDocument: return self._textDocument.isRedoAvailable() return False
[docs] def document(self) -> TTkTextDocument: '''document''' return self._textDocument
[docs] def setDocument(self, document:TTkTextDocument): '''setDocument''' if self._textDocument: self._textDocument.contentsChanged.disconnect(self._documentChanged) self._textDocument.cursorPositionChanged.disconnect(self._cursorPositionChanged) self._textDocument.undoAvailable.disconnect(self._undoAvailable) self._textDocument.redoAvailable.disconnect(self._redoAvailable) self._textWrap.wrapChanged.disconnect(self.update) if not document: document = TTkTextDocument() self._textDocument = document self._textCursor = TTkTextCursor(document=self._textDocument) self._textWrap = TTkTextWrap(document=self._textDocument) self._textDocument.contentsChanged.connect(self._documentChanged) self._textDocument.cursorPositionChanged.connect(self._cursorPositionChanged) self._textDocument.undoAvailable.connect(self._undoAvailable) self._textDocument.redoAvailable.connect(self._redoAvailable) # Trigger an update when the rewrap happen self._textWrap.wrapChanged.connect(self.update)
# forward textWrap Methods
[docs] def wrapWidth(self, *args, **kwargs) -> None: return self._textWrap.wrapWidth(*args, **kwargs)
[docs] def setWrapWidth(self, *args, **kwargs) -> None: return self._textWrap.setWrapWidth(*args, **kwargs)
[docs] def wordWrapMode(self, *args, **kwargs) -> None: return self._textWrap.wordWrapMode(*args, **kwargs)
[docs] def setWordWrapMode(self, *args, **kwargs) -> None: return self._textWrap.setWordWrapMode(*args, **kwargs)
[docs] def textCursor(self) -> TTkTextCursor: return self._textCursor
[docs] def isReadOnly(self) -> bool : return self._readOnly
[docs] def setReadOnly(self, ro): self._readOnly = ro self.disableWidgetCursor(ro)
def clear(self): self.setText(TTkString())
[docs] def lineWrapMode(self): return self._lineWrapMode
[docs] def setLineWrapMode(self, mode): self._lineWrapMode = mode if mode == TTkK.NoWrap: self._textWrap.disable() else: self._textWrap.enable() if mode == TTkK.WidgetWidth: self._textWrap.setWrapWidth(self.width()) self._textWrap.rewrap()
[docs] @pyTTkSlot(str) def setText(self, text): self.viewMoveTo(0, 0) self._textDocument.setText(text) self._updateSize()
[docs] @pyTTkSlot(str) def append(self, text): self._textDocument.appendText(text) self._updateSize()
[docs] @pyTTkSlot() def undo(self): if c := self._textDocument.restoreSnapshotPrev(): self._textCursor.restore(c)
[docs] @pyTTkSlot() def redo(self): if c := self._textDocument.restoreSnapshotNext(): self._textCursor.restore(c)
[docs] @pyTTkSlot() def clear(self): pass
[docs] @pyTTkSlot() def copy(self): if not self._textCursor.hasSelection(): txt = TTkString('\n').join(self._textCursor.getLinesUnderCursor()) else: txt = self._textCursor.selectedText() self._clipboard.setText(txt)
[docs] @pyTTkSlot() def cut(self): if not self._textCursor.hasSelection(): self._textCursor.movePosition(moveMode=TTkTextCursor.MoveAnchor, operation=TTkTextCursor.StartOfLine) self._textCursor.movePosition(moveMode=TTkTextCursor.KeepAnchor, operation=TTkTextCursor.EndOfLine) self._textCursor.movePosition(moveMode=TTkTextCursor.KeepAnchor, operation=TTkTextCursor.Right) self.copy() self._textCursor.removeSelectedText()
[docs] @pyTTkSlot() def paste(self): txt = self._clipboard.text() self.pasteEvent(txt)
@pyTTkSlot() def _documentChanged(self): self._rewrap() self.textChanged.emit() def _rewrap(self): self._textWrap.rewrap() self.viewChanged.emit() self.update()
[docs] @pyTTkSlot(TTkColor) def setColor(self, color): self.textCursor().setColor(color)
@pyTTkSlot(TTkTextCursor) def _cursorPositionChanged(self, cursor): if cursor == self._textCursor: self.currentColorChanged.emit(cursor.positionColor()) self._pushCursor() def resizeEvent(self, w, h): if ( self.lineWrapMode() == TTkK.WidgetWidth and w != self._lastWrapUsed and w > self._textWrap._tabSpaces ): self._textWrap.setWrapWidth(w) self._lastWrapUsed = w self._rewrap() return super().resizeEvent(w,h) def _updateSize(self): self._hsize = max( len(l) for l in self._textDocument._dataLines ) + 1
[docs] def viewFullAreaSize(self) -> tuple[int,int]: if self.lineWrapMode() == TTkK.NoWrap: return self._hsize, self._textWrap.size() elif self.lineWrapMode() == TTkK.WidgetWidth: return self.width(), self._textWrap.size() elif self.lineWrapMode() == TTkK.FixedWidth: return self.wrapWidth(), self._textWrap.size()
def _pushCursor(self): ox, oy = self.getViewOffsets() x,y = self._textWrap.dataToScreenPosition( self._textCursor.position().line, self._textCursor.position().pos) y -= oy x -= ox self._cursorParams = {'pos': (x,y), 'replace': self._replace} if self._replace: self.setWidgetCursor(pos=(x,y), type=TTkK.Cursor_Blinking_Block) else: self.setWidgetCursor(pos=(x,y), type=TTkK.Cursor_Blinking_Bar) self.update() def _setCursorPos(self, x, y, moveAnchor=True, addCursor=False): x,y = self._textWrap.normalizeScreenPosition(x,y) line, pos = self._textWrap.screenToDataPosition(x,y) if addCursor: self._textCursor.addCursor(line, pos) else: self._textCursor.clearCursors() self._textCursor.setPosition(line, pos, moveMode=TTkTextCursor.MoveAnchor if moveAnchor else TTkTextCursor.KeepAnchor ) self._scrolToInclude(x,y) return x, y def _scrolToInclude(self, x, y): # Scroll the area (if required) to include the position x,y _,_,w,h = self.geometry() offx, offy = self.getViewOffsets() offx = max(min(offx, x),x-w+1) offy = max(min(offy, y),y-h+1) self.viewMoveTo(offx, offy) def mousePressEvent(self, evt) -> bool: if self._readOnly: return super().mousePressEvent(evt) ox, oy = self.getViewOffsets() self._setCursorPos(evt.x + ox, evt.y + oy, addCursor=evt.mod&TTkK.ControlModifier==TTkK.ControlModifier) self._textCursor.clearColor() self._pushCursor() self.update() return True def mouseReleaseEvent(self, evt) -> bool: if self._textCursor.hasSelection(): self.copy() return True def mouseDragEvent(self, evt) -> bool: if self._readOnly: return super().mouseDragEvent(evt) ox, oy = self.getViewOffsets() x,y = self._setCursorPos(evt.x + ox, evt.y + oy, moveAnchor=False) self._textCursor.clearColor() self._scrolToInclude(x,y) self._pushCursor() self.update() return True def mouseDoubleClickEvent(self, evt) -> bool: if self._readOnly: return super().mouseDoubleClickEvent(evt) self._textCursor.select(TTkTextCursor.WordUnderCursor) if self._textCursor.hasSelection(): self.copy() self._textCursor.clearColor() self._pushCursor() self.update() return True def mouseTapEvent(self, evt) -> bool: if self._readOnly: return super().mouseTapEvent(evt) self._textCursor.select(TTkTextCursor.LineUnderCursor) if self._textCursor.hasSelection(): self.copy() self._textCursor.clearColor() self._pushCursor() self.update() return True def pasteEvent(self, txt:str): txt = TTkString(txt) if not self._multiLine: txt = TTkString().join(txt.split('\n')) if self._replace: self._textCursor.replaceText(txt, moveCursor=True) else: self._textCursor.insertText(txt, moveCursor=True) # Scroll to align to the cursor p = self._textCursor.position() cx, cy = self._textWrap.dataToScreenPosition(p.line, p.pos) self._updateSize() self._scrolToInclude(cx,cy) self._pushCursor() self.update() return True def keyEvent(self, evt): if self._readOnly: return super().keyEvent(evt) # Keep a snapshot in case of those actions if (( evt.type == TTkK.Character and ( ( evt.key == ' ' ) or ( evt.key == '\n') or ( evt.key == '\t') or ( self._textCursor.hasSelection() ) ) ) or ( evt.type == TTkK.SpecialKey and ( ( evt.key == TTkK.Key_Enter ) or ( evt.key == TTkK.Key_Delete ) or ( evt.key == TTkK.Key_Backspace ) or ( self._textDocument.changed() and evt.key == TTkK.Key_Z ) or ( evt.mod==TTkK.ControlModifier and ( evt.key == TTkK.Key_V or evt.key == TTkK.Key_X or ( evt.key == TTkK.Key_Z and self._textDocument.changed() ) ) ) ) ) ): # TTkLog.debug(f"Saving {self._textCursor.selectedText()} {self._textCursor._properties[0].anchor.pos}") self._textDocument.saveSnapshot(self._textCursor.copy()) if evt.key == TTkK.Key_Tab and evt.mod==TTkK.NoModifier: evt = TTkKeyEvent(TTkK.Character, '\t', '\t', TTkK.NoModifier) if evt.type == TTkK.SpecialKey: _,_,w,h = self.geometry() # TODO: Remove this HACK As soon as possible # Due to the lack of 'INS' key on many laptops # I emulate this key using # CTRL + HOME if evt.key == TTkK.Key_Home and evt.mod==TTkK.ControlModifier: evt.key = TTkK.Key_Insert evt.mod = TTkK.NoModifier moveMode = TTkTextCursor.MoveAnchor if evt.mod==TTkK.ShiftModifier: moveMode = TTkTextCursor.KeepAnchor ctrl = evt.mod==TTkK.ControlModifier if ctrl: p = self._textCursor.position() cx, cy = self._textWrap.dataToScreenPosition(p.line, p.pos) if evt.key == TTkK.Key_Up: self._setCursorPos(cx, cy-1, addCursor=True) self._textCursor.clearColor() elif evt.key == TTkK.Key_Down: self._setCursorPos(cx, cy+1, addCursor=True) self._textCursor.clearColor() elif evt.key == TTkK.Key_C: self.copy() elif evt.key == TTkK.Key_V: self.paste() elif evt.key == TTkK.Key_X: self.cut() elif evt.key == TTkK.Key_Z: self.undo() elif evt.key == TTkK.Key_Y: self.redo() elif evt.key == TTkK.Key_A: self._textCursor.select(TTkTextCursor.SelectionType.Document) elif evt.key == TTkK.Key_Up: self._textCursor.movePosition(moveMode=moveMode, operation=TTkTextCursor.Up, textWrap=self._textWrap) self._textCursor.clearColor() elif evt.key == TTkK.Key_Down: self._textCursor.movePosition(moveMode=moveMode, operation=TTkTextCursor.Down, textWrap=self._textWrap) self._textCursor.clearColor() elif evt.key == TTkK.Key_Left: self._textCursor.movePosition(moveMode=moveMode, operation=TTkTextCursor.Left) self._textCursor.clearColor() elif evt.key == TTkK.Key_Right: self._textCursor.movePosition(moveMode=moveMode, operation=TTkTextCursor.Right) self._textCursor.clearColor() elif evt.key == TTkK.Key_End: self._textCursor.movePosition(moveMode=moveMode, operation=TTkTextCursor.EndOfLine) self._textCursor.clearColor() elif evt.key == TTkK.Key_Home: self._textCursor.movePosition(moveMode=moveMode, operation=TTkTextCursor.StartOfLine) self._textCursor.clearColor() elif evt.key == TTkK.Key_PageUp: self._textCursor.movePosition(moveMode=moveMode, operation=TTkTextCursor.Up, textWrap=self._textWrap, n=h) #self._setCursorPos(cx , cy - h) self._textCursor.clearColor() elif evt.key == TTkK.Key_PageDown: self._textCursor.movePosition(moveMode=moveMode, operation=TTkTextCursor.Down, textWrap=self._textWrap, n=h) #self._setCursorPos(cx , cy + h) self._textCursor.clearColor() elif evt.key == TTkK.Key_Escape: self._textCursor.clearCursors() self._textCursor.clearSelection() self._textCursor.clearColor() elif evt.key == TTkK.Key_Insert: self._replace = not self._replace elif evt.key == TTkK.Key_Delete: if not self._textCursor.hasSelection(): self._textCursor.movePosition(TTkTextCursor.Right, TTkTextCursor.KeepAnchor) self._textCursor.removeSelectedText() elif evt.key == TTkK.Key_Backspace: if not self._textCursor.hasSelection(): self._textCursor.movePosition(TTkTextCursor.Left, TTkTextCursor.KeepAnchor) self._textCursor.removeSelectedText() elif evt.key == TTkK.Key_Enter: if self._multiLine: self._textCursor.insertText('\n', moveCursor=True) else: # No action performed return super().keyEvent(evt) # Scroll to align to the cursor p = self._textCursor.position() cx, cy = self._textWrap.dataToScreenPosition(p.line, p.pos) self._updateSize() self._scrolToInclude(cx,cy) self._pushCursor() self.update() return True else: # Input char if self._replace: self._textCursor.replaceText(evt.key, moveCursor=True) else: self._textCursor.insertText(evt.key, moveCursor=True) # Scroll to align to the cursor p = self._textCursor.position() cx, cy = self._textWrap.dataToScreenPosition(p.line, p.pos) self._updateSize() self._scrolToInclude(cx,cy) self._pushCursor() self.update() return True return super().keyEvent(evt) def paintEvent(self, canvas): ox, oy = self.getViewOffsets() style = self.currentStyle() color = style['color'] selectColor = style['selectedColor'] lineColor = style['lineColor'] h = self.height() subLines = self._textWrap._lines[oy:oy+h] if not subLines: return outLines = self._textCursor.getHighlightedLines(subLines[0][0], subLines[-1][0], selectColor) for y, l in enumerate(subLines): t = outLines[l[0]-subLines[0][0]] canvas.drawTTkString(pos=(-ox,y), text=t.substring(l[1][0],l[1][1]).tab2spaces(self._textWrap._tabSpaces)) if self._lineWrapMode == TTkK.FixedWidth: canvas.drawVLine(pos=(self._textWrap._wrapWidth,0), size=h, color=lineColor)
[docs] class TTkTextEdit(TTkAbstractScrollArea): __doc__ = ''' :py:class:`TTkTextEdit` is a container widget which place :py:class:`TTkTextEditView` in a scrolling area with on-demand scroll bars. ''' + TTkTextEditView.__doc__ __slots__ = ( ['_textEditView', '_lineNumberView', '_lineNumber'] + (_forwardedSignals:=[ # Forwarded Signals From TTkTexteditView # Signals 'focusChanged', 'currentColorChanged', 'undoAvailable', 'redoAvailable', 'textChanged']) + (_forwardedMethods:=[ # Forwarded Methods From TTkTexteditView # Forwarded Methods 'clear', 'setText', 'append', 'isReadOnly', 'setReadOnly', 'document', 'wrapWidth', 'setWrapWidth', 'multiLine', 'lineWrapMode', 'setLineWrapMode', 'wordWrapMode', 'setWordWrapMode', 'textCursor', 'setFocus', 'setColor', 'cut', 'copy', 'paste', 'undo', 'redo', 'isUndoAvailable', 'isRedoAvailable', # Export Methods, 'toAnsi', 'toRawText', 'toPlainText', # 'toHtml', 'toMarkdown', ]) ) _forwardWidget = TTkTextEditView def __init__(self, *, # TTkWidget init parent:TTkWidget=None, visible:bool=True, # TTkTextEditView init readOnly:bool=False, multiLine:bool=True, document:TTkTextDocument=None, # TTkText init textEditView:TTkTextEditView=None, lineNumber:bool=False, lineNumberStarting:int=0, **kwargs) -> None: ''' :param textEditView: a custom TextEdit View to be used instead of the default one. :type textEditView: :py:class:`TTkTextEditView`, optional :param lineNumber: show the line number on the left side, defaults to False :type lineNumber: bool, optional :param lineNumberStarting: set the starting number of the left line number column, defaults to 0 :type lineNumberStarting: int, optional ''' super().__init__(parent=parent, visible=visible, **kwargs) self._textEditView = textEditView if textEditView else TTkTextEditView(readOnly=readOnly, multiLine=multiLine, document=document) # self.setFocusPolicy(self._textEditView.focusPolicy()) # self._textEditView.setFocusPolicy(TTkK.ParentFocus) self._lineNumber = lineNumber textEditLayout = TTkAbstractScrollViewGridLayout() textEditLayout.addWidget(self._textEditView,0,1) self._lineNumberView = _TTkTextEditViewLineNumber(visible=self._lineNumber, startingNumber=lineNumberStarting) self._lineNumberView.setTextWrap(self._textEditView._textWrap) textEditLayout.addWidget(self._lineNumberView,0,0) self.setViewport(textEditLayout) for _attr in self._forwardedSignals+self._forwardedMethods: setattr(self,_attr,getattr(self._textEditView,_attr))
[docs] def textEditView(self): '''textEditView''' return self._textEditView
[docs] def getLineNumber(self): '''getLineNumber''' return self._lineNumberView.isVisible()
[docs] @pyTTkSlot(bool) def setLineNumber(self, ln): '''setLineNumber''' self._lineNumberView.setVisible(ln)
[docs] def lineNumberStarting(self): '''lineNumberStarting''' return self._lineNumberView._startingNumber
[docs] @pyTTkSlot(int) def setLineNumberStarting(self, starting): '''setLineNumberStarting''' self._lineNumberView._startingNumber = starting self._lineNumberView._wrapChanged()
[docs] def setDocument(self, document): '''setDocument''' self._textEditView.setDocument(document) self._lineNumberView.setTextWrap(self._textEditView._textWrap)