From 2dbe708f5a28a57c98d52cc71d6ac7e88a7ece4e Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 1 Mar 2024 15:49:46 +0000 Subject: [PATCH] Affed palette and fill tool in the dumb.paint.tool --- tools/dumb.paint.tool.py | 100 +++++++++++++++++-- tools/dumb_paint_lib/__init__.py | 3 +- tools/dumb_paint_lib/paintarea.py | 107 ++++++++++++++++++-- tools/dumb_paint_lib/palette.py | 156 ++++++++++++++++++++++++++++++ tools/dumb_paint_lib/textarea.py | 12 +-- 5 files changed, 354 insertions(+), 24 deletions(-) create mode 100644 tools/dumb_paint_lib/palette.py diff --git a/tools/dumb.paint.tool.py b/tools/dumb.paint.tool.py index e1a92840..9f70e364 100755 --- a/tools/dumb.paint.tool.py +++ b/tools/dumb.paint.tool.py @@ -31,13 +31,93 @@ from dumb_paint_lib import * ttk.TTkTheme.loadTheme(ttk.TTkTheme.NERD) +class LeftPanel(ttk.TTkVBoxLayout): + __slots__ = ('_palette', + # Signals + 'toolSelected') + def __init__(self, *args, **kwargs): + self.toolSelected = ttk.pyTTkSignal(PaintArea.Tool) + super().__init__(*args, **kwargs) + self._palette = Palette(maxHeight=12) + self.addWidget(self._palette) + + # Layout for the toggle buttons + lToggleFgBg = ttk.TTkHBoxLayout() + cb_p_fg = ttk.TTkCheckbox(text="-FG-", checked=ttk.TTkK.Checked) + cb_p_bg = ttk.TTkCheckbox(text="-BG-", checked=ttk.TTkK.Checked) + lToggleFgBg.addWidgets([cb_p_fg,cb_p_bg]) + lToggleFgBg.addItem(ttk.TTkLayout()) + cb_p_fg.toggled.connect(self._palette.enableFg) + cb_p_bg.toggled.connect(self._palette.enableBg) + self.addItem(lToggleFgBg) + + # Toolset + lTools = ttk.TTkGridLayout() + ra_brush = ttk.TTkRadioButton(radiogroup="tools", text="Brush", checked=True) + ra_line = ttk.TTkRadioButton(radiogroup="tools", text="Line", enabled=False) + ra_rect = ttk.TTkRadioButton(radiogroup="tools", text="Rect") + ra_oval = ttk.TTkRadioButton(radiogroup="tools", text="Oval", enabled=False) + + ra_rect_f = ttk.TTkRadioButton(radiogroup="toolsRectFill", text="Fill" , enabled=False, checked=True) + ra_rect_e = ttk.TTkRadioButton(radiogroup="toolsRectFill", text="Empty", enabled=False) + + @ttk.pyTTkSlot(bool) + def _emitTool(checked): + if not checked: return + tool = PaintArea.Tool.BRUSH + if ra_brush.isChecked(): + tool = PaintArea.Tool.BRUSH + elif ra_rect.isChecked(): + if ra_rect_e.isChecked(): + tool = PaintArea.Tool.RECTEMPTY + else: + tool = PaintArea.Tool.RECTFILL + self.toolSelected.emit(tool) + + ra_rect.toggled.connect(ra_rect_f.setEnabled) + ra_rect.toggled.connect(ra_rect_e.setEnabled) + + 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) + + lTools.addWidget(ra_brush,0,0) + lTools.addWidget(ra_line,1,0) + lTools.addWidget(ra_rect,2,0) + lTools.addWidget(ra_rect_f,2,1) + lTools.addWidget(ra_rect_e,2,2) + lTools.addWidget(ra_oval,3,0) + self.addItem(lTools) + + # brush + # line + # rettangle [empty,fill] + # oval [empty,fill] + self.addItem(ttk.TTkLayout()) + + + + def palette(self): + return self._palette + + class PaintTemplate(ttk.TTkAppTemplate): def __init__(self, border=False, **kwargs): super().__init__(border, **kwargs) + parea = PaintArea() + ptoolkit = PaintToolKit() + tarea = TextArea() - self.setWidget(pa:=PaintArea() , self.MAIN) - self.setItem( ptk:=PaintToolKit(), self.TOP, size=3, fixed=True) - self.setItem( ta:=TextArea() , self.RIGHT, size=50) + leftPanel = LeftPanel() + palette = leftPanel.palette() + + self.setItem(leftPanel , self.LEFT, size=16*2) + self.setWidget(parea , self.MAIN) + self.setItem(ptoolkit , self.TOP, size=3, fixed=True) + self.setItem(tarea , self.RIGHT, size=50) self.setMenuBar(appMenuBar:=ttk.TTkMenuBarLayout(), self.TOP) fileMenu = appMenuBar.addMenu("&File") @@ -45,6 +125,9 @@ class PaintTemplate(ttk.TTkAppTemplate): buttonClose = fileMenu.addMenu("&Save") buttonClose = fileMenu.addMenu("Save &As...") fileMenu.addSpacer() + fileMenu.addMenu("Load Palette") + fileMenu.addMenu("Save Palette") + fileMenu.addSpacer() buttonExit = fileMenu.addMenu("E&xit") buttonExit.menuButtonClicked.connect(ttk.TTkHelper.quit) @@ -56,10 +139,15 @@ class PaintTemplate(ttk.TTkAppTemplate): helpMenu.addMenu("About ...").menuButtonClicked helpMenu.addMenu("About tlogg").menuButtonClicked - ptk.updatedColor.connect(pa.setGlyphColor) - ta.charSelected.connect(ptk.glyphFromString) - ta.charSelected.connect(pa.glyphFromString) + palette.colorSelected.connect(parea.setGlyphColor) + palette.colorSelected.connect(ptoolkit.setColor) + ptoolkit.updatedColor.connect(parea.setGlyphColor) + tarea.charSelected.connect(ptoolkit.glyphFromString) + tarea.charSelected.connect(parea.glyphFromString) + leftPanel.toolSelected.connect(parea.setTool) + parea.setGlyphColor(palette.color()) + ptoolkit.setColor(palette.color()) root = ttk.TTk( diff --git a/tools/dumb_paint_lib/__init__.py b/tools/dumb_paint_lib/__init__.py index 0e08a46d..2083f789 100644 --- a/tools/dumb_paint_lib/__init__.py +++ b/tools/dumb_paint_lib/__init__.py @@ -1,2 +1,3 @@ from .paintarea import * -from .textarea import * \ No newline at end of file +from .textarea import * +from .palette import * \ No newline at end of file diff --git a/tools/dumb_paint_lib/paintarea.py b/tools/dumb_paint_lib/paintarea.py index 0e616bc2..77e93e8f 100644 --- a/tools/dumb_paint_lib/paintarea.py +++ b/tools/dumb_paint_lib/paintarea.py @@ -29,6 +29,8 @@ import TermTk as ttk class PaintToolKit(ttk.TTkGridLayout): + + __slots__ = ('_rSelect', '_rPaint', '_lgliph', '_cbFg', '_cbBg', '_bpFg', '_bpBg', '_glyph', #Signals @@ -63,6 +65,8 @@ class PaintToolKit(ttk.TTkGridLayout): self._refreshColor(emit=False) + + @ttk.pyTTkSlot() def _refreshColor(self, emit=True): color =self.color() @@ -78,7 +82,8 @@ class PaintToolKit(ttk.TTkGridLayout): def glyphFromString(self, ch:ttk.TTkString): if len(ch)<=0: return self._glyph = ch.charAt(0) - self.setColor(ch.colorAt(0)) + self._refreshColor() + # self.setColor(ch.colorAt(0)) def color(self): color = ttk.TTkColor() @@ -88,6 +93,7 @@ class PaintToolKit(ttk.TTkGridLayout): 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) @@ -107,10 +113,14 @@ class PaintToolKit(ttk.TTkGridLayout): self._refreshColor(emit=False) class PaintArea(ttk.TTkWidget): + class Tool(int): + BRUSH = 0x01 + RECTFILL = 0x02 + RECTEMPTY = 0x03 __slots__ = ('_canvasArea', '_canvasSize', '_transparentColor', - '_mouseMove', + '_mouseMove', '_mouseFill', '_tool', '_glyph', '_glyphColor') def __init__(self, *args, **kwargs): @@ -118,8 +128,10 @@ class PaintArea(ttk.TTkWidget): self._canvasSize = (0,0) self._canvasArea = {'data':[],'colors':[]} self._glyph = 'X' - self._glyphColor = ttk.TTkColor.fg("#0000FF") + self._glyphColor = ttk.TTkColor.RST self._mouseMove = None + self._mouseFill = None + self._tool = self.Tool.BRUSH super().__init__(*args, **kwargs) self.resizeCanvas(80,25) self.setFocusPolicy(ttk.TTkK.ClickFocus + ttk.TTkK.TabFocus) @@ -133,7 +145,18 @@ class PaintArea(ttk.TTkWidget): self._canvasArea['colors'][i] = (self._canvasArea['colors'][i] + [ttk.TTkColor.RST for _ in range(w)])[:w] self.update() + def leaveEvent(self, evt): + self._mouseMove = None + self.update() + return super().leaveEvent(evt) + + @ttk.pyTTkSlot(Tool) + def setTool(self, tool): + self._tool = tool + self.update() + def mouseMoveEvent(self, evt) -> bool: + # self._mouseFill = None x,y = evt.x, evt.y w,h = self._canvasSize if 0<=x bool: - if self._placeGlyph(evt.x, evt.y): + 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) def mousePressEvent(self, evt) -> bool: - if self._placeGlyph(evt.x, evt.y): + 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) + + 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 return super().mousePressEvent(evt) @ttk.pyTTkSlot(ttk.TTkString) def glyphFromString(self, ch:ttk.TTkString): if len(ch)<=0: return self._glyph = ch.charAt(0) - self._glyphColor = ch.colorAt(0) + # self._glyphColor = ch.colorAt(0) def glyph(self): return self._glyph @@ -175,6 +220,42 @@ class PaintArea(ttk.TTkWidget): def setGlyphColor(self, color): self._glyphColor = color + def _placeFill(self): + if not self._mouseFill: return False + w,h = self._canvasSize + ax,ay,bx,by = self._mouseFill + 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) + + self._mouseFill = None + self._mouseMove = None + + data = self._canvasArea['data'] + colors = self._canvasArea['colors'] + glyph = self._glyph + color = self._glyphColor + + if self._tool == self.Tool.RECTFILL: + for row in data[fay:fby+1]: + row[fax:fbx+1] = [glyph]*(fbx-fax+1) + for row in colors[fay:fby+1]: + row[fax:fbx+1] = [color]*(fbx-fax+1) + if self._tool == self.Tool.RECTEMPTY: + data[fay][fax:fbx+1] = [glyph]*(fbx-fax+1) + data[fby][fax:fbx+1] = [glyph]*(fbx-fax+1) + colors[fay][fax:fbx+1] = [color]*(fbx-fax+1) + colors[fby][fax:fbx+1] = [color]*(fbx-fax+1) + for row in data[fay:fby]: + row[fax]=row[fbx]=glyph + for row in colors[fay:fby]: + row[fax]=row[fbx]=color + self.update() + return True + def _placeGlyph(self,x,y): self._mouseMove = None w,h = self._canvasSize @@ -204,4 +285,16 @@ class PaintArea(ttk.TTkWidget): x,y = self._mouseMove gc = self._glyphColor canvas._data[y][x] = self._glyph - canvas._colors[y][x] = gc if gc._bg else gc+tc \ No newline at end of file + canvas._colors[y][x] = gc if gc._bg else gc+tc + if self._mouseFill: + ax,ay,bx,by = self._mouseFill + 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 + gc = self._glyphColor + canvas.fill(pos=(x,y), size=(w,h), + color=gc if gc._bg else gc+tc, + char=self._glyph) \ No newline at end of file diff --git a/tools/dumb_paint_lib/palette.py b/tools/dumb_paint_lib/palette.py new file mode 100644 index 00000000..de1ff017 --- /dev/null +++ b/tools/dumb_paint_lib/palette.py @@ -0,0 +1,156 @@ +# MIT License +# +# Copyright (c) 2021 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__ = ['Palette'] + +import sys, os + +sys.path.append(os.path.join(sys.path[0],'../..')) +import TermTk as ttk + +_defaultPalette = [ + [( 64, 0, 0),(102, 0, 0),(140, 0, 0),(178, 0, 0),(217, 0, 0),(255, 0, 0),(255, 51, 51),(255,102,102),( 0, 32, 64),( 0, 51,102), ( 0, 70,140),( 0, 89,178),( 0,108,217),( 0,128,255),( 51,153,255),(102,178,255)], + [( 64, 16, 0),(102, 26, 0),(140, 35, 0),(178, 45, 0),(217, 54, 0),(255, 64, 0),(255,102, 51),(255,140,102),( 0, 0, 64),( 0, 0,102), ( 0, 0,140),( 0, 0,178),( 0, 0,217),( 0, 0,255),( 51, 51,255),(102,102,255)], + [( 64, 32, 0),(102, 51, 0),(140, 70, 0),(178, 89, 0),(217,108, 0),(255,128, 0),(255,153, 51),(255,178,102),( 16, 0, 64),( 26, 0,102), ( 35, 0,140),( 45, 0,178),( 54, 0,217),( 64, 0,255),(102, 51,255),(140,102,255)], + [( 64, 48, 0),(102, 77, 0),(140,105, 0),(178,134, 0),(217,163, 0),(255,191, 0),(255,204, 51),(255,217,102),( 32, 0, 64),( 51, 0,102), ( 70, 0,140),( 89, 0,178),(108, 0,217),(128, 0,255),(153, 51,255),(178,102,255)], + [( 64, 64, 0),(102,102, 0),(140,140, 0),(178,178, 0),(217,217, 0),(255,255, 0),(255,255, 51),(255,255,102),( 48, 0, 64),( 77, 0,102), (105, 0,140),(134, 0,178),(163, 0,217),(191, 0,255),(204, 51,255),(217,102,255)], + [( 48, 64, 0),( 77,102, 0),(105,140, 0),(134,178, 0),(163,217, 0),(191,255, 0),(204,255, 51),(217,255,102),( 64, 0, 64),(102, 0,102), (140, 0,140),(178, 0,178),(217, 0,217),(255, 0,255),(255, 51,255),(255,102,255)], + [( 32, 64, 0),( 51,102, 0),( 70,140, 0),( 89,178, 0),(108,217, 0),(128,255, 0),(153,255, 51),(178,255,102),( 64, 0, 48),(102, 0, 77), (140, 0,105),(178, 0,134),(217, 0,163),(255, 0,191),(255, 51,204),(255,102,217)], + [( 0, 64, 0),( 0,102, 0),( 0,140, 0),( 0,178, 0),( 0,217, 0),( 0,255, 0),( 51,255, 51),(102,255,102),( 64, 0, 32),(102, 0, 51), (140, 0, 70),(178, 0, 89),(217, 0,108),(255, 0,128),(255, 51,153),(255,102,178)], + [( 0, 64, 32),( 0,102, 51),( 0,140, 70),( 0,178, 89),( 0,217,108),( 0,255,128),( 51,255,153),(102,255,178),( 64, 0, 16),(102, 0, 26), (140, 0, 35),(178, 0, 45),(217, 0, 54),(255, 0, 64),(255, 51,102),(255,102,140)], + [( 0, 64, 48),( 0,102, 77),( 0,140,105),( 0,178,134),( 0,217,163),( 0,255,191),( 51,255,204),(102,255,217),( 26, 26, 26),( 51, 51, 51), ( 77, 77, 77),(102,102,102),(128,128,128),(158,158,158),(191,191,191),(222,222,222)], + [( 0, 64, 64),( 0,102,102),( 0,140,140),( 0,178,178),( 0,217,217),( 0,255,255),( 51,255,255),(102,255,255),( 26, 20, 13),( 51, 41, 26), ( 77, 61, 38),(102, 82, 51),(128,102, 64),(158,134,100),(191,171,143),(222,211,195)], + [( 0, 48, 64),( 0, 77,102),( 0,105,140),( 0,134,178),( 0,163,217),( 0,191,255),( 51,204,255),(102,217,255),( 0, 0, 0),( 0, 0, 0), ( 0, 0, 0),( 0, 0, 0),(255,255,255),(255,255,255),(255,255,255),(255,255,255)]] + +class Palette(ttk.TTkWidget): + __slots__ = ('_bg', '_fg', '_mouseMove', '_palette', + '_enabledFg', '_enabledBg', + #signals + 'colorSelected') + def __init__(self, *args, **kwargs): + self.colorSelected = ttk.pyTTkSignal(ttk.TTkColor) + self._fg = (5,3) + self._bg = (9,2) + self._enabledFg = True + self._enabledBg = True + self._mouseMove = None + self.setPalette(_defaultPalette) + super().__init__(*args, **kwargs) + + def setPalette(self, palette): + self._palette = [] + for row in palette: + colors = [] + for r,g,b in row: + colors.append(( + ttk.TTkColor.fg(f"#{r<<16|g<<8|b:06x}"), + ttk.TTkColor.bg(f"#{r<<16|g<<8|b:06x}"))) + self._palette.append(colors) + self.update() + + def color(self): + palette = self._palette + fx,fy = self._fg + bx,by = self._bg + fg = palette[fy][fx][0] + bg = palette[by][bx][1] + if self._enabledFg and self._enabledBg: + return fg+bg + elif self._enabledFg: + return fg + elif self._enabledBg: + return bg + else: + return ttk.TTkColor.RST + + @ttk.pyTTkSlot(bool) + def enableFg(self, enable=True): + self._enabledFg = enable + self.colorSelected.emit(self.color()) + self.update() + + @ttk.pyTTkSlot(bool) + def enableBg(self, enable=True): + self._enabledBg = enable + self.colorSelected.emit(self.color()) + self.update() + + def mousePressEvent(self, evt) -> bool: + x,y = evt.x, evt.y + self._mouseMove = None + if 0<=x<32 and 0<=y<12: + if evt.key == ttk.TTkK.RightButton: + self._bg = (x//2,y) + elif evt.key == ttk.TTkK.LeftButton: + self._fg = (x//2,y) + self.colorSelected.emit(self.color()) + self.update() + return True + self.update() + return super().mousePressEvent(evt) + + def leaveEvent(self, evt): + self._mouseMove = None + self.update() + return super().leaveEvent(evt) + + def mouseMoveEvent(self, evt) -> bool: + x,y = evt.x, evt.y + if 0<=x<32 and 0<=y<12: + self._mouseMove = (x//2, y) + self.update() + return True + self._mouseMove = None + self.update() + return super().mouseMoveEvent(evt) + + def paintEvent(self, canvas: ttk.TTkCanvas): + palette = self._palette + for y,row in enumerate(palette): + for x,col in enumerate(row): + canvas.drawText(pos=(x*2,y),text=' ',color=col[1]) + # Draw Mouse Move + if self._mouseMove: + x,y = self._mouseMove + r,g,b = palette[y][x][0].fgToRGB() + chc = ttk.TTkColor.fg('#000000') if r+b+g > (100*3) else ttk.TTkColor.fg('#FFFFFF') + color = palette[y][x][1] + chc + canvas.drawText(pos=(x*2,y), text="◀▶", color=color) + # Draw FG Ref + x,y = self._fg + r,g,b = palette[y][x][0].fgToRGB() + if self._enabledFg: + chc = ttk.TTkColor.fg('#000000') if r+b+g > (128*3) else ttk.TTkColor.fg('#FFFFFF') + else: + chc = ttk.TTkColor.fg('#666666') if r+b+g > (128*3) else ttk.TTkColor.fg('#999999') + color = palette[y][x][1] + chc + canvas.drawChar(pos=(x*2,y), char="F", color=color) + # Draw BG ref + x,y = self._bg + r,g,b = palette[y][x][0].fgToRGB() + if self._enabledBg: + chc = ttk.TTkColor.fg('#000000') if r+b+g > (128*3) else ttk.TTkColor.fg('#FFFFFF') + else: + chc = ttk.TTkColor.fg('#666666') if r+b+g > (128*3) else ttk.TTkColor.fg('#999999') + color = palette[y][x][1] + chc + canvas.drawChar(pos=(x*2+1,y), char="B", color=color) + diff --git a/tools/dumb_paint_lib/textarea.py b/tools/dumb_paint_lib/textarea.py index d1251615..67456523 100644 --- a/tools/dumb_paint_lib/textarea.py +++ b/tools/dumb_paint_lib/textarea.py @@ -27,14 +27,6 @@ import sys, os sys.path.append(os.path.join(sys.path[0],'../..')) import TermTk as ttk - - - - - - - - _StartingText = '''Ansi Editor: ─ ┌─┬┐ ┏━┳┓ ┎─┰┒ ┍━┯┑ ┏┭┲━┱┮┓ @@ -56,8 +48,8 @@ _StartingText = '''Ansi Editor: ═ └───────────────────┘▓ ╯ ╰─────╯ ║ ░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓█ ╰ ╱ ╲ ╳ -▄ ▀ █ - Half,Full -▁ ▂ ▃ ▄ ▅ ▆ ▇ █ +▄ ▀ █ <- Half,Full +▁ ▂ ▃ ▄ ▅ ▆ ▇ █ Spaces: " " █ Full █ ▌ ▐ ▏ ▕ ▔ ▁ ▉ ░ ▒ ▓ █