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.
 
 
 
 
 

526 lines
21 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__ = ['PaintArea','PaintScrollArea','CanvasLayer']
import TermTk as ttk
from .canvaslayer import CanvasLayer
from .const import ToolType
from .glbls import glbls
class PaintArea(ttk.TTkAbstractScrollView):
__slots__ = ('_transparentColor',
'_documentPos','_documentSize',
'_mouseMove', '_mouseDrag', '_mousePress', '_mouseRelease',
'_moveData','_resizeData',
'_clipboard',
'_tool',
'_glyph', '_glyphColor', '_glyphEnabled', '_areaBrush')
def __init__(self, *args, **kwargs):
self._transparentColor = {'base':ttk.TTkColor.RST,'dim':ttk.TTkColor.RST}
self._glyph = 'X'
self._glyphColor = ttk.TTkColor.RST
self._glyphEnabled = True
self._areaBrush = CanvasLayer()
self._areaBrush.changed.connect(self.update)
self._moveData = None
self._resizeData = None
self._mouseMove = None
self._mouseDrag = None
self._mousePress = None
self._mouseRelease = None
self._tool = 0
self._documentPos = (6,3)
self._documentSize = ( 0, 0)
self._clipboard = ttk.TTkClipboard()
super().__init__(*args, **kwargs)
self.setTrans(ttk.TTkColor.bg('#FF00FF'))
self.resizeCanvas(*glbls.documentSize)
self.setFocusPolicy(ttk.TTkK.ClickFocus + ttk.TTkK.TabFocus)
glbls.brush.toolTypeChanged.connect(self.setTool)
glbls.brush.areaChanged.connect( self.setAreaBrush)
glbls.brush.glyphChanged.connect( self.updateGlyph)
glbls.brush.colorChanged.connect( self.updateGlyph)
glbls.brush.glyphEnabledChanged.connect(self.updateGlyph)
glbls.layers.changed.connect(self.update)
glbls.layers.layerAdded.connect(self.update)
glbls.layers.layerDeleted.connect(self.update)
glbls.layers.layerSelected.connect(self.update)
glbls.layers.layersOrderChanged.connect(self.update)
# Retrieve the default values
self.setTool( glbls.brush.toolType())
self.updateGlyph()
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 cl in glbls.layers.layers():
lx,ly = cl.pos()
lw,lh = cl.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+1,h+1
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 resizeCanvas(self, w, h):
self._documentSize = (w,h)
glbls.documentSize = (w,h)
self._retuneGeometry()
self.update()
def superResize(self,x,y,w,h):
dx,dy = self._documentPos
dw,dh = self._documentSize
if (x,y,w,h) == (dx,dy,dw,dh): return
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 glbls.layers.layers():
lx,ly = l.pos()
l.move(lx+diffx,ly+diffy)
self._documentPos = (x,y)
self._documentSize = (w,h)
glbls.documentSize = (w,h)
self.update()
def exportLayer(self, full=False, palette=True, crop=True) -> dict:
if glbls.layers.selected():
return glbls.layers.selected().exportLayer(full=full,palette=palette,crop=crop)
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':[cl.exportLayer(full=full,palette=palette,crop=crop) for cl in reversed(glbls.layers.layers())]}
return outData
def exportImage(self) -> str:
pw,ph = self._documentSize
image = ttk.TTkCanvas(width=pw,height=ph)
for cl in reversed(glbls.layers.layers()):
lx,ly = cl.pos()
cl.drawInCanvas(pos=(lx,ly),canvas=image)
return image.toAnsi()
def leaveEvent(self, evt):
self._mouseMove = None
self._moveData = None
if current := glbls.layers.selected():
current.cleanPreview()
self.update()
return super().leaveEvent(evt)
@ttk.pyTTkSlot(ToolType)
def setTool(self, tool):
self._tool = tool
self.update()
def _handleAction(self):
if not glbls.layers.selected(): return
dx,dy = self._documentPos
dw,dh = self._documentSize
ox, oy = self.getViewOffsets()
mp = self._mousePress
mm = self._mouseMove
md = self._mouseDrag
mr = self._mouseRelease
l = glbls.layers.selected()
lx,ly = l.pos()
if self._tool & ToolType.PICKGLYPH:
if mp:
mpx,mpy = mp
color = ttk.TTkColor.RST
glyph = None
for lm in glbls.layers.layers():
lmx,lmy = lm.pos()
if lm.isOpaque(mpx-lmx-dx,mpy-lmy-dy):
_gl, _co = lm.glyphColorAt(mpx-lmx-dx,mpy-lmy-dy)
if not glyph and color == ttk.TTkColor.RST:
glyph = _gl
color = _co
elif color.hasBackground():
if _co.hasBackground():
if color.hasForeground():
color = color.foreground() + _co.background()
else:
color = _co.background()
else:
break
glbls.brush.setColor(color)
glbls.brush.setGlyph(glyph if glyph else ' ')
if mr:
glbls.brush.setToolType(self._tool & ~ToolType.PICKGLYPH)
self._mousePress = None
self._mouseMove = None
self._mouseDrag = None
self._mouseRelease = None
elif self._tool & ToolType.MOVE and mp and not md:
if self._tool & ToolType.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 glbls.layers.layers():
mpx,mpy = mp
lmx,lmy = lm.pos()
self._moveData = {'type':PaintArea,'pos':(dx,dy)}
if lm.isOpaque(mpx-lmx-dx,mpy-lmy-dy):
tml = lm
self._moveData = {'type':CanvasLayer,'pos':tml.pos(),'layer':tml}
glbls.layers.selectLayer(lm)
break
elif self._tool & ToolType.MOVE and mp and md:
# Move/Resize Tool
if self._tool & ToolType.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)
glbls.layers.selectLayer(self._moveData['layer'])
elif mData['type']==PaintArea:
px,py = self._moveData['pos']
self._documentPos = (px+pdx,py+pdy)
self._retuneGeometry()
elif self._tool & ToolType.BRUSH:
if mp and self._tool & ToolType.PICKAREA:
glbls.brush.setToolType(self._tool & ~ToolType.PICKAREA)
mpx,mpy = mp
for lm in glbls.layers.layers():
lmx,lmy = lm.pos()
if lm.isOpaque(mpx-lmx-dx,mpy-lmy-dy):
glbls.brush.setArea(lm.trim().toTTkString())
break
self._mousePress = None
self._mouseMove = None
self._mouseDrag = None
self._mouseRelease = None
elif self._tool & ToolType.PICKAREA:
pass # Do not show any preview if we are in picking mode
elif mp or md:
if md: mx,my = md
else: mx,my = mp
preview=False
transparent=self._tool & ToolType.TRANSPARENT
if self._tool & ToolType.GLYPH:
glbls.layers.selected().placeGlyph(mx-lx-dx,my-ly-dy,self._glyph,self._glyphColor,self._glyphEnabled,preview)
if self._tool & ToolType.AREA:
glbls.layers.selected().placeArea(mx-lx-dx,my-ly-dy,self._areaBrush,transparent,preview)
elif mm:
mx,my = mm
preview=True
transparent=self._tool & ToolType.TRANSPARENT
if self._tool & ToolType.GLYPH:
glbls.layers.selected().placeGlyph(mx-lx-dx,my-ly-dy,self._glyph,self._glyphColor,self._glyphEnabled,preview)
if self._tool & ToolType.AREA:
glbls.layers.selected().placeArea(mx-lx-dx,my-ly-dy,self._areaBrush,transparent,preview)
elif self._tool in (ToolType.RECTEMPTY, ToolType.RECTFILL):
if mr and mp:
mpx,mpy = mp
mrx,mry = mr
preview=False
glbls.layers.selected().placeFill((mpx-lx-dx,mpy-ly-dy,mrx-lx-dx,mry-ly-dy),self._tool,self._glyph,self._glyphColor,self._glyphEnabled,preview)
elif md and mp:
mpx,mpy = mp
mrx,mry = md
preview=True
glbls.layers.selected().placeFill((mpx-lx-dx,mpy-ly-dy,mrx-lx-dx,mry-ly-dy),self._tool,self._glyph,self._glyphColor,self._glyphEnabled,preview)
elif mm:
mpx,mpy = mm
mrx,mry = mm
preview=True
glbls.layers.selected().placeFill((mpx-lx-dx,mpy-ly-dy,mrx-lx-dx,mry-ly-dy),self._tool,self._glyph,self._glyphColor,self._glyphEnabled,preview)
self.update()
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:
ox, oy = self.getViewOffsets()
self._mouseDrag=(evt.x+ox,evt.y+oy)
self._mouseMove= None
self._handleAction()
return True
def mousePressEvent(self, evt) -> bool:
ox, oy = self.getViewOffsets()
self._mousePress=(evt.x+ox,evt.y+oy)
self._moveData = None
self._mouseMove = None
self._mouseDrag = None
self._mouseRelease = None
self._handleAction()
return True
def mouseReleaseEvent(self, evt) -> bool:
ox, oy = self.getViewOffsets()
self._mouseRelease=(evt.x+ox,evt.y+oy)
self._mouseMove = None
self._handleAction()
self._moveData = None
self._resizeData = None
self._mousePress = None
self._mouseDrag = None
self._mouseRelease = None
glbls.saveSnapshot()
return super().mousePressEvent(evt)
def keyEvent(self, evt) -> bool:
ret = None
cl = glbls.layers.selected()
if cl and evt.key == ttk.TTkK.Key_Up:
x,y = cl.pos()
cl.move(x,y-1)
ret = True
elif cl and evt.key == ttk.TTkK.Key_Down:
x,y = cl.pos()
cl.move(x,y+1)
ret = True
elif cl and evt.key == ttk.TTkK.Key_Left:
x,y = cl.pos()
cl.move(x-1,y)
ret = True
elif cl and evt.key == ttk.TTkK.Key_Right:
x,y = cl.pos()
cl.move(x+1,y)
ret = True
elif evt.mod==ttk.TTkK.ControlModifier and evt.key == ttk.TTkK.Key_V:
self.paste()
ret = True
elif evt.mod==ttk.TTkK.ControlModifier and evt.key == ttk.TTkK.Key_C:
if glbls.layers.selected():
text = glbls.layers.selected().toTTkString()
self.copy(text)
ret = True
elif cl and evt.key == ttk.TTkK.Key_Delete:
glbls.layers.delLayer()
glbls.saveSnapshot()
else:
return super().keyEvent(evt)
self._retuneGeometry()
self.update()
if ret is None:
return super().keyEvent(evt)
return ret
@ttk.pyTTkSlot()
def paste(self):
txt = self._clipboard.text()
self.pasteEvent(txt)
@ttk.pyTTkSlot(ttk.TTkString)
def copy(self, text):
self._clipboard.setText(text)
def pasteEvent(self, txt:str):
glbls.layers.addLayer().importTTkString(ttk.TTkString(txt))
glbls.saveSnapshot()
self.update()
return True
@ttk.pyTTkSlot(ttk.TTkString)
def setAreaBrush(self, ab:ttk.TTkString):
self._areaBrush = CanvasLayer()
self._areaBrush.importTTkString(ab)
@ttk.pyTTkSlot()
def updateGlyph(self):
self.setGlyph(glbls.brush.glyph())
self.setGlyphColor(glbls.brush.color())
self._glyphEnabled = glbls.brush.glyphEnabled()
def glyph(self):
return self._glyph
@ttk.pyTTkSlot(str)
def setGlyph(self, glyph):
if len(glyph) <= 0:
return
if isinstance(glyph,str):
self._glyph = glyph[0]
if isinstance(glyph,ttk.TTkString):
self._glyph = glyph.charAt(0)
self._glyphColor = glyph.colorAt(0)
def glyphColor(self):
return self._glyphColor
@ttk.pyTTkSlot(ttk.TTkColor)
def setGlyphColor(self, color):
self._glyphColor = color
@ttk.pyTTkSlot(ttk.TTkColor)
def setTrans(self, color:ttk.TTkColor):
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()
def paintEvent(self, canvas:ttk.TTkCanvas):
dx,dy = self._documentPos
ox, oy = self.getViewOffsets()
dw,dh = self._documentSize
dox,doy = dx-ox,dy-oy
cw,ch = canvas.size()
# w=min(cw,dw)
# h=min(ch,dh)
tcb = self._transparentColor['base']
tcd = self._transparentColor['dim']
if cl:=glbls.layers.selected():
tclb = self._transparentColor['layer']
tcld = self._transparentColor['layerDim']
lx,ly = cl.pos()
lw,lh = cl.size()
canvas.fill(pos=(0 ,ly+doy),size=(cw,lh),color=tcld)
canvas.fill(pos=(lx+dox,0 ),size=(lw,ch),color=tcld)
canvas.fill(pos=(lx+dox,ly+doy),size=(lw,lh),color=tclb)
canvas.fill(pos=(dx-ox,dy-oy),size=(dw,dh),color=tcb)
canvas.fill(pos=(0 ,dy-oy-1), size=(cw,1),color=tcd)
canvas.fill(pos=(0 ,dy-oy+dh),size=(cw,1),color=tcd)
canvas.fill(pos=(dx-ox-2 ,0 ),size=(2,ch),color=tcd)
canvas.fill(pos=(dx-ox+dw,0 ),size=(2,ch),color=tcd)
for cl in reversed(glbls.layers.layers()):
lx,ly = cl.pos()
cl.drawInCanvas(pos=(lx+dox,ly+doy),canvas=canvas)
if self._tool & ToolType.RESIZE:
rd = self._resizeData
def _drawResizeBorders(_rx,_ry,_rw,_rh,_sel,_color=ttk.TTkColor.RST):
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 _color)
canvas.drawText(pos=(_rx ,_ry+_rh-1),text=''*_rw, color=selColor if _sel & ttk.TTkK.BOTTOM else _color)
for _y in range(_ry,_ry+_rh):
canvas.drawText(pos=(_rx ,_y),text='',color=selColor if _sel & ttk.TTkK.LEFT else _color)
canvas.drawText(pos=(_rx+_rw-1,_y),text='',color=selColor if _sel & ttk.TTkK.RIGHT else _color)
canvas.drawChar(pos=(_rx ,_ry ), char='', color=_color)
canvas.drawChar(pos=(_rx+_rw-1,_ry ), char='', color=_color)
canvas.drawChar(pos=(_rx ,_ry+_rh-1), char='', color=_color)
canvas.drawChar(pos=(_rx+_rw-1,_ry+_rh-1), char='', color=_color)
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
if cl:=glbls.layers.selected():
lx,ly = cl.pos()
lw,lh = cl.size()
_drawResizeBorders(lx+dx-ox-1, ly+dy-oy-1, lw+2, lh+2, sLayer)
_drawResizeBorders(dx-ox-1, dy-oy-1, dw+2, dh+2, sMain, _color=ttk.TTkColor.YELLOW)
class PaintScrollArea(ttk.TTkAbstractScrollArea):
def __init__(self, pwidget:PaintArea, **kwargs):
super().__init__(**kwargs)
self.setViewport(pwidget)