You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

340 lines
13 KiB

#!/usr/bin/env python3
# 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
[Tutorial](https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tutorial/002-layout.md)
The grid layout allows an automatic place all the widgets in a grid
the empty rows/cols are resized to the "columnMinHeight,columnMinWidth" parameters
TTkGridLayout columnMinWidth
Widget1 Widget2 Widget3
(0,0) (0,1) (0,3)
columnMinHeight
Widget4
(2,0)
Widget5
(3,3)
'''
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.log import TTkLog
from TermTk.TTkLayouts.layout import TTkLayout, TTkWidgetItem
class TTkGridLayout(TTkLayout):
__slots__ = ('_gridItems','_columnMinWidth','_columnMinHeight')
def __init__(self, *args, **kwargs):
'''
TTkGridLayout constructor
Args:
columnMinWidth (int, optional, default=0): the minimum width of the column
columnMinHeight (int, optional, default=0): the minimum height of the column
'''
TTkLayout.__init__(self, *args, **kwargs)
self._gridItems = [[]]
self._columnMinWidth = kwargs.get('columnMinWidth',0)
self._columnMinHeight = kwargs.get('columnMinHeight',0)
def _gridUsedsize(self):
rows = 1
cols = 0
for gridRow in range(len(self._gridItems)):
if rows < gridRow:
rows = gridRow
for gridCol in range(len(self._gridItems[0])):
if self._gridItems[gridRow][gridCol] is not None:
if cols < gridCol:
cols = gridCol
return (rows+1, cols+1)
def _reshapeGrid(self, size):
rows = size[0]
cols = size[1]
# remove extra rows
if rows < len(self._gridItems):
self._gridItems = self._gridItems[:rows]
elif rows > len(self._gridItems):
self._gridItems += [None]*(rows-len(self._gridItems))
# remove extra cols
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)
# addWidget(self, widget, row, col)
def addWidget(self, *args, **kwargs):
widget = args[0]
self.removeWidget(widget)
item = TTkWidgetItem(widget=widget)
if len(args) == 3:
TTkGridLayout.addItem(self, item, args[1], args[2])
else:
TTkGridLayout.addItem(self, item)
widget.update()
def replaceItem(self, item, index): pass
def addItem(self, *args, **kwargs):
item = args[0]
self.removeItem(item)
if len(args) == 3:
row = args[1]
col = args[2]
else:
# Append The widget at the end
row = 0
col = len(self._gridItems[0])
#retrieve the max col/rows to reshape the grid
maxrow = row
maxcol = col
for child in self.children():
if maxrow < child._row: maxrow = child._row
if maxcol < child._col: maxcol = child._col
# reshape the gridItems
maxrow += 1
maxcol += 1
# 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))
item._row = row
item._col = col
self._gridItems[row][col] = item
TTkLayout.addItem(self, item)
def removeItem(self, item):
TTkLayout.removeItem(self, item)
for gridRow in range(len(self._gridItems)):
for gridCol in range(len(self._gridItems[0])):
if self._gridItems[gridRow][gridCol] == item:
self._gridItems[gridRow][gridCol] = None
self._reshapeGrid(self._gridUsedsize())
def removeWidget(self, widget):
TTkLayout.removeWidget(self, widget)
for gridRow in range(len(self._gridItems)):
for gridCol in range(len(self._gridItems[0])):
if self._gridItems[gridRow][gridCol] is not None and \
self._gridItems[gridRow][gridCol].layoutItemType == TTkK.WidgetItem and \
self._gridItems[gridRow][gridCol].widget() == widget:
self._gridItems[gridRow][gridCol] = None
self._reshapeGrid(self._gridUsedsize())
def itemAtPosition(self, row: int, col: int):
if row >= len(self._gridItems) or \
col >= len(self._gridItems[0]):
return None
return self._gridItems[row][col]
def minimumColWidth(self, gridCol: int) -> int:
colw = 0
anyItem = False
for gridRow in range(len(self._gridItems)):
item = self._gridItems[gridRow][gridCol]
if item is not None and \
( item.layoutItemType == TTkK.LayoutItem or item.isVisible() ):
anyItem = True
w = item.minimumWidth()
if colw < w:
colw = w
if not anyItem:
return self._columnMinWidth
return colw
def minimumRowHeight(self, gridRow: int):
rowh = 0
anyItem = False
for item in self._gridItems[gridRow]:
if item is not None and \
( item.layoutItemType == TTkK.LayoutItem or item.isVisible() ):
anyItem = True
h = item.minimumHeight()
if rowh < h:
rowh = h
if not anyItem:
return self._columnMinHeight
return rowh
def maximumColWidth(self, gridCol: int) -> int:
colw = 0x10000
anyItem = False
for gridRow in range(len(self._gridItems)):
item = self._gridItems[gridRow][gridCol]
if item is not None and \
( item.layoutItemType == TTkK.LayoutItem or item.isVisible() ):
anyItem = True
w = item.maximumWidth()
if colw > w:
colw = w
if not anyItem:
return self._columnMinWidth
return colw
def maximumRowHeight(self, gridRow: int):
rowh = 0x10000
anyItem = False
for item in self._gridItems[gridRow]:
if item is not None and \
( item.layoutItemType == TTkK.LayoutItem or item.isVisible() ):
anyItem = True
h = item.maximumHeight()
if rowh > h:
rowh = h
if not anyItem:
return self._columnMinHeight
return rowh
def minimumWidth(self) -> int:
''' process the widgets and get the min size '''
minw = 0
for gridCol in range(len(self._gridItems[0])):
minw += self.minimumColWidth(gridCol)
return minw
def minimumHeight(self) -> int:
''' process the widgets and get the min size '''
minh = 0
for gridRow in range(len(self._gridItems)):
minh += self.minimumRowHeight(gridRow)
return minh
def maximumWidth(self) -> int:
''' process the widgets and get the min size '''
if not self._gridItems[0]:
return 0x1000
maxw = 0
for gridCol in range(len(self._gridItems[0])):
maxw += self.maximumColWidth(gridCol)
return maxw
def maximumHeight(self) -> int:
''' process the widgets and get the min size '''
if not self._gridItems[0]:
return 0x1000
maxh = 0
for gridRow in range(len(self._gridItems)):
maxh += self.maximumRowHeight(gridRow)
return maxh
def update(self, *args, **kwargs):
x, y, w, h = self.geometry()
newx, newy = x, y
# 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(len(self._gridItems)) ]
sortedWidths = [ [i, self.minimumColWidth(i), self.maximumColWidth(i), -1] for i in range(len(self._gridItems[0])) ]
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"w,h:({w,h}) mh:{minHeight} sh:{sortedHeights}")
#TTkLog.debug(f"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
item.setGeometry(
horSizes[col][0], vertSizes[row][0] ,
horSizes[col][1], vertSizes[row][1] )
#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)
return True