Browse Source

Merge branch 'dev' into TTkTerminal

pull/162/head
Eugenio Parodi 3 years ago
parent
commit
92d4a73e06
  1. 32
      TermTk/TTkCore/TTkTerm/input.py
  2. 14
      TermTk/TTkCore/TTkTerm/term_base.py
  3. 10
      TermTk/TTkCore/string.py
  4. 7
      TermTk/TTkCore/ttk.py
  5. 20
      TermTk/TTkGui/textcursor.py
  6. 3
      TermTk/TTkWidgets/TTkPickers/textpicker.py
  7. 26
      TermTk/TTkWidgets/lineedit.py
  8. 29
      TermTk/TTkWidgets/texedit.py
  9. 3
      TermTk/TTkWidgets/widget.py
  10. 3
      tests/test.input.raw.py
  11. 57
      tests/test.input.win.py

32
TermTk/TTkCore/TTkTerm/input.py

@ -46,12 +46,16 @@ class TTkInput:
'_readInput',
'_leftLastTime', '_midLastTime', '_rightLastTime',
'_leftTap', '_midTap', '_rightTap',
'_pasteBuffer', '_bracketedPaste',
# Signals
'inputEvent'
'inputEvent', 'pasteEvent'
)
def __init__(self):
self.inputEvent = pyTTkSignal(TTkKeyEvent, TTkMouseEvent)
self.pasteEvent = pyTTkSignal(str)
self._pasteBuffer = ""
self._bracketedPaste = False
self._readInput = None
self._leftLastTime = 0
self._midLastTime = 0
@ -79,7 +83,20 @@ class TTkInput:
mouse_re = re.compile(r"\033\[<(\d+);(\d+);(\d+)([mM])")
def key_process(self, stdinRead):
if self._bracketedPaste:
if stdinRead.endswith("\033[201~"):
self._pasteBuffer += stdinRead[:-6]
self._bracketedPaste = False
# due to the CRNL methos (don't ask me why) the terminal
# is substituting all the \n with \r
self.pasteEvent.emit(self._pasteBuffer.replace('\r','\n'))
self._pasteBuffer = ""
else:
self._pasteBuffer += stdinRead
return
mevt,kevt = None, None
if not stdinRead.startswith("\033[<"):
# Key Event
kevt = TTkKeyEvent.parse(stdinRead)
@ -153,12 +170,17 @@ class TTkInput:
evt = TTkMouseEvent.Move
mevt = TTkMouseEvent(x, y, key, evt, mod, tap, m.group(0).replace("\033", "<ESC>"))
if kevt or mevt:
self.inputEvent.emit(kevt, mevt)
return
if kevt is None and mevt is None:
hex = [f"0x{ord(x):02x}" for x in stdinRead]
TTkLog.error("UNHANDLED: "+stdinRead.replace("\033","<ESC>") + " - "+",".join(hex))
if stdinRead.startswith("\033[200~"):
self._pasteBuffer = stdinRead[6:]
self._bracketedPaste = True
return
self.inputEvent.emit(kevt, mevt)
hex = [f"0x{ord(x):02x}" for x in stdinRead]
TTkLog.error("UNHANDLED: "+stdinRead.replace("\033","<ESC>") + " - "+",".join(hex))
def main():

14
TermTk/TTkCore/TTkTerm/term_base.py

@ -29,6 +29,9 @@ class TTkTermBase():
ALT_SCREEN = "\033[?1049h" #* Switch to alternate screen
NORMAL_SCREEN = "\033[?1049l" #* Switch to normal screen
SET_BRACKETED_PM = "\033[?2004h" # Ps = 2 0 0 4 ⇒ Set bracketed paste mode, xterm.
RESET_BRACKETED_PM = "\033[?2004l" # Ps = 2 0 0 4 ⇒ Reset bracketed paste mode, xterm.
class Mouse():
ON = "\033[?1002h\033[?1006h" # Enable reporting of mouse position on click and release
OFF = "\033[?1002l\033[?1006l" # Disable mouse reporting
@ -95,7 +98,10 @@ class TTkTermBase():
TTkTermBase.mouse = mouse | directMouse
TTkTermBase.directMouse = directMouse
TTkTermBase.Cursor.hide()
TTkTermBase.push(TTkTermBase.ALT_SCREEN + TTkTermBase.CLEAR + TTkTermBase.Cursor.HIDE + TTkTermBase.escTitle(TTkTermBase.title))
TTkTermBase.push(TTkTermBase.escTitle(TTkTermBase.title))
TTkTermBase.push(TTkTermBase.ALT_SCREEN)
TTkTermBase.push(TTkTermBase.SET_BRACKETED_PM)
TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.Cursor.HIDE)
if TTkTermBase.mouse:
TTkTermBase.push(TTkTermBase.Mouse.ON)
if TTkTermBase.directMouse:
@ -107,20 +113,20 @@ class TTkTermBase():
@staticmethod
def exit():
TTkTermBase.push(TTkTermBase.Mouse.OFF + TTkTermBase.Mouse.DIRECT_OFF)
TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.NORMAL_SCREEN + TTkTermBase.Cursor.SHOW + TTkTermBase.escTitle())
TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.NORMAL_SCREEN + TTkTermBase.RESET_BRACKETED_PM + TTkTermBase.Cursor.SHOW + TTkTermBase.escTitle())
TTkTermBase.setEcho(True)
TTkTermBase.CRNL(True)
@staticmethod
def stop():
TTkTermBase.push(TTkTermBase.Mouse.OFF + TTkTermBase.Mouse.DIRECT_OFF)
TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.NORMAL_SCREEN + TTkTermBase.Cursor.SHOW + TTkTermBase.escTitle())
TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.NORMAL_SCREEN + TTkTermBase.RESET_BRACKETED_PM + TTkTermBase.Cursor.SHOW + TTkTermBase.escTitle())
TTkTermBase.setEcho(True)
TTkTermBase.CRNL(True)
@staticmethod
def cont():
TTkTermBase.push(TTkTermBase.ALT_SCREEN + TTkTermBase.CLEAR + TTkTermBase.Cursor.HIDE + TTkTermBase.escTitle(TTkTermBase.title))
TTkTermBase.push(TTkTermBase.ALT_SCREEN + TTkTermBase.SET_BRACKETED_PM + TTkTermBase.CLEAR + TTkTermBase.Cursor.HIDE + TTkTermBase.escTitle(TTkTermBase.title))
if TTkTermBase.mouse:
TTkTermBase.push(TTkTermBase.Mouse.ON)
if TTkTermBase.directMouse:

10
TermTk/TTkCore/string.py

@ -72,6 +72,16 @@ class TTkString():
self._checkWidth()
# raise AttributeError(f"{type(text)} not supported in TTkString")
@staticmethod
def _importString1(text, colors):
ret = TTkString()
ret._text = text
ret._colors = colors
ret._baseColor = colors[-1]
ret._hasTab = '\t' in text
ret._checkWidth()
return ret
@staticmethod
def _parseAnsi(text, color = TTkColor.RST):
pos = 0

7
TermTk/TTkCore/ttk.py

@ -100,6 +100,7 @@ class TTk(TTkWidget):
self._termDirectMouse = kwargs.get('mouseTrack',False)
self._input = TTkInput()
self._input.inputEvent.connect(self._processInput)
self._input.pasteEvent.connect(self._processPaste)
self._title = kwargs.get('title','TermTk')
self._sigmask = kwargs.get('sigmask', TTkK.NONE)
self._showMouseCursor = os.environ.get("TTK_MOUSE",kwargs.get('mouseCursor', False))
@ -179,6 +180,12 @@ class TTk(TTkWidget):
return
self._input.start()
@pyTTkSlot(str)
def _processPaste(self, txt:str):
if focusWidget := TTkHelper.getFocus():
while focusWidget and not focusWidget.pasteEvent(txt):
focusWidget = focusWidget.parentWidget()
@pyTTkSlot(TTkKeyEvent, TTkMouseEvent)
def _processInput(self, kevt, mevt):
self._drawMutex.acquire()

20
TermTk/TTkGui/textcursor.py

@ -308,7 +308,7 @@ class TTkTextCursor():
l = self._document._dataLines[-1]
self.setPosition(len(self._document._dataLines)-1, len(l), moveMode, cID=cID)
operations = {
op = {
TTkTextCursor.Right : moveRight,
TTkTextCursor.Left : moveLeft,
TTkTextCursor.Up : moveUpDown(-1),
@ -316,18 +316,19 @@ class TTkTextCursor():
TTkTextCursor.EndOfLine : moveEndOfLine,
TTkTextCursor.StartOfLine: moveHome,
TTkTextCursor.End: moveEnd,
}
}.get(operation,lambda _:_)
for cID, prop in enumerate(self._properties):
for _ in range(n):
for cID, prop in enumerate(self._properties):
p = prop.position
operations.get(operation,lambda _:_)(cID,p,n)
op(cID,p,n)
self._checkCursors(notify=self.position().toNum()!=currPos)
def document(self):
return self._document
def replaceText(self, text):
def replaceText(self, text, moveCursor=False):
# if there is no selection, just select the next n chars till the end of the line
# the newline is not replaced
for p in self._properties:
@ -340,9 +341,9 @@ class TTkTextCursor():
pos = self._document._dataLines[line].nextPos(pos)
pos = min(size,pos)
p.anchor.set(line,pos)
return self.insertText(text)
return self.insertText(text, moveCursor)
def insertText(self, text):
def insertText(self, text, moveCursor=False):
_lineFirst = -1
if self.hasSelection():
_lineFirst, _lineRem, _lineAdd = self._removeSelectedText()
@ -395,6 +396,8 @@ class TTkTextCursor():
for nl in reversed(newLines[1:]):
self._document._dataLines.insert(l+1, nl)
# Move/Shift the cursors based on the pasted content
#
# 2 scenarios:
# 1) No Newline(s) added
# p p+1 p+2
@ -417,7 +420,8 @@ class TTkTextCursor():
diffPos = len(text.split('\n')[-1]) - p
else:
diffPos = len(text)
for pp in self._properties[i+1:]:
# Realign all the cursos (move the same if required)
for pp in self._properties[i+(0 if moveCursor else 1):]:
if pp.position.line == l:
pp.position.pos += diffPos
pp.anchor.pos += diffPos

3
TermTk/TTkWidgets/TTkPickers/textpicker.py

@ -165,8 +165,7 @@ class TTkTextDialogPicker(TTkWindow):
def _showEmojiPicker():
ep = _emojiPicker(size=(40,10))
def _addEmoji(e):
self._textEdit.textCursor().insertText(e)
self._textEdit.textCursor().movePosition(TTkTextCursor.Right)
self._textEdit.textCursor().insertText(e, moveCursor=True)
ep.emojiClicked.connect(_addEmoji)
TTkHelper.overlay(btn_emoji, ep, 0, 0)

26
TermTk/TTkWidgets/lineedit.py

@ -177,6 +177,32 @@ class TTkLineEdit(TTkWidget):
self.update()
return True
def pasteEvent(self, txt:str):
txt = TTkString().join(txt.split('\n'))
text = self._text
if self._selectionFrom < self._selectionTo:
pre = text.substring(to=self._selectionFrom)
post = text.substring(fr=self._selectionTo)
self._cursorPos = self._selectionFrom
else:
pre = text.substring(to=self._cursorPos)
if self._replace:
post = text.substring(fr=self._cursorPos+1)
else:
post = text.substring(fr=self._cursorPos)
text = pre + txt + post
if self._inputType & TTkK.Input_Number and \
not text.lstrip('-').isdigit():
return True
self.setText(text, self._cursorPos+txt.termWidth())
self._pushCursor()
self.textEdited.emit(self._text)
return True
def keyEvent(self, evt):
baseText = self._text
if evt.type == TTkK.SpecialKey:

29
TermTk/TTkWidgets/texedit.py

@ -289,9 +289,7 @@ class TTkTextEditView(TTkAbstractScrollView):
@pyTTkSlot()
def paste(self):
txt = self._clipboard.text()
if not self._multiLine:
txt = TTkString().join(txt.split('\n'))
self._textCursor.insertText(txt)
self.pasteEvent(txt)
@pyTTkSlot()
def _documentChanged(self):
@ -421,6 +419,23 @@ class TTkTextEditView(TTkAbstractScrollView):
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)
@ -523,8 +538,7 @@ class TTkTextEditView(TTkAbstractScrollView):
self._textCursor.removeSelectedText()
elif evt.key == TTkK.Key_Enter:
if self._multiLine:
self._textCursor.insertText('\n')
self._textCursor.movePosition(TTkTextCursor.Right)
self._textCursor.insertText('\n', moveCursor=True)
# Scroll to align to the cursor
p = self._textCursor.position()
cx, cy = self._textWrap.dataToScreenPosition(p.line, p.pos)
@ -535,10 +549,9 @@ class TTkTextEditView(TTkAbstractScrollView):
return True
else: # Input char
if self._replace:
self._textCursor.replaceText(evt.key)
self._textCursor.replaceText(evt.key, moveCursor=True)
else:
self._textCursor.insertText(evt.key)
self._textCursor.movePosition(TTkTextCursor.Right)
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)

3
TermTk/TTkWidgets/widget.py

@ -395,6 +395,9 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
return True
return False
def pasteEvent(self, txt:str):
return False
_mouseOver = None
_mouseOverTmp = None
_mouseOverProcessed = False

3
tests/test.input.raw.py

@ -42,6 +42,7 @@ def reset():
TTkTerm.push("\033[?1002l")
TTkTerm.push("\033[?1015l")
TTkTerm.push("\033[?1006l")
TTkTerm.push("\033[?1049l") # Switch to normal screen
TTkTerm.push("\033[?2004l") # Paste Bracketed mode
reset()
@ -51,6 +52,8 @@ TTkTerm.push("\033[?2004h") # Paste Bracketed mode
# TTkTerm.push("\033[?1002h")
# TTkTerm.push("\033[?1006h")
# TTkTerm.push("\033[?1015h")
TTkTerm.push("\033[?1049h") # Switch to alternate screen
# TTkTerm.push(TTkTerm.Mouse.ON)
# TTkTerm.push(TTkTerm.Mouse.DIRECT_ON)
TTkTerm.setEcho(False)

57
tests/test.input.win.py

@ -0,0 +1,57 @@
#!/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 sys, os
import logging
sys.path.append(os.path.join(sys.path[0],'..'))
from TermTk import TTkLog, TTkK, TTkGridLayout, TTk, TTkLogViewer, TTkHelper
def keyCallback(kevt=None, mevt=None):
if mevt is not None:
TTkLog.info(f"Mouse Event: {mevt}")
if kevt is not None:
if kevt.type == TTkK.Character:
TTkLog.info(f"Key Event: char '{kevt.key}' {kevt}")
else:
TTkLog.info(f"Key Event: Special '{kevt}'")
if kevt.key == "q":
input.close()
return False
return True
def pasteCallback(txt:str):
TTkLog.info(f"PASTE:")
for s in txt.split('\n'):
TTkLog.info(f" | {s}")
return True
root = TTk(layout=TTkGridLayout())
TTkLogViewer(parent=root)
TTkHelper._rootWidget._input.inputEvent.connect(keyCallback)
TTkHelper._rootWidget._input.pasteEvent.connect(pasteCallback)
root.mainloop()
Loading…
Cancel
Save