source: orange/Orange/OrangeWidgets/OWReport.py @ 10580:c4cbae8dcf8b

Revision 10580:c4cbae8dcf8b, 19.0 KB checked in by markotoplak, 2 years ago (diff)

Moved deprecation functions, progress bar support and environ into Orange.utils. Orange imports cleanly, although it is not tested yet.

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.utils 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        filename = unicode(filename)
232       
233        if not filename:
234            return
235       
236        path, fname = os.path.split(filename)
237        self.saveDir = path
238        if not os.path.exists(path):
239            try:
240                os.makedirs(path)
241            except:
242                QMessageBox.error(None, "Error", "Cannot create directory "+path)
243
244        tt = file(self.indexfile, "rt").read()
245       
246        index = "<br/>".join('<a href="#%s">%s</a>' % (self.tree.item(i).elementId, self.re_h1.search(self.tree.item(i).content).group("name"))
247                             for i in range(self.tree.count()))
248           
249######## Rewrite this to go through individual tree nodes. For one reason: this code used to work
250##       when the HTML stored in tree nodes included DIV and H1 tags, which it does not any more,
251##       so they have to be added here         
252
253        data = "\n".join(self.tree.item(i).content for i in range(self.tree.count()))
254
255        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))
256        tt = self.browser_re.sub("\\1", tt)
257       
258        filepref = "file:///"+self.tempdir
259        if filepref[-1] != os.sep:
260            filepref += os.sep
261        lfilepref = len(filepref)
262        imspos = -1
263        subdir = None
264        while True:
265            imspos = tt.find(filepref, imspos+1)
266            if imspos == -1:
267                break
268           
269            if not subdir:
270                subdir = os.path.splitext(fname)[0]
271                if subdir == fname:
272                    subdir += "_data"
273                cnt = 0
274                osubdir = subdir
275                while os.path.exists(os.path.join(path, subdir)):
276                    cnt += 1
277                    subdir = "%s%05i" % (osubdir, cnt)
278                absubdir = os.path.join(path, subdir)
279                os.mkdir(absubdir)
280
281            imname = tt[imspos+lfilepref:tt.find('"', imspos)]
282            shutil.copy(os.path.join(filepref[8:], imname), os.path.join(absubdir, imname))
283        if subdir:
284            tt = tt.replace(filepref, subdir+"/")
285        file(filename, "wb").write(tt.encode("utf8"))
286 
287
288    def saveXML(self):
289        filename = QFileDialog.getSaveFileName(self, "Export Report", self.saveDir, "XML file (*.xml)")
290        filename = unicode(filename)
291        if not filename:
292            return
293
294        outf = file(filename, "wt")
295        outf.write('<?xml version="1.0" encoding="ascii"?>\n<report version="1.0">\n')
296       
297        for i in range(self.tree.count()):
298            item = self.tree.item(i)
299            outf.write('<entry name="%s" time="%s">\n' % (item.name, item.time))
300
301            filepref = "file:///"+self.tempdir
302            if filepref[-1] != os.sep:
303                filepref += os.sep
304            lfilepref = len(filepref)
305            imspos = -1
306            data = item.data
307            while True:
308                imspos = data.find(filepref, imspos+1)
309                if imspos == -1:
310                    break
311                imname = data[imspos+lfilepref:data.find('"', imspos)]
312                fname = os.path.join(filepref[8:], imname)
313                outf.write('    <binary name="%s"><![CDATA[%s]]></binary>\n' % (imname, binascii.b2a_base64(file(fname, "rb").read())))
314               
315            data = data.replace(filepref, "binary:///")
316            outf.write("<content><![CDATA[%s]]></content>\n\n\n" % data)
317            outf.write('</entry>\n')
318        outf.write('</report>')
319       
320
321    def loadXML(self):
322        filename = QFileDialog.getOpenFileName(self, "Import Report", self.saveDir, "XML file (*.xml)")
323        filename = unicode(filename)
324       
325        if not filename:
326            return
327
328        filepref = "file:///"+self.tempdir
329        if 1:#try:
330            x = xml.dom.minidom.parse(file(str(filename)))
331            x.normalize()
332            entries = []
333            files = []
334            for entry in x.getElementsByTagName("entry"):
335                data = entry.getElementsByTagName("content")[0].firstChild.data
336                for fle in entry.getElementsByTagName("binary"):
337                    name = oname = fle.getAttribute("name")
338                    base, ext = os.path.splitext(name)
339                    i = 0
340                    while os.path.exists(os.path.join(self.tempdir, name)):
341                        i += 1
342                        name = "%s%04i%s" % (base, i, ext)
343                    data = data.replace("binary:///"+oname, filepref+"/"+name)
344                    filedata = binascii.a2b_base64(fle.firstChild.data)
345                    files.append((name, filedata)) 
346                name = entry.getAttribute("name")
347                time = entry.getAttribute("time")
348                entries.append((name, data, None, QIcon(), time))
349               
350            for fname, fdata in files:
351                print fname, len(fdata)
352                file(os.path.join(self.tempdir, fname), "wb").write(fdata)
353            for entry in entries:
354                print entry[1]
355                self(*entry)
356        #except:
357            pass
358            # !!!!!!!!!!!
359       
360def getDepth(item, expanded=True):
361    ccount = item.childCount()
362    return 1 + (ccount and (not expanded or item.isExpanded()) and max(getDepth(item.child(cc), expanded) for cc in range(ccount)))
363
364# Need to use the tree's columnCount - children may have unattended additional columns
365# (this happens, e.g. in the tree viewer)
366def printTree(item, level, depthRem, visibleColumns, expanded=True):
367    res = '<tr>'+'<td width="16px"></td>'*level + \
368          '<td colspan="%i">%s</td>' % (depthRem, item.text(0) or (not level and "<root>") or "") + \
369          ''.join('<td style="padding-left:10px">%s</td>' % item.text(i) for i in visibleColumns) + \
370          '</tr>\n'
371    if not expanded or item.isExpanded():
372        for i in range(item.childCount()):
373            res += printTree(item.child(i), level+1, depthRem-1, visibleColumns, expanded)
374    return res
375   
376                   
377def reportTree(tree, expanded=True):
378    tops = tree.topLevelItemCount()
379    header = tree.headerItem()
380    visibleColumns = [i for i in range(1, tree.columnCount()) if not tree.isColumnHidden(i)] 
381
382    depth = tops and max(getDepth(tree.topLevelItem(cc), expanded) for cc in range(tops))
383    res = "<table>\n"
384    res += '<tr><th colspan="%i">%s</th>' % (depth, header.text(0))
385    res += ''.join('<th>%s</th>' % header.text(i) for i in visibleColumns)
386    res += '</tr>\n'
387    res += ''.join(printTree(tree.topLevelItem(cc), 0, depth, visibleColumns, expanded) for cc in range(tops))
388    res += "</table>\n"
389    return res
390 
391
392def reportCell(item, tag, style):
393    if not item:
394        return '<%s style="%s"/>' % (tag, style)
395    if isinstance(item, QTableWidgetItem):
396        alignment = {Qt.AlignLeft: "left", Qt.AlignRight: "right", Qt.AlignHCenter: "center"}.get(item.textAlignment() & Qt.AlignHorizontal_Mask, "left")
397        text = item.text().replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
398        return '<%s style="%s; text-align: %s">%s</%s>' % (tag, style, alignment, text, tag)
399    elif isinstance(item, QModelIndex):
400        align = item.data(Qt.TextAlignmentRole)
401        align, ok = align.toInt() if align.isValid() else Qt.AlignLeft, True 
402        alignment = {Qt.AlignLeft: "left", Qt.AlignRight: "right", Qt.AlignHCenter: "center"}.get(align & Qt.AlignHorizontal_Mask, "left")
403        value = item.data(Qt.DisplayRole)
404        if value.type() >= QVariant.UserType:
405            text = str(value.toPyObject())
406        else:
407            text = str(value.toString())
408        text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
409        return '<%s style="%s; text-align: %s">%s</%s>' % (tag, style, alignment, text, tag)
410    elif isinstance(item, tuple): #(QAbstractItemModel, headerIndex)
411        model, index = item
412        align = model.headerData(index, Qt.Horizontal, Qt.TextAlignmentRole)
413        align, ok = align.toInt() if align.isValid() else Qt.AlignLeft, True
414        alignment = {Qt.AlignLeft: "left", Qt.AlignRight: "right", Qt.AlignHCenter: "center"}.get(align & Qt.AlignHorizontal_Mask, "left")
415        text = str(model.headerData(index, Qt.Horizontal, Qt.DisplayRole).toString()).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
416        return '<%s style="%s; text-align: %s">%s</%s>' % (tag, style, alignment, text, tag)
417   
418def reportTable(table):
419    ncols = table.model().columnCount()
420    res = '<table style="border-bottom: thin solid black">\n'
421    vheadVisible = table.verticalHeader().isVisible()
422    shownColumns = [i for i in range(ncols) if not table.isColumnHidden(i)]
423    if table.horizontalHeader().isVisible():
424        res += "<tr>"+'<th></th>'*vheadVisible + "".join(reportCell(table.horizontalHeaderItem(i) if isinstance(table, QTableWidget) else (table.model(), i),
425                                                                    "th", "padding-left: 4px; padding-right: 4px;") for i in shownColumns) + "</tr>\n"
426        res += '<tr style="height: 2px">'+'<th colspan="%i"  style="border-bottom: thin solid black; height: 2px;"></th>' % (ncols+vheadVisible)
427    for j in range(table.model().rowCount()):
428        res += "<tr>"
429        if vheadVisible:
430            if isinstance(table, QTableWidget):
431                vhi = table.verticalHeaderItem(j)
432                text = vhi.text() if vhi else ""
433            else:
434                text = str(table.model().headerData(j, Qt.Vertical, Qt.DisplayRole).toString())
435               
436            res += "<th>%s</th>" % text
437        res += "".join(reportCell(table.item(j, i) if isinstance(table, QTableWidget) else table.model().index(j, i),
438                                  "td", "") for i in shownColumns) + "</tr>\n"
439    res += "</table>\n"
440    return res
Note: See TracBrowser for help on using the repository browser.