Browse Source

Added mouse selection to the text edit

pull/22/head
Eugenio Parodi 4 years ago
parent
commit
b748cc4f7e
  1. 3
      README.md
  2. 13
      TermTk/TTkCore/string.py
  3. 144
      TermTk/TTkWidgets/texedit.py
  4. 62
      demo/showcase/textedit.py

3
README.md

@ -90,8 +90,9 @@ cprofilev -f profiler.txt
- [Textual](https://github.com/Textualize/textual) - TUI (Text User Interface) framework for Python inspired by modern web development
- [Rich](https://github.com/Textualize/rich) - Python library for rich text and beautiful formatting in the terminal
- [PyCuT](https://github.com/ceccopierangiolieugenio/pyCuT) - terminal graphic library loosely based on QT api (my previous failed attempt)
- [pyTooling.TerminalUI](https://github.com/pyTooling/pyTooling.TerminalUI) - A set of helpers to implement a text user interface (TUI) in a terminal.
- Non Python
- [Turbo Vision](http://tvision.sourceforge.net)
- [ncurses](https://en.wikipedia.org/wiki/Ncurses)
- [tui.el](https://github.com/ebpa/tui.el) - An experimental text-based UI framework for Emacs modeled after React
- [tui.el](https://github.com/ebpa/tui.el) - An experimental text-based UI framework for Emacs modeled after React

13
TermTk/TTkCore/string.py

@ -160,10 +160,12 @@ class TTkString():
return ret
def setColor(self, color, match=None, posFrom=0, posTo=0):
def setColor(self, color, match=None, posFrom=None, posTo=None):
ret = TTkString()
ret._text += self._text
if match:
if posFrom == posTo == None:
ret._colors = [color]*len(self._text)
elif match:
ret._colors += self._colors
start=0
lenMatch = len(match)
@ -176,7 +178,7 @@ class TTkString():
for i in range(posFrom, posTo):
ret._colors[i] = color
else:
ret._colors = [color]*len(self._text)
ret._colors += self._colors
return ret
def substring(self, fr=None, to=None):
@ -206,4 +208,7 @@ class TTkString():
return re.search(regexp, self._text, re.IGNORECASE if ignoreCase else 0)
def findall(self, regexp, ignoreCase=False):
return re.findall(regexp, self._text, re.IGNORECASE if ignoreCase else 0)
return re.findall(regexp, self._text, re.IGNORECASE if ignoreCase else 0)
def getIndexes(self, char):
return [i for i,c in enumerate(self._text) if c==char]

144
TermTk/TTkWidgets/texedit.py

@ -33,34 +33,49 @@ from TermTk.TTkAbstract.abstractscrollarea import TTkAbstractScrollArea
from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView
'''
Design:
'''
class _TTkTextEditView(TTkAbstractScrollView):
__slots__ = ('_lines', '_hsize')
__slots__ = (
'_lines', '_hsize',
'_cursorPos', '_cursorParams', '_selectionFrom', '_selectionTo',
'_replace',
'_readOnly'
)
def __init__(self, *args, **kwargs):
super().__init__(self, *args, **kwargs)
self._name = kwargs.get('name' , '_TTkTextEditView' )
self._readOnly = True
self._hsize = 0
self._lines = []
self._lines = ['']
self._replace = False
self._cursorPos = (0,0)
self._selectionFrom = (0,0)
self._selectionTo = (0,0)
self._cursorParams = None
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
@pyTTkSlot(str)
def setText(self, text):
self._lines = [line for line in text.split('\n')]
self.viewMoveTo(0, 0)
self._updateSize()
self.viewChanged.emit()
self.update()
def isReadOnly(self) -> bool :
return self._readOnly
def setReadOnly(self, ro):
self._readOnly = ro
@pyTTkSlot(str)
def setLines(self, lines):
self._lines = lines
def setText(self, text):
if type(text) == str:
text = TTkString() + text
self._lines = text.split('\n')
self.viewMoveTo(0, 0)
self._updateSize()
self.viewChanged.emit()
self.update()
def _updateSize(self):
self._hsize = 0
for l in self._lines:
self._hsize = max(self._hsize, len(l))
self._hsize = max( [ len(l) for l in self._lines ] )
def viewFullAreaSize(self) -> (int, int):
return self._hsize, len(self._lines)
@ -68,17 +83,112 @@ class _TTkTextEditView(TTkAbstractScrollView):
def viewDisplayedSize(self) -> (int, int):
return self.size()
def _pushCursor(self):
if self._readOnly or not self.hasFocus():
return
ox, oy = self.getViewOffsets()
x = self._cursorPos[0]-ox
y = self._cursorPos[1]-oy
if x >= self.width() or y>=self.height() or \
self._selectionFrom != self._selectionTo or \
x<0 or y<0:
TTkHelper.hideCursor()
return
# Avoid the show/move cursor to be called again if in the same position
if self._cursorParams and \
self._cursorParams['pos'] == (x,y) and \
self._cursorParams['replace'] == self._replace:
return
self._cursorParams = {'pos': (x,y), 'replace': self._replace}
TTkHelper.moveCursor(self,x,y)
if self._replace:
TTkHelper.showCursor(TTkK.Cursor_Blinking_Block)
else:
TTkHelper.showCursor(TTkK.Cursor_Blinking_Bar)
self.update()
def mousePressEvent(self, evt) -> bool:
if self._readOnly:
return super().mousePressEvent(evt)
ox, oy = self.getViewOffsets()
y = max(0,min(evt.y + oy,len(self._lines)))
x = max(0,min(evt.x + ox,len(self._lines[y])))
self._cursorPos = (x,y)
self._selectionFrom = (x,y)
self._selectionTo = (x,y)
# TTkLog.debug(f"{self._cursorPos=}")
self.update()
return True
def mouseDragEvent(self, evt) -> bool:
if self._readOnly:
return super().mouseDragEvent(evt)
ox, oy = self.getViewOffsets()
y = max(0,min(evt.y + oy,len(self._lines)))
x = max(0,min(evt.x + ox,len(self._lines[y])))
self._selectionFrom = ( min(x,self._cursorPos[0]), min(y,self._cursorPos[1]) )
self._selectionTo = ( max(x,self._cursorPos[0]), max(y,self._cursorPos[1]) )
self.update()
return True
def mouseDoubleClickEvent(self, evt) -> bool:
if self._readOnly:
return super().mouseDoubleClickEvent(evt)
return True
def mouseTapEvent(self, evt) -> bool:
if self._readOnly:
return super().mouseTapEvent(evt)
return True
def keyEvent(self, evt):
if self._readOnly:
return super().keyEvent(evt)
return True
def focusInEvent(self):
self._pushCursor()
self.update()
def focusOutEvent(self):
TTkHelper.hideCursor()
def paintEvent(self):
ox, oy = self.getViewOffsets()
for y, t in enumerate(self._lines[oy:]):
if self.hasFocus():
color = TTkCfg.theme.lineEditTextColorFocus
selectColor = TTkCfg.theme.lineEditTextColorSelected
else:
color = TTkCfg.theme.lineEditTextColor
selectColor = TTkCfg.theme.lineEditTextColorSelected
h = self.height()
for y, t in enumerate(self._lines[oy:oy+h]):
if self._selectionFrom[1] <= y+oy <= self._selectionTo[1]:
pf = 0 if y+oy > self._selectionFrom[1] else self._selectionFrom[0]
pt = len(t) if y+oy < self._selectionTo[1] else self._selectionTo[0]
t = t.setColor(color=selectColor, posFrom=pf, posTo=pt )
self._canvas.drawText(pos=(-ox,y), text=t)
self._pushCursor()
class TTkTextEdit(TTkAbstractScrollArea):
__slots__ = ('_textEditView', 'setText', 'setColoredLines')
__slots__ = (
'_textEditView',
# Forwarded Methods
'setText', 'isReadOnly', 'setReadOnly'
)
def __init__(self, *args, **kwargs):
super().__init__(self, *args, **kwargs)
self._name = kwargs.get('name' , 'TTkTextEdit' )
self._textEditView = _TTkTextEditView()
self.setViewport(self._textEditView)
self.setText = self._textEditView.setText
self.setLines = self._textEditView.setLines
self.isReadOnly = self._textEditView.isReadOnly
self.setReadOnly = self._textEditView.setReadOnly

62
demo/showcase/textedit.py

@ -0,0 +1,62 @@
#!/usr/bin/env python3
# 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.
import os
import sys
import random
import argparse
sys.path.append(os.path.join(sys.path[0],'../..'))
import TermTk as ttk
words = ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit,", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua.", "Ut", "enim", "ad", "minim", "veniam,", "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip", "ex", "ea", "commodo", "consequat.", "Duis", "aute", "irure", "dolor", "in", "reprehenderit", "in", "voluptate", "velit", "esse", "cillum", "dolore", "eu", "fugiat", "nulla", "pariatur.", "Excepteur", "sint", "occaecat", "cupidatat", "non", "proident,", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollit", "anim", "id", "est", "laborum."]
def getWord():
return random.choice(words)
def getSentence(a,b):
return " ".join([getWord() for i in range(0,random.randint(a,b))])
def demoTextEdit(root=None):
te = ttk.TTkTextEdit(parent=root)
te.setReadOnly(False)
te.setText('\n'.join([ getSentence(10,20) for _ in range(50)]))
return te
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-f', help='Full Screen', action='store_true')
args = parser.parse_args()
ttk.TTkLog.use_default_file_logging()
root = ttk.TTk()
if args.f:
rootTree1 = root
root.setLayout(ttk.TTkGridLayout())
else:
rootTree1 = ttk.TTkWindow(parent=root,pos = (0,0), size=(70,40), title="Test Text Edit", layout=ttk.TTkGridLayout(), border=True)
demoTextEdit(rootTree1)
root.mainloop()
if __name__ == "__main__":
main()
Loading…
Cancel
Save