deleteslivers for the emulab aggregate
[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 readContent(self):
195         raise ValueError("readContent needs to be implemented in the subclass")
196
197     def updateView(self):
198         del self.document
199         del self.model
200         self.document = None
201         self.model = None
202
203         self.document = QDomDocument(self.title)
204         self.model = DomModel(self.document, self)
205
206         self.view.setModel(self.model)
207
208         self.document.setContent(self.readContent())
209
210         if self.document.childNodes().count() == 0:
211             # empty document - do nothing
212             pass
213         elif self.document.childNodes().item(0).nodeType() == QDomNode.ProcessingInstructionNode:
214             # the first item is the <xml> tag, so expand the second
215             self.view.expand(self.model.index(1,0))
216         else:
217             # the document didn't start with an <xml> tag; let's try to expand
218             # the first item on the assumption that it is our root level xml
219             # tag.
220             self.view.expand(self.model.index(0,0))
221
222
223
224 class XmlDelegate(QItemDelegate):
225     
226     def __init__(self, parent=None):
227         QAbstractItemDelegate.__init__(self, parent)
228         self.delegates = {}
229
230     def insertNodeDelegate(self, nodeType, delegate):
231         delegate.setParent(self)
232         self.delegates[nodeType] = delegate
233
234     def removeNodeDelegate(self, nodeType, delegate):
235         if nodeType in self.delegates:
236             del self.delegates[nodeType]
237     
238     def paint(self, painter, option, index):
239         if isinstance(index.model().data(index),QVariant):
240             return
241         nodeType = index.model().data(index).property('nodeType')
242         delegate = self.delegates.get(str(nodeType.toString()))
243         #print "TYPE:", str(type(str(nodeType.toString())))
244         #print "DELEGS DICT:", self.delegates
245         #print "NODETYPE:", nodeType.toString()
246         if delegate is not None:
247             #print "WOW DELEG ISNT NONE"
248             delegate.paint(painter, option, index)
249         else:
250             #print "ELSE BRANCH"
251             # not sure this will ever work. this delegate
252             # doesn't know about my QObject strategy.
253             QItemDelegate.paint(self, painter, option, index)
254
255     def sizeHint(self, option, index):
256         fm = option.fontMetrics
257         if isinstance(index.model().data(index),QVariant):
258             return QSize(0, 0)
259         text = index.model().data(index).property('content').toString()
260         document = QTextDocument()
261         document.setDefaultFont(option.font)
262         document.setHtml(text)
263         # the +5 is for margin. The +4 is voodoo;
264         # fm.height just give it too small.
265         return QSize(document.idealWidth() + 5, fm.height() + 4)
266
267 class ElemNodeDelegate(QAbstractItemDelegate):
268     def paint(self, painter, option, index): 
269         text = index.model().data(index)
270         palette = QApplication.palette()
271         document = QTextDocument()
272         document.setDefaultFont(option.font)
273         nonHighGlobPattern = '&lt;<b><font color="#b42be2">%s</font></b>%s&gt;'
274         nonHighAttPattern = ' <b>%s</b>="<font color="#1e90ff">%s</font>"'
275         highGlobPattern = '&lt;<b>%s</b>%s&gt;'
276         highAttPattern = ' <b>%s</b>="%s"'
277         def getHtmlText(plainText, globPattern, attPattern):
278 #            print "PLAIN TEXT:", plainText
279             tmp = plainText.split(' ', 1)
280 #            print "TMP:", tmp
281             elemName = tmp[0]
282             AttListHtml = ''
283             if len(tmp) > 1:
284                 # many elems don't have atts...
285                 # use shlex.split so we can handle quoted strings with spaces
286                 # in them, like <link enpoints="foo bar">. Note that there are
287                 # documented problems with shlex.split and unicode, so we
288                 # convert any potential unicode to a string first.
289                 attList = shlex.split(str(tmp[1]))
290                 for att in attList:
291                     tmp = att.split('=',1)
292                     if len(tmp)>=2:
293                         attName = tmp[0]
294                         attValue = tmp[1]
295                     else:
296                         # this shouldn't happen, but if it does, pretend the
297                         # attribute value is blank.
298                         attName = tmp[0]
299                         attValue = ""
300                     AttListHtml += (nonHighAttPattern % (attName, attValue))
301             html = (globPattern % (elemName, AttListHtml))
302             return html
303         def colorize(color, text):
304             return '<font color=' + color + '>' + text + '</font>'
305         text = str(index.model().data(index).property('content').toString())
306 #        print "TEXT:", text
307         if option.state & QStyle.State_Selected:
308             htmlText = colorize(palette.highlightedText().color().name(),
309                                 getHtmlText(text, highGlobPattern, highAttPattern))
310             document.setHtml(QString(htmlText))
311         else:
312             htmlText = getHtmlText(text, nonHighGlobPattern, nonHighAttPattern)
313             document.setHtml(QString(htmlText))
314         color = palette.highlight().color() \
315             if option.state & QStyle.State_Selected \
316             else palette.base().color()
317         painter.save()
318 #        print "COLOR:", color.name()
319         # voodoo: if not highlighted, filling the rect
320         # with the base color makes no difference
321         painter.fillRect(option.rect, color)
322         painter.translate(option.rect.x(), option.rect.y())
323         document.drawContents(painter)
324         painter.restore()
325
326     def sizeHint(self, option, index):
327         sizeHint(self, option, index)
328
329 class TextNodeDelegate(QAbstractItemDelegate):
330     def paint(self, painter, option, index): 
331         #print "TEXT DELEG CALLED"
332         paint(self, painter, option, index)
333
334     def sizeHint(self, option, index):
335         sizeHint(self, option, index)
336
337 class CommentNodeDelegate(QAbstractItemDelegate):
338     def paint(self, painter, option, index): 
339         #print "TEXT DELEG CALLED"
340         paint(self, painter, option, index)
341
342 def paint(self, painter, option, index):
343     text = index.model().data(index).property('content').toString()
344     palette = QApplication.palette()
345     document = QTextDocument()
346     document.setDefaultFont(option.font)
347     if option.state & QStyle.State_Selected:
348         rx = QRegExp(QString('<font .*>'))
349         rx.setMinimal(True)
350         # If selected, I remove the <font color="..."> by hand,
351         # and give the highlight color
352         document.setHtml(QString("<font color=%1>%2</font>") \
353                              .arg(palette.highlightedText().color().name())\
354                              .arg(text.replace(rx, QString('')).
355                                   replace(QString('</font>'),QString(''))))
356     else:
357         document.setHtml(text)
358     color = palette.highlight().color() \
359         if option.state & QStyle.State_Selected \
360         else palette.base().color()
361     painter.save()
362     # voodoo: if not highlighted, filling the rect
363     # with the base color makes no difference
364     painter.fillRect(option.rect, color)
365     painter.translate(option.rect.x(), option.rect.y())
366     document.drawContents(painter)
367     painter.restore()
368