source: orange-bioinformatics/orangecontrib/bio/widgets/OWGOEnrichmentAnalysis.py @ 1999:c60995640805

Revision 1999:c60995640805, 49.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 weeks ago (diff)

Threaded widget initialization.

Line 
1"""
2<name>GO Browser</name>
3<description>Enrichment analysis for Gene Ontology terms.</description>
4<contact>Ales Erjavec</contact>
5<icon>icons/GOBrowser.svg</icon>
6<priority>2020</priority>
7"""
8
9from __future__ import absolute_import, with_statement
10
11from collections import defaultdict
12from functools import partial
13import gc
14import sys, os, tarfile, math
15from os.path import join as p_join
16
17from Orange.orng import orngServerFiles
18from Orange.orng.orngDataCaching import data_hints
19from Orange.OrangeWidgets import OWGUI
20from Orange.OrangeWidgets.OWWidget import *
21from Orange.OrangeWidgets.OWConcurrent import ThreadExecutor
22
23from .. import obiGene, obiGO, obiProb, obiTaxonomy
24from .utils.download import EnsureDownloaded
25
26NAME = "GO Browser"
27DESCRIPTION = "Enrichment analysis for Gene Ontology terms."
28ICON = "icons/GOBrowser.svg"
29PRIORITY = 2020
30
31INPUTS = [("Cluster Examples", Orange.data.Table,
32           "SetClusterDataset", Single + Default),
33          ("Reference Examples", Orange.data.Table,
34           "SetReferenceDataset")]
35
36OUTPUTS = [("Selected Examples", Orange.data.Table),
37           ("Unselected Examples", Orange.data.Table),
38           ("Example With Unknown Genes", Orange.data.Table),
39           ("Enrichment Report", Orange.data.Table)]
40
41REPLACES = ["_bioinformatics.widgets.OWGOEnrichmentAnalysis.OWGOEnrichmentAnalysis"]
42
43
44dataDir = orngServerFiles.localpath("GO")
45
46def listAvailable():
47    files = orngServerFiles.listfiles("GO")
48    ret = {}
49    for file in files:
50        tags = orngServerFiles.info("GO", file)["tags"]
51        td = dict([tuple(tag.split(":")) for tag in tags if tag.startswith("#") and ":" in tag])
52        if "association" in file.lower():
53            ret[td.get("#organism", file)] = file
54    orgMap = {"352472":"44689"}
55    essential = ["gene_association.%s.tar.gz" % obiGO.from_taxid(id) for id in obiTaxonomy.essential_taxids() if obiGO.from_taxid(id)]
56    essentialNames = [obiTaxonomy.name(id) for id in obiTaxonomy.essential_taxids() if obiGO.from_taxid(id)]
57    ret.update(zip(essentialNames, essential))
58    return ret
59
60class _disablegc(object):
61    def __enter__(self):
62        gc.disable()
63    def __exit__(self, *args):
64        gc.enable()
65
66def getOrgFileName(org):
67    from Orange.orng import orngServerFiles
68    files = orngServerFiles.listfiles("go")
69    return [f for f in files if org in f].pop()
70
71class TreeNode(object):
72    def __init__(self, tuple, children):
73        self.tuple = tuple
74        self.children = children
75
76class GOTreeWidget(QTreeWidget):
77    def contextMenuEvent(self, event):
78        QTreeWidget.contextMenuEvent(self, event)
79##        print event.x(), event.y()
80        term = self.itemAt(event.pos()).term
81        self._currMenu = QMenu()
82        self._currAction = self._currMenu.addAction("View term on AmiGO website")
83##        self.connect(self, SIGNAL("triggered(QAction*)"), partial(self.BrowserAction, term))
84        self.connect(self._currAction, SIGNAL("triggered()"), lambda :self.BrowserAction(term))
85        self._currMenu.popup(event.globalPos())
86
87    def BrowserAction(self, term):
88        import webbrowser
89        if isinstance(term, obiGO.Term):
90            term = term.id
91        webbrowser.open("http://amigo.geneontology.org/cgi-bin/amigo/term-details.cgi?term="+term)
92       
93    def paintEvent(self, event):
94        QTreeWidget.paintEvent(self, event)
95        if getattr(self, "_userMessage", None):
96            painter = QPainter(self.viewport())
97            font = QFont(self.font())
98            font.setPointSize(15)
99            painter.setFont(font)
100            painter.drawText(self.viewport().geometry(), Qt.AlignCenter, self._userMessage)
101            painter.end()
102
103
104class OWGOEnrichmentAnalysis(OWWidget):
105    settingsList = ["annotationIndex", "useReferenceDataset", "aspectIndex",
106                    "geneAttrIndex", "geneMatcherSettings",
107                    "filterByNumOfInstances", "minNumOfInstances",
108                    "filterByPValue", "maxPValue", "selectionDirectAnnotation",
109                    "filterByPValue_nofdr", "maxPValue_nofdr",
110                    "selectionDisjoint", "selectionType",
111                    "selectionAddTermAsClass", "useAttrNames", "probFunc"
112                    ]
113
114    contextHandlers = {"": DomainContextHandler(
115                                "",
116                                ["geneAttrIndex", "useAttrNames",
117                                 "annotationIndex", "geneMatcherSettings"],
118                                matchValues=1)
119                       }
120
121    def __init__(self, parent=None, signalManager=None, name="GO Browser"):
122        OWWidget.__init__(self, parent, signalManager, name)
123        self.inputs = [("Cluster Examples", ExampleTable,
124                        self.SetClusterDataset, Default),
125                       ("Reference Examples", ExampleTable,
126                        self.SetReferenceDataset, Single + NonDefault)]
127
128        self.outputs = [("Selected Examples", ExampleTable, Default),
129                        ("Unselected Examples", ExampleTable, Default),
130                        ("Example With Unknown Genes", ExampleTable, Default),
131                        ("Enrichment Report", ExampleTable)]
132
133        self.annotationIndex = 0
134        self.autoFindBestOrg = False
135        self.useReferenceDataset = 0
136        self.aspectIndex = 0
137        self.geneAttrIndex = 0
138        self.useAttrNames = False
139        self.geneMatcherSettings = [True, False, False, False]
140        self.filterByNumOfInstances = False
141        self.minNumOfInstances = 1
142        self.filterByPValue = True
143        self.maxPValue = 0.1
144        self.filterByPValue_nofdr = False
145        self.maxPValue_nofdr = 0.1
146        self.probFunc = 0
147        self.selectionDirectAnnotation = 0
148        self.selectionDisjoint = 0
149        self.selectionAddTermAsClass = 0
150        self.selectionChanging = 0
151       
152        # check usage of all evidences
153        for etype in obiGO.evidenceTypesOrdered:
154            varName = "useEvidence" + etype
155            if varName not in self.settingsList: 
156                self.settingsList.append(varName)
157            code = compile("self.%s = True" % (varName), ".", "single")
158            exec(code)
159        self.annotationCodes = []
160       
161        self.loadSettings()
162       
163        #############
164        ##GUI
165        #############
166        self.tabs = OWGUI.tabWidget(self.controlArea)
167        ##Input tab
168        self.inputTab = OWGUI.createTabPage(self.tabs, "Input")
169        box = OWGUI.widgetBox(self.inputTab, "Info")
170        self.infoLabel = OWGUI.widgetLabel(box, "No data on input\n")
171       
172        OWGUI.button(box, self, "Ontology/Annotation Info", callback=self.ShowInfo,
173                     tooltip="Show information on loaded ontology and annotations",
174                     debuggingEnabled=0)
175        box = OWGUI.widgetBox(self.inputTab, "Organism", addSpace=True)
176        self.annotationComboBox = OWGUI.comboBox(box, self, "annotationIndex",
177                            items = self.annotationCodes, callback=self.Update,
178                            tooltip="Select organism", debuggingEnabled=0)
179
180        self.geneAttrIndexCombo = OWGUI.comboBox(self.inputTab, self, "geneAttrIndex",
181                            box="Gene names", callback=self.Update,
182                            tooltip="Use this attribute to extract gene names from input data")
183        OWGUI.checkBox(self.geneAttrIndexCombo.box, self, "useAttrNames", "Use data attributes names",
184                       disables=[(-1, self.geneAttrIndexCombo)], callback=self.Update, 
185                       tooltip="Use attribute names for gene names")
186        OWGUI.button(self.geneAttrIndexCombo.box, self, "Gene matcher settings", 
187                     callback=self.UpdateGeneMatcher, 
188                     tooltip="Open gene matching settings dialog", 
189                     debuggingEnabled=0)
190       
191        self.referenceRadioBox = OWGUI.radioButtonsInBox(self.inputTab, self, "useReferenceDataset", 
192                                                         ["Entire genome", "Reference set (input)"],
193                                                         tooltips=["Use entire genome for reference",
194                                                                   "Use genes from Referece Examples input signal as reference"],
195                                                         box="Reference", callback=self.Update)
196        self.referenceRadioBox.buttons[1].setDisabled(True)
197        OWGUI.radioButtonsInBox(self.inputTab, self, "aspectIndex", ["Biological process",
198                                                                     "Cellular component",
199                                                                     "Molecular function"], 
200                                box="Aspect", callback=self.Update)
201       
202        self.geneAttrIndexCombo.setDisabled(bool(self.useAttrNames))
203       
204        ##Filter tab
205        self.filterTab = OWGUI.createTabPage(self.tabs, "Filter")
206        box = OWGUI.widgetBox(self.filterTab, "Filter GO Term Nodes", addSpace=True)
207        OWGUI.checkBox(box, self, "filterByNumOfInstances", "Genes",
208                       callback=self.FilterAndDisplayGraph, 
209                       tooltip="Filter by number of input genes mapped to a term")
210        OWGUI.spin(OWGUI.indentedBox(box), self, 'minNumOfInstances', 1, 100, 
211                   step=1, label='#:', labelWidth=15, 
212                   callback=self.FilterAndDisplayGraph, 
213                   callbackOnReturn=True, 
214                   tooltip="Min. number of input genes mapped to a term")
215       
216        OWGUI.checkBox(box, self, "filterByPValue_nofdr", "p-value",
217                       callback=self.FilterAndDisplayGraph, 
218                       tooltip="Filter by term p-value")
219        OWGUI.doubleSpin(OWGUI.indentedBox(box), self, 'maxPValue_nofdr', 1e-8, 1, 
220                         step=1e-8,  label='p:', labelWidth=15, 
221                         callback=self.FilterAndDisplayGraph, 
222                         callbackOnReturn=True, 
223                         tooltip="Max term p-value")
224
225        #use filterByPValue for FDR, as it was the default in prior versions
226        OWGUI.checkBox(box, self, "filterByPValue", "FDR",
227                       callback=self.FilterAndDisplayGraph, 
228                       tooltip="Filter by term FDR")
229        OWGUI.doubleSpin(OWGUI.indentedBox(box), self, 'maxPValue', 1e-8, 1, 
230                         step=1e-8,  label='p:', labelWidth=15, 
231                         callback=self.FilterAndDisplayGraph, 
232                         callbackOnReturn=True, 
233                         tooltip="Max term p-value")
234
235        box = OWGUI.widgetBox(box, "Significance test")
236
237        OWGUI.radioButtonsInBox(box, self, "probFunc", ["Binomial", "Hypergeometric"], 
238                                tooltips=["Use binomial distribution test", 
239                                          "Use hypergeometric distribution test"], 
240                                callback=self.Update)
241        box = OWGUI.widgetBox(self.filterTab, "Evidence codes in annotation", 
242                              addSpace=True)
243        self.evidenceCheckBoxDict = {}
244        for etype in obiGO.evidenceTypesOrdered:
245            self.evidenceCheckBoxDict[etype] = OWGUI.checkBox(box, self, "useEvidence"+etype, etype,
246                                            callback=self.Update, tooltip=obiGO.evidenceTypes[etype])
247       
248        ##Select tab
249        self.selectTab = OWGUI.createTabPage(self.tabs, "Select")
250        box = OWGUI.radioButtonsInBox(self.selectTab, self, "selectionDirectAnnotation", 
251                                      ["Directly or Indirectly", "Directly"], 
252                                      box="Annotated genes", 
253                                      callback=self.ExampleSelection)
254       
255        box = OWGUI.widgetBox(self.selectTab, "Output", addSpace=True)
256        OWGUI.radioButtonsInBox(box, self, "selectionDisjoint", 
257                                btnLabels=["All selected genes", 
258                                           "Term-specific genes", 
259                                           "Common term genes"], 
260                                tooltips=["Outputs genes annotated to all selected GO terms", 
261                                          "Outputs genes that appear in only one of selected GO terms", 
262                                          "Outputs genes common to all selected GO terms"], 
263                                callback=[self.ExampleSelection,
264                                          self.UpdateAddClassButton])
265        self.addClassCB = OWGUI.checkBox(box, self, "selectionAddTermAsClass",
266                                         "Add GO Term as class", 
267                                         callback=self.ExampleSelection)
268
269        # ListView for DAG, and table for significant GOIDs
270        self.DAGcolumns = ['GO term', 'Cluster', 'Reference', 'p-value', 'FDR', 'Genes', 'Enrichment']
271       
272        self.splitter = QSplitter(Qt.Vertical, self.mainArea)
273        self.mainArea.layout().addWidget(self.splitter)
274
275        # list view
276        self.listView = GOTreeWidget(self.splitter)
277        self.listView.setSelectionMode(QAbstractItemView.ExtendedSelection)
278        self.listView.setAllColumnsShowFocus(1)
279        self.listView.setColumnCount(len(self.DAGcolumns))
280        self.listView.setHeaderLabels(self.DAGcolumns)
281       
282        self.listView.header().setClickable(True)
283        self.listView.header().setSortIndicatorShown(True)
284        self.listView.setSortingEnabled(True)
285        self.listView.setItemDelegateForColumn(6, EnrichmentColumnItemDelegate(self))
286        self.listView.setRootIsDecorated(True)
287
288       
289        self.connect(self.listView, SIGNAL("itemSelectionChanged()"), self.ViewSelectionChanged)
290       
291        # table of significant GO terms
292        self.sigTerms = QTreeWidget(self.splitter)
293        self.sigTerms.setColumnCount(len(self.DAGcolumns))
294        self.sigTerms.setHeaderLabels(self.DAGcolumns)
295        self.sigTerms.setSortingEnabled(True)
296        self.sigTerms.setSelectionMode(QAbstractItemView.ExtendedSelection)
297        self.sigTerms.setItemDelegateForColumn(6, EnrichmentColumnItemDelegate(self))
298       
299        self.connect(self.sigTerms, SIGNAL("itemSelectionChanged()"), self.TableSelectionChanged)
300        self.splitter.show()
301
302        self.sigTableTermsSorted = []
303        self.graph = {}
304       
305        self.loadedAnnotationCode = "---"
306       
307        self.inputTab.layout().addStretch(1)
308        self.filterTab.layout().addStretch(1)
309        self.selectTab.layout().addStretch(1)
310       
311        self.resize(1000, 800)
312
313        self.clusterDataset = None
314        self.referenceDataset = None
315        self.ontology = None
316        self.annotations = None
317        self.treeStructRootKey = None
318        self.probFunctions = [obiProb.Binomial(), obiProb.Hypergeometric()]
319        self.selectedTerms = []
320
321        self.connect(self, SIGNAL("widgetStateChanged(QString, int, QString)"), self.onStateChanged)
322
323        self.setBlocking(True)
324        self._executor = ThreadExecutor()
325        self._init = EnsureDownloaded(
326            [("Taxonomy", "ncbi_taxonomy.tar.gz"),
327             ("GO", "taxonomy.pickle")]
328        )
329        self._init.finished.connect(self.UpdateOrganismComboBox)
330        self._executor.submit(self._init)
331
332    def UpdateOrganismComboBox(self):
333        try:
334            self.annotationFiles = listAvailable()
335            self.annotationCodes = sorted(self.annotationFiles.keys())
336            self.annotationComboBox.clear()
337            self.annotationComboBox.addItems(self.annotationCodes)
338            self.annotationComboBox.setCurrentIndex(self.annotationIndex)
339        finally:
340            self.setBlocking(False)
341
342    def UpdateGeneMatcher(self):
343        dialog = GeneMatcherDialog(self, defaults=self.geneMatcherSettings, modal=True)
344        if dialog.exec_():
345            self.geneMatcherSettings = [getattr(dialog, item[0]) for item in dialog.items]
346            if self.annotations:
347                self.SetGeneMatcher()
348                if self.clusterDataset:
349                    self.Update()
350               
351    def Update(self):
352        if self.clusterDataset:
353            pb = OWGUI.ProgressBar(self, 100)
354            self.Load(pb=pb)
355            self.FilterUnknownGenes()
356            graph = self.Enrichment(pb=pb)
357            self.SetGraph(graph)
358
359    def UpdateGOAndAnnotation(self, tags=[]):
360        from .OWUpdateGenomicsDatabases import OWUpdateGenomicsDatabases
361        w = OWUpdateGenomicsDatabases(parent = self, searchString=" ".join(tags))
362        w.setModal(True)
363        w.show()
364        self.UpdateAnnotationComboBox()
365
366    def UpdateAnnotationComboBox(self):
367        if self.annotationCodes:
368            curr = self.annotationCodes[min(self.annotationIndex, len(self.annotationCodes)-1)]
369        else:
370            curr = None
371        self.annotationFiles = listAvailable()
372        self.annotationCodes = self.annotationFiles.keys()
373        index = curr and self.annotationCodes.index(curr) or 0
374        self.annotationComboBox.clear()
375        self.annotationComboBox.addItems(self.annotationCodes)
376        self.annotationComboBox.setCurrentIndex(index)
377        if not self.annotationCodes:
378            self.error(0, "No downloaded annotations!!\nClick the update button and update annotationa for at least one organism!")
379        else:
380            self.error(0)
381
382    def SetGenesComboBox(self):
383        self.candidateGeneAttrs = self.clusterDataset.domain.variables + self.clusterDataset.domain.getmetas().values()
384        self.candidateGeneAttrs = filter(lambda v: v.varType in [orange.VarTypes.String,
385                                                                 orange.VarTypes.Other,
386                                                                 orange.VarTypes.Discrete], 
387                                         self.candidateGeneAttrs)
388        self.geneAttrIndexCombo.clear()
389        self.geneAttrIndexCombo.addItems([a.name for a in  self.candidateGeneAttrs])
390
391    def FindBestGeneAttrAndOrganism(self):
392        if self.autoFindBestOrg: 
393            organismGenes = dict([(o,set(go.getCachedGeneNames(o))) for o in self.annotationCodes])
394        else:
395            currCode = self.annotationCodes[min(self.annotationIndex, len(self.annotationCodes)-1)]
396            filename = p_join(dataDir, self.annotationFiles[currCode])
397            try:
398                f = tarfile.open(filename)
399                info = [info for info in f.getmembers() if info.name.startswith("gene_names")].pop()
400                geneNames = cPickle.loads(f.extractfile(info).read().replace("\r\n", "\n"))
401            except Exception, ex:
402                geneNames = cPickle.loads(open(p_join(filename, "gene_names.pickle")).read().replace("\r\n", "\n"))
403            organismGenes = {currCode: set(geneNames)}
404        candidateGeneAttrs = self.clusterDataset.domain.attributes + self.clusterDataset.domain.getmetas().values()
405        candidateGeneAttrs = filter(lambda v: v.varType in [orange.VarTypes.String, 
406                                                            orange.VarTypes.Other, 
407                                                            orange.VarTypes.Discrete], 
408                                    candidateGeneAttrs)
409        attrNames = [v.name for v in self.clusterDataset.domain.variables]
410        cn = {}
411        for attr in candidateGeneAttrs:
412            vals = [str(e[attr]) for e in self.clusterDataset]
413            if any("," in val for val in vals):
414                vals = reduce(list.__add__, (val.split(",") for val in vals))
415            for organism, s in organismGenes.items():
416                l = filter(lambda a: a in s, vals)
417                cn[(attr,organism)] = len(set(l))
418        for organism, s in organismGenes.items():
419            l = filter(lambda a: a in s, attrNames)
420            cn[("_var_names_", organism)] = len(set(l))
421           
422        cn = cn.items()
423        cn.sort(lambda a,b:-cmp(a[1],b[1]))
424        ((bestAttr, organism), count) = cn[0]
425        if bestAttr=="_var_names_" and count<=len(attrNames)/10.0 or \
426           bestAttr!="_var_names_" and count<=len(self.clusterDataset)/10.0:
427            return
428       
429        self.annotationIndex = self.annotationCodes.index(organism)
430        if bestAttr=="_var_names_":
431            self.useAttrNames = True
432            self.geneAttrIndex = 0
433        else:
434            self.useAttrNames = False
435            self.geneAttrIndex = candidateGeneAttrs.index(bestAttr)
436
437    def SetClusterDataset(self, data=None):
438        if not self.annotationCodes:
439            QTimer.singleShot(200, lambda: self.SetClusterDataset(data))
440            return
441        self.closeContext()
442        self.clusterDataset = data
443        self.infoLabel.setText("\n")
444        if data:
445            self.SetGenesComboBox()
446            try:
447                taxid = data_hints.get_hint(data, "taxid", "")
448                code = obiGO.from_taxid(taxid)
449                filename = "gene_association.%s.tar.gz" % code
450                if filename in self.annotationFiles.values():
451                    self.annotationIndex = \
452                            [i for i, name in enumerate(self.annotationCodes) \
453                             if self.annotationFiles[name] == filename].pop()
454            except Exception:
455                pass
456            self.useAttrNames = data_hints.get_hint(data, "genesinrows",
457                                                    self.useAttrNames)
458            self.openContext("", data)
459            self.Update()
460        else:
461            self.infoLabel.setText("No data on input\n")
462            self.warning(0)
463            self.warning(1)
464            self.openContext("", None)
465            self.ClearGraph()
466            self.send("Selected Examples", None)
467            self.send("Unselected Examples", None)
468            self.send("Example With Unknown Genes", None)
469            self.send("Enrichment Report", None)
470
471    def SetReferenceDataset(self, data=None):
472        self.referenceDataset = data
473        self.referenceRadioBox.buttons[1].setDisabled(not bool(data))
474        self.referenceRadioBox.buttons[1].setText("Reference set")
475        if self.clusterDataset and self.useReferenceDataset:
476            self.useReferenceDataset = 0 if not data else 1
477            graph = self.Enrichment()
478            self.SetGraph(graph)
479        elif self.clusterDataset:
480            self.UpdateReferenceSetButton()
481
482    def UpdateReferenceSetButton(self):
483        allgenes, refgenes = None, None
484        if self.referenceDataset:
485            try:
486                allgenes = self.GenesFromExampleTable(self.referenceDataset)
487            except Exception:
488                allgenes = []
489            refgenes, unknown = self.FilterAnnotatedGenes(allgenes)
490        self.referenceRadioBox.buttons[1].setDisabled(not bool(allgenes))
491        self.referenceRadioBox.buttons[1].setText("Reference set " + ("(%i genes, %i matched)" % (len(allgenes), len(refgenes)) if allgenes and refgenes else ""))
492
493    def GenesFromExampleTable(self, data):
494        if self.useAttrNames:
495            genes = [v.name for v in data.domain.variables]
496        else:
497            attr = self.candidateGeneAttrs[min(self.geneAttrIndex, len(self.candidateGeneAttrs) - 1)]
498            genes = [str(ex[attr]) for ex in data if not ex[attr].isSpecial()]
499            if any("," in gene for gene in genes):
500                self.information(0, "Separators detected in gene names. Assuming multiple genes per example.")
501                genes = reduce(list.__add__, (genes.split(",") for genes in genes))
502        return genes
503       
504    def FilterAnnotatedGenes(self, genes):
505        matchedgenes = self.annotations.GetGeneNamesTranslator(genes).values()
506        return matchedgenes, [gene for gene in genes if gene not in matchedgenes]
507       
508    def FilterUnknownGenes(self):
509        if not self.useAttrNames and self.candidateGeneAttrs:
510            geneAttr = self.candidateGeneAttrs[min(self.geneAttrIndex, len(self.candidateGeneAttrs)-1)]
511            examples = []
512            for ex in self.clusterDataset:
513                if not any(self.annotations.genematcher.match(n.strip()) for n in str(ex[geneAttr]).split(",")):
514                    examples.append(ex)
515
516            self.send("Example With Unknown Genes", examples and orange.ExampleTable(examples) or None)
517        else:
518            self.send("Example With Unknown Genes", None)
519
520    def Load(self, pb=None):
521        go_files, tax_files = orngServerFiles.listfiles("GO"), orngServerFiles.listfiles("Taxonomy")
522        calls = []
523        pb, finish = (OWGUI.ProgressBar(self, 0), True) if pb is None else (pb, False)
524        count = 0
525        if not tax_files:
526            calls.append(("Taxonomy", "ncbi_taxnomy.tar.gz"))
527            count += 1
528        org = self.annotationCodes[min(self.annotationIndex, len(self.annotationCodes)-1)]
529        if org != self.loadedAnnotationCode:
530            count += 1
531            if self.annotationFiles[org] not in go_files:
532                calls.append(("GO", self.annotationFiles[org]))
533                count += 1
534               
535        if "gene_ontology_edit.obo.tar.gz" not in go_files:
536            calls.append(("GO", "gene_ontology_edit.obo.tar.gz"))
537            count += 1
538        if not self.ontology:
539            count += 1
540        pb.iter += count*100
541       
542        for i, args in enumerate(calls):
543            orngServerFiles.localpath_download(*args, **dict(callback=pb.advance))
544           
545        i = len(calls)
546        if not self.ontology:
547            self.ontology = obiGO.Ontology(progressCallback=lambda value: pb.advance())
548            i+=1
549        if org != self.loadedAnnotationCode:
550            self.annotations = None
551            gc.collect() # Force run garbage collection.
552            code = self.annotationFiles[org].split(".")[-3]
553            self.annotations = obiGO.Annotations(code, genematcher=obiGene.GMDirect(), progressCallback=lambda value: pb.advance())
554            i+=1
555            self.loadedAnnotationCode = org
556            count = defaultdict(int)
557            geneSets = defaultdict(set)
558
559            for anno in self.annotations.annotations:
560                count[anno.evidence]+=1
561                geneSets[anno.evidence].add(anno.geneName)
562            for etype in obiGO.evidenceTypesOrdered:
563                self.evidenceCheckBoxDict[etype].setEnabled(bool(count[etype]))
564                self.evidenceCheckBoxDict[etype].setText(etype+": %i annots(%i genes)" % (count[etype], len(geneSets[etype])))
565        if finish:
566            pb.finish()
567           
568    def SetGeneMatcher(self):
569        if self.annotations:
570            taxid = self.annotations.taxid
571            matchers = []
572            for matcher, use in zip([obiGene.GMGO, obiGene.GMKEGG, obiGene.GMNCBI, obiGene.GMAffy], self.geneMatcherSettings):
573                if use:
574                    try:
575                        if taxid == "352472":
576                            matchers.extend([matcher(taxid), obiGene.GMDicty(),
577                                            [matcher(taxid), obiGene.GMDicty()]])
578                            # The reason machers are duplicated is that we want `matcher` or `GMDicty` to
579                            # match genes by them self if possible. Only use the joint matcher if they fail.   
580                        else:
581                            matchers.append(matcher(taxid))
582                    except Exception, ex:
583                        print ex
584            self.annotations.genematcher = obiGene.matcher(matchers)
585            self.annotations.genematcher.set_targets(self.annotations.geneNames)
586           
587    def Enrichment(self, pb=None):
588        pb = OWGUI.ProgressBar(self, 100) if pb is None else pb
589        if not self.annotations.ontology:
590            self.annotations.ontology = self.ontology
591           
592        if isinstance(self.annotations.genematcher, obiGene.GMDirect):
593            self.SetGeneMatcher()
594        self.error(1)
595        self.warning([0, 1])
596        try:   
597            if self.useAttrNames:
598                clusterGenes = [v.name for v in self.clusterDataset.domain.variables]
599                self.information(0)
600            else:
601                geneAttr = self.candidateGeneAttrs[min(self.geneAttrIndex, len(self.candidateGeneAttrs)-1)]
602                clusterGenes = [str(ex[geneAttr]) for ex in self.clusterDataset if not ex[geneAttr].isSpecial()]
603                if any("," in gene for gene in clusterGenes):
604                    self.information(0, "Separators detected in cluster gene names. Assuming multiple genes per example.")
605                    clusterGenes = reduce(list.__add__, (genes.split(",") for genes in clusterGenes))
606                else:
607                    self.information(0)
608        except Exception, ex:
609            self.error(1, "Failed to extract gene names from input dataset! %s" % str(ex))
610            return {}
611        genesCount = len(clusterGenes)
612        genesSetCount = len(set(clusterGenes))
613       
614        self.clusterGenes = clusterGenes = self.annotations.GetGeneNamesTranslator(clusterGenes).values()
615       
616#        self.clusterGenes = clusterGenes = filter(lambda g: g in self.annotations.aliasMapper or g in self.annotations.additionalAliases, clusterGenes)
617        self.infoLabel.setText("%i unique genes on input\n%i (%.1f%%) genes with known annotations" % (genesSetCount, len(clusterGenes), 100.0*len(clusterGenes)/genesSetCount if genesSetCount else 0.0))
618       
619        referenceGenes = None
620        if self.referenceDataset:
621            try:
622                if self.useAttrNames:
623                    referenceGenes = [v.name for v in self.referenceDataset.domain.variables]
624                    self.information(1)
625                else:
626                    referenceGenes = [str(ex[geneAttr]) for ex in self.referenceDataset if not ex[geneAttr].isSpecial()]
627                    if any("," in gene for gene in clusterGenes):
628                        self.information(1, "Separators detected in reference gene names. Assuming multiple genes per example.")
629                        referenceGenes = reduce(list.__add__, (genes.split(",") for genes in referenceGenes))
630                    else:
631                        self.information(1)
632
633                refc = len(referenceGenes)
634#                referenceGenes = filter(lambda g: g in self.annotations.aliasMapper or g in self.annotations.additionalAliases, referenceGenes)
635                referenceGenes = self.annotations.GetGeneNamesTranslator(referenceGenes).values()
636                self.referenceRadioBox.buttons[1].setText("Reference set (%i genes, %i matched)" % (refc, len(referenceGenes)))
637                self.referenceRadioBox.buttons[1].setDisabled(False)
638                self.information(2)
639            except Exception, er:
640                if not self.referenceDataset:
641                    self.information(2, "Unable to extract gene names from reference dataset. Using entire genome for reference")
642                else:
643                    self.referenceRadioBox.buttons[1].setText("Reference set")
644                    self.referenceRadioBox.buttons[1].setDisabled(True)
645                referenceGenes = self.annotations.geneNames
646                self.useReferenceDataset = 0
647        else:
648            self.useReferenceDataset = 0
649        if not self.useReferenceDataset:
650            self.information(2)
651            self.information(1)
652            referenceGenes = self.annotations.geneNames
653        self.referenceGenes = referenceGenes
654        evidences = []
655        for etype in obiGO.evidenceTypesOrdered:
656            if getattr(self, "useEvidence"+etype):
657                evidences.append(etype)
658        aspect = ["P", "C", "F"][self.aspectIndex]
659       
660        if clusterGenes:
661            self.terms = terms = self.annotations.GetEnrichedTerms(clusterGenes, referenceGenes, evidences, aspect=aspect,
662                                                                   prob=self.probFunctions[self.probFunc], useFDR=False,
663                                                                   progressCallback=lambda value:pb.advance() )
664            ids = []
665            pvals = []
666            for i,d in self.terms.items():
667                ids.append(i)
668                pvals.append(d[1])
669            for i,fdr in zip(ids, obiProb.FDR(pvals)): #save FDR as the last part of the tuple
670                terms[i] = tuple(list(terms[i]) + [ fdr ])
671
672        else:
673            self.terms = terms = {}
674        if not self.terms:
675            self.warning(0, "No enriched terms found.")
676        else:
677            self.warning(0)
678           
679        pb.finish()
680        self.treeStructDict = {}
681        ids = self.terms.keys()
682       
683        self.treeStructRootKey = None
684       
685        parents = {}
686        for id in ids:
687            parents[id] = set([term for typeId, term in self.ontology[id].related])
688           
689        children = {}
690        for term in self.terms:
691            children[term] = set([id for id in ids if term in parents[id]])
692           
693        for term in self.terms:
694            self.treeStructDict[term] = TreeNode(self.terms[term], children[term])
695            if not self.ontology[term].related and not getattr(self.ontology[term], "is_obsolete", False):
696                self.treeStructRootKey = term
697        return terms
698       
699    def FilterGraph(self, graph):
700        if self.filterByPValue_nofdr:
701            graph = obiGO.filterByPValue(graph, self.maxPValue_nofdr)
702        if self.filterByPValue: #FDR
703            graph = dict(filter(lambda (k, e): e[3] <= self.maxPValue, graph.items()))
704        if self.filterByNumOfInstances:
705            graph = dict(filter(lambda (id,(genes, p, rc, fdr)):len(genes)>=self.minNumOfInstances, graph.items()))
706        return graph
707
708    def FilterAndDisplayGraph(self):
709        if self.clusterDataset:
710            self.graph = self.FilterGraph(self.originalGraph)
711            if self.originalGraph and not self.graph:
712                self.warning(1, "All found terms were filtered out.")
713            else:
714                self.warning(1)
715            self.ClearGraph()
716            self.DisplayGraph()
717
718    def SetGraph(self, graph=None):
719        self.originalGraph = graph
720        if graph:
721            self.FilterAndDisplayGraph()
722        else:
723            self.graph = {}
724            self.ClearGraph()
725
726    def ClearGraph(self):
727        self.listView.clear()
728        self.listViewItems=[]
729        self.sigTerms.clear()
730
731    def DisplayGraph(self):
732        fromParentDict = {}
733        self.termListViewItemDict = {}
734        self.listViewItems = []
735        enrichment = lambda t:float(len(t[0])) / t[2] * (float(len(self.referenceGenes))/len(self.clusterGenes))
736        maxFoldEnrichment = max([enrichment(term) for term in self.graph.values()] or [1])
737
738
739        def addNode(term, parent, parentDisplayNode):
740            if (parent, term) in fromParentDict:
741                return
742            if term in self.graph:
743                displayNode = GOTreeWidgetItem(self.ontology[term], self.graph[term], len(self.clusterGenes), len(self.referenceGenes), maxFoldEnrichment, parentDisplayNode)
744                displayNode.goId = term
745                self.listViewItems.append(displayNode)
746                if term in self.termListViewItemDict:
747                    self.termListViewItemDict[term].append(displayNode)
748                else:
749                    self.termListViewItemDict[term] = [displayNode]
750                fromParentDict[(parent, term)] = True
751                parent = term
752            else:
753                displayNode = parentDisplayNode
754
755            for c in self.treeStructDict[term].children:
756                addNode(c, parent, displayNode)
757
758        if self.treeStructDict:
759            addNode(self.treeStructRootKey, None, self.listView)
760
761        terms = self.graph.items()
762        terms = sorted(terms, key=lambda item: item[1][1])
763        self.sigTableTermsSorted = [t[0] for t in terms]
764
765        self.sigTerms.clear()
766        for i, (t_id, (genes, p_value, refCount, fdr)) in enumerate(terms):
767            item = GOTreeWidgetItem(self.ontology[t_id],
768                                    (genes, p_value, refCount, fdr),
769                                    len(self.clusterGenes),
770                                    len(self.referenceGenes),
771                                    maxFoldEnrichment,
772                                    self.sigTerms)
773            item.goId = t_id
774
775        self.listView.expandAll()
776        for i in range(5):
777            self.listView.resizeColumnToContents(i)
778            self.sigTerms.resizeColumnToContents(i)
779        self.sigTerms.resizeColumnToContents(6)
780        width = min(self.listView.columnWidth(0), 350)
781        self.listView.setColumnWidth(0, width)
782        self.sigTerms.setColumnWidth(0, width)
783
784        # Create and send the enrichemnt report table.
785        termsDomain = orange.Domain(
786            [orange.StringVariable("GO Term Id"),
787             orange.StringVariable("GO Term Name"),
788             orange.FloatVariable("Cluster Frequency"),
789             orange.FloatVariable("Reference Frequency"),
790             orange.FloatVariable("p-value"),
791             orange.FloatVariable("FDR"),
792             orange.FloatVariable("Enrichment"),
793             orange.StringVariable("Genes")
794             ], None)
795
796        terms = [[t_id,
797                  self.ontology[t_id].name,
798                  float(len(genes)) / len(self.clusterGenes),
799                  float(r_count) / len(self.referenceGenes),
800                  p_value,
801                  fdr,
802                  float(len(genes)) / len(self.clusterGenes) * \
803                  float(len(self.referenceGenes)) / r_count,
804                  ",".join(genes)
805                  ]
806                 for t_id, (genes, p_value, r_count, fdr) in terms]
807
808        if terms:
809            termsTable = orange.ExampleTable(termsDomain, terms)
810        else:
811            termsTable = orange.ExampleTable(termsDomain)
812        self.send("Enrichment Report", termsTable)
813
814    def ViewSelectionChanged(self):
815        if self.selectionChanging:
816            return
817
818        self.selectionChanging = 1
819        self.selectedTerms = []
820        selected = self.listView.selectedItems()
821        self.selectedTerms = list(set([lvi.term.id for lvi in selected]))
822        self.ExampleSelection()
823        self.selectionChanging = 0
824
825    def TableSelectionChanged(self):
826        if self.selectionChanging:
827            return
828       
829        self.selectionChanging = 1
830        self.selectedTerms = []
831        selectedIds = set([self.sigTerms.itemFromIndex(index).goId for index in self.sigTerms.selectedIndexes()])
832       
833        for i in range(self.sigTerms.topLevelItemCount()):
834            item = self.sigTerms.topLevelItem(i)
835            selected = item.goId in selectedIds
836            term = item.goId
837           
838            if selected:
839                self.selectedTerms.append(term)
840               
841            for lvi in self.termListViewItemDict[term]:
842                try:
843                    lvi.setSelected(selected)
844                    if selected: lvi.setExpanded(True)
845                except RuntimeError:    ##Underlying C/C++ object deleted (why??)
846                    pass
847               
848        self.ExampleSelection()
849        self.selectionChanging = 0
850           
851   
852    def UpdateAddClassButton(self):
853        self.addClassCB.setEnabled(self.selectionDisjoint == 1)
854       
855    def ExampleSelection(self):
856        selectedExamples = []
857        unselectedExamples = []
858        selectedGenes = []
859       
860        #change by Marko. don't do anything if there is no3 dataset
861        if not self.clusterDataset:
862            return
863       
864        selectedGenes = reduce(set.union, [v[0] for id, v in self.graph.items() if id in self.selectedTerms], set())
865        evidences = []
866        for etype in obiGO.evidenceTypesOrdered:
867            if getattr(self, "useEvidence"+etype):
868                evidences.append(etype)
869        allTerms = self.annotations.GetAnnotatedTerms(selectedGenes, 
870                          directAnnotationOnly=self.selectionDirectAnnotation, 
871                          evidenceCodes=evidences)
872           
873        if self.selectionDisjoint:
874            count = defaultdict(int)
875            for term in self.selectedTerms:
876                for g in allTerms.get(term, []):
877                    count[g]+=1
878            ccount = 1 if self.selectionDisjoint==1 else len(self.selectedTerms)
879            selectedGenes = [gene for gene, c in count.items() if c==ccount and gene in selectedGenes]
880        else:
881            selectedGenes = reduce(set.union, [allTerms.get(term, []) for term in self.selectedTerms], set())
882
883        if self.useAttrNames:
884            vars = [self.clusterDataset.domain[gene] for gene in set(selectedGenes)]
885            newDomain = orange.Domain(vars, self.clusterDataset.domain.classVar)
886            newdata = orange.ExampleTable(newDomain, self.clusterDataset)
887            self.send("Selected Examples", newdata)
888            self.send("Unselected Examples", None)
889        elif self.candidateGeneAttrs:
890            geneAttr = self.candidateGeneAttrs[min(self.geneAttrIndex, len(self.candidateGeneAttrs)-1)]
891            if self.selectionDisjoint == 1:
892                goVar = orange.EnumVariable("GO Term", values=list(self.selectedTerms))
893                newDomain = orange.Domain(self.clusterDataset.domain.variables, goVar)
894                newDomain.addmetas(self.clusterDataset.domain.getmetas())
895#            else:
896#                goVar = orange.StringVariable("GO Terms")
897#                newDomain = orange.Domain(self.clusterDataset.domain)
898#                newDomain.addmeta(orange.newmetaid(), goVar)
899           
900           
901            for ex in self.clusterDataset:
902                if not ex[geneAttr].isSpecial() and any(gene in selectedGenes for gene in str(ex[geneAttr]).split(",")):
903                    if self.selectionDisjoint == 1 and self.selectionAddTermAsClass:
904                        terms = filter(lambda term: any(gene in self.graph[term][0] for gene in str(ex[geneAttr]).split(",")) , self.selectedTerms)
905                        term = sorted(terms)[0]
906                        ex =  orange.Example(newDomain, ex)
907                        ex[goVar] = goVar(term)
908#                        ex.setclass(newClass(term))
909                    selectedExamples.append(ex)
910                else:
911                    unselectedExamples.append(ex)
912                   
913            if selectedExamples:
914                selectedExamples = orange.ExampleTable(selectedExamples)
915            else:
916                selectedExamples = None
917               
918            if unselectedExamples:
919                unselectedExamples = orange.ExampleTable(unselectedExamples)
920            else:
921                unselectedExamples = None
922
923            self.send("Selected Examples", selectedExamples)
924            self.send("Unselected Examples", unselectedExamples)
925
926    def ShowInfo(self):
927        dialog = QDialog(self)
928        dialog.setModal(False)
929        dialog.setLayout(QVBoxLayout())
930        label = QLabel(dialog)
931        label.setText("Ontology:\n"+self.ontology.header if self.ontology else "Ontology not loaded!")
932        dialog.layout().addWidget(label)
933
934        label = QLabel(dialog)
935        label.setText("Annotations:\n"+self.annotations.header.replace("!", "") if self.annotations else "Annotations not loaded!")
936        dialog.layout().addWidget(label)
937        dialog.show()
938       
939    def sendReport(self):
940        self.reportSettings("Settings", [("Organism", self.annotationCodes[min(self.annotationIndex, len(self.annotationCodes) - 1)]),
941                                         ("Significance test", ("Binomial" if self.probFunc == 0 else "Hypergeometric") )])
942        self.reportSettings("Filter", ([("Min cluster size", self.minNumOfInstances)] if self.filterByNumOfInstances else []) + \
943                                      ([("Max p-value", self.maxPValue_nofdr)] if self.filterByPValue_nofdr else []) + \
944                                      ([("Max FDR", self.maxPValue)] if self.filterByPValue else []))
945
946        def treeDepth(item):
947            return 1 + max([treeDepth(item.child(i)) for i in range(item.childCount())] +[0])
948       
949        def printTree(item, level, treeDepth):
950            text = '<tr>' + '<td width=16px></td>' * level
951            text += '<td colspan="%i">%s: %s</td>' % (treeDepth - level, item.term.id, item.term.name)
952            text += ''.join('<td>%s</td>' % item.text(i) for i in range(1, 5) + [6]) + '</tr>\n'
953            for i in range(item.childCount()):
954                text += printTree(item.child(i), level + 1, treeDepth)
955            return text
956       
957        treeDepth = max([treeDepth(self.listView.topLevelItem(i)) for i in range(self.listView.topLevelItemCount())] + [0])
958       
959        tableText = '<table>\n<tr>' + ''.join('<th>%s</th>' % s for s in ["Term:", "List:", "Reference:", "p-value:", "FDR", "Enrichment:"]) + '</tr>'
960       
961        treeText = '<table>\n' +  '<th colspan="%i">%s</th>' % (treeDepth, "Term:") 
962        treeText += ''.join('<th>%s</th>' % s for s in ["List:", "Reference:", "p-value:", "FDR", "Enrichment:"]) + '</tr>'
963       
964        for index in range(self.sigTerms.topLevelItemCount()):
965            item = self.sigTerms.topLevelItem(index)
966            tableText += printTree(item, 0, 1) 
967        tableText += '</table>' 
968       
969        for index in range(self.listView.topLevelItemCount()):
970            item = self.listView.topLevelItem(index)
971            treeText += printTree(item, 0, treeDepth)
972       
973        self.reportSection("Enriched Terms")
974        self.reportRaw(tableText)
975       
976        self.reportSection("Enriched Terms in the Ontology Tree")
977        self.reportRaw(treeText)
978       
979    def onStateChanged(self, stateType, id, text):
980        if stateType == "Warning":
981            self.listView._userMessage = text
982            self.listView.viewport().update()
983           
984    def onDeleteWidget(self):
985        """ Called before the widget is removed from the canvas.
986        """
987        self.annotations = None
988        self.ontology = None
989        gc.collect() # Force collection
990       
991
992fmtp = lambda score: "%0.5f" % score if score > 10e-4 else "%0.1e" % score
993fmtpdet = lambda score: "%0.9f" % score if score > 10e-4 else "%0.5e" % score
994
995class GOTreeWidgetItem(QTreeWidgetItem):
996    def __init__(self, term, enrichmentResult, nClusterGenes, nRefGenes, maxFoldEnrichment, parent):
997        QTreeWidgetItem.__init__(self, parent)
998        self.term = term
999        self.enrichmentResult = enrichmentResult
1000        self.nClusterGenes = nClusterGenes
1001        self.nRefGenes = nRefGenes
1002        self.maxFoldEnrichment = maxFoldEnrichment
1003        self.enrichment = enrichment = lambda t:float(len(t[0])) / t[2] * (float(nRefGenes)/nClusterGenes)
1004        self.setText(0, term.name)
1005        fmt = "%" + str(-int(math.log(nClusterGenes))) + "i (%.2f%%)"
1006        self.setText(1, fmt % (len(enrichmentResult[0]), 100.0*len(self.enrichmentResult[0])/nClusterGenes))
1007        fmt = "%" + str(-int(math.log(nRefGenes))) + "i (%.2f%%)"
1008        self.setText(2, fmt % (enrichmentResult[2], 100.0*enrichmentResult[2]/nRefGenes))
1009        self.setText(3, fmtp(enrichmentResult[1]))
1010        self.setToolTip(3, fmtpdet(enrichmentResult[1]))
1011        self.setText(4, fmtp(enrichmentResult[3])) #FDR
1012        self.setToolTip(4, fmtpdet(enrichmentResult[3]))
1013        self.setText(5, ", ".join(enrichmentResult[0]))
1014        self.setText(6, "%.2f" % (enrichment(enrichmentResult)))
1015        self.setToolTip(6, "%.2f" % (enrichment(enrichmentResult)))
1016        self.setToolTip(0, "<p>" + term.__repr__()[6:].strip().replace("\n", "<br>"))
1017        self.sortByData = [term.name, len(self.enrichmentResult[0]), enrichmentResult[2], enrichmentResult[1], enrichmentResult[3], ", ".join(enrichmentResult[0]), enrichment(enrichmentResult)]
1018
1019    def data(self, col, role):
1020        if role == Qt.UserRole:
1021            return QVariant(self.enrichment(self.enrichmentResult) / self.maxFoldEnrichment)
1022        else:
1023            return QTreeWidgetItem.data(self, col, role)
1024
1025    def __lt__(self, other):
1026        col = self.treeWidget().sortColumn()
1027        return self.sortByData[col] < other.sortByData[col]
1028   
1029class EnrichmentColumnItemDelegate(QItemDelegate):
1030    def paint(self, painter, option, index):
1031        self.drawBackground(painter, option, index)
1032        value, ok = index.data(Qt.UserRole).toDouble()
1033        if ok:
1034            painter.save()
1035            painter.setBrush(QBrush(Qt.white, Qt.SolidPattern))
1036            painter.drawRect(option.rect)
1037            painter.setBrush(QBrush(Qt.blue, Qt.SolidPattern))
1038            painter.drawRect(option.rect.x(), option.rect.y(), value*(option.rect.width()-1), option.rect.height()-1)
1039            painter.restore()
1040        else:
1041            QItemDelegate.paint(self, painter, option, index)
1042       
1043       
1044class GeneMatcherDialog(OWWidget):
1045    items = [("useGO", "Use gene names from Gene Ontology annotations"),
1046             ("useKEGG", "Use gene names from KEGG Genes database"),
1047             ("useNCBI", "Use gene names from NCBI Gene info database"),
1048             ("useAffy", "Use Affymetrix platform reference ids")]
1049    settingsList = [item[0] for item in items]
1050    def __init__(self, parent=None, defaults=[True, False, False, False], enabled=[False, True, True, True], **kwargs):
1051        OWWidget.__init__(self, parent, **kwargs)
1052        for item, default in zip(self.items, defaults):
1053            setattr(self, item[0], default)
1054           
1055        self.loadSettings()
1056        for item, enable in zip(self.items, enabled):
1057            cb = OWGUI.checkBox(self, self, *item)
1058            cb.setEnabled(enable)
1059           
1060        box = OWGUI.widgetBox(self, orientation="horizontal")
1061        OWGUI.button(box, self, "OK", callback=self.accept)
1062        OWGUI.button(box, self, "Cancel", callback=self.reject)
1063       
1064       
1065if __name__=="__main__":
1066    import sys
1067    app = QApplication(sys.argv)
1068    w=OWGOEnrichmentAnalysis()
1069    data = orange.ExampleTable("brown-selected.tab")
1070    w.show()
1071    w.SetClusterDataset(data)
1072    app.exec_()
1073    w.saveSettings()
1074       
Note: See TracBrowser for help on using the repository browser.