diff --git a/demo/showcase/date_time.py b/demo/showcase/date_time.py
old mode 100644
new mode 100755
diff --git a/demo/showcase/dndtabs.py b/demo/showcase/dndtabs.py
index 366ec56e..42113d86 100755
--- a/demo/showcase/dndtabs.py
+++ b/demo/showcase/dndtabs.py
@@ -29,7 +29,7 @@ import TermTk as ttk
def demoDnDTabs(root=None, border=True):
vsplitter = ttk.TTkSplitter(parent=root, orientation=ttk.TTkK.VERTICAL)
- tabWidget1 = ttk.TTkTabWidget(parent=vsplitter, border=border)
+ tabWidget1 = ttk.TTkTabWidget(parent=vsplitter, border=True)
hsplitter = ttk.TTkSplitter(parent=vsplitter)
tabWidget2 = ttk.TTkTabWidget(parent=hsplitter, border=False, barType=ttk.TTkBarType.DEFAULT_2)
tabWidget3 = ttk.TTkTabWidget(parent=hsplitter, border=False, barType=ttk.TTkBarType.NERD_1)
@@ -44,8 +44,12 @@ def demoDnDTabs(root=None, border=True):
tabWidget1.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame1.6"), "Label 1.6")
tabWidget1.addTab(ttk.TTkTestWidget( border=True, title="Frame1.7"), "Label Test 1.7")
tabWidget1.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame1.8"), "Label 1.8")
- tabWidget2.addTab(ttk.TTkTestWidget( border=True, title="Frame1.9"), "Label Test 1.9")
- tabWidget3.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame1.10"), "Label 1.10")
+
+ tabWidget2.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame1.10"), "Label 2.1")
+ tabWidget2.addTab(ttk.TTkTestWidget( border=True, title="Frame1.9"), "Label Test 2.2")
+
+ tabWidget3.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame1.10"), "Label 3.1")
+ tabWidget3.addTab(ttk.TTkTestWidget( border=True, title="Frame1.9"), "Label Test 3.2")
fileMenu1 = tabWidget1.addMenu("XX")
fileMenu1.addMenu("Open")
diff --git a/docs/MDNotes/internals/focus.keypress.md b/docs/MDNotes/internals/focus.keypress.md
new file mode 100644
index 00000000..2d50b1b7
--- /dev/null
+++ b/docs/MDNotes/internals/focus.keypress.md
@@ -0,0 +1,70 @@
+# Current
+
+## Current Status of the focus/keypress logic
+
+```
+
+ └─▶ KeyEvent
+ └─▶ TTk._key_event()
+ try 1 ─▶ send KeyEvent to Focus Widget
+ try 2 ─▶ send KeyEvent to Shortcut Engine
+ try 3 ─▶ Check and handle for next Focus
+ try 4 ─▶ check and handle for prev focus
+```
+
+## Reworked Status of the focus/keypress logic
+
+1) Add default KeyPress handler
+
+ This handler is supposed to switch the focus (next/prev) or return False
+
+1) Add focus proxy/orchestrator/helper in TTkContainer (New Class? or internally Managed?)
+
+ ####ß Require (so far)
+
+ * Next Focus
+ * Prev Focus
+ * First Focus
+ * Last Focus
+ * Get Focussed
+ * Focus Widget
+ * UnFocus Widget
+ * Add Widget(s)
+ * Remove Widget(s)
+ * Insert Widget(s)
+
+1) Key Propagation
+
+ ```
+
+ └─▶ KeyEvent
+ └─▶ TTk.keyEvent(kevt)
+ try 1 ─▶ send KeyEvent to
+ └─▶ super().keyEvent(kevt) (TTkContainer)
+ try 1 : send key event to the focussed
+ if return False;
+ try 2 : if Tab/Right focus Next
+ try 3 : if ^Tab/Left focus Prev
+ If nothing execute return False
+ if not handled, the tab/direction key switch reached the last/first widget:
+ try 3 ─▶ Tab/Right focus the first
+ try 4 ─▶ ^Tab/Left focus the last
+ ```
+
+2) Focus Propagation
+
+ ```
+ ```
+
+# TODO
+
+[x] - Implement root handler to handle overlay widgets where the focus switch should be contained in the overlay
+[x] - Remove nextFocus,prevFocus from the helper
+[ ] - Investigate other widgets focus propagation
+[ ] - Switch Focus to the menu
+[ ] - Type TTkLayout and add docstrings
+[ ] - Add deprecated methods in ttkhelper
+[x] - Investigate lineedit of the combobox
+[x] - Tab Widget: Adapt to the new logic
+[x] - DateTime: Adapt to the new logic
+[ ] - Tab Widget: Apply Highlight colors
diff --git a/libs/pyTermTk/TermTk/TTkAbstract/abstractscrollview.py b/libs/pyTermTk/TermTk/TTkAbstract/abstractscrollview.py
index 087ab251..a28ba735 100644
--- a/libs/pyTermTk/TermTk/TTkAbstract/abstractscrollview.py
+++ b/libs/pyTermTk/TermTk/TTkAbstract/abstractscrollview.py
@@ -356,7 +356,7 @@ class TTkAbstractScrollViewGridLayout(TTkGridLayout, TTkAbstractScrollViewInterf
self._viewOffsetY == y: # Nothong to do
return
self._excludeEvent = True
- for widget in self.iterWidgets(recurse=False):
+ for widget in self.iterWidgets():
widget.viewMoveTo(x,y)
self._excludeEvent = False
self._viewOffsetX = x
@@ -397,7 +397,7 @@ class TTkAbstractScrollViewGridLayout(TTkGridLayout, TTkAbstractScrollViewInterf
# Override this function
def viewFullAreaSize(self) -> tuple[int,int]:
w,h=0,0
- for widget in self.iterWidgets(recurse=False):
+ for widget in self.iterWidgets():
ww,wh = widget.viewFullAreaSize()
w = max(w,ww)
h = max(h,wh)
@@ -406,7 +406,7 @@ class TTkAbstractScrollViewGridLayout(TTkGridLayout, TTkAbstractScrollViewInterf
# Override this function
def viewDisplayedSize(self) -> tuple[int,int]:
w,h=0,0
- for widget in self.iterWidgets(recurse=False):
+ for widget in self.iterWidgets():
ww,wh = widget.viewDisplayedSize()
w = max(w,ww)
h = max(h,wh)
diff --git a/libs/pyTermTk/TermTk/TTkCore/helper.py b/libs/pyTermTk/TermTk/TTkCore/helper.py
index b99938cc..1745e258 100644
--- a/libs/pyTermTk/TermTk/TTkCore/helper.py
+++ b/libs/pyTermTk/TermTk/TTkCore/helper.py
@@ -45,7 +45,6 @@ class TTkHelper:
This is a collection of helper utilities to be used all around TermTk
'''
- _focusWidget: Optional[TTkWidget] = None
_rootCanvas: Optional[TTkCanvas] = None
_rootWidget: Optional[TTk] = None
_updateWidget:Set[TTkWidget] = set()
@@ -55,14 +54,6 @@ class TTkHelper:
_cursor: bool = False
_cursorType: str = TTkTerm.Cursor.BLINKING_BLOCK
_cursorWidget: Optional[TTkWidget] = 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: list[TTkHelper._Overlay] = []
@staticmethod
def updateAll() -> None:
@@ -126,165 +117,43 @@ class TTkHelper:
def getTerminalSize() -> Tuple[int, int]:
return TTkGlbl.term_w, TTkGlbl.term_h
- @staticmethod
- def rootOverlay(widget: Optional[TTkWidget]) -> Optional[TTkWidget]:
- if not widget:
- 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
-
- @staticmethod
- def focusLastModal() -> None:
- if modal := TTkHelper.getLastModal():
- modal._widget.setFocus()
-
- @staticmethod
- def getLastModal() -> Optional[TTkHelper._Overlay]:
- modal = None
- for o in TTkHelper._overlay:
- if o._modal:
- modal = o
- return modal
-
@staticmethod
def checkModalOverlay(widget: TTkWidget) -> bool:
- #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
+ if not TTkHelper._rootWidget:
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
-
- @staticmethod
- def isOverlay(widget: Optional[TTkWidget]) -> bool:
- return TTkHelper.rootOverlay(widget) is not None
+ return TTkHelper._rootWidget._checkModalOverlay(widget)
@staticmethod
def overlay(caller: Optional[TTkWidget], widget: TTkWidget, x:int, y:int, modal:bool=False, forceBoundaries:bool=True, toolWindow:bool=False) -> None:
'''overlay'''
if not TTkHelper._rootWidget:
return
- 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()
-
- @staticmethod
- def getOverlay() -> Optional[TTkWidget]:
- if TTkHelper._overlay:
- return TTkHelper._overlay[-1]._widget
- return None
+ TTkHelper._rootWidget.overlay(
+ caller=caller,
+ widget=widget,
+ pos=(x,y),
+ modal=modal,
+ forceBoundaries=forceBoundaries,
+ toolWindow=toolWindow
+ )
@staticmethod
def removeOverlay() -> None:
if not TTkHelper._rootWidget:
return
- 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()
+ return TTkHelper._rootWidget._removeOverlay()
@staticmethod
def removeOverlayAndChild(widget: Optional[TTkWidget]) -> None:
- if not TTkHelper._rootWidget or not widget:
- return
- if not TTkHelper.isOverlay(widget):
+ if not TTkHelper._rootWidget:
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()
+ return TTkHelper._rootWidget._removeOverlayAndChild(widget=widget)
@staticmethod
def removeOverlayChild(widget: TTkWidget) -> None:
if not TTkHelper._rootWidget:
return
- 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()
+ return TTkHelper._rootWidget._removeOverlayChild(widget=widget)
@staticmethod
def setMousePos(pos: Tuple[int, int]) -> None:
@@ -419,74 +288,6 @@ class TTkHelper:
layout = layout.parent()
return (wx, wy)
- @staticmethod
- def nextFocus(widget: TTkWidget) -> None:
- from TermTk.TTkWidgets.container import TTkContainer
- if not TTkHelper._rootWidget:
- return
- rootWidget = TTkHelper.rootOverlay(widget)
- checkWidget:Optional[TTkWidget] = widget
- if not rootWidget:
- rootWidget = TTkHelper._rootWidget
- if checkWidget == rootWidget:
- checkWidget = None
- if not isinstance(rootWidget, TTkContainer):
- return
- 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 checkWidget:
- if w == checkWidget:
- checkWidget=None
- continue
- if w.isEnabled() and w.focusPolicy() & TTkK.TabFocus == TTkK.TabFocus:
- w.setFocus()
- w.update()
- return
- if first:
- first.setFocus()
- first.update()
-
- @staticmethod
- def prevFocus(widget: TTkWidget) -> None:
- from TermTk.TTkWidgets.container import TTkContainer
- if not TTkHelper._rootWidget:
- return
- rootWidget = TTkHelper.rootOverlay(widget)
- checkWidget:Optional[TTkWidget] = widget
- if not rootWidget:
- rootWidget = TTkHelper._rootWidget
- if checkWidget == rootWidget:
- checkWidget = None
- if not isinstance(rootWidget, TTkContainer):
- return
- prev = None
- for w in rootWidget.rootLayout().iterWidgets():
- # TTkLog.debug(f"{w._name} {widget}")
- if w == checkWidget:
- checkWidget=None
- if prev:
- break
- if w.isEnabled() and w.focusPolicy() & TTkK.TabFocus == TTkK.TabFocus:
- prev = w
- if prev:
- prev.setFocus()
- prev.update()
-
- @staticmethod
- def setFocus(widget: TTkWidget) -> None:
- TTkHelper._focusWidget = widget
-
- @staticmethod
- def getFocus() -> Optional[TTkWidget]:
- return TTkHelper._focusWidget
-
- @staticmethod
- def clearFocus() -> None:
- TTkHelper._focusWidget = None
-
@staticmethod
def showCursor(cursorType: int = TTkK.Cursor_Blinking_Block) -> None:
newType = {
diff --git a/libs/pyTermTk/TermTk/TTkCore/ttk.py b/libs/pyTermTk/TermTk/TTkCore/ttk.py
index fead7f3d..479b0385 100644
--- a/libs/pyTermTk/TermTk/TTkCore/ttk.py
+++ b/libs/pyTermTk/TermTk/TTkCore/ttk.py
@@ -30,7 +30,7 @@ import threading
import platform
import contextlib
-from typing import Optional, Callable, List
+from typing import Optional, List
from TermTk.TTkCore.drivers import TTkSignalDriver
from TermTk.TTkCore.TTkTerm.input import TTkInput
@@ -44,10 +44,9 @@ from TermTk.TTkCore.cfg import TTkCfg, TTkGlbl
from TermTk.TTkCore.helper import TTkHelper
from TermTk.TTkCore.timer import TTkTimer
from TermTk.TTkCore.color import TTkColor
-from TermTk.TTkCore.shortcut import TTkShortcut
from TermTk.TTkWidgets.about import TTkAbout
from TermTk.TTkWidgets.widget import TTkWidget
-from TermTk.TTkWidgets.container import TTkContainer
+from TermTk.TTkWidgets.rootcontainer import _TTkRootContainer
class _TTkStderrHandler(io.TextIOBase):
@@ -105,7 +104,7 @@ class _MouseCursor():
self.updated.emit()
-class TTk(TTkContainer):
+class TTk(_TTkRootContainer):
__slots__ = (
'_termMouse', '_termDirectMouse',
'_title',
@@ -288,7 +287,7 @@ class TTk(TTkContainer):
@pyTTkSlot(str)
def _processPaste(self, txt:str):
- if focusWidget := TTkHelper.getFocus():
+ if focusWidget := self._getFocusWidget():
while focusWidget and not focusWidget.pasteEvent(txt):
focusWidget = focusWidget.parentWidget()
@@ -296,7 +295,7 @@ class TTk(TTkContainer):
def _processInput(self, kevt, mevt):
with self._drawMutex:
if kevt is not None:
- self._key_event(kevt)
+ self.keyEvent(kevt)
if mevt is not None:
self._mouse_event(mevt)
@@ -320,7 +319,7 @@ class TTk(TTkContainer):
# Mouse Events forwarded straight to the Focus widget:
# - Drag
# - Release
- focusWidget = TTkHelper.getFocus()
+ focusWidget = self._getFocusWidget()
if ( focusWidget is not None and
( mevt.evt == TTkK.Drag or
mevt.evt == TTkK.Release ) and
@@ -343,32 +342,12 @@ class TTk(TTkContainer):
TTkHelper.dndEnter(None)
if mevt.evt == TTkK.Press and focusWidget:
focusWidget.clearFocus()
- TTkHelper.focusLastModal()
+ self._focusLastModal()
# Clean the Drag and Drop in case of mouse release
if mevt.evt == TTkK.Release:
TTkHelper.dndEnd()
- def _key_event(self, kevt):
- keyHandled = False
- # TTkLog.debug(f"Key: {kevt}")
- focusWidget = TTkHelper.getFocus()
- # TTkLog.debug(f"{focusWidget}")
- if focusWidget is not None:
- keyHandled = focusWidget.keyEvent(kevt)
- if not keyHandled:
- TTkShortcut.processKey(kevt, focusWidget)
- # Handle Next Focus Key Binding
- if not keyHandled and \
- ((kevt.key == TTkK.Key_Tab and kevt.mod == TTkK.NoModifier) or
- ( kevt.key == TTkK.Key_Right or kevt.key == TTkK.Key_Down)):
- TTkHelper.nextFocus(focusWidget if focusWidget else self)
- # Handle Prev Focus Key Binding
- if not keyHandled and \
- ((kevt.key == TTkK.Key_Tab and kevt.mod == TTkK.ShiftModifier) or
- ( kevt.key == TTkK.Key_Left or kevt.key == TTkK.Key_Up)):
- TTkHelper.prevFocus(focusWidget if focusWidget else self)
-
def _time_event(self):
# Event.{wait and clear} should be atomic,
# BUTt: ( y )
diff --git a/libs/pyTermTk/TermTk/TTkLayouts/layout.py b/libs/pyTermTk/TermTk/TTkLayouts/layout.py
index 97233ae6..4aac13bc 100644
--- a/libs/pyTermTk/TermTk/TTkLayouts/layout.py
+++ b/libs/pyTermTk/TermTk/TTkLayouts/layout.py
@@ -24,11 +24,18 @@
**Layout** [:ref:`Tutorial `]
'''
+from __future__ import annotations
+
__all__ = ['TTkLayoutItem', 'TTkLayout']
+from typing import TYPE_CHECKING,Generator
+
from TermTk.TTkCore.log import TTkLog
from TermTk.TTkCore.constant import TTkK
+if TYPE_CHECKING:
+ from TermTk.TTkWidgets.widget import TTkWidget
+
class TTkLayoutItem():
''' :py:class:`~TTkLayoutItem` is the base class of layout Items inherited by :py:class:`~TTkLayout`, :py:class:`~TTkWidgetItem`, and all the derived layout managers.
@@ -220,13 +227,29 @@ class TTkLayout(TTkLayoutItem):
else:
return self._parent.parentWidget()
- def iterWidgets(self, onlyVisible=True, recurse=True):
- for child in self._items:
+ def iterWidgets(
+ self,
+ onlyVisible: bool = True,
+ recurse: bool = True,
+ reverse: bool = False) -> Generator[TTkWidget, None, None]:
+ '''
+ Iterate over all widgets in the layout.
+
+ :param onlyVisible: if True, only yield visible widgets
+ :type onlyVisible: bool
+ :param recurse: if True, recursively iterate through nested layouts
+ :type recurse: bool
+ :param reverse: if True, iterate in reverse order
+ :type reverse: bool
+
+ :return: generator yielding widgets
+ :rtype: Generator[:py:class:`TTkWidget`, None, None]
+ '''
+ items = reversed(self._items) if reverse else self._items
+ for child in items:
if child._layoutItemType == TTkK.WidgetItem:
if onlyVisible and not child.widget().isVisible(): continue
yield child.widget()
- if recurse and hasattr(cw:=child.widget(),'rootLayout'):
- yield from cw.rootLayout().iterWidgets(onlyVisible, recurse)
if child._layoutItemType == TTkK.LayoutItem and recurse:
yield from child.iterWidgets(onlyVisible, recurse)
@@ -237,14 +260,10 @@ class TTkLayout(TTkLayoutItem):
def zSortedItems(self): return self._zSortedItems
def replaceItem(self, item, index):
- self._items[index] = item
- self._zSortItems()
- self.update()
- item.setParent(self)
- if item._layoutItemType == TTkK.WidgetItem:
- item.widget().setParent(self.parentWidget())
- if self.parentWidget():
- self.parentWidget().update(repaint=True, updateLayout=True)
+ if index < 0 or index >= len(self._items):
+ raise ValueError(f"The {index=} is not inside the items list")
+ self.removeItem(item=self._items[index])
+ self.insertItem(item=item,index=index)
def addItem(self, item):
self.insertItems(len(self._items),[item])
@@ -403,7 +422,8 @@ class TTkWidgetItem(TTkLayoutItem):
TTkLayoutItem.__init__(self, layoutItemType=TTkK.LayoutItemTypes.WidgetItem, **kwargs)
self._widget = widget
- def widget(self): return self._widget
+ def widget(self) -> TTkWidget:
+ return self._widget
def isVisible(self): return self._widget.isVisible()
diff --git a/libs/pyTermTk/TermTk/TTkWidgets/Fancy/tableview.py b/libs/pyTermTk/TermTk/TTkWidgets/Fancy/tableview.py
index 66798084..e96dfae7 100644
--- a/libs/pyTermTk/TermTk/TTkWidgets/Fancy/tableview.py
+++ b/libs/pyTermTk/TermTk/TTkWidgets/Fancy/tableview.py
@@ -443,7 +443,7 @@ class TTkFancyTableView(TTkAbstractScrollView):
self._viewOffsetY == y: # Nothong to do
return
self._excludeEvent = True
- for widget in self.layout().iterWidgets(recurse=False):
+ for widget in self.layout().iterWidgets():
widget.viewMoveTo(x,y)
self._excludeEvent = False
self._viewOffsetX = x
diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table_edit_proxy.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table_edit_proxy.py
index 850c9e08..573c9171 100644
--- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table_edit_proxy.py
+++ b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table_edit_proxy.py
@@ -507,7 +507,7 @@ class _DateTime_DateProxy(TTkDate, TTkTableProxyEditWidget, _DateTime_KeyGeneric
return self.newKeyEvent(evt,super().keyEvent)
-class _DateTime_DateTimeProxy(TTkDateTime, TTkTableProxyEditWidget):
+class _DateTime_DateTimeProxy(TTkDateTime, TTkTableProxyEditWidget, _DateTime_KeyGeneric):
''' DateTime editor for table cells
Extends :py:class:`TTkDateTime` with table-specific signals
@@ -565,15 +565,12 @@ class _DateTime_DateTimeProxy(TTkDateTime, TTkTableProxyEditWidget):
def keyEvent(self, evt: TTkKeyEvent) -> bool:
''' Handle keyboard events for navigation
- Always triggers right navigation on any key event.
-
:param evt: The keyboard event
:type evt: TTkKeyEvent
- :return: Always returns True
+ :return: True if event was handled, False otherwise
:rtype: bool
'''
- self.leavingTriggered.emit(TTkTableEditLeaving.RIGHT)
- return True
+ return self.newKeyEvent(evt,super().keyEvent)
class _TextPickerProxy(TTkTextPicker, TTkTableProxyEditWidget):
''' Rich text editor for table cells
diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py
index d3d6f1b4..a81c1c44 100644
--- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py
+++ b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py
@@ -1233,6 +1233,10 @@ class TTkTableWidget(TTkAbstractScrollView):
self.update()
def keyEvent(self, evt:TTkKeyEvent) -> bool:
+ if _pw:=self._edit_proxy_widget:
+ if _pw.widget.keyEvent(evt=evt):
+ return True
+
# rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
if self._currentPos:
diff --git a/libs/pyTermTk/TermTk/TTkWidgets/combobox.py b/libs/pyTermTk/TermTk/TTkWidgets/combobox.py
index 7b1163e1..0e90801f 100644
--- a/libs/pyTermTk/TermTk/TTkWidgets/combobox.py
+++ b/libs/pyTermTk/TermTk/TTkWidgets/combobox.py
@@ -441,7 +441,7 @@ class TTkComboBox(TTkContainer):
( evt.type == TTkK.SpecialKey and evt.key in [TTkK.Key_Enter,TTkK.Key_Down] ):
self._pressEvent()
return True
- return False
+ return super().keyEvent(evt=evt)
def focusInEvent(self) -> None:
if self._editable:
diff --git a/libs/pyTermTk/TermTk/TTkWidgets/container.py b/libs/pyTermTk/TermTk/TTkWidgets/container.py
index 6722423d..889fe9f3 100644
--- a/libs/pyTermTk/TermTk/TTkWidgets/container.py
+++ b/libs/pyTermTk/TermTk/TTkWidgets/container.py
@@ -20,15 +20,19 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
+from __future__ import annotations
+
__all__ = ['TTkContainer', 'TTkPadding']
-from typing import NamedTuple, Optional
+from typing import NamedTuple, Optional, List
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.shortcut import TTkShortcut
from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent
+from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent
from TermTk.TTkLayouts.layout import TTkLayout
from TermTk.TTkWidgets.widget import TTkWidget
@@ -151,6 +155,84 @@ class TTkContainer(TTkWidget):
self._layout.setParent(self)
self.update(updateLayout=True)
+ def _getFocusWidget(self) -> Optional[TTkWidget]:
+ if (_pw:=self.parentWidget()):
+ return _pw._getFocusWidget()
+ return None
+
+ def _setFocusWidget(self, widget:Optional[TTkWidget]) -> None:
+ if not (_pw:=self.parentWidget()):
+ return
+ _pw._setFocusWidget(widget)
+ self.update()
+
+ def _getFirstFocus(self, widget:Optional[TTkWidget], focusPolicy:TTkK.FocusPolicy) -> Optional[TTkWidget]:
+ widgets = list(self.layout().iterWidgets(onlyVisible=True,recurse=True,reverse=False))
+
+ if widget:
+ if widget not in (_lw:=widgets):
+ return None
+ index_widget = _lw.index(widget)
+ widgets = _lw[index_widget+1:]
+
+ for _w in widgets:
+ if focusPolicy & _w.focusPolicy():
+ return _w
+ if isinstance(_w,TTkContainer) and (_fw:=_w._getFirstFocus(widget=None,focusPolicy=focusPolicy)):
+ return _fw
+ return None
+
+ def _getLastFocus(self, widget:Optional[TTkWidget], focusPolicy:TTkK.FocusPolicy) -> Optional[TTkWidget]:
+ widgets = list(self.layout().iterWidgets(onlyVisible=True,recurse=True,reverse=True))
+
+ if widget:
+ if widget not in (_lw:=widgets):
+ return None
+ index_widget = _lw.index(widget)
+ widgets = _lw[index_widget+1:]
+
+ for _w in widgets:
+ if isinstance(_w,TTkContainer) and (_fw:=_w._getLastFocus(widget=None,focusPolicy=focusPolicy)):
+ return _fw
+ if focusPolicy & _w.focusPolicy():
+ return _w
+ return None
+
+ def _focusChildWidget(self) -> Optional[TTkWidget]:
+ if not (_fw := self._getFocusWidget()):
+ return None
+ while (_pw:=_fw.parentWidget()) and _pw is not self:
+ _fw = _pw
+ if _pw is self:
+ return _fw
+ return None
+
+ def keyEvent(self, evt:TTkKeyEvent) -> bool:
+ if (_cfw := self._focusChildWidget()) is not None:
+ if _cfw.keyEvent(evt):
+ return True
+
+ if TTkShortcut.processKey(evt, _cfw):
+ return True
+
+ # Handle Next Focus Key Binding
+ if ( (evt.key == TTkK.Key_Tab and evt.mod == TTkK.NoModifier) or
+ (evt.key in (TTkK.Key_Right, TTkK.Key_Down ) ) ) :
+ if _nfw:=self._getFirstFocus(widget=_cfw,focusPolicy=TTkK.FocusPolicy.TabFocus):
+ _nfw.setFocus()
+ return True
+ if ( (evt.key == TTkK.Key_Tab and evt.mod == TTkK.ShiftModifier) or
+ (evt.key in ( TTkK.Key_Left, TTkK.Key_Up ) ) ) :
+ if self._getFocusWidget() is self:
+ return False
+ if _pfw:=self._getLastFocus(widget=_cfw,focusPolicy=TTkK.FocusPolicy.TabFocus):
+ _pfw.setFocus()
+ return True
+ if _cfw and TTkK.FocusPolicy.TabFocus & self.focusPolicy():
+ self.setFocus()
+ return True
+ return False
+
def addWidget(self, widget:TTkWidget) -> None:
'''
.. warning::
@@ -165,7 +247,8 @@ class TTkContainer(TTkWidget):
parentWidget.layout().addWidget(childWidget)
'''
TTkLog.error(".addWidget(...) is deprecated, use .layout().addWidget(...)")
- if self.layout(): self.layout().addWidget(widget)
+ if self.layout():
+ self.layout().addWidget(widget)
def removeWidget(self, widget:TTkWidget) -> None:
'''
@@ -181,7 +264,8 @@ class TTkContainer(TTkWidget):
parentWidget.layout().removeWidget(childWidget)
'''
TTkLog.error(".removeWidget(...) is deprecated, use .layout().removeWidget(...)")
- if self.layout(): self.layout().removeWidget(widget)
+ if self.layout():
+ self.layout().removeWidget(widget)
# def forwardStyleTo(self, widget:TTkWidget):
# widget._currentStyle |= self._currentStyle
@@ -190,9 +274,9 @@ class TTkContainer(TTkWidget):
def _processForwardStyle(self) -> None:
if not self._forwardStyle: return
def _getChildren():
- for w in self.rootLayout().iterWidgets(onlyVisible=True, recurse=False):
+ for w in self.rootLayout().iterWidgets(onlyVisible=True):
yield w
- for w in self.layout().iterWidgets(onlyVisible=True, recurse=False):
+ for w in self.layout().iterWidgets(onlyVisible=True):
yield w
for w in _getChildren():
@@ -395,4 +479,7 @@ class TTkContainer(TTkWidget):
for w in self.rootLayout().iterWidgets(onlyVisible=False, recurse=True):
if w._name == name:
return w
+ if isinstance(w, TTkContainer):
+ if _w:=w.getWidgetByName(name):
+ return _w
return None
diff --git a/libs/pyTermTk/TermTk/TTkWidgets/datetime_date_form.py b/libs/pyTermTk/TermTk/TTkWidgets/datetime_date_form.py
index ebba8d65..59e75b27 100644
--- a/libs/pyTermTk/TermTk/TTkWidgets/datetime_date_form.py
+++ b/libs/pyTermTk/TermTk/TTkWidgets/datetime_date_form.py
@@ -550,7 +550,15 @@ class _TTkDateCal(TTkWidget):
'focus': {'color': TTkColor.fgbg("#888888","#000066")+TTkColor.UNDERLINE}
}
- __slots__ = ('_state')
+ __slots__ = ('_state', 'dateChanged')
+
+ dateChanged:pyTTkSignal
+ '''
+ This signal is emitted whenever the selected date changes.
+
+ :param date: The new selected date
+ :type date: :py:class:`datetime.date`
+ '''
_state:_TTkDateWidgetState
@@ -563,6 +571,7 @@ class _TTkDateCal(TTkWidget):
:param state: Shared state object for date management
:type state: :py:class:`_TTkDateWidgetState`
'''
+ self.dateChanged = pyTTkSignal(datetime.date)
self._state = state
super().__init__(**kwargs|{'size':(20,6)})
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
@@ -597,6 +606,7 @@ class _TTkDateCal(TTkWidget):
return True
elif evt.key == ' ':
self._state.setDate(self._state._highlighted)
+ self.dateChanged.emit(self._state._highlighted)
self.update()
return True
return False
@@ -625,6 +635,7 @@ class _TTkDateCal(TTkWidget):
if 0 <= y <= 5 and 0 <= x < 20:
if _d := self._getDayFromPos(x,y):
self._state.setDate(_d)
+ self.dateChanged.emit(_d)
self.update()
return True
@@ -753,12 +764,12 @@ class TTkDateForm(TTkContainer):
date = datetime.date.today()
self._state = _TTkDateWidgetState(date=date)
self._state.highlightedChanged.connect(self.update)
- self.dateChanged = self._state.dateChanged
size = (20,8)
super().__init__(**kwargs|{'size':size, 'minSize':size})
self._calWidget = _TTkDateCal(parent=self, pos=(0,2), state=self._state)
self._yearWidget = _TTkDateYear(parent=self, pos=(2,0), state=self._state)
self._monthWidget = _TTkDateMonth(parent=self, pos=(12,0), state=self._state)
+ self.dateChanged = self._calWidget.dateChanged
def date(self) -> datetime.date:
'''
diff --git a/libs/pyTermTk/TermTk/TTkWidgets/datetime_datetime.py b/libs/pyTermTk/TermTk/TTkWidgets/datetime_datetime.py
index 9d161edc..182aa83b 100644
--- a/libs/pyTermTk/TermTk/TTkWidgets/datetime_datetime.py
+++ b/libs/pyTermTk/TermTk/TTkWidgets/datetime_datetime.py
@@ -132,4 +132,4 @@ class TTkDateTime(TTkContainer):
self._datetime = datetime
self._dateWidget.setDate(datetime.date())
self._timeWidget.setTime(datetime.time())
- self.datetimeChanged.emit(datetime)
\ No newline at end of file
+ self.datetimeChanged.emit(datetime)
diff --git a/libs/pyTermTk/TermTk/TTkWidgets/rootcontainer.py b/libs/pyTermTk/TermTk/TTkWidgets/rootcontainer.py
new file mode 100644
index 00000000..b333cb33
--- /dev/null
+++ b/libs/pyTermTk/TermTk/TTkWidgets/rootcontainer.py
@@ -0,0 +1,393 @@
+# 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.
+
+__all__ = []
+
+from typing import Optional, List, Tuple
+
+from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent
+from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent
+from TermTk.TTkCore.constant import TTkK
+from TermTk.TTkCore.shortcut import TTkShortcut
+from TermTk.TTkWidgets.container import TTkContainer
+from TermTk.TTkWidgets.widget import TTkWidget
+
+def _absPos(widget: TTkWidget) -> 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)
+
+class _TTkOverlay():
+ __slots__ = ('_widget','_prevFocus','_modal')
+
+ _widget:TTkWidget
+ _prevFocus:Optional[TTkWidget]
+ _modal:bool
+
+ def __init__(
+ self,
+ pos:Tuple[int,int],
+ widget:TTkWidget,
+ prevFocus:Optional[TTkWidget],
+ modal:bool):
+ self._widget = widget
+ self._prevFocus = prevFocus
+ self._modal = modal
+ widget.move(*pos)
+
+class _TTkRootContainer(TTkContainer):
+ ''' _TTkRootContainer:
+
+ Internal root container class that manages the application's root widget hierarchy and focus navigation.
+
+ This class is not meant to be used directly by application code. It is instantiated internally by :py:class:`TTk`
+ to provide the top-level container for all widgets and handle keyboard-based focus traversal.
+
+ The root container manages focus cycling when Tab/Shift+Tab or arrow keys are pressed and no widget
+ consumes the event, ensuring focus loops back to the first/last focusable widget.
+ '''
+ __slots__ = (
+ '_focusWidget',
+ '_overlay')
+
+ _focusWidget:Optional[TTkWidget]
+ _overlay:List[_TTkOverlay]
+
+ def __init__(self, **kwargs) -> None:
+ self._focusWidget = None
+ self._overlay = []
+ super().__init__(**kwargs)
+
+ def _getFocusWidget(self) -> Optional[TTkWidget]:
+ '''
+ Returns the currently focused widget.
+
+ :return: the widget with focus, or None if no widget has focus
+ :rtype: :py:class:`TTkWidget` or None
+ '''
+ return self._focusWidget
+
+ def _setFocusWidget(self, widget:Optional[TTkWidget]) -> None:
+ '''
+ Sets the currently focused widget and triggers a repaint.
+
+ :param widget: the widget to receive focus, or None to clear focus
+ :type widget: :py:class:`TTkWidget` or None
+ '''
+ if self._focusWidget is widget:
+ return
+ self._focusWidget = widget
+ self.update()
+
+ def _loopFocus(self, container:TTkContainer, evt:TTkKeyEvent) -> bool:
+ if ( (evt.key == TTkK.Key_Tab and evt.mod == TTkK.NoModifier) or
+ (evt.key in (TTkK.Key_Right, TTkK.Key_Down ) ) ) :
+ if _nfw:=container._getFirstFocus(widget=None,focusPolicy=TTkK.FocusPolicy.TabFocus):
+ _nfw.setFocus()
+ return True
+ if ( (evt.key == TTkK.Key_Tab and evt.mod == TTkK.ShiftModifier) or
+ (evt.key in ( TTkK.Key_Left, TTkK.Key_Up ) ) ) :
+ if _pfw:=container._getLastFocus(widget=None,focusPolicy=TTkK.FocusPolicy.TabFocus):
+ _pfw.setFocus()
+ return True
+ return False
+
+ def _handleOverlay(self, evt:TTkKeyEvent) -> bool:
+ if not self._overlay:
+ return False
+ _overlay = self._overlay[-1]
+ _widget = _overlay._widget
+ if _widget.keyEvent(evt=evt):
+ return True
+ if isinstance(_widget, TTkContainer) and self._loopFocus(evt=evt, container=_widget):
+ return True
+
+ def overlay(self,
+ caller: Optional[TTkWidget],
+ widget: TTkWidget,
+ pos:Tuple[int,int],
+ modal:bool=False,
+ forceBoundaries:bool=True,
+ toolWindow:bool=False) -> None:
+ '''
+ Adds a widget as an overlay on top of the current widget hierarchy.
+
+ The overlay widget is positioned relative to the caller widget and automatically
+ adjusted to stay within the root container boundaries (if forceBoundaries is True).
+ Overlays can be modal (blocking interaction with underlying widgets) or non-modal.
+
+ :param caller: the widget relative to which the overlay is positioned, or None to use root
+ :type caller: :py:class:`TTkWidget` or None
+ :param widget: the widget to display as an overlay
+ :type widget: :py:class:`TTkWidget`
+ :param pos: the (x, y) position offset relative to the caller widget
+ :type pos: tuple[int, int]
+ :param modal: if True, blocks interaction with underlying widgets
+ :type modal: bool
+ :param forceBoundaries: if True, adjusts position and size to keep overlay within root boundaries
+ :type forceBoundaries: bool
+ :param toolWindow: if True, treats the overlay as a tool window without focus management
+ :type toolWindow: bool
+ '''
+ if not caller:
+ caller = self
+ x,y = pos
+ wx, wy = _absPos(caller)
+ w,h = widget.size()
+ rw,rh = self.rootLayout().size()
+
+ # Try to keep the overlay widget inside the rootContainer boundaries
+ if forceBoundaries:
+ wx = max(0, wx+x if wx+x+w < rw else rw-w )
+ wy = max(0, wy+y if wy+y+h < rh else rh-h )
+ mw,mh = widget.minimumSize()
+ ww = min(w,max(mw, rw))
+ wh = min(h,max(mh, rh))
+ widget.resize(ww,wh)
+ else:
+ wx += x
+ wy += y
+
+ wi = widget.widgetItem()
+ # Forcing the layer to:
+ # TTkLayoutItem.LAYER1 = 0x40000000
+ wi.setLayer(wi.LAYER1)
+
+ if toolWindow:
+ widget.move(wx,wy)
+ else:
+ _fw = self._getFocusWidget()
+ self._overlay.append(_TTkOverlay(
+ pos=(wx,wy),
+ widget=widget,
+ prevFocus=_fw,
+ modal=modal))
+
+ self.rootLayout().addWidget(widget)
+ widget.raiseWidget()
+ widget.setFocus()
+ if isinstance(widget, TTkContainer):
+ for w in widget.rootLayout().iterWidgets(onlyVisible=True):
+ w.update()
+
+ def _removeOverlay(self) -> None:
+ '''
+ Removes the topmost overlay widget and restores focus to the previous widget.
+
+ If the overlay is modal, it must be explicitly removed before any underlying
+ overlays can be removed. Focus is restored to the widget that had focus before
+ the overlay was added.
+ '''
+ if not self._overlay:
+ return
+ bkFocus = None
+ # Remove the first element also if it is modal
+ self._overlay[-1]._modal = False
+ while self._overlay:
+ if self._overlay[-1]._modal:
+ break
+ owidget = self._overlay.pop()
+ bkFocus = owidget._prevFocus
+ self.rootLayout().removeWidget(owidget._widget)
+ if _fw:=self._getFocusWidget():
+ _fw.clearFocus()
+ if bkFocus:
+ bkFocus.setFocus()
+
+ def _removeOverlayAndChild(self, widget: Optional[TTkWidget]) -> None:
+ '''
+ Removes the specified overlay widget and all overlays added after it.
+
+ This method finds the root overlay containing the given widget and removes it
+ along with any child overlays that were added on top of it. Focus is restored
+ to the widget that had focus before the overlay stack was created.
+
+ :param widget: the widget whose root overlay (and children) should be removed
+ :type widget: :py:class:`TTkWidget` or None
+ '''
+ if not widget:
+ return
+ if not self._isOverlay(widget):
+ return
+ if len(self._overlay) <= 1:
+ return self._removeOverlay()
+ rootWidget = self._rootOverlay(widget)
+ bkFocus = None
+ found = False
+ newOverlay = []
+ for o in self._overlay:
+ if o._widget == rootWidget:
+ found = True
+ bkFocus = o._prevFocus
+ if not found:
+ newOverlay.append(o)
+ else:
+ self.rootLayout().removeWidget(o._widget)
+ self._overlay = newOverlay
+ if bkFocus:
+ bkFocus.setFocus()
+ if not found:
+ self._removeOverlay()
+
+ def _removeOverlayChild(self, widget: TTkWidget) -> None:
+ '''
+ Removes all overlay widgets that were added after the specified widget.
+
+ This method preserves the overlay containing the given widget but removes
+ all overlays that were added on top of it. If the widget is not found in
+ the overlay stack, removes the topmost overlay.
+
+ :param widget: the widget whose child overlays should be removed
+ :type widget: :py:class:`TTkWidget`
+ '''
+ rootWidget = self._rootOverlay(widget)
+ found = False
+ newOverlay = []
+ for o in self._overlay:
+ if o._widget == rootWidget:
+ found = True
+ newOverlay.append(o)
+ continue
+ if not found:
+ newOverlay.append(o)
+ else:
+ self.rootLayout().removeWidget(o._widget)
+ self._overlay = newOverlay
+ if not found:
+ self._removeOverlay()
+
+ def _focusLastModal(self) -> None:
+ '''
+ Sets focus to the last modal overlay widget in the stack.
+
+ This method is used internally to ensure modal overlays maintain focus
+ when interaction is attempted with underlying widgets.
+ '''
+ if modal := self._getLastModal():
+ modal._widget.setFocus()
+
+ def _checkModalOverlay(self, widget: TTkWidget) -> bool:
+ '''
+ Checks if a widget is allowed to receive input given the current modal overlay state.
+
+ :param widget: the widget to check for input permission
+ :type widget: :py:class:`TTkWidget`
+
+ :return: True if the widget can receive input, False if blocked by a modal overlay
+ :rtype: bool
+ '''
+ #if not TTkHelper._overlay:
+ # # There are no Overlays
+ # return True
+
+ if not (lastModal := self._getLastModal()):
+ return True
+
+ # if not TTkHelper._overlay[-1]._modal:
+ # # The last window is not modal
+ # return True
+ if not (rootWidget := self._rootOverlay(widget)):
+ # This widget is not overlay
+ return False
+ if rootWidget in [ o._widget for o in self._overlay[self._overlay.index(lastModal):]]:
+ return True
+ # if TTkHelper._overlay[-1]._widget == rootWidget:
+ # return True
+ return False
+
+ def _getLastModal(self) -> Optional[_TTkOverlay]:
+ '''
+ Returns the last modal overlay in the stack.
+
+ :return: the last modal overlay wrapper, or None if no modal overlays exist
+ :rtype: :py:class:`_TTkOverlay` or None
+ '''
+ modal = None
+ for o in self._overlay:
+ if o._modal:
+ modal = o
+ return modal
+
+ def _isOverlay(self, widget: Optional[TTkWidget]) -> bool:
+ '''
+ Checks if a widget is part of the overlay hierarchy.
+
+ :param widget: the widget to check
+ :type widget: :py:class:`TTkWidget` or None
+
+ :return: True if the widget is contained in any overlay, False otherwise
+ :rtype: bool
+ '''
+ return self._rootOverlay(widget) is not None
+
+ def _rootOverlay(self, widget: Optional[TTkWidget]) -> Optional[TTkWidget]:
+ '''
+ Finds the root overlay widget that contains the specified widget.
+
+ Traverses the widget's parent hierarchy to find which overlay (if any)
+ contains it as a child.
+
+ :param widget: the widget to search for in the overlay hierarchy
+ :type widget: :py:class:`TTkWidget` or None
+
+ :return: the root overlay widget containing the specified widget, or None if not in any overlay
+ :rtype: :py:class:`TTkWidget` or None
+ '''
+ if not widget:
+ return None
+ if not self._overlay:
+ return None
+ overlayWidgets = [o._widget for o in self._overlay]
+ while widget is not None:
+ if widget in overlayWidgets:
+ return widget
+ widget = widget.parentWidget()
+ return None
+
+ def keyEvent(self, evt:TTkKeyEvent) -> bool:
+ '''
+ Handles keyboard events for focus navigation.
+
+ Implements focus cycling behavior when Tab/Shift+Tab or arrow keys are pressed
+ and no child widget consumes the event. When the last focusable widget is reached,
+ focus cycles back to the first widget (and vice versa).
+
+ :param evt: the keyboard event
+ :type evt: :py:class:`TTkKeyEvent`
+
+ :return: True if the event was handled, False otherwise
+ :rtype: bool
+ '''
+ if self._handleOverlay(evt=evt):
+ return True
+ if super().keyEvent(evt=evt):
+ return True
+
+ # If this is reached after a tab focus event, it means that either
+ # no focus widgets are defined
+ # or the last/first focus is reached - the focus need to go to start from the opposite side
+ return self._loopFocus(evt=evt, container=self)
diff --git a/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py b/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py
index d18340ae..ffe06435 100644
--- a/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py
+++ b/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py
@@ -23,7 +23,7 @@
__all__ = ['TTkTabButton', 'TTkTabBar', 'TTkTabWidget', 'TTkBarType']
from enum import Enum
-from typing import List, Tuple
+from typing import List, Tuple, Optional
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.helper import TTkHelper
@@ -89,11 +89,15 @@ _tabStyle = {
}
_tabStyleNormal = {
- 'default': {'borderColor': TTkColor.RST},
+ 'default': {'borderColor': TTkColor.RST},
+ 'disabled': {'borderColor': TTkColor.fg('#888888')},
+ 'focus': {'borderColor': TTkColor.RST},
}
_tabStyleFocussed = {
- 'default': {'borderColor': TTkColor.fg("#ffff00") + TTkColor.BOLD},
+ 'default': {'borderColor': TTkColor.fg("#ffff00") + TTkColor.BOLD},
+ 'disabled': {'borderColor': TTkColor.fg('#888888')},
+ 'focus': {'borderColor': TTkColor.fg("#ffff00") + TTkColor.BOLD},
}
@@ -188,7 +192,6 @@ class TTkTabButton(_TTkTabColorButton):
super().__init__(**kwargs)
self._closeButtonPressed = False
self._resetSize()
- self.setFocusPolicy(TTkK.ClickFocus)
def _resetSize(self):
style = self.currentStyle()
@@ -241,18 +244,18 @@ class TTkTabButton(_TTkTabColorButton):
self._closeButtonPressed = True
return True
return super().mouseReleaseEvent(evt)
+
def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool:
x,y = evt.x,evt.y
w,h = self.size()
offY = self._barType.offY()
- if parent:=self.parentWidget():
- parent.setFocus()
if self._closable and y == offY and w-4<=x bool:
drag = TTkDrag()
self._closeButtonPressed = False
@@ -363,7 +366,6 @@ class _TTkTabScrollerButton(_TTkTabColorButton):
self.resize(2, self._barType.vSize())
self.setMinimumSize(2, self._barType.vSize())
self.setMaximumSize(2, self._barType.vSize())
- self.setFocusPolicy(TTkK.ParentFocus)
def side(self):
return self._side
@@ -436,23 +438,41 @@ _labels= │◀│La│Label1║Label2║Label3│Label4│▶│
'''
class TTkTabBar(TTkContainer):
-
'''TTkTabBar'''
classStyle = _tabStyle
__slots__ = (
'_tabButtons', '_tabMovable', '_barType',
- '_highlighted', '_currentIndex','_lastIndex',
+ '_highlighted', '_currentIndex', '_lastIndex',
'_leftScroller', '_rightScroller',
'_tabClosable',
'_sideEnd',
#Signals
'currentChanged', 'tabBarClicked', 'tabCloseRequested')
+ currentChanged: pyTTkSignal
+ tabBarClicked: pyTTkSignal
+ tabCloseRequested: pyTTkSignal
+
+ _tabButtons:List[TTkTabButton]
+ _currentIndex:int
+ _lastIndex:int
+ _highlighted:int
+ _tabMovable:bool
+ _tabClosable:bool
+ _sideEnd:int
+ _barType:TTkBarType
+ _leftScroller:_TTkTabScrollerButton
+ _rightScroller:_TTkTabScrollerButton
+
def __init__(self, *,
closable:bool=False,
small:bool=True,
barType:TTkBarType=TTkBarType.NONE,
**kwargs) -> None:
+ self.currentChanged = pyTTkSignal(int)
+ self.tabBarClicked = pyTTkSignal(int)
+ self.tabCloseRequested = pyTTkSignal(int)
+
self._tabButtons:list[TTkTabButton] = []
self._currentIndex = -1
self._lastIndex = -1
@@ -474,12 +494,7 @@ class TTkTabBar(TTkContainer):
self.layout().addWidget(self._leftScroller)
self.layout().addWidget(self._rightScroller)
- # Signals
- self.currentChanged = pyTTkSignal(int)
- self.tabBarClicked = pyTTkSignal(int)
- self.tabCloseRequested = pyTTkSignal(int)
-
- self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
+ self.setFocusPolicy(TTkK.ParentFocus)
def mergeStyle(self, style):
super().mergeStyle(style)
@@ -709,6 +724,8 @@ class TTkTabWidget(TTkFrame):
'tabData', 'setTabData', 'currentData',
'currentIndex', 'setCurrentIndex', 'tabCloseRequested')
+ _tabWidgets:List[TTkWidget]
+
def __init__(self, *,
closable:bool=False,
barType:TTkBarType=TTkBarType.NONE,
@@ -729,8 +746,7 @@ class TTkTabWidget(TTkFrame):
self._topRightLayout = None
self._tabBar.currentChanged.connect(self._tabChanged)
- self.setFocusPolicy(self._tabBar.focusPolicy())
- self._tabBar.setFocusPolicy(TTkK.ParentFocus)
+ self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
self._spacer = TTkSpacer(parent=self)
@@ -761,6 +777,8 @@ class TTkTabWidget(TTkFrame):
self.tabBarClicked = self._tabBar.tabBarClicked
self.tabCloseRequested = self._tabBar.tabCloseRequested
+ self.tabBarClicked.connect(self.setFocus)
+
self.focusChanged.connect(self._focusChanged)
def _focusChanged(self, focus):
@@ -777,11 +795,11 @@ class TTkTabWidget(TTkFrame):
return self._tabWidgets.index(widget)
return -1
- def tabButton(self, index) -> TTkTabButton:
+ def tabButton(self, index:int) -> TTkTabButton:
'''tabButton'''
return self._tabBar.tabButton(index)
- def widget(self, index):
+ def widget(self, index:int) -> Optional[TTkWidget]:
'''widget'''
if 0 <= index < len(self._tabWidgets):
return self._tabWidgets[index]
@@ -795,7 +813,7 @@ class TTkTabWidget(TTkFrame):
return self._spacer
@pyTTkSlot(TTkWidget)
- def setCurrentWidget(self, widget):
+ def setCurrentWidget(self, widget:TTkWidget) -> None:
'''setCurrentWidget'''
for i, w in enumerate(self._tabWidgets):
if widget == w:
@@ -803,7 +821,7 @@ class TTkTabWidget(TTkFrame):
break
@pyTTkSlot(int)
- def _tabChanged(self, index):
+ def _tabChanged(self, index:int) -> None:
self._spacer.show()
for i, widget in enumerate(self._tabWidgets):
if index == i:
@@ -813,7 +831,16 @@ class TTkTabWidget(TTkFrame):
widget.hide()
def keyEvent(self, evt:TTkKeyEvent) -> bool:
- return self._tabBar.keyEvent(evt)
+ if self.hasFocus() and self._tabBar.keyEvent(evt=evt):
+ return True
+ return super().keyEvent(evt)
+
+ def mousePressEvent(self, evt:TTkMouseEvent) -> bool:
+ return True
+ def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool:
+ return True
+ def mouseTapEvent(self, evt:TTkMouseEvent) -> bool:
+ return True
def _dropNewTab(self, x:int, y:int, data:_TTkNewTabWidgetDragData) -> None:
w = data.widget()
diff --git a/libs/pyTermTk/TermTk/TTkWidgets/widget.py b/libs/pyTermTk/TermTk/TTkWidgets/widget.py
index 9b07f240..d3868fc4 100644
--- a/libs/pyTermTk/TermTk/TTkWidgets/widget.py
+++ b/libs/pyTermTk/TermTk/TTkWidgets/widget.py
@@ -43,7 +43,7 @@ from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent
if TYPE_CHECKING:
from TermTk import TTkContainer
-class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
+class TTkWidget(TMouseEvents, TKeyEvents, TDragEvents):
''' Widget sizes:
::
@@ -110,7 +110,7 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
'_name', '_parent',
'_x', '_y', '_width', '_height',
'_maxw', '_maxh', '_minw', '_minh',
- '_focus','_focus_policy',
+ '_focus_policy',
'_canvas', '_widgetItem',
'_visible',
'_pendingMouseRelease',
@@ -132,7 +132,6 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
_maxh:int
_minw:int
_minh:int
- _focus:bool
_focus_policy:TTkK.FocusPolicy
_canvas:TTkCanvas
_widgetItem:TTkWidgetItem
@@ -229,7 +228,7 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
self._widgetCursorType = TTkK.Cursor_Blinking_Bar
self._name = name if name else self.__class__.__name__
- self._parent:TTkWidget = parent
+ self._parent = parent
self._pendingMouseRelease = False
@@ -239,7 +238,6 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
self._maxw, self._maxh = maxSize if maxSize else (maxWidth,maxHeight)
self._minw, self._minh = minSize if minSize else (minWidth,minHeight)
- self._focus = False
self._focus_policy = TTkK.NoFocus
self._visible = visible
@@ -710,30 +708,29 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
@pyTTkSlot()
def setFocus(self) -> None:
'''Focus the widget'''
- # TTkLog.debug(f"setFocus: {self._name} - {self._focus}")
- if self._focus and self == TTkHelper.getFocus(): return
- tmp = TTkHelper.getFocus()
- if tmp == self: return
- if tmp is not None:
- tmp.clearFocus()
- TTkHelper.setFocus(self)
- self._focus = True
- self.focusChanged.emit(self._focus)
+ if not (_p:=self._parent):
+ return
+ if (_old_fw:=_p._getFocusWidget()) is self:
+ return
+ if _old_fw:
+ _old_fw.clearFocus()
+ _p._setFocusWidget(self)
+ self.focusChanged.emit(True)
self.focusInEvent()
- TTkHelper.removeOverlayChild(self)
- self._pushWidgetCursor()
self._processStyleEvent(TTkWidget._S_DEFAULT)
+ self._pushWidgetCursor()
+ TTkHelper.removeOverlayChild(self)
+ self.update()
def clearFocus(self) -> None:
'''Remove the Focus state of this widget'''
- # TTkLog.debug(f"clearFocus: {self._name} - {self._focus}")
- if not self._focus and self != TTkHelper.getFocus(): return
- TTkHelper.clearFocus()
- self._focus = False
- self.focusChanged.emit(self._focus)
+ if not (_p:=self._parent) or _p._getFocusWidget() is not self:
+ return
+ _p._setFocusWidget(None)
+ self.focusChanged.emit(False)
self.focusOutEvent()
self._processStyleEvent(TTkWidget._S_DEFAULT)
- self.update(repaint=True, updateLayout=False)
+ self.update()
def hasFocus(self) -> bool:
'''
@@ -741,7 +738,7 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
:return: bool
'''
- return self._focus
+ return bool((_p:=self._parent) and _p._getFocusWidget() is self)
def getCanvas(self) -> TTkCanvas:
return self._canvas
@@ -926,7 +923,7 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
def _pushWidgetCursor(self):
if ( self._widgetCursorEnabled and
self._visible and
- ( self._focus or self == TTkHelper.cursorWidget() ) ):
+ ( self.hasFocus() or self == TTkHelper.cursorWidget() ) ):
cx,cy = self._widgetCursor
ax, ay = TTkHelper.absPos(self)
if ( self == TTkHelper.widgetAt(cx+ax, cy+ay) or
diff --git a/tests/pytest/widgets/test_add_remove_widget.py b/tests/pytest/widgets/test_add_remove_widget.py
new file mode 100644
index 00000000..4bb5a15f
--- /dev/null
+++ b/tests/pytest/widgets/test_add_remove_widget.py
@@ -0,0 +1,430 @@
+# 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 sys, os
+
+sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk'))
+import TermTk as ttk
+
+
+def test_add_widget_to_container():
+ '''
+ Test that adding widgets to a container using addWidget() correctly sets the parent relationship.
+ Verifies that widgets initially have no parent, and after being added to a container,
+ their parentWidget() returns the container.
+ '''
+ container = ttk.TTkContainer()
+ widget1 = ttk.TTkWidget()
+ widget2 = ttk.TTkWidget()
+
+ assert widget1.parentWidget() is None
+ assert widget2.parentWidget() is None
+
+ container.addWidget(widget1)
+ assert widget1.parentWidget() is container
+
+ container.addWidget(widget2)
+ assert widget2.parentWidget() is container
+
+def test_add_widget_to_container_02():
+ '''
+ Test that widgets added via parent= constructor parameter correctly establish
+ the parent-child relationship immediately upon construction.
+ '''
+ container = ttk.TTkContainer()
+ widget1 = ttk.TTkWidget(parent=container)
+ widget2 = ttk.TTkWidget(parent=container)
+
+ assert widget1.parentWidget() is container
+ assert widget2.parentWidget() is container
+
+def test_remove_widget_from_container():
+ '''
+ Test that removing a widget from a container using removeWidget() correctly
+ unsets the parent relationship, returning the widget to an orphaned state.
+ '''
+ container = ttk.TTkContainer()
+ widget = ttk.TTkWidget()
+
+ container.addWidget(widget)
+ assert widget.parentWidget() is container
+
+ container.removeWidget(widget)
+ assert widget.parentWidget() is None
+
+def test_remove_widget_from_container_02():
+ '''
+ Test that removing a widget that was added via parent= constructor parameter
+ correctly unsets the parent relationship.
+ '''
+ container = ttk.TTkContainer()
+ widget = ttk.TTkWidget(parent=container)
+
+ assert widget.parentWidget() is container
+ container.removeWidget(widget)
+ assert widget.parentWidget() is None
+
+def test_gridlayout_add_remove():
+ '''
+ Test :py:class:`TTkGridLayout` add/remove operations using container.layout().addWidget().
+ Verifies that widgets added to specific grid positions have their parent correctly set
+ to the container, and that removing widgets unsets the parent relationship while
+ preserving other widgets' parents.
+ '''
+ container = ttk.TTkContainer()
+ layout = ttk.TTkGridLayout()
+ container.setLayout(layout)
+
+ widget1 = ttk.TTkWidget()
+ widget2 = ttk.TTkWidget()
+ widget3 = ttk.TTkWidget()
+
+ # Add widgets to grid using container.layout()
+ container.layout().addWidget(widget1, 0, 0)
+ container.layout().addWidget(widget2, 0, 1)
+ container.layout().addWidget(widget3, 1, 0)
+
+ assert widget1.parentWidget() is container
+ assert widget2.parentWidget() is container
+ assert widget3.parentWidget() is container
+
+ # Remove widget using container.layout()
+ container.layout().removeWidget(widget2)
+ assert widget2.parentWidget() is None
+ assert widget1.parentWidget() is container
+ assert widget3.parentWidget() is container
+
+
+def test_hboxlayout_add_remove():
+ '''
+ Test :py:class:`TTkHBoxLayout` add/remove operations using container.layout().addWidget().
+ Verifies that widgets are correctly parented when added sequentially, and that
+ removing specific widgets from the middle of the layout updates their parent
+ relationships while preserving others.
+ '''
+ container = ttk.TTkContainer()
+ layout = ttk.TTkHBoxLayout()
+ container.setLayout(layout)
+
+ widgets = [ttk.TTkWidget() for _ in range(4)]
+
+ # Add all widgets using container.layout()
+ for widget in widgets:
+ container.layout().addWidget(widget)
+ assert widget.parentWidget() is container
+
+ # Remove middle widgets using container.layout()
+ container.layout().removeWidget(widgets[1])
+ container.layout().removeWidget(widgets[2])
+
+ assert widgets[0].parentWidget() is container
+ assert widgets[1].parentWidget() is None
+ assert widgets[2].parentWidget() is None
+ assert widgets[3].parentWidget() is container
+
+
+def test_vboxlayout_add_remove():
+ '''
+ Test :py:class:`TTkVBoxLayout` add/remove operations using container.layout().addWidget().
+ Verifies parent relationships when adding button widgets vertically, and that
+ removing the first widget correctly unsets its parent while preserving others.
+ '''
+ container = ttk.TTkContainer()
+ layout = ttk.TTkVBoxLayout()
+ container.setLayout(layout)
+
+ widget1 = ttk.TTkButton(text='Button 1')
+ widget2 = ttk.TTkButton(text='Button 2')
+ widget3 = ttk.TTkButton(text='Button 3')
+
+ container.layout().addWidget(widget1)
+ container.layout().addWidget(widget2)
+ container.layout().addWidget(widget3)
+
+ assert widget1.parentWidget() is container
+ assert widget2.parentWidget() is container
+ assert widget3.parentWidget() is container
+
+ # Remove first widget using container.layout()
+ container.layout().removeWidget(widget1)
+ assert widget1.parentWidget() is None
+ assert widget2.parentWidget() is container
+
+
+def test_nested_layouts():
+ '''
+ Test parent relationships with nested layouts using container.layout().addWidget().
+ Verifies that when a container with its own layout is added to another container,
+ the parent relationships form a proper hierarchy. Widgets remain parented to their
+ immediate container even when that container is removed from the root.
+ '''
+ root = ttk.TTkContainer()
+ root_layout = ttk.TTkVBoxLayout()
+ root.setLayout(root_layout)
+
+ # Create nested container
+ nested = ttk.TTkContainer()
+ nested_layout = ttk.TTkHBoxLayout()
+ nested.setLayout(nested_layout)
+
+ widget1 = ttk.TTkWidget()
+ widget2 = ttk.TTkWidget()
+
+ # Add nested container to root using root.layout()
+ root.layout().addWidget(nested)
+ assert nested.parentWidget() is root
+
+ # Add widgets to nested layout using nested.layout()
+ nested.layout().addWidget(widget1)
+ nested.layout().addWidget(widget2)
+
+ assert widget1.parentWidget() is nested
+ assert widget2.parentWidget() is nested
+
+ # Remove nested container using root.layout()
+ root.layout().removeWidget(nested)
+ assert nested.parentWidget() is None
+ assert widget1.parentWidget() is nested # Still parented to nested
+
+
+def test_replace_widget_in_layout():
+ '''
+ Test that replacing a widget in the same layout position correctly maintains
+ parent relationships using container.layout() operations. The removed widget
+ should have no parent, while the replacement widget should be parented to the container.
+ '''
+ container = ttk.TTkContainer()
+ layout = ttk.TTkGridLayout()
+ container.setLayout(layout)
+
+ widget1 = ttk.TTkWidget()
+ widget2 = ttk.TTkWidget()
+
+ container.layout().addWidget(widget1, 0, 0)
+ assert widget1.parentWidget() is container
+
+ # Replace widget1 with widget2 using container.layout()
+ container.layout().removeWidget(widget1)
+ container.layout().addWidget(widget2, 0, 0)
+
+ assert widget1.parentWidget() is None
+ assert widget2.parentWidget() is container
+
+
+def test_move_widget_between_containers():
+ '''
+ Test that moving a widget between containers correctly updates the parent relationship.
+ The widget should first be parented to the first container, then have no parent briefly,
+ then be parented to the second container.
+ '''
+ container1 = ttk.TTkContainer()
+ container2 = ttk.TTkContainer()
+ widget = ttk.TTkWidget()
+
+ container1.addWidget(widget)
+ assert widget.parentWidget() is container1
+
+ # Move to second container
+ container1.removeWidget(widget)
+ container2.addWidget(widget)
+
+ assert widget.parentWidget() is container2
+
+
+def test_complex_layout_operations():
+ '''
+ Test complex add/remove operations across multiple layout types using container.layout().addWidget().
+ Creates a hierarchy with :py:class:`TTkVBoxLayout`, :py:class:`TTkHBoxLayout`, and :py:class:`TTkGridLayout`.
+ Verifies that all parent relationships are correctly maintained throughout the hierarchy,
+ and that removing a section of the layout tree properly updates parent relationships.
+ '''
+ root = ttk.TTkContainer()
+ vbox = ttk.TTkVBoxLayout()
+ root.setLayout(vbox)
+
+ # Create horizontal section
+ hbox_container = ttk.TTkContainer()
+ hbox = ttk.TTkHBoxLayout()
+ hbox_container.setLayout(hbox)
+
+ # Create grid section
+ grid_container = ttk.TTkContainer()
+ grid = ttk.TTkGridLayout()
+ grid_container.setLayout(grid)
+
+ widgets = [ttk.TTkButton(text=f'Btn{i}') for i in range(6)]
+
+ # Add to vbox using root.layout()
+ root.layout().addWidget(hbox_container)
+ root.layout().addWidget(grid_container)
+
+ # Add to hbox using hbox_container.layout()
+ hbox_container.layout().addWidget(widgets[0])
+ hbox_container.layout().addWidget(widgets[1])
+
+ # Add to grid using grid_container.layout()
+ grid_container.layout().addWidget(widgets[2], 0, 0)
+ grid_container.layout().addWidget(widgets[3], 0, 1)
+ grid_container.layout().addWidget(widgets[4], 1, 0)
+ grid_container.layout().addWidget(widgets[5], 1, 1)
+
+ # Verify all parents
+ assert hbox_container.parentWidget() is root
+ assert grid_container.parentWidget() is root
+ assert widgets[0].parentWidget() is hbox_container
+ assert widgets[1].parentWidget() is hbox_container
+ assert widgets[2].parentWidget() is grid_container
+ assert widgets[3].parentWidget() is grid_container
+
+ # Remove grid section using root.layout()
+ root.layout().removeWidget(grid_container)
+ assert grid_container.parentWidget() is None
+ assert widgets[2].parentWidget() is grid_container # Still parented to grid_container
+
+ # Remove widgets from hbox using hbox_container.layout()
+ hbox_container.layout().removeWidget(widgets[0])
+ assert widgets[0].parentWidget() is None
+ assert widgets[1].parentWidget() is hbox_container
+
+def test_nested_layout_widgets_01():
+ '''
+ Test that widgets added to a nested layout (one layout containing another) correctly
+ inherit the parent from the container that owns the root layout. Widgets added to
+ the nested layout should be parented to the container, not to the layout itself.
+ '''
+ layout1 = ttk.TTkLayout()
+ layout2 = ttk.TTkLayout()
+
+ layout1.addItem(layout2)
+
+ container = ttk.TTkContainer(layout=layout1)
+
+ widget1 = ttk.TTkWidget()
+ widget2 = ttk.TTkWidget()
+
+ assert widget1.parentWidget() is None
+ assert widget2.parentWidget() is None
+
+ layout2.addWidget(widget1)
+ assert widget1.parentWidget() is container
+
+ layout2.addWidget(widget2)
+ assert widget2.parentWidget() is container
+
+def test_nested_layout_widgets_02():
+ '''
+ Test that widgets added to a nested layout before the layout is attached to a container
+ get their parent set correctly when setLayout() is called. Also verifies that removing
+ the nested layout from the parent layout unsets the widgets' parents.
+ '''
+ layout1 = ttk.TTkLayout()
+ layout2 = ttk.TTkLayout()
+
+ layout1.addItem(layout2)
+
+ container = ttk.TTkContainer()
+
+ widget1 = ttk.TTkWidget()
+ widget2 = ttk.TTkWidget()
+
+ layout2.addWidget(widget1)
+ layout2.addWidget(widget2)
+
+ assert widget1.parentWidget() is None
+ assert widget2.parentWidget() is None
+
+ container.setLayout(layout1)
+
+ assert widget1.parentWidget() is container
+ assert widget2.parentWidget() is container
+
+ layout1.removeItem(layout2)
+
+ assert widget1.parentWidget() is None
+ assert widget2.parentWidget() is None
+
+def test_nested_layout_widgets_03():
+ '''
+ Test that replacing a container's layout with a different layout correctly unsets
+ the parent relationships for widgets in the old layout's hierarchy. Widgets in
+ nested layouts should have their parents cleared when the root layout is replaced.
+ '''
+ layout1 = ttk.TTkLayout()
+ layout2 = ttk.TTkLayout()
+ layout3 = ttk.TTkLayout()
+
+ layout1.addItem(layout2)
+
+ container = ttk.TTkContainer()
+
+ widget1 = ttk.TTkWidget()
+ widget2 = ttk.TTkWidget()
+
+ layout2.addWidget(widget1)
+ layout2.addWidget(widget2)
+
+ assert widget1.parentWidget() is None
+ assert widget2.parentWidget() is None
+
+ container.setLayout(layout1)
+
+ assert widget1.parentWidget() is container
+ assert widget2.parentWidget() is container
+
+ container.setLayout(layout3)
+
+ assert widget1.parentWidget() is None
+ assert widget2.parentWidget() is None
+
+def test_nested_layout_widgets_04():
+ '''
+ Test complex parent relationships where a container with its own child widgets
+ is added to a nested layout within another container. Verifies that the container
+ maintains its parent relationship to its widgets, while also being correctly parented
+ to the outer container. Removing the nested layout should only affect the intermediate
+ container's parent, not the leaf widgets' relationship to their immediate parent.
+ '''
+ layout1 = ttk.TTkLayout()
+ layout2 = ttk.TTkLayout()
+
+ layout1.addItem(layout2)
+
+ container1 = ttk.TTkContainer(layout=layout1)
+ container2 = ttk.TTkContainer()
+
+ widget1 = ttk.TTkWidget(parent=container2)
+ widget2 = ttk.TTkWidget(parent=container2)
+
+ assert container2.parentWidget() is None
+ assert widget1.parentWidget() is container2
+ assert widget2.parentWidget() is container2
+
+ layout2.addWidget(container2)
+ assert container2.parentWidget() is container1
+ assert widget1.parentWidget() is container2
+ assert widget2.parentWidget() is container2
+
+ layout1.removeItem(layout2)
+ assert container2.parentWidget() is None
+ assert widget1.parentWidget() is container2
+ assert widget2.parentWidget() is container2
+
diff --git a/tests/pytest/widgets/test_focus_01.py b/tests/pytest/widgets/test_focus_01.py
new file mode 100644
index 00000000..47333cb4
--- /dev/null
+++ b/tests/pytest/widgets/test_focus_01.py
@@ -0,0 +1,202 @@
+# 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 sys, os
+
+sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk'))
+import TermTk as ttk
+
+def test_focus_01_no_root():
+ '''
+ Container ─┬─▶ Widget1
+ ├─▶ Widget3
+ └─▶ Widget3
+ '''
+ container = ttk.TTkContainer()
+ widget1 = ttk.TTkWidget(parent=container)
+ widget2 = ttk.TTkWidget(parent=container)
+ widget3 = ttk.TTkWidget(parent=container)
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ widget2.setFocus()
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ widget2.clearFocus()
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+def test_focus_01_with_root():
+ '''
+ Container ─┬─▶ Widget1
+ ├─▶ Widget3
+ └─▶ Widget3
+ '''
+ root = ttk.TTk()
+ widget1 = ttk.TTkWidget(parent=root)
+ widget2 = ttk.TTkWidget(parent=root)
+ widget3 = ttk.TTkWidget(parent=root)
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ widget2.setFocus()
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ widget2.clearFocus()
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+def test_focus_02():
+ '''
+ Root ─▶ Container ─┬─▶ Widget1
+ ├─▶ Widget2
+ └─▶ Widget3
+ '''
+ root = ttk.TTk()
+ container = ttk.TTkContainer(parent=root)
+ widget1 = ttk.TTkWidget(parent=container)
+ widget2 = ttk.TTkWidget(parent=container)
+ widget3 = ttk.TTkWidget(parent=container)
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ widget2.setFocus()
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ widget2.clearFocus()
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+def test_focus_03_sequential():
+ '''Test sequential focus changes between widgets'''
+ root = ttk.TTk()
+ widget1 = ttk.TTkWidget(parent=root)
+ widget2 = ttk.TTkWidget(parent=root)
+ widget3 = ttk.TTkWidget(parent=root)
+
+ widget1.setFocus()
+ assert False is root.hasFocus()
+ assert True is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ widget2.setFocus()
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ widget3.setFocus()
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+
+def test_focus_04_nested_containers():
+ '''
+ Root ─▶ Container1 ─┬─▶ Widget1
+ └─▶ Container2 ─┬─▶ Widget2
+ └─▶ Widget3
+ '''
+ root = ttk.TTk()
+ container1 = ttk.TTkContainer(parent=root)
+ container2 = ttk.TTkContainer(parent=container1)
+ widget1 = ttk.TTkWidget(parent=container1)
+ widget2 = ttk.TTkWidget(parent=container2)
+ widget3 = ttk.TTkWidget(parent=container2)
+
+ widget2.setFocus()
+ assert False is container1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ widget1.setFocus()
+ assert False is container1.hasFocus()
+ assert False is container2.hasFocus()
+ assert True is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+def test_focus_05_multiple_setFocus():
+ '''Test calling setFocus multiple times on same widget'''
+ root = ttk.TTk()
+ widget1 = ttk.TTkWidget(parent=root)
+ widget2 = ttk.TTkWidget(parent=root)
+
+ widget1.setFocus()
+ widget1.setFocus()
+ assert (widget1.hasFocus(), widget2.hasFocus()) == (True, False)
+
+def test_focus_06_clearFocus_without_focus():
+ '''Test clearFocus on widget that doesn't have focus'''
+ root = ttk.TTk()
+ widget1 = ttk.TTkWidget(parent=root)
+ widget2 = ttk.TTkWidget(parent=root)
+
+ widget1.setFocus()
+ widget2.clearFocus()
+ assert (widget1.hasFocus(), widget2.hasFocus()) == (True, False)
+
+def test_focus_07_single_widget():
+ '''Test focus behavior with single widget'''
+ root = ttk.TTk()
+ widget = ttk.TTkWidget(parent=root)
+
+ assert (root.hasFocus(), widget.hasFocus()) == (False, False)
+
+ widget.setFocus()
+ assert (root.hasFocus(), widget.hasFocus()) == (False, True)
+
+ widget.clearFocus()
+ assert (root.hasFocus(), widget.hasFocus()) == (False, False)
\ No newline at end of file
diff --git a/tests/pytest/widgets/test_focus_02_first_focus.py b/tests/pytest/widgets/test_focus_02_first_focus.py
new file mode 100644
index 00000000..f4ab7bff
--- /dev/null
+++ b/tests/pytest/widgets/test_focus_02_first_focus.py
@@ -0,0 +1,113 @@
+# 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 sys, os
+
+sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk'))
+import TermTk as ttk
+
+
+def test_next_prev_widget_01():
+ '''
+ Container ─┬─▶ Widget1
+ ├─▶ Widget2
+ └─▶ Widget3
+ '''
+ container = ttk.TTkContainer()
+ widget1 = ttk.TTkWidget(parent=container)
+ widget2 = ttk.TTkWidget(parent=container)
+ widget3 = ttk.TTkWidget(parent=container)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ # Forward
+ ff = container._getFirstFocus(widget=None, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget1
+ ff = container._getFirstFocus(widget=widget1, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget2
+ ff = container._getFirstFocus(widget=widget2, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget3
+ ff = container._getFirstFocus(widget=widget3, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is None
+
+ # Reverse
+ ff = container._getLastFocus(widget=None, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget3
+ ff = container._getLastFocus(widget=widget3, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget2
+ ff = container._getLastFocus(widget=widget2, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget1
+ ff = container._getLastFocus(widget=widget1, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is None
+
+def test_next_prev_widget_02_nested():
+ '''
+ Root ─▶ Container1 ─┬─▶ Widget1
+ └─▶ Container2 ─┬─▶ Widget2
+ ├─▶ Widget3
+ └─▶ Widget4
+ '''
+ root = ttk.TTk()
+ container1 = ttk.TTkContainer(parent=root)
+ widget1 = ttk.TTkWidget(parent=container1)
+ container2 = ttk.TTkContainer(parent=container1)
+ widget2 = ttk.TTkWidget(parent=container2)
+ widget3 = ttk.TTkWidget(parent=container2)
+ widget4 = ttk.TTkWidget(parent=container2)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget4.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ # Forward
+ ff = container1._getFirstFocus(widget=None, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget1
+ ff = container2._getFirstFocus(widget=None, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget2
+
+ ff = container1._getFirstFocus(widget=widget1, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget2
+
+ ff = container2._getFirstFocus(widget=widget2, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget3
+ ff = container2._getFirstFocus(widget=widget3, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget4
+ ff = container2._getFirstFocus(widget=widget4, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is None
+
+ # Reverse
+ ff = container1._getLastFocus(widget=None, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget4
+ ff = container2._getLastFocus(widget=None, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget4
+
+ ff = container1._getLastFocus(widget=widget1, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is None
+
+ ff = container2._getLastFocus(widget=widget4, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget3
+ ff = container2._getLastFocus(widget=widget3, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is widget2
+ ff = container2._getLastFocus(widget=widget2, focusPolicy=ttk.TTkK.FocusPolicy.TabFocus)
+ assert ff is None
+
diff --git a/tests/pytest/widgets/test_focus_02_tab.py b/tests/pytest/widgets/test_focus_02_tab.py
new file mode 100644
index 00000000..bfef5061
--- /dev/null
+++ b/tests/pytest/widgets/test_focus_02_tab.py
@@ -0,0 +1,556 @@
+# 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 sys, os
+
+sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk'))
+import TermTk as ttk
+
+
+def test_focus_01_tab():
+ '''
+ Container ─┬─▶ Widget1
+ ├─▶ Widget2
+ └─▶ Widget3
+ '''
+ root = ttk.TTk()
+ widget1 = ttk.TTkWidget(parent=root)
+ widget2 = ttk.TTkWidget(parent=root)
+ widget3 = ttk.TTkWidget(parent=root)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ widget2.setFocus()
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ tab_key = ttk.TTkKeyEvent(
+ type=ttk.TTkK.KeyType.SpecialKey,
+ key=ttk.TTkK.Key_Tab,
+ mod=ttk.TTkK.NoModifier,
+ code='',)
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert True is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+def test_focus_01_tab_reverse():
+ '''
+ Container ─┬─▶ Widget1
+ ├─▶ Widget2
+ └─▶ Widget3
+ '''
+ root = ttk.TTk()
+ widget1 = ttk.TTkWidget(parent=root)
+ widget2 = ttk.TTkWidget(parent=root)
+ widget3 = ttk.TTkWidget(parent=root)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ widget2.setFocus()
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ tab_key = ttk.TTkKeyEvent(
+ type=ttk.TTkK.KeyType.SpecialKey,
+ key=ttk.TTkK.Key_Tab,
+ mod=ttk.TTkK.ShiftModifier,
+ code='',)
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert True is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+
+def test_focus_04_nested_containers():
+ '''
+ Root ─▶ Container1 ─┬─▶ Widget1
+ └─▶ Container2 ─┬─▶ Widget2
+ └─▶ Widget3
+ '''
+ root = ttk.TTk()
+ container1 = ttk.TTkContainer(parent=root)
+ widget1 = ttk.TTkWidget(parent=container1)
+ container2 = ttk.TTkContainer(parent=container1)
+ widget2 = ttk.TTkWidget(parent=container2)
+ widget3 = ttk.TTkWidget(parent=container2)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ widget2.setFocus()
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ tab_key = ttk.TTkKeyEvent(
+ type=ttk.TTkK.KeyType.SpecialKey,
+ key=ttk.TTkK.Key_Tab,
+ mod=ttk.TTkK.NoModifier,
+ code='',)
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert True is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+def test_focus_04_nested_containers_reversed():
+ '''
+ Root ─▶ Container1 ─┬─▶ Widget1
+ └─▶ Container2 ─┬─▶ Widget2
+ ├─▶ Widget3
+ └─▶ Widget4
+ '''
+ root = ttk.TTk()
+ container1 = ttk.TTkContainer(parent=root)
+ widget1 = ttk.TTkWidget(parent=container1)
+ container2 = ttk.TTkContainer(parent=container1)
+ widget2 = ttk.TTkWidget(parent=container2)
+ widget3 = ttk.TTkWidget(parent=container2)
+ widget4 = ttk.TTkWidget(parent=container2)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget4.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ widget3.setFocus()
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+ assert False is widget4.hasFocus()
+
+ tab_key = ttk.TTkKeyEvent(
+ type=ttk.TTkK.KeyType.SpecialKey,
+ key=ttk.TTkK.Key_Tab,
+ mod=ttk.TTkK.ShiftModifier,
+ code='',)
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+ assert False is widget4.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert True is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+ assert False is widget4.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+ assert True is widget4.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is root.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+ assert False is widget4.hasFocus()
+
+def test_focus_container_with_tab_focus():
+ '''
+ Container (TabFocus) ─┬─▶ Widget1
+ ├─▶ Widget2
+ └─▶ Widget3
+ '''
+ root = ttk.TTk()
+ container = ttk.TTkContainer(parent=root)
+ container.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget1 = ttk.TTkWidget(parent=container)
+ widget2 = ttk.TTkWidget(parent=container)
+ widget3 = ttk.TTkWidget(parent=container)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ container.setFocus()
+
+ assert True is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ tab_key = ttk.TTkKeyEvent(
+ type=ttk.TTkK.KeyType.SpecialKey,
+ key=ttk.TTkK.Key_Tab,
+ mod=ttk.TTkK.NoModifier,
+ code='',)
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container.hasFocus()
+ assert True is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert True is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+def test_focus_nested_containers_with_tab_focus():
+ '''
+ Root ─▶ Container1 (TabFocus) ─┬─▶ Widget1
+ └─▶ Container2 (TabFocus) ─┬─▶ Widget2
+ └─▶ Widget3
+ '''
+ root = ttk.TTk()
+ container1 = ttk.TTkContainer(parent=root)
+ container1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget1 = ttk.TTkWidget(parent=container1)
+ container2 = ttk.TTkContainer(parent=container1)
+ container2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2 = ttk.TTkWidget(parent=container2)
+ widget3 = ttk.TTkWidget(parent=container2)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ container1.setFocus()
+
+ assert True is container1.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ tab_key = ttk.TTkKeyEvent(
+ type=ttk.TTkK.KeyType.SpecialKey,
+ key=ttk.TTkK.Key_Tab,
+ mod=ttk.TTkK.NoModifier,
+ code='',)
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container1.hasFocus()
+ assert True is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container1.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container1.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container1.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert True is container1.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+def test_focus_nested_containers_with_tab_focus_reversed():
+ '''
+ Root ─▶ Container1 (TabFocus) ─┬─▶ Widget1
+ └─▶ Container2 (TabFocus) ─┬─▶ Widget2
+ └─▶ Widget3
+ '''
+ root = ttk.TTk()
+ container1 = ttk.TTkContainer(name='container1', parent=root)
+ widget1 = ttk.TTkWidget(name='widget1', parent=container1)
+ container2 = ttk.TTkContainer(name='container2', parent=container1)
+ widget2 = ttk.TTkWidget(name='widget2', parent=container2)
+ widget3 = ttk.TTkWidget(name='widget3', parent=container2)
+ container1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ container2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ widget2.setFocus()
+
+ assert False is container1.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ tab_key = ttk.TTkKeyEvent(
+ type=ttk.TTkK.KeyType.SpecialKey,
+ key=ttk.TTkK.Key_Tab,
+ mod=ttk.TTkK.ShiftModifier,
+ code='',)
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container1.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container1.hasFocus()
+ assert True is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert True is container1.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container1.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container1.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+def test_focus_mixed_containers_tab_focus():
+ '''
+ Root ─▶ Container1 (No TabFocus) ─┬─▶ Widget1
+ ├─▶ Container2 (TabFocus) ─┬─▶ Widget2
+ │ └─▶ Widget3
+ └─▶ Widget4
+ '''
+ root = ttk.TTk()
+ container1 = ttk.TTkContainer(parent=root)
+ widget1 = ttk.TTkWidget(parent=container1)
+ container2 = ttk.TTkContainer(parent=container1)
+ container2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2 = ttk.TTkWidget(parent=container2)
+ widget3 = ttk.TTkWidget(parent=container2)
+ widget4 = ttk.TTkWidget(parent=container1)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget4.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ widget1.setFocus()
+
+ assert True is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+ assert False is widget4.hasFocus()
+
+ tab_key = ttk.TTkKeyEvent(
+ type=ttk.TTkK.KeyType.SpecialKey,
+ key=ttk.TTkK.Key_Tab,
+ mod=ttk.TTkK.NoModifier,
+ code='',)
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is widget1.hasFocus()
+ assert True is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+ assert False is widget4.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+ assert False is widget4.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+ assert False is widget4.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+ assert True is widget4.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert True is widget1.hasFocus()
+ assert False is container2.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+ assert False is widget4.hasFocus()
+
+def test_focus_container_tab_focus_reversed():
+ '''
+ Container (TabFocus) ─┬─▶ Widget1
+ ├─▶ Widget2
+ └─▶ Widget3
+ '''
+ root = ttk.TTk()
+ container = ttk.TTkContainer(parent=root)
+ container.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget1 = ttk.TTkWidget(parent=container)
+ widget2 = ttk.TTkWidget(parent=container)
+ widget3 = ttk.TTkWidget(parent=container)
+ widget1.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget2.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+ widget3.setFocusPolicy(ttk.TTkK.FocusPolicy.TabFocus)
+
+ widget2.setFocus()
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ tab_key = ttk.TTkKeyEvent(
+ type=ttk.TTkK.KeyType.SpecialKey,
+ key=ttk.TTkK.Key_Tab,
+ mod=ttk.TTkK.ShiftModifier,
+ code='',)
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container.hasFocus()
+ assert True is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert True is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert False is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert False is widget2.hasFocus()
+ assert True is widget3.hasFocus()
+
+ root.keyEvent(evt=tab_key)
+
+ assert False is container.hasFocus()
+ assert False is widget1.hasFocus()
+ assert True is widget2.hasFocus()
+ assert False is widget3.hasFocus()
\ No newline at end of file
diff --git a/tests/t.ui/test.ui.001.window.01.py b/tests/t.ui/test.ui.001.window.01.py
index 1e1a8373..ccf33ecf 100755
--- a/tests/t.ui/test.ui.001.window.01.py
+++ b/tests/t.ui/test.ui.001.window.01.py
@@ -30,5 +30,5 @@ import TermTk as ttk
ttk.TTkLog.use_default_file_logging()
root = ttk.TTk()
-ttk.TTkWindow(parent=root, pops=(0,0), size=(30,5), border=True)
+ttk.TTkWindow(parent=root, pos=(0,0), size=(30,5), border=True)
root.mainloop()
\ No newline at end of file
diff --git a/tests/t.ui/test.ui.037.prototype.01.rootContainer.py b/tests/t.ui/test.ui.037.prototype.01.rootContainer.py
new file mode 100644
index 00000000..e6b47c43
--- /dev/null
+++ b/tests/t.ui/test.ui.037.prototype.01.rootContainer.py
@@ -0,0 +1,62 @@
+
+#!/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 sys, os
+
+sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk'))
+import TermTk as ttk
+
+
+class MovementContainer(ttk.TTkContainer):
+ def __init__(self, *, widget=ttk.TTkWidget, **kwargs):
+ super().__init__(**kwargs)
+ self.layout().addWidget(widget=widget)
+ widget.sizeChanged.connect(self._sizeChanged)
+ # widget.positionChanged.connect(self._positionChanged)
+
+
+ @ttk.pyTTkSlot(int,int)
+ def _sizeChanged(self,w,h):
+ self.resize(w,h)
+
+ @ttk.pyTTkSlot(int,int)
+ def _positionChanged(self,x,y):
+ ox,oy = self.layout().offset()
+ self.layout().setOffset(-x,-y)
+ self.move(x,y)
+
+ def paintEvent(self, canvas):
+ canvas.fill(color=ttk.TTkColor.BG_RED)
+
+root = ttk.TTk()
+
+win = ttk.TTkWindow(size=(40,15))
+frame = ttk.TTkResizableFrame(size=(40,15), border=True)
+
+mcWin = MovementContainer(widget=win, parent=root, pos=(10,5), size=(40,15))
+mcFrame = MovementContainer(widget=frame, parent=root, pos=(5,2), size=(40,15))
+
+
+root.mainloop()
diff --git a/tests/t.ui/test.ui.037.prototype.02.modal.py b/tests/t.ui/test.ui.037.prototype.02.modal.py
new file mode 100644
index 00000000..0111f6c2
--- /dev/null
+++ b/tests/t.ui/test.ui.037.prototype.02.modal.py
@@ -0,0 +1,69 @@
+#!/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 sys, os
+
+sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk'))
+import TermTk as ttk
+
+
+class TryModal(ttk.TTkWindow):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ overlay_btn = ttk.TTkButton(parent=self, pos=(0,0), size=(15,3), border=True, text='Overlay')
+ modal_btn = ttk.TTkButton(parent=self, pos=(0,3), size=(15,3), border=True, text='Modal')
+ toolbox_btn = ttk.TTkButton(parent=self, pos=(0,6), size=(15,3), border=True, text='Toolbox')
+
+ def _overlay():
+ win = TryModal(size=(30,13), title="Overlay")
+ ttk.TTkHelper.overlay(
+ caller=overlay_btn,
+ widget=win,
+ x=0,y=0)
+ overlay_btn.clicked.connect(_overlay)
+
+ def _modal():
+ win = TryModal(size=(30,13), title="Modal")
+ ttk.TTkHelper.overlay(
+ caller=modal_btn,
+ widget=win,
+ modal=True,
+ x=0,y=0)
+ modal_btn.clicked.connect(_modal)
+
+ def _toolbox():
+ win = TryModal(size=(30,13), title="Toolbox")
+ ttk.TTkHelper.overlay(
+ caller=toolbox_btn,
+ widget=win,
+ toolWindow=True,
+ x=0,y=0)
+ toolbox_btn.clicked.connect(_toolbox)
+
+root = ttk.TTk()
+
+TryModal(parent=root, pos=(5,2), size=(30,13), title="Root")
+
+root.mainloop()
+
diff --git a/tests/t.ui/test.ui.038.tab_focus.py b/tests/t.ui/test.ui.038.tab_focus.py
new file mode 100755
index 00000000..ca00c620
--- /dev/null
+++ b/tests/t.ui/test.ui.038.tab_focus.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+
+# MIT License
+#
+# Copyright (c) 2021 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
+
+sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk'))
+import TermTk as ttk
+
+ttk.TTkLog.use_default_file_logging()
+
+root = ttk.TTk()
+
+win1 = ttk.TTkWindow(parent=root, title='1', pos=( 0,0), size=(40,10), border=True)
+win2 = ttk.TTkWindow(parent=root, title='2', pos=(20,3), size=(40,15), border=True)
+win3 = ttk.TTkWindow(parent=win2, title='3', pos=( 0,0), size=(30,10), border=True)
+win4 = ttk.TTkWindow(parent=win2, title='4', pos=( 5,4), size=(30,5), border=True)
+win5 = ttk.TTkWindow(parent=win3, title='5', pos=( 0,0), size=(20,5), border=True)
+
+win1.setFocusPolicy(ttk.TTkK.TabFocus | ttk.TTkK.ClickFocus)
+win2.setFocusPolicy(ttk.TTkK.TabFocus | ttk.TTkK.ClickFocus)
+win3.setFocusPolicy(ttk.TTkK.TabFocus | ttk.TTkK.ClickFocus)
+win4.setFocusPolicy(ttk.TTkK.TabFocus | ttk.TTkK.ClickFocus)
+win5.setFocusPolicy(ttk.TTkK.TabFocus | ttk.TTkK.ClickFocus)
+
+root.mainloop()
\ No newline at end of file