source: orange/Orange/OrangeWidgets/OWReport.py @ 9873:42551f01dcb3

Revision 9873:42551f01dcb3, 18.9 KB checked in by anze <anze.staric@…>, 2 years ago (diff)

OrangeQt documentation builds without adding orngdir to path.

Line 
1 # Widgets cannot be reset to the settings they had at the time of reporting.
2 # The reason lies in the OWGUI callback mechanism: callbacks are triggered only
3 # when the controls are changed by the user. If the related widget's attribute
4 # is changed programmatically, the control is updated but the callback is not
5 # called. This is done intentionally and with a very solid reason: it enables us
6 # to do multiple changes without, for instance, the widget being redrawn every time.
7 # Besides, it would probably lead to cycles or at least a great number of redundant calls.
8 # However, since setting attributes does not trigger callbacks, setting the attributes
9 # here would have not other effect than changing the widget's controls and leaving it
10 # in undefined (possibly invalid) state. The reason why we do not have these problems
11 # in "normal" use of settings is that the context independent settings are loaded only
12 # when the widget is initialized and the context dependent settings are retrieved when
13 # the new data is sent and the widget "knows" it has to reconfigure.
14 # The only solution would be to require all the widgets have a method for updating
15 # everything from scratch according to settings. This would require a lot of work, which
16 # could even not be feasible. For instance, there are widget which get the data, compute
17 # something and discard the data. This is good since it is memory efficient, but it
18 # may prohibit the widget from implementing the update-from-the-scratch method. 
19 
20 
21from OWWidget import *
22from OWWidget import *
23from PyQt4.QtWebKit import *
24
25from Orange.misc import environ
26
27import os, time, tempfile, shutil, re, shutil, pickle, binascii
28import xml.dom.minidom
29
30report = None
31def escape(s):
32    return s.replace("\\", "\\\\").replace("\n", "\\n").replace("'", "\\'")
33
34
35class MyListWidget(QListWidget):
36    def __init__(self, parent, widget):
37        QListWidget.__init__(self, parent)
38        self.widget = widget
39       
40    def dropEvent(self, ev):
41        QListWidget.dropEvent(self, ev)
42        self.widget.rebuildHtml()
43
44    def mousePressEvent(self, ev):
45        QListWidget.mousePressEvent(self, ev)
46        node = self.currentItem() 
47        if ev.button() == Qt.RightButton and node:
48            self.widget.nodePopup.popup(ev.globalPos())
49
50   
51class ReportWindow(OWWidget):
52    indexfile = os.path.join(environ.widget_install_dir, "report", "index.html")
53   
54    def __init__(self):
55        OWWidget.__init__(self, None, None, "Report")
56        self.dontScroll = False
57        global report
58        report = self
59        self.counter = 0
60       
61        self.tempdir = tempfile.mkdtemp("", "orange-report-")
62
63        self.tree = MyListWidget(self.controlArea, self)
64        self.tree.setDragEnabled(True)
65        self.tree.setDragDropMode(QAbstractItemView.InternalMove)
66        self.tree.setFixedWidth(200)
67        self.controlArea.layout().addWidget(self.tree)
68        QObject.connect(self.tree, SIGNAL("currentItemChanged(QListWidgetItem *, QListWidgetItem *)"), self.selectionChanged)
69        QObject.connect(self.tree, SIGNAL("itemActivated ( QListWidgetItem *)"), self.raiseWidget)
70        QObject.connect(self.tree, SIGNAL("itemDoubleClicked ( QListWidgetItem *)"), self.raiseWidget)
71        QObject.connect(self.tree, SIGNAL("itemChanged ( QListWidgetItem *)"), self.itemChanged)
72
73        self.treeItems = {}
74
75        self.reportBrowser = QWebView(self.mainArea)
76        self.reportBrowser.setUrl(QUrl.fromLocalFile(self.indexfile))
77        self.reportBrowser.page().mainFrame().addToJavaScriptWindowObject("myself", self)
78        frame = self.reportBrowser.page().mainFrame()
79        self.javascript = frame.evaluateJavaScript
80        frame.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAsNeeded)
81        self.mainArea.layout().addWidget(self.reportBrowser)
82
83        box = OWGUI.widgetBox(self.controlArea)
84#        exportButton = OWGUI.button(box, self, "&Export", self.saveXML)
85#        OWGUI.button(box, self, "&Import", self.loadXML)
86        saveButton = OWGUI.button(box, self, "&Save", self.saveReport)
87        printButton = OWGUI.button(box, self, "&Print", self.printReport)
88        saveButton.setAutoDefault(0)
89       
90        self.nodePopup = QMenu("Widget")
91        self.showWidgetAction = self.nodePopup.addAction( "Show widget",  self.showActiveNodeWidget)
92        self.nodePopup.addSeparator()
93        #self.renameAction = self.nodePopup.addAction( "&Rename", self.renameActiveNode, Qt.Key_F2)
94        self.deleteAction = self.nodePopup.addAction("Remove", self.removeActiveNode, Qt.Key_Delete)
95        self.deleteAllAction = self.nodePopup.addAction("Remove All", self.clearReport)
96        self.nodePopup.setEnabled(1)
97
98        self.resize(900, 850)
99       
100    # this should have been __del__, but it doesn't get called!
101    def removeTemp(self):
102        try:
103            shutil.rmtree(self.tempdir)
104        except:
105            pass
106
107
108    def __call__(self, name, data, widgetId, icon, wtime=None):
109        if not self.isVisible():
110            self.show()
111        else:
112            self.raise_()
113        self.counter += 1
114        elid = "N%03i" % self.counter
115
116        widnode = QListWidgetItem(icon, name, self.tree)
117        widnode.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsEditable)
118        widnode.elementId = elid
119        widnode.widgetId = widgetId
120        widnode.time = wtime or time.strftime("%a %b %d %y, %H:%M:%S")
121        widnode.data = data
122        widnode.name = name
123        self.tree.addItem(widnode)
124        self.treeItems[elid] = widnode
125        self.addEntry(widnode)
126       
127       
128    def addEntry(self, widnode, scrollIntoView=True):
129        newEntry = """
130        <div id="%s" onClick="myself.changeItem(this.id);">
131            <a name="%s" />
132            <h1>%s<span class="timestamp">%s</span></h1>
133            <div class="insideh1">
134                %s
135            </div>
136        </div>
137        """ % (widnode.elementId, widnode.elementId, widnode.name, widnode.time, widnode.data)
138        widnode.content = newEntry
139        self.javascript("document.body.innerHTML += '%s'" % escape(newEntry))
140        if scrollIntoView:
141            self.javascript("document.getElementById('%s').scrollIntoView();" % widnode.elementId)
142
143
144    def selectionChanged(self, current, previous):
145        if current:
146            if self.dontScroll:
147                self.javascript("document.getElementById('%s').className = 'selected';" % current.elementId)
148            else:
149                self.javascript("""
150                    var newsel = document.getElementById('%s');
151                    newsel.className = 'selected';
152                    newsel.scrollIntoView();""" % current.elementId)
153#            if not self.dontScroll:
154#                self.javascript("newsel.scrollIntoView(document.getElementById('%s'));" % current.elementId)
155            self.showWidgetAction.setEnabled(current.widgetId >= 0)
156        if previous:
157            self.javascript("document.getElementById('%s').className = '';" % previous.elementId)
158       
159       
160    def rebuildHtml(self):
161        self.javascript("document.body.innerHTML = ''")
162        for i in range(self.tree.count()):
163            self.addEntry(self.tree.item(i))
164        selected = self.tree.selectedItems()
165        if selected:
166            self.selectionChanged(selected[0], None)
167       
168       
169    @pyqtSignature("QString") 
170    def changeItem(self, elid):
171        self.dontScroll = True
172        item = self.treeItems[str(elid)]
173        self.tree.setCurrentItem(item)
174        self.tree.scrollToItem(item)
175        self.dontScroll = False
176 
177    def raiseWidget(self, node):
178        for widget in self.widgets:
179            if widget.instance.widgetId == node.widgetId:
180                break
181        else:
182            return
183        widget.instance.reshow()
184       
185    def showActiveNodeWidget(self):
186        node = self.tree.currentItem()
187        if node:
188            self.raiseWidget(node)
189           
190    re_h1 = re.compile(r'<h1>(?P<name>.*?)<span class="timestamp">')
191    def itemChanged(self, node):
192        if hasattr(node, "content"):
193            be, en = self.re_h1.search(node.content).span("name")
194            node.content = node.content[:be] + str(node.text()) + node.content[en:]
195            self.rebuildHtml()
196
197    def removeActiveNode(self):
198        node = self.tree.currentItem()
199        if node:
200            self.tree.takeItem(self.tree.row(node))
201        self.rebuildHtml()
202
203    def clearReport(self):
204        self.tree.clear()
205        self.rebuildHtml()
206       
207    def printReport(self):
208        printer = QPrinter()
209        printDialog = QPrintDialog(printer, self)
210        printDialog.setWindowTitle("Print report")
211        if (printDialog.exec_() != QDialog.Accepted):
212            return
213        getattr(self.reportBrowser, "print")(printer)
214       
215       
216    def createDirectory(self):
217        tmpPathName = os.tempnam(orange-report)
218        os.mkdir(tmpPathName)
219        return tmpPathName
220   
221    def getUniqueFileName(self, patt):
222        for i in xrange(1000000):
223            fn = os.path.join(self.tempdir, patt % i)
224            if not os.path.exists(fn):
225                return "file:///"+fn, fn
226
227    img_re = re.compile(r'<IMG.*?\ssrc="(?P<imgname>[^"]*)"', re.DOTALL+re.IGNORECASE)
228    browser_re = re.compile(r'<!--browsercode(.*?)-->')
229    def saveReport(self):
230        filename = QFileDialog.getSaveFileName(self, "Save Report", self.saveDir, "Web page (*.html *.htm)")
231        if not filename:
232            return
233
234        filename = str(filename)
235        path, fname = os.path.split(filename)
236        self.saveDir = path
237        if not os.path.exists(path):
238            try:
239                os.makedirs(path)
240            except:
241                QMessageBox.error(None, "Error", "Cannot create directory "+path)
242
243        tt = file(self.indexfile, "rt").read()
244       
245        index = "<br/>".join('<a href="#%s">%s</a>' % (self.tree.item(i).elementId, self.re_h1.search(self.tree.item(i).content).group("name"))
246                             for i in range(self.tree.count()))
247           
248######## Rewrite this to go through individual tree nodes. For one reason: this code used to work
249##       when the HTML stored in tree nodes included DIV and H1 tags, which it does not any more,
250##       so they have to be added here         
251
252        data = "\n".join(self.tree.item(i).content for i in range(self.tree.count()))
253
254        tt = tt.replace("<body>", '<body><table width="100%%"><tr><td valign="top"><p style="padding-top:25px;">Index</p>%s</td><td>%s</td></tr></table>' % (index, data))
255        tt = self.browser_re.sub("\\1", tt)
256       
257        filepref = "file:///"+self.tempdir
258        if filepref[-1] != os.sep:
259            filepref += os.sep
260        lfilepref = len(filepref)
261        imspos = -1
262        subdir = None
263        while True:
264            imspos = tt.find(filepref, imspos+1)
265            if imspos == -1:
266                break
267           
268            if not subdir:
269                subdir = os.path.splitext(fname)[0]
270                if subdir == fname:
271                    subdir += "_data"
272                cnt = 0
273                osubdir = subdir
274                while os.path.exists(os.path.join(path, subdir)):
275                    cnt += 1
276                    subdir = "%s%05i" % (osubdir, cnt)
277                absubdir = os.path.join(path, subdir)
278                os.mkdir(absubdir)
279
280            imname = tt[imspos+lfilepref:tt.find('"', imspos)]
281            shutil.copy(os.path.join(filepref[8:], imname), os.path.join(absubdir, imname))
282        if subdir:
283            tt = tt.replace(filepref, subdir+"/")
284        file(filename, "wb").write(tt.encode("utf8"))
285 
286
287    def saveXML(self):
288        filename = QFileDialog.getSaveFileName(self, "Export Report", self.saveDir, "XML file (*.xml)")
289        if not filename:
290            return
291
292        outf = file(str(filename), "wt")
293        outf.write('<?xml version="1.0" encoding="ascii"?>\n<report version="1.0">\n')
294       
295        for i in range(self.tree.count()):
296            item = self.tree.item(i)
297            outf.write('<entry name="%s" time="%s">\n' % (item.name, item.time))
298
299            filepref = "file:///"+self.tempdir
300            if filepref[-1] != os.sep:
301                filepref += os.sep
302            lfilepref = len(filepref)
303            imspos = -1
304            data = item.data
305            while True:
306                imspos = data.find(filepref, imspos+1)
307                if imspos == -1:
308                    break
309                imname = data[imspos+lfilepref:data.find('"', imspos)]
310                fname = os.path.join(filepref[8:], imname)
311                outf.write('    <binary name="%s"><![CDATA[%s]]></binary>\n' % (imname, binascii.b2a_base64(file(fname, "rb").read())))
312               
313            data = data.replace(filepref, "binary:///")
314            outf.write("<content><![CDATA[%s]]></content>\n\n\n" % data)
315            outf.write('</entry>\n')
316        outf.write('</report>')
317       
318
319    def loadXML(self):
320        filename = QFileDialog.getOpenFileName(self, "Import Report", self.saveDir, "XML file (*.xml)")
321        if not filename:
322            return
323
324        filepref = "file:///"+self.tempdir
325        if 1:#try:
326            x = xml.dom.minidom.parse(file(str(filename)))
327            x.normalize()
328            entries = []
329            files = []
330            for entry in x.getElementsByTagName("entry"):
331                data = entry.getElementsByTagName("content")[0].firstChild.data
332                for fle in entry.getElementsByTagName("binary"):
333                    name = oname = fle.getAttribute("name")
334                    base, ext = os.path.splitext(name)
335                    i = 0
336                    while os.path.exists(os.path.join(self.tempdir, name)):
337                        i += 1
338                        name = "%s%04i%s" % (base, i, ext)
339                    data = data.replace("binary:///"+oname, filepref+"/"+name)
340                    filedata = binascii.a2b_base64(fle.firstChild.data)
341                    files.append((name, filedata)) 
342                name = entry.getAttribute("name")
343                time = entry.getAttribute("time")
344                entries.append((name, data, None, QIcon(), time))
345               
346            for fname, fdata in files:
347                print fname, len(fdata)
348                file(os.path.join(self.tempdir, fname), "wb").write(fdata)
349            for entry in entries:
350                print entry[1]
351                self(*entry)
352        #except:
353            pass
354            # !!!!!!!!!!!
355       
356def getDepth(item, expanded=True):
357    ccount = item.childCount()
358    return 1 + (ccount and (not expanded or item.isExpanded()) and max(getDepth(item.child(cc), expanded) for cc in range(ccount)))
359
360# Need to use the tree's columnCount - children may have unattended additional columns
361# (this happens, e.g. in the tree viewer)
362def printTree(item, level, depthRem, visibleColumns, expanded=True):
363    res = '<tr>'+'<td width="16px"></td>'*level + \
364          '<td colspan="%i">%s</td>' % (depthRem, item.text(0) or (not level and "<root>") or "") + \
365          ''.join('<td style="padding-left:10px">%s</td>' % item.text(i) for i in visibleColumns) + \
366          '</tr>\n'
367    if not expanded or item.isExpanded():
368        for i in range(item.childCount()):
369            res += printTree(item.child(i), level+1, depthRem-1, visibleColumns, expanded)
370    return res
371   
372                   
373def reportTree(tree, expanded=True):
374    tops = tree.topLevelItemCount()
375    header = tree.headerItem()
376    visibleColumns = [i for i in range(1, tree.columnCount()) if not tree.isColumnHidden(i)] 
377
378    depth = tops and max(getDepth(tree.topLevelItem(cc), expanded) for cc in range(tops))
379    res = "<table>\n"
380    res += '<tr><th colspan="%i">%s</th>' % (depth, header.text(0))
381    res += ''.join('<th>%s</th>' % header.text(i) for i in visibleColumns)
382    res += '</tr>\n'
383    res += ''.join(printTree(tree.topLevelItem(cc), 0, depth, visibleColumns, expanded) for cc in range(tops))
384    res += "</table>\n"
385    return res
386 
387
388def reportCell(item, tag, style):
389    if not item:
390        return '<%s style="%s"/>' % (tag, style)
391    if isinstance(item, QTableWidgetItem):
392        alignment = {Qt.AlignLeft: "left", Qt.AlignRight: "right", Qt.AlignHCenter: "center"}.get(item.textAlignment() & Qt.AlignHorizontal_Mask, "left")
393        text = item.text().replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
394        return '<%s style="%s; text-align: %s">%s</%s>' % (tag, style, alignment, text, tag)
395    elif isinstance(item, QModelIndex):
396        align = item.data(Qt.TextAlignmentRole)
397        align, ok = align.toInt() if align.isValid() else Qt.AlignLeft, True 
398        alignment = {Qt.AlignLeft: "left", Qt.AlignRight: "right", Qt.AlignHCenter: "center"}.get(align & Qt.AlignHorizontal_Mask, "left")
399        value = item.data(Qt.DisplayRole)
400        if value.type() >= QVariant.UserType:
401            text = str(value.toPyObject())
402        else:
403            text = str(value.toString())
404        text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
405        return '<%s style="%s; text-align: %s">%s</%s>' % (tag, style, alignment, text, tag)
406    elif isinstance(item, tuple): #(QAbstractItemModel, headerIndex)
407        model, index = item
408        align = model.headerData(index, Qt.Horizontal, Qt.TextAlignmentRole)
409        align, ok = align.toInt() if align.isValid() else Qt.AlignLeft, True
410        alignment = {Qt.AlignLeft: "left", Qt.AlignRight: "right", Qt.AlignHCenter: "center"}.get(align & Qt.AlignHorizontal_Mask, "left")
411        text = str(model.headerData(index, Qt.Horizontal, Qt.DisplayRole).toString()).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
412        return '<%s style="%s; text-align: %s">%s</%s>' % (tag, style, alignment, text, tag)
413   
414def reportTable(table):
415    ncols = table.model().columnCount()
416    res = '<table style="border-bottom: thin solid black">\n'
417    vheadVisible = table.verticalHeader().isVisible()
418    shownColumns = [i for i in range(ncols) if not table.isColumnHidden(i)]
419    if table.horizontalHeader().isVisible():
420        res += "<tr>"+'<th></th>'*vheadVisible + "".join(reportCell(table.horizontalHeaderItem(i) if isinstance(table, QTableWidget) else (table.model(), i),
421                                                                    "th", "padding-left: 4px; padding-right: 4px;") for i in shownColumns) + "</tr>\n"
422        res += '<tr style="height: 2px">'+'<th colspan="%i"  style="border-bottom: thin solid black; height: 2px;"></th>' % (ncols+vheadVisible)
423    for j in range(table.model().rowCount()):
424        res += "<tr>"
425        if vheadVisible:
426            if isinstance(table, QTableWidget):
427                vhi = table.verticalHeaderItem(j)
428                text = vhi.text() if vhi else ""
429            else:
430                text = str(table.model().headerData(j, Qt.Vertical, Qt.DisplayRole).toString())
431               
432            res += "<th>%s</th>" % text
433        res += "".join(reportCell(table.item(j, i) if isinstance(table, QTableWidget) else table.model().index(j, i),
434                                  "td", "") for i in shownColumns) + "</tr>\n"
435    res += "</table>\n"
436    return res
Note: See TracBrowser for help on using the repository browser.