From 89939f6f41fcecfd19952a6c637766f7e2076c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eugenio=20Parodi=20=F0=9F=8C=B6=EF=B8=8F?= Date: Tue, 21 Jan 2025 11:19:34 +0000 Subject: [PATCH] Added the ExtraSelections in the TTKTextEdit --- TermTk/TTkCore/canvas.py | 18 +- TermTk/TTkCore/color.py | 62 ++----- TermTk/TTkCore/constant.py | 11 ++ TermTk/TTkCore/string.py | 10 +- TermTk/TTkGui/textcursor.py | 50 +++-- TermTk/TTkWidgets/texedit.py | 157 ++++++++++++++-- tests/t.draw/test.draw.009.colorMix.py | 82 +++++++++ .../t.ui/test.ui.018.TextEdit.04.Pygments.py | 173 ++++++++++++++++++ 8 files changed, 467 insertions(+), 96 deletions(-) create mode 100755 tests/t.draw/test.draw.009.colorMix.py create mode 100755 tests/t.ui/test.ui.018.TextEdit.04.Pygments.py diff --git a/TermTk/TTkCore/canvas.py b/TermTk/TTkCore/canvas.py index 08ac6cf1..aa19fd32 100644 --- a/TermTk/TTkCore/canvas.py +++ b/TermTk/TTkCore/canvas.py @@ -232,18 +232,16 @@ class TTkCanvas(): text = text.align(width=width, alignment=alignment, color=color) txt, colors = text.tab2spaces().getData() + a,b = max(0,-x), min(len(txt),self._width-x) + self._data[y][x+a:x+b] = txt[a:b] if forceColor: colors=[color]*len(colors) - a,b = max(0,-x), min(len(txt),self._width-x) - for i in range(a,b): - #self._set(y, x+i, txt[i-x], colors[i-x]) - self._data[y][x+i] = txt[i] - if colors[i] == TTkColor.RST != color: - self._colors[y][x+i] = color.mod(x+i,y) - elif (not colors[i].hasBackground()) and color.hasBackground(): - self._colors[y][x+i] = (color + colors[i]).mod(x+i,y) - else: - self._colors[y][x+i] = colors[i].mod(x+i,y) + else: + for i in range(a,b): + if color != TTkColor.RST: + self._colors[y][x+i] = (colors[i] | color).mod(x+i,y) + else: + self._colors[y][x+i] = colors[i].mod(x+i,y) # Check the full wide chars on the edge of the two canvasses if ((0 <= (x+a) < self._width) and self._data[y][x+a] == ''): self._data[y][x+a] = TTkCfg.theme.unicodeWideOverflowCh[0] diff --git a/TermTk/TTkCore/color.py b/TermTk/TTkCore/color.py index 1242b92d..37d14ddc 100644 --- a/TermTk/TTkCore/color.py +++ b/TermTk/TTkCore/color.py @@ -205,19 +205,11 @@ class _TTkColor: self._fg == other._fg and self._bg == other._bg ) - # # self | other - # def __or__(self, other): - # # TTkLog.debug("__add__") - # if other._clean: - # return other - # clean = self._clean - # fg: str = self._fg or other._fg - # bg: str = self._bg or other._bg - # colorMod = self._colorMod or other._colorMod - # return _TTkColor( - # fg=fg, bg=bg, - # colorMod=colorMod, - # clean=clean) + # self | other + def __or__(self, other): + c = self.copy() + c._clean = False + return other + c # self + other def __add__(self, other): @@ -307,21 +299,11 @@ class _TTkColor_mod(_TTkColor): ( self._mod == (other._mod if isinstance(other,_TTkColor_mod) else 0)) ) - # # self | other - # def __or__(self, other): - # # TTkLog.debug("__add__") - # if other._clean: - # return other - # otherMod = other._mod if isinstance(other,_TTkColor_mod) else 0 - # clean = self._clean - # fg: str = self._fg or other._fg - # bg: str = self._bg or other._bg - # mod: str = self._mod + otherMod - # colorMod = self._colorMod or other._colorMod - # return _TTkColor_mod( - # fg=fg, bg=bg, mod=mod, - # colorMod=colorMod, - # clean=clean) + # self | other + def __or__(self, other): + c = self.copy() + c._clean = False + return other + c # self + other def __add__(self, other): @@ -407,24 +389,12 @@ class _TTkColor_mod_link(_TTkColor_mod): ( self._link == (other._link if isinstance(other,_TTkColor_mod_link) else 0)) ) - # # self | other - # def __or__(self, other): - # # TTkLog.debug("__add__") - # if other._clean: - # return other - # otherMod = other._mod if isinstance(other,_TTkColor_mod) else 0 - # otherLink = other._link if isinstance(other,_TTkColor_mod_link) else '' - # clean = self._clean - # fg: str = self._fg or other._fg - # bg: str = self._bg or other._bg - # mod: str = self._mod + otherMod - # link:str = self._link or otherLink - # colorMod = self._colorMod or other._colorMod - # return _TTkColor_mod_link( - # fg=fg, bg=bg, mod=mod, - # colorMod=colorMod, link=link, - # clean=clean) - + # self | other + def __or__(self, other): + c = self.copy() + c._clean = False + return other + c + # self + other def __add__(self, other): # TTkLog.debug("__add__") diff --git a/TermTk/TTkCore/constant.py b/TermTk/TTkCore/constant.py index a7261611..af8753d8 100644 --- a/TermTk/TTkCore/constant.py +++ b/TermTk/TTkCore/constant.py @@ -125,6 +125,17 @@ class TTkConstant: # ContiguousSelection = SelectionMode.ContiguousSelection MultiSelection = SelectionMode.MultiSelection + class SelectionFormat(int): + ''' + Selection properties + + .. autosummary:: + FullWidthSelection + ''' + FullWidthSelection = 0x06000 + '''When set on the characterFormat of a selection, the whole width of the text will be shown selected.''' + + # Graph types FILLED = 0x0001 LINE = 0x0002 diff --git a/TermTk/TTkCore/string.py b/TermTk/TTkCore/string.py index 983a1775..3e58e928 100644 --- a/TermTk/TTkCore/string.py +++ b/TermTk/TTkCore/string.py @@ -452,7 +452,7 @@ class TTkString(): return ret - def completeColor(self, color, match=None, posFrom=None, posTo=None) -> Self: + def completeColor(self, color:TTkColor, match=None, posFrom=None, posTo=None) -> Self: ''' Complete the color of the entire string or a slice of it The Fg and/or Bg of the string is replaced with the selected Fg/Bg color only if missing @@ -479,17 +479,17 @@ class TTkString(): while pos := self._text.index(match, start) if match in self._text[start:] else None: start = pos+lenMatch for i in range(pos, pos+lenMatch): - ret._colors[i] += color + ret._colors[i] |= color elif posFrom == posTo == None: - ret._colors = [c+color for c in self._colors] + ret._colors = [c|color for c in self._colors] elif posFrom < posTo: ret._colors = self._colors.copy() posFrom = min(len(self._text),posFrom) posTo = min(len(self._text),posTo) for i in range(posFrom, posTo): - ret._colors[i] += color + ret._colors[i] |= color else: - ret._colors = [c+color for c in self._colors] + ret._colors = [c|color for c in self._colors] return ret diff --git a/TermTk/TTkGui/textcursor.py b/TermTk/TTkGui/textcursor.py index 255990ee..7f535587 100644 --- a/TermTk/TTkGui/textcursor.py +++ b/TermTk/TTkGui/textcursor.py @@ -221,6 +221,9 @@ class TTkTextCursor(): def position(self) -> _CP: return self._properties[self._cID].position + + def cursors(self) -> list[_CP]: + return self._properties def addCursor(self, line:int, pos:int) -> None: self._cID = 0 @@ -490,6 +493,7 @@ class TTkTextCursor(): xTo += len(m.group(0)) p.position.pos = xTo p.anchor.pos = xFrom + p.anchor.line = line self._checkCursors(notify=self.position().toNum()!=currPos) def selectedText(self) -> TTkString: @@ -577,12 +581,38 @@ class TTkTextCursor(): self._document._dataLines[l] = line.setColor(color=color, posFrom=pf, posTo=pt) self._autoChanged = True self._document.setChanged(True) - self._document._acquire() + self._document._release() self._document.contentsChanged.emit() # self._document.contentsChange.emit(0,0,0) self._autoChanged = True - def getHighlightedLines(self, fr:int, to:int, color:TTkColor) -> list[TTkString]: + def _getCoveredLines(self, fr:int, to:int, lines:list[TTkColor], color:TTkColor) -> list[TTkColor]: + for p in self._properties: + selSt = p.selectionStart().line + selEn = p.selectionEnd().line + if selEn >= fr and selSt<=to: + for i in range(max(selSt,fr),min(selEn,to)+1): + lines[i-fr] = color + return lines + + def _getBlinkingCursors(self, fr:int, to:int, lines, color:TTkColor) -> list[TTkString]: + ret = lines + # Add Blinking cursor + if len(self._properties)>1: + for p in self._properties: + cp = p.position + if not 0<=(cp.line-fr) list[TTkString]: # Create a list of cursors (filtering out the ones which # position/selection is outside the screen boundaries) sel = [] @@ -593,7 +623,7 @@ class TTkTextCursor(): sel.append((selSt,selEn,p)) # Retrieve the sublist of lines to be required (displayed) - ret = self._document._dataLines[fr:to+1] + ret = lines # Apply the selection color for each of them for s in sel: selSt, selEn, _ = s @@ -602,18 +632,4 @@ class TTkTextCursor(): pf = 0 if i > selSt.line else selSt.pos pt = len(l) if i < selEn.line else selEn.pos ret[i-fr] = l.setColor(color=color, posFrom=pf, posTo=pt) - # Add Blinking cursor - if len(self._properties)>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) - if p.pos == len(ret[p.line-fr]): - ret[p.line-fr] = ret[p.line-fr]+TTkString('↵',color+TTkColor.BLINKING) - elif ret[p.line-fr].charAt(p.pos) == ' ': - ret[p.line-fr].setCharAt(pos=p.pos, char='∙') - # ret[p.line-fr].setColorAt(pos=p.pos, color=TTkCfg.theme.treeLineColor+TTkColor.BLINKING) - #elif ret[p.line-fr].charAt(p.pos) == '\t': - # ret[p.line-fr].setCharAt(pos=p.pos, char='\t') - return ret diff --git a/TermTk/TTkWidgets/texedit.py b/TermTk/TTkWidgets/texedit.py index 415335b4..e5d008a4 100644 --- a/TermTk/TTkWidgets/texedit.py +++ b/TermTk/TTkWidgets/texedit.py @@ -22,7 +22,6 @@ __all__ = ['TTkTextEditView', 'TTkTextEdit'] -from math import log10, floor from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.cfg import TTkCfg @@ -127,6 +126,79 @@ class TTkTextEditView(TTkAbstractScrollView): :ref:`ttkdesigner Tutorial ` ''' + + class ExtraSelection(): + ''' + The :py:class:`ExtraSelection` structure provides a way of specifying a character format for a given selection in a document. + + :param format: A format that is used to specify the type of selection, defaults to :py:class:`TTkK.NONE`. + :type format: :py:class:`TTkK.SelectionFormat` + :param color: The color used to specify the foreground/background color/mod for the selection. + :type color: :py:class:`TTkColor` + :param cursor: A cursor that contains a selection in a :py:class:`QTextDocument`. + :type cursor: :py:class:`TTkTextCursor` + ''' + + __slots__ = ('_format', '_color', '_cursor') + def __init__(self, + format:TTkK.SelectionFormat=TTkK.NONE, + color:TTkColor=TTkColor.RST, + cursor:TTkTextCursor=None) -> None: + self._color = color + self._format = format + self._cursor = cursor if cursor else TTkTextCursor() + + def color(self) -> TTkColor: + ''' + This propery holds the color that is used for the selection. + + :rtype: :py:class:`TTkColor` + ''' + return self._color + + def setColor(self, color:TTkColor) -> None: + ''' + Set the color. + + :param color: A color that is used for the selection. + :type color: :py:class:`TTkColor` + ''' + self._color = color + + def format(self) -> TTkK.SelectionFormat: + ''' + This propery holds the format that is used to specify the type of selection. + + :rtype: :py:class:`TTkK.SelectionFormat` + ''' + return self._format + + def setFormat(self, format:TTkK.SelectionFormat) -> None: + ''' + Set the format. + + :param format: A format that is used to specify the type of selection. + :type format: :py:class:`TTkK.SelectionFormat` + ''' + self._format = format + + def cursor(self) -> TTkTextCursor: + ''' + This propery holds the fcursor that contains a selection in a :py:class:`QTextDocument`. + + :rtype: :py:class:`TTkTextCursor` + ''' + return self._cursor + + def setCursor(self, cursor:TTkTextCursor) -> None: + ''' + Set the cursor. + + :param cursor: A cursor that contains a selection in a :py:class:`QTextDocument`. + :type cursor: :py:class:`TTkTextCursor` + ''' + self._cursor = cursor + currentColorChanged:pyTTkSignal ''' This signal is emitted if the current character color has changed, @@ -136,6 +208,14 @@ class TTkTextEditView(TTkAbstractScrollView): :type color: :py:class:`TTkColor` ''' + cursorPositionChanged:pyTTkSignal + ''' + This signal is emitted whenever the position of the cursor changed. + + :param cursor: the cursor changed. + :type cursor: :py:class:`TTkTextCursor` + ''' + undoAvailable:pyTTkSignal ''' This signal is emitted whenever undo operations become available (available is true) @@ -175,6 +255,7 @@ class TTkTextEditView(TTkAbstractScrollView): __slots__ = ( '_textDocument', '_hsize', '_textCursor', '_cursorParams', + '_extraSelections', '_textWrap', '_lineWrapMode', '_lastWrapUsed', '_replace', '_readOnly', '_multiCursor', @@ -185,7 +266,7 @@ class TTkTextEditView(TTkAbstractScrollView): # 'wrapWidth', 'setWrapWidth', # 'wordWrapMode', 'setWordWrapMode', # Signals - 'currentColorChanged', + 'currentColorChanged', 'cursorPositionChanged', 'undoAvailable', 'redoAvailable', 'textChanged' ) @@ -217,20 +298,22 @@ class TTkTextEditView(TTkAbstractScrollView): ''' self.currentColorChanged = pyTTkSignal(TTkColor) + self.cursorPositionChanged = pyTTkSignal(TTkTextCursor) self.undoAvailable = pyTTkSignal(bool) self.redoAvailable = pyTTkSignal(bool) self.textChanged = pyTTkSignal() - self._readOnly = readOnly - self._multiLine = multiLine - self._multiCursor = True - self._hsize = 0 + self._readOnly:bool = readOnly + self._multiLine:bool = multiLine + self._multiCursor:bool = True + self._extraSelections:list[TTkTextEditView.ExtraSelection] = [] + self._hsize:int = 0 self._lastWrapUsed = 0 self._lineWrapMode = TTkK.NoWrap self._replace = False self._cursorParams = None - self._textDocument = None - self._textCursor = None + self._textDocument:TTkTextDocument = None + self._textCursor:TTkTextCursor = None self._textWrap = None self._clipboard = TTkClipboard() @@ -326,6 +409,26 @@ class TTkTextEditView(TTkAbstractScrollView): def wordWrapMode(self, *args, **kwargs) -> None: return self._textWrap.wordWrapMode(*args, **kwargs) def setWordWrapMode(self, *args, **kwargs) -> None: return self._textWrap.setWordWrapMode(*args, **kwargs) + def extraSelections(self) -> list[ExtraSelection]: + ''' + Returns previously set extra selections. + + :rtype: list[:py:class:`ExtraSelection`] + ''' + return self._extraSelections + + def setExtraSelections(self, extraSelections:list[ExtraSelection]) -> None: + ''' + This function allows temporarily marking certain regions in the document with a given color, + specified as selections. This can be useful for example in a programming editor to mark a + whole line of text with a given background color to indicate the existence of a breakpoint. + + :param extraSelections: the list of extra selections. + :type extraSelections: list[:py:class:`ExtraSelection`] + ''' + self._extraSelections = extraSelections + self.update() + def textCursor(self) -> TTkTextCursor: return self._textCursor @@ -417,6 +520,7 @@ class TTkTextEditView(TTkAbstractScrollView): def _cursorPositionChanged(self, cursor:TTkTextCursor) -> None: if cursor == self._textCursor: self.currentColorChanged.emit(cursor.positionColor()) + self.cursorPositionChanged.emit(cursor) self._pushCursor() def resizeEvent(self, w:int, h:int) -> None: @@ -676,43 +780,59 @@ class TTkTextEditView(TTkAbstractScrollView): def paintEvent(self, canvas: TTkCanvas) -> None: ox, oy = self.getViewOffsets() + w,h = self.size() style = self.currentStyle() color = style['color'] selectColor = style['selectedColor'] lineColor = style['lineColor'] - backgroundColor = self._textDocument._backgroundColor + backgroundColors = [self._textDocument._backgroundColor]*h - if backgroundColor != TTkColor.RST: - canvas.fill(color=backgroundColor) - - 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) + fr = subLines[0][0] + to = subLines[-1][0] + outLines = self._textDocument._dataLines[fr:to+1] + outLines = self._textCursor._getHighlightedLines(fr, to, outLines, selectColor) + + for extraSelection in self._extraSelections: + esCursor = extraSelection._cursor + esColor = extraSelection._color + esFormat = extraSelection._format + if esFormat == TTkK.SelectionFormat.FullWidthSelection: + backgroundColors = esCursor._getCoveredLines(fr, to, backgroundColors, esColor) + outLines = esCursor._getHighlightedLines(fr, to, outLines, esColor) + + outLines = self._textCursor._getBlinkingCursors(fr, to, outLines, selectColor) for y, l in enumerate(subLines): - t = outLines[l[0]-subLines[0][0]] + t = outLines[l[0]-subLines[0][0]] + bg = backgroundColors[l[0]-subLines[0][0]] text:TTkString = t.substring(l[1][0],l[1][1]).tab2spaces(self._textWrap._tabSpaces) - if backgroundColor != TTkColor.RST: - text = text.completeColor(backgroundColor) + if bg != TTkColor.RST: + canvas.fill(color=bg,pos=(0,y), size=(w,1)) + text = text.completeColor(bg) canvas.drawTTkString(pos=(-ox,y), text=text) if self._lineWrapMode == TTkK.FixedWidth: canvas.drawVLine(pos=(self._textWrap._wrapWidth,0), size=h, color=lineColor) + + 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__ + ExtraSelection = TTkTextEditView.ExtraSelection + __slots__ = ( ['_textEditView', '_lineNumberView', '_lineNumber'] + (_forwardedSignals:=[ # Forwarded Signals From TTkTexteditView # Signals - 'focusChanged', 'currentColorChanged', + 'focusChanged', 'currentColorChanged', 'cursorPositionChanged', 'undoAvailable', 'redoAvailable', 'textChanged']) + (_forwardedMethods:=[ # Forwarded Methods From TTkTexteditView @@ -723,6 +843,7 @@ class TTkTextEdit(TTkAbstractScrollArea): 'lineWrapMode', 'setLineWrapMode', 'wordWrapMode', 'setWordWrapMode', 'textCursor', 'setFocus', 'setColor', + 'extraSelections', 'setExtraSelections', 'cut', 'copy', 'paste', 'undo', 'redo', 'isUndoAvailable', 'isRedoAvailable', # Export Methods, diff --git a/tests/t.draw/test.draw.009.colorMix.py b/tests/t.draw/test.draw.009.colorMix.py new file mode 100755 index 00000000..9b136059 --- /dev/null +++ b/tests/t.draw/test.draw.009.colorMix.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 Eugenio Parodi +# +# 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 + +sys.path.append(os.path.join(sys.path[0],'../..')) +import TermTk as ttk + +def testColor(prefix:str,c1:ttk.TTkColor, c2:ttk.TTkColor): + str1 = ttk.TTkString('Eugenio',c1) + str2 = ttk.TTkString('Parodi' ,c2) + rst = "\033]8;;\033\\" + print(prefix + (str1+str2).toAnsi() + rst) + +fg_r = ttk.TTkColor.FG_RED +fg_g = ttk.TTkColor.FG_GREEN +fg_b = ttk.TTkColor.FG_BLUE +bg_r = ttk.TTkColor.BG_RED +bg_g = ttk.TTkColor.BG_GREEN +bg_b = ttk.TTkColor.BG_BLUE +fg_r_l = ttk.TTkColor.fg('#FF0000',link='https://github.com/ceccopierangiolieugenio/pyTermTk') +fg_g_l = ttk.TTkColor.fg('#00FF00',link='https://github.com/ceccopierangiolieugenio/pyTermTk') +fg_b_l = ttk.TTkColor.fg('#0000FF',link='https://github.com/ceccopierangiolieugenio/pyTermTk') +bg_r_l = ttk.TTkColor.bg('#FF0000',link='https://github.com/ceccopierangiolieugenio/pyTermTk') +bg_g_l = ttk.TTkColor.bg('#00FF00',link='https://github.com/ceccopierangiolieugenio/pyTermTk') +bg_b_l = ttk.TTkColor.bg('#0000FF',link='https://github.com/ceccopierangiolieugenio/pyTermTk') +testColor("r ",fg_r, bg_r) +testColor("g ",fg_g, bg_g) +testColor("b ",fg_b, bg_b) +testColor("rl ",fg_r_l, bg_r_l) +testColor("gl ",fg_g_l, bg_g_l) +testColor("bl ",fg_b_l, bg_b_l) + +c1 = fg_r + bg_g +c2 = fg_r | bg_g +testColor("T1 ",c1,c2) + +m1 = bg_b + fg_g +m2 = bg_b | fg_g +testColor("T2 ",m1,m2) + +m3 = ttk.TTkColor.FG_YELLOW + m1 +m4 = m1 + ttk.TTkColor.FG_YELLOW +m5 = ttk.TTkColor.FG_YELLOW | m1 +m6 = m1 | ttk.TTkColor.FG_YELLOW +testColor("M1 ",m3,m4) +testColor("M1 ",m5,m6) + +m3 = ttk.TTkColor.BG_YELLOW + m1 +m4 = m1 + ttk.TTkColor.BG_YELLOW +m5 = ttk.TTkColor.BG_YELLOW | m1 +m6 = m1 | ttk.TTkColor.BG_YELLOW +testColor("M2 ",m3,m4) +testColor("M2 ",m5,m6) + +m3 = ttk.TTkColor.RST + m1 +m4 = m1 + ttk.TTkColor.RST +m5 = ttk.TTkColor.RST | m1 +m6 = m1 | ttk.TTkColor.RST +testColor("M3 ",m3,m4) +testColor("M3 ",m5,m6) \ No newline at end of file diff --git a/tests/t.ui/test.ui.018.TextEdit.04.Pygments.py b/tests/t.ui/test.ui.018.TextEdit.04.Pygments.py new file mode 100755 index 00000000..cf8fbb7e --- /dev/null +++ b/tests/t.ui/test.ui.018.TextEdit.04.Pygments.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2021 Eugenio Parodi +# +# 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 + +def demoTextEdit(root, filename): + frame = ttk.TTkFrame(parent=root, border=False, layout=ttk.TTkGridLayout()) + + te = ttk.TTkTextEdit() + te.setReadOnly(False) + + with open(filename, 'r') as f: + content = f.read() + doc = ttk.TextDocumentHighlight(text=content) + te.setDocument(doc) + + # use the widget size to wrap + # te.setLineWrapMode(ttk.TTkK.WidgetWidth) + # te.setWordWrapMode(ttk.TTkK.WordWrap) + + # Use a fixed wrap size + # te.setLineWrapMode(ttk.TTkK.FixedWidth) + # te.setWrapWidth(100) + + frame.layout().addWidget(te,1,0,1,10) + frame.layout().addWidget(ttk.TTkLabel(text="Wrap: ", maxWidth=6),0,0) + frame.layout().addWidget(lineWrap := ttk.TTkComboBox(list=['NoWrap','WidgetWidth','FixedWidth']),0,1) + frame.layout().addWidget(ttk.TTkLabel(text=" Type: ",maxWidth=7),0,2) + frame.layout().addWidget(wordWrap := ttk.TTkComboBox(list=['WordWrap','WrapAnywhere'], enabled=False),0,3) + frame.layout().addWidget(ttk.TTkLabel(text=" FixW: ",maxWidth=7),0,4) + frame.layout().addWidget(fixWidth := ttk.TTkSpinBox(value=te.wrapWidth(), maxWidth=6, maximum=500, minimum=10, enabled=False),0,5) + frame.layout().addWidget(ttk.TTkLabel(text=" Lexer: ",maxWidth=8),0,6) + frame.layout().addWidget(lexers := ttk.TTkComboBox(list=ttk.TextDocumentHighlight.getLexers()),0,7) + frame.layout().addWidget(ttk.TTkLabel(text=" Style: ",maxWidth=8),0,8) + frame.layout().addWidget(styles := ttk.TTkComboBox(list=ttk.TextDocumentHighlight.getStyles()),0,9) + + + lineWrap.setCurrentIndex(0) + wordWrap.setCurrentIndex(1) + + fixWidth.valueChanged.connect(te.setWrapWidth) + lexers.currentTextChanged.connect(doc.setLexer) + styles.currentTextChanged.connect(doc.setStyle) + + @ttk.pyTTkSlot(int) + def _lineWrapCallback(index): + if index == 0: + te.setLineWrapMode(ttk.TTkK.NoWrap) + wordWrap.setDisabled() + fixWidth.setDisabled() + elif index == 1: + te.setLineWrapMode(ttk.TTkK.WidgetWidth) + wordWrap.setEnabled() + fixWidth.setDisabled() + else: + te.setLineWrapMode(ttk.TTkK.FixedWidth) + wordWrap.setEnabled() + fixWidth.setEnabled() + + lineWrap.currentIndexChanged.connect(_lineWrapCallback) + + @ttk.pyTTkSlot(int) + def _wordWrapCallback(index): + if index == 0: + te.setWordWrapMode(ttk.TTkK.WordWrap) + else: + te.setWordWrapMode(ttk.TTkK.WrapAnywhere) + + @ttk.pyTTkSlot(ttk.TTkTextCursor) + def _positionChanged(cursor:ttk.TTkTextCursor): + extra_selections = [] + + # Hiighlight YELLOW all the selected lines + cursor = te.textCursor().copy() + lines = [] + for cur in cursor.cursors(): + selSt = cur.selectionStart().line + selEn = cur.selectionEnd().line + lines += [x for x in range(selSt,selEn+1)] + cursor.clearCursors() + cursor.clearSelection() + for x in set(lines): + cursor.addCursor(x,0) + selection = ttk.TTkTextEdit.ExtraSelection( + cursor=cursor, + color=ttk.TTkColor.BG_YELLOW, + format=ttk.TTkK.SelectionFormat.FullWidthSelection) + extra_selections.append(selection) + + # Highlight Red only the lines under the cursor positions + cursor = te.textCursor().copy() + cursor.clearSelection() + selection = ttk.TTkTextEdit.ExtraSelection( + cursor=cursor, + color=ttk.TTkColor.BG_RED, + format=ttk.TTkK.SelectionFormat.FullWidthSelection) + extra_selections.append(selection) + + # Highlight GREEN the words under the cursor positions + cursor = te.textCursor().copy() + cursor.select(ttk.TTkTextCursor.SelectionType.WordUnderCursor) + selection = ttk.TTkTextEdit.ExtraSelection( + cursor=cursor, + color=ttk.TTkColor.BG_GREEN) + extra_selections.append(selection) + + te.setExtraSelections(extra_selections) + + wordWrap.currentIndexChanged.connect(_wordWrapCallback) + te.cursorPositionChanged.connect(_positionChanged) + + return frame + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('filename', type=str, nargs='+', + help='the filename/s') + args = parser.parse_args() + + ttk.TTkTheme.loadTheme(ttk.TTkTheme.NERD) + + root = ttk.TTk(layout=ttk.TTkGridLayout()) + appTemplate = ttk.TTkAppTemplate(parent=root) + appTemplate.setWidget(fileTree := ttk.TTkFileTree(), position=ttk.TTkK.LEFT, size=30) + appTemplate.setItem(layoutArea := ttk.TTkLayout(), position=appTemplate.Position.MAIN) + + def _openFile(fileName): + newPos = (0,0) + oldPos = [win.pos() for win in layoutArea.children()] + while newPos in oldPos: + newPos = (newPos[0]+1,newPos[1]+1,) + + win = ttk.TTkWindow(pos = newPos, size=(100,40), title=f"Test Text Edit ({fileName})", layout=ttk.TTkGridLayout(), border=True) + layoutArea.addWidget(win) + demoTextEdit(win, fileName) + + for file in args.filename: + _openFile(file) + + fileTree.fileActivated.connect(lambda x: _openFile(x.path())) + + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file