class Table(QAbstractTableModel): def __init__(self, ob, parent): QAbstractTableModel.__init__(self) # store things self.ob = ob self.stack = QUndoStack() self._parent = parent # Make sure we have Name information first # _header contains the row headers self._header = [ QVariant('X'), QVariant('#'), QVariant(getattr(self.ob, 'UniqueName', 'name'))] # _tooltips contains the tooltips self._tooltips = [ QVariant('An X indicates row is disabled %s'%bool), QVariant('Comment for row'), QVariant('Object name %s'%str)] # _required is a list of required columns self._required = [] # _defaults is a dict of column -> default values self._defaults = {0: QVariant(False), 1: QVariant("")} # _optional is a list of optional columns self._optional = [2] # _cItems is a dict of column -> QVariant QStringList items, returned # to combo box self._cItems = {} # _cValues is a dict of column -> list of QVariant values, stored when # corresponding label stored by combobox self._cValues = {} # _idents is a list of identifier lookup fields self._idents = [] # _types is a list of types for validation self._types = [bool, str, str] # rows is a list of rows. each row is a list of QVariants self.rows = [] # work out the header and descriptions from the ArgInfo object a = ob.ArgInfo # for required names just process the ArgType object for name in a.required_names: self.__processArgType(name, a.descriptions[name]) # for defaulted names give it a default too for name, default in zip(a.default_names, a.default_values): self.__processArgType(name, a.descriptions[name], default = default) # for optional names flag it as optional for name in a.optional_names: self.__processArgType(name, a.descriptions[name], optional = True) # maps (filt, without, upto) -> (timestamp, stringList) self._cachedNameList = {} # this is the top left item visible in the TableView widget self.topLeftIndex = None def __processArgType(self, name, ob, **args): # this is the column index col = len(self._header) # If it's a name then be careful not to add it twice if name == getattr(self.ob, 'UniqueName', 'name'): assert ob.typ == str, 'Object name must be a string' self._tooltips[2] = QVariant(ob.desc) else: # add the header, type and tooltip self._header.append(QVariant(name)) self._types.append(ob.typ) self._tooltips.append(QVariant(ob.desc)) # if we have a default value, set it if 'default' in args: if args['default'] is None: self._defaults[col] = QVariant('None') else: self._defaults[col] = QVariant(args['default']) # if this is optional elif 'optional' in args: self._optional.append(col) # it must be required else: self._required.append(col) # if we have combo box items if hasattr(ob, 'labels'): self._cItems[col] = QVariant( QStringList([QString(str(x)) for x in ob.labels])) # if we have combo box values if hasattr(ob, 'values'): self._cValues[col] = [QVariant(x) for x in ob.values] # if it's an ident if hasattr(ob, 'ident'): self._idents.append(col) def __convert(self, variant, typ): # convert to the requested type val = variant.toString() if typ == bool: if val.toLower() == QString("true"): return (True, True) elif val.toLower() == QString("false"): return (False, True) elif "$(" in val: return (val, True) return (val, False) elif typ == int: if "$(" in val: return (val, True) return variant.toInt() elif typ == float: if "$(" in val: return (val, True) _, ret = variant.toDouble() return (val, ret) elif typ == str: return (variant.toString(), True) else: return (variant, False) def createElements(self, doc, name): # create xml elements from this table header = [ str(x.toString()) for x in self._header ] for row in self.rows: el = doc.createElement(name) # lookup and add attributes for i in range(2, len(row)): if not row[i].isNull(): val = str(row[i].toString()) # We want True and False to be capitalised in the xml file if self._types[i] == bool and val in ["true", "false"]: val = val.title() el.setAttribute(header[i], val) if len(row[1].toString()) > 0: doc.documentElement.appendChild(doc.createComment(str(row[1].toString()).strip())) if row[0].toBool() == True: # can't put -- in a comment unfortunately... el = doc.createComment(el.toxml().replace("--", "&dashdash;")) doc.documentElement.appendChild(el) def addNode(self, node, commented = False, commentText = ""): # add xml nodes as rows in the table w = [] row = [ QVariant() ] * len(self._header) self.rows.append(row) if commented: row[0] = QVariant(True) else: row[0] = QVariant(False) for attr, value in node.attributes.items(): attr = str(attr) value = str(value) index = -1 for i, item in enumerate(self._header): if str(item.toString()) == attr: index = i break if index == -1: w.append('%s doesn\'t have attr %s' % (node.nodeName, attr)) continue typ = self._types[index] row[index] = QVariant(self.__convert(QVariant(value), typ)[0]) if not commented: invalid = self._isInvalid(row[index], len(self.rows)-1, index) if invalid: w.append('%s.%s: %s' %(node.nodeName, attr, invalid)) # add the row to the table if commentText: row[1] = QVariant(commentText) return w def flags(self, index): return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable def rowCount(self, parent = None): return len(self.rows) def columnCount(self, parent = None): return len(self._header) def headerData(self, section, orientation, role = Qt.DisplayRole ): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self._header[section] elif self.rows and self.rows[section] and \ not self.rows[section][2].isNull(): return self.rows[section][2] else: return QVariant('[row %s]' % (section+1)) elif role == Qt.ToolTipRole and orientation == Qt.Horizontal: return self._tooltips[section] else: return QVariant() # put the change request on the undo stack def setData(self, index, val, role=Qt.EditRole): if role == Qt.EditRole and \ val != self.rows[index.row()][index.column()]: col = index.column() if col in self._cValues: # lookup the display in the list of _cItems for i, v in enumerate(self._cItems[col].toStringList()): if v.toLower() == val.toString().toLower(): val = self._cValues[col][i] self.stack.push( ChangeValueCommand(index.row(), index.column(), val, self)) return True else: return False def insertRows(self, row, count, parent = QModelIndex()): if count > 1: self.stack.beginMacro('Insert rows %d..%d'%(row+1, row+count)) for row in range(row, row+count): self.stack.push(RowCommand(row, self, parent)) if count > 1: self.stack.endMacro() def removeRows(self, row, count, parent = QModelIndex()): if count > 1: self.stack.beginMacro('Remove rows %d..%d'%(row+1, row+count)) for row in reversed(range(row, row+count)): self.stack.push(RowCommand(row, self, parent, False)) if count > 1: self.stack.endMacro() def sectionMoved(self, logicalIndex, oldVisualIndex, newVisualIndex, parent = QModelIndex()): assert oldVisualIndex == logicalIndex, \ "oldVisualIndex %d should be equal to logicalIndex %d" % ( oldVisualIndex, logicalIndex) self.stack.beginMacro('Move row %d to %d'%(oldVisualIndex, newVisualIndex)) # grab the old data olddata = [QVariant(x) for x in self.rows[oldVisualIndex]] # delete the old row self.stack.push(RowCommand(oldVisualIndex, self, parent, False)) # create a command to make a new row with the old data cmd = RowCommand(newVisualIndex, self, parent) cmd.rowdata = olddata self.stack.push(cmd) self.stack.endMacro() def _isCommented(self, row): return self.rows[row][0].toBool() def _nameList(self, filt = None, without = None, upto = None): # need to search all tables for a string list of object names # filt is a ModuleBase subclass to filter by # without is a row number to exclude from the current table # upto means only look at objects up to "upto" row in the current table if (filt, without, upto) in self._cachedNameList: timestamp, sl = self._cachedNameList[(filt, without, upto)] if self._parent.lastModified() < timestamp: return sl sl = QStringList() for name in self._parent.getTableNames(): table = self._parent._tables[name] # if we have a filter, then make sure this table is a subclass of it if filt is not None and type(filt) == types.ClassType and \ type(table.ob) == types.ClassType and \ not issubclass(table.ob, filt): # if we are only going up to a certain table and this is it if table == self and upto is not None: return sl continue for i,trow in enumerate(table.rows): if table == self: # if the current table is self, make sure we are excluding # the without row if without is not None and without == i: continue # make sure we only go up to upto if upto is not None and upto == i: return sl # add a non-null name, which is not commented out to the list if not trow[2].isNull() and not \ (not trow[0].isNull() and trow[0].toBool() == True): sl.append(trow[2].toString()) # store the cached value self._cachedNameList[(filt, without, upto)] = (time.time(), sl) return sl def _isInvalid(self, value, row, col): # check that required rows are filled in if value.isNull(): if col in self._required: return 'Required argument not filled in' else: return False # check that names are unique elif col == 2: name = value.toString() index = self._nameList(without = row).indexOf(name) if index != -1: return 'Object with name "%s" already exists' % name # check that idents are valid elif col in self._idents: name = value.toString() ob = self._types[col] index = self._nameList(filt = ob, upto = row).indexOf(name) if index == -1: return 'Can\'t perform identifier lookup on "%s"' % name # check that enums are valid elif col in self._cValues: if not max([value == x for x in self._cValues[col]]): return '"%s" is not a supported enum' % value.toString() # check that choices are valid elif col in self._cItems: if not max( [value == QVariant(x) for x in self._cItems[col].toStringList()]): return '"%s" is not a supported choice' % value.toString() # check the type of basetypes else: typ = self._types[col] v, ret = self.__convert(value, typ) if ret != True: return 'Cannot convert "%s" to %s' % (value.toString(), typ) return False def _isDefault(self, value, col): return value.isNull() and col in self._defaults def data(self, index, role): col = index.column() row = index.row() value = self.rows[row][col] # default view if role == Qt.DisplayRole: # comment row if col == 1: if len(value.toString()) > 0: return QVariant("#..") else: return QVariant() # if the cell is defaulted, display the default value elif self._isDefault(value, col): value = self._defaults[col] # if we've got a combo box lookup the appropriate value for the enum if col in self._cValues: # lookup the display in the list of _cItems for i, v in enumerate(self._cValues[col]): if v.toString() == value.toString(): return QVariant(self._cItems[col].toStringList()[i]) # display commented out rows as X elif col == 0: if value.toBool(): return QVariant(QString('X')) else: return QVariant(QString('')) # empty string rows should be "" elif not value.isNull() and self._types[col] == str and \ str(value.toString()) == '' and col != 1: value = QVariant(QString('""')) return value # text editor elif role == Qt.EditRole: # if the cell is defaulted, display the default value if self._isDefault(value, col): value = self._defaults[col] # if we've got a combo box lookup the appropriate value for the enum if col in self._cValues: # lookup the display in the list of _cItems for i, v in enumerate(self._cValues[col]): if v.toString() == value.toString(): return QVariant(self._cItems[col].toStringList()[i]) # empty string rows should be "" elif not value.isNull() and self._types[col] == str and \ str(value.toString()) == '' and col != 1: value = QVariant(QString('""')) return value elif role == Qt.ToolTipRole: # tooltip error = self._isInvalid(value, row, col) text = str(self._tooltips[col].toString()) if error: text = '***Error: %s\n%s'%(error, text) if col in self._idents: lines = ['\nPossible Values: '] for name in self._nameList(filt = self._types[col], upto = row): if len(lines[-1]) > 80: lines.append('') lines[-1] += str(name) + ', ' text += '\n'.join(lines).rstrip(' ,') if col == 1 and len(value.toString()) > 0: text += ":\n\n" + value.toString() return QVariant(text) elif role == Qt.ForegroundRole: # cell foreground if self._isCommented(row): # comment return QVariant(QColor(120,140,180)) if self._isDefault(value, col): # is default arg (always valid) return QVariant(QColor(160,160,160)) elif self._isInvalid(value, row, col): # invalid return QVariant(QColor(255,0,0)) else: # valid return QVariant(QColor(0,0,0)) elif role == Qt.BackgroundRole: # cell background if self._isCommented(row): # commented return QVariant(QColor(160,180,220)) elif self._isInvalid(value, row, col): # invalid return QVariant(QColor(255,200,200)) elif col in self._defaults: # has default return QVariant(QColor(255,255,240)) elif col in self._optional: #is optional return QVariant(QColor(180,180,180)) else: # valid return QVariant(QColor(250,250,250)) elif role == Qt.UserRole: # combo box asking for list of items if col in self._idents: return QVariant( self._nameList(filt = self._types[col], upto = row)) elif col in self._cItems: return self._cItems[col] else: return QVariant() else: return QVariant() def clearIndexes(self, indexes): # clear cells from a list of QModelIndex's begun = False for item in indexes: if not self.rows[item.row()][item.column()].isNull(): if not begun: begun = True celltexts = [ '(%s, %d)' % (self._header[c.column()].toString(), c.row() + 1) \ for c in indexes ] self.stack.beginMacro('Cleared Cells: '+' '.join(celltexts)) self.setData(item, QVariant(), Qt.EditRole) if begun: self.stack.endMacro()