From 4ff4c06da4d55912d76d81ffd99a5940c671e3a9 Mon Sep 17 00:00:00 2001 From: Pier CeccoPierangioliEugenio Date: Sat, 20 Dec 2025 15:11:31 +0000 Subject: [PATCH] chore(tab): add tab status class and reworked highlighted (#555) --- demo/showcase/dndtabs.py | 3 +- docs/MDNotes/internals/focus.keypress.md | 20 +- libs/pyTermTk/TermTk/TTkTheme/draw_ascii.py | 4 +- libs/pyTermTk/TermTk/TTkTheme/draw_utf8.py | 4 +- libs/pyTermTk/TermTk/TTkWidgets/kodetab.py | 21 +- libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py | 655 +++++++++++------ tests/pytest/widgets/test_kode_tab.py | 595 ++++++++++++++++ tests/pytest/widgets/test_tab.py | 666 ++++++++++++++++++ ...st.ui.012.tab.py => test.ui.012.tab.01.py} | 2 +- tests/t.ui/test.ui.012.tab.02.py | 87 +++ 10 files changed, 1807 insertions(+), 250 deletions(-) create mode 100644 tests/pytest/widgets/test_kode_tab.py create mode 100644 tests/pytest/widgets/test_tab.py rename tests/t.ui/{test.ui.012.tab.py => test.ui.012.tab.01.py} (96%) create mode 100755 tests/t.ui/test.ui.012.tab.02.py diff --git a/demo/showcase/dndtabs.py b/demo/showcase/dndtabs.py index 42113d86..9427d826 100755 --- a/demo/showcase/dndtabs.py +++ b/demo/showcase/dndtabs.py @@ -69,9 +69,10 @@ def demoDnDTabs(root=None, border=True): def main(): parser = argparse.ArgumentParser() parser.add_argument('-f', help='Full Screen', action='store_true') + parser.add_argument('-t', help='Track Mouse', action='store_true') args = parser.parse_args() - root = ttk.TTk() + root = ttk.TTk(mouseTrack=args.t) if args.f: rootTab = root root.setLayout(ttk.TTkGridLayout()) diff --git a/docs/MDNotes/internals/focus.keypress.md b/docs/MDNotes/internals/focus.keypress.md index 2d50b1b7..a7ad1f17 100644 --- a/docs/MDNotes/internals/focus.keypress.md +++ b/docs/MDNotes/internals/focus.keypress.md @@ -58,13 +58,13 @@ # 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 + - [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 + - [x] - Tab Widget: Apply Highlight colors diff --git a/libs/pyTermTk/TermTk/TTkTheme/draw_ascii.py b/libs/pyTermTk/TermTk/TTkTheme/draw_ascii.py index d685355e..2572bf9d 100644 --- a/libs/pyTermTk/TermTk/TTkTheme/draw_ascii.py +++ b/libs/pyTermTk/TermTk/TTkTheme/draw_ascii.py @@ -106,7 +106,9 @@ class TTkTheme(): #21 22 23 24 25 26 27 28 29 30 '=','=','\\','/','X','X','=','=','-','X', #31 32 33 34 35 36 37 38 39 40 - '<','>','|','|','-','-','X' + '<','>','|','|','-','-','X','X','X','X', + #41 42 43 44 45 + '/','-','-','\\','|', ) braille=( diff --git a/libs/pyTermTk/TermTk/TTkTheme/draw_utf8.py b/libs/pyTermTk/TermTk/TTkTheme/draw_utf8.py index 2a8d02dc..e9e707d7 100644 --- a/libs/pyTermTk/TermTk/TTkTheme/draw_utf8.py +++ b/libs/pyTermTk/TermTk/TTkTheme/draw_utf8.py @@ -162,7 +162,9 @@ class TTkTheme(): #21 22 23 24 25 26 27 28 29 30 '╚','╝','╰','╯','⣿','⣿','╒','╕','┴','X', #31 32 33 34 35 36 37 38 39 40 - '◀','▶','╿','╽','╼','╾','X' + '◀','▶','╿','╽','╼','╾','X','X','X','X', + #41 42 43 44 45 + '┏','━','┳','┓','┃', ) ''' Tab Examples diff --git a/libs/pyTermTk/TermTk/TTkWidgets/kodetab.py b/libs/pyTermTk/TermTk/TTkWidgets/kodetab.py index 4637384a..309ed264 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/kodetab.py +++ b/libs/pyTermTk/TermTk/TTkWidgets/kodetab.py @@ -22,7 +22,7 @@ __all__ = ['TTkKodeTab'] -from typing import Callable, Iterator, Tuple, List +from typing import Callable, Iterator, Tuple, List, Any from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.helper import TTkHelper @@ -121,9 +121,6 @@ class _TTkKodeTab(TTkTabWidget): self.update() return True - def _dropNewKodeTab(self, y:int, data:_TTkNewTabWidgetDragData) -> bool: - pass - def dropEvent(self, evt:TTkDnDEvent) -> bool: self._frameOverlay = None x,y = evt.x, evt.y @@ -147,7 +144,7 @@ class _TTkKodeTab(TTkTabWidget): splitter.setStyle(_splitter_NERD_1_style) splitter.addWidget(self) index=offset - splitter.insertWidget(index+offset, kt:=_TTkKodeTab(baseWidget=self._baseWidget, border=self.border(), barType=self._barType, closable=self.tabsClosable())) + splitter.insertWidget(index+offset, kt:=_TTkKodeTab(baseWidget=self._baseWidget, border=self.border(), barType=self._tabStatus.barType, closable=self.tabsClosable())) kt._dropEventProxy = self._dropEventProxy kt.kodeTabCloseRequested.connect(self._baseWidget._handleKodeTabCloseRequested) for (_w,_l) in data: @@ -180,7 +177,7 @@ class _TTkKodeTab(TTkTabWidget): w,h = self.size() h-=3 y-=3 - index = tw._tabBar._tabButtons.index(tb) + index = tw._tabStatus.tabButtons.index(tb) widget = tw._tabWidgets[index] label = tb.text() dropData = [(widget,label)] @@ -210,7 +207,7 @@ class _TTkKodeTab(TTkTabWidget): def _kodeTabClosed(self, widget=None): # Remove the widget and/or all the cascade empty splitters fwold = self._baseWidget._getFirstWidget() - widget = widget if issubclass(type(widget), _TTkKodeTab) else self + widget = widget if isinstance(widget, _TTkKodeTab) else self if not widget._tabWidgets: if splitter := widget.parentWidget(): while splitter.count() == 1 and splitter != self._baseWidget: @@ -271,18 +268,18 @@ class TTkKodeTab(TTkSplitter): return item if type(item)==_TTkKodeTab else None def iterItems(self) -> Iterator[Tuple[_TTkKodeTab, int]]: - def _iterSplitter(split:TTkSplitter): + def _iterSplitter(split:TTkSplitter) -> Iterator[Tuple[_TTkKodeTab, int]]: for i in range(split.count()): _wid = split.widget(i) - if issubclass(type(_wid), TTkSplitter): + if isinstance(_wid, TTkSplitter): yield from _iterSplitter(_wid) - elif issubclass(type(_wid), _TTkKodeTab): + elif isinstance(_wid, _TTkKodeTab): yield from _wid.iterItems() yield from _iterSplitter(self) - def setDropEventProxy(self, proxy:Callable) -> None: + def setDropEventProxy(self, proxy:Callable[[TTkDnDEvent], bool]) -> None: for widget in self.layout().iterWidgets(onlyVisible=False): - if issubclass(type(widget),_TTkKodeTab): + if isinstance(widget, _TTkKodeTab): widget.setDropEventProxy(proxy) return super().setDropEventProxy(proxy) diff --git a/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py b/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py index ffe06435..5c4e2ded 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py +++ b/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py @@ -20,10 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from __future__ import annotations + __all__ = ['TTkTabButton', 'TTkTabBar', 'TTkTabWidget', 'TTkBarType'] from enum import Enum -from typing import List, Tuple, Optional +from dataclasses import dataclass +from typing import List, Tuple, Optional, Any, Dict from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.helper import TTkHelper @@ -65,27 +68,118 @@ class TTkBarType(Enum): TTkBarType.DEFAULT_2:0, TTkBarType.NERD_1:0}.get(self,1) +class _TTkScrollerStatus(Enum): + ACTIVE = 0x01 + HIGHLIGHTED = 0x02 + INACTIVE = 0x03 + +class _TTkTabStatus(): + __slots__ = ( + "statusUpdated", "currentChanged", + "tabBar", "tabButtons", "barType", + "currentIndex", "highlighted") + + statusUpdated:pyTTkSignal + tabBar:TTkTabBar + tabButtons:List[TTkTabButton] + barType:TTkBarType + highlighted:int + currentIndex:int + + def __init__( + self, + tabBar:TTkTabBar, + barType:TTkBarType): + self.tabBar = tabBar + self.barType = barType + self.statusUpdated = pyTTkSignal() + self.currentChanged = pyTTkSignal(int) + self.tabButtons = [] + self.highlighted = -1 + self.currentIndex = -1 + + @pyTTkSlot() + def _moveToTheLeft(self): + self._setCurrentIndex(self.currentIndex-1) + + @pyTTkSlot() + def _andMoveToTheRight(self): + self._setCurrentIndex(self.currentIndex+1) + + # @pyTTkSlot(TTkTabButton) + def _setCurrentButton(self, button:TTkTabButton) -> None: + '''setCurrentButton''' + index = self.tabButtons.index(button) + self._setCurrentIndex(index) + + @pyTTkSlot(int) + def _setCurrentIndex(self, index) -> None: + '''setCurrentIndex''' + if ( ( 0 <= index < len(self.tabButtons) ) and + ( self.currentIndex != index or + self.highlighted != -1 ) ): + self.highlighted = -1 + if (self.currentIndex != index): + self.currentIndex = index + self.currentChanged.emit(index) + self.statusUpdated.emit() + + @pyTTkSlot(int) + def _resetHighlighted(self) -> None: + if self.highlighted != -1: + self.highlighted = -1 + self.statusUpdated.emit() + + def _insertButton(self, index:int, button:TTkTabButton) -> None: + self.tabButtons.insert(index,button) + self.statusUpdated.connect(button.update) + if index <= self.currentIndex: + self.currentIndex += 1 + self.currentChanged.emit(self.currentIndex) + if self.currentIndex < 0: + self.currentIndex = 0 + self.currentChanged.emit(0) + + def _popButton(self, index:int) -> Optional[TTkTabButton]: + if 0 <= index < len(self.tabButtons): + button = self.tabButtons.pop(index) + self.statusUpdated.disconnect(button.update) + self.highlighted = -1 + if self.currentIndex >= index: + self.currentIndex -= 1 + self.currentChanged.emit(self.currentIndex) + self.statusUpdated.emit() + return button + return None + _tabGlyphs = { 'scroller': ['◀','▶'], 'border' : { TTkBarType.DEFAULT_3 : [], TTkBarType.DEFAULT_2 : [], + # 0 1 2 3 4 5 TTkBarType.NERD_1 : ['🭛','🭦','🭡','🭖','╱','╲'], } } -_tabStyle = { +_tabStyle:Dict[str,Any] = { 'default': {'color': TTkColor.fgbg("#dddd88","#000044"), 'bgColor': TTkColor.fgbg("#000000","#8888aa"), 'borderColor': TTkColor.RST, - 'tabOffsetColor': TTkColor.RST, + 'borderHighlightColors': { + 'main' : TTkColor.fg('#00FFFF'), + 'fade' : TTkColor.fg('#88FF88'), + }, + 'scrollerColors': { + 'default': TTkColor.fg('#BBBBBB'), + 'highlight': TTkColor.fg('#00FFFF'), + 'inactive': TTkColor.fg('#888888'), + }, 'glyphs':_tabGlyphs}, 'disabled': {'color': TTkColor.fg('#888888'), - 'borderColor':TTkColor.fg('#888888'), - 'tabOffsetColor': TTkColor.RST}, + 'borderColor':TTkColor.fg('#888888')}, 'focus': {'color': TTkColor.fgbg("#dddd88","#000044")+TTkColor.BOLD, - 'borderColor': TTkColor.fg("#ffff00") + TTkColor.BOLD, - 'tabOffsetColor': TTkColor.RST}, + 'borderColor': TTkColor.fg("#ffff00") + TTkColor.BOLD}, } _tabStyleNormal = { @@ -102,14 +196,15 @@ _tabStyleFocussed = { - class _TTkTabWidgetDragData(): __slots__ = ('_tabButton', '_tabWidget') - def __init__(self, b, tw): + def __init__(self, b:TTkTabButton, tw:TTkTabWidget): self._tabButton = b self._tabWidget = tw - def tabButton(self): return self._tabButton - def tabWidget(self): return self._tabWidget + def tabButton(self) -> TTkTabButton: + return self._tabButton + def tabWidget(self) -> TTkTabWidget: + return self._tabWidget class _TTkNewTabWidgetDragData(): __slots__ = ('_label', '_widget', '_closable', '_data') @@ -126,34 +221,39 @@ class _TTkNewTabWidgetDragData(): class _TTkTabBarDragData(): __slots__ = ('_tabButton','_tabBar') def __init__(self, b, tb): - self._tabButton = b - self._tabBar = tb - def tabButton(self): return self._tabButton - def tabBar(self): return self._tabBar + self._tabButton:TTkTabButton = b + self._tabBar:TTkTabBar = tb + def tabButton(self) -> TTkTabButton: + return self._tabButton + def tabBar(self) -> TTkTabBar: + return self._tabBar # class _TTkTabColorButton(TTkContainer): class _TTkTabColorButton(TTkWidget): classStyle = _tabStyle | { - 'hover': {'color': TTkColor.fgbg("#dddd88","#000050")+TTkColor.BOLD, - 'borderColor': TTkColor.fg("#AAFFFF")+TTkColor.BOLD}, + 'hover': { + 'color': TTkColor.fgbg("#dddd88","#000050")+TTkColor.BOLD, + 'bgColor': TTkColor.fgbg("#007771","#8888aa")+TTkColor.BOLD, + 'borderColor': TTkColor.fg("#AAFFFF")+TTkColor.BOLD + }, } __slots__ = ( - '_barType', + '_tabStatus', # Signals - 'clicked' + 'tcbClicked' ) + _tabStatus:_TTkTabStatus + tcbClicked:pyTTkSignal def __init__(self, *, - barType:TTkBarType=TTkBarType.DEFAULT_3, + tabStatus:_TTkTabStatus, **kwargs) -> None: - self.clicked = pyTTkSignal() - - self._barType = barType - + self.tcbClicked = pyTTkSignal(_TTkTabColorButton) + self._tabStatus = tabStatus super().__init__(forwardStyle=True, **kwargs) def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool: - self.clicked.emit() + self.tcbClicked.emit(self) return True def keyEvent(self, evt:TTkKeyEvent) -> bool: @@ -162,7 +262,7 @@ class _TTkTabColorButton(TTkWidget): self._keyPressed = True self._pressed = True self.update() - self.clicked.emit() + self.tcbClicked.emit(self) return True return False @@ -176,8 +276,9 @@ class TTkTabButton(_TTkTabColorButton): '''TTkTabButton''' __slots__ = ( - '_data','_sideEnd', '_tabStatus', '_closable', + '_data','_sideEnd', '_buttonStatus', '_closable', 'closeClicked', '_closeButtonPressed','_data', '_text') + def __init__(self, *, text:TTkString='', data:object=None, @@ -185,7 +286,7 @@ class TTkTabButton(_TTkTabColorButton): **kwargs) -> None: self._text = TTkString(text.replace('\n','')) self._sideEnd = TTkK.NONE - self._tabStatus = TTkK.Unchecked + self._buttonStatus = TTkK.Unchecked self._data = data self._closable = closable self.closeClicked = pyTTkSignal() @@ -198,9 +299,9 @@ class TTkTabButton(_TTkTabColorButton): size = self.text().termWidth() + 2 if self._closable: size += len(style['closeGlyph']) - self.resize(size, self._barType.vSize()) - self.setMinimumSize(size, self._barType.vSize()) - self.setMaximumSize(size, self._barType.vSize()) + self.resize(size, self._tabStatus.barType.vSize()) + self.setMinimumSize(size, self._tabStatus.barType.vSize()) + self.setMaximumSize(size, self._tabStatus.barType.vSize()) def text(self) -> TTkString: return self._text @@ -223,11 +324,11 @@ class TTkTabButton(_TTkTabColorButton): self._sideEnd = sideEnd self.update() - def tabStatus(self): - return self._tabStatus + def buttonStatus(self): + return self._buttonStatus - def setTabStatus(self, status): - self._tabStatus = status + def setButtonStatus(self, status): + self._buttonStatus = status self.update() # This is a hack to force the action aftet the keypress @@ -239,7 +340,7 @@ class TTkTabButton(_TTkTabColorButton): if self._closable and evt.key == TTkK.MidButton: self.closeClicked.emit() return True - offY = self._barType.offY() + offY = self._tabStatus.barType.offY() if self._closable and y == offY and w-4<=x bool: x,y = evt.x,evt.y w,h = self.size() - offY = self._barType.offY() + offY = self._tabStatus.barType.offY() if self._closable and y == offY and w-4<=x None: + self._scrollerStatus = _TTkScrollerStatus.ACTIVE self._side = side - self._sideEnd = self._side + self._sideEnd = side super().__init__(**kwargs) - self.resize(2, self._barType.vSize()) - self.setMinimumSize(2, self._barType.vSize()) - self.setMaximumSize(2, self._barType.vSize()) + self.resize(2, self._tabStatus.barType.vSize()) + self.setMinimumSize(2, self._tabStatus.barType.vSize()) + self.setMaximumSize(2, self._tabStatus.barType.vSize()) def side(self): return self._side @@ -374,6 +570,11 @@ class _TTkTabScrollerButton(_TTkTabColorButton): self._side = side self.update() + def setScrollerStatus(self, status) -> None: + if status != self._scrollerStatus: + self._scrollerStatus = status + self.update() + def sideEnd(self): return self._sideEnd @@ -388,45 +589,63 @@ class _TTkTabScrollerButton(_TTkTabColorButton): def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool: return False def mouseTapEvent(self, evt:TTkMouseEvent) -> bool: - self.clicked.emit() + self.tcbClicked.emit(self) return True - def paintEvent(self, canvas): + def paintEvent(self, canvas:TTkCanvas) -> None: style = self.currentStyle() glyphs = style['glyphs']['scroller'] + scrollerColors = style['scrollerColors'] borderColor = style['borderColor'] - offsetColor = style['tabOffsetColor'] - # textColor = style['color'] + + arrowColor:TTkColor = scrollerColors['default'] + if self._tabStatus.highlighted == -1: + pass + elif ( self._side == TTkK.LEFT and + self._tabStatus.highlighted == 0 ): + arrowColor = scrollerColors['inactive'] + elif ( self._side == TTkK.RIGHT and + self._tabStatus.highlighted >= len(self._tabStatus.tabButtons)-1 ): + arrowColor = scrollerColors['inactive'] + else: + arrowColor = scrollerColors['highlight'] tt = TTkCfg.theme.tab - if self._barType == TTkBarType.DEFAULT_3: + if self._tabStatus.barType == TTkBarType.DEFAULT_3: lse = tt[11] if self._sideEnd & TTkK.LEFT else tt[13] rse = tt[15] if self._sideEnd & TTkK.RIGHT else tt[13] if self._side == TTkK.LEFT: + # Draw Border canvas.drawText(pos=(0,0), color=borderColor, text=tt[7] +tt[1]) - canvas.drawText(pos=(0,1), color=borderColor, text=tt[9] +tt[31]) + canvas.drawText(pos=(0,1), color=borderColor, text=tt[9] ) canvas.drawText(pos=(0,2), color=borderColor, text=lse +tt[12]) - canvas.drawChar(pos=(1,1), char=glyphs[0], color=offsetColor) + # Draw Arrow + canvas.drawChar(pos=(1,1), char=glyphs[0], color=arrowColor) else: + # Draw Border canvas.drawText(pos=(0,0), color=borderColor, text=tt[1] +tt[8]) - canvas.drawText(pos=(0,1), color=borderColor, text=tt[32]+tt[9]) + canvas.drawText(pos=(1,1), color=borderColor, text= tt[9]) canvas.drawText(pos=(0,2), color=borderColor, text=tt[12]+rse) - canvas.drawChar(pos=(0,1), char=glyphs[1], color=offsetColor) - elif self._barType == TTkBarType.DEFAULT_2: + # Draw Arrow + canvas.drawChar(pos=(0,1), char=glyphs[1], color=arrowColor) + elif self._tabStatus.barType == TTkBarType.DEFAULT_2: if self._side == TTkK.LEFT: + # Draw Border canvas.drawText(pos=(0,0), color=borderColor, text=tt[9] +tt[31]) canvas.drawText(pos=(0,1), color=borderColor, text=tt[23]+tt[1]) - canvas.drawChar(pos=(1,0), char=glyphs[0], color=offsetColor) + # Draw Arrow + canvas.drawChar(pos=(1,0), char=glyphs[0], color=arrowColor) else: + # Draw Border canvas.drawText(pos=(0,0), color=borderColor, text=tt[32]+tt[9]) canvas.drawText(pos=(0,1), color=borderColor, text=tt[1] +tt[24]) - canvas.drawChar(pos=(0,0), char=glyphs[1], color=offsetColor) - elif self._barType == TTkBarType.NERD_1: - border = style['glyphs']['border'][self._barType] + # Draw Arrow + canvas.drawChar(pos=(0,0), char=glyphs[1], color=arrowColor) + elif self._tabStatus.barType == TTkBarType.NERD_1: if self._side == TTkK.LEFT: - canvas.drawText(pos=(0,0),color=style['bgColor'],text=f" {glyphs[0]}{border[5]}") + canvas.drawText(pos=(0,0),color=style['bgColor']+arrowColor,text=f" {glyphs[0]}") else: - canvas.drawText(pos=(0,0),color=style['bgColor'],text=f" {glyphs[1]}{border[4]}") + canvas.drawText(pos=(0,0),color=style['bgColor']+arrowColor,text=f"{glyphs[1]} ") ''' _curentIndex = 2 @@ -441,8 +660,8 @@ class TTkTabBar(TTkContainer): '''TTkTabBar''' classStyle = _tabStyle __slots__ = ( - '_tabButtons', '_tabMovable', '_barType', - '_highlighted', '_currentIndex', '_lastIndex', + '_tabStatus', + '_tabMovable', '_leftScroller', '_rightScroller', '_tabClosable', '_sideEnd', @@ -453,14 +672,11 @@ class TTkTabBar(TTkContainer): tabBarClicked: pyTTkSignal tabCloseRequested: pyTTkSignal - _tabButtons:List[TTkTabButton] - _currentIndex:int - _lastIndex:int - _highlighted:int + _tabStatus:_TTkTabStatus _tabMovable:bool _tabClosable:bool _sideEnd:int - _barType:TTkBarType + _tabStatus:_TTkTabStatus _leftScroller:_TTkTabScrollerButton _rightScroller:_TTkTabScrollerButton @@ -469,24 +685,24 @@ class TTkTabBar(TTkContainer): 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 - self._highlighted = -1 + self._tabStatus = _TTkTabStatus( + tabBar = self, + barType = barType, + ) + self.currentChanged = self._tabStatus.currentChanged + self._tabMovable = False self._tabClosable = closable self._sideEnd = TTkK.LEFT | TTkK.RIGHT - self._barType = barType if barType == TTkBarType.NONE: - self._barType = TTkBarType.DEFAULT_2 if small else TTkBarType.DEFAULT_3 - self._leftScroller = _TTkTabScrollerButton(barType=self._barType,side=TTkK.LEFT) - self._rightScroller = _TTkTabScrollerButton(barType=self._barType,side=TTkK.RIGHT) - self._leftScroller.clicked.connect( self._moveToTheLeft) - self._rightScroller.clicked.connect(self._andMoveToTheRight) + self._tabStatus.barType = TTkBarType.DEFAULT_2 if small else TTkBarType.DEFAULT_3 + self._leftScroller = _TTkTabScrollerButton(tabStatus=self._tabStatus,side=TTkK.LEFT) + self._rightScroller = _TTkTabScrollerButton(tabStatus=self._tabStatus,side=TTkK.RIGHT) + self._leftScroller.tcbClicked.connect( self._tabStatus._moveToTheLeft) + self._rightScroller.tcbClicked.connect(self._tabStatus._andMoveToTheRight) super().__init__(forwardStyle=False, **kwargs) @@ -495,10 +711,13 @@ class TTkTabBar(TTkContainer): self.layout().addWidget(self._rightScroller) self.setFocusPolicy(TTkK.ParentFocus) + self._tabStatus.statusUpdated.connect(self._updateTabs) + self._tabStatus.statusUpdated.connect(self._leftScroller.update) + self._tabStatus.statusUpdated.connect(self._rightScroller.update) def mergeStyle(self, style): super().mergeStyle(style) - for t in self._tabButtons: + for t in self._tabStatus.tabButtons: t.mergeStyle(style) self._leftScroller.mergeStyle(style) self._rightScroller.mergeStyle(style) @@ -514,53 +733,51 @@ class TTkTabBar(TTkContainer): def addTab(self, label, data=None, closable=None) -> int: '''addTab''' - return self.insertTab(len(self._tabButtons), label=label, data=data, closable=closable) + return self.insertTab(len(self._tabStatus.tabButtons), label=label, data=data, closable=closable) def insertTab(self, index, label, data=None, closable=None) -> int: '''insertTab''' - if index <= self._currentIndex: - self._currentIndex += 1 - button = TTkTabButton(parent=self, text=label, barType=self._barType, closable=self._tabClosable if closable is None else closable, data=data) - self._tabButtons.insert(index,button) - button.clicked.connect(lambda :self.setCurrentIndex(self._tabButtons.index(button))) - button.clicked.connect(lambda :self.tabBarClicked.emit(self._tabButtons.index(button))) - button.closeClicked.connect(lambda :self.tabCloseRequested.emit(self._tabButtons.index(button))) + button = TTkTabButton(parent=self, text=label, tabStatus=self._tabStatus, closable=self._tabClosable if closable is None else closable, data=data) + self._tabStatus._insertButton(index,button) + button.tcbClicked.connect(self._tcbClickedHandler) + button.closeClicked.connect(lambda :self.tabCloseRequested.emit(self._tabStatus.tabButtons.index(button))) self._updateTabs() return index + @pyTTkSlot(TTkTabButton) + def _tcbClickedHandler(self, btn:TTkTabButton): + index = self._tabStatus.tabButtons.index(btn) + self.setCurrentIndex(index) + self.tabBarClicked.emit(index) + @pyTTkSlot(int) - def removeTab(self, index): + def removeTab(self, index:int) -> None: '''removeTab''' - button = self._tabButtons[index] - button.clicked.clear() + if not (button := self._tabStatus._popButton(index)): + return + button.tcbClicked.clear() button.closeClicked.clear() self.layout().removeWidget(button) - self._tabButtons.pop(index) - if self._currentIndex == index: - self._lastIndex = -2 - if self._currentIndex >= index: - self._currentIndex -= 1 - self._highlighted = self._currentIndex self._updateTabs() def currentData(self): - return self.tabData(self._currentIndex) + return self.tabData(self._tabStatus.currentIndex) def tabButton(self, index): '''tabButton''' - if 0 <= index < len(self._tabButtons): - return self._tabButtons[index] + if 0 <= index < len(self._tabStatus.tabButtons): + return self._tabStatus.tabButtons[index] return None def tabData(self, index): '''tabData''' - if 0 <= index < len(self._tabButtons): - return self._tabButtons[index].data() + if 0 <= index < len(self._tabStatus.tabButtons): + return self._tabStatus.tabButtons[index].data() return None def setTabData(self, index, data): '''setTabData''' - self._tabButtons[index].setData(data) + self._tabStatus.tabButtons[index].setData(data) def tabsClosable(self): '''tabsClosable''' @@ -572,16 +789,12 @@ class TTkTabBar(TTkContainer): def currentIndex(self): '''currentIndex''' - return self._currentIndex + return self._tabStatus.currentIndex @pyTTkSlot(int) def setCurrentIndex(self, index): '''setCurrentIndex''' - TTkLog.debug(index) - if 0 <= index < len(self._tabButtons): - self._currentIndex = index - self._highlighted = index - self._updateTabs() + self._tabStatus._setCurrentIndex(index) def resizeEvent(self, w, h): self._updateTabs() @@ -590,7 +803,7 @@ class TTkTabBar(TTkContainer): w = self.width() # Find the tabs used size max size maxLen = 0 - sizes = [t.width()-1 for t in self._tabButtons] + sizes = [t.width()-1 for t in self._tabStatus.tabButtons] for s in sizes: maxLen += s if maxLen <= w: self._leftScroller.hide() @@ -604,9 +817,11 @@ class TTkTabBar(TTkContainer): w-=4 shrink = w/maxLen offx = 2 + self._leftScroller.update() + self._rightScroller.update() posx=0 - for t in self._tabButtons: + for t in self._tabStatus.tabButtons: tmpx = offx+min(int(posx*shrink),w-t.width()) sideEnd = TTkK.NONE if tmpx==0: @@ -618,60 +833,43 @@ class TTkTabBar(TTkContainer): posx += t.width()-1 # ZReorder the widgets: - for i in range(0,max(0,self._currentIndex)): - self._tabButtons[i].raiseWidget() - for i in reversed(range(max(0,self._currentIndex),len(self._tabButtons))): - self._tabButtons[i].raiseWidget() - - if self._currentIndex == -1: - self._currentIndex = len(self._tabButtons)-1 - - if self._lastIndex != self._currentIndex: - self._lastIndex = self._currentIndex - self.currentChanged.emit(self._currentIndex) + for i in range(0,max(0,self._tabStatus.currentIndex)): + self._tabStatus.tabButtons[i].raiseWidget() + for i in reversed(range(max(0,self._tabStatus.currentIndex),len(self._tabStatus.tabButtons))): + self._tabStatus.tabButtons[i].raiseWidget() # set the buttons text color based on the selection/offset - for i,b in enumerate(self._tabButtons): - if i == self._highlighted != self._currentIndex: - b.setTabStatus(TTkK.PartiallyChecked) + for i,b in enumerate(self._tabStatus.tabButtons): + if i == self._tabStatus.highlighted != self._tabStatus.currentIndex: + b.setButtonStatus(TTkK.PartiallyChecked) b.raiseWidget() - elif i == self._currentIndex: - b.setTabStatus(TTkK.Checked) + elif i == self._tabStatus.currentIndex: + b.setButtonStatus(TTkK.Checked) else: - b.setTabStatus(TTkK.Unchecked) + b.setButtonStatus(TTkK.Unchecked) self.update() - def _moveToTheLeft(self): - self._currentIndex = max(self._currentIndex-1,0) - self._highlighted = self._currentIndex - self._updateTabs() - - def _andMoveToTheRight(self): - self._currentIndex = min(self._currentIndex+1,len(self._tabButtons)-1) - self._highlighted = self._currentIndex - self._updateTabs() - def wheelEvent(self, evt:TTkMouseEvent) -> bool: if evt.evt in (TTkK.WHEEL_Up,TTkK.WHEEL_Left): - self._moveToTheLeft() + self._tabStatus._moveToTheLeft() elif evt.evt in (TTkK.WHEEL_Down,TTkK.WHEEL_Right): - self._andMoveToTheRight() + self._tabStatus._andMoveToTheRight() return True def keyEvent(self, evt:TTkKeyEvent) -> bool: if evt.type == TTkK.SpecialKey: if evt.key == TTkK.Key_Right: - self._highlighted = min(self._highlighted+1,len(self._tabButtons)-1) + self._tabStatus.highlighted = min(self._tabStatus.highlighted+1,len(self._tabStatus.tabButtons)-1) self._updateTabs() return True elif evt.key == TTkK.Key_Left: - self._highlighted = max(self._highlighted-1,0) + self._tabStatus.highlighted = max(self._tabStatus.highlighted-1,0) self._updateTabs() return True if ( evt.type == TTkK.Character and evt.key==" " ) or \ ( evt.type == TTkK.SpecialKey and evt.key == TTkK.Key_Enter ): - self._currentIndex = self._highlighted + self._tabStatus.currentIndex = self._tabStatus.highlighted self._updateTabs() return True return False @@ -681,16 +879,16 @@ class TTkTabBar(TTkContainer): borderColor = style['borderColor'] w = self.width() tt = TTkCfg.theme.tab - if self._barType == TTkBarType.DEFAULT_2: - lse = tt[23] if self._sideEnd & TTkK.LEFT else tt[19] - rse = tt[24] if self._sideEnd & TTkK.RIGHT else tt[19] + if self._tabStatus.barType == TTkBarType.DEFAULT_2: + lse = tt[36] if self._sideEnd & TTkK.LEFT else tt[19] + rse = tt[35] if self._sideEnd & TTkK.RIGHT else tt[19] canvas.drawText(pos=(0,1),text=lse + tt[19]*(w-2) + rse, color=borderColor) - elif self._barType == TTkBarType.DEFAULT_3: + elif self._tabStatus.barType == TTkBarType.DEFAULT_3: lse = tt[11] if self._sideEnd & TTkK.LEFT else tt[12] rse = tt[15] if self._sideEnd & TTkK.RIGHT else tt[12] canvas.drawText(pos=(0,2),text=lse + tt[12]*(w-2) + rse, color=borderColor) - elif self._barType == TTkBarType.NERD_1: - # glyphs = style['glyphs']['border'][self._barType] + elif self._tabStatus.barType == TTkBarType.NERD_1: + # glyphs = style['glyphs']['border'][self._tabStatus.barType] canvas.fill(color=style['bgColor']) # canvas.drawText(pos=(0,0),color=borderColor,text="-x----------------------------------------") @@ -715,7 +913,8 @@ class TTkTabWidget(TTkFrame): '''TTkTabWidget''' classStyle = _tabStyle __slots__ = ( - '_tabBarTopLayout', '_tabBar', '_barType', '_topLeftLayout', '_topRightLayout', + '_tabStatus', + '_tabBarTopLayout', '_topLeftLayout', '_topRightLayout', '_tabWidgets', '_spacer', # Forward Signals 'currentChanged', 'tabBarClicked', @@ -725,6 +924,7 @@ class TTkTabWidget(TTkFrame): 'currentIndex', 'setCurrentIndex', 'tabCloseRequested') _tabWidgets:List[TTkWidget] + _tabStatus:_TTkTabStatus def __init__(self, *, closable:bool=False, @@ -732,50 +932,53 @@ class TTkTabWidget(TTkFrame): **kwargs) -> None: self._tabWidgets = [] self._tabBarTopLayout = TTkGridLayout() - self._barType = barType + + tabBar = TTkTabBar( + barType=barType, + closable=closable) + self._tabStatus = tabBar._tabStatus super().__init__(forwardStyle=False, **kwargs) if barType == TTkBarType.NONE: - self._barType = TTkBarType.DEFAULT_3 if self.border() else TTkBarType.DEFAULT_2 + self._tabStatus.barType = TTkBarType.DEFAULT_3 if self.border() else TTkBarType.DEFAULT_2 + + - self._tabBar = TTkTabBar( - barType=self._barType, - closable=closable) self._topLeftLayout = None self._topRightLayout = None - self._tabBar.currentChanged.connect(self._tabChanged) + self._tabStatus.tabBar.currentChanged.connect(self._tabChanged) self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus) self._spacer = TTkSpacer(parent=self) self.setLayout(TTkGridLayout()) - if self._barType == TTkBarType.DEFAULT_3: - self._tabBarTopLayout.addWidget(self._tabBar,0,1,3,1) + if self._tabStatus.barType == TTkBarType.DEFAULT_3: + self._tabBarTopLayout.addWidget(self._tabStatus.tabBar,0,1,3,1) self.setPadding(3,1,1,1) - elif self._barType == TTkBarType.DEFAULT_2: - self._tabBarTopLayout.addWidget(self._tabBar,0,1,2,1) + elif self._tabStatus.barType == TTkBarType.DEFAULT_2: + self._tabBarTopLayout.addWidget(self._tabStatus.tabBar,0,1,2,1) self.setPadding(2,0,0,0) - elif self._barType == TTkBarType.NERD_1: - self._tabBarTopLayout.addWidget(self._tabBar,0,1,1,1) + elif self._tabStatus.barType == TTkBarType.NERD_1: + self._tabBarTopLayout.addWidget(self._tabStatus.tabBar,0,1,1,1) self.setPadding(1,0,0,0) self.rootLayout().addItem(self._tabBarTopLayout) self._tabBarTopLayout.setGeometry(0,0,self._width,self._padt) # forwarded methods - self.currentIndex = self._tabBar.currentIndex - self.setCurrentIndex = self._tabBar.setCurrentIndex - self.tabData = self._tabBar.tabData - self.setTabData = self._tabBar.setTabData - self.currentData = self._tabBar.currentData - self.tabsClosable = self._tabBar.tabsClosable - self.setTabsClosable = self._tabBar.setTabsClosable + self.currentIndex = self._tabStatus.tabBar.currentIndex + self.setCurrentIndex = self._tabStatus.tabBar.setCurrentIndex + self.tabData = self._tabStatus.tabBar.tabData + self.setTabData = self._tabStatus.tabBar.setTabData + self.currentData = self._tabStatus.tabBar.currentData + self.tabsClosable = self._tabStatus.tabBar.tabsClosable + self.setTabsClosable = self._tabStatus.tabBar.setTabsClosable # forwarded Signals - self.currentChanged = self._tabBar.currentChanged - self.tabBarClicked = self._tabBar.tabBarClicked - self.tabCloseRequested = self._tabBar.tabCloseRequested + self.currentChanged = self._tabStatus.tabBar.currentChanged + self.tabBarClicked = self._tabStatus.tabBar.tabBarClicked + self.tabCloseRequested = self._tabStatus.tabBar.tabCloseRequested self.tabBarClicked.connect(self.setFocus) @@ -783,9 +986,10 @@ class TTkTabWidget(TTkFrame): def _focusChanged(self, focus): if focus: - self._tabBar.mergeStyle(_tabStyleFocussed) + self._tabStatus.tabBar.mergeStyle(_tabStyleFocussed) else: - self._tabBar.mergeStyle(_tabStyleNormal) + self._tabStatus.highlighted = -1 + self._tabStatus.tabBar.mergeStyle(_tabStyleNormal) def count(self) -> int: return len(self._tabWidgets) @@ -797,7 +1001,7 @@ class TTkTabWidget(TTkFrame): def tabButton(self, index:int) -> TTkTabButton: '''tabButton''' - return self._tabBar.tabButton(index) + return self._tabStatus.tabBar.tabButton(index) def widget(self, index:int) -> Optional[TTkWidget]: '''widget''' @@ -818,7 +1022,6 @@ class TTkTabWidget(TTkFrame): for i, w in enumerate(self._tabWidgets): if widget == w: self.setCurrentIndex(i) - break @pyTTkSlot(int) def _tabChanged(self, index:int) -> None: @@ -831,7 +1034,7 @@ class TTkTabWidget(TTkFrame): widget.hide() def keyEvent(self, evt:TTkKeyEvent) -> bool: - if self.hasFocus() and self._tabBar.keyEvent(evt=evt): + if self.hasFocus() and self._tabStatus.tabBar.keyEvent(evt=evt): return True return super().keyEvent(evt) @@ -848,34 +1051,34 @@ class TTkTabWidget(TTkFrame): l = data.label() c = data.closable() if y < 3: - tbx = self._tabBar.x() + tbx = self._tabStatus.tabBar.x() newIndex = 0 - for b in self._tabBar._tabButtons: + for b in self._tabStatus.tabButtons: if tbx+b.x()+b.width()/2 < x: newIndex += 1 self.insertTab(newIndex, w, l, d, c) self.setCurrentIndex(newIndex) else: self.addTab(w, l, d, c) - self.setCurrentIndex(len(self._tabBar._tabButtons)-1) + self.setCurrentIndex(len(self._tabStatus.tabButtons)-1) def dropEvent(self, evt:TTkDnDEvent) -> bool: data = evt.data() x, y = evt.x, evt.y if not data: return False - elif isinstance(data,_TTkTabWidgetDragData): + elif isinstance(data, _TTkTabWidgetDragData): tb = data.tabButton() tw = data.tabWidget() - index = tw._tabBar._tabButtons.index(tb) + index = tw._tabStatus.tabButtons.index(tb) widget = tw.widget(index) data = tw.tabData(index) if TTkHelper.isParent(self, tw): return False if y < 3: - tbx = self._tabBar.x() + tbx = self._tabStatus.tabBar.x() newIndex = 0 - for b in self._tabBar._tabButtons: + for b in self._tabStatus.tabButtons: if tbx+b.x()+b.width()/2 < x: newIndex += 1 if tw == self: @@ -884,6 +1087,10 @@ class TTkTabWidget(TTkFrame): tw.removeTab(index) self.insertTab(newIndex, widget, tb.text(), data, tb._closable) self.setCurrentIndex(newIndex) + if self.hasFocus(): + self._tabStatus.tabBar.mergeStyle(_tabStyleFocussed) + else: + self._tabStatus.tabBar.mergeStyle(_tabStyleNormal) #self._tabChanged(newIndex) elif tw != self: tw.removeTab(index) @@ -907,7 +1114,7 @@ class TTkTabWidget(TTkFrame): def addMenu(self, text, position=TTkK.LEFT, data=None) -> TTkMenuBarButton: '''addMenu''' button = _TTkTabMenuButton(text=text, data=data) - self._tabBar.setSideEnd(self._tabBar.sideEnd() & ~position) + self._tabStatus.tabBar.setSideEnd(self._tabStatus.tabBar.sideEnd() & ~position) if position==TTkK.LEFT: if not self._topLeftLayout: self._topLeftLayout = TTkHBoxLayout() @@ -927,21 +1134,21 @@ class TTkTabWidget(TTkFrame): widget.hide() self._tabWidgets.append(widget) self.layout().addWidget(widget) - return self._tabBar.addTab(label, data, closable) + return self._tabStatus.tabBar.addTab(label, data, closable) def insertTab(self, index, widget, label, data=None, closable=None) -> int: '''insertTab''' widget.hide() self._tabWidgets.insert(index, widget) self.layout().addWidget(widget) - return self._tabBar.insertTab(index, label, data, closable) + return self._tabStatus.tabBar.insertTab(index, label, data, closable) @pyTTkSlot(int) def removeTab(self, index) -> None: '''removeTab''' self.layout().removeWidget(self._tabWidgets[index]) self._tabWidgets.pop(index) - self._tabBar.removeTab(index) + self._tabStatus.tabBar.removeTab(index) def resizeEvent(self, w, h): self._tabBarTopLayout.setGeometry(0,0,w,self._padt) diff --git a/tests/pytest/widgets/test_kode_tab.py b/tests/pytest/widgets/test_kode_tab.py new file mode 100644 index 00000000..8f42c633 --- /dev/null +++ b/tests/pytest/widgets/test_kode_tab.py @@ -0,0 +1,595 @@ +# 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 +from TermTk.TTkWidgets.kodetab import _TTkKodeTab +from TermTk.TTkWidgets.tabwidget import _TTkTabWidgetDragData, _TTkNewTabWidgetDragData +from TermTk.TTkGui.drag import TTkDnDEvent + +# ============================================================================ +# TTkKodeTab Basic Tests +# ============================================================================ + +def test_kodetab_initialization(): + ''' + Test that TTkKodeTab initializes correctly. + ''' + kodeTab = ttk.TTkKodeTab() + + assert kodeTab is not None + assert isinstance(kodeTab, ttk.TTkSplitter) + + # Should have at least one internal _TTkKodeTab widget + first_widget = kodeTab._getFirstWidget() + assert first_widget is not None + assert isinstance(first_widget, _TTkKodeTab) + +def test_kodetab_add_single_tab(): + ''' + Test adding a single tab to TTkKodeTab. + ''' + kodeTab = ttk.TTkKodeTab() + widget = ttk.TTkWidget() + + index = kodeTab.addTab(widget, "Tab 1") + + assert index == 0 + assert kodeTab._lastKodeTabWidget.count() == 1 + assert kodeTab._lastKodeTabWidget.widget(0) == widget + +def test_kodetab_add_multiple_tabs(): + ''' + Test adding multiple tabs to TTkKodeTab. + ''' + kodeTab = ttk.TTkKodeTab() + + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + widget3 = ttk.TTkWidget() + + index1 = kodeTab.addTab(widget1, "Tab 1") + index2 = kodeTab.addTab(widget2, "Tab 2") + index3 = kodeTab.addTab(widget3, "Tab 3") + + assert index1 == 0 + assert index2 == 1 + assert index3 == 2 + assert kodeTab._lastKodeTabWidget.count() == 3 + +def test_kodetab_add_tab_with_data(): + ''' + Test adding tabs with associated data. + ''' + kodeTab = ttk.TTkKodeTab() + + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + kodeTab.addTab(widget1, "Tab 1", data="data1") + kodeTab.addTab(widget2, "Tab 2", data={"key": "value"}) + + assert kodeTab._lastKodeTabWidget.tabData(0) == "data1" + assert kodeTab._lastKodeTabWidget.tabData(1) == {"key": "value"} + +def test_kodetab_set_current_widget(): + ''' + Test setting the current widget in TTkKodeTab. + ''' + kodeTab = ttk.TTkKodeTab() + + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + widget3 = ttk.TTkWidget() + + kodeTab.addTab(widget1, "Tab 1") + kodeTab.addTab(widget2, "Tab 2") + kodeTab.addTab(widget3, "Tab 3") + + kodeTab.setCurrentWidget(widget2) + assert kodeTab._lastKodeTabWidget.currentWidget() == widget2 + + kodeTab.setCurrentWidget(widget3) + assert kodeTab._lastKodeTabWidget.currentWidget() == widget3 + +def test_kodetab_iter_items(): + ''' + Test iterating over all tabs in TTkKodeTab. + ''' + kodeTab = ttk.TTkKodeTab() + + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + widget3 = ttk.TTkWidget() + + kodeTab.addTab(widget1, "Tab 1") + kodeTab.addTab(widget2, "Tab 2") + kodeTab.addTab(widget3, "Tab 3") + + items = list(kodeTab.iterItems()) + + assert len(items) == 3 + for tab_widget, index in items: + assert isinstance(tab_widget, _TTkKodeTab) + assert 0 <= index < tab_widget.count() + +def test_kodetab_signals(): + ''' + Test that TTkKodeTab signals are emitted correctly. + ''' + kodeTab = ttk.TTkKodeTab() + + current_changed_called = [] + tab_clicked_called = [] + tab_added_called = [] + + kodeTab.currentChanged.connect( + lambda tw, i, w, d: current_changed_called.append((tw, i, w, d)) + ) + kodeTab.tabBarClicked.connect( + lambda tw, i, w, d: tab_clicked_called.append((tw, i, w, d)) + ) + kodeTab.tabAdded.connect( + lambda tw, i: tab_added_called.append((tw, i)) + ) + + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + kodeTab.addTab(widget1, "Tab 1") + assert len(tab_added_called) == 1 + assert len(current_changed_called) == 1 + + kodeTab.addTab(widget2, "Tab 2") + assert len(tab_added_called) == 2 + +def test_kodetab_close_requested_signal(): + ''' + Test that kodeTabCloseRequested signal is emitted correctly. + ''' + kodeTab = ttk.TTkKodeTab(closable=True) + + close_requested_called = [] + kodeTab.kodeTabCloseRequested.connect( + lambda tw, i: close_requested_called.append((tw, i)) + ) + + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + kodeTab.addTab(widget1, "Tab 1") + kodeTab.addTab(widget2, "Tab 2") + + # Simulate close request + kodeTab._lastKodeTabWidget._handleTabCloseRequested(0) + + assert len(close_requested_called) == 1 + assert close_requested_called[0][1] == 0 + +# ============================================================================ +# TTkKodeTab Remove/Close Tests +# ============================================================================ + +def test_kodetab_remove_tab(): + ''' + Test removing tabs from the internal tab widget. + ''' + kodeTab = ttk.TTkKodeTab() + + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + widget3 = ttk.TTkWidget() + + kodeTab.addTab(widget1, "Tab 1") + kodeTab.addTab(widget2, "Tab 2") + kodeTab.addTab(widget3, "Tab 3") + + assert kodeTab._lastKodeTabWidget.count() == 3 + + kodeTab._lastKodeTabWidget.removeTab(1) + + assert kodeTab._lastKodeTabWidget.count() == 2 + assert kodeTab._lastKodeTabWidget.widget(0) == widget1 + assert kodeTab._lastKodeTabWidget.widget(1) == widget3 + +def test_kodetab_remove_all_tabs(): + ''' + Test removing all tabs triggers cleanup of empty widgets. + ''' + kodeTab = ttk.TTkKodeTab() + + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + kodeTab.addTab(widget1, "Tab 1") + kodeTab.addTab(widget2, "Tab 2") + + internal_tab = kodeTab._lastKodeTabWidget + + internal_tab.removeTab(0) + internal_tab.removeTab(0) + + # Should still have the base internal tab widget + assert kodeTab.count() >= 0 + +def test_kodetab_kode_tab_closed(): + ''' + Test _kodeTabClosed cleanup logic. + ''' + kodeTab = ttk.TTkKodeTab() + + widget1 = ttk.TTkWidget() + kodeTab.addTab(widget1, "Tab 1") + + internal_tab = kodeTab._lastKodeTabWidget + + # Remove the tab + internal_tab.removeTab(0) + + # Trigger cleanup + internal_tab._kodeTabClosed() + + # Verify state + assert isinstance(kodeTab._getFirstWidget(), _TTkKodeTab) + +# ============================================================================ +# TTkKodeTab Drag and Drop Tests +# ============================================================================ + +def test_kodetab_drag_enter_event(): + ''' + Test drag enter event handling. + ''' + kodeTab = ttk.TTkKodeTab() + widget = ttk.TTkWidget() + kodeTab.addTab(widget, "Tab 1") + + internal_tab = kodeTab._lastKodeTabWidget + + # Create a mock drag event + evt = TTkDnDEvent(pos=(5,5), data=None) + + # Test drag enter + result = internal_tab.dragEnterEvent(evt) + assert result is True + +def test_kodetab_drag_leave_event(): + ''' + Test drag leave event handling. + ''' + kodeTab = ttk.TTkKodeTab() + widget = ttk.TTkWidget() + kodeTab.addTab(widget, "Tab 1") + + internal_tab = kodeTab._lastKodeTabWidget + internal_tab._frameOverlay = (0, 0, 10, 10) + + evt = TTkDnDEvent(pos=(5,5), data=None) + + result = internal_tab.dragLeaveEvent(evt) + assert result is True + assert internal_tab._frameOverlay is None + +def test_kodetab_drop_event_new_tab_data(): + ''' + Test dropping new tab data onto TTkKodeTab. + ''' + kodeTab = ttk.TTkKodeTab() + widget1 = ttk.TTkWidget() + kodeTab.addTab(widget1, "Tab 1") + + internal_tab = kodeTab._lastKodeTabWidget + + # Create new tab drag data + new_widget = ttk.TTkWidget() + drag_data = _TTkNewTabWidgetDragData("New Tab", new_widget, data="test_data") + + # Create drop event in center (should add to existing tab bar) + evt = TTkDnDEvent(pos=(50,25), data=drag_data) + + result = internal_tab.dropEvent(evt) + + # Verify drop was handled + assert result in (True, False) # Depends on implementation details + +# def test_kodetab_drop_event_tab_widget_data(): +# ''' +# Test dropping tab from another widget. +# ''' +# kodeTab1 = ttk.TTkKodeTab() +# kodeTab2 = ttk.TTkKodeTab() + +# widget1 = ttk.TTkWidget() +# widget2 = ttk.TTkWidget() + +# kodeTab1.addTab(widget1, "Tab 1") +# kodeTab2.addTab(widget2, "Tab 2") + +# internal_tab1 = kodeTab1._lastKodeTabWidget +# internal_tab2 = kodeTab2._lastKodeTabWidget + +# # Get the tab button to drag +# button = internal_tab2.tabButton(0) + +# # Create drag data +# drag_data = _TTkTabWidgetDragData(button, internal_tab2) + +# # Drop in center +# evt = TTkDnDEvent(pos=(50,25), data=drag_data) + +# result = internal_tab1.dropEvent(evt) + +# # Verify handling occurred +# assert result in (True, False) + +def test_kodetab_drop_event_list_of_new_tabs(): + ''' + Test dropping a list of new tabs. + ''' + kodeTab = ttk.TTkKodeTab() + widget1 = ttk.TTkWidget() + kodeTab.addTab(widget1, "Tab 1") + + internal_tab = kodeTab._lastKodeTabWidget + + # Create list of new tab data + new_widget1 = ttk.TTkWidget() + new_widget2 = ttk.TTkWidget() + + drag_data_list = [ + _TTkNewTabWidgetDragData("New Tab 1", new_widget1), + _TTkNewTabWidgetDragData("New Tab 2", new_widget2) + ] + + # Drop in center + evt = TTkDnDEvent(pos=(50,25), data=drag_data_list) + + result = internal_tab.dropEvent(evt) + + assert result in (True, False) + +def test_kodetab_drop_creates_horizontal_split(): + ''' + Test that dropping on left/right zones creates horizontal splits. + ''' + kodeTab = ttk.TTkKodeTab() + widget1 = ttk.TTkWidget() + kodeTab.addTab(widget1, "Tab 1") + + internal_tab = kodeTab._lastKodeTabWidget + + # Mock size for positioning + internal_tab._size = (100, 50) + + new_widget = ttk.TTkWidget() + drag_data = _TTkNewTabWidgetDragData("New Tab", new_widget) + + # Drop on left zone (x < w/4) + evt = TTkDnDEvent(pos=(10,25), data=drag_data) + + initial_count = kodeTab.count() + result = internal_tab.dropEvent(evt) + + # A split may have been created + # Exact behavior depends on implementation + assert kodeTab.count() >= initial_count + +def test_kodetab_drop_creates_vertical_split(): + ''' + Test that dropping on top/bottom zones creates vertical splits. + ''' + kodeTab = ttk.TTkKodeTab() + widget1 = ttk.TTkWidget() + kodeTab.addTab(widget1, "Tab 1") + + internal_tab = kodeTab._lastKodeTabWidget + + # Mock size + internal_tab._size = (100, 50) + + new_widget = ttk.TTkWidget() + drag_data = _TTkNewTabWidgetDragData("New Tab", new_widget) + + # Drop on top zone (y < h/4, adjusted for tab bar) + evt = TTkDnDEvent(pos=(50,5), data=drag_data) + + initial_count = kodeTab.count() + result = internal_tab.dropEvent(evt) + + assert kodeTab.count() >= initial_count + +def test_kodetab_set_drop_event_proxy(): + ''' + Test setting drop event proxy propagates to internal widgets. + ''' + kodeTab = ttk.TTkKodeTab() + widget = ttk.TTkWidget() + kodeTab.addTab(widget, "Tab 1") + + proxy_called = [] + + def proxy(evt): + proxy_called.append(evt) + return True + + kodeTab.setDropEventProxy(proxy) + + # Verify proxy is set + internal_tab = kodeTab._lastKodeTabWidget + assert internal_tab._dropEventProxy == proxy + +# ============================================================================ +# TTkKodeTab Complex Scenarios +# ============================================================================ + +def test_kodetab_multiple_splits(): + ''' + Test creating multiple splits through tab operations. + ''' + kodeTab = ttk.TTkKodeTab() + + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + kodeTab.addTab(widget1, "Tab 1") + kodeTab.addTab(widget2, "Tab 2") + + # Count all tab items + items_before = list(kodeTab.iterItems()) + assert len(items_before) == 2 + +def test_kodetab_get_first_widget(): + ''' + Test getting the first internal widget. + ''' + kodeTab = ttk.TTkKodeTab() + + first = kodeTab._getFirstWidget() + assert isinstance(first, _TTkKodeTab) + assert first._baseWidget == kodeTab + +def test_kodetab_has_menu(): + ''' + Test checking if internal tab has menu. + ''' + kodeTab = ttk.TTkKodeTab() + widget = ttk.TTkWidget() + kodeTab.addTab(widget, "Tab 1") + + internal_tab = kodeTab._lastKodeTabWidget + + # Initially should not have menu + assert internal_tab._hasMenu() is False + +def test_kodetab_add_menu(): + ''' + Test adding menu to TTkKodeTab. + ''' + kodeTab = ttk.TTkKodeTab() + widget = ttk.TTkWidget() + kodeTab.addTab(widget, "Tab 1") + + # Add a menu item + menu_item = kodeTab.addMenu("File", data="file_data") + + assert menu_item is not None + +def test_kodetab_bar_type(): + ''' + Test different bar types for TTkKodeTab. + ''' + from TermTk.TTkWidgets.tabwidget import TTkBarType + + kodeTab1 = ttk.TTkKodeTab(barType=TTkBarType.DEFAULT_3) + kodeTab2 = ttk.TTkKodeTab(barType=TTkBarType.NERD_1) + + assert kodeTab1._barType == TTkBarType.DEFAULT_3 + assert kodeTab2._barType == TTkBarType.NERD_1 + +def test_kodetab_empty_after_all_removals(): + ''' + Test that TTkKodeTab handles becoming empty gracefully. + ''' + kodeTab = ttk.TTkKodeTab() + + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + kodeTab.addTab(widget1, "Tab 1") + kodeTab.addTab(widget2, "Tab 2") + + internal_tab = kodeTab._lastKodeTabWidget + + # Remove all tabs + internal_tab.removeTab(0) + internal_tab.removeTab(0) + + # Trigger cleanup + internal_tab._kodeTabClosed() + + # Should still be in valid state + assert kodeTab._getFirstWidget() is not None + +def test_kodetab_paint_child_canvas(): + ''' + Test that paintChildCanvas handles frame overlay correctly. + ''' + kodeTab = ttk.TTkKodeTab() + widget = ttk.TTkWidget() + kodeTab.addTab(widget, "Tab 1") + + internal_tab = kodeTab._lastKodeTabWidget + + # Set frame overlay + internal_tab._frameOverlay = (10, 10, 50, 30) + + # Call paintChildCanvas (should not raise) + try: + internal_tab.paintChildCanvas() + assert True + except Exception as e: + assert False, f"paintChildCanvas raised exception: {e}" + + # Clear overlay + internal_tab._frameOverlay = None + internal_tab.paintChildCanvas() + +def test_kodetab_drop_invalid_data(): + ''' + Test that dropping invalid data returns False. + ''' + kodeTab = ttk.TTkKodeTab() + widget = ttk.TTkWidget() + kodeTab.addTab(widget, "Tab 1") + + internal_tab = kodeTab._lastKodeTabWidget + + # Create event with invalid data + evt = TTkDnDEvent(pos=(50,25), data="invalid_data") + + result = internal_tab.dropEvent(evt) + + # Should not accept invalid data + assert result is False + +def test_kodetab_iter_items_after_operations(): + ''' + Test that iterItems works correctly after add/remove operations. + ''' + kodeTab = ttk.TTkKodeTab() + + widgets = [ttk.TTkWidget() for _ in range(5)] + + for i, widget in enumerate(widgets): + kodeTab.addTab(widget, f"Tab {i}") + + items = list(kodeTab.iterItems()) + assert len(items) == 5 + + # Remove some tabs + kodeTab._lastKodeTabWidget.removeTab(2) + kodeTab._lastKodeTabWidget.removeTab(1) + + items = list(kodeTab.iterItems()) + assert len(items) == 3 diff --git a/tests/pytest/widgets/test_tab.py b/tests/pytest/widgets/test_tab.py new file mode 100644 index 00000000..4dbfea2c --- /dev/null +++ b/tests/pytest/widgets/test_tab.py @@ -0,0 +1,666 @@ +# 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 + +# ============================================================================ +# TTkTabBar Tests +# ============================================================================ + +def test_tabbar_add_tabs(): + ''' + Test adding tabs to TTkTabBar using addTab(). + Verifies that tabs are added correctly and currentIndex is updated. + ''' + tabBar = ttk.TTkTabBar() + + assert tabBar.currentIndex() == -1 + + index0 = tabBar.addTab("Tab 1") + assert index0 == 0 + assert tabBar.currentIndex() == 0 + + index1 = tabBar.addTab("Tab 2", data="data2") + assert index1 == 1 + assert tabBar.currentIndex() == 0 + + index2 = tabBar.addTab("Tab 3") + assert index2 == 2 + assert tabBar.currentIndex() == 0 + +def test_tabbar_insert_tabs(): + ''' + Test inserting tabs at specific positions in TTkTabBar. + Verifies that tabs are inserted at correct indices and currentIndex is adjusted. + ''' + tabBar = ttk.TTkTabBar() + + tabBar.addTab("Tab 1") + tabBar.addTab("Tab 2") + tabBar.addTab("Tab 3") + + # Insert at beginning + index = tabBar.insertTab(0, "Tab 0") + assert index == 0 + assert tabBar.currentIndex() == 1 # Current should shift + + # Insert in middle + index = tabBar.insertTab(2, "Tab 1.5") + assert index == 2 + assert tabBar.currentIndex() == 1 # Current shouldn't change + +def test_tabbar_remove_tabs(): + ''' + Test removing tabs from TTkTabBar. + Verifies that tabs are removed correctly and currentIndex is adjusted. + ''' + tabBar = ttk.TTkTabBar() + + tabBar.addTab("Tab 0") + tabBar.addTab("Tab 1") + tabBar.addTab("Tab 2") + tabBar.addTab("Tab 3") + + tabBar.setCurrentIndex(2) + assert tabBar.currentIndex() == 2 + + # Remove tab before current + tabBar.removeTab(0) + assert tabBar.currentIndex() == 1 # Should decrement + + # Remove current tab + tabBar.removeTab(1) + assert tabBar.currentIndex() == 0 # Should stay but point to next tab + + # Remove last tab + tabBar.removeTab(1) + assert tabBar.currentIndex() == 0 + +def test_tabbar_tab_data(): + ''' + Test setting and getting tab data. + ''' + tabBar = ttk.TTkTabBar() + + tabBar.addTab("Tab 1", data="data1") + tabBar.addTab("Tab 2", data={"key": "value"}) + tabBar.addTab("Tab 3") + + assert tabBar.tabData(0) == "data1" + assert tabBar.tabData(1) == {"key": "value"} + assert tabBar.tabData(2) is None + + # Test setTabData + tabBar.setTabData(2, "new_data") + assert tabBar.tabData(2) == "new_data" + + # Test currentData + tabBar.setCurrentIndex(1) + assert tabBar.currentData() == {"key": "value"} + +def test_tabbar_current_index(): + ''' + Test setting and getting current index. + ''' + tabBar = ttk.TTkTabBar() + + tabBar.addTab("Tab 0") + tabBar.addTab("Tab 1") + tabBar.addTab("Tab 2") + + assert tabBar.currentIndex() == 0 + + tabBar.setCurrentIndex(1) + assert tabBar.currentIndex() == 1 + + tabBar.setCurrentIndex(2) + assert tabBar.currentIndex() == 2 + + # Test invalid index (should not change) + tabBar.setCurrentIndex(10) + assert tabBar.currentIndex() == 2 + + tabBar.setCurrentIndex(-1) + assert tabBar.currentIndex() == 2 + +def test_tabbar_signals(): + ''' + Test that signals are emitted correctly. + ''' + tabBar = ttk.TTkTabBar() + + current_changed_called = [] + tab_clicked_called = [] + tab_close_called = [] + + tabBar.currentChanged.connect(lambda i: current_changed_called.append(i)) + tabBar.tabBarClicked.connect(lambda i: tab_clicked_called.append(i)) + tabBar.tabCloseRequested.connect(lambda i: tab_close_called.append(i)) + + tabBar.addTab("Tab 0") + assert len(current_changed_called) == 1 + assert current_changed_called[0] == 0 + + tabBar.addTab("Tab 1") + tabBar.setCurrentIndex(1) + assert len(current_changed_called) == 2 + assert current_changed_called[1] == 1 + +def test_tabbar_closable_tabs(): + ''' + Test closable tabs functionality. + ''' + tabBar = ttk.TTkTabBar(closable=True) + + assert tabBar.tabsClosable() is True + + tabBar.addTab("Tab 0") + tabBar.addTab("Tab 1", closable=False) # Override for this tab + tabBar.addTab("Tab 2") + + # Test that tabs can be added with different closable settings + button0 = tabBar.tabButton(0) + button1 = tabBar.tabButton(1) + button2 = tabBar.tabButton(2) + + assert button0 is not None + assert button1 is not None + assert button2 is not None + +# ============================================================================ +# TTkTabWidget Tests +# ============================================================================ + +def test_tabwidget_add_tabs(): + ''' + Test adding tabs to TTkTabWidget with widgets. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + assert tabWidget.count() == 0 + assert tabWidget.currentIndex() == -1 + + index0 = tabWidget.addTab(widget0, "Tab 0") + assert index0 == 0 + assert tabWidget.count() == 1 + assert tabWidget.currentIndex() == 0 + assert widget0.parentWidget() == tabWidget + + index1 = tabWidget.addTab(widget1, "Tab 1", data="data1") + assert index1 == 1 + assert tabWidget.count() == 2 + + index2 = tabWidget.addTab(widget2, "Tab 2") + assert index2 == 2 + assert tabWidget.count() == 3 + +def test_tabwidget_insert_tabs(): + ''' + Test inserting tabs at specific positions. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + widget3 = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0") + tabWidget.addTab(widget1, "Tab 1") + tabWidget.addTab(widget2, "Tab 2") + + # Insert at beginning + index = tabWidget.insertTab(0, widget3, "Tab -1") + assert index == 0 + assert tabWidget.count() == 4 + assert tabWidget.widget(0) == widget3 + assert tabWidget.currentIndex() == 1 # Should shift + +def test_tabwidget_remove_tabs(): + ''' + Test removing tabs from TTkTabWidget. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0") + tabWidget.addTab(widget1, "Tab 1") + tabWidget.addTab(widget2, "Tab 2") + + assert tabWidget.count() == 3 + + tabWidget.removeTab(1) + assert tabWidget.count() == 2 + assert tabWidget.widget(0) == widget0 + assert tabWidget.widget(1) == widget2 + + tabWidget.removeTab(0) + assert tabWidget.count() == 1 + assert tabWidget.widget(0) == widget2 + +def test_tabwidget_current_widget(): + ''' + Test getting and setting current widget. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0") + tabWidget.addTab(widget1, "Tab 1") + tabWidget.addTab(widget2, "Tab 2") + + assert tabWidget.currentWidget() == widget0 + + tabWidget.setCurrentWidget(widget1) + assert tabWidget.currentWidget() == widget1 + assert tabWidget.currentIndex() == 1 + + tabWidget.setCurrentWidget(widget2) + assert tabWidget.currentWidget() == widget2 + assert tabWidget.currentIndex() == 2 + +def test_tabwidget_widget_by_index(): + ''' + Test accessing widgets by index. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0") + tabWidget.addTab(widget1, "Tab 1") + tabWidget.addTab(widget2, "Tab 2") + + assert tabWidget.widget(0) == widget0 + assert tabWidget.widget(1) == widget1 + assert tabWidget.widget(2) == widget2 + assert tabWidget.widget(10) is None + assert tabWidget.widget(-1) is None + +def test_tabwidget_index_of(): + ''' + Test finding index of a widget. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + widget_not_added = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0") + tabWidget.addTab(widget1, "Tab 1") + tabWidget.addTab(widget2, "Tab 2") + + assert tabWidget.indexOf(widget0) == 0 + assert tabWidget.indexOf(widget1) == 1 + assert tabWidget.indexOf(widget2) == 2 + assert tabWidget.indexOf(widget_not_added) == -1 + +def test_tabwidget_tab_data(): + ''' + Test tab data in TTkTabWidget. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0", data="data0") + tabWidget.addTab(widget1, "Tab 1", data={"key": "value"}) + + assert tabWidget.tabData(0) == "data0" + assert tabWidget.tabData(1) == {"key": "value"} + + tabWidget.setTabData(0, "new_data") + assert tabWidget.tabData(0) == "new_data" + + assert tabWidget.currentData() == "new_data" + +def test_tabwidget_signals(): + ''' + Test that TTkTabWidget signals are emitted correctly. + ''' + tabWidget = ttk.TTkTabWidget() + + current_changed_called = [] + tab_clicked_called = [] + + tabWidget.currentChanged.connect(lambda i: current_changed_called.append(i)) + tabWidget.tabBarClicked.connect(lambda i: tab_clicked_called.append(i)) + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0") + assert len(current_changed_called) == 1 + + tabWidget.addTab(widget1, "Tab 1") + tabWidget.setCurrentIndex(1) + assert len(current_changed_called) == 2 + assert current_changed_called[1] == 1 + +def test_tabwidget_widget_visibility(): + ''' + Test that only the current widget is visible. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0") + tabWidget.addTab(widget1, "Tab 1") + tabWidget.addTab(widget2, "Tab 2") + + # Initially all should be hidden (added hidden) + assert widget0.isVisible() is True + assert widget1.isVisible() is False + assert widget2.isVisible() is False + + # After setting index, current should be visible + tabWidget.setCurrentIndex(0) + # Note: Visibility is controlled internally, check currentWidget + assert tabWidget.currentWidget() == widget0 + + tabWidget.setCurrentIndex(1) + assert tabWidget.currentWidget() == widget1 + + tabWidget.setCurrentIndex(2) + assert tabWidget.currentWidget() == widget2 + +def test_tabwidget_closable(): + ''' + Test closable tabs in TTkTabWidget. + ''' + tabWidget = ttk.TTkTabWidget(closable=True) + + assert tabWidget.tabsClosable() is True + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0") + tabWidget.addTab(widget1, "Tab 1", closable=False) + + # Both tabs should be added + assert tabWidget.count() == 2 + +def test_tabwidget_remove_all_tabs(): + ''' + Test removing all tabs from TTkTabWidget. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + widget2 = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0") + tabWidget.addTab(widget1, "Tab 1") + tabWidget.addTab(widget2, "Tab 2") + + assert tabWidget.count() == 3 + + tabWidget.removeTab(0) + tabWidget.removeTab(0) + tabWidget.removeTab(0) + + assert tabWidget.count() == 0 + assert tabWidget.currentIndex() == -1 + +def test_tabwidget_bar_type(): + ''' + Test different bar types. + ''' + from TermTk.TTkWidgets.tabwidget import TTkBarType + + tabWidget1 = ttk.TTkTabWidget(barType=TTkBarType.DEFAULT_3) + tabWidget2 = ttk.TTkTabWidget(barType=TTkBarType.DEFAULT_2) + tabWidget3 = ttk.TTkTabWidget(barType=TTkBarType.NERD_1) + + # Just verify they can be created + assert tabWidget1 is not None + assert tabWidget2 is not None + assert tabWidget3 is not None + +# ============================================================================ +# Drag and Drop Related Tests +# ============================================================================ + +def test_tabwidget_drag_data_classes(): + ''' + Test that drag data classes exist and can be instantiated. + ''' + from TermTk.TTkWidgets.tabwidget import ( + _TTkTabWidgetDragData, + _TTkNewTabWidgetDragData, + _TTkTabBarDragData + ) + + tabWidget = ttk.TTkTabWidget() + widget = ttk.TTkWidget() + tabWidget.addTab(widget, "Tab 0") + + button = tabWidget.tabButton(0) + + # Test _TTkTabWidgetDragData + drag_data1 = _TTkTabWidgetDragData(button, tabWidget) + assert drag_data1.tabButton() == button + assert drag_data1.tabWidget() == tabWidget + + # Test _TTkNewTabWidgetDragData + new_widget = ttk.TTkWidget() + drag_data2 = _TTkNewTabWidgetDragData("Label", new_widget, data="test", closable=True) + assert drag_data2.label() == "Label" + assert drag_data2.widget() == new_widget + assert drag_data2.data() == "test" + assert drag_data2.closable() is True + + # Test _TTkTabBarDragData + tabBar = ttk.TTkTabBar() + tabBar.addTab("Tab") + bar_button = tabBar.tabButton(0) + drag_data3 = _TTkTabBarDragData(bar_button, tabBar) + assert drag_data3.tabButton() == bar_button + assert drag_data3.tabBar() == tabBar + +def test_tabbar_tab_button(): + ''' + Test accessing tab buttons from TTkTabBar. + ''' + tabBar = ttk.TTkTabBar() + + tabBar.addTab("Tab 0") + tabBar.addTab("Tab 1") + tabBar.addTab("Tab 2") + + button0 = tabBar.tabButton(0) + button1 = tabBar.tabButton(1) + button2 = tabBar.tabButton(2) + + assert button0 is not None + assert button1 is not None + assert button2 is not None + + # Test button text + assert button0.text() == "Tab 0" + assert button1.text() == "Tab 1" + assert button2.text() == "Tab 2" + + # Test invalid index + assert tabBar.tabButton(10) is None + assert tabBar.tabButton(-1) is None + +def test_tabwidget_tab_button(): + ''' + Test accessing tab buttons from TTkTabWidget. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + + tabWidget.addTab(widget0, "Tab 0") + tabWidget.addTab(widget1, "Tab 1") + + button0 = tabWidget.tabButton(0) + button1 = tabWidget.tabButton(1) + + assert button0 is not None + assert button1 is not None + assert button0.text() == "Tab 0" + assert button1.text() == "Tab 1" + +def test_tabbar_multiple_add_remove(): + ''' + Test multiple add/remove operations to ensure state consistency. + ''' + tabBar = ttk.TTkTabBar() + + # Add multiple tabs + for i in range(5): + tabBar.addTab(f"Tab {i}") + + assert tabBar.currentIndex() == 0 + + # Remove some tabs + tabBar.removeTab(2) + tabBar.removeTab(1) + + # Add more tabs + tabBar.addTab("New Tab 1") + tabBar.addTab("New Tab 2") + + # Verify state is consistent + assert tabBar.tabButton(0) is not None + assert tabBar.tabButton(0).text() == "Tab 0" + +def test_tabwidget_multiple_add_remove(): + ''' + Test multiple add/remove operations with widgets. + ''' + tabWidget = ttk.TTkTabWidget() + + widgets = [ttk.TTkWidget() for _ in range(5)] + + # Add all widgets + for i, widget in enumerate(widgets): + tabWidget.addTab(widget, f"Tab {i}") + + assert tabWidget.count() == 5 + + # Remove some + tabWidget.removeTab(2) + tabWidget.removeTab(1) + + assert tabWidget.count() == 3 + + # Add more + new_widget = ttk.TTkWidget() + tabWidget.addTab(new_widget, "New Tab") + + assert tabWidget.count() == 4 + +def test_tabbar_set_current_after_remove(): + ''' + Test that setting current index works correctly after removing tabs. + ''' + tabBar = ttk.TTkTabBar() + + tabBar.addTab("Tab 0") + tabBar.addTab("Tab 1") + tabBar.addTab("Tab 2") + tabBar.addTab("Tab 3") + + tabBar.setCurrentIndex(3) + assert tabBar.currentIndex() == 3 + + # Remove current tab + tabBar.removeTab(3) + + # Current should be adjusted + assert tabBar.currentIndex() < 3 + + # Should be able to set to valid index + tabBar.setCurrentIndex(1) + assert tabBar.currentIndex() == 1 + +def test_tabwidget_parent_relationship(): + ''' + Test that widgets maintain correct parent relationships when added to tabs. + ''' + tabWidget = ttk.TTkTabWidget() + + widget0 = ttk.TTkWidget() + widget1 = ttk.TTkWidget() + + assert widget0.parentWidget() is None + assert widget1.parentWidget() is None + + tabWidget.addTab(widget0, "Tab 0") + assert widget0.parentWidget() == tabWidget + + tabWidget.addTab(widget1, "Tab 1") + assert widget1.parentWidget() == tabWidget + + # After removal, parent should still be tabWidget (it's in the layout) + tabWidget.removeTab(0) + # Note: The widget may still have tabWidget as parent depending on implementation + +def test_tabbar_empty_initialization(): + ''' + Test that empty TTkTabBar initializes correctly. + ''' + tabBar = ttk.TTkTabBar() + + assert tabBar.currentIndex() == -1 + assert tabBar.tabButton(0) is None + assert tabBar.tabData(0) is None + assert tabBar.currentData() is None + +def test_tabwidget_empty_initialization(): + ''' + Test that empty TTkTabWidget initializes correctly. + ''' + tabWidget = ttk.TTkTabWidget() + + assert tabWidget.count() == 0 + assert tabWidget.currentIndex() == -1 + assert tabWidget.widget(0) is None + assert tabWidget.indexOf(ttk.TTkWidget()) == -1 diff --git a/tests/t.ui/test.ui.012.tab.py b/tests/t.ui/test.ui.012.tab.01.py similarity index 96% rename from tests/t.ui/test.ui.012.tab.py rename to tests/t.ui/test.ui.012.tab.01.py index 670af86c..be97ffe9 100755 --- a/tests/t.ui/test.ui.012.tab.py +++ b/tests/t.ui/test.ui.012.tab.01.py @@ -24,7 +24,7 @@ import sys, os -sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) import TermTk as ttk ttk.TTkLog.use_default_file_logging() diff --git a/tests/t.ui/test.ui.012.tab.02.py b/tests/t.ui/test.ui.012.tab.02.py new file mode 100755 index 00000000..6211c55e --- /dev/null +++ b/tests/t.ui/test.ui.012.tab.02.py @@ -0,0 +1,87 @@ +#!/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 + +root = ttk.TTk(mouseTrack=True) + +winTabbed1 = ttk.TTkWindow(parent=root,pos=(0,0), size=(80,20), title="Test Tab 1", border=True, layout=ttk.TTkGridLayout()) +tabWidget1 = ttk.TTkTabWidget(parent=winTabbed1, border=True, barType=ttk.TTkBarType.DEFAULT_3) +tabWidget1.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame1.1"), "Label 1.1") +tabWidget1.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame1.2"), "Label 1.2") +tabWidget1.addTab(ttk.TTkTestWidget(border=True, title="Frame1.3"), "Label Test 1.3") +tabWidget1.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame1.4"), "Label 1.4") +tabWidget1.addTab(ttk.TTkTestWidget(border=True, title="Frame1.5"), "Label Test 1.5") +tabWidget1.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame1.6"), "Label 1.6") + +winTabbed2 = ttk.TTkWindow(parent=root,pos=(10,2), size=(80,20), title="Test Tab 2", border=True, layout=ttk.TTkGridLayout()) +tabWidget2 = ttk.TTkTabWidget(parent=winTabbed2, border=True, barType=ttk.TTkBarType.DEFAULT_3) +tabWidget2.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame2.1"), "Label 2.1") +tabWidget2.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame2.2"), "Label 2.2") +tabWidget2.addTab(ttk.TTkTestWidget(border=True, title="Frame2.3"), "Label Test 2.3") +tabWidget2.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame2.4"), "Label 2.4") +tabWidget2.addTab(ttk.TTkTestWidget(border=True, title="Frame2.5"), "Label Test 2.5") +tabWidget2.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame2.6"), "Label 2.6") +tabWidget2.addMenu("Foo") +tabWidget2.addMenu("Bar", ttk.TTkK.RIGHT) + +winTabbed3 = ttk.TTkWindow(parent=root,pos=(20,4), size=(80,20), title="Test Tab 3", border=True, layout=ttk.TTkGridLayout()) +tabWidget3 = ttk.TTkTabWidget(parent=winTabbed3, border=True, barType=ttk.TTkBarType.DEFAULT_2) +tabWidget3.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame3.1"), "Label 3.1") +tabWidget3.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame3.2"), "Label 3.2") +tabWidget3.addTab(ttk.TTkTestWidget(border=True, title="Frame3.3"), "Label Test 3.3") +tabWidget3.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame3.4"), "Label 3.4") +tabWidget3.addTab(ttk.TTkTestWidget(border=True, title="Frame3.5"), "Label Test 3.5") +tabWidget3.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame3.6"), "Label 3.6") + +winTabbed4 = ttk.TTkWindow(parent=root,pos=(30,6), size=(80,20), title="Test Tab 4", border=True, layout=ttk.TTkGridLayout()) +tabWidget4 = ttk.TTkTabWidget(parent=winTabbed4, border=True, barType=ttk.TTkBarType.DEFAULT_2) +tabWidget4.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame4.1"), "Label 4.1") +tabWidget4.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame4.2"), "Label 4.2") +tabWidget4.addTab(ttk.TTkTestWidget(border=True, title="Frame4.3"), "Label Test 4.3") +tabWidget4.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame4.4"), "Label 4.4") +tabWidget4.addTab(ttk.TTkTestWidget(border=True, title="Frame4.5"), "Label Test 4.5") +tabWidget4.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame4.6"), "Label 4.6") +tabWidget4.addMenu("Baz") +tabWidget4.addMenu("Foo", ttk.TTkK.RIGHT) + +winTabbed5 = ttk.TTkWindow(parent=root,pos=(40,8), size=(80,20), title="Test Tab 5", border=True, layout=ttk.TTkGridLayout()) +tabWidget5 = ttk.TTkTabWidget(parent=winTabbed5, border=True, barType=ttk.TTkBarType.NERD_1) +tabWidget5.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame5.1"), "Label 5.1") +tabWidget5.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame5.2"), "Label 5.2") +tabWidget5.addTab(ttk.TTkTestWidget(border=True, title="Frame5.3"), "Label Test 5.3") +tabWidget5.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame5.4"), "Label 5.4") +tabWidget5.addTab(ttk.TTkTestWidget(border=True, title="Frame5.5"), "Label Test 5.5") +tabWidget5.addTab(ttk.TTkTestWidgetSizes(border=True, title="Frame5.6"), "Label 5.6") + +winLogs = ttk.TTkWindow(parent=root,pos=(45,10), size=(100,30), title="Logs", border=True, layout=ttk.TTkGridLayout()) +ttk.TTkLogViewer(parent=winLogs) + +winKeys = ttk.TTkWindow(parent=root,pos=(50,15), size=(100,7), title="Key Press", border=True, layout=ttk.TTkGridLayout()) +ttk.TTkKeyPressView(parent=winKeys) + +root.mainloop() \ No newline at end of file