Source code for TermTk.TTkLayouts.gridlayout

# 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.

'''
**Grid Layout** [:ref:`Tutorial <Layout-Tutorial_Intro>`]
'''

__all__ = ['TTkGridLayout']

from TermTk.TTkCore.constant import TTkK
from TermTk.TTkLayouts.layout import TTkLayout

[docs] class TTkGridLayout(TTkLayout): ''' The grid layout allows an automatic place all the widgets in a grid, <br/> the empty rows/cols are resized to the "rowMinHeight,columnMinWidth" parameters :: TTkGridLayout ┌┐ columnMinWidth ╔═════════╤═════════╤╤═════════╗ ║ Widget1 │ Widget2 ││ Widget3 ║ ║ (0,0) │ (0,1) ││ (0,3) ║ ╟─────────┼─────────┼┼─────────╢ ┐ rowMinHeight ╟─────────┼─────────┼┼─────────╢ ┘ ║ Widget4 │ ││ ║ ║ (2,0) │ ││ ║ ╟─────────┼─────────┼┼─────────╢ ║ │ ││ Widget5 ║ ║ │ ││ (3,3) ║ ╚═════════╧═════════╧╧═════════╝ :param int columnMinWidth: the minimum width of the column, optional, defaults to 0 :param int rowMinHeight: the minimum height of the column, optional, defaults to 0 ''' __slots__ = ('_gridItems','_columnMinWidth','_rowMinHeight', '_rows', '_cols', '_horSizes', '_verSizes') def __init__(self, *, columnMinWidth:int=0, rowMinHeight:int=0, **kwargs) -> None: self._rows = 0 self._cols = 0 self._gridItems = [[]] self._horSizes = [] self._verSizes = [] self._columnMinWidth = columnMinWidth self._rowMinHeight = rowMinHeight TTkLayout.__init__(self, **kwargs) def _gridUsedsize(self): rows = 0 cols = 0 for gridRow in range(self._rows): for gridCol in range(self._cols): if item:=self._gridItems[gridRow][gridCol]: rows = max(rows, gridRow+item._rowspan) cols = max(cols, gridCol+item._colspan) return (rows, cols) def _reshapeGrid(self, size): rows, cols = size self._rows, self._cols = size # remove extra rows if rows < len(self._gridItems): self._gridItems = self._gridItems[:rows] self._verSizes = self._verSizes[:rows] elif rows > len(self._gridItems): self._gridItems += [None]*(rows-len(self._gridItems)) self._verSizes += [(0,0)]*(rows-len(self._verSizes)) # remove extra cols if cols < len(self._gridItems): self._horSizes = self._verSizes[:cols] elif cols > len(self._gridItems): self._horSizes += [(0,0)]*(cols-len(self._horSizes)) for gridRow in range(len(self._gridItems)): if self._gridItems[gridRow] is None: self._gridItems[gridRow] = [None]*(cols) continue sizeRow = len(self._gridItems[gridRow]) if cols < sizeRow: self._gridItems[gridRow] = self._gridItems[gridRow][:cols] elif cols > sizeRow: self._gridItems[gridRow] += [None]*(cols-sizeRow)
[docs] def gridSize(self): return self._rows, self._cols
[docs] def getSizes(self): return self._horSizes, self._verSizes
[docs] def columnMinWidth(self): return self._columnMinWidth
[docs] def setColumnMinWidth(self, cmw): if self._columnMinWidth == cmw: return self._columnMinWidth = cmw self.update()
[docs] def rowMinHeight(self): return self._rowMinHeight
[docs] def setRowMinHeight(self, rmh): if self._rowMinHeight == rmh: return self._rowMinHeight = rmh self.update()
[docs] def gridItems(self): return self._gridItems
[docs] def repack(self): rown=coln= -1 # remove empty rows for r in reversed(range(self._rows)): if not(any([self.itemAtPosition(r,c) for c in range(self._cols)])): # the row is empty self._gridItems.pop(r) # Realign the rows for rown,r in enumerate(self._gridItems): for coln,w in enumerate(r): if w: w._row = rown self._reshapeGrid((rown+1,self._cols)) #remove empty cols unusedCols = [] for c in range(self._cols): if not(any([self.itemAtPosition(r,c) for r in range(self._rows)])): unusedCols.append(c) for c in reversed(unusedCols): for r in self._gridItems: r.pop(c) # Realign the cols for rown,r in enumerate(self._gridItems): for coln,w in enumerate(r): if w: w._col = coln self._reshapeGrid((rown+1,coln+1)) self.update()
[docs] def insertColumn(self, col): self._cols += 1 for c in self.children(): if c._col >= col: c._col += 1 for i,r in enumerate(self._gridItems): self._gridItems[i][col:col] = [None] self.update()
[docs] def insertRow(self, row): self._rows += 1 for c in self.children(): if c._row >= row: c._row += 1 self._gridItems.insert(row, [None]*self._cols) self.update()
# addWidget(self, widget, row, col) def addWidget(self, widget, row=None, col=None, rowspan=1, colspan=1, direction=TTkK.HORIZONTAL): '''Add the widget to this :py:class:`TTkGridLayout`, this function uses :meth:`~addItem` :param widget: the widget to be added :type widget: :py:class:`TTkWidget` :param int row: the row of the grid, optional, defaults to None :param int col: the col of the grid, optional, defaults to None :param int rowspan: the rows used by the widget, optional, defaults to 1 :param int colspan: the cols used by the widget, optional, defaults to 1 :param direction: The direction the new item will be added if row/col are not specified, defaults to defaults to :py:class:`~TermTk.TTkCore.constant.TTkConstant.Direction.HORIZONTAL` :type direction: :py:class:`TTkConstant.Direction` ''' TTkGridLayout.addWidgets(self,[widget], row, col, rowspan, colspan, direction) def addWidgets(self, widgets, row=None, col=None, rowspan=1, colspan=1, direction=TTkK.HORIZONTAL): '''Add the widgets to this :py:class:`TTkGridLayout`, this function uses :meth:`~addItem` :param widgets: the widgets to be added :type widgets: list of :py:class:`TTkWidget` :param int row: the row of the grid, optional, defaults to None :param int col: the col of the grid, optional, defaults to None :param int rowspan: the rows used by the widget, optional, defaults to 1 :param int colspan: the cols used by the widget, optional, defaults to 1 :param direction: The direction the new items will be added if row/col are not specified, defaults to defaults to :py:class:`~TermTk.TTkCore.constant.TTkConstant.Direction.HORIZONTAL` :type direction: :py:class:`TTkConstant.Direction` ''' self.removeWidgets(widgets) items = [w.widgetItem() for w in widgets] TTkGridLayout.addItems(self, items, row, col, rowspan, colspan, direction) for w in widgets: w.update() def replaceItem(self, item, index): pass def addItem(self, item, row=None, col=None, rowspan=1, colspan=1, direction=TTkK.HORIZONTAL): '''Add the item to this :py:class:`TTkGridLayout` :param item: the item to be added :type item: :py:class:`TTkLayoutItem` :param int row: the row of the grid, optional, defaults to None :param int col: the col of the grid, optional, defaults to None :param int rowspan: the rows used by the item, optional, defaults to 1 :param int colspan: the cols used by the item, optional, defaults to 1 :param direction: The direction the new item will be added if row/col are not specified, defaults to defaults to :py:class:`~TermTk.TTkCore.constant.TTkConstant.Direction.HORIZONTAL` :type direction: :py:class:`TTkConstant.Direction` ''' self.addItems([item],row,col,rowspan,colspan,direction) def addItems(self, items, row=None, col=None, rowspan=1, colspan=1, direction=TTkK.HORIZONTAL): '''Add the items to this :py:class:`TTkGridLayout` :param items: the items to be added :type items: list of :py:class:`TTkLayoutItem` :param int row: the row of the grid, optional, defaults to None :param int col: the col of the grid, optional, defaults to None :param int rowspan: the rows used by the item, optional, defaults to 1 :param int colspan: the cols used by the item, optional, defaults to 1 :param direction: The direction the new items will be added if row/col are not specified, defaults to defaults to :py:class:`~TermTk.TTkCore.constant.TTkConstant.Direction.HORIZONTAL` :type direction: :py:class:`TTkConstant.Direction` ''' nitems = len(items) self.removeItems(items) if row is None and col is None: # Append The widget at the end if direction==TTkK.HORIZONTAL: row = 0 col = self._cols else: row = self._rows col = 0 #retrieve the max col/rows to reshape the grid maxrow = row + rowspan * nitems maxcol = col + colspan * nitems for child in self.children(): maxrow = max(maxrow, child._row + child._rowspan) maxcol = max(maxcol, child._col + child._colspan) # TODO: This is RUBBISH!!! self._reshapeGrid(size=(maxrow,maxcol)) if self._gridItems[row][col] is not None: # TODO: Handle the LayoutItem self.removeItem(self._gridItems[row][col]) self._reshapeGrid(size=(maxrow,maxcol)) for item in items: item._row = row item._col = col item._rowspan = rowspan item._colspan = colspan self._gridItems[row][col] = item if direction==TTkK.HORIZONTAL: col += colspan else: row += rowspan TTkLayout.addItems(self, items) def removeItem(self, item): self.removeItems([item]) def removeItems(self, items): TTkLayout.removeItems(self, items) for gridRow in range(self._rows): for gridCol in range(self._cols): if self._gridItems[gridRow][gridCol] in items: self._gridItems[gridRow][gridCol] = None self._reshapeGrid(self._gridUsedsize()) def removeWidget(self, widget): self.removeWidgets([widget]) def removeWidgets(self, widgets): TTkLayout.removeWidgets(self, widgets) for gridRow in range(self._rows): for gridCol in range(self._cols): if self._gridItems[gridRow][gridCol] is not None and \ self._gridItems[gridRow][gridCol]._layoutItemType == TTkK.WidgetItem and \ self._gridItems[gridRow][gridCol].widget() in widgets: self._gridItems[gridRow][gridCol] = None self._reshapeGrid(self._gridUsedsize())
[docs] def itemAtPosition(self, row: int, col: int): if ( row<0 or row >= self._rows or col<0 or col >= self._cols ): return None if item := self._gridItems[row][col]: return item for item in self.children(): if item._row + item._rowspan > row >= item._row and \ item._col + item._colspan > col >= item._col : return item return None
[docs] def minimumColWidth(self, gridCol: int) -> int: colw = 0 anyItem = False for gridRow in range(self._rows): item = self.itemAtPosition(gridRow,gridCol) if item is not None and \ ( item._layoutItemType == TTkK.LayoutItem or item.isVisible() ): anyItem = True w = item.minimumWidthSpan(gridCol) if colw < w: colw = w if not anyItem: return self._columnMinWidth return colw
[docs] def minimumRowHeight(self, gridRow: int): rowh = 0 anyItem = False for gridCol in range(self._cols): item = self.itemAtPosition(gridRow,gridCol) if item is not None and \ ( item._layoutItemType == TTkK.LayoutItem or item.isVisible() ): anyItem = True h = item.minimumHeightSpan(gridRow) if rowh < h: rowh = h if not anyItem: return self._rowMinHeight return rowh
[docs] def maximumColWidth(self, gridCol: int) -> int: colw = 0x10000 anyItem = False for gridRow in range(self._rows): item = self.itemAtPosition(gridRow,gridCol) if item is not None and \ ( item._layoutItemType == TTkK.LayoutItem or item.isVisible() ): anyItem = True w = item.maximumWidthSpan(gridCol) if colw > w: colw = w if not anyItem: return self._columnMinWidth return colw
[docs] def maximumRowHeight(self, gridRow: int): rowh = 0x10000 anyItem = False for gridCol in range(self._cols): item = self.itemAtPosition(gridRow,gridCol) if item is not None and \ ( item._layoutItemType == TTkK.LayoutItem or item.isVisible() ): anyItem = True h = item.maximumHeightSpan(gridRow) if rowh > h: rowh = h if not anyItem: return self._rowMinHeight return rowh
[docs] def minimumWidth(self) -> int: ''' process the widgets and get the min size ''' minw = 0 for gridCol in range(self._cols): minw += self.minimumColWidth(gridCol) return minw
[docs] def minimumHeight(self) -> int: ''' process the widgets and get the min size ''' minh = 0 for gridRow in range(self._rows): minh += self.minimumRowHeight(gridRow) return minh
[docs] def maximumWidth(self) -> int: ''' process the widgets and get the min size ''' if not self._rows: return 0x1000 maxw = 0 for gridCol in range(self._cols): maxw += self.maximumColWidth(gridCol) return maxw
[docs] def maximumHeight(self) -> int: ''' process the widgets and get the min size ''' if not self._cols: return 0x1000 maxh = 0 for gridRow in range(self._rows): maxh += self.maximumRowHeight(gridRow) return maxh
def update(self, *args, **kwargs) -> None: _, _, w, h = self.geometry() newx, newy = 0, 0 # Sorted List of minimum heights # min max val # content IDs 0 1 2 3 sortedHeights = [ [i, self.minimumRowHeight(i), self.maximumRowHeight(i), -1] for i in range(self._rows) ] sortedWidths = [ [i, self.minimumColWidth(i), self.maximumColWidth(i), -1] for i in range(self._cols) ] sortedHeights = sorted(sortedHeights, key=lambda h: h[1]) sortedWidths = sorted(sortedWidths, key=lambda w: w[1]) minWidth = 0 minHeight = 0 for i in sortedWidths: minWidth += i[1] for i in sortedHeights: minHeight += i[1] if h < minHeight: h = minHeight if w < minWidth: w = minWidth # TTkLog.debug(f"Height: w,h:({w,h}) mh:{minHeight} sh:{sortedHeights}") # TTkLog.debug(f"width: w,h:({w,h}) mw:{minWidth} sw:{sortedWidths}") def parseSizes(sizes, space, out): iterate = True freeSpace = space leftSlots = len(sizes) while iterate and leftSlots > 0: iterate = False for item in sizes: if item[3] != -1: continue if freeSpace < 0: freeSpace=0 sliceSize = freeSpace//leftSlots mins = item[1] maxs = item[2] if sliceSize >= maxs: iterate = True freeSpace -= maxs leftSlots -= 1 item[3] = maxs elif sliceSize < mins: iterate = True freeSpace -= mins leftSlots -= 1 item[3] = mins # Push the sizes for item in sizes: out[item[0]] = [0,item[3]] if item[3] == -1: sliceSize = freeSpace//leftSlots out[item[0]] = [0,sliceSize] freeSpace -= sliceSize leftSlots -= 1 vertSizes = [None]*len(sortedHeights) horSizes = [None]*len(sortedWidths) parseSizes(sortedHeights,h, vertSizes) parseSizes(sortedWidths, w, horSizes) for i in horSizes: i[0] = newx newx += i[1] for i in vertSizes: i[0] = newy newy += i[1] # TTkLog.debug(f"h:{horSizes} v:{vertSizes}") # loop and set the geometry of any item for item in self.children(): col = item._col row = item._row x,y = horSizes[col][0], vertSizes[row][0] w = sum( horSizes[col+i][1] for i in range(item._colspan) ) h = sum( vertSizes[row+i][1] for i in range(item._rowspan) ) item.setGeometry(x, y, w, h) #TTkLog.debug(f"Children: {item.geometry()}") if item._layoutItemType == TTkK.WidgetItem and not item.isEmpty(): #TTkLog.debug(f"Children name: {item.widget()._name}") item.widget().update(*args, **kwargs) elif item._layoutItemType == TTkK.LayoutItem: item.update(*args, **kwargs) self._horSizes = horSizes self._verSizes = vertSizes return True