source: orange-bioinformatics/_bioinformatics/widgets/OWSelectGenes.py @ 1846:9be60f38b837

Revision 1846:9be60f38b837, 10.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 months ago (diff)

Added 'Select Genes' widget.

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