You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
13 KiB
324 lines
13 KiB
# MIT License |
|
# |
|
# Copyright (c) 2024 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
|
# |
|
# Permission is hereby granted, free of charge, to any person obtaining a copy |
|
# of this software and associated documentation files (the "Software"), to deal |
|
# in the Software without restriction, including without limitation the rights |
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
# copies of the Software, and to permit persons to whom the Software is |
|
# furnished to do so, subject to the following conditions: |
|
# |
|
# The above copyright notice and this permission notice shall be included in all |
|
# copies or substantial portions of the Software. |
|
# |
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
# SOFTWARE. |
|
|
|
__all__ = ['Layers','LayerData'] |
|
|
|
import sys, os |
|
|
|
sys.path.append(os.path.join(sys.path[0],'../..')) |
|
import TermTk as ttk |
|
|
|
class LayerData(): |
|
__slots__ = ('_name','_data', |
|
#signals |
|
'nameChanged','visibilityToggled') |
|
def __init__(self,name:ttk.TTkString=ttk.TTkString('New'),data=None) -> None: |
|
self._name:ttk.TTkString = ttk.TTkString(name) if type(name)==str else name |
|
self.visibilityToggled = ttk.pyTTkSignal(bool) |
|
self._data = data |
|
self.nameChanged = ttk.pyTTkSignal(str) |
|
def name(self): |
|
return self._name |
|
def setName(self,name): |
|
self.nameChanged.emit(name) |
|
self._name = name |
|
def data(self): |
|
return self._data |
|
def setData(self,data): |
|
self._data = data |
|
|
|
class _layerButton(ttk.TTkContainer): |
|
classStyle = { |
|
'default': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#000044"), |
|
'borderColor': ttk.TTkColor.fg('#CCDDDD'), |
|
'grid':1}, |
|
'disabled': {'color': ttk.TTkColor.fg('#888888'), |
|
'borderColor':ttk.TTkColor.fg('#888888'), |
|
'grid':0}, |
|
'hover': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#000050")+ttk.TTkColor.BOLD, |
|
'borderColor': ttk.TTkColor.fg("#FFFFCC")+ttk.TTkColor.BOLD, |
|
'grid':1}, |
|
'selected': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#004488"), |
|
'borderColor': ttk.TTkColor.fg("#FFFF00"), |
|
'grid':0}, |
|
'unchecked': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#000044"), |
|
'borderColor': ttk.TTkColor.RST, |
|
'grid':3}, |
|
'clicked': {'color': ttk.TTkColor.fg("#FFFFDD")+ttk.TTkColor.BOLD, |
|
'borderColor': ttk.TTkColor.fg("#DDDDDD")+ttk.TTkColor.BOLD, |
|
'grid':0}, |
|
'focus': {'color': ttk.TTkColor.fg("#dddd88")+ttk.TTkColor.bg("#000044")+ttk.TTkColor.BOLD, |
|
'borderColor': ttk.TTkColor.fg("#ffff00") + ttk.TTkColor.BOLD, |
|
'grid':1}, |
|
} |
|
|
|
__slots__ = ('_layerData','_first', '_isSelected', '_layerVisible', |
|
'_ledit', |
|
# signals |
|
'clicked', 'visibilityToggled', |
|
) |
|
def __init__(self, layer:LayerData, **kwargs): |
|
self.clicked = ttk.pyTTkSignal(_layerButton) |
|
self._layerData:LayerData = layer |
|
self._isSelected = False |
|
self._first = True |
|
self._layerVisible = True |
|
self.visibilityToggled = layer.visibilityToggled |
|
|
|
super().__init__(**kwargs|{'layout':ttk.TTkGridLayout()}) |
|
self.setPadding(1,1,7,2) |
|
self._ledit = ttk.TTkLineEdit(parent=self, text=layer.name(),visible=False) |
|
self._ledit.focusChanged.connect(self._ledit.setVisible) |
|
self._ledit.textEdited.connect(self._textEdited) |
|
# self.setFocusPolicy(ttk.TTkK.ClickFocus) |
|
|
|
@ttk.pyTTkSlot(str) |
|
def _textEdited(self, text): |
|
self._layerData.setName(text) |
|
|
|
def mousePressEvent(self, evt) -> bool: |
|
if evt.x <= 3: |
|
self._layerVisible = not self._layerVisible |
|
self.visibilityToggled.emit(self._layerVisible) |
|
self.setFocus() |
|
self.update() |
|
return True |
|
|
|
def mouseReleaseEvent(self, evt) -> bool: |
|
self.clicked.emit(self) |
|
return True |
|
|
|
def mouseDoubleClickEvent(self, evt) -> bool: |
|
self._ledit.setVisible(True) |
|
self._ledit.setFocus() |
|
return True |
|
|
|
def mouseDragEvent(self, evt) -> bool: |
|
drag = ttk.TTkDrag() |
|
drag.setData(self) |
|
name = self._layerData.name() |
|
pm = ttk.TTkCanvas(width=len(name)+4,height=3) |
|
pm.drawBox(pos=(0,0),size=pm.size()) |
|
pm.drawText(pos=(2,1), text=name) |
|
drag.setHotSpot(5, 1) |
|
drag.setPixmap(pm) |
|
drag.exec() |
|
return True |
|
|
|
def paintEvent(self, canvas: ttk.TTkCanvas): |
|
if self._isSelected: |
|
style = self.style()['selected'] |
|
else: |
|
style = self.currentStyle() |
|
borderColor = style['borderColor'] |
|
textColor = style['color'] |
|
btnVisible = '▣' if self._layerVisible else '□' |
|
w,h = self.size() |
|
canvas.drawText( pos=(0,0),text=f" ┏{'━'*(w-7)}┓",color=borderColor) |
|
canvas.drawText( pos=(0,2),text=f" ┗{'━'*(w-7)}┛",color=borderColor) |
|
if self._first: |
|
canvas.drawText(pos=(0,1),text=f" {btnVisible} - ┃{' '*(w-7)}┃",color=borderColor) |
|
else: |
|
canvas.drawText(pos=(0,1),text=f" {btnVisible} - ╽{' '*(w-7)}╽",color=borderColor) |
|
canvas.drawTTkString(pos=(7,1),text=self._layerData.name(), width=w-9, color=textColor) |
|
|
|
class LayerScrollWidget(ttk.TTkAbstractScrollView): |
|
__slots__ = ('_layers','_selected', '_dropTo', |
|
# Signals |
|
'layerSelected','layerAdded','layerDeleted','layersOrderChanged') |
|
def __init__(self, **kwargs): |
|
self.layerSelected = ttk.pyTTkSignal(LayerData) |
|
self.layerAdded = ttk.pyTTkSignal(LayerData) |
|
self.layerDeleted = ttk.pyTTkSignal(LayerData) |
|
self.layersOrderChanged = ttk.pyTTkSignal(list[LayerData]) |
|
|
|
self._selected = None |
|
self._dropTo = None |
|
self._layers:list[_layerButton] = [] |
|
super().__init__(**kwargs) |
|
self.viewChanged.connect(self._placeTheButtons) |
|
self.viewChanged.connect(self._viewChangedHandler) |
|
|
|
@ttk.pyTTkSlot() |
|
def _viewChangedHandler(self): |
|
x,y = self.getViewOffsets() |
|
self.layout().setOffset(-x,-y) |
|
|
|
def viewFullAreaSize(self) -> tuple: |
|
_,_,w,h = self.layout().fullWidgetAreaGeometry() |
|
return w,h |
|
|
|
def viewDisplayedSize(self) -> tuple: |
|
return self.size() |
|
|
|
def maximumWidth(self): return 0x10000 |
|
def maximumHeight(self): return 0x10000 |
|
def minimumWidth(self): return 0 |
|
def minimumHeight(self): return 0 |
|
|
|
@ttk.pyTTkSlot(_layerButton) |
|
def _clickedLayer(self, layerButton:_layerButton): |
|
if sel:=self._selected: |
|
sel._isSelected = False |
|
sel.update() |
|
self._selected = layerButton |
|
layerButton._isSelected = True |
|
self.layerSelected.emit(layerButton._layerData) |
|
self.update() |
|
|
|
def clear(self): |
|
for layBtn in self._layers: |
|
self.layout().removeWidget(layBtn) |
|
layBtn.clicked.clear() |
|
layBtn.visibilityToggled.clear() |
|
layBtn._layerData.nameChanged.clear() |
|
self._layers.clear() |
|
self.update() |
|
|
|
@ttk.pyTTkSlot() |
|
def moveUp(self): |
|
return self._moveButton(-1) |
|
|
|
@ttk.pyTTkSlot() |
|
def moveDown(self): |
|
return self._moveButton(+1) |
|
|
|
def _moveButton(self,direction): |
|
if not self._selected: return |
|
index = self._layers.index(self._selected) |
|
if index+direction < 0: return |
|
l = self._layers.pop(index) |
|
self._layers.insert(index+direction,l) |
|
self._placeTheButtons() |
|
self.layersOrderChanged.emit([_l._layerData for _l in self._layers]) |
|
|
|
@ttk.pyTTkSlot() |
|
def addLayer(self,name=None, data=None): |
|
name = name if name else f"Layer #{len(self._layers)}" |
|
_l=LayerData(name=name,data=data) |
|
newLayerBtn:_layerButton = _layerButton(parent=self,layer=_l) |
|
self._layers.insert(0,newLayerBtn) |
|
if sel:=self._selected: sel._isSelected = False |
|
self._selected = newLayerBtn |
|
newLayerBtn._isSelected = True |
|
newLayerBtn.clicked.connect(self._clickedLayer) |
|
self.viewChanged.emit() |
|
self._placeTheButtons() |
|
self.layerAdded.emit(newLayerBtn._layerData) |
|
return _l |
|
|
|
def _placeTheButtons(self): |
|
w,h = self.size() |
|
for i,l in enumerate(self._layers): |
|
l._first = i==0 |
|
l.setGeometry(0,i*2,w,3) |
|
l.lowerWidget() |
|
self.update() |
|
|
|
@ttk.pyTTkSlot() |
|
def delLayer(self): |
|
self._layers.remove() |
|
|
|
def dragEnterEvent(self, evt) -> bool: |
|
if type(evt.data())!=_layerButton: return False |
|
x,y = self.getViewOffsets() |
|
self._dropTo = max(0,min(len(self._layers),(evt.y-1+y)//2)) |
|
self.update() |
|
return True |
|
def dragLeaveEvent(self, evt) -> bool: |
|
if type(evt.data())!=_layerButton: return False |
|
self._dropTo = None |
|
self.update() |
|
return True |
|
def dragMoveEvent(self, evt) -> bool: |
|
if type(evt.data())!=_layerButton: return False |
|
x,y = self.getViewOffsets() |
|
self._dropTo = max(0,min(len(self._layers),(evt.y-1+y)//2)) |
|
self.update() |
|
ttk.TTkLog.debug(f"{evt.x},{evt.y-y} - {len(self._layers)} - {self._dropTo}") |
|
return True |
|
def dropEvent(self, evt) -> bool: |
|
if type(evt.data())!=_layerButton: return False |
|
x,y = self.getViewOffsets() |
|
self._dropTo = None |
|
data = evt.data() |
|
# dropPos = len(self._layers)-(evt.y-1)//2 |
|
dropPos = max(0,min(len(self._layers),(evt.y-1+y)//2)) |
|
ttk.TTkLog.debug(f"{evt.x},{evt.y-y} - {len(self._layers)} - {self._dropTo} {dropPos}") |
|
if dropPos > self._layers.index(data): |
|
dropPos -= 1 |
|
self._layers.remove(data) |
|
self._layers.insert(dropPos,data) |
|
self._placeTheButtons() |
|
self.layersOrderChanged.emit([_l._layerData for _l in self._layers]) |
|
return True |
|
|
|
# Stupid hack to paint on top of the child widgets |
|
def paintChildCanvas(self): |
|
super().paintChildCanvas() |
|
offx, offy = self.getViewOffsets() |
|
if self._dropTo == None: return |
|
canvas = self.getCanvas() |
|
w,h = canvas.size() |
|
color = ttk.TTkColor.YELLOW |
|
canvas.drawText(pos=(0,(self._dropTo)*2-offy),text=f"╞{'═'*(w-2)}╍",color=color) |
|
|
|
|
|
|
|
class Layers(ttk.TTkGridLayout): |
|
__slots__ = ('_scrollWidget', |
|
# Forward Methods |
|
'addLayer','clear', |
|
# Forward Signals |
|
'layerSelected','layerAdded','layerDeleted','layersOrderChanged') |
|
def __init__(self, **kwargs): |
|
super().__init__(**kwargs) |
|
self._scrollWidget = _lsw = LayerScrollWidget() |
|
_sa = ttk.TTkAbstractScrollArea(scrollWidget=self._scrollWidget,minWidth=16) |
|
_sa.setViewport(_lsw) |
|
self.addWidget(_sa,0,0,1,5) |
|
self.addWidget(btnAdd :=ttk.TTkButton(text='add') ,1,0) |
|
self.addWidget(btnUp :=ttk.TTkButton(text='▲',maxWidth=3) ,1,1) |
|
self.addWidget(btnDown:=ttk.TTkButton(text='▼',maxWidth=3) ,1,2) |
|
# self.addItem(ttk.TTkLayout(),1,3) |
|
self.addWidget(btnDel :=ttk.TTkButton(text=ttk.TTkString('del',ttk.TTkColor.RED),maxWidth=5),1,4) |
|
|
|
btnAdd.setToolTip( "Create a new Layer\nand add it to the image") |
|
btnDel.setToolTip( "Delete the selected Layer") |
|
btnUp.setToolTip( "Raise the selected Layer one step") |
|
btnDown.setToolTip("Lower the selected Layer one step") |
|
|
|
btnAdd.clicked.connect( _lsw.addLayer) |
|
btnDel.clicked.connect( _lsw.delLayer) |
|
btnUp.clicked.connect( _lsw.moveUp) |
|
btnDown.clicked.connect(_lsw.moveDown) |
|
|
|
# forward signals |
|
self.layerSelected = _lsw.layerSelected |
|
self.layerSelected = _lsw.layerSelected |
|
self.layerAdded = _lsw.layerAdded |
|
self.layerDeleted = _lsw.layerDeleted |
|
self.layersOrderChanged = _lsw.layersOrderChanged |
|
|
|
# forward methods |
|
self.addLayer = _lsw.addLayer |
|
self.clear = _lsw.clear
|
|
|