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.


__all__ = ['TTkTableWidget','TTkHeaderView']

from dataclasses import dataclass

from TermTk.TTkCore.log import TTkLog
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.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.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
class _ClipboardTable(TTkString): __slots__=('_data') def __init__(self,data) -> None: self._data = data super().__init__(self._toTTkString()) def data(self) -> list: return self._data def _toTTkString(self) -> str: 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)
[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. ''' cellChanged: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 ''' cellClicked: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 ''' cellDoubleClicked: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 ''' cellEntered: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 ''' # self.cellPressed = pyTTkSignal(int,int) currentCellChanged: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 ''' 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', '_internal', '_selected', '_selectedBase', '_hSeparatorSelected', '_vSeparatorSelected', '_hoverPos', '_dragPos', '_currentPos', '_sortColumn', '_sortOrder', '_fastCheck', '_guessDataEdit', '_snapshot', '_snapshotId', # Signals # 'cellActivated', 'cellChanged', 'cellClicked', 'cellDoubleClicked', 'cellEntered', # 'cellPressed', 'currentCellChanged', ) def __init__(self, *, tableModel:TTkAbstractTableModel=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._selected = None self._selectedBase = None self._hoverPos = None self._dragPos = None self._currentPos = None self._internal = {} 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) 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) @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] 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][1:]: 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] 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 = [] 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) 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] 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): row,col = self._currentPos if self._currentPos else (0,0) if isinstance(data,_ClipboardTable): rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() 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): # 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(self._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() def _refreshLayout(self): self._selected = None self._selectedBase = None 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._vHeaderSize = vhs = 1+max(len(self._tableModel.headerData(_p, TTkK.VERTICAL)) for _p in range(rows) ) self._hHeaderSize = hhs = 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)] # self._selectedBase = sb = [False]*cols # self._selected = [sb]*rows self.clearSelection() # Overridden function
[docs] 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]+1 h = hhs+self._rowsPos[-1]+1 return w,h
[docs] def clearSelection(self) -> None: ''' Deselects all selected items. The current index will not be changed. ''' rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() self._selected = [[False]*cols for _ in range(rows)] 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. ''' 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.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` ''' 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.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 ''' 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.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 ''' cmp = TTkK.ItemFlag.ItemIsSelectable flagFunc = self._tableModel.flags for row,line in enumerate(self._selected): line[col] = cmp==(cmp&flagFunc(row,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 ''' cols = self._tableModel.columnCount() self._selected[row] = [False]*cols self.update()
[docs] def unselectColumn(self, column:int) -> None: ''' Unselects the given column in the table view :param column: the column to be unselected :type column: int ''' for line in self._selected: line[column] = False 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.dataChanged.disconnect(self.update) self._tableModel = model self._tableModel.dataChanged.connect(self.update) self._refreshLayout() self.viewChanged.emit()
def focusOutEvent(self) -> None: self._hSeparatorSelected = None self._vSeparatorSelected = None def leaveEvent(self, evt): 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 ''' 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 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): 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 _editStr(self, x,y,w,h, row, col, data): _te = TTkTextEdit( parent=self, pos=(x, y), size=(w,h), readOnly=False, wrapMode=TTkK.NoWrap) _tev = _te.textEditView() _te.setText(data) _te.textCursor().movePosition(operation=TTkTextCursor.EndOfLine) _te.setFocus() @pyTTkSlot(bool) def _processClose(change): if change: self.focusChanged.disconnect(_processClose) txt = _te.toRawText() val = str(txt) if txt.isPlainText() else txt self._tableModel_setData([(row,col,val)]) self.update() _te.close() self.setFocus() # Override the key event _ke = _tev.keyEvent _doc = _tev.document() _cur = _tev.textCursor() def _keyEvent(evt): if ( evt.type == TTkK.SpecialKey): _line = _cur.anchor().line _pos = _cur.anchor().pos _lineCount = _doc.lineCount() # _lineLen if evt.mod==TTkK.NoModifier: if evt.key == TTkK.Key_Enter: # self.enterPressed.emit(True) self._moveCurrentCell(diff=(0,+1)) _processClose(True) return True elif evt.key == TTkK.Key_Up: if _line == 0: self._moveCurrentCell(diff=(0,-1)) _processClose(True) return True elif evt.key == TTkK.Key_Down: if _lineCount == 1: self._moveCurrentCell(diff=(0,+1)) _processClose(True) return True elif evt.key == TTkK.Key_Left: if _pos == _line == 0: self._moveCurrentCell(diff=(-1, 0)) _processClose(True) return True elif evt.key == TTkK.Key_Right: if _lineCount == 1 and _pos==len(_doc.toPlainText()): self._moveCurrentCell(diff=(+1, 0)) _processClose(True) return True elif ( evt.type == TTkK.SpecialKey and evt.mod==TTkK.ControlModifier|TTkK.AltModifier and evt.key == TTkK.Key_M ): evt.mod = TTkK.NoModifier evt.key = TTkK.Key_Enter return _ke(evt) _tev.keyEvent = _keyEvent # _tev.enterPressed.connect(_processClose) self.focusChanged.connect(_processClose) def _editNum(self, x,y,w,h, row, col, data): _sb = TTkSpinBox( parent=self, pos=(x, y), size=(w,1), minimum=-1000000, maximum=1000000, value=data) _sb.setFocus() @pyTTkSlot(bool) def _processClose(change): if change: self.focusChanged.disconnect(_processClose) val = _sb.value() self._tableModel_setData([(row,col,val)]) self.update() _sb.close() self.setFocus() # Override the key event _ke = _sb.keyEvent def _keyEvent(evt): if ( evt.type == TTkK.SpecialKey): if evt.mod==TTkK.NoModifier: if evt.key == TTkK.Key_Enter: self._moveCurrentCell( 0,+1) _processClose(True) return True return _ke(evt) _sb.keyEvent = _keyEvent self.focusChanged.connect(_processClose) def _editTTkString(self, x,y,w,h, row, col, data): _tp = TTkTextPicker( parent=self, pos=(x, y), size=(w,h), text=data, autoSize=False, wrapMode=TTkK.NoWrap) _tp.setFocus() @pyTTkSlot(bool) def _processClose(change): if change: self.focusChanged.disconnect(_processClose) txt = _tp.getTTkString() self._tableModel_setData([(row,col,txt)]) self.update() _tp.close() self.setFocus() self.focusChanged.connect(_processClose) 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 type(data) is str: self._editStr(xa,ya,xb-xa,yb-ya,row,col,data) elif type(data) in [int,float]: self._editNum(xa,ya,xb-xa,yb-ya,row,col,data) else: data = self._tableModel.ttkStringData(row, col) if richEditSupport: self._editTTkString(xa,ya,xb-xa,yb-ya,row,col,data) else: self._editStr(xa,ya,xb-xa,yb-ya,row,col,data) 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): # 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): 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) -> 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) -> 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 = [(row,col),(row,col)] _ctrl = evt.mod==TTkK.ControlModifier if row==col==-1: # Corner Press # Select Everything 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: 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: 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._selected[row][col] and _ctrl) else TTkK.TTkItemSelectionModel.Select) self._hoverPos = None self.update() return True def mouseDragEvent(self, evt) -> 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[1] = 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) -> bool: if self._dragPos: rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() state = True (rowa,cola),(rowb,colb) = self._dragPos 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)] 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 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() 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 = [[None]*(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._selected[row][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 = self._tableModel.ttkStringData(_row, _col) if _color != TTkColor.RST: txt = txt.completeColor(_color) for i,line in enumerate(txt.split('\n')): y = i+_ya canvas.drawTTkString(pos=(_xa,y), text=line, width=_xb-_xa, 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: _belowColor:TTkColor = _colorCache2d[_row+1-rowa][_col-cola] # force black border if there are selections _sa = self._selected[_row ][_col ] _sb = self._selected[_row+1][_col ] if (showHS and showVS) and _sa and not _sb: _bgA:TTkColor = c if (c:=cellColor.background()) else TTkColor.RST _bgB:TTkColor = TTkColor.RST elif (showHS and showVS) and not _sa and _sb: _bgA:TTkColor = TTkColor.RST _bgB:TTkColor = c if (c:=_belowColor.background()) else TTkColor.RST else: _bgA:TTkColor = c if (c:=cellColor.background()) else TTkColor.RST _bgB:TTkColor = c if (c:=_belowColor.background()) else TTkColor.RST if _bgA == _bgB: _char='─' _color = lineColor if _bgA == TTkColor.RST else _bgA + lineColor elif _bgB == TTkColor.RST: _char='▀' _color=_bgA.invertFgBg() elif _bgA == TTkColor.RST: _char='▄' _color=_bgB.invertFgBg() else: _char='▀' _color=_bgB + _bgA.invertFgBg() else: _bgA:TTkColor = c if (c:=cellColor.background()) else TTkColor.RST if self._selected[_row ][_col ]: _char='▀' _color=selectedColorInv elif _bgA:=cellColor.background(): _char='▀' _color=_bgA.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:TTkColor = _colorCache2d[_row-rowa][_col+1-cola] # force black border if there are selections _sa = self._selected[_row ][_col ] _sc = self._selected[_row ][_col+1] if (showHS and showVS) and _sa and not _sc: _bgA:TTkColor = c if (c:=cellColor.background()) else TTkColor.RST _bgC:TTkColor = TTkColor.RST elif (showHS and showVS) and not _sa and _sc: _bgA:TTkColor = TTkColor.RST _bgC:TTkColor = c if (c:=_rightColor.background()) else TTkColor.RST else: _bgA:TTkColor = c if (c:=cellColor.background()) else TTkColor.RST _bgC:TTkColor = c if (c:=_rightColor.background()) else TTkColor.RST if _bgA == _bgC: _char='│' _color = lineColor if _bgA == TTkColor.RST else _bgA + lineColor elif _bgC == TTkColor.RST: _char='▌' _color=_bgA.invertFgBg() elif _bgA == TTkColor.RST: _char='▐' _color=_bgC.invertFgBg() else: _char='▌' _color=_bgC + _bgA.invertFgBg() else: _bgA:TTkColor = c if (c:=cellColor.background()) else TTkColor.RST if self._selected[_row ][_col ]: _char='▌' _color=selectedColorInv elif _bgA:=cellColor.background(): _char=' ' _color=_bgA 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._selected[_row ][_col ] + 0x02 * self._selected[_row ][_col+1] + 0x04 * self._selected[_row+1][_col ] + 0x08 * self._selected[_row+1][_col+1] ) if chId==0x00 or chId==0x0F: _belowColor:TTkColor = _colorCache2d[_row+1-rowa][_col-cola] _bgA:TTkColor = c if (c:=cellColor.background()) else TTkColor.RST _bgB:TTkColor = c if (c:=_belowColor.background()) else TTkColor.RST if _bgA == _bgB: _color = lineColor if _bgA == TTkColor.RST else _bgA + lineColor _char='┼' elif _bgB == TTkColor.RST: _char='▀' _color=_bgA.invertFgBg() elif _bgA == TTkColor.RST: _char='▄' _color=_bgB.invertFgBg() else: _char='▀' _color=_bgB + _bgA.invertFgBg() else: _char = _charList[chId] _color=selectedColorInv elif _col<cols-1: chId = ( 0x01 * self._selected[row ][col ] + 0x02 * self._selected[row ][col+1] ) if chId: _char = _charList[chId] _color=selectedColorInv elif _c:=cellColor.background(): _char='▀' _color = _c.invertFgBg() else: _char = '┴' _color = lineColor elif _row<rows-1: chId = ( (0x01) * self._selected[row ][col ] + (0x04) * self._selected[row+1][col ] ) _belowColor:TTkColor = _colorCache2d[_row+1-rowa][_col-cola] _bgA:TTkColor = c if (c:=cellColor.background()) else TTkColor.RST _bgB:TTkColor = c if (c:=_belowColor.background()) else TTkColor.RST if chId: _char = _charList[chId] _color=selectedColorInv elif _bgA == _bgB == TTkColor.RST: _char = '┤' _color = lineColor elif _bgB == TTkColor.RST: _char='▀' _color=_bgA.invertFgBg() elif _bgA == TTkColor.RST: _char='▄' _color=_bgB.invertFgBg() else: _char='▀' _color=_bgB + _bgA.invertFgBg() else: chId = ( (0x01) * self._selected[row ][col ] ) if chId: _char = _charList[chId] _color=selectedColorInv elif _c:=cellColor.background(): _char='▀' _color = _c.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),(rowb,colb) = self._dragPos 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() )