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