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