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