source: orange-bioinformatics/orangecontrib/bio/widgets/OWGEODatasets.py @ 1944:91a972497fcc

Revision 1944:91a972497fcc, 23.9 KB checked in by markotoplak, 4 months ago (diff)

GEO datasets: when outputting samples as rows and there a multiple types, all types become meta attributes and the only selected type becomes the class.

Line 
1"""<name>GEO Data Sets</name>
2<description>Access to Gene Expression Omnibus data sets.</description>
3<priority>20</priority>
4<contact>Ales Erjavec (ales.erjavec(@at@)fri.uni-lj.si)</contact>
5<icon>icons/GEODataSets.svg</icon>
6"""
7
8from __future__ import absolute_import, with_statement
9
10from collections import defaultdict
11from functools import partial
12import sys, os, glob
13
14from Orange.orng import orngServerFiles
15from Orange.orng.orngDataCaching import data_hints
16from Orange.OrangeWidgets import OWGUI, OWGUIEx
17from Orange.OrangeWidgets.OWWidget import *
18
19from .. import obiGEO
20
21NAME = "GEO Data Sets"
22DESCRIPTION = "Access to Gene Expression Omnibus data sets."
23ICON = "icons/GEODataSets.svg"
24PRIORITY = 20
25
26INPUTS = []
27OUTPUTS = [("Expression Data", Orange.data.Table)]
28
29REPLACES = ["_bioinformatics.widgets.OWGEODatasets.OWGEODatasets"]
30
31
32LOCAL_GDS_COLOR = Qt.darkGreen
33
34TextFilterRole = OWGUI.OrangeUserRole.next()
35
36class TreeModel(QAbstractItemModel):
37    def __init__(self, data, header, parent):
38        QAbstractItemModel.__init__(self, parent)
39        self._data = [[QVariant(s) for s in row] for row in data]
40        self._dataDict = {}
41        self._header = {Qt.Horizontal: dict([(i, {Qt.DisplayRole: h}) for i, h in enumerate(header)])}
42        self._roleData = {Qt.DisplayRole:self._data}
43        dataStore = partial(defaultdict, partial(defaultdict, partial(defaultdict, QVariant)))
44        self._roleData = dataStore(self._roleData)
45        self._header = dataStore(self._header)
46   
47    def setColumnLinks(self, column, links):
48        font =QFont()
49        font.setUnderline(True)
50        font = QVariant(font)
51        for i, link in enumerate(links):
52            self._roleData[LinkRole][i][column] = QVariant(link)
53            self._roleData[Qt.FontRole][i][column] = font
54            self._roleData[Qt.ForegroundRole][i][column] = QVariant(QColor(Qt.blue))
55   
56    def setRoleData(self, role, row, col, data):
57        self._roleData[role][row][col] = data
58       
59    def setData(self, index, value, role=Qt.EditRole):
60        self._roleData[role][index.row()][index.column()] = value
61        self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
62       
63    def data(self, index, role):
64        row, col = index.row(), index.column()
65        return self._roleData[role][row][col]
66       
67    def index(self, row, col, parent=QModelIndex()):
68        return self.createIndex(row, col, 0)
69   
70    def parent(self, index):
71        return QModelIndex()
72   
73    def rowCount(self, index=QModelIndex()):
74        if index.isValid():
75            return 0
76        else:
77            return len(self._data)
78       
79    def columnCount(self, index):
80        return len(self._header[Qt.Horizontal])
81
82    def headerData(self, section, orientation, role):
83        try:
84            return QVariant(self._header[orientation][section][role])
85        except KeyError, er:
86#            print >> sys.stderr, er
87            return QVariant()
88       
89    def setHeaderData(self, section, orientation, value, role=Qt.EditRole):
90        self._header[orientation][section][role] = value
91       
92from Orange.utils import lru_cache
93
94class MySortFilterProxyModel(QSortFilterProxyModel):   
95    def __init__(self, parent=None):
96        QSortFilterProxyModel.__init__(self, parent)
97        self._filter_strings = []
98        self._cache = {}
99        self._cache_fixed = {}
100        self._cache_prefix = {}
101        self._row_text = {}
102       
103        # Create a cached version of _filteredRows
104        self._filteredRows = lru_cache(100)(self._filteredRows) 
105
106    def setSourceModel(self, model):
107        """ Set the source model for the filter
108        """ 
109        self._filter_strings = []
110        self._cache = {}
111        self._cache_fixed = {}
112        self._cache_prefix = {}
113        self._row_text = {}
114        QSortFilterProxyModel.setSourceModel(self, model)
115       
116    def addFilterFixedString(self, string, invalidate=True):
117        """ Add `string` filter to the list of filters. If invalidate is
118        True the filter cache will be recomputed.
119        """
120        self._filter_strings.append(string)
121        all_rows = range(self.sourceModel().rowCount())
122        row_text = [self.rowFilterText(row) for row in all_rows]
123        self._cache[string] = [string in text for text in row_text]
124        if invalidate:
125            self.updateCached()
126            self.invalidateFilter()
127       
128    def removeFilterFixedString(self, index=-1, invalidate=True):
129        """ Remove the `index`-th filter string. If invalidate is True the
130        filter cache will be recomputed.
131        """
132        string = self._filter_strings.pop(index) 
133        del self._cache[string] 
134        if invalidate:
135            self.updateCached()
136            self.invalidateFilter()
137           
138    def setFilterFixedStrings(self, strings):
139        """ Set a list of string to be the new filters.
140        """
141        s_time = time.time()
142        to_remove = set(self._filter_strings) - set(strings)
143        to_add = set(strings) - set(self._filter_strings)
144        for str in to_remove:
145            self.removeFilterFixedString(self._filter_strings.index(str), invalidate=False)
146       
147        for str in to_add:
148            self.addFilterFixedString(str, invalidate=False)
149        self.updateCached()
150        self.invalidateFilter()
151           
152    def _filteredRows(self, filter_strings):
153        """ Return a dictionary mapping row indexes to True False values.
154        .. note:: This helper function is wrapped in the __init__ method.
155        """
156        all_rows = range(self.sourceModel().rowCount())
157        cache = self._cache
158        return dict([(row, all([cache[str][row] for str in filter_strings])) for row in all_rows])
159   
160    def updateCached(self):
161        """ Update the combined filter cache.
162        """
163        self._cache_fixed = self._filteredRows(tuple(sorted(self._filter_strings))) 
164       
165    def setFilterFixedString(self, string):
166        """Should this raise an error? It is not being used.
167        """
168        QSortFilterProxyModel.setFilterFixedString(self, string)
169       
170    def rowFilterText(self, row):
171        """ Return text for `row` to filter on.
172        """
173        f_role = self.filterRole()
174        f_column = self.filterKeyColumn()
175        s_model = self.sourceModel()
176        data = s_model.data(s_model.index(row, f_column), f_role)
177        if isinstance(data, QVariant):
178            data = unicode(data.toString(), errors="ignore")
179        else:
180            data = unicode(data, errors="ignore")
181        return data
182       
183    def filterAcceptsRow(self, row, parent): 
184        return self._cache_fixed.get(row, True)
185   
186    def lessThan(self, left, right):
187        if left.column() == 1 and right.column(): # TODO: Remove fixed column handling
188            left_gds = str(left.data(Qt.DisplayRole).toString())
189            right_gds = str(right.data(Qt.DisplayRole).toString())
190            left_gds = left_gds.lstrip("GDS")
191            right_gds = right_gds.lstrip("GDS")
192            try:
193                return int(left_gds) < int(right_gds)
194            except Exception, ex:
195                pass
196        return QSortFilterProxyModel.lessThan(self, left, right)
197   
198from Orange.OrangeWidgets.OWGUI import LinkStyledItemDelegate, LinkRole
199
200def childiter(item):
201    """ Iterate over the children of an QTreeWidgetItem instance.
202    """
203    for i in range(item.childCount()):
204        yield item.child(i)
205               
206class OWGEODatasets(OWWidget):
207    settingsList = ["outputRows", "mergeSpots", "gdsSelectionStates", "splitterSettings", "currentGds", "autoCommit"]
208
209    def __init__(self, parent=None ,signalManager=None, name=" GEO Data Sets"):
210        OWWidget.__init__(self, parent ,signalManager, name)
211
212        self.outputs = [("Expression Data", ExampleTable)]
213
214        ## Settings
215#        self.selectedSubsets = []
216#        self.sampleSubsets = []
217        self.selectedAnnotation = 0
218        self.includeIf = False
219        self.minSamples = 3
220        self.autoCommit = False
221        self.outputRows = 0
222        self.mergeSpots = True
223        self.filterString = ""
224        self.currentGds = None
225        self.selectionChanged = False
226        self.autoCommit = False
227        self.gdsSelectionStates = {}
228        self.splitterSettings = ['\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\xea\x00\x00\x00\xd7\x01\x00\x00\x00\x07\x01\x00\x00\x00\x02',
229                                 '\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\xb5\x00\x00\x02\x10\x01\x00\x00\x00\x07\x01\x00\x00\x00\x01']
230
231        self.loadSettings()
232
233        ## GUI
234        self.infoBox = OWGUI.widgetLabel(OWGUI.widgetBox(self.controlArea, "Info", addSpace=True), "\n\n")
235
236        box = OWGUI.widgetBox(self.controlArea, "Output", addSpace=True)
237        OWGUI.radioButtonsInBox(box, self, "outputRows", ["Genes or spots", "Samples"], "Rows", callback=self.commitIf)
238        OWGUI.checkBox(box, self, "mergeSpots", "Merge spots of same gene", callback=self.commitIf)
239
240        box = OWGUI.widgetBox(self.controlArea, "Output", addSpace=True)
241        self.commitButton = OWGUI.button(box, self, "Commit", callback=self.commit)
242        cb = OWGUI.checkBox(box, self, "autoCommit", "Commit on any change")
243        OWGUI.setStopper(self, self.commitButton, cb, "selectionChanged", self.commit)
244        OWGUI.rubber(self.controlArea)
245
246        self.filterLineEdit = OWGUIEx.lineEditHint(self.mainArea, self, "filterString", "Filter",
247                                   caseSensitive=False, matchAnywhere=True, 
248                                   #listUpdateCallback=self.filter, callbackOnType=False,
249                                   callback=self.filter,  delimiters=" ")
250       
251        splitter = QSplitter(Qt.Vertical, self.mainArea)
252        self.mainArea.layout().addWidget(splitter)
253        self.treeWidget = QTreeView(splitter)
254       
255        self.treeWidget.setSelectionMode(QAbstractItemView.SingleSelection)
256        self.treeWidget.setRootIsDecorated(False)
257        self.treeWidget.setSortingEnabled(True)
258        self.treeWidget.setAlternatingRowColors(True)
259        self.treeWidget.setUniformRowHeights(True)
260        self.treeWidget.setItemDelegate(LinkStyledItemDelegate(self.treeWidget))
261        self.treeWidget.setItemDelegateForColumn(0, OWGUI.IndicatorItemDelegate(self.treeWidget, role=Qt.DisplayRole))
262       
263        self.connect(self.treeWidget, SIGNAL("itemSelectionChanged ()"), self.updateSelection)
264        self.treeWidget.viewport().setMouseTracking(True)
265       
266        splitterH = QSplitter(Qt.Horizontal, splitter) 
267       
268        box = OWGUI.widgetBox(splitterH, "Description")
269        self.infoGDS = OWGUI.widgetLabel(box, "")
270        self.infoGDS.setWordWrap(True)
271        OWGUI.rubber(box)
272       
273        box = OWGUI.widgetBox(splitterH, "Sample Annotations")
274        self.annotationsTree = QTreeWidget(box)
275        self.annotationsTree.setHeaderLabels(["Type (Sample annotations)", "Sample count"])
276        self.annotationsTree.setRootIsDecorated(True)
277        box.layout().addWidget(self.annotationsTree)
278        self.connect(self.annotationsTree, SIGNAL("itemChanged(QTreeWidgetItem * , int)"), self.annotationSelectionChanged)
279        self._annotationsUpdating = False
280        self.splitters = splitter, splitterH
281        self.connect(splitter, SIGNAL("splitterMoved(int, int)"), self.splitterMoved)
282        self.connect(splitterH, SIGNAL("splitterMoved(int, int)"), self.splitterMoved)
283       
284        for sp, setting in zip(self.splitters, self.splitterSettings):
285            sp.restoreState(setting)
286           
287        self.searchKeys = ["dataset_id", "title", "platform_organism", "description"]
288        self.cells = []
289       
290        QTimer.singleShot(50, self.updateTable)
291        self.resize(1000, 600)
292
293    def updateInfo(self):
294        gds_info = self.gds_info #obiGEO.GDSInfo()
295        text = "%i datasets\n%i datasets cached\n" % (len(gds_info), len(glob.glob(orngServerFiles.localpath("GEO") + "/GDS*")))
296        filtered = self.treeWidget.model().rowCount()
297        if len(self.cells) != filtered:
298            text += ("%i after filtering") % filtered
299        self.infoBox.setText(text)
300       
301    def updateTable(self):
302        self.treeItems = []
303        self.progressBarInit()
304        with orngServerFiles.DownloadProgress.setredirect(self.progressBarSet):
305            self.gds_info = info = obiGEO.GDSInfo()
306
307        self.cells = cells = []
308        gdsLinks = []
309        pmLinks = []
310        localGDS = []
311        full_text_search_data = []
312        self.progressBarSet(10)
313        self.gds = []
314        for i, (name, gds) in enumerate(info.items()):
315            local = os.path.exists(orngServerFiles.localpath(obiGEO.DOMAIN, gds["dataset_id"] + ".soft.gz"))
316            cells.append([" " if local else "", gds["dataset_id"], gds["title"], gds["platform_organism"], len(gds["samples"]), gds["feature_count"],
317                          gds["gene_count"], len(gds["subsets"]), gds.get("pubmed_id", "")])
318
319            gdsLinks.append("http://www.ncbi.nlm.nih.gov/sites/GDSbrowser?acc=%s" % gds["dataset_id"])
320            pmLinks.append("http://www.ncbi.nlm.nih.gov/pubmed/%s" % gds.get("pubmed_id") if gds.get("pubmed_id") else QVariant())
321
322            if local:
323                localGDS.append(i)
324            self.gds.append(gds)
325
326            full_text_search_data.append(unicode(" | ".join([gds.get(key, "").lower() for key in self.searchKeys]), errors="ignore"))
327
328        self.progressBarSet(20)
329        model = TreeModel(cells, ["", "ID", "Title", "Organism", "Samples", "Features", "Genes", "Subsets", "PubMedID"], self.treeWidget)
330        model.setColumnLinks(1, gdsLinks)
331        model.setColumnLinks(8, pmLinks)
332
333        for i, text in enumerate(full_text_search_data):
334            model.setData(model.index(i, 0), QVariant(text), TextFilterRole)
335
336        proxyModel = MySortFilterProxyModel(self.treeWidget)
337        proxyModel.setSourceModel(model)
338        proxyModel.setFilterKeyColumn(0)
339        proxyModel.setFilterRole(TextFilterRole)
340        proxyModel.setFilterCaseSensitivity(False)
341        proxyModel.setFilterFixedString(self.filterString)
342
343        self.treeWidget.setModel(proxyModel)
344        self.connect(self.treeWidget.selectionModel(), SIGNAL("selectionChanged(QItemSelection , QItemSelection )"), self.updateSelection)
345        filterItems = " ".join([self.gds[i][key] for i in range(len(self.gds)) for key in self.searchKeys])
346        filterItems = reduce(lambda s, d: s.replace(d, " "),
347                             [",", ".", ":", ";", "!", "?", "(", ")", "{", "}"
348                              "[", "]", "_", "-", "+", "\\", "|", "/", "%", "#",
349                              "@", "$", "^", "&", "*", "<", ">", "~", "`"],
350                             filterItems.lower())
351        filterItems = sorted(set(filterItems.split(" ")))
352        filterItems = [item for item in filterItems if len(filterItems) > 3]
353        self.filterLineEdit.setItems(filterItems)
354
355        self.progressBarSet(40)
356
357        for i in range(8):
358            self.treeWidget.resizeColumnToContents(i)
359
360        self.progressBarSet(70)
361
362        self.treeWidget.setColumnWidth(1, min(self.treeWidget.columnWidth(1), 300))
363        self.treeWidget.setColumnWidth(2, min(self.treeWidget.columnWidth(2), 200))
364
365        self.progressBarFinished()
366
367        if self.currentGds:
368            gdss = [(i, model.data(model.index(i,1), Qt.DisplayRole)) for i in range(model.rowCount())]
369            current = [i for i, variant in gdss if variant.isValid() and str(variant.toString()) == self.currentGds["dataset_id"]]
370            if current:
371                mapFromSource = self.treeWidget.model().mapFromSource
372                self.treeWidget.selectionModel().select(mapFromSource(model.index(current[0], 0)), QItemSelectionModel.Select | QItemSelectionModel.Rows)
373
374        self.updateInfo()
375
376    def updateSelection(self, *args):
377        current = self.treeWidget.selectedIndexes()
378        mapToSource = self.treeWidget.model().mapToSource
379        current = [mapToSource(index).row() for index in current]
380        if current:
381            self.currentGds = self.gds[current[0]]
382            self.setAnnotations(self.currentGds)
383            self.infoGDS.setText(self.currentGds.get("description", ""))
384        else:
385            self.currentGds = None
386#        self.commitButton.setDisabled(not bool(self.currentGds))
387        self.commitIf()
388       
389   
390    def setAnnotations(self, gds):
391        self._annotationsUpdating = True
392        self.annotationsTree.clear()
393        annotations = reduce(lambda d, info: d[info["type"]].add(info["description"]) or d, gds["subsets"], defaultdict(set))
394        subsetscount = dict([(s["description"], str(len(s["sample_id"]))) for s in gds["subsets"]])
395        for type, subsets in annotations.items():
396            key = (gds["dataset_id"], type)
397            subsetItem = QTreeWidgetItem(self.annotationsTree, [type])
398            subsetItem.setFlags(subsetItem.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsTristate)
399            subsetItem.setCheckState(0, self.gdsSelectionStates.get(key, Qt.Checked))
400            subsetItem.key = key
401            for subset in subsets:
402                key = (gds["dataset_id"], type, subset)
403                item = QTreeWidgetItem(subsetItem, [subset, subsetscount.get(subset, "")])
404                item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
405                item.setCheckState(0, self.gdsSelectionStates.get(key, Qt.Checked))
406                item.key = key
407        self._annotationsUpdating = False
408        self.annotationsTree.expandAll()
409        for i in range(self.annotationsTree.columnCount()):
410            self.annotationsTree.resizeColumnToContents(i)
411               
412    def annotationSelectionChanged(self, item, column):
413        if self._annotationsUpdating:
414            return 
415        for i in range(self.annotationsTree.topLevelItemCount()):
416            item = self.annotationsTree.topLevelItem(i)
417            self.gdsSelectionStates[item.key] = item.checkState(0)
418            for j in range(item.childCount()):
419                child = item.child(j)
420                self.gdsSelectionStates[child.key] = child.checkState(0)
421       
422    def rowFiltered(self, row):
423        filterStrings = self.filterString.lower().split()
424        try:
425            string = " ".join([self.gds[row].get(key, "").lower() for key in self.searchKeys])
426            return not all([s in string for s in filterStrings])
427        except UnicodeDecodeError:
428            string = " ".join([unicode(self.gds[row].get(key, "").lower(), errors="ignore") for key in self.searchKeys])
429            return not all([s in string for s in filterStrings])
430   
431    def filter(self):
432        filter_string = unicode(self.filterLineEdit.text(), errors="ignore")
433        proxyModel = self.treeWidget.model()
434        if proxyModel:
435            strings = filter_string.lower().strip().split()
436            proxyModel.setFilterFixedStrings(strings)
437            self.updateInfo()
438
439    def selectedSamples(self):
440        """ Return the currently selected sample annotations (list of
441        sample type, sample value tuples).
442       
443        .. note:: if some Sample annotation type has no selected values.
444                  this method will return all values for it.
445       
446        """
447        samples = []
448        unused_types = []
449        used_types = []
450        for stype in childiter(self.annotationsTree.invisibleRootItem()): 
451            selected_values = []
452            all_values = []
453            for sval in childiter(stype):
454                value = (str(stype.text(0)), str(sval.text(0)))
455                if self.gdsSelectionStates.get(sval.key, True):
456                    selected_values.append(value)
457                all_values.append(value)
458            if selected_values:
459                samples.extend(selected_values)
460                used_types.append(str(stype.text(0)))
461            else:
462                # If no sample of sample type is selected we don't filter on it.
463                samples.extend(all_values)
464                unused_types.append(str(stype.text(0)))
465       
466        return samples, used_types
467   
468    def commitIf(self):
469        if self.autoCommit:
470            self.commit()
471        else:
472            self.selectionChanged = True
473   
474    def commit(self):
475        if self.currentGds:
476            self.error(0) 
477            sample_type = None
478            self.progressBarInit()
479            self.progressBarSet(10)
480           
481            def getdata(gds_id, **kwargs):
482                gds = obiGEO.GDS(gds_id)
483                data = gds.getdata(**kwargs)
484                return data
485           
486            _, groups = self.selectedSamples()
487            if len(groups) == 1 and self.outputRows:
488                sample_type = groups[0]
489
490            self.setEnabled(False)
491            call = self.asyncCall(getdata, (self.currentGds["dataset_id"],), dict(report_genes=self.mergeSpots,
492                                           transpose=self.outputRows,
493                                           sample_type=sample_type),
494                                  onResult=self.onData,
495                                  onFinished=lambda: self.setEnabled(True),
496                                  onError=self.onAsyncError,
497                                  threadPool=QThreadPool.globalInstance()
498                                 )
499            call.__call__() #invoke
500
501    def onAsyncError(self, (exctype, value, tb)):
502        import ftplib
503        if issubclass(exctype, ftplib.error_temp):
504            self.error(0, "Can not download dataset from NCBI ftp server! Try again later.")
505        elif issubclass(exctype, ftplib.all_errors):
506            self.error(0, "Error while connecting to the NCBI ftp server! %s" % str(value))
507        else:
508            sys.excepthook(exctype, value, tb)
509           
510        self.progressBarFinished()
511
512    def onData(self, data):
513        self.progressBarSet(50)
514       
515        samples,_ = self.selectedSamples()
516       
517        self.warning(0)
518        message = None
519        if self.outputRows:
520            def samplesinst(ex):
521                out = []
522                for i,a in data.domain.get_metas().items():
523                    out.append((a.name, ex[i].value))
524                if data.domain.class_var.name != 'class':
525                    out.append((data.domain.class_var.name, ex[-1].value))
526                return out
527            samples = set(samples)
528
529            select = [1 if samples.issuperset(samplesinst(ex)) else 0 for ex in data]
530            data = data.select(select)
531            if len(data) == 0:
532                message = "No samples with selected sample annotations."
533        else:
534            samples = set(samples)
535            domain = orange.Domain([attr for attr in data.domain.attributes if samples.issuperset(attr.attributes.items())], data.domain.classVar)
536            domain.addmetas(data.domain.getmetas())
537            if len(domain.attributes) == 0:
538                message = "No samples with selected sample annotations."
539            stypes = set(s[0] for s in samples)
540            for attr in domain.attributes:
541                attr.attributes = dict([(key, value) for key, value in attr.attributes.items() if key in stypes])
542            data = orange.ExampleTable(domain, data)
543       
544        if message is not None:
545            self.warning(0, message)
546           
547        data_hints.set_hint(data, "taxid", self.currentGds.get("taxid", ""), 10.0)
548        data_hints.set_hint(data, "genesinrows", self.outputRows, 10.0)
549       
550        self.progressBarFinished()
551        self.send("Expression Data", data)
552
553        model = self.treeWidget.model().sourceModel()
554        row = self.gds.index(self.currentGds)
555#            model._roleData[Qt.ForegroundRole][row].update(zip(range(1, 7), [QVariant(QColor(LOCAL_GDS_COLOR))] * 6))
556        model.setData(model.index(row, 0),  QVariant(" "), Qt.DisplayRole) 
557#            model.emit(SIGNAL("dataChanged(const QModelIndex &, const QModelIndex &)"), model.index(row, 0), model.index(row, 0))
558        self.updateInfo()
559        self.selectionChanged = False
560       
561    def splitterMoved(self, *args):
562        self.splitterSettings = [str(sp.saveState()) for sp in self.splitters]
563
564if __name__ == "__main__":
565    app = QApplication(sys.argv)
566    w = OWGEODatasets()
567    w.show()
568    app.exec_()
569    w.saveSettings()
Note: See TracBrowser for help on using the repository browser.