Browse Source

Merge pull request #244 from ceccopierangiolieugenio/DumbPaintTool

Dumb Paint Tool
pull/252/head
Pier CeccoPierangioliEugenio 2 years ago committed by GitHub
parent
commit
0094de7f87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      Makefile
  2. 59
      TermTk/TTkCore/color.py
  3. 1
      TermTk/TTkCrossTools/__init__.py
  4. 159
      TermTk/TTkCrossTools/savetools.py
  5. 23
      TermTk/TTkGui/clipboard.py
  6. 17
      TermTk/__init__.py
  7. 10
      docs/MDNotes/Fonts/Font2Glyph.md
  8. 2
      docs/MDNotes/webExporter/Javascript Open,DnD Files.md
  9. 10
      tests/sandbox/Makefile
  10. 8
      tests/sandbox/sandbox.NerdFont.html
  11. 8
      tests/sandbox/standalone.fullscreen.NerdFont.html
  12. 4
      tools/check.import.sh
  13. 35
      tools/dumb.paint.tool.py
  14. 10
      tools/dumb_paint_lib/__init__.py
  15. 33
      tools/dumb_paint_lib/about.py
  16. 374
      tools/dumb_paint_lib/canvaslayer.py
  17. 324
      tools/dumb_paint_lib/layers.py
  18. 347
      tools/dumb_paint_lib/maintemplate.py
  19. 593
      tools/dumb_paint_lib/paintarea.py
  20. 123
      tools/dumb_paint_lib/painttoolkit.py
  21. 1
      tools/dumb_paint_lib/palette.py
  22. 918
      tools/dumb_paint_lib/tui/paintToolKit.tui.json
  23. 0
      tools/dumb_paint_lib/tui/quickImport.tui.json
  24. 35
      tools/webExporter/index.html
  25. 116
      tools/webExporter/js/ttkproxy.js
  26. 28
      tools/webExporterInit.sh

4
Makefile

@ -116,6 +116,10 @@ deployTest: .venv
. .venv/bin/activate ; \ . .venv/bin/activate ; \
python3 -m twine upload --repository testpypi tmp/dist/* --verbose python3 -m twine upload --repository testpypi tmp/dist/* --verbose
itchDumbPaintToolexporter:
tools/webExporterInit.sh
python3 -m http.server --directory tmp
test: .venv test: .venv
# Record a stream # Record a stream
# tests/pytest/test_001_demo.py -r test.input.bin # tests/pytest/test_001_demo.py -r test.input.bin

59
TermTk/TTkCore/color.py

@ -113,7 +113,7 @@ class _TTkColor:
cmax = max(r,g,b) cmax = max(r,g,b)
cmin = min(r,g,b) cmin = min(r,g,b)
lum = (cmax-cmin)/2 lum = (cmax+cmin)/2
if cmax == cmin: if cmax == cmin:
return 0,0,lum return 0,0,lum
@ -388,10 +388,67 @@ class TTkColor(_TTkColor):
color_1 = color_fg_red + color_bg_blue color_1 = color_fg_red + color_bg_blue
color_2 = color_fg_red + TTkColor.bg('#FFFF00') color_2 = color_fg_red + TTkColor.bg('#FFFF00')
color_3 = color_2 + TTkColor.UNDERLINE + TTkColor.BOLD color_3 = color_2 + TTkColor.UNDERLINE + TTkColor.BOLD
# Use presets
color_4 = TTkColor.RED
color_5 = TTkColor.BG_YELLOW + color_4
color_6 = color_5 + TTkColor.UNDERLINE + TTkColor.BOLD
''' '''
RST = _TTkColor() RST = _TTkColor()
'''Reset to the default terminal color and modifiers''' '''Reset to the default terminal color and modifiers'''
BLACK = _TTkColor(fg=( 0, 0, 0))
'''(fg) #000000 - Black'''
WHITE = _TTkColor(fg=(255,255,255))
'''(fg) #FFFFFF - White'''
RED = _TTkColor(fg=(255, 0, 0))
'''(fg) #FF0000 - Red'''
GREEN = _TTkColor(fg=( 0,255, 0))
'''(fg) #00FF00 - Green'''
BLUE = _TTkColor(fg=( 0, 0,255))
'''(fg) #0000FF - Blue'''
CYAN = _TTkColor(fg=( 0,255,255))
'''(fg) #00FFFF - Cyan'''
MAGENTA = _TTkColor(fg=(255, 0,255))
'''(fg) #FF00FF - Magenta'''
YELLOW = _TTkColor(fg=(255,255, 0))
'''(fg) #FFFF00 - Yellow'''
FG_BLACK = BLACK
'''(fg) #000000 - Black'''
FG_WHITE = WHITE
'''(fg) #FFFFFF - White'''
FG_RED = RED
'''(fg) #FF0000 - Red'''
FG_GREEN = GREEN
'''(fg) #00FF00 - Green'''
FG_BLUE = BLUE
'''(fg) #0000FF - Blue'''
FG_CYAN = CYAN
'''(fg) #00FFFF - Cyan'''
FG_MAGENTA = MAGENTA
'''(fg) #FF00FF - Magenta'''
FG_YELLOW = YELLOW
'''(fg) #FFFF00 - Yellow'''
BG_BLACK = BLACK.invertFgBg()
'''(bg) #000000 - Black'''
BG_WHITE = WHITE.invertFgBg()
'''(bg) #FFFFFF - White'''
BG_RED = RED.invertFgBg()
'''(bg) #FF0000 - Red'''
BG_GREEN = GREEN.invertFgBg()
'''(bg) #00FF00 - Green'''
BG_BLUE = BLUE.invertFgBg()
'''(bg) #0000FF - Blue'''
BG_CYAN = CYAN.invertFgBg()
'''(bg) #00FFFF - Cyan'''
BG_MAGENTA = MAGENTA.invertFgBg()
'''(bg) #FF00FF - Magenta'''
BG_YELLOW = YELLOW.invertFgBg()
'''(bg) #FFFF00 - Yellow'''
# Modifiers: # Modifiers:
BOLD = _TTkColor(mod=TTkHelper.Color.BOLD) BOLD = _TTkColor(mod=TTkHelper.Color.BOLD)
'''**Bold** modifier''' '''**Bold** modifier'''

1
TermTk/TTkCrossTools/__init__.py

@ -0,0 +1 @@
from .savetools import *

159
TermTk/TTkCrossTools/savetools.py

@ -0,0 +1,159 @@
# 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__ = ['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

23
TermTk/TTkGui/clipboard.py

@ -61,10 +61,21 @@ class TTkClipboard():
try: try:
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 as _c import pyodideProxy
TTkClipboard._manager = _c import asyncio
TTkClipboard._setText = _c.copy async def _async_co():
TTkClipboard._text = _c.paste 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'): elif importlib.util.find_spec('copykitten'):
TTkLog.info("Using 'copykitten' as clipboard manager") TTkLog.info("Using 'copykitten' as clipboard manager")
import copykitten as _c import copykitten as _c
@ -113,13 +124,13 @@ class TTkClipboard():
except Exception as e: except Exception as e:
TTkLog.error("Clipboard error, try to export X11 if you are running this UI via SSH") TTkLog.error("Clipboard error, try to export X11 if you are running this UI via SSH")
for line in str(e).split("\n"): for line in str(e).split("\n"):
TTkLog.error(line) TTkLog.error(str(line))
@staticmethod @staticmethod
def text(): def text():
'''text''' '''text'''
if TTkClipboard._text: if TTkClipboard._text:
txt = "" txt = None
try: try:
txt = TTkClipboard._text() txt = TTkClipboard._text()
except Exception as e: except Exception as e:

17
TermTk/__init__.py

@ -1,11 +1,12 @@
from .TTkCore import * from .TTkCore import *
from .TTkTheme import * from .TTkTheme import *
from .TTkGui import * from .TTkGui import *
from .TTkWidgets import * from .TTkWidgets import *
from .TTkTypes import * from .TTkTypes import *
from .TTkLayouts import * from .TTkLayouts import *
from .TTkTestWidgets import * from .TTkTestWidgets import *
from .TTkAbstract import * from .TTkAbstract import *
from .TTkUiTools import * from .TTkUiTools import *
from .TTkCrossTools import *
TTkCfg.theme = TTkTheme() TTkCfg.theme = TTkTheme()

10
docs/MDNotes/Fonts/Font2Glyph.md

@ -13,4 +13,12 @@ You can use this command:
/usr/share/fonts/truetype/unifont/unifont_upper.ttf: Unifont Upper:style=Regular /usr/share/fonts/truetype/unifont/unifont_upper.ttf: Unifont Upper:style=Regular
/usr/share/fonts/opentype/3270/3270Condensed-Regular.otf: IBM 3270 Condensed:style=Condensed /usr/share/fonts/opentype/3270/3270Condensed-Regular.otf: IBM 3270 Condensed:style=Condensed
``` ```
Check the font rendering in the browser:
https://typezebra.com
OK:
* FreeSans Regular
* FreeSerif Regular
* FreeMono Regular

2
docs/MDNotes/webExporter/Javascript Open,DnD Files.md

@ -0,0 +1,2 @@
https://dev.to/shubhamtiwari909/drag-and-drop-file-using-javascript-2h99
https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/

10
tests/sandbox/Makefile

@ -4,7 +4,7 @@ testSandbox:
python3 -m http.server --directory ./ python3 -m http.server --directory ./
www: www:
mkdir -p www/pyodide www/xterm/ www/xterm-addon-fit www/codemirror www/codemirror/theme www/codemirror/modes www/fontawesome www/w2ui www/webfonts www/nerdfonts mkdir -p www/pyodide www/xterm/ www/xterm-addon-fit www/codemirror www/codemirror/theme www/codemirror/modes www/fontawesome www/w2ui www/webfonts www/fonts/nerdfonts
wget -P www/pyodide/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/pyodide/pyodide.js wget -P www/pyodide/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/pyodide/pyodide.js
wget -P www/pyodide/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/pyodide/python_stdlib.zip wget -P www/pyodide/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/pyodide/python_stdlib.zip
@ -25,8 +25,8 @@ www:
wget -P www/fontawesome/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/fontawesome/fontawesome.min.css wget -P www/fontawesome/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/fontawesome/fontawesome.min.css
wget -P www/webfonts/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/webfonts/fa-regular-400.woff2 wget -P www/webfonts/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/webfonts/fa-regular-400.woff2
wget -P www/nerdfonts/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/nerdfonts/HurmitNerdFontMono-Regular.otf wget -P www/fonts/nerdfonts/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/nerdfonts/HurmitNerdFontMono-Regular.otf
wget -P www/nerdfonts/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/nerdfonts/DejaVuSansMNerdFont-Regular.ttf wget -P www/fonts/nerdfonts/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/nerdfonts/DejaVuSansMNerdFont-Regular.ttf
wget -P www/w2ui/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/w2ui/w2ui-2.0.min.js wget -P www/w2ui/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/w2ui/w2ui-2.0.min.js
wget -P www/w2ui/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/w2ui/w2ui-2.0.min.css wget -P www/w2ui/ https://ceccopierangiolieugenio.github.io/binaryRepo/pyTermTk/www/w2ui/w2ui-2.0.min.css
@ -43,6 +43,7 @@ updateXterm: www
npm install xterm npm install xterm
npm install xterm-addon-fit npm install xterm-addon-fit
npm install xterm-addon-unicode11 npm install xterm-addon-unicode11
npm install xterm-addon-canvas
cp node_modules/xterm/css/xterm.css \ cp node_modules/xterm/css/xterm.css \
node_modules/xterm/lib/xterm.js \ node_modules/xterm/lib/xterm.js \
node_modules/xterm/lib/xterm.js.map \ node_modules/xterm/lib/xterm.js.map \
@ -53,6 +54,9 @@ updateXterm: www
cp node_modules/xterm-addon-fit/lib/xterm-addon-fit.js \ cp node_modules/xterm-addon-fit/lib/xterm-addon-fit.js \
node_modules/xterm-addon-fit/lib/xterm-addon-fit.js.map \ node_modules/xterm-addon-fit/lib/xterm-addon-fit.js.map \
www/xterm-addon-fit/ www/xterm-addon-fit/
cp node_modules/xterm-addon-canvas/lib/xterm-addon-canvas.js \
node_modules/xterm-addon-canvas/lib/xterm-addon-canvas.js.map \
www/xterm-addon-fit/
rm -rf node_modules package.json package-lock.json rm -rf node_modules package.json package-lock.json
buildSandbox: www buildSandbox: www

8
tests/sandbox/sandbox.NerdFont.html

@ -29,9 +29,9 @@
/* /*
@font-face { @font-face {
font-family: "NerdFont"; font-family: "NerdFont";
src: url(www/nerdfonts/HurmitNerdFontMono-Regular.otf) format("opentype"); src: url(www/fonts/nerdfonts/HurmitNerdFontMono-Regular.otf) format("opentype");
src: url(www/nerdfonts/DejaVuSansMNerdFont-Regular.ttf) format("truetype"); src: url(www/fonts/nerdfonts/DejaVuSansMNerdFont-Regular.ttf) format("truetype");
src: url(www/nerdfonts/DejaVuSansMNerdFont-Regular.ttf) format("truetype"); src: url(www/fonts/nerdfonts/DejaVuSansMNerdFont-Regular.ttf) format("truetype");
} }
*/ */
</style> </style>
@ -84,7 +84,7 @@
<script type="text/javascript"> <script type="text/javascript">
// Workaround from: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API // Workaround from: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API
const font = new FontFace("NerdFont", "url(www/nerdfonts/DejaVuSansMNerdFont-Regular.ttf)"); const font = new FontFace("NerdFont", "url(www/fonts/nerdfonts/DejaVuSansMNerdFont-Regular.ttf)");
document.fonts.add(font); document.fonts.add(font);
font.load(); font.load();
document.fonts.ready.then(() => {main()}); document.fonts.ready.then(() => {main()});

8
tests/sandbox/standalone.fullscreen.NerdFont.html

@ -23,9 +23,9 @@
/* /*
@font-face { @font-face {
font-family: "NerdFont"; font-family: "NerdFont";
src: url(www/nerdfonts/HurmitNerdFontMono-Regular.otf) format("opentype"); src: url(www/fonts/nerdfonts/HurmitNerdFontMono-Regular.otf) format("opentype");
src: url(www/nerdfonts/DejaVuSansMNerdFont-Regular.ttf) format("truetype"); src: url(www/fonts/nerdfonts/DejaVuSansMNerdFont-Regular.ttf) format("truetype");
src: url(www/nerdfonts/DejaVuSansMNerdFont-Regular.ttf) format("truetype"); src: url(www/fonts/nerdfonts/DejaVuSansMNerdFont-Regular.ttf) format("truetype");
} }
*/ */
</style> </style>
@ -36,7 +36,7 @@
<script type="text/javascript"> <script type="text/javascript">
// Workaround from: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API // Workaround from: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API
const font = new FontFace("NerdFont", "url(www/nerdfonts/DejaVuSansMNerdFont-Regular.ttf)"); const font = new FontFace("NerdFont", "url(www/fonts/nerdfonts/DejaVuSansMNerdFont-Regular.ttf)");
document.fonts.add(font); document.fonts.add(font);
font.load(); font.load();
document.fonts.ready.then(() => {main()}); document.fonts.ready.then(() => {main()});

4
tools/check.import.sh

@ -36,7 +36,8 @@ __check(){
-e "propertyanimation.py:from inspect import getfullargspec" \ -e "propertyanimation.py:from inspect import getfullargspec" \
-e "propertyanimation.py:from types import LambdaType" \ -e "propertyanimation.py:from types import LambdaType" \
-e "propertyanimation.py:import time, math" \ -e "propertyanimation.py:import time, math" \
-e "terminal.py:from select import select" | -e "savetools.py:import importlib.util" \
-e "savetools.py:import json" |
grep -v \ grep -v \
-e "TTkTerm/input_mono.py:from time import time" \ -e "TTkTerm/input_mono.py:from time import time" \
-e "TTkTerm/input_mono.py:import platform" \ -e "TTkTerm/input_mono.py:import platform" \
@ -71,6 +72,7 @@ __check(){
-e "drivers/__init__.py:import platform" | -e "drivers/__init__.py:import platform" |
grep -v \ grep -v \
-e "TTkTerminal/debugterminal.py:import struct, fcntl, termios" \ -e "TTkTerminal/debugterminal.py:import struct, fcntl, termios" \
-e "TTkTerminal/debugterminal.py:from select import select" \
-e "TTkTerminal/terminalview.py:import struct, fcntl, termios" \ -e "TTkTerminal/terminalview.py:import struct, fcntl, termios" \
-e "TTkTerminal/terminalview.py:from select import select" \ -e "TTkTerminal/terminalview.py:from select import select" \
-e "TTkTerminal/terminalview.py:from .terminalview_CSI_DEC import _TTkTerminal_CSI_DEC" \ -e "TTkTerminal/terminalview.py:from .terminalview_CSI_DEC import _TTkTerminal_CSI_DEC" \

35
tools/dumb.paint.tool.py

@ -32,16 +32,25 @@ from dumb_paint_lib import PaintTemplate
ttk.TTkTheme.loadTheme(ttk.TTkTheme.NERD) ttk.TTkTheme.loadTheme(ttk.TTkTheme.NERD)
root = ttk.TTk( def main():
title="Dumb Paint Tool", parser = argparse.ArgumentParser()
layout=ttk.TTkGridLayout(), parser.add_argument('filename', type=str, nargs='?', help='the file to open')
mouseTrack=True, # parser.add_argument('-k', '--showkeys', action='store_true', help='display the keypresses and mouse interactions')
sigmask=( args = parser.parse_args()
ttk.TTkTerm.Sigmask.CTRL_C |
ttk.TTkTerm.Sigmask.CTRL_Q | root = ttk.TTk(
ttk.TTkTerm.Sigmask.CTRL_S | title="Dumb Paint Tool",
ttk.TTkTerm.Sigmask.CTRL_Z )) layout=ttk.TTkGridLayout(),
mouseTrack=True,
PaintTemplate(parent=root) sigmask=(
# ttk.TTkTerm.Sigmask.CTRL_C |
root.mainloop() ttk.TTkTerm.Sigmask.CTRL_Q |
ttk.TTkTerm.Sigmask.CTRL_S |
ttk.TTkTerm.Sigmask.CTRL_Z ))
PaintTemplate(parent=root,fileName=args.filename)
root.mainloop()
if __name__ == '__main__':
main()

10
tools/dumb_paint_lib/__init__.py

@ -1,4 +1,8 @@
from .maintemplate import * from .maintemplate import *
from .paintarea import * from .paintarea import *
from .textarea import * from .painttoolkit import *
from .palette import * from .textarea import *
from .palette import *
from .layers import *
from .canvaslayer import *
from .about import *

33
tools/dumb_paint_lib/about.py

@ -0,0 +1,33 @@
# 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__ = ['About']
from TermTk.TTkCore.log import TTkLog
from TermTk.TTkCore.color import TTkColor
from TermTk.TTkCore.string import TTkString
from TermTk import TTkAbout, TTkWindow
# from .cfg import TTkDesignerCfg
class About(TTkAbout):
def paintEvent(self, canvas):
super().paintEvent(canvas)

374
tools/dumb_paint_lib/canvaslayer.py

@ -0,0 +1,374 @@
# 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__ = ['CanvasLayer']
import sys, os
sys.path.append(os.path.join(sys.path[0],'../..'))
import TermTk as ttk
# Canvas Layer structure
# The data may include more areas than the visible one
# This is helpful in case of resize to not lose the drawn areas
#
# |---| OffsetX
# x
# ╭────────────────╮ - -
# │ │ | OffsetY |
# y │ ┌───────┐ │ \ - | Data
# │ │Visible│ │ | h |
# │ └───────┘ │ / |
# │ │ |
# └────────────────┘ -
# \---w--/
class CanvasLayer():
class Tool(int):
MOVE = 0x01
RESIZE = 0x02
BRUSH = 0x04
RECTFILL = 0x08
RECTEMPTY = 0x10
CLONE = 0x20
__slot__ = ('_pos','_name','_visible','_size','_data','_colors','_preview','_offset')
def __init__(self) -> None:
self._pos = (0,0)
self._size = (0,0)
self._offset = (0,0)
self._name = ""
self._visible = True
self._preview = None
self._data: list[list[str ]] = []
self._colors:list[list[ttk.TTkColor]] = []
def pos(self):
return self._pos
def size(self):
return self._size
def visible(self):
return self._visible
@ttk.pyTTkSlot(bool)
def setVisible(self, visible):
self._visible = visible
def name(self):
return self._name
@ttk.pyTTkSlot(str)
def setName(self, name):
self._name = name
def isOpaque(self,x,y):
if not self._visible: return False
ox,oy = self._offset
w,h = self._size
data = self._data
colors = self._colors
if 0<=x<w and 0<=y<h:
return data[oy+y][ox+x] != ' ' or colors[oy+y][ox+x].background()
return False
def move(self,x,y):
self._pos=(x,y)
def resize(self,w,h):
self._size = (w,h)
self._data = (self._data + [[] for _ in range(h)])[:h]
self._colors = (self._colors + [[] for _ in range(h)])[:h]
for i in range(h):
self._data[i] = (self._data[i] + [' ' for _ in range(w)])[:w]
self._colors[i] = (self._colors[i] + [ttk.TTkColor.RST for _ in range(w)])[:w]
def superResize(self,dx,dy,dw,dh):
ox,oy = self._offset
x,y = self.pos()
w,h = self.size()
daw = len(self._data[0])
dah = len(self._data)
diffx = dx-x
diffy = dy-y
self._preview = None
if ox < x-dx: # we need to resize and move ox
_nw = x-dx-ox
ox = x-dx
self._data = [([' ']*_nw ) + _r for _r in self._data]
self._colors = [([ttk.TTkColor.RST]*_nw) + _r for _r in self._colors]
if dw+ox > daw:
_nw = dw+ox-daw
self._data = [_r + ([' ']*_nw ) for _r in self._data]
self._colors = [_r + ([ttk.TTkColor.RST]*_nw) for _r in self._colors]
daw = len(self._data[0])
if oy < y-dy: # we need to resize and move ox
_nh = y-dy-oy
oy = y-dy
self._data = [[' ']*daw for _ in range(_nh)] + self._data
self._colors = [[ttk.TTkColor.RST]*daw for _ in range(_nh)] + self._colors
if dh+oy > dah:
_nh = dh+oy-dah
self._data = self._data + [[' ']*daw for _ in range(_nh)]
self._colors = self._colors + [[ttk.TTkColor.RST]*daw for _ in range(_nh)]
self._offset = (ox+diffx,oy+diffy)
self._pos = (dx,dy)
self._size = (dw,dh)
def clean(self):
w,h = self._size
self._offset = (0,0)
self._preview = None
for i in range(h):
self._data[i] = [' ']*w
self._colors[i] = [ttk.TTkColor.RST]*w
def exportLayer(self, full=False, palette=True, crop=True):
# xa|----------| xb
# px |-----------| = max(px,px+xa-ox)
# Offset |------| pw
# Data |----------------------------|
# daw
# Don't try this at home
ox,oy = self._offset
px,py = self.pos()
pw,ph = self.size()
if full:
data = self._data
colors = self._colors
else:
data = [row[ox:ox+pw] for row in self._data[ oy:oy+ph] ]
colors = [row[ox:ox+pw] for row in self._colors[oy:oy+ph] ]
ox=oy=0
daw = len(data[0])
dah = len(data)
# get the bounding box
if crop:
xa,xb,ya,yb = daw,0,dah,0
for y,(drow,crow) in enumerate(zip(data,colors)):
for x,(d,c) in enumerate(zip(drow,crow)):
if d != ' ' or c.background():
xa = min(x,xa)
xb = max(x,xb)
ya = min(y,ya)
yb = max(y,yb)
if (xa,xb,ya,yb) == (daw,0,dah,0):
xa=xb=ya=yb=0
else:
xa,xb,ya,yb = 0,daw,0,dah
# Visble Area intersecting the bounding box
vxa,vya = max(px,px+xa-ox), max(py,py+ya-oy)
vxb,vyb = min(px+pw,vxa+xb-xa),min(py+ph,vya+yb-ya)
vw,vh = vxb-vxa+1, vyb-vya+1
outData = {
'version':'1.1.0',
'size':[vw,vh],
'pos': (vxa,vya),
'name':str(self.name()),
'data':[], 'colors':[]}
if palette:
palette = outData['palette'] = []
for row in colors:
for c in row:
fg = f"{c.getHex(ttk.TTkK.Foreground)}" if c.foreground() else None
bg = f"{c.getHex(ttk.TTkK.Background)}" if c.background() else None
if (pc:=(fg,bg)) not in palette:
palette.append(pc)
if full:
wslice = slice(xa,xb+1)
hslice = slice(ya,yb+1)
outData['offset'] = (max(0,ox-xa),max(0,oy-ya))
else:
wslice = slice(ox+vxa-px,ox+vxa-px+vw)
hslice = slice(oy+vya-py,oy+vya-py+vh)
for row in data[hslice]:
outData['data'].append(row[wslice])
for row in colors[hslice]:
outData['colors'].append([])
for c in row[wslice]:
fg = f"{c.getHex(ttk.TTkK.Foreground)}" if c.foreground() else None
bg = f"{c.getHex(ttk.TTkK.Background)}" if c.background() else None
if palette:
outData['colors'][-1].append(palette.index((fg,bg)))
else:
outData['colors'][-1].append((fg,bg))
return outData
def _import_v1_1_0(self, dd):
self._import_v1_0_0(dd)
self._offset = dd.get('offset',(0,0))
def _import_v1_0_0(self, dd):
self._pos = dd['pos']
self._size = dd['size']
self._name = dd['name']
self._data = dd['data']
def _getColor(cd):
fg,bg = cd
if fg and bg: return ttk.TTkColor.fg(fg)+ttk.TTkColor.bg(bg)
elif fg: return ttk.TTkColor.fg(fg)
elif bg: return ttk.TTkColor.bg(bg)
else: return ttk.TTkColor.RST
if 'palette' in dd:
palette = [_getColor(c) for c in dd['palette']]
self._colors = [[palette[c] for c in row] for row in dd['colors']]
else:
self._colors = [[_getColor(c) for c in row] for row in dd['colors']]
def _import_v0_0_0(self, dd):
# Legacy old import
w = len(dd['data'][0]) + 10
h = len(dd['data']) + 4
x,y=5,2
self.resize(w,h)
self._pos = (0,0)
for i,rd in enumerate(dd['data']):
for ii,cd in enumerate(rd):
self._data[i+y][ii+x] = cd
for i,rd in enumerate(dd['colors']):
for ii,cd in enumerate(rd):
fg,bg = cd
if fg and bg:
self._colors[i+y][ii+x] = ttk.TTkColor.fg(fg)+ttk.TTkColor.bg(bg)
elif fg:
self._colors[i+y][ii+x] = ttk.TTkColor.fg(fg)
elif bg:
self._colors[i+y][ii+x] = ttk.TTkColor.bg(bg)
else:
self._colors[i+y][ii+x] = ttk.TTkColor.RST
def importLayer(self, dd):
self.clean()
if 'version' in dd:
ver = dd['version']
if ver == ('1.0.0'):
self._import_v1_0_0(dd)
elif ver == ('1.1.0'):
self._import_v1_1_0(dd)
else:
self._import_v0_0_0(dd)
def placeFill(self,geometry,tool,glyph:str,color:ttk.TTkColor,preview=False):
ox,oy = self._offset
w,h = self._size
ax,ay,bx,by = geometry
ax = max(0,min(w-1,ax))
ay = max(0,min(h-1,ay))
bx = max(0,min(w-1,bx))
by = max(0,min(h-1,by))
fax,fay = ox+min(ax,bx), oy+min(ay,by)
fbx,fby = ox+max(ax,bx), oy+max(ay,by)
color = color if glyph != ' ' else color.background()
color = color if color else ttk.TTkColor.RST
if preview:
data = [_r.copy() for _r in self._data]
colors = [_r.copy() for _r in self._colors]
self._preview = {'data':data,'colors':colors}
else:
self._preview = None
data = self._data
colors = self._colors
if tool == CanvasLayer.Tool.RECTFILL:
for row in data[fay:fby+1]:
row[fax:fbx+1] = [glyph]*(fbx-fax+1)
for row in colors[fay:fby+1]:
row[fax:fbx+1] = [color]*(fbx-fax+1)
if tool == CanvasLayer.Tool.RECTEMPTY:
data[fay][fax:fbx+1] = [glyph]*(fbx-fax+1)
data[fby][fax:fbx+1] = [glyph]*(fbx-fax+1)
colors[fay][fax:fbx+1] = [color]*(fbx-fax+1)
colors[fby][fax:fbx+1] = [color]*(fbx-fax+1)
for row in data[fay:fby]:
row[fax]=row[fbx]=glyph
for row in colors[fay:fby]:
row[fax]=row[fbx]=color
return True
def placeGlyph(self,x,y,glyph:str,color:ttk.TTkColor,preview=False):
ox,oy = self._offset
w,h = self._size
color = color if glyph != ' ' else color.background()
color = color if color else ttk.TTkColor.RST
if preview:
data = [_r.copy() for _r in self._data]
colors = [_r.copy() for _r in self._colors]
self._preview = {'data':data,'colors':colors}
else:
self._preview = None
data = self._data
colors = self._colors
if 0<=x<w and 0<=y<h:
data[ oy+y][ox+x] = glyph
colors[oy+y][ox+x] = color
return True
return False
def drawInCanvas(self, pos, canvas:ttk.TTkCanvas):
if not self._visible: return
px,py = pos
pw,ph = self._size
cw,ch = canvas.size()
if px+pw<0 or py+ph<0:return
if px>=cw or py>=ch:return
# Data Offset
ox,oy = self._offset
# x,y position in the Canvas
cx = max(0,px)
cy = max(0,py)
# x,y position in the Layer
lx,ly = (cx-px),(cy-py)
# Area to be copyed
dw = min(cw-cx,pw-lx)
dh = min(ch-cy,ph-ly)
if _p := self._preview:
data = _p['data']
colors = _p['colors']
else:
data = self._data
colors = self._colors
for y in range(cy,cy+dh):
for x in range(cx,cx+dw):
gl = data[ oy+y+ly-cy][ox+x+lx-cx]
c = colors[oy+y+ly-cy][ox+x+lx-cx]
if gl==' ' and c._bg:
canvas._data[y][x] = gl
canvas._colors[y][x] = c
elif gl!=' ':
canvas._data[y][x] = gl
cc = canvas._colors[y][x]
newC = c.copy()
newC._bg = c._bg if c._bg else cc._bg
canvas._colors[y][x] = newC

324
tools/dumb_paint_lib/layers.py

@ -0,0 +1,324 @@
# 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__ = ['Layers','LayerData']
import sys, os
sys.path.append(os.path.join(sys.path[0],'../..'))
import TermTk as ttk
class LayerData():
__slots__ = ('_name','_data',
#signals
'nameChanged','visibilityToggled')
def __init__(self,name:ttk.TTkString=ttk.TTkString('New'),data=None) -> None:
self._name:ttk.TTkString = ttk.TTkString(name) if type(name)==str else name
self.visibilityToggled = ttk.pyTTkSignal(bool)
self._data = data
self.nameChanged = ttk.pyTTkSignal(str)
def name(self):
return self._name
def setName(self,name):
self.nameChanged.emit(name)
self._name = name
def data(self):
return self._data
def setData(self,data):
self._data = data
class _layerButton(ttk.TTkContainer):
classStyle = {
'default': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#000044"),
'borderColor': ttk.TTkColor.fg('#CCDDDD'),
'grid':1},
'disabled': {'color': ttk.TTkColor.fg('#888888'),
'borderColor':ttk.TTkColor.fg('#888888'),
'grid':0},
'hover': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#000050")+ttk.TTkColor.BOLD,
'borderColor': ttk.TTkColor.fg("#FFFFCC")+ttk.TTkColor.BOLD,
'grid':1},
'selected': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#004488"),
'borderColor': ttk.TTkColor.fg("#FFFF00"),
'grid':0},
'unchecked': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#000044"),
'borderColor': ttk.TTkColor.RST,
'grid':3},
'clicked': {'color': ttk.TTkColor.fg("#FFFFDD")+ttk.TTkColor.BOLD,
'borderColor': ttk.TTkColor.fg("#DDDDDD")+ttk.TTkColor.BOLD,
'grid':0},
'focus': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#000044")+ttk.TTkColor.BOLD,
'borderColor': ttk.TTkColor.fg("#ffff00") + ttk.TTkColor.BOLD,
'grid':1},
}
__slots__ = ('_layerData','_first', '_isSelected', '_layerVisible',
'_ledit',
# signals
'clicked', 'visibilityToggled',
)
def __init__(self, layer:LayerData, **kwargs):
self.clicked = ttk.pyTTkSignal(_layerButton)
self._layerData:LayerData = layer
self._isSelected = False
self._first = True
self._layerVisible = True
self.visibilityToggled = layer.visibilityToggled
super().__init__(**kwargs|{'layout':ttk.TTkGridLayout()})
self.setPadding(1,1,7,2)
self._ledit = ttk.TTkLineEdit(parent=self, text=layer.name(),visible=False)
self._ledit.focusChanged.connect(self._ledit.setVisible)
self._ledit.textEdited.connect(self._textEdited)
# self.setFocusPolicy(ttk.TTkK.ClickFocus)
@ttk.pyTTkSlot(str)
def _textEdited(self, text):
self._layerData.setName(text)
def mousePressEvent(self, evt) -> bool:
if evt.x <= 3:
self._layerVisible = not self._layerVisible
self.visibilityToggled.emit(self._layerVisible)
self.setFocus()
self.update()
return True
def mouseReleaseEvent(self, evt) -> bool:
self.clicked.emit(self)
return True
def mouseDoubleClickEvent(self, evt) -> bool:
self._ledit.setVisible(True)
self._ledit.setFocus()
return True
def mouseDragEvent(self, evt) -> bool:
drag = ttk.TTkDrag()
drag.setData(self)
name = self._layerData.name()
pm = ttk.TTkCanvas(width=len(name)+4,height=3)
pm.drawBox(pos=(0,0),size=pm.size())
pm.drawText(pos=(2,1), text=name)
drag.setHotSpot(5, 1)
drag.setPixmap(pm)
drag.exec()
return True
def paintEvent(self, canvas: ttk.TTkCanvas):
if self._isSelected:
style = self.style()['selected']
else:
style = self.currentStyle()
borderColor = style['borderColor']
textColor = style['color']
btnVisible = '' if self._layerVisible else ''
w,h = self.size()
canvas.drawText( pos=(0,0),text=f"{''*(w-7)}",color=borderColor)
canvas.drawText( pos=(0,2),text=f"{''*(w-7)}",color=borderColor)
if self._first:
canvas.drawText(pos=(0,1),text=f" {btnVisible} - ┃{' '*(w-7)}",color=borderColor)
else:
canvas.drawText(pos=(0,1),text=f" {btnVisible} - ╽{' '*(w-7)}",color=borderColor)
canvas.drawTTkString(pos=(7,1),text=self._layerData.name(), width=w-9, color=textColor)
class LayerScrollWidget(ttk.TTkAbstractScrollView):
__slots__ = ('_layers','_selected', '_dropTo',
# Signals
'layerSelected','layerAdded','layerDeleted','layersOrderChanged')
def __init__(self, **kwargs):
self.layerSelected = ttk.pyTTkSignal(LayerData)
self.layerAdded = ttk.pyTTkSignal(LayerData)
self.layerDeleted = ttk.pyTTkSignal(LayerData)
self.layersOrderChanged = ttk.pyTTkSignal(list[LayerData])
self._selected = None
self._dropTo = None
self._layers:list[_layerButton] = []
super().__init__(**kwargs)
self.viewChanged.connect(self._placeTheButtons)
self.viewChanged.connect(self._viewChangedHandler)
@ttk.pyTTkSlot()
def _viewChangedHandler(self):
x,y = self.getViewOffsets()
self.layout().setOffset(-x,-y)
def viewFullAreaSize(self) -> tuple:
_,_,w,h = self.layout().fullWidgetAreaGeometry()
return w,h
def viewDisplayedSize(self) -> tuple:
return self.size()
def maximumWidth(self): return 0x10000
def maximumHeight(self): return 0x10000
def minimumWidth(self): return 0
def minimumHeight(self): return 0
@ttk.pyTTkSlot(_layerButton)
def _clickedLayer(self, layerButton:_layerButton):
if sel:=self._selected:
sel._isSelected = False
sel.update()
self._selected = layerButton
layerButton._isSelected = True
self.layerSelected.emit(layerButton._layerData)
self.update()
def clear(self):
for layBtn in self._layers:
self.layout().removeWidget(layBtn)
layBtn.clicked.clear()
layBtn.visibilityToggled.clear()
layBtn._layerData.nameChanged.clear()
self._layers.clear()
self.update()
@ttk.pyTTkSlot()
def moveUp(self):
return self._moveButton(-1)
@ttk.pyTTkSlot()
def moveDown(self):
return self._moveButton(+1)
def _moveButton(self,direction):
if not self._selected: return
index = self._layers.index(self._selected)
if index+direction < 0: return
l = self._layers.pop(index)
self._layers.insert(index+direction,l)
self._placeTheButtons()
self.layersOrderChanged.emit([_l._layerData for _l in self._layers])
@ttk.pyTTkSlot()
def addLayer(self,name=None, data=None):
name = name if name else f"Layer #{len(self._layers)}"
_l=LayerData(name=name,data=data)
newLayerBtn:_layerButton = _layerButton(parent=self,layer=_l)
self._layers.insert(0,newLayerBtn)
if sel:=self._selected: sel._isSelected = False
self._selected = newLayerBtn
newLayerBtn._isSelected = True
newLayerBtn.clicked.connect(self._clickedLayer)
self.viewChanged.emit()
self._placeTheButtons()
self.layerAdded.emit(newLayerBtn._layerData)
return _l
def _placeTheButtons(self):
w,h = self.size()
for i,l in enumerate(self._layers):
l._first = i==0
l.setGeometry(0,i*2,w,3)
l.lowerWidget()
self.update()
@ttk.pyTTkSlot()
def delLayer(self):
self._layers.remove()
def dragEnterEvent(self, evt) -> bool:
if type(evt.data())!=_layerButton: return False
x,y = self.getViewOffsets()
self._dropTo = max(0,min(len(self._layers),(evt.y-1+y)//2))
self.update()
return True
def dragLeaveEvent(self, evt) -> bool:
if type(evt.data())!=_layerButton: return False
self._dropTo = None
self.update()
return True
def dragMoveEvent(self, evt) -> bool:
if type(evt.data())!=_layerButton: return False
x,y = self.getViewOffsets()
self._dropTo = max(0,min(len(self._layers),(evt.y-1+y)//2))
self.update()
ttk.TTkLog.debug(f"{evt.x},{evt.y-y} - {len(self._layers)} - {self._dropTo}")
return True
def dropEvent(self, evt) -> bool:
if type(evt.data())!=_layerButton: return False
x,y = self.getViewOffsets()
self._dropTo = None
data = evt.data()
# dropPos = len(self._layers)-(evt.y-1)//2
dropPos = max(0,min(len(self._layers),(evt.y-1+y)//2))
ttk.TTkLog.debug(f"{evt.x},{evt.y-y} - {len(self._layers)} - {self._dropTo} {dropPos}")
if dropPos > self._layers.index(data):
dropPos -= 1
self._layers.remove(data)
self._layers.insert(dropPos,data)
self._placeTheButtons()
self.layersOrderChanged.emit([_l._layerData for _l in self._layers])
return True
# Stupid hack to paint on top of the child widgets
def paintChildCanvas(self):
super().paintChildCanvas()
offx, offy = self.getViewOffsets()
if self._dropTo == None: return
canvas = self.getCanvas()
w,h = canvas.size()
color = ttk.TTkColor.YELLOW
canvas.drawText(pos=(0,(self._dropTo)*2-offy),text=f"{''*(w-2)}",color=color)
class Layers(ttk.TTkGridLayout):
__slots__ = ('_scrollWidget',
# Forward Methods
'addLayer','clear',
# Forward Signals
'layerSelected','layerAdded','layerDeleted','layersOrderChanged')
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._scrollWidget = _lsw = LayerScrollWidget()
_sa = ttk.TTkAbstractScrollArea(scrollWidget=self._scrollWidget,minWidth=16)
_sa.setViewport(_lsw)
self.addWidget(_sa,0,0,1,5)
self.addWidget(btnAdd :=ttk.TTkButton(text='add') ,1,0)
self.addWidget(btnUp :=ttk.TTkButton(text='',maxWidth=3) ,1,1)
self.addWidget(btnDown:=ttk.TTkButton(text='',maxWidth=3) ,1,2)
# self.addItem(ttk.TTkLayout(),1,3)
self.addWidget(btnDel :=ttk.TTkButton(text=ttk.TTkString('del',ttk.TTkColor.RED),maxWidth=5),1,4)
btnAdd.setToolTip( "Create a new Layer\nand add it to the image")
btnDel.setToolTip( "Delete the selected Layer")
btnUp.setToolTip( "Raise the selected Layer one step")
btnDown.setToolTip("Lower the selected Layer one step")
btnAdd.clicked.connect( _lsw.addLayer)
btnDel.clicked.connect( _lsw.delLayer)
btnUp.clicked.connect( _lsw.moveUp)
btnDown.clicked.connect(_lsw.moveDown)
# forward signals
self.layerSelected = _lsw.layerSelected
self.layerSelected = _lsw.layerSelected
self.layerAdded = _lsw.layerAdded
self.layerDeleted = _lsw.layerDeleted
self.layersOrderChanged = _lsw.layersOrderChanged
# forward methods
self.addLayer = _lsw.addLayer
self.clear = _lsw.clear

347
tools/dumb_paint_lib/maintemplate.py

@ -22,21 +22,25 @@
__all__ = ['PaintTemplate'] __all__ = ['PaintTemplate']
import sys, os import sys, os, json
sys.path.append(os.path.join(sys.path[0],'../..')) sys.path.append(os.path.join(sys.path[0],'../..'))
import TermTk as ttk import TermTk as ttk
from .paintarea import PaintArea, PaintToolKit from .paintarea import PaintArea, PaintScrollArea
from .palette import Palette from .canvaslayer import CanvasLayer
from .textarea import TextArea from .painttoolkit import PaintToolKit
from .palette import Palette
from .textarea import TextArea
from .layers import Layers,LayerData
from .about import About
class LeftPanel(ttk.TTkVBoxLayout): class LeftPanel(ttk.TTkVBoxLayout):
__slots__ = ('_palette', __slots__ = ('_palette',
# Signals # Signals
'toolSelected') 'toolSelected')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.toolSelected = ttk.pyTTkSignal(PaintArea.Tool) self.toolSelected = ttk.pyTTkSignal(CanvasLayer.Tool)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._palette = Palette(maxHeight=12) self._palette = Palette(maxHeight=12)
self.addWidget(self._palette) self.addWidget(self._palette)
@ -53,43 +57,62 @@ class LeftPanel(ttk.TTkVBoxLayout):
# Toolset # Toolset
lTools = ttk.TTkGridLayout() lTools = ttk.TTkGridLayout()
ra_move = ttk.TTkRadioButton(radiogroup="tools", text="Select/Move", enabled=True)
ra_select = ttk.TTkRadioButton(radiogroup="tools", text="Select",enabled=False)
ra_brush = ttk.TTkRadioButton(radiogroup="tools", text="Brush", checked=True) ra_brush = ttk.TTkRadioButton(radiogroup="tools", text="Brush", checked=True)
ra_line = ttk.TTkRadioButton(radiogroup="tools", text="Line", enabled=False) ra_line = ttk.TTkRadioButton(radiogroup="tools", text="Line", enabled=False)
ra_rect = ttk.TTkRadioButton(radiogroup="tools", text="Rect") ra_rect = ttk.TTkRadioButton(radiogroup="tools", text="Rect")
ra_oval = ttk.TTkRadioButton(radiogroup="tools", text="Oval", enabled=False) ra_oval = ttk.TTkRadioButton(radiogroup="tools", text="Oval", enabled=False)
ra_rect_f = ttk.TTkRadioButton(radiogroup="toolsRectFill", text="Fill" , enabled=False, checked=True) ra_rect_f = ttk.TTkRadioButton(radiogroup="toolsRectFill", text="Fill" , enabled=False, checked=True)
ra_rect_e = ttk.TTkRadioButton(radiogroup="toolsRectFill", text="Empty", enabled=False) ra_rect_e = ttk.TTkRadioButton(radiogroup="toolsRectFill", text="Empty", enabled=False)
@ttk.pyTTkSlot(bool) cb_move_r = ttk.TTkCheckbox(text="Resize", enabled=False)
def _emitTool(checked):
if not checked: return @ttk.pyTTkSlot()
tool = PaintArea.Tool.BRUSH def _checkTools():
if ra_brush.isChecked(): tool = CanvasLayer.Tool.BRUSH
tool = PaintArea.Tool.BRUSH if ra_move.isChecked():
tool = CanvasLayer.Tool.MOVE
if cb_move_r.isChecked():
tool |= CanvasLayer.Tool.RESIZE
elif ra_brush.isChecked():
tool = CanvasLayer.Tool.BRUSH
elif ra_rect.isChecked(): elif ra_rect.isChecked():
if ra_rect_e.isChecked(): if ra_rect_e.isChecked():
tool = PaintArea.Tool.RECTEMPTY tool = CanvasLayer.Tool.RECTEMPTY
else: else:
tool = PaintArea.Tool.RECTFILL tool = CanvasLayer.Tool.RECTFILL
self.toolSelected.emit(tool) self.toolSelected.emit(tool)
@ttk.pyTTkSlot(bool)
def _emitTool(checked):
if not checked: return
_checkTools()
ra_rect.toggled.connect(ra_rect_f.setEnabled) ra_rect.toggled.connect(ra_rect_f.setEnabled)
ra_rect.toggled.connect(ra_rect_e.setEnabled) ra_rect.toggled.connect(ra_rect_e.setEnabled)
ra_move.toggled.connect(cb_move_r.setEnabled)
ra_move.toggled.connect( _emitTool)
ra_select.toggled.connect( _emitTool)
ra_brush.toggled.connect( _emitTool) ra_brush.toggled.connect( _emitTool)
ra_line.toggled.connect( _emitTool) ra_line.toggled.connect( _emitTool)
ra_rect.toggled.connect( _emitTool) ra_rect.toggled.connect( _emitTool)
ra_rect_f.toggled.connect( _emitTool) ra_rect_f.toggled.connect( _emitTool)
ra_rect_e.toggled.connect( _emitTool) ra_rect_e.toggled.connect( _emitTool)
ra_oval.toggled.connect( _emitTool) ra_oval.toggled.connect( _emitTool)
cb_move_r.toggled.connect( _checkTools)
lTools.addWidget(ra_brush,0,0)
lTools.addWidget(ra_line,1,0) lTools.addWidget(ra_move ,0,0)
lTools.addWidget(ra_rect,2,0) lTools.addWidget(cb_move_r,0,1)
lTools.addWidget(ra_rect_f,2,1) lTools.addWidget(ra_select,1,0)
lTools.addWidget(ra_rect_e,2,2) lTools.addWidget(ra_brush ,2,0)
lTools.addWidget(ra_oval,3,0) lTools.addWidget(ra_line ,3,0)
lTools.addWidget(ra_rect ,4,0)
lTools.addWidget(ra_rect_f,4,1)
lTools.addWidget(ra_rect_e,4,2)
lTools.addWidget(ra_oval ,5,0)
self.addItem(lTools) self.addItem(lTools)
# brush # brush
@ -104,69 +127,105 @@ class LeftPanel(ttk.TTkVBoxLayout):
return self._palette return self._palette
class ExportArea(ttk.TTkGridLayout): class ExportArea(ttk.TTkGridLayout):
__slots__ = ('_paintArea', '_te') __slots__ = ('_paintArea', '_te','_cbCrop', '_cbFull', '_cbPal')
def __init__(self, paintArea, **kwargs): def __init__(self, paintArea:PaintArea, **kwargs):
self._paintArea = paintArea self._paintArea:PaintArea = paintArea
super().__init__(**kwargs) super().__init__(**kwargs)
self._te = ttk.TTkTextEdit(lineNumber=True, readOnly=False) self._te = ttk.TTkTextEdit(lineNumber=True, readOnly=False)
btn_ex = ttk.TTkButton(text="Export") btn_exIm = ttk.TTkButton(text="Export Image")
self.addWidget(btn_ex ,0,0) btn_exLa = ttk.TTkButton(text="Export Layer")
self.addWidget(self._te,1,0,1,3) btn_exPr = ttk.TTkButton(text="Export Document")
self._cbCrop = ttk.TTkCheckbox(text="Crop",checked=True)
self._cbFull = ttk.TTkCheckbox(text="Full",checked=True)
self._cbPal = ttk.TTkCheckbox(text="Palette",checked=True)
self.addWidget(btn_exLa ,0,0)
self.addWidget(btn_exIm ,0,1)
self.addWidget(btn_exPr ,0,2)
self.addWidget(self._cbCrop,0,3)
self.addWidget(self._cbFull,0,4)
self.addWidget(self._cbPal ,0,5)
self.addWidget(self._te,1,0,1,7)
btn_exLa.clicked.connect(self._exportLayer)
btn_exPr.clicked.connect(self._exportDocument)
btn_exIm.clicked.connect(self._exportImage)
btn_ex.clicked.connect(self._export) @ttk.pyTTkSlot()
def _exportImage(self):
crop = self._cbCrop.isChecked()
palette = self._cbPal.isChecked()
full = self._cbFull.isChecked()
image = self._paintArea.exportImage()
self._te.setText(image)
@ttk.pyTTkSlot() @ttk.pyTTkSlot()
def _export(self): def _exportLayer(self):
# Don't try this at home crop = self._cbCrop.isChecked()
pw,ph = self._paintArea._canvasSize palette = self._cbPal.isChecked()
data = self._paintArea._canvasArea['data'] full = self._cbFull.isChecked()
colors = self._paintArea._canvasArea['colors'] dd = self._paintArea.exportLayer(full=full,palette=palette,crop=crop)
# get the bounding box if not dd:
xa,xb,ya,yb = pw,0,ph,0 self._te.setText('# No Data toi be saved!!!')
for y,row in enumerate(data):
for x,d in enumerate(row):
c = colors[y][x]
if d != ' ' or c.background():
xa = min(x,xa)
xb = max(x,xb)
ya = min(y,ya)
yb = max(y,yb)
if xa>xb or ya>yb:
self._te.setText("No Picture Found!!!")
return return
out = "data = {'data': [\n" self._te.setText('# Compressed Data:')
outData = {'data':[], 'colors':[]}
for row in data[ya:yb+1]:
out += " ["
outData['data'].append(row[xa:xb+1])
for c in row[xa:xb+1]:
out += f"'{c}',"
out += "],\n"
out += " ],\n"
out += " 'colors': [\n"
for row in colors[ya:yb+1]:
out += " ["
outData['colors'].append([])
for c in row[xa:xb+1]:
fg = f"{c.getHex(ttk.TTkK.Foreground)}" if c.foreground() else None
bg = f"{c.getHex(ttk.TTkK.Background)}" if c.background() else None
out += f"('{fg}','{bg}'),"
outData['colors'][-1].append((fg,bg))
out += "],\n"
out += " ]}\n"
self._te.setText(out)
self._te.append('\n# Compressed Data:')
self._te.append('data = TTkUtil.base64_deflate_2_obj(') self._te.append('data = TTkUtil.base64_deflate_2_obj(')
b64str = ttk.TTkUtil.obj_inflate_2_base64(outData) b64str = ttk.TTkUtil.obj_inflate_2_base64(dd)
b64list = ' "' + '" +\n "'.join([b64str[i:i+128] for i in range(0,len(b64str),128)]) + '")' b64list = ' "' + '" +\n "'.join([b64str[i:i+128] for i in range(0,len(b64str),128)]) + '")'
self._te.append(b64list) self._te.append(b64list)
self._te.append('\n# Uncompressed Data:')
outTxt = '{\n'
for i in dd:
if i in ('data','colors','palette'): continue
if type(dd[i]) == str:
outTxt += f" '{i}':'{dd[i]}',\n"
else:
outTxt += f" '{i}':{dd[i]},\n"
outTxt += " 'data':[\n"
for l in dd['data']:
outTxt += f" {l},\n"
outTxt += " ],'colors':[\n"
for l in dd['colors']:
outTxt += f" {l},\n"
if 'palette' in dd:
outTxt += " ],'palette':["
for i,l in enumerate(dd['palette']):
if not i%10:
outTxt += f"\n "
outTxt += f"{l},"
outTxt += "]}\n"
self._te.append(outTxt)
@ttk.pyTTkSlot()
def _exportDocument(self):
crop = self._cbCrop.isChecked()
palette = self._cbPal.isChecked()
full = self._cbFull.isChecked()
dd = self._paintArea.exportDocument(full=full,palette=palette,crop=crop)
if not dd:
self._te.setText('# No Data to be saved!!!')
return
self._te.setText('# Compressed Data:')
self._te.append('data = TTkUtil.base64_deflate_2_obj(')
b64str = ttk.TTkUtil.obj_inflate_2_base64(dd)
b64list = ' "' + '" +\n "'.join([b64str[i:i+128] for i in range(0,len(b64str),128)]) + '")'
self._te.append(b64list)
self._te.append('\n# Uncompressed Data:')
outTxt = '{\n'
for i in dd:
if i=='layers': continue
if type(dd[i]) == str:
outTxt += f" '{i}':'{dd[i]}',\n"
else:
outTxt += f" '{i}':{dd[i]},\n"
outTxt += " 'layers':[\n"
for l in dd['layers']:
outTxt += f" {l},\n"
outTxt += "]}\n"
self._te.append(outTxt)
# Layout: # Layout:
# #
@ -178,12 +237,14 @@ class ExportArea(ttk.TTkGridLayout):
# Export # Export
# #
class PaintTemplate(ttk.TTkAppTemplate): class PaintTemplate(ttk.TTkAppTemplate):
def __init__(self, border=False, **kwargs): __slots__ = ('_parea','_layers')
def __init__(self, fileName=None, border=False, **kwargs):
super().__init__(border, **kwargs) super().__init__(border, **kwargs)
self._parea = PaintArea() self._parea = parea = PaintArea()
self._layers = layers = Layers()
ptoolkit = PaintToolKit() ptoolkit = PaintToolKit()
tarea = TextArea() tarea = TextArea()
expArea = ExportArea(self._parea) expArea = ExportArea(parea)
leftPanel = LeftPanel() leftPanel = LeftPanel()
palette = leftPanel.palette() palette = leftPanel.palette()
@ -192,21 +253,24 @@ class PaintTemplate(ttk.TTkAppTemplate):
rightPanel.addWidget(tarea) rightPanel.addWidget(tarea)
# rightPanel.addItem(expArea, title="Export") # rightPanel.addItem(expArea, title="Export")
# rightPanel.setSizes([None,5]) # rightPanel.setSizes([None,5])
rightPanel.addItem(layers, title='Layers')
rightPanel.setSizes([None,9])
self.setItem(expArea, self.BOTTOM, title="Export") self.setItem(expArea, self.BOTTOM, title="Export")
self.setItem(leftPanel , self.LEFT, size=16*2) self.setItem(leftPanel , self.LEFT, size=16*2)
self.setWidget(self._parea , self.MAIN) self.setWidget(PaintScrollArea(parea) , self.MAIN)
self.setItem(ptoolkit , self.TOP, fixed=True) self.setWidget(ptoolkit , self.TOP, fixed=True)
self.setItem(rightPanel , self.RIGHT, size=50) self.setItem(rightPanel , self.RIGHT, size=40)
self.setMenuBar(appMenuBar:=ttk.TTkMenuBarLayout(), self.TOP) self.setMenuBar(appMenuBar:=ttk.TTkMenuBarLayout(), self.TOP)
fileMenu = appMenuBar.addMenu("&File") fileMenu = appMenuBar.addMenu("&File")
buttonOpen = fileMenu.addMenu("&Open") fileMenu.addMenu("&Open" ).menuButtonClicked.connect(self._open)
buttonClose = fileMenu.addMenu("&Save") fileMenu.addMenu("&Save" ).menuButtonClicked.connect(self._save)
buttonClose = fileMenu.addMenu("Save &As...") fileMenu.addMenu("Save &As...").menuButtonClicked.connect(self._saveAs)
fileMenu.addSpacer() fileMenu.addSpacer()
fileMenu.addMenu("&Import").menuButtonClicked.connect(self.importDictWin) fileMenu.addMenu("&Import").menuButtonClicked.connect(self.importDictWin)
menuExport = fileMenu.addMenu("&Export")
fileMenu.addSpacer() fileMenu.addSpacer()
fileMenu.addMenu("Load Palette") fileMenu.addMenu("Load Palette")
fileMenu.addMenu("Save Palette") fileMenu.addMenu("Save Palette")
@ -214,13 +278,24 @@ class PaintTemplate(ttk.TTkAppTemplate):
buttonExit = fileMenu.addMenu("E&xit") buttonExit = fileMenu.addMenu("E&xit")
buttonExit.menuButtonClicked.connect(ttk.TTkHelper.quit) buttonExit.menuButtonClicked.connect(ttk.TTkHelper.quit)
menuExport.addMenu("&Ascii/Txt").menuButtonClicked.connect(self._saveAsAscii)
menuExport.addMenu("&Ansi").menuButtonClicked.connect(self._saveAsAnsi)
menuExport.addMenu("&Python")
menuExport.addMenu("&Bash")
# extraMenu = appMenuBar.addMenu("E&xtra") # extraMenu = appMenuBar.addMenu("E&xtra")
# extraMenu.addMenu("Scratchpad").menuButtonClicked.connect(self.scratchpad) # extraMenu.addMenu("Scratchpad").menuButtonClicked.connect(self.scratchpad)
# extraMenu.addSpacer() # extraMenu.addSpacer()
def _showAbout(btn):
ttk.TTkHelper.overlay(None, About(), 30,10)
def _showAboutTTk(btn):
ttk.TTkHelper.overlay(None, ttk.TTkAbout(), 30,10)
helpMenu = appMenuBar.addMenu("&Help", alignment=ttk.TTkK.RIGHT_ALIGN) helpMenu = appMenuBar.addMenu("&Help", alignment=ttk.TTkK.RIGHT_ALIGN)
helpMenu.addMenu("About ...").menuButtonClicked helpMenu.addMenu("About ...").menuButtonClicked.connect(_showAboutTTk)
helpMenu.addMenu("About tlogg").menuButtonClicked helpMenu.addMenu("About DPT").menuButtonClicked.connect(_showAbout)
palette.colorSelected.connect(self._parea.setGlyphColor) palette.colorSelected.connect(self._parea.setGlyphColor)
palette.colorSelected.connect(ptoolkit.setColor) palette.colorSelected.connect(ptoolkit.setColor)
@ -233,9 +308,107 @@ class PaintTemplate(ttk.TTkAppTemplate):
self._parea.setGlyphColor(palette.color()) self._parea.setGlyphColor(palette.color())
ptoolkit.setColor(palette.color()) ptoolkit.setColor(palette.color())
parea.selectedLayer.connect(ptoolkit.updateLayer)
@ttk.pyTTkSlot(LayerData)
def _layerSelected(l:LayerData):
parea.setCurrentLayer(l.data())
layers.layerAdded.connect(self._layerAdded)
layers.layerSelected.connect(_layerSelected)
layers.layersOrderChanged.connect(self._layersOrderChanged)
layers.addLayer(name="Background")
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):
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()
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)
dd = json.load(fp)
if 'layers' in dd:
self.importDocument(dd)
else:
self._layers.addLayer(name="Import")
self._parea.importLayer(dd)
# Connect and handle Layers event
@ttk.pyTTkSlot(LayerData)
def _layerAdded(self, l:LayerData):
nl = self._parea.newLayer()
nl.setName(l.name())
l.setData(nl)
l.nameChanged.connect(nl.setName)
l.visibilityToggled.connect(nl.setVisible)
l.visibilityToggled.connect(self._parea.update)
@ttk.pyTTkSlot(list[LayerData])
def _layersOrderChanged(self, layers:list[LayerData]):
self._parea._canvasLayers = [ld.data() for ld in reversed(layers)]
self._parea.update()
def importDocument(self, dd):
self._parea.importDocument(dd)
self._layers.clear()
# Little Hack that I don't know how to overcome
self._layers.layerAdded.disconnect(self._layerAdded)
for l in self._parea.canvasLayers():
ld = self._layers.addLayer(name=l.name(),data=l)
ld.nameChanged.connect(l.setName)
ld.visibilityToggled.connect(l.setVisible)
ld.visibilityToggled.connect(self._parea.update)
self._layers.layerAdded.connect(self._layerAdded)
@ttk.pyTTkSlot() @ttk.pyTTkSlot()
def importDictWin(self): def importDictWin(self):
newWindow = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"quickImport.tui.json")) newWindow = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"tui/quickImport.tui.json"))
te = newWindow.getWidgetByName("TextEdit") te = newWindow.getWidgetByName("TextEdit")
@ttk.pyTTkSlot() @ttk.pyTTkSlot()
@ -266,7 +439,11 @@ class PaintTemplate(ttk.TTkAppTemplate):
ttk.TTkHelper.overlay(None, messageBox, 5, 5, True) ttk.TTkHelper.overlay(None, messageBox, 5, 5, True)
return return
self._parea.importLayer(dd) if 'layers' in dd:
self.importDocument(dd)
else:
self._layers.addLayer(name="Import")
self._parea.importLayer(dd)
newWindow.close() newWindow.close()

593
tools/dumb_paint_lib/paintarea.py

@ -20,227 +20,324 @@
# 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__ = ['PaintArea','PaintToolKit'] __all__ = ['PaintArea','PaintScrollArea','CanvasLayer']
import sys, os import sys, os
sys.path.append(os.path.join(sys.path[0],'../..')) sys.path.append(os.path.join(sys.path[0],'../..'))
import TermTk as ttk import TermTk as ttk
from .canvaslayer import CanvasLayer
class PaintToolKit(ttk.TTkGridLayout): class PaintArea(ttk.TTkAbstractScrollView):
__slots__ = ('_canvasLayers', '_currentLayer',
__slots__ = ('_rSelect', '_rPaint', '_lgliph',
'_cbFg', '_cbBg',
'_bpFg', '_bpBg', '_bpDef',
'_glyph',
#Signals
'updatedColor', 'updatedTrans')
def __init__(self, *args, **kwargs):
self._glyph = 'X'
self.updatedColor = ttk.pyTTkSignal(ttk.TTkColor)
self.updatedTrans = ttk.pyTTkSignal(ttk.TTkColor)
super().__init__(*args, **kwargs)
self._rSelect = ttk.TTkRadioButton(text='Select ' , maxWidth=10)
self._rPaint = ttk.TTkRadioButton(text='Paint ' )
self._lgliph = ttk.TTkLabel(text="" , maxWidth=8)
self._cbFg = ttk.TTkCheckbox(text="Fg" , maxWidth= 6)
self._cbBg = ttk.TTkCheckbox(text="Bg" )
self._bpFg = ttk.TTkColorButtonPicker(enabled=False, maxWidth= 6)
self._bpBg = ttk.TTkColorButtonPicker(enabled=False, )
self._bpDef = ttk.TTkColorButtonPicker(color=ttk.TTkColor.bg('#FF00FF'), maxWidth=6)
self.addWidget(self._rSelect ,0,0)
self.addWidget(self._rPaint ,1,0)
self.addWidget(self._lgliph ,0,1,2,1)
self.addWidget(self._cbFg ,0,2)
self.addWidget(self._cbBg ,1,2)
self.addWidget(self._bpFg ,0,3)
self.addWidget(self._bpBg ,1,3)
self.addWidget(ttk.TTkLabel(text=" Trans:", maxWidth=7) ,1,4)
self.addWidget(self._bpDef ,1,5)
self.addItem(ttk.TTkLayout() ,0,6,2,1)
self._cbFg.toggled.connect(self._bpFg.setEnabled)
self._cbBg.toggled.connect(self._bpBg.setEnabled)
self._cbFg.toggled.connect(self._refreshColor)
self._cbBg.toggled.connect(self._refreshColor)
self._bpFg.colorSelected.connect(self._refreshColor)
self._bpBg.colorSelected.connect(self._refreshColor)
self._bpDef.colorSelected.connect(self.updatedTrans.emit)
self._refreshColor(emit=False)
@ttk.pyTTkSlot()
def _refreshColor(self, emit=True):
color =self.color()
self._lgliph.setText(
ttk.TTkString("Glyph\n '") +
ttk.TTkString(self._glyph,color) +
ttk.TTkString("'"))
if emit:
self.updatedColor.emit(color)
@ttk.pyTTkSlot(ttk.TTkString)
def glyphFromString(self, ch:ttk.TTkString):
if len(ch)<=0: return
self._glyph = ch.charAt(0)
self._refreshColor()
# self.setColor(ch.colorAt(0))
def color(self):
color = ttk.TTkColor()
if self._cbFg.checkState() == ttk.TTkK.Checked:
color += self._bpFg.color().invertFgBg()
if self._cbBg.checkState() == ttk.TTkK.Checked:
color += self._bpBg.color()
return color
@ttk.pyTTkSlot(ttk.TTkColor)
def setColor(self, color:ttk.TTkColor):
if fg := color.foreground():
self._cbFg.setCheckState(ttk.TTkK.Checked)
self._bpFg.setEnabled()
self._bpFg.setColor(fg.invertFgBg())
else:
self._cbFg.setCheckState(ttk.TTkK.Unchecked)
self._bpFg.setDisabled()
if bg := color.background():
self._cbBg.setCheckState(ttk.TTkK.Checked)
self._bpBg.setEnabled()
self._bpBg.setColor(bg)
else:
self._cbBg.setCheckState(ttk.TTkK.Unchecked)
self._bpBg.setDisabled()
self._refreshColor(emit=False)
class PaintArea(ttk.TTkWidget):
class Tool(int):
BRUSH = 0x01
RECTFILL = 0x02
RECTEMPTY = 0x03
__slots__ = ('_canvasArea', '_canvasSize',
'_transparentColor', '_transparentColor',
'_mouseMove', '_mouseFill', '_tool', '_documentPos','_documentSize',
'_glyph', '_glyphColor') '_mouseMove', '_mouseDrag', '_mousePress', '_mouseRelease',
'_moveData','_resizeData',
'_tool',
'_glyph', '_glyphColor',
# Signals
'selectedLayer')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._transparentColor = ttk.TTkColor.bg('#FF00FF') self.selectedLayer = ttk.pyTTkSignal(CanvasLayer)
self._canvasSize = (0,0) self._transparentColor = {'base':ttk.TTkColor.RST,'dim':ttk.TTkColor.RST}
self._canvasArea = {'data':[],'colors':[]} self._currentLayer:CanvasLayer = None
self._canvasLayers:list[CanvasLayer] = []
self._glyph = 'X' self._glyph = 'X'
self._glyphColor = ttk.TTkColor.RST self._glyphColor = ttk.TTkColor.RST
self._moveData = None
self._resizeData = None
self._mouseMove = None self._mouseMove = None
self._mouseFill = None self._mouseDrag = None
self._tool = self.Tool.BRUSH self._mousePress = None
self._mouseRelease = None
self._tool = CanvasLayer.Tool.BRUSH
self._documentPos = (6,3)
self._documentSize = ( 0, 0)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.setTrans(ttk.TTkColor.bg('#FF00FF'))
self.resizeCanvas(80,25) self.resizeCanvas(80,25)
self.setFocusPolicy(ttk.TTkK.ClickFocus + ttk.TTkK.TabFocus) self.setFocusPolicy(ttk.TTkK.ClickFocus + ttk.TTkK.TabFocus)
def _getGeometry(self):
dx,dy = self._documentPos
dw,dh = self._documentSize
ww,wh = self.size()
x1,y1 = min(0,dx),min(0,dy)
x2,y2 = max(dx+dw,ww),max(dy+dh,wh)
for l in self._canvasLayers:
lx,ly = l.pos()
lw,lh = l.size()
x1 = min(x1,dx+lx)
y1 = min(y1,dy+ly)
x2 = max(x2,dx+lx+lw)
y2 = max(y2,dy+ly+lh)
# ttk.TTkLog.debug(f"{x1=},{y1=},{x2-x1=},{y2-y1=}")
return x1,y1,x2-x1,y2-y1
def _retuneGeometry(self):
ox, oy = self.getViewOffsets()
dx,dy = self._documentPos
x1,y1,_,_ = self._getGeometry()
dx1,dy1 = max(0,dx,dx-x1),max(0,dy,dy-y1)
self._documentPos = dx1,dy1
self.viewChanged.emit()
# dx,dy = self._documentPos
# self.chan
self.viewMoveTo(ox+dx1-dx,oy+dy1-dy)
# If the area move to be adapted to the
# Negative coordinates, the reference values used in
# mouse press, moveData, resizeData need to be
# adapted to the new topology
if mp:=self._mousePress:
mpx,mpy = mp
self._mousePress = (mpx-x1,mpy-y1)
if md:=self._moveData:
mx,my=md['pos']
md['pos']=(mx+dx1-dx,my+dy1-dy)
# if rd:=self._resizeData:
# rx,ry,rw,rh=rd['geometry']
# # rd['geometry']=(rx+dx1-dx,ry+dy1-dy,rw,rh)
def viewFullAreaSize(self) -> tuple[int,int]:
_,_,w,h = self._getGeometry()
return w,h
def viewDisplayedSize(self) -> tuple:
return self.size()
def maximumWidth(self): return 0x10000
def maximumHeight(self): return 0x10000
def minimumWidth(self): return 0
def minimumHeight(self): return 0
def canvasLayers(self):
return self._canvasLayers
def resizeCanvas(self, w, h): def resizeCanvas(self, w, h):
self._canvasSize = (w,h) self._documentSize = (w,h)
self._canvasArea['data'] = (self._canvasArea['data'] + [[] for _ in range(h)])[:h] self._retuneGeometry()
self._canvasArea['colors'] = (self._canvasArea['colors'] + [[] for _ in range(h)])[:h]
for i in range(h):
self._canvasArea['data'][i] = (self._canvasArea['data'][i] + [' ' for _ in range(w)])[:w]
self._canvasArea['colors'][i] = (self._canvasArea['colors'][i] + [ttk.TTkColor.RST for _ in range(w)])[:w]
self.update() self.update()
def clean(self): def superResize(self,x,y,w,h):
w,h = self._canvasSize dx,dy = self._documentPos
for i in range(h): dw,dh = self._documentSize
self._canvasArea['data'][i] = [' ']*w if (x,y,w,h) == (dx,dy,dw,dh): return
self._canvasArea['colors'][i] = [ttk.TTkColor.RST]*w if w<0: x=dx;w=dw
if h<0: y=dy;h=dh
if self._documentPos != (x,y):
diffx = dx-x
diffy = dy-y
for l in self._canvasLayers:
lx,ly = l.pos()
l.move(lx+diffx,ly+diffy)
self._documentPos = (x,y)
self._documentSize = (w,h)
self.update()
def setCurrentLayer(self, layer:CanvasLayer):
self._currentLayer = layer
def newLayer(self) -> CanvasLayer:
newLayer = CanvasLayer()
w,h = self._documentSize
w,h = (w,h) if (w,h)!=(0,0) else (80,24)
newLayer.resize(w,h)
self._currentLayer = newLayer
self._canvasLayers.append(newLayer)
return newLayer
def importLayer(self, dd): def importLayer(self, dd):
w,h = self._canvasSize self._currentLayer.importLayer(dd)
w = len(dd['data'][0]) + 10 self._retuneGeometry()
h = len(dd['data']) + 4
x,y=5,2 def importDocument(self, dd):
self._canvasLayers = []
self.resizeCanvas(w,h) if (
self.clean() ( 'version' in dd and dd['version'] == '1.0.0' ) or
( 'version' in dd and dd['version'] == '1.0.1' and dd['type'] == 'DumbPaintTool/Document') ):
for i,rd in enumerate(dd['data']): self.resizeCanvas(*dd['size'])
for ii,cd in enumerate(rd): for l in dd['layers']:
self._canvasArea['data'][i+y][ii+x] = cd nl = self.newLayer()
for i,rd in enumerate(dd['colors']): nl.importLayer(l)
for ii,cd in enumerate(rd): else:
fg,bg = cd ttk.TTkLog.error("File Format not recognised")
if fg and bg: self._retuneGeometry()
self._canvasArea['colors'][i+y][ii+x] = ttk.TTkColor.fg(fg)+ttk.TTkColor.bg(bg)
elif fg: def exportImage(self):
self._canvasArea['colors'][i+y][ii+x] = ttk.TTkColor.fg(fg) return {}
elif bg:
self._canvasArea['colors'][i+y][ii+x] = ttk.TTkColor.bg(bg) def exportLayer(self, full=False, palette=True, crop=True) -> dict:
else: if self._currentLayer:
self._canvasArea['colors'][i+y][ii+x] = ttk.TTkColor.RST return self._currentLayer.exportLayer(full=full,palette=palette,crop=crop)
self.update() return {}
def exportDocument(self, full=True, palette=True, crop=True) -> dict:
pw,ph = self._documentSize
outData = {
'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
def exportImage(self) -> dict:
pw,ph = self._documentSize
image = ttk.TTkCanvas(width=pw,height=ph)
for l in self._canvasLayers:
lx,ly = l.pos()
l.drawInCanvas(pos=(lx,ly),canvas=image)
return image.toAnsi()
def leaveEvent(self, evt): def leaveEvent(self, evt):
self._mouseMove = None self._mouseMove = None
self._moveData = None
self.update() self.update()
return super().leaveEvent(evt) return super().leaveEvent(evt)
@ttk.pyTTkSlot(Tool) @ttk.pyTTkSlot(CanvasLayer.Tool)
def setTool(self, tool): def setTool(self, tool):
self._tool = tool self._tool = tool
self.update() self.update()
def mouseMoveEvent(self, evt) -> bool: def _handleAction(self):
# self._mouseFill = None dx,dy = self._documentPos
x,y = evt.x, evt.y dw,dh = self._documentSize
w,h = self._canvasSize ox, oy = self.getViewOffsets()
if 0<=x<w and 0<=y<h: mp = self._mousePress
self._mouseMove = (x, y) mm = self._mouseMove
self.update() md = self._mouseDrag
return True mr = self._mouseRelease
self._mouseMove = None l = self._currentLayer
lx,ly = l.pos()
if self._tool & CanvasLayer.Tool.MOVE and mp and not md:
if self._tool & CanvasLayer.Tool.RESIZE and not md:
mpx,mpy = mp
self._resizeData = None
def _getSelected(_x,_y,_w,_h):
_selected = ttk.TTkK.NONE
if _x <= mpx < _x+_w and mpy == _y: _selected |= ttk.TTkK.TOP
if _x <= mpx < _x+_w and mpy == _y+_h-1: _selected |= ttk.TTkK.BOTTOM
if _y <= mpy < _y+_h and mpx == _x: _selected |= ttk.TTkK.LEFT
if _y <= mpy < _y+_h and mpx == _x+_w-1: _selected |= ttk.TTkK.RIGHT
return _selected
# Main Area Resize Borders
if selected := _getSelected(dx-1,dy-1,dw+2,dh+2):
self._resizeData = {'type':PaintArea,'selected':selected,'cb':self.superResize,'geometry':(dx,dy,dw,dh)}
elif l:
# Selected Layer Resize Borders
lx,ly = l.pos()
lw,lh = l.size()
if selected := _getSelected(dx+lx-1,dy+ly-1,lw+2,lh+2):
self._resizeData = {'type':CanvasLayer,'selected':selected,'cb':l.superResize,'geometry':(lx,ly,lw,lh)}
if not self._resizeData:
# Get The Layer to Move
self._moveData = None
for lm in reversed(self._canvasLayers):
mpx,mpy = mp
lmx,lmy = lm.pos()
self._moveData = {'type':PaintArea,'pos':(dx,dy)}
if lm.isOpaque(mpx-lmx-dx,mpy-lmy-dy):
self._currentLayer = lm
tml = lm
self._moveData = {'type':CanvasLayer,'pos':tml.pos(),'layer':tml}
self.selectedLayer.emit(lm)
break
elif self._tool & CanvasLayer.Tool.MOVE and mp and md:
# Move/Resize Tool
if self._tool & CanvasLayer.Tool.RESIZE and (rData:=self._resizeData):
_rx,_ry,_rw,_rh = rData['geometry']
_rdx,_rdy,_rdw,_rdh=(_rx,_ry,_rw,_rh)
mpx,mpy = mp
mdx,mdy = md
diffx = mdx-mpx
diffy = mdy-mpy
if rData['selected'] & ttk.TTkK.TOP: _rdh-=diffy ; _rdy+=diffy
if rData['selected'] & ttk.TTkK.BOTTOM: _rdh+=diffy
if rData['selected'] & ttk.TTkK.LEFT: _rdw-=diffx ; _rdx+=diffx
if rData['selected'] & ttk.TTkK.RIGHT: _rdw+=diffx
rData['cb'](_rdx,_rdy,_rdw,_rdh)
if not self._resizeData and (mData:=self._moveData):
mpx,mpy = mp
mdx,mdy = md
pdx,pdy = mdx-mpx,mdy-mpy
if mData['type']==CanvasLayer:
px,py = self._moveData['pos']
self._moveData['layer'].move(px+pdx,py+pdy)
self.selectedLayer.emit(self._moveData['layer'])
elif mData['type']==PaintArea:
px,py = self._moveData['pos']
self._documentPos = (px+pdx,py+pdy)
self._retuneGeometry()
elif self._tool == CanvasLayer.Tool.BRUSH:
if mp or md:
if md: mx,my = md
else: mx,my = mp
preview=False
self._currentLayer.placeGlyph(mx-lx-dx,my-ly-dy,self._glyph,self._glyphColor,preview)
elif mm:
mx,my = mm
preview=True
self._currentLayer.placeGlyph(mx-lx-dx,my-ly-dy,self._glyph,self._glyphColor,preview)
elif self._tool in (CanvasLayer.Tool.RECTEMPTY, CanvasLayer.Tool.RECTFILL):
if mr and mp:
mpx,mpy = mp
mrx,mry = mr
preview=False
self._currentLayer.placeFill((mpx-lx-dx,mpy-ly-dy,mrx-lx-dx,mry-ly-dy),self._tool,self._glyph,self._glyphColor,preview)
elif md:
mpx,mpy = mp
mrx,mry = md
preview=True
self._currentLayer.placeFill((mpx-lx-dx,mpy-ly-dy,mrx-lx-dx,mry-ly-dy),self._tool,self._glyph,self._glyphColor,preview)
self.update() self.update()
return super().mouseMoveEvent(evt)
def mouseMoveEvent(self, evt) -> bool:
ox, oy = self.getViewOffsets()
self._mouseMove = (evt.x+ox,evt.y+oy)
self._mouseDrag = None
self._handleAction()
return True
def mouseDragEvent(self, evt) -> bool: def mouseDragEvent(self, evt) -> bool:
x,y = evt.x,evt.y ox, oy = self.getViewOffsets()
if self._tool == self.Tool.BRUSH: self._mouseDrag=(evt.x+ox,evt.y+oy)
if self._placeGlyph(evt.x, evt.y): self._mouseMove= None
return True self._handleAction()
if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL) and self._mouseFill: return True
mx,my = self._mouseFill[:2]
self._mouseFill = [mx,my,x,y]
self.update()
return True
return super().mouseDragEvent(evt)
def mousePressEvent(self, evt) -> bool: def mousePressEvent(self, evt) -> bool:
x,y = evt.x,evt.y ox, oy = self.getViewOffsets()
if self._tool == self.Tool.BRUSH: self._mousePress=(evt.x+ox,evt.y+oy)
if self._placeGlyph(x,y): self._moveData = None
return True self._mouseMove = None
if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL): self._mouseDrag = None
self._mouseFill = [x,y,x,y] self._mouseRelease = None
self.update() self._handleAction()
return True return True
return super().mousePressEvent(evt)
def mouseReleaseEvent(self, evt) -> bool: def mouseReleaseEvent(self, evt) -> bool:
x,y = evt.x,evt.y ox, oy = self.getViewOffsets()
if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL): self._mouseRelease=(evt.x+ox,evt.y+oy)
self._placeFill() self._mouseMove = None
self.update() self._handleAction()
return True self._moveData = None
self._mouseFill = None self._resizeData = None
self._mousePress = None
self._mouseDrag = None
self._mouseRelease = None
return super().mousePressEvent(evt) return super().mousePressEvent(evt)
@ttk.pyTTkSlot(ttk.TTkString) @ttk.pyTTkSlot(ttk.TTkString)
def glyphFromString(self, ch:ttk.TTkString): def glyphFromString(self, ch:ttk.TTkString):
if len(ch)<=0: return if len(ch)<=0: return
self._glyph = ch.charAt(0) self._glyph = ch.charAt(0)
# self._glyphColor = ch.colorAt(0)
def glyph(self): def glyph(self):
return self._glyph return self._glyph
@ -258,85 +355,71 @@ class PaintArea(ttk.TTkWidget):
self._glyphColor = color self._glyphColor = color
@ttk.pyTTkSlot(ttk.TTkColor) @ttk.pyTTkSlot(ttk.TTkColor)
def setTrans(self, color): def setTrans(self, color:ttk.TTkColor):
self._transparentColor = color r,g,b = color.bgToRGB()
self._transparentColor = {
'base':color,
'dim': ttk.TTkColor.bg(f'#{int(r*0.3):02x}{int(g*0.3):02x}{int(b*0.3):02x}'),
'layer': ttk.TTkColor.bg(f'#{int(r*0.6):02x}{int(g*0.6):02x}{int(b*0.6):02x}'),
'layerDim':ttk.TTkColor.bg(f'#{int(r*0.2):02x}{int(g*0.2):02x}{int(b*0.2):02x}')}
self.update() self.update()
def _placeFill(self): def paintEvent(self, canvas:ttk.TTkCanvas):
if not self._mouseFill: return False dx,dy = self._documentPos
w,h = self._canvasSize ox, oy = self.getViewOffsets()
ax,ay,bx,by = self._mouseFill dw,dh = self._documentSize
ax = max(0,min(w-1,ax)) dox,doy = dx-ox,dy-oy
ay = max(0,min(h-1,ay))
bx = max(0,min(w-1,bx))
by = max(0,min(h-1,by))
fax,fay = min(ax,bx), min(ay,by)
fbx,fby = max(ax,bx), max(ay,by)
self._mouseFill = None
self._mouseMove = None
data = self._canvasArea['data']
colors = self._canvasArea['colors']
glyph = self._glyph
color = self._glyphColor
if self._tool == self.Tool.RECTFILL:
for row in data[fay:fby+1]:
row[fax:fbx+1] = [glyph]*(fbx-fax+1)
for row in colors[fay:fby+1]:
row[fax:fbx+1] = [color]*(fbx-fax+1)
if self._tool == self.Tool.RECTEMPTY:
data[fay][fax:fbx+1] = [glyph]*(fbx-fax+1)
data[fby][fax:fbx+1] = [glyph]*(fbx-fax+1)
colors[fay][fax:fbx+1] = [color]*(fbx-fax+1)
colors[fby][fax:fbx+1] = [color]*(fbx-fax+1)
for row in data[fay:fby]:
row[fax]=row[fbx]=glyph
for row in colors[fay:fby]:
row[fax]=row[fbx]=color
self.update()
return True
def _placeGlyph(self,x,y):
self._mouseMove = None
w,h = self._canvasSize
data = self._canvasArea['data']
colors = self._canvasArea['colors']
if 0<=x<w and 0<=y<h:
data[y][x] = self._glyph
colors[y][x] = self._glyphColor
self.update()
return True
return False
def paintEvent(self, canvas: ttk.TTkCanvas):
pw,ph = self._canvasSize
cw,ch = canvas.size() cw,ch = canvas.size()
w=min(cw,pw) w=min(cw,dw)
h=min(ch,ph) h=min(ch,dh)
data = self._canvasArea['data'] tcb = self._transparentColor['base']
colors = self._canvasArea['colors'] tcd = self._transparentColor['dim']
tc = self._transparentColor
for y in range(h): if l:=self._currentLayer:
canvas._data[y][0:w] = data[y][0:w] tclb = self._transparentColor['layer']
for x in range(w): tcld = self._transparentColor['layerDim']
c = colors[y][x] lx,ly = l.pos()
canvas._colors[y][x] = c if c._bg else c+tc lw,lh = l.size()
if self._mouseMove: canvas.fill(pos=(0 ,ly+doy),size=(cw,lh),color=tcld)
x,y = self._mouseMove canvas.fill(pos=(lx+dox,0 ),size=(lw,ch),color=tcld)
gc = self._glyphColor canvas.fill(pos=(lx+dox,ly+doy),size=(lw,lh),color=tclb)
canvas._data[y][x] = self._glyph canvas.fill(pos=(dx-ox,dy-oy),size=(dw,dh),color=tcb)
canvas._colors[y][x] = gc if gc._bg else gc+tc canvas.fill(pos=(0 ,dy-oy-1), size=(cw,1),color=tcd)
if self._mouseFill: canvas.fill(pos=(0 ,dy-oy+dh),size=(cw,1),color=tcd)
ax,ay,bx,by = self._mouseFill canvas.fill(pos=(dx-ox-2 ,0 ),size=(2,ch),color=tcd)
ax = max(0,min(w-1,ax)) canvas.fill(pos=(dx-ox+dw,0 ),size=(2,ch),color=tcd)
ay = max(0,min(h-1,ay))
bx = max(0,min(w-1,bx)) for l in self._canvasLayers:
by = max(0,min(h-1,by)) lx,ly = l.pos()
x,y = min(ax,bx), min(ay,by) l.drawInCanvas(pos=(lx+dox,ly+doy),canvas=canvas)
w,h = max(ax-x,bx-x)+1, max(ay-y,by-y)+1
gc = self._glyphColor if self._tool & CanvasLayer.Tool.RESIZE:
canvas.fill(pos=(x,y), size=(w,h), rd = self._resizeData
color=gc if gc._bg else gc+tc, def _drawResizeBorders(_rx,_ry,_rw,_rh,_sel):
char=self._glyph) selColor = ttk.TTkColor.YELLOW + ttk.TTkColor.BG_BLUE
# canvas.drawBox(pos=_pos,size=_size)
canvas.drawText(pos=(_rx ,_ry ),text=''*_rw, color=selColor if _sel & ttk.TTkK.TOP else ttk.TTkColor.RST)
canvas.drawText(pos=(_rx ,_ry+_rh-1),text=''*_rw, color=selColor if _sel & ttk.TTkK.BOTTOM else ttk.TTkColor.RST)
for _y in range(_ry,_ry+_rh):
canvas.drawText(pos=(_rx ,_y),text='',color=selColor if _sel & ttk.TTkK.LEFT else ttk.TTkColor.RST)
canvas.drawText(pos=(_rx+_rw-1,_y),text='',color=selColor if _sel & ttk.TTkK.RIGHT else ttk.TTkColor.RST)
canvas.drawChar(pos=(_rx ,_ry ), char='')
canvas.drawChar(pos=(_rx+_rw-1,_ry ), char='')
canvas.drawChar(pos=(_rx ,_ry+_rh-1), char='')
canvas.drawChar(pos=(_rx+_rw-1,_ry+_rh-1), char='')
sMain = rd['selected'] if rd and rd['type'] == PaintArea else ttk.TTkK.NONE
sLayer = rd['selected'] if rd and rd['type'] == CanvasLayer else ttk.TTkK.NONE
_drawResizeBorders(dx-ox-1, dy-oy-1, dw+2, dh+2, sMain)
if l:=self._currentLayer:
lx,ly = l.pos()
lw,lh = l.size()
_drawResizeBorders(lx+dx-ox-1, ly+dy-oy-1, lw+2, lh+2, sLayer)
class PaintScrollArea(ttk.TTkAbstractScrollArea):
def __init__(self, pwidget:PaintArea, **kwargs):
super().__init__(**kwargs)
self.setViewport(pwidget)

123
tools/dumb_paint_lib/painttoolkit.py

@ -0,0 +1,123 @@
# 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__ = ['PaintToolKit']
import sys, os
sys.path.append(os.path.join(sys.path[0],'../..'))
import TermTk as ttk
from .paintarea import *
class PaintToolKit(ttk.TTkContainer):
__slots__ = ('_rSelect', '_rPaint', '_lgliph',
'_cbFg', '_cbBg',
'_bpFg', '_bpBg', '_bpDef',
'_sbDx','_sbDy','_sbDw','_sbDh',
'_sbLx','_sbLy','_sbLw','_sbLh',
'_glyph',
#Signals
'updatedColor', 'updatedTrans')
def __init__(self, *args, **kwargs):
ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"tui/paintToolKit.tui.json"),self)
self._glyph = 'X'
self.updatedColor = ttk.pyTTkSignal(ttk.TTkColor)
self.updatedTrans = ttk.pyTTkSignal(ttk.TTkColor)
self._lgliph = self.getWidgetByName("lglyph")
self._cbFg = self.getWidgetByName("cbFg")
self._cbBg = self.getWidgetByName("cbBg")
self._bpFg = self.getWidgetByName("bpFg")
self._bpBg = self.getWidgetByName("bpBg")
self._bpDef = self.getWidgetByName("bpDef")
self._sbDx = self.getWidgetByName("sbDx")
self._sbDy = self.getWidgetByName("sbDy")
self._sbDw = self.getWidgetByName("sbDw")
self._sbDh = self.getWidgetByName("sbDh")
self._sbLx = self.getWidgetByName("sbLx")
self._sbLy = self.getWidgetByName("sbLy")
self._sbLw = self.getWidgetByName("sbLw")
self._sbLh = self.getWidgetByName("sbLh")
self._bpDef.setColor(ttk.TTkColor.bg('#FF00FF'))
self._cbFg.toggled.connect(self._refreshColor)
self._cbBg.toggled.connect(self._refreshColor)
self._bpFg.colorSelected.connect(self._refreshColor)
self._bpBg.colorSelected.connect(self._refreshColor)
self._bpDef.colorSelected.connect(self.updatedTrans.emit)
self._refreshColor(emit=False)
@ttk.pyTTkSlot(CanvasLayer)
def updateLayer(self, layer:CanvasLayer):
lx,ly = layer.pos()
lw,lh = layer.size()
self._sbLx.setValue(lx)
self._sbLy.setValue(ly)
self._sbLw.setValue(lw)
self._sbLh.setValue(lh)
@ttk.pyTTkSlot()
def _refreshColor(self, emit=True):
color =self.color()
self._lgliph.setText(
ttk.TTkString("Glyph: '") +
ttk.TTkString(self._glyph,color) +
ttk.TTkString("'"))
if emit:
self.updatedColor.emit(color)
@ttk.pyTTkSlot(ttk.TTkString)
def glyphFromString(self, ch:ttk.TTkString):
if len(ch)<=0: return
self._glyph = ch.charAt(0)
self._refreshColor()
# self.setColor(ch.colorAt(0))
def color(self):
color = ttk.TTkColor()
if self._cbFg.checkState() == ttk.TTkK.Checked:
color += self._bpFg.color().invertFgBg()
if self._cbBg.checkState() == ttk.TTkK.Checked:
color += self._bpBg.color()
return color
@ttk.pyTTkSlot(ttk.TTkColor)
def setColor(self, color:ttk.TTkColor):
if fg := color.foreground():
self._cbFg.setCheckState(ttk.TTkK.Checked)
self._bpFg.setEnabled()
self._bpFg.setColor(fg.invertFgBg())
else:
self._cbFg.setCheckState(ttk.TTkK.Unchecked)
self._bpFg.setDisabled()
if bg := color.background():
self._cbBg.setCheckState(ttk.TTkK.Checked)
self._bpBg.setEnabled()
self._bpBg.setColor(bg)
else:
self._cbBg.setCheckState(ttk.TTkK.Unchecked)
self._bpBg.setDisabled()
self._refreshColor(emit=False)

1
tools/dumb_paint_lib/palette.py

@ -55,6 +55,7 @@ class Palette(ttk.TTkWidget):
self._mouseMove = None self._mouseMove = None
self.setPalette(_defaultPalette) self.setPalette(_defaultPalette)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.setFocusPolicy(ttk.TTkK.ClickFocus)
def setPalette(self, palette): def setPalette(self, palette):
self._palette = [] self._palette = []

918
tools/dumb_paint_lib/tui/paintToolKit.tui.json

@ -0,0 +1,918 @@
{
"version": "2.0.0",
"tui": {
"class": "TTkContainer",
"params": {
"Name": "MainWindow",
"Position": [
4,
2
],
"Size": [
78,
2
],
"Min Width": 50,
"Min Height": 2,
"Max Width": 65536,
"Max Height": 2,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Padding": [
0,
0,
0,
0
],
"Layout": "TTkLayout"
},
"layout": {
"class": "TTkLayout",
"params": {
"Geometry": [
0,
0,
78,
2
]
},
"children": [
{
"class": "TTkCheckbox",
"params": {
"Name": "cbFg",
"Position": [
12,
0
],
"Size": [
5,
1
],
"Min Width": 5,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 1,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "Fg",
"Tristate": false,
"Checked": false,
"Check State": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkCheckbox",
"params": {
"Name": "cbBg",
"Position": [
12,
1
],
"Size": [
5,
1
],
"Min Width": 5,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 1,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "Bg",
"Tristate": false,
"Checked": false,
"Check State": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-1",
"Position": [
0,
1
],
"Size": [
5,
1
],
"Min Width": 5,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "Trans",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "lglyph",
"Position": [
0,
0
],
"Size": [
12,
1
],
"Min Width": 6,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "Glyph:",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkColorButtonPicker",
"params": {
"Name": "bpDef",
"Position": [
5,
1
],
"Size": [
6,
1
],
"Min Width": 1,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 1,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "",
"Border": false,
"Checkable": false,
"Checked": false,
"Color": "\u001b[48;2;0;0;0m"
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-2",
"Position": [
25,
0
],
"Size": [
3,
1
],
"Min Width": 3,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "Doc",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkColorButtonPicker",
"params": {
"Name": "bpBg",
"Position": [
17,
1
],
"Size": [
6,
1
],
"Min Width": 2,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 1,
"Visible": true,
"Enabled": false,
"ToolTip": "",
"Text": "",
"Border": false,
"Checkable": false,
"Checked": false,
"Color": "\u001b[48;2;0;0;0m"
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkColorButtonPicker",
"params": {
"Name": "bpFg",
"Position": [
17,
0
],
"Size": [
6,
1
],
"Min Width": 2,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 1,
"Visible": true,
"Enabled": false,
"ToolTip": "",
"Text": "",
"Border": false,
"Checkable": false,
"Checked": false,
"Color": "\u001b[48;2;0;0;0m"
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-4",
"Position": [
29,
0
],
"Size": [
1,
1
],
"Min Width": 1,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "x",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-5",
"Position": [
29,
1
],
"Size": [
1,
1
],
"Min Width": 1,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "y",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkSpinBox",
"params": {
"Name": "sbDx",
"Position": [
31,
0
],
"Size": [
6,
1
],
"Min Width": 4,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Padding": [
0,
0,
0,
2
],
"Layout": "TTkGridLayout",
"Value": 0,
"Minimum": -100,
"Maximum": 100
},
"layout": {
"class": "TTkGridLayout",
"params": {
"Geometry": [
0,
0,
4,
1
]
},
"children": []
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkSpinBox",
"params": {
"Name": "sbDy",
"Position": [
31,
1
],
"Size": [
6,
1
],
"Min Width": 4,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Padding": [
0,
0,
0,
2
],
"Layout": "TTkGridLayout",
"Value": 0,
"Minimum": -100,
"Maximum": 100
},
"layout": {
"class": "TTkGridLayout",
"params": {
"Geometry": [
0,
0,
4,
1
]
},
"children": []
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-6",
"Position": [
38,
0
],
"Size": [
1,
1
],
"Min Width": 1,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "w",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-7",
"Position": [
38,
1
],
"Size": [
1,
1
],
"Min Width": 1,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "h",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkSpinBox",
"params": {
"Name": "sbDw",
"Position": [
40,
0
],
"Size": [
6,
1
],
"Min Width": 4,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Padding": [
0,
0,
0,
2
],
"Layout": "TTkGridLayout",
"Value": 0,
"Minimum": 0,
"Maximum": 99
},
"layout": {
"class": "TTkGridLayout",
"params": {
"Geometry": [
0,
0,
4,
1
]
},
"children": []
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkSpinBox",
"params": {
"Name": "sbDh",
"Position": [
40,
1
],
"Size": [
6,
1
],
"Min Width": 4,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Padding": [
0,
0,
0,
2
],
"Layout": "TTkGridLayout",
"Value": 0,
"Minimum": 0,
"Maximum": 99
},
"layout": {
"class": "TTkGridLayout",
"params": {
"Geometry": [
0,
0,
4,
1
]
},
"children": []
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-3",
"Position": [
48,
0
],
"Size": [
5,
1
],
"Min Width": 5,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "Layer",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-8",
"Position": [
54,
0
],
"Size": [
1,
1
],
"Min Width": 1,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "x",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-9",
"Position": [
54,
1
],
"Size": [
1,
1
],
"Min Width": 1,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "y",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkSpinBox",
"params": {
"Name": "sbLx",
"Position": [
56,
0
],
"Size": [
6,
1
],
"Min Width": 4,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Padding": [
0,
0,
0,
2
],
"Layout": "TTkGridLayout",
"Value": 0,
"Minimum": -100,
"Maximum": 100
},
"layout": {
"class": "TTkGridLayout",
"params": {
"Geometry": [
0,
0,
4,
1
]
},
"children": []
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkSpinBox",
"params": {
"Name": "sbLy",
"Position": [
56,
1
],
"Size": [
6,
1
],
"Min Width": 4,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Padding": [
0,
0,
0,
2
],
"Layout": "TTkGridLayout",
"Value": 0,
"Minimum": -100,
"Maximum": 100
},
"layout": {
"class": "TTkGridLayout",
"params": {
"Geometry": [
0,
0,
4,
1
]
},
"children": []
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-10",
"Position": [
63,
0
],
"Size": [
1,
1
],
"Min Width": 1,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "w",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkLabel",
"params": {
"Name": "TTkLabel-11",
"Position": [
63,
1
],
"Size": [
1,
1
],
"Min Width": 1,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Text": "h",
"Color": "\u001b[0m",
"Alignment": 0
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkSpinBox",
"params": {
"Name": "sbLw",
"Position": [
65,
0
],
"Size": [
6,
1
],
"Min Width": 4,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Padding": [
0,
0,
0,
2
],
"Layout": "TTkGridLayout",
"Value": 0,
"Minimum": 0,
"Maximum": 99
},
"layout": {
"class": "TTkGridLayout",
"params": {
"Geometry": [
0,
0,
4,
1
]
},
"children": []
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
},
{
"class": "TTkSpinBox",
"params": {
"Name": "sbLh",
"Position": [
65,
1
],
"Size": [
6,
1
],
"Min Width": 4,
"Min Height": 1,
"Max Width": 65536,
"Max Height": 65536,
"Visible": true,
"Enabled": true,
"ToolTip": "",
"Padding": [
0,
0,
0,
2
],
"Layout": "TTkGridLayout",
"Value": 0,
"Minimum": 0,
"Maximum": 99
},
"layout": {
"class": "TTkGridLayout",
"params": {
"Geometry": [
0,
0,
4,
1
]
},
"children": []
},
"row": 0,
"col": 0,
"rowspan": 1,
"colspan": 1
}
]
}
},
"connections": [
{
"sender": "cbFg",
"receiver": "bpFg",
"signal": "toggled(bool)",
"slot": "setEnabled(bool)"
},
{
"sender": "cbBg",
"receiver": "bpBg",
"signal": "toggled(bool)",
"slot": "setEnabled(bool)"
}
]
}

0
tools/dumb_paint_lib/quickImport.tui.json → tools/dumb_paint_lib/tui/quickImport.tui.json

35
tools/webExporter/index.html

@ -9,16 +9,26 @@
<link href="www/xterm/xterm.css" rel="stylesheet" /> <link href="www/xterm/xterm.css" rel="stylesheet" />
<script src="www/xterm/xterm.js"></script> <script src="www/xterm/xterm.js"></script>
<script src="www/xterm-addon-fit/xterm-addon-fit.js"></script> <script src="www/xterm-addon-fit/xterm-addon-fit.js"></script>
<!--
<script src="www/xterm-addon-canvas/xterm-addon-canvas.js"></script>
-->
<script src="www/xterm-addon-unicode11/xterm-addon-unicode11.js"></script> <script src="www/xterm-addon-unicode11/xterm-addon-unicode11.js"></script>
<script src="www/file-saver/FileSaver.js"></script>
<script src="js/ttkproxy.js"></script> <script src="js/ttkproxy.js"></script>
<style> <style>
body {
height: 100%;
overflow-y: hidden;
overflow-x: hidden;
}
.xterm .xterm-viewport {overflow-y: hidden;} .xterm .xterm-viewport {overflow-y: hidden;}
</style> </style>
</head> </head>
<body> <body>
<div id="terminal" contenteditable="true" onpaste="pasteFunction()" oncontextmenu="return false;" style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px"></div> <div id="terminal" contenteditable="true" onpaste="pasteFunction()" oncontextmenu="return false;" style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px"></div>
<input type="file" id="file-input" hidden />
<script type="text/javascript"> <script type="text/javascript">
@ -28,8 +38,10 @@
} }
// Workaround from: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API // Workaround from: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API
// const font = new FontFace("NerdFont", "url(www/nerdfonts/DejaVuSansMNerdFont-Regular.ttf)"); // const font = new FontFace("pyTermTkFont", "url(www/fonts/nerdfonts/DejaVuSansMNerdFont-Regular.ttf)");
const font = new FontFace("NerdFont", "url(www/opentype/3270SemiCondensed-Regular.otf)"); // const font = new FontFace("pyTermTkFont", "url(www/fonts/opentype/3270SemiCondensed-Regular.otf)");
const font = new FontFace("pyTermTkFont", "url(www/fonts/opentype/3270-Regular.otf)");
// const font = new FontFace("pyTermTkFont", "url(www/fonts/unifont/unifont_upper.ttf)");
document.fonts.add(font); document.fonts.add(font);
font.load(); font.load();
document.fonts.ready.then(() => {fetchData()}); document.fonts.ready.then(() => {fetchData()});
@ -45,6 +57,20 @@
main(json) main(json)
} }
async function paste(){
if(navigator.clipboard){
try {
// let text = null
let text = await navigator.clipboard.readText().then((clipText) => clipText)
console.log('Pasted content: ', text)
return text
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}
return null
}
/* pyodide demo */ /* pyodide demo */
async function mainTTk(term,json){ async function mainTTk(term,json){
ttkProxy = new TTkProxy(term) ttkProxy = new TTkProxy(term)
@ -69,7 +95,9 @@
/* xterm.js */ /* xterm.js */
var term = new Terminal({ var term = new Terminal({
allowProposedApi: true, allowProposedApi: true,
fontFamily: 'NerdFont'}); fontSize: 17,
// fontFamily: 'FreeSerif Regular'});
fontFamily: 'pyTermTkFont'});
/* https://www.npmjs.com/package/xterm-addon-fit */ /* https://www.npmjs.com/package/xterm-addon-fit */
const fitAddon = new FitAddon.FitAddon(); const fitAddon = new FitAddon.FitAddon();
@ -77,6 +105,7 @@
const unicode11Addon = new Unicode11Addon.Unicode11Addon(); const unicode11Addon = new Unicode11Addon.Unicode11Addon();
term.loadAddon(fitAddon); term.loadAddon(fitAddon);
// term.loadAddon(new CanvasAddon.CanvasAddon());
term.loadAddon(unicode11Addon); term.loadAddon(unicode11Addon);
term.unicode.activeVersion = '11'; term.unicode.activeVersion = '11';

116
tools/webExporter/js/ttkproxy.js

@ -22,19 +22,87 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
*/ */
async function permissionsCheck() {
if (navigator.clipboard){
const read = await navigator.permissions.query({
name: 'clipboard-read',
});
const write = await navigator.permissions.query({
name: 'clipboard-write',
});
return write.state === 'granted' && read.state !== 'denied';
}
return false
}
// Declaration // Declaration
class TTkProxy { class TTkProxy {
constructor(term) { constructor(term) {
this.term = term this.term = term
document.getElementById("file-input").addEventListener("change", (e) => {
e.preventDefault();
const file = e.target.files[0]
let reader = new FileReader()
reader.onload = (event) => {
let data = {
'type':file.type,
'name':file.name,
'size':file.size,
'data':event.target.result}
this.ttk_fileOpen(data)
}
if (file.type.startsWith('application/json') ||
file.type.startsWith('text')){
reader.readAsText(file);
}
})
document.getElementById("terminal").addEventListener("dragover", (e) => {
e.preventDefault(); //preventing from default behaviour
//dropArea.classList.add("active");
//dragFile.textContent = "Release to Upload File";
})
document.getElementById("terminal").addEventListener("dragleave", () => {
//dropArea.classList.remove("active");
// dragFile.textContent = "Drag files here to upload";
})
document.getElementById("terminal").addEventListener("drop", (e) => {
e.preventDefault();
const target = e.dataTransfer;
const file = target.files[0]
let reader = new FileReader()
reader.onload = (event) => {
let data = {
'type':file.type,
'name':file.name,
'size':file.size,
'data':event.target.result}
this.ttk_dragOpen(data)
}
if (file.type.startsWith('application/json') ||
file.type.startsWith('text')){
reader.readAsText(file);
}
})
} }
pyodideProxy = { pyodideProxy = {
copy: function(text){ openFile: function(encoding){
let input = document.getElementById("file-input")
input.accept = encoding
input.click();
},
saveFile: function(name, content, encoding){
const blob = new Blob([content], {type: encoding});
saveAs(blob, name);
},
copy: async function(text){
console.log("Copying:",text) console.log("Copying:",text)
if(navigator.clipboard){ const permission = await permissionsCheck()
if(permission){
navigator.clipboard.writeText(text) navigator.clipboard.writeText(text)
return //codes below wont be executed return //codes below wont be executed
} }
const active = document.activeElement
const textArea = document.createElement("textarea") const textArea = document.createElement("textarea")
textArea.value = text textArea.value = text
@ -46,13 +114,23 @@ class TTkProxy {
document.execCommand('copy') document.execCommand('copy')
document.body.removeChild(textArea) document.body.removeChild(textArea)
active.focus()
}, },
paste: function(){ paste: function(){
if(navigator.clipboard){ /*
text = navigator.clipboard.readText() const permission = await permissionsCheck()
console.log("Pasted:",text) if(permission){
return text try {
// let text = null
let text = await navigator.clipboard.readText().then((txt) => txt)
// const text = navigator.clipboard.readText()
console.log('Pasted content: ', text)
return text
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
} }
*/
return null return null
}, },
consoleLog: function(m){ consoleLog: function(m){
@ -135,6 +213,16 @@ class TTkProxy {
from TermTk.TTkCore.TTkTerm.input import TTkInput from TermTk.TTkCore.TTkTerm.input import TTkInput
import pyodideProxy import pyodideProxy
def ttk_dragOpen(data):
data = data.to_py()
ttk.ttkEmitDragOpen(data['type'],data)
# ttk_log(f"{type(data.to_py())=}, {str(data.to_py())}")
def ttk_fileOpen(data):
data = data.to_py()
ttk.ttkEmitFileOpen(data['type'],data)
# ttk_log(f"{type(data.to_py())=}, {str(data.to_py())}")
def ttk_input(val): def ttk_input(val):
kevt,mevt,paste = TTkInput.key_process(val) kevt,mevt,paste = TTkInput.key_process(val)
if kevt or mevt: if kevt or mevt:
@ -155,7 +243,7 @@ class TTkProxy {
def ttk_log(val): def ttk_log(val):
# hex = [f"0x{ord(x):02x}" for x in val] # hex = [f"0x{ord(x):02x}" for x in val]
ttk.TTkLog.debug("---> "+val.replace("\\033","<ESC>") + " - ") ttk.TTkLog.debug("---> "+val.replace("\\033","<ESC>") + " - ")
ttk.TTkHelper.paintAll() # ttk.TTkHelper.paintAll()
def ttk_clean(): def ttk_clean():
if ttk.TTkHelper._rootWidget: if ttk.TTkHelper._rootWidget:
@ -187,12 +275,14 @@ class TTkProxy {
`,{ globals: this.namespace } `,{ globals: this.namespace }
); );
this.ttk_log = this.namespace.get("ttk_log"); this.ttk_log = this.namespace.get("ttk_log");
this.ttk_input = this.namespace.get("ttk_input"); this.ttk_input = this.namespace.get("ttk_input");
this.ttk_timer = this.namespace.get("ttk_timer"); this.ttk_timer = this.namespace.get("ttk_timer");
this.ttk_resize = this.namespace.get("ttk_resize"); this.ttk_resize = this.namespace.get("ttk_resize");
this.ttk_clean = this.namespace.get("ttk_clean"); this.ttk_clean = this.namespace.get("ttk_clean");
this.ttk_init = this.namespace.get("ttk_init"); this.ttk_init = this.namespace.get("ttk_init");
this.ttk_dragOpen = this.namespace.get("ttk_dragOpen");
this.ttk_fileOpen = this.namespace.get("ttk_fileOpen");
this.pyodideProxy.ttk_timer = this.ttk_timer this.pyodideProxy.ttk_timer = this.ttk_timer
this.pyodideProxy.term = this.term this.pyodideProxy.term = this.term

28
tools/webExporterInit.sh

@ -48,10 +48,13 @@ mkdir -p ${_TMP_PATH}/bin \
${_TMP_PATH}/www/pyodide \ ${_TMP_PATH}/www/pyodide \
${_TMP_PATH}/www/xterm/ \ ${_TMP_PATH}/www/xterm/ \
${_TMP_PATH}/www/xterm-addon-fit \ ${_TMP_PATH}/www/xterm-addon-fit \
${_TMP_PATH}/www/xterm-addon-canvas \
${_TMP_PATH}/www/xterm-addon-unicode11 \ ${_TMP_PATH}/www/xterm-addon-unicode11 \
${_TMP_PATH}/www/file-saver\
${_TMP_PATH}/www/webfonts \ ${_TMP_PATH}/www/webfonts \
${_TMP_PATH}/www/nerdfonts \ ${_TMP_PATH}/www/fonts/nerdfonts \
${_TMP_PATH}/www/opentype ${_TMP_PATH}/www/fonts/opentype \
${_TMP_PATH}/www/fonts/unifont
function _download { function _download {
_P=$1 _P=$1
@ -63,11 +66,12 @@ function _download {
}; };
_download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide.js _download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide.js
# _download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide.js.map
# _download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide.js # _download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide.js
_download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide-lock.json _download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide-lock.json
_download ${_TMP_PATH}/www/pyodide/ www/pyodide/python_stdlib.zip _download ${_TMP_PATH}/www/pyodide/ www/pyodide/python_stdlib.zip
_download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide.asm.js _download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide.asm.js
_download ${_TMP_PATH}/www/pyodide/ www/pyodide/repodata.json # _download ${_TMP_PATH}/www/pyodide/ www/pyodide/repodata.json
_download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide.asm.wasm _download ${_TMP_PATH}/www/pyodide/ www/pyodide/pyodide.asm.wasm
_download ${_TMP_PATH}/www/xterm/ www/xterm/xterm.css _download ${_TMP_PATH}/www/xterm/ www/xterm/xterm.css
@ -77,12 +81,20 @@ _download ${_TMP_PATH}/www/xterm/ www/xterm/xterm.js.map
_download ${_TMP_PATH}/www/xterm-addon-fit/ www/xterm-addon-fit/xterm-addon-fit.js _download ${_TMP_PATH}/www/xterm-addon-fit/ www/xterm-addon-fit/xterm-addon-fit.js
_download ${_TMP_PATH}/www/xterm-addon-fit/ www/xterm-addon-fit/xterm-addon-fit.js.map _download ${_TMP_PATH}/www/xterm-addon-fit/ www/xterm-addon-fit/xterm-addon-fit.js.map
_download ${_TMP_PATH}/www/xterm-addon-canvas/ www/xterm-addon-canvas/xterm-addon-canvas.js
_download ${_TMP_PATH}/www/xterm-addon-canvas/ www/xterm-addon-canvas/xterm-addon-canvas.js.map
_download ${_TMP_PATH}/www/xterm-addon-unicode11/ www/xterm-addon-unicode11/xterm-addon-unicode11.js _download ${_TMP_PATH}/www/xterm-addon-unicode11/ www/xterm-addon-unicode11/xterm-addon-unicode11.js
_download ${_TMP_PATH}/www/xterm-addon-unicode11/ www/xterm-addon-unicode11/xterm-addon-unicode11.js.map
_download ${_TMP_PATH}/www/file-saver/ www/file-saver/FileSaver.js
# _download ${_TMP_PATH}/www/webfonts/ www/webfonts/fa-regular-400.woff2 # _download ${_TMP_PATH}/www/fonts/webfonts/ www/fonts/webfonts/fa-regular-400.woff2
# _download ${_TMP_PATH}/www/nerdfonts/ www/nerdfonts/HurmitNerdFontMono-Regular.otf # _download ${_TMP_PATH}/www/fonts/nerdfonts/ www/fonts/nerdfonts/HurmitNerdFontMono-Regular.otf
# _download ${_TMP_PATH}/www/nerdfonts/ www/nerdfonts/DejaVuSansMNerdFont-Regular.ttf # _download ${_TMP_PATH}/www/fonts/nerdfonts/ www/fonts/nerdfonts/DejaVuSansMNerdFont-Regular.ttf
_download ${_TMP_PATH}/www/opentype/ www/opentype/3270SemiCondensed-Regular.otf # _download ${_TMP_PATH}/www/fonts/opentype/ www/fonts/opentype/3270SemiCondensed-Regular.otf
_download ${_TMP_PATH}/www/fonts/opentype/ www/fonts/opentype/3270-Regular.otf
# _download ${_TMP_PATH}/www/fonts/unifont/ www/fonts/unifont/unifont_upper.ttf
_download ${_TMP_PATH}/www/ www/favicon.ico _download ${_TMP_PATH}/www/ www/favicon.ico
@ -112,4 +124,4 @@ echo '{
rm -rf ${_TMP_PATH}/TermTk rm -rf ${_TMP_PATH}/TermTk
pushd ${_TMP_PATH} pushd ${_TMP_PATH}
zip -r ${_PWD}/itchExport.zip * zip -r ${_PWD}/itchExport.zip *
popd popd

Loading…
Cancel
Save