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