Skip to content

Commit bb9475a

Browse files
chore(tree): add selection api (#611)
1 parent 0006afd commit bb9475a

10 files changed

Lines changed: 726 additions & 26 deletions

File tree

.github/workflows/testing.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,30 +61,30 @@ jobs:
6161
shell: bash
6262
run: |
6363
python -m pip install --upgrade pip
64-
python -m pip install -e '.[test]'
64+
python -m pip install uv
65+
uv venv
66+
uv pip install -e '.[test]'
6567
6668
- name: Lint with flake8
6769
run: |
6870
# stop the build if there are Python syntax errors or undefined names
69-
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude .venv,build,tmp
71+
uv run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude .venv,build,tmp
7072
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
7173
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
7274
7375
- name: pytest
7476
run: |
7577
DDDD=$(pwd)
76-
pytest ${DDDD}/tests/pytest
77-
pytest ${DDDD}/apps
78+
uv run pytest ${DDDD}/tests/pytest
79+
uv run pytest ${DDDD}/apps
7880
7981
- name: Dumb Smoke Test
8082
run: |
8183
# To fix a folder permissin issue let's try to run the test from /tmp
8284
DDDD=$(pwd)
83-
mkdir -p /tmp/Eugenio
84-
cd /tmp/Eugenio
8585
# Download the input test
8686
mkdir -p tmp
8787
wget -O tmp/test.input.001.bin https://github.com/ceccopierangiolieugenio/binaryRepo/raw/master/pyTermTk/tests/test.input.001.bin
8888
wget -O tmp/test.input.002.bin https://github.com/ceccopierangiolieugenio/binaryRepo/raw/master/pyTermTk/tests/test.input.002.bin
8989
wget -O tmp/test.input.003.bin https://github.com/ceccopierangiolieugenio/binaryRepo/raw/master/pyTermTk/tests/test.input.003.bin
90-
pytest ${DDDD}/tests/pytest/run_*
90+
uv run pytest ${DDDD}/tests/pytest/run_*

libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/filetree.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,55 @@ def setDragDropMode(self, dndMode:TTkK.DragDropMode):
274274
setDragDropMode
275275
'''
276276
return self._fileTreeWidget.setDragDropMode(dndMode=dndMode)
277+
def setSelectionMode(self, mode:TTkK.SelectionMode) -> None:
278+
'''
279+
.. seealso:: this method is forwarded to :py:meth:`TTkFileTreeWidget.setSelectionMode`
280+
281+
Sets the current selection model to the given selectionModel.
282+
283+
:param mode: the selection mode used in this tree
284+
:type mode: :py:class:`TTkK.SelectionMode`
285+
'''
286+
return self._fileTreeWidget.setSelectionMode(mode=mode)
287+
def clearSelection(self) -> None:
288+
'''
289+
.. seealso:: this method is forwarded to :py:meth:`TTkFileTreeWidget.clearSelection`
290+
291+
Deselects all selected items.
292+
'''
293+
return self._fileTreeWidget.clearSelection()
294+
def setCurrentItem(self, item:Optional[TTkTreeWidgetItem]) -> None:
295+
'''
296+
.. seealso:: this method is forwarded to :py:meth:`TTkFileTreeWidget.setCurrentItem`
297+
298+
Selects the specified item as the current one.
299+
300+
:param item: the item to be selected, None clears the selection
301+
:type item: :py:class:`TTkTreeWidgetItem` or None
302+
'''
303+
return self._fileTreeWidget.setCurrentItem(item=item)
304+
def selectItem(self, item:TTkTreeWidgetItem) -> None:
305+
'''
306+
.. seealso:: this method is forwarded to :py:meth:`TTkFileTreeWidget.selectItem`
307+
308+
Adds the specified item to the current selection.
309+
310+
In single selection mode this replaces the previous selection.
311+
312+
:param item: the item to be selected
313+
:type item: :py:class:`TTkTreeWidgetItem`
314+
'''
315+
return self._fileTreeWidget.selectItem(item=item)
316+
def deselectItem(self, item:TTkTreeWidgetItem) -> None:
317+
'''
318+
.. seealso:: this method is forwarded to :py:meth:`TTkFileTreeWidget.deselectItem`
319+
320+
Removes the specified item from the current selection.
321+
322+
:param item: the item to be deselected
323+
:type item: :py:class:`TTkTreeWidgetItem`
324+
'''
325+
return self._fileTreeWidget.deselectItem(item=item)
277326
@pyTTkSlot()
278327
def expandAll(self) -> None:
279328
'''

libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tree.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class TTkTree(TTkAbstractScrollArea):
4747
'setColumnWidth', 'resizeColumnToContents',
4848
'sortColumn', 'sortItems',
4949
'dragDropMode', 'setDragDropMode',
50+
'setSelectionMode',
51+
'clearSelection', 'setCurrentItem', 'selectItem', 'deselectItem',
5052
'expandAll', 'collapseAll',
5153
'invisibleRootItem', 'itemAt',
5254
# 'appendItem', 'setAlignment', 'setColumnColors', 'setColumnSize', 'setHeader',
@@ -217,6 +219,55 @@ def setDragDropMode(self, dndMode:TTkK.DragDropMode):
217219
setDragDropMode
218220
'''
219221
return self._treeView.setDragDropMode(dndMode=dndMode)
222+
def setSelectionMode(self, mode:TTkK.SelectionMode) -> None:
223+
'''
224+
.. seealso:: this method is forwarded to :py:meth:`TTkTreeWidget.setSelectionMode`
225+
226+
Sets the current selection model to the given selectionModel.
227+
228+
:param mode: the selection mode used in this tree
229+
:type mode: :py:class:`TTkK.SelectionMode`
230+
'''
231+
return self._treeView.setSelectionMode(mode=mode)
232+
def clearSelection(self) -> None:
233+
'''
234+
.. seealso:: this method is forwarded to :py:meth:`TTkTreeWidget.clearSelection`
235+
236+
Deselects all selected items.
237+
'''
238+
return self._treeView.clearSelection()
239+
def setCurrentItem(self, item:Optional[TTkTreeWidgetItem]) -> None:
240+
'''
241+
.. seealso:: this method is forwarded to :py:meth:`TTkTreeWidget.setCurrentItem`
242+
243+
Selects the specified item as the current one.
244+
245+
:param item: the item to be selected, None clears the selection
246+
:type item: :py:class:`TTkTreeWidgetItem` or None
247+
'''
248+
return self._treeView.setCurrentItem(item=item)
249+
def selectItem(self, item:TTkTreeWidgetItem) -> None:
250+
'''
251+
.. seealso:: this method is forwarded to :py:meth:`TTkTreeWidget.selectItem`
252+
253+
Adds the specified item to the current selection.
254+
255+
In single selection mode this replaces the previous selection.
256+
257+
:param item: the item to be selected
258+
:type item: :py:class:`TTkTreeWidgetItem`
259+
'''
260+
return self._treeView.selectItem(item=item)
261+
def deselectItem(self, item:TTkTreeWidgetItem) -> None:
262+
'''
263+
.. seealso:: this method is forwarded to :py:meth:`TTkTreeWidget.deselectItem`
264+
265+
Removes the specified item from the current selection.
266+
267+
:param item: the item to be deselected
268+
:type item: :py:class:`TTkTreeWidgetItem`
269+
'''
270+
return self._treeView.deselectItem(item=item)
220271
@pyTTkSlot()
221272
def expandAll(self) -> None:
222273
'''

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

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ def _getColumnContentSize(self, column:int, offset:int) -> int:
6060
if not limited_page:
6161
return 0
6262
if column==0:
63-
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)
63+
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), default=1)
6464
else:
65-
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)
65+
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), default=1)
6666
return size-1
6767

6868
def _get_page_root(self, index:int, size:int) -> List[Tuple[int,int,TTkTreeWidgetItem]]:
@@ -246,7 +246,7 @@ class _DropTreeData:
246246
items: List[TTkTreeWidgetItem]
247247

248248
def __init__(self, *,
249-
header:List[TTkString]=[],
249+
header:Optional[List[TTkString]]=None,
250250
sortingEnabled:bool=True,
251251
selectionMode:TTkK.SelectionMode=TTkK.SelectionMode.SingleSelection,
252252
dragDropMode:TTkK.DragDropMode=TTkK.DragDropMode.NoDragDrop,
@@ -278,7 +278,7 @@ def __init__(self, *,
278278
self._sortOrder = TTkK.AscendingOrder
279279
self._rootItem = _RootWidgetItem()
280280
super().__init__(**kwargs)
281-
self.setHeaderLabels(header)
281+
self.setHeaderLabels(header if header is not None else [])
282282
self.setMinimumHeight(1)
283283
self.setFocusPolicy(TTkK.ClickFocus)
284284
self.clear()
@@ -321,6 +321,7 @@ def clear(self) -> None:
321321
if self._rootItem:
322322
self._rootItem.dataChanged.disconnect(self._refreshCache)
323323
self._rootItem = _RootWidgetItem()
324+
self._selected = []
324325
self._rootItem.dataChanged.connect(self._refreshCache)
325326
self.sortItems(self._sortColumn, self._sortOrder)
326327
self.viewChanged.emit()
@@ -348,6 +349,24 @@ def addTopLevelItems(self, items:List[TTkTreeWidgetItem]) -> None:
348349
self.viewChanged.emit()
349350
self.update()
350351

352+
def _itemInTree(self, item:TTkTreeWidgetItem) -> bool:
353+
if not item:
354+
return False
355+
if item is self._rootItem:
356+
return True
357+
# Traverse up from the item to the root using parent chain
358+
current = item
359+
while current is not None:
360+
if current is self._rootItem:
361+
return True
362+
current = current._parent
363+
return False
364+
365+
def _pruneSelection(self) -> None:
366+
if not self._selected:
367+
return
368+
self._selected = [_i for _i in self._selected if self._itemInTree(_i)]
369+
351370
def takeTopLevelItem(self, index:int) -> Optional[TTkTreeWidgetItem]:
352371
'''
353372
Removes the top-level item at the given index in the tree and returns it, otherwise returns None;
@@ -358,6 +377,7 @@ def takeTopLevelItem(self, index:int) -> Optional[TTkTreeWidgetItem]:
358377
:rtype: Optional[:py:class:`TTkTreeWidgetItem`]
359378
'''
360379
ret = self._rootItem.takeChild(index)
380+
self._pruneSelection()
361381
self.viewChanged.emit()
362382
self.update()
363383
return ret
@@ -396,18 +416,87 @@ def setSelectionMode(self, mode:TTkK.SelectionMode) -> None:
396416
:param mode: the selection mode used in this tree
397417
:type mode: :py:class:`TTkK.SelectionMode`
398418
'''
419+
self._pruneSelection()
399420
self._selectionMode = mode
421+
if mode == TTkK.SelectionMode.NoSelection:
422+
self.clearSelection()
423+
elif mode == TTkK.SelectionMode.SingleSelection and len(self._selected) > 1:
424+
self._selected = self._selected[:1]
425+
self.update()
400426

401427
def selectedItems(self) -> List[TTkTreeWidgetItem]:
402428
'''
403429
Returns a list of all selected non-hidden items.
404430
405431
:rtype: List[:py:class:`TTkTreeWidgetItem`]
406432
'''
433+
self._pruneSelection()
407434
if self._selected:
408435
return self._selected
409436
return []
410437

438+
def clearSelection(self) -> None:
439+
'''
440+
Deselects all selected items.
441+
'''
442+
if not self._selected:
443+
return
444+
self._selected = []
445+
self.update()
446+
447+
def setCurrentItem(self, item:Optional[TTkTreeWidgetItem]) -> None:
448+
'''
449+
Selects the specified item as the current one.
450+
451+
:param item: the item to be selected, None clears the selection
452+
:type item: :py:class:`TTkTreeWidgetItem` or None
453+
'''
454+
if item is None:
455+
self.clearSelection()
456+
return
457+
if not self._itemInTree(item):
458+
return
459+
if self._selectionMode == TTkK.SelectionMode.NoSelection:
460+
return
461+
self._selected = [item]
462+
self.update()
463+
464+
def selectItem(self, item:TTkTreeWidgetItem) -> None:
465+
'''
466+
Adds the specified item to the current selection.
467+
468+
In single selection mode this replaces the previous selection.
469+
470+
:param item: the item to be selected
471+
:type item: :py:class:`TTkTreeWidgetItem`
472+
'''
473+
if not self._itemInTree(item):
474+
return
475+
if self._selectionMode == TTkK.SelectionMode.NoSelection:
476+
return
477+
if self._selectionMode == TTkK.SelectionMode.SingleSelection:
478+
self._selected = [item]
479+
self.update()
480+
return
481+
if item not in self._selected:
482+
self._selected.append(item)
483+
self.update()
484+
485+
def deselectItem(self, item:TTkTreeWidgetItem) -> None:
486+
'''
487+
Removes the specified item from the current selection.
488+
489+
:param item: the item to be deselected
490+
:type item: :py:class:`TTkTreeWidgetItem`
491+
'''
492+
if not self._selected:
493+
return
494+
if item in self._selected:
495+
self._selected.remove(item)
496+
self.update()
497+
return
498+
self._pruneSelection()
499+
411500
def setHeaderLabels(self, labels:List[TTkString]) -> None:
412501
'''
413502
Adds a column in the header for each item in the labels list, and sets the label for each column.
@@ -570,7 +659,7 @@ def mouseDoubleClickEvent(self, evt:TTkMouseEvent) -> bool:
570659
self.itemExpanded.emit(item)
571660
else:
572661
self.itemCollapsed.emit(item)
573-
self._selected = [item]
662+
self.setCurrentItem(item)
574663
col = -1
575664
for i, c in enumerate(self._columnsPos):
576665
if x < c:

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ def emitDataChanged(self):
135135

136136
def _addChild(self, parent:TTkTreeWidgetItem, child:TTkTreeWidgetItem):
137137
self._children.append(child)
138-
child._parent = self._parent
139-
child._sortOrder = self._parent._sortOrder
140-
child._sortColumn = self._parent._sortColumn
138+
child._parent = parent
139+
child._sortOrder = parent._sortOrder
140+
child._sortColumn = parent._sortColumn
141141
child.dataChanged.connect(self.emitDataChanged)
142142
child._sizeChanged.connect(self._childrenSizeChangedHandler)
143143

@@ -165,18 +165,21 @@ def takeChild(self, index) -> Optional[TTkTreeWidgetItem]:
165165
0<= index < len(self._children) ):
166166
return None
167167
child = self._children.pop(index)
168+
child._parent = None
168169
child.dataChanged.disconnect(self.emitDataChanged)
169170
child._sizeChanged.disconnect(self._childrenSizeChangedHandler)
170171
self._childrenSizeChangedHandler(None, -child.size())
171172
self.emitDataChanged()
172173
return child
173174

174175
def takeChildren(self) -> List[TTkTreeWidgetItem]:
175-
children = self._children
176+
children = self._children.copy()
176177
for child in children:
178+
child._parent = None
177179
child.dataChanged.disconnect(self.emitDataChanged)
178180
child._sizeChanged.disconnect(self._childrenSizeChangedHandler)
179181
self._childrenSizeChangedHandler(None, -self._total_size)
182+
self._children = []
180183
self.emitDataChanged()
181184
return children
182185

@@ -421,15 +424,15 @@ def addChild(self, child:TTkTreeWidgetItem) -> None:
421424
self._children = _TTkTreeChildren(self)
422425
self._children._childrenSizeChanged.connect(self._sizeChangedHandler)
423426
self._children.dataChanged.connect(self.emitDataChanged)
424-
child = self._children.addChild(self, child)
427+
self._children.addChild(self, child)
425428
self._setDefaultIcon()
426429

427430
def addChildren(self, children:List[TTkTreeWidgetItem]) -> None:
428431
if not self._children:
429432
self._children = _TTkTreeChildren(self)
430433
self._children._childrenSizeChanged.connect(self._sizeChangedHandler)
431434
self._children.dataChanged.connect(self.emitDataChanged)
432-
children = self._children.addChildren(self, children)
435+
self._children.addChildren(self, children)
433436
self._setDefaultIcon()
434437

435438
def removeChild(self, child:TTkTreeWidgetItem) -> None:

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
"mypy>=1.15.0"
3737
]
3838
docs = [
39-
"Sphinx==8.2.3",
40-
"sphinx-book-theme==1.1.4"
39+
"Sphinx==8.2.3; python_version>='3.11'",
40+
"sphinx-book-theme==1.1.4; python_version>='3.11'"
4141
]
4242

4343
[tool.setuptools]

0 commit comments

Comments
 (0)