From b5f9889acca27079435d9954e1e4d04b602f209e Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 26 Nov 2024 14:01:28 +0000 Subject: [PATCH 1/6] updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1df012e7..97143d02 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__/ *$py.class # other +pygments pyTermTk-Docs tests/test.dummy.py *.swp From 7321f45dc7cb00a45a05f2dcf69265d1a1e499c2 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 4 Dec 2024 09:45:18 +0000 Subject: [PATCH 2/6] Changed file permissin --- tests/t.generic/test.argspec.001.py | 0 tests/t.generic/test.classes.001.slots.py | 0 tests/t.generic/test.classes.001.slots.typing.py | 0 tests/t.generic/test.dataclass.001.py | 0 tests/t.generic/test.generic.005.fnctl.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tests/t.generic/test.argspec.001.py mode change 100644 => 100755 tests/t.generic/test.classes.001.slots.py mode change 100644 => 100755 tests/t.generic/test.classes.001.slots.typing.py mode change 100644 => 100755 tests/t.generic/test.dataclass.001.py mode change 100644 => 100755 tests/t.generic/test.generic.005.fnctl.py diff --git a/tests/t.generic/test.argspec.001.py b/tests/t.generic/test.argspec.001.py old mode 100644 new mode 100755 diff --git a/tests/t.generic/test.classes.001.slots.py b/tests/t.generic/test.classes.001.slots.py old mode 100644 new mode 100755 diff --git a/tests/t.generic/test.classes.001.slots.typing.py b/tests/t.generic/test.classes.001.slots.typing.py old mode 100644 new mode 100755 diff --git a/tests/t.generic/test.dataclass.001.py b/tests/t.generic/test.dataclass.001.py old mode 100644 new mode 100755 diff --git a/tests/t.generic/test.generic.005.fnctl.py b/tests/t.generic/test.generic.005.fnctl.py old mode 100644 new mode 100755 From 48a9e7db7d59582f5cfc4e66a966c13d57b2ba55 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 4 Dec 2024 09:47:14 +0000 Subject: [PATCH 3/6] Removed combobox/listwidget bottleneck on adding multiple items --- TermTk/TTkWidgets/combobox.py | 5 ++--- TermTk/TTkWidgets/listwidget.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/TermTk/TTkWidgets/combobox.py b/TermTk/TTkWidgets/combobox.py index 2f1591c7..ee5646a7 100644 --- a/TermTk/TTkWidgets/combobox.py +++ b/TermTk/TTkWidgets/combobox.py @@ -336,9 +336,8 @@ def _pressEvent(self) -> bool: self._popupFrame = TTkResizableFrame(layout=TTkGridLayout(), size=(frameWidth,frameHeight)) TTkHelper.overlay(self, self._popupFrame, 0, 0) listw = TTkList(parent=self._popupFrame) - TTkLog.debug(f"{self._list}") - for item in self._list: - listw.addItem(item) + # TTkLog.debug(f"{self._list}") + listw.addItems(self._list) if self._id != -1: listw.setCurrentRow(self._id) listw.textClicked.connect(self._callback) diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index 835e3e79..8980c2c0 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -272,8 +272,7 @@ def addItem(self, item, data=None): def addItems(self, items): '''addItems''' - for item in items: - self.addItem(item) + self.addItemsAt(items=items, pos=len(self._items)) def _placeItems(self): minw = self.width() @@ -292,6 +291,7 @@ def addItemAt(self, item, pos, data=None): def addItemsAt(self, items, pos): '''addItemsAt''' + items = [TTkAbstractListItem(text=i) if isinstance(i, str) or isinstance(i, TTkString) else i for i in items] for item in items: if not issubclass(type(item),TTkAbstractListItem): TTkLog.error(f"{item=} is not an TTkAbstractListItem") From 87748ec7948041841f3ad2d7c0a034bb2d819369 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 9 Dec 2024 14:57:51 +0000 Subject: [PATCH 4/6] Added pygments support --- .vscode/launch.json | 9 +- TermTk/TTkGui/__init__.py | 7 + TermTk/TTkGui/textdocument.py | 105 ++++--- TermTk/TTkGui/textdocument_highlight.py | 61 ++++ .../TTkGui/textdocument_highlight_pygments.py | 288 ++++++++++++++++++ TermTk/TTkWidgets/texedit.py | 101 +++--- ...py => test.ui.018.TextEdit.01.Pygments.py} | 0 .../t.ui/test.ui.018.TextEdit.02.Pygments.py | 118 +++++++ 8 files changed, 593 insertions(+), 96 deletions(-) create mode 100644 TermTk/TTkGui/textdocument_highlight.py create mode 100644 TermTk/TTkGui/textdocument_highlight_pygments.py rename tests/t.ui/{test.ui.018.TextEdit.Pygments.py => test.ui.018.TextEdit.01.Pygments.py} (100%) create mode 100755 tests/t.ui/test.ui.018.TextEdit.02.Pygments.py diff --git a/.vscode/launch.json b/.vscode/launch.json index d8bf6b8c..14fb4554 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,14 @@ "program": "${file}", "console": "integratedTerminal", "justMyCode": true, + },{ + "name": "Python: Current File with Args", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": true, + "args": ["tools/webExporter/js/ttkproxy.js"] },{ "name": "Python: Test Player", "type": "debugpy", @@ -30,7 +38,6 @@ "env": { "PYTHONPATH": "./tools" } - },{ "name": "Python: TTk Designer Quick", "type": "debugpy", diff --git a/TermTk/TTkGui/__init__.py b/TermTk/TTkGui/__init__.py index fed17c31..a2077a26 100644 --- a/TermTk/TTkGui/__init__.py +++ b/TermTk/TTkGui/__init__.py @@ -1,6 +1,13 @@ +import importlib.util + from .drag import * from .textwrap1 import * from .textcursor import * from .textdocument import * from .clipboard import * from .tooltip import * + +if importlib.util.find_spec('pygments'): + from .textdocument_highlight_pygments import * +else: + from .textdocument_highlight import * diff --git a/TermTk/TTkGui/textdocument.py b/TermTk/TTkGui/textdocument.py index e74cd6f4..8064e751 100644 --- a/TermTk/TTkGui/textdocument.py +++ b/TermTk/TTkGui/textdocument.py @@ -25,58 +25,59 @@ from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot from TermTk.TTkCore.string import TTkString +from TermTk.TTkCore.color import TTkColor class TTkTextDocument(): - ''' - Undo,Redo Logic - - Old: - - :: - - _snapshotId: = last saved/undo/redo state - 3 = doc4 - _snapshots: - [doc1, doc2, doc3, doc4, doc5, doc6, . . .] - - New: - - :: - - SnapshotId: - 2 - Snapshots: _lastSnap _dataLines (unstaged) - ╒═══╕ ╒═══╕ ╒═══╕ ╒═══╕ ╒═══╕ ╒═══╕ - │ 0 │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ - └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ - Cursors: - c0, c1, c2, c3, c4 = _lastCursor - Diffs: - [ d01, d12, d23, d34 ] = Forward Diffs - [ d10, d21, d32, d43 ] = Backward Diffs - Slices: = common txt slices between snapshots - [ s01, s12, s23, s34 ] - - :: - - Data Structure - ╔═══════════════╗ ╔═══════════════╗ - ║ Snapshot B ║ ┌─────────────>║ Snapshot C ║ - ╟───────────────╢ │ ╟───────────────╢ - ║ _nextDiff ║──────┐ │ ║ _nextDiff ║───> Next snapshot - ┌───║ _prevDiff ║ │ │ ┌───║ _prevDiff ║ or Null if at the end - │ ╚═══════════════╝ │ │ │ ╚═══════════════╝ - V A V │ V - ╔═══════════════╗ │ ╔═══════════════╗ ╔═══════════════╗ - ║ Diff B->A ║ │ ║ Diff B->C ║ ║ Diff C->B ║ - ╟───────────────╢ │ ╟───────────────╢ ╟───────────────╢ - ║ slice = txtBA ║ │ ║ slice = txtBC ║ ║ slice = txtBA ║ - ║ snap ║ │ ║ snap ║ ║ snap ║ - ╚═══════════════╝ │ ╚═══════════════╝ ╚═══════════════╝ - │ │ - └─────────────────────────────┘ - - ''' + # ''' + # Undo,Redo Logic + # + # Old: + # + # :: + # + # _snapshotId: = last saved/undo/redo state + # 3 = doc4 + # _snapshots: + # [doc1, doc2, doc3, doc4, doc5, doc6, . . .] + # + # New: + # + # :: + # + # SnapshotId: + # 2 + # Snapshots: _lastSnap _dataLines (unstaged) + # ╒═══╕ ╒═══╕ ╒═══╕ ╒═══╕ ╒═══╕ ╒═══╕ + # │ 0 │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ + # └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ + # Cursors: + # c0, c1, c2, c3, c4 = _lastCursor + # Diffs: + # [ d01, d12, d23, d34 ] = Forward Diffs + # [ d10, d21, d32, d43 ] = Backward Diffs + # Slices: = common txt slices between snapshots + # [ s01, s12, s23, s34 ] + # + # :: + # + # Data Structure + # ╔═══════════════╗ ╔═══════════════╗ + # ║ Snapshot B ║ ┌─────────────>║ Snapshot C ║ + # ╟───────────────╢ │ ╟───────────────╢ + # ║ _nextDiff ║──────┐ │ ║ _nextDiff ║───> Next snapshot + # ┌───║ _prevDiff ║ │ │ ┌───║ _prevDiff ║ or Null if at the end + # │ ╚═══════════════╝ │ │ │ ╚═══════════════╝ + # V A V │ V + # ╔═══════════════╗ │ ╔═══════════════╗ ╔═══════════════╗ + # ║ Diff B->A ║ │ ║ Diff B->C ║ ║ Diff C->B ║ + # ╟───────────────╢ │ ╟───────────────╢ ╟───────────────╢ + # ║ slice = txtBA ║ │ ║ slice = txtBC ║ ║ slice = txtBA ║ + # ║ snap ║ │ ║ snap ║ ║ snap ║ + # ╚═══════════════╝ │ ╚═══════════════╝ ╚═══════════════╝ + # │ │ + # └─────────────────────────────┘ + # + # ''' class _snapDiff(): ''' Doc: @@ -122,8 +123,10 @@ def _getSnap(self, lines, d): '_dataLines', '_modified', '_snap', '_snapChanged', '_lastSnap', '_lastCursor', + '_backgroundColor', # Signals 'contentsChange', 'contentsChanged', + 'formatChanged', 'cursorPositionChanged', 'undoAvailable', 'redoAvailable', 'undoCommandAdded', 'modificationChanged' @@ -133,10 +136,12 @@ def __init__(self, *, text:TTkString=" ") -> None: self.cursorPositionChanged = pyTTkSignal(TTkTextCursor) self.contentsChange = pyTTkSignal(int,int,int) # int line, int linesRemoved, int linesAdded self.contentsChanged = pyTTkSignal() + self.formatChanged = pyTTkSignal() self.undoAvailable = pyTTkSignal(bool) self.redoAvailable = pyTTkSignal(bool) self.undoCommandAdded = pyTTkSignal() self.modificationChanged = pyTTkSignal(bool) + self._backgroundColor = TTkColor.RST text = text self._dataLines = [TTkString(t) for t in text.split('\n')] self._modified = False diff --git a/TermTk/TTkGui/textdocument_highlight.py b/TermTk/TTkGui/textdocument_highlight.py new file mode 100644 index 00000000..281cf68e --- /dev/null +++ b/TermTk/TTkGui/textdocument_highlight.py @@ -0,0 +1,61 @@ +# MIT License +# +# Copyright (c) 2024 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. + +__all__ = ['TextDocumentHighlight'] + +from threading import Lock + +from TermTk.TTkCore.log import TTkLog +from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal +from TermTk.TTkGui import TTkTextDocument + +class TextDocumentHighlight(TTkTextDocument): + __slots__ = ( + '_highlightDocMutex', + #Signals + 'highlightUpdate') + def __init__(self, *args, **kwargs): + self.highlightUpdate = pyTTkSignal() + self._highlightDocMutex = Lock() + super().__init__(*args, **kwargs) + TTkLog.warn("Pygments not found!!!") + + @staticmethod + def getStyles() -> list[str]: + return [] + + @staticmethod + def getLexers() -> list[str]: + return [] + + @pyTTkSlot(str) + def setStyle(self, alias:str) -> None: + pass + + @pyTTkSlot(str) + def setLexer(self, alias:str) -> None: + pass + + @pyTTkSlot(str) + def guessLexerFromFilename(self, fileName:str) -> None: + pass + diff --git a/TermTk/TTkGui/textdocument_highlight_pygments.py b/TermTk/TTkGui/textdocument_highlight_pygments.py new file mode 100644 index 00000000..2a12d598 --- /dev/null +++ b/TermTk/TTkGui/textdocument_highlight_pygments.py @@ -0,0 +1,288 @@ +# MIT License +# +# Copyright (c) 2024 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. + +__all__ = ['TextDocumentHighlight'] + +from threading import Lock + +from pygments import highlight +from pygments.util import ClassNotFound +from pygments.styles import get_all_styles +from pygments.lexers import guess_lexer, guess_lexer_for_filename, get_lexer_by_name, special, get_all_lexers +from pygments.formatters import TerminalFormatter, Terminal256Formatter, TerminalTrueColorFormatter +from pygments.formatter import Formatter +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Token, Whitespace + +from TermTk.TTkCore.log import TTkLog +from TermTk.TTkCore.color import TTkColor +from TermTk.TTkCore.string import TTkString +from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal +from TermTk.TTkCore.timer import TTkTimer + +from TermTk.TTkGui.textdocument import TTkTextDocument + +class _TTkFormatter(Formatter): + class Data(): + __slots__=('lines', 'block', 'error', 'multiline') + def __init__(self, lines, block): + self.lines = lines + self.block = block + self.error = None + self.multiline = False + + __slots__ = ('_dl', '_blockNum', '_highlightStyles') + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._highlightStyles = {} + self._blockNum = 1 + for token, style in self.style: + # Token = Token.Comment.PreprocFile + # style = { + # 'color': '6272a4', + # 'bgcolor': None, + # 'bold': False, 'italic': False, 'underline': False, + # 'border': None, + # 'roman': None, 'sans': None, 'mono': None, + # 'ansicolor': None, 'bgansicolor': None} + + # TTkLog.debug(f"{token=} {style=}") + color = TTkColor.RST + if style['color']: + color += TTkColor.fg(f"#{style['color']}") + if style['bgcolor']: + color += TTkColor.bg(f"#{style['bgcolor']}") + if style['bold']: + color += TTkColor.BOLD + if style['italic']: + color += TTkColor.ITALIC + if style['underline']: + color += TTkColor.UNDERLINE + self._highlightStyles[token] = color + + def setDl(self,dl): + self._dl = dl + + def format(self, tokensource, _): + multiline = False + multilineId = 0 + for ttype, value in tokensource: + if ttype == Error and self._dl.error is None: + self._dl.error = len(self._dl.lines)-1 + # self._dl.multiline = ttype == Comment.Multiline + multiline = ttype == Comment.Multiline + + while ttype not in self._highlightStyles: + ttype = ttype.parent + # TTkLog.debug (f"{ttype=}") + # TTkLog.debug (f"{value=}") + color = self._highlightStyles[ttype] + + values = value.split('\n') + + self._dl.lines[-1] += TTkString(values[0],color) + self._dl.lines += [TTkString(t,color) for t in values[1:]] + self._dl.block[-1] = self._blockNum + self._dl.block += [self._blockNum]*(len(values)-1) + + # self._dl.lines += [TTkString(t) for t in value.split('\n')] + + # multiline = len(values)>1 if self._dl.lines[-1]._text == values[-1] else self._dl.multiline + # if self._dl.lines[-1]._text == '' or not multiline: + # self._blockNum += 1 + # multilineId = len(self._dl.lines) + + if multiline: + multilineId += len(values) + else: + multilineId = 0 + self._blockNum += 1 + + if multiline: + self._dl.multiline = multilineId + +class TextDocumentHighlight(TTkTextDocument): + _linesRefreshed:int = 30 + __slots__ = ( + '_timerRefresh', + '_highlightDocMutex', + '_blocks', '_changedContent', '_refreshContent', + '_lexer', '_formatter', + #Signals + 'highlightUpdate') + def __init__(self, **kwargs): + self.highlightUpdate = pyTTkSignal() + self._highlightDocMutex = Lock() + self._lexer = None + self._blocks = [] + # self._formatter = _TTkFormatter(style='dracula') + self._formatter = _TTkFormatter(style='gruvbox-dark') + super().__init__(**kwargs) + self._timerRefresh = TTkTimer() + self._timerRefresh.timeout.connect(self._refreshEvent) + self._changedContent = (0,0,len(self._dataLines)) + self._refreshContent = (0,TextDocumentHighlight._linesRefreshed) + self.contentsChange.connect(lambda a,b,c: TTkLog.debug(f"{a=} {b=} {c=}")) + self.contentsChange.connect(self._saveChangedContent) + + try: + self._lexer = guess_lexer(self.toPlainText()) + TTkLog.debug(f"Using Lexer: {self._lexer.name}") + except ClassNotFound: + self._lexer = special.TextLexer() + + self._timerRefresh.start(0.3) + + @staticmethod + def getStyles() -> list[str]: + return sorted(get_all_styles()) + + @staticmethod + def getLexers() -> list[str]: + return sorted(list(set(b for a in get_all_lexers() for b in a[1]))) + + @pyTTkSlot(str) + def setStyle(self, alias:str) -> None: + self._formatter = formatter = _TTkFormatter(style=alias) + if (color:=formatter.style.background_color) and color != "#000000": + self._backgroundColor = TTkColor.bg(color) + else: + self._backgroundColor = TTkColor.RST + TTkLog.debug(f"{color=} {alias=} {formatter.style}") + self._changedContent = (0,0,len(self._dataLines)) + self._refreshContent = (0,TextDocumentHighlight._linesRefreshed) + self._timerRefresh.start(0.3) + + @pyTTkSlot(str) + def setLexer(self, alias:str) -> None: + try: + self._lexer = get_lexer_by_name(alias) + self._changedContent = (0,0,len(self._dataLines)) + self._refreshContent = (0,TextDocumentHighlight._linesRefreshed) + self._timerRefresh.start(0.3) + TTkLog.debug(f"Using Lexer: {self._lexer.name}") + except ClassNotFound: + self._lexer = special.TextLexer() + + @pyTTkSlot(str) + def guessLexerFromFilename(self, fileName:str) -> None: + with open(fileName, 'r') as f: + content = f.read() + try: + self._lexer = guess_lexer_for_filename(fileName, content) + TTkLog.debug(f"Using Lexer: {self._lexer.name}") + except ClassNotFound: + self._lexer = special.TextLexer() + + @pyTTkSlot(int,int,int) + def _saveChangedContent(self,a,b,c): + if self._changedContent: + self._changedContent = TTkTextDocument._mergeChangesSlices(self._changedContent,(a,b,c)) + else: + self._changedContent = (a,b,c) + if not self._refreshContent: + self._refreshContent = (self._changedContent[0], TextDocumentHighlight._linesRefreshed) + self._timerRefresh.start(0.1) + + @pyTTkSlot() + def _refreshEvent(self): + if not self._refreshContent: return + self._highlightDocMutex.acquire() + + ra,rb = self._refreshContent + + if self._changedContent: + ca,cb,cc = self._changedContent + self._changedContent = None + self._blocks[ca:ca+cb] = [0]*cc + ra = min(ra,ca) + + # find the beginning of the current block + # TTkLog.debug(self._blocks) + if ra and self._blocks: + blockId = self._blocks[ra] + for i,v in enumerate(reversed(self._blocks[:ra])): + # TTkLog.debug(f"{i=}:{v=} {blockId=}") + if v == blockId or not blockId: + blockId = v + ra -= 1 + rb += 1 + else: + break + + # TTkLog.debug(f"{ra=} {rb=}") + + eof = False + if (ra+rb) >= len(self._dataLines): + rb = len(self._dataLines)-ra + eof=True + + tsl = self._dataLines[ra:ra+rb] + # Find the offset from the first not empty line + # because pygments autostrip the heading empty lines + offset = 0 + for i,l in enumerate(tsl): + if l != '': + offset = i + break + + # TTkLog.debug(f"Refresh {self._lexer.name} {ra=} {rb=}") + tsl1 = [TTkString()]*(offset+1) + block = [0]*(offset+1) + + kfd = _TTkFormatter.Data(tsl1, block) + self._formatter.setDl(kfd) + + rawl = [l._text for l in tsl[offset:]] + rawt = '\n'.join(rawl) + highlight(rawt, self._lexer, self._formatter) + + # for ll in tsl: + # TTkLog.debug(f"1: -{ll}-") + # for ll in tsl1: + # TTkLog.debug(f"2: -{ll}-") + + tsl1 = tsl1[:rb] + block = block[:rb] + self._dataLines[ra:ra+rb] = tsl1 + tsl[len(tsl1):] + self._blocks[ra:ra+rb] = block + [-1]*(rb-len(block)) + # TTkLog.debug(self._blocks) + + if kfd.error is not None: + self._refreshContent = (ra+kfd.error,rb<<1) + # TTkLog.debug(f"Error: {self._refreshContent=}") + elif kfd.multiline is not None: + self._refreshContent = (ra+kfd.multiline,rb<<1) + elif (ra+rb) < len(self._dataLines): + self._refreshContent = (ra+rb,TextDocumentHighlight._linesRefreshed) + else: + self._refreshContent = None + # TTkLog.debug(f"{self._refreshContent=}") + + if not eof: + self._timerRefresh.start(0.03) + else: + TTkLog.debug(f"Refresh {self._lexer.name} DONE!!!") + + self._highlightDocMutex.release() + self.highlightUpdate.emit() + self.formatChanged.emit() diff --git a/TermTk/TTkWidgets/texedit.py b/TermTk/TTkWidgets/texedit.py index 4ba1073f..2340346f 100644 --- a/TermTk/TTkWidgets/texedit.py +++ b/TermTk/TTkWidgets/texedit.py @@ -24,13 +24,15 @@ from math import log10, floor -from TermTk.TTkCore.color import TTkColor from TermTk.TTkCore.log import TTkLog -from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.cfg import TTkCfg +from TermTk.TTkCore.constant import TTkK +from TermTk.TTkCore.color import TTkColor from TermTk.TTkCore.string import TTkString +from TermTk.TTkCore.canvas import TTkCanvas from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent +from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent from TermTk.TTkGui.clipboard import TTkClipboard from TermTk.TTkGui.textwrap1 import TTkTextWrap from TermTk.TTkGui.textcursor import TTkTextCursor @@ -59,14 +61,14 @@ def __init__(self, startingNumber=0, **kwargs) -> None: super().__init__(**kwargs) self.setMaximumWidth(2) - def _wrapChanged(self): + def _wrapChanged(self) -> None: dt = max(1,self._textWrap._lines[-1][0]) off = self._startingNumber width = 1+max(len(str(int(dt+off))),len(str(int(off)))) self.setMaximumWidth(width) self.update() - def setTextWrap(self, tw): + def setTextWrap(self, tw) -> None: self._textWrap = tw tw.wrapChanged.connect(self._wrapChanged) self._wrapChanged() @@ -77,7 +79,7 @@ def viewFullAreaSize(self) -> tuple[int,int]: else: return self.size() - def paintEvent(self, canvas): + def paintEvent(self, canvas: TTkCanvas) -> None: if not self._textWrap: return _, oy = self.getViewOffsets() w, h = self.size() @@ -244,11 +246,11 @@ def multiLine(self) -> bool : return self._multiLine @pyTTkSlot(bool) - def _undoAvailable(self, available): + def _undoAvailable(self, available) -> None: self.undoAvailable.emit(available) @pyTTkSlot(bool) - def _redoAvailable(self, available): + def _redoAvailable(self, available) -> None: self.redoAvailable.emit(available) # def toHtml(self, encoding): pass @@ -261,31 +263,31 @@ def _redoAvailable(self, available): # return self._textDocument.toMarkdown() # return "" - def toAnsi(self): + def toAnsi(self) -> str: '''toAnsi''' if self._textDocument: return self._textDocument.toAnsi() return "" - def toPlainText(self): + def toPlainText(self) ->str: '''toPlainText''' if self._textDocument: return self._textDocument.toPlainText() return "" - def toRawText(self): + def toRawText(self) -> TTkString: '''toRawText''' if self._textDocument: return self._textDocument.toRawText() return TTkString() - def isUndoAvailable(self): + def isUndoAvailable(self) -> bool: '''isUndoAvailable''' if self._textDocument: return self._textDocument.isUndoAvailable() return False - def isRedoAvailable(self): + def isRedoAvailable(self) -> bool: '''isRedoAvailable''' if self._textDocument: return self._textDocument.isRedoAvailable() @@ -295,13 +297,14 @@ def document(self) -> TTkTextDocument: '''document''' return self._textDocument - def setDocument(self, document:TTkTextDocument): + def setDocument(self, document:TTkTextDocument) -> None: '''setDocument''' if self._textDocument: self._textDocument.contentsChanged.disconnect(self._documentChanged) self._textDocument.cursorPositionChanged.disconnect(self._cursorPositionChanged) self._textDocument.undoAvailable.disconnect(self._undoAvailable) self._textDocument.redoAvailable.disconnect(self._redoAvailable) + self._textDocument.formatChanged.disconnect(self.update) self._textWrap.wrapChanged.disconnect(self.update) if not document: document = TTkTextDocument() @@ -312,6 +315,7 @@ def setDocument(self, document:TTkTextDocument): self._textDocument.cursorPositionChanged.connect(self._cursorPositionChanged) self._textDocument.undoAvailable.connect(self._undoAvailable) self._textDocument.redoAvailable.connect(self._redoAvailable) + self._textDocument.formatChanged.connect(self.update) # Trigger an update when the rewrap happen self._textWrap.wrapChanged.connect(self.update) @@ -327,53 +331,53 @@ def textCursor(self) -> TTkTextCursor: def isReadOnly(self) -> bool : return self._readOnly - def setReadOnly(self, ro): + def setReadOnly(self, ro) -> None: self._readOnly = ro self.disableWidgetCursor(ro) - def clear(self): + def clear(self) -> None: self.setText(TTkString()) - def lineWrapMode(self): + def lineWrapMode(self) -> TTkK.LineWrapMode: return self._lineWrapMode - def setLineWrapMode(self, mode): + def setLineWrapMode(self, mode:TTkK.LineWrapMode): self._lineWrapMode = mode - if mode == TTkK.NoWrap: + if mode == TTkK.LineWrapMode.NoWrap: self._textWrap.disable() else: self._textWrap.enable() - if mode == TTkK.WidgetWidth: + if mode == TTkK.LineWrapMode.WidgetWidth: self._textWrap.setWrapWidth(self.width()) self._textWrap.rewrap() @pyTTkSlot(str) - def setText(self, text): + def setText(self, text) -> None: self.viewMoveTo(0, 0) self._textDocument.setText(text) self._updateSize() @pyTTkSlot(str) - def append(self, text): + def append(self, text) -> None: self._textDocument.appendText(text) self._updateSize() @pyTTkSlot() - def undo(self): + def undo(self) -> None: if c := self._textDocument.restoreSnapshotPrev(): self._textCursor.restore(c) @pyTTkSlot() - def redo(self): + def redo(self) -> None: if c := self._textDocument.restoreSnapshotNext(): self._textCursor.restore(c) @pyTTkSlot() - def clear(self): + def clear(self) -> None: pass @pyTTkSlot() - def copy(self): + def copy(self) -> None: if not self._textCursor.hasSelection(): txt = TTkString('\n').join(self._textCursor.getLinesUnderCursor()) else: @@ -381,7 +385,7 @@ def copy(self): self._clipboard.setText(txt) @pyTTkSlot() - def cut(self): + def cut(self) -> None: if not self._textCursor.hasSelection(): self._textCursor.movePosition(moveMode=TTkTextCursor.MoveAnchor, operation=TTkTextCursor.StartOfLine) self._textCursor.movePosition(moveMode=TTkTextCursor.KeepAnchor, operation=TTkTextCursor.EndOfLine) @@ -390,31 +394,31 @@ def cut(self): self._textCursor.removeSelectedText() @pyTTkSlot() - def paste(self): + def paste(self) -> None: txt = self._clipboard.text() self.pasteEvent(txt) @pyTTkSlot() - def _documentChanged(self): + def _documentChanged(self) -> None: self._rewrap() self.textChanged.emit() - def _rewrap(self): + def _rewrap(self) -> None: self._textWrap.rewrap() self.viewChanged.emit() self.update() @pyTTkSlot(TTkColor) - def setColor(self, color): + def setColor(self, color:TTkColor) -> None: self.textCursor().setColor(color) @pyTTkSlot(TTkTextCursor) - def _cursorPositionChanged(self, cursor): + def _cursorPositionChanged(self, cursor:TTkTextCursor) -> None: if cursor == self._textCursor: self.currentColorChanged.emit(cursor.positionColor()) self._pushCursor() - def resizeEvent(self, w, h): + def resizeEvent(self, w:int, h:int) -> None: if ( self.lineWrapMode() == TTkK.WidgetWidth and w != self._lastWrapUsed and w > self._textWrap._tabSpaces ): @@ -423,7 +427,7 @@ def resizeEvent(self, w, h): self._rewrap() return super().resizeEvent(w,h) - def _updateSize(self): + def _updateSize(self) -> None: self._hsize = max( len(l) for l in self._textDocument._dataLines ) + 1 def viewFullAreaSize(self) -> tuple[int,int]: @@ -434,7 +438,7 @@ def viewFullAreaSize(self) -> tuple[int,int]: elif self.lineWrapMode() == TTkK.FixedWidth: return self.wrapWidth(), self._textWrap.size() - def _pushCursor(self): + def _pushCursor(self) -> None: ox, oy = self.getViewOffsets() x,y = self._textWrap.dataToScreenPosition( @@ -452,7 +456,7 @@ def _pushCursor(self): self.update() - def _setCursorPos(self, x, y, moveAnchor=True, addCursor=False): + def _setCursorPos(self, x, y, moveAnchor=True, addCursor=False) -> tuple[int,int]: x,y = self._textWrap.normalizeScreenPosition(x,y) line, pos = self._textWrap.screenToDataPosition(x,y) if addCursor: @@ -464,7 +468,7 @@ def _setCursorPos(self, x, y, moveAnchor=True, addCursor=False): self._scrolToInclude(x,y) return x, y - def _scrolToInclude(self, x, y): + def _scrolToInclude(self, x, y) -> None: # Scroll the area (if required) to include the position x,y _,_,w,h = self.geometry() offx, offy = self.getViewOffsets() @@ -472,7 +476,7 @@ def _scrolToInclude(self, x, y): offy = max(min(offy, y),y-h+1) self.viewMoveTo(offx, offy) - def mousePressEvent(self, evt) -> bool: + def mousePressEvent(self, evt: TTkMouseEvent) -> bool: if self._readOnly: return super().mousePressEvent(evt) ox, oy = self.getViewOffsets() @@ -482,12 +486,12 @@ def mousePressEvent(self, evt) -> bool: self.update() return True - def mouseReleaseEvent(self, evt) -> bool: + def mouseReleaseEvent(self, evt: TTkMouseEvent) -> bool: if self._textCursor.hasSelection(): self.copy() return True - def mouseDragEvent(self, evt) -> bool: + def mouseDragEvent(self, evt: TTkMouseEvent) -> bool: if self._readOnly: return super().mouseDragEvent(evt) ox, oy = self.getViewOffsets() @@ -498,7 +502,7 @@ def mouseDragEvent(self, evt) -> bool: self.update() return True - def mouseDoubleClickEvent(self, evt) -> bool: + def mouseDoubleClickEvent(self, evt: TTkMouseEvent) -> bool: if self._readOnly: return super().mouseDoubleClickEvent(evt) self._textCursor.select(TTkTextCursor.WordUnderCursor) @@ -509,7 +513,7 @@ def mouseDoubleClickEvent(self, evt) -> bool: self.update() return True - def mouseTapEvent(self, evt) -> bool: + def mouseTapEvent(self, evt: TTkMouseEvent) -> bool: if self._readOnly: return super().mouseTapEvent(evt) self._textCursor.select(TTkTextCursor.LineUnderCursor) @@ -520,7 +524,7 @@ def mouseTapEvent(self, evt) -> bool: self.update() return True - def pasteEvent(self, txt:str): + def pasteEvent(self, txt:str) -> bool: txt = TTkString(txt) if not self._multiLine: txt = TTkString().join(txt.split('\n')) @@ -537,7 +541,7 @@ def pasteEvent(self, txt:str): self.update() return True - def keyEvent(self, evt): + def keyEvent(self, evt: TTkKeyEvent) -> bool: if self._readOnly: return super().keyEvent(evt) @@ -669,13 +673,17 @@ def keyEvent(self, evt): return super().keyEvent(evt) - def paintEvent(self, canvas): + def paintEvent(self, canvas: TTkCanvas) -> None: ox, oy = self.getViewOffsets() style = self.currentStyle() color = style['color'] selectColor = style['selectedColor'] lineColor = style['lineColor'] + backgroundColor = self._textDocument._backgroundColor + + if backgroundColor != TTkColor.RST: + canvas.fill(color=backgroundColor) h = self.height() subLines = self._textWrap._lines[oy:oy+h] @@ -684,7 +692,10 @@ def paintEvent(self, canvas): for y, l in enumerate(subLines): t = outLines[l[0]-subLines[0][0]] - canvas.drawTTkString(pos=(-ox,y), text=t.substring(l[1][0],l[1][1]).tab2spaces(self._textWrap._tabSpaces)) + text:TTkString = t.substring(l[1][0],l[1][1]).tab2spaces(self._textWrap._tabSpaces) + if backgroundColor != TTkColor.RST: + text = text.completeColor(backgroundColor) + canvas.drawTTkString(pos=(-ox,y), text=text) if self._lineWrapMode == TTkK.FixedWidth: canvas.drawVLine(pos=(self._textWrap._wrapWidth,0), size=h, color=lineColor) diff --git a/tests/t.ui/test.ui.018.TextEdit.Pygments.py b/tests/t.ui/test.ui.018.TextEdit.01.Pygments.py similarity index 100% rename from tests/t.ui/test.ui.018.TextEdit.Pygments.py rename to tests/t.ui/test.ui.018.TextEdit.01.Pygments.py diff --git a/tests/t.ui/test.ui.018.TextEdit.02.Pygments.py b/tests/t.ui/test.ui.018.TextEdit.02.Pygments.py new file mode 100755 index 00000000..1bfae2fe --- /dev/null +++ b/tests/t.ui/test.ui.018.TextEdit.02.Pygments.py @@ -0,0 +1,118 @@ +#!/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, filenames): + frame = ttk.TTkFrame(parent=root, border=False, layout=ttk.TTkGridLayout()) + + te = ttk.TTkTextEdit() + te.setReadOnly(False) + + file = filenames[0] + with open(file, '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,9) + 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().addWidget(ttk.TTkLabel(text=" Lexer: ",maxWidth=8),0,5) + frame.layout().addWidget(lexers := ttk.TTkComboBox(list=ttk.TextDocumentHighlight.getLexers()),0,6) + frame.layout().addWidget(ttk.TTkLabel(text=" Style: ",maxWidth=8),0,7) + frame.layout().addWidget(styles := ttk.TTkComboBox(list=ttk.TextDocumentHighlight.getStyles()),0,8) + + + 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) + + wordWrap.currentIndexChanged.connect(_wordWrapCallback) + + return frame + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-f', help='Full Screen', action='store_true') + parser.add_argument('filename', type=str, nargs='+', + help='the filename/s') + args = parser.parse_args() + + root = ttk.TTk() + if args.f: + rootTree = root + root.setLayout(ttk.TTkGridLayout()) + else: + rootTree = ttk.TTkWindow(parent=root,pos = (0,0), size=(100,40), title="Test Text Edit", layout=ttk.TTkGridLayout(), border=True) + demoTextEdit(rootTree, args.filename) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file From 9bc752725036bbb062d807e68d24f6495313bd62 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 9 Dec 2024 15:21:06 +0000 Subject: [PATCH 5/6] fix test import --- TermTk/TTkGui/textdocument_highlight.py | 4 +--- tools/check.import.sh | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/TermTk/TTkGui/textdocument_highlight.py b/TermTk/TTkGui/textdocument_highlight.py index 281cf68e..28b331af 100644 --- a/TermTk/TTkGui/textdocument_highlight.py +++ b/TermTk/TTkGui/textdocument_highlight.py @@ -22,7 +22,7 @@ __all__ = ['TextDocumentHighlight'] -from threading import Lock +# from threading import Lock from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal @@ -30,12 +30,10 @@ class TextDocumentHighlight(TTkTextDocument): __slots__ = ( - '_highlightDocMutex', #Signals 'highlightUpdate') def __init__(self, *args, **kwargs): self.highlightUpdate = pyTTkSignal() - self._highlightDocMutex = Lock() super().__init__(*args, **kwargs) TTkLog.warn("Pygments not found!!!") diff --git a/tools/check.import.sh b/tools/check.import.sh index be5198d1..79f2c17c 100755 --- a/tools/check.import.sh +++ b/tools/check.import.sh @@ -48,6 +48,10 @@ __check(){ -e "TTkTerm/input_thread.py:import threading, queue" \ -e "TTkTerm/input_thread.py:from ..drivers import TTkInputDriver" \ -e "TTkTerm/input.py:from .input_thread import *" | + grep -v \ + -e "TTkGui/__init__.py:import importlib.util" \ + -e "TTkGui/textdocument_highlight_pygments.py:from threading import Lock" \ + -e "TTkGui/textdocument_highlight_pygments.py:from pygments" | grep -v \ -e "TTkTerm/term.py:from ..drivers import *" \ -e "drivers/unix_thread.py:import sys, os" \ From 29dda22c8d7d4e9dbaab88b185597b24416cfbe0 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 9 Dec 2024 17:17:00 +0000 Subject: [PATCH 6/6] Fixed FG text color if background is not null in the pygments integration --- TermTk/TTkGui/textcursor.py | 8 +++++ TermTk/TTkGui/textdocument.py | 16 +++++++++ TermTk/TTkGui/textdocument_highlight.py | 2 -- .../TTkGui/textdocument_highlight_pygments.py | 33 ++++++++++++++----- tools/check.import.sh | 2 +- 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/TermTk/TTkGui/textcursor.py b/TermTk/TTkGui/textcursor.py index e4b31edd..255990ee 100644 --- a/TermTk/TTkGui/textcursor.py +++ b/TermTk/TTkGui/textcursor.py @@ -348,6 +348,7 @@ def document(self) -> TTkTextDocument: def replaceText(self, text:TTkString, moveCursor:bool=False) -> None: # if there is no selection, just select the next n chars till the end of the line # the newline is not replaced + self._document._acquire() for p in self._properties: if not p.hasSelection(): line = p.position.line @@ -358,9 +359,11 @@ def replaceText(self, text:TTkString, moveCursor:bool=False) -> None: pos = self._document._dataLines[line].nextPos(pos) pos = min(size,pos) p.anchor.set(line,pos) + self._document._release() return self.insertText(text, moveCursor) def insertText(self, text:TTkString, moveCursor:bool=False) -> None: + self._document._acquire() _lineFirst = -1 if self.hasSelection(): _lineFirst, _lineRem, _lineAdd = self._removeSelectedText() @@ -446,6 +449,7 @@ def insertText(self, text:TTkString, moveCursor:bool=False) -> None: pp.anchor.line += diffLine self._autoChanged = True self._document.setChanged(True) + self._document._release() self._document.contentsChanged.emit() self._document.contentsChange.emit(lineFirst, lineRem, lineAdd) self._autoChanged = False @@ -552,14 +556,17 @@ def _alignPoint(point,st,en): def removeSelectedText(self) -> None: if not self.hasSelection(): return + self._document._acquire() a,b,c = self._removeSelectedText() self._autoChanged = True self._document.setChanged(True) + self._document._release() self._document.contentsChanged.emit() self._document.contentsChange.emit(a,b,c) self._autoChanged = False def applyColor(self, color:TTkColor) -> None: + self._document._acquire() for p in self._properties: selSt = p.selectionStart() selEn = p.selectionEnd() @@ -570,6 +577,7 @@ def applyColor(self, color:TTkColor) -> None: self._document._dataLines[l] = line.setColor(color=color, posFrom=pf, posTo=pt) self._autoChanged = True self._document.setChanged(True) + self._document._acquire() self._document.contentsChanged.emit() # self._document.contentsChange.emit(0,0,0) self._autoChanged = True diff --git a/TermTk/TTkGui/textdocument.py b/TermTk/TTkGui/textdocument.py index 8064e751..39a91849 100644 --- a/TermTk/TTkGui/textdocument.py +++ b/TermTk/TTkGui/textdocument.py @@ -22,6 +22,8 @@ __all__ = ['TTkTextDocument'] +from threading import Lock + from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot from TermTk.TTkCore.string import TTkString @@ -124,6 +126,7 @@ def _getSnap(self, lines, d): '_snap', '_snapChanged', '_lastSnap', '_lastCursor', '_backgroundColor', + '_docMutex', # Signals 'contentsChange', 'contentsChanged', 'formatChanged', @@ -133,6 +136,7 @@ def _getSnap(self, lines, d): ) def __init__(self, *, text:TTkString=" ") -> None: from TermTk.TTkGui.textcursor import TTkTextCursor + self._docMutex = Lock() self.cursorPositionChanged = pyTTkSignal(TTkTextCursor) self.contentsChange = pyTTkSignal(int,int,int) # int line, int linesRemoved, int linesAdded self.contentsChanged = pyTTkSignal() @@ -175,6 +179,12 @@ def __init__(self, *, text:TTkString=" ") -> None: # z1 = l1+a1 + (a2-r2) # z2 = l2+a2 + def _acquire(self) -> None: + self._docMutex.acquire() + + def _release(self) -> None: + self._docMutex.release() + @staticmethod def _mergeChangesSlices(ch1,ch2): l1,r1,a1 = ch1 @@ -226,10 +236,12 @@ def setText(self, text): remLines = len(self._dataLines) if not isinstance(text, str) and not isinstance(text,TTkString): text=str(text) + self._acquire() self._dataLines = [TTkString(t) for t in text.split('\n')] self._modified = False self._lastSnap = self._dataLines.copy() self._snap = TTkTextDocument._snapshot(self._lastCursor, None, None) + self._release() self.contentsChanged.emit() self.contentsChange.emit(0,remLines,len(self._dataLines)) self._snapChanged = None @@ -237,11 +249,13 @@ def setText(self, text): def appendText(self, text): if type(text) == str: text = TTkString() + text + self._acquire() oldLines = len(self._dataLines) self._dataLines += text.split('\n') self._modified = False self._lastSnap = self._dataLines.copy() self._snap = TTkTextDocument._snapshot(self._lastCursor, None, None) + self._release() self.contentsChanged.emit() self.contentsChange.emit(oldLines,0,len(self._dataLines)-oldLines) self._snapChanged = None @@ -297,10 +311,12 @@ def _restoreSnapshotDiff(self, next=True): (not next and not self._snap._prevDiff) ): return None + self._acquire() if next: self._snap = self._snap.getNextSnap(self._dataLines) else: self._snap = self._snap.getPrevSnap(self._dataLines) + self._release() self._lastSnap = self._dataLines.copy() self._lastCursor = self._snap._cursor.copy() diff --git a/TermTk/TTkGui/textdocument_highlight.py b/TermTk/TTkGui/textdocument_highlight.py index 28b331af..98296d27 100644 --- a/TermTk/TTkGui/textdocument_highlight.py +++ b/TermTk/TTkGui/textdocument_highlight.py @@ -22,8 +22,6 @@ __all__ = ['TextDocumentHighlight'] -# from threading import Lock - from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal from TermTk.TTkGui import TTkTextDocument diff --git a/TermTk/TTkGui/textdocument_highlight_pygments.py b/TermTk/TTkGui/textdocument_highlight_pygments.py index 2a12d598..e1f4cc28 100644 --- a/TermTk/TTkGui/textdocument_highlight_pygments.py +++ b/TermTk/TTkGui/textdocument_highlight_pygments.py @@ -22,8 +22,6 @@ __all__ = ['TextDocumentHighlight'] -from threading import Lock - from pygments import highlight from pygments.util import ClassNotFound from pygments.styles import get_all_styles @@ -50,8 +48,9 @@ def __init__(self, lines, block): self.error = None self.multiline = False - __slots__ = ('_dl', '_blockNum', '_highlightStyles') + __slots__ = ('_dl', '_blockNum', '_highlightStyles', '_defaultColor') def __init__(self, *args, **kwargs): + self._defaultColor = TTkColor.RST super().__init__(*args, **kwargs) self._highlightStyles = {} self._blockNum = 1 @@ -82,6 +81,9 @@ def __init__(self, *args, **kwargs): def setDl(self,dl): self._dl = dl + def setDefaultColor(self, color:TTkColor) -> None: + self._defaultColor = color + def format(self, tokensource, _): multiline = False multilineId = 0 @@ -95,7 +97,9 @@ def format(self, tokensource, _): ttype = ttype.parent # TTkLog.debug (f"{ttype=}") # TTkLog.debug (f"{value=}") - color = self._highlightStyles[ttype] + color:TTkColor = self._highlightStyles[ttype] + if not color.hasForeground(): + color += self._defaultColor values = value.split('\n') @@ -124,16 +128,16 @@ class TextDocumentHighlight(TTkTextDocument): _linesRefreshed:int = 30 __slots__ = ( '_timerRefresh', - '_highlightDocMutex', '_blocks', '_changedContent', '_refreshContent', '_lexer', '_formatter', + '_defaultForegroundColor', #Signals 'highlightUpdate') def __init__(self, **kwargs): self.highlightUpdate = pyTTkSignal() - self._highlightDocMutex = Lock() self._lexer = None self._blocks = [] + self._defaultForegroundColor = TTkColor.RST # self._formatter = _TTkFormatter(style='dracula') self._formatter = _TTkFormatter(style='gruvbox-dark') super().__init__(**kwargs) @@ -141,7 +145,7 @@ def __init__(self, **kwargs): self._timerRefresh.timeout.connect(self._refreshEvent) self._changedContent = (0,0,len(self._dataLines)) self._refreshContent = (0,TextDocumentHighlight._linesRefreshed) - self.contentsChange.connect(lambda a,b,c: TTkLog.debug(f"{a=} {b=} {c=}")) + # self.contentsChange.connect(lambda a,b,c: TTkLog.debug(f"{a=} {b=} {c=}")) self.contentsChange.connect(self._saveChangedContent) try: @@ -167,6 +171,16 @@ def setStyle(self, alias:str) -> None: self._backgroundColor = TTkColor.bg(color) else: self._backgroundColor = TTkColor.RST + + if self._backgroundColor == TTkColor.RST: + self._defaultForegroundColor = TTkColor.RST + else: + r,g,b = self._backgroundColor.bgToRGB() + if r+g+b < 127*3: + self._defaultForegroundColor = TTkColor.WHITE + else: + self._defaultForegroundColor = TTkColor.BLACK + TTkLog.debug(f"{color=} {alias=} {formatter.style}") self._changedContent = (0,0,len(self._dataLines)) self._refreshContent = (0,TextDocumentHighlight._linesRefreshed) @@ -206,7 +220,7 @@ def _saveChangedContent(self,a,b,c): @pyTTkSlot() def _refreshEvent(self): if not self._refreshContent: return - self._highlightDocMutex.acquire() + self._acquire() ra,rb = self._refreshContent @@ -251,6 +265,7 @@ def _refreshEvent(self): kfd = _TTkFormatter.Data(tsl1, block) self._formatter.setDl(kfd) + self._formatter.setDefaultColor(self._defaultForegroundColor) rawl = [l._text for l in tsl[offset:]] rawt = '\n'.join(rawl) @@ -283,6 +298,6 @@ def _refreshEvent(self): else: TTkLog.debug(f"Refresh {self._lexer.name} DONE!!!") - self._highlightDocMutex.release() + self._release() self.highlightUpdate.emit() self.formatChanged.emit() diff --git a/tools/check.import.sh b/tools/check.import.sh index 79f2c17c..934e56be 100755 --- a/tools/check.import.sh +++ b/tools/check.import.sh @@ -50,7 +50,7 @@ __check(){ -e "TTkTerm/input.py:from .input_thread import *" | grep -v \ -e "TTkGui/__init__.py:import importlib.util" \ - -e "TTkGui/textdocument_highlight_pygments.py:from threading import Lock" \ + -e "TTkGui/textdocument.py:from threading import Lock" \ -e "TTkGui/textdocument_highlight_pygments.py:from pygments" | grep -v \ -e "TTkTerm/term.py:from ..drivers import *" \