From d94b5136340593fb63ef438940b9684b048a04f4 Mon Sep 17 00:00:00 2001 From: Pier CeccoPierangioliEugenio Date: Sat, 25 Oct 2025 13:18:36 +0100 Subject: [PATCH] feat(TTkTable): add selection proxy (#488) --- .vscode/settings.json | 5 +- .../dumbPaintTool/app/paintarea.py | 2 +- .../ttkDesigner/app/menuBarEditor.py | 2 +- libs/pyTermTk/TermTk/TTkCore/constant.py | 8 +- libs/pyTermTk/TermTk/TTkCore/string.py | 2 +- .../TermTk/TTkWidgets/TTkModelView/table.py | 8 +- .../TTkWidgets/TTkModelView/tablewidget.py | 433 +++--- .../TTkWidgets/TTkTerminal/terminalview.py | 2 +- libs/pyTermTk/TermTk/TTkWidgets/button.py | 2 +- libs/pyTermTk/TermTk/TTkWidgets/checkbox.py | 2 +- libs/pyTermTk/TermTk/TTkWidgets/combobox.py | 2 +- libs/pyTermTk/TermTk/TTkWidgets/lineedit.py | 2 +- libs/pyTermTk/TermTk/TTkWidgets/listwidget.py | 2 +- libs/pyTermTk/TermTk/TTkWidgets/menu.py | 2 +- .../pyTermTk/TermTk/TTkWidgets/radiobutton.py | 2 +- libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py | 2 +- libs/pyTermTk/TermTk/TTkWidgets/widget.py | 7 +- tests/pytest/modelView/test_tablewidget.py | 1214 ++++++----------- .../test_tablewidget_selectionproxy.py | 563 ++++++++ tests/weakref/test.05.TermTk.02.py | 2 +- 20 files changed, 1315 insertions(+), 949 deletions(-) create mode 100644 tests/pytest/modelView/test_tablewidget_selectionproxy.py diff --git a/.vscode/settings.json b/.vscode/settings.json index bdc20ae7..3c9b4dbb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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/", ] } \ No newline at end of file diff --git a/apps/dumbPaintTool/dumbPaintTool/app/paintarea.py b/apps/dumbPaintTool/dumbPaintTool/app/paintarea.py index e13e7ac0..1949212f 100644 --- a/apps/dumbPaintTool/dumbPaintTool/app/paintarea.py +++ b/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) diff --git a/apps/ttkDesigner/ttkDesigner/app/menuBarEditor.py b/apps/ttkDesigner/ttkDesigner/app/menuBarEditor.py index b83af597..aad9bdaf 100644 --- a/apps/ttkDesigner/ttkDesigner/app/menuBarEditor.py +++ b/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) diff --git a/libs/pyTermTk/TermTk/TTkCore/constant.py b/libs/pyTermTk/TermTk/TTkCore/constant.py index 8ad1b60a..e1f9b2ed 100644 --- a/libs/pyTermTk/TermTk/TTkCore/constant.py +++ b/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:: diff --git a/libs/pyTermTk/TermTk/TTkCore/string.py b/libs/pyTermTk/TermTk/TTkCore/string.py index 8636bb48..c2f42643 100644 --- a/libs/pyTermTk/TermTk/TTkCore/string.py +++ b/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 diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table.py index 413164d5..e755c920 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table.py +++ b/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` diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py index 08dbe72a..f8925f4c 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py +++ b/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=w: return if _col bool: ''' This property holds whether the button has a border diff --git a/libs/pyTermTk/TermTk/TTkWidgets/checkbox.py b/libs/pyTermTk/TermTk/TTkWidgets/checkbox.py index 45a7eb3d..3c577d5c 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/checkbox.py +++ b/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 diff --git a/libs/pyTermTk/TermTk/TTkWidgets/combobox.py b/libs/pyTermTk/TermTk/TTkWidgets/combobox.py index 0a429b8a..c7c459db 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/combobox.py +++ b/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: diff --git a/libs/pyTermTk/TermTk/TTkWidgets/lineedit.py b/libs/pyTermTk/TermTk/TTkWidgets/lineedit.py index 9479d7b8..c71c0764 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/lineedit.py +++ b/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) diff --git a/libs/pyTermTk/TermTk/TTkWidgets/listwidget.py b/libs/pyTermTk/TermTk/TTkWidgets/listwidget.py index 83875604..1db1e9bd 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/listwidget.py +++ b/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() diff --git a/libs/pyTermTk/TermTk/TTkWidgets/menu.py b/libs/pyTermTk/TermTk/TTkWidgets/menu.py index 49d59114..2efae5cc 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/menu.py +++ b/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): diff --git a/libs/pyTermTk/TermTk/TTkWidgets/radiobutton.py b/libs/pyTermTk/TermTk/TTkWidgets/radiobutton.py index e92f997a..2f1f7083 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/radiobutton.py +++ b/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] diff --git a/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py b/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py index bbc1d5b0..d18340ae 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/tabwidget.py +++ b/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) diff --git a/libs/pyTermTk/TermTk/TTkWidgets/widget.py b/libs/pyTermTk/TermTk/TTkWidgets/widget.py index c922b5af..9b07f240 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/widget.py +++ b/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 diff --git a/tests/pytest/modelView/test_tablewidget.py b/tests/pytest/modelView/test_tablewidget.py index 5d379863..4ed8a78c 100644 --- a/tests/pytest/modelView/test_tablewidget.py +++ b/tests/pytest/modelView/test_tablewidget.py @@ -24,11 +24,101 @@ import os import sys import pytest +import io from unittest.mock import Mock, MagicMock, patch sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk')) import TermTk as ttk +from TermTk.TTkWidgets.TTkModelView.tablewidget import _DragPosType, _ClipboardTable, _SelectionProxy, TTkHeaderView +class TestTTkHeaderView: + """Test cases for TTkHeaderView class""" + + def test_init_default(self): + """Test default initialization""" + header = TTkHeaderView() + assert header.isVisible() == True + assert hasattr(header, 'visibilityUpdated') + + def test_init_with_visibility(self): + """Test initialization with specific visibility""" + header_visible = TTkHeaderView(visible=True) + header_hidden = TTkHeaderView(visible=False) + + assert header_visible.isVisible() == True + assert header_hidden.isVisible() == False + + def test_set_visible(self): + """Test setVisible method""" + header = TTkHeaderView() + + header.setVisible(False) + assert header.isVisible() == False + + header.setVisible(True) + assert header.isVisible() == True + + def test_show_hide(self): + """Test show and hide methods""" + header = TTkHeaderView(visible=False) + + header.show() + assert header.isVisible() == True + + header.hide() + assert header.isVisible() == False + + def test_visibility_signal(self): + """Test visibility signal emission""" + header = TTkHeaderView() + signal_calls = [] + + @ttk.pyTTkSlot(bool) + def mock_slot(visible): + signal_calls.append(visible) + + header.visibilityUpdated.connect(mock_slot) + + header.hide() + assert len(signal_calls) == 1 + assert signal_calls[0] == False + + header.show() + assert len(signal_calls) == 2 + assert signal_calls[1] == True + +class TestClipboardTable: + """Test cases for _ClipboardTable class""" + + def test_init_empty(self): + """Test initialization with empty data""" + clipboard = _ClipboardTable([]) + assert clipboard.data() == [] + assert isinstance(clipboard, ttk.TTkString) + + def test_init_with_data(self): + """Test initialization with data""" + data = [ + [(0, 0, 'A1'), (0, 1, 'B1')], + [(1, 0, 'A2'), (1, 1, 'B2')] + ] + clipboard = _ClipboardTable(data) + + assert clipboard.data() == data + assert isinstance(clipboard, ttk.TTkString) + assert len(str(clipboard)) > 0 + + def test_string_conversion(self): + """Test TTkString conversion""" + data = [ + [(0, 0, 'Cell1'), (0, 1, 'Cell2')], + [(1, 0, 'Cell3'), (1, 1, 'Cell4')] + ] + clipboard = _ClipboardTable(data) + + string_repr = str(clipboard) + # Should contain the cell data in some formatted way + assert 'Cell1' in string_repr or str(clipboard).find('Cell') >= 0 class TestTTkTableWidget: """Test cases for TTkTableWidget class""" @@ -73,112 +163,173 @@ class TestTTkTableWidget: assert widget.rowCount() == 3 assert widget.columnCount() == 3 - def test_init_with_separator_options(self): - """Test initialization with separator visibility options""" + def test_init_parameters(self): + """Test initialization with all parameters""" widget = ttk.TTkTableWidget( + tableModel=self.table_model, vSeparator=False, - hSeparator=False + hSeparator=False, + vHeader=False, + hHeader=False, + sortingEnabled=True, + dataPadding=3 ) + assert widget.model() == self.table_model assert not widget.vSeparatorVisibility() assert not widget.hSeparatorVisibility() - - def test_init_with_header_options(self): - """Test initialization with header visibility options""" - widget = ttk.TTkTableWidget( - vHeader=False, - hHeader=False - ) - assert not widget.verticalHeader().isVisible() assert not widget.horizontalHeader().isVisible() + assert widget.isSortingEnabled() + assert widget._dataPadding == 3 - def test_init_with_sorting_enabled(self): - """Test initialization with sorting enabled""" - widget = ttk.TTkTableWidget(sortingEnabled=True) + def test_header_views(self): + """Test header view functionality""" + widget = ttk.TTkTableWidget(tableModel=self.table_model) - assert widget.isSortingEnabled() + v_header = widget.verticalHeader() + h_header = widget.horizontalHeader() - def test_init_with_data_padding(self): - """Test initialization with custom data padding""" - widget = ttk.TTkTableWidget(dataPadding=3) + assert isinstance(v_header, TTkHeaderView) + assert isinstance(h_header, TTkHeaderView) + assert v_header.isVisible() + assert h_header.isVisible() - # Data padding is internal, but we can verify the widget was created - assert widget is not None + # Test header visibility changes trigger layout refresh + with patch.object(widget, '_headerVisibilityChanged') as mock_refresh: + v_header.hide() + # Signal should have been emitted and connected + assert not v_header.isVisible() - def test_model_getter_setter(self): - """Test model getter and setter""" + def test_model_management(self): + """Test model getter/setter and related functionality""" widget = ttk.TTkTableWidget() original_model = widget.model() - # Set new model + # Test model change widget.setModel(self.table_model) assert widget.model() == self.table_model assert widget.model() != original_model - # Verify model data is accessible - assert widget.rowCount() == 3 - assert widget.columnCount() == 3 + # Test that model signals are connected + assert widget.model().dataChanged._connected_slots + assert widget.model().modelChanged._connected_slots + + # Test model change triggers layout refresh + with patch.object(widget, '_refreshLayout') as mock_refresh: + new_model = ttk.TTkTableModelList(data=[['test']]) + widget.setModel(new_model) + # _refreshLayout should be called when model changes + # Note: This depends on the modelChanged signal being emitted - def test_row_column_count(self): - """Test row and column count methods""" + def test_selection_proxy_integration(self): + """Test selection proxy integration""" widget = ttk.TTkTableWidget(tableModel=self.table_model) - assert widget.rowCount() == 3 - assert widget.columnCount() == 3 + proxy = widget._select_proxy + assert isinstance(proxy, _SelectionProxy) + assert proxy._rows == 3 + assert proxy._cols == 3 - def test_current_row_column(self): - """Test current row and column methods""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) + # Test selection operations update proxy + widget.selectRow(1) + assert proxy.isRowSelected(1) - # Initially should be at (0, 0) or (-1, -1) if no selection - current_row = widget.currentRow() - current_col = widget.currentColumn() + widget.selectColumn(2) + assert proxy.isColSelected(2) - assert isinstance(current_row, int) - assert isinstance(current_col, int) - assert current_row >= -1 - assert current_col >= -1 + widget.clearSelection() + assert not proxy.isRowSelected(1) + assert not proxy.isColSelected(2) - def test_header_views(self): - """Test vertical and horizontal header views""" + def test_current_cell_management(self): + """Test current cell position management""" widget = ttk.TTkTableWidget(tableModel=self.table_model) - v_header = widget.verticalHeader() - h_header = widget.horizontalHeader() + # Initially no current position + assert widget._currentPos is None + assert widget.currentRow() == 0 # Default + assert widget.currentColumn() == 0 # Default + + # Test setting current cell + widget._setCurrentCell(1, 2) + assert widget._currentPos == (1, 2) + assert widget.currentRow() == 1 + assert widget.currentColumn() == 2 + + # # Test movement + # widget._moveCurrentCell(row=1, col=1) + # assert widget._currentPos == (2, 3) # Should be (1+1, 2+1) if movement works + + # Test boundary handling + widget._moveCurrentCell(row=5, col=5) # Should clamp to table bounds + assert widget._currentPos[0] < widget.rowCount() + assert widget._currentPos[1] < widget.columnCount() + + def test_snapshot_system(self): + """Test undo/redo snapshot system""" + widget = ttk.TTkTableWidget(tableModel=self.table_model) - assert isinstance(v_header, ttk.TTkHeaderView) - assert isinstance(h_header, ttk.TTkHeaderView) - assert v_header.isVisible() - assert h_header.isVisible() + # Initially no snapshots + assert len(widget._snapshot) == 0 + assert widget._snapshotId == 0 + assert not widget.isUndoAvailable() + assert not widget.isRedoAvailable() - def test_separator_visibility(self): - """Test separator visibility methods""" - widget = ttk.TTkTableWidget() + # Create a snapshot by modifying data + original_value = widget.model().data(0, 0) + widget._tableModel_setData([(0, 0, 'NewValue')]) + + # Should have a snapshot now + assert widget.isUndoAvailable() + assert not widget.isRedoAvailable() + assert widget.model().data(0, 0) == 'NewValue' - # Default should be visible - assert widget.hSeparatorVisibility() - assert widget.vSeparatorVisibility() + # Test undo + widget.undo() + assert widget.model().data(0, 0) == original_value + assert not widget.isUndoAvailable() + assert widget.isRedoAvailable() - # Test setters - widget.setHSeparatorVisibility(False) - widget.setVSeparatorVisibility(False) + # Test redo + widget.redo() + assert widget.model().data(0, 0) == 'NewValue' + assert widget.isUndoAvailable() + assert not widget.isRedoAvailable() - assert not widget.hSeparatorVisibility() - assert not widget.vSeparatorVisibility() + def test_clipboard_operations(self): + """Test clipboard operations""" + widget = ttk.TTkTableWidget(tableModel=self.table_model) + + # Select some cells + widget.setSelection((0, 0), (2, 2), ttk.TTkK.TTkItemSelectionModel.Select) + + # Test copy + widget.copy() + # Should not raise exception - # Test setting back to visible - widget.setHSeparatorVisibility(True) - widget.setVSeparatorVisibility(True) + # Test cut + original_values = [ + [widget.model().data(r, c) for c in range(2)] + for r in range(2) + ] + widget.cut() - assert widget.hSeparatorVisibility() - assert widget.vSeparatorVisibility() + # Values should be cleared after cut + for r in range(2): + for c in range(2): + if widget._select_proxy.isCellSelected(c, r): + assert widget.model().data(r, c) == '' + + # Test paste + widget.paste() + # Should restore some values or handle gracefully def test_sorting_functionality(self): - """Test sorting enabled/disabled and sort by column""" + """Test sorting functionality""" widget = ttk.TTkTableWidget(tableModel=self.table_model) - # Initially sorting should be disabled + # Initially disabled assert not widget.isSortingEnabled() # Enable sorting @@ -186,454 +337,133 @@ class TestTTkTableWidget: assert widget.isSortingEnabled() # Test sorting by column + original_first_name = widget.model().data(0, 0) widget.sortByColumn(0, ttk.TTkK.SortOrder.AscendingOrder) - # Note: We can't easily verify the actual sorting without checking the model state - - # Disable sorting - widget.setSortingEnabled(False) - assert not widget.isSortingEnabled() - - def test_selection_methods(self): - """Test selection-related methods""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) - - # Test clear selection - widget.clearSelection() - # Test select all - widget.selectAll() - - # Test select row - widget.selectRow(0) - - # Test select column - widget.selectColumn(1) - - # Test set selection with position and size - widget.setSelection((0, 0), (2, 2), ttk.TTkK.TTkItemSelectionModel.Select) + # Can't easily test actual sorting without knowing internal implementation + # but should not raise exception - # These methods should not raise exceptions - # Actual selection state testing would require more complex setup + # Test descending sort + widget.sortByColumn(0, ttk.TTkK.SortOrder.DescendingOrder) - def test_column_width_operations(self): - """Test column width setting and resizing""" + def test_resize_operations(self): + """Test column and row resize operations""" widget = ttk.TTkTableWidget(tableModel=self.table_model) - # Test setting column width + # Test individual operations widget.setColumnWidth(0, 20) - - # Test resize column to contents - widget.resizeColumnToContents(0) - - # Test resize all columns to contents - widget.resizeColumnsToContents() - - # These methods should not raise exceptions - - def test_row_height_operations(self): - """Test row height setting and resizing""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) - - # Test setting row height widget.setRowHeight(0, 3) - # Test resize row to contents + # Test resize to contents + widget.resizeColumnToContents(0) widget.resizeRowToContents(0) - # Test resize all rows to contents + # Test resize all to contents + widget.resizeColumnsToContents() widget.resizeRowsToContents() - # These methods should not raise exceptions - - def test_clipboard_operations(self): - """Test copy, cut, and paste operations""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) - - # These operations should not raise exceptions - widget.copy() - widget.cut() - widget.paste() - - def test_undo_redo_operations(self): - """Test undo and redo operations""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) - - # Initially, there should be no undo/redo available - # But the methods should not raise exceptions - widget.undo() - widget.redo() - - def test_signals_exist(self): - """Test that all expected signals exist""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) - - # Test that signals are accessible - assert hasattr(widget, 'cellChanged') - assert hasattr(widget, 'cellClicked') - assert hasattr(widget, 'cellDoubleClicked') - assert hasattr(widget, 'cellEntered') - assert hasattr(widget, 'currentCellChanged') - - # Test that they are signal objects - assert isinstance(widget.cellChanged, ttk.pyTTkSignal) - assert isinstance(widget.cellClicked, ttk.pyTTkSignal) - assert isinstance(widget.cellDoubleClicked, ttk.pyTTkSignal) - assert isinstance(widget.cellEntered, ttk.pyTTkSignal) - assert isinstance(widget.currentCellChanged, ttk.pyTTkSignal) - - def test_signal_connections(self): - """Test signal connections""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) + # These should not raise exceptions - # Create mock slots - - cell_changed_slot = Mock() - cell_clicked_slot = Mock() - current_cell_changed_slot = Mock() - @ttk.pyTTkSlot(int,int) - def _mock_cell_changed_slot(row:int,col:int): - cell_changed_slot(row, col) - @ttk.pyTTkSlot(int,int) - def _mock_cell_clicked_slot(row:int,col:int): - cell_clicked_slot(row, col) - @ttk.pyTTkSlot(int,int,int,int) - def _mock_cell_cchanged_slot(a:int,b:int,c:int,d:int): - current_cell_changed_slot(a,b,c,d) - - # Connect signals - widget.cellChanged.connect(_mock_cell_changed_slot) - widget.cellClicked.connect(_mock_cell_clicked_slot) - widget.currentCellChanged.connect(_mock_cell_cchanged_slot) - - # Verify connections were made (signals should have connections) - assert len(widget.cellChanged._connected_slots) > 0 - assert len(widget.cellClicked._connected_slots) > 0 - assert len(widget.currentCellChanged._connected_slots) > 0 - - def test_model_change_propagation(self): - """Test that model changes are propagated to the widget""" + def test_view_area_calculation(self): + """Test view area size calculation""" widget = ttk.TTkTableWidget(tableModel=self.table_model) - # Modify the model - self.table_model.setData(0, 0, 'Modified') - - # The widget should be notified (through signal connections) - # We can't easily test the actual update without a more complex setup - assert widget.model().data(0, 0) == 'Modified' - - def test_empty_model_handling(self): - """Test widget behavior with empty model""" - empty_model = ttk.TTkTableModelList(data=[[]]) - widget = ttk.TTkTableWidget(tableModel=empty_model) - - assert widget.rowCount() == 1 - assert widget.columnCount() == 0 - - def test_large_model_handling(self): - """Test widget behavior with large model""" - large_data = [[f'Cell_{i}_{j}' for j in range(100)] for i in range(100)] - large_model = ttk.TTkTableModelList(data=large_data) - widget = ttk.TTkTableWidget(tableModel=large_model) - - assert widget.rowCount() == 100 - assert widget.columnCount() == 100 - - def test_model_replacement(self): - """Test replacing the model after widget creation""" - widget = ttk.TTkTableWidget() - original_row_count = widget.rowCount() - original_col_count = widget.columnCount() - - # Replace with our test model - widget.setModel(self.table_model) - - # Verify the change - assert widget.rowCount() != original_row_count - assert widget.columnCount() != original_col_count - assert widget.rowCount() == 3 - assert widget.columnCount() == 3 - - def test_widget_with_different_model_types(self): - """Test widget with different types of table models""" - # Test with TTkTableModelList - list_model = ttk.TTkTableModelList(data=self.test_data) - widget1 = ttk.TTkTableWidget(tableModel=list_model) - assert widget1.rowCount() == 3 - - # Test with TTkTableModelCSV (using in-memory data) - import io - csv_data = io.StringIO("Name,Age\nAlice,25\nBob,30") - csv_model = ttk.TTkTableModelCSV(fd=csv_data) - widget2 = ttk.TTkTableWidget(tableModel=csv_model) - assert widget2.rowCount() == 2 - - def test_header_visibility_changes(self): - """Test changing header visibility after initialization""" - widget = ttk.TTkTableWidget() - - # Initially headers should be visible - assert widget.verticalHeader().isVisible() - assert widget.horizontalHeader().isVisible() + # With headers + width, height = widget.viewFullAreaSize() + assert width > 0 + assert height > 0 - # Hide headers + # Without headers widget.verticalHeader().hide() widget.horizontalHeader().hide() - assert not widget.verticalHeader().isVisible() - assert not widget.horizontalHeader().isVisible() - - # Show headers again - widget.verticalHeader().show() - widget.horizontalHeader().show() - - assert widget.verticalHeader().isVisible() - assert widget.horizontalHeader().isVisible() - - def test_focus_policy(self): - """Test that the widget has proper focus policy""" - widget = ttk.TTkTableWidget() - - # Should accept click and tab focus - focus_policy = widget.focusPolicy() - assert focus_policy & ttk.TTkK.ClickFocus - assert focus_policy & ttk.TTkK.TabFocus - - def test_minimum_size(self): - """Test minimum size constraints""" - widget = ttk.TTkTableWidget() - - # Should have minimum height of 1 - min_size = widget.minimumSize() - assert min_size[1] >= 1 # height should be at least 1 - - def test_style_properties(self): - """Test that the widget has proper style properties""" - widget = ttk.TTkTableWidget() - - # Should have class style defined - assert hasattr(ttk.TTkTableWidget, 'classStyle') - assert isinstance(ttk.TTkTableWidget.classStyle, dict) - assert 'default' in ttk.TTkTableWidget.classStyle + width_no_headers, height_no_headers = widget.viewFullAreaSize() + assert width_no_headers <= width + assert height_no_headers <= height - def test_boundary_conditions(self): - """Test boundary conditions and edge cases""" + def test_find_cell_functionality(self): + """Test _findCell coordinate conversion""" widget = ttk.TTkTableWidget(tableModel=self.table_model) - # Test with invalid row/column indices (should not crash) - try: - widget.selectRow(-1) - widget.selectRow(1000) - widget.selectColumn(-1) - widget.selectColumn(1000) - widget.setColumnWidth(-1, 10) - widget.setColumnWidth(1000, 10) - widget.setRowHeight(-1, 2) - widget.setRowHeight(1000, 2) - except Exception as e: - # Some operations might raise exceptions, but they shouldn't crash - pass - - def test_integration_with_scroll_area(self): - """Test that the widget works as expected within a scroll area (via TTkTable)""" - table = ttk.TTkTable(tableModel=self.table_model) - - # TTkTable should forward methods to the internal TTkTableWidget - assert table.rowCount() == 3 - assert table.columnCount() == 3 - assert table.model() == self.table_model - - def test_zero_rows_model(self): - """Test widget behavior with model that has zero rows""" - # Create model with zero rows but some columns - empty_rows_model = ttk.TTkTableModelList( - data=[], - header=['Col1', 'Col2', 'Col3'] - ) - widget = ttk.TTkTableWidget(tableModel=empty_rows_model) - - assert widget.rowCount() == 0 - assert widget.columnCount() == 0 - assert widget.currentRow() == 0 # No valid current row - assert widget.currentColumn() == 0 # No valid current column - - # Selection operations should handle empty rows gracefully - widget.clearSelection() - widget.selectAll() # Should not crash with no rows - - # Column operations should still work - widget.setColumnWidth(0, 50) - widget.resizeColumnsToContents() - - # Row operations should handle empty case - widget.resizeRowsToContents() # Should not crash with no rows - - def test_zero_columns_model(self): - """Test widget behavior with model that has zero columns""" - # Create model with zero columns but some rows - empty_cols_model = ttk.TTkTableModelList( - data=[[], [], []], # 3 empty rows - header=[] - ) - widget = ttk.TTkTableWidget(tableModel=empty_cols_model) - - assert widget.rowCount() == 3 - assert widget.columnCount() == 0 - assert widget.currentRow() == 0 # No valid current row - assert widget.currentColumn() == 0 # No valid current column - - # Selection operations should handle empty columns gracefully - widget.clearSelection() - widget.selectAll() # Should not crash with no columns - - # Row operations should still work - widget.setRowHeight(0, 2) - widget.resizeRowsToContents() - - # Column operations should handle empty case - widget.resizeColumnsToContents() # Should not crash with no columns - - def test_completely_empty_model(self): - """Test widget behavior with model that has zero rows and zero columns""" - # Create completely empty model - empty_model = ttk.TTkTableModelList( - data=[], - header=[] - ) - widget = ttk.TTkTableWidget(tableModel=empty_model) - - assert widget.rowCount() == 0 - assert widget.columnCount() == 0 - assert widget.currentRow() == 0 - assert widget.currentColumn() == 0 - - # All operations should handle completely empty case gracefully - widget.clearSelection() - widget.selectAll() - widget.resizeColumnsToContents() - widget.resizeRowsToContents() - - # Clipboard operations with empty model - widget.copy() # Should not crash - widget.cut() # Should not crash - widget.paste() # Should not crash - - # Undo/redo with empty model - widget.undo() - widget.redo() - - def test_single_cell_model(self): - """Test widget behavior with model that has exactly one cell""" - single_cell_model = ttk.TTkTableModelList( - data=[['SingleCell']], - header=['OnlyColumn'] - ) - widget = ttk.TTkTableWidget(tableModel=single_cell_model) - - assert widget.rowCount() == 1 - assert widget.columnCount() == 1 - - # Selection operations - widget.selectAll() - widget.selectRow(0) - widget.selectColumn(0) - widget.setSelection((0, 0), (1, 1), ttk.TTkK.TTkItemSelectionModel.Select) - - # Resize operations - widget.setColumnWidth(0, 20) - widget.setRowHeight(0, 3) - widget.resizeColumnToContents(0) - widget.resizeRowToContents(0) - - def test_model_transitions(self): - """Test transitioning between different model sizes""" + # Test with headers + row, col = widget._findCell(10, 5, headers=True) + assert isinstance(row, int) + assert isinstance(col, int) + assert row >= -1 # -1 for header, >= 0 for cells + assert col >= -1 # -1 for header, >= 0 for cells + + # Test without headers + row, col = widget._findCell(10, 5, headers=False) + assert isinstance(row, int) + assert isinstance(col, int) + assert row >= 0 + assert col >= 0 + + def test_mouse_events(self): + """Test mouse event handling""" widget = ttk.TTkTableWidget(tableModel=self.table_model) - # Start with normal model - assert widget.rowCount() == 3 - assert widget.columnCount() == 3 - - # Transition to empty rows - empty_rows_model = ttk.TTkTableModelList(data=[], header=['A', 'B']) - widget.setModel(empty_rows_model) - assert widget.rowCount() == 0 - assert widget.columnCount() == 0 - - # Transition to empty columns - empty_cols_model = ttk.TTkTableModelList(data=[[], []], header=[]) - widget.setModel(empty_cols_model) - assert widget.rowCount() == 2 - assert widget.columnCount() == 0 - - # Transition to completely empty - empty_model = ttk.TTkTableModelList(data=[], header=[]) - widget.setModel(empty_model) - assert widget.rowCount() == 0 - assert widget.columnCount() == 0 - - # Transition back to normal model - widget.setModel(self.table_model) - assert widget.rowCount() == 3 - assert widget.columnCount() == 3 - - def test_boundary_operations_on_empty_models(self): - """Test boundary operations on various empty model configurations""" + # Mock mouse events + press_event = Mock() + press_event.x = 15 + press_event.y = 2 + press_event.mod = ttk.TTkK.NoModifier - # Test with zero rows model - zero_rows_model = ttk.TTkTableModelList(data=[], header=['A', 'B']) - widget = ttk.TTkTableWidget(tableModel=zero_rows_model) + move_event = Mock() + move_event.x = 20 + move_event.y = 3 + move_event.mod = ttk.TTkK.NoModifier - # These should not crash even with no rows - try: - widget.selectRow(0) # Invalid row - widget.unselectRow(0) # Invalid row - widget.setRowHeight(0, 2) # Invalid row - widget.resizeRowToContents(0) # Invalid row - except (IndexError, ValueError): - pass # Expected for invalid indices - - # Test with zero columns model - zero_cols_model = ttk.TTkTableModelList(data=[[], []], header=[]) - widget.setModel(zero_cols_model) - - # These should not crash even with no columns - try: - widget.selectColumn(0) # Invalid column - widget.unselectColumn(0) # Invalid column - widget.setColumnWidth(0, 20) # Invalid column - widget.resizeColumnToContents(0) # Invalid column - except (IndexError, ValueError): - pass # Expected for invalid indices + release_event = Mock() + release_event.x = 20 + release_event.y = 3 + release_event.mod = ttk.TTkK.NoModifier - def test_empty_model_sorting(self): - """Test sorting operations on empty models""" + double_click_event = Mock() + double_click_event.x = 15 + double_click_event.y = 2 + double_click_event.mod = ttk.TTkK.NoModifier - # Test sorting with zero rows - zero_rows_model = ttk.TTkTableModelList(data=[], header=['Name', 'Age']) - widget = ttk.TTkTableWidget(tableModel=zero_rows_model, sortingEnabled=True) + # Test event handling (should not crash) + with patch.object(widget, '_findCell', return_value=(1, 1)): + assert widget.mousePressEvent(press_event) == True + assert widget.mouseMoveEvent(move_event) == True + assert widget.mouseReleaseEvent(release_event) == True + assert widget.mouseDoubleClickEvent(double_click_event) == True - assert widget.isSortingEnabled() - # Sorting empty data should not crash - widget.sortByColumn(0, ttk.TTkK.SortOrder.AscendingOrder) - widget.sortByColumn(1, ttk.TTkK.SortOrder.DescendingOrder) + def test_keyboard_events(self): + """Test keyboard event handling""" + widget = ttk.TTkTableWidget(tableModel=self.table_model) - # Test sorting with zero columns - zero_cols_model = ttk.TTkTableModelList(data=[[], []], header=[]) - widget.setModel(zero_cols_model) + # Set initial position + widget._setCurrentCell(1, 1) + + # Mock keyboard events + key_events = [ + Mock(key=ttk.TTkK.Key_Up, mod=ttk.TTkK.NoModifier), + Mock(key=ttk.TTkK.Key_Down, mod=ttk.TTkK.NoModifier), + Mock(key=ttk.TTkK.Key_Left, mod=ttk.TTkK.NoModifier), + Mock(key=ttk.TTkK.Key_Right, mod=ttk.TTkK.NoModifier), + Mock(key=ttk.TTkK.Key_Tab, mod=ttk.TTkK.NoModifier), + Mock(key=ttk.TTkK.Key_Backtab, mod=ttk.TTkK.NoModifier), + Mock(key=ttk.TTkK.Key_Enter, mod=ttk.TTkK.NoModifier), + Mock(key=ttk.TTkK.Key_Space, mod=ttk.TTkK.NoModifier), + ] - # Sorting with no columns should handle gracefully - try: - widget.sortByColumn(0, ttk.TTkK.SortOrder.AscendingOrder) # Invalid column - except (IndexError, ValueError): - pass # Expected for invalid column index + for event in key_events: + # Should handle keyboard navigation + result = widget.keyEvent(event) + # Result depends on implementation, but should not crash - def test_empty_model_signals(self): - """Test that signals work correctly with empty models""" - empty_model = ttk.TTkTableModelList(data=[], header=[]) - widget = ttk.TTkTableWidget(tableModel=empty_model) + def test_signal_emissions(self): + """Test signal emissions""" + widget = ttk.TTkTableWidget(tableModel=self.table_model) # Connect signals to mock slots cell_changed_calls = [] cell_clicked_calls = [] + cell_entered_calls = [] + current_cell_changed_calls = [] @ttk.pyTTkSlot(int, int) def mock_cell_changed(row, col): @@ -643,335 +473,183 @@ class TestTTkTableWidget: def mock_cell_clicked(row, col): cell_clicked_calls.append((row, col)) - widget.cellChanged.connect(mock_cell_changed) - widget.cellClicked.connect(mock_cell_clicked) - - # Signals should be connected even with empty model - assert len(widget.cellChanged._connected_slots) > 0 - assert len(widget.cellClicked._connected_slots) > 0 - - # No signals should be emitted for empty model operations - widget.clearSelection() - widget.selectAll() - - # Should have no signal calls since there are no cells - assert len(cell_changed_calls) == 0 - assert len(cell_clicked_calls) == 0 - - def test_mouse_press_normal_cell(self): - """Test mouse press on a normal cell""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) - - # Mock the mouse event for clicking on a cell - # Assuming cell (1,1) would be around position (15, 2) accounting for headers - mock_event = Mock() - mock_event.x = 15 - mock_event.y = 2 - mock_event.mod = ttk.TTkK.NoModifier - - # Mock the internal methods that would be called - with patch.object(widget, '_findCell', return_value=(1, 1)): - with patch.object(widget, '_setCurrentCell') as mock_set_current: - with patch.object(widget, 'setSelection') as mock_set_selection: - # Test cell click - result = widget.mousePressEvent(mock_event) - - # Verify the event was handled - assert result is True - - # Verify current cell was set - mock_set_current.assert_called_once_with(1, 1) - - # Verify selection was made - mock_set_selection.assert_called_once() - - def test_mouse_press_header_row_selection(self): - """Test mouse press on row header for row selection""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) - - # Mock mouse event for clicking on row header (col = -1) - mock_event = Mock() - mock_event.x = 2 # In header area - mock_event.y = 3 # Row 1 - mock_event.mod = ttk.TTkK.NoModifier - - with patch.object(widget, '_findCell', return_value=(1, -1)): - with patch.object(widget, 'clearSelection') as mock_clear: - with patch.object(widget, 'selectRow') as mock_select_row: - result = widget.mousePressEvent(mock_event) - - assert result is True - mock_clear.assert_called_once() - mock_select_row.assert_called_once_with(1) - - def test_mouse_press_header_column_selection(self): - """Test mouse press on column header for column selection""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) - - # Mock mouse event for clicking on column header (row = -1) - mock_event = Mock() - mock_event.x = 15 # Column 1 - mock_event.y = 0 # In header area - mock_event.mod = ttk.TTkK.NoModifier - - with patch.object(widget, '_findCell', return_value=(-1, 1)): - with patch.object(widget, 'clearSelection') as mock_clear: - with patch.object(widget, 'selectColumn') as mock_select_col: - result = widget.mousePressEvent(mock_event) - - assert result is True - mock_clear.assert_called_once() - mock_select_col.assert_called_once_with(1) - - def test_mouse_press_corner_select_all(self): - """Test mouse press on corner (both row and col = -1) for select all""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) - - # Mock mouse event for clicking on corner - mock_event = Mock() - mock_event.x = 2 - mock_event.y = 0 - mock_event.mod = ttk.TTkK.NoModifier - - with patch.object(widget, '_findCell', return_value=(-1, -1)): - with patch.object(widget, 'selectAll') as mock_select_all: - result = widget.mousePressEvent(mock_event) - - assert result is True - mock_select_all.assert_called_once() - - def test_mouse_press_with_ctrl_modifier(self): - """Test mouse press with Ctrl modifier for multi-selection""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) - - # Mock mouse event with Ctrl modifier - mock_event = Mock() - mock_event.x = 15 - mock_event.y = 2 - mock_event.mod = ttk.TTkK.ControlModifier - - with patch.object(widget, '_findCell', return_value=(1, 1)): - with patch.object(widget, '_setCurrentCell') as mock_set_current: - with patch.object(widget, 'setSelection') as mock_set_selection: - # Pre-select the cell to test deselection with Ctrl - widget._selected = [[False, False, False] for _ in range(3)] - widget._selected[1][1] = True - - result = widget.mousePressEvent(mock_event) - - assert result is True - mock_set_current.assert_called_once_with(1, 1) - - def test_mouse_press_empty_table_zero_rows(self): - """Test mouse press on table with zero rows""" - empty_model = ttk.TTkTableModelList(data=[], header=['A', 'B']) - widget = ttk.TTkTableWidget(tableModel=empty_model) - - # Mock mouse event - mock_event = Mock() - mock_event.x = 10 - mock_event.y = 5 - mock_event.mod = ttk.TTkK.NoModifier - - with patch.object(widget, '_findCell', return_value=(0, 0)): - # Should handle gracefully even with empty table - result = widget.mousePressEvent(mock_event) - - # Should still return True (handled) but not crash - assert result is True - - def test_mouse_press_empty_table_zero_columns(self): - """Test mouse press on table with zero columns""" - empty_model = ttk.TTkTableModelList(data=[[], []], header=[]) - widget = ttk.TTkTableWidget(tableModel=empty_model) - - # Mock mouse event - mock_event = Mock() - mock_event.x = 10 - mock_event.y = 5 - mock_event.mod = ttk.TTkK.NoModifier - - with patch.object(widget, '_findCell', return_value=(0, 0)): - # Should handle gracefully even with empty columns - result = widget.mousePressEvent(mock_event) + @ttk.pyTTkSlot(int, int) + def mock_cell_entered(row, col): + cell_entered_calls.append((row, col)) - # Should still return True (handled) but not crash - assert result is True + @ttk.pyTTkSlot(int, int, int, int) + def mock_current_cell_changed(curr_row, curr_col, prev_row, prev_col): + current_cell_changed_calls.append((curr_row, curr_col, prev_row, prev_col)) - def test_mouse_press_completely_empty_table(self): - """Test mouse press on completely empty table""" + widget.cellChanged.connect(mock_cell_changed) + widget.cellClicked.connect(mock_cell_clicked) + widget.cellEntered.connect(mock_cell_entered) + widget.currentCellChanged.connect(mock_current_cell_changed) + + # Trigger events that should emit signals + widget._cellChanged.emit(1, 2) + widget._cellClicked.emit(0, 1) + widget._cellEntered.emit(2, 0) + widget._currentCellChanged.emit(1, 1, 0, 0) + + # Verify signals were received + assert len(cell_changed_calls) == 1 + assert cell_changed_calls[0] == (1, 2) + assert len(cell_clicked_calls) == 1 + assert cell_clicked_calls[0] == (0, 1) + assert len(cell_entered_calls) == 1 + assert cell_entered_calls[0] == (2, 0) + assert len(current_cell_changed_calls) == 1 + assert current_cell_changed_calls[0] == (1, 1, 0, 0) + + def test_edge_cases(self): + """Test edge cases and boundary conditions""" + # Empty model empty_model = ttk.TTkTableModelList(data=[], header=[]) widget = ttk.TTkTableWidget(tableModel=empty_model) - # Mock mouse event - mock_event = Mock() - mock_event.x = 10 - mock_event.y = 5 - mock_event.mod = ttk.TTkK.NoModifier - - with patch.object(widget, '_findCell', return_value=(0, 0)): - # Should handle gracefully even with completely empty table - result = widget.mousePressEvent(mock_event) - - # Should still return True (handled) but not crash - assert result is True - - def test_mouse_press_single_cell_table(self): - """Test mouse press on table with single cell""" - single_cell_model = ttk.TTkTableModelList(data=[['OnlyCell']], header=['OnlyCol']) - widget = ttk.TTkTableWidget(tableModel=single_cell_model) - - # Mock mouse event for the single cell - mock_event = Mock() - mock_event.x = 10 - mock_event.y = 2 - mock_event.mod = ttk.TTkK.NoModifier + assert widget.rowCount() == 0 + assert widget.columnCount() == 0 - with patch.object(widget, '_findCell', return_value=(0, 0)): - with patch.object(widget, '_setCurrentCell') as mock_set_current: - result = widget.mousePressEvent(mock_event) + # Operations should handle empty model gracefully + widget.selectAll() + widget.clearSelection() + widget.resizeColumnsToContents() + widget.resizeRowsToContents() + widget.copy() + widget.cut() + widget.paste() - assert result is True - mock_set_current.assert_called_once_with(0, 0) + # Single cell model + single_model = ttk.TTkTableModelList(data=[['Cell']], header=['Col']) + widget.setModel(single_model) - def test_mouse_press_boundary_cells(self): - """Test mouse press on boundary cells (first/last row/column)""" - widget = ttk.TTkTableWidget(tableModel=self.table_model) + assert widget.rowCount() == 1 + assert widget.columnCount() == 1 - test_cases = [ - (0, 0), # Top-left cell - (0, 2), # Top-right cell - (2, 0), # Bottom-left cell - (2, 2), # Bottom-right cell - ] + widget.selectAll() + widget._setCurrentCell(0, 0) - for row, col in test_cases: - mock_event = Mock() - mock_event.x = 10 + col * 10 # Approximate position - mock_event.y = 2 + row * 2 # Approximate position - mock_event.mod = ttk.TTkK.NoModifier + # Large model stress test + large_data = [[f'Cell_{i}_{j}' for j in range(50)] for i in range(50)] + large_model = ttk.TTkTableModelList(data=large_data) + widget.setModel(large_model) - with patch.object(widget, '_findCell', return_value=(row, col)): - with patch.object(widget, '_setCurrentCell') as mock_set_current: - result = widget.mousePressEvent(mock_event) + assert widget.rowCount() == 50 + assert widget.columnCount() == 50 - assert result is True - mock_set_current.assert_called_with(row, col) + # Should handle large models without issues + widget.selectRow(25) + widget.selectColumn(25) - def test_mouse_press_signal_emission(self): - """Test that mouse press emits appropriate signals""" + def test_focus_and_events(self): + """Test focus handling and event processing""" widget = ttk.TTkTableWidget(tableModel=self.table_model) - # Connect signal to mock slot - cell_clicked_calls = [] - - @ttk.pyTTkSlot(int, int) - def mock_cell_clicked(row, col): - cell_clicked_calls.append((row, col)) - - widget.cellClicked.connect(mock_cell_clicked) - - # Mock mouse event - mock_event = Mock() - mock_event.x = 15 - mock_event.y = 2 - mock_event.mod = ttk.TTkK.NoModifier + # Test focus policy + focus_policy = widget.focusPolicy() + assert focus_policy & ttk.TTkK.ClickFocus + assert focus_policy & ttk.TTkK.TabFocus - with patch.object(widget, '_findCell', return_value=(1, 1)): - result = widget.mousePressEvent(mock_event) + # Test focus events + widget.focusOutEvent() # Should not crash - assert result is True - # Verify signal was emitted - assert len(cell_clicked_calls) == 1 - assert cell_clicked_calls[0] == (1, 1) + # Test leave event + leave_event = Mock() + leave_event.x = 100 + leave_event.y = 100 + result = widget.leaveEvent(leave_event) + # Should handle gracefully - def test_mouse_press_invalid_coordinates(self): - """Test mouse press with coordinates outside table bounds""" + def test_internal_state_consistency(self): + """Test internal state consistency""" widget = ttk.TTkTableWidget(tableModel=self.table_model) - # Mock mouse event with coordinates that might be outside bounds - mock_event = Mock() - mock_event.x = 1000 # Very large x - mock_event.y = 1000 # Very large y - mock_event.mod = ttk.TTkK.NoModifier - - # _findCell should handle this gracefully and return valid coordinates - with patch.object(widget, '_findCell', return_value=(2, 2)): # Last valid cell - result = widget.mousePressEvent(mock_event) + # Test that internal positions are consistent + widget._setCurrentCell(1, 2) + assert widget._currentPos == (1, 2) + assert widget.currentRow() == 1 + assert widget.currentColumn() == 2 - # Should not crash and should handle gracefully - assert result is True - - def test_mouse_press_separator_handling(self): - """Test mouse press on separators (if separators are enabled)""" - widget = ttk.TTkTableWidget(tableModel=self.table_model, vSeparator=True, hSeparator=True) - - # Mock mouse event on vertical separator - mock_event = Mock() - mock_event.x = 20 # Position that might be on separator - mock_event.y = 0 # In header area - mock_event.mod = ttk.TTkK.NoModifier - - # Mock the getViewOffsets method - with patch.object(widget, 'getViewOffsets', return_value=(0, 0)): - with patch.object(widget, '_findCell', return_value=(-1, 1)): - # Mock separator detection - widget._colsPos = [10, 20, 30] # Separator at x=20 + # Test drag position initialization + assert widget._dragPos is None + assert widget._hoverPos is None + assert widget._hSeparatorSelected is None + assert widget._vSeparatorSelected is None - result = widget.mousePressEvent(mock_event) + # Test selection proxy consistency + proxy = widget._select_proxy + assert proxy._rows == widget.rowCount() + assert proxy._cols == widget.columnCount() - # Should handle separator selection - assert result is True + def test_style_and_appearance(self): + """Test style properties""" + widget = ttk.TTkTableWidget() - def test_mouse_press_state_changes(self): - """Test that mouse press updates internal state correctly""" + # Test class style exists + assert hasattr(ttk.TTkTableWidget, 'classStyle') + assert isinstance(ttk.TTkTableWidget.classStyle, dict) + assert 'default' in ttk.TTkTableWidget.classStyle + assert 'disabled' in ttk.TTkTableWidget.classStyle + + # Test style properties contain expected keys + default_style = ttk.TTkTableWidget.classStyle['default'] + expected_keys = ['color', 'lineColor', 'headerColor', 'hoverColor', + 'currentColor', 'selectedColor', 'separatorColor'] + for key in expected_keys: + assert key in default_style + assert isinstance(default_style[key], ttk.TTkColor) + + def test_paste_event_handling(self): + """Test paste event with different data types""" widget = ttk.TTkTableWidget(tableModel=self.table_model) + widget._setCurrentCell(0, 0) - # Ensure initial state - assert widget._currentPos is None - assert widget._hoverPos is None - assert widget._dragPos is None - - # Mock mouse event - mock_event = Mock() - mock_event.x = 15 - mock_event.y = 2 - mock_event.mod = ttk.TTkK.NoModifier + # Test paste with TTkString + text_data = ttk.TTkString("Hello\tWorld\nFoo\tBar") + result = widget.pasteEvent(text_data) + assert result == True - with patch.object(widget, '_findCell', return_value=(1, 1)): - widget.mousePressEvent(mock_event) + # Test paste with _ClipboardTable + clipboard_data = [ + [(0, 0, 'A1'), (0, 1, 'B1')], + [(1, 0, 'A2'), (1, 1, 'B2')] + ] + clipboard_table = _ClipboardTable(clipboard_data) + result = widget.pasteEvent(clipboard_table) + assert result == True - # Verify state changes - assert widget._currentPos == (1, 1) - assert widget._hoverPos is None # Should be reset - assert widget._dragPos == [(1, 1), (1, 1)] # Should be initialized + # Test paste with unsupported data + result = widget.pasteEvent("regular string") + assert result == True # Should handle gracefully - def test_mouse_press_with_disabled_model_flags(self): - """Test mouse press on cells with different model flags""" + def test_model_flags_integration(self): + """Test integration with model flags""" widget = ttk.TTkTableWidget(tableModel=self.table_model) - # Mock model flags to return non-selectable for certain cells - original_flags = widget._tableModel.flags - + # Mock model flags def mock_flags(row, col): if row == 1 and col == 1: return ttk.TTkK.ItemFlag.NoItemFlags # Not selectable - return original_flags(row, col) + return ttk.TTkK.ItemFlag.ItemIsSelectable | ttk.TTkK.ItemFlag.ItemIsEnabled + original_flags = widget._tableModel.flags widget._tableModel.flags = mock_flags try: - mock_event = Mock() - mock_event.x = 15 - mock_event.y = 2 - mock_event.mod = ttk.TTkK.NoModifier - - with patch.object(widget, '_findCell', return_value=(1, 1)): - result = widget.mousePressEvent(mock_event) - - # Should still handle the event - assert result is True + # Test selection respects flags + widget._select_proxy.updateModel( + cols=widget.columnCount(), + rows=widget.rowCount(), + flags=widget._tableModel.flags + ) + + widget.selectAll() + # Cell (1,1) should not be selected due to flags + assert not widget._select_proxy.isCellSelected(1, 1) + # Other cells should be selected + assert widget._select_proxy.isCellSelected(0, 0) + assert widget._select_proxy.isCellSelected(2, 2) finally: - # Restore original flags widget._tableModel.flags = original_flags diff --git a/tests/pytest/modelView/test_tablewidget_selectionproxy.py b/tests/pytest/modelView/test_tablewidget_selectionproxy.py new file mode 100644 index 00000000..bc3eed8b --- /dev/null +++ b/tests/pytest/modelView/test_tablewidget_selectionproxy.py @@ -0,0 +1,563 @@ +#!/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. + +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) \ No newline at end of file diff --git a/tests/weakref/test.05.TermTk.02.py b/tests/weakref/test.05.TermTk.02.py index 647c211a..65a215dd 100755 --- a/tests/weakref/test.05.TermTk.02.py +++ b/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}")