Browse Source

fix(TTkTree): crash on empty tree (#490)

split-mainloop-in-init-and-run
Pier CeccoPierangioliEugenio 5 months ago committed by GitHub
parent
commit
8007710116
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      libs/pyTermTk/TermTk/TTkCore/constant.py
  2. 4
      libs/pyTermTk/TermTk/TTkGui/drag.py
  3. 20
      libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidget.py
  4. 6
      libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py
  5. 165
      tests/pytest/modelView/test_treewidget.py
  6. 2
      tests/t.ui/test.ui.029.image.tool.01.py

8
libs/pyTermTk/TermTk/TTkCore/constant.py

@ -20,6 +20,8 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations
__all__ = ['TTkConstant', 'TTkK']
from enum import IntEnum, Flag
@ -274,6 +276,12 @@ class TTkConstant:
DescendingOrder = 0x01
'''The items are sorted descending e.g. starts with 'ZZZ' ends with 'AAA' in Latin-1 locales'''
def invert(order:TTkConstant.SortOrder) -> TTkConstant.SortOrder:
if order == TTkConstant.SortOrder.AscendingOrder:
return TTkConstant.SortOrder.AscendingOrder
else:
return TTkConstant.SortOrder.DescendingOrder
AscendingOrder = SortOrder.AscendingOrder
DescendingOrder = SortOrder.DescendingOrder

4
libs/pyTermTk/TermTk/TTkGui/drag.py

@ -22,7 +22,7 @@
__all__ = ['TTkDrag', 'TTkDnDEvent', 'TTkDnD']
from typing import Any
from typing import Any, Union
from TermTk.TTkCore.helper import TTkHelper
from TermTk.TTkCore.canvas import TTkCanvas
@ -149,7 +149,7 @@ class TTkDrag(TTkDnD):
super().__init__(**kwargs)
# def setPixmap(self, pixmap:TTkWidget|TTkCanvas) -> None:
def setPixmap(self, pixmap:TTkWidget) -> None:
def setPixmap(self, pixmap:Union[TTkWidget,TTkCanvas]) -> None:
'''
Sets the pixmap used to represent the data in a drag and drop operation.
If a :py:class:`TTkWidget` is provided as pixmap, its default rendering will be used in the pixmap :py:class:`TTkCanvas`.

20
libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidget.py

@ -43,7 +43,7 @@ from dataclasses import dataclass
class _RootWidgetItem(TTkTreeWidgetItem):
__slots__ = ('_widgets_buffer','_widgets_buffer_check')
_widgets_buffer:List[int]
_widgets_buffer:List[tuple[int, int, TTkTreeWidgetItem]]
_widgets_buffer_check:int
def __init__(self):
@ -57,6 +57,8 @@ class _RootWidgetItem(TTkTreeWidgetItem):
if offset < 0x200:
offset = 0x200
limited_page = self._get_page_root(offset-0x200,0x400)
if not limited_page:
return 0
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:
@ -220,7 +222,7 @@ class TTkTreeWidget(TTkAbstractScrollView):
'separatorColor': TTkColor.fg("#888888")},
}
__slots__ = ( '_rootItem', '_cache',
__slots__ = ( '_rootItem',
'_header', '_columnsPos',
'_selectionMode',
'_selectedId', '_selected', '_separatorSelected',
@ -232,11 +234,12 @@ class TTkTreeWidget(TTkAbstractScrollView):
_selected:List[TTkTreeWidgetItem]
_rootItem:_RootWidgetItem
_separatorSelected:Optional[int]
@dataclass(frozen=True)
class _DropTreeData:
widget: TTkAbstractScrollView
items: List[TTkAbstractItemModel]
items: List[TTkTreeWidgetItem]
def __init__(self, *,
header:List[TTkString]=[],
@ -261,21 +264,17 @@ class TTkTreeWidget(TTkAbstractScrollView):
self._itemDoubleClicked = pyTTkSignal(TTkTreeWidgetItem, int)
self._itemExpanded = pyTTkSignal(TTkTreeWidgetItem)
self._itemCollapsed = pyTTkSignal(TTkTreeWidgetItem)
self._cache = []
self._selectionMode = selectionMode
self._dndMode = dragDropMode
self._selected = []
self._selectedId = None
self._separatorSelected = None
self._header = header if header else []
self._columnsPos = []
self._sortingEnabled=sortingEnabled
self._sortColumn = -1
self._sortOrder = TTkK.AscendingOrder
self._rootItem = _RootWidgetItem()
super().__init__(**kwargs)
self.setHeaderLabels(header)
self.setMinimumHeight(1)
self.setFocusPolicy(TTkK.ClickFocus)
self.clear()
@ -403,7 +402,7 @@ class TTkTreeWidget(TTkAbstractScrollView):
'''
if self._selected:
return self._selected
return None
return []
def setHeaderLabels(self, labels:List[TTkString]) -> None:
'''
@ -601,7 +600,7 @@ class TTkTreeWidget(TTkAbstractScrollView):
break
elif x < c:
# I-th header selected
order = not self._sortOrder if self._sortColumn == i else TTkK.AscendingOrder
order = TTkK.SortOrder.invert(self._sortOrder) if self._sortColumn == i else TTkK.AscendingOrder
self.sortItems(i, order)
break
return True
@ -643,6 +642,7 @@ class TTkTreeWidget(TTkAbstractScrollView):
self.itemClicked.emit(item, col)
self.update()
return True
return True
def mouseDragEvent(self, evt:TTkMouseEvent) -> bool:
# columnPos (Selected = 2)

6
libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py

@ -188,10 +188,10 @@ class _TTkTreeChildren(TTkAbstractItemModel):
def children(self) -> List[TTkTreeWidgetItem]:
return self._children
def indexOfChild(self, child:TTkTreeWidgetItem) -> Optional[int]:
def indexOfChild(self, child:TTkTreeWidgetItem) -> int:
if child in self._children:
return self._children.index(child)
return None
return -1
def expandAll(self) -> None:
for child in self._children:
@ -482,8 +482,6 @@ class TTkTreeWidgetItem(TTkAbstractItemModel):
return self._children.indexOfChild(child)
except ValueError:
return -1
finally:
return -1
def icon(self, col:int) -> TTkString:
if col >= len(self._icon):

165
tests/pytest/modelView/test_treewidget.py

@ -0,0 +1,165 @@
import pytest
from unittest.mock import MagicMock, patch
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.string import TTkString
from TermTk.TTkWidgets.TTkModelView.treewidget import TTkTreeWidget
from TermTk.TTkWidgets.TTkModelView.treewidgetitem import TTkTreeWidgetItem
class TestTTkTreeWidgetEmpty:
"""Test TTkTreeWidget with empty tree to catch crashes"""
def test_empty_tree_creation(self):
"""Test creating an empty tree widget"""
tree = TTkTreeWidget()
assert tree is not None
assert tree._rootItem is not None
def test_empty_tree_view_area_size(self):
"""Test viewFullAreaSize with empty tree"""
tree = TTkTreeWidget()
w, h = tree.viewFullAreaSize()
assert w == 0
assert h == 1 # Header row
def test_empty_tree_clear(self):
"""Test clear on empty tree"""
tree = TTkTreeWidget()
tree.clear() # Should not crash
assert tree._rootItem.size() == 0
def test_empty_tree_paint_event(self):
"""Test paintEvent with empty tree"""
tree = TTkTreeWidget()
canvas = MagicMock()
tree.paintEvent(canvas) # Should not crash
def test_empty_tree_selected_items(self):
"""Test selectedItems with empty tree"""
tree = TTkTreeWidget()
selected = tree.selectedItems()
assert selected == []
def test_empty_tree_mouse_press(self):
"""Test mouse press on empty tree"""
tree = TTkTreeWidget()
evt = MagicMock()
evt.x = 5
evt.y = 5
evt.mod = 0
tree.mousePressEvent(evt) # Should not crash
def test_empty_tree_resize_column(self):
"""Test resizeColumnToContents with empty tree"""
tree = TTkTreeWidget(header=[TTkString("Col1")])
tree.resizeColumnToContents(0) # Should not crash
class TestTTkTreeWidgetBasic:
"""Test basic TTkTreeWidget functionality"""
def test_tree_with_header(self):
"""Test tree creation with headers"""
tree = TTkTreeWidget(header=[TTkString("A"), TTkString("B")])
assert len(tree._header) == 2
assert len(tree._columnsPos) == 2
def test_add_top_level_item(self):
"""Test adding a top-level item"""
tree = TTkTreeWidget()
item = TTkTreeWidgetItem(["Item 1"])
tree.addTopLevelItem(item)
assert tree._rootItem.size() == 1
assert tree.topLevelItem(0) == item
def test_add_multiple_top_level_items(self):
"""Test adding multiple top-level items"""
tree = TTkTreeWidget()
items = [TTkTreeWidgetItem([f"Item {i}"]) for i in range(3)]
tree.addTopLevelItems(items)
assert tree._rootItem.size() == 3
def test_take_top_level_item(self):
"""Test removing top-level item"""
tree = TTkTreeWidget()
item = TTkTreeWidgetItem(["Item 1"])
tree.addTopLevelItem(item)
taken = tree.takeTopLevelItem(0)
assert taken == item
assert tree._rootItem.size() == 0
def test_index_of_top_level_item(self):
"""Test finding index of top-level item"""
tree = TTkTreeWidget()
items = [TTkTreeWidgetItem([f"Item {i}"]) for i in range(3)]
tree.addTopLevelItems(items)
assert tree.indexOfTopLevelItem(items[1]) == 1
def test_invisible_root_item(self):
"""Test getting invisible root item"""
tree = TTkTreeWidget()
root = tree.invisibleRootItem()
assert root == tree._rootItem
class TestTTkTreeWidgetSelection:
"""Test selection modes"""
def test_single_selection_mode(self):
"""Test single selection mode"""
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.SingleSelection)
assert tree.selectionMode() == TTkK.SelectionMode.SingleSelection
def test_multi_selection_mode(self):
"""Test multi selection mode"""
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection)
assert tree.selectionMode() == TTkK.SelectionMode.MultiSelection
def test_set_selection_mode(self):
"""Test changing selection mode"""
tree = TTkTreeWidget()
tree.setSelectionMode(TTkK.SelectionMode.MultiSelection)
assert tree.selectionMode() == TTkK.SelectionMode.MultiSelection
class TestTTkTreeWidgetSorting:
"""Test sorting functionality"""
def test_sorting_enabled_default(self):
"""Test sorting is enabled by default"""
tree = TTkTreeWidget()
assert tree.isSortingEnabled() is True
def test_set_sorting_enabled(self):
"""Test enabling/disabling sorting"""
tree = TTkTreeWidget(sortingEnabled=False)
assert tree.isSortingEnabled() is False
tree.setSortingEnabled(True)
assert tree.isSortingEnabled() is True
def test_sort_column(self):
"""Test sort column getter"""
tree = TTkTreeWidget()
assert tree.sortColumn() == -1
def test_sort_items_when_disabled(self):
"""Test sorting when disabled does nothing"""
tree = TTkTreeWidget(sortingEnabled=False)
tree.sortItems(0, TTkK.AscendingOrder)
assert tree._sortColumn == -1
class TestTTkTreeWidgetColumns:
"""Test column operations"""
def test_column_width(self):
"""Test getting column width"""
tree = TTkTreeWidget(header=[TTkString("A"), TTkString("B")])
width = tree.columnWidth(0)
assert width == 20 # Default width
def test_set_column_width(self):
"""Test setting column width"""
tree = TTkTreeWidget(header=[TTkString("A"), TTkString("B")])
tree.setColumnWidth(0, 30)
assert tree.columnWidth(0) == 30 + 1 # separator

2
tests/t.ui/test.ui.029.image.tool.01.py

@ -39,7 +39,7 @@ splitter = ttk.TTkSplitter(parent=root)
splitter.addWidget(fileTree:=ttk.TTkFileTree(path='tmp'), 15)
splitter.addWidget(mainSplitter := ttk.TTkSplitter(orientation=ttk.TTkK.VERTICAL))
mainSplitter.addWidget(sa := ttk.TTkScrollArea())
mainSplitter.addWidget(controlsWidget := ttk.TTkWidget(layout=ttk.TTkGridLayout()),6)
mainSplitter.addWidget(controlsWidget := ttk.TTkContainer(layout=ttk.TTkGridLayout()),6)
mainSplitter.addWidget(te := ttk.TTkTextEdit(lineNumber=True))
mainSplitter.addWidget(ttk.TTkLogViewer(),6)

Loading…
Cancel
Save