diff --git a/TermTk/TTkGui/textcursor.py b/TermTk/TTkGui/textcursor.py index a4324c52..3ecb2dfe 100644 --- a/TermTk/TTkGui/textcursor.py +++ b/TermTk/TTkGui/textcursor.py @@ -22,6 +22,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from TermTk.TTkCore.log import TTkLog from TermTk.TTkGui.textdocument import TTkTextDocument class TTkTextCursor(): @@ -47,6 +48,83 @@ class TTkTextCursor(): LineUnderCursor = SelectionType.LineUnderCursor WordUnderCursor = SelectionType.WordUnderCursor + class MoveOperation(): + NoMove = 0 + '''Keep the cursor where it is''' + Start = 1 + '''Move to the start of the document.''' + StartOfLine = 3 + '''Move to the start of the current line.''' + StartOfBlock = 4 + '''Move to the start of the current block.''' + StartOfWord = 5 + '''Move to the start of the current word.''' + PreviousBlock = 6 + '''Move to the start of the previous block.''' + PreviousCharacter = 7 + '''Move to the previous character.''' + PreviousWord = 8 + '''Move to the beginning of the previous word.''' + Up = 2 + '''Move up one line.''' + Left = 9 + '''Move left one character.''' + WordLeft = 10 + '''Move left one word.''' + End = 11 + '''Move to the end of the document.''' + EndOfLine = 13 + '''Move to the end of the current line.''' + EndOfWord = 14 + '''Move to the end of the current word.''' + EndOfBlock = 15 + '''Move to the end of the current block.''' + NextBlock = 16 + '''Move to the beginning of the next block.''' + NextCharacter = 17 + '''Move to the next character.''' + NextWord = 18 + '''Move to the next word.''' + Down = 12 + '''Move down one line.''' + Right = 19 + '''Move right one character.''' + WordRight = 20 + '''Move right one word.''' + NextCell = 21 + '''Move to the beginning of the next table cell inside the current table. If the current cell is the last cell in the row, the cursor will move to the first cell in the next row.''' + PreviousCell = 22 + '''Move to the beginning of the previous table cell inside the current table. If the current cell is the first cell in the row, the cursor will move to the last cell in the previous row.''' + NextRow = 23 + '''Move to the first new cell of the next row in the current table.''' + PreviousRow = 24 + '''Move to the last cell of the previous row in the current table.''' + NoMove = MoveOperation.NoMove + Start = MoveOperation.Start + StartOfLine = MoveOperation.StartOfLine + StartOfBlock = MoveOperation.StartOfBlock + StartOfWord = MoveOperation.StartOfWord + PreviousBlock = MoveOperation.PreviousBlock + PreviousCharacter = MoveOperation.PreviousCharacter + PreviousWord = MoveOperation.PreviousWord + Up = MoveOperation.Up + Left = MoveOperation.Left + WordLeft = MoveOperation.WordLeft + End = MoveOperation.End + EndOfLine = MoveOperation.EndOfLine + EndOfWord = MoveOperation.EndOfWord + EndOfBlock = MoveOperation.EndOfBlock + NextBlock = MoveOperation.NextBlock + NextCharacter = MoveOperation.NextCharacter + NextWord = MoveOperation.NextWord + Down = MoveOperation.Down + Right = MoveOperation.Right + WordRight = MoveOperation.WordRight + NextCell = MoveOperation.NextCell + PreviousCell = MoveOperation.PreviousCell + NextRow = MoveOperation.NextRow + PreviousRow = MoveOperation.PreviousRow + class _prop(): __slots__ = ('anchor', 'position') def __init__(self, anchor, position): @@ -87,13 +165,12 @@ class TTkTextCursor(): self.pos = p self.line = l - __slots__ = ('_document', '_properties', '_docData') + __slots__ = ('_document', '_properties') def __init__(self, *args, **kwargs): self._properties = [TTkTextCursor._prop( TTkTextCursor._CP(), TTkTextCursor._CP())] self._document = kwargs.get('document',TTkTextDocument()) - self._docData = self._document._dataLines def anchor(self): return self._properties[0].anchor @@ -102,12 +179,24 @@ class TTkTextCursor(): return self._properties[0].position def setPosition(self, line, pos, moveMode=MoveMode.MoveAnchor ): + TTkLog.debug(f"{line=}, {pos=}, {moveMode=}") self._properties[0].position.set(line,pos) if moveMode==TTkTextCursor.MoveAnchor: self._properties[0].anchor.set(line,pos) - #def movePosition(self, operation, moveMode=MoveMode.MoveAnchor, n=1 ): - # pass + def movePosition(self, operation, moveMode=MoveMode.MoveAnchor, n=1 ): + if operation == TTkTextCursor.Right: + p = self.position() + if p.pos < len(self._document._dataLines[p.line]): + self.setPosition(p.line, p.pos+1, moveMode) + elif p.line < len(self._document._dataLines)-1: + self.setPosition(p.line+1, 0, moveMode) + elif operation == TTkTextCursor.Left: + p = self.position() + if p.pos > 0: + self.setPosition(p.line, p.pos-1, moveMode) + elif p.line > 0: + self.setPosition(p.line-1, len(self._document._dataLines[p.line-1]) , moveMode) def document(self): return self._document @@ -130,15 +219,15 @@ class TTkTextCursor(): elif selection == TTkTextCursor.SelectionType.LineUnderCursor: line = self._properties[0].position.line self._properties[0].position.pos = 0 - self._properties[0].anchor.pos = len(self._docData[line]) + self._properties[0].anchor.pos = len(self._document._dataLines[line]) elif selection == TTkTextCursor.SelectionType.WordUnderCursor: line = self._properties[0].position.line pos = self._properties[0].position.pos # Split the current line from the current cursor position # search the leftmost(on the right slice)/rightmost(on the left slice) word # in order to match the full word under the cursor - splitBefore = self._docData[line].substring(to=pos) - splitAfter = self._docData[line].substring(fr=pos) + splitBefore = self._document._dataLines[line].substring(to=pos) + splitAfter = self._document._dataLines[line].substring(fr=pos) xFrom = pos xTo = pos selectRE = '[a-zA-Z0-9:,./]*' @@ -161,7 +250,13 @@ class TTkTextCursor(): if not self.hasSelection(): return selSt = self.selectionStart() selEn = self.selectionEnd() - self._docData[selSt.line] = self._docData[selSt.line].substring(to=selSt.pos) + \ - self._docData[selEn.line].substring(fr=selEn.pos) - self._document._dataLines = self._docData[:selSt.line+1] + self._docData[selEn.line+1:] + self._document._dataLines[selSt.line] = self._document._dataLines[selSt.line].substring(to=selSt.pos) + \ + self._document._dataLines[selEn.line].substring(fr=selEn.pos) + self._document._dataLines = self._document._dataLines[:selSt.line+1] + self._document._dataLines[selEn.line+1:] self.setPosition(selSt.line, selSt.pos) + self._document.contentsChanged.emit() + self._document.contentsChange.emit(selSt.line, selEn.line-selSt.line, 1) + + def getHighlightedLine(self, line, color): + + return self._document._dataLines[line] diff --git a/TermTk/TTkGui/textdocument.py b/TermTk/TTkGui/textdocument.py index 50175e62..55f8c864 100644 --- a/TermTk/TTkGui/textdocument.py +++ b/TermTk/TTkGui/textdocument.py @@ -32,7 +32,7 @@ class TTkTextDocument(): 'contentsChange', 'contentsChanged', ) def __init__(self, *args, **kwargs): - self.contentsChange = pyTTkSignal(int,int,int,int) # int,int position, int charsRemoved, int charsAdded + self.contentsChange = pyTTkSignal(int,int,int) # int line, int linesRemoved, int linesAdded self.contentsChanged = pyTTkSignal() text = kwargs.get('text',"") self._dataLines = [TTkString(t) for t in text.split('\n')] @@ -46,15 +46,14 @@ class TTkTextDocument(): def setText(self, text): self._dataLines = [TTkString(t) for t in text.split('\n')] self.contentsChanged.emit() - self.contentsChange.emit(0,0,0,len(text)) + self.contentsChange.emit(0,0,len(self._dataLines)) def appendText(self, text): if type(text) == str: text = TTkString() + text oldLines = len(self._dataLines) - oldPos = len(self._dataLines[-1]) self._dataLines += text.split('\n') self.contentsChanged.emit() - self.contentsChange.emit(oldLines,oldPos,0,len(text)) + self.contentsChange.emit(oldLines,0,len(self._dataLines)-oldLines) diff --git a/TermTk/TTkWidgets/texedit.py b/TermTk/TTkWidgets/texedit.py index e89946e8..3f55a828 100644 --- a/TermTk/TTkWidgets/texedit.py +++ b/TermTk/TTkWidgets/texedit.py @@ -36,7 +36,7 @@ from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView class _TTkTextEditView(TTkAbstractScrollView): __slots__ = ( '_textDocument', '_hsize', '_lines', - '_textCursor', '_cursorPos', '_cursorParams', '_selectionFrom', '_selectionTo', + '_textCursor', '_cursorParams', '_tabSpaces', '_lineWrapMode', '_wordWrapMode', '_wrapWidth', '_lastWrapUsed', '_replace', @@ -56,6 +56,7 @@ class _TTkTextEditView(TTkAbstractScrollView): self._readOnly = True self._textDocument = TTkTextDocument() self._textCursor = TTkTextCursor(document=self._textDocument) + self._textDocument.contentsChanged.connect(self._rewrap) self._hsize = 0 self._lines = [(0,(0,0))] self._tabSpaces = 4 @@ -64,9 +65,6 @@ class _TTkTextEditView(TTkAbstractScrollView): self._lineWrapMode = TTkK.NoWrap self._wordWrapMode = TTkK.WrapAnywhere self._replace = False - self._cursorPos = (0,0) - self._selectionFrom = (0,0) - self._selectionTo = (0,0) self._cursorParams = None self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus) @@ -103,16 +101,13 @@ class _TTkTextEditView(TTkAbstractScrollView): @pyTTkSlot(str) def setText(self, text): self.viewMoveTo(0, 0) - self._textDocument = TTkTextDocument(text) - self._textCursor = TTkTextCursor(document=self._textDocument) + self._textDocument.setText(text) self._updateSize() - self._rewrap() @pyTTkSlot(str) def append(self, text): self._textDocument.appendText(text) self._updateSize() - self._rewrap() def _rewrap(self): self._lines = [] @@ -179,8 +174,9 @@ class _TTkTextEditView(TTkAbstractScrollView): return ox, oy = self.getViewOffsets() - y = self._textCursor.position().line - x,_ = self._cursorFromLinePos(y, self._textCursor.position().pos) + x,y = self._cursorFromDataPos( + self._textCursor.position().line, + self._textCursor.position().pos) y -= oy x -= ox @@ -204,38 +200,15 @@ class _TTkTextEditView(TTkAbstractScrollView): self.update() def _setCursorPos(self, x, y, alignRightTab=False): - x,y = self._cursorAlign(x,y, alignRightTab) - self._cursorPos = (x,y) - self._selectionFrom = (x,y) - self._selectionTo = (x,y) - self._setCursorPosNEW(x,y) - self._scrolToInclude(x,y) + self._setCursorPosNEW(x,y,alignRightTab) def _setCursorPosNEW(self, x, y, alignRightTab=False, moveAnchor=True): x,y = self._cursorAlign(x,y, alignRightTab) _, pos = self._linePosFromCursor(x,y) - self._textCursor.setPosition(y, pos, + self._textCursor.setPosition(self._lines[y][0], pos, moveMode=TTkTextCursor.MoveAnchor if moveAnchor else TTkTextCursor.KeepAnchor) - TTkLog.debug(f"{self._cursorPos=} - {pos=}") self._scrolToInclude(x,y) - def _moveHCursor(self, x,y, hoff): - l, dx = self._linePosFromCursor(x,y) - dt, _ = self._lines[y] - # Due to the internal usage I assume hoff 1 or -1 - dx += hoff - if hoff > 0 and dx>len(l) and dt0) - def _scrolToInclude(self, x, y): # Scroll the area (if required) to include the position x,y _,_,w,h = self.geometry() @@ -250,7 +223,6 @@ class _TTkTextEditView(TTkAbstractScrollView): def _eraseSelection(self): if not self._textCursor.hasSelection(): return self._textCursor.removeSelectedText() - self._rewrap() def _cursorAlign(self, x, y, alignRightTab = False): ''' @@ -281,6 +253,12 @@ class _TTkTextEditView(TTkAbstractScrollView): dt, (fr, to) = self._lines[y] return self._textDocument._dataLines[dt], fr+self._textDocument._dataLines[dt].substring(fr,to).tabCharPos(x,self._tabSpaces) + def _widgetPositionFromTextCursor(self, line, pos): + for i,l in enumerate(self._lines): + if l[0] == line and l[1][0] <= pos < l[1][1]: + return pos-l[1][0], i + return 0,0 + def _cursorFromLinePos(self,liney,p): ''' return the x,y cursor position relative to the widget from the @@ -325,94 +303,40 @@ class _TTkTextEditView(TTkAbstractScrollView): ox, oy = self.getViewOffsets() x,y = self._cursorAlign(evt.x + ox, evt.y + oy) self._setCursorPosNEW(x,y,moveAnchor=False) - cx = self._cursorPos[0] - cy = self._cursorPos[1] - - if y < cy: # Mouse Dragged above the cursor - self._selectionFrom = ( x, y ) - self._selectionTo = ( cx, cy ) - elif y > cy: # Mouse Dragged below the cursor - self._selectionFrom = ( cx, cy ) - self._selectionTo = ( x, y ) - else: # Mouse on the same line of the cursor - self._selectionFrom = ( min(cx,x), y ) - self._selectionTo = ( max(cx,x), y ) - self._scrolToInclude(x,y) - self.update() return True def mouseDoubleClickEvent(self, evt) -> bool: if self._readOnly: return super().mouseDoubleClickEvent(evt) - ox, oy = self.getViewOffsets() - x,y = self._cursorAlign(evt.x + ox, evt.y + oy) - - l,p = self._linePosFromCursor(x,y) - before = l.substring(to=p) - after = l.substring(fr=p) - - xFrom = len(before) - xTo = len(before) - - selectRE = '[a-zA-Z0-9:,./]*' - - if m := before.search(selectRE+'$'): - xFrom -= len(m.group(0)) - if m := after.search('^'+selectRE): - xTo += len(m.group(0)) - - self._selectionFrom = self._cursorFromLinePos(y,xFrom) - self._selectionTo = self._cursorFromLinePos(y,xTo) - self._cursorPos = self._selectionFrom - self._textCursor.select(TTkTextCursor.WordUnderCursor) - self.update() return True def mouseTapEvent(self, evt) -> bool: if self._readOnly: return super().mouseTapEvent(evt) - ox, oy = self.getViewOffsets() - x,y = self._cursorAlign(evt.x + ox, evt.y + oy) - self._cursorPos = (x,y) - - l,_ = self._linePosFromCursor(x,y) - - self._selectionFrom = self._cursorFromLinePos(y,0) - self._selectionTo = self._cursorFromLinePos(y,len(l)) - self._cursorPos = self._selectionFrom - self._textCursor.select(TTkTextCursor.LineUnderCursor) - self.update() return True - #def mouseReleaseEvent(self, evt) -> bool: - # if self._readOnly: - # return super().mouseReleaseEvent(evt) - # ox, oy = self.getViewOffsets() - # self._cursorPos = self._selectionFrom - # self.update() - # return True - def keyEvent(self, evt): if self._readOnly: return super().keyEvent(evt) if evt.type == TTkK.SpecialKey: _,_,w,h = self.geometry() - cx, cy = self._cursorPos + p = self._textCursor.position() + cx, cy = self._cursorFromDataPos(p.line, p.pos) dt, (fr, to) = self._lines[cy] # Don't Handle the special tab key, for now if evt.key == TTkK.Key_Tab: return False if evt.key == TTkK.Key_Up: self._setCursorPos(cx , cy-1) elif evt.key == TTkK.Key_Down: self._setCursorPos(cx , cy+1) - elif evt.key == TTkK.Key_Left: self._moveHCursor( cx , cy , -1 ) - elif evt.key == TTkK.Key_Right: self._moveHCursor( cx , cy , +1 ) + elif evt.key == TTkK.Key_Left: self._textCursor.movePosition(TTkTextCursor.Left) + elif evt.key == TTkK.Key_Right: self._textCursor.movePosition(TTkTextCursor.Right) elif evt.key == TTkK.Key_End: self._setCursorPos(w , cy ) elif evt.key == TTkK.Key_Home: self._setCursorPos(0 , cy ) elif evt.key == TTkK.Key_PageUp: self._setCursorPos(cx , cy - h) @@ -493,9 +417,9 @@ class _TTkTextEditView(TTkAbstractScrollView): for y, l in enumerate(self._lines[oy:oy+h]): t = self._textDocument._dataLines[l[0]] # apply the selection color if required - if selSt.line <= y+oy <= selEn.line: - pf = 0 if y+oy > selSt.line else selSt.pos - pt = len(t) if y+oy < selEn.line else selEn.pos + if selSt.line <= l[0] <= selEn.line: + pf = 0 if l[0] > selSt.line else selSt.pos + pt = len(t) if l[0] < selEn.line else selEn.pos t = t.setColor(color=selectColor, posFrom=pf, posTo=pt ) self._canvas.drawText(pos=(-ox,y), text=t.substring(l[1][0],l[1][1]).tab2spaces(self._tabSpaces)) if self._lineWrapMode == TTkK.FixedWidth: