Browse Source

feat: add save feature (#407)

pull/397/head
Pier CeccoPierangioliEugenio 10 months ago committed by GitHub
parent
commit
26ff9b2f0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      apps/ttkode/ttkode/__main__.py
  2. 3
      apps/ttkode/ttkode/app/__init__.py
  3. 26
      apps/ttkode/ttkode/app/state.py
  4. 196
      apps/ttkode/ttkode/app/ttkode.py
  5. 30
      apps/ttkode/ttkode/proxy.py

6
apps/ttkode/ttkode/__main__.py

@ -33,7 +33,6 @@ from TermTk import TTkLog
from ttkode import TTkodeHelper
from ttkode import ttkodeProxy
from ttkode.app.ttkode import TTKode
from ttkode.app.cfg import TTKodeCfg
@ -62,10 +61,7 @@ def main():
TTkodeHelper._loadPlugins()
ttkode = TTKode()
ttkodeProxy.setTTKode(ttkode)
root = TTk( layout=ttkode,
root = TTk( layout=ttkodeProxy.ttkode(),
title="TTkode",
mouseTrack=True,
sigmask=(

3
apps/ttkode/ttkode/app/__init__.py

@ -22,5 +22,6 @@
# SOFTWARE.
from .cfg import *
from .state import TTKodeState
# from .glbl import *
from .ttkode import *
from .ttkode import TTKode, TTKodeWidget

26
apps/ttkode/ttkode/app/state.py

@ -0,0 +1,26 @@
# 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.
__all__ = ['TTKodeState']
class TTKodeState():
pass

196
apps/ttkode/ttkode/app/ttkode.py

@ -20,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
__all__ = ['TTKode']
__all__ = ['TTKode', 'TTKodeWidget']
import os
from typing import List,Tuple,Optional,Any
@ -31,6 +31,10 @@ from TermTk.TTkWidgets.tabwidget import _TTkNewTabWidgetDragData
from .about import About
from .activitybar import TTKodeActivityBar
class TTKodeWidget():
def closeRequested(self, tab:ttk.TTkTabWidget, num:int):
raise NotImplementedError()
class TTKodeFileWidgetItem(ttk.TTkTreeWidgetItem):
__slots__ = ('_path', '_lineNumber')
def __init__(self, *args, path:str, lineNumber:int=0, **kwargs) -> None:
@ -45,16 +49,24 @@ class TTKodeFileWidgetItem(ttk.TTkTreeWidgetItem):
class _TextDocument(ttk.TextDocumentHighlight):
__slots__ = ('_filePath', '_tabText',
'fileChangedStatus', '_changedStatus', '_savedSnapshot')
def __init__(self, filePath:str="", tabText:ttk.TTkString=ttk.TTkString(), **kwargs):
def __init__(self, filePath:str="", **kwargs):
self.fileChangedStatus:ttk.pyTTkSignal = ttk.pyTTkSignal(bool, _TextDocument)
self._filePath:str = filePath
self._tabText = tabText
self._changedStatus:bool = False
self._genTabText()
super().__init__(**kwargs)
self._savedSnapshot = self.snapshootId()
self.guessLexerFromFilename(filePath)
self.contentsChanged.connect(self._handleContentChanged)
def _genTabText(self):
self._tabText = (
ttk.TTkString(ttk.TTkCfg.theme.fileIcon.getIcon(self._filePath),ttk.TTkCfg.theme.fileIconColor) +
ttk.TTkColor.RST + " " + os.path.basename(self._filePath) )
def isChanged(self) -> bool:
return self._changedStatus
def getTabButtonStyle(self) -> dict:
if self._changedStatus:
return {'default':{'closeGlyph':''}}
@ -70,17 +82,67 @@ class _TextDocument(ttk.TextDocumentHighlight):
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 save(self) -> None:
try:
with open(self._filePath, 'w') as f:
f.write(self.toPlainText())
self._changedStatus = False
self._savedSnapshot = self.snapshootId()
self.fileChangedStatus.emit(self._changedStatus, self)
except Exception as e:
ttk.TTkLog.error(f"Error saving file: {e}")
def saveToFile(self, fileName:str) -> None:
self._filePath = fileName
self._genTabText()
self.save()
class _TextEdit(ttk.TTkTextEdit, TTKodeWidget):
__slots__ = ('docFocussed')
def __init__(self, **kwargs):
self.docFocussed = ttk.pyTTkSignal(_TextDocument)
super().__init__(**kwargs)
self.cursorPositionChanged.connect(self._positionChanged)
self.textEditView().focusChanged.connect(self._handleFocusChanged)
@ttk.pyTTkSlot(bool)
def _handleFocusChanged(self, focus:bool) -> None:
if focus:
self.docFocussed.emit(self.document())
def closeRequested(self, tab:ttk.TTkTabWidget, num:int):
from ttkode import ttkodeProxy
doc = self.document()
docs = [wid.document() for wid in ttkodeProxy.iterWidgets(_TextEdit) if wid is not self]
if not doc.isChanged() or doc in docs:
ttkodeProxy.closeTab(self)
else:
pass
# Do you want to save the changes you made to ""?
# Your saves will be lost if you don't save them.
# Save, Don't Save, Cancel
messageBox = ttk.TTkMessageBox(
title="🚨 Close? 🚨",
text=ttk.TTkString(f"Do you want to save the change\nyou made to {os.path.basename(doc._filePath)}?\n\nYour saves will be lost\nif you don't save them."),
icon=ttk.TTkMessageBox.Icon.Warning,
standardButtons=
ttk.TTkMessageBox.StandardButton.Discard|
ttk.TTkMessageBox.StandardButton.Save|
ttk.TTkMessageBox.StandardButton.Cancel)
@ttk.pyTTkSlot(ttk.TTkMessageBox.StandardButton)
def _cb(btn):
if btn == ttk.TTkMessageBox.StandardButton.Save:
doc.save()
self.textEditView().focusChanged.clear()
ttkodeProxy.closeTab(self)
if btn == ttk.TTkMessageBox.StandardButton.Discard:
self.textEditView().focusChanged.clear()
ttkodeProxy.closeTab(self)
elif btn == ttk.TTkMessageBox.StandardButton.Cancel:
return
messageBox.buttonSelected.clear()
messageBox.buttonSelected.connect(_cb)
ttk.TTkHelper.overlay(None, messageBox, 5, 5, True)
@ttk.pyTTkSlot(ttk.TTkTextCursor)
def _positionChanged(self, cursor:ttk.TTkTextCursor):
@ -106,8 +168,10 @@ class _TextEdit(ttk.TTkTextEdit):
tedit.textCursor().setPosition(line=linenum,pos=0)
class TTKode(ttk.TTkGridLayout):
__slots__ = ('_kodeTab', '_activityBar')
__slots__ = ('_kodeTab', '_activityBar', '_lastDoc')
_lastDoc:Optional[_TextDocument]
def __init__(self, **kwargs):
self._lastDoc = None
super().__init__(**kwargs)
appTemplate = ttk.TTkAppTemplate(border=False)
@ -119,15 +183,17 @@ class TTKode(ttk.TTkGridLayout):
appTemplate.setMenuBar(appMenuBar:=ttk.TTkMenuBarLayout(), ttk.TTkAppTemplate.MAIN)
fileMenu = appMenuBar.addMenu("&File")
fileMenu.addMenu("Open").menuButtonClicked.connect(self._showFileDialog)
fileMenu.addMenu("&Save").menuButtonClicked.connect(self.saveLastDoc)
fileMenu.addMenu("Save &As...").menuButtonClicked.connect(self.saveLastDocAs)
fileMenu.addMenu("Close") # .menuButtonClicked.connect(self._closeFile)
fileMenu.addMenu("Exit").menuButtonClicked.connect(lambda _:ttk.TTkHelper.quit())
fileMenu.addMenu("Exit").menuButtonClicked.connect(self._quit)
def _showAbout(btn):
ttk.TTkHelper.overlay(None, About(), 30,10)
def _showAboutTTk(btn):
ttk.TTkHelper.overlay(None, ttk.TTkAbout(), 30,10)
appMenuBar.addMenu("&Quit", alignment=ttk.TTkK.RIGHT_ALIGN).menuButtonClicked.connect(ttk.TTkHelper.quit)
appMenuBar.addMenu("&Quit", alignment=ttk.TTkK.RIGHT_ALIGN).menuButtonClicked.connect(self._quit)
helpMenu = appMenuBar.addMenu("&Help", alignment=ttk.TTkK.RIGHT_ALIGN)
helpMenu.addMenu("About ...").menuButtonClicked.connect(_showAbout)
helpMenu.addMenu("About ttk").menuButtonClicked.connect(_showAboutTTk)
@ -161,14 +227,86 @@ class TTKode(ttk.TTkGridLayout):
self._kodeTab.tabAdded.connect(self._tabAdded)
self._kodeTab.kodeTabCloseRequested.connect(self._handleTabCloseRequested)
ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.Key_S).activated.connect(self.saveLastDoc)
@ttk.pyTTkSlot(_TextDocument)
def _handleDocFocussed(self, doc:_TextDocument):
self._lastDoc = doc
@ttk.pyTTkSlot()
def _quit(self):
from ttkode import ttkodeProxy
docs = set(os.path.basename(wid.document()._filePath) for wid in ttkodeProxy.iterWidgets(_TextEdit) if wid.document().isChanged())
if docs:
messageBox = ttk.TTkMessageBox(
title="🚨 Quit? 🚨",
text=ttk.TTkString("Do you want to quit?\nThere are still unsaved documents,\nif you quit those changes will be lost" + ''.join([f"\n - {_d}" for _d in docs])),
icon=ttk.TTkMessageBox.Icon.Warning,
standardButtons=
ttk.TTkMessageBox.StandardButton.Ok|
ttk.TTkMessageBox.StandardButton.Cancel)
@ttk.pyTTkSlot(ttk.TTkMessageBox.StandardButton)
def _cb(btn):
if btn == ttk.TTkMessageBox.StandardButton.Ok:
ttk.TTkHelper.quit()
elif btn == ttk.TTkMessageBox.StandardButton.Cancel:
return
messageBox.buttonSelected.clear()
messageBox.buttonSelected.connect(_cb)
ttk.TTkHelper.overlay(None, messageBox, 5, 5, True)
else:
ttk.TTkHelper.quit()
@ttk.pyTTkSlot()
def saveLastDoc(self):
if self._lastDoc:
self._lastDoc.save()
@ttk.pyTTkSlot()
def saveLastDocAs(self):
if not self._lastDoc:
return
def _approveFile(fileName):
if os.path.exists(fileName):
@ttk.pyTTkSlot(ttk.TTkMessageBox.StandardButton)
def _cb(btn):
if btn == ttk.TTkMessageBox.StandardButton.Save:
self._lastDoc.saveToFile(fileName)
elif btn == ttk.TTkMessageBox.StandardButton.Cancel:
return
messageBox = ttk.TTkMessageBox(
text= (
ttk.TTkString( f'A file named "{os.path.basename(fileName)}" already exists.\nDo you want to replace it?', ttk.TTkColor.BOLD) +
ttk.TTkString( f'\n\nReplacing it will overwrite its contents.') ),
icon=ttk.TTkMessageBox.Icon.Warning,
standardButtons=
ttk.TTkMessageBox.StandardButton.Discard|
ttk.TTkMessageBox.StandardButton.Save|
ttk.TTkMessageBox.StandardButton.Cancel)
messageBox.buttonSelected.connect(_cb)
ttk.TTkHelper.overlay(None, messageBox, 5, 5, True)
else:
self._lastDoc.saveToFile(fileName)
filePicker = ttk.TTkFileDialogPicker(
pos = (3,3), size=(80,30),
acceptMode=ttk.TTkK.AcceptMode.AcceptSave,
caption="Save As...",
path=self._lastDoc._filePath,
fileMode=ttk.TTkK.FileMode.AnyFile ,
filter="All Files (*)")
filePicker.pathPicked.connect(_approveFile)
ttk.TTkHelper.overlay(None, filePicker, 5, 5, True)
@ttk.pyTTkSlot(ttk.TTkTabWidget, int)
def _handleTabCloseRequested(self, tab:ttk.TTkTabWidget, num:int):
tab.removeTab(num)
# tab.removeTab(num)
tab.widget(num).closeRequested(tab, num)
def _getTabButtonFromWidget(self, widget:ttk.TTkWidget) -> ttk.TTkTabButton:
for item, tab in self._kodeTab.iterWidgets():
if item == widget:
return tab
for kt, index in self._kodeTab.iterItems():
if kt.widget(index) == widget:
return kt.tabButton(index)
return None
ttk.pyTTkSlot(ttk.TTkTabWidget, int)
@ -185,26 +323,26 @@ class TTKode(ttk.TTkGridLayout):
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()
for kt, index in self._kodeTab.iterItems():
if issubclass(type(wid:=kt.widget(index)), _TextEdit):
doc = wid.document()
if issubclass(type(doc), _TextDocument):
if filePath == doc._filePath:
return doc, item
return doc, wid
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 = _TextDocument(text=content, filePath=filePath)
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())
for kt, index in self._kodeTab.iterItems():
if issubclass(type(wid:=kt.widget(index)), _TextEdit):
if doc == wid.document():
kt.tabButton(index).mergeStyle(doc.getTabButtonStyle())
kt.tabButton(index).setText(doc._tabText)
def _openFile(self, filePath, line:int=0, pos:int=0):
filePath = os.path.realpath(filePath)
@ -213,6 +351,7 @@ class TTKode(ttk.TTkGridLayout):
self._kodeTab.setCurrentWidget(tedit)
else:
tedit = _TextEdit(document=doc, readOnly=False, lineNumber=True)
tedit.docFocussed.connect(self._handleDocFocussed)
self._kodeTab.addTab(tedit, doc._tabText)
self._kodeTab.setCurrentWidget(tedit)
tedit.goToLine(line)
@ -235,6 +374,7 @@ class TTKode(ttk.TTkGridLayout):
if filePath:
doc, _ = self._getDocument(filePath=filePath)
tedit = _TextEdit(document=doc, readOnly=False, lineNumber=True)
tedit.docFocussed.connect(self._handleDocFocussed)
tedit.goToLine(linenum)
newData = _TTkNewTabWidgetDragData(
widget=tedit,

30
apps/ttkode/ttkode/proxy.py

@ -26,7 +26,7 @@ from typing import Optional, Callable, Any
import TermTk as ttk
from ttkode.app.ttkode import TTKode
from ttkode.app.ttkode import TTKode, TTKodeWidget
class TTKodeViewerProxy():
__slots__ = ('_fileName')
@ -41,26 +41,30 @@ class TTKodeProxy():
'_ttkode',
# Signals
)
_ttkode:Optional[TTKode]
_openFileCb:Optional[Callable[[Any, int, int], Any]]
def __init__(self) -> None:
self._openFileCb = None
self._ttkode = None
def setTTKode(self, ttkode:TTKode) -> None:
_ttkode:TTKode
_openFileCb:Callable[[Any, int, int], Any]
def __init__(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 iterWidgets(self, widType=TTKodeWidget):
for kt, index in self._ttkode._kodeTab.iterItems():
if issubclass(type(wid:=kt.widget(index)), widType):
yield wid
@ttk.pyTTkSlot(TTKodeWidget)
def closeTab(self, widget:TTKodeWidget) -> None:
for kt, index in self._ttkode._kodeTab.iterItems():
if kt.widget(index)==widget:
kt.removeTab(index)
def setOpenFile(self, cb):
self._openFileCb = cb
def openFile(self, fileName:str, line:int=0, pos:int=0):
if self._ttkode and self._openFileCb:
return self._openFileCb(fileName, line, pos)
return self._openFileCb(fileName, line, pos)
ttkodeProxy:TTKodeProxy = TTKodeProxy()
ttkodeProxy:TTKodeProxy = TTKodeProxy(ttkode=TTKode())
Loading…
Cancel
Save