source: orange/orange/OrangeWidgets/OWReport.py @ 7706:3a7f40995f8e

Revision 7706:3a7f40995f8e, 18.7 KB checked in by ales_erjavec <ales.erjavec@…>, 3 years ago (diff)
  • add content attribute to QListWidgetItem at creation creation (needed when saving)
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        widnode.content = newEntry
137        self.javascript("document.body.innerHTML += '%s'" % escape(newEntry))
138        if scrollIntoView:
139            self.javascript("document.getElementById('%s').scrollIntoView();" % widnode.elementId)
140
141
142    def selectionChanged(self, current, previous):
143        if current:
144            if self.dontScroll:
145                self.javascript("document.getElementById('%s').className = 'selected';" % current.elementId)
146            else:
147                self.javascript("""
148                    var newsel = document.getElementById('%s');
149                    newsel.className = 'selected';
150                    newsel.scrollIntoView();""" % current.elementId)
151#            if not self.dontScroll:
152#                self.javascript("newsel.scrollIntoView(document.getElementById('%s'));" % current.elementId)
153            self.showWidgetAction.setEnabled(current.widgetId >= 0)
154        if previous:
155            self.javascript("document.getElementById('%s').className = '';" % previous.elementId)
156       
157       
158    def rebuildHtml(self):
159        self.javascript("document.body.innerHTML = ''")
160        for i in range(self.tree.count()):
161            self.addEntry(self.tree.item(i))
162        selected = self.tree.selectedItems()
163        if selected:
164            self.selectionChanged(selected[0], None)
165       
166       
167    @pyqtSignature("QString") 
168    def changeItem(self, elid):
169        self.dontScroll = True
170        item = self.treeItems[str(elid)]
171        self.tree.setCurrentItem(item)
172        self.tree.scrollToItem(item)
173        self.dontScroll = False
174 
175    def raiseWidget(self, node):
176        for widget in self.widgets:
177            if widget.instance.widgetId == node.widgetId:
178                break
179        else:
180            return
181        widget.instance.reshow()
182       
183    def showActiveNodeWidget(self):
184        node = self.tree.currentItem()
185        if node:
186            self.raiseWidget(node)
187           
188    re_h1 = re.compile(r'<h1>(?P<name>.*?)<span class="timestamp">')
189    def itemChanged(self, node):
190        if hasattr(node, "content"):
191            be, en = self.re_h1.search(node.content).span("name")
192            node.content = node.content[:be] + str(node.text()) + node.content[en:]
193            self.rebuildHtml()
194
195    def removeActiveNode(self):
196        node = self.tree.currentItem()
197        if node:
198            self.tree.takeItem(self.tree.row(node))
199        self.rebuildHtml()
200
201    def clearReport(self):
202        self.tree.clear()
203        self.rebuildHtml()
204       
205    def printReport(self):
206        printer = QPrinter()
207        printDialog = QPrintDialog(printer, self)
208        printDialog.setWindowTitle("Print report")
209        if (printDialog.exec_() != QDialog.Accepted):
210            return
211        getattr(self.reportBrowser, "print")(printer)
212       
213       
214    def createDirectory(self):
215        tmpPathName = os.tempnam(orange-report)
216        os.mkdir(tmpPathName)
217        return tmpPathName
218   
219    def getUniqueFileName(self, patt):
220        for i in xrange(1000000):
221            fn = os.path.join(self.tempdir, patt % i)
222            if not os.path.exists(fn):
223                return "file:///"+fn, fn
224
225    img_re = re.compile(r'<IMG.*?\ssrc="(?P<imgname>[^"]*)"', re.DOTALL+re.IGNORECASE)
226    browser_re = re.compile(r'<!--browsercode(.*?)-->')
227    def saveReport(self):
228        filename = QFileDialog.getSaveFileName(self, "Save Report", self.saveDir, "Web page (*.html *.htm)")
229        if not filename:
230            return
231
232        filename = str(filename)
233        path, fname = os.path.split(filename)
234        self.saveDir = path
235        if not os.path.exists(path):
236            try:
237                os.makedirs(path)
238            except:
239                QMessageBox.error(None, "Error", "Cannot create directory "+path)
240
241        tt = file(self.indexfile, "rt").read()
242       
243        index = "<br/>".join('<a href="#%s">%s</a>' % (self.tree.item(i).elementId, self.re_h1.search(self.tree.item(i).content).group("name"))
244                             for i in range(self.tree.count()))
245           
246######## Rewrite this to go through individual tree nodes. For one reason: this code used to work
247##       when the HTML stored in tree nodes included DIV and H1 tags, which it does not any more,
248##       so they have to be added here         
249
250        data = "\n".join(self.tree.item(i).content for i in range(self.tree.count()))
251
252        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))
253        tt = self.browser_re.sub("\\1", tt)
254       
255        filepref = "file:///"+self.tempdir
256        if filepref[-1] != os.sep:
257            filepref += os.sep
258        lfilepref = len(filepref)
259        imspos = -1
260        subdir = None
261        while True:
262            imspos = tt.find(filepref, imspos+1)
263            if imspos == -1:
264                break
265           
266            if not subdir:
267                subdir = os.path.splitext(fname)[0]
268                if subdir == fname:
269                    subdir += "_data"
270                cnt = 0
271                osubdir = subdir
272                while os.path.exists(os.path.join(path, subdir)):
273                    cnt += 1
274                    subdir = "%s%05i" % (osubdir, cnt)
275                absubdir = os.path.join(path, subdir)
276                os.mkdir(absubdir)
277
278            imname = tt[imspos+lfilepref:tt.find('"', imspos)]
279            shutil.copy(os.path.join(filepref[8:], imname), os.path.join(absubdir, imname))
280        if subdir:
281            tt = tt.replace(filepref, subdir+"/")
282        file(filename, "wb").write(tt.encode("utf8"))
283 
284
285    def saveXML(self):
286        filename = QFileDialog.getSaveFileName(self, "Export Report", self.saveDir, "XML file (*.xml)")
287        if not filename:
288            return
289
290        outf = file(str(filename), "wt")
291        outf.write('<?xml version="1.0" encoding="ascii"?>\n<report version="1.0">\n')
292       
293        for i in range(self.tree.count()):
294            item = self.tree.item(i)
295            outf.write('<entry name="%s" time="%s">\n' % (item.name, item.time))
296
297            filepref = "file:///"+self.tempdir
298            if filepref[-1] != os.sep:
299                filepref += os.sep
300            lfilepref = len(filepref)
301            imspos = -1
302            data = item.data
303            while True:
304                imspos = data.find(filepref, imspos+1)
305                if imspos == -1:
306                    break
307                imname = data[imspos+lfilepref:data.find('"', imspos)]
308                fname = os.path.join(filepref[8:], imname)
309                outf.write('    <binary name="%s"><![CDATA[%s]]></binary>\n' % (imname, binascii.b2a_base64(file(fname, "rb").read())))
310               
311            data = data.replace(filepref, "binary:///")
312            outf.write("<content><![CDATA[%s]]></content>\n\n\n" % data)
313            outf.write('</entry>\n')
314        outf.write('</report>')
315       
316
317    def loadXML(self):
318        filename = QFileDialog.getOpenFileName(self, "Import Report", self.saveDir, "XML file (*.xml)")
319        if not filename:
320            return
321
322        filepref = "file:///"+self.tempdir
323        if 1:#try:
324            x = xml.dom.minidom.parse(file(str(filename)))
325            x.normalize()
326            entries = []
327            files = []
328            for entry in x.getElementsByTagName("entry"):
329                data = entry.getElementsByTagName("content")[0].firstChild.data
330                for fle in entry.getElementsByTagName("binary"):
331                    name = oname = fle.getAttribute("name")
332                    base, ext = os.path.splitext(name)
333                    i = 0
334                    while os.path.exists(os.path.join(self.tempdir, name)):
335                        i += 1
336                        name = "%s%04i%s" % (base, i, ext)
337                    data = data.replace("binary:///"+oname, filepref+"/"+name)
338                    filedata = binascii.a2b_base64(fle.firstChild.data)
339                    files.append((name, filedata)) 
340                name = entry.getAttribute("name")
341                time = entry.getAttribute("time")
342                entries.append((name, data, None, QIcon(), time))
343               
344            for fname, fdata in files:
345                print fname, len(fdata)
346                file(os.path.join(self.tempdir, fname), "wb").write(fdata)
347            for entry in entries:
348                print entry[1]
349                self(*entry)
350        #except:
351            pass
352            # !!!!!!!!!!!
353       
354def getDepth(item, expanded=True):
355    ccount = item.childCount()
356    return 1 + (ccount and (not expanded or item.isExpanded()) and max(getDepth(item.child(cc), expanded) for cc in range(ccount)))
357
358# Need to use the tree's columnCount - children may have unattended additional columns
359# (this happens, e.g. in the tree viewer)
360def printTree(item, level, depthRem, visibleColumns, expanded=True):
361    res = '<tr>'+'<td width="16px"></td>'*level + \
362          '<td colspan="%i">%s</td>' % (depthRem, item.text(0) or (not level and "<root>") or "") + \
363          ''.join('<td style="padding-left:10px">%s</td>' % item.text(i) for i in visibleColumns) + \
364          '</tr>\n'
365    if not expanded or item.isExpanded():
366        for i in range(item.childCount()):
367            res += printTree(item.child(i), level+1, depthRem-1, visibleColumns, expanded)
368    return res
369   
370                   
371def reportTree(tree, expanded=True):
372    tops = tree.topLevelItemCount()
373    header = tree.headerItem()
374    visibleColumns = [i for i in range(1, tree.columnCount()) if not tree.isColumnHidden(i)] 
375
376    depth = tops and max(getDepth(tree.topLevelItem(cc), expanded) for cc in range(tops))
377    res = "<table>\n"
378    res += '<tr><th colspan="%i">%s</th>' % (depth, header.text(0))
379    res += ''.join('<th>%s</th>' % header.text(i) for i in visibleColumns)
380    res += '</tr>\n'
381    res += ''.join(printTree(tree.topLevelItem(cc), 0, depth, visibleColumns, expanded) for cc in range(tops))
382    res += "</table>\n"
383    return res
384 
385
386def reportCell(item, tag, style):
387    if not item:
388        return '<%s style="%s"/>' % (tag, style)
389    if isinstance(item, QTableWidgetItem):
390        alignment = {Qt.AlignLeft: "left", Qt.AlignRight: "right", Qt.AlignHCenter: "center"}.get(item.textAlignment() & Qt.AlignHorizontal_Mask, "left")
391        text = item.text().replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
392        return '<%s style="%s; text-align: %s">%s</%s>' % (tag, style, alignment, text, tag)
393    elif isinstance(item, QModelIndex):
394        align = item.data(Qt.TextAlignmentRole)
395        align, ok = align.toInt() if align.isValid() else Qt.AlignLeft, True 
396        alignment = {Qt.AlignLeft: "left", Qt.AlignRight: "right", Qt.AlignHCenter: "center"}.get(align & Qt.AlignHorizontal_Mask, "left")
397        text = str(item.data(Qt.DisplayRole).toString()).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, tuple): #(QAbstractItemModel, headerIndex)
400        model, ind = item
401        align = model.headerData(index, Qt.Horizontal, Qt.TextAlignmentRole)
402        align, ok = align.toInt() if align.isValid() else Qt.AlignLeft, True
403        alignment = {Qt.AlignLeft: "left", Qt.AlignRight: "right", Qt.AlignHCenter: "center"}.get(align & Qt.AlignHorizontal_Mask, "left")
404        text = str(model.headerData(index, Qt.Horizontal, Qt.DisplayRole).toString()).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
405        return '<%s style="%s; text-align: %s">%s</%s>' % (tag, style, alignment, text, tag)
406   
407def reportTable(table):
408    ncols = table.model().columnCount()
409    res = '<table style="border-bottom: thin solid black">\n'
410    vheadVisible = table.verticalHeader().isVisible()
411    shownColumns = [i for i in range(ncols) if not table.isColumnHidden(i)]
412    if table.horizontalHeader().isVisible():
413        res += "<tr>"+'<th></th>'*vheadVisible + "".join(reportCell(table.horizontalHeaderItem(i) if isinstance(table, QTableWidget) else (table.model(), i),
414                                                                    "th", "padding-left: 4px; padding-right: 4px;") for i in shownColumns) + "</tr>\n"
415        res += '<tr style="height: 2px">'+'<th colspan="%i"  style="border-bottom: thin solid black; height: 2px;"></th>' % (ncols+vheadVisible)
416    for j in range(table.model().rowCount()):
417        res += "<tr>"
418        if vheadVisible:
419            if isinstance(table, QTableWidget):
420                vhi = table.verticalHeaderItem(j)
421                text = vhi.text() if vhi else ""
422            else:
423                text = str(table.model().headerData(j, Qt.Vertical, Qt.DisplayRole).toString())
424               
425            res += "<th>%s</th>" % text
426        res += "".join(reportCell(table.item(j, i) if isinstance(table, QTableWidget) else table.model().index(j, i),
427                                  "td", "") for i in shownColumns) + "</tr>\n"
428    res += "</table>\n"
429    return res
Note: See TracBrowser for help on using the repository browser.