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