Setting tag sface-0.9-9
[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         self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
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 = self.initView()
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.connect(self.view, SIGNAL('expanded(QModelIndex)'), self.onItemExpanded)
187         self.updateView()
188
189     def initView(self):
190         return XmlView(self)
191
192     def show(self):
193         self.updateView()
194         QDialog.show(self)
195
196     def readContent(self):
197         raise ValueError("readContent needs to be implemented in the subclass")
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
210         self.document.setContent(self.readContent())
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         self.view.header().resizeSection(0,0)
225         self.view.resizeColumnToContents(0)
226
227     def onItemExpanded(self, index):
228         self.view.header().resizeSection(0,0)
229         self.view.resizeColumnToContents(0)
230
231 class XmlDelegate(QItemDelegate):
232     
233     def __init__(self, parent=None):
234         QAbstractItemDelegate.__init__(self, parent)
235         self.delegates = {}
236
237     def insertNodeDelegate(self, nodeType, delegate):
238         delegate.setParent(self)
239         self.delegates[nodeType] = delegate
240
241     def removeNodeDelegate(self, nodeType, delegate):
242         if nodeType in self.delegates:
243             del self.delegates[nodeType]
244     
245     def paint(self, painter, option, index):
246         if isinstance(index.model().data(index),QVariant):
247             return
248         nodeType = index.model().data(index).property('nodeType')
249         delegate = self.delegates.get(str(nodeType.toString()))
250         #print "TYPE:", str(type(str(nodeType.toString())))
251         #print "DELEGS DICT:", self.delegates
252         #print "NODETYPE:", nodeType.toString()
253         if delegate is not None:
254             #print "WOW DELEG ISNT NONE"
255             delegate.paint(painter, option, index)
256         else:
257             #print "ELSE BRANCH"
258             # not sure this will ever work. this delegate
259             # doesn't know about my QObject strategy.
260             QItemDelegate.paint(self, painter, option, index)
261
262     def sizeHint(self, option, index):
263         fm = option.fontMetrics
264         if isinstance(index.model().data(index),QVariant):
265             return QSize(0, 0)
266
267         nodeType = index.model().data(index).property('nodeType')
268         delegate = self.delegates.get(str(nodeType.toString()), None)
269
270         # Use the text from the appropriate delegate if we have it, to
271         # compute the size. Should probably finish the sizeHint() methods
272         # of the delegates at some point, and call them instead.
273         if delegate!=None and hasattr(delegate, "getItemText"):
274             text = delegate.getItemText(option, index)
275         else:
276             text = index.model().data(index).property('content').toString()
277
278         document = QTextDocument()
279         document.setDefaultFont(option.font)
280         document.setHtml(text)
281
282         # the +5 is for margin. The +4 is voodoo;
283         # fm.height just give it too small.
284         return QSize(document.idealWidth() + 5, fm.height() + 4)
285
286 class ElemNodeDelegate(QAbstractItemDelegate):
287     def getItemText(self, option, index):
288         text = index.model().data(index)
289         nonHighGlobPattern = '&lt;<b><font color="#b42be2">%s</font></b>%s&gt;'
290         nonHighAttPattern = ' <b>%s</b>="<font color="#1e90ff">%s</font>"'
291         highGlobPattern = '&lt;<b>%s</b>%s&gt;'
292         highAttPattern = ' <b>%s</b>="%s"'
293
294         def getHtmlText(plainText, globPattern, attPattern):
295 #            print "PLAIN TEXT:", plainText
296             tmp = plainText.split(' ', 1)
297 #            print "TMP:", tmp
298             elemName = tmp[0]
299             AttListHtml = ''
300             if len(tmp) > 1:
301                 # many elems don't have atts...
302                 # use shlex.split so we can handle quoted strings with spaces
303                 # in them, like <link enpoints="foo bar">. Note that there are
304                 # documented problems with shlex.split and unicode, so we
305                 # convert any potential unicode to a string first.
306                 attList = shlex.split(str(tmp[1]))
307                 for att in attList:
308                     tmp = att.split('=',1)
309                     if len(tmp)>=2:
310                         attName = tmp[0]
311                         attValue = tmp[1]
312                     else:
313                         # this shouldn't happen, but if it does, pretend the
314                         # attribute value is blank.
315                         attName = tmp[0]
316                         attValue = ""
317                     AttListHtml += (nonHighAttPattern % (attName, attValue))
318             html = (globPattern % (elemName, AttListHtml))
319             return html
320
321         def colorize(color, text):
322             return '<font color=' + color + '>' + text + '</font>'
323
324         text = str(index.model().data(index).property('content').toString())
325 #        print "TEXT:", text
326         if option.state & QStyle.State_Selected:
327             palette = QApplication.palette()
328             htmlText = colorize(palette.highlightedText().color().name(),
329                                 getHtmlText(text, highGlobPattern, highAttPattern))
330         else:
331             htmlText = getHtmlText(text, nonHighGlobPattern, nonHighAttPattern)
332
333         return htmlText
334
335     def paint(self, painter, option, index):
336         palette = QApplication.palette()
337         document = QTextDocument()
338         document.setDefaultFont(option.font)
339
340         htmlText = self.getItemText(option, index)
341         document.setHtml(QString(htmlText))
342
343         color = palette.highlight().color() \
344             if option.state & QStyle.State_Selected \
345             else palette.base().color()
346
347         painter.save()
348 #        print "COLOR:", color.name()
349         # voodoo: if not highlighted, filling the rect
350         # with the base color makes no difference
351         painter.fillRect(option.rect, color)
352         painter.translate(option.rect.x(), option.rect.y())
353         document.drawContents(painter)
354         painter.restore()
355
356 class TextNodeDelegate(QAbstractItemDelegate):
357     def paint(self, painter, option, index): 
358         #print "TEXT DELEG CALLED"
359         paint(self, painter, option, index)
360
361 class CommentNodeDelegate(QAbstractItemDelegate):
362     def paint(self, painter, option, index): 
363         #print "TEXT DELEG CALLED"
364         paint(self, painter, option, index)
365
366     def paint(self, painter, option, index):
367         text = index.model().data(index).property('content').toString()
368         palette = QApplication.palette()
369         document = QTextDocument()
370         document.setDefaultFont(option.font)
371         if option.state & QStyle.State_Selected:
372             rx = QRegExp(QString('<font .*>'))
373             rx.setMinimal(True)
374             # If selected, I remove the <font color="..."> by hand,
375             # and give the highlight color
376             document.setHtml(QString("<font color=%1>%2</font>") \
377                                  .arg(palette.highlightedText().color().name())\
378                                  .arg(text.replace(rx, QString('')).
379                                       replace(QString('</font>'),QString(''))))
380         else:
381             document.setHtml(text)
382         color = palette.highlight().color() \
383             if option.state & QStyle.State_Selected \
384             else palette.base().color()
385         painter.save()
386         # voodoo: if not highlighted, filling the rect
387         # with the base color makes no difference
388         painter.fillRect(option.rect, color)
389         painter.translate(option.rect.x(), option.rect.y())
390         document.drawContents(painter)
391         painter.restore()
392