From 26ce74c0225baa84b7212eb84e3fb843dd2ad480 Mon Sep 17 00:00:00 2001 From: Pier CeccoPierangioliEugenio Date: Tue, 2 Sep 2025 00:15:35 +0100 Subject: [PATCH] chore(TTkTree): performance improvement due to smart caching (#451) --- .vscode/launch.json | 28 +- TermTk | 1 - demo/showcase/filepicker.py | 2 +- libs/pyTermTk/TermTk/TTkCore/constant.py | 4 +- libs/pyTermTk/TermTk/TTkCore/string.py | 4 +- libs/pyTermTk/TermTk/TTkLayouts/layout.py | 11 +- .../TTkWidgets/TTkModelView/filetreewidget.py | 12 - .../TTkModelView/filetreewidgetitem.py | 15 +- .../TTkWidgets/TTkModelView/treewidget.py | 238 ++++---- .../TTkWidgets/TTkModelView/treewidgetitem.py | 523 ++++++++++++------ tests/pytest/run_test_001_demo.py | 2 +- tests/pytest/run_test_002_textedit.py | 2 +- tests/pytest/test_003_string.py | 2 +- tests/pytest/test_005_tree.py | 472 ++++++++++++++++ tests/t.ui/test.ui.011.tree.02.widgets.py | 2 +- tests/t.ui/test.ui.011.tree.03.widgets.py | 2 +- tests/t.ui/test.ui.011.tree.04.dnd.01.py | 2 +- tests/t.ui/test.ui.011.tree.05.paging.py | 144 +++++ tests/timeit/02.array.10.List.creation.py | 90 +++ tests/timeit/33.tree.01.iterate.py | 219 ++++++++ tests/timeit/33.tree.02.iterate.py | 159 ++++++ tests/timeit/33.tree.03.iterate.py | 221 ++++++++ tests/timeit/33.tree.04.listify.py | 150 +++++ 23 files changed, 1992 insertions(+), 313 deletions(-) delete mode 120000 TermTk create mode 100644 tests/pytest/test_005_tree.py create mode 100755 tests/t.ui/test.ui.011.tree.05.paging.py create mode 100755 tests/timeit/02.array.10.List.creation.py create mode 100755 tests/timeit/33.tree.01.iterate.py create mode 100755 tests/timeit/33.tree.02.iterate.py create mode 100755 tests/timeit/33.tree.03.iterate.py create mode 100755 tests/timeit/33.tree.04.listify.py diff --git a/.vscode/launch.json b/.vscode/launch.json index e56b17f7..1fc60d27 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -46,7 +46,8 @@ "console": "integratedTerminal", "justMyCode": true, "env": { - "TERMTK_GPM": "1" + "TERMTK_GPM": "1", + "PYTHONPATH": "./libs/pyTermTk" }, }, { @@ -59,7 +60,10 @@ "args": [ "-p", "tmp/test.input.001.bin" - ] + ], + "env": { + "PYTHONPATH": "./libs/pyTermTk" + } }, { "name": "py Debug: Module ttkDesigner", @@ -69,7 +73,7 @@ "console": "integratedTerminal", "justMyCode": true, "env": { - "PYTHONPATH": "./apps/ttkDesigner" + "PYTHONPATH": "./libs/pyTermTk:./apps/ttkDesigner" } }, { @@ -80,7 +84,7 @@ "console": "integratedTerminal", "justMyCode": true, "env": { - "PYTHONPATH": "./tools" + "PYTHONPATH": "./libs/pyTermTk:./tools" }, "args": [ "ttkDesigner/tui/newWindow.tui.json" @@ -94,7 +98,7 @@ "console": "integratedTerminal", "justMyCode": true, "env": { - "PYTHONPATH": "./apps/dumbPaintTool" + "PYTHONPATH": "./libs/pyTermTk:./apps/dumbPaintTool" } }, { @@ -105,7 +109,7 @@ "console": "integratedTerminal", "justMyCode": true, "env": { - "PYTHONPATH": "./tools" + "PYTHONPATH": "./libs/pyTermTk:./tools" }, "args": [ "experiments/untitled.DPT.json" @@ -130,7 +134,7 @@ "console": "integratedTerminal", "justMyCode": true, "env": { - "PYTHONPATH": "./apps/tlogg" + "PYTHONPATH": "./apps/tlogg:./libs/pyTermTk" } }, { @@ -139,7 +143,10 @@ "request": "launch", "program": "demo/demo.py", "console": "integratedTerminal", - "justMyCode": true + "justMyCode": true, + "env": { + "PYTHONPATH": "./libs/pyTermTk" + } }, { "name": "Python: Demo Mouse tracking", @@ -150,7 +157,10 @@ "justMyCode": true, "args": [ "-t" - ] + ], + "env": { + "PYTHONPATH": "./libs/pyTermTk" + } }, { "name": "Python: sphinx", diff --git a/TermTk b/TermTk deleted file mode 120000 index 547ca510..00000000 --- a/TermTk +++ /dev/null @@ -1 +0,0 @@ -libs/pyTermTk/TermTk \ No newline at end of file diff --git a/demo/showcase/filepicker.py b/demo/showcase/filepicker.py index aba689fe..88f07723 100755 --- a/demo/showcase/filepicker.py +++ b/demo/showcase/filepicker.py @@ -24,7 +24,7 @@ import sys, os, argparse -sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) import TermTk as ttk diff --git a/libs/pyTermTk/TermTk/TTkCore/constant.py b/libs/pyTermTk/TermTk/TTkCore/constant.py index e3313fb1..1acde11f 100644 --- a/libs/pyTermTk/TermTk/TTkCore/constant.py +++ b/libs/pyTermTk/TermTk/TTkCore/constant.py @@ -262,7 +262,7 @@ class TTkConstant: DontShowIndicator = ChildIndicatorPolicy.DontShowIndicator DontShowIndicatorWhenChildless = ChildIndicatorPolicy.DontShowIndicatorWhenChildless - class SortOrder(int): + class SortOrder(IntEnum): '''This enum describes how the items in a widget are sorted. .. autosummary:: @@ -418,7 +418,7 @@ class TTkConstant: Input_Password = 0x04 # Alignment - class Alignment(int): + class Alignment(IntEnum): ''' This type is used to describe alignment. .. autosummary:: diff --git a/libs/pyTermTk/TermTk/TTkCore/string.py b/libs/pyTermTk/TermTk/TTkCore/string.py index a44e6e92..eabbd0c6 100644 --- a/libs/pyTermTk/TermTk/TTkCore/string.py +++ b/libs/pyTermTk/TermTk/TTkCore/string.py @@ -119,7 +119,7 @@ class TTkString(): def __str__(self) -> str: return self._text - def __add__(self, other:TTkString) -> TTkString: + def __add__(self, other:Union[TTkStringType,TTkColor]) -> TTkString: ret = TTkString() ret._baseColor = self._baseColor if isinstance(other, TTkString): @@ -141,7 +141,7 @@ class TTkString(): ret._baseColor = other return ret - def __radd__(self, other:TTkString) -> TTkString: + def __radd__(self, other:TTkStringType) -> TTkString: ret = TTkString() ret._baseColor = self._baseColor if isinstance(other, TTkString): diff --git a/libs/pyTermTk/TermTk/TTkLayouts/layout.py b/libs/pyTermTk/TermTk/TTkLayouts/layout.py index 7f772fb7..97233ae6 100644 --- a/libs/pyTermTk/TermTk/TTkLayouts/layout.py +++ b/libs/pyTermTk/TermTk/TTkLayouts/layout.py @@ -301,9 +301,18 @@ class TTkLayout(TTkLayoutItem): '''removeItem''' self.removeItems([item]) + def clear(self) -> None: + '''clear''' + for item in self._items: + if item._layoutItemType == TTkK.WidgetItem: + item.widget().setParent(None) + item.setParent(None) + self._items = [] + self._zSortItems() + def removeItems(self, items): '''removeItems''' - for item in items: + for item in items.copy(): if item in self._items: self._items.remove(item) if item._layoutItemType == TTkK.WidgetItem: diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/filetreewidget.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/filetreewidget.py index 03488b7d..30fc8dfd 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/filetreewidget.py +++ b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/filetreewidget.py @@ -158,8 +158,6 @@ class TTkFileTreeWidget(TTkTreeWidget): self.resizeColumnToContents(1) self.resizeColumnToContents(2) self.resizeColumnToContents(3) - self.itemExpanded.connect(self._folderExpanded) - self.itemCollapsed.connect(self._folderCollapsed) self.itemExpanded.connect(self._updateChildren) self.itemActivated.connect(self._activated) @@ -222,7 +220,6 @@ class TTkFileTreeWidget(TTkTreeWidget): raw = [ n , -1 , typef , rawTime ], path=nodePath, type=TTkFileTreeWidgetItem.DIR, - icon=TTkString() + TTkCfg.theme.folderIconColor + TTkCfg.theme.fileIcon.folderClose + TTkColor.RST, childIndicatorPolicy=TTkK.ShowIndicator)) elif os.path.isfile(nodePath) or os.path.islink(nodePath): @@ -252,18 +249,9 @@ class TTkFileTreeWidget(TTkTreeWidget): raw = [ n , rawSize , typef , rawTime ], path=nodePath, type=TTkFileTreeWidgetItem.FILE, - icon=TTkString() + TTkCfg.theme.fileIconColor + TTkCfg.theme.fileIcon.getIcon(n) + TTkColor.RST, childIndicatorPolicy=TTkK.DontShowIndicator)) return ret - @staticmethod - def _folderExpanded(item): - item.setIcon(0, TTkString() + TTkCfg.theme.folderIconColor + TTkCfg.theme.fileIcon.folderOpen + TTkColor.RST,) - - @staticmethod - def _folderCollapsed(item): - item.setIcon(0, TTkString() + TTkCfg.theme.folderIconColor + TTkCfg.theme.fileIcon.folderClose + TTkColor.RST,) - @pyTTkSlot(TTkFileTreeWidgetItem) def _updateChildren(self, item): if item.children(): return diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/filetreewidgetitem.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/filetreewidgetitem.py index ca334de2..8711eb2b 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/filetreewidgetitem.py +++ b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/filetreewidgetitem.py @@ -24,6 +24,8 @@ __all__ = ['TTkFileTreeWidgetItem'] import re +from TermTk.TTkCore.cfg import TTkCfg +from TermTk.TTkCore.string import TTkString from TermTk.TTkCore.constant import TTkK from TermTk.TTkWidgets.TTkModelView.treewidgetitem import TTkTreeWidgetItem @@ -44,7 +46,7 @@ class TTkFileTreeWidgetItem(TTkTreeWidgetItem): self.setTextAlignment(1, TTkK.RIGHT_ALIGN) def setFilter(self, filter:str) -> None: - for c in self._children: + for c in self.children(): c.dataChanged.disconnect(self.emitDataChanged) c._processFilter(filter) c.setFilter(filter) @@ -59,6 +61,17 @@ class TTkFileTreeWidgetItem(TTkTreeWidgetItem): else: self.setHidden(True) + def icon(self, col): + if col > 0: + return super().icon(col) + if self._type == TTkFileTreeWidgetItem.FILE: + return TTkString( ' '+TTkCfg.theme.fileIcon.getIcon(self._path)+' ', TTkCfg.theme.fileIconColor) + else: + if self._expanded: + return TTkString(' '+TTkCfg.theme.fileIcon.folderOpen+' ', TTkCfg.theme.folderIconColor) + else: + return TTkString(' '+TTkCfg.theme.fileIcon.folderClose+' ', TTkCfg.theme.folderIconColor) + def sortData(self, col:int): return self._raw[col] diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidget.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidget.py index 016937af..522916c2 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidget.py +++ b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidget.py @@ -22,9 +22,10 @@ __all__ = ['TTkTreeWidget'] -from typing import List +from typing import List,Tuple,Optional from TermTk.TTkCore.cfg import TTkCfg +from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.color import TTkColor from TermTk.TTkCore.string import TTkString @@ -39,6 +40,59 @@ from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot from dataclasses import dataclass +class _RootWidgetItem(TTkTreeWidgetItem): + __slots__ = ('_widgets_buffer','_widgets_buffer_check') + + _widgets_buffer:List[int] + _widgets_buffer_check:int + + def __init__(self): + self._widgets_buffer = [] + self._widgets_buffer_check=0 + super().__init__(expanded=True) + + def _getColumnContentSize(self, column:int, offset:int) -> int: + if offset+0x200 > (_sz:=self.size()): + offset = _sz-0x200 + if offset < 0x200: + offset = 0x200 + limited_page = self._get_page_root(offset-0x200,0x400) + if column==0: + size = max(max(_l+_i.icon(column).termWidth()+_t.termWidth() for _t in _i.data(column).split('\n')) for _l,_y,_i in limited_page if not _y) + else: + size = max(max((_i.icon(column)+_t).termWidth() for _t in _i.data(column).split('\n')) for _l,_y,_i in limited_page if not _y) + return size-1 + + def _get_page_root(self, index:int, size:int) -> List[Tuple[int,int,TTkTreeWidgetItem]]: + if self._children: + if self._widgets_buffer_check >= len(self._children._buffer): + self._widgets_buffer = [] + self._widgets_buffer_check=0 + page = self._children.get_page(0, index, size) + if any(_wbi[1] != self._children._buffer[_wbi[0]] for _wbi in self._widgets_buffer): + self._widgets_buffer = [] + self._widgets_buffer_check=0 + if self._widgets_buffer_check < len(self._children._buffer): + for i,(_l,_y,_i) in enumerate(self._children._buffer[self._widgets_buffer_check:]): + if not _y and _i.hasWidgets(): + self._widgets_buffer.append((_l,i,_i)) + self._widgets_buffer_check = len(self._children._buffer) + return page + return [] + + def _item_at(self, pos:int) -> Optional[Tuple[int,int,TTkTreeWidgetItem]]: + if pos < 0 or not self._children: + return None + if page := self._children.get_page(0, pos, 1): + return page[0] + else: + return None + + def size(self): + if self._children: + return self._children.size() + return 0 + class TTkTreeWidget(TTkAbstractScrollView): ''' The :py:class:`TTkTreeWidget` class is a convenience class that provides a standard tree @@ -153,12 +207,14 @@ class TTkTreeWidget(TTkAbstractScrollView): 'default': { 'color': TTkColor.RST, 'lineColor': TTkColor.fg("#444444"), + 'lineHeightColor': TTkColor.fg("#666666"), 'headerColor': TTkColor.fg("#ffffff")+TTkColor.bg("#444444")+TTkColor.BOLD, 'selectedColor': TTkColor.fg("#ffff88")+TTkColor.bg("#000066")+TTkColor.BOLD, 'separatorColor': TTkColor.fg("#444444")}, 'disabled': { 'color': TTkColor.fg("#888888"), 'lineColor': TTkColor.fg("#888888"), + 'lineHeightColor': TTkColor.fg("#666666"), 'headerColor': TTkColor.fg("#888888"), 'selectedColor': TTkColor.fg("#888888"), 'separatorColor': TTkColor.fg("#888888")}, @@ -174,16 +230,8 @@ class TTkTreeWidget(TTkAbstractScrollView): '_itemChanged', '_itemClicked', '_itemDoubleClicked', '_itemExpanded', '_itemCollapsed', '_itemActivated' ) - @dataclass(frozen=True) - class _Cache: - item: TTkTreeWidgetItem - level: int - data: list - widgets: list - firstLine: bool - - _cache:List[_Cache] _selected:List[TTkTreeWidgetItem] + _rootItem:_RootWidgetItem @dataclass(frozen=True) class _DropTreeData: @@ -214,6 +262,8 @@ class TTkTreeWidget(TTkAbstractScrollView): self._itemExpanded = pyTTkSignal(TTkTreeWidgetItem) self._itemCollapsed = pyTTkSignal(TTkTreeWidgetItem) + self._cache = [] + self._selectionMode = selectionMode self._dndMode = dragDropMode self._selected = [] @@ -221,17 +271,19 @@ class TTkTreeWidget(TTkAbstractScrollView): self._separatorSelected = None self._header = header if header else [] self._columnsPos = [] - self._cache = [] self._sortingEnabled=sortingEnabled self._sortColumn = -1 self._sortOrder = TTkK.AscendingOrder - self._rootItem = TTkTreeWidgetItem(expanded=True) + self._rootItem = _RootWidgetItem() super().__init__(**kwargs) self.setMinimumHeight(1) self.setFocusPolicy(TTkK.ClickFocus) self.clear() self.setPadding(1,0,0,0) self.viewChanged.connect(self._viewChangedHandler) + self._alignWidgets() + self.sizeChanged.connect(self._alignWidgets) + self._rootItem.dataChanged.connect(self._refreshCache) @pyTTkSlot() def _viewChangedHandler(self) -> None: @@ -241,46 +293,36 @@ class TTkTreeWidget(TTkAbstractScrollView): # Overridden function def viewFullAreaSize(self) -> tuple[int, int]: w = self._columnsPos[-1]+1 if self._columnsPos else 0 - h = self._rootItem.size() + h = self._rootItem.size()+1 # TTkLog.debug(f"{w=} {h=}") return w,h def clear(self) -> None: '''clear''' # Remove all the widgets - for ri in self._rootItem.children(): - ri.setTreeItemParent(None) if self._rootItem: self._rootItem.dataChanged.disconnect(self._refreshCache) - self._rootItem = TTkTreeWidgetItem(expanded=True) + self._rootItem = _RootWidgetItem() self._rootItem.dataChanged.connect(self._refreshCache) self.sortItems(self._sortColumn, self._sortOrder) - self._refreshCache() self.viewChanged.emit() self.update() def addTopLevelItem(self, item:TTkTreeWidgetItem) -> None: '''addTopLevelItem''' self._rootItem.addChild(item) - item.setTreeItemParent(self) - self._refreshCache() self.viewChanged.emit() self.update() def addTopLevelItems(self, items:TTkTreeWidgetItem) -> None: '''addTopLevelItems''' self._rootItem.addChildren(items) - self._rootItem.setTreeItemParent(self) - #for item in items: - # item.setTreeItemParent(self) - self._refreshCache() self.viewChanged.emit() self.update() def takeTopLevelItem(self, index) -> None: '''takeTopLevelItem''' self._rootItem.takeChild(index) - self._refreshCache() self.viewChanged.emit() self.update() @@ -341,7 +383,10 @@ class TTkTreeWidget(TTkAbstractScrollView): if not self._sortingEnabled: return self._sortColumn = col self._sortOrder = order + self._rootItem.dataChanged.disconnect(self._refreshCache) self._rootItem.sortChildren(col, order) + self._rootItem.dataChanged.connect(self._refreshCache) + self._refreshCache() def columnWidth(self, column:int) -> int: '''columnWidth''' @@ -363,9 +408,8 @@ class TTkTreeWidget(TTkAbstractScrollView): def resizeColumnToContents(self, column:int) -> None: '''resizeColumnToContents''' - if not self._cache: - return - contentSize = max(row.data[column].termWidth() for row in self._cache) + _,oy = self.getViewOffsets() + contentSize = self._rootItem._getColumnContentSize(column, oy) self.setColumnWidth(column, contentSize) @pyTTkSlot() @@ -376,7 +420,6 @@ class TTkTreeWidget(TTkAbstractScrollView): self._rootItem.dataChanged.disconnect(self._refreshCache) self._rootItem.expandAll() self._rootItem.dataChanged.connect(self._refreshCache) - self._refreshCache() @pyTTkSlot() def collapseAll(self) -> None: @@ -386,7 +429,6 @@ class TTkTreeWidget(TTkAbstractScrollView): self._rootItem.dataChanged.disconnect(self._refreshCache) self._rootItem.collapseAll() self._rootItem.dataChanged.connect(self._refreshCache) - self._refreshCache() def mouseDoubleClickEvent(self, evt:TTkMouseEvent) -> bool: x,y = evt.x, evt.y @@ -403,8 +445,9 @@ class TTkTreeWidget(TTkAbstractScrollView): return True y += oy-1 - if 0 <= y < len(self._cache): - item = self._cache[y].item + if _item_at := self._rootItem._item_at(y): + _,_,_i = _item_at + item = _i if item.childIndicatorPolicy() == TTkK.DontShowIndicatorWhenChildless and item.children() or \ item.childIndicatorPolicy() == TTkK.ShowIndicator: item.setExpanded(not item.isExpanded()) @@ -424,7 +467,6 @@ class TTkTreeWidget(TTkAbstractScrollView): break self.itemDoubleClicked.emit(item, col) self.itemActivated.emit(item, col) - self.update() return True @@ -453,13 +495,14 @@ class TTkTreeWidget(TTkAbstractScrollView): return True # Handle Tree/Table Events y += oy-1 - if 0 <= y < len(self._cache): - item = self._cache[y].item - level = self._cache[y].level + if _item_at := self._rootItem._item_at(y): + _l, _yi, _i = _item_at + item = _i + level = _l # check if the expand button is pressed with +-1 tollerance - if level*2 <= x < level*2+3 and \ - ( item.childIndicatorPolicy() == TTkK.DontShowIndicatorWhenChildless and item.children() or - item.childIndicatorPolicy() == TTkK.ShowIndicator ): + if ( _yi==0 and level*2 <= x < level*2+3 and \ + ( item.childIndicatorPolicy() == TTkK.DontShowIndicatorWhenChildless and item.children() or + item.childIndicatorPolicy() == TTkK.ShowIndicator )): item.setExpanded(not item.isExpanded()) if item.isExpanded(): self.itemExpanded.emit(item) @@ -487,7 +530,7 @@ class TTkTreeWidget(TTkAbstractScrollView): break self.itemClicked.emit(item, col) self.update() - return True + return True def mouseDragEvent(self, evt:TTkMouseEvent) -> bool: # columnPos (Selected = 2) @@ -534,78 +577,44 @@ class TTkTreeWidget(TTkAbstractScrollView): return True return False + @pyTTkSlot() def _alignWidgets(self) -> None: - for y,c in enumerate(self._cache): - if not c.firstLine: - continue - for i,w in enumerate(c.widgets): - if w: - _pos = self._columnsPos[i-1]+1 if i else 3 + c.level*2 - _width = self._columnsPos[i] - _pos - _height = w.height() - w.setGeometry(_pos,y,_width,_height) - w.show() + self.layout().clear() + + ox, oy = self.getViewOffsets() + w,h = self.size() + self._rootItem._get_page_root(0,oy+h) + + if not self._rootItem._widgets_buffer: + self.update() + return + + wids = [] + for _l,_y,_i in self._rootItem._widgets_buffer: + for _il in range(len(self._header)): + if _wid:=_i.widget(_il): + _pos = self._columnsPos[_il-1]+1 if _il else 3 + _l*2 + _width = self._columnsPos[_il] - _pos + _height = _wid.height() + _wid.setGeometry(_pos,_y,_width,_height) + _wid.show() + wids.append(_wid) + if wids: + self.layout().addWidgets(wids) @pyTTkSlot() def _refreshCache(self) -> None: - # I save a representation of the displayed tree in a cache array - # to avoid eccessve recursion over the items and - # identify quickly the nth displayed line to improve the interaction - # - # _cache is an array of TTkTreeWidget._Cache: - # [ item, level, data=[txtCol1, txtCol2, txtCol3, ... ]] - self._cache = [] - def _addToCache(_child, _level:int) -> None: - _data = [] - _widgets = [] - _h =_child.height() - for _il in range(len(self._header)): - _lines = _child.data(_il).split('\n') - if _il==0: - _data0 = [] - for _id in range(_h): - # Trying to define an icon to obtain this results on multiline field - # ▶ Label - # ┊ NewLine 1 - # │ NewLine 2 - # ╽ - if _id == 0: - _icon = " "+_child.icon(_il)+" " - elif _id == _h-1: - _icon = TTkString(" ╽ ", TTkColor.fg("#666666")) - elif _id == 1: - _icon = TTkString(" ┊ ", TTkColor.fg("#666666")) - else: - _icon = TTkString(" │ ", TTkColor.fg("#666666")) - _text = _lines[_id] if _id None: style = self.currentStyle() color= style['color'] lineColor= style['lineColor'] + lineHeightColor= style['lineHeightColor'] headerColor= style['headerColor'] selectedColor= style['selectedColor'] separatorColor= style['separatorColor'] @@ -628,16 +637,25 @@ class TTkTreeWidget(TTkAbstractScrollView): for sy in range(1,h): canvas.drawChar(pos=(sx-x,sy), char=tt[4], color=lineColor) - # Draw cache - for i, c in enumerate(self._cache): - if i-y<0: continue - item = c.item + col_slices = list(zip([0]+[_p+1 for _p in self._columnsPos], self._columnsPos)) + for _y, (_l, _yi, _i) in enumerate(self._rootItem._get_page_root(y,h)): for il in range(len(self._header)): - lx = 0 if il==0 else self._columnsPos[il-1]+1 - lx1 = self._columnsPos[il] - - text = c.data[il] - if item.isSelected(): - canvas.drawText(pos=(lx-x,i-y+1), text=text.completeColor(selectedColor), width=lx1-lx, alignment=item.textAlignment(il), color=selectedColor) - else: - canvas.drawText(pos=(lx-x,i-y+1), text=text, width=lx1-lx, alignment=item.textAlignment(il)) + _lx,_lx1 = col_slices[il] + _width = _lx1-_lx + _ih = _i.height() + _data = _i.data(il).split('\n') + [TTkString()]*_ih + if il==0: # First Column + if _yi == 0: + _icon = f"{' '*_l}"+_i.icon(il) + elif _yi == _ih-1: + _icon = TTkString(f"{' '*_l} ╽ ", lineHeightColor) + elif _yi == 1: + _icon = TTkString(f"{' '*_l} ┊ ", lineHeightColor) + else: + _icon = TTkString(f"{' '*_l} │ ", lineHeightColor) + _text=_icon+_data[_yi] + else: # Other columns + _text=_data[_yi] + if _i.isSelected(): + _text = (_text + ' '*_width).completeColor(selectedColor) + canvas.drawTTkString(text=_text,pos=(_lx-x,_y+1),width=_width) diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py index e0c4ab57..536ace12 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py +++ b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py @@ -20,20 +20,209 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from __future__ import annotations + __all__ = ['TTkTreeWidgetItem'] -try: - from typing import Self -except: - class Self(): pass +from dataclasses import dataclass +from typing import List, Tuple, Iterator, Generator, Optional, Callable, Any, ClassVar from TermTk.TTkCore.cfg import TTkCfg from TermTk.TTkCore.constant import TTkK -from TermTk.TTkCore.string import TTkString +from TermTk.TTkCore.string import TTkString, TTkStringType from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal from TermTk.TTkWidgets import TTkWidget from TermTk.TTkAbstract.abstractitemmodel import TTkAbstractItemModel +@dataclass +class _TTkTreeChildren(TTkAbstractItemModel): + __slots__ = ( + '_parent', + '_level', + '_total_size', + '_buffer','_buffer_link', + '_children', + '_childrenSizeChanged') + + _parent:TTkTreeWidgetItem + _level:int + _total_size:int + _buffer:List[Tuple[int,int,TTkTreeWidgetItem]] + _buffer_link:List[Tuple[int,int]] + _children: List[TTkTreeWidgetItem] + + def __init__(self, parent:TTkTreeWidgetItem): + self._parent = parent + self._childrenSizeChanged = pyTTkSignal(TTkTreeWidgetItem,int) + self._level = 0 + self._total_size = 0 + self._buffer = [] + self._buffer_link = [] + self._children = [] + super().__init__() + + def _childrenSizeChangedHandler(self, item:Optional[TTkTreeWidgetItem], diffSize:int) -> None: + if item: + self.clearBufferFromIndex(self._children.index(item)) + else: + self.clearBuffer() + self._total_size += diffSize + self._childrenSizeChanged.emit(self, diffSize) + + def get_link(self, index:int) -> int: + if index<0: + return 0 + if index >= len(self._buffer_link): + return len(self._buffer) + return self._buffer_link[index][0] + + def clearBuffer(self): + self._buffer = [] + self._buffer_link = [] + + def clearBufferFromIndex(self, index:int) -> None: + if index<0: + self.clearBuffer() + elif index >= len(self._buffer_link): + pass + else: + link = self._buffer_link[index][0] + self._buffer[link:] = [] + self._buffer_link[index+1:] = [] + self._buffer_link[index] = (link,0) + + def get_page(self, level:int, index:int, size:int) -> List[Tuple[int,int,TTkTreeWidgetItem]]: + # Add the item to the buffer + if self._level != level: + self.clearBuffer() + self._level = level + if not self._buffer: + self._buffer_link = [(0, 0)] + final_index = index+size + buffered_size = len(self._buffer) + while buffered_size < final_index: + last_index = len(self._buffer_link)-1 + if len(self._children) <= last_index: + break + # | last_index + # | ch_last_index = ch_h + # | | => left to fetch = (ch_h, final_index - buffered_size) + # Children * <---> * <--|xxxx| > * < > + # / + # item *<-------------|xxxx| buffer + # | final_index + # buffered_size + # + ch_buffer_index, ch_h = self._buffer_link[last_index] + child = self._children[last_index] + ch_s = child.size() + if ch_h != ch_s: + ch_index = ch_h + ch_size = final_index - ch_buffer_index - ch_h + child_page = child._get_page(level, ch_index, ch_size) + + self._buffer.extend(child_page) + ch_h += len(child_page) + self._buffer_link[last_index] = (ch_buffer_index, ch_h) + buffered_size = len(self._buffer) + if ch_h == ch_s: + self._buffer_link.append((buffered_size, 0)) + return self._buffer[index:final_index] + + @pyTTkSlot() + def emitDataChanged(self): + self.dataChanged.emit() + + def _addChild(self, parent:TTkTreeWidgetItem, child:TTkTreeWidgetItem): + self._children.append(child) + child._parent = self._parent + child._sortOrder = self._parent._sortOrder + child._sortColumn = self._parent._sortColumn + child.dataChanged.connect(self.emitDataChanged) + child._sizeChanged.connect(self._childrenSizeChangedHandler) + + def addChild(self, parent:TTkTreeWidgetItem, child:TTkTreeWidgetItem): + self._addChild(parent, child) + self._childrenSizeChangedHandler(child, child.size()) + self.sort() + self.emitDataChanged() + + def addChildren(self, parent:TTkTreeWidgetItem, children:List[TTkTreeWidgetItem]): + if children: + for child in children: + self._addChild(parent, child) + sizes = sum(_c.size() for _c in children) + self._childrenSizeChangedHandler(children[0], sizes) + self.sort() + self.emitDataChanged() + + def removeChild(self, child:TTkTreeWidgetItem) -> None: + if child in self._children: + self.takeChild(self._children.index(child)) + + def takeChild(self, index) -> Optional[TTkTreeWidgetItem]: + if not ( self._children and + 0<= index < len(self._children) ): + return None + child = self._children.pop(index) + child.dataChanged.disconnect(self.emitDataChanged) + child._sizeChanged.disconnect(self._childrenSizeChangedHandler) + self._childrenSizeChangedHandler(None, -child.size()) + self.emitDataChanged() + return child + + def takeChildren(self) -> List[TTkTreeWidgetItem]: + children = self._children + for child in children: + child.dataChanged.disconnect(self.emitDataChanged) + child._sizeChanged.disconnect(self._childrenSizeChangedHandler) + self._childrenSizeChangedHandler(None, -self._total_size) + self.emitDataChanged() + return children + + def child(self, index:int) -> Optional[TTkTreeWidgetItem]: + if 0 <= index < len(self._children): + return self._children[index] + return None + + def children(self) -> List[TTkTreeWidgetItem]: + return self._children + + def indexOfChild(self, child:TTkTreeWidgetItem) -> Optional[int]: + if child in self._children: + return self._children.index(child) + return None + + def expandAll(self) -> None: + for child in self._children: + child.setExpanded(True) + child.expandAll() + + def collapseAll(self) -> None: + for child in self._children: + child.setExpanded(False) + child.collapseAll() + + def sort(self): + if self._parent._sortColumn == -1: return + self._children = sorted( + self._children, + key = lambda _i : _i.data(self._parent._sortColumn), + reverse = self._parent._sortOrder == TTkK.DescendingOrder) + for c in self._children: + c.dataChanged.disconnect(self.emitDataChanged) + c._sizeChanged.disconnect(self._childrenSizeChangedHandler) + c.sortChildren(self._parent._sortColumn, self._parent._sortOrder) + c._sizeChanged.connect(self._childrenSizeChangedHandler) + c.dataChanged.connect(self.emitDataChanged) + self.clearBuffer() + self.emitDataChanged() + + def size(self): + if not self._total_size: + self._total_size = sum(_c.size() for _c in self._children) + return self._total_size + class TTkTreeWidgetItem(TTkAbstractItemModel): ''' The :py:class:`TTkTreeWidgetItem` class provides an item for use with the :py:class:'TTkTree' convenience class. @@ -60,32 +249,45 @@ class TTkTreeWidgetItem(TTkAbstractItemModel): ''' - __slots__ = ('_parent', '_data', '_widgets', '_height', '_alignment', '_children', '_expanded', '_selected', '_hidden', - '_childIndicatorPolicy', '_icon', '_defaultIcon', - '_sortColumn', '_sortOrder', '_hasWidgets', '_parentWidget', + __slots__ = ( + '_parent', '_data', '_widgets', '_height', '_alignment', + '_children', '_expanded', '_selected', '_hidden', + '_childIndicatorPolicy', '_icon', '_defaultIcon', + '_sortColumn', '_sortOrder', '_hasWidgets', + '_buffer', '_level', # Signals # 'refreshData' - 'heightChanged' + 'heightChanged', '_sizeChanged', ) + + _icon:List[TTkString] + _alignment:List[TTkK.Alignment] + _sortOrder:TTkK.SortOrder + _buffer:List[Tuple[int,int,TTkTreeWidgetItem]] + _children:Optional[_TTkTreeChildren] + _childIndicatorPolicy:TTkK.ChildIndicatorPolicy + def __init__(self, *args, - parent:Self=None, + parent:Optional[TTkTreeWidgetItem]=None, expanded:bool=False, selected:bool=False, hidden:bool=False, - icon:TTkString=None, + icon:TTkStringType='', childIndicatorPolicy:TTkK.ChildIndicatorPolicy =TTkK.ChildIndicatorPolicy.DontShowIndicatorWhenChildless, **kwargs) -> None: # Signals # self.refreshData = pyTTkSignal(TTkTreeWidgetItem) self.heightChanged = pyTTkSignal(int) + self._sizeChanged = pyTTkSignal(TTkTreeWidgetItem,int) + self._children = None + self._buffer = [] + self._level = 0 self._hasWidgets = False - self._children = [] - self._parentWidget = None self._height = 1 data = args[0] if len(args)>0 and type(args[0])==list else [TTkString()] # self._data = [i if issubclass(type(i), TTkString) else TTkString(i) if isinstance(i,str) else TTkString() for i in data] - self._parent = None + self._parent = parent self._childIndicatorPolicy = childIndicatorPolicy self._defaultIcon = True self._expanded = expanded @@ -98,21 +300,23 @@ class TTkTreeWidgetItem(TTkAbstractItemModel): self._data, self._widgets = self._processDataInput(data) self._alignment = [TTkK.LEFT_ALIGN]*len(self._data) - self._icon = ['']*len(self._data) + self._icon = [TTkString()]*len(self._data) self._setDefaultIcon() if icon: - self._icon[0] = icon + self._icon[0] = ' '+TTkString(icon)+TTkString(' ') self._defaultIcon = False if parent: parent.addChild(self) - def _processDataInputWidget(self, widget, index): + def _sizeChangedHandler(self, item:TTkTreeWidgetItem, diffSize:int) -> None: + if self._expanded or item==self: + self._sizeChanged.emit(self, diffSize) + + def _processDataInputWidget(self, widget:TTkWidget, index:int) -> TTkString: self._hasWidgets = True widget.hide() widget.sizeChanged.connect(self._widgetSizeChanged) self._height = max(self._height,widget.height()) - if self._parentWidget: - widget.setTreeItemParent(self._parentWidget) if hasattr(widget, 'text'): ret = widget.text() if hasattr(widget,'textChanged'): @@ -145,235 +349,218 @@ class TTkTreeWidgetItem(TTkAbstractItemModel): def _setDefaultIcon(self): if not self._defaultIcon: return - self._icon[0] = TTkCfg.theme.tree[0] - if self._childIndicatorPolicy == TTkK.DontShowIndicatorWhenChildless and self._children or \ - self._childIndicatorPolicy == TTkK.ShowIndicator: + self._icon[0] = TTkString(' '+TTkCfg.theme.tree[0]+' ') + if ( self._childIndicatorPolicy == TTkK.DontShowIndicatorWhenChildless and + self._children and self._children._children or + self._childIndicatorPolicy == TTkK.ShowIndicator ): if self._expanded: - self._icon[0] = TTkCfg.theme.tree[2] + self._icon[0] = TTkString(' '+TTkCfg.theme.tree[2]+' ') else: - self._icon[0] = TTkCfg.theme.tree[1] + self._icon[0] = TTkString(' '+TTkCfg.theme.tree[1]+' ') @pyTTkSlot(int, int) def _widgetSizeChanged(self, _, h): if h != self._height: h = max(max([len(s.split("\n")) for s in self._data]), max(w.height() for w in self._widgets if w)) if h != self._height: + diffSize = h - self._height self._height = h + self._buffer = [] self.heightChanged.emit(h) - if self._parentWidget: - self._parentWidget._refreshCache() + self._sizeChangedHandler(self,diffSize) def height(self): - return self._height - - def _clearTreeItemParent(self): - widgets = [] - if self._hasWidgets: - widgets += [w for w in self._widgets if w and w.parentWidget()] - # for widget in widgets: - # if pw := widget.parentWidget(): - # pw.rootLayout().removeWidgets([w for w in self._widgets if w]) - if self._parentWidget: - self._parentWidget.rootLayout().removeWidgets(widgets) - self._parentWidget = None - for c in self._children: - widgets += c._clearTreeItemParent() - return widgets - - def _setTreeItemParent(self, parent): - self._parentWidget = parent - widgets = [] - if self._hasWidgets: - widgets += [w for w in self._widgets if w] - # parent.layout().addWidgets(widgets) - for c in self._children: - widgets += c._setTreeItemParent(parent) - return widgets - - def setTreeItemParent(self, parent): - if parent: - widgets = self._setTreeItemParent(parent) - parent.layout().addWidgets(widgets) - else: - # pw = self._parentWidget - widgets = self._clearTreeItemParent() - # pw.rootLayout().removeWidgets(widgets) - - + return 0 if self._hidden else self._height + + def _get_page(self, level:int, index:int, size:int) -> List[Tuple[int,int,TTkTreeWidgetItem]]: + if self._hidden: + return [] + _h = self._height + _to = index+size + + if self._level != level: + self._buffer=[] + self._level = level + + if not self._buffer: + self._buffer = [(level, _y, self) for _y in range(_h)] + + # The page is among the children + if self._expanded and self._children and index >= _h : + return self._children.get_page(level+1, index-_h, size) + elif _to <= _h: # The page is in this node + return self._buffer[index:_to] + elif index < _h and self._expanded and self._children: # the page include the current item and the children + return self._buffer[index:] + self._children.get_page(level+1, 0, size+index-_h) + return self._buffer[index:] def hasWidgets(self): return self._hasWidgets - def isHidden(self): + def isHidden(self) -> bool: return self._hidden - def setHidden(self, hide): - if hide == self._hidden: return + def setHidden(self, hide:bool) -> None: + if hide == self._hidden: + return self._hidden = hide - self.dataChanged.emit() + if hide: + self._sizeChangedHandler(self,-self._height) + else: + self._sizeChangedHandler(self,self._height) + self.emitDataChanged() - def childIndicatorPolicy(self): + def childIndicatorPolicy(self) -> TTkK.ChildIndicatorPolicy: return self._childIndicatorPolicy - def setChildIndicatorPolicy(self, policy): + def setChildIndicatorPolicy(self, policy:TTkK.ChildIndicatorPolicy) -> None: self._childIndicatorPolicy = policy self._setDefaultIcon() - def _addChild(self, child): - self._children.append(child) - child._parent = self - child._sortOrder = self._sortOrder - child._sortColumn = self._sortColumn - self._setDefaultIcon() - self._sort(children=False) - if self._parentWidget: - child.setTreeItemParent(self._parentWidget) - child.dataChanged.connect(self.emitDataChanged) - def addChild(self, child): - self._addChild(child) - self.dataChanged.emit() + def addChild(self, child:TTkTreeWidgetItem) -> None: + if not self._children: + self._children = _TTkTreeChildren(self) + self._children._childrenSizeChanged.connect(self._sizeChangedHandler) + self._children.dataChanged.connect(self.emitDataChanged) + child = self._children.addChild(self, child) + self._setDefaultIcon() - def addChildren(self, children): - for child in children: - self._addChild(child) - self.dataChanged.emit() + def addChildren(self, children:List[TTkTreeWidgetItem]) -> None: + if not self._children: + self._children = _TTkTreeChildren(self) + self._children._childrenSizeChanged.connect(self._sizeChangedHandler) + self._children.dataChanged.connect(self.emitDataChanged) + children = self._children.addChildren(self, children) + self._setDefaultIcon() - def removeChild(self, child): - if child in self._children: - self.takeChild(self._children.index(child)) + def removeChild(self, child:TTkTreeWidgetItem) -> None: + if not self._children: + return + self._children.removeChild(child) + if not self._children.size(): + self._children.dataChanged.disconnect(self.emitDataChanged) + self._children._childrenSizeChanged.disconnect(self._sizeChangedHandler) + self._children = None + self._setDefaultIcon() - def takeChild(self, index): - if not (self._children and 0<= index < len(self._children)): + def takeChild(self, index:int) -> Optional[TTkTreeWidgetItem]: + if not self._children: return None - child = self._children.pop(index) - child.dataChanged.disconnect(self.emitDataChanged) - child.setTreeItemParent(None) - self.dataChanged.emit() + child = self._children.takeChild(index) + if not self._children.size(): + self._children.dataChanged.disconnect(self.emitDataChanged) + self._children._childrenSizeChanged.disconnect(self._sizeChangedHandler) + self._children = None + self._setDefaultIcon() return child - def takeChildren(self): - children = self._children - for child in children: - child.dataChanged.disconnect(self.emitDataChanged) - child.setTreeItemParent(None) - self._children = [] - self.dataChanged.emit() + def takeChildren(self) -> List[TTkTreeWidgetItem]: + if not self._children: + return [] + children = self._children.takeChildren() + self._children.dataChanged.disconnect(self.emitDataChanged) + self._children._childrenSizeChanged.disconnect(self._sizeChangedHandler) + self._children = None + self._setDefaultIcon() return children + def child(self, index:int) -> Optional[TTkTreeWidgetItem]: + if not self._children: + return None + return self._children.child(index) - def child(self, index): - if 0 <= index < len(self._children): - return self._children[index] - return None - - def children(self): - return [x for x in self._children if not x.isHidden()] + def children(self) -> List[TTkTreeWidgetItem]: + if not self._children: + return [] + return self._children.children() - def indexOfChild(self, child): - if child in self._children: - return self._children.index(child) - return None + def indexOfChild(self, child:TTkTreeWidgetItem) -> Optional[int]: + if not self._children: + return None + return self._children.indexOfChild(child) - def icon(self, col): + def icon(self, col:int) -> TTkString: if col >= len(self._icon): - return '' + return TTkString() return self._icon[col] - def setIcon(self, col, icon): + def setIcon(self, col:int, icon:TTkStringType) -> None: if col==0: self._defaultIcon = False - self._icon[col] = icon + if isinstance(icon,str): + self._icon[col] = TTkString(' '+icon+' ') + else: + self._icon[col] = ' '+icon+TTkString(' ') self.dataChanged.emit() - def textAlignment(self, col): + def textAlignment(self, col:int) -> TTkK.Alignment: if col >= len(self._alignment): return TTkK.LEFT_ALIGN return self._alignment[col] - def setTextAlignment(self, col, alignment): + def setTextAlignment(self, col:int, alignment:TTkK.Alignment) -> None: self._alignment[col] = alignment self.dataChanged.emit() - def data(self, col, role=None): + def data(self, col:int, role:Any=None) -> TTkString: if col >= len(self._data): - return '' + return TTkString() return self._data[col] - def widget(self, col, role=None): + def widget(self, col:int, role:Any=None) -> Optional[TTkWidget]: if col >= len(self._data): return None return self._widgets[col] def expandAll(self) -> None: - for child in self._children: - child.setExpanded(True) - child.expandAll() + if self._children: + self._children.expandAll() def collapseAll(self) -> None: - for child in self._children: - child.setExpanded(False) - child.collapseAll() - - def sortData(self, col): - return self.data(col) - - def _sort(self, children): - if self._sortColumn == -1: return - self._children = sorted( - self._children, - key = lambda x : x.sortData(self._sortColumn), - reverse = self._sortOrder == TTkK.DescendingOrder) - # Broadcast the sorting to the children - if children: - for c in self._children: - c.dataChanged.disconnect(self.emitDataChanged) - c.sortChildren(self._sortColumn, self._sortOrder) - c.dataChanged.connect(self.emitDataChanged) + if self._children: + self._children.collapseAll() - def sortChildren(self, col, order): + def sortChildren(self, col:int, order:TTkK.SortOrder) -> None: self._sortColumn = col self._sortOrder = order - if not self._children: return - self._sort(children=True) - self.dataChanged.emit() + if not self._children: + return + self._children.sort() @pyTTkSlot() - def emitDataChanged(self): + def emitDataChanged(self) -> None: self.dataChanged.emit() # def setDisabled(disabled): # pass - def setExpanded(self, expand): - # hide all the widgets if this item is not expanded - if not expand: - def _recurseHide(item): - for c in item._children: - if c._hasWidgets: - for widget in [w for w in c._widgets if w]: - widget.hide() - if c._expanded: - _recurseHide(c) - _recurseHide(self) + def setExpanded(self, expand:bool) -> None: + if self._expanded != expand and self._children: + if expand: + self._sizeChangedHandler(self, self._children.size()) + else: + self._sizeChangedHandler(self, -self._children.size()) self._expanded = expand self._setDefaultIcon() - self.emitDataChanged() + self.dataChanged.emit() - def setSelected(self, select): + def setSelected(self, select:bool) -> None: self._selected = select # def isDisabled(): # pass - def isExpanded(self): + def isExpanded(self) -> bool: return self._expanded - def isSelected(self): + def isSelected(self) -> bool: return self._selected - def size(self): - if self._expanded: - return self._height + sum(c.size() for c in self.children()) - else: - return self._height + def size(self) -> int: + if self._hidden: + return 0 + if ( self._expanded and + self._children ): + return self._height + self._children.size() + return self._height diff --git a/tests/pytest/run_test_001_demo.py b/tests/pytest/run_test_001_demo.py index c9c065a7..416f084d 100755 --- a/tests/pytest/run_test_001_demo.py +++ b/tests/pytest/run_test_001_demo.py @@ -28,7 +28,7 @@ import pickle import threading sys.path.append(os.path.join(sys.path[0],'../../demo')) -sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) import demo diff --git a/tests/pytest/run_test_002_textedit.py b/tests/pytest/run_test_002_textedit.py index a60eef17..1b734c9f 100755 --- a/tests/pytest/run_test_002_textedit.py +++ b/tests/pytest/run_test_002_textedit.py @@ -27,7 +27,7 @@ import queue import pickle import threading -sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) import TermTk as ttk diff --git a/tests/pytest/test_003_string.py b/tests/pytest/test_003_string.py index f784bab1..6bca19d0 100644 --- a/tests/pytest/test_003_string.py +++ b/tests/pytest/test_003_string.py @@ -23,7 +23,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 diff --git a/tests/pytest/test_005_tree.py b/tests/pytest/test_005_tree.py new file mode 100644 index 00000000..50849c2f --- /dev/null +++ b/tests/pytest/test_005_tree.py @@ -0,0 +1,472 @@ +# 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 +import pytest +from typing import Union, Optional, List, Tuple + +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) + +import TermTk as ttk + +def _gen_childs(num:int,prefix:str,nesting=2) -> List[ttk.TTkTreeWidgetItem]: + ret = [] + for i in range(num): + _c = ttk.TTkTreeWidgetItem([f"{prefix} A {i}"+'\nabc'*i, f"{prefix} B {i}", f"{prefix} C {i}"]) + ret.append(_c) + if i%2: + _c.setExpanded(True) + if nesting: + _c.addChildren(_gen_childs(num,f"{prefix}_X",nesting-1)) + return ret + +def _add_children(item:ttk.TTkTreeWidgetItem): + item.addChildren(_gen_childs(4,'l2',2)) + +def _create_tree() -> Tuple[ttk.TTkTreeWidgetItem,ttk.TTkTreeWidgetItem,ttk.TTkTreeWidgetItem]: + l0 = ttk.TTkTreeWidgetItem(["XX0","XX0","XX0"]) + l1 = ttk.TTkTreeWidgetItem(["String A", "String B", "String C"]) + l2 = ttk.TTkTreeWidgetItem(["String AA", "String BB", "String CC"]) + l3 = ttk.TTkTreeWidgetItem(["String AAA", "String BBB", "String CCC"]) + l4 = ttk.TTkTreeWidgetItem(["String AAAA", "String BBBB", "String CCCC"]) + l5 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + + l2.addChild(l5) + + l1.addChildren(_gen_childs(2,'l1',1)) + l2.addChildren(_gen_childs(4,'l2',2)) + l3.addChildren(_gen_childs(3,'l3',1)) + l4.addChildren(_gen_childs(2,'l4',1)) + l5.addChildren(_gen_childs(2,'l5',1)) + + l0.addChild(l1) + l0.addChild(l2) + l0.addChild(l3) + l0.addChild(l4) + + l0.setExpanded(True) + l2.setExpanded(True) + l5.setExpanded(True) + l4.setExpanded(True) + + return l0, l2, l5 + +def _format_item(item:ttk.TTkTreeWidgetItem) -> str: + return f"{item.data(0)} h:{item.height()} {item.data(1)} {item.data(2)}".replace('\n','_\\n_') + +def _print_tree(child:ttk.TTkTreeWidgetItem, level:int=0): + if child.isExpanded() and child.children(): + print(' '*level, ' v ', _format_item(child)) + for i,c in enumerate(child.children()): + _print_tree(c,level+1) + elif child.children(): + print(' '*level, ' > ', _format_item(child)) + else: + print(' '*level, ' - ', _format_item(child)) + +# def test_tree_item_iterate_skip(): +# tree,_,_ = _create_tree() + +# print('\nTree:') +# _print_tree(tree) + +# # for i,(a,b) in enumerate(tree.iterate()): +# # print(f"{i:03} - ", b, ' '*b, _format_item(a)) + +# # print('\nSkip 3') +# # for i,(a,b) in enumerate(tree.iterate(skip=3),3): +# # print(f"{i:03} - ", b, ' '*b, _format_item(a)) + +# # print('\nSkip 7') +# # for i,(a,b) in enumerate(tree.iterate(skip=7),7): +# # print(f"{i:03} - ", b, ' '*b, _format_item(a)) + +# full = [(a,b) for a,b in tree._iterate()] + +# assert full == [(a,b) for a,b in tree._iterate()] +# assert full == [(a,b) for a,b in tree._iterate(skip= 0)] +# assert full[ 3:] == [(a,b) for a,b in tree._iterate(skip= 3)] +# assert full[ 5:] == [(a,b) for a,b in tree._iterate(skip= 5)] +# assert full[ 6:] == [(a,b) for a,b in tree._iterate(skip= 6)] +# assert full[10:] == [(a,b) for a,b in tree._iterate(skip=10)] +# assert full[15:] == [(a,b) for a,b in tree._iterate(skip=15)] +# assert full[20:] == [(a,b) for a,b in tree._iterate(skip=20)] +# assert full[30:] == [(a,b) for a,b in tree._iterate(skip=30)] +# assert full[80:] == [(a,b) for a,b in tree._iterate(skip=80)] +# assert [] == [(a,b) for a,b in tree._iterate(skip=80)] + + + +def test_tree_item_iterate_skip_2(): + def my_gen(): + for i in range(10): + yield i + + # Save progress + gen = my_gen() + + # Resume from saved_index + for i,item in enumerate(gen): + print(i, item) + if i==5: + break + + for i,item in enumerate(gen): + print(i, item) + +# def test_tree_item_listify(): +# tree,c1,c2 = _create_tree() + +# print('\nTree:') +# _print_tree(tree) + +# for i,(a,b,c) in enumerate(tree.listify()): +# print(f"{i:03} - ", b, c, ' '*b, _format_item(a)) + +# full = [(a,b) for a,b in tree._iterate()] + +# print('expand') +# c1.setExpanded(False) +# for i,(a,b,c) in enumerate(tree.listify()): +# print(f"{i:03} - ", b, c, ' '*b, _format_item(a)) + +# print('expand') +# c1.setExpanded(True) +# for i,(a,b,c) in enumerate(tree.listify()): +# print(f"{i:03} - ", b, c, ' '*b, _format_item(a)) + +# print('expand') +# c2.setExpanded(False) +# for i,(a,b,c) in enumerate(tree.listify()): +# print(f"{i:03} - ", b, c, ' '*b, _format_item(a)) + +# print('expand') +# c2.setExpanded(True) +# for i,(a,b,c) in enumerate(tree.listify()): +# print(f"{i:03} - ", b, c, ' '*b, _format_item(a)) + +# print('expand c1 False') +# c1.setExpanded(False) +# for i,(a,b,c) in enumerate(tree.listify()): +# print(f"{i:03} - ", b, c, ' '*b, _format_item(a)) + +# print('expand c2 False') +# c2.setExpanded(False) +# for i,(a,b,c) in enumerate(tree.listify()): +# print(f"{i:03} - ", b, c, ' '*b, _format_item(a)) + +# print('expand c2 True') +# c2.setExpanded(True) +# for i,(a,b,c) in enumerate(tree.listify()): +# print(f"{i:03} - ", b, c, ' '*b, _format_item(a)) + +# print('expand c1 True') +# c1.setExpanded(True) +# for i,(a,b,c) in enumerate(tree.listify()): +# print(f"{i:03} - ", b, c, ' '*b, _format_item(a)) + +def _get_node_size(item:ttk.TTkTreeWidgetItem): + size = item.height() + if item.isExpanded(): + for _ch in item.children(): + size += _get_node_size(_ch) + return size + +def _test_size_ch(ch:ttk.TTkTreeWidgetItem): + print(_format_item(ch)) + assert _get_node_size(ch) == ch.size() , _format_item(ch) + +# Test first the children and the parent +def _loop_1(item:ttk.TTkTreeWidgetItem): + if item.isExpanded(): + for _ch in item.children(): + _loop_1(_ch) + _test_size_ch(item) + +# Test first the parent and the children +def _loop_2(item:ttk.TTkTreeWidgetItem): + _test_size_ch(item) + if item.isExpanded(): + for _ch in item.children(): + _loop_2(_ch) + +def test_tree_sizes_simple_1(): + l0 = ttk.TTkTreeWidgetItem(["l0 XX0","XX0","XX0"], expanded=True) + l1 = ttk.TTkTreeWidgetItem(["l1 String A", "String B", "String C"], expanded=True) + l2 = ttk.TTkTreeWidgetItem(["l2 String AA", "String BB", "String CC"], expanded=True) + l3 = ttk.TTkTreeWidgetItem(["l3 String AAA\nAAA\nAAA", "String BBB", "String CCC"], expanded=False) + l4 = ttk.TTkTreeWidgetItem(["l4 String AAAA", "String BBBB", "String CCCC"], expanded=True) + l5 = ttk.TTkTreeWidgetItem(["l5 String AAAAA", "String BBBBB\nB\nB", "String CCCCC"], expanded=True) + + l0.addChildren([l1,l2,l4]) + l1.addChild(l3) + l3.addChild(l5) + + _loop_1(l0) + +def test_tree_sizes_simple_2(): + l0 = ttk.TTkTreeWidgetItem(["XX0","XX0","XX0"], expanded=False) + l1 = ttk.TTkTreeWidgetItem(["String A", "String B", "String C"], expanded=False) + l2 = ttk.TTkTreeWidgetItem(["String AA", "String BB", "String CC"], expanded=False) + + l0.addChild(l1) + + l0.setExpanded(True) + l1.setExpanded(True) + l2.setExpanded(True) + + l1.addChild(l2) + + assert _get_node_size(l0) == l0.size() , _format_item(l0) + +def test_tree_sizes_simple_3(): + l0 = ttk.TTkTreeWidgetItem(["XX0","XX0","XX0"], expanded=True) + l1 = ttk.TTkTreeWidgetItem(["String A", "String B", "String C"], expanded=True) + l3 = ttk.TTkTreeWidgetItem(["String AAA\nAAA\nAAA", "String BBB", "String CCC"], expanded=False) + l5 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB\nB\nB", "String CCCCC"], expanded=True) + + l0.addChild(l1) + l1.addChild(l3) + l3.addChild(l5) + + _loop_1(l0) + +def test_tree_sizes_simple_4_expanding(): + l0 = ttk.TTkTreeWidgetItem(["XX0","XX0","XX0"], expanded=True) + l1 = ttk.TTkTreeWidgetItem(["String A", "String B", "String C"], expanded=True) + l2 = ttk.TTkTreeWidgetItem(["String AA", "String BB", "String CC"], expanded=True) + l3 = ttk.TTkTreeWidgetItem(["String AAA\nAAA\nAAA", "String BBB", "String CCC"], expanded=False) + l4 = ttk.TTkTreeWidgetItem(["String AAAA", "String BBBB", "String CCCC"], expanded=True) + l5 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB\nB\nB", "String CCCCC"], expanded=True) + + l0.addChildren([l1,l2,l4]) + l1.addChild(l3) + l3.addChild(l5) + + def _expand_test(item:ttk.TTkTreeWidgetItem): + item.setExpanded(True) + _loop_1(item) + item.setExpanded(False) + _loop_1(item) + item.setExpanded(True) + _loop_1(item) + for ch in item.children(): + ch.setExpanded(True) + _loop_1(item) + ch.setExpanded(False) + _loop_1(item) + ch.setExpanded(True) + _loop_1(item) + ch.collapseAll() + _loop_1(item) + ch.expandAll() + _loop_1(item) + item.collapseAll() + _loop_1(item) + item.expandAll() + _loop_1(item) + + _expand_test(l0) + _expand_test(l1) + _expand_test(l2) + _expand_test(l3) + _expand_test(l4) + _expand_test(l5) + +def test_tree_sizes_widgets_1(): + + btn_1 = ttk.TTkButton(text='Test B 1', border=True, height=5) + btn_2 = ttk.TTkButton(text='Test B 2', border=True, height=5) + btn_3 = ttk.TTkButton(text='Test B 3', border=True, height=5) + + l0 = ttk.TTkTreeWidgetItem(["l0 XX0","XX0","XX0"], expanded=True) + l1 = ttk.TTkTreeWidgetItem(["l1 String A", "String B", "String C"], expanded=True) + l2 = ttk.TTkTreeWidgetItem(["l2 String AA", "String BB", "String CC"], expanded=True) + l3 = ttk.TTkTreeWidgetItem([btn_1, "l3 String BBB", "String CCC"], expanded=False) + l4 = ttk.TTkTreeWidgetItem(["l4 String AAAA", "String BBBB", btn_3], expanded=True) + l5 = ttk.TTkTreeWidgetItem(["l5 String AAAAA", btn_2, "String CCCCC"], expanded=True) + + l0.addChildren([l1,l2,l4]) + l1.addChild(l3) + l3.addChild(l5) + + print('\nTree:') + _print_tree(l0) + + _loop_1(l0) + + assert l0.size() == 13 + btn_1.resize(10,10) + assert l0.size() == 18 + btn_2.resize(10,10) + assert l0.size() == 18 + btn_3.resize(10,10) + assert l0.size() == 23 + +def test_tree_sizes_complex(): + tree,c1,c2 = _create_tree() + + _loop_1(tree) + + tree,c1,c2 = _create_tree() + + _loop_2(tree) + +def test_tree_sizes_adding_nodes(): + tree,c1,c2 = _create_tree() + + _loop_1(tree) + _add_children(c1) + _loop_1(tree) + _add_children(c2) + _loop_1(tree) + + tree,c1,c2 = _create_tree() + + _loop_2(tree) + _add_children(c1) + _loop_1(tree) + _add_children(c2) + _loop_1(tree) + +def _get_full_tree_page(item:ttk.TTkTreeWidgetItem) -> List[ttk.TTkTreeWidgetItem]: + ret = [item]*item._height + if item.isExpanded(): + for ch in item.children(): + ret.extend(_get_full_tree_page(ch)) + return ret + +def test_tree_get_page(): + tree,c1,c2 = _create_tree() + + full_page = _get_full_tree_page(tree) + + print('\nTree:') + _print_tree(tree) + + def _test_page(index,size): + page = tree._get_page(0,index,size) + # print(f"Testing: {index=} {size=} , page size={len(page)}") + assert ( + [f"{c.isExpanded()} {c.data(0)}" for c in full_page[index:index+size]] == + [f"{c.isExpanded()} {c.data(0)}" for _,_,c in page] ) + + # _test_page(0,1) + # _test_page(0,2) + # _test_page(0,3) + # _test_page(0,4) + # _test_page(0,5) + # _test_page(0,5) + # _test_page(0,6) + # _test_page(0,7) + # _test_page(0,10) + # _test_page(0,100) + + for i in range(0,100,1): + for j in range(0,100,1): + _test_page(i,j) + + print("\n - 0,10") + page = tree._get_page(0,0,10) + for a,b,c in page: + print(a, b, ' '*a, _format_item(c)) + + print("\n - 2,5") + page = tree._get_page(0,2,5) + for a,b,c in page: + print(a, b, ' '*a, _format_item(c)) + +def test_tree_sort(): + tree,c1,c2 = _create_tree() + + def _test(): + full_page = _get_full_tree_page(tree) + page = tree._get_page(0,0,10000) + assert ( + [f"{c.isExpanded()} {c.data(0)}" for c in full_page] == + [f"{c.isExpanded()} {c.data(0)}" for _,_,c in page] ) + + _test() + + tree.sortChildren(0,ttk.TTkK.AscendingOrder) + _test() + + tree.sortChildren(0,ttk.TTkK.DescendingOrder) + _test() + + tree.sortChildren(1,ttk.TTkK.AscendingOrder) + _test() + + tree.sortChildren(1,ttk.TTkK.DescendingOrder) + _test() + + tree.sortChildren(2,ttk.TTkK.AscendingOrder) + _test() + + tree.sortChildren(2,ttk.TTkK.DescendingOrder) + _test() + + +def test_tree_take_child(): + tree,c1,c2 = _create_tree() + + print('\nTree:') + _print_tree(tree) + + def _test_page(): + full_page = _get_full_tree_page(tree) + page = tree._get_page(0,0,1000) + assert len(full_page) == tree.size() == len(page) + assert ( + [f"{c.isExpanded()} {c.data(0)}" for c in full_page] == + [f"{c.isExpanded()} {c.data(0)}" for _,_,c in page] ) + print(f"Testing: size={len(page)},{tree.size()}") + + _test_page() + + c2.takeChild(1) + _test_page() + + c1.takeChildren() + _test_page() + +def test_tree_show_hide(): + l0 = ttk.TTkTreeWidgetItem(["l0", "l0", "l0", "l0", "l0"],expanded=True) + l1 = ttk.TTkTreeWidgetItem(["l1", "l1", "l1", "l1", "l1"],expanded=True) + l2 = ttk.TTkTreeWidgetItem(["l2", "l2", "l2", "l2", "l2"],expanded=True) + l3 = ttk.TTkTreeWidgetItem(["l3", "l3", "l3", "l3", "l3"],expanded=True) + l4 = ttk.TTkTreeWidgetItem(["l4", "l4", "l4", "l4", "l4"],expanded=True) + l5 = ttk.TTkTreeWidgetItem(["l5", "l5", "l5", "l5", "l5"],expanded=True) + + l0.addChildren([l1,l2,l4]) + l1.addChild(l3) + l3.addChild(l5) + + print(l0.size()) + l5.setHidden(True) + print(l0.size()) + l4.setHidden(True) + print(l0.size()) + l5.setHidden(False) + print(l0.size()) + l4.setHidden(False) + print(l0.size()) diff --git a/tests/t.ui/test.ui.011.tree.02.widgets.py b/tests/t.ui/test.ui.011.tree.02.widgets.py index 518b6c09..019d66e4 100755 --- a/tests/t.ui/test.ui.011.tree.02.widgets.py +++ b/tests/t.ui/test.ui.011.tree.02.widgets.py @@ -26,7 +26,7 @@ import os import sys import argparse -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.011.tree.03.widgets.py b/tests/t.ui/test.ui.011.tree.03.widgets.py index 4fc1a55c..e66a66e4 100755 --- a/tests/t.ui/test.ui.011.tree.03.widgets.py +++ b/tests/t.ui/test.ui.011.tree.03.widgets.py @@ -26,7 +26,7 @@ import os import sys import argparse -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.011.tree.04.dnd.01.py b/tests/t.ui/test.ui.011.tree.04.dnd.01.py index 1e0ba867..c4b4af17 100755 --- a/tests/t.ui/test.ui.011.tree.04.dnd.01.py +++ b/tests/t.ui/test.ui.011.tree.04.dnd.01.py @@ -29,7 +29,7 @@ import os import sys import argparse -sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) import TermTk as ttk class DropClass(ttk.TTkFrame): diff --git a/tests/t.ui/test.ui.011.tree.05.paging.py b/tests/t.ui/test.ui.011.tree.05.paging.py new file mode 100755 index 00000000..a5a8045d --- /dev/null +++ b/tests/t.ui/test.ui.011.tree.05.paging.py @@ -0,0 +1,144 @@ +#!/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. + +# Demo inspired from: +# https://stackoverflow.com/questions/41204234/python-pyqt5-qtreewidget-sub-item + +import os +import sys +from threading import Thread + +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) +import TermTk as ttk + +ttk.TTkLog.use_default_file_logging() + + +root = ttk.TTk() + +base_btn = ttk.TTkButton(parent=root, text="Test Base", pos=(0,0), size=(20,3), border=True) +enough_btn = ttk.TTkButton(parent=root, text="Test Enough", pos=(20,0), size=(20,3), border=True) +many_btn = ttk.TTkButton(parent=root, text="Test Many", pos=(40,0), size=(20,3), border=True) +winTree = ttk.TTkWindow(parent=root,pos = (0,3), size=(80,30), title="Test Tree 1", layout=ttk.TTkGridLayout(), border=True) + +winLog = ttk.TTkWindow(parent=root,pos = (5,10), size=(100,30), title="Logs", layout=ttk.TTkGridLayout(), border=True) +ttk.TTkLogViewer(parent=winLog) + +tw = ttk.TTkTree(parent=winTree) +tw.setHeaderLabels(["Column 1", "Column 2", "Column 3"]) + +@ttk.pyTTkSlot() +def _add_base(): + tw.clear() + l1 = ttk.TTkTreeWidgetItem(["String A", "String B\nxyz\nabc\n123", "String C"]) + l2 = ttk.TTkTreeWidgetItem(["String AA", "String BB", "String CC"]) + l3 = ttk.TTkTreeWidgetItem(["String AAA", "String BBB", "String CCC\nxyz\nabc\n123"]) + l4 = ttk.TTkTreeWidgetItem(["String AAAA", "String BBBB", "String CCCC"]) + l5 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB\nxyz\nabc\n123", "String CCCCC"]) + l2.addChild(l5) + + + for i in range(3): + l1_child = ttk.TTkTreeWidgetItem(["Child A" + str(i), "Child B" + str(i), "Child C" + str(i)]) + l1.addChild(l1_child) + + for j in range(2): + l2_child = ttk.TTkTreeWidgetItem(["Child AA\nxyz\nabc\n123" + str(j), "Child BB" + str(j), "Child CC" + str(j)]) + l2.addChild(l2_child) + + for j in range(2): + l3_child = ttk.TTkTreeWidgetItem(["Child AAA" + str(j), "Child BBB" + str(j), "Child CCC" + str(j)]) + l3.addChild(l3_child) + + for j in range(2): + l4_child = ttk.TTkTreeWidgetItem(["Child AAAA" + str(j), "Child BBBB\nxyz\nabc\n123" + str(j), "Child CCCC" + str(j)]) + l4.addChild(l4_child) + + for j in range(2): + l5_child = ttk.TTkTreeWidgetItem(["Child AAAAA" + str(j), "Child BBBBB\nxyz\nabc\n123" + str(j), "Child CCCCC" + str(j)]) + l5.addChild(l5_child) + + + tw.addTopLevelItem(l1) + tw.addTopLevelItem(l2) + tw.addTopLevelItem(l3) + tw.addTopLevelItem(l4) + l1.setExpanded(True) + l3.setExpanded(True) + + +@ttk.pyTTkSlot() +def _add_many(): + _add_base() + + def _many_loop(): + ttk.TTkLog.debug('Many Loop START!!!') + num = 1 + for i in range(10): + ttk.TTkLog.debug(f"Loop: {i}") + _entries = [] + num = num<<1 + for ii in range(num): + _txt = f"Child A {ii}" + '\n'.join([f"l:{ii} - {_i}" for _i in range(ii)]) + _e = ttk.TTkTreeWidgetItem([f"({i}-{ii}) String A", "String B", "String C"]) + _e.addChildren([ + ttk.TTkTreeWidgetItem([_txt, "Child B" + str(ii), "Child C" + str(ii)]) + for i in range(3) + ]) + _entries.append(_e) + if not ii%3: + _e.setExpanded(True) + tw.addTopLevelItems(_entries) + ttk.TTkLog.debug('DONE!!!') + Thread(target=_many_loop).start() + +@ttk.pyTTkSlot() +def _add_enough(): + _add_base() + + def _many_loop(): + ttk.TTkLog.debug('Many Loop START!!!') + num = 1 + for i in range(4): + _entries = [] + num = num<<1 + for ii in range(num): + _txt = f"Child A {ii}" + '\n'.join([f"l:{ii} - {_i}" for _i in range(ii)]) + _e = ttk.TTkTreeWidgetItem([f"({i}-{ii}) String A", "String B", "String C"]) + _e.addChildren([ + ttk.TTkTreeWidgetItem([_txt, "Child B" + str(ii), "Child C" + str(ii)]) + for i in range(3) + ]) + _entries.append(_e) + if not ii%3: + _e.setExpanded(True) + tw.addTopLevelItems(_entries) + ttk.TTkLog.debug('DONE!!!') + Thread(target=_many_loop).start() + +base_btn.clicked.connect(_add_base) +enough_btn.clicked.connect(_add_enough) +many_btn.clicked.connect(_add_many) + +root.mainloop() \ No newline at end of file diff --git a/tests/timeit/02.array.10.List.creation.py b/tests/timeit/02.array.10.List.creation.py new file mode 100755 index 00000000..a77079dc --- /dev/null +++ b/tests/timeit/02.array.10.List.creation.py @@ -0,0 +1,90 @@ +#!/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. + +from __future__ import annotations + +import sys, os + +from dataclasses import dataclass +from enum import Enum,Flag,auto +import timeit + +from typing import List, Tuple, Iterator + +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) + +import TermTk as ttk + +txt = "Eugenio" + +l = [txt] * 1000000 +l1 = [txt] * 1000000 +l2 = [txt] * 1000000 +l3 = [txt] * 1000000 +l4 = [txt] * 1000000 + +def test_ti_01_append(): + _ret = [] + for i in l: + _ret.append(i) + return len(_ret) + +def test_ti_02_copy(): + _ret = [] + _ret[:] = l + return len(_ret) + +def test_ti_03_copy_2(): + _ret = [None]*len(l) + for i,ii in enumerate(l): + _ret[i] = ii + return len(_ret) + +def test_ti_03_copy_3(): + _ret = [] + for i in range(len(l) // 256): + _ret.extend(l[i:i+256]) + return len(_ret) + +def test_ti_03_copy_4(): + _ret = [] + _ret.extend(l) + return len(_ret) + +def test_ti_03_copy_5(): + _ret = l.copy() + return len(_ret) + +def test_ti_04_reduce_01(): + _ret = l4[:5000] + return len(_ret) + +loop = 100 + +a:dict = {} + +for testName in sorted([tn for tn in globals() if tn.startswith('test_ti_')]): + result = timeit.timeit(f'{testName}(*a)', globals=globals(), number=loop) + # print(f"test{iii}) fps {loop / result :.3f} - s {result / loop:.10f} - {result / loop} {globals()[testName](*a)}") + print(f"{testName} | {result / loop:.10f} sec. | {loop / result : 15.3f} Fps ╞╡-> {globals()[testName](*a)}") diff --git a/tests/timeit/33.tree.01.iterate.py b/tests/timeit/33.tree.01.iterate.py new file mode 100755 index 00000000..5f835442 --- /dev/null +++ b/tests/timeit/33.tree.01.iterate.py @@ -0,0 +1,219 @@ +#!/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. + +from __future__ import annotations + +import sys, os + +from dataclasses import dataclass +from enum import Enum,Flag,auto +import timeit + +from typing import List, Tuple, Iterator + +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) + +import TermTk as ttk + +def _create_tree() -> ttk.TTkTreeWidgetItem: + l0 = ttk.TTkTreeWidgetItem(["XX0","XX0","XX0"]) + l1 = ttk.TTkTreeWidgetItem(["String A", "String B", "String C"]) + l2 = ttk.TTkTreeWidgetItem(["String AA", "String BB", "String CC"]) + l3 = ttk.TTkTreeWidgetItem(["String AAA", "String BBB", "String CCC"]) + l4 = ttk.TTkTreeWidgetItem(["String AAAA", "String BBBB", "String CCCC"]) + l5 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l51 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l511 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l5111 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l52 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l521 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + + l2.addChild(l5) + l5.addChild(l51) + l51.addChild(l511) + l511.addChild(l5111) + l5.addChild(l52) + l52.addChild(l521) + + def _addChilds(p:ttk.TTkTreeWidgetItem,num:int,prefix:str,nesting=2): + for i in range(num): + _c = ttk.TTkTreeWidgetItem([f"{prefix} A {i}", f"{prefix} B {i}", f"{prefix} C {i}"]) + _c._height = i + p.addChild(_c) + if i%2: + _c.setExpanded(True) + if nesting: + _addChilds(_c,num,f"{prefix}_X",nesting-1) + + _addChilds(l1,10,'l1',3) + _addChilds(l2,10000,'l2',0) + _addChilds(l2,20,'l2',2) + _addChilds(l3,30,'l3',2) + _addChilds(l4,10,'l4',2) + _addChilds(l5,20,'l5',2) + _addChilds(l51,30,'l51',2) + _addChilds(l511,30,'l511',2) + _addChilds(l5111,40,'l5111',2) + _addChilds(l52,50,'l52',2) + _addChilds(l521,10,'l521',2) + + l0.addChild(l1) + l0.addChild(l2) + l0.addChild(l3) + l0.addChild(l4) + + l0.setExpanded(True) + l2.setExpanded(True) + l5.setExpanded(True) + l51.setExpanded(True) + l511.setExpanded(True) + l5111.setExpanded(True) + l52.setExpanded(True) + l521.setExpanded(True) + l4.setExpanded(True) + + return l0 + +def _format_item(item:ttk.TTkTreeWidgetItem) -> str: + return f"{item.data(0)} {item.data(1)} {item.data(2)}" + +def _full_iterate(p:ttk.TTkTreeWidgetItem, level:int=0) -> Iterator[ttk.TTkTreeWidgetItem]: + for _c in p._children: + yield _c, level + if _c._expanded: + yield from _full_iterate(_c,level+1) + +def _full_full_iterate(p:ttk.TTkTreeWidgetItem, level:int=0) -> Iterator[ttk.TTkTreeWidgetItem]: + for _c in p._children: + yield _c, level + yield from _c._iterate(level+1) + +def _get_size_iterate_1(p:ttk.TTkTreeWidgetItem) -> int: + return len(p._children) + sum(_get_size_iterate_1(_c) for _c in p._children if _c.isExpanded()) + +def _get_size_iterate_2(p:ttk.TTkTreeWidgetItem) -> int: + _ret = len(p._children) + for _c in p._children: + if _c.isExpanded(): + _ret += _get_size_iterate_2(_c) + return _ret + +def _get_size_iterate_3(p:ttk.TTkTreeWidgetItem) -> int: + # return p.height() + _ret = 0 + for _c in p._children: + _ret += _c._height + if _c.isExpanded(): + _ret += _get_size_iterate_3(_c) + return _ret + +tree = _create_tree() + +print('Total: ', len(list(_full_full_iterate(tree)))) + +def test_ti_0_00_0(): return len([(a,b) for a,b in _full_iterate(tree)]) +def test_ti_0_01_0(): return len([(a,b) for a,b in tree._iterate()]) +def test_ti_0_02_0(): return len([(a,b) for a,b in tree._iterate(skip=10)]) +def test_ti_0_03_0(): return len([(a,b) for a,b in tree._iterate(skip=100)]) +def test_ti_0_04_0(): return len([(a,b) for a,b in tree._iterate(skip=500)]) +def test_ti_0_05_0(): return len([(a,b) for a,b in tree._iterate(skip=1000)]) +def test_ti_0_06_0(): return len([(a,b) for a,b in tree._iterate(skip=10000)]) +def test_ti_0_07_0(): return len([(a,b) for a,b in tree._iterate(skip=50000)]) +def test_ti_0_08_0(): return len([(a,b) for a,b in tree._iterate(skip=100000)]) + +def test_ti_0_00_1(): return len([None for _ in _full_iterate(tree)]) +def test_ti_0_01_1(): return len([None for _ in tree._iterate()]) +def test_ti_0_02_1(): return len([None for _ in tree._iterate(skip=10)]) +def test_ti_0_03_1(): return len([None for _ in tree._iterate(skip=100)]) +def test_ti_0_04_1(): return len([None for _ in tree._iterate(skip=500)]) +def test_ti_0_05_1(): return len([None for _ in tree._iterate(skip=1000)]) +def test_ti_0_06_1(): return len([None for _ in tree._iterate(skip=10000)]) +def test_ti_0_07_1(): return len([None for _ in tree._iterate(skip=50000)]) +def test_ti_0_08_1(): return len([None for _ in tree._iterate(skip=100000)]) + +def test_ti_0_00_2(): return len(list(_full_iterate(tree))) +def test_ti_0_01_2(): return len(list(tree._iterate())) +def test_ti_0_02_2(): return len(list(tree._iterate(skip=10))) +def test_ti_0_03_2(): return len(list(tree._iterate(skip=100))) +def test_ti_0_04_2(): return len(list(tree._iterate(skip=500))) +def test_ti_0_05_2(): return len(list(tree._iterate(skip=1000))) +def test_ti_0_06_2(): return len(list(tree._iterate(skip=10000))) +def test_ti_0_07_2(): return len(list(tree._iterate(skip=50000))) +def test_ti_0_08_2(): return len(list(tree._iterate(skip=100000))) + +def test_ti_1_00(): + for a,b in _full_iterate(tree): + return _format_item(a) +def test_ti_1_01(): + for a,b in tree._iterate(): + return _format_item(a) +def test_ti_1_02(): + for a,b in tree._iterate(skip=10): + return _format_item(a) +def test_ti_1_03(): + for a,b in tree._iterate(skip=100): + return _format_item(a) +def test_ti_1_04(): + for a,b in tree._iterate(skip=500): + return _format_item(a) +def test_ti_1_05(): + for a,b in tree._iterate(skip=1000): + return _format_item(a) +def test_ti_1_06(): + for a,b in tree._iterate(skip=10000): + return _format_item(a) +def test_ti_1_07(): + for a,b in tree._iterate(skip=50000): + return _format_item(a) +def test_ti_1_08(): + for a,b in tree._iterate(skip=100000): + return _format_item(a) + +def test_ti_2_00(): + for _ in _full_iterate(tree): pass +def test_ti_2_01(): + for _ in tree._iterate(): pass +def test_ti_2_02(): + for _ in tree._iterate(skip=10): pass +def test_ti_2_03(): + for _ in tree._iterate(skip=100): pass +def test_ti_2_04(): + for _ in tree._iterate(skip=500): pass +def test_ti_2_05(): + for _ in tree._iterate(skip=1000): pass +def test_ti_2_06(): + for _ in tree._iterate(skip=10000): pass +def test_ti_2_07(): + for _ in tree._iterate(skip=50000): pass +def test_ti_2_08(): + for _ in tree._iterate(skip=100000): pass + +loop = 100 + +a:dict = {} + +for testName in sorted([tn for tn in globals() if tn.startswith('test_ti_')]): + result = timeit.timeit(f'{testName}(*a)', globals=globals(), number=loop) + # print(f"test{iii}) fps {loop / result :.3f} - s {result / loop:.10f} - {result / loop} {globals()[testName](*a)}") + print(f"{testName} | {result / loop:.10f} sec. | {loop / result : 15.3f} Fps ╞╡-> {globals()[testName](*a)}") diff --git a/tests/timeit/33.tree.02.iterate.py b/tests/timeit/33.tree.02.iterate.py new file mode 100755 index 00000000..18398770 --- /dev/null +++ b/tests/timeit/33.tree.02.iterate.py @@ -0,0 +1,159 @@ +#!/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. + +from __future__ import annotations + +import sys, os + +from dataclasses import dataclass +from enum import Enum,Flag,auto +import timeit + +from typing import List, Tuple, Iterator + +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) + +import TermTk as ttk + +def _create_tree() -> ttk.TTkTreeWidgetItem: + l0 = ttk.TTkTreeWidgetItem(["XX0","XX0","XX0"]) + l1 = ttk.TTkTreeWidgetItem(["String A", "String B", "String C"]) + l2 = ttk.TTkTreeWidgetItem(["String AA", "String BB", "String CC"]) + l3 = ttk.TTkTreeWidgetItem(["String AAA", "String BBB", "String CCC"]) + l4 = ttk.TTkTreeWidgetItem(["String AAAA", "String BBBB", "String CCCC"]) + l5 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l51 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l511 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l5111 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l52 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l521 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + + l2.addChild(l5) + l5.addChild(l51) + l51.addChild(l511) + l511.addChild(l5111) + l5.addChild(l52) + l52.addChild(l521) + + def _addChilds(p:ttk.TTkTreeWidgetItem,num:int,prefix:str,nesting=2): + for i in range(num): + _c = ttk.TTkTreeWidgetItem([f"{prefix} A {i}", f"{prefix} B {i}", f"{prefix} C {i}"]) + _c._height = i + p.addChild(_c) + if i%2: + _c.setExpanded(True) + if nesting: + _addChilds(_c,num,f"{prefix}_X",nesting-1) + + _addChilds(l1,10,'l1',3) + _addChilds(l2,10000,'l2',0) + _addChilds(l2,20,'l2',2) + _addChilds(l3,30,'l3',2) + _addChilds(l4,10,'l4',2) + _addChilds(l5,20,'l5',2) + _addChilds(l51,30,'l51',2) + _addChilds(l511,30,'l511',2) + _addChilds(l5111,40,'l5111',2) + _addChilds(l52,50,'l52',2) + _addChilds(l521,10,'l521',2) + + l0.addChild(l1) + l0.addChild(l2) + l0.addChild(l3) + l0.addChild(l4) + + l0.setExpanded(True) + l2.setExpanded(True) + l5.setExpanded(True) + l51.setExpanded(True) + l511.setExpanded(True) + l5111.setExpanded(True) + l52.setExpanded(True) + l521.setExpanded(True) + l4.setExpanded(True) + + return l0 + +def _format_item(item:ttk.TTkTreeWidgetItem) -> str: + return f"{item.data(0)} {item.data(1)} {item.data(2)}" + +def _full_iterate(p:ttk.TTkTreeWidgetItem, level:int=0) -> Iterator[ttk.TTkTreeWidgetItem]: + for _c in p._children: + yield _c, level + if _c._expanded: + yield from _c._iterate(level+1) + +def _full_full_iterate(p:ttk.TTkTreeWidgetItem, level:int=0) -> Iterator[ttk.TTkTreeWidgetItem]: + for _c in p._children: + yield _c, level + yield from _c._iterate(level+1) + +def _get_size_iterate_1(p:ttk.TTkTreeWidgetItem) -> int: + return len(p._children) + sum(_get_size_iterate_1(_c) for _c in p._children if _c.isExpanded()) + +def _get_size_iterate_2(p:ttk.TTkTreeWidgetItem) -> int: + _ret = len(p._children) + for _c in p._children: + if _c.isExpanded(): + _ret += _get_size_iterate_2(_c) + return _ret + +def _get_size_iterate_3(p:ttk.TTkTreeWidgetItem) -> int: + # return p.height() + _ret = 0 + for _c in p._children: + _ret += _c._height + if _c.isExpanded(): + _ret += _get_size_iterate_3(_c) + return _ret + +tree = _create_tree() + +print('Total: ', len(list(_full_full_iterate(tree)))) + +def test_ti_0_00_01(): return _get_size_iterate_1(tree) +def test_ti_0_00_02(): return _get_size_iterate_2(tree) +def test_ti_0_00_03(): return _get_size_iterate_3(tree) +def test_ti_0_00_04(): return _get_size_iterate_3(tree) +def test_ti_0_00_05(): return _get_size_iterate_2(tree) +def test_ti_0_00_06(): return _get_size_iterate_1(tree) +def test_ti_0_00_07(): return _get_size_iterate_1(tree) +def test_ti_0_00_08(): return _get_size_iterate_2(tree) +def test_ti_0_00_09(): return _get_size_iterate_3(tree) +def test_ti_0_00_10(): return _get_size_iterate_3(tree) +def test_ti_0_00_11(): return _get_size_iterate_2(tree) +def test_ti_0_00_12(): return _get_size_iterate_1(tree) + +def test_ti_0_01_01(): return tree.size() +def test_ti_0_01_02(): return tree.size() + + +loop = 100 + +a:dict = {} + +for testName in sorted([tn for tn in globals() if tn.startswith('test_ti_')]): + result = timeit.timeit(f'{testName}(*a)', globals=globals(), number=loop) + # print(f"test{iii}) fps {loop / result :.3f} - s {result / loop:.10f} - {result / loop} {globals()[testName](*a)}") + print(f"{testName} | {result / loop:.10f} sec. | {loop / result : 15.3f} Fps ╞╡-> {globals()[testName](*a)}") diff --git a/tests/timeit/33.tree.03.iterate.py b/tests/timeit/33.tree.03.iterate.py new file mode 100755 index 00000000..31ebc89d --- /dev/null +++ b/tests/timeit/33.tree.03.iterate.py @@ -0,0 +1,221 @@ +#!/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. + +from __future__ import annotations + +import sys, os + +from dataclasses import dataclass +from enum import Enum,Flag,auto +import timeit + +from typing import List, Tuple, Iterator + +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) + +import TermTk as ttk + +def _create_tree() -> ttk.TTkTreeWidgetItem: + l0 = ttk.TTkTreeWidgetItem(["XX0","XX0","XX0"]) + l1 = ttk.TTkTreeWidgetItem(["String A", "String B", "String C"]) + l2 = ttk.TTkTreeWidgetItem(["String AA", "String BB", "String CC"]) + l3 = ttk.TTkTreeWidgetItem(["String AAA", "String BBB", "String CCC"]) + l4 = ttk.TTkTreeWidgetItem(["String AAAA", "String BBBB", "String CCCC"]) + l5 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l51 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l511 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l5111 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l52 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l521 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + + l2.addChild(l5) + l5.addChild(l51) + l51.addChild(l511) + l511.addChild(l5111) + l5.addChild(l52) + l52.addChild(l521) + + def _addChilds(p:ttk.TTkTreeWidgetItem,num:int,prefix:str,nesting=2): + for i in range(num): + _c = ttk.TTkTreeWidgetItem([f"{prefix} A {i}", f"{prefix} B {i}", f"{prefix} C {i}"]) + _c._height = i + p.addChild(_c) + if i%2: + _c.setExpanded(True) + if nesting: + _addChilds(_c,num,f"{prefix}_X",nesting-1) + + _addChilds(l1,10,'l1',3) + _addChilds(l2,100,'l2',0) + _addChilds(l2,20,'l2',2) + _addChilds(l3,30,'l3',2) + _addChilds(l4,10,'l4',2) + _addChilds(l5,20,'l5',2) + _addChilds(l51,30,'l51',2) + _addChilds(l511,30,'l511',2) + _addChilds(l5111,40,'l5111',2) + _addChilds(l52,50,'l52',2) + _addChilds(l521,10,'l521',2) + + l0.addChild(l1) + l0.addChild(l2) + l0.addChild(l3) + l0.addChild(l4) + + l0.setExpanded(True) + l2.setExpanded(True) + l5.setExpanded(True) + l51.setExpanded(True) + l511.setExpanded(True) + l5111.setExpanded(True) + l52.setExpanded(True) + l521.setExpanded(True) + l4.setExpanded(True) + + return l0 + +def _format_item(item:ttk.TTkTreeWidgetItem) -> str: + return f"{item.data(0)} {item.data(1)} {item.data(2)}" + +def _full_iterate(p:ttk.TTkTreeWidgetItem, level:int=0) -> Iterator[ttk.TTkTreeWidgetItem]: + for _c in p._children: + for _y in range(_c._height): + yield _c, level, _y + if _c._expanded: + yield from _full_iterate(_c,level+1) + +def _full_full_iterate(p:ttk.TTkTreeWidgetItem, level:int=0) -> Iterator[ttk.TTkTreeWidgetItem]: + for _c in p._children: + yield _c, level + yield from _c._iterate(level+1) + +def _get_size_iterate_1(p:ttk.TTkTreeWidgetItem) -> int: + return len(p._children) + sum(_get_size_iterate_1(_c) for _c in p._children if _c.isExpanded()) + +def _get_size_iterate_2(p:ttk.TTkTreeWidgetItem) -> int: + _ret = len(p._children) + for _c in p._children: + if _c.isExpanded(): + _ret += _get_size_iterate_2(_c) + return _ret + +def _get_size_iterate_3(p:ttk.TTkTreeWidgetItem) -> int: + # return p.height() + _ret = 0 + for _c in p._children: + _ret += _c._height + if _c.isExpanded(): + _ret += _get_size_iterate_3(_c) + return _ret + +tree = _create_tree() + +print('Total: ', len(list(_full_full_iterate(tree)))) + +def test_ti_0_00_0(): return len([x for x in _full_iterate(tree)]) +def test_ti_0_00_1(): return len([x for x in _full_iterate(tree)]) +def test_ti_0_01_0(): return len([x for x in tree.iterate_h()]) +def test_ti_0_02_0(): return len([x for x in tree.iterate_h(skip=10)]) +def test_ti_0_03_0(): return len([x for x in tree.iterate_h(skip=100)]) +def test_ti_0_04_0(): return len([x for x in tree.iterate_h(skip=500)]) +def test_ti_0_05_0(): return len([x for x in tree.iterate_h(skip=1000)]) +def test_ti_0_06_0(): return len([x for x in tree.iterate_h(skip=10000)]) +def test_ti_0_07_0(): return len([x for x in tree.iterate_h(skip=100000)]) +def test_ti_0_08_0(): return len([x for x in tree.iterate_h(skip=1000000)]) + +def test_ti_0_00_1(): return len([None for _ in _full_iterate(tree)]) +def test_ti_0_01_1(): return len([None for _ in tree.iterate_h()]) +def test_ti_0_02_1(): return len([None for _ in tree.iterate_h(skip=10)]) +def test_ti_0_03_1(): return len([None for _ in tree.iterate_h(skip=100)]) +def test_ti_0_04_1(): return len([None for _ in tree.iterate_h(skip=500)]) +def test_ti_0_05_1(): return len([None for _ in tree.iterate_h(skip=1000)]) +def test_ti_0_06_1(): return len([None for _ in tree.iterate_h(skip=10000)]) +def test_ti_0_07_1(): return len([None for _ in tree.iterate_h(skip=100000)]) +def test_ti_0_08_1(): return len([None for _ in tree.iterate_h(skip=1000000)]) + +def test_ti_0_00_2(): return len(list(_full_iterate(tree))) +def test_ti_0_01_2(): return len(list(tree.iterate_h())) +def test_ti_0_02_2(): return len(list(tree.iterate_h(skip=10))) +def test_ti_0_03_2(): return len(list(tree.iterate_h(skip=100))) +def test_ti_0_04_2(): return len(list(tree.iterate_h(skip=500))) +def test_ti_0_05_2(): return len(list(tree.iterate_h(skip=1000))) +def test_ti_0_06_2(): return len(list(tree.iterate_h(skip=10000))) +def test_ti_0_07_2(): return len(list(tree.iterate_h(skip=100000))) +def test_ti_0_08_2(): return len(list(tree.iterate_h(skip=1000000))) + +def test_ti_1_00(): + for a,b,c in _full_iterate(tree): + return _format_item(a) +def test_ti_1_01(): + for a,b,c in tree.iterate_h(): + return _format_item(a) +def test_ti_1_02(): + for a,b,c in tree.iterate_h(skip=10): + return _format_item(a) +def test_ti_1_03(): + for a,b,c in tree.iterate_h(skip=100): + return _format_item(a) +def test_ti_1_04(): + for a,b,c in tree.iterate_h(skip=500): + return _format_item(a) +def test_ti_1_05(): + for a,b,c in tree.iterate_h(skip=1000): + return _format_item(a) +def test_ti_1_06(): + for a,b,c in tree.iterate_h(skip=10000): + return _format_item(a) +def test_ti_1_07(): + for a,b,c in tree.iterate_h(skip=100000): + return _format_item(a) +def test_ti_1_08(): + for a,b,c in tree.iterate_h(skip=1000000): + return _format_item(a) + +def test_ti_2_00(): + for _ in _full_iterate(tree): pass +def test_ti_2_01(): + for _ in tree.iterate_h(): pass +def test_ti_2_02(): + for _ in tree.iterate_h(skip=10): pass +def test_ti_2_03(): + for _ in tree.iterate_h(skip=100): pass +def test_ti_2_04(): + for _ in tree.iterate_h(skip=500): pass +def test_ti_2_05(): + for _ in tree.iterate_h(skip=1000): pass +def test_ti_2_06(): + for _ in tree.iterate_h(skip=10000): pass +def test_ti_2_07(): + for _ in tree.iterate_h(skip=100000): pass +def test_ti_2_08(): + for _ in tree.iterate_h(skip=1000000): pass + +loop = 10 + +a:dict = {} + +for testName in sorted([tn for tn in globals() if tn.startswith('test_ti_')]): + result = timeit.timeit(f'{testName}(*a)', globals=globals(), number=loop) + # print(f"test{iii}) fps {loop / result :.3f} - s {result / loop:.10f} - {result / loop} {globals()[testName](*a)}") + print(f"{testName} | {result / loop:.10f} sec. | {loop / result : 15.3f} Fps ╞╡-> {globals()[testName](*a)}") diff --git a/tests/timeit/33.tree.04.listify.py b/tests/timeit/33.tree.04.listify.py new file mode 100755 index 00000000..654e2f6f --- /dev/null +++ b/tests/timeit/33.tree.04.listify.py @@ -0,0 +1,150 @@ +#!/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. + +from __future__ import annotations + +import sys, os + +from dataclasses import dataclass +from enum import Enum,Flag,auto +import timeit + +from typing import List, Tuple, Iterator + +sys.path.append(os.path.join(sys.path[0],'../../libs/pyTermTk')) + +import TermTk as ttk + +def _create_tree() -> ttk.TTkTreeWidgetItem: + l0 = ttk.TTkTreeWidgetItem(["XX0","XX0","XX0"]) + l1 = ttk.TTkTreeWidgetItem(["String A", "String B", "String C"]) + l2 = ttk.TTkTreeWidgetItem(["String AA", "String BB", "String CC"]) + l3 = ttk.TTkTreeWidgetItem(["String AAA", "String BBB", "String CCC"]) + l4 = ttk.TTkTreeWidgetItem(["String AAAA", "String BBBB", "String CCCC"]) + l5 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l51 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l511 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l5111 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l52 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + l521 = ttk.TTkTreeWidgetItem(["String AAAAA", "String BBBBB", "String CCCCC"]) + + l2.addChild(l5) + l5.addChild(l51) + l51.addChild(l511) + l511.addChild(l5111) + l5.addChild(l52) + l52.addChild(l521) + + def _addChilds(p:ttk.TTkTreeWidgetItem,num:int,prefix:str,nesting=2): + _children = [] + for i in range(num): + _c = ttk.TTkTreeWidgetItem([f"{prefix} A {i}", f"{prefix} B {i}", f"{prefix} C {i}"]) + _c._height = i + _children.append(_c) + if i%2: + _c.setExpanded(True) + if nesting: + _addChilds(_c,num,f"{prefix}_X",nesting-1) + p.addChildren(_children) + + _addChilds(l1,10,'l1',3) + _addChilds(l2,100,'l2',0) + _addChilds(l2,20,'l2',2) + _addChilds(l3,30,'l3',2) + _addChilds(l4,10,'l4',2) + _addChilds(l5,20,'l5',2) + _addChilds(l51,30,'l51',2) + _addChilds(l511,30,'l511',2) + _addChilds(l5111,40,'l5111',2) + _addChilds(l52,50,'l52',2) + _addChilds(l521,10,'l521',2) + + l0.addChild(l1) + l0.addChild(l2) + l0.addChild(l3) + l0.addChild(l4) + + l0.setExpanded(True) + l2.setExpanded(True) + l5.setExpanded(True) + l51.setExpanded(True) + l511.setExpanded(True) + l5111.setExpanded(True) + l52.setExpanded(True) + l521.setExpanded(True) + l4.setExpanded(True) + + return l0 + +def _format_item(item:ttk.TTkTreeWidgetItem) -> str: + return f"{item.data(0)} {item.data(1)} {item.data(2)}" + +def _full_iterate(p:ttk.TTkTreeWidgetItem, level:int=0) -> Iterator[ttk.TTkTreeWidgetItem]: + for _c in p._children: + for _y in range(_c._height): + yield _c, level, _y + if _c._expanded: + yield from _full_iterate(_c,level+1) + +def _full_full_iterate(p:ttk.TTkTreeWidgetItem, level:int=0) -> Iterator[ttk.TTkTreeWidgetItem]: + for _c in p._children: + yield _c, level + yield from _c._iterate(level+1) + +def _get_size_iterate_1(p:ttk.TTkTreeWidgetItem) -> int: + return len(p._children) + sum(_get_size_iterate_1(_c) for _c in p._children if _c.isExpanded()) + +def _get_size_iterate_2(p:ttk.TTkTreeWidgetItem) -> int: + _ret = len(p._children) + for _c in p._children: + if _c.isExpanded(): + _ret += _get_size_iterate_2(_c) + return _ret + +def _get_size_iterate_3(p:ttk.TTkTreeWidgetItem) -> int: + # return p.height() + _ret = 0 + for _c in p._children: + _ret += _c._height + if _c.isExpanded(): + _ret += _get_size_iterate_3(_c) + return _ret + +tree = _create_tree() + +print('Total: ', len(list(_full_full_iterate(tree)))) + +def test_ti_0_00_0(): return len([x for x in _full_iterate(tree)]) +def test_ti_0_00_1(): return len([x for x in _full_iterate(tree)]) +def test_ti_0_01_0(): return len(tree.listify()) +def test_ti_0_02_0(): return tree.size() + +loop = 100 + +a:dict = {} + +for testName in sorted([tn for tn in globals() if tn.startswith('test_ti_')]): + result = timeit.timeit(f'{testName}(*a)', globals=globals(), number=loop) + # print(f"test{iii}) fps {loop / result :.3f} - s {result / loop:.10f} - {result / loop} {globals()[testName](*a)}") + print(f"{testName} | {result / loop:.10f} sec. | {loop / result : 15.3f} Fps ╞╡-> {globals()[testName](*a)}")