From a112bde7a758cb082b7099cbd2b1e539b19b45bc Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sun, 12 Feb 2023 23:41:20 +0000 Subject: [PATCH] Added ToolTip feature --- TermTk/TTkCore/helper.py | 27 +++++++++ TermTk/TTkCore/timer.py | 104 ++++++++++++++++++++++++----------- TermTk/TTkCore/ttk.py | 1 + TermTk/TTkGui/__init__.py | 3 +- TermTk/TTkGui/drag.py | 1 - TermTk/TTkGui/tooltip.py | 79 ++++++++++++++++++++++++++ TermTk/TTkWidgets/widget.py | 35 ++++++++++-- tests/test.ui.025.toolTip.py | 47 ++++++++++++++++ 8 files changed, 256 insertions(+), 41 deletions(-) create mode 100644 TermTk/TTkGui/tooltip.py create mode 100755 tests/test.ui.025.toolTip.py diff --git a/TermTk/TTkCore/helper.py b/TermTk/TTkCore/helper.py index 221a71a6..6726a201 100644 --- a/TermTk/TTkCore/helper.py +++ b/TermTk/TTkCore/helper.py @@ -440,3 +440,30 @@ class TTkHelper: TTkHelper._rootWidget.rootLayout().removeWidget(TTkHelper._dnd['d'].pixmap()) TTkHelper._dnd = None TTkHelper._rootWidget.update() + + # ToolTip Helper Methods + toolTipWidget = None + toolTipTrigger = lambda _: True + toolTipReset = lambda : True + + @staticmethod + def toolTipShow(tt): + TTkHelper.toolTipClose() + if not TTkHelper._rootWidget: return + TTkHelper.toolTipWidget = tt + rw,rh = TTkHelper._rootWidget.size() + tw,th = tt.size() + mx,my = TTkHelper._mousePos + x = max(0, min(mx-(tw//2),rw-tw)) + if my <= th: # Draw below the Mouse + y = my+1 + else: # Draw above the Mouse + y = max(0,my-th) + tt.move(x,y) + TTkHelper._rootWidget.rootLayout().addWidget(tt) + tt.raiseWidget() + + def toolTipClose(): + TTkHelper.toolTipReset() + if TTkHelper.toolTipWidget: + TTkHelper.toolTipWidget.close() diff --git a/TermTk/TTkCore/timer.py b/TermTk/TTkCore/timer.py index d73ce55e..8bba89d3 100644 --- a/TermTk/TTkCore/timer.py +++ b/TermTk/TTkCore/timer.py @@ -81,24 +81,15 @@ if importlib.util.find_spec('pyodideProxy'): def stop(self): pass else: - class TTkTimer(threading.Thread): + # from .log import TTkLog + class TTkTimer(): _timers = [] - __slots__ = ( - 'timeout', '_timerEvent', - '_delay', '_delayLock', '_quit', - '_stopTime') + __slots__ = ('timeout', '_timer') def __init__(self): # Define Signals self.timeout = pyTTkSignal() - - self._timerEvent = threading.Event() - self._quit = threading.Event() - self._stopTime = 0 - self._delay=0 - self._delayLock = threading.Lock() - threading.Thread.__init__(self) + self._timer = None TTkTimer._timers.append(self) - threading.Thread.start(self) @staticmethod def quitAll(): @@ -106,29 +97,76 @@ else: timer.quit() def quit(self): - self._quit.set() - self._delay=1 - self._timerEvent.set() - - def run(self): - while self._timerEvent.wait(): - self._timerEvent.clear() - while self._delay > 0: - # self._delayLock.acquire() - delay = self._delay - self._delay = 0 - # self._delayLock.release() - if self._quit.wait(delay): - return - self.timeout.emit() + if self._timer: + self._timer.cancel() @pyTTkSlot(int) def start(self, sec=0): - self._lastTime = time.time() - self._delay = sec - self._timerEvent.set() + if self._timer: + self._timer.cancel() + self._timer = threading.Timer(sec, self.timeout.emit) + self._timer.start() @pyTTkSlot() def stop(self): - # TODO: Timer.stop() - self._stopTime = time.time() + if self._timer: + self._timer.cancel() + +# class TTkTimer(threading.Thread): +# _timers = [] +# __slots__ = ( +# 'timeout', '_timerEvent', +# '_delay', '_delayLock', '_quit', +# '_stopTime') +# def __init__(self): +# # Define Signals +# self.timeout = pyTTkSignal() +# +# self._timerEvent = threading.Event() +# self._quit = threading.Event() +# self._stopTime = 0 +# self._delay=0 +# self._delayLock = threading.Lock() +# threading.Thread.__init__(self) +# TTkTimer._timers.append(self) +# threading.Thread.start(self) +# +# @staticmethod +# def quitAll(): +# for timer in TTkTimer._timers: +# timer.quit() +# +# def quit(self): +# self._quit.set() +# self._delay=1 +# self._timerEvent.set() +# +# def run(self): +# while self._timerEvent.wait(): +# self._timerEvent.clear() +# self._delayLock.acquire() +# if not self._delay: +# self._delayLock.release() +# continue +# while self._delay > 0: +# self._delayLock.acquire() +# delay = self._delay +# self._delay = 0 +# self._delayLock.release() +# if self._quit.wait(delay): +# return +# self.timeout.emit() +# +# @pyTTkSlot(int) +# def start(self, sec=0): +# self._lastTime = time.time() +# self._delayLock.acquire() +# self._delay = sec +# self._delayLock.release() +# self._timerEvent.set() +# +# @pyTTkSlot() +# def stop(self): +# # TODO: Timer.stop() +# self._stopTime = time.time() +# \ No newline at end of file diff --git a/TermTk/TTkCore/ttk.py b/TermTk/TTkCore/ttk.py index 984debf0..f9566cf1 100644 --- a/TermTk/TTkCore/ttk.py +++ b/TermTk/TTkCore/ttk.py @@ -180,6 +180,7 @@ class TTk(TTkWidget): # Upload the global mouse position # Mainly used by the drag pixmap display TTkHelper.setMousePos((mevt.x,mevt.y)) + TTkWidget._mouseOverProcessed = False # Avoid to broadcast a key release after a multitap event if mevt.evt == TTkK.Release and self._lastMultiTap: return diff --git a/TermTk/TTkGui/__init__.py b/TermTk/TTkGui/__init__.py index 8ba6988a..a4fc9a3f 100644 --- a/TermTk/TTkGui/__init__.py +++ b/TermTk/TTkGui/__init__.py @@ -2,4 +2,5 @@ from .drag import TTkDrag, TTkDropEvent from .textwrap1 import TTkTextWrap from .textcursor import TTkTextCursor from .textdocument import TTkTextDocument -from .clipboard import TTkClipboard \ No newline at end of file +from .clipboard import TTkClipboard +from .tooltip import TTkToolTip \ No newline at end of file diff --git a/TermTk/TTkGui/drag.py b/TermTk/TTkGui/drag.py index d4e91b3c..261f6d68 100644 --- a/TermTk/TTkGui/drag.py +++ b/TermTk/TTkGui/drag.py @@ -30,7 +30,6 @@ class _TTkDragDisplayWidget(TTkWidget): __slots__ = ('_pixmap') def __init__(self, *args, **kwargs): TTkWidget.__init__(self, *args, **kwargs) - self._name = kwargs.get('name' , '_TTkDragDisplayWidget' ) self._x, self._y = TTkHelper.mousePos() def setPixmap(self, pixmap): diff --git a/TermTk/TTkGui/tooltip.py b/TermTk/TTkGui/tooltip.py new file mode 100644 index 00000000..6378b64b --- /dev/null +++ b/TermTk/TTkGui/tooltip.py @@ -0,0 +1,79 @@ +# MIT License +# +# Copyright (c) 2023 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. + +# from TermTk.TTkCore.helper import TTkHelper +from TermTk.TTkCore.log import TTkLog +from TermTk.TTkCore.canvas import TTkCanvas +from TermTk.TTkCore.color import TTkColor +from TermTk.TTkCore.timer import TTkTimer +from TermTk.TTkCore.helper import TTkHelper +from TermTk.TTkCore.string import TTkString +from TermTk.TTkWidgets.widget import TTkWidget +from TermTk.TTkCore.signal import pyTTkSlot + +class _TTkToolTipDisplayWidget(TTkWidget): + __slots__ = ('_toolTip', '_x', '_y') + def __init__(self, *args, **kwargs): + TTkWidget.__init__(self, *args, **kwargs) + self._toolTip = kwargs.get('toolTip',TTkString()).split('\n') + w = 2+max([s.termWidth() for s in self._toolTip]) + h = 2+len(self._toolTip) + self.resize(w,h) + + def mouseEvent(self, evt): return False + + def paintEvent(self): + w,h = self.size() + borderColor = TTkColor.fg("#888888") + canvas = self.getCanvas() + canvas.drawBox(pos=(0,0),size=(w,h), color=borderColor) + canvas.drawChar(pos=(0, 0), char='โ•ญ', color=borderColor) + canvas.drawChar(pos=(w-1,0), char='โ•ฎ', color=borderColor) + canvas.drawChar(pos=(w-1,h-1),char='โ•ฏ', color=borderColor) + canvas.drawChar(pos=(0, h-1),char='โ•ฐ', color=borderColor) + for i,s in enumerate(self._toolTip,1): + canvas.drawTTkString(pos=(1,i), text=s) + + +class TTkToolTip(): + toolTipTimer = TTkTimer() + toolTip = TTkString() + + @pyTTkSlot() + @staticmethod + def _toolTipShow(): + # TTkLog.debug(f"TT:{TTkToolTip.toolTip}") + TTkHelper.toolTipShow(_TTkToolTipDisplayWidget(toolTip=TTkToolTip.toolTip)) + + @staticmethod + def trigger(toolTip): + # TTkToolTip.toolTipTimer.stop() + TTkToolTip.toolTip = toolTip + TTkToolTip.toolTipTimer.start(1) + + @staticmethod + def reset(): + TTkToolTip.toolTipTimer.stop() + +TTkToolTip.toolTipTimer.timeout.connect(TTkToolTip._toolTipShow) +TTkHelper.toolTipTrigger = TTkToolTip.trigger +TTkHelper.toolTipReset = TTkToolTip.reset \ No newline at end of file diff --git a/TermTk/TTkWidgets/widget.py b/TermTk/TTkWidgets/widget.py index 0e615bb3..cde8fc04 100644 --- a/TermTk/TTkWidgets/widget.py +++ b/TermTk/TTkWidgets/widget.py @@ -26,6 +26,7 @@ from TermTk.TTkCore.cfg import TTkCfg, TTkGlbl from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.helper import TTkHelper +from TermTk.TTkCore.string import TTkString from TermTk.TTkCore.canvas import TTkCanvas from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot from TermTk.TTkTemplates.lookandfeel import TTkLookAndFeel @@ -80,6 +81,9 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): :param int minHeight: the minHeight of the widget, defaults to 0 :param [int,int] minSize: the minSize [width,height] of the widget, optional + :param toolTip: This property holds the widget's tooltip + :type toolTip: :class:`~TermTk.TTkCore.string.TTkString` + :param lookAndFeel: the style helper to be used for any customization :type lookAndFeel: :class:`~TermTk.TTkTemplates.lookandfeel.TTkTTkLookAndFeel` @@ -100,6 +104,7 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): '_pendingMouseRelease', '_enabled', '_lookAndFeel', + '_toolTip', #Signals 'focusChanged') @@ -138,6 +143,8 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): self._visible = kwargs.get('visible', True) self._enabled = kwargs.get('enabled', True) + self._toolTip = TTkString(kwargs.get('toolTip','')) + self._focus = False self._focus_policy = TTkK.NoFocus @@ -354,6 +361,7 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): _mouseOver = None _mouseOverTmp = None + _mouseOverProcessed = False def mouseEvent(self, evt): ''' .. caution:: Don't touch this! ''' if not self._enabled: return True @@ -402,15 +410,24 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): # handle Enter/Leave Events # _mouseOverTmp hold the top widget under the mouse # if different than self it means that it is a child - if ( TTkWidget._mouseOver != TTkWidget._mouseOverTmp == self ): - if TTkWidget._mouseOver: - TTkWidget._mouseOver.leaveEvent(evt) - TTkWidget._mouseOver = self - TTkWidget._mouseOver.enterEvent(evt) - 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 @@ -673,6 +690,12 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): self._lookAndFeel = laf self._lookAndFeel.modified.connect(self.update) + def toolTip(self): + return self._toolTip + + def setToolTip(self, toolTip): + self._toolTip = toolTip + _ttkProperties = { 'X' : { 'init': {'name':'x', 'type':int } , diff --git a/tests/test.ui.025.toolTip.py b/tests/test.ui.025.toolTip.py new file mode 100755 index 00000000..bdd682bd --- /dev/null +++ b/tests/test.ui.025.toolTip.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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 sys, os, argparse, math, random + +sys.path.append(os.path.join(sys.path[0],'..')) +import TermTk as ttk +from math import sin, cos + +ttk.TTkLog.use_default_file_logging() + +root = ttk.TTk(title="pyTermTk Demo", mouseTrack=True) + +ttk.TTkLabel( parent=root, text="Label 1", pos=(0,0), size=(10,1), toolTip="TT Label 1") +ttk.TTkButton(parent=root, text="Button 1", pos=(0,1), size=(10,1), toolTip="TT Button 1") +ttk.TTkButton(parent=root, text="Button 2", pos=(0,2), size=(10,3), toolTip="TT Button 2", border=True) +ttk.TTkButton(parent=root, text="Button 3", pos=(0,5), size=(20,3), toolTip="TT Button 3\n\nNewline", border=True) +ttk.TTkButton(parent=root, text="Button 3", pos=(21,0), size=(20,10), border=True, + toolTip= + ttk.TTkString(color=ttk.TTkColor.fg("#ff0000") ,text=" L๐Ÿ˜Žrem ipsum\n")+ + ttk.TTkString(color=ttk.TTkColor.fg("#00ff00") ,text="dolor sit amet,\n โŒš โค ๐Ÿ’™ ๐Ÿ™‹'\nYepp!!!")) + +rf = ttk.TTkWindow(parent=root, title="LOG", pos=(0,10), size=(90,20), layout=ttk.TTkGridLayout(), toolTip="TT Log Window\n With\nLogDump") +ttk.TTkLogViewer(parent=rf) + +root.mainloop() \ No newline at end of file