Source code for TermTk.TTkWidgets.TTkModelView.tablewidget

# MIT License
#
# Copyright (c) 2024 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from __future__ import annotations

__all__ = ['TTkTableWidget','TTkHeaderView']

from typing import Optional, List, Tuple, Callable, Iterator, Any, Protocol
from dataclasses import dataclass

from TermTk.TTkCore.helper import TTkHelper
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.string import TTkString
from TermTk.TTkCore.color import TTkColor
from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot
from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent
from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent

from TermTk.TTkGui.clipboard import TTkClipboard
from TermTk.TTkGui.textcursor import TTkTextCursor

from TermTk.TTkWidgets.texedit  import TTkTextEdit
from TermTk.TTkWidgets.spinbox  import TTkSpinBox
from TermTk.TTkWidgets.TTkPickers.textpicker import TTkTextPicker
from TermTk.TTkWidgets.TTkModelView.tablemodellist import TTkTableModelList, TTkModelIndex
from TermTk.TTkWidgets.TTkModelView.table_edit_proxy import TTkTableProxyEdit, TTkTableProxyEditWidget, TTkTableEditLeaving

from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView
from TermTk.TTkAbstract.abstracttablemodel import TTkAbstractTableModel

[docs] class TTkHeaderView(): '''TTkHeaderView This is a placeholder for a proper "TTkHeaderView" ''' __slots__ = ('_visible','visibilityUpdated') def __init__(self, visible=True) -> None: self.visibilityUpdated = pyTTkSignal(bool) self._visible = visible
[docs] @pyTTkSlot(bool) def setVisible(self, visible: bool) -> None: '''setVisible''' if self._visible == visible: return self._visible = visible self.visibilityUpdated.emit(visible)
[docs] @pyTTkSlot() def show(self) -> None: '''show''' self.setVisible(True)
[docs] @pyTTkSlot() def hide(self) -> None: '''hide''' self.setVisible(False)
[docs] def isVisible(self) -> bool: return self._visible
_ClipboardTableData = List[List[Tuple[int,int,Any]]] class _ClipboardTable(TTkString): __slots__=('_data') _data:_ClipboardTableData def __init__(self,data:_ClipboardTableData) -> None: self._data = data super().__init__(self._toTTkString()) def data(self) -> _ClipboardTableData: return self._data def _toTTkString(self) -> TTkString: if not self._data: return TTkString() def _lineHeight(_line): return max(len(str(_item[2]).split('\n')) for _item in _line) ret = [] minx,maxx = min(_a:=[_item[1] for _line in self._data for _item in _line]),max(_a) # miny,maxy = min(_a:=[x[0][0] for x in self._data]),max(_a) cols = maxx-minx+1 colSizes=[0]*cols for line in self._data: height = _lineHeight(line) baseStr = TTkString() retLines = [[baseStr]*cols for _ in range(height)] for c,item in enumerate(line): row,col,data = item for r,txt in enumerate(TTkString(data).split('\n')): 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]) 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] @dataclass class _ProxyWidgetLocation: __slots__ = ('widget', 'row', 'col') widget: TTkTableProxyEditWidget row: int col: int
[docs] class TTkTableWidget(TTkAbstractScrollView): ''' A :py:class:`TTkTableWidget` implements a table view that displays items from a model. :: Customer Id ╿First Name ╿Last Name ╿Company ╿City ╿ 1 │DD37Cf93aecA6Dc │Sheryl │Baxter │Rasmussen Group │East Leonard │ ╾╌╌┼────────────────┼───────────┼────────────┼────────────────────────────────┼────────────────────┤ 2 │1Ef7b82A4CAAD10 │Preston │Lozano │Vega-Gentry │East Jimmychester │ ╾╌╌┼────────────────┼───────────┼────────────┼────────────────────────────────┼────────────────────┤ 3 │6F94879bDAfE5a6 │Roy │Berry │Murillo-Perry │Isabelborough │ ╾╌╌┼────────────────┼───────────┼────────────┼────────────────────────────────┼────────────────────┤ 4 │5Cef8BFA16c5e3c │Linda │Olsen │Dominguez, Mcmillan and Donovan │Bensonview │ ╾╌╌┼────────────────┼───────────┼────────────┼────────────────────────────────┼────────────────────┤ 5 │053d585Ab6b3159 │Joanna │Bender │Martin, Lang and Andrade │West Priscilla │ ╾╌╌┼────────────────┼───────────┼────────────┼────────────────────────────────┼────────────────────┤ 6 │2d08FB17EE273F4 │Aimee │Downs │Steele Group │Chavezborough │ ╾╌╌┴────────────────┴───────────┴────────────┴────────────────────────────────┴────────────────────┘ The :py:class:`TTkTableWidget` class is one of the Model/View Classes and is part of TermTk's model/view framework. :py:class:`TTkTableWidget` implements the methods to allow it to display data provided by models derived from the :py:class:`TTkAbstractTableModel` class. **Navigation** You can navigate the cells in the table by clicking on a cell with the mouse, or by using the arrow keys, you can also hit Tab and Backtab to move from cell to cell. **Visual Appearance** The table has a vertical header that can be obtained using the :meth:`verticalHeader` function, and a horizontal header that is available through the :meth:`horizontalHeader` function. The height of each row in the table can be set using :meth:`setRowHeight`; similarly, the width of columns can be set using :meth:`setColumnWidth`. They can be selected with :meth:`selectRow` and :meth:`selectColumn`. The table will show a grid depending on the :meth:`setHSeparatorVisibility` :meth:`setVSeparatorVisibility` methods. The items shown in a table view, like those in the other item views, are rendered and edited using standard delegates. However, for some tasks it is sometimes useful to be able to insert widgets in a table instead. Widgets are set for particular indexes with the setIndexWidget() function, and later retrieved with indexWidget(). By default, the cells in a table do not expand to fill the available space. You can make the cells fill the available space by stretching the last header section. To distribute the available space according to the space requirement of each column or row, call the view's :meth:`resizeColumnsToContents` or :meth:`resizeRowsToContents` functions. ''' @property def cellChanged(self) -> pyTTkSignal: ''' This signal is emitted whenever the data of the item in the cell specified by row and column has changed. :param row: the row :type row: int :param col: the column :type col: int ''' return self._cellChanged @property def cellClicked(self) -> pyTTkSignal: ''' This signal is emitted whenever a cell in the table is clicked. The row and column specified is the cell that was clicked. :param row: the row :type row: int :param col: the column :type col: int ''' return self._cellClicked @property def cellDoubleClicked(self) -> pyTTkSignal: ''' This signal is emitted whenever a cell in the table is double clicked. The row and column specified is the cell that was double clicked. :param row: the row :type row: int :param col: the column :type col: int ''' return self._cellDoubleClicked @property def cellEntered(self) -> pyTTkSignal: ''' This signal is emitted when the mouse cursor enters a cell. The cell is specified by row and column. :param row: the row :type row: int :param col: the column :type col: int ''' return self._cellEntered # self.cellPressed = pyTTkSignal(int,int) @property def currentCellChanged(self) -> pyTTkSignal: ''' This signal is emitted whenever the current cell changes. The cell specified by **prevRow** and **prevCol** is the cell that previously had the focus, the cell specified by **currRow** and **currCol** is the new current cell. :param currRow: the current row :type currRow: int :param currColumn: the current column :type currColumn: int :param prevRow: the previous row :type prevRow: int :param prevCol: the previous column :type prevCol: int ''' return self._currentCellChanged classStyle = { 'default': { 'color': TTkColor.RST, 'lineColor': TTkColor.fg("#444444"), 'headerColor': TTkColor.fg("#FFFFFF")+TTkColor.bg("#444444")+TTkColor.BOLD, 'hoverColor': TTkColor.fg("#FFFF00")+TTkColor.bg("#0088AA")+TTkColor.BOLD, 'currentColor': TTkColor.fg("#FFFF00")+TTkColor.bg("#0088FF")+TTkColor.BOLD, 'selectedColor': TTkColor.bg("#0066AA"), 'separatorColor': TTkColor.fg("#555555")+TTkColor.bg("#444444")}, 'disabled': { 'color': TTkColor.fg("#888888"), 'lineColor': TTkColor.fg("#888888"), 'headerColor': TTkColor.fg("#888888"), 'hoverColor': TTkColor.bg("#888888"), 'currentColor': TTkColor.bg("#888888"), 'selectedColor': TTkColor.fg("#888888"), 'separatorColor': TTkColor.fg("#888888")}, } '''default style''' __slots__ = ( '_tableModel', '_clipboard', '_vHeaderSize', '_hHeaderSize', '_showVSeparators', '_showHSeparators', '_verticalHeader', '_horizontallHeader', '_colsPos', '_rowsPos', '_sortingEnabled', '_dataPadding', '_select_proxy', '_hSeparatorSelected', '_vSeparatorSelected', '_hoverPos', '_dragPos', '_currentPos', '_sortColumn', '_sortOrder', '_fastCheck', '_guessDataEdit', '_snapshot', '_snapshotId', '_edit_proxy', '_edit_proxy_widget', # Signals # '_cellActivated', '_cellChanged', '_cellClicked', '_cellDoubleClicked', '_cellEntered', # '_cellPressed', '_currentCellChanged', ) _select_proxy:_SelectionProxy _edit_proxy:TTkTableProxyEdit _edit_proxy_widget:Optional[_ProxyWidgetLocation] _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, tableEditProxy:Optional[TTkTableProxyEdit]=None, vSeparator:bool=True, hSeparator:bool=True, vHeader:bool=True, hHeader:bool=True, sortingEnabled=False, dataPadding=1, **kwargs) -> None: ''' :param tableModel: the model for the view to present. :type tableModel: :py:class:`TTkAbstractTableModel` :param vSeparator: show the vertical separators, defaults to True :type vSeparator: bool, optional :param hSeparator: show the horizontal separators, defaults to True :type hSeparator: bool, optional :param vHeader: show the vertical header, defaults to True :type vHeader: bool, optional :param hHeader: show the horizontal header, defaults to True :type hHeader: bool, optional :param sortingEnabled: enable the column sorting, defaults to False :type sortingEnabled: bool, optional :param dataPadding: the right column padding, defaults to 1 :type dataPadding: int, optional ''' # Signals # self.itemActivated = pyTTkSignal(TTkTableWidgetItem, int) # self.itemChanged = pyTTkSignal(TTkTableWidgetItem, int) # self.itemClicked = pyTTkSignal(TTkTableWidgetItem, int) # self.itemDoubleClicked = pyTTkSignal(TTkTableWidgetItem, int) # self.itemExpanded = pyTTkSignal(TTkTableWidgetItem) # self.itemCollapsed = pyTTkSignal(TTkTableWidgetItem) # self._cellActivated = pyTTkSignal(int,int) self._cellChanged = pyTTkSignal(int,int) self._cellClicked = pyTTkSignal(int,int) self._cellDoubleClicked = pyTTkSignal(int,int) self._cellEntered = pyTTkSignal(int,int) # self._cellPressed = pyTTkSignal(int,int) self._currentCellChanged = pyTTkSignal(int,int,int,int) # self._currentItemChanged(QTableWidgetItem *current, QTableWidgetItem *previous) self._fastCheck = True self._guessDataEdit = True self._clipboard = TTkClipboard() self._snapshot = [] self._snapshotId = 0 self._dataPadding = dataPadding self._sortingEnabled = sortingEnabled self._showHSeparators = hSeparator self._showVSeparators = vSeparator self._verticalHeader = TTkHeaderView(visible=vHeader) self._horizontallHeader = TTkHeaderView(visible=hHeader) self._select_proxy = _SelectionProxy() self._edit_proxy = tableEditProxy if tableEditProxy else TTkTableProxyEdit() self._edit_proxy_widget = None self._hoverPos = None self._dragPos = None self._currentPos = None self._hSeparatorSelected = None self._vSeparatorSelected = None self._sortColumn = -1 self._sortOrder = TTkK.AscendingOrder self._tableModel = tableModel if tableModel else TTkTableModelList(data=[['']*10 for _ in range(10)]) self._tableModel.dataChanged.connect(self.update) self._tableModel.modelChanged.connect(self._refreshLayout) super().__init__(**kwargs) self._refreshLayout() self.setMinimumHeight(1) 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) 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() self.clearSelection() 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].pos self._setCurrentCell(cpsi.row(),cpsi.col()) self._moveCurrentCell(diff=(0,0)) self.update()
[docs] @pyTTkSlot() def undo(self) -> None: ''' Undoes the last operation if undo is available. ''' if self._snapshotId == 0: return self._snapshotId-=1 self._restoreSnapshot(self._snapshotId, newData=False)
[docs] @pyTTkSlot() def redo(self) -> None: ''' Redoes the last operation if redo is available. ''' if self._snapshotId >= len(self._snapshot): return self._restoreSnapshot(self._snapshotId, newData=True) self._snapshotId+=1
[docs] def isUndoAvailable(self) -> bool: ''' isUndoAvailable :return: bool ''' return self._snapshotId > 0
[docs] def isRedoAvailable(self) -> bool: ''' isRedoAvailable :return: bool ''' return self._snapshotId < len(self._snapshot)
[docs] @pyTTkSlot() def copy(self) -> None: ''' Copies any selected cells to the clipboard. ''' 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 = [(row,col) for row,col in self._select_proxy.iterateSelected()] mods = [] for _row,_col in selected: mods.append((_row,_col,'')) self._tableModel_setData(mods) self.update()
[docs] @pyTTkSlot() def cut(self) -> None: ''' Copies the selected ccells to the clipboard and deletes them from the table. ''' self.copy() self._cleanSelectedContent()
[docs] @pyTTkSlot() def paste(self) -> None: ''' Pastes the text/cells from the clipboard into the table at the current cursor position. ''' data = self._clipboard.text() self.pasteEvent(data)
def pasteEvent(self, data:object) -> bool: row,col = self._currentPos if self._currentPos else (0,0) rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() if not rows or not cols: return True if isinstance(data,_ClipboardTable) and data.data(): dataList = [] linearData = [_item for _line in data.data() for _item in _line] minx,maxx = min(_a:=[_item[1] for _item in linearData]),max(_a) miny,maxy = min(_a:=[_item[0] for _item in linearData]),max(_a) for _dl in data.data(): for item in _dl: _r,_c,_d = item _r+=row-miny _c+=col-minx if _r<rows and _c<cols: dataList.append((_r,_c,_d)) if dataList: self._tableModel_setData(dataList) elif isinstance(data,TTkString): self._tableModel_setData([(row,col,data)]) else: self._tableModel_setData([(row,col,str(data))]) self.update() return True 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: oldData = self._tableModel.data(row=row,col=col) dataIndex = self._tableModel.index(row=row,col=col) if newData == oldData: continue self.cellChanged.emit(row,col) snaps.append(_SnapItem( dataIndex=dataIndex, oldData=oldData, newData=newData)) self._tableModel.setData(row=row,col=col,data=newData) if snaps: row,col = self._currentPos if self._currentPos else (0,0) self._saveSnapshot(snaps,self._tableModel.index(row=row,col=col))
[docs] @pyTTkSlot(bool) def setSortingEnabled(self, enable:bool) -> None: ''' If enable is true, enables sorting for the table and immediately trigger a call to :meth:`sortByColumn` with the current sort section and order **Note**: Setter function for property sortingEnabled. :param enable: the availability of undo :type enable: bool ''' if enable == self._sortingEnabled: return self._sortingEnabled = enable self.sortByColumn(self._sortColumn, self._sortOrder)
[docs] def isSortingEnabled(self) -> bool: ''' This property holds whether sorting is enabled If this property is true, sorting is enabled for the table. If this property is false, sorting is not enabled. The default value is false. **Note**: . Setting the property to true with :meth:`setSortingEnabled` immediately triggers a call to :meth:`sortByColumn` with the current sort section and order. :return: bool ''' return self._sortingEnabled
[docs] @pyTTkSlot(int, TTkK.SortOrder) def sortByColumn(self, column:int, order:TTkK.SortOrder) -> None: ''' Sorts the model by the values in the given column and order. column may be -1, in which case no sort indicator will be shown and the model will return to its natural, unsorted order. Note that not all models support this and may even crash in this case. :param column: the column used for the sorting, -1 to keep the table unsorted :type column: bool :param order: the sort order :type order: :py:class:`TTkK.SortOrder` ''' self._sortColumn = column self._sortOrder = order self._tableModel.sort(column,order) self.update()
@pyTTkSlot() def _headerVisibilityChanged(self): showVH = self._verticalHeader.isVisible() showHH = self._horizontallHeader.isVisible() vhs = self._vHeaderSize if showVH else 0 hhs = self._hHeaderSize if showHH else 0 self.setPadding(hhs,0,vhs,0) self.viewChanged.emit() @pyTTkSlot() def _refreshLayout(self): self._hoverPos = None self._dragPos = None self._currentPos = None self._hSeparatorSelected = None self._vSeparatorSelected = None self._sortColumn = -1 self._sortOrder = TTkK.AscendingOrder self._snapshot = [] 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) if self._showVSeparators: self._colsPos = [(1+x)*11 for x in range(cols)] else: self._colsPos = [(1+x)*10 for x in range(cols)] if self._showHSeparators: 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() # Overridden function def viewFullAreaSize(self) -> tuple[int, int]: showVH = self._verticalHeader.isVisible() showHH = self._horizontallHeader.isVisible() hhs = self._hHeaderSize if showHH else 0 vhs = self._vHeaderSize if showVH else 0 w = vhs+(self._colsPos[-1] if self._colsPos else 0)+1 h = hhs+(self._rowsPos[-1] if self._rowsPos else 0)+1 return w,h
[docs] def clearSelection(self) -> None: ''' Deselects all selected items. The current index will not be changed. ''' self._removeProxyWidget() self._select_proxy.clear() self.update()
[docs] def selectAll(self) -> None: ''' Selects all items in the view. This function will use the selection behavior set on the view when selecting. ''' self._removeProxyWidget() self._select_proxy.selectAll() self.update()
[docs] def setSelection(self, pos:tuple[int,int], size:tuple[int,int], flags:TTkK.TTkItemSelectionModel) -> None: ''' Selects the items within the given rect and in accordance with the specified selection flags. :param pos: the x,y position of the rect :type pos: tuple[int,int] :param size: the width,height of the rect used for the selection :type size: tuple[int,int] :param flags: the selection model used (i.e. :py:class:`TTkItemSelectionModel.Select`) :type flags: :py:class:`TTkItemSelectionModel` ''' self._removeProxyWidget() self._select_proxy.setSelection(pos=pos, size=size, flags=flags) self.update()
[docs] def selectRow(self, row:int) -> None: ''' Selects the given row in the table view :param row: the row to be selected :type row: int ''' self._removeProxyWidget() self._select_proxy.selectRow(row=row) self.update()
[docs] def selectColumn(self, col:int) -> None: ''' Selects the given column in the table view :param col: the column to be selected :type col: int ''' self._removeProxyWidget() self._select_proxy.selectColumn(col=col) self.update()
[docs] def unselectRow(self, row:int) -> None: ''' Unselects the given row in the table view :param row: the row to be unselected :type row: int ''' self._removeProxyWidget() self._select_proxy.unselectRow(row=row) self.update()
[docs] def unselectColumn(self, col:int) -> None: ''' Unselects the given column in the table view :param col: the column to be unselected :type col: int ''' self._removeProxyWidget() self._select_proxy.unselectColumn(col=col) self.update()
@pyTTkSlot() def _viewChangedHandler(self) -> None: x,y = self.getViewOffsets() self.layout().setOffset(-x,-y) self.update()
[docs] def rowCount(self) -> int: ''' Returns the number of rows. :return: int ''' return self._tableModel.rowCount()
[docs] def currentRow(self) -> int: ''' Returns the row of the current item. :return: int ''' if _cp := self._currentPos: return _cp[0] return 0
[docs] def columnCount(self) -> int: ''' Returns the number of columns. :return: int ''' return self._tableModel.columnCount()
[docs] def currentColumn(self) -> int: ''' Returns the column of the current item. :return: int ''' if _cp := self._currentPos: return _cp[1] return 0
[docs] def verticalHeader(self) -> TTkHeaderView: ''' Returns the table view's vertical header. :return: :py:class:`TTkHeaderView` ''' return self._verticalHeader
[docs] def horizontalHeader(self) -> TTkHeaderView: ''' Returns the table view's horizontal header. :return: :py:class:`TTkHeaderView` ''' return self._horizontallHeader
[docs] def hSeparatorVisibility(self) -> bool: ''' Returns the visibility status of the horizontal separator :return: bool ''' return self._showHSeparators
[docs] def vSeparatorVisibility(self) -> bool: ''' Returns the visibility status of the vertical separator :return: bool ''' return self._showVSeparators
[docs] def setHSeparatorVisibility(self, visibility:bool) -> None: ''' Set the the visibility of the horizontal separators (lines) :: Customer Id First Name Last Name Company 1 │ DD37Cf93aecA6Dc Sheryl Baxter Rasmussen Group ╾╌╌┼─────────────────────────────────────────────────────────── 2 │ 1Ef7b82A4CAAD10 Preston Lozano Vega-Gentry ╾╌╌┼─────────────────────────────────────────────────────────── 3 │ 6F94879bDAfE5a6 Roy Berry Murillo-Perry ╾╌╌┼─────────────────────────────────────────────────────────── :param visibility: the visibility status :type visibility: bool ''' if self._showHSeparators == visibility: return self._showHSeparators = visibility if visibility: self._rowsPos = [v+i for i,v in enumerate(self._rowsPos,1)] else: self._rowsPos = [v-i for i,v in enumerate(self._rowsPos,1)] self.viewChanged.emit()
[docs] def setVSeparatorVisibility(self, visibility:bool): ''' Set the the visibility of the vertical separators (lines) :: Customer Id ╿First Name ╿Last Name ╿Company ╿ 1 │ DD37Cf93aecA6Dc │Sheryl │Baxter │Rasmussen Group │ 2 │ 1Ef7b82A4CAAD10 │Preston │Lozano │Vega-Gentry │ 3 │ 6F94879bDAfE5a6 │Roy │Berry │Murillo-Perry │ 4 │ 5Cef8BFA16c5e3c │Linda │Olsen │Dominguez, Mcmillan and Don │ 5 │ 053d585Ab6b3159 │Joanna │Bender │Martin, Lang and Andrade │ 6 │ 2d08FB17EE273F4 │Aimee │Downs │Steele Group │ :param visibility: the visibility status :type visibility: bool ''' if self._showVSeparators == visibility: return self._showVSeparators = visibility if visibility: self._colsPos = [v+i for i,v in enumerate(self._colsPos,1)] else: self._colsPos = [v-i for i,v in enumerate(self._colsPos,1)] self.viewChanged.emit()
[docs] def model(self) -> TTkAbstractTableModel: ''' Returns the model that this view is presenting. :return: :py:class:`TTkAbstractTableModel` ''' return self._tableModel
[docs] def setModel(self, model:TTkAbstractTableModel) -> None: ''' Sets the model for the view to present. :param model: :type model: :py:class:`TTkAbstractTableModel` ''' self._tableModel.modelChanged.disconnect(self._refreshLayout) self._tableModel.dataChanged.disconnect(self.update) self._tableModel = model self._tableModel.dataChanged.connect(self.update) self._tableModel.modelChanged.connect(self._refreshLayout) self._refreshLayout()
def focusOutEvent(self) -> None: self._hSeparatorSelected = None self._vSeparatorSelected = None def leaveEvent(self, evt:TTkMouseEvent) -> bool: self._hoverPos = None self.update() return super().leaveEvent(evt)
[docs] @pyTTkSlot(int,int) def setColumnWidth(self, column:int, width: int) -> None: ''' Sets the width of the given column. :param column: the column :type column: int :param width: its width :type width: int ''' if column < 0 or column >= len(self._colsPos): return i = column prevPos = self._colsPos[i-1] if i>0 else -1 if self._showVSeparators: newPos = prevPos + width + 1 else: newPos = prevPos + width oldPos = self._colsPos[i] diff = newPos-oldPos for ii in range(i,len(self._colsPos)): self._colsPos[ii] += diff self.viewChanged.emit() self.update()
def _columnContentsSize(self, column:int) -> int: def _wid(_c): txt = self._tableModel.ttkStringData(_c, column) return max(t.termWidth() for t in txt.split('\n')) rows = self._tableModel.rowCount() if self._fastCheck: w,h = self.size() row,_ = self._findCell(w//2, h//2, False) rowa,rowb = max(0,row-100), min(row+100,rows) else: rowa,rowb = 0,rows return max(_wid(i) for i in range(rowa,rowb))+self._dataPadding
[docs] @pyTTkSlot(int) def resizeColumnToContents(self, column:int) -> None: ''' Resizes the given column based on the size hints of the delegate used to render each item in the column. :param column: the column to be resized :type column: int ''' self.setColumnWidth(column, self._columnContentsSize(column))
[docs] @pyTTkSlot() def resizeColumnsToContents(self) -> None: ''' Resizes all columns based on the size hints of the delegate used to render each item in the columns. ''' _d = 1 if self._showVSeparators else 0 cols = self._tableModel.columnCount() pos = -1 for _c in range(cols): pos += _d+self._columnContentsSize(_c) self._colsPos[_c] = pos self.viewChanged.emit() self.update()
[docs] @pyTTkSlot(int,int) def setRowHeight(self, row:int, height: int) -> None: ''' Sets the height of the given row. :param row: the row :type row: int :param height: its height :type height: int ''' i = row prevPos = self._rowsPos[i-1] if i>0 else -1 if self._showHSeparators: newPos = prevPos + height + 1 else: newPos = prevPos + height oldPos = self._rowsPos[i] diff = newPos-oldPos for ii in range(i,len(self._rowsPos)): self._rowsPos[ii] += diff self.viewChanged.emit() self.update()
def _rowContentsSize(self, row:int) -> int: def _hei(_c): txt = self._tableModel.ttkStringData(row, _c) return len(txt.split('\n')) cols = self._tableModel.columnCount() if self._fastCheck: w,h = self.size() _,col = self._findCell(w//2, h//2, False) cola,colb = max(0,col-30), min(col+30,cols) else: cola,colb = 0,cols return 0 if cola>=colb else max(_hei(i) for i in range(cola,colb))
[docs] @pyTTkSlot(int) def resizeRowToContents(self, row:int) -> None: ''' Resizes the given row based on the size hints of the delegate used to render each item in the row. :param row: the row to be resized :type row: int ''' self.setRowHeight(row, self._rowContentsSize(row))
[docs] @pyTTkSlot() def resizeRowsToContents(self) -> None: ''' Resizes all rows based on the size hints of the delegate used to render each item in the rows. ''' rows = self._tableModel.rowCount() _d = 1 if self._showHSeparators else 0 pos = -1 for _r in range(rows): pos += _d + self._rowContentsSize(_r) self._rowsPos[_r] = pos self.viewChanged.emit() self.update()
def _findCell(self, x, y, headers) -> Tuple[int,int]: showVH = self._verticalHeader.isVisible() showHH = self._horizontallHeader.isVisible() hhs = self._hHeaderSize if showHH else 0 vhs = self._vHeaderSize if showVH else 0 ox, oy = self.getViewOffsets() rp = self._rowsPos cp = self._colsPos row = 0 col = 0 if headers and y<hhs: row = -1 else: y += oy-hhs for row,py in enumerate(rp): if py>=y: break if headers and x<vhs: col = -1 else: x += ox-vhs for col,px in enumerate(cp): if px>=x: break return row,col def _alignWidgets(self) -> None: if not (epwl:=self._edit_proxy_widget): return epw = epwl.widget row = epwl.row col = epwl.col showHS = self._showHSeparators showVS = self._showVSeparators rp = self._rowsPos cp = self._colsPos xa,xb = 1+cp[col-1] if col>0 else 0, cp[col] + (0 if showVS else 1) ya,yb = 1+rp[row-1] if row>0 else 0, rp[row] + (0 if showHS else 1) epw.setGeometry(xa,ya,xb-xa,yb-ya) def _removeProxyWidget(self, direction:TTkTableEditLeaving=TTkTableEditLeaving.NONE) -> None: if not (epwl:=self._edit_proxy_widget): return epw = epwl.widget row = epwl.row col = epwl.col self._edit_proxy_widget = None self.layout().removeWidget(epw) data = epw.getCellData() self._tableModel_setData([(row,col,data)]) epw.proxyDispose() if direction == TTkTableEditLeaving.TOP: self._moveCurrentCell(diff=(0,-1)) elif direction == TTkTableEditLeaving.BOTTOM: self._moveCurrentCell(diff=(0,+1)) if direction == TTkTableEditLeaving.LEFT: self._moveCurrentCell(diff=(-1,0)) elif direction == TTkTableEditLeaving.RIGHT: self._moveCurrentCell(diff=(+1,0)) TTkHelper.hideCursor() self.setFocus() def _placeProxyWidget(self, proxyWidgetLocation:_ProxyWidgetLocation) -> None: self._removeProxyWidget() self._edit_proxy_widget = proxyWidgetLocation proxyWidget = proxyWidgetLocation.widget self._alignWidgets() self.layout().addWidget(proxyWidget) proxyWidget.leavingTriggered.connect(self._removeProxyWidget) proxyWidget.setFocus() def _editCell(self, row:int, col:int, richEditSupport:bool=True) -> None: if not (self._tableModel.flags(row=row,col=col) & TTkK.ItemFlag.ItemIsEditable): return showHS = self._showHSeparators showVS = self._showVSeparators rp = self._rowsPos cp = self._colsPos xa,xb = 1+cp[col-1] if col>0 else 0, cp[col] + (0 if showVS else 1) ya,yb = 1+rp[row-1] if row>0 else 0, rp[row] + (0 if showHS else 1) # Mark only the current cell as aselected rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() self.clearSelection() self.setSelection(pos=(col,row),size=(1,1),flags=TTkK.TTkItemSelectionModel.Select) data = self._tableModel.data(row, col) if proxyWidget := self._edit_proxy.getProxyWidget(data, rich=richEditSupport): epwl = _ProxyWidgetLocation(widget=proxyWidget, row=row, col=col) self._placeProxyWidget(epwl) def _setCurrentCell(self, currRow:int, currCol:int) -> None: prevRow,prevCol = self._currentPos if self._currentPos else (0,0) self._currentPos = (currRow,currCol) if (currRow,currRow)!=(prevRow,prevCol): self.currentCellChanged.emit(currRow,currCol,prevRow,prevCol) def _moveCurrentCell(self, col=0, row=0, borderStop=True, diff=None): rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() if diff: row,col = self._currentPos if self._currentPos else (0,0) dc,dr = diff row+=dr col+=dc if borderStop: row = max(0,min(row, rows-1)) col = max(0,min(col, cols-1)) else: if col >= cols: col=0 ; row+=1 if col < 0: col=cols-1 ; row-=1 if row >= rows: row=0 if row < 0: row=rows-1 self._setCurrentCell(row,col) # move the offset to include the cell w,h = self.size() ox, oy = self.getViewOffsets() showVH = self._verticalHeader.isVisible() showHH = self._horizontallHeader.isVisible() hhs = self._hHeaderSize if showHH else 0 vhs = self._vHeaderSize if showVH else 0 cxa,cxb = self._colsPos[col-1] if col else 0, self._colsPos[col] cya,cyb = self._rowsPos[row-1] if row else 0, self._rowsPos[row] if w+ox-vhs < cxb: ox=cxb+vhs-w if ox > cxa: ox=cxa if h+oy-hhs < cyb: oy=cyb+hhs-h if oy > cya: oy=cya self.viewMoveTo(ox,oy) self.update() def keyEvent(self, evt:TTkKeyEvent) -> bool: # rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() if self._currentPos: row,col = self._currentPos else: row,col = 0,0 if evt.type == TTkK.SpecialKey: if evt.mod==TTkK.ControlModifier: if evt.key == TTkK.Key_Z: self.undo() elif evt.key == TTkK.Key_Y: self.redo() elif evt.key == TTkK.Key_C: self.copy() elif evt.key == TTkK.Key_V: self.paste() elif evt.key == TTkK.Key_X: self.cut() elif evt.key == TTkK.Key_Tab: # Process Next/Prev if evt.mod == TTkK.NoModifier: self._moveCurrentCell(col=col+1, row=row, borderStop=False) elif evt.mod == TTkK.ShiftModifier: self._moveCurrentCell(col=col-1, row=row, borderStop=False) elif evt.key == TTkK.Key_PageDown: _,h = self.size() rp=self._rowsPos[row] for dy,rh in enumerate(self._rowsPos[row:]): if rh-rp >= h: break self._moveCurrentCell(col=col, row=row+dy, borderStop=True) elif evt.key == TTkK.Key_PageUp: _,h = self.size() rp=self._rowsPos[row] for dy,rh in enumerate(self._rowsPos[row::-1]): if rp-rh >= h: break self._moveCurrentCell(col=col, row=row-dy, borderStop=True) elif evt.key == TTkK.Key_Home: self._moveCurrentCell(col=0, row=row, borderStop=True) elif evt.key == TTkK.Key_End: self._moveCurrentCell(col=cols, row=row, borderStop=True) elif evt.mod==TTkK.NoModifier: if evt.key == TTkK.Key_Up: self._moveCurrentCell(col=col , row=row-1, borderStop=True) elif evt.key == TTkK.Key_Down: self._moveCurrentCell(col=col , row=row+1, borderStop=True) elif evt.key == TTkK.Key_Left: self._moveCurrentCell(col=col-1, row=row , borderStop=True) elif evt.key == TTkK.Key_Right: self._moveCurrentCell(col=col+1, row=row , borderStop=True) elif evt.key == TTkK.Key_Enter: if (self._tableModel.flags(row=row,col=col) & TTkK.ItemFlag.ItemIsEditable): self._editCell(row,col,richEditSupport=False) else: self._moveCurrentCell(col=col , row=row+1, borderStop=False) elif evt.key in (TTkK.Key_Delete, TTkK.Key_Backspace): self._cleanSelectedContent() self.update() return True else: if (self._tableModel.flags(row=row,col=col) & TTkK.ItemFlag.ItemIsEditable): self._tableModel_setData([(row,col,evt.key)]) self._editCell(row,col,richEditSupport=False) return True def mouseDoubleClickEvent(self, evt:TTkMouseEvent) -> bool: x,y = evt.x, evt.y ox, oy = self.getViewOffsets() showHS = self._showHSeparators showVS = self._showVSeparators showVH = self._verticalHeader.isVisible() showHH = self._horizontallHeader.isVisible() hhs = self._hHeaderSize if showHH else 0 vhs = self._vHeaderSize if showVH else 0 self._hSeparatorSelected = None self._vSeparatorSelected = None rp = self._rowsPos cp = self._colsPos # Handle Header Events # And return if handled # This is important to handle the header selection in the next part if showVS and y < hhs: _x = x+ox-vhs for i, c in enumerate(self._colsPos): if _x == c: # I-th separator selected self.resizeColumnToContents(i) return True # return True elif showHS and x < vhs: _y = y+oy-hhs for i, r in enumerate(self._rowsPos): if _y == r: # I-th separator selected # I-th separator selected self.resizeRowToContents(i) return True row,col = self._findCell(x,y, headers=False) self.cellDoubleClicked.emit(row,col) self._editCell(row,col) return True def mouseMoveEvent(self, evt:TTkMouseEvent) -> bool: x,y = evt.x,evt.y ox, oy = self.getViewOffsets() showHS = self._showHSeparators showVS = self._showVSeparators showVH = self._verticalHeader.isVisible() showHH = self._horizontallHeader.isVisible() hhs = self._hHeaderSize if showHH else 0 vhs = self._vHeaderSize if showVH else 0 self._hoverPos = (row,col) = self._findCell(x,y, headers=True) if showVS and row==-1: _x = x+ox-vhs for i, c in enumerate(self._colsPos): if _x == c: # Over the I-th separator self._hoverPos = None self.update() return True if showHS and col==-1: _y = y+oy-hhs for i, r in enumerate(self._rowsPos): if _y == r: # Over the I-th separator self._hoverPos = None self.update() return True if row>=0 and col>>0: self.cellEntered.emit(row,col) self.update() return True def mousePressEvent(self, evt:TTkMouseEvent) -> bool: x,y = evt.x, evt.y ox, oy = self.getViewOffsets() showHS = self._showHSeparators showVS = self._showVSeparators showVH = self._verticalHeader.isVisible() showHH = self._horizontallHeader.isVisible() hhs = self._hHeaderSize if showHH else 0 vhs = self._vHeaderSize if showVH else 0 self._hSeparatorSelected = None self._vSeparatorSelected = None # Handle Header Events # And return if handled # 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: # I-th separator selected 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: order = TTkK.SortOrder.DescendingOrder if self._sortOrder==TTkK.SortOrder.AscendingOrder else TTkK.SortOrder.AscendingOrder else: order = TTkK.SortOrder.AscendingOrder self.sortByColumn(_i,order) return True elif showHS and x < vhs: _y = y+oy-hhs for _i, r in enumerate(self._rowsPos): if _y == r: # I-th separator selected self._vSeparatorSelected = _i self.update() return True row,col = self._findCell(x,y, headers=True) if not row==col==-1: self._dragPos = _DragPosType(fr=(row,col),to=(row,col)) _ctrl = evt.mod==TTkK.ControlModifier if row==col==-1: # Corner Press # Select Everything self.selectAll() elif col==-1: # Row select if not _ctrl: self.clearSelection() if self._select_proxy.isRowSelected(row=row): self.unselectRow(row) else: self.selectRow(row) elif row==-1: # Col select if not _ctrl: self.clearSelection() if self._select_proxy.isColSelected(col=col): self.unselectColumn(col) else: self.selectColumn(col) else: # Cell Select 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._select_proxy.isColSelected(col=col) and _ctrl) else TTkK.TTkItemSelectionModel.Select ) ) self._hoverPos = None self.update() return True def mouseDragEvent(self, evt:TTkMouseEvent) -> bool: # columnPos (Selected = 2) # 0 1 2 3 4 # ----|-------|--------|----------|---| # Mouse (Drag) Pos # ^ # I consider at least 4 char (3+1) as spacing # Min Selected Pos = (Selected+1) * 4 x,y = evt.x, evt.y ox, oy = self.getViewOffsets() showVH = self._verticalHeader.isVisible() showHH = self._horizontallHeader.isVisible() 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.to = self._findCell(x,y, headers=False) self.update() return True if self._hSeparatorSelected is not None: x += ox-vhs ss = self._hSeparatorSelected pos = max((ss+1)*4, x) diff = pos - self._colsPos[ss] # Align the previous Separators if pushed for i in range(ss): self._colsPos[i] = min(self._colsPos[i], pos-(ss-i)*4) # Align all the other Separators relative to the selection for i in range(ss, len(self._colsPos)): self._colsPos[i] += diff self._alignWidgets() self.viewChanged.emit() self.update() return True if self._vSeparatorSelected is not None: y += oy-hhs ss = self._vSeparatorSelected pos = max((ss+1)*2-1, y) diff = pos - self._rowsPos[ss] # Align the previous Separators if pushed for i in range(ss): self._rowsPos[i] = min(self._rowsPos[i], pos-(ss-i)*2) # Align all the other Separators relative to the selection for i in range(ss, len(self._rowsPos)): self._rowsPos[i] += diff self._alignWidgets() self.viewChanged.emit() self.update() return True return False def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool: if self._dragPos: rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() state = True 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._select_proxy.isCellSelected(row=max(0,rowa), col=max(0,cola)) else: # Clear the selection if no ctrl has been pressed self.clearSelection() if rowa == -1: cola,colb=min(cola,colb),max(cola,colb) rowa,rowb=0,rows-1 elif cola == -1: rowa,rowb=min(rowa,rowb),max(rowa,rowb) cola,colb=0,cols-1 else: cola,colb=min(cola,colb),max(cola,colb) rowa,rowb=min(rowa,rowb),max(rowa,rowb) self.setSelection(pos = (cola,rowa), size = (colb-cola+1,rowb-rowa+1), flags = TTkK.TTkItemSelectionModel.Select if state else TTkK.TTkItemSelectionModel.Clear) self._hoverPos = None self._dragPos = None if self._edit_proxy_widget: self._edit_proxy_widget.widget.setFocus() self.update() return True # # -1 X # <-(0,0)->│<-(1,0)->│<-(2,0)->│<-(3,0)->│ # 1 ─────────┼─────────┼─────────┼─────────┼ # <-(0,1)->│<-(1,1)->│<-(2,1)->│<-(3,1)->│ # 3 ─────────┼─────────┼─────────┼─────────┼ # <-(0,2)->│<-(1,2)->│<-(2,2)->│<-(3,2)->│ # 4 ─────────┼─────────┼─────────┼─────────┼ # <-(0,3)->│<-(1,3)->│<-(2,3)->│<-(3,3)->│ h-cell = 5 = 10-(4+1) # │ abc │ │ │ # │ de │ │ │ # │ │ │ │ # │ │ │ │ # 10 ─────────┼─────────┼─────────┼─────────┼ # <-(0,4)->│<-(1,4)->│<-(2,4)->│<-(3,4)->│ # 12 ─────────┼─────────┼─────────┼─────────┼ # <-(0,5)->│<-(1,5)->│<-(2,5)->│<-(3,5)->│ # 14 ─────────┼─────────┼─────────┼─────────┼ # -1 X # 0 <-(0,0)->│<-(1,0)->│<-(2,0)->│<-(3,0)->│ # 1 <-(0,1)->│<-(1,1)->│<-(2,1)->│<-(3,1)->│ # 2 <-(0,2)->│<-(1,2)->│<-(2,2)->│<-(3,2)->│ # 3 <-(0,3)->│<-(1,3)->│<-(2,3)->│<-(3,3)->│ h-cell = 5 = 10-(4+1) # │ abc │ │ │ # │ de │ │ │ # │ │ │ │ # │ │ │ │ # 8 <-(0,4)->│<-(1,4)->│<-(2,4)->│<-(3,4)->│ # 9 <-(0,5)->│<-(1,5)->│<-(2,5)->│<-(3,5)->│ # def paintEvent(self, canvas) -> None: style = self.currentStyle() color:TTkColor= style['color'] lineColor:TTkColor= style['lineColor'] headerColor:TTkColor= style['headerColor'] hoverColor:TTkColor= style['hoverColor'] currentColor:TTkColor= style['currentColor'] selectedColor:TTkColor= style['selectedColor'] separatorColor:TTkColor= style['separatorColor'] selectedColorInv:TTkColor = selectedColor.background().invertFgBg() vHSeparator = TTkString('▐', separatorColor) ox,oy = self.getViewOffsets() w,h = self.size() rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() if not rows or not cols: return rp = self._rowsPos cp = self._colsPos showVH = self._verticalHeader.isVisible() showHH = self._horizontallHeader.isVisible() hhs = self._hHeaderSize if showHH else 0 vhs = self._vHeaderSize if showVH else 0 showHS = self._showHSeparators showVS = self._showVSeparators sliceCol=list(zip([-1]+cp,cp)) sliceRow=list(zip([-1]+rp,rp)) # NOTE: Add Color Cache # NOTE: Add Select/Hover Cache # Draw cell and right/bottom corner # Find First/Last displayed Rows rowa, rowb = 0,rows-1 for row in range(rows): ya,yb = sliceRow[row] ya,yb = ya+hhs-oy, yb+hhs-oy if ya>h : rowb = row break if yb<hhs: rowa = row continue # Use this in range rrows = (rowa,rowb+1) # Find First/Last displayed Cols cola, colb = 0, cols-1 for col in range(cols): xa,xb = sliceCol[col] xa,xb = xa+vhs-ox, xb+vhs-ox if xa>w : colb = col break if xb<vhs: cola = col continue # Use this in range rcols = (cola,colb+1) # Cache Cells _cellsCache = [] _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: ya,yb = ya+hhs-oy+1, yb+hhs-oy else: ya,yb = ya+hhs-oy+1, yb+hhs-oy+1 if ya>h : break if yb<hhs: continue rowColor = color.mod(0,row) for col in range(*rcols): xa,xb = sliceCol[col] if showVS: xa,xb = xa+vhs-ox+1, xb+vhs-ox else: xa,xb = xa+vhs-ox+1, xb+vhs-ox+1 if xa>w : break if xb<vhs: continue 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._select_proxy.isCellSelected(row=row, col=col) else rowColor ) _colorCache2d[row-rowa][col-cola] = cellColor _cellsCache.append([row,col,xa,xb,ya,yb,cellColor]) def _drawCellContent(_col,_row,_xa,_xb,_ya,_yb,_color): _txt, _align = self._tableModel.displayData(_row, _col) if _color != TTkColor.RST: _txt = _txt.completeColor(_color) for _i,_line in enumerate(_txt.split('\n')): _y = _i+_ya _width=_xb-_xa _line = _line.align(width=_width, color=_color, alignment=_align) canvas.drawTTkString(pos=(_xa,_y), text=_line, width=_width, color=_color) if _y >= _yb-1: break canvas.fill(pos=(_xa,_y+1),size=(_xb-_xa,_yb-_y-1),color=_color) def _drawCellBottom(_col,_row,_xa,_xb,_ya,_yb,cellColor): if _yb>=h: return if _row<rows-1: _belowColor2 = _colorCache2d[_row+1-rowa][_col-cola] # force black border if there are selections _sa = self._select_proxy.isCellSelected(row=_row , col=_col) _sb = self._select_proxy.isCellSelected(row=_row+1, col=_col) if (showHS and showVS) and _sa and not _sb: _bgA5 = cellColor.background() _bgB5 = TTkColor.RST elif (showHS and showVS) and not _sa and _sb: _bgA5 = TTkColor.RST _bgB5 = _belowColor2.background() else: _bgA5 = cellColor.background() _bgB5 = _belowColor2.background() if _bgA5 == _bgB5: _char='─' _color = lineColor if _bgA5 == TTkColor.RST else _bgA5 + lineColor elif _bgB5 == TTkColor.RST: _char='▀' _color=_bgA5.invertFgBg() elif _bgA5 == TTkColor.RST: _char='▄' _color=_bgB5.invertFgBg() else: _char='▀' _color=_bgB5 + _bgA5.invertFgBg() else: if self._select_proxy.isCellSelected(row=_row, col=_col): _char='▀' _color=selectedColorInv elif cellColor.hasBackground(): _char='▀' _color=cellColor.background().invertFgBg() else: _char='─' _color=lineColor canvas.fill(pos=(_xa,_yb), size=(_xb-_xa,1), char=_char, color=_color) def _drawCellRight(_col,_row,_xa,_xb,_ya,_yb,cellColor): if _xb>=w: return if _col<cols-1: _rightColor = _colorCache2d[_row-rowa][_col+1-cola] # force black border if there are selections _sa = self._select_proxy.isCellSelected(row=_row, col=_col ) _sc = self._select_proxy.isCellSelected(row=_row, col=_col+1) if (showHS and showVS) and _sa and not _sc: _bgA4 = cellColor.background() _bgC = TTkColor.RST elif (showHS and showVS) and not _sa and _sc: _bgA4 = TTkColor.RST _bgC = _rightColor.background() else: _bgA4 = cellColor.background() _bgC = _rightColor.background() if _bgA4 == _bgC: _char='│' _color = lineColor if _bgA4 == TTkColor.RST else _bgA4 + lineColor elif _bgC == TTkColor.RST: _char='▌' _color=_bgA4.invertFgBg() elif _bgA4 == TTkColor.RST: _char='▐' _color=_bgC.invertFgBg() else: _char='▌' _color=_bgC + _bgA4.invertFgBg() else: if self._select_proxy.isCellSelected(row=_row, col=_col): _char='▌' _color=selectedColorInv elif cellColor.hasBackground(): _char=' ' _color=cellColor.background() else: _char='│' _color=lineColor canvas.fill(pos=(_xb,_ya), size=(1,_yb-_ya), char=_char, color=_color) _charList = [ # 0x00 0x01 0x02 0x03 ' ', '▘', '▝', '▀', # 0x04 0x05 0x06 0x07 '▖', '▌', '▞', '▛', # 0x08 0x09 0x0A 0x0B '▗', '▚', '▐', '▜', # 0x0C 0x0D 0x0E 0x0F '▄', '▙', '▟', '█'] def _drawCellCorner(_col:int,_row:int,_xa:int,_xb:int,_ya:int,_yb:int,cellColor:TTkColor): if _yb>=h or _xb>=w: return _char = 'X' _color = cellColor if _row<rows-1 and _col<cols-1: # Check if there are selected cells: chId = ( 0x01 * self._select_proxy.isCellSelected(row=_row , col=_col ) + 0x02 * self._select_proxy.isCellSelected(row=_row , col=_col+1) + 0x04 * self._select_proxy.isCellSelected(row=_row+1, col=_col ) + 0x08 * self._select_proxy.isCellSelected(row=_row+1, col=_col+1) ) if chId==0x00 or chId==0x0F: _belowColor3 = _colorCache2d[_row+1-rowa][_col-cola] _bgA3 = cellColor.background() _bgB3 = _belowColor3.background() if _bgA3 == _bgB3: _color = lineColor if _bgA3 == TTkColor.RST else _bgA3 + lineColor _char='┼' elif _bgB3 == TTkColor.RST: _char='▀' _color=_bgA3.invertFgBg() elif _bgA3 == TTkColor.RST: _char='▄' _color=_bgB3.invertFgBg() else: _char='▀' _color=_bgB3 + _bgA3.invertFgBg() else: _char = _charList[chId] _color=selectedColorInv elif _col<cols-1: chId = ( 0x01 * self._select_proxy.isCellSelected(row=row, col=col ) + 0x02 * self._select_proxy.isCellSelected(row=row, col=col+1) ) if chId: _char = _charList[chId] _color=selectedColorInv elif cellColor.hasBackground(): _char='▀' _color = cellColor.background().invertFgBg() else: _char = '┴' _color = lineColor elif _row<rows-1: chId = ( (0x01) * self._select_proxy.isCellSelected(row=row , col=col ) + (0x04) * self._select_proxy.isCellSelected(row=row+1, col=col ) ) _belowColor1:TTkColor = _colorCache2d[_row+1-rowa][_col-cola] _bgA1:TTkColor = cellColor.background() _bgB1:TTkColor = _belowColor1.background() if chId: _char = _charList[chId] _color=selectedColorInv elif _bgA1 == _bgB1 == TTkColor.RST: _char = '┤' _color = lineColor elif _bgB1 == TTkColor.RST: _char='▀' _color=_bgA1.invertFgBg() elif _bgA1 == TTkColor.RST: _char='▄' _color=_bgB1.invertFgBg() else: _char='▀' _color=_bgB1 + _bgA1.invertFgBg() else: chId = ( (0x01) * self._select_proxy.isCellSelected(row=row, col=col) ) if chId: _char = _charList[chId] _color=selectedColorInv elif cellColor.hasBackground(): _char='▀' _color = cellColor.background().invertFgBg() else: _char = '┘' _color = lineColor canvas.fill(pos=(_xb,_yb), size=(1,1), char=_char, color=_color) # # Draw Cells for row,col,xa,xb,ya,yb,cellColor in _cellsCache: _drawCellContent(col,row,xa,xb,ya,yb,cellColor) if showHS: _drawCellBottom(col,row,xa,xb,ya,yb,cellColor) if showVS: _drawCellRight( col,row,xa,xb,ya,yb,cellColor) if showHS and showVS: _drawCellCorner(col,row,xa,xb,ya,yb,cellColor) # return f"cc={len(_cellsCache)} size={(w,h)} tw={(sliceCol[0],sliceCol[-1])} th={(sliceRow[0],sliceRow[-1])}" if self._hoverPos: row,col = self._hoverPos if row == -1: ya,yb = -1,rp[-1] else: ya,yb = sliceRow[row] if col == -1: xa,xb = -1,cp[-1] else: xa,xb = sliceCol[col] if showVS: xa,xb = xa+vhs-ox, xb+vhs-ox else: xa,xb = xa+vhs-ox, xb+vhs-ox+1 if showHS: ya,yb = ya+hhs-oy, yb+hhs-oy else: ya,yb = ya+hhs-oy, yb+hhs-oy+1 # _drawCell(col,row,xa,xb,ya,yb,hoverColor) # Draw Borders # Top, Bottom hoverColorInv = hoverColor.background().invertFgBg() canvas.drawTTkString(pos=(xa,ya), text=TTkString('▗'+('▄'*(xb-xa-1))+'▖',hoverColorInv)) canvas.drawTTkString(pos=(xa,yb), text=TTkString('▝'+('▀'*(xb-xa-1))+'▘',hoverColorInv)) # Left, Right canvas.fill(char='▐',pos=(xa,ya+1), size=(1,yb-ya-1), color=hoverColorInv) canvas.fill(char='▌',pos=(xb,ya+1), size=(1,yb-ya-1), color=hoverColorInv) if self._dragPos: rowa,cola = self._dragPos.fr rowb,colb = self._dragPos.to if rowa == -1: cola,colb = min(cola,colb),max(cola,colb) xa = sliceCol[cola][0]-ox+vhs xb = sliceCol[colb][1]-ox+vhs + (0 if showHS else 1) ya,yb = -1-oy+hhs,rp[-1]-oy+hhs elif cola == -1: rowa,rowb = min(rowa,rowb),max(rowa,rowb) ya = sliceRow[rowa][0]-oy+hhs yb = sliceRow[rowb][1]-oy+hhs + (0 if showVS else 1) xa,xb = -1-ox+vhs,cp[-1]-ox+vhs else: cola,colb = min(cola,colb),max(cola,colb) rowa,rowb = min(rowa,rowb),max(rowa,rowb) xa = sliceCol[cola][0]-ox+vhs xb = sliceCol[colb][1]-ox+vhs + (0 if showHS else 1) ya = sliceRow[rowa][0]-oy+hhs yb = sliceRow[rowb][1]-oy+hhs + (0 if showVS else 1) hoverColorInv = hoverColor.background().invertFgBg() canvas.drawTTkString(pos=(xa,ya), text=TTkString('▗'+('▄'*(xb-xa-1))+'▖',hoverColorInv)) canvas.drawTTkString(pos=(xa,yb), text=TTkString('▝'+('▀'*(xb-xa-1))+'▘',hoverColorInv)) canvas.fill(char='▐',pos=(xa,ya+1), size=(1,yb-ya-1), color=hoverColorInv) canvas.fill(char='▌',pos=(xb,ya+1), size=(1,yb-ya-1), color=hoverColorInv) if self._currentPos: row,col = self._currentPos xa = sliceCol[col][0]-ox+vhs xb = sliceCol[col][1]-ox+vhs + (0 if showVS else 1) ya = sliceRow[row][0]-oy+hhs yb = sliceRow[row][1]-oy+hhs + (0 if showHS else 1) currentColorInv = currentColor.background().invertFgBg() if showVS and showHS: canvas.drawTTkString(pos=(xa,ya), text=TTkString('▗'+('▄'*(xb-xa-1))+'▖',currentColorInv)) canvas.drawTTkString(pos=(xa,yb), text=TTkString('▝'+('▀'*(xb-xa-1))+'▘',currentColorInv)) canvas.fill(char='▐',pos=(xa,ya+1), size=(1,yb-ya-1), color=currentColorInv) canvas.fill(char='▌',pos=(xb,ya+1), size=(1,yb-ya-1), color=currentColorInv) # elif showHS: # canvas.drawTTkString(pos=(xa+1,ya), text=TTkString( '▄'*(xb-xa-1) ,currentColorInv)) # canvas.drawTTkString(pos=(xa+1,yb), text=TTkString( '▀'*(xb-xa-1) ,currentColorInv)) # if showVS: # canvas.fill(char='▐',pos=(xa,ya+1), size=(1,yb-ya-1), color=currentColorInv) # canvas.fill(char='▌',pos=(xb,ya+1), size=(1,yb-ya-1), color=currentColorInv) # Draw H-Header first: if showHH: for col in range(*rcols): txt = self._tableModel.headerData(col,TTkK.HORIZONTAL) if isinstance(txt,TTkString): pass elif type(txt) == str: txt = TTkString(txt) else: txt = TTkString(f"{txt}") xa,xb = sliceCol[col] if showVS: xa,xb = xa+vhs-ox+1, xb+vhs-ox else: xa,xb = xa+vhs-ox+1, xb+vhs-ox+1 canvas.drawText(pos=(xa,0), text=txt, width=xb-xa, color=headerColor) if self._sortingEnabled: s = '•' if col != self._sortColumn else '▼' if self._sortOrder == TTkK.AscendingOrder else '▲' canvas.drawText(pos=(xb-1,0), text=s, color=headerColor) if showVS: canvas.drawChar(pos=(xb,0), char='╿', color=headerColor) # Draw V-Header : if showVH: hlineHead = TTkString('╾'+'╌'*(vhs-2), color=headerColor) + vHSeparator for row in range(*rrows): ya,yb = sliceRow[row] if showHS: ya,yb = ya+hhs-oy+1, yb+hhs-oy else: ya,yb = ya+hhs-oy+1, yb+hhs-oy+1 if ya>h : break if yb<hhs: continue txt = self._tableModel.headerData(row,TTkK.VERTICAL) if isinstance(txt,TTkString): pass elif type(txt) == str: txt = TTkString(txt) else: txt = TTkString(f"{txt}") canvas.drawTTkString(pos=(0 ,ya), text=txt, width=vhs, color=headerColor) canvas.drawTTkString(pos=(vhs-1,ya), text=vHSeparator) for y in range(ya+1,yb): canvas.drawTTkString(pos=(0,y), text=vHSeparator, width=vhs, alignment=TTkK.RIGHT_ALIGN, color=headerColor) if showHS: canvas.drawTTkString(pos=(0,yb), text=hlineHead) # Draw Top/Left Corner canvas.drawText(pos=(0,0), text=' ', width=vhs, color=separatorColor.invertFgBg() )