# MIT License
#
# Copyright (c) 2023 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__ = ['TTkContainer', 'TTkPadding']
from typing import NamedTuple
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.log import TTkLog
from TermTk.TTkCore.helper import TTkHelper
from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot
from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent
from TermTk.TTkLayouts.layout import TTkLayout
from TermTk.TTkWidgets.widget import TTkWidget
[docs]
class TTkPadding(NamedTuple):
top : int = 0
bottom : int = 0
left : int = 0
right : int = 0
[docs]
class TTkContainer(TTkWidget):
'''
:py:class:`TTkContainer` include the layout management through :py:class:`TTkLayout` or one of its implementations.
It is the base class of all the composite widgets like :py:class:`TTkSpinBox`
where a :py:class:`TTkLineEdit` is placed and shown when edited, or in the :py:class:`TTkFrame` and
its extension :py:class:`TTkWindow` where the :py:class:`TTkLayout` is used for the child
widgets placements or the :py:class:`TTkMenuBarLayout`,
the main root widget :py:class:`TTk` inherit the layout capabilities of :py:class:`TTkContainer` for any widget placed in the `pyTermTk <https://github.com/ceccopierangiolieugenio/pyTermTk>`__ app.
Main widgets subclassing :py:class:`TTkContainer`:
#. :py:class:`TTk`
#. :py:class:`TTkFrame`
#. :py:class:`TTkWindow`
#. :py:class:`TTkSplitter`
#. :py:class:`TTkAppTemplate`
#. :py:class:`TTkTabWidget`
#. :py:class:`TTkTreeWidget`
More info and examples are available in the :ref:`Tutorial <Layout-Tutorial_Intro>`
.. _Container-Layout-Topology:
:py:class:`TTkContainer` Layout topology:
::
Terminal area (i.e. XTerm) = TTk
┌────────────────────────────────────────────┐
│ │
│ TTkContainer │
│ < -------- width -----> │
│ (x,y)╔═════════════════════════╗ ^ │
│ ║ padt (Top Padding) ║ | │
│ ║ ┌───────────────┐ ║ | height │
│ ║padl│ Layout │padr║ | │
│ ║ └───────────────┘ ║ | │
│ ║ padb (Bottom Pad.) ║ | │
│ ╚═════════════════════════╝ v │
└────────────────────────────────────────────┘
Example:
.. code-block:: python
from TermTk import TTk, TTkButton, TTkHBoxLayout
# The class TTk, that could be referred as the terminal layout, is a subclass of TTkContainer
root = TTk( layout=TTkHBoxLayout(), padding=(1,2,4,8))
TTkButton(parent=root, border=True, text="Btn 1!")
TTkButton(parent=root, border=True, text="Btn 2!")
TTkButton(parent=root, border=True, text="Btn 3!")
TTkButton(parent=root, border=True, text="Btn 4!")
root.mainloop()
'''
__slots__ = (
'_padt', '_padb', '_padl', '_padr',
'_forwardStyle',
'_layout')
def __init__(self, *,
layout:TTkLayout=None,
padding:TTkPadding = None,
paddingTop:int = 0,
paddingBottom:int = 0,
paddingLeft:int = 0,
paddingRight:int = 0,
forwardStyle:bool = False,
**kwargs) -> None:
'''
:param layout: the layout of this widget, optional, defaults to :py:class:`TTkLayout`
:type layout: :mod:`TermTk.TTkLayouts`
:param padding: the padding (top, bottom, left, right) of the widget, defaults to (0,0,0,0)
:type padding: :py:class:`TTkPadding`
:param paddingTop: the Top padding, override Top padding if already defined, optional, default=0 if padding is not defined
:type paddingTop: int
:param paddingBottom: the Bottom padding, override Bottom padding if already defined, optional, default=0 if padding is not defined
:type paddingBottom: int
:param paddingLeft: the Left padding, override Left padding if already defined, optional, default=0 if padding is not defined
:type paddingLeft: int
:param paddingRight: the Right padding, override Right padding if already defined, optional, default=0 if padding is not defined
:type paddingRight: int
:param bool forwardStyle: [**Experimental**] any change of style will reflect the children, defaults to False
:type forwardStyle: bool
'''
self._forwardStyle = forwardStyle
if padding:
self._padt = padding[0]
self._padb = padding[1]
self._padl = padding[2]
self._padr = padding[3]
else:
self._padt = paddingTop
self._padb = paddingBottom
self._padl = paddingLeft
self._padr = paddingRight
self._layout = TTkLayout() # root layout
self._layout.addItem(layout if layout else TTkLayout()) # main layout
super().__init__(**kwargs)
self._layout.setParent(self)
self.update(updateLayout=True)
# def forwardStyleTo(self, widget:TTkWidget):
# widget._currentStyle |= self._currentStyle
# widget.update()
def _processForwardStyle(self) -> None:
if not self._forwardStyle: return
def _getChildren():
for w in self.rootLayout().iterWidgets(onlyVisible=True, recurse=False):
yield w
for w in self.layout().iterWidgets(onlyVisible=True, recurse=False):
yield w
for w in _getChildren():
self.setCurrentStyle(w._currentStyle | self._currentStyle)
if issubclass(type(w),TTkContainer):
w._processForwardStyle()
def setCurrentStyle(self, *args, **kwargs) -> None:
super().setCurrentStyle(*args, **kwargs)
self._processForwardStyle()
@staticmethod
def _paintChildCanvas(canvas, item, geometry, offset):
''' .. caution:: Don't touch this! '''
lx,ly,lw,lh = geometry
ox, oy = offset
if item.layoutItemType() == TTkK.WidgetItem and not item.isEmpty():
child = item.widget()
cx,cy,cw,ch = child.geometry()
canvas.paintCanvas(
child.getCanvas(),
(cx+ox, cy+oy, cw, ch), # geometry
( 0, 0, cw, ch), # slice
( lx, ly, lw, lh)) # bound
else:
for child in item.zSortedItems:
# The Parent Layout Geometry (lx,ly,lw,lh) include the padding of the layout
igx, igy, igw, igh = item.geometry()
iox, ioy = item.offset()
# Moved Layout to the new geometry (ix,iy,iw,ih)
ix = igx+ox # + iox
iy = igy+oy # + ioy
iw = igw # -iox
ih = igh # -ioy
# return if Child outside the bound
if ix+iw < lx and ix > lx+lw and iy+ih < ly and iy > ly+lh: continue
# Crop the Layout based on the Parent Layout Geometry
bx = max(ix,lx)
by = max(iy,ly)
bw = min(ix+iw,lx+lw)-bx
bh = min(iy+ih,ly+lh)-by
TTkContainer._paintChildCanvas(canvas, child, (bx,by,bw,bh), (ix+iox,iy+ioy))
def paintChildCanvas(self) -> None:
''' .. caution:: Don't touch this! '''
TTkContainer._paintChildCanvas(self._canvas, self.rootLayout(), self.rootLayout().geometry(), self.rootLayout().offset())
[docs]
def getPadding(self) -> TTkPadding:
''' Retrieve the :py:class:`TTkContainer`'s paddings sizes as shown in :ref:`Layout Topology <Container-Layout-Topology>`
:return: :py:class:`TTkPadding` the NamedTuple representing a 4 int values tuple (top,bottom,left,right)
'''
return TTkPadding(self._padt, self._padb, self._padl, self._padr)
[docs]
def setPadding(self, top: int, bottom: int, left: int, right: int) -> None:
''' Set the :py:class:`TTkContainer`'s paddings sizes as shown in :ref:`Layout Topology <Container-Layout-Topology>`
:param int top: top padding
:param int bottom: bottom padding
:param int left: left padding
:param int right: right padding
'''
if self._padt == top and self._padb == bottom and \
self._padl == left and self._padr == right: return
self._padt = top
self._padb = bottom
self._padl = left
self._padr = right
self.update(repaint=True, updateLayout=True)
@staticmethod
def _mouseEventLayoutHandle(evt, layout):
''' .. caution:: Don't touch this! '''
x, y = evt.x, evt.y
lx,ly,lw,lh =layout.geometry()
lox, loy = layout.offset()
lx,ly,lw,lh = lx+lox, ly+loy, lw-lox, lh-loy
# opt of bounds
if x<lx or x>=lx+lw or y<ly or y>=lh+ly:
return False
x-=lx
y-=ly
for item in reversed(layout.zSortedItems):
# for item in 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()
# Skip the mouse event if outside this widget
if not (wx <= x < wx+ww and wy <= y < wy+wh): continue
wevt = evt.clone(pos=(x-wx, y-wy))
if widget.mouseEvent(wevt):
return True
elif item.layoutItemType() == TTkK.LayoutItem:
levt = evt.clone(pos=(x, y))
if TTkContainer._mouseEventLayoutHandle(levt, item):
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
[docs]
def setLayout(self, layout:TTkLayout) -> None:
'''
Set the :ref:`Layout <Layout-Tutorial_Intro>` used by this widget to place all the child widgets.
:param layout:
:type layout: :py:class:`TTkLayout`
'''
self._layout.replaceItem(layout, 0)
#self.layout().setParent(self)
self.update(repaint=True, updateLayout=True)
[docs]
def layout(self) -> TTkLayout:
'''
Get the :ref:`Layout <Layout-Tutorial_Intro>`
:return: The :ref:`Layout <Layout-Tutorial_Intro>` used
:rtype: :py:class:`TTkLayout` or derived
'''
return self._layout.itemAt(0)
[docs]
def rootLayout(self) -> TTkLayout:
'''
This is a root layout mainly used to place items that are not supposed to be inside the main layout (i.e. the menu elements)
.. caution:: Use this layout only if you know what you are doing
:return: :py:class:``TTkLayout`
'''
return self._layout
def maximumHeight(self) -> int:
wMaxH = self._maxh
if self.layout() is not None:
lMaxH = self.layout().maximumHeight() + self._padt + self._padb
if lMaxH < wMaxH:
return lMaxH
return wMaxH
def maximumWidth(self) -> int:
wMaxW = self._maxw
if self.layout() is not None:
lMaxW = self.layout().maximumWidth() + self._padl + self._padr
if lMaxW < wMaxW:
return lMaxW
return wMaxW
def minimumHeight(self) -> int:
wMinH = self._minh
if self.layout() is not None:
lMinH = self.layout().minimumHeight() + self._padt + self._padb
if lMinH > wMinH:
return lMinH
return wMinH
def minimumWidth(self) -> int:
wMinW = self._minw
if self.layout() is not None:
lMinW = self.layout().minimumWidth() + self._padl + self._padr
if lMinW > wMinW:
return lMinW
return wMinW
@pyTTkSlot()
def show(self) -> None:
if self._visible: return
self._visible = True
self._canvas.show()
self.update(updateLayout=True, updateParent=True)
for w in self.rootLayout().iterWidgets(onlyVisible=True):
w.update()
@pyTTkSlot()
def hide(self) -> None:
if not self._visible: return
self._visible = False
self._canvas.hide()
self.update(repaint=False, updateParent=True)
def update(self, repaint: bool = True, updateLayout: bool = False, updateParent: bool = False) -> None:
super().update(repaint=repaint, updateLayout=updateLayout, updateParent=updateParent)
if updateLayout and self.rootLayout() is not None and self.size() != (0,0):
self.rootLayout().setGeometry(0,0,self._width,self._height)
self.layout().setGeometry(
self._padl, self._padt,
self._width - self._padl - self._padr,
self._height - self._padt - self._padb)
self.rootLayout().update()
def getWidgetByName(self, name: str) -> TTkWidget:
'''
Return the widget from its name.
:param name: the widget's name, normally defined in the constructor or through :py:meth:`TTkWidget.setName`
:type name:
:return: :py:class:`TTkWidget`
'''
if name == self._name:
return self
for w in self.rootLayout().iterWidgets(onlyVisible=False, recurse=True):
if w._name == name:
return w
return None