restore baris tree expansion feature. Removed >print< statements.
[sface.git] / sface / xmlwidget.py
1 import os
2 import sys
3
4 from PyQt4.QtCore import *
5 from PyQt4.QtGui import *
6 from PyQt4.QtXml import *
7
8 from sface.config import config
9 from sface.screens.sfascreen import SfaScreen
10
11 class nodeData(QVariant):
12     def __init__(self, *args, **kws):
13         QVariant.__init__(self, *args, **kws)
14         self.type = None
15
16     def setType(self, typ):
17         self.type = typ
18
19     def getType(self):
20         return self.type
21
22 class DomModel(QAbstractItemModel):
23     def __init__(self, document, parent = 0):
24         QAbstractItemModel.__init__(self, parent)
25         self.domDocument = document
26         # one of the children of the rootItem is the 'xml' thing.
27         # here I delete it.
28         childList = document.childNodes()
29         for i in range(childList.count()):
30             currElem = childList.item(i)
31             if (currElem.nodeType() == QDomNode.ProcessingInstructionNode):
32                 document.removeChild(currElem)
33                 break
34         self.rootItem = DomItem(document, 0);
35
36     def data(self, index, role = Qt.DisplayRole):
37         # for interesting nodes, returns a dict wrapped into a QVariant.
38         if not index.isValid():
39             return QVariant()
40         if role != Qt.DisplayRole:
41             return QVariant()
42         node = index.internalPointer().node()
43         attributeMap = node.attributes()
44
45         col = index.column()
46         if col == 0:
47             if node.nodeType() == QDomNode.ElementNode:
48                 qslist = QStringList()
49                 for i in range(attributeMap.count()):
50                     attr = attributeMap.item(i)
51                     elem = ' %s="%s"' % (attr.nodeName(), attr.nodeValue())
52                     qslist.append(elem)
53                 ElemNameAndAtts = '%s%s'% (node.nodeName(), qslist.join(' '))
54                 answer = nodeData(ElemNameAndAtts)
55                 answer.setType('element')
56                 return answer
57             elif node.nodeType() == QDomNode.AttributeNode:
58                 return QVariant()
59             elif node.nodeType() == QDomNode.TextNode:
60                 answer = nodeData(node.nodeValue())
61                 answer.setType('text')
62                 return answer
63             elif node.nodeType() == QDomNode.CDATASectionNode:
64                 return QString('unsupported node type')
65             elif node.nodeType() == QDomNode.EntityReferenceNode:
66                 return QString('unsupported node type')
67             elif node.nodeType() == QDomNode.EntityNode:
68                 return QString('unsupported node type')
69             elif node.nodeType() == QDomNode.ProcessingInstructionNode:
70                 return QVariant()
71             elif node.nodeType() == QDomNode.CommentNode:
72                 answer = nodeData(node.nodeValue())
73                 answer.setType('comment')
74                 return answer
75             elif node.nodeType() == QDomNode.DocumentNode:
76                 return QString('unsupported node type')
77             elif node.nodeType() == QDomNode.DocumentTypeNode:
78                 return QString('unsupported node type')
79             elif node.nodeType() == QDomNode.DocumentFragmentNode:
80                 return QString('unsupported node type')
81             elif node.nodeType() == QDomNode.NotationNode:
82                 return QString('unsupported node type')
83             elif node.nodeType() == QDomNode.BaseNode:
84                 return QString('unsupported node type')
85             elif node.nodeType() == QDomNode.CharacterDataNode:
86                 return QString('unsupported node type')
87             else:
88                 return QVariant()
89         else:
90             return QVariant()
91
92     def flags(self, index):
93         if not index.isValid():
94             return Qt.ItemIsEnabled
95         return Qt.ItemIsEnabled | Qt.ItemIsSelectable
96         
97     def headerData(self, section, orientation, role):
98         return QVariant()
99
100     def index(self, row, column, parent=None):
101         if not parent or not parent.isValid():
102             parentItem = self.rootItem
103         else:
104             parentItem = parent.internalPointer()
105
106         childItem = parentItem.child(row)
107         # childItem would be None to say "false"?
108         if childItem:
109             return self.createIndex(row, column, childItem)
110         else:
111             return QModelIndex()
112
113     def parent(self, child):
114         if not child.isValid():
115             return QModelIndex()
116         childItem = child.internalPointer()
117         parentItem = childItem.parent()
118         
119         if not parentItem or parentItem == self.rootItem:
120             return QModelIndex()
121         return self.createIndex(parentItem.row(), 0, parentItem)
122
123     def rowCount(self, parent=None):
124         if not parent or not parent.isValid():
125             parentItem = self.rootItem
126         else:
127             parentItem = parent.internalPointer()
128
129         return parentItem.node().childNodes().count()
130
131     def columnCount(self, parent):
132         # just one column we'll print tag name (and attributes) or the
133         # tag content
134         return 1
135
136
137 class DomItem:
138     # wrapper around PyQt4.QtXml.QDomNode it keeps an hash of
139     # childrens for performance reasons
140
141     def __init__(self, node, row, parent = 0):
142         # node is of type PyQt4.QtXml.QDomNode
143         self.domNode = node
144         self.parentItem = parent
145         self.rowNumber = row
146         self.childItems = {}
147
148     def child(self, i):
149         if i in self.childItems:
150             return self.childItems[i]
151         if i >= 0 and i < self.domNode.childNodes().count():
152             childNode = self.domNode.childNodes().item(i)
153             childItem = DomItem(childNode, i, self)
154             self.childItems[i] = childItem
155             return childItem
156         return None
157             
158     def parent(self):
159         return self.parentItem
160
161     def node(self):
162         return self.domNode
163
164     def row(self):
165         return self.rowNumber
166
167 class XmlView(QTreeView):
168     def __init__(self, parent=None):
169         QTreeView.__init__(self, parent)
170
171         delegate = XmlDelegate(self)
172         delegate.insertNodeDelegate('element', ElemNodeDelegate())
173         delegate.insertNodeDelegate('text', TextNodeDelegate())
174         delegate.insertNodeDelegate('comment', CommentNodeDelegate())
175         self.setItemDelegate(delegate)
176
177         self.setAnimated(True)
178         self.setItemsExpandable(True)
179         self.setRootIsDecorated(True)
180         self.setHeaderHidden(True)
181         self.setAttribute(Qt.WA_MacShowFocusRect, 0)
182         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
183
184 class XmlDelegate(QItemDelegate):
185     
186     def __init__(self, parent=None):
187         QAbstractItemDelegate.__init__(self, parent)
188         self.delegates = {}
189
190     def insertNodeDelegate(self, nodeType, delegate):
191         delegate.setParent(self)
192         self.delegates[nodeType] = delegate
193
194     def removeNodeDelegate(self, nodeType, delegate):
195         if nodeType in self.delegates:
196             del self.delegates[nodeType]
197     
198     def paint(self, painter, option, index):
199         nodeData = index.model().data(index)
200         nodeType = nodeData.getType()
201         delegate = self.delegates.get(nodeType)
202         if delegate is not None:
203             delegate.paint(painter, option, index)
204         else:
205             QItemDelegate.paint(self, painter, option, index)
206
207     def sizeHint(self, option, index):
208         fm = option.fontMetrics
209         nodeData = index.model().data(index)
210         nodeType = nodeData.getType()
211         text = nodeData.toString()
212         if nodeType == 'element' or nodeType == 'comment':
213             numlines = 1
214         elif nodeType == 'text':
215             nl = text.count('\n')
216             numlines = nl if nl > 0 else 1
217         else:
218             numlines = 1
219         document = QTextDocument()
220         document.setDefaultFont(option.font)
221         document.setHtml(text)
222         # the +5 is for margin. The +4 is voodoo;
223         # fm.height just give it too small.
224         return QSize(document.idealWidth() + 5, (fm.height() + 4) * numlines)    
225
226 class ElemNodeDelegate(QAbstractItemDelegate):
227     def paint(self, painter, option, index): 
228         palette = QApplication.palette()
229         document = QTextDocument()
230         document.setDefaultFont(option.font)
231         nonHighGlobPattern = '&lt;<b><font color="#b42be2">%s</font></b>%s&gt;'
232         nonHighAttPattern = ' <b>%s</b>="<font color="#1e90ff">%s</font>"'
233         highGlobPattern = '&lt;<b>%s</b>%s&gt;'
234         highAttPattern = ' <b>%s</b>="%s"'
235         def getHtmlText(plainText, globPattern, attPattern):
236             tmp = plainText.split(' ', 1)
237             elemName = tmp[0]
238             AttListHtml = ''
239             if len(tmp) > 1:
240                 # many elems don't have atts...
241                 attList = tmp[1].split()
242                 for att in attList:
243                     tmp = att.split('=')
244                     attName = tmp[0]
245                     attValue = tmp[1][1:-1]
246                     AttListHtml += (attPattern % (attName, attValue))
247             html = (globPattern % (elemName, AttListHtml))
248             return html
249         def colorize(color, text):
250             return '<font color=' + color + '>' + text + '</font>'
251         nodeData = index.model().data(index)
252         nodeType = nodeData.getType()
253         # Uff... QString Vs string...
254         text = str(nodeData.toString())
255         if option.state & QStyle.State_Selected:
256             htmlText = colorize(palette.highlightedText().color().name(),
257                                 getHtmlText(text, highGlobPattern, highAttPattern))
258             document.setHtml(QString(htmlText))
259         else:
260             htmlText = getHtmlText(text, nonHighGlobPattern, nonHighAttPattern)
261             document.setHtml(QString(htmlText))
262         color = palette.highlight().color() \
263             if option.state & QStyle.State_Selected \
264             else palette.base().color()
265         painter.save()
266         # voodoo: if not highlighted, filling the rect
267         # with the base color makes no difference
268         painter.fillRect(option.rect, color)
269         painter.translate(option.rect.x(), option.rect.y())
270         document.drawContents(painter)
271         painter.restore()
272
273 class TextNodeDelegate(QAbstractItemDelegate):
274     def paint(self, painter, option, index): 
275         palette = QApplication.palette()
276         document = QTextDocument()
277         document.setDefaultFont(option.font)
278         def verbatimize(text):
279             text.replace('\n', '<br>')
280             return '<pre>' + text + '</pre'
281         def colorize(color, text):
282             return '<font color=' + color + '>' + text + '</font>'
283         nodeData = index.model().data(index)
284         nodeType = nodeData.getType()
285         text = nodeData.toString()
286         if option.state & QStyle.State_Selected:
287             htmlText = colorize(palette.highlightedText().color().name(),
288                                 verbatimize(text))
289             document.setHtml(QString(htmlText))
290         else:
291             htmlText = verbatimize(text)
292             document.setHtml(QString(htmlText))
293         color = palette.highlight().color() \
294             if option.state & QStyle.State_Selected \
295             else palette.base().color()
296         painter.save()
297         # voodoo: if not highlighted, filling the rect
298         # with the base color makes no difference
299         painter.fillRect(option.rect, color)
300         painter.translate(option.rect.x(), option.rect.y())
301         document.drawContents(painter)
302         painter.restore()
303
304 class CommentNodeDelegate(QAbstractItemDelegate):
305     def paint(self, painter, option, index): 
306         paint(self, painter, option, index)
307
308 def paint(self, painter, option, index):
309     text = index.model().data(index).property('content').toString()
310     palette = QApplication.palette()
311     document = QTextDocument()
312     document.setDefaultFont(option.font)
313     if option.state & QStyle.State_Selected:
314         rx = QRegExp(QString('<font .*>'))
315         rx.setMinimal(True)
316         # If selected, I remove the <font color="..."> by hand,
317         # and give the highlight color
318         document.setHtml(QString("<font color=%1>%2</font>") \
319                              .arg(palette.highlightedText().color().name())\
320                              .arg(text.replace(rx, QString('')).
321                                   replace(QString('</font>'),QString(''))))
322     else:
323         document.setHtml(text)
324     color = palette.highlight().color() \
325         if option.state & QStyle.State_Selected \
326         else palette.base().color()
327     painter.save()
328     # voodoo: if not highlighted, filling the rect
329     # with the base color makes no difference
330     painter.fillRect(option.rect, color)
331     painter.translate(option.rect.x(), option.rect.y())
332     document.drawContents(painter)
333     painter.restore()
334
335