From 2d84c389ce4c1294c1bd01f44ce7ea16e0e648fc Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 11 Jul 2022 23:24:24 +0100 Subject: [PATCH] Added multicursor kayboard movements --- TermTk/TTkCore/color.py | 3 ++ TermTk/TTkGui/textcursor.py | 98 +++++++++++++++++++++++++++++------- TermTk/TTkGui/textwrap.py | 4 +- TermTk/TTkWidgets/texedit.py | 29 +++++++---- demo/demo.py | 24 ++++----- 5 files changed, 114 insertions(+), 44 deletions(-) diff --git a/TermTk/TTkCore/color.py b/TermTk/TTkCore/color.py index 8173990f..74456bcf 100644 --- a/TermTk/TTkCore/color.py +++ b/TermTk/TTkCore/color.py @@ -252,6 +252,9 @@ class TTkColor(_TTkColor): STRIKETROUGH = _TTkColor(mod='\033[9m') ''':strike:`Striketrough` modifier''' + BLINKING = _TTkColor(mod='\033[5m') + '''"Blinking" modifier''' + @staticmethod def fg(*args, **kwargs): ''' Helper to generate a Foreground color diff --git a/TermTk/TTkGui/textcursor.py b/TermTk/TTkGui/textcursor.py index 39a17c14..2d5deb63 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.color import TTkColor from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.string import TTkString from TermTk.TTkGui.textdocument import TTkTextDocument @@ -179,25 +180,67 @@ class TTkTextCursor(): def position(self): return self._properties[0].position - def setPosition(self, line, pos, moveMode=MoveMode.MoveAnchor ): + def addCursor(self, line, pos): + self._properties.insert(0, + TTkTextCursor._prop( + TTkTextCursor._CP(line, pos), + TTkTextCursor._CP(line, pos))) + + def cleanCursors(self): + self._properties = self._properties[:1] + + def setPosition(self, line, pos, moveMode=MoveMode.MoveAnchor, cID=0): # TTkLog.debug(f"{line=}, {pos=}, {moveMode=}") - self._properties[0].position.set(line,pos) + self._properties[cID].position.set(line,pos) if moveMode==TTkTextCursor.MoveAnchor: - self._properties[0].anchor.set(line,pos) + self._properties[cID].anchor.set(line,pos) - def movePosition(self, operation, moveMode=MoveMode.MoveAnchor, n=1 ): - if operation == TTkTextCursor.Right: - p = self.position() + def movePosition(self, operation, moveMode=MoveMode.MoveAnchor, n=1, textWrap=None): + def moveRight(cID,p,_): if p.pos < len(self._document._dataLines[p.line]): - self.setPosition(p.line, p.pos+1, moveMode) + self.setPosition(p.line, p.pos+1, moveMode, cID=cID) elif p.line < len(self._document._dataLines)-1: - self.setPosition(p.line+1, 0, moveMode) - elif operation == TTkTextCursor.Left: - p = self.position() + self.setPosition(p.line+1, 0, moveMode, cID=cID) + def moveLeft(cID,p,_): if p.pos > 0: - self.setPosition(p.line, p.pos-1, moveMode) + self.setPosition(p.line, p.pos-1, moveMode, cID=cID) elif p.line > 0: - self.setPosition(p.line-1, len(self._document._dataLines[p.line-1]) , moveMode) + self.setPosition(p.line-1, len(self._document._dataLines[p.line-1]) , moveMode, cID=cID) + def moveUpDown(offset): + def _moveUpDown(cID,p,n): + cx, cy = textWrap.dataToScreenPosition(p.line, p.pos) + x, y = textWrap.normalizeScreenPosition(cx,cy+offset*n) + line, pos = textWrap.screenToDataPosition(x,y) + self.setPosition(line, pos, moveMode, cID=cID) + return _moveUpDown + def moveEnd(cID,p,_): + l = self._document._dataLines[p.line] + self.setPosition(p.line, len(l), moveMode, cID=cID) + def moveHome(cID,p,_): + self.setPosition(p.line, 0, moveMode, cID=cID) + + operations = { + TTkTextCursor.Right : moveRight, + TTkTextCursor.Left : moveLeft, + TTkTextCursor.Up : moveUpDown(-1), + TTkTextCursor.Down : moveUpDown(+1), + TTkTextCursor.EndOfLine : moveEnd, + TTkTextCursor.StartOfLine: moveHome + } + + for cID, prop in enumerate(self._properties): + p = prop.position + operations.get(operation,lambda _:_)(cID,p,n) + #if operation == TTkTextCursor.Right: + # if p.pos < len(self._document._dataLines[p.line]): + # self.setPosition(p.line, p.pos+1, moveMode, cID=cID) + # elif p.line < len(self._document._dataLines)-1: + # self.setPosition(p.line+1, 0, moveMode, cID=cID) + #elif operation == TTkTextCursor.Left: + # if p.pos > 0: + # self.setPosition(p.line, p.pos-1, moveMode, cID=cID) + # elif p.line > 0: + # self.setPosition(p.line-1, len(self._document._dataLines[p.line-1]) , moveMode, cID=cID) def document(self): return self._document @@ -272,13 +315,30 @@ class TTkTextCursor(): self._document.contentsChange.emit(a,b,c) def getHighlightedLines(self, fr, to, color): - selSt = self.selectionStart() - selEn = self.selectionEnd() - ret = [] - for i,l in enumerate(self._document._dataLines[fr:to+1],fr): - if selSt.line <= i <= selEn.line: + # Create a list of cursors (filtering out the ones which + # position/selection is outside the screen boundaries) + sel = [] + for p in self._properties: + selSt = p.selectionStart() + selEn = p.selectionEnd() + if selEn.line >= fr and selSt.line<=to: + sel.append((selSt,selEn,p)) + + # Retrieve the sublist of lines to be required (displayed) + ret = self._document._dataLines[fr:to+1] + # Apply the selection color for each of them + for s in sel: + selSt, selEn, _ = s + for i in range(max(selSt.line,fr),min(selEn.line+1,to+1)): + l = ret[i-fr] pf = 0 if i > selSt.line else selSt.pos pt = len(l) if i < selEn.line else selEn.pos - l = l.setColor(color=color, posFrom=pf, posTo=pt) - ret.append(l) + ret[i-fr] = l.setColor(color=color, posFrom=pf, posTo=pt) + # Add Blinking cursor + if len(sel)>1: + for s in sel: + _, _, prop = s + p = prop.position + ret[p.line-fr] = (ret[p.line-fr]+" ").setColor(color=color+TTkColor.BLINKING, posFrom=p.pos, posTo=p.pos+1) + return ret diff --git a/TermTk/TTkGui/textwrap.py b/TermTk/TTkGui/textwrap.py index d4216b8a..af957f1e 100644 --- a/TermTk/TTkGui/textwrap.py +++ b/TermTk/TTkGui/textwrap.py @@ -78,7 +78,7 @@ class TTkTextWrap(): self._lines = [] if not self._enable: def _process(i,l): - self._lines.append((i,(0,len(l)))) + self._lines.append((i,(0,len(l)+1))) else: if not (w := self._wrapWidth): return @@ -92,7 +92,7 @@ class TTkTextWrap(): while len(l): fl = l.tab2spaces(self._tabSpaces) if len(fl) <= w: - self._lines.append((i,(fr,fr+len(l)))) + self._lines.append((i,(fr,fr+len(l)+1))) l=[] else: to = max(1,l.tabCharPos(w,self._tabSpaces)) diff --git a/TermTk/TTkWidgets/texedit.py b/TermTk/TTkWidgets/texedit.py index cc74dabc..d5ed31d8 100644 --- a/TermTk/TTkWidgets/texedit.py +++ b/TermTk/TTkWidgets/texedit.py @@ -167,12 +167,16 @@ class _TTkTextEditView(TTkAbstractScrollView): TTkHelper.showCursor(TTkK.Cursor_Blinking_Bar) self.update() - def _setCursorPos(self, x, y, moveAnchor=True, newCursor=False): + def _setCursorPos(self, x, y, moveAnchor=True, addCursor=False): x,y = self._textWrap.normalizeScreenPosition(x,y) line, pos = self._textWrap.screenToDataPosition(x,y) - TTkLog.debug(f"{newCursor=}") - self._textCursor.setPosition(line, pos, - moveMode=TTkTextCursor.MoveAnchor if moveAnchor else TTkTextCursor.KeepAnchor) + TTkLog.debug(f"{addCursor=}") + if addCursor: + self._textCursor.addCursor(line, pos) + else: + self._textCursor.cleanCursors() + self._textCursor.setPosition(line, pos, + moveMode=TTkTextCursor.MoveAnchor if moveAnchor else TTkTextCursor.KeepAnchor ) self._scrolToInclude(x,y) return x, y @@ -188,7 +192,7 @@ class _TTkTextEditView(TTkAbstractScrollView): if self._readOnly: return super().mousePressEvent(evt) ox, oy = self.getViewOffsets() - self._setCursorPos(evt.x + ox, evt.y + oy, newCursor=evt.mod&TTkK.ControlModifier==TTkK.ControlModifier) + self._setCursorPos(evt.x + ox, evt.y + oy, addCursor=evt.mod&TTkK.ControlModifier==TTkK.ControlModifier) self.update() return True @@ -227,14 +231,14 @@ class _TTkTextEditView(TTkAbstractScrollView): # 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) + if evt.key == TTkK.Key_Up: self._textCursor.movePosition(TTkTextCursor.Up, textWrap=self._textWrap) + elif evt.key == TTkK.Key_Down: self._textCursor.movePosition(TTkTextCursor.Down, textWrap=self._textWrap) 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) - elif evt.key == TTkK.Key_PageDown: self._setCursorPos(cx , cy + h) + elif evt.key == TTkK.Key_End: self._textCursor.movePosition(TTkTextCursor.EndOfLine) + elif evt.key == TTkK.Key_Home: self._textCursor.movePosition(TTkTextCursor.StartOfLine) + elif evt.key == TTkK.Key_PageUp: self._textCursor.movePosition(TTkTextCursor.Up, textWrap=self._textWrap, n=h) #self._setCursorPos(cx , cy - h) + elif evt.key == TTkK.Key_PageDown: self._textCursor.movePosition(TTkTextCursor.Down, textWrap=self._textWrap, n=h) #self._setCursorPos(cx , cy + h) elif evt.key == TTkK.Key_Insert: self._replace = not self._replace self._setCursorPos(cx , cy) @@ -249,6 +253,9 @@ class _TTkTextEditView(TTkAbstractScrollView): elif evt.key == TTkK.Key_Enter: self._textCursor.insertText('\n') self._textCursor.movePosition(TTkTextCursor.Right) + p = self._textCursor.position() + cx, cy = self._textWrap.dataToScreenPosition(p.line, p.pos) + self._scrolToInclude(cx,cy) self.update() return True else: # Input char diff --git a/demo/demo.py b/demo/demo.py index ca00b6ea..46174bf7 100755 --- a/demo/demo.py +++ b/demo/demo.py @@ -66,29 +66,29 @@ def stupidPythonHighlighter(txt): return ret # Operators - txt = _colorize(re.compile('[\+\*\/\=\-\<\>\!]'), txt, ttk.TTkColor.fg('#00AAAA')) + txt = _colorize(re.compile(r'[\+\*\/\=\-\<\>\!]'), txt, ttk.TTkColor.fg('#00AAAA')) # Bool - txt = _colorize(re.compile('True|False'), txt, ttk.TTkColor.fg('#0000FF')) + txt = _colorize(re.compile(r'True|False'), txt, ttk.TTkColor.fg('#0000FF')) # Numbers - txt = _colorize(re.compile('[0-9]'), txt, ttk.TTkColor.fg('#00FFFF')) + txt = _colorize(re.compile(r'[0-9]'), txt, ttk.TTkColor.fg('#00FFFF')) # Functions - txt = _colorize(re.compile('[ \t]*def[ \t]*'), txt, ttk.TTkColor.fg('#00FFFF')) - txt = _colorize(re.compile('[^= \t\.\()]*[\t ]*\('), txt, ttk.TTkColor.fg('#AAAA00')) + txt = _colorize(re.compile(r'[ \t]*def[ \t]*'), txt, ttk.TTkColor.fg('#00FFFF')) + txt = _colorize(re.compile(r'[^= \t\.\()]*[\t ]*\('), txt, ttk.TTkColor.fg('#AAAA00')) # Objects - txt = _colorize(re.compile('[^= \t\.\()]*\.'), txt, ttk.TTkColor.fg('#44AA00')) + txt = _colorize(re.compile(r'[^= \t\.\()]*\.'), txt, ttk.TTkColor.fg('#44AA00')) # Fix Extra colors - txt = _colorize(re.compile('[\(\)]'), txt, ttk.TTkColor.RST) - txt = _colorize(re.compile("'[^']*'"), txt, ttk.TTkColor.fg('#FF8800')) + txt = _colorize(re.compile(r'[\(\)]'), txt, ttk.TTkColor.RST) + txt = _colorize(re.compile(r"'[^']*'"), txt, ttk.TTkColor.fg('#FF8800')) # Strings - txt = _colorize(re.compile('"[^"]*"'), txt, ttk.TTkColor.fg('#FF8800')) - txt = _colorize(re.compile("'[^']*'"), txt, ttk.TTkColor.fg('#FF8800')) + txt = _colorize(re.compile(r'"[^"]*"'), txt, ttk.TTkColor.fg('#FF8800')) + txt = _colorize(re.compile(r"'[^']*'"), txt, ttk.TTkColor.fg('#FF8800')) # Comments - txt = _colorize(re.compile('#.*\n'), txt, ttk.TTkColor.fg('#00FF00')) + txt = _colorize(re.compile(r'#.*\n'), txt, ttk.TTkColor.fg('#00FF00')) return txt def showSource(file): @@ -260,6 +260,6 @@ if __name__ == "__main__": main() def test_demo(): - root = ttk.TTk() + root = ttk.TTk(layout=ttk.TTkGridLayout()) assert demoShowcase(root) != None root.quit() \ No newline at end of file