From 75ae3f6f62b9f0a6f9afe953d366f8a474bc479a Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 13 Mar 2024 09:50:20 +0000 Subject: [PATCH 01/28] DumbPaintTool - Initial Layers Implementation --- TermTk/TTkCore/color.py | 57 +++++++ tools/dumb_paint_lib/__init__.py | 3 +- tools/dumb_paint_lib/layers.py | 237 +++++++++++++++++++++++++++ tools/dumb_paint_lib/maintemplate.py | 42 +++-- tools/dumb_paint_lib/paintarea.py | 230 ++++++++++++++++---------- 5 files changed, 465 insertions(+), 104 deletions(-) create mode 100644 tools/dumb_paint_lib/layers.py diff --git a/TermTk/TTkCore/color.py b/TermTk/TTkCore/color.py index 19b4bae7..f8294af7 100644 --- a/TermTk/TTkCore/color.py +++ b/TermTk/TTkCore/color.py @@ -388,10 +388,67 @@ class TTkColor(_TTkColor): color_1 = color_fg_red + color_bg_blue color_2 = color_fg_red + TTkColor.bg('#FFFF00') 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() '''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: BOLD = _TTkColor(mod=TTkHelper.Color.BOLD) '''**Bold** modifier''' diff --git a/tools/dumb_paint_lib/__init__.py b/tools/dumb_paint_lib/__init__.py index 9a2ea381..f45cf51e 100644 --- a/tools/dumb_paint_lib/__init__.py +++ b/tools/dumb_paint_lib/__init__.py @@ -1,4 +1,5 @@ from .maintemplate import * from .paintarea import * from .textarea import * -from .palette import * \ No newline at end of file +from .palette import * +from .layers import * \ No newline at end of file diff --git a/tools/dumb_paint_lib/layers.py b/tools/dumb_paint_lib/layers.py new file mode 100644 index 00000000..fd2468a2 --- /dev/null +++ b/tools/dumb_paint_lib/layers.py @@ -0,0 +1,237 @@ +# MIT License +# +# Copyright (c) 2024 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +__all__ = ['Layers','Layer'] + +import sys, os + +sys.path.append(os.path.join(sys.path[0],'../..')) +import TermTk as ttk + +class Layer(): + __slots__ = ('_name','_data') + def __init__(self,name:ttk.TTkString=ttk.TTkString('New'),data=None) -> None: + self._name:ttk.TTkString = name + self._data = data + def name(self): + return self._name + +class _layerButton(ttk.TTkWidget): + 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 = ('_layer','_first', '_isSelected', + # signals + 'clicked' + ) + def __init__(self, layer:Layer, **kwargs): + self.clicked = ttk.pyTTkSignal(_layerButton) + self._layer:Layer = layer + self._isSelected = False + self._first = True + super().__init__(**kwargs) + + def mouseReleaseEvent(self, evt) -> bool: + self.clicked.emit(self) + return True + + def paintEvent(self, canvas: ttk.TTkCanvas): + # if self.isEnabled() and self._checkable: + # if self._checked: + # style = self.style()['checked'] + # else: + # style = self.style()['unchecked'] + # if self.hasFocus(): + # borderColor = self.style()['focus']['borderColor'] + # else: + # borderColor = style['borderColor'] + # else: + # style = self.currentStyle() + # borderColor = style['borderColor'] + if self._isSelected: + style = self.style()['selected'] + else: + style = self.currentStyle() + borderColor = style['borderColor'] + textColor = style['color'] + 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" □ ▣ ┃{' '*(w-7)}┃",color=borderColor) + else: + canvas.drawText(pos=(0,1),text=f" □ ▣ ╽{' '*(w-7)}╽",color=borderColor) + canvas.drawTTkString(pos=(7,1),text=self._layer.name(), color=textColor) + +class LayerScrollWidget(ttk.TTkAbstractScrollView): + __slots__ = ('_layers','_selected', + # Signals + 'layerSelected','layerAdded','layerDeleted') + def __init__(self, **kwargs): + self.layerSelected = ttk.pyTTkSignal(Layer) + self.layerAdded = ttk.pyTTkSignal(Layer) + self.layerDeleted = ttk.pyTTkSignal(Layer) + firstLayer:_layerButton = _layerButton(layer=Layer(name=ttk.TTkString('Background'))) + self._selected = firstLayer + firstLayer._isSelected = True + firstLayer.clicked.connect(self._clickedLayer) + self._layers:list[_layerButton] = [firstLayer] + super().__init__(**kwargs) + self.viewChanged.connect(self._placeTheButtons) + self.layout().addWidget(firstLayer) + self._placeTheButtons() + self.layerAdded.emit(firstLayer) + + def viewFullAreaSize(self) -> tuple: + _,_,w,h = self.layout().fullWidgetAreaGeometry() + # w,_ = self.size() + # h = 3*len(self._layers) + return w,h + + def viewDisplayedSize(self) -> tuple: + return self.size() + + # def mouseMoveEvent(self, evt) -> bool: + # offx,offy = self.getViewOffsets() + # y = evt.y-offy + # llen = len(self._layers) + # if y==0: self._mouseMoved = self._layers[-1] + # elifse: self._mouseMoved = self._layers[y] + # return super().mouseMoveEvent(evt) + + 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): + if sel:=self._selected: + sel._isSelected = False + sel.update() + self._selected = layerButton + layerButton._isSelected = True + self.update() + + @ttk.pyTTkSlot() + def addLayer(self): + _l=Layer() + newLayer:_layerButton = _layerButton(parent=self,layer=_l) + self._layers.insert(0,newLayer) + if sel:=self._selected: sel._isSelected = False + self._selected = newLayer + newLayer._isSelected = True + newLayer.clicked.connect(self._clickedLayer) + self.viewChanged.emit() + self._placeTheButtons() + self.layerAdded.emit(newLayer) + 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 paintEvent(self, canvas: ttk.TTkCanvas): + # w,h = self.size() + # offx,offy = self.getViewOffsets() + # llen = len(self._layers) + # color = ttk.TTkColor.RST + # colorSelected = ttk.TTkColor.fg('#FFFF66') + # colorMouseMoved = ttk.TTkColor.bg('#003333') + # colorHidden = ttk.TTkColor.bg('#003333') + # for i,l in enumerate(self._layers): + # # drawing from the bottom + # py = 2*(llen-1)-i*2 + # canvas.drawText( pos=(0,py ),text=f" ┏{'━'*(w-7)}┓") + # canvas.drawText( pos=(0,py+2),text=f" ┗{'━'*(w-7)}┛") + # if i None: + self._size = (0,0) + self._data = [] + self._colors = [] + + 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 copy(self): + w,h = self._size + ret = CanvasLayer() + ret._size = (w,h) + ret._data = [d.copy() for d in self._data] + ret._colors = [c.copy() for c in self._colors] + + def clean(self): + w,h = self._size + for i in range(h): + self._data[i] = [' ']*w + self._colors[i] = [ttk.TTkColor.RST]*w + + def importLayer(self, dd): + w,h = self._size + w = len(dd['data'][0]) + 10 + h = len(dd['data']) + 4 + x,y=5,2 + + self.resizeCanvas(w,h) + self.clean() + + 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 placeFill(self,geometry,tool,glyph,color): + 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 = min(ax,bx), min(ay,by) + fbx,fby = max(ax,bx), max(ay,by) + + data = self._data + colors = self._colors + + if tool == PaintArea.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 == PaintArea.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,color): + w,h = self._size + data = self._data + colors = self._colors + if 0<=x Date: Wed, 13 Mar 2024 11:57:53 +0000 Subject: [PATCH 02/28] Added Move layer --- tools/dumb_paint_lib/maintemplate.py | 6 +- tools/dumb_paint_lib/paintarea.py | 168 ++++++++++++++++++--------- 2 files changed, 119 insertions(+), 55 deletions(-) diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index c89fe5c8..79670f38 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -68,7 +68,9 @@ class LeftPanel(ttk.TTkVBoxLayout): def _emitTool(checked): if not checked: return tool = PaintArea.Tool.BRUSH - if ra_brush.isChecked(): + if ra_move.isChecked(): + tool = PaintArea.Tool.MOVE + elif ra_brush.isChecked(): tool = PaintArea.Tool.BRUSH elif ra_rect.isChecked(): if ra_rect_e.isChecked(): @@ -80,6 +82,8 @@ class LeftPanel(ttk.TTkVBoxLayout): ra_rect.toggled.connect(ra_rect_f.setEnabled) ra_rect.toggled.connect(ra_rect_e.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) diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 07ab0351..8fe907a1 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -119,12 +119,21 @@ class PaintToolKit(ttk.TTkGridLayout): self._refreshColor(emit=False) class CanvasLayer(): - __slot_ = ('_size','_data','_colors') + __slot_ = ('_pos','_size','_data','_colors') def __init__(self) -> None: + self._pos = (0,0) self._size = (0,0) self._data = [] self._colors = [] + def pos(self): + return self._pos + def size(self): + return self._size + + 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] @@ -139,6 +148,7 @@ class CanvasLayer(): ret._size = (w,h) ret._data = [d.copy() for d in self._data] ret._colors = [c.copy() for c in self._colors] + return ret def clean(self): w,h = self._size @@ -213,25 +223,37 @@ class CanvasLayer(): px,py = pos pw,ph = self._size cw,ch = canvas.size() - w=min(cw,pw) - h=min(ch,ph) + if px+pw<0 or py+ph<0:return + if px>=cw or py>=ch:return + # 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) + data = self._data colors = self._colors - for y in range(h): - canvas._data[y][0:w] = data[y][0:w] - for x in range(w): - c = colors[y][x] - canvas._colors[y][x] = c if c._bg else c+canvas._colors[y][x].background() + for y in range(cy,cy+dh): + canvas._data[y][cx:cx+dw] = data[y+ly-cy][lx:lx+dw] + for x in range(cx,cx+dw): + c = colors[y+ly-cy][x+lx-cx] + canvas._colors[y][x] = c if c._bg else c+canvas._colors[y][x] class PaintArea(ttk.TTkWidget): class Tool(int): - BRUSH = 0x01 - RECTFILL = 0x02 - RECTEMPTY = 0x03 + MOVE = 0x01 + BRUSH = 0x02 + RECTFILL = 0x03 + RECTEMPTY = 0x04 __slots__ = ('_canvasLayers', '_currentLayer', '_transparentColor', - '_mouseMove', '_mouseFill', '_tool', + '_mouseMove', '_mouseDrag', '_mousePress', '_mouseRelease', + '_posBk', + '_tool', '_glyph', '_glyphColor') def __init__(self, *args, **kwargs): @@ -240,8 +262,11 @@ class PaintArea(ttk.TTkWidget): self._canvasLayers:list[CanvasLayer] = [self._currentLayer] self._glyph = 'X' self._glyphColor = ttk.TTkColor.RST + self._posBk = (0,0) self._mouseMove = None - self._mouseFill = None + self._mouseDrag = None + self._mousePress = None + self._mouseRelease = None self._tool = self.Tool.BRUSH super().__init__(*args, **kwargs) self.resizeCanvas(80,25) @@ -266,48 +291,82 @@ class PaintArea(ttk.TTkWidget): self._tool = tool self.update() + def _handleAction(self): + mp = self._mousePress + mm = self._mouseMove + md = self._mouseDrag + mr = self._mouseRelease + l = self._currentLayer + lx,ly = l.pos() + if self._tool == self.Tool.MOVE and mp and not md: + self._posBk = (lx,ly) + elif self._tool == self.Tool.MOVE and mp and md: + mpx,mpy = mp + mdx,mdy = md + px,py = self._posBk + dx,dy = mdx-mpx,mdy-mpy + l.move(px+dx,py+dy) + elif self._tool == self.Tool.BRUSH and (mp or md): + if md: mx,my = md + else: mx,my = mp + self._currentLayer.placeGlyph(lx+mx,ly+my,self._glyph,self._glyphColor) + elif self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL) and mr and mp: + mpx,mpy = mp + mrx,mry = mr + self._currentLayer.placeFill((mpx,mpy,mrx,mry),self._tool,self._glyph,self._glyphColor) + self.update() + def mouseMoveEvent(self, evt) -> bool: - # self._mouseFill = None - x,y = evt.x, evt.y - w,h = self._canvasSize - if 0<=x bool: - x,y = evt.x,evt.y - if self._tool == self.Tool.BRUSH: - if self._placeGlyph(evt.x, evt.y): - return True - if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL) and self._mouseFill: - mx,my = self._mouseFill[:2] - self._mouseFill = [mx,my,x,y] - self.update() - return True - return super().mouseDragEvent(evt) + self._mouseDrag=(evt.x,evt.y) + self._mouseMove= None + self._handleAction() + #x,y = evt.x,evt.y + #if self._tool == self.Tool.BRUSH: + # if self._placeGlyph(evt.x, evt.y): + # return True + #if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL) and self._mouseDrag: + # mx,my = self._mouseDrag[:2] + # self._mouseDrag = [mx,my,x,y] + # self.update() + # return True + #return super().mouseDragEvent(evt) + return True def mousePressEvent(self, evt) -> bool: - x,y = evt.x,evt.y - if self._tool == self.Tool.BRUSH: - if self._placeGlyph(x,y): - return True - if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL): - self._mouseFill = [x,y,x,y] - self.update() - return True - return super().mousePressEvent(evt) + self._mousePress=(evt.x,evt.y) + self._mouseMove = None + self._mouseDrag = None + self._mouseRelease = None + self._handleAction() + # x,y = evt.x,evt.y + # if self._tool == self.Tool.BRUSH: + # if self._placeGlyph(x,y): + # return True + # if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL): + # self._mouseDrag = [x,y,x,y] + # self.update() + # return True + # return super().mousePressEvent(evt) + return True def mouseReleaseEvent(self, evt) -> bool: - x,y = evt.x,evt.y - if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL): - self._placeFill() - self.update() - return True - self._mouseFill = None + self._mouseRelease=(evt.x,evt.y) + self._mouseMove = None + self._handleAction() + # if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL): + # self._placeFill() + # self.update() + # return True + self._mousePress = None + self._mouseDrag = None + self._mouseRelease = None return super().mousePressEvent(evt) @ttk.pyTTkSlot(ttk.TTkString) @@ -337,9 +396,9 @@ class PaintArea(ttk.TTkWidget): self.update() def _placeFill(self): - if not self._mouseFill: return False - mfill = self._mouseFill - self._mouseFill = None + if not self._mouseDrag: return False + mfill = self._mouseDrag + self._mouseDrag = None self._mouseMove = None ret = self._currentLayer.placeFill(mfill,self._tool,self._glyph,self._glyphColor) self.update() @@ -361,7 +420,7 @@ class PaintArea(ttk.TTkWidget): tc = self._transparentColor canvas.fill(pos=(0,0),size=(pw,ph),color=tc) for l in self._canvasLayers: - l.drawInCanvas(pos=(0,0),canvas=canvas) + l.drawInCanvas(pos=l.pos(),canvas=canvas) # for y in range(h): # canvas._data[y][0:w] = data[y][0:w] # for x in range(w): @@ -372,8 +431,9 @@ class PaintArea(ttk.TTkWidget): gc = self._glyphColor canvas._data[y][x] = self._glyph canvas._colors[y][x] = gc if gc._bg else gc+tc - if self._mouseFill: - ax,ay,bx,by = self._mouseFill + if self._mouseDrag and self._mousePress: + ax,ay = self._mousePress + bx,by = self._mouseDrag ax = max(0,min(w-1,ax)) ay = max(0,min(h-1,ay)) bx = max(0,min(w-1,bx)) @@ -386,7 +446,7 @@ class PaintArea(ttk.TTkWidget): canvas.fill(pos=(x,y), size=(w,h), color=gc if gc._bg else gc+tc, char=gl) - if self._tool == PaintArea.Tool.RECTEMPTY: + elif self._tool == PaintArea.Tool.RECTEMPTY: canvas.drawText(pos=(x,y ),text=gl*w,color=gc) canvas.drawText(pos=(x,y+h-1),text=gl*w,color=gc) for y in range(y+1,y+h-1): From c1be0a88c8a058535bde4fc958e30592e5d7410e Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 13 Mar 2024 14:30:27 +0000 Subject: [PATCH 03/28] Layout select --- tools/dumb_paint_lib/layers.py | 86 +++++++++------------------- tools/dumb_paint_lib/maintemplate.py | 18 +++++- tools/dumb_paint_lib/paintarea.py | 71 ++++++++++------------- tools/dumb_paint_lib/palette.py | 1 + 4 files changed, 75 insertions(+), 101 deletions(-) diff --git a/tools/dumb_paint_lib/layers.py b/tools/dumb_paint_lib/layers.py index fd2468a2..a14e2d5f 100644 --- a/tools/dumb_paint_lib/layers.py +++ b/tools/dumb_paint_lib/layers.py @@ -20,20 +20,26 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -__all__ = ['Layers','Layer'] +__all__ = ['Layers','LayerData'] import sys, os sys.path.append(os.path.join(sys.path[0],'../..')) import TermTk as ttk -class Layer(): +class LayerData(): __slots__ = ('_name','_data') def __init__(self,name:ttk.TTkString=ttk.TTkString('New'),data=None) -> None: - self._name:ttk.TTkString = name + self._name:ttk.TTkString = ttk.TTkString(name) if type(name)==str else name self._data = data def name(self): return self._name + def setName(self,name): + self._name = name + def data(self): + return self._data + def setData(self,data): + self._data = data class _layerButton(ttk.TTkWidget): classStyle = { @@ -60,13 +66,13 @@ class _layerButton(ttk.TTkWidget): 'grid':1}, } - __slots = ('_layer','_first', '_isSelected', + __slots__ = ('_layer','_first', '_isSelected', # signals 'clicked' ) - def __init__(self, layer:Layer, **kwargs): + def __init__(self, layer:LayerData, **kwargs): self.clicked = ttk.pyTTkSignal(_layerButton) - self._layer:Layer = layer + self._layer:LayerData = layer self._isSelected = False self._first = True super().__init__(**kwargs) @@ -108,63 +114,49 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): # Signals 'layerSelected','layerAdded','layerDeleted') def __init__(self, **kwargs): - self.layerSelected = ttk.pyTTkSignal(Layer) - self.layerAdded = ttk.pyTTkSignal(Layer) - self.layerDeleted = ttk.pyTTkSignal(Layer) - firstLayer:_layerButton = _layerButton(layer=Layer(name=ttk.TTkString('Background'))) - self._selected = firstLayer - firstLayer._isSelected = True - firstLayer.clicked.connect(self._clickedLayer) - self._layers:list[_layerButton] = [firstLayer] + self.layerSelected = ttk.pyTTkSignal(LayerData) + self.layerAdded = ttk.pyTTkSignal(LayerData) + self.layerDeleted = ttk.pyTTkSignal(LayerData) + self._selected = None + self._layers:list[_layerButton] = [] super().__init__(**kwargs) self.viewChanged.connect(self._placeTheButtons) - self.layout().addWidget(firstLayer) - self._placeTheButtons() - self.layerAdded.emit(firstLayer) def viewFullAreaSize(self) -> tuple: _,_,w,h = self.layout().fullWidgetAreaGeometry() - # w,_ = self.size() - # h = 3*len(self._layers) return w,h def viewDisplayedSize(self) -> tuple: return self.size() - # def mouseMoveEvent(self, evt) -> bool: - # offx,offy = self.getViewOffsets() - # y = evt.y-offy - # llen = len(self._layers) - # if y==0: self._mouseMoved = self._layers[-1] - # elifse: self._mouseMoved = self._layers[y] - # return super().mouseMoveEvent(evt) - 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): + def _clickedLayer(self, layerButton:_layerButton): if sel:=self._selected: sel._isSelected = False sel.update() self._selected = layerButton layerButton._isSelected = True + self.layerSelected.emit(layerButton._layer) self.update() @ttk.pyTTkSlot() - def addLayer(self): - _l=Layer() - newLayer:_layerButton = _layerButton(parent=self,layer=_l) - self._layers.insert(0,newLayer) + def addLayer(self,name=None): + name = name if name else f"Layer #{len(self._layers)}" + _l=LayerData(name=name) + newLayerBtn:_layerButton = _layerButton(parent=self,layer=_l) + self._layers.insert(0,newLayerBtn) if sel:=self._selected: sel._isSelected = False - self._selected = newLayer - newLayer._isSelected = True - newLayer.clicked.connect(self._clickedLayer) + self._selected = newLayerBtn + newLayerBtn._isSelected = True + newLayerBtn.clicked.connect(self._clickedLayer) self.viewChanged.emit() self._placeTheButtons() - self.layerAdded.emit(newLayer) + self.layerAdded.emit(newLayerBtn._layer) return _l def _placeTheButtons(self): @@ -175,32 +167,10 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): l.lowerWidget() self.update() - - @ttk.pyTTkSlot() def delLayer(self): self._layers.remove() - # def paintEvent(self, canvas: ttk.TTkCanvas): - # w,h = self.size() - # offx,offy = self.getViewOffsets() - # llen = len(self._layers) - # color = ttk.TTkColor.RST - # colorSelected = ttk.TTkColor.fg('#FFFF66') - # colorMouseMoved = ttk.TTkColor.bg('#003333') - # colorHidden = ttk.TTkColor.bg('#003333') - # for i,l in enumerate(self._layers): - # # drawing from the bottom - # py = 2*(llen-1)-i*2 - # canvas.drawText( pos=(0,py ),text=f" ┏{'━'*(w-7)}┓") - # canvas.drawText( pos=(0,py+2),text=f" ┗{'━'*(w-7)}┛") - # if i None: self._pos = (0,0) self._size = (0,0) - self._data = [] - self._colors = [] + self._data: list[list[str ]] = [] + self._colors:list[list[ttk.TTkColor]] = [] def pos(self): return self._pos @@ -237,10 +237,19 @@ class CanvasLayer(): data = self._data colors = self._colors for y in range(cy,cy+dh): - canvas._data[y][cx:cx+dw] = data[y+ly-cy][lx:lx+dw] for x in range(cx,cx+dw): - c = colors[y+ly-cy][x+lx-cx] - canvas._colors[y][x] = c if c._bg else c+canvas._colors[y][x] + gl = data[y+ly-cy][x+lx-cx] + c = colors[y+ly-cy][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 + class PaintArea(ttk.TTkWidget): class Tool(int): @@ -277,6 +286,18 @@ class PaintArea(ttk.TTkWidget): self._canvasSize = (w,h) self.update() + def setCurrentLayer(self, layer:CanvasLayer): + self._currentLayer = layer + + def newLayer(self) -> CanvasLayer: + newLayer = CanvasLayer() + w,h = self.size() + 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): self._currentLayer.importLayer(dd) self.update() @@ -293,7 +314,7 @@ class PaintArea(ttk.TTkWidget): def _handleAction(self): mp = self._mousePress - mm = self._mouseMove + # mm = self._mouseMove md = self._mouseDrag mr = self._mouseRelease l = self._currentLayer @@ -309,34 +330,23 @@ class PaintArea(ttk.TTkWidget): elif self._tool == self.Tool.BRUSH and (mp or md): if md: mx,my = md else: mx,my = mp - self._currentLayer.placeGlyph(lx+mx,ly+my,self._glyph,self._glyphColor) + self._currentLayer.placeGlyph(mx-lx,my-ly,self._glyph,self._glyphColor) elif self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL) and mr and mp: mpx,mpy = mp mrx,mry = mr - self._currentLayer.placeFill((mpx,mpy,mrx,mry),self._tool,self._glyph,self._glyphColor) + self._currentLayer.placeFill((mpx-lx,mpy-ly,mrx-lx,mry-ly),self._tool,self._glyph,self._glyphColor) self.update() def mouseMoveEvent(self, evt) -> bool: self._mouseMove = (evt.x,evt.y) self._mouseDrag = None self.update() - # self._handleAction() return True def mouseDragEvent(self, evt) -> bool: self._mouseDrag=(evt.x,evt.y) self._mouseMove= None self._handleAction() - #x,y = evt.x,evt.y - #if self._tool == self.Tool.BRUSH: - # if self._placeGlyph(evt.x, evt.y): - # return True - #if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL) and self._mouseDrag: - # mx,my = self._mouseDrag[:2] - # self._mouseDrag = [mx,my,x,y] - # self.update() - # return True - #return super().mouseDragEvent(evt) return True def mousePressEvent(self, evt) -> bool: @@ -345,25 +355,12 @@ class PaintArea(ttk.TTkWidget): self._mouseDrag = None self._mouseRelease = None self._handleAction() - # x,y = evt.x,evt.y - # if self._tool == self.Tool.BRUSH: - # if self._placeGlyph(x,y): - # return True - # if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL): - # self._mouseDrag = [x,y,x,y] - # self.update() - # return True - # return super().mousePressEvent(evt) return True def mouseReleaseEvent(self, evt) -> bool: self._mouseRelease=(evt.x,evt.y) self._mouseMove = None self._handleAction() - # if self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL): - # self._placeFill() - # self.update() - # return True self._mousePress = None self._mouseDrag = None self._mouseRelease = None @@ -373,7 +370,6 @@ class PaintArea(ttk.TTkWidget): def glyphFromString(self, ch:ttk.TTkString): if len(ch)<=0: return self._glyph = ch.charAt(0) - # self._glyphColor = ch.colorAt(0) def glyph(self): return self._glyph @@ -415,17 +411,10 @@ class PaintArea(ttk.TTkWidget): cw,ch = canvas.size() w=min(cw,pw) h=min(ch,ph) - # data = self._canvasArea['data'] - # colors = self._canvasArea['colors'] tc = self._transparentColor canvas.fill(pos=(0,0),size=(pw,ph),color=tc) for l in self._canvasLayers: l.drawInCanvas(pos=l.pos(),canvas=canvas) - # for y in range(h): - # canvas._data[y][0:w] = data[y][0:w] - # for x in range(w): - # c = colors[y][x] - # canvas._colors[y][x] = c if c._bg else c+tc if self._mouseMove: x,y = self._mouseMove gc = self._glyphColor diff --git a/tools/dumb_paint_lib/palette.py b/tools/dumb_paint_lib/palette.py index f036fb43..854a9ad3 100644 --- a/tools/dumb_paint_lib/palette.py +++ b/tools/dumb_paint_lib/palette.py @@ -55,6 +55,7 @@ class Palette(ttk.TTkWidget): self._mouseMove = None self.setPalette(_defaultPalette) super().__init__(*args, **kwargs) + self.setFocusPolicy(ttk.TTkK.ClickFocus) def setPalette(self, palette): self._palette = [] From 3f1df10e91f3c55b74d9ff4d01686114a78adce8 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Thu, 14 Mar 2024 17:09:40 +0000 Subject: [PATCH 04/28] Added import/export layer/document --- tools/dumb_paint_lib/layers.py | 14 ++- tools/dumb_paint_lib/maintemplate.py | 131 ++++++++++++++---------- tools/dumb_paint_lib/paintarea.py | 145 ++++++++++++++++++++++----- 3 files changed, 209 insertions(+), 81 deletions(-) diff --git a/tools/dumb_paint_lib/layers.py b/tools/dumb_paint_lib/layers.py index a14e2d5f..bd037ee7 100644 --- a/tools/dumb_paint_lib/layers.py +++ b/tools/dumb_paint_lib/layers.py @@ -144,10 +144,17 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): self.layerSelected.emit(layerButton._layer) self.update() + def clear(self): + for layBtn in self._layers: + self.layout().removeWidget(layBtn) + layBtn.clicked.clear() + self._layers.clear() + self.update() + @ttk.pyTTkSlot() - def addLayer(self,name=None): + def addLayer(self,name=None, data=None): name = name if name else f"Layer #{len(self._layers)}" - _l=LayerData(name=name) + _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 @@ -174,7 +181,7 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): class Layers(ttk.TTkGridLayout): __slots__ = ('_scrollWidget', # Forward Methods - 'addLayer', + 'addLayer','clear', # Forward Signals 'layerSelected','layerAdded','layerDeleted','layerOrderChanged') def __init__(self, **kwargs): @@ -205,3 +212,4 @@ class Layers(ttk.TTkGridLayout): # forward methods self.addLayer = _lsw.addLayer + self.clear = _lsw.clear diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index 5094abfe..de5c97e0 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -114,8 +114,8 @@ class LeftPanel(ttk.TTkVBoxLayout): class ExportArea(ttk.TTkGridLayout): __slots__ = ('_paintArea', '_te') - def __init__(self, paintArea, **kwargs): - self._paintArea = paintArea + 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") @@ -129,59 +129,65 @@ class ExportArea(ttk.TTkGridLayout): self.addWidget(self._te,1,0,1,5) btn_exLa.clicked.connect(self._exportLayer) + btn_exPr.clicked.connect(self._exportDocument) @ttk.pyTTkSlot() def _exportLayer(self): - # Don't try this at home - pw,ph = self._paintArea._canvasSize - data = self._paintArea._canvasArea['data'] - colors = self._paintArea._canvasArea['colors'] - # get the bounding box - xa,xb,ya,yb = pw,0,ph,0 - 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!!!") + dd = self._paintArea.exportLayer() + if not dd: + self._te.setText('# No Data toi be saved!!!') return - out = "data = {'data': [\n" - 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.setText('# Compressed Data:') 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)]) + '")' self._te.append(b64list) + self._te.append('\n# Uncompressed Data:') + outTxt = '{\n' + for i in dd: + if i in ('data','colors'): continue + outTxt += f" '{i}':'{dd[i]}',\n" + for l in dd['data']: + outTxt += f" {l},\n" + outTxt += " ],'colors':[\n" + for l in dd['colors']: + outTxt += f" {l},\n" + 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): + dd = self._paintArea.exportDocument() + 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: # @@ -193,13 +199,14 @@ class ExportArea(ttk.TTkGridLayout): # Export # class PaintTemplate(ttk.TTkAppTemplate): + __slots__ = ('_parea','_layers') def __init__(self, border=False, **kwargs): super().__init__(border, **kwargs) - self._parea = parea = PaintArea() + self._parea = parea = PaintArea() + self._layers = layers = Layers() ptoolkit = PaintToolKit() tarea = TextArea() - layers = Layers() - expArea = ExportArea(self._parea) + expArea = ExportArea(parea) leftPanel = LeftPanel() palette = leftPanel.palette() @@ -251,20 +258,30 @@ class PaintTemplate(ttk.TTkAppTemplate): self._parea.setGlyphColor(palette.color()) ptoolkit.setColor(palette.color()) - # Connect and handle Layers event - @ttk.pyTTkSlot(LayerData) - def _layerAdded(l:LayerData): - nl = parea.newLayer() - l.setData(nl) - @ttk.pyTTkSlot(LayerData) def _layerSelected(l:LayerData): parea.setCurrentLayer(l.data()) - layers.layerAdded.connect(_layerAdded) + layers.layerAdded.connect(self._layerAdded) layers.layerSelected.connect(_layerSelected) layers.addLayer(name="Background") + # Connect and handle Layers event + @ttk.pyTTkSlot(LayerData) + def _layerAdded(self, l:LayerData): + nl = self._parea.newLayer() + nl.setName(l.name()) + l.setData(nl) + + 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(): + self._layers.addLayer(name=l.name(),data=l) + 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__)),"quickImport.tui.json")) @@ -298,7 +315,11 @@ class PaintTemplate(ttk.TTkAppTemplate): ttk.TTkHelper.overlay(None, messageBox, 5, 5, True) return - self._parea.importLayer(dd) + if 'layers' in dd: + self.importDocument(dd) + else: + self._layers.addLayer(name="Import") + self._parea.importLayer(dd) newWindow.close() diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index b9db0b82..7c174b85 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -119,10 +119,11 @@ class PaintToolKit(ttk.TTkGridLayout): self._refreshColor(emit=False) class CanvasLayer(): - __slot__ = ('_pos','_size','_data','_colors') + __slot__ = ('_pos','_name','_size','_data','_colors') def __init__(self) -> None: self._pos = (0,0) self._size = (0,0) + self._name = "" self._data: list[list[str ]] = [] self._colors:list[list[ttk.TTkColor]] = [] @@ -131,6 +132,11 @@ class CanvasLayer(): def size(self): return self._size + def name(self): + return self._name + def setName(self, name): + self._name = name + def move(self,x,y): self._pos=(x,y) @@ -156,29 +162,94 @@ class CanvasLayer(): self._data[i] = [' ']*w self._colors[i] = [ttk.TTkColor.RST]*w - def importLayer(self, dd): - w,h = self._size - w = len(dd['data'][0]) + 10 - h = len(dd['data']) + 4 - x,y=5,2 + def exportLayer(self): + # Don't try this at home + px,py = self.pos() + pw,ph = self.size() + data = self._data + colors = self._colors + # get the bounding box + xa,xb,ya,yb = pw,0,ph,0 + 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,ya,yb) == (pw,0,ph,0): + xa=xb=ya=yb=0 + + + #if xa>xb or ya>yb: + # return {} + + outData = { + 'version':'1.0.0', + 'size':[xb-xa+1,yb-ya+1], + 'pos': (px+xa,py+ya), + 'name':str(self.name()), + 'data':[], 'colors':[], '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) + + for row in data[ya:yb+1]: + outData['data'].append(row[xa:xb+1]) + for row in colors[ya:yb+1]: + 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 + outData['colors'][-1].append(palette.index((fg,bg))) + return outData - self.resizeCanvas(w,h) + def importLayer(self, dd): self.clean() - 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): + if 'version' in dd and dd['version']=='1.0.0': + 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: - 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 + 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']] + else: # 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 placeFill(self,geometry,tool,glyph,color): w,h = self._size @@ -267,8 +338,8 @@ class PaintArea(ttk.TTkWidget): def __init__(self, *args, **kwargs): self._transparentColor = ttk.TTkColor.bg('#FF00FF') - self._currentLayer:CanvasLayer = CanvasLayer() - self._canvasLayers:list[CanvasLayer] = [self._currentLayer] + self._currentLayer:CanvasLayer = None + self._canvasLayers:list[CanvasLayer] = [] self._glyph = 'X' self._glyphColor = ttk.TTkColor.RST self._posBk = (0,0) @@ -281,8 +352,12 @@ class PaintArea(ttk.TTkWidget): self.resizeCanvas(80,25) self.setFocusPolicy(ttk.TTkK.ClickFocus + ttk.TTkK.TabFocus) + def canvasLayers(self): + return self._canvasLayers + def resizeCanvas(self, w, h): - self._currentLayer.resize(w,h) + if self._currentLayer: + self._currentLayer.resize(w,h) self._canvasSize = (w,h) self.update() @@ -302,6 +377,30 @@ class PaintArea(ttk.TTkWidget): self._currentLayer.importLayer(dd) self.update() + def importDocument(self, dd): + self._canvasLayers = [] + if 'version' in dd and dd['version']=='1.0.0': + self.resizeCanvas(*dd['size']) + for l in dd['layers']: + nl = self.newLayer() + nl.importLayer(l) + + def exportImage(self): + return {} + + def exportLayer(self) -> dict: + if self._currentLayer: + return self._currentLayer.exportLayer() + return {} + + def exportDocument(self): + pw,ph = self._canvasSize + outData = { + 'version':'1.0.0', + 'size':(pw,ph), + 'layers':[l.exportLayer() for l in self._canvasLayers]} + return outData + def leaveEvent(self, evt): self._mouseMove = None self.update() From efa0bdc3af7b1fbefce511cb471733d6c673d7e6 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Thu, 14 Mar 2024 18:00:29 +0000 Subject: [PATCH 05/28] Added layer name edit --- tools/dumb_paint_lib/layers.py | 24 +++++++++++++++++++++--- tools/dumb_paint_lib/maintemplate.py | 6 ++++-- tools/dumb_paint_lib/paintarea.py | 1 + 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/tools/dumb_paint_lib/layers.py b/tools/dumb_paint_lib/layers.py index bd037ee7..423fa04c 100644 --- a/tools/dumb_paint_lib/layers.py +++ b/tools/dumb_paint_lib/layers.py @@ -28,20 +28,24 @@ sys.path.append(os.path.join(sys.path[0],'../..')) import TermTk as ttk class LayerData(): - __slots__ = ('_name','_data') + __slots__ = ('_name','_data', + #signals + 'nameChanged') 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._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.TTkWidget): +class _layerButton(ttk.TTkContainer): classStyle = { 'default': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#000044"), 'borderColor': ttk.TTkColor.fg('#CCDDDD'), @@ -67,6 +71,7 @@ class _layerButton(ttk.TTkWidget): } __slots__ = ('_layer','_first', '_isSelected', + '_ledit', # signals 'clicked' ) @@ -75,12 +80,25 @@ class _layerButton(ttk.TTkWidget): self._layer:LayerData = layer self._isSelected = False self._first = True - super().__init__(**kwargs) + 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) + + @ttk.pyTTkSlot(str) + def _textEdited(self, text): + self._layer.setName(text) 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 paintEvent(self, canvas: ttk.TTkCanvas): # if self.isEnabled() and self._checkable: # if self._checked: diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index de5c97e0..802ace98 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -223,7 +223,7 @@ class PaintTemplate(ttk.TTkAppTemplate): self.setItem(leftPanel , self.LEFT, size=16*2) self.setWidget(self._parea , self.MAIN) self.setItem(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) fileMenu = appMenuBar.addMenu("&File") @@ -272,6 +272,7 @@ class PaintTemplate(ttk.TTkAppTemplate): nl = self._parea.newLayer() nl.setName(l.name()) l.setData(nl) + l.nameChanged.connect(nl.setName) def importDocument(self, dd): self._parea.importDocument(dd) @@ -279,7 +280,8 @@ class PaintTemplate(ttk.TTkAppTemplate): # Little Hack that I don't know how to overcome self._layers.layerAdded.disconnect(self._layerAdded) for l in self._parea.canvasLayers(): - self._layers.addLayer(name=l.name(),data=l) + ld = self._layers.addLayer(name=l.name(),data=l) + ld.nameChanged.connect(l.setName) self._layers.layerAdded.connect(self._layerAdded) @ttk.pyTTkSlot() diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 7c174b85..b6f1e0b4 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -134,6 +134,7 @@ class CanvasLayer(): def name(self): return self._name + @ttk.pyTTkSlot(str) def setName(self, name): self._name = name From d8d0b1d14aa82bdb6e4a2f78b8bd5e0c7021fbb3 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Thu, 14 Mar 2024 23:15:56 +0000 Subject: [PATCH 06/28] added Layers visibility --- tools/dumb_paint_lib/layers.py | 26 +++++++++++++++++++++----- tools/dumb_paint_lib/maintemplate.py | 4 ++++ tools/dumb_paint_lib/paintarea.py | 10 +++++++++- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/tools/dumb_paint_lib/layers.py b/tools/dumb_paint_lib/layers.py index 423fa04c..77abdd3e 100644 --- a/tools/dumb_paint_lib/layers.py +++ b/tools/dumb_paint_lib/layers.py @@ -30,9 +30,10 @@ import TermTk as ttk class LayerData(): __slots__ = ('_name','_data', #signals - 'nameChanged') + '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): @@ -70,26 +71,38 @@ class _layerButton(ttk.TTkContainer): 'grid':1}, } - __slots__ = ('_layer','_first', '_isSelected', + __slots__ = ('_layer','_first', '_isSelected', '_layerVisible', '_ledit', # signals - 'clicked' + 'clicked', 'visibilityToggled', ) def __init__(self, layer:LayerData, **kwargs): self.clicked = ttk.pyTTkSignal(_layerButton) self._layer: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._layer.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 @@ -118,13 +131,14 @@ class _layerButton(ttk.TTkContainer): 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" □ ▣ ┃{' '*(w-7)}┃",color=borderColor) + canvas.drawText(pos=(0,1),text=f" {btnVisible} - ┃{' '*(w-7)}┃",color=borderColor) else: - canvas.drawText(pos=(0,1),text=f" □ ▣ ╽{' '*(w-7)}╽",color=borderColor) + canvas.drawText(pos=(0,1),text=f" {btnVisible} - ╽{' '*(w-7)}╽",color=borderColor) canvas.drawTTkString(pos=(7,1),text=self._layer.name(), color=textColor) class LayerScrollWidget(ttk.TTkAbstractScrollView): @@ -166,6 +180,8 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): for layBtn in self._layers: self.layout().removeWidget(layBtn) layBtn.clicked.clear() + layBtn.visibilityToggled.clear() + layBtn._layer.nameChanged.clear() self._layers.clear() self.update() diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index 802ace98..5f55b7a5 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -273,6 +273,8 @@ class PaintTemplate(ttk.TTkAppTemplate): nl.setName(l.name()) l.setData(nl) l.nameChanged.connect(nl.setName) + l.visibilityToggled.connect(nl.setVisible) + l.visibilityToggled.connect(self._parea.update) def importDocument(self, dd): self._parea.importDocument(dd) @@ -282,6 +284,8 @@ class PaintTemplate(ttk.TTkAppTemplate): 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() diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index b6f1e0b4..bafc552e 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -119,11 +119,12 @@ class PaintToolKit(ttk.TTkGridLayout): self._refreshColor(emit=False) class CanvasLayer(): - __slot__ = ('_pos','_name','_size','_data','_colors') + __slot__ = ('_pos','_name','_visible','_size','_data','_colors') def __init__(self) -> None: self._pos = (0,0) self._size = (0,0) self._name = "" + self._visible = True self._data: list[list[str ]] = [] self._colors:list[list[ttk.TTkColor]] = [] @@ -132,6 +133,12 @@ class CanvasLayer(): 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) @@ -292,6 +299,7 @@ class CanvasLayer(): 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() From 1a929084a307171945f7be49faf577175fb0a89c Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 15 Mar 2024 10:15:47 +0000 Subject: [PATCH 07/28] Added Layer move Up/Down --- tools/dumb_paint_lib/layers.py | 48 ++++++++++++++++++++-------- tools/dumb_paint_lib/maintemplate.py | 6 ++++ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/tools/dumb_paint_lib/layers.py b/tools/dumb_paint_lib/layers.py index 77abdd3e..50589d4d 100644 --- a/tools/dumb_paint_lib/layers.py +++ b/tools/dumb_paint_lib/layers.py @@ -71,14 +71,14 @@ class _layerButton(ttk.TTkContainer): 'grid':1}, } - __slots__ = ('_layer','_first', '_isSelected', '_layerVisible', + __slots__ = ('_layerData','_first', '_isSelected', '_layerVisible', '_ledit', # signals 'clicked', 'visibilityToggled', ) def __init__(self, layer:LayerData, **kwargs): self.clicked = ttk.pyTTkSignal(_layerButton) - self._layer:LayerData = layer + self._layerData:LayerData = layer self._isSelected = False self._first = True self._layerVisible = True @@ -93,7 +93,7 @@ class _layerButton(ttk.TTkContainer): @ttk.pyTTkSlot(str) def _textEdited(self, text): - self._layer.setName(text) + self._layerData.setName(text) def mousePressEvent(self, evt) -> bool: if evt.x <= 3: @@ -139,16 +139,18 @@ class _layerButton(ttk.TTkContainer): 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._layer.name(), color=textColor) + canvas.drawTTkString(pos=(7,1),text=self._layerData.name(), color=textColor) class LayerScrollWidget(ttk.TTkAbstractScrollView): __slots__ = ('_layers','_selected', # Signals - 'layerSelected','layerAdded','layerDeleted') + 'layerSelected','layerAdded','layerDeleted','layersOrderChanged') def __init__(self, **kwargs): self.layerSelected = ttk.pyTTkSignal(LayerData) - self.layerAdded = ttk.pyTTkSignal(LayerData) - self.layerDeleted = ttk.pyTTkSignal(LayerData) + self.layerAdded = ttk.pyTTkSignal(LayerData) + self.layerDeleted = ttk.pyTTkSignal(LayerData) + self.layersOrderChanged = ttk.pyTTkSignal(list[LayerData]) + self._selected = None self._layers:list[_layerButton] = [] super().__init__(**kwargs) @@ -173,7 +175,7 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): sel.update() self._selected = layerButton layerButton._isSelected = True - self.layerSelected.emit(layerButton._layer) + self.layerSelected.emit(layerButton._layerData) self.update() def clear(self): @@ -181,10 +183,27 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): self.layout().removeWidget(layBtn) layBtn.clicked.clear() layBtn.visibilityToggled.clear() - layBtn._layer.nameChanged.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)}" @@ -197,7 +216,7 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): newLayerBtn.clicked.connect(self._clickedLayer) self.viewChanged.emit() self._placeTheButtons() - self.layerAdded.emit(newLayerBtn._layer) + self.layerAdded.emit(newLayerBtn._layerData) return _l def _placeTheButtons(self): @@ -217,7 +236,7 @@ class Layers(ttk.TTkGridLayout): # Forward Methods 'addLayer','clear', # Forward Signals - 'layerSelected','layerAdded','layerDeleted','layerOrderChanged') + 'layerSelected','layerAdded','layerDeleted','layersOrderChanged') def __init__(self, **kwargs): super().__init__(**kwargs) self._scrollWidget = _lsw = LayerScrollWidget() @@ -235,14 +254,17 @@ class Layers(ttk.TTkGridLayout): 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) + 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 diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index 5f55b7a5..fd5d6b1a 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -264,6 +264,7 @@ class PaintTemplate(ttk.TTkAppTemplate): layers.layerAdded.connect(self._layerAdded) layers.layerSelected.connect(_layerSelected) + layers.layersOrderChanged.connect(self._layersOrderChanged) layers.addLayer(name="Background") # Connect and handle Layers event @@ -276,6 +277,11 @@ class PaintTemplate(ttk.TTkAppTemplate): 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() From 59a05b663ea59415db4533388efa330ff4f41ee7 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 15 Mar 2024 11:39:58 +0000 Subject: [PATCH 08/28] Added Drag'nDrop in the layers to reorder them --- tools/dumb_paint_lib/layers.py | 71 +++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/tools/dumb_paint_lib/layers.py b/tools/dumb_paint_lib/layers.py index 50589d4d..f248eb11 100644 --- a/tools/dumb_paint_lib/layers.py +++ b/tools/dumb_paint_lib/layers.py @@ -112,19 +112,19 @@ class _layerButton(ttk.TTkContainer): 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.isEnabled() and self._checkable: - # if self._checked: - # style = self.style()['checked'] - # else: - # style = self.style()['unchecked'] - # if self.hasFocus(): - # borderColor = self.style()['focus']['borderColor'] - # else: - # borderColor = style['borderColor'] - # else: - # style = self.currentStyle() - # borderColor = style['borderColor'] if self._isSelected: style = self.style()['selected'] else: @@ -139,10 +139,10 @@ class _layerButton(ttk.TTkContainer): 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(), color=textColor) + canvas.drawTTkString(pos=(7,1),text=self._layerData.name(), width=w-9, color=textColor) class LayerScrollWidget(ttk.TTkAbstractScrollView): - __slots__ = ('_layers','_selected', + __slots__ = ('_layers','_selected', '_dropTo', # Signals 'layerSelected','layerAdded','layerDeleted','layersOrderChanged') def __init__(self, **kwargs): @@ -152,6 +152,7 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): self.layersOrderChanged = ttk.pyTTkSignal(list[LayerData]) self._selected = None + self._dropTo = None self._layers:list[_layerButton] = [] super().__init__(**kwargs) self.viewChanged.connect(self._placeTheButtons) @@ -231,6 +232,48 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): def delLayer(self): self._layers.remove() + def dragEnterEvent(self, evt) -> bool: + if type(evt.data())!=_layerButton: return False + self._dropTo = max(0,min(len(self._layers),(evt.y-1)//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 + self._dropTo = max(0,min(len(self._layers),(evt.y-1)//2)) + self.update() + ttk.TTkLog.debug(f"{evt.x},{evt.y} - {len(self._layers)} - {self._dropTo}") + return True + def dropEvent(self, evt) -> bool: + if type(evt.data())!=_layerButton: return False + self._dropTo = None + data = evt.data() + # dropPos = len(self._layers)-(evt.y-1)//2 + dropPos = max(0,min(len(self._layers),(evt.y-1)//2)) + ttk.TTkLog.debug(f"{evt.x},{evt.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() + if self._dropTo == None: return + canvas = self.getCanvas() + w,h = canvas.size() + color = ttk.TTkColor.YELLOW + canvas.drawText(pos=(0,self._dropTo*2),text=f"╞{'═'*(w-2)}╍",color=color) + + + class Layers(ttk.TTkGridLayout): __slots__ = ('_scrollWidget', # Forward Methods From 2f44f2cf66c88e74920aea576dcd244b1cc37632 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 15 Mar 2024 14:03:41 +0000 Subject: [PATCH 09/28] Move the selected layouts --- tools/dumb_paint_lib/paintarea.py | 36 +++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index bafc552e..d61cd27a 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -145,6 +145,15 @@ class CanvasLayer(): def setName(self, name): self._name = name + def isOpaque(self,x,y): + if not self._visible: return False + w,h = self._size + data = self._data + colors = self._colors + if 0<=x bool: self._mousePress=(evt.x,evt.y) + self._moveData = None self._mouseMove = None self._mouseDrag = None self._mouseRelease = None @@ -469,6 +490,7 @@ class PaintArea(ttk.TTkWidget): self._mouseRelease=(evt.x,evt.y) self._mouseMove = None self._handleAction() + self._moveData = None self._mousePress = None self._mouseDrag = None self._mouseRelease = None @@ -504,12 +526,14 @@ class PaintArea(ttk.TTkWidget): mfill = self._mouseDrag self._mouseDrag = None self._mouseMove = None + self._moveData = None ret = self._currentLayer.placeFill(mfill,self._tool,self._glyph,self._glyphColor) self.update() return ret def _placeGlyph(self,x,y): self._mouseMove = None + self._moveData = None ret = self._currentLayer.placeGlyph(x,y,self._glyph,self._glyphColor) self.update() return ret From 89e410a825af128dcff5cf8034f45099a2b0f93b Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sat, 16 Mar 2024 10:39:31 +0000 Subject: [PATCH 10/28] Remde the toolkit with the ttkDesigner, basic OpenFile, you can drag the main area, added markers --- tools/dumb.paint.tool.py | 35 +- tools/dumb_paint_lib/layers.py | 22 +- tools/dumb_paint_lib/maintemplate.py | 25 +- tools/dumb_paint_lib/paintToolKit.tui.json | 918 +++++++++++++++++++++ tools/dumb_paint_lib/paintarea.py | 212 +++-- 5 files changed, 1137 insertions(+), 75 deletions(-) create mode 100644 tools/dumb_paint_lib/paintToolKit.tui.json diff --git a/tools/dumb.paint.tool.py b/tools/dumb.paint.tool.py index 2c1cd39d..cf058aec 100755 --- a/tools/dumb.paint.tool.py +++ b/tools/dumb.paint.tool.py @@ -32,16 +32,25 @@ from dumb_paint_lib import PaintTemplate ttk.TTkTheme.loadTheme(ttk.TTkTheme.NERD) -root = ttk.TTk( - title="Dumb Paint Tool", - layout=ttk.TTkGridLayout(), - mouseTrack=True, - sigmask=( - ttk.TTkTerm.Sigmask.CTRL_C | - ttk.TTkTerm.Sigmask.CTRL_Q | - ttk.TTkTerm.Sigmask.CTRL_S | - ttk.TTkTerm.Sigmask.CTRL_Z )) - -PaintTemplate(parent=root) - -root.mainloop() \ No newline at end of file +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('filename', type=str, nargs='?', help='the file to open') + # parser.add_argument('-k', '--showkeys', action='store_true', help='display the keypresses and mouse interactions') + args = parser.parse_args() + + root = ttk.TTk( + title="Dumb Paint Tool", + layout=ttk.TTkGridLayout(), + mouseTrack=True, + sigmask=( + # ttk.TTkTerm.Sigmask.CTRL_C | + 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() \ No newline at end of file diff --git a/tools/dumb_paint_lib/layers.py b/tools/dumb_paint_lib/layers.py index f248eb11..a1085674 100644 --- a/tools/dumb_paint_lib/layers.py +++ b/tools/dumb_paint_lib/layers.py @@ -156,6 +156,12 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): 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() @@ -234,7 +240,8 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): def dragEnterEvent(self, evt) -> bool: if type(evt.data())!=_layerButton: return False - self._dropTo = max(0,min(len(self._layers),(evt.y-1)//2)) + 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: @@ -244,17 +251,19 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): return True def dragMoveEvent(self, evt) -> bool: if type(evt.data())!=_layerButton: return False - self._dropTo = max(0,min(len(self._layers),(evt.y-1)//2)) + 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} - {len(self._layers)} - {self._dropTo}") + 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)//2)) - ttk.TTkLog.debug(f"{evt.x},{evt.y} - {len(self._layers)} - {self._dropTo} {dropPos}") + 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) @@ -266,11 +275,12 @@ class LayerScrollWidget(ttk.TTkAbstractScrollView): # 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),text=f"╞{'═'*(w-2)}╍",color=color) + canvas.drawText(pos=(0,(self._dropTo)*2-offy),text=f"╞{'═'*(w-2)}╍",color=color) diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index fd5d6b1a..3474addc 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -22,12 +22,12 @@ __all__ = ['PaintTemplate'] -import sys, os +import sys, os, json sys.path.append(os.path.join(sys.path[0],'../..')) import TermTk as ttk -from .paintarea import PaintArea, PaintToolKit +from .paintarea import PaintArea, PaintToolKit, PaintScrollArea from .palette import Palette from .textarea import TextArea from .layers import Layers,LayerData @@ -200,7 +200,7 @@ class ExportArea(ttk.TTkGridLayout): # class PaintTemplate(ttk.TTkAppTemplate): __slots__ = ('_parea','_layers') - def __init__(self, border=False, **kwargs): + def __init__(self, fileName=None, border=False, **kwargs): super().__init__(border, **kwargs) self._parea = parea = PaintArea() self._layers = layers = Layers() @@ -221,8 +221,8 @@ class PaintTemplate(ttk.TTkAppTemplate): self.setItem(expArea, self.BOTTOM, title="Export") self.setItem(leftPanel , self.LEFT, size=16*2) - self.setWidget(self._parea , self.MAIN) - self.setItem(ptoolkit , self.TOP, fixed=True) + 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) @@ -266,6 +266,21 @@ class PaintTemplate(ttk.TTkAppTemplate): layers.layerSelected.connect(_layerSelected) layers.layersOrderChanged.connect(self._layersOrderChanged) layers.addLayer(name="Background") + if fileName: + self._openFile(fileName) + + 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) + 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) diff --git a/tools/dumb_paint_lib/paintToolKit.tui.json b/tools/dumb_paint_lib/paintToolKit.tui.json new file mode 100644 index 00000000..18e4fbaf --- /dev/null +++ b/tools/dumb_paint_lib/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": [ + 8, + 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": [ + 30, + 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": [ + 30, + 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": [ + 37, + 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": [ + 37, + 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": [ + 38, + 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": [ + 38, + 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": [ + 46, + 0 + ], + "Size": [ + 6, + 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": [ + 52, + 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": [ + 52, + 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": [ + 53, + 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": [ + 53, + 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": [ + 60, + 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": [ + 60, + 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": [ + 61, + 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": [ + 61, + 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)" + } + ] +} \ No newline at end of file diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index d61cd27a..1fe85abe 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -__all__ = ['PaintArea','PaintToolKit','CanvasLayer'] +__all__ = ['PaintArea','PaintScrollArea','PaintToolKit','CanvasLayer'] import sys, os @@ -28,9 +28,7 @@ sys.path.append(os.path.join(sys.path[0],'../..')) import TermTk as ttk -class PaintToolKit(ttk.TTkGridLayout): - - +class PaintToolKit(ttk.TTkContainer): __slots__ = ('_rSelect', '_rPaint', '_lgliph', '_cbFg', '_cbBg', '_bpFg', '_bpBg', '_bpDef', @@ -38,32 +36,18 @@ class PaintToolKit(ttk.TTkGridLayout): #Signals 'updatedColor', 'updatedTrans') def __init__(self, *args, **kwargs): + ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"paintToolKit.tui.json"),self) 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, enabled=False) - self._rPaint = ttk.TTkRadioButton(text='Paint ' , enabled=False) - 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._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._bpDef.setColor(ttk.TTkColor.bg('#FF00FF')) self._cbFg.toggled.connect(self._refreshColor) self._cbBg.toggled.connect(self._refreshColor) @@ -77,13 +61,12 @@ class PaintToolKit(ttk.TTkGridLayout): def _refreshColor(self, emit=True): color =self.color() self._lgliph.setText( - ttk.TTkString("Glyph\n '") + + 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 @@ -342,7 +325,7 @@ class CanvasLayer(): canvas._colors[y][x] = newC -class PaintArea(ttk.TTkWidget): +class PaintArea(ttk.TTkAbstractScrollView): class Tool(int): MOVE = 0x01 BRUSH = 0x02 @@ -351,13 +334,14 @@ class PaintArea(ttk.TTkWidget): __slots__ = ('_canvasLayers', '_currentLayer', '_transparentColor', + '_documentPos','_documentOffset','_documentSize', '_mouseMove', '_mouseDrag', '_mousePress', '_mouseRelease', '_moveData', '_tool', '_glyph', '_glyphColor') def __init__(self, *args, **kwargs): - self._transparentColor = ttk.TTkColor.bg('#FF00FF') + self._transparentColor = {'base':ttk.TTkColor.RST,'dim':ttk.TTkColor.RST} self._currentLayer:CanvasLayer = None self._canvasLayers:list[CanvasLayer] = [] self._glyph = 'X' @@ -368,17 +352,61 @@ class PaintArea(ttk.TTkWidget): self._mousePress = None self._mouseRelease = None self._tool = self.Tool.BRUSH + self._documentOffset = ( 0, 0) + self._documentPos = (6,3) + self._documentSize = ( 0, 0) super().__init__(*args, **kwargs) + self.setTrans(ttk.TTkColor.bg('#FF00FF')) self.resizeCanvas(80,25) self.setFocusPolicy(ttk.TTkK.ClickFocus + ttk.TTkK.TabFocus) + def _getGeometry(self): + dx,dy = self._documentPos + # doffx,doffy = self._documentOffset + # dx+=doffx + # dy+=doffy + dw,dh = self._documentSize + ww,wh = self.size() + # dw,dh = max(dw,dx+ww),max(dh,dx+wh) + 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): + x1,y1,_,_ = self._getGeometry() + self._documentOffset = (max(0,-x1),max(0,-y1)) + self.viewMoveTo(max(0,-x1),max(0,-y1)) + self.viewChanged.emit() + # dx,dy = self._documentPos + # self.chan + + 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): if self._currentLayer: self._currentLayer.resize(w,h) - self._canvasSize = (w,h) + self._documentSize = (w,h) self.update() def setCurrentLayer(self, layer:CanvasLayer): @@ -395,7 +423,7 @@ class PaintArea(ttk.TTkWidget): def importLayer(self, dd): self._currentLayer.importLayer(dd) - self.update() + self._retuneGeometry() def importDocument(self, dd): self._canvasLayers = [] @@ -404,6 +432,7 @@ class PaintArea(ttk.TTkWidget): for l in dd['layers']: nl = self.newLayer() nl.importLayer(l) + self._retuneGeometry() def exportImage(self): return {} @@ -414,7 +443,7 @@ class PaintArea(ttk.TTkWidget): return {} def exportDocument(self): - pw,ph = self._canvasSize + pw,ph = self._documentSize outData = { 'version':'1.0.0', 'size':(pw,ph), @@ -433,6 +462,11 @@ class PaintArea(ttk.TTkWidget): self.update() def _handleAction(self): + dx,dy = self._documentPos + doffx,doffy = self._documentOffset + dx+=doffx + dy+=doffy + ox, oy = self.getViewOffsets() mp = self._mousePress # mm = self._mouseMove md = self._mouseDrag @@ -449,12 +483,16 @@ class PaintArea(ttk.TTkWidget): tml = lm self._moveData = {'pos':tml.pos(),'layer':tml} break - elif self._tool == self.Tool.MOVE and mp and md and self._moveData: + elif self._tool == self.Tool.MOVE and mp and md: mpx,mpy = mp mdx,mdy = md - px,py = self._moveData['pos'] - dx,dy = mdx-mpx,mdy-mpy - self._moveData['layer'].move(px+dx,py+dy) + pdx,pdy = mdx-mpx,mdy-mpy + if self._moveData: + px,py = self._moveData['pos'] + self._moveData['layer'].move(px+pdx,py+pdy) + else: + self._documentPos = (dx+pdx,dy+pdy) + self._retuneGeometry() elif self._tool == self.Tool.BRUSH and (mp or md): if md: mx,my = md else: mx,my = mp @@ -466,19 +504,34 @@ class PaintArea(ttk.TTkWidget): self.update() def mouseMoveEvent(self, evt) -> bool: - self._mouseMove = (evt.x,evt.y) + dx,dy = self._documentPos + doffx,doffy = self._documentOffset + dx+=doffx + dy+=doffy + ox, oy = self.getViewOffsets() + self._mouseMove=(evt.x+ox-dx,evt.y+oy-dy) self._mouseDrag = None self.update() return True def mouseDragEvent(self, evt) -> bool: - self._mouseDrag=(evt.x,evt.y) + dx,dy = self._documentPos + doffx,doffy = self._documentOffset + dx+=doffx + dy+=doffy + ox, oy = self.getViewOffsets() + self._mouseDrag=(evt.x+ox-dx,evt.y+oy-dy) self._mouseMove= None self._handleAction() return True def mousePressEvent(self, evt) -> bool: - self._mousePress=(evt.x,evt.y) + dx,dy = self._documentPos + doffx,doffy = self._documentOffset + dx+=doffx + dy+=doffy + ox, oy = self.getViewOffsets() + self._mousePress=(evt.x+ox-dx,evt.y+oy-dy) self._moveData = None self._mouseMove = None self._mouseDrag = None @@ -487,7 +540,12 @@ class PaintArea(ttk.TTkWidget): return True def mouseReleaseEvent(self, evt) -> bool: - self._mouseRelease=(evt.x,evt.y) + dx,dy = self._documentPos + doffx,doffy = self._documentOffset + dx+=doffx + dy+=doffy + ox, oy = self.getViewOffsets() + self._mouseRelease=(evt.x+ox-dx,evt.y+oy-dy) self._mouseMove = None self._handleAction() self._moveData = None @@ -517,8 +575,13 @@ class PaintArea(ttk.TTkWidget): self._glyphColor = color @ttk.pyTTkSlot(ttk.TTkColor) - def setTrans(self, color): - self._transparentColor = color + 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 _placeFill(self): @@ -539,14 +602,56 @@ class PaintArea(ttk.TTkWidget): return ret def paintEvent(self, canvas:ttk.TTkCanvas): - pw,ph = self._canvasSize + dx,dy = self._documentPos + doffx,doffy = self._documentOffset + dx+=doffx + dy+=doffy + ox, oy = self.getViewOffsets() + dw,dh = self._documentSize + dox,doy = dx-ox,dy-oy cw,ch = canvas.size() - w=min(cw,pw) - h=min(ch,ph) - tc = self._transparentColor - canvas.fill(pos=(0,0),size=(pw,ph),color=tc) + w=min(cw,dw) + h=min(ch,dh) + tcb = self._transparentColor['base'] + tcd = self._transparentColor['dim'] + # canvas.fill(pos=(0 ,dy-oy),size=(cw,dh),color=tcd) + # canvas.fill(pos=(dx-ox,0 ),size=(dw,ch),color=tcd) + + if l:=self._currentLayer: + tclb = self._transparentColor['layer'] + tcld = self._transparentColor['layerDim'] + lx,ly = l.pos() + lw,lh = l.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) + + # Draw canvas/currentLayout ruler + + ruleColor = ttk.TTkColor.fg("#444444") + # # canvas.drawText(pos=((0,dy-oy-1 )),text="═"*cw,color=ruleColor) + # # canvas.drawText(pos=((0,dy-oy+dh)),text="═"*cw,color=ruleColor) + # # canvas.drawText(pos=((0,dy-oy-1 )),text="▁"*cw,color=ruleColor) + # # canvas.drawText(pos=((0,dy-oy+dh)),text="▔"*cw,color=ruleColor) + # canvas.drawText(pos=((0,dy-oy-1 )),text="▄"*cw,color=ruleColor) + # canvas.drawText(pos=((0,dy-oy+dh)),text="▀"*cw,color=ruleColor) + # for y in range(ch): + # canvas.drawText(pos=((dx-ox-1 ,y)),text="▐",color=ruleColor) + # canvas.drawText(pos=((dx-ox+dw,y)),text="▌",color=ruleColor) + # canvas.drawText(pos=((dx-ox-1 ,dy-oy-1 )),text="▟",color=ruleColor) + # canvas.drawText(pos=((dx-ox+dw,dy-oy-1 )),text="▙",color=ruleColor) + # canvas.drawText(pos=((dx-ox-1 ,dy-oy+dh)),text="▜",color=ruleColor) + # canvas.drawText(pos=((dx-ox+dw,dy-oy+dh)),text="▛",color=ruleColor) + for l in self._canvasLayers: - l.drawInCanvas(pos=l.pos(),canvas=canvas) + lx,ly = l.pos() + l.drawInCanvas(pos=(lx+dox,ly+doy),canvas=canvas) + return if self._mouseMove: x,y = self._mouseMove gc = self._glyphColor @@ -572,4 +677,9 @@ class PaintArea(ttk.TTkWidget): canvas.drawText(pos=(x,y+h-1),text=gl*w,color=gc) for y in range(y+1,y+h-1): canvas.drawChar(pos=(x ,y),char=gl,color=gc) - canvas.drawChar(pos=(x+w-1,y),char=gl,color=gc) \ No newline at end of file + canvas.drawChar(pos=(x+w-1,y),char=gl,color=gc) + +class PaintScrollArea(ttk.TTkAbstractScrollArea): + def __init__(self, pwidget:PaintArea, **kwargs): + super().__init__(**kwargs) + self.setViewport(pwidget) \ No newline at end of file From 11be48bc8e65e40e9eb827e6c633dfd028708133 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sun, 17 Mar 2024 21:44:53 +0000 Subject: [PATCH 11/28] Added PaintPreview --- tools/dumb_paint_lib/__init__.py | 9 +- tools/dumb_paint_lib/maintemplate.py | 28 ++- tools/dumb_paint_lib/paintarea.py | 286 +++++++++++++-------------- tools/dumb_paint_lib/painttoolkit.py | 123 ++++++++++++ 4 files changed, 293 insertions(+), 153 deletions(-) create mode 100644 tools/dumb_paint_lib/painttoolkit.py diff --git a/tools/dumb_paint_lib/__init__.py b/tools/dumb_paint_lib/__init__.py index f45cf51e..79da5866 100644 --- a/tools/dumb_paint_lib/__init__.py +++ b/tools/dumb_paint_lib/__init__.py @@ -1,5 +1,6 @@ from .maintemplate import * -from .paintarea import * -from .textarea import * -from .palette import * -from .layers import * \ No newline at end of file +from .paintarea import * +from .painttoolkit import * +from .textarea import * +from .palette import * +from .layers import * \ No newline at end of file diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index 3474addc..7ba24cb0 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -27,10 +27,11 @@ import sys, os, json sys.path.append(os.path.join(sys.path[0],'../..')) import TermTk as ttk -from .paintarea import PaintArea, PaintToolKit, PaintScrollArea -from .palette import Palette -from .textarea import TextArea -from .layers import Layers,LayerData +from .paintarea import PaintArea, PaintScrollArea +from .painttoolkit import PaintToolKit +from .palette import Palette +from .textarea import TextArea +from .layers import Layers,LayerData class LeftPanel(ttk.TTkVBoxLayout): __slots__ = ('_palette', @@ -54,7 +55,7 @@ class LeftPanel(ttk.TTkVBoxLayout): # Toolset lTools = ttk.TTkGridLayout() - ra_move = ttk.TTkRadioButton(radiogroup="tools", text="Move", enabled=True) + 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) @@ -64,12 +65,16 @@ class LeftPanel(ttk.TTkVBoxLayout): 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(bool) def _emitTool(checked): if not checked: return tool = PaintArea.Tool.BRUSH if ra_move.isChecked(): - tool = PaintArea.Tool.MOVE + tool = PaintArea.Tool.MOVE + if cb_move_r.isChecked(): + tool |= PaintArea.Tool.RESIZE elif ra_brush.isChecked(): tool = PaintArea.Tool.BRUSH elif ra_rect.isChecked(): @@ -81,6 +86,7 @@ class LeftPanel(ttk.TTkVBoxLayout): 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) @@ -92,6 +98,7 @@ class LeftPanel(ttk.TTkVBoxLayout): ra_oval.toggled.connect( _emitTool) 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) @@ -232,6 +239,7 @@ class PaintTemplate(ttk.TTkAppTemplate): buttonClose = fileMenu.addMenu("Save &As...") fileMenu.addSpacer() fileMenu.addMenu("&Import").menuButtonClicked.connect(self.importDictWin) + menuExport = fileMenu.addMenu("&Export") fileMenu.addSpacer() fileMenu.addMenu("Load Palette") fileMenu.addMenu("Save Palette") @@ -239,6 +247,12 @@ class PaintTemplate(ttk.TTkAppTemplate): buttonExit = fileMenu.addMenu("E&xit") buttonExit.menuButtonClicked.connect(ttk.TTkHelper.quit) + menuExport.addMenu("&Ascii/Txt") + menuExport.addMenu("&Ansi") + menuExport.addMenu("&Python") + menuExport.addMenu("&Bash") + + # extraMenu = appMenuBar.addMenu("E&xtra") # extraMenu.addMenu("Scratchpad").menuButtonClicked.connect(self.scratchpad) # extraMenu.addSpacer() @@ -258,6 +272,8 @@ class PaintTemplate(ttk.TTkAppTemplate): 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()) diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 1fe85abe..92255635 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -28,86 +28,15 @@ sys.path.append(os.path.join(sys.path[0],'../..')) import TermTk as ttk -class PaintToolKit(ttk.TTkContainer): - __slots__ = ('_rSelect', '_rPaint', '_lgliph', - '_cbFg', '_cbBg', - '_bpFg', '_bpBg', '_bpDef', - '_glyph', - #Signals - 'updatedColor', 'updatedTrans') - def __init__(self, *args, **kwargs): - ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"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._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() - 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) class CanvasLayer(): - __slot__ = ('_pos','_name','_visible','_size','_data','_colors') + __slot__ = ('_pos','_name','_visible','_size','_data','_colors','_preview') def __init__(self) -> None: self._pos = (0,0) self._size = (0,0) self._name = "" self._visible = True + self._preview = None self._data: list[list[str ]] = [] self._colors:list[list[ttk.TTkColor]] = [] @@ -251,7 +180,7 @@ class CanvasLayer(): else: self._colors[i+y][ii+x] = ttk.TTkColor.RST - def placeFill(self,geometry,tool,glyph,color): + def placeFill(self,geometry,tool,glyph,color,preview=False): w,h = self._size ax,ay,bx,by = geometry ax = max(0,min(w-1,ax)) @@ -262,8 +191,14 @@ class CanvasLayer(): fbx,fby = max(ax,bx), max(ay,by) color = color if glyph != ' ' else color.background() - data = self._data - colors = self._colors + 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 == PaintArea.Tool.RECTFILL: for row in data[fay:fby+1]: @@ -281,11 +216,17 @@ class CanvasLayer(): row[fax]=row[fbx]=color return True - def placeGlyph(self,x,y,glyph,color): + def placeGlyph(self,x,y,glyph,color,preview=False): w,h = self._size color = color if glyph != ' ' else color.background() - data = self._data - colors = self._colors + 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 bool: @@ -511,7 +554,7 @@ class PaintArea(ttk.TTkAbstractScrollView): ox, oy = self.getViewOffsets() self._mouseMove=(evt.x+ox-dx,evt.y+oy-dy) self._mouseDrag = None - self.update() + self._handleAction() return True def mouseDragEvent(self, evt) -> bool: @@ -584,23 +627,6 @@ class PaintArea(ttk.TTkAbstractScrollView): 'layerDim':ttk.TTkColor.bg(f'#{int(r*0.2):02x}{int(g*0.2):02x}{int(b*0.2):02x}')} self.update() - def _placeFill(self): - if not self._mouseDrag: return False - mfill = self._mouseDrag - self._mouseDrag = None - self._mouseMove = None - self._moveData = None - ret = self._currentLayer.placeFill(mfill,self._tool,self._glyph,self._glyphColor) - self.update() - return ret - - def _placeGlyph(self,x,y): - self._mouseMove = None - self._moveData = None - ret = self._currentLayer.placeGlyph(x,y,self._glyph,self._glyphColor) - self.update() - return ret - def paintEvent(self, canvas:ttk.TTkCanvas): dx,dy = self._documentPos doffx,doffy = self._documentOffset @@ -633,7 +659,7 @@ class PaintArea(ttk.TTkAbstractScrollView): # Draw canvas/currentLayout ruler - ruleColor = ttk.TTkColor.fg("#444444") + # ruleColor = ttk.TTkColor.fg("#444444") # # canvas.drawText(pos=((0,dy-oy-1 )),text="═"*cw,color=ruleColor) # # canvas.drawText(pos=((0,dy-oy+dh)),text="═"*cw,color=ruleColor) # # canvas.drawText(pos=((0,dy-oy-1 )),text="▁"*cw,color=ruleColor) @@ -651,33 +677,7 @@ class PaintArea(ttk.TTkAbstractScrollView): for l in self._canvasLayers: lx,ly = l.pos() l.drawInCanvas(pos=(lx+dox,ly+doy),canvas=canvas) - return - if self._mouseMove: - x,y = self._mouseMove - gc = self._glyphColor - canvas._data[y][x] = self._glyph - canvas._colors[y][x] = gc if gc._bg else gc+tc - if self._mouseDrag and self._mousePress: - ax,ay = self._mousePress - bx,by = self._mouseDrag - 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)) - x,y = min(ax,bx), min(ay,by) - w,h = max(ax-x,bx-x)+1, max(ay-y,by-y)+1 - gl = self._glyph - gc = self._glyphColor - if self._tool == PaintArea.Tool.RECTFILL: - canvas.fill(pos=(x,y), size=(w,h), - color=gc if gc._bg else gc+tc, - char=gl) - elif self._tool == PaintArea.Tool.RECTEMPTY: - canvas.drawText(pos=(x,y ),text=gl*w,color=gc) - canvas.drawText(pos=(x,y+h-1),text=gl*w,color=gc) - for y in range(y+1,y+h-1): - canvas.drawChar(pos=(x ,y),char=gl,color=gc) - canvas.drawChar(pos=(x+w-1,y),char=gl,color=gc) + # Draw Preview for mouse move/drag class PaintScrollArea(ttk.TTkAbstractScrollArea): def __init__(self, pwidget:PaintArea, **kwargs): diff --git a/tools/dumb_paint_lib/painttoolkit.py b/tools/dumb_paint_lib/painttoolkit.py new file mode 100644 index 00000000..834b6f12 --- /dev/null +++ b/tools/dumb_paint_lib/painttoolkit.py @@ -0,0 +1,123 @@ +# MIT License +# +# Copyright (c) 2024 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +__all__ = ['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__)),"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) From e84091ae2d6f9cf1c89c55f09e689788927ed4d1 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 18 Mar 2024 14:19:59 +0000 Subject: [PATCH 12/28] Added resize feature --- tools/dumb_paint_lib/maintemplate.py | 15 +- tools/dumb_paint_lib/paintToolKit.tui.json | 34 +-- tools/dumb_paint_lib/paintarea.py | 336 +++++++++++---------- 3 files changed, 203 insertions(+), 182 deletions(-) diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index 7ba24cb0..6ff2e201 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -67,9 +67,8 @@ class LeftPanel(ttk.TTkVBoxLayout): cb_move_r = ttk.TTkCheckbox(text="Resize", enabled=False) - @ttk.pyTTkSlot(bool) - def _emitTool(checked): - if not checked: return + @ttk.pyTTkSlot() + def _checkTools(): tool = PaintArea.Tool.BRUSH if ra_move.isChecked(): tool = PaintArea.Tool.MOVE @@ -84,18 +83,24 @@ class LeftPanel(ttk.TTkVBoxLayout): tool = PaintArea.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_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) diff --git a/tools/dumb_paint_lib/paintToolKit.tui.json b/tools/dumb_paint_lib/paintToolKit.tui.json index 18e4fbaf..757ec06e 100644 --- a/tools/dumb_paint_lib/paintToolKit.tui.json +++ b/tools/dumb_paint_lib/paintToolKit.tui.json @@ -191,7 +191,7 @@ 0 ], "Size": [ - 8, + 3, 1 ], "Min Width": 3, @@ -331,7 +331,7 @@ "params": { "Name": "sbDx", "Position": [ - 30, + 31, 0 ], "Size": [ @@ -378,7 +378,7 @@ "params": { "Name": "sbDy", "Position": [ - 30, + 31, 1 ], "Size": [ @@ -425,7 +425,7 @@ "params": { "Name": "TTkLabel-6", "Position": [ - 37, + 38, 0 ], "Size": [ @@ -453,7 +453,7 @@ "params": { "Name": "TTkLabel-7", "Position": [ - 37, + 38, 1 ], "Size": [ @@ -481,7 +481,7 @@ "params": { "Name": "sbDw", "Position": [ - 38, + 40, 0 ], "Size": [ @@ -528,7 +528,7 @@ "params": { "Name": "sbDh", "Position": [ - 38, + 40, 1 ], "Size": [ @@ -575,11 +575,11 @@ "params": { "Name": "TTkLabel-3", "Position": [ - 46, + 48, 0 ], "Size": [ - 6, + 5, 1 ], "Min Width": 5, @@ -603,7 +603,7 @@ "params": { "Name": "TTkLabel-8", "Position": [ - 52, + 54, 0 ], "Size": [ @@ -631,7 +631,7 @@ "params": { "Name": "TTkLabel-9", "Position": [ - 52, + 54, 1 ], "Size": [ @@ -659,7 +659,7 @@ "params": { "Name": "sbLx", "Position": [ - 53, + 56, 0 ], "Size": [ @@ -706,7 +706,7 @@ "params": { "Name": "sbLy", "Position": [ - 53, + 56, 1 ], "Size": [ @@ -753,7 +753,7 @@ "params": { "Name": "TTkLabel-10", "Position": [ - 60, + 63, 0 ], "Size": [ @@ -781,7 +781,7 @@ "params": { "Name": "TTkLabel-11", "Position": [ - 60, + 63, 1 ], "Size": [ @@ -809,7 +809,7 @@ "params": { "Name": "sbLw", "Position": [ - 61, + 65, 0 ], "Size": [ @@ -856,7 +856,7 @@ "params": { "Name": "sbLh", "Position": [ - 61, + 65, 1 ], "Size": [ diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 92255635..dbec363b 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -20,20 +20,34 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -__all__ = ['PaintArea','PaintScrollArea','PaintToolKit','CanvasLayer'] +__all__ = ['PaintArea','PaintScrollArea','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(): - __slot__ = ('_pos','_name','_visible','_size','_data','_colors','_preview') + __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 @@ -59,11 +73,12 @@ class CanvasLayer(): 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 daw: + _nw = x+dw+ox-daw + self._data = [_r + ([' ']*_nw ) for _r in self._data] + self._colors = [_r + ([ttk.TTkColor.RST]*_nw) for _r in self._colors] + 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 y+dh+oy > dah: + _nh = y+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): # Don't try this at home px,py = self.pos() @@ -111,10 +153,6 @@ class CanvasLayer(): if (xa,xb,ya,yb) == (pw,0,ph,0): xa=xb=ya=yb=0 - - #if xa>xb or ya>yb: - # return {} - outData = { 'version':'1.0.0', 'size':[xb-xa+1,yb-ya+1], @@ -181,14 +219,15 @@ class CanvasLayer(): self._colors[i+y][ii+x] = ttk.TTkColor.RST def placeFill(self,geometry,tool,glyph,color,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 = min(ax,bx), min(ay,by) - fbx,fby = max(ax,bx), max(ay,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() if preview: @@ -217,6 +256,7 @@ class CanvasLayer(): return True def placeGlyph(self,x,y,glyph,color,preview=False): + ox,oy = self._offset w,h = self._size color = color if glyph != ' ' else color.background() if preview: @@ -228,8 +268,8 @@ class CanvasLayer(): data = self._data colors = self._colors if 0<=x=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) @@ -257,8 +299,8 @@ class CanvasLayer(): colors = self._colors for y in range(cy,cy+dh): for x in range(cx,cx+dw): - gl = data[y+ly-cy][x+lx-cx] - c = colors[y+ly-cy][x+lx-cx] + 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 @@ -270,84 +312,6 @@ class CanvasLayer(): canvas._colors[y][x] = newC -class PaintToolKit(ttk.TTkContainer): - __slots__ = ('_rSelect', '_rPaint', '_lgliph', - '_cbFg', '_cbBg', - '_bpFg', '_bpBg', '_bpDef', - '_glyph', - #Signals - 'updatedColor', 'updatedTrans') - def __init__(self, *args, **kwargs): - ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"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._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): - pass - - @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) - - class PaintArea(ttk.TTkAbstractScrollView): class Tool(int): MOVE = 0x01 @@ -358,9 +322,9 @@ class PaintArea(ttk.TTkAbstractScrollView): __slots__ = ('_canvasLayers', '_currentLayer', '_transparentColor', - '_documentPos','_documentOffset','_documentSize', + '_documentPos','_documentSize', '_mouseMove', '_mouseDrag', '_mousePress', '_mouseRelease', - '_moveData', + '_moveData','_resizeData', '_tool', '_glyph', '_glyphColor', # Signals @@ -374,12 +338,12 @@ class PaintArea(ttk.TTkAbstractScrollView): self._glyph = 'X' self._glyphColor = ttk.TTkColor.RST self._moveData = None + self._resizeData = None self._mouseMove = None self._mouseDrag = None self._mousePress = None self._mouseRelease = None self._tool = self.Tool.BRUSH - self._documentOffset = ( 0, 0) self._documentPos = (6,3) self._documentSize = ( 0, 0) super().__init__(*args, **kwargs) @@ -389,12 +353,8 @@ class PaintArea(ttk.TTkAbstractScrollView): def _getGeometry(self): dx,dy = self._documentPos - # doffx,doffy = self._documentOffset - # dx+=doffx - # dy+=doffy dw,dh = self._documentSize ww,wh = self.size() - # dw,dh = max(dw,dx+ww),max(dh,dx+wh) x1,y1 = min(0,dx),min(0,dy) x2,y2 = max(dx+dw,ww),max(dy+dh,wh) for l in self._canvasLayers: @@ -408,9 +368,10 @@ class PaintArea(ttk.TTkAbstractScrollView): return x1,y1,x2-x1,y2-y1 def _retuneGeometry(self): + dx,dy = self._documentPos x1,y1,_,_ = self._getGeometry() - self._documentOffset = (max(0,-x1),max(0,-y1)) - self.viewMoveTo(max(0,-x1),max(0,-y1)) + self._documentPos = max(dx,-dx,-x1),max(dy,-dy,-y1) + # self.viewMoveTo(max(0,-x1),max(0,-y1)) self.viewChanged.emit() # dx,dy = self._documentPos # self.chan @@ -431,9 +392,18 @@ class PaintArea(ttk.TTkAbstractScrollView): return self._canvasLayers def resizeCanvas(self, w, h): - if self._currentLayer: - self._currentLayer.resize(w,h) self._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 + self._documentPos = (x,y) + self._documentSize = (w,h) self.update() def setCurrentLayer(self, layer:CanvasLayer): @@ -490,9 +460,7 @@ class PaintArea(ttk.TTkAbstractScrollView): def _handleAction(self): dx,dy = self._documentPos - doffx,doffy = self._documentOffset - dx+=doffx - dy+=doffy + dw,dh = self._documentSize ox, oy = self.getViewOffsets() mp = self._mousePress mm = self._mouseMove @@ -500,81 +468,109 @@ class PaintArea(ttk.TTkAbstractScrollView): mr = self._mouseRelease l = self._currentLayer lx,ly = l.pos() + if self._tool & self.Tool.MOVE and mp and not md: - # Get The Layer to Move - self._moveData = None - for lm in reversed(self._canvasLayers): + if self._tool & self.Tool.RESIZE and not md: mpx,mpy = mp - lmx,lmy = lm.pos() - if lm.isOpaque(mpx-lmx,mpy-lmy): - self._currentLayer = lm - tml = lm - self._moveData = {'pos':tml.pos(),'layer':tml} - self.selectedLayer.emit(lm) - break + 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 & self.Tool.MOVE and mp and md: - mpx,mpy = mp - mdx,mdy = md - pdx,pdy = mdx-mpx,mdy-mpy - if self._moveData: - px,py = self._moveData['pos'] - self._moveData['layer'].move(px+pdx,py+pdy) - self.selectedLayer.emit(self._moveData['layer']) - else: - self._documentPos = (dx+pdx,dy+pdy) + # Move/Resize Tool + if self._tool & self.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 == self.Tool.BRUSH: if mp or md: if md: mx,my = md else: mx,my = mp preview=False - self._currentLayer.placeGlyph(mx-lx,my-ly,self._glyph,self._glyphColor,preview) + 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,my-ly,self._glyph,self._glyphColor,preview) + self._currentLayer.placeGlyph(mx-lx-dx,my-ly-dy,self._glyph,self._glyphColor,preview) + elif self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL): if mr and mp: mpx,mpy = mp mrx,mry = mr preview=False - self._currentLayer.placeFill((mpx-lx,mpy-ly,mrx-lx,mry-ly),self._tool,self._glyph,self._glyphColor,preview) + 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,mpy-ly,mrx-lx,mry-ly),self._tool,self._glyph,self._glyphColor,preview) + self._currentLayer.placeFill((mpx-lx-dx,mpy-ly-dy,mrx-lx-dx,mry-ly-dy),self._tool,self._glyph,self._glyphColor,preview) self.update() def mouseMoveEvent(self, evt) -> bool: - dx,dy = self._documentPos - doffx,doffy = self._documentOffset - dx+=doffx - dy+=doffy ox, oy = self.getViewOffsets() - self._mouseMove=(evt.x+ox-dx,evt.y+oy-dy) - self._mouseDrag = None + self._mouseMove = (evt.x+ox,evt.y+oy) + self._mouseDrag = None self._handleAction() return True def mouseDragEvent(self, evt) -> bool: - dx,dy = self._documentPos - doffx,doffy = self._documentOffset - dx+=doffx - dy+=doffy ox, oy = self.getViewOffsets() - self._mouseDrag=(evt.x+ox-dx,evt.y+oy-dy) + self._mouseDrag=(evt.x+ox,evt.y+oy) self._mouseMove= None self._handleAction() return True def mousePressEvent(self, evt) -> bool: - dx,dy = self._documentPos - doffx,doffy = self._documentOffset - dx+=doffx - dy+=doffy ox, oy = self.getViewOffsets() - self._mousePress=(evt.x+ox-dx,evt.y+oy-dy) + self._mousePress=(evt.x+ox,evt.y+oy) self._moveData = None self._mouseMove = None self._mouseDrag = None @@ -583,15 +579,12 @@ class PaintArea(ttk.TTkAbstractScrollView): return True def mouseReleaseEvent(self, evt) -> bool: - dx,dy = self._documentPos - doffx,doffy = self._documentOffset - dx+=doffx - dy+=doffy ox, oy = self.getViewOffsets() - self._mouseRelease=(evt.x+ox-dx,evt.y+oy-dy) + 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 @@ -629,9 +622,6 @@ class PaintArea(ttk.TTkAbstractScrollView): def paintEvent(self, canvas:ttk.TTkCanvas): dx,dy = self._documentPos - doffx,doffy = self._documentOffset - dx+=doffx - dy+=doffy ox, oy = self.getViewOffsets() dw,dh = self._documentSize dox,doy = dx-ox,dy-oy @@ -679,6 +669,32 @@ class PaintArea(ttk.TTkAbstractScrollView): l.drawInCanvas(pos=(lx+dox,ly+doy),canvas=canvas) # Draw Preview for mouse move/drag + if self._tool & self.Tool.RESIZE: + rd = self._resizeData + def _drawResizeBorders(_rx,_ry,_rw,_rh,_sel): + 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) From 5a279dea93ba76366d33918d994026a9cf6b2305 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 18 Mar 2024 16:54:29 +0000 Subject: [PATCH 13/28] Added layer export 1.1.0 with offset support --- tools/dumb_paint_lib/maintemplate.py | 41 +++-- tools/dumb_paint_lib/paintarea.py | 215 +++++++++++++++------------ 2 files changed, 151 insertions(+), 105 deletions(-) diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index 6ff2e201..a5868ee8 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -125,7 +125,7 @@ class LeftPanel(ttk.TTkVBoxLayout): return self._palette class ExportArea(ttk.TTkGridLayout): - __slots__ = ('_paintArea', '_te') + __slots__ = ('_paintArea', '_te','_cbCrop', '_cbFull', '_cbPal') def __init__(self, paintArea:PaintArea, **kwargs): self._paintArea:PaintArea = paintArea super().__init__(**kwargs) @@ -133,19 +133,26 @@ class ExportArea(ttk.TTkGridLayout): btn_exIm = ttk.TTkButton(text="Export Image") btn_exLa = ttk.TTkButton(text="Export Layer") btn_exPr = ttk.TTkButton(text="Export Document") - btn_cbCrop = ttk.TTkCheckbox(text="Crop",checked=True) + 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(btn_cbCrop,0,3) - self.addWidget(self._te,1,0,1,5) + 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) @ttk.pyTTkSlot() def _exportLayer(self): - dd = self._paintArea.exportLayer() + 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 @@ -159,24 +166,32 @@ class ExportArea(ttk.TTkGridLayout): self._te.append('\n# Uncompressed Data:') outTxt = '{\n' for i in dd: - if i in ('data','colors'): continue - outTxt += f" '{i}':'{dd[i]}',\n" + 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" - outTxt += " ],'palette':[" - for i,l in enumerate(dd['palette']): - if not i%10: - outTxt += f"\n " - outTxt += f"{l}," + 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): - dd = self._paintArea.exportDocument() + 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 diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index dbec363b..42794e35 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -133,90 +133,141 @@ class CanvasLayer(): self._colors[i] = [ttk.TTkColor.RST]*w - def exportLayer(self): + 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() - data = self._data - colors = self._colors + + 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 - xa,xb,ya,yb = pw,0,ph,0 - 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,ya,yb) == (pw,0,ph,0): - xa=xb=ya=yb=0 + 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.0.0', - 'size':[xb-xa+1,yb-ya+1], - 'pos': (px+xa,py+ya), + 'version':'1.1.0', + 'size':[vw,vh], + 'pos': (vxa,vya), 'name':str(self.name()), - 'data':[], 'colors':[], '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) + '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[ya:yb+1]: - outData['data'].append(row[xa:xb+1]) - for row in colors[ya:yb+1]: + for row in data[hslice]: + outData['data'].append(row[wslice]) + for row in colors[hslice]: outData['colors'].append([]) - for c in row[xa:xb+1]: + 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 - outData['colors'][-1].append(palette.index((fg,bg))) + 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 and dd['version']=='1.0.0': - 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']] - else: # 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 + 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,color,preview=False): ox,oy = self._offset @@ -434,17 +485,17 @@ class PaintArea(ttk.TTkAbstractScrollView): def exportImage(self): return {} - def exportLayer(self) -> dict: + def exportLayer(self, full=False, palette=True, crop=True) -> dict: if self._currentLayer: - return self._currentLayer.exportLayer() + return self._currentLayer.exportLayer(full=full,palette=palette,crop=crop) return {} - def exportDocument(self): + def exportDocument(self, full=True, palette=True, crop=True) -> dict: pw,ph = self._documentSize outData = { 'version':'1.0.0', 'size':(pw,ph), - 'layers':[l.exportLayer() for l in self._canvasLayers]} + 'layers':[l.exportLayer(full=full,palette=palette,crop=crop) for l in self._canvasLayers]} return outData def leaveEvent(self, evt): @@ -630,8 +681,6 @@ class PaintArea(ttk.TTkAbstractScrollView): h=min(ch,dh) tcb = self._transparentColor['base'] tcd = self._transparentColor['dim'] - # canvas.fill(pos=(0 ,dy-oy),size=(cw,dh),color=tcd) - # canvas.fill(pos=(dx-ox,0 ),size=(dw,ch),color=tcd) if l:=self._currentLayer: tclb = self._transparentColor['layer'] @@ -647,27 +696,9 @@ class PaintArea(ttk.TTkAbstractScrollView): canvas.fill(pos=(dx-ox-2 ,0 ),size=(2,ch),color=tcd) canvas.fill(pos=(dx-ox+dw,0 ),size=(2,ch),color=tcd) - # Draw canvas/currentLayout ruler - - # ruleColor = ttk.TTkColor.fg("#444444") - # # canvas.drawText(pos=((0,dy-oy-1 )),text="═"*cw,color=ruleColor) - # # canvas.drawText(pos=((0,dy-oy+dh)),text="═"*cw,color=ruleColor) - # # canvas.drawText(pos=((0,dy-oy-1 )),text="▁"*cw,color=ruleColor) - # # canvas.drawText(pos=((0,dy-oy+dh)),text="▔"*cw,color=ruleColor) - # canvas.drawText(pos=((0,dy-oy-1 )),text="▄"*cw,color=ruleColor) - # canvas.drawText(pos=((0,dy-oy+dh)),text="▀"*cw,color=ruleColor) - # for y in range(ch): - # canvas.drawText(pos=((dx-ox-1 ,y)),text="▐",color=ruleColor) - # canvas.drawText(pos=((dx-ox+dw,y)),text="▌",color=ruleColor) - # canvas.drawText(pos=((dx-ox-1 ,dy-oy-1 )),text="▟",color=ruleColor) - # canvas.drawText(pos=((dx-ox+dw,dy-oy-1 )),text="▙",color=ruleColor) - # canvas.drawText(pos=((dx-ox-1 ,dy-oy+dh)),text="▜",color=ruleColor) - # canvas.drawText(pos=((dx-ox+dw,dy-oy+dh)),text="▛",color=ruleColor) - for l in self._canvasLayers: lx,ly = l.pos() l.drawInCanvas(pos=(lx+dox,ly+doy),canvas=canvas) - # Draw Preview for mouse move/drag if self._tool & self.Tool.RESIZE: rd = self._resizeData From e24d480176deea9b21b4283ffcb7df6a913854ec Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 18 Mar 2024 17:13:42 +0000 Subject: [PATCH 14/28] Added Image/ANSI Export --- tools/dumb_paint_lib/__init__.py | 3 +- tools/dumb_paint_lib/canvaslayer.py | 370 ++++++++++++++++++++++++++ tools/dumb_paint_lib/maintemplate.py | 24 +- tools/dumb_paint_lib/paintarea.py | 377 ++------------------------- 4 files changed, 414 insertions(+), 360 deletions(-) create mode 100644 tools/dumb_paint_lib/canvaslayer.py diff --git a/tools/dumb_paint_lib/__init__.py b/tools/dumb_paint_lib/__init__.py index 79da5866..ce1faa75 100644 --- a/tools/dumb_paint_lib/__init__.py +++ b/tools/dumb_paint_lib/__init__.py @@ -3,4 +3,5 @@ from .paintarea import * from .painttoolkit import * from .textarea import * from .palette import * -from .layers import * \ No newline at end of file +from .layers import * +from .canvaslayer import * \ No newline at end of file diff --git a/tools/dumb_paint_lib/canvaslayer.py b/tools/dumb_paint_lib/canvaslayer.py new file mode 100644 index 00000000..2fde080a --- /dev/null +++ b/tools/dumb_paint_lib/canvaslayer.py @@ -0,0 +1,370 @@ +# MIT License +# +# Copyright (c) 2024 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +__all__ = ['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 + + __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 daw: + _nw = x+dw+ox-daw + self._data = [_r + ([' ']*_nw ) for _r in self._data] + self._colors = [_r + ([ttk.TTkColor.RST]*_nw) for _r in self._colors] + 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 y+dh+oy > dah: + _nh = y+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,color,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() + 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,color,preview=False): + ox,oy = self._offset + w,h = self._size + color = color if glyph != ' ' else color.background() + 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=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 diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index a5868ee8..bc680a72 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -28,6 +28,7 @@ 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 @@ -38,7 +39,7 @@ class LeftPanel(ttk.TTkVBoxLayout): # Signals 'toolSelected') def __init__(self, *args, **kwargs): - self.toolSelected = ttk.pyTTkSignal(PaintArea.Tool) + self.toolSelected = ttk.pyTTkSignal(CanvasLayer.Tool) super().__init__(*args, **kwargs) self._palette = Palette(maxHeight=12) self.addWidget(self._palette) @@ -69,18 +70,18 @@ class LeftPanel(ttk.TTkVBoxLayout): @ttk.pyTTkSlot() def _checkTools(): - tool = PaintArea.Tool.BRUSH + tool = CanvasLayer.Tool.BRUSH if ra_move.isChecked(): - tool = PaintArea.Tool.MOVE + tool = CanvasLayer.Tool.MOVE if cb_move_r.isChecked(): - tool |= PaintArea.Tool.RESIZE + tool |= CanvasLayer.Tool.RESIZE elif ra_brush.isChecked(): - tool = PaintArea.Tool.BRUSH + tool = CanvasLayer.Tool.BRUSH elif ra_rect.isChecked(): if ra_rect_e.isChecked(): - tool = PaintArea.Tool.RECTEMPTY + tool = CanvasLayer.Tool.RECTEMPTY else: - tool = PaintArea.Tool.RECTFILL + tool = CanvasLayer.Tool.RECTFILL self.toolSelected.emit(tool) @ttk.pyTTkSlot(bool) @@ -146,6 +147,15 @@ class ExportArea(ttk.TTkGridLayout): 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(full=full,palette=palette,crop=crop) + self._te.setText(image) @ttk.pyTTkSlot() def _exportLayer(self): diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 42794e35..07562b83 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -27,350 +27,9 @@ 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(): - __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 daw: - _nw = x+dw+ox-daw - self._data = [_r + ([' ']*_nw ) for _r in self._data] - self._colors = [_r + ([ttk.TTkColor.RST]*_nw) for _r in self._colors] - 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 y+dh+oy > dah: - _nh = y+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,color,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() - 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 == PaintArea.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 == PaintArea.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,color,preview=False): - ox,oy = self._offset - w,h = self._size - color = color if glyph != ' ' else color.background() - 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=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 - +from .canvaslayer import CanvasLayer class PaintArea(ttk.TTkAbstractScrollView): - class Tool(int): - MOVE = 0x01 - RESIZE = 0x02 - BRUSH = 0x04 - RECTFILL = 0x08 - RECTEMPTY = 0x10 - __slots__ = ('_canvasLayers', '_currentLayer', '_transparentColor', '_documentPos','_documentSize', @@ -394,7 +53,7 @@ class PaintArea(ttk.TTkAbstractScrollView): self._mouseDrag = None self._mousePress = None self._mouseRelease = None - self._tool = self.Tool.BRUSH + self._tool = CanvasLayer.Tool.BRUSH self._documentPos = (6,3) self._documentSize = ( 0, 0) super().__init__(*args, **kwargs) @@ -453,7 +112,13 @@ class PaintArea(ttk.TTkAbstractScrollView): if (x,y,w,h) == (dx,dy,dw,dh): return if w<0: x=dx;w=dw if h<0: y=dy;h=dh - self._documentPos = (x,y) + 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() @@ -498,13 +163,21 @@ class PaintArea(ttk.TTkAbstractScrollView): 'layers':[l.exportLayer(full=full,palette=palette,crop=crop) for l in self._canvasLayers]} return outData + def exportImage(self, full=True, palette=True, crop=True) -> 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): self._mouseMove = None self._moveData = None self.update() return super().leaveEvent(evt) - @ttk.pyTTkSlot(Tool) + @ttk.pyTTkSlot(CanvasLayer.Tool) def setTool(self, tool): self._tool = tool self.update() @@ -520,8 +193,8 @@ class PaintArea(ttk.TTkAbstractScrollView): l = self._currentLayer lx,ly = l.pos() - if self._tool & self.Tool.MOVE and mp and not md: - if self._tool & self.Tool.RESIZE and not md: + 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): @@ -554,9 +227,9 @@ class PaintArea(ttk.TTkAbstractScrollView): self.selectedLayer.emit(lm) break - elif self._tool & self.Tool.MOVE and mp and md: + elif self._tool & CanvasLayer.Tool.MOVE and mp and md: # Move/Resize Tool - if self._tool & self.Tool.RESIZE and (rData:=self._resizeData): + 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 @@ -581,7 +254,7 @@ class PaintArea(ttk.TTkAbstractScrollView): self._documentPos = (px+pdx,py+pdy) self._retuneGeometry() - elif self._tool == self.Tool.BRUSH: + elif self._tool == CanvasLayer.Tool.BRUSH: if mp or md: if md: mx,my = md else: mx,my = mp @@ -592,7 +265,7 @@ class PaintArea(ttk.TTkAbstractScrollView): preview=True self._currentLayer.placeGlyph(mx-lx-dx,my-ly-dy,self._glyph,self._glyphColor,preview) - elif self._tool in (self.Tool.RECTEMPTY, self.Tool.RECTFILL): + elif self._tool in (CanvasLayer.Tool.RECTEMPTY, CanvasLayer.Tool.RECTFILL): if mr and mp: mpx,mpy = mp mrx,mry = mr @@ -700,7 +373,7 @@ class PaintArea(ttk.TTkAbstractScrollView): lx,ly = l.pos() l.drawInCanvas(pos=(lx+dox,ly+doy),canvas=canvas) - if self._tool & self.Tool.RESIZE: + if self._tool & CanvasLayer.Tool.RESIZE: rd = self._resizeData def _drawResizeBorders(_rx,_ry,_rw,_rh,_sel): selColor = ttk.TTkColor.YELLOW + ttk.TTkColor.BG_BLUE From 1706e2146604ec57845dfcc7447ff61149da54fc Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 18 Mar 2024 18:09:24 +0000 Subject: [PATCH 15/28] Retuned the negative placement --- tools/dumb_paint_lib/maintemplate.py | 2 +- tools/dumb_paint_lib/paintarea.py | 10 +++++++++- tools/dumb_paint_lib/painttoolkit.py | 2 +- tools/dumb_paint_lib/{ => tui}/paintToolKit.tui.json | 0 tools/dumb_paint_lib/{ => tui}/quickImport.tui.json | 0 5 files changed, 11 insertions(+), 3 deletions(-) rename tools/dumb_paint_lib/{ => tui}/paintToolKit.tui.json (100%) rename tools/dumb_paint_lib/{ => tui}/quickImport.tui.json (100%) diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index bc680a72..459659a6 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -357,7 +357,7 @@ class PaintTemplate(ttk.TTkAppTemplate): @ttk.pyTTkSlot() 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") @ttk.pyTTkSlot() diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 07562b83..df3b4a80 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -78,13 +78,21 @@ class PaintArea(ttk.TTkAbstractScrollView): return x1,y1,x2-x1,y2-y1 def _retuneGeometry(self): + ox, oy = self.getViewOffsets() dx,dy = self._documentPos x1,y1,_,_ = self._getGeometry() - self._documentPos = max(dx,-dx,-x1),max(dy,-dy,-y1) + dx1,dy1 = max(0,dx,dx-x1),max(0,dy,dy-y1) + self._documentPos = dx1,dy1 # self.viewMoveTo(max(0,-x1),max(0,-y1)) self.viewChanged.emit() # dx,dy = self._documentPos # self.chan + if md:=self._moveData: + mx,my=md['pos'] + md['pos']=(mx+dx1-dx,my+dy1-dy) + if rd:=self._resizeData: + rx,ry=rd['pos'] + rd['pos']=(rx+dx1-dx,ry+dy1-dy) def viewFullAreaSize(self) -> tuple[int,int]: _,_,w,h = self._getGeometry() diff --git a/tools/dumb_paint_lib/painttoolkit.py b/tools/dumb_paint_lib/painttoolkit.py index 834b6f12..9408aef1 100644 --- a/tools/dumb_paint_lib/painttoolkit.py +++ b/tools/dumb_paint_lib/painttoolkit.py @@ -39,7 +39,7 @@ class PaintToolKit(ttk.TTkContainer): #Signals 'updatedColor', 'updatedTrans') def __init__(self, *args, **kwargs): - ttk.TTkUiLoader.loadFile(os.path.join(os.path.dirname(os.path.abspath(__file__)),"paintToolKit.tui.json"),self) + 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) diff --git a/tools/dumb_paint_lib/paintToolKit.tui.json b/tools/dumb_paint_lib/tui/paintToolKit.tui.json similarity index 100% rename from tools/dumb_paint_lib/paintToolKit.tui.json rename to tools/dumb_paint_lib/tui/paintToolKit.tui.json diff --git a/tools/dumb_paint_lib/quickImport.tui.json b/tools/dumb_paint_lib/tui/quickImport.tui.json similarity index 100% rename from tools/dumb_paint_lib/quickImport.tui.json rename to tools/dumb_paint_lib/tui/quickImport.tui.json From a558b14a7bf79be3333ea05fd9b67ccea7d08766 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 19 Mar 2024 10:45:26 +0000 Subject: [PATCH 16/28] Tuned the new topology change during move or drag --- tools/dumb_paint_lib/paintarea.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index df3b4a80..20310c14 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -83,10 +83,17 @@ class PaintArea(ttk.TTkAbstractScrollView): x1,y1,_,_ = self._getGeometry() dx1,dy1 = max(0,dx,dx-x1),max(0,dy,dy-y1) self._documentPos = dx1,dy1 - # self.viewMoveTo(max(0,-x1),max(0,-y1)) 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) From e26f8f956a155b3249a74b15587c7f2d87676cfa Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 19 Mar 2024 23:29:54 +0000 Subject: [PATCH 17/28] I am too tired to figure out why this new math is fixing the crash, I litterally just commented out few things and it is working definitely better --- tools/dumb_paint_lib/canvaslayer.py | 12 ++++++------ tools/dumb_paint_lib/paintarea.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/dumb_paint_lib/canvaslayer.py b/tools/dumb_paint_lib/canvaslayer.py index 2fde080a..41d6fa54 100644 --- a/tools/dumb_paint_lib/canvaslayer.py +++ b/tools/dumb_paint_lib/canvaslayer.py @@ -108,22 +108,22 @@ class CanvasLayer(): diffx = dx-x diffy = dy-y self._preview = None - if ox<=x-dx: # we need to resize and move ox + 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 x+dw+ox > daw: - _nw = x+dw+ox-daw + 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] - if oy<=y-dy: # we need to resize and move ox + 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 y+dh+oy > dah: - _nh = y+dh+oy-dah + 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) diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 20310c14..14fc3bef 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -97,9 +97,9 @@ class PaintArea(ttk.TTkAbstractScrollView): if md:=self._moveData: mx,my=md['pos'] md['pos']=(mx+dx1-dx,my+dy1-dy) - if rd:=self._resizeData: - rx,ry=rd['pos'] - rd['pos']=(rx+dx1-dx,ry+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() From 213e19c0ab744327e09e83806fcd4545bc25b37a Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 20 Mar 2024 10:11:25 +0000 Subject: [PATCH 18/28] Fix for the diagonal layer resize crash --- tools/dumb_paint_lib/canvaslayer.py | 1 + tools/dumb_paint_lib/paintarea.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/dumb_paint_lib/canvaslayer.py b/tools/dumb_paint_lib/canvaslayer.py index 41d6fa54..99dd142d 100644 --- a/tools/dumb_paint_lib/canvaslayer.py +++ b/tools/dumb_paint_lib/canvaslayer.py @@ -117,6 +117,7 @@ class CanvasLayer(): _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 diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 14fc3bef..4d880cab 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -99,7 +99,7 @@ class PaintArea(ttk.TTkAbstractScrollView): 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) + # # rd['geometry']=(rx+dx1-dx,ry+dy1-dy,rw,rh) def viewFullAreaSize(self) -> tuple[int,int]: _,_,w,h = self._getGeometry() From 4c189ce498f91849f8561d84a48fb78c2a174c34 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 20 Mar 2024 10:12:24 +0000 Subject: [PATCH 19/28] hsl color fix --- TermTk/TTkCore/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TermTk/TTkCore/color.py b/TermTk/TTkCore/color.py index f8294af7..616687f7 100644 --- a/TermTk/TTkCore/color.py +++ b/TermTk/TTkCore/color.py @@ -113,7 +113,7 @@ class _TTkColor: cmax = max(r,g,b) cmin = min(r,g,b) - lum = (cmax-cmin)/2 + lum = (cmax+cmin)/2 if cmax == cmin: return 0,0,lum From a12ac99dea30e6359e7a3cfb650e79eb95b7a2bf Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 20 Mar 2024 11:03:54 +0000 Subject: [PATCH 20/28] Added DumbPaintTool Exporter --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index c9755d9d..9d221ee0 100644 --- a/Makefile +++ b/Makefile @@ -113,6 +113,10 @@ deployTest: .venv . .venv/bin/activate ; \ python3 -m twine upload --repository testpypi tmp/dist/* --verbose +itchDumbPaintToolexporter: + tools/webExporterInit.sh + python3 -m http.server --directory tmp + test: .venv # Record a stream # tests/pytest/test_001_demo.py -r test.input.bin From 890878495c6e72a3c540a0183b60fd29880eb43c Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 20 Mar 2024 16:03:17 +0000 Subject: [PATCH 21/28] Fixed the new layout size --- tools/dumb_paint_lib/maintemplate.py | 16 +++++++++++++--- tools/dumb_paint_lib/paintarea.py | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index 459659a6..0ec7d59f 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -154,7 +154,7 @@ class ExportArea(ttk.TTkGridLayout): crop = self._cbCrop.isChecked() palette = self._cbPal.isChecked() full = self._cbFull.isChecked() - image = self._paintArea.exportImage(full=full,palette=palette,crop=crop) + image = self._paintArea.exportImage() self._te.setText(image) @ttk.pyTTkSlot() @@ -265,8 +265,8 @@ class PaintTemplate(ttk.TTkAppTemplate): self.setMenuBar(appMenuBar:=ttk.TTkMenuBarLayout(), self.TOP) fileMenu = appMenuBar.addMenu("&File") buttonOpen = fileMenu.addMenu("&Open") - buttonClose = fileMenu.addMenu("&Save") - buttonClose = fileMenu.addMenu("Save &As...") + 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") @@ -315,6 +315,16 @@ class PaintTemplate(ttk.TTkAppTemplate): if fileName: self._openFile(fileName) + @ttk.pyTTkSlot() + def _save(self): + image = self._parea.exportImage() + ttk.ttkCrossSave('untitled.DPT.txt', image, ttk.TTkEncoding.TEXT_PLAIN) + + @ttk.pyTTkSlot() + def _saveAs(self): + image = self._parea.exportImage() + ttk.ttkCrossSaveAs('untitled.DPT.txt', image, ttk.TTkEncoding.TEXT_PLAIN) + def _openFile(self, fileName): ttk.TTkLog.info(f"Open: {fileName}") diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 4d880cab..83a703df 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -142,7 +142,7 @@ class PaintArea(ttk.TTkAbstractScrollView): def newLayer(self) -> CanvasLayer: newLayer = CanvasLayer() - w,h = self.size() + w,h = self._documentSize w,h = (w,h) if (w,h)!=(0,0) else (80,24) newLayer.resize(w,h) self._currentLayer = newLayer @@ -178,7 +178,7 @@ class PaintArea(ttk.TTkAbstractScrollView): 'layers':[l.exportLayer(full=full,palette=palette,crop=crop) for l in self._canvasLayers]} return outData - def exportImage(self, full=True, palette=True, crop=True) -> dict: + def exportImage(self) -> dict: pw,ph = self._documentSize image = ttk.TTkCanvas(width=pw,height=ph) for l in self._canvasLayers: From 9bed4ca906ff8f2162f4b7b2048f01b039cbfdad Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 22 Mar 2024 15:12:28 +0000 Subject: [PATCH 22/28] Fix crash using no color --- tools/dumb_paint_lib/canvaslayer.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/dumb_paint_lib/canvaslayer.py b/tools/dumb_paint_lib/canvaslayer.py index 99dd142d..53fab60a 100644 --- a/tools/dumb_paint_lib/canvaslayer.py +++ b/tools/dumb_paint_lib/canvaslayer.py @@ -49,6 +49,7 @@ class CanvasLayer(): BRUSH = 0x04 RECTFILL = 0x08 RECTEMPTY = 0x10 + CLONE = 0x20 __slot__ = ('_pos','_name','_visible','_size','_data','_colors','_preview','_offset') def __init__(self) -> None: @@ -277,7 +278,7 @@ class CanvasLayer(): else: self._import_v0_0_0(dd) - def placeFill(self,geometry,tool,glyph,color,preview=False): + 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 @@ -289,6 +290,7 @@ class CanvasLayer(): 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] @@ -314,10 +316,11 @@ class CanvasLayer(): row[fax]=row[fbx]=color return True - def placeGlyph(self,x,y,glyph,color,preview=False): + 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] @@ -327,7 +330,7 @@ class CanvasLayer(): data = self._data colors = self._colors if 0<=x Date: Fri, 22 Mar 2024 15:14:33 +0000 Subject: [PATCH 23/28] Tuned the standard open/save/clipboard --- TermTk/TTkCrossTools/__init__.py | 1 + TermTk/TTkCrossTools/savetools.py | 159 +++++++++++++++++++++++++++ TermTk/TTkGui/clipboard.py | 23 +++- tools/dumb_paint_lib/maintemplate.py | 51 +++++++-- tools/dumb_paint_lib/paintarea.py | 9 +- tools/webExporter/index.html | 35 +++++- tools/webExporter/js/ttkproxy.js | 116 ++++++++++++++++--- tools/webExporterInit.sh | 28 +++-- 8 files changed, 382 insertions(+), 40 deletions(-) create mode 100644 TermTk/TTkCrossTools/__init__.py create mode 100644 TermTk/TTkCrossTools/savetools.py diff --git a/TermTk/TTkCrossTools/__init__.py b/TermTk/TTkCrossTools/__init__.py new file mode 100644 index 00000000..25c440ef --- /dev/null +++ b/TermTk/TTkCrossTools/__init__.py @@ -0,0 +1 @@ +from .savetools import * \ No newline at end of file diff --git a/TermTk/TTkCrossTools/savetools.py b/TermTk/TTkCrossTools/savetools.py new file mode 100644 index 00000000..12469577 --- /dev/null +++ b/TermTk/TTkCrossTools/savetools.py @@ -0,0 +1,159 @@ +# MIT License +# +# Copyright (c) 2024 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +__all__ = ['ttkCrossOpen', 'ttkCrossSave', 'ttkCrossSaveAs', 'TTkEncoding', 'ttkConnectDragOpen', 'ttkEmitDragOpen', 'ttkEmitFileOpen'] + +import os +import importlib.util +import json + +from TermTk import pyTTkSlot, pyTTkSignal +from TermTk import TTkLog +from TermTk import TTkMessageBox, TTkFileDialogPicker, TTkHelper, TTkString, TTkK, TTkColor + +ttkCrossOpen = None +ttkCrossSave = None +ttkCrossSaveAs = None +ttkEmitDragOpen = None +ttkEmitFileOpen = None +ttkConnectDragOpen = None + +class TTkEncoding(str): + TEXT = "text" + TEXT_PLAIN = "text/plain" + TEXT_PLAIN_UTF8 = "text/plain;charset=utf-8" + APPLICATION = 'application' + APPLICATION_JSON = 'application/json' + IMAGE = 'image' + IMAGE_PNG = 'image/png' + IMAGE_SVG = 'image/svg+xml' + IMAGE_JPG = 'image/jpeg' + +if importlib.util.find_spec('pyodideProxy'): + TTkLog.info("Using 'pyodideProxy' as clipboard manager") + import pyodideProxy + ttkDragOpen = {} + ttkFileOpen = pyTTkSignal(dict) + + def _open(path, encoding, filter, cb=None): + if not cb: return + ttkFileOpen.connect(cb) + pyodideProxy.openFile(encoding) + + def _save(filePath, content, encoding, filter=None): + pyodideProxy.saveFile(os.path.basename(filePath), content, encoding) + + def _connectDragOpen(encoding, cb): + if not encoding in ttkDragOpen: + ttkDragOpen[encoding] = pyTTkSignal(dict) + return ttkDragOpen[encoding].connect(cb) + + def _emitDragOpen(encoding, data): + for do in [ttkDragOpen[e] for e in ttkDragOpen if encoding.startswith(e)]: + do.emit(data) + + def _emitFileOpen(encoding, data): + ttkFileOpen.emit(data) + ttkFileOpen.clear() + + ttkCrossOpen = _open + ttkCrossSave = _save + ttkCrossSaveAs = _save + ttkEmitDragOpen = _emitDragOpen + ttkEmitFileOpen = _emitFileOpen + ttkConnectDragOpen = _connectDragOpen + +else: + def _crossDecoder_text(fileName) : + with open(fileName) as fp: + return fp.read() + def _crossDecoder_json(fileName) : + with open(fileName) as fp: + # return json.load(fp) + return fp.read() + def _crossDecoder_image(fileName): + return None + + _crossDecoder = { + TTkEncoding.TEXT : _crossDecoder_text , + TTkEncoding.TEXT_PLAIN : _crossDecoder_text , + TTkEncoding.TEXT_PLAIN_UTF8 : _crossDecoder_text , + TTkEncoding.APPLICATION : _crossDecoder_json , + TTkEncoding.APPLICATION_JSON : _crossDecoder_json , + TTkEncoding.IMAGE : _crossDecoder_image , + TTkEncoding.IMAGE_PNG : _crossDecoder_image , + TTkEncoding.IMAGE_SVG : _crossDecoder_image , + TTkEncoding.IMAGE_JPG : _crossDecoder_image , + } + + def _open(path, encoding, filter, cb=None): + if not cb: return + def __openFile(fileName): + _decoder = _crossDecoder.get(encoding,lambda _:None) + content = _decoder(fileName) + cb({'name':fileName, 'data':content}) + filePicker = TTkFileDialogPicker(pos = (3,3), size=(100,30), caption="Open", path=path, fileMode=TTkK.FileMode.ExistingFile ,filter=filter) + filePicker.pathPicked.connect(__openFile) + TTkHelper.overlay(None, filePicker, 5, 5, True) + + def _save(filePath, content, encoding): + TTkLog.info(f"Saving to: {filePath}") + with open(filePath,'w') as fp: + fp.write(content) + + def _saveAs(filePath, content, encoding, filter, cb=None): + if not cb: return + def _approveFile(fileName): + if os.path.exists(fileName): + @pyTTkSlot(TTkMessageBox.StandardButton) + def _cb(btn): + if btn == TTkMessageBox.StandardButton.Save: + ttkCrossSave(fileName,content,encoding) + elif btn == TTkMessageBox.StandardButton.Cancel: + return + if cb: + cb() + messageBox = TTkMessageBox( + text= ( + TTkString( f'A file named "{os.path.basename(fileName)}" already exists.\nDo you want to replace it?', TTkColor.BOLD) + + TTkString( f'\n\nReplacing it will overwrite its contents.') ), + icon=TTkMessageBox.Icon.Warning, + standardButtons=TTkMessageBox.StandardButton.Discard|TTkMessageBox.StandardButton.Save|TTkMessageBox.StandardButton.Cancel) + messageBox.buttonSelected.connect(_cb) + TTkHelper.overlay(None, messageBox, 5, 5, True) + else: + ttkCrossSave(fileName,content,encoding) + filePicker = TTkFileDialogPicker( + size=(100,30), path=filePath, + acceptMode=TTkK.AcceptMode.AcceptSave, + caption="Save As...", + fileMode=TTkK.FileMode.AnyFile , + filter=filter) + filePicker.pathPicked.connect(_approveFile) + TTkHelper.overlay(None, filePicker, 5, 5, True) + + ttkCrossOpen = _open + ttkCrossSave = _save + ttkCrossSaveAs = _saveAs + ttkEmitDragOpen = lambda a:None + ttkEmitFileOpen = lambda a:None + ttkConnectDragOpen = lambda a,b:None diff --git a/TermTk/TTkGui/clipboard.py b/TermTk/TTkGui/clipboard.py index 07e5b022..da626127 100644 --- a/TermTk/TTkGui/clipboard.py +++ b/TermTk/TTkGui/clipboard.py @@ -61,10 +61,21 @@ class TTkClipboard(): try: if importlib.util.find_spec('pyodideProxy'): TTkLog.info("Using 'pyodideProxy' as clipboard manager") - import pyodideProxy as _c - TTkClipboard._manager = _c - TTkClipboard._setText = _c.copy - TTkClipboard._text = _c.paste + import pyodideProxy + import asyncio + async def _async_co(): + text = await pyodideProxy.paste() + TTkLog.debug(f"ttkProxy paste_co: {text}") + return text + def _paste(): + loop = asyncio.get_event_loop() + text = loop.run_until_complete(_async_co()) + # text = loop.run_until_complete(pyodideProxy.paste()) + TTkLog.debug(f"ttkProxy paste: {text=} {_async_co()=}") + return text + TTkClipboard._manager = pyodideProxy + TTkClipboard._setText = pyodideProxy.copy + TTkClipboard._text = pyodideProxy.paste # _paste elif importlib.util.find_spec('copykitten'): TTkLog.info("Using 'copykitten' as clipboard manager") import copykitten as _c @@ -113,13 +124,13 @@ class TTkClipboard(): except Exception as e: TTkLog.error("Clipboard error, try to export X11 if you are running this UI via SSH") for line in str(e).split("\n"): - TTkLog.error(line) + TTkLog.error(str(line)) @staticmethod def text(): '''text''' if TTkClipboard._text: - txt = "" + txt = None try: txt = TTkClipboard._text() except Exception as e: diff --git a/tools/dumb_paint_lib/maintemplate.py b/tools/dumb_paint_lib/maintemplate.py index 0ec7d59f..ea2c4a35 100644 --- a/tools/dumb_paint_lib/maintemplate.py +++ b/tools/dumb_paint_lib/maintemplate.py @@ -264,7 +264,7 @@ class PaintTemplate(ttk.TTkAppTemplate): self.setMenuBar(appMenuBar:=ttk.TTkMenuBarLayout(), self.TOP) fileMenu = appMenuBar.addMenu("&File") - buttonOpen = fileMenu.addMenu("&Open") + fileMenu.addMenu("&Open" ).menuButtonClicked.connect(self._open) fileMenu.addMenu("&Save" ).menuButtonClicked.connect(self._save) fileMenu.addMenu("Save &As...").menuButtonClicked.connect(self._saveAs) fileMenu.addSpacer() @@ -277,8 +277,8 @@ class PaintTemplate(ttk.TTkAppTemplate): buttonExit = fileMenu.addMenu("E&xit") buttonExit.menuButtonClicked.connect(ttk.TTkHelper.quit) - menuExport.addMenu("&Ascii/Txt") - menuExport.addMenu("&Ansi") + menuExport.addMenu("&Ascii/Txt").menuButtonClicked.connect(self._saveAsAscii) + menuExport.addMenu("&Ansi").menuButtonClicked.connect(self._saveAsAnsi) menuExport.addMenu("&Python") menuExport.addMenu("&Bash") @@ -315,23 +315,58 @@ class PaintTemplate(ttk.TTkAppTemplate): if fileName: self._openFile(fileName) + ttk.ttkConnectDragOpen(ttk.TTkEncoding.APPLICATION_JSON, self._openDragData) + + @ttk.pyTTkSlot() + def _open(self): + ttk.ttkCrossOpen( + path='.', + encoding=ttk.TTkEncoding.APPLICATION_JSON, + filter="DumbPaintTool Files (*.DPT.json);;Json Files (*.json);;All Files (*)", + cb=self._openDragData) + @ttk.pyTTkSlot() def _save(self): - image = self._parea.exportImage() - ttk.ttkCrossSave('untitled.DPT.txt', image, ttk.TTkEncoding.TEXT_PLAIN) + doc = self._parea.exportDocument() + ttk.ttkCrossSave('untitled.DPT.json', json.dumps(doc, indent=1), ttk.TTkEncoding.APPLICATION_JSON) @ttk.pyTTkSlot() def _saveAs(self): + doc = self._parea.exportDocument() + ttk.ttkCrossSaveAs('untitled.DPT.json', json.dumps(doc, indent=1), ttk.TTkEncoding.APPLICATION_JSON, + filter="DumbPaintTool Files (*.DPT.json);;Json Files (*.json);;All Files (*)") + + @ttk.pyTTkSlot() + def _saveAsAnsi(self): + image = self._parea.exportImage() + text = ttk.TTkString(image) + ttk.ttkCrossSaveAs('untitled.DPT.Ansi.txt', text.toAnsi(), ttk.TTkEncoding.TEXT_PLAIN_UTF8, + filter="Ansi text Files (*.Ansi.txt);;Text Files (*.txt);;All Files (*)") + + @ttk.pyTTkSlot() + def _saveAsAscii(self): image = self._parea.exportImage() - ttk.ttkCrossSaveAs('untitled.DPT.txt', image, ttk.TTkEncoding.TEXT_PLAIN) + text = ttk.TTkString(image) + ttk.ttkCrossSaveAs('untitled.DPT.ASCII.txt', text.toAscii(), ttk.TTkEncoding.TEXT_PLAIN_UTF8, + filter="ASCII Text Files (*.ASCII.txt);;Text Files (*.txt);;All Files (*)") + + @ttk.pyTTkSlot(dict) + def _openDragData(self, data): + dd = json.loads(data['data']) + if 'layers' in dd: + self.importDocument(dd) + else: + self._layers.addLayer(name="Import") + self._parea.importLayer(dd) def _openFile(self, fileName): ttk.TTkLog.info(f"Open: {fileName}") with open(fileName) as fp: # dd = json.load(fp) - text = fp.read() - dd = eval(text) + # text = fp.read() + # dd = eval(text) + dd = json.load(fp) if 'layers' in dd: self.importDocument(dd) else: diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 83a703df..5b2e689d 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -155,11 +155,15 @@ class PaintArea(ttk.TTkAbstractScrollView): def importDocument(self, dd): self._canvasLayers = [] - if 'version' in dd and dd['version']=='1.0.0': + if ( + ( 'version' in dd and dd['version'] == '1.0.0' ) or + ( 'version' in dd and dd['version'] == '1.0.1' and dd['type'] == 'DumbPaintTool/Document') ): self.resizeCanvas(*dd['size']) for l in dd['layers']: nl = self.newLayer() nl.importLayer(l) + else: + ttk.TTkLog.error("File Format not recognised") self._retuneGeometry() def exportImage(self): @@ -173,7 +177,8 @@ class PaintArea(ttk.TTkAbstractScrollView): def exportDocument(self, full=True, palette=True, crop=True) -> dict: pw,ph = self._documentSize outData = { - 'version':'1.0.0', + 'type':'DumbPaintTool/Document', + 'version':'1.0.1', 'size':(pw,ph), 'layers':[l.exportLayer(full=full,palette=palette,crop=crop) for l in self._canvasLayers]} return outData diff --git a/tools/webExporter/index.html b/tools/webExporter/index.html index 0545777f..74c03b96 100644 --- a/tools/webExporter/index.html +++ b/tools/webExporter/index.html @@ -9,16 +9,26 @@ + +
+