diff --git a/.vscode/launch.json b/.vscode/launch.json index 6151ff44..e9c443c5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -113,7 +113,7 @@ "console": "integratedTerminal", "justMyCode": true, "env": { - "PYTHONPATH": "./apps/ttkode" + "PYTHONPATH": "./apps/ttkode:./libs/pyTermTk" } }, { diff --git a/apps/ttkode/ttkode/app/__init__.py b/apps/ttkode/ttkode/app/__init__.py index 936b7d49..009ddf3a 100644 --- a/apps/ttkode/ttkode/app/__init__.py +++ b/apps/ttkode/ttkode/app/__init__.py @@ -1,6 +1,4 @@ -#!/usr/bin/env python3 - # MIT License # # Copyright (c) 2021 Eugenio Parodi diff --git a/apps/ttkode/ttkode/app/main.py b/apps/ttkode/ttkode/app/main.py index 04ceb784..a79bac0c 100644 --- a/apps/ttkode/ttkode/app/main.py +++ b/apps/ttkode/ttkode/app/main.py @@ -63,7 +63,7 @@ def main(): TTkodeHelper._loadPlugins() - ttkode = TTKode(files=args.filename) + ttkode = TTKode() ttkodeProxy.setTTKode(ttkode) root = TTk( layout=ttkode, @@ -73,8 +73,12 @@ def main(): # TTkTerm.Sigmask.CTRL_C | TTkTerm.Sigmask.CTRL_Q | TTkTerm.Sigmask.CTRL_S | + TTkTerm.Sigmask.CTRL_Y | TTkTerm.Sigmask.CTRL_Z )) + for file in args.filename: + ttkodeProxy.openFile(file) + TTkodeHelper._runPlugins() root.mainloop() diff --git a/apps/ttkode/ttkode/app/ttkode.py b/apps/ttkode/ttkode/app/ttkode.py index a3ed0af3..11b34b66 100644 --- a/apps/ttkode/ttkode/app/ttkode.py +++ b/apps/ttkode/ttkode/app/ttkode.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # MIT License # # Copyright (c) 2021 Eugenio Parodi @@ -25,31 +23,15 @@ __all__ = ['TTKode'] import os +from typing import List,Tuple,Optional,Any -from TermTk import TTkK, TTkLog, TTkCfg, TTkColor, TTkTheme, TTkTerm, TTkHelper -from TermTk import TTkString -from TermTk import pyTTkSlot, pyTTkSignal - -from TermTk import TTkFrame, TTkButton -from TermTk import TTkKodeTab -from TermTk import TTkFileDialogPicker -from TermTk import TTkFileTree, TTkTextEdit, TTkTextCursor - -from TermTk import TTkGridLayout -from TermTk import TTkSplitter,TTkAppTemplate -from TermTk import TextDocumentHighlight -from TermTk import TTkLogViewer -from TermTk import TTkMenuBarLayout -from TermTk import TTkAbout -from TermTk import TTkTestWidget, TTkTestWidgetSizes -from TermTk import TTkDnDEvent -from TermTk import TTkTreeWidget, TTkTreeWidgetItem, TTkFileTreeWidgetItem +import TermTk as ttk from TermTk.TTkWidgets.tabwidget import _TTkNewTabWidgetDragData from .about import About from .activitybar import TTKodeActivityBar -class TTKodeFileWidgetItem(TTkTreeWidgetItem): +class TTKodeFileWidgetItem(ttk.TTkTreeWidgetItem): __slots__ = ('_path', '_lineNumber') def __init__(self, *args, path:str, lineNumber:int=0, **kwargs) -> None: self._path = path @@ -60,117 +42,181 @@ class TTKodeFileWidgetItem(TTkTreeWidgetItem): def lineNumber(self) -> int: return self._lineNumber -class _TextDocument(TextDocumentHighlight): - __slots__ = ('_filePath') - def __init__(self, filePath:str="", **kwargs): - self._filePath = filePath +class _TextDocument(ttk.TextDocumentHighlight): + __slots__ = ('_filePath', '_tabText', + 'fileChangedStatus', '_changedStatus', '_savedSnapshot') + def __init__(self, filePath:str="", tabText:ttk.TTkString=ttk.TTkString(), **kwargs): + self.fileChangedStatus:ttk.pyTTkSignal = ttk.pyTTkSignal(bool, _TextDocument) + self._filePath:str = filePath + self._tabText = tabText + self._changedStatus:bool = False super().__init__(**kwargs) + self._savedSnapshot = self.snapshootId() self.guessLexerFromFilename(filePath) + self.contentsChanged.connect(self._handleContentChanged) -class TTKode(TTkGridLayout): - __slots__ = ('_kodeTab', '_documents', '_activityBar') - def __init__(self, *, files, **kwargs): - self._documents = {} - + def getTabButtonStyle(self) -> dict: + if self._changedStatus: + return {'default':{'closeGlyph':' ● '}} + else: + return {'default':{'closeGlyph':' □ '}} + + def _handleContentChanged(self) -> None: + '''A signal is emitted when the file status change, marking it as modified or not''' + # ttk.TTkLog.debug(f"{self.isUndoAvailable()=} == {self._changedStatus=}") + curState = self.changed() or self._savedSnapshot != self.snapshootId() + if self._changedStatus != curState: + self._changedStatus = not self._changedStatus + ttk.TTkLog.debug(f"{self.isUndoAvailable()=} == {self._changedStatus=}") + self.fileChangedStatus.emit(self._changedStatus, self) + + def save(self): + self._changedStatus = False + self._savedSnapshot = self.snapshootId() + self.fileChangedStatus.emit(self._changedStatus, self) + pass + +class _TextEdit(ttk.TTkTextEdit): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.cursorPositionChanged.connect(self._positionChanged) + + + @ttk.pyTTkSlot(ttk.TTkTextCursor) + def _positionChanged(self, cursor:ttk.TTkTextCursor): + extra_selections = [] + # Highlight Red only the lines under the cursor positions + cursor = self.textCursor().copy() + cursor.clearSelection() + selection = ttk.TTkTextEdit.ExtraSelection( + cursor=cursor, + color=ttk.TTkColor.bg("#333300"), + format=ttk.TTkK.SelectionFormat.FullWidthSelection) + extra_selections.append(selection) + self.setExtraSelections(extra_selections) + + def goToLine(self, linenum: int) -> None: + w,h = self.size() + h = min(h, self.document().lineCount()) + tedit:ttk.TTkTextEditView = self + tedit.textCursor().movePosition(operation=ttk.TTkTextCursor.MoveOperation.End) + tedit.ensureCursorVisible() + tedit.textCursor().setPosition(line=linenum-h//3,pos=0) + tedit.ensureCursorVisible() + tedit.textCursor().setPosition(line=linenum,pos=0) + +class TTKode(ttk.TTkGridLayout): + __slots__ = ('_kodeTab', '_activityBar') + def __init__(self, **kwargs): super().__init__(**kwargs) - appTemplate = TTkAppTemplate(border=False) + appTemplate = ttk.TTkAppTemplate(border=False) self.addWidget(appTemplate) - self._kodeTab = TTkKodeTab(border=False, closable=True) + self._kodeTab = ttk.TTkKodeTab(border=False, barType=ttk.TTkBarType.NERD_1 ,closable=True) self._kodeTab.setDropEventProxy(self._dropEventProxyFile) - appTemplate.setMenuBar(appMenuBar:=TTkMenuBarLayout(), TTkAppTemplate.MAIN) + appTemplate.setMenuBar(appMenuBar:=ttk.TTkMenuBarLayout(), ttk.TTkAppTemplate.MAIN) fileMenu = appMenuBar.addMenu("&File") fileMenu.addMenu("Open").menuButtonClicked.connect(self._showFileDialog) fileMenu.addMenu("Close") # .menuButtonClicked.connect(self._closeFile) - fileMenu.addMenu("Exit").menuButtonClicked.connect(lambda _:TTkHelper.quit()) + fileMenu.addMenu("Exit").menuButtonClicked.connect(lambda _:ttk.TTkHelper.quit()) def _showAbout(btn): - TTkHelper.overlay(None, About(), 30,10) + ttk.TTkHelper.overlay(None, About(), 30,10) def _showAboutTTk(btn): - TTkHelper.overlay(None, TTkAbout(), 30,10) + ttk.TTkHelper.overlay(None, ttk.TTkAbout(), 30,10) - appMenuBar.addMenu("&Quit", alignment=TTkK.RIGHT_ALIGN).menuButtonClicked.connect(TTkHelper.quit) - helpMenu = appMenuBar.addMenu("&Help", alignment=TTkK.RIGHT_ALIGN) + appMenuBar.addMenu("&Quit", alignment=ttk.TTkK.RIGHT_ALIGN).menuButtonClicked.connect(ttk.TTkHelper.quit) + helpMenu = appMenuBar.addMenu("&Help", alignment=ttk.TTkK.RIGHT_ALIGN) helpMenu.addMenu("About ...").menuButtonClicked.connect(_showAbout) helpMenu.addMenu("About ttk").menuButtonClicked.connect(_showAboutTTk) - fileTree = TTkFileTree(path='.', dragDropMode=TTkK.DragDropMode.AllowDrag) + fileTree = ttk.TTkFileTree(path='.', dragDropMode=ttk.TTkK.DragDropMode.AllowDrag) self._activityBar = TTKodeActivityBar() - self._activityBar.addActivity(name="Explorer", icon=TTkString("╔██\n╚═╝"), widget=fileTree, select=True) + self._activityBar.addActivity(name="Explorer", icon=ttk.TTkString("╔██\n╚═╝"), widget=fileTree, select=True) - appTemplate.setWidget(self._kodeTab, TTkAppTemplate.MAIN) - appTemplate.setItem(self._activityBar, TTkAppTemplate.LEFT, size=30) - appTemplate.setWidget(TTkLogViewer(), TTkAppTemplate.BOTTOM, title="Logs", size=3) - - for file in files: - self._openFile(file) + appTemplate.setWidget(self._kodeTab, ttk.TTkAppTemplate.MAIN) + appTemplate.setItem(self._activityBar, ttk.TTkAppTemplate.LEFT, size=30) + appTemplate.setWidget(ttk.TTkLogViewer(), ttk.TTkAppTemplate.BOTTOM, title="Logs", size=3) fileTree.fileActivated.connect(lambda x: self._openFile(x.path())) - - pyTTkSlot() + self._kodeTab.tabAdded.connect(self._tabAdded) + + def _getTabButtonFromWidget(self, widget:ttk.TTkWidget) -> ttk.TTkTabButton: + for item, tab in self._kodeTab.iterWidgets(): + if item == widget: + return tab + return None + + ttk.pyTTkSlot(ttk.TTkTabWidget, int) + def _tabAdded(self, tw:ttk.TTkTabWidget, index:int): + tb = tw.tabButton(index) + wid = tw.widget(index) + if isinstance(wid,_TextEdit): + tb.mergeStyle(wid.document().getTabButtonStyle()) + + ttk.pyTTkSlot() def _showFileDialog(self): - filePicker = TTkFileDialogPicker(pos = (3,3), size=(75,24), caption="Pick Something", path=".", fileMode=TTkK.FileMode.AnyFile ,filter="All Files (*);;Python Files (*.py);;Bash scripts (*.sh);;Markdown Files (*.md)") + filePicker = ttk.TTkFileDialogPicker(pos = (3,3), size=(75,24), caption="Pick Something", path=".", fileMode=ttk.TTkK.FileMode.AnyFile ,filter="All Files (*);;Python Files (*.py);;Bash scripts (*.sh);;Markdown Files (*.md)") filePicker.pathPicked.connect(self._openFile) - TTkHelper.overlay(None, filePicker, 20, 5, True) - - def _openFile(self, filePath, lineNumber=0): + ttk.TTkHelper.overlay(None, filePicker, 20, 5, True) + + def _getDocument(self, filePath) -> Tuple[_TextDocument, Optional[_TextEdit]]: + for item, _ in self._kodeTab.iterWidgets(): + if issubclass(type(item), _TextEdit): + doc = item.document() + if issubclass(type(doc), _TextDocument): + if filePath == doc._filePath: + return doc, item + with open(filePath, 'r') as f: + content = f.read() + tabText = ttk.TTkString(ttk.TTkCfg.theme.fileIcon.getIcon(filePath),ttk.TTkCfg.theme.fileIconColor) + ttk.TTkColor.RST + " " + os.path.basename(filePath) + td = _TextDocument(text=content, filePath=filePath, tabText=tabText) + td.fileChangedStatus.connect(self._handleFileChangedStatus) + return td, None + + ttk.pyTTkSlot(bool, _TextDocument) + def _handleFileChangedStatus(self, status:bool, doc:_TextDocument) -> None: + # ttk.TTkLog.debug(f"Status ({status}) -> {doc._filePath}") + for item, tab in self._kodeTab.iterWidgets(): + if issubclass(type(item), _TextEdit): + if doc == item.document(): + tab.mergeStyle(doc.getTabButtonStyle()) + + def _openFile(self, filePath, line:int=0, pos:int=0): filePath = os.path.realpath(filePath) - if filePath in self._documents: - doc = self._documents[filePath]['doc'] + doc, tedit = self._getDocument(filePath=filePath) + if tedit: + self._kodeTab.setCurrentWidget(tedit) else: - with open(filePath, 'r') as f: - content = f.read() - doc = _TextDocument(text=content, filePath=filePath) - self._documents[filePath] = {'doc':doc,'tabs':[]} - tedit = TTkTextEdit(document=doc, readOnly=False, lineNumber=True) - label = TTkString(TTkCfg.theme.fileIcon.getIcon(filePath),TTkCfg.theme.fileIconColor) + TTkColor.RST + " " + os.path.basename(filePath) - - self._kodeTab.addTab(tedit, label) - self._kodeTab.setCurrentWidget(tedit) - - if lineNumber: - tedit.textCursor().movePosition(operation=TTkTextCursor.MoveOperation.End) - tedit.ensureCursorVisible() - tedit.textCursor().setPosition(line=lineNumber,pos=0) - tedit.ensureCursorVisible() - newCursor = tedit.textCursor().copy() - newCursor.clearSelection() - selection = TTkTextEdit.ExtraSelection( - cursor=newCursor, - color=TTkColor.bg("#444400"), - format=TTkK.SelectionFormat.FullWidthSelection) - tedit.setExtraSelections([selection]) + tedit = _TextEdit(document=doc, readOnly=False, lineNumber=True) + self._kodeTab.addTab(tedit, doc._tabText) + self._kodeTab.setCurrentWidget(tedit) + tedit.goToLine(line) tedit.setFocus() - def _dropEventProxyFile(self, evt:TTkDnDEvent): + def _dropEventProxyFile(self, evt:ttk.TTkDnDEvent): data = evt.data() filePath = None - if ( issubclass(type(data), TTkTreeWidget._DropTreeData) and - data.items ): - if issubclass(type(data.items[0]), TTkFileTreeWidgetItem): - item:TTkFileTreeWidgetItem = data.items[0] - filePath = os.path.realpath(item.path()) + if ( issubclass(type(data), ttk.TTkTreeWidget._DropTreeData) and data.items ): + if issubclass(type(data.items[0]), ttk.TTkFileTreeWidgetItem): + linenum:int = 0 + ftwi:ttk.TTkFileTreeWidgetItem = data.items[0] + filePath = os.path.realpath(ftwi.path()) elif issubclass(type(data.items[0]), TTKodeFileWidgetItem): - item:TTkFileTreeWidgetItem = data.items[0] - filePath = os.path.realpath(item.path()) + kfwi:TTKodeFileWidgetItem = data.items[0] + linenum:int = kfwi.lineNumber() + filePath = os.path.realpath(kfwi.path()) if filePath: - if filePath in self._documents: - doc = self._documents[filePath]['doc'] - else: - with open(filePath, 'r') as f: - content = f.read() - doc = _TextDocument(text=content, filePath=filePath) - self._documents[filePath] = {'doc':doc,'tabs':[]} - tedit = TTkTextEdit(document=doc, readOnly=False, lineNumber=True) - label = TTkString(TTkCfg.theme.fileIcon.getIcon(filePath),TTkCfg.theme.fileIconColor) + TTkColor.RST + " " + os.path.basename(filePath) - + doc, _ = self._getDocument(filePath=filePath) + tedit = _TextEdit(document=doc, readOnly=False, lineNumber=True) + tedit.goToLine(linenum) newData = _TTkNewTabWidgetDragData( widget=tedit, - label=label, + label=doc._tabText, data=None, closable=True ) @@ -178,6 +224,3 @@ class TTKode(TTkGridLayout): newEvt.setData(newData) return newEvt return evt - # def _closeFile(): - # if (index := KodeTab.lastUsed.currentIndex()) >= 0: - # KodeTab.lastUsed.removeTab(index) diff --git a/apps/ttkode/ttkode/plugins/_010/findwidget.py b/apps/ttkode/ttkode/plugins/_010/findwidget.py index e0945535..5386a320 100644 --- a/apps/ttkode/ttkode/plugins/_010/findwidget.py +++ b/apps/ttkode/ttkode/plugins/_010/findwidget.py @@ -100,7 +100,7 @@ def _walk_with_gitignore(root): yield dirpath, filenames -class _ExpandButton(ttk.TTkButton): +class _ToggleButton(ttk.TTkButton): def __init__(self, **kwargs): params = { 'border':False, @@ -140,19 +140,26 @@ class FindWidget(ttk.TTkContainer): super().__init__(**kwargs) self.setLayout(layout:=ttk.TTkGridLayout()) + searchLayout = ttk.TTkGridLayout() - searchLayout.addWidget(expandReplace:=_ExpandButton(), 0, 0) - searchLayout.addWidget(search :=ttk.TTkLineEdit(hint='Search'), 0, 1) + searchLayout.addWidget(expandReplace:=_ToggleButton(), 0, 0) + searchLayout.addWidget(search :=ttk.TTkLineEdit(hint='Search'), 0, 1) searchLayout.addWidget(repl__l:=ttk.TTkLabel(visible=False, text='sub:'), 1, 0) searchLayout.addWidget(ft_in_l:=ttk.TTkLabel(visible=False, text='inc:'), 2, 0) searchLayout.addWidget(ft_ex_l:=ttk.TTkLabel(visible=False, text='exc:'), 3, 0) searchLayout.addWidget(replace:=ttk.TTkLineEdit(visible=False, hint='Replace'), 1, 1) searchLayout.addWidget(ft_incl:=ttk.TTkLineEdit(visible=False, hint='Files to include'), 2, 1) searchLayout.addWidget(ft_excl:=ttk.TTkLineEdit(visible=False, hint='Files to exclude'), 3, 1) - layout.addItem(searchLayout, 0, 0) - layout.addWidget(btn_search:=ttk.TTkButton(text="Search", border=False), 4,0) - layout.addWidget(res_tree:=ttk.TTkTree(dragDropMode=ttk.TTkK.DragDropMode.AllowDrag), 5,0) + + controlsLayout = ttk.TTkGridLayout() + controlsLayout.addWidget(btn_search:=ttk.TTkButton(text="Search", border=False), 0,0) + controlsLayout.addWidget(btn_replace:=ttk.TTkButton(text='Replace', border=False, enabled=False), 0, 1) + controlsLayout.addWidget(btn_expand :=ttk.TTkButton(text='+', maxWidth=3, border=False), 0, 2) + controlsLayout.addWidget(btn_collapse:=ttk.TTkButton(text='-', maxWidth=3, border=False), 0, 3) + layout.addItem(controlsLayout,1,0) + + layout.addWidget(res_tree:=ttk.TTkTree(dragDropMode=ttk.TTkK.DragDropMode.AllowDrag), 2,0) res_tree.setHeaderLabels(["Results"]) res_tree.setColumnWidth(0,100) @@ -170,15 +177,24 @@ class FindWidget(ttk.TTkContainer): self._files_exc_le = ft_excl btn_search.clicked.connect(self._search) + btn_expand.clicked.connect(self._results_tree.expandAll) + btn_collapse.clicked.connect(self._results_tree.collapseAll) search.returnPressed.connect(self._search) res_tree.itemActivated.connect(self._activated) + @ttk.pyTTkSlot(str) + def _replace_txt(value): + btn_replace.setEnabled(bool(value)) + + self._replace_le.textChanged.connect(_replace_txt) + + @ttk.pyTTkSlot(ttk.TTkTreeWidgetItem, int) def _activated(self, item:ttk.TTkTreeWidgetItem, _): if isinstance(item, _MatchTreeWidgetItem): file = item.path() line = item.lineNumber() - ttkodeProxy.ttkode()._openFile(file, line) + ttkodeProxy.openFile(file, line) @ttk.pyTTkSlot() @@ -197,10 +213,13 @@ class FindWidget(ttk.TTkContainer): ttk.TTkString( ttk.TTkCfg.theme.fileIcon.getIcon(file), ttk.TTkCfg.theme.fileIconColor) + " " + - ttk.TTkString(f" {file} ", ttk.TTkColor.YELLOW+ttk.TTkColor.BOLD) + + ttk.TTkString(f" {file} ", ttk.TTkColor.YELLOW+ttk.TTkColor.BOLD+ttk.TTkColor.bg('#000088')) + ttk.TTkString(f" {root} ", ttk.TTkColor.fg("#888888")) ],expanded=True) for num,line in matches: + line = line.lstrip(' ') + # index = line.find(search_pattern) + # outLine = item.addChild( _MatchTreeWidgetItem([ ttk.TTkString(str(num)+" ",ttk.TTkColor.CYAN) + diff --git a/apps/ttkode/ttkode/proxy.py b/apps/ttkode/ttkode/proxy.py index e74ef61d..15025b05 100644 --- a/apps/ttkode/ttkode/proxy.py +++ b/apps/ttkode/ttkode/proxy.py @@ -22,6 +22,8 @@ __all__ = ['TTKodeViewerProxy', 'ttkodeProxy'] +from typing import Optional, Callable, Any + import TermTk as ttk from ttkode.app.ttkode import TTKode @@ -39,21 +41,26 @@ class TTKodeProxy(): '_ttkode', # Signals ) - _ttkode:TTKode + _ttkode:Optional[TTKode] + _openFileCb:Optional[Callable[[Any, int, int], Any]] def __init__(self) -> None: - self._openFileCb = lambda _ : None + self._openFileCb = None self._ttkode = None def setTTKode(self, ttkode:TTKode) -> None: self._ttkode = ttkode + self._openFileCb = ttkode._openFile def ttkode(self) -> TTKode: + if not self._ttkode: + raise Exception("TTkode uninitialized") return self._ttkode def setOpenFile(self, cb): self._openFileCb = cb - def openFile(self, fileName): - return self._openFileCb(fileName) + def openFile(self, fileName:str, line:int=0, pos:int=0): + if self._ttkode and self._openFileCb: + return self._openFileCb(fileName, line, pos) ttkodeProxy:TTKodeProxy = TTKodeProxy() \ No newline at end of file