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