From 2b08a2254e341b03bb7728c1dc39bb8213f1f1c5 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 22 Mar 2024 15:14:33 +0000 Subject: [PATCH] Tuned the standard open/save/clipboard --- TermTk/TTkCrossTools/__init__.py | 1 + TermTk/TTkCrossTools/savetools.py | 159 +++++++++++++++++++++++++++ TermTk/TTkGui/clipboard.py | 23 +++- tools/dumb_paint_lib/maintemplate.py | 51 +++++++-- tools/dumb_paint_lib/paintarea.py | 9 +- tools/webExporter/index.html | 35 +++++- tools/webExporter/js/ttkproxy.js | 116 ++++++++++++++++--- tools/webExporterInit.sh | 28 +++-- 8 files changed, 382 insertions(+), 40 deletions(-) create mode 100644 TermTk/TTkCrossTools/__init__.py create mode 100644 TermTk/TTkCrossTools/savetools.py diff --git a/TermTk/TTkCrossTools/__init__.py b/TermTk/TTkCrossTools/__init__.py new file mode 100644 index 00000000..25c440ef --- /dev/null +++ b/TermTk/TTkCrossTools/__init__.py @@ -0,0 +1 @@ +from .savetools import * \ No newline at end of file diff --git a/TermTk/TTkCrossTools/savetools.py b/TermTk/TTkCrossTools/savetools.py new file mode 100644 index 00000000..12469577 --- /dev/null +++ b/TermTk/TTkCrossTools/savetools.py @@ -0,0 +1,159 @@ +# 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__ = ['ttkCrossOpen', 'ttkCrossSave', 'ttkCrossSaveAs', 'TTkEncoding', 'ttkConnectDragOpen', 'ttkEmitDragOpen', 'ttkEmitFileOpen'] + +import os +import importlib.util +import json + +from TermTk import pyTTkSlot, pyTTkSignal +from TermTk import TTkLog +from TermTk import TTkMessageBox, TTkFileDialogPicker, TTkHelper, TTkString, TTkK, TTkColor + +ttkCrossOpen = None +ttkCrossSave = None +ttkCrossSaveAs = None +ttkEmitDragOpen = None +ttkEmitFileOpen = None +ttkConnectDragOpen = None + +class TTkEncoding(str): + TEXT = "text" + TEXT_PLAIN = "text/plain" + TEXT_PLAIN_UTF8 = "text/plain;charset=utf-8" + APPLICATION = 'application' + APPLICATION_JSON = 'application/json' + IMAGE = 'image' + IMAGE_PNG = 'image/png' + IMAGE_SVG = 'image/svg+xml' + IMAGE_JPG = 'image/jpeg' + +if importlib.util.find_spec('pyodideProxy'): + TTkLog.info("Using 'pyodideProxy' as clipboard manager") + import pyodideProxy + ttkDragOpen = {} + ttkFileOpen = pyTTkSignal(dict) + + def _open(path, encoding, filter, cb=None): + if not cb: return + ttkFileOpen.connect(cb) + pyodideProxy.openFile(encoding) + + def _save(filePath, content, encoding, filter=None): + pyodideProxy.saveFile(os.path.basename(filePath), content, encoding) + + def _connectDragOpen(encoding, cb): + if not encoding in ttkDragOpen: + ttkDragOpen[encoding] = pyTTkSignal(dict) + return ttkDragOpen[encoding].connect(cb) + + def _emitDragOpen(encoding, data): + for do in [ttkDragOpen[e] for e in ttkDragOpen if encoding.startswith(e)]: + do.emit(data) + + def _emitFileOpen(encoding, data): + ttkFileOpen.emit(data) + ttkFileOpen.clear() + + ttkCrossOpen = _open + ttkCrossSave = _save + ttkCrossSaveAs = _save + ttkEmitDragOpen = _emitDragOpen + ttkEmitFileOpen = _emitFileOpen + ttkConnectDragOpen = _connectDragOpen + +else: + def _crossDecoder_text(fileName) : + with open(fileName) as fp: + return fp.read() + def _crossDecoder_json(fileName) : + with open(fileName) as fp: + # return json.load(fp) + return fp.read() + def _crossDecoder_image(fileName): + return None + + _crossDecoder = { + TTkEncoding.TEXT : _crossDecoder_text , + TTkEncoding.TEXT_PLAIN : _crossDecoder_text , + TTkEncoding.TEXT_PLAIN_UTF8 : _crossDecoder_text , + TTkEncoding.APPLICATION : _crossDecoder_json , + TTkEncoding.APPLICATION_JSON : _crossDecoder_json , + TTkEncoding.IMAGE : _crossDecoder_image , + TTkEncoding.IMAGE_PNG : _crossDecoder_image , + TTkEncoding.IMAGE_SVG : _crossDecoder_image , + TTkEncoding.IMAGE_JPG : _crossDecoder_image , + } + + def _open(path, encoding, filter, cb=None): + if not cb: return + def __openFile(fileName): + _decoder = _crossDecoder.get(encoding,lambda _:None) + content = _decoder(fileName) + cb({'name':fileName, 'data':content}) + filePicker = TTkFileDialogPicker(pos = (3,3), size=(100,30), caption="Open", path=path, fileMode=TTkK.FileMode.ExistingFile ,filter=filter) + filePicker.pathPicked.connect(__openFile) + TTkHelper.overlay(None, filePicker, 5, 5, True) + + def _save(filePath, content, encoding): + TTkLog.info(f"Saving to: {filePath}") + with open(filePath,'w') as fp: + fp.write(content) + + def _saveAs(filePath, content, encoding, filter, cb=None): + if not cb: return + def _approveFile(fileName): + if os.path.exists(fileName): + @pyTTkSlot(TTkMessageBox.StandardButton) + def _cb(btn): + if btn == TTkMessageBox.StandardButton.Save: + ttkCrossSave(fileName,content,encoding) + elif btn == TTkMessageBox.StandardButton.Cancel: + return + if cb: + cb() + messageBox = TTkMessageBox( + text= ( + TTkString( f'A file named "{os.path.basename(fileName)}" already exists.\nDo you want to replace it?', TTkColor.BOLD) + + TTkString( f'\n\nReplacing it will overwrite its contents.') ), + icon=TTkMessageBox.Icon.Warning, + standardButtons=TTkMessageBox.StandardButton.Discard|TTkMessageBox.StandardButton.Save|TTkMessageBox.StandardButton.Cancel) + messageBox.buttonSelected.connect(_cb) + TTkHelper.overlay(None, messageBox, 5, 5, True) + else: + ttkCrossSave(fileName,content,encoding) + filePicker = TTkFileDialogPicker( + size=(100,30), path=filePath, + acceptMode=TTkK.AcceptMode.AcceptSave, + caption="Save As...", + fileMode=TTkK.FileMode.AnyFile , + filter=filter) + filePicker.pathPicked.connect(_approveFile) + TTkHelper.overlay(None, filePicker, 5, 5, True) + + ttkCrossOpen = _open + ttkCrossSave = _save + ttkCrossSaveAs = _saveAs + ttkEmitDragOpen = lambda a:None + ttkEmitFileOpen = lambda a:None + ttkConnectDragOpen = lambda a,b:None diff --git a/TermTk/TTkGui/clipboard.py b/TermTk/TTkGui/clipboard.py index 07e5b022..da626127 100644 --- a/TermTk/TTkGui/clipboard.py +++ b/TermTk/TTkGui/clipboard.py @@ -61,10 +61,21 @@ class TTkClipboard(): try: if importlib.util.find_spec('pyodideProxy'): TTkLog.info("Using 'pyodideProxy' as clipboard manager") - import pyodideProxy as _c - TTkClipboard._manager = _c - TTkClipboard._setText = _c.copy - TTkClipboard._text = _c.paste + import pyodideProxy + import asyncio + async def _async_co(): + text = await pyodideProxy.paste() + TTkLog.debug(f"ttkProxy paste_co: {text}") + return text + def _paste(): + loop = asyncio.get_event_loop() + text = loop.run_until_complete(_async_co()) + # text = loop.run_until_complete(pyodideProxy.paste()) + TTkLog.debug(f"ttkProxy paste: {text=} {_async_co()=}") + return text + TTkClipboard._manager = pyodideProxy + TTkClipboard._setText = pyodideProxy.copy + TTkClipboard._text = pyodideProxy.paste # _paste elif importlib.util.find_spec('copykitten'): TTkLog.info("Using 'copykitten' as clipboard manager") import copykitten as _c @@ -113,13 +124,13 @@ class TTkClipboard(): except Exception as e: TTkLog.error("Clipboard error, try to export X11 if you are running this UI via SSH") for line in str(e).split("\n"): - TTkLog.error(line) + TTkLog.error(str(line)) @staticmethod def text(): '''text''' if TTkClipboard._text: - txt = "" + txt = None try: txt = TTkClipboard._text() except Exception as e: diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index 0ec7d59f..ea2c4a35 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -264,7 +264,7 @@ class PaintTemplate(ttk.TTkAppTemplate): self.setMenuBar(appMenuBar:=ttk.TTkMenuBarLayout(), self.TOP) fileMenu = appMenuBar.addMenu("&File") - buttonOpen = fileMenu.addMenu("&Open") + fileMenu.addMenu("&Open" ).menuButtonClicked.connect(self._open) fileMenu.addMenu("&Save" ).menuButtonClicked.connect(self._save) fileMenu.addMenu("Save &As...").menuButtonClicked.connect(self._saveAs) fileMenu.addSpacer() @@ -277,8 +277,8 @@ class PaintTemplate(ttk.TTkAppTemplate): buttonExit = fileMenu.addMenu("E&xit") buttonExit.menuButtonClicked.connect(ttk.TTkHelper.quit) - menuExport.addMenu("&Ascii/Txt") - menuExport.addMenu("&Ansi") + menuExport.addMenu("&Ascii/Txt").menuButtonClicked.connect(self._saveAsAscii) + menuExport.addMenu("&Ansi").menuButtonClicked.connect(self._saveAsAnsi) menuExport.addMenu("&Python") menuExport.addMenu("&Bash") @@ -315,23 +315,58 @@ class PaintTemplate(ttk.TTkAppTemplate): if fileName: self._openFile(fileName) + ttk.ttkConnectDragOpen(ttk.TTkEncoding.APPLICATION_JSON, self._openDragData) + + @ttk.pyTTkSlot() + def _open(self): + ttk.ttkCrossOpen( + path='.', + encoding=ttk.TTkEncoding.APPLICATION_JSON, + filter="DumbPaintTool Files (*.DPT.json);;Json Files (*.json);;All Files (*)", + cb=self._openDragData) + @ttk.pyTTkSlot() def _save(self): - image = self._parea.exportImage() - ttk.ttkCrossSave('untitled.DPT.txt', image, ttk.TTkEncoding.TEXT_PLAIN) + doc = self._parea.exportDocument() + ttk.ttkCrossSave('untitled.DPT.json', json.dumps(doc, indent=1), ttk.TTkEncoding.APPLICATION_JSON) @ttk.pyTTkSlot() def _saveAs(self): + doc = self._parea.exportDocument() + ttk.ttkCrossSaveAs('untitled.DPT.json', json.dumps(doc, indent=1), ttk.TTkEncoding.APPLICATION_JSON, + filter="DumbPaintTool Files (*.DPT.json);;Json Files (*.json);;All Files (*)") + + @ttk.pyTTkSlot() + def _saveAsAnsi(self): + image = self._parea.exportImage() + text = ttk.TTkString(image) + ttk.ttkCrossSaveAs('untitled.DPT.Ansi.txt', text.toAnsi(), ttk.TTkEncoding.TEXT_PLAIN_UTF8, + filter="Ansi text Files (*.Ansi.txt);;Text Files (*.txt);;All Files (*)") + + @ttk.pyTTkSlot() + def _saveAsAscii(self): image = self._parea.exportImage() - ttk.ttkCrossSaveAs('untitled.DPT.txt', image, ttk.TTkEncoding.TEXT_PLAIN) + text = ttk.TTkString(image) + ttk.ttkCrossSaveAs('untitled.DPT.ASCII.txt', text.toAscii(), ttk.TTkEncoding.TEXT_PLAIN_UTF8, + filter="ASCII Text Files (*.ASCII.txt);;Text Files (*.txt);;All Files (*)") + + @ttk.pyTTkSlot(dict) + def _openDragData(self, data): + dd = json.loads(data['data']) + if 'layers' in dd: + self.importDocument(dd) + else: + self._layers.addLayer(name="Import") + self._parea.importLayer(dd) def _openFile(self, fileName): ttk.TTkLog.info(f"Open: {fileName}") with open(fileName) as fp: # dd = json.load(fp) - text = fp.read() - dd = eval(text) + # text = fp.read() + # dd = eval(text) + dd = json.load(fp) if 'layers' in dd: self.importDocument(dd) else: diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 83a703df..5b2e689d 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -155,11 +155,15 @@ class PaintArea(ttk.TTkAbstractScrollView): def importDocument(self, dd): self._canvasLayers = [] - if 'version' in dd and dd['version']=='1.0.0': + if ( + ( 'version' in dd and dd['version'] == '1.0.0' ) or + ( 'version' in dd and dd['version'] == '1.0.1' and dd['type'] == 'DumbPaintTool/Document') ): self.resizeCanvas(*dd['size']) for l in dd['layers']: nl = self.newLayer() nl.importLayer(l) + else: + ttk.TTkLog.error("File Format not recognised") self._retuneGeometry() def exportImage(self): @@ -173,7 +177,8 @@ class PaintArea(ttk.TTkAbstractScrollView): def exportDocument(self, full=True, palette=True, crop=True) -> dict: pw,ph = self._documentSize outData = { - 'version':'1.0.0', + 'type':'DumbPaintTool/Document', + 'version':'1.0.1', 'size':(pw,ph), 'layers':[l.exportLayer(full=full,palette=palette,crop=crop) for l in self._canvasLayers]} return outData diff --git a/tools/webExporter/index.html b/tools/webExporter/index.html index 0545777f..74c03b96 100644 --- a/tools/webExporter/index.html +++ b/tools/webExporter/index.html @@ -9,16 +9,26 @@ + +
+