Source code for TermTk.TTkWidgets.Fancy.tableview

# MIT License
#
# Copyright (c) 2021 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__ = ['TTkFancyTableView']

from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal
from TermTk.TTkCore.color import TTkColor
from TermTk.TTkCore.string import TTkString
from TermTk.TTkWidgets.widget import TTkWidget
from TermTk.TTkLayouts.gridlayout import TTkGridLayout
from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView, TTkAbstractScrollViewGridLayout

class _TTkFancyTableViewHeader(TTkAbstractScrollView):
    __slots__ = ('_header', '_alignments', '_headerColor', '_columns')
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._columns = kwargs.get('columns' , [-1] )
        self._header = [TTkString()]*len(self._columns)
        self._alignments = [TTkK.NONE]*len(self._columns)
        self._headerColor = kwargs.get('headerColor' , TTkColor.BOLD )
        self.setMaximumHeight(1)
        self.setMinimumHeight(1)

    # Override this function
    def viewFullAreaSize(self) -> (int, int):
        return self.size()

    # Override this function
    def viewDisplayedSize(self) -> (int, int):
        return self.size()

    @pyTTkSlot(int, int)
    def viewMoveTo(self, x: int, y: int):
        pass

    def setAlignment(self, alignments):
        if len(alignments) != len(self._columns):
            return
        self._alignments = alignments

    def setHeader(self, header):
        if len(header) != len(self._columns):
            return
        self._header = [TTkString(i) if isinstance(i,str) else i if issubclass(type(i), TTkString) else TTkString() for i in header]


    def setColumnSize(self, columns):
        self._columns = columns
        self._header += [TTkString()]*(len(self._columns)-len(self._header))
        self._alignments = [TTkK.NONE]*len(self._columns)

    def paintEvent(self, canvas):
        w,h = self.size()
        total = 0
        variableCols = 0
        # Retrieve the free size
        for width in self._columns:
            if width > 0:
                total += width
            else:
                variableCols += 1
        # Define the list of cols sizes
        sizes = []
        for width in self._columns:
            if width > 0:
                sizes.append(width)
            else:
                sizes.append((w-total)//variableCols)
                variableCols -= 1
        colors = [self._headerColor]*len(self._header)
        canvas.drawTableLine(pos=(0,0), items=self._header, sizes=sizes, colors=colors, alignments=self._alignments)


class _TTkFancyTableView(TTkAbstractScrollView):
    __slots__ = (
            '_alignments', '_headerColor',
            '_columns', '_columnColors',
            '_tableDataId', '_tableDataText', '_tableDataWidget', '_shownWidgets',
            '_selectColor', '_selected',
            '_tableWidth',
            # Signals
            'activated', 'doubleClicked')
    def __init__(self, *args, **kwargs):
        self._tableDataId = []
        self._tableDataText = []
        self._tableDataWidget = []
        self._shownWidgets = []
        super().__init__(*args, **kwargs)
        # define signals
        self.activated = pyTTkSignal(int) # Value
        self.doubleClicked = pyTTkSignal(int) # Value

        self._tableWidth = 0
        self._columns = kwargs.get('columns' , [-1] )
        self._alignments = [TTkK.NONE]*len(self._columns)
        self._columnColors = kwargs.get('columnColors' , [TTkColor.RST]*len(self._columns) )
        self._selectColor = kwargs.get('selectColor' , TTkColor.BOLD )
        self._headerColor = kwargs.get('headerColor' , TTkColor.BOLD )
        self._selected = -1
        self.setFocusPolicy(TTkK.ClickFocus)
        self.viewChanged.connect(self._viewChangedHandler)

    def _initSignals(self):
        # self.cellActivated(int row, int column)
        # self.cellChanged(int row, int column)
        # self.cellClicked(int row, int column)
        # self.cellDoubleClicked(int row, int column)
        # self.cellEntered(int row, int column)
        # self.cellPressed(int row, int column)
        # self.currentCellChanged(int currentRow, int currentColumn, int previousRow, int previousColumn)
        # self.currentItemChanged(QTableWidgetItem *current, QTableWidgetItem *previous)
        # self.itemActivated(QTableWidgetItem *item)
        # self.itemChanged(QTableWidgetItem *item)
        # self.itemClicked(QTableWidgetItem *item)
        # self.itemDoubleClicked(QTableWidgetItem *item)
        # self.itemEntered(QTableWidgetItem *item)
        # self.itemPressed(QTableWidgetItem *item)
        # self.itemSelectionChanged()
        pass

    @pyTTkSlot()
    def _viewChangedHandler(self):
        w,h = self.size()
        _, oy = self.getViewOffsets()
        total = 0
        variableCols = 0
        # Retrieve the free size
        for width in self._columns:
            if width > 0:
                total += width
            else:
                variableCols += 1
        # Define the list of cols sizes
        sizes = []
        for width in self._columns:
            if width > 0:
                sizes.append(width)
            else:
                sizes.append((w-total)//variableCols)
                variableCols -= 1

        maxItems = len(self._tableDataText)
        itemFrom = oy
        if itemFrom > maxItems-h: itemFrom = maxItems-h
        if itemFrom < 0 : itemFrom = 0
        itemTo   = itemFrom + h
        if itemTo > maxItems: itemTo = maxItems

        widgetsToHide = self._shownWidgets
        self._shownWidgets = []
        for y, it in enumerate(range(itemFrom, itemTo)):
            x = 0
            item = self._tableDataWidget[it]
            for iw, widget in enumerate(item):
                size = sizes[iw]
                if widget is not None:
                    if widget.parentWidget() != self:
                        self.layout().addWidget(widget)
                    widget.setGeometry(x,y,size,1)
                    widget.show()
                    self._shownWidgets.append(widget)
                x+=size+1
        for widget in widgetsToHide:
            if widget not in self._shownWidgets:
                widget.hide()



    def viewFullAreaSize(self) -> (int, int):
        return self._tableWidth, len(self._tableDataText)

    def viewDisplayedSize(self) -> (int, int):
        return self.size()

    # def items(self): return self._tableDataText

    def setAlignment(self, alignments):
        if len(alignments) != len(self._columns):
            return
        self._alignments = alignments

    def setColumnSize(self, columns):
        self._columns = columns
        self._columnColors = [TTkColor.RST]*len(self._columns)
        self._alignments = [TTkK.NONE]*len(self._columns)

    def setColumnColors(self, colors):
        if len(colors) != len(self._columns):
            return
        self._columnColors = colors

    def _processValues(self):
        size = 0
        for txt in self._tableDataText:
            size=max(size,sum(t.termWidth() for t in txt))
        if self._tableWidth != size:
            self._tableWidth = size
            self.viewChanged.emit()


    def appendItem(self, item, id=None):
        if len(item) != len(self._columns):
            return
        textItem = [TTkString(i) if isinstance(i,str) else i if issubclass(type(i), TTkString) else TTkString() for i in item]
        widgetItem = [i if isinstance(i,TTkWidget) else None for i in item]
        if id is not None:
            self._tableDataId.append(id)
        else:
            self._tableDataId.append(item)
        self._tableDataText.append(textItem)
        self._tableDataWidget.append(widgetItem)
        self._processValues()
        self.viewChanged.emit()
        self.update()

    def insertItem(self, index: int, item, id=None):
        if len(item) != len(self._columns):
            return#
        textItem = [TTkString(i) if isinstance(i,str) else i if issubclass(type(i), TTkString) else TTkString() for i in item]
        widgetItem = [i if isinstance(i,TTkWidget) else None for i in item]
        if id is not None:
            self._tableDataId.insert(index, id)
        else:
            self._tableDataId.insert(index, item)
        self._tableDataText.insert(index, textItem)
        self._tableDataWidget.insert(index, widgetItem)
        self._processValues()
        self.viewChanged.emit()
        self.update()

    def removeItem(self, item):
        index = self.indexOf(item)
        self.removeItemAt(index)
        self._processValues()
        self.viewChanged.emit()
        self.update()

    def removeItemAt(self, index: int):
        if self._selected == index:
            self._selected = -1
        del self._tableDataId[index]
        del self._tableDataText[index]
        del self._tableDataWidget[index]
        self._processValues()
        self.viewChanged.emit()
        self.update()

    def removeItemsFrom(self, index: int):
        if self._selected >= index:
            self._selected = -1
        self._tableDataId = self._tableDataId[:index]
        self._tableDataText = self._tableDataText[:index]
        self._tableDataWidget = self._tableDataWidget[:index]
        self._processValues()
        self.viewChanged.emit()
        self.update()

    def itemAt(self, index: int):
        if 0 <= index < len(self._tableDataId):
            if item:=self._tableDataWidget[index]:
                return item
            else:
                return self._tableDataText[index]
        return None

    def dataAt(self, index: int):
        if 0 <= index < len(self._tableDataId):
            return self._tableDataId[index]
        return None

    def indexOf(self, id) -> int:
        for index, value in enumerate(self._tableDataId):
            if id is value:
                return index
        return -1

    def mouseDoubleClickEvent(self, evt):
        _,y = evt.x, evt.y
        _, oy = self.getViewOffsets()
        if y >= 0:
            selected = oy + y
            if selected >= len(self._tableDataText):
                selected = -1
            self._selected = selected
            self.update()
            self.doubleClicked.emit(self._selected)
        return True

    def mousePressEvent(self, evt):
        _,y = evt.x, evt.y
        _, oy = self.getViewOffsets()
        if y >= 0:
            selected = oy + y
            if selected >= len(self._tableDataText):
                selected = -1
            self._selected = selected
            self.update()
            self.activated.emit(self._selected)
        return True

    def paintEvent(self, canvas):
        w,h = self.size()
        ox, oy = self.getViewOffsets()
        total = 0
        variableCols = 0
        # Retrieve the free size
        for width in self._columns:
            if width > 0:
                total += width
            else:
                variableCols += 1
        # Define the list of cols sizes
        sizes = []
        varSizes = []
        for width in self._columns:
            if width > 0:
                sizes.append(width)
            else:
                varSizes.append(len(sizes))
                sizes.append((w-total)//variableCols)
                variableCols -= 1

        maxItems = len(self._tableDataText)
        itemFrom = oy
        if itemFrom > maxItems-h: itemFrom = maxItems-h
        if itemFrom < 0 : itemFrom = 0
        itemTo   = itemFrom + h
        if itemTo > maxItems: itemTo = maxItems
        # TTkLog.debug(f"moveto:{self._moveTo}, maxItems:{maxItems}, f:{itemFrom}, t{itemTo}, h:{h}, sel:{self._selected}")

        for y, it in enumerate(range(itemFrom, itemTo)):
            item = self._tableDataText[it].copy()
            if self._selected > 0:
                val = self._selected - itemFrom
            else:
                val = h//2
            if val < 0 : val = 0
            if val > h : val = h
            for vid in varSizes:
                strPos = item[vid].tabCharPos(ox)
                item[vid] = item[vid].substring(fr=strPos)
            if it == self._selected:
                colors = [self._selectColor]*len(self._columnColors)
                canvas.drawTableLine(pos=(0,y), items=item, sizes=sizes, colors=colors, alignments=self._alignments)
            else:
                colors = [c.modParam(val=-val) for c in self._columnColors]
                canvas.drawTableLine(pos=(0,y), items=item, sizes=sizes, colors=colors, alignments=self._alignments)

[docs]class TTkFancyTableView(TTkAbstractScrollView): __slots__ = ( '_header', '_tableView', '_showHeader', 'activated', '_excludeEvent', # Forwarded Methods 'setHeader', 'setColumnColors', 'appendItem', 'itemAt', 'dataAt', 'indexOf', 'insertItem', 'removeItem', 'removeItemAt', 'removeItemsFrom', 'doubleClicked') def __init__(self, **kwargs): self._excludeEvent = False super().__init__(**(kwargs|{'layout':TTkGridLayout()})) if 'parent' in kwargs: kwargs.pop('parent') self._showHeader = kwargs.get('showHeader', True) self._tableView = _TTkFancyTableView(**kwargs) self._header = _TTkFancyTableViewHeader(**kwargs) self.layout().addWidget(self._header,0,0) self.layout().addWidget(self._tableView,1,0) self._tableView.viewChanged.connect(self._viewChanged) self._tableView.viewMovedTo.connect(self._viewMovedTo) # Forward the tableSignals self.activated = self._tableView.activated self.doubleClicked = self._tableView.doubleClicked if not self._showHeader: self._header.hide() # Forward Methods self.setHeader = self._header.setHeader self.setColumnColors = self._tableView.setColumnColors self.appendItem = self._tableView.appendItem self.dataAt = self._tableView.dataAt self.itemAt = self._tableView.itemAt self.indexOf = self._tableView.indexOf self.insertItem = self._tableView.insertItem self.removeItem = self._tableView.removeItem self.removeItemAt = self._tableView.removeItemAt self.removeItemsFrom = self._tableView.removeItemsFrom @pyTTkSlot() def _viewChanged(self): if self._excludeEvent: return self.viewChanged.emit() @pyTTkSlot(int,int) def _viewMovedTo(self, x, y): if self._excludeEvent: return self.viewMoveTo(x, y) @pyTTkSlot(int, int) def viewMoveTo(self, x: int, y: int): fw, fh = self.viewFullAreaSize() dw, dh = self.viewDisplayedSize() rangex = fw - dw rangey = fh - dh # TTkLog.debug(f"x:{x},y:{y}, full:{fw,fh}, display:{dw,dh}, range:{rangex,rangey}") x = max(0,min(rangex,x)) y = max(0,min(rangey,y)) # TTkLog.debug(f"x:{x},y:{y}, wo:{self._viewOffsetX,self._viewOffsetY}") if self._viewOffsetX == x and \ self._viewOffsetY == y: # Nothong to do return self._excludeEvent = True for widget in self.layout().iterWidgets(recurse=False): widget.viewMoveTo(x,y) self._excludeEvent = False self._viewOffsetX = x self._viewOffsetY = y self.viewMovedTo.emit(x,y) self.viewChanged.emit() self.update() def getViewOffsets(self): return self._tableView.getViewOffsets() def viewFullAreaSize(self) -> (int, int): return self._tableView.viewFullAreaSize() def viewDisplayedSize(self) -> (int, int): return self._tableView.viewDisplayedSize() def setAlignment(self, *args, **kwargs) : self._tableView.setAlignment(*args, **kwargs) self._header.setAlignment(*args, **kwargs) def setColumnSize(self, *args, **kwargs) : self._tableView.setColumnSize(*args, **kwargs) self._header.setColumnSize(*args, **kwargs)