From 8a13ac2a4b8e3db2e972b077c3b2e847ceea66ea Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 18 Jul 2022 22:29:49 +0100 Subject: [PATCH] Added TTkTextCharFormat --- TermTk/TTkCore/color.py | 27 +++++++++++++++++++ TermTk/TTkGui/__init__.py | 1 + TermTk/TTkGui/textcursor.py | 29 ++++++++++++++++----- TermTk/TTkGui/textdocument.py | 3 +++ TermTk/TTkGui/textformat.py | 49 +++++++++++++++++++++++++++++++++++ TermTk/TTkWidgets/texedit.py | 13 ++++++++++ demo/showcase/textedit.py | 42 ++++++++++++++++++++++++------ docs/MDNotes/Resources.md | 4 +++ 8 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 TermTk/TTkGui/textformat.py diff --git a/TermTk/TTkCore/color.py b/TermTk/TTkCore/color.py index 74456bcf..032c8576 100644 --- a/TermTk/TTkCore/color.py +++ b/TermTk/TTkCore/color.py @@ -69,6 +69,33 @@ class _TTkColor: self._mod = mod self._colorMod = colorMod + def foreground(self): + if self._fg: + return _TTkColor(fg=self._fg) + else: + return None + + def background(self): + if self._bg: + return _TTkColor(fg=self._bg) + else: + return None + + def bold(self) -> bool: + return TTkColor.BOLD._mod in self._mod + + def italic(self) -> bool: + return TTkColor.ITALIC._mod in self._mod + + def underline(self) -> bool: + return TTkColor.UNDERLINE._mod in self._mod + + def strikethrough(self) -> bool: + return TTkColor.STRIKETROUGH._mod in self._mod + + def blinking(self) -> bool: + return TTkColor.BLINKING._mod in self._mod + def colorType(self): return \ ( TTkK.Foreground if self._fg != "" else TTkK.NONE ) | \ diff --git a/TermTk/TTkGui/__init__.py b/TermTk/TTkGui/__init__.py index 1f0a8cf7..7a79b546 100644 --- a/TermTk/TTkGui/__init__.py +++ b/TermTk/TTkGui/__init__.py @@ -1,4 +1,5 @@ from .drag import * from .textwrap import TTkTextWrap from .textcursor import TTkTextCursor +from. textformat import TTkTextCharFormat from .textdocument import TTkTextDocument \ No newline at end of file diff --git a/TermTk/TTkGui/textcursor.py b/TermTk/TTkGui/textcursor.py index d37ce82c..872f3774 100644 --- a/TermTk/TTkGui/textcursor.py +++ b/TermTk/TTkGui/textcursor.py @@ -24,8 +24,8 @@ from TermTk.TTkCore.color import TTkColor from TermTk.TTkCore.log import TTkLog -from TermTk.TTkCore.cfg import TTkCfg from TermTk.TTkCore.string import TTkString +from TermTk.TTkGui.textformat import TTkTextCharFormat from TermTk.TTkGui.textdocument import TTkTextDocument class TTkTextCursor(): @@ -184,21 +184,32 @@ class TTkTextCursor(): TTkTextCursor._prop( TTkTextCursor._CP(line, pos), TTkTextCursor._CP(line, pos))) - self._checkCursors() + self._checkCursors(notify=True) def cleanCursors(self): p = self._properties[self._cID] self._cID = 0 self._properties = [p] + def charFormat(self): + p = self.position() + l = self._document._dataLines[p.line] + if p.pos < len(l): + color = l.colorAt(p.pos) + else: + color = TTkColor() + return TTkTextCharFormat(color=color) + def setPosition(self, line, pos, moveMode=MoveMode.MoveAnchor, cID=0): # TTkLog.debug(f"{line=}, {pos=}, {moveMode=}") self._properties[cID].position.set(line,pos) if moveMode==TTkTextCursor.MoveAnchor: self._properties[cID].anchor.set(line,pos) + self._document.cursorPositionChanged.emit(self) - def _checkCursors(self): + def _checkCursors(self, notify=False): currCurs = self._properties[self._cID] + currPos = currCurs.position.toNum() # Sort the cursors based on the starting position self._properties = sorted( self._properties, @@ -221,8 +232,11 @@ class TTkTextCursor(): op.anchor=np.selectionEnd() self._properties = newProperties self._cID = self._properties.index(currCurs) + if notify or currPos != currCurs.position.toNum(): + self._document.cursorPositionChanged.emit(self) def movePosition(self, operation, moveMode=MoveMode.MoveAnchor, n=1, textWrap=None): + currPos = self.position().toNum() def moveRight(cID,p,_): if p.pos < len(self._document._dataLines[p.line]): self.setPosition(p.line, p.pos+1, moveMode, cID=cID) @@ -259,7 +273,7 @@ class TTkTextCursor(): p = prop.position operations.get(operation,lambda _:_)(cID,p,n) - self._checkCursors() + self._checkCursors(notify=self.position().toNum()!=currPos) def document(self): return self._document @@ -315,6 +329,7 @@ class TTkTextCursor(): pp.anchor.line += diffLine self._document.contentsChanged.emit() self._document.contentsChange.emit(l,b,c) + self._document.cursorPositionChanged.emit(self) def selectionStart(self): return self._properties[self._cID].selectionStart() @@ -323,6 +338,7 @@ class TTkTextCursor(): return self._properties[self._cID].selectionEnd() def select(self, selection): + currPos = self.position().toNum() for p in self._properties: if selection == TTkTextCursor.SelectionType.Document: pass @@ -347,7 +363,7 @@ class TTkTextCursor(): xTo += len(m.group(0)) p.position.pos = xTo p.anchor.pos = xFrom - self._checkCursors() + self._checkCursors(notify=self.position().toNum()!=currPos) def hasSelection(self): for p in self._properties: @@ -361,6 +377,7 @@ class TTkTextCursor(): p.anchor.line = p.position.line def _removeSelectedText(self): + currPos = self.position().toNum() def _alignPoint(point,st,en): point.line += st.line - en.line if point.line == st.line: @@ -375,7 +392,7 @@ class TTkTextCursor(): _alignPoint(pp.position, selSt, selEn) _alignPoint(pp.anchor, selSt, selEn) self.setPosition(selSt.line, selSt.pos, cID=i) - self._checkCursors() + self._checkCursors(notify=self.position().toNum()!=currPos) return selSt.line, selEn.line-selSt.line, 1 def removeSelectedText(self): diff --git a/TermTk/TTkGui/textdocument.py b/TermTk/TTkGui/textdocument.py index 55f8c864..7a09fb74 100644 --- a/TermTk/TTkGui/textdocument.py +++ b/TermTk/TTkGui/textdocument.py @@ -30,8 +30,11 @@ class TTkTextDocument(): '_dataLines', # Signals 'contentsChange', 'contentsChanged', + 'cursorPositionChanged' ) def __init__(self, *args, **kwargs): + from TermTk.TTkGui.textcursor import TTkTextCursor + self.cursorPositionChanged = pyTTkSignal(TTkTextCursor) self.contentsChange = pyTTkSignal(int,int,int) # int line, int linesRemoved, int linesAdded self.contentsChanged = pyTTkSignal() text = kwargs.get('text',"") diff --git a/TermTk/TTkGui/textformat.py b/TermTk/TTkGui/textformat.py new file mode 100644 index 00000000..02dd08f2 --- /dev/null +++ b/TermTk/TTkGui/textformat.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2022 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. + +class TTkTextCharFormat(): + __slots__ = ('_color') + def __init__(self, color): + self._color = color + + def foreground(self): + return self._color.foreground() + + def background(self): + return self._color.background() + + def bold(self): + return self._color.bold() + + def italic(self): + return self._color.italic() + + def underline(self): + return self._color.underline() + + def strikethrough(self): + return self._color.strikethrough() + + def blinking(self): + return self._color.blinking() diff --git a/TermTk/TTkWidgets/texedit.py b/TermTk/TTkWidgets/texedit.py index 71adcc17..4be12245 100644 --- a/TermTk/TTkWidgets/texedit.py +++ b/TermTk/TTkWidgets/texedit.py @@ -31,6 +31,7 @@ from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot from TermTk.TTkCore.helper import TTkHelper from TermTk.TTkGui.textwrap import TTkTextWrap from TermTk.TTkGui.textcursor import TTkTextCursor +from TermTk.TTkGui.textformat import TTkTextCharFormat from TermTk.TTkGui.textdocument import TTkTextDocument from TermTk.TTkAbstract.abstractscrollarea import TTkAbstractScrollArea from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView @@ -59,6 +60,7 @@ class _TTkTextEditView(TTkAbstractScrollView): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._name = kwargs.get('name' , '_TTkTextEditView' ) + self.currentCharFormatChanged = pyTTkSignal(TTkTextCharFormat) self._readOnly = True self._multiCursor = True self._hsize = 0 @@ -68,6 +70,7 @@ class _TTkTextEditView(TTkAbstractScrollView): self._lineWrapMode = TTkK.NoWrap self._textWrap = TTkTextWrap(document=self._textDocument) self._textDocument.contentsChanged.connect(self._rewrap) + self._textDocument.cursorPositionChanged.connect(self._cursorPositionChanged) self._replace = False self._cursorParams = None self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus) @@ -112,11 +115,17 @@ class _TTkTextEditView(TTkAbstractScrollView): self._textDocument.appendText(text) self._updateSize() + @pyTTkSlot() def _rewrap(self): self._textWrap.rewrap() self.viewChanged.emit() self.update() + @pyTTkSlot(TTkTextCursor) + def _cursorPositionChanged(self, cursor): + if cursor == self._textCursor: + self.currentCharFormatChanged.emit(cursor.charFormat()) + def resizeEvent(self, w, h): if w != self._lastWrapUsed and w>self._textWrap._tabSpaces: self._lastWrapUsed = w @@ -311,6 +320,9 @@ class TTkTextEdit(TTkAbstractScrollArea): 'wrapWidth', 'setWrapWidth', 'lineWrapMode', 'setLineWrapMode', 'wordWrapMode', 'setWordWrapMode', + # Signals + 'focusChanged', 'currentCharFormatChanged' + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -333,3 +345,4 @@ class TTkTextEdit(TTkAbstractScrollArea): self.setWordWrapMode = self._textEditView.setWordWrapMode # Forward Signals self.focusChanged = self._textEditView.focusChanged + self.currentCharFormatChanged = self._textEditView.currentCharFormatChanged diff --git a/demo/showcase/textedit.py b/demo/showcase/textedit.py index ec303c64..0ee0d438 100755 --- a/demo/showcase/textedit.py +++ b/demo/showcase/textedit.py @@ -50,6 +50,18 @@ def demoTextEdit(root=None): te.setReadOnly(False) + @ttk.pyTTkSlot(ttk.TTkTextCharFormat) + def _currentCharFormatChangedCB(format): + fg = format.foreground() + bg = format.background() + bold = format.bold() + italic = format.italic() + underline = format.underline() + strikethrough = format.strikethrough() + ttk.TTkLog.debug(f"{fg=} {bg=} {bold=} {italic=} {underline=} {strikethrough= }") + + te.currentCharFormatChanged.connect(_currentCharFormatChangedCB) + te.setText(ttk.TTkString("Text Edit DEMO\n",ttk.TTkColor.UNDERLINE+ttk.TTkColor.BOLD+ttk.TTkColor.ITALIC)) # Load ANSI input @@ -83,14 +95,28 @@ def demoTextEdit(root=None): # te.setLineWrapMode(ttk.TTkK.FixedWidth) # te.setWrapWidth(100) - frame.layout().addWidget(te,1,0,1,6) - 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(), maximum=500, minimum=10, enabled=False),0,5) - + frame.layout().addItem(wrapLayout := ttk.TTkGridLayout(), 0,0) + frame.layout().addItem(fontLayout := ttk.TTkGridLayout(), 1,0) + frame.layout().addWidget(te,2,0) + + wrapLayout.addWidget(ttk.TTkLabel(text="Wrap: ", maxWidth=6),0,0) + wrapLayout.addWidget(lineWrap := ttk.TTkComboBox(list=['NoWrap','WidgetWidth','FixedWidth']),0,1) + wrapLayout.addWidget(ttk.TTkLabel(text=" Type: ",maxWidth=7),0,2) + wrapLayout.addWidget(wordWrap := ttk.TTkComboBox(list=['WordWrap','WrapAnywhere'], enabled=False),0,3) + wrapLayout.addWidget(ttk.TTkLabel(text=" FixW: ",maxWidth=7),0,4) + wrapLayout.addWidget(fixWidth := ttk.TTkSpinBox(value=te.wrapWidth(), maxWidth=5, maximum=500, minimum=10, enabled=False),0,5) + + fontLayout.addWidget(ttk.TTkCheckbox(text=" FG"),0,0) + fontLayout.addWidget(ttk.TTkColorButtonPicker(border=True, maxSize=(7,3)),1,0) + # fontLayout.addWidget(ttk.TTkSpacer(maxWidth=3),0,1,2,1) + fontLayout.addWidget(ttk.TTkCheckbox(text=" BG"),0,2) + fontLayout.addWidget(ttk.TTkColorButtonPicker(border=True, maxSize=(7 ,3)),1,2) + # fontLayout.addWidget(ttk.TTkSpacer(maxWidth=3),0,3,2,1) + fontLayout.addWidget(ttk.TTkButton(border=True, maxSize=(5,3), text='a', color=ttk.TTkColor.BOLD),1,4) + fontLayout.addWidget(ttk.TTkButton(border=True, maxSize=(5,3), text='a', color=ttk.TTkColor.ITALIC),1,5) + fontLayout.addWidget(ttk.TTkButton(border=True, maxSize=(5,3), text='a', color=ttk.TTkColor.UNDERLINE),1,6) + fontLayout.addWidget(ttk.TTkButton(border=True, maxSize=(5,3), text='a', color=ttk.TTkColor.STRIKETROUGH),1,7) + fontLayout.addWidget(ttk.TTkSpacer(),0,10,2,1) lineWrap.setCurrentIndex(0) wordWrap.setCurrentIndex(1) diff --git a/docs/MDNotes/Resources.md b/docs/MDNotes/Resources.md index 1a68b12f..6169ae1e 100644 --- a/docs/MDNotes/Resources.md +++ b/docs/MDNotes/Resources.md @@ -25,6 +25,10 @@ https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#file-ansi-md #### Hyperlink https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda +#### ANSI 16 256 24bit color conversion +https://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html +#### ANSI Colors +https://talyian.github.io/ansicolors/ #### Blinking Text ```bash