You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

452 lines
18 KiB

# 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__ = ['PaintTemplate']
import sys, os, json
sys.path.append(os.path.join(sys.path[0],'../..'))
import TermTk as ttk
from .paintarea import PaintArea, PaintScrollArea
from .canvaslayer import CanvasLayer
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):
__slots__ = ('_palette',
# Signals
'toolSelected')
def __init__(self, *args, **kwargs):
self.toolSelected = ttk.pyTTkSignal(CanvasLayer.Tool)
super().__init__(*args, **kwargs)
self._palette = Palette(maxHeight=12)
self.addWidget(self._palette)
# Layout for the toggle buttons
lToggleFgBg = ttk.TTkHBoxLayout()
cb_p_fg = ttk.TTkCheckbox(text="-FG-", checked=ttk.TTkK.Checked)
cb_p_bg = ttk.TTkCheckbox(text="-BG-", checked=ttk.TTkK.Checked)
lToggleFgBg.addWidgets([cb_p_fg,cb_p_bg])
lToggleFgBg.addItem(ttk.TTkLayout())
cb_p_fg.toggled.connect(self._palette.enableFg)
cb_p_bg.toggled.connect(self._palette.enableBg)
self.addItem(lToggleFgBg)
# Toolset
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_line = ttk.TTkRadioButton(radiogroup="tools", text="Line", enabled=False)
ra_rect = ttk.TTkRadioButton(radiogroup="tools", text="Rect")
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_e = ttk.TTkRadioButton(radiogroup="toolsRectFill", text="Empty", enabled=False)
cb_move_r = ttk.TTkCheckbox(text="Resize", enabled=False)
@ttk.pyTTkSlot()
def _checkTools():
tool = CanvasLayer.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():
if ra_rect_e.isChecked():
tool = CanvasLayer.Tool.RECTEMPTY
else:
tool = CanvasLayer.Tool.RECTFILL
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_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_line.toggled.connect( _emitTool)
ra_rect.toggled.connect( _emitTool)
ra_rect_f.toggled.connect( _emitTool)
ra_rect_e.toggled.connect( _emitTool)
ra_oval.toggled.connect( _emitTool)
cb_move_r.toggled.connect( _checkTools)
lTools.addWidget(ra_move ,0,0)
lTools.addWidget(cb_move_r,0,1)
lTools.addWidget(ra_select,1,0)
lTools.addWidget(ra_brush ,2,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)
# brush
# line
# rettangle [empty,fill]
# oval [empty,fill]
self.addItem(ttk.TTkLayout())
def palette(self):
return self._palette
class ExportArea(ttk.TTkGridLayout):
__slots__ = ('_paintArea', '_te','_cbCrop', '_cbFull', '_cbPal')
def __init__(self, paintArea:PaintArea, **kwargs):
self._paintArea:PaintArea = paintArea
super().__init__(**kwargs)
self._te = ttk.TTkTextEdit(lineNumber=True, readOnly=False)
btn_exIm = ttk.TTkButton(text="Export Image")
btn_exLa = ttk.TTkButton(text="Export Layer")
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)
@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()
def _exportLayer(self):
crop = self._cbCrop.isChecked()
palette = self._cbPal.isChecked()
full = self._cbFull.isChecked()
dd = self._paintArea.exportLayer(full=full,palette=palette,crop=crop)
if not dd:
self._te.setText('# No Data toi 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 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:
#
# Palette Brushes
# Drawing Area (Chars/Glyphs)
# Tools
#
# Layouts
# Export
#
class PaintTemplate(ttk.TTkAppTemplate):
__slots__ = ('_parea','_layers')
def __init__(self, fileName=None, border=False, **kwargs):
super().__init__(border, **kwargs)
self._parea = parea = PaintArea()
self._layers = layers = Layers()
ptoolkit = PaintToolKit()
tarea = TextArea()
expArea = ExportArea(parea)
leftPanel = LeftPanel()
palette = leftPanel.palette()
rightPanel = ttk.TTkSplitter(orientation=ttk.TTkK.VERTICAL)
rightPanel.addWidget(tarea)
# rightPanel.addItem(expArea, title="Export")
# rightPanel.setSizes([None,5])
rightPanel.addItem(layers, title='Layers')
rightPanel.setSizes([None,9])
self.setItem(expArea, self.BOTTOM, title="Export")
self.setItem(leftPanel , self.LEFT, size=16*2)
self.setWidget(PaintScrollArea(parea) , self.MAIN)
self.setWidget(ptoolkit , self.TOP, fixed=True)
self.setItem(rightPanel , self.RIGHT, size=40)
self.setMenuBar(appMenuBar:=ttk.TTkMenuBarLayout(), self.TOP)
fileMenu = appMenuBar.addMenu("&File")
fileMenu.addMenu("&Open" ).menuButtonClicked.connect(self._open)
fileMenu.addMenu("&Save" ).menuButtonClicked.connect(self._save)
fileMenu.addMenu("Save &As...").menuButtonClicked.connect(self._saveAs)
fileMenu.addSpacer()
fileMenu.addMenu("&Import").menuButtonClicked.connect(self.importDictWin)
menuExport = fileMenu.addMenu("&Export")
fileMenu.addSpacer()
fileMenu.addMenu("Load Palette")
fileMenu.addMenu("Save Palette")
fileMenu.addSpacer()
buttonExit = fileMenu.addMenu("E&xit")
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.addMenu("Scratchpad").menuButtonClicked.connect(self.scratchpad)
# 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.addMenu("About ...").menuButtonClicked.connect(_showAboutTTk)
helpMenu.addMenu("About DPT").menuButtonClicked.connect(_showAbout)
palette.colorSelected.connect(self._parea.setGlyphColor)
palette.colorSelected.connect(ptoolkit.setColor)
ptoolkit.updatedColor.connect(self._parea.setGlyphColor)
ptoolkit.updatedTrans.connect(self._parea.setTrans)
tarea.charSelected.connect(ptoolkit.glyphFromString)
tarea.charSelected.connect(self._parea.glyphFromString)
leftPanel.toolSelected.connect(self._parea.setTool)
self._parea.setGlyphColor(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()
def importDictWin(self):
newWindow = ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"tui/quickImport.tui.json"))
te = newWindow.getWidgetByName("TextEdit")
@ttk.pyTTkSlot()
def _importDict(te=te):
def _probeCompressedText(_text):
import re
ret = ""
for _t in _text.split('\n'):
if m := re.match(r'^ *["\']([A-Za-z0-9+/]+[=]{0,2})["\' +]*$',_t):
ret += m.group(1)
elif not re.match(r'^ *$',_t): # exclude empty lines
return ""
return ret
text = te.toPlainText()
if compressed := _probeCompressedText(text):
dd = ttk.TTkUtil.base64_deflate_2_obj(compressed)
else:
try:
dd = eval(text)
except Exception as e:
ttk.TTkLog.error(str(e))
messageBox = ttk.TTkMessageBox(text= str(e),icon=ttk.TTkMessageBox.Icon.Warning)
ttk.TTkHelper.overlay(None, messageBox, 5, 5, True)
return
if type(dd) is not dict:
messageBox = ttk.TTkMessageBox(text= f"Input is {type(dd)}\nImport data must be a dict or \ncompressed String definition",icon=ttk.TTkMessageBox.Icon.Warning)
ttk.TTkHelper.overlay(None, messageBox, 5, 5, True)
return
if 'layers' in dd:
self.importDocument(dd)
else:
self._layers.addLayer(name="Import")
self._parea.importLayer(dd)
newWindow.close()
newWindow.getWidgetByName("BtnDict" ).clicked.connect(_importDict)
ttk.TTkHelper.overlay(None, newWindow, 10, 4, modal=True)