# -*- coding: utf-8 -*- """ This module holds the TableLayout and TableCell classes They are used to intelligent layout widgets in cells in a table... """ # Copyright 2002, 2003 St James Software # # This file is part of jToolkit. # # jToolkit is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # jToolkit is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with jToolkit; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from jToolkit.widgets import widgets class TableCell(widgets.ContentWidget): def __init__(self, contents, newattribs={}, rowspan=1, colspan=1): widgets.ContentWidget.__init__(self, 'td', contents) self.attribs = {'type':'td', 'rowspan':rowspan, 'colspan':colspan} self.overrideattribs(newattribs) self.hidden = 0 self.width = 0 self.isempty = 0 def gethtml(self): if self.hidden: return "" else: return widgets.ContentWidget.gethtml(self) class TableLayout(widgets.Widget): # maintains a dictionary of cells in rows, indexed by row number and column number def __init__(self, newattribs = {}): widgets.Widget.__init__(self, "TABLE") self.attribs = {'cellspacing':'1', 'cellpadding':'1'} self.overrideattribs(newattribs) self.rowsdict = {} def hasrow(self, rownum): return rownum in self.rowsdict def setrow(self, rownum): self.rowsdict[rownum] = {} def getrow(self, rownum): return self.rowsdict.get(rownum, {}) def hascell(self, rownum, colnum): return self.hasrow(rownum) and colnum in self.getrow(rownum) def setcell(self, rownum, colnum, value): """sets the cell in rownum, colnum position to value""" # add the row if it doesn't yet exist if not self.hasrow(rownum): self.setrow(rownum) self.getrow(rownum)[colnum] = value def delcell(self, rownum, colnum): """removes the cell in rownum, colnum position""" if not self.hasrow(rownum): raise KeyError, "row %d does not exist" % rownum else: del self.getrow(rownum)[colnum] def getcell(self, rownum, colnum): """returns the cell in position rownum, colnum""" cell = self.getrow(rownum).get(colnum, None) if cell is None: return self.newemptycell() else: return cell def newemptycell(self): emptycell = TableCell('') emptycell.width = 0 emptycell.isempty = 1 return emptycell def maxrowspan(self, row): if len(row) == 0: return 0 return max([cell.attribs['rowspan'] for cell in row.itervalues()]) def rowmaxcolnum(self, row): if len(row) == 0: return 0 return max([col + cell.attribs['colspan']-1 for col, cell in row.iteritems()]) def minrownum(self): if len(self.rowsdict) == 0: return 0 return min(self.rowsdict.iterkeys()) def maxrownum(self): if len(self.rowsdict) == 0: return 0 return max([rownum + self.maxrowspan(row)-1 for rownum, row in self.rowsdict.iteritems()]) def rowrange(self): return range(self.minrownum(), self.maxrownum()+1) def mincolnum(self): if len(self.rowsdict) == 0: return 0 return min([min(row.iterkeys()) for row in self.rowsdict.itervalues()]) def maxcolnum(self): if len(self.rowsdict) == 0: return 0 return max([self.rowmaxcolnum(row) for row in self.rowsdict.itervalues()]) def colrange(self): return range(self.mincolnum(), self.maxcolnum()+1) def getrowcontents(self, rownum): contentslist = [self.getcell(rownum, colnum).gethtml() for colnum in self.colrange()] # TODO: investigate ways to neaten this up while still preventing ASCII decoding error... typelist = [type(p) for p in contentslist] if unicode in typelist and str in typelist: for n in range(len(contentslist)): if isinstance(contentslist[n], str): contentslist[n] = contentslist[n].decode('utf8') return "" + "".join(contentslist) + "\r" def getcontents(self): contentslist = [self.getrowcontents(rownum) for rownum in self.rowrange()] # TODO: investigate ways to neaten this up while still preventing ASCII decoding error... typelist = [type(p) for p in contentslist] if unicode in typelist and str in typelist: for n in range(len(contentslist)): if isinstance(contentslist[n], str): contentslist[n] = contentslist[n].decode('utf8') return "".join(contentslist) def rowempty(self, rownum): """returns whether this row is empty (contains no non-empty cells)""" for colnum in self.colrange(): if self.hascell(rownum, colnum) and not self.getcell(rownum, colnum).isempty: return 0 return 1 def colempty(self, colnum): """returns whether this column is empty (contains no non-empty cells)""" for rownum in self.rowrange(): if self.hascell(rownum, colnum) and not self.getcell(rownum, colnum).isempty: return 0 return 1 def adjustrowspan(self, rownum, deletedrownum): """adjusts all the rowspans for this rownum to reflect the fact that deletedrownum is gone""" for colnum in self.colrange(): if self.hascell(rownum, colnum): cell = self.getcell(rownum, colnum) if cell.attribs['rowspan'] > deletedrownum - rownum: cell.attribs['rowspan'] -= 1 def adjustcolspan(self, colnum, deletedcolnum): """adjusts all the colspans for this colnum to reflect the dact that deletedcolnum is gone""" for rownum in self.rowrange(): if self.hascell(rownum, colnum): cell = self.getcell(rownum, colnum) if cell.attribs['colspan'] > deletedcolnum - colnum: cell.attribs['colspan'] -= 1 def removerow(self, rownum): """removes the row from the table, shrinking other rownums, and adjusting rowspans...""" if rownum in self.rowsdict: del self.rowsdict[rownum] for otherrownum in self.rowrange(): # adjust rowspans for rows before this one if not otherrownum in self.rowsdict: continue if otherrownum < rownum: # do a general check to see if this is neccessary at all for this row if self.maxrowspan(self.rowsdict[otherrownum]) > rownum - otherrownum: self.adjustrowspan(otherrownum, rownum) elif otherrownum > rownum: # shift row one up... self.rowsdict[otherrownum-1] = self.rowsdict[otherrownum] del self.rowsdict[otherrownum] def removecol(self, colnum): """removes the column from the table, shrinking other colnums, and adjusting colspans...""" for rownum in self.rowrange(): if self.hascell(rownum, colnum): self.delcell(rownum, colnum) for othercolnum in self.colrange(): # adjust colspans for cols before this one if othercolnum < colnum: self.adjustcolspan(othercolnum, colnum) elif othercolnum > colnum: # shift col one up... for rownum in self.rowrange(): if self.hascell(rownum, othercolnum): cell = self.getcell(rownum, othercolnum) self.delcell(rownum, othercolnum) self.setcell(rownum, othercolnum-1, cell) def shrinkrange(self): """removes all the empty rows and columns, reducing the range...""" if len(self.rowsdict) == 0: return # detect which rows are empty emptyrows = [] for rownum in self.rowrange(): if self.rowempty(rownum): emptyrows.append(rownum) # remove the rows, adjusting for the rows that have already been removed... adjustment = 0 for rownum in emptyrows: self.removerow(rownum-adjustment) adjustment += 1 # detect which columns are empty emptycols = [] for colnum in self.colrange(): if self.colempty(colnum): emptycols.append(colnum) # remove the colums, adjusting for the colums that have already been removed... adjustment = 0 for colnum in emptycols: self.removecol(colnum-adjustment) adjustment += 1 def fillemptycells(self, emptycellmaker=None): if emptycellmaker is None: emptycellmaker = self.newemptycell for rownum in self.rowrange(): for colnum in self.colrange(): if not self.hascell(rownum, colnum): self.setcell(rownum, colnum, emptycellmaker()) def mergeduplicates(self, excluderows=[], emptycellmaker=None): """merge cells that are the same object...""" if emptycellmaker is None: emptycellmaker = self.newemptycell for colnum in self.colrange(): lastcell = None for rownum in self.rowrange(): if rownum in excluderows: # allows us to skip the title etc continue cell = self.getcell(rownum, colnum) if cell == lastcell and lastcell is not None: lastcell.attribs['rowspan'] += 1 self.setcell(rownum, colnum, emptycellmaker()) else: lastcell = cell def hidecoveredcells(self): """hide blank cells that are underneath another cell with rowspan/colspan""" self.fillemptycells() for rownum in self.rowrange(): for colnum in self.colrange(): cell = self.getcell(rownum,colnum) if not cell.hidden: for blankrowoffset in range(0,cell.attribs['rowspan']): for blankcoloffset in range(0,cell.attribs['colspan']): # don't overwrite the current cell! if blankrowoffset != 0 or blankcoloffset != 0: self.hidecell(rownum+blankrowoffset, colnum+blankcoloffset) def hidecell(self, rownum, colnum): cell = self.getcell(rownum, colnum) cell.hidden = 1 def rowwidth(self, rownum): width = 0 for cell in self.getrow(rownum).itervalues(): width += getattr(cell, 'width', 0) return width def maxwidth(self): return max([self.rowwidth(rownum) for rownum in self.rowrange()]) def calcweights(self): # calculate column widths for all the cells maxwidth = self.maxwidth() for rownum in self.rowrange(): for colnum in self.colrange(): cell = self.getcell(rownum, colnum) if cell.width != 0: width=100.0*cell.width/maxwidth styleattribs = {'width':'%d%%' % int(width)} cell.overrideattribs({'style':styleattribs})