source: orange-bioinformatics/_bioinformatics/widgets/OWSelectGenes.py @ 1851:aa8ea42e2c8d

Revision 1851:aa8ea42e2c8d, 12.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 months ago (diff)

Added an option to order instances by the selected gene names.

Line 
1import re
2import unicodedata
3from collections import defaultdict
4
5from contextlib import contextmanager
6
7from PyQt4.QtGui import (
8    QLabel, QWidget, QPlainTextEdit, QSyntaxHighlighter, QTextCharFormat,
9    QTextCursor, QCompleter, QStringListModel, QListView
10)
11
12from PyQt4.QtCore import Qt, QEvent, pyqtSignal as Signal
13
14import Orange
15
16from Orange.OrangeWidgets.OWWidget import *
17from Orange.OrangeWidgets.OWItemModels import VariableListModel
18from Orange.OrangeWidgets import OWGUI
19
20
21NAME = "Select Genes"
22DESCRIPTION = "Select a specified subset of the input genes."
23ICON = "icons/SelectGenes.svg"
24
25INPUTS = [("Data", Orange.data.Table, "set_data")]
26OUTPUTS = [("Selected Data", Orange.data.Table)]
27
28
29class OWSelectGenes(OWWidget):
30
31    contextHandlers = {
32        "": DomainContextHandler(
33            "", ["geneIndex", "selection"]
34        )
35    }
36
37    settingsList = ["autoCommit", "preserveOrder"]
38
39    def __init__(self, parent=None, signalManager=None, title=NAME):
40        OWWidget.__init__(self, parent, signalManager, title,
41                          wantMainArea=False)
42
43        self.selection = []
44        self.geneIndex = None
45        self.autoCommit = False
46        self.preserveOrder = True
47
48        self.loadSettings()
49
50        # Input variables that could contain names
51        self.variables = VariableListModel()
52        # All gene names from the input (in self.geneIndex column)
53        self.geneNames = []
54        # Output changed flag
55        self._changedFlag = False
56
57        box = OWGUI.widgetBox(self.controlArea, "Gene Attribute")
58        self.attrsCombo = OWGUI.comboBox(
59            box, self, "geneIndex",
60            callback=self._onGeneIndexChanged,
61            tooltip="Column with gene names"
62        )
63        self.attrsCombo.setModel(self.variables)
64
65        box = OWGUI.widgetBox(self.controlArea, "Gene Selection")
66        self.entryField = ListTextEdit(box)
67        self.entryField.setTabChangesFocus(True)
68        self.entryField.setToolTip("Enter selected gene names")
69        self.entryField.textChanged.connect(self._textChanged)
70
71        box.layout().addWidget(self.entryField)
72
73        completer = ListCompleter(self)
74        completer.setCompletionMode(QCompleter.PopupCompletion)
75        completer.setCaseSensitivity(Qt.CaseInsensitive)
76        completer.setMaxVisibleItems(10)
77        completer.popup().setAlternatingRowColors(True)
78        completer.setModel(QStringListModel([], self))
79
80        self.entryField.setCompleter(completer)
81
82        self.hightlighter = NameHighlight(self.entryField.document())
83
84        box = OWGUI.widgetBox(self.controlArea, "Output")
85        OWGUI.checkBox(box, self, "preserveOrder", "Preserve input order",
86                       tooltip="Preserve the order of the input data "
87                               "instances.",
88                       callback=self.invalidateOutput)
89        cb = OWGUI.checkBox(box, self, "autoCommit", "Auto commit")
90        button = OWGUI.button(box, self, "Commit", callback=self.commit)
91
92        OWGUI.setStopper(self, button, cb, "_changedFlag", self.commit)
93
94    def set_data(self, data):
95        """
96        Set the input data.
97        """
98        self.closeContext("")
99        self.warning()
100        self.data = data
101        if data is not None:
102            attrs = gene_candidates(data)
103            self.variables[:] = attrs
104            self.attrsCombo.setCurrentIndex(0)
105            self.geneIndex = 0
106            self.selection = []
107        else:
108            self.variables[:] = []
109            self.geneIndex = -1
110            self.warning(0, "No suitable columns for gene names.")
111
112        self._changedFlag = True
113        self._updateCompletionModel()
114
115        self.openContext("", data)
116
117        self.entryField.setPlainText(" ".join(self.selection))
118
119        self.commit()
120
121    @property
122    def geneVar(self):
123        if self.data is not None and self.geneIndex >= 0:
124            return self.variables[self.geneIndex]
125        else:
126            return None
127
128    def invalidateOutput(self):
129        if self.autoCommit:
130            self.commit()
131        else:
132            self._changedFlag = True
133
134    def commit(self):
135        gene = self.geneVar
136
137        if gene is not None:
138            if self.preserveOrder:
139                selection = set(self.selection)
140                sel = [inst for inst in self.data
141                       if str(inst[gene]) in selection]
142            else:
143                by_genes = defaultdict(list)
144                for inst in self.data:
145                    by_genes[str(inst[gene])].append(inst)
146
147                sel = []
148                for name in self.selection:
149                    sel.extend(by_genes.get(name, []))
150
151            if sel:
152                data = Orange.data.Table(self.data.domain, sel)
153            else:
154                data = Orange.data.Table(self.data.domain)
155
156        else:
157            data = None
158
159        self.send("Selected Data", data)
160        self._changedFlag = False
161
162    def _updateCompletionModel(self):
163        var = self.geneVar
164        if var is not None:
165            names = [str(inst[var]) for inst in self.data
166                     if not inst[var].isSpecial()]
167        else:
168            names = []
169
170        self.geneNames = names
171        self.entryField.completer().model().setStringList(sorted(set(names)))
172        self.hightlighter.setNames(names)
173
174    def _onGeneIndexChanged(self):
175        self._updateCompletionModel()
176        self.invalidateOutput()
177
178    def _textChanged(self):
179        names = self.entryField.list()
180        selection = set(names).intersection(self.geneNames)
181        curr_selection = set(self.selection).intersection(self.geneNames)
182
183        if selection != curr_selection:
184            self.selection = names
185            self.invalidateOutput()
186
187            names = set(self.geneNames) - set(names)
188            self.entryField.completer().model().setStringList(sorted(names))
189
190
191def is_string(feature):
192    return isinstance(feature, Orange.feature.String)
193
194
195def domain_variables(domain):
196    vars = (domain.features +
197            domain.class_vars +
198            domain.getmetas().values())
199    return vars
200
201
202def gene_candidates(data):
203    vars = domain_variables(data.domain)
204    vars = filter(is_string, vars)
205    return vars
206
207
208class ListTextEdit(QPlainTextEdit):
209    def __init__(self, parent=None, **kwargs):
210        QPlainTextEdit.__init__(self, parent, **kwargs)
211
212        self._completer = None
213
214    def setCompleter(self, completer):
215        """
216        Set a completer for list items.
217        """
218        if self._completer is not None:
219            self._completer.setWidget(None)
220            self._completer.activated.disconnect(self._insertCompletion)
221
222        self._completer = completer
223
224        if self._completer:
225            self._completer.setWidget(self)
226            self._completer.activated.connect(self._insertCompletion)
227
228    def completer(self):
229        """
230        Return the completer.
231        """
232        return self._completer
233
234    def setList(self, list):
235        text = " ".join(list)
236        self.setPlainText(text)
237
238    def list(self):
239        return [name for name in unicode(self.toPlainText()).split()
240                if name.strip()]
241
242    def keyPressEvent(self, event):
243        if self._completer.popup().isVisible():
244            if event.key() in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape,
245                               Qt.Key_Tab, Qt.Key_Backtab]:
246                # These need to propagate to the completer.
247                event.ignore()
248                return
249
250        QPlainTextEdit.keyPressEvent(self, event)
251
252        if not len(event.text()) or not is_printable(unicode(event.text())[0]):
253            return
254
255        text = unicode(self.toPlainText())
256        pos = self.textCursor().position()
257
258        if pos == len(text) or not(text[pos].strip()):
259            # At end of text or whitespace
260            # TODO: Match all whitespace characters.
261            start_sp = text.rfind(" ", 0, pos) + 1
262            start_n = text.rfind("\n", 0, pos) + 1
263            start = max(start_sp, start_n)
264
265            prefix = text[start:pos]
266
267            if prefix:
268                if self._completer.completionPrefix() != prefix:
269                    self._completer.setCompletionPrefix(text[start:pos])
270
271                rect = self.cursorRect()
272                popup = self._completer.popup()
273                if popup.isVisible():
274                    rect.setWidth(popup.width())
275                else:
276                    rect.setWidth(popup.sizeHintForColumn(0) +
277                                  popup.verticalScrollBar().sizeHint().width())
278
279                self._completer.complete(rect)
280
281            elif self._completer.popup().isVisible():
282                self._completer.popup().hide()
283
284    def _insertCompletion(self, item):
285        if isinstance(item, list):
286            completion = " ".join(item)
287        else:
288            completion = unicode(item)
289
290        prefix = self._completer.completionPrefix()
291
292        cursor = self.textCursor()
293        # Replace the prefix with the full completion (correcting for the
294        # case-insensitive search).
295        cursor.setPosition(cursor.position() - len(prefix),
296                           QTextCursor.KeepAnchor)
297
298        cursor.insertText(completion + " ")
299
300
301class NameHighlight(QSyntaxHighlighter):
302    def __init__(self, parent=None, **kwargs):
303        super(NameHighlight, self).__init__(parent)
304
305        self._names = set()
306
307        self._format = QTextCharFormat()
308        self._format.setForeground(Qt.blue)
309
310        self._unrecognized_format = QTextCharFormat()
311#         self._unrecognized_format.setFontStrikeOut(True)
312
313    def setNames(self, names):
314        self._names = set(names)
315        self.rehighlight()
316
317    def names(self):
318        return set(self._names)
319
320    def highlightBlock(self, text):
321        text = unicode(text)
322        pattern = re.compile(r"\S+")
323        for match in pattern.finditer(text):
324            name = text[match.start(): match.end()]
325            match_len = match.end() - match.start()
326
327            if not name.strip():
328                continue
329
330            if name in self._names:
331                format = self._format
332            else:
333                format = self._unrecognized_format
334
335            self.setFormat(match.start(), match_len, format)
336
337
338def toString(variant):
339    if isinstance(variant, QVariant):
340        return unicode(variant.toString())
341    else:
342        return unicode(variant)
343
344
345@contextmanager
346def signals_blocked(obj):
347    blocked = obj.signalsBlocked()
348    obj.blockSignals(True)
349    try:
350        yield
351    finally:
352        obj.blockSignals(blocked)
353
354
355class ListCompleter(QCompleter):
356    activated = Signal(list)
357
358    def __init__(self, *args, **kwargs):
359        QCompleter.__init__(self, *args, **kwargs)
360
361        popup = QListView()
362        popup.setEditTriggers(QListView.NoEditTriggers)
363        popup.setSelectionMode(QListView.ExtendedSelection)
364
365        self.setPopup(popup)
366
367    def setPopup(self, popup):
368        QCompleter.setPopup(self, popup)
369
370        popup.viewport().installEventFilter(self)
371        popup.doubleClicked.connect(self._complete)
372
373    def eventFilter(self, receiver, event):
374        if event.type() == QEvent.KeyPress and receiver is self.popup():
375            if event.key() in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab]:
376                self._complete()
377                return True
378
379        elif event.type() == QEvent.MouseButtonRelease and \
380                receiver is self.popup().viewport():
381            # Process the event without emitting 'clicked', ... signal to
382            # override the default QCompleter behavior
383            with signals_blocked(self.popup()):
384                QApplication.sendEvent(self.popup(), event)
385                return True
386
387        return QCompleter.eventFilter(self, receiver, event)
388
389    def _complete(self):
390        selection = self.popup().selectionModel().selection()
391        indexes = selection.indexes()
392
393        items = [toString(index.data(self.completionRole()))
394                 for index in indexes]
395
396        if self.popup().isVisible():
397            self.popup().hide()
398
399        self.activated.emit(items)
400
401
402# All control character categories.
403_control = set(["Cc", "Cf", "Cs", "Co", "Cn"])
404
405
406def is_printable(unichar):
407    """
408    Return True if the unicode character `unichar` is a printable character.
409    """
410    return unicodedata.category(unichar) not in _control
411
412
413def test():
414    app = QApplication([])
415    w = OWSelectGenes()
416    data = Orange.data.Table("brown-selected")
417    w.set_data(data)
418    w.show()
419    app.exec_()
420    w.deleteLater()
421    del w
422    app.processEvents()
423
424if __name__ == "__main__":
425    test()
Note: See TracBrowser for help on using the repository browser.