source: orange/Orange/OrangeWidgets/Data/OWImageViewer.py @ 11871:be57aa910cf9

Revision 11871:be57aa910cf9, 23.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 weeks ago (diff)

Changed image scaling (zoom).

The final shown image size is based on the pixmap's original size
(before it was based on a 'fixed' tile size).

Line 
1import weakref
2import logging
3from xml.sax.saxutils import escape
4from collections import namedtuple
5from functools import partial
6from itertools import izip_longest
7
8from PyQt4.QtCore import pyqtSignal as Signal
9from PyQt4.QtNetwork import (
10    QNetworkAccessManager, QNetworkDiskCache, QNetworkRequest, QNetworkReply
11)
12
13from OWWidget import *
14from OWItemModels import VariableListModel
15from OWConcurrent import Future, FutureWatcher
16
17import OWGUI
18
19_log = logging.getLogger(__name__)
20
21
22NAME = "Image Viewer"
23DESCRIPTION = "Views images embedded in the data."
24LONG_DESCRIPTION = ""
25ICON = "icons/ImageViewer.svg"
26PRIORITY = 4050
27AUTHOR = "Ales Erjavec"
28AUTHOR_EMAIL = "ales.erjavec(@at@)fri.uni-lj.si"
29INPUTS = [("Data", Orange.data.Table, "setData")]
30OUTPUTS = [("Data", Orange.data.Table, )]
31
32
33class GraphicsPixmapWidget(QGraphicsWidget):
34
35    def __init__(self, pixmap, parent=None):
36        QGraphicsWidget.__init__(self, parent)
37        self.setCacheMode(QGraphicsItem.ItemCoordinateCache)
38        self._pixmap = pixmap
39        self._pixmapSize = QSizeF()
40        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
41
42    def setPixmap(self, pixmap):
43        if self._pixmap != pixmap:
44            self._pixmap = QPixmap(pixmap)
45            self.updateGeometry()
46
47    def pixmap(self):
48        return QPixmap(self._pixmap)
49
50    def setPixmapSize(self, size):
51        if self._pixmapSize != size:
52            self._pixmapSize = QSizeF(size)
53            self.updateGeometry()
54
55    def pixmapSize(self):
56        if self._pixmapSize.isValid():
57            return QSizeF(self._pixmapSize)
58        else:
59            return QSizeF(self._pixmap.size())
60
61    def sizeHint(self, which, constraint=QSizeF()):
62        if which == Qt.PreferredSize:
63            return self.pixmapSize()
64        else:
65            return QGraphicsWidget.sizeHint(self, which, constraint)
66
67    def paint(self, painter, option, widget=0):
68        if self._pixmap.isNull():
69            return
70
71        rect = self.contentsRect()
72
73        pixsize = self.pixmapSize()
74        pixrect = QRectF(QPointF(0, 0), pixsize)
75        pixrect.moveCenter(rect.center())
76
77        painter.save()
78        painter.setPen(QPen(QColor(0, 0, 0, 50), 3))
79        painter.drawRoundedRect(pixrect, 2, 2)
80        painter.setRenderHint(QPainter.SmoothPixmapTransform)
81        source = QRectF(QPointF(0, 0), QSizeF(self._pixmap.size()))
82        painter.drawPixmap(pixrect, self._pixmap, source)
83        painter.restore()
84
85
86class GraphicsTextWidget(QGraphicsWidget):
87
88    def __init__(self, text, parent=None):
89        QGraphicsWidget.__init__(self, parent)
90        self.labelItem = QGraphicsTextItem(self)
91        self.setHtml(text)
92
93        self.labelItem.document().documentLayout().documentSizeChanged.connect(
94            self.onLayoutChanged
95        )
96
97    def onLayoutChanged(self, *args):
98        self.updateGeometry()
99
100    def sizeHint(self, which, constraint=QSizeF()):
101        if which == Qt.MinimumSize:
102            return self.labelItem.boundingRect().size()
103        else:
104            return self.labelItem.boundingRect().size()
105
106    def setTextWidth(self, width):
107        self.labelItem.setTextWidth(width)
108
109    def setHtml(self, text):
110        self.labelItem.setHtml(text)
111
112
113class GraphicsThumbnailWidget(QGraphicsWidget):
114
115    def __init__(self, pixmap, title="", parent=None):
116        QGraphicsWidget.__init__(self, parent)
117
118        self._title = None
119        self._size = QSizeF()
120
121        layout = QGraphicsLinearLayout(Qt.Vertical, self)
122        layout.setSpacing(2)
123        layout.setContentsMargins(5, 5, 5, 5)
124        self.setContentsMargins(0, 0, 0, 0)
125
126        self.pixmapWidget = GraphicsPixmapWidget(pixmap, self)
127        self.labelWidget = GraphicsTextWidget(title, self)
128
129        layout.addItem(self.pixmapWidget)
130        layout.addItem(self.labelWidget)
131
132        layout.setAlignment(self.pixmapWidget, Qt.AlignCenter)
133        layout.setAlignment(self.labelWidget, Qt.AlignHCenter | Qt.AlignBottom)
134        self.setLayout(layout)
135
136        self.setSizePolicy(QSizePolicy.MinimumExpanding,
137                           QSizePolicy.MinimumExpanding)
138
139        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
140        self.setTitle(title)
141        self.setTitleWidth(100)
142
143    def setPixmap(self, pixmap):
144        self.pixmapWidget.setPixmap(pixmap)
145        self._updatePixmapSize()
146
147    def pixmap(self):
148        return self.pixmapWidget.pixmap()
149
150    def setTitle(self, title):
151        if self._title != title:
152            self._title = title
153            self.labelWidget.setHtml(
154                '<center>' + escape(title) + '</center>'
155            )
156            self.layout().invalidate()
157
158    def title(self):
159        return self._title
160
161    def setThumbnailSize(self, size):
162        if self._size != size:
163            self._size = QSizeF(size)
164            self._updatePixmapSize()
165            self.labelWidget.setTextWidth(max(100, size.width()))
166
167    def setTitleWidth(self, width):
168        self.labelWidget.setTextWidth(width)
169        self.layout().invalidate()
170
171    def paint(self, painter, option, widget=0):
172        contents = self.contentsRect()
173        if self.isSelected():
174            painter.save()
175            painter.setPen(QPen(QColor(125, 162, 206, 192)))
176            painter.setBrush(QBrush(QColor(217, 232, 252, 192)))
177            painter.drawRoundedRect(QRectF(contents.topLeft(),
178                                           self.geometry().size()), 3, 3)
179            painter.restore()
180
181    def _updatePixmapSize(self):
182        pixmap = self.pixmap()
183        if not pixmap.isNull() and self._size.isValid():
184            pixsize = QSizeF(self.pixmap().size())
185            pixsize.scale(self._size, Qt.KeepAspectRatio)
186        else:
187            pixsize = QSizeF()
188        self.pixmapWidget.setPixmapSize(pixsize)
189
190
191class ThumbnailWidget(QGraphicsWidget):
192
193    def __init__(self, parent=None):
194        QGraphicsWidget.__init__(self, parent)
195        self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
196        self.setContentsMargins(10, 10, 10, 10)
197        layout = QGraphicsGridLayout()
198        layout.setContentsMargins(0, 0, 0, 0)
199        layout.setSpacing(10)
200        self.setLayout(layout)
201
202    def setGeometry(self, geom):
203        super(ThumbnailWidget, self).setGeometry(geom)
204        self.reflow(self.size().width())
205
206    def reflow(self, width):
207        if not self.layout():
208            return
209
210        left, right, _, _ = self.getContentsMargins()
211        layout = self.layout()
212        width -= left + right
213
214        hints = self._hints(Qt.PreferredSize)
215        widths = [max(24, h.width()) for h in hints]
216        ncol = self._fitncols(widths, layout.horizontalSpacing(), width)
217
218        if ncol == layout.columnCount():
219            return
220
221        items = [layout.itemAt(i) for i in range(layout.count())]
222
223        # first remove all items from the layout
224        for item in items:
225            layout.removeItem(item)
226        # add them back in updated positions
227        for i, item in enumerate(items):
228            layout.addItem(item, i // ncol, i % ncol)
229
230    def items(self):
231        layout = self.layout()
232        if layout:
233            return [layout.itemAt(i) for i in range(layout.count())]
234        else:
235            return []
236
237    def _hints(self, which):
238        return [item.effectiveSizeHint(which) for item in self.items()]
239
240    def _fitncols(self, widths, spacing, constraint):
241        def sliced(seq, ncol):
242            return [seq[i:i + ncol] for i in range(0, len(seq), ncol)]
243
244        def flow_width(widths, spacing, ncol):
245            W = sliced(widths, ncol)
246            col_widths = map(max, izip_longest(*W, fillvalue=0))
247            return sum(col_widths) + (ncol - 1) * spacing
248
249        ncol_best = 1
250        for ncol in range(2, len(widths)):
251            w = flow_width(widths, spacing, ncol)
252            if w <= constraint:
253                ncol_best = ncol
254            else:
255                break
256
257        return ncol_best
258
259
260class GraphicsScene(QGraphicsScene):
261
262    selectionRectPointChanged = Signal(QPointF)
263
264    def __init__(self, *args):
265        QGraphicsScene.__init__(self, *args)
266        self.selectionRect = None
267
268    def mousePressEvent(self, event):
269        QGraphicsScene.mousePressEvent(self, event)
270
271    def mouseMoveEvent(self, event):
272        if event.buttons() & Qt.LeftButton:
273            screenPos = event.screenPos()
274            buttonDown = event.buttonDownScreenPos(Qt.LeftButton)
275            if (screenPos - buttonDown).manhattanLength() > 2.0:
276                self.updateSelectionRect(event)
277        QGraphicsScene.mouseMoveEvent(self, event)
278
279    def mouseReleaseEvent(self, event):
280        if event.button() == Qt.LeftButton:
281            if self.selectionRect:
282                self.removeItem(self.selectionRect)
283                self.selectionRect = None
284        QGraphicsScene.mouseReleaseEvent(self, event)
285
286    def updateSelectionRect(self, event):
287        pos = event.scenePos()
288        buttonDownPos = event.buttonDownScenePos(Qt.LeftButton)
289        rect = QRectF(pos, buttonDownPos).normalized()
290        rect = rect.intersected(self.sceneRect())
291        if not self.selectionRect:
292            self.selectionRect = QGraphicsRectItem()
293            self.selectionRect.setBrush(QColor(10, 10, 10, 20))
294            self.selectionRect.setPen(QPen(QColor(200, 200, 200, 200)))
295            self.addItem(self.selectionRect)
296        self.selectionRect.setRect(rect)
297        if event.modifiers() & Qt.ControlModifier or \
298                event.modifiers() & Qt.ShiftModifier:
299            path = self.selectionArea()
300        else:
301            path = QPainterPath()
302        path.addRect(rect)
303        self.setSelectionArea(path)
304        self.selectionRectPointChanged.emit(pos)
305
306
307_ImageItem = namedtuple(
308    "_ImageItem",
309    ["widget",    # GraphicsThumbnailWidget belonging to this item.
310     "url",       # Composed final url.
311     "future"]    # Future instance yielding an QImage
312)
313
314
315class OWImageViewer(OWWidget):
316    contextHandlers = {
317        "": DomainContextHandler("", ["imageAttr", "titleAttr"])
318    }
319    settingsList = ["zoom"]
320
321    def __init__(self, parent=None, signalManager=None, name="Image viewer"):
322        OWWidget.__init__(self, parent, signalManager, name, wantGraph=True)
323
324        self.inputs = [("Data", ExampleTable, self.setData)]
325        self.outputs = [("Data", ExampleTable)]
326
327        self.imageAttr = 0
328        self.titleAttr = 0
329        self.zoom = 25
330        self.autoCommit = False
331        self.selectionChangedFlag = False
332
333        #
334        # GUI
335        #
336
337        self.loadSettings()
338
339        self.info = OWGUI.widgetLabel(
340            OWGUI.widgetBox(self.controlArea, "Info"),
341            "Waiting for input\n"
342        )
343
344        self.imageAttrCB = OWGUI.comboBox(
345            self.controlArea, self, "imageAttr",
346            box="Image Filename Attribute",
347            tooltip="Attribute with image filenames",
348            callback=[self.clearScene, self.setupScene],
349            addSpace=True
350        )
351
352        self.titleAttrCB = OWGUI.comboBox(
353            self.controlArea, self, "titleAttr",
354            box="Title Attribute",
355            tooltip="Attribute with image title",
356            callback=self.updateTitles,
357            addSpace=True
358        )
359
360        OWGUI.hSlider(
361            self.controlArea, self, "zoom",
362            box="Zoom", minValue=1, maxValue=100, step=1,
363            callback=self.updateZoom,
364            createLabel=False
365        )
366
367        OWGUI.separator(self.controlArea)
368
369        box = OWGUI.widgetBox(self.controlArea, "Selection")
370        b = OWGUI.button(box, self, "Commit", callback=self.commit)
371        cb = OWGUI.checkBox(
372            box, self, "autoCommit", "Commit on any change",
373            tooltip="Send selections on any change",
374            callback=self.commitIf
375        )
376
377        OWGUI.setStopper(self, b, cb, "selectionChangedFlag",
378                         callback=self.commit)
379
380        OWGUI.rubber(self.controlArea)
381
382        self.scene = GraphicsScene()
383        self.sceneView = QGraphicsView(self.scene, self)
384        self.sceneView.setAlignment(Qt.AlignTop | Qt.AlignLeft)
385        self.sceneView.setRenderHint(QPainter.Antialiasing, True)
386        self.sceneView.setRenderHint(QPainter.TextAntialiasing, True)
387        self.sceneView.setFocusPolicy(Qt.WheelFocus)
388        self.sceneView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
389        self.sceneView.installEventFilter(self)
390        self.mainArea.layout().addWidget(self.sceneView)
391
392        self.scene.selectionChanged.connect(self.onSelectionChanged)
393        self.scene.selectionRectPointChanged.connect(
394            self.onSelectionRectPointChanged, Qt.QueuedConnection
395        )
396        self.graphButton.clicked.connect(self.saveScene)
397        self.resize(800, 600)
398
399        self.thumbnailWidget = None
400        self.sceneLayout = None
401        self.selectedExamples = []
402
403        #: List of _ImageItems
404        self.items = []
405
406        self._errcount = 0
407        self._successcount = 0
408
409        self.loader = ImageLoader(self)
410
411    def setData(self, data):
412        self.data = data
413        self.closeContext("")
414        self.information(0)
415        self.error(0)
416        self.imageAttrCB.clear()
417        self.titleAttrCB.clear()
418        self.clearScene()
419
420        if data is not None:
421            self.allAttrs = data.domain.variables + data.domain.getmetas().values()
422            self.stringAttrs = [attr for attr in self.allAttrs
423                                if isinstance(attr, Orange.feature.String)]
424
425            self.stringAttrs = sorted(
426                self.stringAttrs,
427                key=lambda attr: 0 if "type" in attr.attributes else 1
428            )
429
430            self.imageAttrCB.setModel(VariableListModel(self.stringAttrs))
431            self.titleAttrCB.setModel(VariableListModel(self.allAttrs))
432
433            self.openContext("", data)
434
435            self.imageAttr = max(min(self.imageAttr, len(self.stringAttrs) - 1), 0)
436            self.titleAttr = max(min(self.titleAttr, len(self.allAttrs) - 1), 0)
437
438            if self.stringAttrs:
439                self.setupScene()
440        else:
441            self.info.setText("Waiting for input\n")
442
443    def setupScene(self):
444        self.information(0)
445        self.error(0)
446        if self.data:
447            attr = self.stringAttrs[self.imageAttr]
448            titleAttr = self.allAttrs[self.titleAttr]
449            instances = [inst for inst in self.data
450                         if not inst[attr].isSpecial()]
451            widget = ThumbnailWidget()
452            layout = widget.layout()
453
454            self.scene.addItem(widget)
455
456            for i, inst in enumerate(instances):
457                url = self.urlFromValue(inst[attr])
458                title = str(inst[titleAttr])
459
460                thumbnail = GraphicsThumbnailWidget(
461                    QPixmap(), title=title, parent=widget
462                )
463
464                thumbnail.setToolTip(unicode(url.toString()))
465                thumbnail.instance = inst
466                layout.addItem(thumbnail, i / 5, i % 5)
467
468                if url.isValid():
469                    future = self.loader.get(url)
470                    watcher = FutureWatcher(future, parent=thumbnail)
471
472                    def set_pixmap(thumb=thumbnail, future=future):
473                        if future.cancelled():
474                            return
475                        if future.exception():
476                            # Should be some generic error image.
477                            pixmap = QPixmap()
478                            thumb.setToolTip(unicode(thumb.toolTip()) + "\n" +
479                                             str(future.exception()))
480                        else:
481                            pixmap = QPixmap.fromImage(future.result())
482
483                        thumb.setPixmap(pixmap)
484                        if not pixmap.isNull():
485                            thumb.setThumbnailSize(self.pixmapSize(pixmap))
486
487                        self._updateStatus(future)
488
489                    watcher.finished.connect(set_pixmap, Qt.QueuedConnection)
490                else:
491                    future = None
492                self.items.append(_ImageItem(thumbnail, url, future))
493
494            widget.show()
495            widget.geometryChanged.connect(self._updateSceneRect)
496
497            self.info.setText("Retrieving...\n")
498            self.thumbnailWidget = widget
499            self.sceneLayout = layout
500
501        if self.sceneLayout:
502            width = (self.sceneView.width() -
503                     self.sceneView.verticalScrollBar().width())
504            self.thumbnailWidget.reflow(width)
505            self.thumbnailWidget.setPreferredWidth(width)
506            self.sceneLayout.activate()
507
508    def filenameFromValue(self, value):
509        variable = value.variable
510        origin = variable.attributes.get("origin", "")
511        name = str(value)
512        return os.path.join(origin, name)
513
514    def urlFromValue(self, value):
515        variable = value.variable
516        origin = variable.attributes.get("origin", "")
517        if origin and QDir(origin).exists():
518            origin = QUrl.fromLocalFile(origin)
519        elif origin:
520            origin = QUrl(origin)
521            if not origin.scheme():
522                origin.setScheme("file")
523        else:
524            origin = QUrl("")
525        base = unicode(origin.path())
526        if base.strip() and not base.endswith("/"):
527            origin.setPath(base + "/")
528
529        name = QUrl(str(value))
530        url = origin.resolved(name)
531        if not url.scheme():
532            url.setScheme("file")
533        return url
534
535    def pixmapSize(self, pixmap):
536        """
537        Return the preferred pixmap size based on the current `zoom` value.
538        """
539        scale = 2 * self.zoom / 100.0
540        size = QSizeF(pixmap.size()) * scale
541        return size.expandedTo(QSizeF(16, 16))
542
543    def clearScene(self):
544        for item in self.items:
545            if item.future:
546                item.future._reply.close()
547                item.future.cancel()
548
549        self.items = []
550        self._errcount = 0
551        self._successcount = 0
552
553        self.scene.clear()
554        self.thumbnailWidget = None
555        self.sceneLayout = None
556
557    def thumbnailItems(self):
558        return [item.widget for item in self.items]
559
560    def updateZoom(self):
561        for item in self.thumbnailItems():
562            item.setThumbnailSize(self.pixmapSize(item.pixmap()))
563
564        if self.thumbnailWidget:
565            width = (self.sceneView.width() -
566                     self.sceneView.verticalScrollBar().width())
567
568            self.thumbnailWidget.reflow(width)
569            self.thumbnailWidget.setPreferredWidth(width)
570
571        if self.sceneLayout:
572            self.sceneLayout.activate()
573
574    def updateTitles(self):
575        titleAttr = self.allAttrs[self.titleAttr]
576        for item in self.items:
577            item.widget.setTitle(str(item.widget.instance[titleAttr]))
578
579    def onSelectionChanged(self):
580        selected = [item.widget for item in self.items
581                    if item.widget.isSelected()]
582        self.selectedExamples = [item.instance for item in selected]
583        self.commitIf()
584
585    def onSelectionRectPointChanged(self, point):
586        self.sceneView.ensureVisible(QRectF(point, QSizeF(1, 1)), 5, 5)
587
588    def commitIf(self):
589        if self.autoCommit:
590            self.commit()
591        else:
592            self.selectionChangedFlag = True
593
594    def commit(self):
595        if self.data:
596            if self.selectedExamples:
597                selected = Orange.data.Table(self.data.domain, self.selectedExamples)
598            else:
599                selected = None
600            self.send("Data", selected)
601        else:
602            self.send("Data", None)
603        self.selectionChangedFlag = False
604
605    def saveScene(self):
606        from OWDlgs import OWChooseImageSizeDlg
607        sizeDlg = OWChooseImageSizeDlg(self.scene, parent=self)
608        sizeDlg.exec_()
609
610    def _updateStatus(self, future):
611        if future.cancelled():
612            return
613
614        if future.exception():
615            self._errcount += 1
616            _log.debug("Error: %r", future.exception())
617        else:
618            self._successcount += 1
619
620        count = len([item for item in self.items if item.future is not None])
621        self.info.setText(
622            "Retrieving:\n" +
623            "{} of {} images" .format(self._successcount, count))
624
625        if self._errcount + self._successcount == count:
626            if self._errcount:
627                self.info.setText(
628                    "Done:\n" +
629                    "{} images, {} errors".format(count, self._errcount)
630                )
631            else:
632                self.info.setText(
633                    "Done:\n" +
634                    "{} images".format(count)
635                )
636            attr = self.stringAttrs[self.imageAttr]
637            if self._errcount == count and not "type" in attr.attributes:
638                self.error(0,
639                           "No images found! Make sure the '%s' attribute "
640                           "is tagged with 'type=image'" % attr.name)
641
642    def _updateSceneRect(self):
643        self.scene.setSceneRect(self.scene.itemsBoundingRect())
644
645    def onDeleteWidget(self):
646        for item in self.items:
647            item.future._reply.abort()
648            item.future.cancel()
649
650    def eventFilter(self, receiver, event):
651        if receiver is self.sceneView and event.type() == QEvent.Resize \
652                and self.thumbnailWidget:
653            width = (self.sceneView.width() -
654                     self.sceneView.verticalScrollBar().width())
655
656            self.thumbnailWidget.reflow(width)
657            self.thumbnailWidget.setPreferredWidth(width)
658
659        return super(OWImageViewer, self).eventFilter(receiver, event)
660
661
662class ImageLoader(QObject):
663
664    #: A weakref to a QNetworkAccessManager used for image retrieval.
665    #: (we can only have only one QNetworkDiskCache opened on the same
666    #: directory)
667    _NETMANAGER_REF = None
668
669    def __init__(self, parent=None):
670        QObject.__init__(self, parent=None)
671        assert QThread.currentThread() is QApplication.instance().thread()
672
673        netmanager = self._NETMANAGER_REF and self._NETMANAGER_REF()
674        if netmanager is None:
675            netmanager = QNetworkAccessManager()
676            cache = QNetworkDiskCache()
677            cache.setCacheDirectory(
678                os.path.join(environ.widget_settings_dir,
679                             __name__ + ".ImageLoader.Cache")
680            )
681            netmanager.setCache(cache)
682            ImageLoader._NETMANAGER_REF = weakref.ref(netmanager)
683        self._netmanager = netmanager
684
685    def get(self, url):
686        future = Future()
687        url = url = QUrl(url)
688        request = QNetworkRequest(url)
689        request.setAttribute(
690            QNetworkRequest.CacheLoadControlAttribute,
691            QNetworkRequest.PreferCache
692        )
693
694        # Future yielding a QNetworkReply when finished.
695        reply = self._netmanager.get(request)
696        future._reply = reply
697
698        def on_reply_ready(reply, future):
699            if reply.error() == QNetworkReply.OperationCanceledError:
700                # The network request itself was canceled
701                future.cancel()
702                return
703
704            if reply.error() != QNetworkReply.NoError:
705                # XXX Maybe convert the error into standard
706                # http and urllib exceptions.
707                future.set_exception(Exception(reply.errorString()))
708                return
709
710            reader = QImageReader(reply)
711            image = reader.read()
712
713            if image.isNull():
714                future.set_exception(Exception(reader.errorString()))
715            else:
716                future.set_result(image)
717
718        reply.finished.connect(partial(on_reply_ready, reply, future))
719        return future
720
721
722if __name__ == "__main__":
723    app = QApplication([])
724    w = OWImageViewer()
725    w.show()
726    data = Orange.data.Table(os.path.expanduser("~/Downloads/pex11_orng_sample/pex11_sample.tab"))
727    os.chdir(os.path.expanduser("~/Downloads/pex11_orng_sample/"))
728    w.setData(data)
729    app.exec_()
730    w.saveSettings()
Note: See TracBrowser for help on using the repository browser.