diff --git a/libs/pyTermTk/TermTk/TTkCore/constant.py b/libs/pyTermTk/TermTk/TTkCore/constant.py index e1f9b2ed..f9ab4732 100644 --- a/libs/pyTermTk/TermTk/TTkCore/constant.py +++ b/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 diff --git a/libs/pyTermTk/TermTk/TTkGui/drag.py b/libs/pyTermTk/TermTk/TTkGui/drag.py index 94160494..8dcec576 100644 --- a/libs/pyTermTk/TermTk/TTkGui/drag.py +++ b/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`. diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidget.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidget.py index f353718d..b49bbd15 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidget.py +++ b/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) diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py index 43469593..70b67f67 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py +++ b/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): diff --git a/tests/pytest/modelView/test_treewidget.py b/tests/pytest/modelView/test_treewidget.py new file mode 100644 index 00000000..fd2243e6 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/tests/t.ui/test.ui.029.image.tool.01.py b/tests/t.ui/test.ui.029.image.tool.01.py index 9a885e5c..37c39b02 100755 --- a/tests/t.ui/test.ui.029.image.tool.01.py +++ b/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)