From 2f8c05f6451244653923689a555be236902e6337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eugenio=20Parodi=20=F0=9F=8C=B6=EF=B8=8F?= Date: Fri, 3 Jan 2025 17:38:08 +0000 Subject: [PATCH] Documented the Drag and Drop and added a bunch of examples and fixed its handling and improved the logging --- TermTk/TTkCore/TTkTerm/inputmouse.py | 9 + TermTk/TTkCore/log.py | 5 +- TermTk/TTkTemplates/dragevents.py | 2 + TermTk/TTkWidgets/container.py | 103 +-------- TermTk/TTkWidgets/widget.py | 13 +- demo/showcase/dragndrop.py | 5 +- docs/source/info/resources/dragdrop.rst | 203 +++++++++++++++++- tests/ansi.images.json | 18 +- tests/sandbox/Makefile | 2 +- tutorial/examples/DragAndDrop/dnd.01.basic.py | 84 ++++++++ .../examples/DragAndDrop/dnd.02.events.01.py | 96 +++++++++ .../examples/DragAndDrop/dnd.02.events.02.py | 135 ++++++++++++ .../examples/DragAndDrop/dnd.03.pixmap.01.py | 87 ++++++++ .../examples/DragAndDrop/dnd.03.pixmap.02.py | 110 ++++++++++ .../examples/DragAndDrop/dnd.03.pixmap.03.py | 134 ++++++++++++ .../examples/DragAndDrop/dnd.03.pixmap.04.py | 119 ++++++++++ .../examples/DragAndDrop/dnd.04.hotSpot.01.py | 125 +++++++++++ tutorial/examples/ansi.images.json | 17 ++ 18 files changed, 1141 insertions(+), 126 deletions(-) mode change 100644 => 120000 tests/ansi.images.json create mode 100755 tutorial/examples/DragAndDrop/dnd.01.basic.py create mode 100755 tutorial/examples/DragAndDrop/dnd.02.events.01.py create mode 100755 tutorial/examples/DragAndDrop/dnd.02.events.02.py create mode 100755 tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py create mode 100755 tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py create mode 100755 tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py create mode 100755 tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py create mode 100755 tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py create mode 100644 tutorial/examples/ansi.images.json diff --git a/TermTk/TTkCore/TTkTerm/inputmouse.py b/TermTk/TTkCore/TTkTerm/inputmouse.py index 385c3df2..00ded022 100644 --- a/TermTk/TTkCore/TTkTerm/inputmouse.py +++ b/TermTk/TTkCore/TTkTerm/inputmouse.py @@ -92,6 +92,15 @@ def __init__(self, x: int, y: int, key: int, evt: int, mod: int, tap: int, raw: 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) diff --git a/TermTk/TTkCore/log.py b/TermTk/TTkCore/log.py index a84ba17b..ebbe8a1a 100644 --- a/TermTk/TTkCore/log.py +++ b/TermTk/TTkCore/log.py @@ -80,8 +80,9 @@ def _process_msg(mode: int, msg: str): 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): diff --git a/TermTk/TTkTemplates/dragevents.py b/TermTk/TTkTemplates/dragevents.py index ab66977e..86e7fae8 100644 --- a/TermTk/TTkTemplates/dragevents.py +++ b/TermTk/TTkTemplates/dragevents.py @@ -50,6 +50,8 @@ def dragLeaveEvent(self, evt:TTkDnDEvent) -> bool: .. 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` diff --git a/TermTk/TTkWidgets/container.py b/TermTk/TTkWidgets/container.py index 0477961c..83befe8f 100644 --- a/TermTk/TTkWidgets/container.py +++ b/TermTk/TTkWidgets/container.py @@ -292,107 +292,8 @@ def _mouseEventLayoutHandle(evt, layout): 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: ''' diff --git a/TermTk/TTkWidgets/widget.py b/TermTk/TTkWidgets/widget.py index fd3676b5..78265853 100644 --- a/TermTk/TTkWidgets/widget.py +++ b/TermTk/TTkWidgets/widget.py @@ -392,6 +392,9 @@ def pasteEvent(self, txt:str) -> None: ''' return False + def _mouseEventParseChildren(self, evt:TTkMouseEvent) -> bool: + return False + _mouseOver = None _mouseOverTmp = None _mouseOverProcessed = False @@ -416,6 +419,9 @@ def mouseEvent(self, evt:TTkMouseEvent) -> bool: # 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 @@ def mouseEvent(self, evt:TTkMouseEvent) -> bool: 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) diff --git a/demo/showcase/dragndrop.py b/demo/showcase/dragndrop.py index 6ab79d68..f54fb38a 100755 --- a/demo/showcase/dragndrop.py +++ b/demo/showcase/dragndrop.py @@ -62,7 +62,10 @@ def dropEvent(self, evt:ttk.TTkDnDEvent) -> bool: 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 diff --git a/docs/source/info/resources/dragdrop.rst b/docs/source/info/resources/dragdrop.rst index 90ce027f..a308a35f 100644 --- a/docs/source/info/resources/dragdrop.rst +++ b/docs/source/info/resources/dragdrop.rst @@ -4,4 +4,205 @@ Drag and Drop ============= -TBD \ No newline at end of file +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 `__ (`tryItOnline `__) + +-------- +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 `__ (`tryItOnline `__) + + +------ +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 `__ (`tryItOnline `__) + * `tutorial/examples/DragAndDrop/dnd.02.events.02.py `__ (`tryItOnline `__) + + +------ +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 `__ (`tryItOnline `__) + * `tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py `__ (`tryItOnline `__) + * `tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py `__ (`tryItOnline `__) + * `tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py `__ (`tryItOnline `__) + + +------- +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 `__ (`tryItOnline `__) + +-------- +Examples +-------- + +* `tutorial/examples/DragAndDrop/dnd.01.basic.py `__ (`tryItOnline `__) +* `tutorial/examples/DragAndDrop/dnd.02.events.01.py `__ (`tryItOnline `__) +* `tutorial/examples/DragAndDrop/dnd.02.events.02.py `__ (`tryItOnline `__) +* `tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py `__ (`tryItOnline `__) +* `tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py `__ (`tryItOnline `__) +* `tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py `__ (`tryItOnline `__) +* `tutorial/examples/DragAndDrop/dnd.04.pixmap.03.py `__ (`tryItOnline `__) +* `tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py `__ (`tryItOnline `__) diff --git a/tests/ansi.images.json b/tests/ansi.images.json deleted file mode 100644 index 2694623c..00000000 --- a/tests/ansi.images.json +++ /dev/null @@ -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=" - } -} \ No newline at end of file diff --git a/tests/ansi.images.json b/tests/ansi.images.json new file mode 120000 index 00000000..705442eb --- /dev/null +++ b/tests/ansi.images.json @@ -0,0 +1 @@ +../tutorial/examples/ansi.images.json \ No newline at end of file diff --git a/tests/sandbox/Makefile b/tests/sandbox/Makefile index 429706f3..da9d3151 100644 --- a/tests/sandbox/Makefile +++ b/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 diff --git a/tutorial/examples/DragAndDrop/dnd.01.basic.py b/tutorial/examples/DragAndDrop/dnd.01.basic.py new file mode 100755 index 00000000..113808f1 --- /dev/null +++ b/tutorial/examples/DragAndDrop/dnd.01.basic.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +# 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() \ No newline at end of file diff --git a/tutorial/examples/DragAndDrop/dnd.02.events.01.py b/tutorial/examples/DragAndDrop/dnd.02.events.01.py new file mode 100755 index 00000000..fb5ff967 --- /dev/null +++ b/tutorial/examples/DragAndDrop/dnd.02.events.01.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +# 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() \ No newline at end of file diff --git a/tutorial/examples/DragAndDrop/dnd.02.events.02.py b/tutorial/examples/DragAndDrop/dnd.02.events.02.py new file mode 100755 index 00000000..8b500783 --- /dev/null +++ b/tutorial/examples/DragAndDrop/dnd.02.events.02.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +# 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() \ No newline at end of file diff --git a/tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py b/tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py new file mode 100755 index 00000000..6cdc1eb3 --- /dev/null +++ b/tutorial/examples/DragAndDrop/dnd.03.pixmap.01.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +# 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() \ No newline at end of file diff --git a/tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py b/tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py new file mode 100755 index 00000000..0817cb1f --- /dev/null +++ b/tutorial/examples/DragAndDrop/dnd.03.pixmap.02.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +# 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() \ No newline at end of file diff --git a/tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py b/tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py new file mode 100755 index 00000000..d22bef66 --- /dev/null +++ b/tutorial/examples/DragAndDrop/dnd.03.pixmap.03.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +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() \ No newline at end of file diff --git a/tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py b/tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py new file mode 100755 index 00000000..3703619d --- /dev/null +++ b/tutorial/examples/DragAndDrop/dnd.03.pixmap.04.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +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() \ No newline at end of file diff --git a/tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py b/tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py new file mode 100755 index 00000000..22dbb6d8 --- /dev/null +++ b/tutorial/examples/DragAndDrop/dnd.04.hotSpot.01.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +# 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() \ No newline at end of file diff --git a/tutorial/examples/ansi.images.json b/tutorial/examples/ansi.images.json new file mode 100644 index 00000000..2694623c --- /dev/null +++ b/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=" + } +} \ No newline at end of file