#!/usr/bin/env python #**************************************************************************** # treeitem.py, provides non-GUI base classes for tree items # # TreeLine, an information storage program # Copyright (C) 2005, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, Version 2. This program is # distributed in the hope that it will be useful, but WITTHOUT ANY WARRANTY. #**************************************************************************** try: from __main__ import __version__ except ImportError: __version__ = '' import copy, numbering, os, codecs from xml.sax.saxutils import escape from nodeformat import NodeFormat from output import OutputGroup, OutputItem import treedoc import globalref _defaultTitle = _('New') class TreeItem: """Data storage item for tree structure""" def __init__(self, parent, nodeFormat, initText='', addDefaultData=False): self.parent = parent self.nodeFormat = nodeFormat self.data = {} if initText: self.setTitle(initText) if addDefaultData: nodeFormat.setInitDefaultData(self.data) self.open = False self.childList = [] self.viewData = None self.level = 0 # updated only by numbered descend list def title(self): """Return title text""" text = self.nodeFormat.formatTitle(self) if not text: text = _('[BLANK TITLE]') return text def setTitle(self, title, addUndo=False): """Set title text, return True if changed successfully""" title = u' '.join(title.split()) if title: if self.nodeFormat.setTitle(title, self, addUndo): globalref.docRef.modified = True return True return False def changeType(self, newFormat): """Change nodeFormat to new, update title fields if they'd be blank""" origTitle = self.nodeFormat.formatTitle(self) self.nodeFormat = newFormat if not self.nodeFormat.formatTitle(self): self.nodeFormat.setTitle(origTitle, self, False) def formatText(self, skipEmpty=True, addPrefix=False, addSuffix=False, \ internal=False): """Return list of formatted text lines""" return self.nodeFormat.formatText(self, skipEmpty, addPrefix, \ addSuffix, internal) def formatChildText(self, skipEmpty=True, addExtra=False, internal=False): """Return list of all children's formatted text lines""" if not self.childList: return [] result = [] sep = [] prefixAdded = False for child, next in map(None, self.childList, self.childList[1:]): addPrefix = not prefixAdded prefixAdded = True addSuffix = False if not next or not child.nodeFormat.equalPrefix(next.nodeFormat): addSuffix = True prefixAdded = False result.extend(sep + child.formatText(skipEmpty, addPrefix, \ addSuffix, internal)) sep = globalref.docRef.spaceBetween and [u''] or [] return result def refFieldText(self): """Return text from ref field""" return self.data.get(self.nodeFormat.refField.name, '') def outputItemList(self, includeRoot=True, openOnly=False, \ addAnchors=False, level=0): """Return list of OutputItems""" outList = OutputGroup() if includeRoot: lines = self.formatText() if not lines: lines = [''] if addAnchors: for anchor in self.refFieldText().split('\n'): if anchor: lines[0] = u'%s' % (anchor, lines[0]) outList.append(OutputItem(lines, level)) if not self.prevSibling(): outList[-1].firstSibling = True if not self.nextSibling(): outList[-1].lastSibling = True if self.childList: outList[-1].hasChildren = True outList[-1].prefix = self.nodeFormat.sibPrefix outList[-1].suffix = self.nodeFormat.sibSuffix else: level -= 1 if self.open or not openOnly: for child in self.childList: outList.extend(child.outputItemList(True, openOnly, \ addAnchors, level + 1)) return outList def branchXml(self, typeList=None, writeOptions=False): """Return list of xml lines, include format info if not in typeList""" if typeList == None: # default writes no format info typeList = globalref.docRef.treeFormats xmlList = [u'<%s item="y"' % escape(self.nodeFormat.name, \ treedoc.escDict)] if writeOptions: if not globalref.docRef.spaceBetween: xmlList[0] += u' nospace="y"' if not globalref.docRef.lineBreaks: xmlList[0] += u' nobreaks="y"' if not globalref.docRef.formHtml: xmlList[0] += u' nohtml="y"' if globalref.docRef.childFieldSep != \ globalref.docRef.childFieldSepDflt: xmlList[0] += u' childsep="%s"' % \ escape(globalref.docRef.childFieldSep, \ treedoc.escDict) if globalref.docRef.spellChkLang: xmlList[0] += u' spellchk="%s"' % \ escape(globalref.docRef.spellChkLang, \ treedoc.escDict) if globalref.docRef.xslCssLink: xmlList[0] += u' xslcss="%s"' % globalref.docRef.xslCssLink if __version__: xmlList[0] += u' tlversion="%s"' % \ escape(__version__, treedoc.escDict) addFormat = self.nodeFormat not in typeList # writes format on 1st only nodeFormat = self.nodeFormat if addFormat: xmlList.extend(nodeFormat.formatXml()) typeList.append(self.nodeFormat) xmlList[-1] += u'>' for field in nodeFormat.fieldList: text = self.data.get(field.name, '') if text or addFormat: escKey = escape(field.englishName(), treedoc.escDict) fieldFormat = '' if addFormat: fieldFormat = field.writeXml() if field == nodeFormat.refField: fieldFormat += u' ref="y"' xmlList.append(u'<%s%s>%s%s>' % \ (escKey, fieldFormat, \ escape(text, treedoc.escDict), escKey)) for child in self.childList: xmlList.extend(child.branchXml(typeList)) if writeOptions: # write format info for any unused formats for format in globalref.docRef.treeFormats: if format not in typeList: name = escape(format.name, treedoc.escDict) xmlList.append(u'<%s item="n"' % name) xmlList.extend(format.formatXml()) xmlList[-1] += u'>' for field in format.fieldList: escKey = escape(field.englishName(), treedoc.escDict) fieldFormat = field.writeXml() if field == format.refField: fieldFormat += u' ref="y"' xmlList.append(u'<%s%s>%s>' % (escKey, fieldFormat, \ escKey)) xmlList.append(u'%s>' % name) xmlList.append(u'%s>' % escape(self.nodeFormat.name, \ treedoc.escDict)) return xmlList def exportToText(self, level=0, openOnly=False): """Write tabbed list of descendants titles""" textList = [u'\t' * level + self.title()] if self.open or not openOnly: for child in self.childList: textList.extend(child.exportToText(level + 1, openOnly)) return textList def exportDir(self, parentTitle=None, header='', footer=''): """Write dir structure with html tables""" if not self.childList: return try: dirName = self.data.get(self.nodeFormat.fieldList[0].name, '').\ encode(treedoc.TreeDoc.localEncoding, \ 'replace') if not os.access(dirName, os.R_OK): os.mkdir(dirName, 0755) os.chdir(dirName) except (OSError, ValueError, UnicodeError): print 'Error - cannot make directory', dirName raise IOError(_('Error - cannot make directory')) title = self.title() lines = [u'', u'', u'
', \ u'', u'%s' '%s
' % \ (label, parentTitle)) lines.extend([u'| %s | ' % cell for cell in headings]) lines.append(u'
|---|
| %s | ' % cell for cell in textList]) lines.append(u'
' % indent) for child in self.childList: result.extend(child.exportHtmlBookmarks(level + 1)) result.append(u'%s
' % indent) return result def exportGenericXml(self, textFieldName, level=0): """Return text list with descendant nodes in generic XML format""" indentsPerLevel = 3 indent = ' ' * (indentsPerLevel * level) result = u'%s<%s' % (indent, self.nodeFormat.name) for fieldName in self.nodeFormat.fieldNames(): text = self.data.get(fieldName, '') if text and fieldName != textFieldName: result = u'%s %s="%s"' % (result, fieldName, \ escape(text, treedoc.escDict)) result += u'>' if textFieldName in self.nodeFormat.fieldNames(): text = self.data.get(textFieldName, '') if text: result += escape(text, treedoc.escDict) if not self.childList: return [u'%s%s>' % (result, self.nodeFormat.name)] result = [result] for child in self.childList: result.extend(child.exportGenericXml(textFieldName, level+1)) result.append(u'%s%s>' % (indent, self.nodeFormat.name)) return result def loadTabbedChildren(self, bufList, level=0): """Recursive read of TreeItems from tabbed buffer""" while bufList: if bufList[0][0] == level + 1: buf = bufList.pop(0) child = TreeItem(self, self.nodeFormat, buf[1]) self.childList.append(child) if not child.loadTabbedChildren(bufList, level + 1): return False elif 0 < bufList[0][0] <= level: return True else: return False return True def editChildList(self, textList): """Update child names and order from textList, update undos and view""" stripList = filter(None, [u' '.join(text.split()) for text in textList]) if len(stripList) == len(self.childList): # assume rename if length is same for child, text in zip(self.childList, stripList): if child.title() != text: child.setTitle(text, True) globalref.updateViewTreeItem(child, True) else: # find new child positions if length differs oldLen = len(self.childList) newType = globalref.docRef.treeFormats.\ findFormat(self.nodeFormat.childType) if not newType: newType = oldLen and self.childList[0].nodeFormat or \ self.nodeFormat globalref.docRef.undoStore.addChildListUndo(self) newChildList = [] for text in stripList: try: newChildList.append(self.childList.pop(\ [child.title() for child in self.childList].index(text))) except ValueError: newChildList.append(TreeItem(self, newType, text, True)) if oldLen == 0 and newChildList: self.open = True self.childList = newChildList globalref.updateViewTree() globalref.docRef.modified = True def descendantList(self, inclClosed=False, level=0): """Recursive list of TreeItems, default to open only, sets level num, returns list""" descendList = [self] self.level = level if self.open or inclClosed: for child in self.childList: descendList.extend(child.descendantList(inclClosed, level+1)) return descendList def descendantGen(self): """Return generator to step thru all descendants (including closed), include self""" yield self for child in self.childList: for item in child.descendantGen(): yield item def descendantGenNoRoot(self): """Return generator to step thru all descendants (including closed), do not include self""" for child in self.childList: yield child for item in child.descendantGenNoRoot(): yield item def ancestorList(self): """Return list all parents and grandparents of self""" item = self.parent result = [] while item: result.append(item) item = item.parent return result def allAncestorsOpen(self): """Returns True if all ancestors are set open""" closeList = [item for item in self.ancestorList() if not item.open] if closeList: return False return True def hasDescendant(self, child): """Return True if self has descendant child""" for item in self.descendantGen(): if item is child: return True return False def lastDescendant(self, inclClosed=False): """Return self's last descendant, required to be open if not inclClosed""" item = self while True: if item.childList and (item.open or inclClosed): item = item.childList[-1] else: return item def usesType(self, nodeFormat): """Return True if dataType is used by self or descendants""" for item in self.descendantGen(): if item.nodeFormat == nodeFormat: return True return False def numChildren(self): """Return number of children""" return len(self.childList) def childPos(self, child): """Return the number of the referenced child or -1""" for num, item in enumerate(self.childList): if item is child: return num return -1 def childText(self): """Return list of child item strings (not recursive)""" return [child.title() for child in self.childList] def maxDescendLevel(self, thisLevel=0): """Return max number of levels below this node""" if not self.childList: return thisLevel return max([child.maxDescendLevel(thisLevel + 1) for child \ in self.childList]) def prevSibling(self): """Return nearest older sibling or None""" if self.parent: i = self.parent.childPos(self) if i > 0: return self.parent.childList[i-1] return None def nextSibling(self): """Return next younger sibling or None""" if self.parent: i = self.parent.childPos(self) + 1 if i < len(self.parent.childList): return self.parent.childList[i] return None def prevItem(self, inclClosed=False): """Return previous sibling or parent or None""" sib = self.prevSibling() if sib: while sib.numChildren() and (sib.open or inclClosed): sib = sib.childList[-1] return sib return self.parent def nextItem(self, inclClosed=False): """Return first child, next sibling or ancestors next sibling or None""" if self.childList and (self.open or inclClosed): return self.childList[0] ancestor = self while ancestor: sib = ancestor.nextSibling() if sib: return sib ancestor = ancestor.parent return None def delete(self): """Remove self from the tree structure - return parent on success""" parent = self.parent if not parent: return None parent.childList.remove(self) self.parent = None globalref.docRef.modified = True return parent def addChild(self, text=_defaultTitle, pos=-1): """Add new child before position, -1 is at end - return new item""" if pos < 0: pos = len(self.childList) newFormat = globalref.docRef.treeFormats.\ findFormat(self.nodeFormat.childType) if not newFormat: newFormat = self.childList and self.childList[0].nodeFormat or \ self.nodeFormat newItem = TreeItem(self, newFormat, text, True) self.childList.insert(pos, newItem) globalref.docRef.modified = True return newItem def insertSibling(self, text=_defaultTitle, inAfter=False): """Add new sibling before or after self - return new item on success""" if not self.parent: return None pos = self.parent.childPos(self) if inAfter: pos += 1 newFormat = globalref.docRef.treeFormats.findFormat(self.parent.\ nodeFormat.childType) if not newFormat: newFormat = self.nodeFormat newItem = TreeItem(self.parent, newFormat, text, True) self.parent.childList.insert(pos, newItem) globalref.docRef.modified = True return newItem def addTree(self, rootItem, pos=-1): """Add new tree as a child before position, -1 is at end return item""" if pos < 0: pos = len(self.childList) self.childList.insert(pos, rootItem) rootItem.parent = self globalref.docRef.modified = True return rootItem def insertTree(self, rootItem, inAfter=False): """Add new tree before or after self - return item on success""" if not self.parent: return None pos = self.parent.childPos(self) if inAfter: pos += 1 self.parent.childList.insert(pos, rootItem) rootItem.parent = self.parent globalref.docRef.modified = True return rootItem def indent(self): """Becomes a child of the previous sibling - return self on success""" newParent = self.prevSibling() if not newParent: return None self.delete() newParent.addTree(self, -1) globalref.docRef.modified = True return self def unindent(self): """Becomes its parents next sibling - return self on success""" sibling = self.parent if not sibling or not sibling.parent: return None self.delete() sibling.insertTree(self, True) globalref.docRef.modified = True return self def move(self, amount=-1): """Switch self with sibling, -1=up, 1=down, return self on success""" if self.parent: i = self.parent.childPos(self) j = i + amount if 0 <= j < len(self.parent.childList): self.parent.childList[i], self.parent.childList[j] = \ self.parent.childList[j], self globalref.docRef.modified = True return self return None def openBranch(self, setOpen=True): """Recursive open/close of all descendants, close if setOpen=False""" self.open = setOpen for child in self.childList: child.openBranch(setOpen) def openParents(self, openSelf=True): """Open self's ancestors and self if openSelf, return list of changed items""" openList = [] item = openSelf and self or self.parent while item: if not item.open and item.childList: item.open = True openList.append(item) item = item.parent return openList def cmpItems(self, item1, item2): """Compare function for sorting, not case sensitive""" if not globalref.docRef.sortFields[0][0]: factor = globalref.docRef.sortFields[0][1] and 1 or -1 return factor * cmp(item1.title().lower(), item2.title().lower()) for field, direction in globalref.docRef.sortFields: field1 = item1.nodeFormat.findField(field) field2 = item2.nodeFormat.findField(field) factor = direction and 1 or -1 if field1.sortSequence == field2.sortSequence: result = cmp(field1.sortValue(item1.data), \ field2.sortValue(item2.data)) if result != 0: return factor * result else: return factor * cmp(field1.sortSequence, field2.sortSequence) return 0 def sortChildren(self): """Sort item children, not case sensitive""" self.childList.sort(self.cmpItems) globalref.docRef.modified = True def sortType(self, nodeFormat): """Sort item descendants of a given format type""" childOfTypeList = [child for child in self.childList \ if child.nodeFormat == nodeFormat] if childOfTypeList: childOfTypeList.sort(self.cmpItems) if len(childOfTypeList) < len(self.childList): childOfTypeList.extend([child for child in self.childList \ if child.nodeFormat != type]) self.childList = childOfTypeList for child in self.childList: child.sortType(nodeFormat) globalref.docRef.modified = True def sortBranch(self): """Sort item descendants, not case sensitive""" self.childList.sort(self.cmpItems) for child in self.childList: child.sortBranch() globalref.docRef.modified = True def matchWords(self, wordList): """Return True if all words are in data fields, not case sensitive""" dataStr = u' '.join(self.data.values()).lower() for word in wordList: if dataStr.find(word) == -1: return False return True def matchRefText(self, searchStr): """Return True if searchStr matches a line in ref field data""" lines = self.data.get(self.nodeFormat.refField.name, '').split('\n') if searchStr in lines: return True return False def cmpFields(self, fieldNames, item): """Return True if listed fields are the same in item and self""" for field in fieldNames: if self.data.get(field, '') != item.data.get(field, ''): return False return True def findEqivFields(self, fieldNames, itemList): """Return first item from list with same listed fields or None""" for item in itemList: if self.cmpFields(fieldNames, item): return item return None def commonFields(self, itemList): """Return names of fields that are common to all items in list""" if not itemList: return [] typeList = [] for item in itemList: if item.nodeFormat not in typeList: typeList.append(item.nodeFormat) fieldNames = typeList.pop(0).fieldNames() for type in typeList: typeFields = type.fieldNames() for field in fieldNames[:]: if field not in typeFields: fieldNames.remove(field) return fieldNames def editFields(self, valueDict): """Set values for fields based on dictionary""" for field in valueDict.keys(): self.data[field] = valueDict[field] globalref.docRef.modified = True def descendTypes(self): """Return list of all types found in descendants""" types = [] for item in self.descendantGenNoRoot(): if item.nodeFormat not in types: types.append(item.nodeFormat) return types def branchFields(self): """Return names of all fields found in self and descendents""" types = [] for item in self.descendantGen(): if item.nodeFormat not in types: types.append(item.nodeFormat) fieldNames = [] for type in types: for field in type.fieldNames(): if field not in fieldNames: fieldNames.append(field) return fieldNames def setConditionalType(self): """Set self to type based on auto condtional settings""" genericName = self.nodeFormat.genericType if not genericName: genericName = self.nodeFormat.name formatList = globalref.docRef.treeFormats.derivedDict.\ get(genericName, [])[:] if not formatList: return formatList.remove(self.nodeFormat) formatList.insert(0, self.nodeFormat) # reorder to give priority neutralResult = None for format in formatList: if format.conditional: if format.conditional.evaluate(self.data): self.nodeFormat = format return elif not neutralResult: neutralResult = format if neutralResult: self.nodeFormat = neutralResult def setDescendantCondTypes(self): """Set children recursively to type based on condtional settings""" self.setConditionalType() for child in self.childList: child.setDescendantCondTypes() def filterDescendants(self, type, expr): """Remove children of given type recursively if expr is false""" for child in self.childList[:]: if child.nodeFormat != type or expr(child.data): child.filterDescendants(type, expr) else: self.childList.remove(child) child.parent = None globalref.docRef.modified = True def addChildCat(self, catList): """Add child's category items as a new child level to expand data""" catSuffix = _('TYPE', 'child category suffix') newType = u'%s_%s' % (catList[0], catSuffix) num = 1 formatNames = globalref.docRef.treeFormats.nameList() while newType in formatNames and \ globalref.docRef.treeFormats[formatNames.index(newType)].\ fieldNames() != catList: newType = u'%s_%s_%d' % (catList[0], catSuffix, num) num += 1 newFormat = NodeFormat(newType, {}, catList[0]) globalref.docRef.treeFormats.append(newFormat) for field in catList[1:]: newFormat.addNewField(field) newItems = [] for child in self.childList: newParent = child.findEqivFields(catList, newItems) if not newParent: newParent = TreeItem(self, newFormat) for field in catList: newParent.data[field] = child.data.get(field, '') newItems.append(newParent) newParent.childList.append(child) child.parent = newParent self.childList = newItems globalref.docRef.modified = True def flatChildCat(self): """Collapse data by merging fields""" origTreeFormats = copy.deepcopy(globalref.docRef.treeFormats) self.childList = [item for item in self.descendantGen() if not \ item.childList] for item in self.childList: fieldList = item.nodeFormat.fieldNames() origFields = origTreeFormats.findFormat(item.nodeFormat.name)\ .fieldNames() addedFields = [] oldParent = item.parent while oldParent != self: for field in origTreeFormats.findFormat(oldParent.nodeFormat\ .name).fieldNames(): newField = field num = 1 while newField in origFields or newField in addedFields: newField = u'%s_%d' % (field, num) num += 1 item.data[newField] = oldParent.data.get(field, '') addedFields.append(newField) item.nodeFormat.addFieldIfNew(newField) oldParent = oldParent.parent item.parent = self globalref.docRef.modified = True def arrangeByRef(self, refField): """Arrange data using parent references""" descendList = self.descendantList(True)[1:] for item in descendList: item.childList = [] self.childList = [] for item in descendList: refText = item.data.get(refField, '') parentList = [parent for parent in descendList if \ parent.refFieldText() == refText] if len(parentList) > 1: # pick nearest parent above the item itemPos = descendList.index(item) while len(parentList) > 1 and \ descendList.index(parentList[1]) < itemPos: del parentList[0] if not parentList or parentList[0] == item: item.parent = self else: item.parent = parentList[0] item.parent.childList.append(item) globalref.docRef.modified = True def flatByRef(self, refField): """Collapse data after adding references to parents""" descendList = self.descendantList(True)[1:] self.childList = descendList for item in descendList: item.childList = [] item.data[refField] = item.parent.refFieldText() item.parent = self item.nodeFormat.addFieldIfNew(refField) globalref.docRef.modified = True def updateByRef(self, refRoot): """Update with new fields from reference file with matched ref field Return a tuple describing changes""" refData = {} for item in refRoot.descendantGen(): refData[item.refFieldText()] = item formatChgs = {} numNewEntries = 0 for item in self.descendantGen(): fields = item.nodeFormat.fieldNames() try: ref = refData[item.data[item.nodeFormat.refField.name]] for field in ref.data.keys(): if field not in fields: item.data[field] = ref.data[field] numNewEntries += 1 formatChgs.setdefault(item.nodeFormat.name, {}).\ setdefault(field, \ ref.nodeFormat.findField(field)) except KeyError: pass numChgTypes = 0 numNewFields = 0 for typeName in formatChgs.keys(): type = globalref.docRef.treeFormats.findFormat(typeName) ref = formatChgs[typeName] numChgTypes += 1 for field in ref.values(): type.fieldList.append(field) numNewFields += 1 if numNewEntries: globalref.docRef.modified = True return (numNewEntries, numNewFields, numChgTypes) def addNumbering(self, field, format, rootIncluded, appendToParent, \ singleLevel=False, startNum=1, currentLevel=0): """Add number field to this node and descendants""" if rootIncluded: self.data[field] = numbering.numSeries(startNum, startNum + 1, \ format[currentLevel])[0] self.nodeFormat.addFieldIfNew(field) globalref.docRef.modified = True currentLevel += 1 startNum = 1 if not self.childList: return globalref.docRef.modified = True numList = numbering.numSeries(startNum, \ len(self.childList) + startNum, \ format[currentLevel]) if appendToParent and currentLevel: numList = [self.data[field] + numText for numText in numList] for item in self.childList: item.data[field] = numList.pop(0) item.nodeFormat.addFieldIfNew(field) if not singleLevel: for item in self.childList: item.addNumbering(field, format, False, appendToParent, \ False, 1, currentLevel + 1)