# MIT License
#
# Copyright (c) 2021 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
__all__ = ['TTkHelper']
from TermTk.TTkCore.TTkTerm.colors import TTkTermColor
from TermTk.TTkCore.TTkTerm.term import TTkTerm
from TermTk.TTkCore.cfg import TTkCfg, TTkGlbl
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot
[docs]
class TTkHelper:
'''TTkHelper
This is a collection of helper utilities to be used all around TermTk
'''
# TODO: Add Setter/Getter
_focusWidget = None
_rootCanvas = None
_rootWidget = None
_updateWidget = set()
_updateBuffer = set()
_mousePos = (0,0)
_cursorPos = (0,0)
_cursor = False
_cursorType = TTkTerm.Cursor.BLINKING_BLOCK
_cursorWidget = None
class _Overlay():
__slots__ = ('_widget','_prevFocus','_x','_y','_modal')
def __init__(self,x,y,widget,prevFocus,modal):
self._widget = widget
self._prevFocus = prevFocus
self._modal = modal
widget.move(x,y)
_overlay = []
[docs]
@staticmethod
def updateAll():
if TTkHelper._rootWidget:
TTkHelper._rootWidget.update(repaint=True, updateLayout=True)
for w in TTkHelper._rootWidget.layout().iterWidgets():
w.update(repaint=True, updateLayout=True)
[docs]
@staticmethod
def unlockPaint():
if rw := TTkHelper._rootWidget:
rw._paintEvent.set()
[docs]
@staticmethod
def addUpdateBuffer(canvas):
if canvas is not TTkHelper._rootCanvas:
TTkHelper._updateBuffer.add(canvas)
quitEvent = pyTTkSignal()
[docs]
@staticmethod
@pyTTkSlot()
def quit():
'''Quit TermTk'''
TTkHelper.quitEvent.emit()
if TTkHelper._rootWidget:
TTkHelper._rootWidget._quit()
[docs]
@staticmethod
@pyTTkSlot()
def aboutTermTk():
'''
Displays a simple message box about `pyTermTk <https://github.com/ceccopierangiolieugenio/pyTermTk>`__.
The message includes the version number of TermTk being used by the application.
This is useful for inclusion in the Help menu of an application, as shown in the Menus example.
This function is a convenience slot for :py:meth:`TTk.aboutTermTk`.
'''
if TTkHelper._rootWidget:
TTkHelper._rootWidget.aboutTermTk()
[docs]
@staticmethod
def getTerminalSize():
return TTkGlbl.term_w, TTkGlbl.term_h
[docs]
@staticmethod
def rootOverlay(widget):
if widget is None:
return None
if not TTkHelper._overlay:
return None
overlayWidgets = [o._widget for o in TTkHelper._overlay]
while widget is not None:
if widget in overlayWidgets:
return widget
widget = widget.parentWidget()
return None
[docs]
@staticmethod
def focusLastModal():
if modal := TTkHelper.getLastModal():
modal._widget.setFocus()
[docs]
@staticmethod
def getLastModal():
modal = None
for o in TTkHelper._overlay:
if o._modal:
modal = o
return modal
[docs]
@staticmethod
def checkModalOverlay(widget):
#if not TTkHelper._overlay:
# # There are no Overlays
# return True
if not (lastModal := TTkHelper.getLastModal()):
return True
# if not TTkHelper._overlay[-1]._modal:
# # The last window is not modal
# return True
if not (rootWidget := TTkHelper.rootOverlay(widget)):
# This widget is not overlay
return False
if rootWidget in [ o._widget for o in TTkHelper._overlay[TTkHelper._overlay.index(lastModal):]]:
return True
# if TTkHelper._overlay[-1]._widget == rootWidget:
# return True
return False
[docs]
@staticmethod
def isOverlay(widget):
return TTkHelper.rootOverlay(widget) is not None
[docs]
@staticmethod
def overlay(caller, widget, x:int, y:int, modal:bool=False, forceBoundaries:bool=True, toolWindow:bool=False):
'''overlay'''
if not caller:
caller = TTkHelper._rootWidget
wx, wy = TTkHelper.absPos(caller)
w,h = widget.size()
# Try to keep the overlay widget inside the terminal
if forceBoundaries:
wx = max(0, wx+x if wx+x+w < TTkGlbl.term_w else TTkGlbl.term_w-w )
wy = max(0, wy+y if wy+y+h < TTkGlbl.term_h else TTkGlbl.term_h-h )
mw,mh = widget.minimumSize()
ww = min(w,max(mw, TTkGlbl.term_w))
wh = min(h,max(mh, TTkGlbl.term_h))
widget.resize(ww,wh)
else:
wx += x
wy += y
wi = widget.widgetItem()
wi.setLayer(wi.LAYER1)
if toolWindow:
# Forcing the layer to:
# TTkLayoutItem.LAYER1 = 0x40000000
widget.move(wx,wy)
else:
TTkHelper._overlay.append(TTkHelper._Overlay(wx,wy,widget,TTkHelper._focusWidget,modal))
TTkHelper._rootWidget.rootLayout().addWidget(widget)
widget.setFocus()
widget.raiseWidget()
if hasattr(widget,'rootLayout'):
for w in widget.rootLayout().iterWidgets(onlyVisible=True):
w.update()
[docs]
@staticmethod
def getOverlay():
if TTkHelper._overlay:
return TTkHelper._overlay[-1]._widget
return None
[docs]
@staticmethod
def removeOverlay():
if not TTkHelper._overlay:
return
bkFocus = None
# Remove the first element also if it is modal
TTkHelper._overlay[-1]._modal = False
while TTkHelper._overlay:
if TTkHelper._overlay[-1]._modal:
break
owidget = TTkHelper._overlay.pop()
bkFocus = owidget._prevFocus
TTkHelper._rootWidget.rootLayout().removeWidget(owidget._widget)
if TTkHelper._focusWidget:
TTkHelper._focusWidget.clearFocus()
if bkFocus:
bkFocus.setFocus()
[docs]
@staticmethod
def removeOverlayAndChild(widget):
if not TTkHelper.isOverlay(widget): return
if len(TTkHelper._overlay) <= 1:
return TTkHelper.removeOverlay()
rootWidget = TTkHelper.rootOverlay(widget)
bkFocus = None
found = False
newOverlay = []
for o in TTkHelper._overlay:
if o._widget == rootWidget:
found = True
bkFocus = o._prevFocus
if not found:
newOverlay.append(o)
else:
TTkHelper._rootWidget.rootLayout().removeWidget(o._widget)
TTkHelper._overlay = newOverlay
if bkFocus:
bkFocus.setFocus()
if not found:
TTkHelper.removeOverlay()
[docs]
@staticmethod
def removeOverlayChild(widget):
rootWidget = TTkHelper.rootOverlay(widget)
found = False
newOverlay = []
for o in TTkHelper._overlay:
if o._widget == rootWidget:
found = True
newOverlay.append(o)
continue
if not found:
newOverlay.append(o)
else:
TTkHelper._rootWidget.rootLayout().removeWidget(o._widget)
TTkHelper._overlay = newOverlay
if not found:
TTkHelper.removeOverlay()
[docs]
@staticmethod
def setMousePos(pos):
TTkHelper._mousePos = pos
# update the position of the Drag and Drop Widget
if TTkHelper._dnd:
hsx, hsy = TTkHelper._dnd['d'].hotSpot()
TTkHelper._dnd['d'].pixmap().move(pos[0]-hsx, pos[1]-hsy)
[docs]
@staticmethod
def mousePos():
return TTkHelper._mousePos
[docs]
@staticmethod
def paintAll():
'''
_updateBuffer = list widgets that require a repaint [paintEvent]
_updateWidget = list widgets that need to be pushed below
'''
if TTkHelper._rootCanvas is None:
return
# Build a list of buffers to be repainted
updateWidgetsBk = TTkHelper._updateWidget.copy()
updateBuffers = TTkHelper._updateBuffer.copy()
TTkHelper._updateWidget.clear()
TTkHelper._updateBuffer.clear()
updateWidgets = set()
# TTkLog.debug(f"{len(TTkHelper._updateBuffer)} {len(TTkHelper._updateWidget)}")
for widget in updateWidgetsBk:
if not widget.isVisibleAndParent(): continue
updateBuffers.add(widget)
updateWidgets.add(widget)
parent = widget.parentWidget()
while parent is not None:
updateBuffers.add(parent)
updateWidgets.add(parent)
parent = parent.parentWidget()
# Paint all the canvas
for widget in updateBuffers:
if not widget.isVisibleAndParent(): continue
# Resize the canvas just before the paintEvent
# to avoid too many allocations
canvas = widget.getCanvas()
canvas.updateSize()
canvas.clean()
widget.paintEvent(canvas)
# Compose all the canvas to the parents
# From the deepest children to the bottom
pushToTerminal = False
sortedUpdateWidget = sorted(updateWidgets, key=lambda w: -TTkHelper.widgetDepth(w))
for widget in sortedUpdateWidget:
if not widget.isVisibleAndParent(): continue
pushToTerminal = True
widget.paintChildCanvas()
if pushToTerminal:
if TTkHelper._cursor:
TTkTerm.Cursor.hide()
if TTkCfg.doubleBuffer:
TTkHelper._rootCanvas.pushToTerminalBuffered(0, 0, TTkGlbl.term_w, TTkGlbl.term_h)
elif TTkCfg.doubleBufferNew:
TTkHelper._rootCanvas.pushToTerminalBufferedNew(0, 0, TTkGlbl.term_w, TTkGlbl.term_h)
else:
TTkHelper._rootCanvas.pushToTerminal(0, 0, TTkGlbl.term_w, TTkGlbl.term_h)
if TTkHelper._cursor:
x,y = TTkHelper._cursorPos
TTkTerm.push(TTkTerm.Cursor.moveTo(y+1,x+1))
TTkTerm.Cursor.show(TTkHelper._cursorType)
[docs]
@staticmethod
def rePaintAll():
if TTkHelper._rootCanvas and TTkHelper._rootWidget:
TTkTerm.push(TTkTerm.CLEAR)
TTkHelper._rootCanvas.cleanBuffers()
TTkHelper._rootWidget.update()
[docs]
@staticmethod
def isParent(widget, parent):
if p := widget.parentWidget():
if p == parent:
return True
return TTkHelper.isParent(p, parent)
return False
[docs]
@staticmethod
def absPos(widget) -> tuple[int,int]:
wx, wy = 0,0
layout = widget.widgetItem()
while layout:
px, py = layout.pos()
ox, oy = layout.offset()
wx, wy = wx+px+ox, wy+py+oy
layout = layout.parent()
return (wx, wy)
[docs]
@staticmethod
def nextFocus(widget):
rootWidget = TTkHelper.rootOverlay(widget)
if not rootWidget:
rootWidget = TTkHelper._rootWidget
if widget == rootWidget:
widget = None
first = None
for w in rootWidget.rootLayout().iterWidgets():
if not first and w.focusPolicy() & TTkK.TabFocus == TTkK.TabFocus:
first = w
# TTkLog.debug(f"{w._name} {widget}")
if widget:
if w == widget:
widget=None
continue
if w.isEnabled() and w.focusPolicy() & TTkK.TabFocus == TTkK.TabFocus:
w.setFocus()
w.update()
return
if first:
first.setFocus()
first.update()
[docs]
@staticmethod
def prevFocus(widget):
rootWidget = TTkHelper.rootOverlay(widget)
if not rootWidget:
rootWidget = TTkHelper._rootWidget
if widget == rootWidget:
widget = None
prev = None
for w in rootWidget.rootLayout().iterWidgets():
# TTkLog.debug(f"{w._name} {widget}")
if w == widget:
widget=None
if prev:
break
if w.isEnabled() and w.focusPolicy() & TTkK.TabFocus == TTkK.TabFocus:
prev = w
if prev:
prev.setFocus()
prev.update()
[docs]
@staticmethod
def setFocus(widget):
TTkHelper._focusWidget = widget
[docs]
@staticmethod
def getFocus():
return TTkHelper._focusWidget
[docs]
@staticmethod
def clearFocus():
TTkHelper._focusWidget = None
[docs]
@staticmethod
def showCursor(cursorType = TTkK.Cursor_Blinking_Block):
newType = {
TTkK.Cursor_Blinking_Block : TTkTerm.Cursor.BLINKING_BLOCK,
TTkK.Cursor_Blinking_Block_Also : TTkTerm.Cursor.BLINKING_BLOCK_ALSO,
TTkK.Cursor_Steady_Block : TTkTerm.Cursor.STEADY_BLOCK,
TTkK.Cursor_Blinking_Underline : TTkTerm.Cursor.BLINKING_UNDERLINE,
TTkK.Cursor_Steady_Underline : TTkTerm.Cursor.STEADY_UNDERLINE,
TTkK.Cursor_Blinking_Bar : TTkTerm.Cursor.BLINKING_BAR,
TTkK.Cursor_Steady_Bar : TTkTerm.Cursor.STEADY_BAR,
}.get(cursorType, TTkTerm.Cursor.BLINKING_BAR)
if not TTkHelper._cursor or TTkHelper._cursorType != newType:
TTkHelper._cursorType = newType
TTkTerm.Cursor.show(TTkHelper._cursorType)
TTkHelper._cursor = True
[docs]
@staticmethod
def hideCursor():
TTkTerm.Cursor.hide()
TTkHelper._cursorType = TTkTerm.Cursor.BLINKING_BLOCK
TTkHelper._cursor = False
# TTkHelper._cursorWidget = None
[docs]
@staticmethod
def moveCursor(widget, x, y):
TTkHelper._cursorWidget = widget
xx, yy = TTkHelper.absPos(widget)
pos = (xx+x,yy+y)
if TTkHelper._cursorPos == pos:
return
TTkHelper._cursorPos = pos
TTkTerm.push(TTkTerm.Cursor.moveTo(yy+y+1,xx+x+1))
# Drag and Drop related helper routines
_dnd = None
[docs]
@staticmethod
def dndInit(drag):
TTkHelper._dnd = {
'd' : drag,
'w' : None
}
TTkHelper._rootWidget.rootLayout().addWidget(drag.pixmap())
drag.pixmap().raiseWidget()
[docs]
@staticmethod
def dndGetDrag():
return TTkHelper._dnd['d'] if TTkHelper._dnd else None
[docs]
@staticmethod
def dndEnter(widget):
TTkHelper._dnd['w'] = widget
[docs]
@staticmethod
def isDnD():
return TTkHelper._dnd is not None
[docs]
@staticmethod
def dndEnd():
if TTkHelper._dnd:
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