Browse Source

Documented the Drag and Drop and added a bunch of examples and fixed its handling and improved the logging

pull/305/head
Eugenio Parodi 🌶️ 1 year ago
parent
commit
2f8c05f645
  1. 9
      TermTk/TTkCore/TTkTerm/inputmouse.py
  2. 5
      TermTk/TTkCore/log.py
  3. 2
      TermTk/TTkTemplates/dragevents.py
  4. 103
      TermTk/TTkWidgets/container.py
  5. 13
      TermTk/TTkWidgets/widget.py
  6. 5
      demo/showcase/dragndrop.py
  7. 203
      docs/source/info/resources/dragdrop.rst
  8. 17
      tests/ansi.images.json
  9. 1
      tests/ansi.images.json
  10. 2
      tests/sandbox/Makefile
  11. 84
      tutorial/examples/DragAndDrop/dnd.01.basic.py
  12. 96
      tutorial/examples/DragAndDrop/dnd.02.events.01.py
  13. 135
      tutorial/examples/DragAndDrop/dnd.02.events.02.py
  14. 87
      tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py
  15. 110
      tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py
  16. 134
      tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py
  17. 119
      tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py
  18. 125
      tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py
  19. 17
      tutorial/examples/ansi.images.json

9
TermTk/TTkCore/TTkTerm/inputmouse.py

@ -92,6 +92,15 @@ class TTkMouseEvent:
self.mod = mod
self.raw = raw
self.tap = tap
def pos(self) -> tuple[int,int]:
'''
Returns the position of the mouse cursor relative to the current widget.
:return: the position.
:rtype: tuple[int,int]
'''
return (self.x, self.y)
def clone(self, pos=None, evt=None):
x,y = pos or (self.x, self.y)

5
TermTk/TTkCore/log.py

@ -80,8 +80,9 @@ class TTkLog:
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe,1)
if len(calframe) > 2:
ctx = _TTkContext(calframe[2])
cb(mode, ctx, msg)
ctx = _TTkContext(calframe[2])
for txt in str(msg).split('\n'):
cb(mode, ctx, txt)
@staticmethod
def debug(msg):

2
TermTk/TTkTemplates/dragevents.py

@ -50,6 +50,8 @@ class TDragEvents():
.. note:: Reimplement this function to handle this event
.. note:: This event is triggered only if :py:meth:`TDragEvents.dragEnterEvent` or :py:meth:`TDragEvents.dragMoveEvent` were previously handled inside this widget.
:param evt: The drop event
:type evt: :py:class:`TTkDnDEvent`

103
TermTk/TTkWidgets/container.py

@ -292,107 +292,8 @@ class TTkContainer(TTkWidget):
return True
return False
_mouseOver = None
_mouseOverTmp = None
_mouseOverProcessed = False
def mouseEvent(self, evt: TTkMouseEvent) -> bool:
''' .. caution:: Don't touch this! '''
if not self._enabled: return False
# Saving self in this global variable
# So that after the "_mouseEventLayoutHandle"
# this tmp value will hold the last widget below the mouse
TTkWidget._mouseOverTmp = self
# Mouse Drag has priority because it
# should be handled by the focused widget and
# not pushed to the unfocused childs
# unless there is a Drag and Drop event ongoing
if evt.evt == TTkK.Drag and not TTkHelper.isDnD():
if self.mouseDragEvent(evt):
return True
if self.rootLayout() is not None:
if TTkContainer._mouseEventLayoutHandle(evt, self.rootLayout()):
return True
# If there is an overlay and it is modal,
# return False if this widget is not part of any
# of the widgets above the modal
if not TTkHelper.checkModalOverlay(self):
return False
# Handle Drag and Drop Events
if TTkHelper.isDnD():
ret = False
if evt.evt == TTkK.Drag:
dndw = TTkHelper.dndWidget()
if dndw == self:
if self.dragMoveEvent(TTkHelper.dndGetDrag().getDragMoveEvent(evt)):
return True
else:
if self.dragEnterEvent(TTkHelper.dndGetDrag().getDragEnterEvent(evt)):
if dndw:
ret = dndw.dragLeaveEvent(TTkHelper.dndGetDrag().getDragLeaveEvent(evt))
TTkHelper.dndEnter(self)
return True
if evt.evt == TTkK.Release:
if self.dropEvent(TTkHelper.dndGetDrag().getDropEvent(evt)):
return True
return ret
# handle Enter/Leave Events
# _mouseOverTmp hold the top widget under the mouse
# if different than self it means that it is a child
if evt.evt == TTkK.Move:
if not TTkWidget._mouseOverProcessed:
if TTkWidget._mouseOver != TTkWidget._mouseOverTmp == self:
if TTkWidget._mouseOver:
# TTkLog.debug(f"Leave: {TTkWidget._mouseOver._name}")
TTkWidget._mouseOver.leaveEvent(evt)
TTkWidget._mouseOver = self
# TTkLog.debug(f"Enter: {TTkWidget._mouseOver._name}")
TTkHelper.toolTipClose()
if self._toolTip and self._toolTip != '':
TTkHelper.toolTipTrigger(self._toolTip)
# TTkHelper.triggerToolTip(self._name)
TTkWidget._mouseOver.enterEvent(evt)
TTkWidget._mouseOverProcessed = True
if self.mouseMoveEvent(evt):
return True
else:
TTkHelper.toolTipClose()
if evt.evt == TTkK.Release:
self._pendingMouseRelease = False
self._processStyleEvent(TTkWidget._S_NONE)
if self.mouseReleaseEvent(evt):
return True
if evt.evt == TTkK.Press:
# in case of parent focus, check the parent that can accept the focus
w = self
while w._parent and (w.focusPolicy() & TTkK.ParentFocus) == TTkK.ParentFocus:
w = w._parent
if w.focusPolicy() & TTkK.ClickFocus == TTkK.ClickFocus:
w.setFocus()
w.raiseWidget()
self._processStyleEvent(TTkWidget._S_PRESSED)
if evt.tap == 2 and self.mouseDoubleClickEvent(evt):
#self._pendingMouseRelease = True
return True
if evt.tap > 1 and self.mouseTapEvent(evt):
return True
if evt.tap == 1 and self.mousePressEvent(evt):
# TTkLog.debug(f"Click {self._name}")
self._pendingMouseRelease = True
return True
if evt.key == TTkK.Wheel:
if self.wheelEvent(evt):
return True
return False
def _mouseEventParseChildren(self, evt:TTkMouseEvent) -> bool:
return TTkContainer._mouseEventLayoutHandle(evt, self.rootLayout())
def setLayout(self, layout:TTkLayout) -> None:
'''

13
TermTk/TTkWidgets/widget.py

@ -392,6 +392,9 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
'''
return False
def _mouseEventParseChildren(self, evt:TTkMouseEvent) -> bool:
return False
_mouseOver = None
_mouseOverTmp = None
_mouseOverProcessed = False
@ -416,6 +419,9 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
# if TTkWidget._mouseEventLayoutHandle(evt, self.rootLayout()):
# return True
if self._mouseEventParseChildren(evt):
return True
# If there is an overlay and it is modal,
# return False if this widget is not part of any
# of the widgets above the modal
@ -428,10 +434,11 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
if evt.evt == TTkK.Drag:
dndw = TTkHelper.dndWidget()
if dndw == self:
if self.dragMoveEvent(TTkHelper.dndGetDrag().getDragMoveEvent(evt)):
return True
self.dragMoveEvent(TTkHelper.dndGetDrag().getDragMoveEvent(evt))
return True
else:
if self.dragEnterEvent(TTkHelper.dndGetDrag().getDragEnterEvent(evt)):
if ( self.dragEnterEvent(TTkHelper.dndGetDrag().getDragEnterEvent(evt)) or
self.dragMoveEvent(TTkHelper.dndGetDrag().getDragMoveEvent(evt))):
if dndw:
ret = dndw.dragLeaveEvent(TTkHelper.dndGetDrag().getDragLeaveEvent(evt))
TTkHelper.dndEnter(self)

5
demo/showcase/dragndrop.py

@ -62,7 +62,10 @@ class DropThings(ttk.TTkFrame):
data = evt.data()
if issubclass(type(data),ttk.TTkWidget):
self.layout().addWidget(data)
data.move(evt.x,evt.y)
# Since the frame by default has a padding of 1
# I align the button to the mouse coordinates by subtracting the Top/Left padding size
t,b,l,r = self.getPadding()
data.move(evt.x-l, evt.y-t)
self.update()
return True
return False

203
docs/source/info/resources/dragdrop.rst

@ -4,4 +4,205 @@
Drag and Drop
=============
TBD
Drag and drop provides a simple visual mechanism which users can use to transfer
information between and within widgets.
Drag and drop is similar in function to the clipboard's cut and paste mechanism.
.. image:: https://github.com/user-attachments/assets/857fd144-7a2a-4173-80b3-d135e62b8235
This document describes the basic drag and drop mechanism and outlines the
approach used to enable it in custom controls.
Drag and drop operations are also supported by many of TermTk's controls,
such as :py:class:`TTkList` or :py:class:`TTkTabWidget`.
---------------------
Drag and Drop Classes
---------------------
These classes deal with drag and drop and the necessary mime type encoding and decoding.
.. currentmodule:: TermTk
.. autosummary::
:caption: Classes:
:template: custom-class-template.01.rst
TTkGui.TTkDrag
TTkGui.TTkDropEvent
--------
Dragging
--------
To start a drag, create a :py:class:`TTkDrag` object, and call its :py:meth:`TTkDrag.exec` function.
In most applications, it is a good idea to begin a drag and drop operation only
after a mouse button has been pressed and the cursor has been moved a certain distance.
However, the simplest way to enable dragging from a widget is to reimplement
the widget's :py:meth:`TTkWidget.mouseDragEvent` and start a drag and drop operation:
.. code:: python
def mouseDragEvent(self, evt:ttk.TTkMouseEvent) -> bool:
if evt.key == ttk.TTkMouseEvent.LeftButton:
drag = ttk.TTkDrag()
drag.setData("LeftClick Drag")
drag.exec()
return True
Note that the :py:meth:`TTkDrag.exec` function does not block the main event loop.
.. seealso::
* `tutorial/examples/DragAndDrop/dnd.01.basic.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.01.basic.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.01.basic.py>`__)
--------
Dropping
--------
To be able to receive the content dropped on a widget, reimplement
the :py:meth:`TDragEvents.dropEvent` event handler functions.
.. code:: python
def dropEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drop data: {evt.data()}, Position: {evt.pos()}")
return True
.. seealso::
* `tutorial/examples/DragAndDrop/dnd.01.basic.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.01.basic.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.01.basic.py>`__)
------
Events
------
There are several events that can be used to customize the drag and drop operation:
* :py:meth:`TDragEvents.dropEvent` - Called when a drag is dropped on the widget.
* :py:meth:`TDragEvents.dragEnterEvent` - Called when a drag enters the widget.
* :py:meth:`TDragEvents.dragMoveEvent` - Called when a drag moves over the widget.
* :py:meth:`TDragEvents.dragLeaveEvent` - Called when a drag leaves the widget if :py:meth:`TDragEvents.dragEnterEvent` or :py:meth:`TDragEvents.dragMoveEvent` are andled inside the widget.
.. code:: python
def dragEnterEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Enter: {evt.data()}, Position: {evt.pos()}")
return True
def dragLeaveEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Leave: {evt.data()}, Position: {evt.pos()}")
return True
def dragMoveEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Move: {evt.data()}, Position: {evt.pos()}")
return True
def dropEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drop: {evt.data()}, Position: {evt.pos()}")
return True
.. seealso::
* `tutorial/examples/DragAndDrop/dnd.02.events.01.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.02.events.01.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.02.events.01.py>`__)
* `tutorial/examples/DragAndDrop/dnd.02.events.02.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.02.events.02.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.02.events.02.py>`__)
------
Pixmap
------
The visual representation of the drag can be customized by setting a pixmap with :py:meth:`TTkDrag.setPixmap`.
By default the pixmap is initialized as a simple text string ("[...]")
but it can be customized by using
a :py:class:`TTkWidget` or :py:class:`TTkCanvas` as a pixmap.
.. image:: https://github.com/user-attachments/assets/7a23f5a9-444b-4e5a-878b-91c4b35ee8d8
You can use the same object as pixmap to have a visual feedback of the widget being dragged:
.. code:: python
def mouseDragEvent(self, evt:ttk.TTkMouseEvent) -> bool:
drag = ttk.TTkDrag()
drag.setPixmap(self)
drag.exec()
return True
Or define another :py:class:`TTkWidget` as pixmap:
.. code:: python
def mouseDragEvent(self, evt:ttk.TTkMouseEvent) -> bool:
button = ttk.TTkButton(text=f"DnD", border=True, size=(25,5))
drag = ttk.TTkDrag()
drag.setPixmap(button)
drag.exec()
return True
Or use a :py:class:`TTkCanvas` as pixmap and draw the required content on it:
.. code:: python
def mouseDragEvent(self, evt:ttk.TTkMouseEvent) -> bool:
pixmap = ttk.TTkCanvas(width=17,height=5)
pixmap.drawText(pos=(0,0),text="╭╼ TXT ╾────────╮")
pixmap.drawText(pos=(0,1),text="│Lorem ipsum dol│")
pixmap.drawText(pos=(0,2),text="│consectetur adi│")
pixmap.drawText(pos=(0,3),text="│sed do eiusmod │")
pixmap.drawText(pos=(0,4),text="╰────────╼ End ╾╯")
drag = ttk.TTkDrag()
drag.setPixmap(pixmap)
drag.exec()
return True
.. seealso::
* `tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py>`__)
* `tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py>`__)
* `tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py>`__)
* `tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py>`__)
-------
HotSpot
-------
The hotspot is the offset of the pixmap related to the cursor position.
It can be set using :py:meth:`TTkDrag.setHotSpot`.
It is useful when the pixmap is not centered on the cursor or when you want to define an offset to allow
the object being dragged from the clicked position:
.. image:: https://github.com/user-attachments/assets/8d999365-c787-4eff-84f2-03ef2b22c37a
.. code:: python
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
drag = ttk.TTkDrag()
drag.setHotSpot((evt.x, evt.y))
drag.setPixmap(self)
drag.exec()
return True
.. seealso::
* `tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py>`__)
--------
Examples
--------
* `tutorial/examples/DragAndDrop/dnd.01.basic.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.01.basic.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.01.basic.py>`__)
* `tutorial/examples/DragAndDrop/dnd.02.events.01.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.02.events.01.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.02.events.01.py>`__)
* `tutorial/examples/DragAndDrop/dnd.02.events.02.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.02.events.02.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.02.events.02.py>`__)
* `tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py>`__)
* `tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py>`__)
* `tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py>`__)
* `tutorial/examples/DragAndDrop/dnd.04.pixmap.03.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py>`__)
* `tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py>`__ (`tryItOnline <https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html?filePath=tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py>`__)

17
tests/ansi.images.json

@ -1,17 +0,0 @@
{
"compressed":{
"Note1:" : "HEX Codes thanks to: https://github.com/ceccopierangiolieugenio/pyTermTk/tree/main/tools/dumbPaintTool",
"pepper" : "eJx9Vk1yFjsMfAtW4QhsOMEr/49dughVnCF3CFUsWGTBAsJ7QOBunIRuyZ6xHUgg7u/zKJLVkltz9+Lzh5f/6M+bd/jw6q27fY0foMQqQZKX7KXd/np4PPeOKs1L9Nj8fm0GqdE2/6eXm+7qNAhFvMcKg/eX+yK5SYyS+C0mCTBco5Um9ZAczIR/gAcZJnenifdJPP64dqPipTjJaTGqTbyLPKc5apIznsDmv9kGmwinJiHzZP5YTuxDkiMTlu2MJWFdNh3/YefhZttSbi4SsDissPx5xTmQU6iKaSRZEmGzw288FLsd8qBdgN23xY4UDLOGJB1WGH29jFBCH52iOcN2LYQlaIhZgneKRhWJQlMA18o0nCRFRXNYcFRXib1ThiGZLopmGJrQduuGED1OlxTNDt3oXSIukZv2bltJGAW5e74g7KUcFSwGvory/jgdOAvP3DoDiBeDxZvSP5w2y+F60+FzxQqj+8koamGPM3N8YPFJ5ccrc7Lms6LZoZwHa9WWmMGDtdgUrTbO874CNjswGbxiGhmRzLZdLlSlFAW7XJWXtNQ1BzQdYhIsJgQBS7fpYvCkDBeboK5ySfPjj5N7VLwpqMWBFJE8W+jLZYTbHQ4FNYKwgK5Mdn5cRlAz9IWCkR30kutZp4CV+qHQ21bbCbByAzKgSArWKIkPAFuFHQXsbAP0DcSixfX46GieZDR2pshg3TxJV6SHSWSn7HTZuAOzTsHqh7RQ0bQaoX2LgSWSD6NpozhqBh1IuVNdd27VpFasRGV0aqLeAlZvtQriEDp7qEUhrDnjxLgbhF4yHQeANSiCZGexnvEGZUPTD32jZktIS58+aVONMXXmRDhGBb2dNw3VCVvrcqYcCmk0IFsk+81MJcSPEeYxwyobbgzUIQLoClcUu52zDnSbWEQrzrgMHkZJYWEjOHbGoXiJRehiMdXARrAfmdIdh87mrlJmuPbhocvz5D7+nVyct3ExZ3i6UVai3cmRIl8vqJGbWWuC/4Quc07vQFsVKAS2e1Q8mc3hKbPUIeYZJn8ckervfmluzrOzuSGtnCnbmOI8TB2suVUHAYu3whcBGTKDL1IXWl8/FdcpRrJl4XGaZBgowaayCQ/nPdaNbMf3BYVTN13XzXmogJ3UFNMoEpWsbMMs0bYqWt95OivE5Y3MQ8UZNw9/uLF8BQjry51qf760Hy+NFLm2TH/y7M6GSiAibSTOUno27xWFF43XbdkcpdnfPqjwGzlVkGD9U2363u3Np39/A7RA/Jk=" ,
"python" : "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" ,
"Note2:" : "Icons from: https://valletyh.itch.io/icon-pack",
"fire" : "eJytlUsOgyAQhrvoovEKbjhBg/iohoO0iUlv4B1c9AhtD9iTFMHwGAbBh9HEDP8Mw8cMjOfP83KSz+MufvKeDnlftpzxomOc8rIZft+XMPPKMRICZGqc1TWvK/EKrxHzIimO+p0iYMkMGUzyuAxTvAKKglFOLYW2WZJW/DVQI43Hx2E180WMdu5kKZGWiM37sZmjtq0CoJaRDMDUExwkNhtpsiXygwShKiUVbEm3NrSlKyEnIoRbj9dHmIFPUA0Rl6H6Vs5iMfSZkH3Nsr56Z4Nxti1zro5J1xICZ7HCsNrZ26IYB+sEDTSeq8AORpxUPFYw56kBYmvH6hhANHwiuoRYGEaYE4o6YTtMv8dY47Cx63DpDlvYFP9IdGffWCBoekP2vv4BVO4f6w==" ,
"fireMini" : "eJylk01OAzEMhVl0xRW4A4rtJJPIF0HiDL1DFyxZsIBShLgPO+7ASXjOJPPTCXRBq1Zp/Y3t9xwfdi9fu6vyuvvE4ebe7fFRScpKQ1IcRb39ijjT/vv4OsXZeXxLHuPZGzCAeJgJIiWnnisSS9Y14oIlGUYiBUvi1wQ6GEQ5VEQMCWskBNSRqddcmLRlKKpvjImjvGL6io/myvWf0BwspSWOQWdvZHhaiIlFbAUoGyJhzVinaI+rKczwD1Zv9XDwcIsqFgSPBRW3nlLhEGqDGtNHzZ10iDSfyx/4IUO1YNEcSrKfMWjmCOrtgg2PZ0Ze8spKJZRytZQpwDlvqCZpbhxXjn2XMy9mLpAZA/BwBqK9QAuQ0ujEaTHKhAsFLrUxkY2WzIn3RTZSDJkb5PCQ11Ly9H+7frmUoWb4WGxitpFJtTJlu7a00U220m2hyy5Khxmm6XcZ6wJ12r6WPXNniC9C2ipypxtXNqOuQKYt0Zd8KqY93/4AlgU7wQ==",
"key" : "eJzFVLsRwjAMpYCGFWgyAedPPua0CHfM4B1SMAKwHF0mQYkTHDuW45gCXwpLeZKefm73z+6wG871jZfTjenCHLyCVCBAVBXwukZBd697QA3lJAvBQDGEtfi5yEsKklcKOEOEGEPpo2UUjWz869GZH8Y39sh8s7PGXHBQDchGz0x5jfoGFhAqqvVm8yqmrBZ/qEzJymVk6hon9DHHm1GPsKlmbnW31CNpEDOY9bIsIYDqtQT9EMgZBAe3MjBB0HwFiM1oZ5sRKMOGNU2vjqelt4Lg7+Vu6NOL/7fWJrUs0v/ojOv4+7Li9rdxeZw/gO97Ug==",
"sword" : "eJzFU8kNwjAQ5AEfWuCTCpDXcXxoG0GihvTAgxKAAvnRBevElo/YJogHUVaKNpPZmfHmsr2/dpvpOj3p4XBmIxUKjRxhkAiKIWgYO9vts+4M48Pga3w+LgHIFAJQCcfHe6AioFA1IPWvVsPeC5kBxACCxsp0QkToFNeAQWJbyoKxZc70JNsRgjYIxiBnsoJLrHXdH8xFGj8fyE/mVthbiEm3oJVDTc1iMdfai/a8ac/PISiIKG9lX4DKNwTQkB2jyqiQVZxWJYSFtwzY/u9qO5wbCZLTE/DKAx1I4s5TjT6+Vg1+npml1fh2ZfBlvZwiknRzp0MOKA3KbKd812+UXT1mI05hXCNNKzPFOaRnXUzbcZVlfOUg63aTltvxDd6kgOU=",
"ring" : "eJylVcFtwzAM7KOPIiv0owkKU7JqCXxkjQCdwTvkkRHaDphJoji2LJGUrMQBHATkmeSdTsz5/e/48TZ9Ti78+PzpRqXCNxqHGjsEY1BDN17/L0k0RBC8y6LgBgTwaAF7ijpXUGuHMkobGyoBQjfUcdog2PD4Auw+8WPo8TBx5VRr08fo3M7a5clgSVxg+ULBpuG07YNQHp0WRcuga2ppvLy78xzGVN17vid5VeIuO0GQUj6YTCCi40sFG4bLaCfh3mCoDkB7D4DfAzo/i5LC1vmihR+o5Z3qudBSLfJzD8iXh0krOoUZuQnV1JGpHDaUSpbUhqq5iqokyiQZy8QdsUtHKslaMb1QSZ/IQXG6MSn7Yz9dteVvxrjg5Po+enqJ57xLd49uIn4lL2KiWc2mYeZ/F7UtZkG7RonJspDNL9qW3JpnhhPY/n7dAHoxGOQ=",
"diamond" : "eJyVlU1uhDAMhbuoVKlX6MbddVXFhDAgX6RSz8AdWPQIbQ/YkzQwgbwkjgIjZpgxH/55Np7l8eft6WF7fbz6Ly+fZiYifxI7SifWCU9GOufmv98vxSz9+pvdJF0/BG45OGeF+SadHe7cekN4rxyi/oON92JvKnpwRoZeeHRXKGafr2OF6tlfHENt8/NWvpJSUnxeVK14sFfTKIqgaDXWx5juVmgJeClBdIvhottdGahzlyEDmkodPkEoul5+kadeZk1mUC9kgzMLXJSpNoxJ9wqZ1dnNNCUUOkhGu2CZvSHf2foLneN8JjKDOSYJWpFKQkIDb0m14gDXClSg2rMLjK75JcVrj3d9Z53F9KkrulPhGkslH868F6VXVfWzN+scLAF1Myxxh1JT5rTLKYTHmcWVx25Hz/9tcOlpWR4Rvt//AVu00Dg=",
"peach" : "eJytVEtuhTAM7KKL6l2hm6i7bioCIR/5Ik96Z+AOLHqEtgfsSRogIYmxXfpUhAQME8ceTzw/fr4+PazX9SW+PN+6SSkVHzB46KEfR9BmAD3q6fvrvcW1BeMXuP6jgwcLFsNLDD9C10bpYnADXQyTYvbxU7shsmaGpXsLvTYQgkCqdpt5lvMQHLgxBZouv/FWdc5ps+GlMO0CeJTyDpP149R3xeWgrA5NpLQPJymGS9EFFHbIICnzRqG0n+6UNvuwpF6Joxow65s8TpIp0xkXXw92ykUcSEWuDUuE/WhUIqdFCi9JdRdhDBZGid6wy330mywe68vSDNKXZ1uywX9NPOmWKEPsYIh3m3NGBaUTpkjtKQNWiWEPo2zxxORKouVbf/9XdZWv91hSyXzm5LnF+TbNFnJmLMkWxnburCNaCnsg2FokyY5jQpptjC+olfe04AyHGk14L2F8lYZMl4+3HxigIEs=",
"whip" : "eJylU8ERwiAQ9KGftOCHChyPhEDmnv6swBlroAcelqAWaCViQHIQCBNlyCRzt7e3CxezfZx3m3FdTvZjfz1qxph9YauQIwdAUC2CEPr1vMVxYeOdnMcD3hTwhqZADQjQ2Qd0IY7dGOglysHuEqyiUDfOXU6s65Co9IfBSpyelgCqEtc5GRXPk2nTn+nz1jxJ/VrieNp0xsLS4aoKz/tPsTPavy6BRG+ls4tStCAMzXQAIV0WSG2mrvyaALxDwe0m7dxcB0g/oBIoVORgEFZ8iyr+Sb5Qr/XjsAeUMjqSUJpDLQCJDpPVb0K9s1CQRaUToiBjqZuQdrIdeAUq3zFbm0J1cz+8AbE/aBQ="
}
}

1
tests/ansi.images.json

@ -0,0 +1 @@
../tutorial/examples/ansi.images.json

2
tests/sandbox/Makefile

@ -72,7 +72,7 @@ buildSandbox: www
$( cd ../../ ; tools/prepareBuild.sh release ; )
find ../../tmp/TermTk/ -name "*.py" | sed 's,.*tmp/,,' | sort | xargs tar cvzf bin/TermTk.tgz -C ../../tmp
find ../../tutorial -name "*.py" | sort | xargs tar cvzf bin/tutorial.tgz
find ../../tutorial -name '*.py' -o -name '*.json' | sort | xargs tar cvzf bin/tutorial.tgz
find ../../demo/paint.py ../../demo/ttkode.py ../../demo/demo.py ../../demo/showcase/*.* | sort | xargs tar cvzf bin/demo.tgz
find ../../tests/ansi.images.json ../../tests/t.ui/*.* | sort | xargs tar cvzf bin/tests.tgz

84
tutorial/examples/DragAndDrop/dnd.01.basic.py

@ -0,0 +1,84 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 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.
# Those 2 lines are required to use the TermTk library in the main folder
import sys, os
sys.path.append(os.path.join(sys.path[0],'../../..'))
import TermTk as ttk
# This example show the basic Drag and Drop functionality;
#
# Each TTkWidget include 4 methods to handle the Drag and Drop events:
# - dragEnterEvent
# - dragLeaveEvent
# - dragMoveEvent
# - dropEvent
# Overriding any of those methods in a subclass will allow the widget to handle the DnD events
#
# To start a Drag and Drop operation, the TTkDrag object must be created and executed.
# The Drag and Drop operation is usually started after a mouseDragEvent as shown in
# this example, but it can be started after any other events/methods or signals.
class DragDrop(ttk.TTkFrame):
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
if evt.key == ttk. TTkMouseEvent.LeftButton:
# Create a new drag object and set some text as DnD Data
drag = ttk.TTkDrag()
drag.setData(f"Test DnD ({self.title()})")
ttk.TTkLog.debug(f"Drag ({self.title()}) -> {drag.data()}, pos={evt.x},{evt.y}")
# Start the drag operation
drag.exec()
return True
def dropEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drop ({self.title()}) <- {evt.data()}, pos={evt.x},{evt.y}")
return True
# Create the root application
# and set its layout to TTkGridLayout in order to
# place the widgets in the following way:
#
# Col 0 Col 1
# +----------------+----------------+
# Row 0 | DragDrop 1 | DragDrop 2 |
# + +----------------+
# Row 1 | | DragDrop 3 |
# +----------------+----------------+
# Row 2 | Log Viewer |
# +----------------+----------------+
#
root = ttk.TTk()
root.setLayout(ttk.TTkGridLayout())
# Add the DragDrop widgets to the root layout
root.layout().addWidget(DragDrop(title="DnD 1"),0,0,2,1)
root.layout().addWidget(DragDrop(title="DnD 2"),0,1,1,1)
root.layout().addWidget(DragDrop(title="DnD 3"),1,1,1,1)
# Add a LogViewer at the bottom to display the log messages
# (Row 2, Col 0, RowSpan 1, ColSpan 2)
root.layout().addWidget(ttk.TTkLogViewer(follow=True),2,0,1,2)
root.mainloop()

96
tutorial/examples/DragAndDrop/dnd.02.events.01.py

@ -0,0 +1,96 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 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.
# Those 2 lines are required to use the TermTk library in the main folder
import sys, os
sys.path.append(os.path.join(sys.path[0],'../../..'))
import TermTk as ttk
# This example show the basic Drag and Drop functionality;
#
# Each TTkWidget include 4 methods to handle the Drag and Drop events:
# - dragEnterEvent
# - dragLeaveEvent
# - dragMoveEvent
# - dropEvent
# Overriding any of those methods in a subclass will allow the widget to handle the DnD events
#
# To start a Drag and Drop operation, the TTkDrag object must be created and executed.
# The Drag and Drop operation is usually started after a mouseDragEvent as shown in
# this example, but it can be started after any other events/methods or signals.
class DragDrop(ttk.TTkFrame):
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
if evt.key == ttk. TTkMouseEvent.LeftButton:
# Create a new drag object and set some text as DnD Data
drag = ttk.TTkDrag()
drag.setData(f"Test DnD ({self.title()})")
ttk.TTkLog.debug(f"Drag ({self.title()}) -> {drag.data()}, pos={evt.pos()}")
# Start the drag operation
drag.exec()
return True
def dragEnterEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Enter ({self.title()}) - {evt.data()}, pos={evt.pos()}")
return True
def dragLeaveEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Leave ({self.title()}) - {evt.data()}, pos={evt.pos()}")
return True
def dragMoveEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Move ({self.title()}) - {evt.data()}, pos={evt.pos()}")
return True
def dropEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drop ({self.title()}) <- {evt.data()}, pos={evt.pos()}")
return True
# Create the root application
# and set its layout to TTkGridLayout in order to
# place the widgets in the following way:
#
# Col 0 Col 1
# +----------------+----------------+
# Row 0 | DragDrop 1 | DragDrop 2 |
# + +----------------+
# Row 1 | | DragDrop 3 |
# +----------------+----------------+
# Row 2 | Log Viewer |
# +----------------+----------------+
#
root = ttk.TTk()
root.setLayout(ttk.TTkGridLayout())
# Add the DragDrop widgets to the root layout
root.layout().addWidget(DragDrop(title="DnD 1"),0,0,2,1)
root.layout().addWidget(DragDrop(title="DnD 2"),0,1,1,1)
root.layout().addWidget(DragDrop(title="DnD 3"),1,1,1,1)
# Add a LogViewer at the bottom to display the log messages
# (Row 2, Col 0, RowSpan 1, ColSpan 2)
root.layout().addWidget(ttk.TTkLogViewer(follow=True),2,0,1,2)
root.mainloop()

135
tutorial/examples/DragAndDrop/dnd.02.events.02.py

@ -0,0 +1,135 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 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.
# Those 2 lines are required to use the TermTk library in the main folder
import sys, os
sys.path.append(os.path.join(sys.path[0],'../../..'))
import TermTk as ttk
# This example show the basic Drag and Drop functionality;
#
# Each TTkWidget include 4 methods to handle the Drag and Drop events:
# - dragEnterEvent
# - dragLeaveEvent
# - dragMoveEvent
# - dropEvent
# Overriding any of those methods in a subclass will allow the widget to handle the DnD events
#
# To start a Drag and Drop operation, the TTkDrag object must be created and executed.
# The Drag and Drop operation is usually started after a mouseDragEvent as shown in
# this example, but it can be started after any other events/methods or signals.
#
# Here I am exploring the different interactions between the Drag and Drop events
# In particular I am testing the dragLeaveEvent whch is triggered only if the
# dragMoveEvent or dragEnterEvent has been handled (returned True) before.
class DragDrop(ttk.TTkFrame):
# Basic Drag and Drop widget
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
if evt.key == ttk. TTkMouseEvent.LeftButton:
# Create a new drag object and set some text as DnD Data
drag = ttk.TTkDrag()
drag.setData(f"Test DnD ({self.title()})")
ttk.TTkLog.debug(f"Drag ({self.title()}) -> {drag.data()}, pos={evt.pos()}")
# Start the drag operation
drag.exec()
return True
def dropEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drop ({self.title()}) <- {evt.data()}, pos={evt.pos()}")
return True
class DragDropMove(DragDrop):
# Drag and Drop widget that handles only the dragMoveEvent
def dragMoveEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Move ({self.title()}) - {evt.data()}, pos={evt.pos()}")
return True
class DragDropEnter(DragDrop):
# Drag and Drop widget that handles only the dragEnterEvent
def dragEnterEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Enter ({self.title()}) - {evt.data()}, pos={evt.pos()}")
return True
class DragDropLeave1(DragDrop):
# Drag and Drop widget that handles the dragEnterEvent and dragLeaveEvent
def dragEnterEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Enter ({self.title()}) - {evt.data()}, pos={evt.pos()}")
return True
def dragLeaveEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Leave ({self.title()}) - {evt.data()}, pos={evt.pos()}")
return True
class DragDropLeave2(DragDrop):
# Drag and Drop widget that handles the dragMoveEvent and dragLeaveEvent
def dragMoveEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Move ({self.title()}) - {evt.data()}, pos={evt.pos()}")
return True
def dragLeaveEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Leave ({self.title()}) - {evt.data()}, pos={evt.pos()}")
return True
class DragDropLeave3(DragDrop):
# Drag and Drop widget that handles only the dragLeaveEvent
# NOTE:
# This widget will never receive the dragLeaveEvent because
# neither the dragMoveEvent or dragEnterEvent are handled
def dragLeaveEvent(self, evt:ttk.TTkDnDEvent) -> bool:
ttk.TTkLog.debug(f"Drag Leave ({self.title()}) - {evt.data()}, pos={evt.pos()}")
return True
# Create the root application
# and set its layout to TTkGridLayout in order to
# place the widgets in the following way:
#
# Col 0 Col 1 Col 2
# +----------------+----------------+-----------------+
# Row 0 | DnD Move | DnD Enter |
# +----------------+----------------+-----------------+
# Row 1 | DnD Move,Leave | DnD only Leave | DnD Enter,Leave |
# +----------------+----------------+-----------------+
# Row 2 | Log Viewer |
# +----------------+----------------+-----------------+
#
root = ttk.TTk()
root.setLayout(ttk.TTkGridLayout())
# Add the DragDrop widgets to the root layout
root.layout().addWidget(DragDropMove( title="DnD Move"), 0,0)
root.layout().addWidget(DragDropEnter( title="DnD Enter"), 0,1,1,2)
root.layout().addWidget(DragDropLeave2(title="DnD Move,Leave"), 1,0)
root.layout().addWidget(DragDropLeave1(title="DnD Enter,Leave"), 1,2)
root.layout().addWidget(DragDropLeave3(title="DnD only Leave"), 1,1)
# Add a LogViewer at the bottom to display the log messages
# (Row 2, Col 0, RowSpan 1, ColSpan 2)
root.layout().addWidget(ttk.TTkLogViewer(follow=True),2,0,1,3)
root.mainloop()

87
tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py

@ -0,0 +1,87 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 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.
# Those 2 lines are required to use the TermTk library in the main folder
import sys, os
sys.path.append(os.path.join(sys.path[0],'../../..'))
import TermTk as ttk
# This example show the basic Drag and Drop pixmap usage;
#
# Anytime a Drag and Drop operation is started, a new TTkButton is created
# and used as DnD Data and Pixmap.
# The same data object is added to the frame in the dropEvent and moved to the mouse coordinates.
class DragDrop(ttk.TTkFrame):
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
if evt.key == ttk. TTkMouseEvent.LeftButton:
# Create a new drag object and
# a new TTkButton as DnD Data and Pixmap
# the default TTkButton canvas will be used as Pixmap
button = ttk.TTkButton(text=f"Test DnD ({self.title()})", border=True, size=(20,3))
drag = ttk.TTkDrag()
drag.setData(button)
drag.setPixmap(button)
ttk.TTkLog.debug(f"Drag ({self.title()}) -> {button.text()}, pos={evt.x},{evt.y}")
# Start the drag operation
drag.exec()
return True
def dropEvent(self, evt:ttk.TTkDnDEvent) -> bool:
button:ttk.TTkButton = evt.data()
self.layout().addWidget(button)
# Since the frame by default has a padding of 1
# I align the button to the mouse coordinates by subtracting the Top/Left padding size
t,b,l,r = self.getPadding()
button.move(evt.x-l, evt.y-t)
ttk.TTkLog.debug(f"Drop ({self.title()}) <- {button.text()}, pos={evt.x},{evt.y}")
return True
# Create the root application
# and set its layout to TTkGridLayout in order to
# place the widgets in the following way:
#
# Col 0 Col 1
# +----------------+----------------+
# Row 0 | DragDrop 1 | DragDrop 2 |
# + +----------------+
# Row 1 | | DragDrop 3 |
# +----------------+----------------+
# Row 2 | Log Viewer |
# +----------------+----------------+
#
root = ttk.TTk()
root.setLayout(ttk.TTkGridLayout())
# Add the DragDrop widgets to the root layout
root.layout().addWidget(DragDrop(title="DnD 1"),0,0,2,1)
root.layout().addWidget(DragDrop(title="DnD 2"),0,1,1,1)
root.layout().addWidget(DragDrop(title="DnD 3"),1,1,1,1)
# Add a LogViewer at the bottom to display the log messages
# (Row 2, Col 0, RowSpan 1, ColSpan 2)
root.layout().addWidget(ttk.TTkLogViewer(follow=True),2,0,1,2)
root.mainloop()

110
tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py

@ -0,0 +1,110 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 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.
# Those 2 lines are required to use the TermTk library in the main folder
import sys, os
sys.path.append(os.path.join(sys.path[0],'../../..'))
import TermTk as ttk
# This example show more advance Drag and Drop pixmap usage;
#
# When the Drag and Drop operation is started, a TTkLabel widget is created
# but a new canvas is built to be used as pixmap.
# This approach increase the flexibility on the styling of the full drag and drop operation
class DragDrop(ttk.TTkFrame):
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
# Create a new drag object, a new TTkLabel as DnD Data and
# a custom Pixmap drawn as a titled box of fixed sizes around
# a snippet of the label's text
label = ttk.TTkLabel(text="Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut\nlabore et dolore magna aliqua.", size=(10,1))
pixmap = ttk.TTkCanvas(width=17,height=5)
pixmap.drawText(pos=(0,0),text="╭───────────────╮")
pixmap.drawText(pos=(2,0),text=f"{self.title()}") # Here for simplicity I am writing the title over the top border
pixmap.drawText(pos=(0,1),text="│Lorem ipsum dol│")
pixmap.drawText(pos=(0,2),text="│consectetur adi│")
pixmap.drawText(pos=(0,3),text="│sed do eiusmod │")
pixmap.drawText(pos=(0,4),text="╰───────────────╯")
# The next condition is meant to show that you can
# handle also the Drag and Drop with the Right or Middle mouse buttons.
if evt.key == ttk. TTkMouseEvent.LeftButton:
pixmap.drawText(pos=(0,4),text="╰───────╼ Left ╾╯")
elif evt.key == ttk. TTkMouseEvent.RightButton:
pixmap.drawText(pos=(0,4),text="╰──────╼ Right ╾╯")
elif evt.key == ttk. TTkMouseEvent.MidButton:
pixmap.drawText(pos=(0,4),text="╰────╼ Eugenio ╾╯")
drag = ttk.TTkDrag()
drag.setData(label)
drag.setPixmap(pixmap)
ttk.TTkLog.debug(f"Drag ({self.title()}) -> pos={evt.x},{evt.y}")
# Start the drag operation
drag.exec()
return True
def dropEvent(self, evt:ttk.TTkDnDEvent) -> bool:
# Similar to the previous example
# I am retrieving the TTkLabel widget used as Drag'nDrop data
# and I am placing it inside the current Frame
# This time I am not removing the padding sizes from the
# position due to the frame I draw in the pixmap that
# already changed the offset of the text being aligned to the final
# dropped Label position.
# BTW, I am not a genious that can figure out all of this upfront,
# this is just the result of trial and errors
label:ttk.TTkLabel = evt.data()
self.layout().addWidget(label)
label.move(evt.x, evt.y)
ttk.TTkLog.debug(f"Drop ({self.title()}) <- pos={evt.x},{evt.y}")
return True
# Create the root application
# and set its layout to TTkGridLayout in order to
# place the widgets in the following way:
#
# Col 0 Col 1
# +----------------+----------------+
# Row 0 | DragDrop 1 | DragDrop 2 |
# + +----------------+
# Row 1 | | DragDrop 3 |
# +----------------+----------------+
# Row 2 | Log Viewer |
# +----------------+----------------+
#
root = ttk.TTk()
root.setLayout(ttk.TTkGridLayout())
# Add the DragDrop widgets to the root layout
root.layout().addWidget(DragDrop(title="DnD 1"),0,0,2,1)
root.layout().addWidget(DragDrop(title="DnD 2"),0,1,1,1)
root.layout().addWidget(DragDrop(title="DnD 3"),1,1,1,1)
# Add a LogViewer at the bottom to display the log messages
# (Row 2, Col 0, RowSpan 1, ColSpan 2)
root.layout().addWidget(ttk.TTkLogViewer(follow=True),2,0,1,2)
root.mainloop()

134
tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py

@ -0,0 +1,134 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 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.
import json
import random
# Those 2 lines are required to use the TermTk library in the main folder
import sys, os
sys.path.append(os.path.join(sys.path[0],'../../..'))
import TermTk as ttk
# Load the images from the ansi.images.json file
# Each entry is a compressed base64 encoded image as a multiline TTkString
imagesFile = os.path.join(os.path.dirname(os.path.abspath(__file__)),'../ansi.images.json')
with open(imagesFile) as f:
d = json.load(f)
# Image exported by the Dumb Paint Tool - Removing the extra '\n' at the end
diamond = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['diamond' ])[0:-1])
fire = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['fire' ])[0:-1])
key = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['key' ])[0:-1])
peach = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['peach' ])[0:-1])
pepper = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['pepper' ])[0:-1])
python = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['python' ])[0:-1])
ring = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['ring' ])[0:-1])
sword = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['sword' ])[0:-1])
whip = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['whip' ])[0:-1])
# Calculate the size of the image, this is a simple helper function.
# Since the images are simple ANSI strings,
# the size is calculated by counting the lines and the max width of the lines
def imageSize(img:ttk.TTkString) -> int:
lines = img.split('\n')
return (
max(line.termWidth() for line in lines),
len(lines))
# This example show a showcase of the Drag and Drop pixmap functionality;
#
# Anytime a Drag and Drop operation is started, a random image is selected and used as Pixmap.
#
# In order to display the images after the drop event, the pixmaps are stored in a list
# and the paintEvent routine is used to draw them on the canvas.
class DragDrop(ttk.TTkFrame):
def __init__(self, **kwargs):
# The list of pixmaps to be drawn in the paintEvent routine
self.pixmaps = []
super().__init__(**kwargs)
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
if evt.key == ttk. TTkMouseEvent.LeftButton:
# Create a new drag object and
# a random image is chosen
imageString = random.choice([diamond,fire,key,peach,ring,sword,whip,pepper,python])
# A canvas is created and the ANSI String is drawn on it line by line
w,h = imageSize(imageString)
pixmap = ttk.TTkCanvas(width=w, height=h+1)
pixmap.setTransparent(True)
pixmap.drawText(pos=(0,0), text=self.title())
for y,line in enumerate(imageString.split('\n')):
pixmap.drawTTkString(pos=(0,y+1), text=line)
drag = ttk.TTkDrag()
drag.setData(pixmap)
drag.setPixmap(pixmap)
ttk.TTkLog.debug(f"Drag ({self.title()}) -> pos={evt.x},{evt.y}")
# Start the drag operation
drag.exec()
return True
def dropEvent(self, evt:ttk.TTkDnDEvent) -> bool:
# When a drop event is received, the pixmap is stored in the list
self.pixmaps.append((evt.x, evt.y, evt.data()))
ttk.TTkLog.debug(f"Drop ({self.title()}) <- pos={evt.x},{evt.y}")
self.update()
return True
def paintEvent(self, canvas:ttk.TTkCanvas) -> None:
_,_,w,h = self.geometry()
# Draw all the pixmaps on the canvas
for x,y,pixmap in self.pixmaps:
canvas.paintCanvas(pixmap, (x,y,w,h), (0,0,w,h), (0,0,w,h))
# Call the base paintEvent to draw the frame on top of the pixmap
super().paintEvent(canvas)
# Create the root application
# and set its layout to TTkGridLayout in order to
# place the widgets in the following way:
#
# Col 0 Col 1
# +----------------+----------------+
# Row 0 | DragDrop 1 | DragDrop 2 |
# + +----------------+
# Row 1 | | DragDrop 3 |
# +----------------+----------------+
# Row 2 | Log Viewer |
# +----------------+----------------+
#
root = ttk.TTk()
root.setLayout(ttk.TTkGridLayout())
# Add the DragDrop widgets to the root layout
root.layout().addWidget(DragDrop(title="DnD 1"),0,0,2,1)
root.layout().addWidget(DragDrop(title="DnD 2"),0,1,1,1)
root.layout().addWidget(DragDrop(title="DnD 3"),1,1,1,1)
# Add a LogViewer at the bottom to display the log messages
# (Row 2, Col 0, RowSpan 1, ColSpan 2)
root.layout().addWidget(ttk.TTkLogViewer(follow=True),2,0,1,2)
root.mainloop()

119
tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py

@ -0,0 +1,119 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 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.
import json
import random
# Those 2 lines are required to use the TermTk library in the main folder
import sys, os
sys.path.append(os.path.join(sys.path[0],'../../..'))
import TermTk as ttk
# This example is another showcase of the Drag Pixmap;
# It is basically a lazy collection of the previous examples
# No drop routine is implemented in this example.
imagesFile = os.path.join(os.path.dirname(os.path.abspath(__file__)),'../ansi.images.json')
with open(imagesFile) as f:
d = json.load(f)
# Image exported by the Dumb Paint Tool - Removing the extra '\n' at the end
diamond = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['diamond' ])[0:-1])
fire = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['fire' ])[0:-1])
key = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['key' ])[0:-1])
peach = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['peach' ])[0:-1])
pepper = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['pepper' ])[0:-1])
python = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['python' ])[0:-1])
ring = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['ring' ])[0:-1])
sword = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['sword' ])[0:-1])
whip = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['whip' ])[0:-1])
def imageSize(img:ttk.TTkString) -> int:
lines = img.split('\n')
return (
max(line.termWidth() for line in lines),
len(lines))
class DragDropBase(ttk.TTkFrame):
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
drag = ttk.TTkDrag()
drag.exec()
return True
class DragDropWidget(ttk.TTkFrame):
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
button = ttk.TTkButton(text=f"DnD: ({self.title()})", border=True, size=(25,5))
drag = ttk.TTkDrag()
drag.setPixmap(button)
drag.exec()
return True
class DragDropTxt(ttk.TTkFrame):
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
pixmap = ttk.TTkCanvas(width=17,height=5)
pixmap.drawText(pos=(0,0),text="╭╼ TXT ╾────────╮")
pixmap.drawText(pos=(0,1),text="│Lorem ipsum dol│")
pixmap.drawText(pos=(0,2),text="│consectetur adi│")
pixmap.drawText(pos=(0,3),text="│sed do eiusmod │")
# The next condition is meant to show that you can
# handle also the Drag and Drop with the Right or Middle mouse buttons.
if evt.key == ttk. TTkMouseEvent.LeftButton:
pixmap.drawText(pos=(0,4),text="╰───────╼ Left ╾╯")
elif evt.key == ttk. TTkMouseEvent.RightButton:
pixmap.drawText(pos=(0,4),text="╰──────╼ Right ╾╯")
elif evt.key == ttk. TTkMouseEvent.MidButton:
pixmap.drawText(pos=(0,4),text="╰────╼ Eugenio ╾╯")
drag = ttk.TTkDrag()
drag.setPixmap(pixmap)
drag.exec()
return True
class DragDropImg(ttk.TTkFrame):
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
if evt.key == ttk. TTkMouseEvent.LeftButton:
imageString = random.choice([diamond,fire,key,peach,ring,sword,whip,pepper,python])
# A canvas is created and the ANSI String is drawn on it line by line
w,h = imageSize(imageString)
pixmap = ttk.TTkCanvas(width=w, height=h+1)
pixmap.setTransparent(True)
pixmap.drawText(pos=(0,0), text=self.title())
for y,line in enumerate(imageString.split('\n')):
pixmap.drawTTkString(pos=(0,y+1), text=line)
drag = ttk.TTkDrag()
drag.setPixmap(pixmap)
drag.exec()
return True
root = ttk.TTk()
root.layout().addWidget(DragDropBase( pos=( 0, 0), size=(25,10), title="Pixmap: Default"))
root.layout().addWidget(DragDropWidget(pos=( 0, 10), size=(25,10), title="Pixmap: Widget"))
root.layout().addWidget(DragDropTxt( pos=(50, 0), size=(25,10), title="Pixmap: Txt"))
root.layout().addWidget(DragDropImg( pos=(50, 10), size=(25,10), title="Pixmap: Img"))
root.mainloop()

125
tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py

@ -0,0 +1,125 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 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.
# Those 2 lines are required to use the TermTk library in the main folder
import sys, os
sys.path.append(os.path.join(sys.path[0],'../../..'))
import TermTk as ttk
# This example show the basic Drag and Drop pixmap usage;
#
# Anytime a Drag and Drop operation is started, a new TTkButton is created
# the hotSpot is set to define the offset from the mouse cursor
class DraggableFrame_FixedHotSpot(ttk.TTkFrame):
# I save the hotSpot in the constructor to be used during the dragging operation
def __init__(self, *, hotSpot ,**kwargs):
self.hotSpot = hotSpot
super().__init__(**kwargs)
self.layout().addWidget(ttk.TTkLabel(text="Drag Me..."))
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
button = ttk.TTkButton(text=f"{self.title()}", border=True, size=self.size())
drag = ttk.TTkDrag()
drag.setHotSpot(self.hotSpot)
drag.setData(button)
drag.setPixmap(button)
ttk.TTkLog.debug(f"Drag ({self.title()}) -> {button.text()}, pos={evt.x},{evt.y}")
# Start the drag operation
drag.exec()
return True
class DraggableFrame_RelativeHotSpot(ttk.TTkFrame):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.layout().addWidget(ttk.TTkLabel(text="Drag Me..."))
def mouseDragEvent(self, evt:ttk. TTkMouseEvent) -> bool:
button = ttk.TTkButton(text=f"HotSpot at\nMouse relative Pos\n\n-->{(evt.x, evt.y)}<--", border=True, size=self.size())
drag = ttk.TTkDrag()
drag.setHotSpot((evt.x, evt.y))
drag.setData(button)
drag.setPixmap(self)
ttk.TTkLog.debug(f"Drag ({self.title()}) -> {button.text()}, pos={evt.x},{evt.y}")
# Start the drag operation
drag.exec()
return True
class DropFrame(ttk.TTkFrame):
def dropEvent(self, evt:ttk.TTkDnDEvent) -> bool:
button:ttk.TTkButton = evt.data()
self.layout().addWidget(button)
# Since the frame by default has a padding of 1
# I align the button to the mouse coordinates by subtracting the Top/Left padding size
t,b,l,r = self.getPadding()
hsx,hsy = evt.hotSpot()
button.move(evt.x-l-hsx, evt.y-t-hsy)
ttk.TTkLog.debug(f"Drop ({self.title()}) <- {button.text()}, pos={evt.x},{evt.y}")
# This is not required in this example
#
# But I just add a logging feedback to the button
# To show that the button has been clicked
# Note: I highly recommend to avoid using lambda as a slot
# The correct way is to have a method in the class, marked as pyTTkSlot,
# capable of handling the signal
button.clicked.connect(lambda: ttk.TTkLog.debug(f"Clicked: {button.text()}"))
return True
# Create the root application
# and set its layout to TTkGridLayout in order to
# place the widgets in the following way:
#
# Col 0 Col 1
# +----------------+----------------+
# Row 0 | DragDrop 1 | DragDrop 2 |
# + +----------------+
# Row 1 | | DragDrop 3 |
# +----------------+----------------+
# Row 2 | Log Viewer |
# +----------------+----------------+
#
root = ttk.TTk()
root.setLayout(ttk.TTkGridLayout())
# Add the DragDrop widgets to the root layout
root.layout().addWidget(df1 := DropFrame(title="DnD 1"),0,0,2,1)
root.layout().addWidget( DropFrame(title="DnD 2"),0,1,1,1)
root.layout().addWidget( DropFrame(title="DnD 3"),1,1,1,1)
df1.layout().addWidget(DraggableFrame_FixedHotSpot(pos=( 0, 0),size=(25,5),title="Fix HotSpot ( 0, 0)", hotSpot=( 0, 0)))
df1.layout().addWidget(DraggableFrame_FixedHotSpot(pos=( 0, 5),size=(25,5),title="Fix HotSpot ( 5, 0)", hotSpot=( 5, 0)))
df1.layout().addWidget(DraggableFrame_FixedHotSpot(pos=( 0,10),size=(25,5),title="Fix HotSpot ( 0, 3)", hotSpot=( 0, 3)))
df1.layout().addWidget(DraggableFrame_FixedHotSpot(pos=( 0,15),size=(25,5),title="Fix HotSpot ( 5, 3)", hotSpot=( 5, 3)))
df1.layout().addWidget(DraggableFrame_FixedHotSpot(pos=(25,10),size=(25,5),title="Fix HotSpot (-5,-3)", hotSpot=(-5,-3)))
df1.layout().addWidget(DraggableFrame_FixedHotSpot(pos=(25,15),size=(25,5),title="Fix HotSpot (10, 3)", hotSpot=(10, 3)))
df1.layout().addWidget(DraggableFrame_RelativeHotSpot(pos=(25,0),size=(25,10),title="Relative HotSpot"))
# Add a LogViewer at the bottom to display the log messages
# (Row 2, Col 0, RowSpan 1, ColSpan 2)
root.layout().addWidget(ttk.TTkLogViewer(follow=True),2,0,1,2)
root.mainloop()

17
tutorial/examples/ansi.images.json

@ -0,0 +1,17 @@
{
"compressed":{
"Note1:" : "HEX Codes thanks to: https://github.com/ceccopierangiolieugenio/pyTermTk/tree/main/tools/dumbPaintTool",
"pepper" : "eJx9Vk1yFjsMfAtW4QhsOMEr/49dughVnCF3CFUsWGTBAsJ7QOBunIRuyZ6xHUgg7u/zKJLVkltz9+Lzh5f/6M+bd/jw6q27fY0foMQqQZKX7KXd/np4PPeOKs1L9Nj8fm0GqdE2/6eXm+7qNAhFvMcKg/eX+yK5SYyS+C0mCTBco5Um9ZAczIR/gAcZJnenifdJPP64dqPipTjJaTGqTbyLPKc5apIznsDmv9kGmwinJiHzZP5YTuxDkiMTlu2MJWFdNh3/YefhZttSbi4SsDissPx5xTmQU6iKaSRZEmGzw288FLsd8qBdgN23xY4UDLOGJB1WGH29jFBCH52iOcN2LYQlaIhZgneKRhWJQlMA18o0nCRFRXNYcFRXib1ThiGZLopmGJrQduuGED1OlxTNDt3oXSIukZv2bltJGAW5e74g7KUcFSwGvory/jgdOAvP3DoDiBeDxZvSP5w2y+F60+FzxQqj+8koamGPM3N8YPFJ5ccrc7Lms6LZoZwHa9WWmMGDtdgUrTbO874CNjswGbxiGhmRzLZdLlSlFAW7XJWXtNQ1BzQdYhIsJgQBS7fpYvCkDBeboK5ySfPjj5N7VLwpqMWBFJE8W+jLZYTbHQ4FNYKwgK5Mdn5cRlAz9IWCkR30kutZp4CV+qHQ21bbCbByAzKgSArWKIkPAFuFHQXsbAP0DcSixfX46GieZDR2pshg3TxJV6SHSWSn7HTZuAOzTsHqh7RQ0bQaoX2LgSWSD6NpozhqBh1IuVNdd27VpFasRGV0aqLeAlZvtQriEDp7qEUhrDnjxLgbhF4yHQeANSiCZGexnvEGZUPTD32jZktIS58+aVONMXXmRDhGBb2dNw3VCVvrcqYcCmk0IFsk+81MJcSPEeYxwyobbgzUIQLoClcUu52zDnSbWEQrzrgMHkZJYWEjOHbGoXiJRehiMdXARrAfmdIdh87mrlJmuPbhocvz5D7+nVyct3ExZ3i6UVai3cmRIl8vqJGbWWuC/4Quc07vQFsVKAS2e1Q8mc3hKbPUIeYZJn8ckervfmluzrOzuSGtnCnbmOI8TB2suVUHAYu3whcBGTKDL1IXWl8/FdcpRrJl4XGaZBgowaayCQ/nPdaNbMf3BYVTN13XzXmogJ3UFNMoEpWsbMMs0bYqWt95OivE5Y3MQ8UZNw9/uLF8BQjry51qf760Hy+NFLm2TH/y7M6GSiAibSTOUno27xWFF43XbdkcpdnfPqjwGzlVkGD9U2363u3Np39/A7RA/Jk=" ,
"python" : "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" ,
"Note2:" : "Icons from: https://valletyh.itch.io/icon-pack",
"fire" : "eJytlUsOgyAQhrvoovEKbjhBg/iohoO0iUlv4B1c9AhtD9iTFMHwGAbBh9HEDP8Mw8cMjOfP83KSz+MufvKeDnlftpzxomOc8rIZft+XMPPKMRICZGqc1TWvK/EKrxHzIimO+p0iYMkMGUzyuAxTvAKKglFOLYW2WZJW/DVQI43Hx2E180WMdu5kKZGWiM37sZmjtq0CoJaRDMDUExwkNhtpsiXygwShKiUVbEm3NrSlKyEnIoRbj9dHmIFPUA0Rl6H6Vs5iMfSZkH3Nsr56Z4Nxti1zro5J1xICZ7HCsNrZ26IYB+sEDTSeq8AORpxUPFYw56kBYmvH6hhANHwiuoRYGEaYE4o6YTtMv8dY47Cx63DpDlvYFP9IdGffWCBoekP2vv4BVO4f6w==" ,
"fireMini" : "eJylk01OAzEMhVl0xRW4A4rtJJPIF0HiDL1DFyxZsIBShLgPO+7ASXjOJPPTCXRBq1Zp/Y3t9xwfdi9fu6vyuvvE4ebe7fFRScpKQ1IcRb39ijjT/vv4OsXZeXxLHuPZGzCAeJgJIiWnnisSS9Y14oIlGUYiBUvi1wQ6GEQ5VEQMCWskBNSRqddcmLRlKKpvjImjvGL6io/myvWf0BwspSWOQWdvZHhaiIlFbAUoGyJhzVinaI+rKczwD1Zv9XDwcIsqFgSPBRW3nlLhEGqDGtNHzZ10iDSfyx/4IUO1YNEcSrKfMWjmCOrtgg2PZ0Ze8spKJZRytZQpwDlvqCZpbhxXjn2XMy9mLpAZA/BwBqK9QAuQ0ujEaTHKhAsFLrUxkY2WzIn3RTZSDJkb5PCQ11Ly9H+7frmUoWb4WGxitpFJtTJlu7a00U220m2hyy5Khxmm6XcZ6wJ12r6WPXNniC9C2ipypxtXNqOuQKYt0Zd8KqY93/4AlgU7wQ==",
"key" : "eJzFVLsRwjAMpYCGFWgyAedPPua0CHfM4B1SMAKwHF0mQYkTHDuW45gCXwpLeZKefm73z+6wG871jZfTjenCHLyCVCBAVBXwukZBd697QA3lJAvBQDGEtfi5yEsKklcKOEOEGEPpo2UUjWz869GZH8Y39sh8s7PGXHBQDchGz0x5jfoGFhAqqvVm8yqmrBZ/qEzJymVk6hon9DHHm1GPsKlmbnW31CNpEDOY9bIsIYDqtQT9EMgZBAe3MjBB0HwFiM1oZ5sRKMOGNU2vjqelt4Lg7+Vu6NOL/7fWJrUs0v/ojOv4+7Li9rdxeZw/gO97Ug==",
"sword" : "eJzFU8kNwjAQ5AEfWuCTCpDXcXxoG0GihvTAgxKAAvnRBevElo/YJogHUVaKNpPZmfHmsr2/dpvpOj3p4XBmIxUKjRxhkAiKIWgYO9vts+4M48Pga3w+LgHIFAJQCcfHe6AioFA1IPWvVsPeC5kBxACCxsp0QkToFNeAQWJbyoKxZc70JNsRgjYIxiBnsoJLrHXdH8xFGj8fyE/mVthbiEm3oJVDTc1iMdfai/a8ac/PISiIKG9lX4DKNwTQkB2jyqiQVZxWJYSFtwzY/u9qO5wbCZLTE/DKAx1I4s5TjT6+Vg1+npml1fh2ZfBlvZwiknRzp0MOKA3KbKd812+UXT1mI05hXCNNKzPFOaRnXUzbcZVlfOUg63aTltvxDd6kgOU=",
"ring" : "eJylVcFtwzAM7KOPIiv0owkKU7JqCXxkjQCdwTvkkRHaDphJoji2LJGUrMQBHATkmeSdTsz5/e/48TZ9Ti78+PzpRqXCNxqHGjsEY1BDN17/L0k0RBC8y6LgBgTwaAF7ijpXUGuHMkobGyoBQjfUcdog2PD4Auw+8WPo8TBx5VRr08fo3M7a5clgSVxg+ULBpuG07YNQHp0WRcuga2ppvLy78xzGVN17vid5VeIuO0GQUj6YTCCi40sFG4bLaCfh3mCoDkB7D4DfAzo/i5LC1vmihR+o5Z3qudBSLfJzD8iXh0krOoUZuQnV1JGpHDaUSpbUhqq5iqokyiQZy8QdsUtHKslaMb1QSZ/IQXG6MSn7Yz9dteVvxrjg5Po+enqJ57xLd49uIn4lL2KiWc2mYeZ/F7UtZkG7RonJspDNL9qW3JpnhhPY/n7dAHoxGOQ=",
"diamond" : "eJyVlU1uhDAMhbuoVKlX6MbddVXFhDAgX6RSz8AdWPQIbQ/YkzQwgbwkjgIjZpgxH/55Np7l8eft6WF7fbz6Ly+fZiYifxI7SifWCU9GOufmv98vxSz9+pvdJF0/BG45OGeF+SadHe7cekN4rxyi/oON92JvKnpwRoZeeHRXKGafr2OF6tlfHENt8/NWvpJSUnxeVK14sFfTKIqgaDXWx5juVmgJeClBdIvhottdGahzlyEDmkodPkEoul5+kadeZk1mUC9kgzMLXJSpNoxJ9wqZ1dnNNCUUOkhGu2CZvSHf2foLneN8JjKDOSYJWpFKQkIDb0m14gDXClSg2rMLjK75JcVrj3d9Z53F9KkrulPhGkslH868F6VXVfWzN+scLAF1Myxxh1JT5rTLKYTHmcWVx25Hz/9tcOlpWR4Rvt//AVu00Dg=",
"peach" : "eJytVEtuhTAM7KKL6l2hm6i7bioCIR/5Ik96Z+AOLHqEtgfsSRogIYmxXfpUhAQME8ceTzw/fr4+PazX9SW+PN+6SSkVHzB46KEfR9BmAD3q6fvrvcW1BeMXuP6jgwcLFsNLDD9C10bpYnADXQyTYvbxU7shsmaGpXsLvTYQgkCqdpt5lvMQHLgxBZouv/FWdc5ps+GlMO0CeJTyDpP149R3xeWgrA5NpLQPJymGS9EFFHbIICnzRqG0n+6UNvuwpF6Joxow65s8TpIp0xkXXw92ykUcSEWuDUuE/WhUIqdFCi9JdRdhDBZGid6wy330mywe68vSDNKXZ1uywX9NPOmWKEPsYIh3m3NGBaUTpkjtKQNWiWEPo2zxxORKouVbf/9XdZWv91hSyXzm5LnF+TbNFnJmLMkWxnburCNaCnsg2FokyY5jQpptjC+olfe04AyHGk14L2F8lYZMl4+3HxigIEs=",
"whip" : "eJylU8ERwiAQ9KGftOCHChyPhEDmnv6swBlroAcelqAWaCViQHIQCBNlyCRzt7e3CxezfZx3m3FdTvZjfz1qxph9YauQIwdAUC2CEPr1vMVxYeOdnMcD3hTwhqZADQjQ2Qd0IY7dGOglysHuEqyiUDfOXU6s65Co9IfBSpyelgCqEtc5GRXPk2nTn+nz1jxJ/VrieNp0xsLS4aoKz/tPsTPavy6BRG+ls4tStCAMzXQAIV0WSG2mrvyaALxDwe0m7dxcB0g/oBIoVORgEFZ8iyr+Sb5Qr/XjsAeUMjqSUJpDLQCJDpPVb0K9s1CQRaUToiBjqZuQdrIdeAUq3zFbm0J1cz+8AbE/aBQ="
}
}
Loading…
Cancel
Save