18 changed files with 637 additions and 101 deletions
@ -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 * |
||||
|
||||
@ -0,0 +1,57 @@
|
||||
# MIT License |
||||
# |
||||
# Copyright (c) 2024 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. |
||||
|
||||
__all__ = ['TextDocumentHighlight'] |
||||
|
||||
from TermTk.TTkCore.log import TTkLog |
||||
from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal |
||||
from TermTk.TTkGui import TTkTextDocument |
||||
|
||||
class TextDocumentHighlight(TTkTextDocument): |
||||
__slots__ = ( |
||||
#Signals |
||||
'highlightUpdate') |
||||
def __init__(self, *args, **kwargs): |
||||
self.highlightUpdate = pyTTkSignal() |
||||
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 |
||||
|
||||
@ -0,0 +1,303 @@
|
||||
# MIT License |
||||
# |
||||
# Copyright (c) 2024 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. |
||||
|
||||
__all__ = ['TextDocumentHighlight'] |
||||
|
||||
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', '_defaultColor') |
||||
def __init__(self, *args, **kwargs): |
||||
self._defaultColor = TTkColor.RST |
||||
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 setDefaultColor(self, color:TTkColor) -> None: |
||||
self._defaultColor = color |
||||
|
||||
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:TTkColor = self._highlightStyles[ttype] |
||||
if not color.hasForeground(): |
||||
color += self._defaultColor |
||||
|
||||
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', |
||||
'_blocks', '_changedContent', '_refreshContent', |
||||
'_lexer', '_formatter', |
||||
'_defaultForegroundColor', |
||||
#Signals |
||||
'highlightUpdate') |
||||
def __init__(self, **kwargs): |
||||
self.highlightUpdate = pyTTkSignal() |
||||
self._lexer = None |
||||
self._blocks = [] |
||||
self._defaultForegroundColor = TTkColor.RST |
||||
# 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 |
||||
|
||||
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) |
||||
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._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) |
||||
self._formatter.setDefaultColor(self._defaultForegroundColor) |
||||
|
||||
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._release() |
||||
self.highlightUpdate.emit() |
||||
self.formatChanged.emit() |
||||
@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3 |
||||
|
||||
# MIT License |
||||
# |
||||
# Copyright (c) 2021 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||
# |
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
# of this software and associated documentation files (the "Software"), to deal |
||||
# in the Software without restriction, including without limitation the rights |
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
# copies of the Software, and to permit persons to whom the Software is |
||||
# furnished to do so, subject to the following conditions: |
||||
# |
||||
# The above copyright notice and this permission notice shall be included in all |
||||
# copies or substantial portions of the Software. |
||||
# |
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
# SOFTWARE. |
||||
|
||||
import os |
||||
import sys |
||||
import random |
||||
import argparse |
||||
|
||||
sys.path.append(os.path.join(sys.path[0],'../..')) |
||||
import TermTk as ttk |
||||
|
||||
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() |
||||
Loading…
Reference in new issue