Browse Source

chore(crossTools): improve typing (#571)

pull/573/head
Pier CeccoPierangioliEugenio 3 months ago committed by GitHub
parent
commit
ebd13f94c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 48
      apps/dumbPaintTool/dumbPaintTool/app/maintemplate.py
  2. 4
      libs/pyTermTk/TermTk/TTkCore/constant.py
  3. 416
      libs/pyTermTk/TermTk/TTkCrossTools/savetools.py
  4. 6
      libs/pyTermTk/TermTk/TTkWidgets/TTkPickers/messagebox.py
  5. 16
      tools/webExporter/js/ttkproxy.js

48
apps/dumbPaintTool/dumbPaintTool/app/maintemplate.py

@ -127,8 +127,8 @@ class PaintTemplate(ttk.TTkAppTemplate):
glbls.clearSnapshot() glbls.clearSnapshot()
glbls.saveSnapshot() glbls.saveSnapshot()
ttk.ttkConnectDragOpen(ttk.TTkEncoding.APPLICATION_JSON, self._openDragData) ttk.TTkCrossTools.ttkConnectDragOpen(ttk.TTkCrossTools.Encoding.APPLICATION_JSON, self._openDragData)
ttk.ttkConnectDragOpen(ttk.TTkEncoding.IMAGE, self._openImageData) ttk.TTkCrossTools.ttkConnectDragOpen(ttk.TTkCrossTools.Encoding.IMAGE, self._openImageData)
ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.Key_Z).activated.connect(glbls.undo) ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.Key_Z).activated.connect(glbls.undo)
ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.Key_Y).activated.connect(glbls.redo) ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.Key_Y).activated.connect(glbls.redo)
@ -147,56 +147,68 @@ class PaintTemplate(ttk.TTkAppTemplate):
@ttk.pyTTkSlot() @ttk.pyTTkSlot()
def _open(self): def _open(self):
ttk.ttkCrossOpen( ttk.TTkCrossTools.open(
path='.', path='.',
encoding=ttk.TTkEncoding.APPLICATION_JSON, encoding=ttk.TTkCrossTools.Encoding.APPLICATION_JSON,
filter="DumbPaintTool Files (*.DPT.json);;Json Files (*.json);;All Files (*)", filter="DumbPaintTool Files (*.DPT.json);;Json Files (*.json);;All Files (*)",
cb=self._openDragData) cb=self._openDragData)
@ttk.pyTTkSlot() @ttk.pyTTkSlot()
def _openImage(self): def _openImage(self):
ttk.ttkCrossOpen( ttk.TTkCrossTools.open(
path='.', path='.',
encoding=ttk.TTkEncoding.IMAGE, encoding=ttk.TTkCrossTools.Encoding.IMAGE,
filter="Images (*.png *.jpg *.gif *.ico);;All Files (*)", filter="Images (*.png *.jpg *.gif *.ico);;All Files (*)",
cb=self._openImageData) cb=self._openImageData)
@ttk.pyTTkSlot() @ttk.pyTTkSlot()
def _save(self): def _save(self):
doc = self._parea.exportDocument() doc = self._parea.exportDocument()
ttk.ttkCrossSave(glbls.fileName(), json.dumps(doc, indent=1), ttk.TTkEncoding.APPLICATION_JSON) ttk.TTkCrossTools.save(
filePath=glbls.fileName(),
content=json.dumps(doc, indent=1),
encoding=ttk.TTkCrossTools.Encoding.APPLICATION_JSON)
@ttk.pyTTkSlot() @ttk.pyTTkSlot()
def _saveAs(self): def _saveAs(self):
doc = self._parea.exportDocument() doc = self._parea.exportDocument()
ttk.ttkCrossSaveAs(glbls.fileName(), json.dumps(doc, indent=1), ttk.TTkEncoding.APPLICATION_JSON, ttk.TTkCrossTools.saveAs(
filePath=glbls.fileName(),
content=json.dumps(doc, indent=1),
encoding=ttk.TTkCrossTools.Encoding.APPLICATION_JSON,
filter="DumbPaintTool Files (*.DPT.json);;Json Files (*.json);;All Files (*)", filter="DumbPaintTool Files (*.DPT.json);;Json Files (*.json);;All Files (*)",
cb=lambda _d:glbls.setFilename(_d['name'])) cb=lambda _d:glbls.setFilename(_d.name))
@ttk.pyTTkSlot() @ttk.pyTTkSlot()
def _saveAsAnsi(self): def _saveAsAnsi(self):
image = self._parea.exportImage() image = self._parea.exportImage()
text = ttk.TTkString(image) text = ttk.TTkString(image)
ttk.ttkCrossSaveAs(glbls.fileName(), text.toAnsi(), ttk.TTkEncoding.TEXT_PLAIN_UTF8, ttk.TTkCrossTools.saveAs(
filePath=glbls.fileName(),
content=text.toAnsi(),
encoding=ttk.TTkCrossTools.Encoding.TEXT_PLAIN_UTF8,
filter="Ansi text Files (*.Ansi.txt);;Text Files (*.txt);;All Files (*)") filter="Ansi text Files (*.Ansi.txt);;Text Files (*.txt);;All Files (*)")
@ttk.pyTTkSlot() @ttk.pyTTkSlot()
def _saveAsAscii(self): def _saveAsAscii(self):
image = self._parea.exportImage() image = self._parea.exportImage()
text = ttk.TTkString(image) text = ttk.TTkString(image)
ttk.ttkCrossSaveAs('untitled.DPT.ASCII.txt', text.toAscii(), ttk.TTkEncoding.TEXT_PLAIN_UTF8, ttk.TTkCrossTools.saveAs(
filePath='untitled.DPT.ASCII.txt',
content=text.toAscii(),
encoding=ttk.TTkCrossTools.Encoding.TEXT_PLAIN_UTF8,
filter="ASCII Text Files (*.ASCII.txt);;Text Files (*.txt);;All Files (*)") filter="ASCII Text Files (*.ASCII.txt);;Text Files (*.txt);;All Files (*)")
@ttk.pyTTkSlot(dict) @ttk.pyTTkSlot(ttk.TTkCrossTools.CB_Data_Open)
def _openImageData(self, data): def _openImageData(self, data:ttk.TTkCrossTools.CB_Data_Open):
newWindow = ImportImage(data['data']) newWindow = ImportImage(data.data)
newWindow.exportedImage.connect(self._parea.pasteEvent) newWindow.exportedImage.connect(self._parea.pasteEvent)
ttk.TTkHelper.overlay(None, newWindow, 10, 4, modal=True) ttk.TTkHelper.overlay(None, newWindow, 10, 4, modal=True)
@ttk.pyTTkSlot(dict) @ttk.pyTTkSlot(ttk.TTkCrossTools.CB_Data_Open)
def _openDragData(self, data): def _openDragData(self, data:ttk.TTkCrossTools.CB_Data_Open):
dd = json.loads(data['data']) dd = json.loads(data.data)
glbls.setFilename(data['name']) glbls.setFilename(data.name)
if 'layers' in dd: if 'layers' in dd:
self.importDocument(dd) self.importDocument(dd)
else: else:

4
libs/pyTermTk/TermTk/TTkCore/constant.py

@ -471,7 +471,7 @@ class TTkConstant:
CENTER_ALIGN = Alignment.CENTER_ALIGN CENTER_ALIGN = Alignment.CENTER_ALIGN
JUSTIFY = Alignment.JUSTIFY JUSTIFY = Alignment.JUSTIFY
class FileMode(int): class FileMode(IntEnum):
'''FileMode '''FileMode
.. autosummary:: .. autosummary::
@ -492,7 +492,7 @@ class TTkConstant:
# Directory = FileMode.Directory # Directory = FileMode.Directory
# ExistingFiles = FileMode.ExistingFiles # ExistingFiles = FileMode.ExistingFiles
class AcceptMode(int): class AcceptMode(IntEnum):
'''AcceptMode '''AcceptMode
.. autosummary:: .. autosummary::

416
libs/pyTermTk/TermTk/TTkCrossTools/savetools.py

@ -20,34 +20,133 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
__all__ = [ '''savetools.py
'ttkCrossOpen',
'ttkCrossSave', 'ttkCrossSaveAs', Cross-platform file operations and clipboard management for pyTermTk.
'TTkEncoding', 'ImageData',
'ttkConnectDragOpen', This module provides a unified interface for file I/O operations that work seamlessly
'ttkEmitDragOpen', 'ttkEmitFileOpen'] across different platforms, including native desktop environments and web-based
Pyodide/WASM deployments. It automatically detects the runtime environment and
adapts its behavior accordingly.
Key Features:
- **Cross-platform file operations**: Open, save, and saveAs methods that work
identically on desktop and web platforms
- **Multiple encoding support**: Text, JSON, and image formats with automatic
decoding and encoding
- **Drag and drop integration**: Native drag-drop support in web environments
- **File dialog integration**: Automatic file picker dialogs on desktop with
JavaScript-based file selection in browsers
- **Image handling**: PIL/Pillow integration for image file operations
- **Type-safe callbacks**: Protocol-based type hints for callback functions
Platform Detection:
The module automatically detects whether it's running in a Pyodide/WASM environment
by checking for the 'pyodideProxy' module. Based on this detection, it provides
platform-specific implementations while maintaining a consistent API.
- **Desktop mode**: Uses native file dialogs (:py:class:`TTkFileDialogPicker`) and
standard Python file I/O
- **Web mode**: Uses JavaScript interop via pyodideProxy for browser-based file
selection and download
Usage Example:
.. code-block:: python
from TermTk.TTkCrossTools import TTkCrossTools
# Open a text file
def handle_open(data: TTkCrossTools.CB_Data_Open):
print(f"Opened: {data.name}")
print(f"Content: {data.data}")
TTkCrossTools.open(
path=".",
encoding=TTkCrossTools.Encoding.TEXT_PLAIN,
filter="Text Files (*.txt)",
cb=handle_open
)
# Save a file
TTkCrossTools.save(
filePath="output.txt",
content="Hello, World!",
encoding=TTkCrossTools.Encoding.TEXT_PLAIN
)
# Save with dialog
def handle_save(data: TTkCrossTools.CB_Data_Save):
print(f"Saved to: {data.name}")
TTkCrossTools.saveAs(
filePath="output.json",
content='{"key": "value"}',
encoding=TTkCrossTools.Encoding.APPLICATION_JSON,
filter="JSON Files (*.json)",
cb=handle_save
)
Supported Encodings:
See :py:class:`TTkCrossTools.Encoding` for the full list of supported MIME types and
encoding formats.
Callback Data Structures:
- :py:class:`_CB_Data_Open`: Contains file name and decoded content
- :py:class:`_CB_Data_Save`: Contains the saved file name
See Also:
- :py:class:`TTkCrossTools`: Main API class
- :py:class:`TTkCrossTools.Encoding`: Encoding type definitions
- :py:class:`TTkFileDialogPicker`: Native file dialog widget
'''
from __future__ import annotations
__all__ = ['TTkCrossTools', '_TTkEncoding']
import os import os
import importlib.util import importlib.util
import json from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable,Optional,List,Tuple,Dict,Any,Protocol,Type
try:
from typing import TypeAlias
except ImportError:
# TODO: Remove this workaround for Python 3.9
TypeAlias = type # Fallback for Python < 3.10 without typing_extensions
from TermTk import pyTTkSlot, pyTTkSignal from TermTk import pyTTkSlot, pyTTkSignal
from TermTk import TTkLog from TermTk import TTkLog
from TermTk import TTkMessageBox, TTkFileDialogPicker, TTkHelper, TTkString, TTkK, TTkColor from TermTk import TTkMessageBox, TTkFileDialogPicker, TTkHelper, TTkString, TTkK, TTkColor
class ImageData: class _TTkEncoding(str, Enum):
size:list[int,int] = (0,0) ''' Encoding types for cross-platform file operations.
data:list[list[list[int,int,int,int]]] = []
ttkCrossOpen = None Defines MIME types and encoding identifiers used by :py:class:`TTkCrossTools`
ttkCrossSave = None for file operations. These encodings determine how file content is decoded
ttkCrossSaveAs = None when opening and encoded when saving.
ttkEmitDragOpen = None
ttkEmitFileOpen = None
ttkConnectDragOpen = None
class TTkEncoding(str): Text Encodings:
- **TEXT**: Generic text encoding
- **TEXT_PLAIN**: Plain text MIME type
- **TEXT_PLAIN_UTF8**: UTF-8 encoded plain text
Application Encodings:
- **APPLICATION**: Generic application data
- **APPLICATION_JSON**: JSON format with automatic parsing
Image Encodings:
- **IMAGE**: Generic image format
- **IMAGE_PNG**: PNG image format
- **IMAGE_SVG**: SVG vector image format
- **IMAGE_JPG**: JPEG image format
Note:
Image encodings require PIL/Pillow to be installed. When opening image
files, the decoded data will be a PIL Image object.
'''
TEXT = "text" TEXT = "text"
TEXT_PLAIN = "text/plain" TEXT_PLAIN = "text/plain"
TEXT_PLAIN_UTF8 = "text/plain;charset=utf-8" TEXT_PLAIN_UTF8 = "text/plain;charset=utf-8"
@ -58,112 +157,187 @@ class TTkEncoding(str):
IMAGE_SVG = 'image/svg+xml' IMAGE_SVG = 'image/svg+xml'
IMAGE_JPG = 'image/jpeg' IMAGE_JPG = 'image/jpeg'
class _OpenProtocol(Protocol):
def __call__(
self,
path: str,
encoding: _TTkEncoding,
filter: str,
cb: Optional[TTkCrossTools.TTkCross_Callback_Open] = None
) -> None: ...
class _SaveProtocol(Protocol):
def __call__(
self,
filePath: str,
content: str,
encoding: _TTkEncoding
) -> None: ...
class _SaveAsProtocol(Protocol):
def __call__(
self,
filePath: str,
content: str,
encoding: _TTkEncoding,
filter: str,
cb: Optional[TTkCrossTools.TTkCross_Callback_Save] = None
) -> None: ...
class _EmitDragOpenProtocol(Protocol):
def __call__(
self,
encoding: _TTkEncoding,
data: _CB_Data_Open
) -> None: ...
class _EmitFileOpenProtocol(Protocol):
def __call__(
self,
encoding: _TTkEncoding,
data: _CB_Data_Open
) -> None: ...
class _ConnectDragOpenProtocol(Protocol):
def __call__(
self,
encoding: _TTkEncoding,
cb: TTkCrossTools.TTkCross_Callback_Open
) -> None: ...
@dataclass
class _CB_Data_Save():
''' Callback data for save operations.
:param name: The full path or name of the saved file
:type name: str
'''
name:str
@dataclass
class _CB_Data_Open():
''' Callback data for open/load operations.
:param name: The full path or name of the opened file
:type name: str
:param data: The decoded file content. Type depends on the encoding:
- Text encodings: str
- JSON encodings: str (raw JSON content)
- Image encodings: PIL.Image.Image object
:type data: Any
'''
name:str
data:Any
if importlib.util.find_spec('pyodideProxy'): if importlib.util.find_spec('pyodideProxy'):
TTkLog.info("Using 'pyodideProxy' as clipboard manager") TTkLog.info("Using 'pyodideProxy' as clipboard manager")
import pyodideProxy import pyodideProxy # type: ignore[import-not-found]
ttkDragOpen = {} ttkDragOpen:Dict[_TTkEncoding,pyTTkSignal] = {}
ttkFileOpen = pyTTkSignal(dict) ttkFileOpen = pyTTkSignal(_CB_Data_Open)
def _open(path, encoding, filter, cb=None): def _open(path:str, encoding:_TTkEncoding, filter:str, cb: Optional[TTkCrossTools.TTkCross_Callback_Open] = None) -> None:
if not cb: return if not cb: return
ttkFileOpen.connect(cb) ttkFileOpen.connect(cb)
pyodideProxy.openFile(encoding) pyodideProxy.openFile(encoding)
def _save(filePath, content, encoding, filter=None, cb=lambda _d:None): def _save(filePath: str, content: str, encoding: _TTkEncoding) -> None:
pyodideProxy.saveFile(os.path.basename(filePath), content, encoding) pyodideProxy.saveFile(os.path.basename(filePath), content, encoding)
def _connectDragOpen(encoding, cb): def _saveAs(filePath:str, content:str, encoding:_TTkEncoding, filter:str, cb: Optional[TTkCrossTools.TTkCross_Callback_Save] = None) -> None:
if encoding not in ttkDragOpen: return _save(
ttkDragOpen[encoding] = pyTTkSignal(dict) filePath=filePath,
return ttkDragOpen[encoding].connect(cb) content=content,
encoding=encoding
)
def _emitDragOpen(encoding, data): def _emitDragOpen(encoding: _TTkEncoding, data: _CB_Data_Open) -> None:
if encoding.startswith(TTkEncoding.IMAGE): if encoding.startswith(_TTkEncoding.IMAGE):
from PIL import Image from PIL import Image
import io import io
im = Image.open(io.BytesIO(data['data'])) im = Image.open(io.BytesIO(data.data))
data['data'] = im data.data = im
for do in [ttkDragOpen[e] for e in ttkDragOpen if encoding.startswith(e)]: for _drag_open in [ttkDragOpen[e] for e in ttkDragOpen if encoding.startswith(e)]:
do.emit(data) _drag_open.emit(data)
def _emitFileOpen(encoding, data): def _emitFileOpen(encoding: _TTkEncoding, data: _CB_Data_Open) -> None:
if encoding.startswith(TTkEncoding.IMAGE): if encoding.startswith(_TTkEncoding.IMAGE):
from PIL import Image from PIL import Image
import io import io
# TTkLog.debug(data['data']) # TTkLog.debug(data['data'])
# TTkLog.debug(type(data['data'])) # TTkLog.debug(type(data['data']))
# Image.open(data['data']) # Image.open(data['data'])
im = Image.open(io.BytesIO(data['data'])) im = Image.open(io.BytesIO(data.data))
# TTkLog.debug(f"{im.size}") # TTkLog.debug(f"{im.size}")
data['data'] = im data.data = im
ttkFileOpen.emit(data) ttkFileOpen.emit(data)
ttkFileOpen.clear() ttkFileOpen.clear()
ttkCrossOpen = _open def _connectDragOpen(encoding: _TTkEncoding, cb: TTkCrossTools.TTkCross_Callback_Open) -> None:
ttkCrossSave = _save if encoding not in ttkDragOpen:
ttkCrossSaveAs = _save ttkDragOpen[encoding] = pyTTkSignal(_CB_Data_Open)
ttkEmitDragOpen = _emitDragOpen return ttkDragOpen[encoding].connect(cb)
ttkEmitFileOpen = _emitFileOpen
ttkConnectDragOpen = _connectDragOpen
else: else:
def _crossDecoder_text(fileName) : def _crossDecoder_text(fileName: str) -> str:
with open(fileName) as fp: with open(fileName) as fp:
return fp.read() return fp.read()
def _crossDecoder_json(fileName) :
def _crossDecoder_json(fileName: str) -> str:
with open(fileName) as fp: with open(fileName) as fp:
# return json.load(fp) # return json.load(fp)
return fp.read() return fp.read()
def _crossDecoder_image(fileName):
def _crossDecoder_image(fileName: str) -> Any:
from PIL import Image from PIL import Image
pilImage = Image.open(fileName) pilImage = Image.open(fileName)
# pilImage = pilImage.convert('RGBA') # pilImage = pilImage.convert('RGBA')
# pilData = list(pilImage.getdata()) # pilData = List(pilImage.getdata())
# data = ImageData() # data = ImageData()
# w,h = data.size = pilImage.size # w,h = data.size = pilImage.size
# data.data = [pilData[i:i+w] for i in range(0, len(pilData), w)] # data.data = [pilData[i:i+w] for i in range(0, len(pilData), w)]
return pilImage return pilImage
_crossDecoder = { _crossDecoder = {
TTkEncoding.TEXT : _crossDecoder_text , _TTkEncoding.TEXT : _crossDecoder_text ,
TTkEncoding.TEXT_PLAIN : _crossDecoder_text , _TTkEncoding.TEXT_PLAIN : _crossDecoder_text ,
TTkEncoding.TEXT_PLAIN_UTF8 : _crossDecoder_text , _TTkEncoding.TEXT_PLAIN_UTF8 : _crossDecoder_text ,
TTkEncoding.APPLICATION : _crossDecoder_json , _TTkEncoding.APPLICATION : _crossDecoder_json ,
TTkEncoding.APPLICATION_JSON : _crossDecoder_json , _TTkEncoding.APPLICATION_JSON : _crossDecoder_json ,
TTkEncoding.IMAGE : _crossDecoder_image , _TTkEncoding.IMAGE : _crossDecoder_image ,
TTkEncoding.IMAGE_PNG : _crossDecoder_image , _TTkEncoding.IMAGE_PNG : _crossDecoder_image ,
TTkEncoding.IMAGE_SVG : _crossDecoder_image , _TTkEncoding.IMAGE_SVG : _crossDecoder_image ,
TTkEncoding.IMAGE_JPG : _crossDecoder_image , _TTkEncoding.IMAGE_JPG : _crossDecoder_image ,
} }
def _open(path, encoding:TTkEncoding, filter:str, cb=None): def _open(path:str, encoding:_TTkEncoding, filter:str, cb: Optional[TTkCrossTools.TTkCross_Callback_Open] = None) -> None:
if not cb: return if not cb: return
if encoding.startswith(TTkEncoding.IMAGE): if encoding.startswith(_TTkEncoding.IMAGE):
if not importlib.util.find_spec('PIL'): return if not importlib.util.find_spec('PIL'): return
def __openFile(fileName): def __openFile(fileName: str) -> None:
_decoder = _crossDecoder.get(encoding,lambda _:None) _decoder = _crossDecoder.get(encoding,lambda _:None)
content = _decoder(fileName) content = _decoder(fileName)
cb({'name':fileName, 'data':content}) cb(TTkCrossTools.CB_Data_Open(name=fileName, data=content))
filePicker = TTkFileDialogPicker(pos = (3,3), size=(100,30), caption="Open", path=path, fileMode=TTkK.FileMode.ExistingFile ,filter=filter) filePicker = TTkFileDialogPicker(pos = (3,3), size=(100,30), caption="Open", path=path, fileMode=TTkK.FileMode.ExistingFile ,filter=filter)
filePicker.pathPicked.connect(__openFile) filePicker.pathPicked.connect(__openFile)
TTkHelper.overlay(None, filePicker, 5, 5, True) TTkHelper.overlay(None, filePicker, 5, 5, True)
def _save(filePath, content, encoding): def _save(filePath:str, content:str, encoding:_TTkEncoding) -> None:
TTkLog.info(f"Saving to: {filePath}") TTkLog.info(f"Saving to: {filePath}")
with open(filePath,'w') as fp: with open(filePath,'w') as fp:
fp.write(content) fp.write(content)
def _saveAs(filePath, content, encoding, filter, cb=lambda _d:None): def _saveAs(filePath:str, content:str, encoding:_TTkEncoding, filter:str, cb: Optional[TTkCrossTools.TTkCross_Callback_Save] = None) -> None:
def _approveFile(fileName): def _approveFile(fileName: str) -> None:
if os.path.exists(fileName): if os.path.exists(fileName):
@pyTTkSlot(TTkMessageBox.StandardButton) @pyTTkSlot(TTkMessageBox.StandardButton)
def _cb(btn): def _cb(btn):
if btn == TTkMessageBox.StandardButton.Save: if btn == TTkMessageBox.StandardButton.Save:
ttkCrossSave(fileName,content,encoding) _save(fileName,content,encoding)
elif btn == TTkMessageBox.StandardButton.Cancel: elif btn == TTkMessageBox.StandardButton.Cancel:
return return
if cb: if cb:
cb({'name':fileName}) cb(TTkCrossTools.CB_Data_Save(name=fileName))
messageBox = TTkMessageBox( messageBox = TTkMessageBox(
text= ( text= (
TTkString( f'A file named "{os.path.basename(fileName)}" already exists.\nDo you want to replace it?', TTkColor.BOLD) + TTkString( f'A file named "{os.path.basename(fileName)}" already exists.\nDo you want to replace it?', TTkColor.BOLD) +
@ -173,9 +347,9 @@ else:
messageBox.buttonSelected.connect(_cb) messageBox.buttonSelected.connect(_cb)
TTkHelper.overlay(None, messageBox, 5, 5, True) TTkHelper.overlay(None, messageBox, 5, 5, True)
else: else:
ttkCrossSave(fileName,content,encoding) _save(fileName,content,encoding)
if cb: if cb:
cb({'name':fileName}) cb(TTkCrossTools.CB_Data_Save(name=fileName))
filePicker = TTkFileDialogPicker( filePicker = TTkFileDialogPicker(
size=(100,30), path=filePath, size=(100,30), path=filePath,
acceptMode=TTkK.AcceptMode.AcceptSave, acceptMode=TTkK.AcceptMode.AcceptSave,
@ -185,9 +359,107 @@ else:
filePicker.pathPicked.connect(_approveFile) filePicker.pathPicked.connect(_approveFile)
TTkHelper.overlay(None, filePicker, 5, 5, True) TTkHelper.overlay(None, filePicker, 5, 5, True)
ttkCrossOpen = _open def _emitDragOpen(encoding: _TTkEncoding, data: _CB_Data_Open) -> None:
ttkCrossSave = _save pass
ttkCrossSaveAs = _saveAs
ttkEmitDragOpen = lambda a:None def _emitFileOpen(encoding: _TTkEncoding, data: _CB_Data_Open) -> None:
ttkEmitFileOpen = lambda a:None pass
ttkConnectDragOpen = lambda a,b:None
def _connectDragOpen(encoding: _TTkEncoding, cb: TTkCrossTools.TTkCross_Callback_Open) -> None:
pass
class TTkCrossTools():
''' Cross-platform file operations and clipboard management.
Provides a unified API for file I/O operations across desktop and web platforms.
Automatically adapts to the runtime environment (native Python or Pyodide/WASM)
and provides appropriate file dialogs and file handling mechanisms.
Class Attributes:
Encoding: Alias for :py:class:`_TTkEncoding` enum
CB_Data_Save: Type alias for save callback data
CB_Data_Open: Type alias for open callback data
TTkCross_Callback: Generic callback type
TTkCross_Callback_Open: Callback type for open operations
TTkCross_Callback_Save: Callback type for save operations
Methods:
open: Open a file with automatic dialog and decoding
save: Save content to a file
saveAs: Save with file picker dialog
ttkEmitDragOpen: Emit drag-drop open events (web only)
ttkEmitFileOpen: Emit file open events (web only)
ttkConnectDragOpen: Connect to drag-drop events (web only)
Platform Behavior:
**Desktop Mode**:
- Uses :py:class:`TTkFileDialogPicker` for file selection
- Direct filesystem access via Python's built-in file operations
- Automatic confirmation dialogs for overwrite operations
**Web Mode (Pyodide)**:
- Uses JavaScript file input elements via pyodideProxy
- Downloads trigger browser's save dialog
- Drag-drop integration for file loading
Usage Example:
.. code-block:: python
# Open a text file
def on_open(data: TTkCrossTools.CB_Data_Open):
print(f"File: {data.name}")
print(f"Content: {data.data}")
TTkCrossTools.open(
path="/home/user",
encoding=TTkCrossTools.Encoding.TEXT_PLAIN,
filter="Text Files (*.txt);;All Files (*)",
cb=on_open
)
# Save with confirmation
def on_save(data: TTkCrossTools.CB_Data_Save):
print(f"Saved to: {data.name}")
TTkCrossTools.saveAs(
filePath="document.txt",
content="Hello, World!",
encoding=TTkCrossTools.Encoding.TEXT_PLAIN,
filter="Text Files (*.txt)",
cb=on_save
)
# Open an image
def on_image(data: TTkCrossTools.CB_Data_Open):
pil_image = data.data # PIL.Image.Image object
print(f"Image size: {pil_image.size}")
TTkCrossTools.open(
path=".",
encoding=TTkCrossTools.Encoding.IMAGE_PNG,
filter="PNG Images (*.png)",
cb=on_image
)
Note:
Image operations require PIL/Pillow to be installed. The library will
gracefully handle missing dependencies by skipping image operations.
'''
Encoding = _TTkEncoding
CB_Data_Save: TypeAlias = _CB_Data_Save
CB_Data_Open: TypeAlias = _CB_Data_Open
# Type alias for callback functions that receive file data
TTkCross_Callback: TypeAlias = Callable[[Dict[str, Any]], None]
TTkCross_Callback_Open: TypeAlias = Callable[[CB_Data_Open], None]
TTkCross_Callback_Save: TypeAlias = Callable[[CB_Data_Save], None]
open: _OpenProtocol = _open
save: _SaveProtocol = _save
saveAs: _SaveAsProtocol = _saveAs
ttkEmitDragOpen: _EmitDragOpenProtocol = _emitDragOpen
ttkEmitFileOpen: _EmitFileOpenProtocol = _emitFileOpen
ttkConnectDragOpen: _ConnectDragOpenProtocol = _connectDragOpen

6
libs/pyTermTk/TermTk/TTkWidgets/TTkPickers/messagebox.py

@ -22,6 +22,8 @@
__all__ = ['TTkMessageBox'] __all__ = ['TTkMessageBox']
from enum import IntEnum, Flag
from TermTk.TTkCore.cfg import TTkCfg from TermTk.TTkCore.cfg import TTkCfg
from TermTk.TTkCore.signal import pyTTkSignal,pyTTkSlot from TermTk.TTkCore.signal import pyTTkSignal,pyTTkSlot
from TermTk.TTkCore.color import TTkColor from TermTk.TTkCore.color import TTkColor
@ -37,7 +39,7 @@ from TermTk.TTkWidgets.label import TTkLabel
from TermTk.TTkWidgets.button import TTkButton from TermTk.TTkWidgets.button import TTkButton
class TTkMessageBox(TTkWindow): class TTkMessageBox(TTkWindow):
class Icon(int): class Icon(IntEnum):
NoIcon = 0 NoIcon = 0
'''the message box does not have any icon.''' '''the message box does not have any icon.'''
Question = 4 Question = 4
@ -49,7 +51,7 @@ class TTkMessageBox(TTkWindow):
Critical = 3 Critical = 3
'''an icon indicating that the message represents a critical problem.''' '''an icon indicating that the message represents a critical problem.'''
class StandardButton(int): class StandardButton(Flag):
Ok = 0x00000400 Ok = 0x00000400
'''An "OK" button defined with the AcceptRole.''' '''An "OK" button defined with the AcceptRole.'''
Open = 0x00002000 Open = 0x00002000

16
tools/webExporter/js/ttkproxy.js

@ -228,12 +228,24 @@ class TTkProxy {
def ttk_dragOpen(data): def ttk_dragOpen(data):
data = data.to_py() data = data.to_py()
ttk.ttkEmitDragOpen(data['type'],data) ttk.TTkCrossTools.ttkEmitDragOpen(
data['type'],
ttk.TTkCrossTools.CB_Data_Open(
name=data['name'],
data=data['data'],
)
)
# ttk_log(f"{type(data.to_py())=}, {str(data.to_py())}") # ttk_log(f"{type(data.to_py())=}, {str(data.to_py())}")
def ttk_fileOpen(data): def ttk_fileOpen(data):
data = data.to_py() data = data.to_py()
ttk.ttkEmitFileOpen(data['type'],data) ttk.TTkCrossTools.ttkEmitFileOpen(
data['type'],
ttk.TTkCrossTools.CB_Data_Open(
name=data['name'],
data=data['data'],
)
)
# ttk_log(f"{type(data.to_py())=}, {str(data.to_py())}") # ttk_log(f"{type(data.to_py())=}, {str(data.to_py())}")
def ttk_input(val): def ttk_input(val):

Loading…
Cancel
Save