source: orange-bioinformatics/_bioinformatics/widgets/OWSelectGenes.py @ 1850:5b514f87a15e

Revision 1850:5b514f87a15e, 11.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 months ago (diff)

Added multi item completion.

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