Browse Source

feat(TTkTable): add selection proxy (#488)

pull/489/head
Pier CeccoPierangioliEugenio 5 months ago committed by GitHub
parent
commit
d94b513634
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .vscode/settings.json
  2. 2
      apps/dumbPaintTool/dumbPaintTool/app/paintarea.py
  3. 2
      apps/ttkDesigner/ttkDesigner/app/menuBarEditor.py
  4. 8
      libs/pyTermTk/TermTk/TTkCore/constant.py
  5. 2
      libs/pyTermTk/TermTk/TTkCore/string.py
  6. 8
      libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table.py
  7. 433
      libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py
  8. 2
      libs/pyTermTk/TermTk/TTkWidgets/TTkTerminal/terminalview.py
  9. 2
      libs/pyTermTk/TermTk/TTkWidgets/button.py
  10. 2
      libs/pyTermTk/TermTk/TTkWidgets/checkbox.py
  11. 2
      libs/pyTermTk/TermTk/TTkWidgets/combobox.py
  12. 2
      libs/pyTermTk/TermTk/TTkWidgets/lineedit.py
  13. 2
      libs/pyTermTk/TermTk/TTkWidgets/listwidget.py
  14. 2
      libs/pyTermTk/TermTk/TTkWidgets/menu.py
  15. 2
      libs/pyTermTk/TermTk/TTkWidgets/radiobutton.py
  16. 2
      libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py
  17. 7
      libs/pyTermTk/TermTk/TTkWidgets/widget.py
  18. 1214
      tests/pytest/modelView/test_tablewidget.py
  19. 563
      tests/pytest/modelView/test_tablewidget_selectionproxy.py
  20. 2
      tests/weakref/test.05.TermTk.02.py

5
.vscode/settings.json vendored

@ -6,9 +6,10 @@
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"mypy-type-checker.args": [
"--config-file=pyproject.toml"
"--config-file=libs/pyTermTk/pyproject.toml"
],
"python.analysis.extraPaths": [
"./libs/pyTermTk",
"libs/pyTermTk",
"apps/",
]
}

2
apps/dumbPaintTool/dumbPaintTool/app/paintarea.py

@ -57,7 +57,7 @@ class PaintArea(ttk.TTkAbstractScrollView):
super().__init__(*args, **kwargs)
self.setTrans(ttk.TTkColor.bg('#FF00FF'))
self.resizeCanvas(*glbls.documentSize)
self.setFocusPolicy(ttk.TTkK.ClickFocus + ttk.TTkK.TabFocus)
self.setFocusPolicy(ttk.TTkK.ClickFocus | ttk.TTkK.TabFocus)
glbls.brush.toolTypeChanged.connect(self.setTool)
glbls.brush.areaChanged.connect( self.setAreaBrush)

2
apps/ttkDesigner/ttkDesigner/app/menuBarEditor.py

@ -172,7 +172,7 @@ class _SubMenuAreaWidget(ttk.TTkAbstractScrollView):
super().__init__(**kwargs)
self.layout().addWidget(self._btnAddSpacer)
self.layout().addWidget(self._btnAddMenu )
self.setFocusPolicy(ttk.TTkK.ClickFocus + ttk.TTkK.TabFocus)
self.setFocusPolicy(ttk.TTkK.ClickFocus | ttk.TTkK.TabFocus)
self.viewChanged.connect(self._viewChangedHandler)
self._btnAddSpacer.clicked.connect(self.addSpacer)
self._btnAddMenu.clicked.connect(self.addMenu)

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

@ -22,7 +22,7 @@
__all__ = ['TTkConstant', 'TTkK']
from enum import IntEnum
from enum import IntEnum, Flag
class TTkConstant:
'''Class container of all the constants used in :mod:`~TermTk`'''
@ -58,7 +58,7 @@ class TTkConstant:
ColorModifier = 0x08
'''The :py:class:`TTkColor` include a color modifier based on :py:class:`TTkColorModifier`'''
class FocusPolicy(IntEnum):
class FocusPolicy(Flag):
'''
This Class type defines the various policies a widget
can have with respect to acquiring keyboard focus.
@ -149,7 +149,7 @@ class TTkConstant:
QUIT_EVENT = 0x08
TIME_EVENT = 0x10
class Direction(int):
class Direction(IntEnum):
'''This class type is used to describe the direction
.. autosummary::
@ -477,7 +477,7 @@ class TTkConstant:
AcceptSave = 1
'''Save'''
class TTkItemSelectionModel(int):
class TTkItemSelectionModel(Flag):
'''These values describes the way the selection model will be updated.
.. autosummary::

2
libs/pyTermTk/TermTk/TTkCore/string.py

@ -602,7 +602,7 @@ class TTkString():
def getIndexes(self, char):
return [i for i,c in enumerate(self._text) if c==char]
def join(self, strings:Union[List[TTkString],List[str]]) -> TTkString:
def join(self, strings:Union[GeneratorType[TTkStringType,None,None],List[TTkStringType]]) -> TTkString:
''' Join the input strings using the current as separator
:param strings: the list of strings to be joined

8
libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table.py

@ -346,16 +346,16 @@ class TTkTable(TTkAbstractScrollArea):
:type row: int
'''
return self._tableView.unselectRow(row=row)
def unselectColumn(self, column:int) -> None:
def unselectColumn(self, col:int) -> None:
'''
.. seealso:: this method is forwarded to :py:meth:`TTkTableWidget.unselectColumn`
Unselects the given column in the table view
:param column: the column to be unselected
:type column: int
:param col: the column to be unselected
:type col: int
'''
return self._tableView.unselectColumn(column=column)
return self._tableView.unselectColumn(col=col)
def rowCount(self) -> int:
'''
.. seealso:: this method is forwarded to :py:meth:`TTkTableWidget.rowCount`

433
libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py

@ -20,10 +20,11 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations
__all__ = ['TTkTableWidget','TTkHeaderView']
from typing import Optional
from typing import Optional, List, Tuple, Callable, Iterator, Any, Protocol
from dataclasses import dataclass
from TermTk.TTkCore.log import TTkLog
@ -74,13 +75,16 @@ class TTkHeaderView():
def isVisible(self) -> bool:
return self._visible
_ClipboardTableData = List[List[Tuple[int,int,Any]]]
class _ClipboardTable(TTkString):
__slots__=('_data')
def __init__(self,data) -> None:
_data:_ClipboardTableData
def __init__(self,data:_ClipboardTableData) -> None:
self._data = data
super().__init__(self._toTTkString())
def data(self) -> list:
def data(self) -> _ClipboardTableData:
return self._data
def _toTTkString(self) -> TTkString:
@ -103,7 +107,149 @@ class _ClipboardTable(TTkString):
colSizes[col-minx] = max(colSizes[col-minx],txt.termWidth())
retLines[r][col-minx] = TTkString(txt)
ret += retLines
return TTkString('\n').join(TTkString(' ').join(s.align(width=colSizes[c]) for c,s in enumerate(l)) for l in ret)
return TTkString('\n').join([TTkString(' ').join([s.align(width=colSizes[c]) for c,s in enumerate(l)]) for l in ret])
class _FlagsCallable(Protocol):
def __call__(self, row: int, col: int) -> TTkK.ItemFlag: ...
class _SelectionProxy():
__slots__ = (
'_selected_2d_list',
'_cols','_rows',
'_flags')
_cols:int
_rows:int
_flags:_FlagsCallable
_selected_2d_list:List[List[bool]]
def __init__(self):
self._cols = 0
self._rows = 0
self._flags = lambda x,y : TTkK.ItemFlag.NoItemFlags
self._selected_2d_list = []
def updateModel(self, cols:int, rows:int, flags:_FlagsCallable) -> None:
self._flags = flags
self.resize(cols=cols, rows=rows)
def resize(self, cols:int, rows:int) -> None:
if cols < 0:
raise ValueError(f"unexpected negative value {cols=}")
if rows < 0:
raise ValueError(f"unexpected negative value {rows=}")
self._rows = rows
self._cols = cols
self.clear()
def clear(self) -> None:
rows = self._rows
cols = self._cols
if not cols or not rows:
self._selected_2d_list = []
self._selected_2d_list = [[False]*cols for _ in range(rows)]
def clearSelection(self) -> None:
self.clear()
def selectAll(self) -> None:
rows = self._rows
cols = self._cols
cmp = TTkK.ItemFlag.ItemIsSelectable
flagFunc = self._flags
self._selected_2d_list = [[cmp==(cmp&flagFunc(row=row,col=col)) for col in range(cols)] for row in range(rows)]
def selectRow(self, row:int) -> None:
if row < 0 or row >= self._rows:
return
cmp = TTkK.ItemFlag.ItemIsSelectable
flagFunc = self._flags
self._selected_2d_list[row] = [cmp==(cmp&flagFunc(row=row,col=col)) for col in range(self._cols)]
def selectColumn(self, col:int) -> None:
if col < 0 or col >= self._cols:
return
cmp = TTkK.ItemFlag.ItemIsSelectable
flagFunc = self._flags
for row,line in enumerate(self._selected_2d_list):
line[col] = cmp==(cmp&flagFunc(row=row,col=col))
def unselectRow(self, row:int) -> None:
if row < 0 or row >= self._rows:
return
self._selected_2d_list[row] = [False]*self._cols
def unselectColumn(self, col:int) -> None:
if col < 0 or col >= self._cols:
return
for line in self._selected_2d_list:
line[col] = False
def setSelection(self, pos:tuple[int,int], size:tuple[int,int], flags:TTkK.TTkItemSelectionModel) -> None:
x,y = pos
w,h = size
cols = self._cols
flagFunc = self._flags
cmp = TTkK.ItemFlag.ItemIsSelectable
if flags & (TTkK.TTkItemSelectionModel.Clear|TTkK.TTkItemSelectionModel.Deselect):
selection = [[False]*w for _ in range(h)]
elif flags & TTkK.TTkItemSelectionModel.Select:
selection = [[cmp==(cmp&flagFunc(col=_x,row=_y)) for _x in range(x,x+w)] for _y in range(y,y+h)]
for line,subst in zip(self._selected_2d_list[y:y+h],selection):
w=min(w,cols-x)
line[x:x+w]=subst[:w]
def isRowSelected(self, row:int) -> bool:
if row < 0 or row >= self._rows:
return False
flagFunc = self._flags
cmp = TTkK.ItemFlag.ItemIsSelectable
return all(_sel for i,_sel in enumerate(self._selected_2d_list[row]) if flagFunc(row,i)&cmp)
def isColSelected(self, col:int) -> bool:
if col < 0 or col >= self._cols:
return False
flagFunc = self._flags
cmp = TTkK.ItemFlag.ItemIsSelectable
return all(_sel[col] for i,_sel in enumerate(self._selected_2d_list) if flagFunc(i,col)&cmp)
def isCellSelected(self, col:int, row:int) -> bool:
if col < 0 or col >= self._cols:
return False
if row < 0 or row >= self._rows:
return False
return self._selected_2d_list[row][col]
def iterateSelected(self) -> Iterator[Tuple[int,int]]:
for row,line in enumerate(self._selected_2d_list):
for col,value in enumerate(line):
if value:
yield (row,col)
def iterateSelectedByRows(self) -> Iterator[List[Tuple[int,int]]]:
for row,line in enumerate(self._selected_2d_list):
selections_in_line = [(row,col) for col,value in enumerate(line) if value]
if selections_in_line:
yield selections_in_line
@dataclass
class _SnapItem():
dataIndex:TTkModelIndex
newData:Any
oldData:Any
@dataclass
class _SnapshotItems():
pos:TTkModelIndex
items:List[_SnapItem]
@dataclass
class _DragPosType():
fr:Tuple[int,int]
to:Tuple[int,int]
class TTkTableWidget(TTkAbstractScrollView):
'''
@ -251,8 +397,7 @@ class TTkTableWidget(TTkAbstractScrollView):
'_colsPos', '_rowsPos',
'_sortingEnabled',
'_dataPadding',
'_internal',
'_selected',
'_select_proxy',
'_hSeparatorSelected', '_vSeparatorSelected',
'_hoverPos', '_dragPos', '_currentPos',
'_sortColumn', '_sortOrder',
@ -266,6 +411,14 @@ class TTkTableWidget(TTkAbstractScrollView):
'_currentCellChanged',
)
_select_proxy:_SelectionProxy
_snapshot:List[_SnapshotItems]
_hoverPos:Optional[Tuple[int,int]]
_currentPos:Optional[Tuple[int,int]]
_hSeparatorSelected:Optional[int]
_vSeparatorSelected:Optional[int]
_dragPos:Optional[_DragPosType]
def __init__(self, *,
tableModel:Optional[TTkAbstractTableModel]=None,
vSeparator:bool=True,
@ -326,11 +479,10 @@ class TTkTableWidget(TTkAbstractScrollView):
self._showVSeparators = vSeparator
self._verticalHeader = TTkHeaderView(visible=vHeader)
self._horizontallHeader = TTkHeaderView(visible=hHeader)
self._selected = []
self._select_proxy = _SelectionProxy()
self._hoverPos = None
self._dragPos = None
self._currentPos = None
self._internal = {}
self._hSeparatorSelected = None
self._vSeparatorSelected = None
self._sortColumn = -1
@ -341,33 +493,29 @@ class TTkTableWidget(TTkAbstractScrollView):
super().__init__(**kwargs)
self._refreshLayout()
self.setMinimumHeight(1)
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
# self._rootItem = TTkTableWidgetItem(expanded=True)
# self.clear()
self.viewChanged.connect(self._viewChangedHandler)
self._verticalHeader.visibilityUpdated.connect( self._headerVisibilityChanged)
self._horizontallHeader.visibilityUpdated.connect(self._headerVisibilityChanged)
@dataclass
class _SnapItem():
dataIndex: TTkModelIndex = None
newData: object = None
oldData: object = None
def _saveSnapshot(self, items:list, currentPos:tuple[int]) -> None:
self._snapshot = self._snapshot[:self._snapshotId] + [[currentPos]+items]
def _saveSnapshot(self, items:list[_SnapItem], currentPos:TTkModelIndex) -> None:
self._snapshot = self._snapshot[:self._snapshotId] + [_SnapshotItems(pos=currentPos, items=items)]
self._snapshotId += 1
def _restoreSnapshot(self, snapId:int,newData=True):
rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
# rows = self._tableModel.rowCount()
# cols = self._tableModel.columnCount()
self.clearSelection()
for i in self._snapshot[snapId][1:]:
row=i.dataIndex.row()
col=i.dataIndex.col()
for _i in self._snapshot[snapId].items:
row=_i.dataIndex.row()
col=_i.dataIndex.col()
self.setSelection(pos=(col,row),size=(1,1),flags=TTkK.TTkItemSelectionModel.Select)
i.dataIndex.setData(i.newData if newData else i.oldData)
cpsi:TTkModelIndex = self._snapshot[snapId][0]
_i.dataIndex.setData(_i.newData if newData else _i.oldData)
cpsi:TTkModelIndex = self._snapshot[snapId].pos
self._setCurrentCell(cpsi.row(),cpsi.col())
self._moveCurrentCell(diff=(0,0))
self.update()
@ -411,20 +559,13 @@ class TTkTableWidget(TTkAbstractScrollView):
'''
Copies any selected cells to the clipboard.
'''
data = []
for row,line in enumerate(self._selected):
dataLine = []
for col,x in enumerate(line):
if x:
dataLine.append((row,col,self._tableModel.data(row,col)))
if dataLine:
data.append(dataLine)
data = [[(row,col,self._tableModel.data(row,col)) for row,col in line] for line in self._select_proxy.iterateSelectedByRows()]
clip = _ClipboardTable(data)
# str(clip)
self._clipboard.setText(clip)
def _cleanSelectedContent(self):
selected = [(_r,_c) for _r,_l in enumerate(self._selected) for _c,_v in enumerate(_l) if _v]
selected = [(row,col) for row,col in self._select_proxy.iterateSelected()]
mods = []
for _row,_col in selected:
mods.append((_row,_col,''))
@ -452,7 +593,7 @@ class TTkTableWidget(TTkAbstractScrollView):
rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
if not rows or not cols:
return
return True
if isinstance(data,_ClipboardTable) and data.data():
dataList = []
linearData = [_item for _line in data.data() for _item in _line]
@ -474,7 +615,7 @@ class TTkTableWidget(TTkAbstractScrollView):
self.update()
return True
def _tableModel_setData(self, dataList:list):
def _tableModel_setData(self, dataList:List[Tuple[int,int,Any]]):
# this is a helper to keep a snapshot copy if the data change
snaps = []
for row,col,newData in dataList:
@ -482,7 +623,7 @@ class TTkTableWidget(TTkAbstractScrollView):
dataIndex = self._tableModel.index(row=row,col=col)
if newData == oldData: continue
self.cellChanged.emit(row,col)
snaps.append(self._SnapItem(
snaps.append(_SnapItem(
dataIndex=dataIndex,
oldData=oldData,
newData=newData))
@ -562,6 +703,7 @@ class TTkTableWidget(TTkAbstractScrollView):
self._snapshotId = 0
rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
self._select_proxy.updateModel(rows=rows, cols=cols, flags=self._tableModel.flags)
self._vHeaderSize = vhs = 0 if not rows else 1+max(len(self._tableModel.headerData(_p, TTkK.VERTICAL)) for _p in range(rows) )
self._hHeaderSize = hhs = 0 if not rows else 1
self.setPadding(hhs,0,vhs,0)
@ -573,6 +715,7 @@ class TTkTableWidget(TTkAbstractScrollView):
self._rowsPos = [1+x*2 for x in range(rows)]
else:
self._rowsPos = [1+x for x in range(rows)]
#TODO: remove
self.clearSelection()
self.viewChanged.emit()
@ -591,9 +734,7 @@ class TTkTableWidget(TTkAbstractScrollView):
Deselects all selected items.
The current index will not be changed.
'''
rows = max(1,self._tableModel.rowCount())
cols = max(1,self._tableModel.columnCount())
self._selected = [[False]*cols for _ in range(rows)]
self._select_proxy.clear()
self.update()
def selectAll(self) -> None:
@ -601,11 +742,7 @@ class TTkTableWidget(TTkAbstractScrollView):
Selects all items in the view.
This function will use the selection behavior set on the view when selecting.
'''
rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
flagFunc = self._tableModel.flags
cmp = TTkK.ItemFlag.ItemIsSelectable
self._selected = [[cmp==(cmp&flagFunc(_r,_c)) for _c in range(cols)] for _r in range(rows)]
self._select_proxy.selectAll()
self.update()
def setSelection(self, pos:tuple[int,int], size:tuple[int,int], flags:TTkK.TTkItemSelectionModel) -> None:
@ -619,18 +756,7 @@ class TTkTableWidget(TTkAbstractScrollView):
:param flags: the selection model used (i.e. :py:class:`TTkItemSelectionModel.Select`)
:type flags: :py:class:`TTkItemSelectionModel`
'''
x,y = pos
w,h = size
rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
flagFunc = self._tableModel.flags
cmp = TTkK.ItemFlag.ItemIsSelectable
if flags & (TTkK.TTkItemSelectionModel.Clear|TTkK.TTkItemSelectionModel.Deselect):
for line in self._selected[y:y+h]:
line[x:x+w]=[False]*w
elif flags & TTkK.TTkItemSelectionModel.Select:
for _r, line in enumerate(self._selected[y:y+h],y):
line[x:x+w]=[cmp==(cmp&flagFunc(_r,_c)) for _c in range(x,min(x+w,cols))]
self._select_proxy.setSelection(pos=pos, size=size, flags=flags)
self.update()
def selectRow(self, row:int) -> None:
@ -640,10 +766,7 @@ class TTkTableWidget(TTkAbstractScrollView):
:param row: the row to be selected
:type row: int
'''
cols = self._tableModel.columnCount()
cmp = TTkK.ItemFlag.ItemIsSelectable
flagFunc = self._tableModel.flags
self._selected[row] = [cmp==(cmp&flagFunc(row,col)) for col in range(cols)]
self._select_proxy.selectRow(row=row)
self.update()
def selectColumn(self, col:int) -> None:
@ -653,10 +776,7 @@ class TTkTableWidget(TTkAbstractScrollView):
:param col: the column to be selected
:type col: int
'''
cmp = TTkK.ItemFlag.ItemIsSelectable
flagFunc = self._tableModel.flags
for row,line in enumerate(self._selected):
line[col] = cmp==(cmp&flagFunc(row,col))
self._select_proxy.selectColumn(col=col)
self.update()
def unselectRow(self, row:int) -> None:
@ -666,19 +786,17 @@ class TTkTableWidget(TTkAbstractScrollView):
:param row: the row to be unselected
:type row: int
'''
cols = self._tableModel.columnCount()
self._selected[row] = [False]*cols
self._select_proxy.unselectRow(row=row)
self.update()
def unselectColumn(self, column:int) -> None:
def unselectColumn(self, col:int) -> None:
'''
Unselects the given column in the table view
:param column: the column to be unselected
:type column: int
:param col: the column to be unselected
:type col: int
'''
for line in self._selected:
line[column] = False
self._select_proxy.unselectColumn(col=col)
self.update()
@pyTTkSlot()
@ -957,7 +1075,7 @@ class TTkTableWidget(TTkAbstractScrollView):
self.viewChanged.emit()
self.update()
def _findCell(self, x, y, headers):
def _findCell(self, x, y, headers) -> Tuple[int,int]:
showVH = self._verticalHeader.isVisible()
showHH = self._horizontallHeader.isVisible()
hhs = self._hHeaderSize if showHH else 0
@ -1316,31 +1434,31 @@ class TTkTableWidget(TTkAbstractScrollView):
# This is important to handle the header selection in the next part
if y < hhs:
_x = x+ox-vhs
for i, c in enumerate(self._colsPos):
if showVS and _x == c:
for _i, _c in enumerate(self._colsPos):
if showVS and _x == _c:
# I-th separator selected
self._hSeparatorSelected = i
self._hSeparatorSelected = _i
self.update()
return True
elif self._sortingEnabled and _x == c-(1 if showVS else 0) : # Pressed the sort otder icon
if self._sortColumn == i:
elif self._sortingEnabled and _x == _c-(1 if showVS else 0) : # Pressed the sort otder icon
if self._sortColumn == _i:
order = TTkK.SortOrder.DescendingOrder if self._sortOrder==TTkK.SortOrder.AscendingOrder else TTkK.SortOrder.AscendingOrder
else:
order = TTkK.SortOrder.AscendingOrder
self.sortByColumn(i,order)
self.sortByColumn(_i,order)
return True
elif showHS and x < vhs:
_y = y+oy-hhs
for i, r in enumerate(self._rowsPos):
for _i, r in enumerate(self._rowsPos):
if _y == r:
# I-th separator selected
self._vSeparatorSelected = i
self._vSeparatorSelected = _i
self.update()
return True
row,col = self._findCell(x,y, headers=True)
if not row==col==-1:
self._dragPos = [(row,col),(row,col)]
self._dragPos = _DragPosType(fr=(row,col),to=(row,col))
_ctrl = evt.mod==TTkK.ControlModifier
if row==col==-1:
# Corner Press
@ -1348,23 +1466,17 @@ class TTkTableWidget(TTkAbstractScrollView):
self.selectAll()
elif col==-1:
# Row select
flagFunc = self._tableModel.flags
cmp = TTkK.ItemFlag.ItemIsSelectable
state = all(_sel for i,_sel in enumerate(self._selected[row]) if flagFunc(row,i)&cmp)
if not _ctrl:
self.clearSelection()
if state:
if self._select_proxy.isRowSelected(row=row):
self.unselectRow(row)
else:
self.selectRow(row)
elif row==-1:
# Col select
flagFunc = self._tableModel.flags
cmp = TTkK.ItemFlag.ItemIsSelectable
state = all(_sel[col] for i,_sel in enumerate(self._selected) if flagFunc(i,col)&cmp)
if not _ctrl:
self.clearSelection()
if state:
if self._select_proxy.isColSelected(col=col):
self.unselectColumn(col)
else:
self.selectColumn(col)
@ -1373,8 +1485,15 @@ class TTkTableWidget(TTkAbstractScrollView):
self.cellClicked.emit(row,col)
# self.cellPressed.emit(row,col)
self._setCurrentCell(row,col)
self.setSelection(pos = (col,row), size = (1,1),
flags = TTkK.TTkItemSelectionModel.Clear if (self._selected[row][col] and _ctrl) else TTkK.TTkItemSelectionModel.Select)
self.setSelection(
pos = (col,row),
size = (1,1),
flags = (
TTkK.TTkItemSelectionModel.Clear
if (self._select_proxy.isColSelected(col=col) and _ctrl)
else TTkK.TTkItemSelectionModel.Select
)
)
self._hoverPos = None
self.update()
return True
@ -1394,7 +1513,7 @@ class TTkTableWidget(TTkAbstractScrollView):
hhs = self._hHeaderSize if showHH else 0
vhs = self._vHeaderSize if showVH else 0
if self._dragPos and not self._hSeparatorSelected and not self._vSeparatorSelected:
self._dragPos[1] = self._findCell(x,y, headers=False)
self._dragPos.to = self._findCell(x,y, headers=False)
self.update()
return True
if self._hSeparatorSelected is not None:
@ -1434,12 +1553,13 @@ class TTkTableWidget(TTkAbstractScrollView):
rows = self._tableModel.rowCount()
cols = self._tableModel.columnCount()
state = True
(rowa,cola),(rowb,colb) = self._dragPos
rowa,cola = self._dragPos.fr
rowb,colb = self._dragPos.to
if evt.mod==TTkK.ControlModifier:
# Pick the status to be applied to the selection if CTRL is Pressed
# In case of line/row selection I choose the element 0 of that line
state = self._selected[max(0,rowa)][max(0,cola)]
state = self._select_proxy.isCellSelected(row=max(0,rowa), col=max(0,cola))
else:
# Clear the selection if no ctrl has been pressed
self.clearSelection()
@ -1563,7 +1683,7 @@ class TTkTableWidget(TTkAbstractScrollView):
# Cache Cells
_cellsCache = []
_colorCache2d = [[None]*(colb+1-cola) for _ in range(rowb+1-rowa)]
_colorCache2d:List[List[TTkColor]] = [[color]*(colb+1-cola) for _ in range(rowb+1-rowa)]
for row in range(*rrows):
ya,yb = sliceRow[row]
if showHS:
@ -1584,7 +1704,7 @@ class TTkTableWidget(TTkAbstractScrollView):
cellColor = (
currentColor if self._currentPos == (row,col) else
hoverColor if self._hoverPos in [(row,col),(-1,col),(row,-1),(-1,-1)] else
selectedColor if self._selected[row][col] else
selectedColor if self._select_proxy.isCellSelected(row=row, col=col) else
rowColor )
_colorCache2d[row-rowa][col-cola] = cellColor
_cellsCache.append([row,col,xa,xb,ya,yb,cellColor])
@ -1604,35 +1724,35 @@ class TTkTableWidget(TTkAbstractScrollView):
def _drawCellBottom(_col,_row,_xa,_xb,_ya,_yb,cellColor):
if _yb>=h: return
if _row<rows-1:
_belowColor:TTkColor = _colorCache2d[_row+1-rowa][_col-cola]
_belowColor2 = _colorCache2d[_row+1-rowa][_col-cola]
# force black border if there are selections
_sa = self._selected[_row ][_col ]
_sb = self._selected[_row+1][_col ]
_sa = self._select_proxy.isCellSelected(row=_row , col=_col)
_sb = self._select_proxy.isCellSelected(row=_row+1, col=_col)
if (showHS and showVS) and _sa and not _sb:
_bgA:TTkColor = cellColor.background()
_bgB:TTkColor = TTkColor.RST
_bgA5 = cellColor.background()
_bgB5 = TTkColor.RST
elif (showHS and showVS) and not _sa and _sb:
_bgA:TTkColor = TTkColor.RST
_bgB:TTkColor = _belowColor.background()
_bgA5 = TTkColor.RST
_bgB5 = _belowColor2.background()
else:
_bgA:TTkColor = cellColor.background()
_bgB:TTkColor = _belowColor.background()
_bgA5 = cellColor.background()
_bgB5 = _belowColor2.background()
if _bgA == _bgB:
if _bgA5 == _bgB5:
_char=''
_color = lineColor if _bgA == TTkColor.RST else _bgA + lineColor
elif _bgB == TTkColor.RST:
_color = lineColor if _bgA5 == TTkColor.RST else _bgA5 + lineColor
elif _bgB5 == TTkColor.RST:
_char=''
_color=_bgA.invertFgBg()
elif _bgA == TTkColor.RST:
_color=_bgA5.invertFgBg()
elif _bgA5 == TTkColor.RST:
_char=''
_color=_bgB.invertFgBg()
_color=_bgB5.invertFgBg()
else:
_char=''
_color=_bgB + _bgA.invertFgBg()
_color=_bgB5 + _bgA5.invertFgBg()
else:
if self._selected[_row ][_col ]:
if self._select_proxy.isCellSelected(row=_row, col=_col):
_char=''
_color=selectedColorInv
elif cellColor.hasBackground():
@ -1646,35 +1766,35 @@ class TTkTableWidget(TTkAbstractScrollView):
def _drawCellRight(_col,_row,_xa,_xb,_ya,_yb,cellColor):
if _xb>=w: return
if _col<cols-1:
_rightColor:TTkColor = _colorCache2d[_row-rowa][_col+1-cola]
_rightColor = _colorCache2d[_row-rowa][_col+1-cola]
# force black border if there are selections
_sa = self._selected[_row ][_col ]
_sc = self._selected[_row ][_col+1]
_sa = self._select_proxy.isCellSelected(row=_row, col=_col )
_sc = self._select_proxy.isCellSelected(row=_row, col=_col+1)
if (showHS and showVS) and _sa and not _sc:
_bgA:TTkColor = cellColor.background()
_bgC:TTkColor = TTkColor.RST
_bgA4 = cellColor.background()
_bgC = TTkColor.RST
elif (showHS and showVS) and not _sa and _sc:
_bgA:TTkColor = TTkColor.RST
_bgC:TTkColor = _rightColor.background()
_bgA4 = TTkColor.RST
_bgC = _rightColor.background()
else:
_bgA:TTkColor = cellColor.background()
_bgC:TTkColor = _rightColor.background()
_bgA4 = cellColor.background()
_bgC = _rightColor.background()
if _bgA == _bgC:
if _bgA4 == _bgC:
_char=''
_color = lineColor if _bgA == TTkColor.RST else _bgA + lineColor
_color = lineColor if _bgA4 == TTkColor.RST else _bgA4 + lineColor
elif _bgC == TTkColor.RST:
_char=''
_color=_bgA.invertFgBg()
elif _bgA == TTkColor.RST:
_color=_bgA4.invertFgBg()
elif _bgA4 == TTkColor.RST:
_char=''
_color=_bgC.invertFgBg()
else:
_char=''
_color=_bgC + _bgA.invertFgBg()
_color=_bgC + _bgA4.invertFgBg()
else:
if self._selected[_row ][_col ]:
if self._select_proxy.isCellSelected(row=_row, col=_col):
_char=''
_color=selectedColorInv
elif cellColor.hasBackground():
@ -1702,35 +1822,35 @@ class TTkTableWidget(TTkAbstractScrollView):
if _row<rows-1 and _col<cols-1:
# Check if there are selected cells:
chId = (
0x01 * self._selected[_row ][_col ] +
0x02 * self._selected[_row ][_col+1] +
0x04 * self._selected[_row+1][_col ] +
0x08 * self._selected[_row+1][_col+1] )
0x01 * self._select_proxy.isCellSelected(row=_row , col=_col ) +
0x02 * self._select_proxy.isCellSelected(row=_row , col=_col+1) +
0x04 * self._select_proxy.isCellSelected(row=_row+1, col=_col ) +
0x08 * self._select_proxy.isCellSelected(row=_row+1, col=_col+1) )
if chId==0x00 or chId==0x0F:
_belowColor:TTkColor = _colorCache2d[_row+1-rowa][_col-cola]
_bgA:TTkColor = cellColor.background()
_bgB:TTkColor = _belowColor.background()
_belowColor3 = _colorCache2d[_row+1-rowa][_col-cola]
_bgA3 = cellColor.background()
_bgB3 = _belowColor3.background()
if _bgA == _bgB:
_color = lineColor if _bgA == TTkColor.RST else _bgA + lineColor
if _bgA3 == _bgB3:
_color = lineColor if _bgA3 == TTkColor.RST else _bgA3 + lineColor
_char=''
elif _bgB == TTkColor.RST:
elif _bgB3 == TTkColor.RST:
_char=''
_color=_bgA.invertFgBg()
elif _bgA == TTkColor.RST:
_color=_bgA3.invertFgBg()
elif _bgA3 == TTkColor.RST:
_char=''
_color=_bgB.invertFgBg()
_color=_bgB3.invertFgBg()
else:
_char=''
_color=_bgB + _bgA.invertFgBg()
_color=_bgB3 + _bgA3.invertFgBg()
else:
_char = _charList[chId]
_color=selectedColorInv
elif _col<cols-1:
chId = (
0x01 * self._selected[row ][col ] +
0x02 * self._selected[row ][col+1] )
0x01 * self._select_proxy.isCellSelected(row=row, col=col ) +
0x02 * self._select_proxy.isCellSelected(row=row, col=col+1) )
if chId:
_char = _charList[chId]
_color=selectedColorInv
@ -1742,30 +1862,30 @@ class TTkTableWidget(TTkAbstractScrollView):
_color = lineColor
elif _row<rows-1:
chId = (
(0x01) * self._selected[row ][col ] +
(0x04) * self._selected[row+1][col ] )
_belowColor:TTkColor = _colorCache2d[_row+1-rowa][_col-cola]
_bgA:TTkColor = cellColor.background()
_bgB:TTkColor = _belowColor.background()
(0x01) * self._select_proxy.isCellSelected(row=row , col=col ) +
(0x04) * self._select_proxy.isCellSelected(row=row+1, col=col ) )
_belowColor1:TTkColor = _colorCache2d[_row+1-rowa][_col-cola]
_bgA1:TTkColor = cellColor.background()
_bgB1:TTkColor = _belowColor1.background()
if chId:
_char = _charList[chId]
_color=selectedColorInv
elif _bgA == _bgB == TTkColor.RST:
elif _bgA1 == _bgB1 == TTkColor.RST:
_char = ''
_color = lineColor
elif _bgB == TTkColor.RST:
elif _bgB1 == TTkColor.RST:
_char=''
_color=_bgA.invertFgBg()
elif _bgA == TTkColor.RST:
_color=_bgA1.invertFgBg()
elif _bgA1 == TTkColor.RST:
_char=''
_color=_bgB.invertFgBg()
_color=_bgB1.invertFgBg()
else:
_char=''
_color=_bgB + _bgA.invertFgBg()
_color=_bgB1 + _bgA1.invertFgBg()
else:
chId = (
(0x01) * self._selected[row ][col ] )
(0x01) * self._select_proxy.isCellSelected(row=row, col=col) )
if chId:
_char = _charList[chId]
_color=selectedColorInv
@ -1823,7 +1943,8 @@ class TTkTableWidget(TTkAbstractScrollView):
canvas.fill(char='',pos=(xb,ya+1), size=(1,yb-ya-1), color=hoverColorInv)
if self._dragPos:
(rowa,cola),(rowb,colb) = self._dragPos
rowa,cola = self._dragPos.fr
rowb,colb = self._dragPos.to
if rowa == -1:
cola,colb = min(cola,colb),max(cola,colb)
xa = sliceCol[cola][0]-ox+vhs

2
libs/pyTermTk/TermTk/TTkWidgets/TTkTerminal/terminalview.py

@ -244,7 +244,7 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC):
self._screen_alt.resize(w,h)
self._screen_normal.resize(w,h)
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
self.enableWidgetCursor()
self.viewChanged.connect(self._viewChangedHandler)
self._screen_normal.bufferedLinesChanged.connect(self._screenChanged)

2
libs/pyTermTk/TermTk/TTkWidgets/button.py

@ -151,7 +151,7 @@ class TTkButton(TTkWidget):
if 'maxHeight' not in kwargs:
self.setMaximumHeight(len(self._text))
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
def border(self) -> bool:
''' This property holds whether the button has a border

2
libs/pyTermTk/TermTk/TTkWidgets/checkbox.py

@ -127,7 +127,7 @@ class TTkCheckbox(TTkWidget):
self.setMinimumSize(3 + len(self._text), 1)
self.setMaximumHeight(1)
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
def text(self) -> TTkString:
''' This property holds the text shown on the checkhox

2
libs/pyTermTk/TermTk/TTkWidgets/combobox.py

@ -352,7 +352,7 @@ class TTkComboBox(TTkContainer):
self.setFocusPolicy(TTkK.ClickFocus)
else:
self._lineEdit.hide()
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
@pyTTkSlot(str)
def _callback(self, label:TTkString) -> None:

2
libs/pyTermTk/TermTk/TTkWidgets/lineedit.py

@ -97,7 +97,7 @@ class TTkLineEdit(TTkWidget):
self.setInputType(inputType)
self.setMaximumHeight(1)
self.setMinimumSize(1,1)
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
self.enableWidgetCursor()
@pyTTkSlot(TTkStringType)

2
libs/pyTermTk/TermTk/TTkWidgets/listwidget.py

@ -234,7 +234,7 @@ class TTkListWidget(TTkAbstractScrollView):
super().__init__(**kwargs)
self.addItems(items)
self.viewChanged.connect(self._viewChangedHandler)
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
self.searchModified.connect(self._searchModifiedHandler)
@pyTTkSlot()

2
libs/pyTermTk/TermTk/TTkWidgets/menu.py

@ -248,7 +248,7 @@ class _TTkMenuAreaWidget(TTkAbstractScrollView):
self._minWidth = 0
self._caller = caller
super().__init__(**kwargs)
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
self.viewChanged.connect(self._viewChangedHandler)
def _resizeEvent(self):

2
libs/pyTermTk/TermTk/TTkWidgets/radiobutton.py

@ -105,7 +105,7 @@ class TTkRadioButton(TTkWidget):
self.setMinimumSize(3 + len(self._text), 1)
self.setMaximumHeight(1)
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
if self._radiogroup not in TTkRadioButton._radioLists:
TTkRadioButton._radioLists[self._radiogroup] = [self]

2
libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py

@ -479,7 +479,7 @@ class TTkTabBar(TTkContainer):
self.tabBarClicked = pyTTkSignal(int)
self.tabCloseRequested = pyTTkSignal(int)
self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus)
self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus)
def mergeStyle(self, style):
super().mergeStyle(style)

7
libs/pyTermTk/TermTk/TTkWidgets/widget.py

@ -418,7 +418,7 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
self.resize(width, height)
self.move(x, y)
def pasteEvent(self, txt:str) -> None:
def pasteEvent(self, txt:str) -> bool:
'''
Callback triggered when a paste event is forwarded to this widget.
@ -426,8 +426,11 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents):
:param txt: the paste object
:type txt: str
:return: the state of the paste operation
:rtype: bool
'''
pass
return False
def _mouseEventParseChildren(self, evt:TTkMouseEvent) -> bool:
return False

1214
tests/pytest/modelView/test_tablewidget.py

File diff suppressed because it is too large Load Diff

563
tests/pytest/modelView/test_tablewidget_selectionproxy.py

@ -0,0 +1,563 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# 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 os
import sys
import pytest
from unittest.mock import Mock
sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk'))
import TermTk as ttk
from TermTk.TTkWidgets.TTkModelView.tablewidget import _SelectionProxy
class TestSelectionProxy:
"""Test cases for _SelectionProxy class"""
def setup_method(self):
"""Set up test fixtures before each test method."""
self.proxy = _SelectionProxy()
# Mock flags function that returns selectable for all cells
def mock_flags_all_selectable(row, col):
return ttk.TTkK.ItemFlag.ItemIsSelectable
# Mock flags function that returns non-selectable for specific cells
def mock_flags_some_non_selectable(row, col):
if row == 1 and col == 1:
return ttk.TTkK.ItemFlag.NoItemFlags
return ttk.TTkK.ItemFlag.ItemIsSelectable
self.mock_flags_all = mock_flags_all_selectable
self.mock_flags_some = mock_flags_some_non_selectable
def test_init_default(self):
"""Test default initialization"""
proxy = _SelectionProxy()
assert proxy._cols == 0
assert proxy._rows == 0
assert proxy._selected_2d_list == []
def test_resize_valid_dimensions(self):
"""Test resizing with valid dimensions"""
self.proxy.resize(3, 4)
assert self.proxy._cols == 3
assert self.proxy._rows == 4
assert len(self.proxy._selected_2d_list) == 4
assert all(len(row) == 3 for row in self.proxy._selected_2d_list)
assert all(all(not cell for cell in row) for row in self.proxy._selected_2d_list)
def test_resize_zero_dimensions(self):
"""Test resizing with zero dimensions"""
self.proxy.resize(0, 0)
assert self.proxy._cols == 0
assert self.proxy._rows == 0
assert self.proxy._selected_2d_list == []
def test_resize_negative_dimensions(self):
"""Test resizing with negative dimensions raises ValueError"""
with pytest.raises(ValueError, match="unexpected negative value"):
self.proxy.resize(-1, 3)
with pytest.raises(ValueError, match="unexpected negative value"):
self.proxy.resize(3, -1)
def test_update_model(self):
"""Test updateModel method"""
self.proxy.updateModel(cols=3, rows=4, flags=self.mock_flags_all)
assert self.proxy._cols == 3
assert self.proxy._rows == 4
assert self.proxy._flags == self.mock_flags_all
def test_clear_with_data(self):
"""Test clearing selection with existing data"""
self.proxy.resize(3, 4)
# Manually set some selections
self.proxy._selected_2d_list[1][2] = True
self.proxy._selected_2d_list[3][0] = True
self.proxy.clear()
assert all(all(not cell for cell in row) for row in self.proxy._selected_2d_list)
def test_clear_selection_alias(self):
"""Test that clearSelection is alias for clear"""
self.proxy.resize(3, 4)
self.proxy._selected_2d_list[1][1] = True
self.proxy.clearSelection()
assert all(all(not cell for cell in row) for row in self.proxy._selected_2d_list)
def test_select_all_with_all_selectable(self):
"""Test selectAll with all cells selectable"""
self.proxy.updateModel(cols=3, rows=2, flags=self.mock_flags_all)
self.proxy.selectAll()
assert all(all(cell for cell in row) for row in self.proxy._selected_2d_list)
def test_select_all_with_some_non_selectable(self):
"""Test selectAll with some cells non-selectable"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_some)
self.proxy.selectAll()
# Cell (1,1) should not be selected due to flags
assert not self.proxy._selected_2d_list[1][1]
# Other cells should be selected
assert self.proxy._selected_2d_list[0][0]
assert self.proxy._selected_2d_list[2][2]
def test_select_row_valid(self):
"""Test selecting a valid row"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
self.proxy.selectRow(1)
# Row 1 should be fully selected
assert all(self.proxy._selected_2d_list[1])
# Other rows should not be selected
assert not any(self.proxy._selected_2d_list[0])
assert not any(self.proxy._selected_2d_list[2])
def test_select_row_with_non_selectable_cells(self):
"""Test selecting row with non-selectable cells"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_some)
self.proxy.selectRow(1)
# Cell (1,1) should not be selected due to flags
assert not self.proxy._selected_2d_list[1][1]
# Other cells in row 1 should be selected
assert self.proxy._selected_2d_list[1][0]
assert self.proxy._selected_2d_list[1][2]
def test_select_row_invalid_indices(self):
"""Test selecting invalid row indices"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_all)
# Should not crash or change anything
self.proxy.selectRow(-1)
self.proxy.selectRow(5)
assert not any(any(row) for row in self.proxy._selected_2d_list)
def test_select_column_valid(self):
"""Test selecting a valid column"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
self.proxy.selectColumn(2)
# Column 2 should be selected in all rows
assert all(self.proxy._selected_2d_list[row][2] for row in range(3))
# Other columns should not be selected
for row in range(3):
assert not self.proxy._selected_2d_list[row][0]
assert not self.proxy._selected_2d_list[row][1]
assert not self.proxy._selected_2d_list[row][3]
def test_select_column_with_non_selectable_cells(self):
"""Test selecting column with non-selectable cells"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_some)
self.proxy.selectColumn(1)
# Cell (1,1) should not be selected due to flags
assert not self.proxy._selected_2d_list[1][1]
# Other cells in column 1 should be selected
assert self.proxy._selected_2d_list[0][1]
assert self.proxy._selected_2d_list[2][1]
def test_select_column_invalid_indices(self):
"""Test selecting invalid column indices"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_all)
# Should not crash or change anything
self.proxy.selectColumn(-1)
self.proxy.selectColumn(5)
assert not any(any(row) for row in self.proxy._selected_2d_list)
def test_unselect_row_valid(self):
"""Test unselecting a valid row"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
self.proxy.selectAll()
self.proxy.unselectRow(1)
# Row 1 should be unselected
assert not any(self.proxy._selected_2d_list[1])
# Other rows should remain selected
assert all(self.proxy._selected_2d_list[0])
assert all(self.proxy._selected_2d_list[2])
def test_unselect_row_invalid_indices(self):
"""Test unselecting invalid row indices"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_all)
self.proxy.selectAll()
original_state = [row[:] for row in self.proxy._selected_2d_list]
# Should not crash or change anything
self.proxy.unselectRow(-1)
self.proxy.unselectRow(5)
assert self.proxy._selected_2d_list == original_state
def test_unselect_column_valid(self):
"""Test unselecting a valid column"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
self.proxy.selectAll()
self.proxy.unselectColumn(2)
# Column 2 should be unselected in all rows
assert not any(self.proxy._selected_2d_list[row][2] for row in range(3))
# Other columns should remain selected
for row in range(3):
assert self.proxy._selected_2d_list[row][0]
assert self.proxy._selected_2d_list[row][1]
assert self.proxy._selected_2d_list[row][3]
def test_unselect_column_invalid_indices(self):
"""Test unselecting invalid column indices"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_all)
self.proxy.selectAll()
original_state = [row[:] for row in self.proxy._selected_2d_list]
# Should not crash or change anything
self.proxy.unselectColumn(-1)
self.proxy.unselectColumn(5)
assert self.proxy._selected_2d_list == original_state
def test_set_selection_select(self):
"""Test setSelection with Select flag"""
self.proxy.updateModel(cols=5, rows=4, flags=self.mock_flags_all)
self.proxy.setSelection(pos=(1, 1), size=(2, 2), flags=ttk.TTkK.TTkItemSelectionModel.Select)
# Selected area should be (1,1) to (2,2)
assert self.proxy._selected_2d_list[1][1]
assert self.proxy._selected_2d_list[1][2]
assert self.proxy._selected_2d_list[2][1]
assert self.proxy._selected_2d_list[2][2]
# Outside area should not be selected
assert not self.proxy._selected_2d_list[0][0]
assert not self.proxy._selected_2d_list[3][3]
def test_set_selection_clear(self):
"""Test setSelection with Clear flag"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
self.proxy.selectAll()
self.proxy.setSelection(pos=(1, 1), size=(2, 1), flags=ttk.TTkK.TTkItemSelectionModel.Clear)
# Specified area should be cleared
assert not self.proxy._selected_2d_list[1][1]
assert not self.proxy._selected_2d_list[1][2]
# Other areas should remain selected
assert self.proxy._selected_2d_list[0][0]
assert self.proxy._selected_2d_list[2][3]
def test_set_selection_deselect(self):
"""Test setSelection with Deselect flag"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
self.proxy.selectAll()
self.proxy.setSelection(pos=(0, 0), size=(2, 2), flags=ttk.TTkK.TTkItemSelectionModel.Deselect)
# Specified area should be deselected
assert not self.proxy._selected_2d_list[0][0]
assert not self.proxy._selected_2d_list[0][1]
assert not self.proxy._selected_2d_list[1][0]
assert not self.proxy._selected_2d_list[1][1]
# Other areas should remain selected
assert self.proxy._selected_2d_list[2][2]
assert self.proxy._selected_2d_list[2][3]
def test_set_selection_boundary_conditions(self):
"""Test setSelection with boundary conditions"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_all)
# Selection extending beyond boundaries should be clipped
self.proxy.setSelection(pos=(2, 2), size=(3, 3), flags=ttk.TTkK.TTkItemSelectionModel.Select)
# Only valid cells should be selected
assert self.proxy._selected_2d_list[2][2]
# Should not crash or cause index errors
def test_is_row_selected_all_selectable(self):
"""Test isRowSelected with all selectable cells"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
self.proxy.selectRow(1)
assert self.proxy.isRowSelected(1)
assert not self.proxy.isRowSelected(0)
assert not self.proxy.isRowSelected(2)
def test_is_row_selected_with_non_selectable_cells(self):
"""Test isRowSelected with non-selectable cells"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_some)
self.proxy.selectRow(1)
# Row 1 should be considered selected even though cell (1,1) is not selectable
# because isRowSelected only considers selectable cells
assert self.proxy.isRowSelected(1)
def test_is_row_selected_invalid_indices(self):
"""Test isRowSelected with invalid indices"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_all)
assert not self.proxy.isRowSelected(-1)
assert not self.proxy.isRowSelected(5)
def test_is_col_selected_all_selectable(self):
"""Test isColSelected with all selectable cells"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
self.proxy.selectColumn(2)
assert self.proxy.isColSelected(2)
assert not self.proxy.isColSelected(0)
assert not self.proxy.isColSelected(3)
def test_is_col_selected_with_non_selectable_cells(self):
"""Test isColSelected with non-selectable cells"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_some)
self.proxy.selectColumn(1)
# Column 1 should be considered selected even though cell (1,1) is not selectable
assert self.proxy.isColSelected(1)
def test_is_col_selected_invalid_indices(self):
"""Test isColSelected with invalid indices"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_all)
assert not self.proxy.isColSelected(-1)
assert not self.proxy.isColSelected(5)
def test_is_cell_selected_valid(self):
"""Test isCellSelected with valid coordinates"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
self.proxy._selected_2d_list[1][2] = True
assert self.proxy.isCellSelected(2, 1)
assert not self.proxy.isCellSelected(1, 1)
assert not self.proxy.isCellSelected(0, 0)
def test_is_cell_selected_invalid_indices(self):
"""Test isCellSelected with invalid indices"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_all)
assert not self.proxy.isCellSelected(-1, 1)
assert not self.proxy.isCellSelected(1, -1)
assert not self.proxy.isCellSelected(5, 1)
assert not self.proxy.isCellSelected(1, 5)
def test_iterate_selected_empty(self):
"""Test iterateSelected with no selections"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_all)
selections = list(self.proxy.iterateSelected())
assert selections == []
def test_iterate_selected_with_selections(self):
"""Test iterateSelected with some selections"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
self.proxy._selected_2d_list[0][1] = True
self.proxy._selected_2d_list[1][2] = True
self.proxy._selected_2d_list[2][3] = True
selections = list(self.proxy.iterateSelected())
expected = [(0, 1), (1, 2), (2, 3)]
assert selections == expected
def test_iterate_selected_by_rows_empty(self):
"""Test iterateSelectedByRows with no selections"""
self.proxy.updateModel(cols=3, rows=3, flags=self.mock_flags_all)
selections = list(self.proxy.iterateSelectedByRows())
assert selections == []
def test_iterate_selected_by_rows_with_selections(self):
"""Test iterateSelectedByRows with selections across multiple rows"""
self.proxy.updateModel(cols=4, rows=4, flags=self.mock_flags_all)
# Row 0: select columns 1, 2
self.proxy._selected_2d_list[0][1] = True
self.proxy._selected_2d_list[0][2] = True
# Row 1: no selections
# Row 2: select column 0
self.proxy._selected_2d_list[2][0] = True
# Row 3: select columns 1, 3
self.proxy._selected_2d_list[3][1] = True
self.proxy._selected_2d_list[3][3] = True
selections = list(self.proxy.iterateSelectedByRows())
expected = [
[(0, 1), (0, 2)], # Row 0
[(2, 0)], # Row 2
[(3, 1), (3, 3)] # Row 3
]
assert selections == expected
def test_iterate_selected_by_rows_single_row_multiple_cols(self):
"""Test iterateSelectedByRows with single row, multiple columns"""
self.proxy.updateModel(cols=5, rows=3, flags=self.mock_flags_all)
self.proxy._selected_2d_list[1][0] = True
self.proxy._selected_2d_list[1][2] = True
self.proxy._selected_2d_list[1][4] = True
selections = list(self.proxy.iterateSelectedByRows())
expected = [[(1, 0), (1, 2), (1, 4)]]
assert selections == expected
def test_empty_proxy_operations(self):
"""Test operations on empty proxy (0 rows, 0 columns)"""
proxy = _SelectionProxy()
# All operations should handle empty state gracefully
proxy.clear()
proxy.clearSelection()
proxy.selectAll()
proxy.selectRow(0)
proxy.selectColumn(0)
proxy.unselectRow(0)
proxy.unselectColumn(0)
assert not proxy.isRowSelected(0)
assert not proxy.isColSelected(0)
assert not proxy.isCellSelected(0, 0)
assert list(proxy.iterateSelected()) == []
assert list(proxy.iterateSelectedByRows()) == []
def test_single_cell_proxy(self):
"""Test operations on single cell proxy (1 row, 1 column)"""
self.proxy.updateModel(cols=1, rows=1, flags=self.mock_flags_all)
# Select the single cell
self.proxy.selectAll()
assert self.proxy.isRowSelected(0)
assert self.proxy.isColSelected(0)
assert self.proxy.isCellSelected(0, 0)
selections = list(self.proxy.iterateSelected())
assert selections == [(0, 0)]
row_selections = list(self.proxy.iterateSelectedByRows())
assert row_selections == [[(0, 0)]]
def test_complex_selection_scenario(self):
"""Test complex selection scenario with mixed operations"""
self.proxy.updateModel(cols=5, rows=4, flags=self.mock_flags_all)
# Select all
self.proxy.selectAll()
# Unselect row 1
self.proxy.unselectRow(1)
# Unselect column 2
self.proxy.unselectColumn(2)
# Set specific selection
self.proxy.setSelection(pos=(3, 1), size=(1, 2), flags=ttk.TTkK.TTkItemSelectionModel.Select)
# Verify final state
# Row 0: all except column 2
assert self.proxy.isCellSelected(0, 0)
assert self.proxy.isCellSelected(1, 0)
assert not self.proxy.isCellSelected(2, 0) # Column 2 unselected
assert self.proxy.isCellSelected(3, 0)
assert self.proxy.isCellSelected(4, 0)
# Row 1: only column 3 (from setSelection)
assert not self.proxy.isCellSelected(0, 1) # Row 1 was unselected
assert not self.proxy.isCellSelected(1, 1)
assert not self.proxy.isCellSelected(2, 1) # Column 2 unselected
assert self.proxy.isCellSelected(3, 1) # Added by setSelection
assert not self.proxy.isCellSelected(4, 1) # Row 1 was unselected
# # Row 2: column 3 (from setSelection)
# assert not self.proxy.isCellSelected(0, 2) # Row was affected by other operations
# assert not self.proxy.isCellSelected(1, 2)
# assert not self.proxy.isCellSelected(2, 2) # Column 2 unselected
# assert self.proxy.isCellSelected(3, 2) # Added by setSelection
# assert not self.proxy.isCellSelected(4, 2)
def test_flags_integration(self):
"""Test integration with different flag functions"""
# Test with all non-selectable flags
def no_flags(row, col):
return ttk.TTkK.ItemFlag.NoItemFlags
self.proxy.updateModel(cols=3, rows=3, flags=no_flags)
self.proxy.selectAll()
# Nothing should be selected
assert not any(any(row) for row in self.proxy._selected_2d_list)
# Test with selective flags
def selective_flags(row, col):
if row % 2 == 0 and col % 2 == 0: # Only even positions
return ttk.TTkK.ItemFlag.ItemIsSelectable
return ttk.TTkK.ItemFlag.NoItemFlags
self.proxy.updateModel(cols=4, rows=4, flags=selective_flags)
self.proxy.selectAll()
# Only cells at even positions should be selected
for row in range(4):
for col in range(4):
if row % 2 == 0 and col % 2 == 0:
assert self.proxy._selected_2d_list[row][col]
else:
assert not self.proxy._selected_2d_list[row][col]
def test_selection_state_consistency(self):
"""Test that selection state remains consistent across operations"""
self.proxy.updateModel(cols=4, rows=3, flags=self.mock_flags_all)
# Perform various operations and check consistency
self.proxy.selectRow(0)
initial_selected = sum(sum(row) for row in self.proxy._selected_2d_list)
self.proxy.selectColumn(1)
# Should have added new selections
current_selected = sum(sum(row) for row in self.proxy._selected_2d_list)
assert current_selected >= initial_selected
self.proxy.clearSelection()
assert sum(sum(row) for row in self.proxy._selected_2d_list) == 0
# Verify dimensions remain correct
assert len(self.proxy._selected_2d_list) == 3
assert all(len(row) == 4 for row in self.proxy._selected_2d_list)

2
tests/weakref/test.05.TermTk.02.py

@ -61,7 +61,7 @@ class TestWid(ttk.TTkWidget):
self.setDefaultSize(kwargs, 10, 10)
super().__init__(*args, **kwargs)
self._b = ttk.pyTTkSignal(bool)
self.setFocusPolicy(ttk.TTkK.ClickFocus + ttk.TTkK.TabFocus)
self.setFocusPolicy(ttk.TTkK.ClickFocus | ttk.TTkK.TabFocus)
def mousePressEvent(self, evt):
# TTkLog.debug(f"{self._text} Test Mouse {evt}")

Loading…
Cancel
Save