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