source: orange/orange/OrangeWidgets/OWReport.py @ 7140:e7d0af61901a

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