Source code for TermTk.TTkCore.helper

# 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 addUpdateWidget(widget): # if not widget.isVisibleAndParent(): return if widget not in TTkHelper._updateWidget: TTkHelper._updateWidget.add(widget) TTkHelper.unlockPaint()
[docs] @staticmethod def addUpdateBuffer(canvas): if canvas is not TTkHelper._rootCanvas: TTkHelper._updateBuffer.add(canvas)
[docs] @staticmethod def registerRootWidget(widget): TTkHelper._rootCanvas = widget.getCanvas() TTkHelper._rootWidget = widget TTkHelper._rootCanvas.enableDoubleBuffer()
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 widgetDepth(widget) -> int: if widget is None: return 0 return 1 + TTkHelper.widgetDepth(widget.parentWidget())
[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 widgetAt(x, y, layout=None): layout = layout if layout else TTkHelper._rootWidget.rootLayout() lx,ly,lw,lh =layout.geometry() lox, loy = layout.offset() lx,ly,lw,lh = lx+lox, ly+loy, lw-lox, lh-loy if x<lx or x>=lx+lw or y<ly or y>=lh+ly: return None x-=lx y-=ly for item in reversed(layout.zSortedItems): if item.layoutItemType() == TTkK.WidgetItem and not item.isEmpty(): widget = item.widget() if not widget._visible: continue wx,wy,ww,wh = widget.geometry() if wx <= x < wx+ww and wy <= y < wy+wh: if hasattr(widget,'rootLayout'): return TTkHelper.widgetAt(x-wx, y-wy, widget.rootLayout()) else: return widget continue elif item.layoutItemType() == TTkK.LayoutItem: if (wid:=TTkHelper.widgetAt(x, y, item)): return wid return layout.parentWidget()
[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))
[docs] @staticmethod def cursorWidget(): return TTkHelper._cursorWidget
# 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 dndWidget(): return TTkHelper._dnd['w'] 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
[docs] @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) wi = tt.widgetItem() wi.setLayer(wi.LAYER3) tt.move(x,y) TTkHelper._rootWidget.rootLayout().addWidget(tt) tt.raiseWidget()
[docs] def toolTipClose(): TTkHelper.toolTipReset() if TTkHelper.toolTipWidget: TTkHelper._rootWidget.rootLayout().removeWidget(TTkHelper.toolTipWidget) TTkHelper.toolTipWidget = None