source: orange-bioinformatics/_bioinformatics/widgets/OWSelectGenes.py @ 1854:e11c55eccd57

Revision 1854:e11c55eccd57, 15.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 months ago (diff)

Fixed input data handling.

Line 
1import re
2import unicodedata
3from collections import defaultdict, namedtuple
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        self.data = None
57
58        box = OWGUI.widgetBox(self.controlArea, "Gene Attribute")
59        self.attrsCombo = OWGUI.comboBox(
60            box, self, "geneIndex",
61            callback=self._onGeneIndexChanged,
62            tooltip="Column with gene names"
63        )
64        self.attrsCombo.setModel(self.variables)
65
66        box = OWGUI.widgetBox(self.controlArea, "Gene Selection")
67        self.entryField = ListTextEdit(box)
68        self.entryField.setTabChangesFocus(True)
69        self.entryField.setToolTip("Enter selected gene names")
70        self.entryField.itemsChanged.connect(self._itemsChanged)
71
72        box.layout().addWidget(self.entryField)
73
74        completer = ListCompleter(self)
75        completer.setCompletionMode(QCompleter.PopupCompletion)
76        completer.setCaseSensitivity(Qt.CaseInsensitive)
77        completer.setMaxVisibleItems(10)
78        completer.popup().setAlternatingRowColors(True)
79        completer.setModel(QStringListModel([], self))
80
81        self.entryField.setCompleter(completer)
82
83        self.hightlighter = NameHighlight(self.entryField.document())
84
85        box = OWGUI.widgetBox(self.controlArea, "Output")
86        OWGUI.checkBox(box, self, "preserveOrder", "Preserve input order",
87                       tooltip="Preserve the order of the input data "
88                               "instances.",
89                       callback=self.invalidateOutput)
90        cb = OWGUI.checkBox(box, self, "autoCommit", "Auto commit")
91        button = OWGUI.button(box, self, "Commit", callback=self.commit)
92
93        OWGUI.setStopper(self, button, cb, "_changedFlag", self.commit)
94
95    def set_data(self, data):
96        """
97        Set the input data.
98        """
99        self.closeContext("")
100        self.warning()
101        self.data = data
102        if data is not None:
103            attrs = gene_candidates(data)
104            self.variables[:] = attrs
105            self.attrsCombo.setCurrentIndex(0)
106            if attrs:
107                self.geneIndex = 0
108            else:
109                self.geneIndex = -1
110                self.warning(0, "No suitable columns for gene names.")
111
112            self.selection = []
113        else:
114            self.variables[:] = []
115            self.geneIndex = -1
116
117        self._changedFlag = True
118        self._updateCompletionModel()
119
120        self.openContext("", data)
121
122        self.entryField.setPlainText(" ".join(self.selection))
123
124        self.commit()
125
126    @property
127    def geneVar(self):
128        if self.data is not None and self.geneIndex >= 0:
129            return self.variables[self.geneIndex]
130        else:
131            return None
132
133    def invalidateOutput(self):
134        if self.autoCommit:
135            self.commit()
136        else:
137            self._changedFlag = True
138
139    def commit(self):
140        gene = self.geneVar
141
142        if gene is not None:
143            if self.preserveOrder:
144                selection = set(self.selection)
145                sel = [inst for inst in self.data
146                       if str(inst[gene]) in selection]
147            else:
148                by_genes = defaultdict(list)
149                for inst in self.data:
150                    by_genes[str(inst[gene])].append(inst)
151
152                sel = []
153                for name in self.selection:
154                    sel.extend(by_genes.get(name, []))
155
156            if sel:
157                data = Orange.data.Table(self.data.domain, sel)
158            else:
159                data = Orange.data.Table(self.data.domain)
160
161        else:
162            data = None
163
164        self.send("Selected Data", data)
165        self._changedFlag = False
166
167    def _updateCompletionModel(self):
168        var = self.geneVar
169        if var is not None:
170            names = [str(inst[var]) for inst in self.data
171                     if not inst[var].isSpecial()]
172        else:
173            names = []
174
175        self.geneNames = names
176        self.entryField.completer().model().setStringList(sorted(set(names)))
177        self.hightlighter.setNames(names)
178
179    def _onGeneIndexChanged(self):
180        self._updateCompletionModel()
181        self.invalidateOutput()
182
183    def _itemsChanged(self, names):
184        selection = set(names).intersection(self.geneNames)
185        curr_selection = set(self.selection).intersection(self.geneNames)
186
187        if selection != curr_selection:
188            self.selection = names
189            self.invalidateOutput()
190
191            names = set(self.geneNames) - set(names)
192            self.entryField.completer().model().setStringList(sorted(names))
193
194
195def is_string(feature):
196    return isinstance(feature, Orange.feature.String)
197
198
199def domain_variables(domain):
200    vars = (domain.features +
201            domain.class_vars +
202            domain.getmetas().values())
203    return vars
204
205
206def gene_candidates(data):
207    vars = domain_variables(data.domain)
208    vars = filter(is_string, vars)
209    return vars
210
211
212_CompletionState = namedtuple(
213    "_CompletionState",
214    ["start",  # completion prefix start position
215     "pos",  # cursor position
216     "anchor"]  # anchor position (inline completion end)
217)
218
219
220class ListTextEdit(QPlainTextEdit):
221    itemsChanged = Signal(list)
222
223    def __init__(self, parent=None, **kwargs):
224        QPlainTextEdit.__init__(self, parent, **kwargs)
225
226        self._items = None
227        self._completer = None
228        self._completionState = _CompletionState(-1, -1, -1)
229
230        self.cursorPositionChanged.connect(self._cursorPositionChanged)
231        self.textChanged.connect(self._textChanged)
232
233    def setCompleter(self, completer):
234        """
235        Set a completer for list items.
236        """
237        if self._completer is not None:
238            self._completer.setWidget(None)
239            self._completer.activated.disconnect(self._insertCompletion)
240
241        self._completer = completer
242
243        if self._completer:
244            self._completer.setWidget(self)
245            self._completer.activated.connect(self._insertCompletion)
246
247    def completer(self):
248        """
249        Return the completer.
250        """
251        return self._completer
252
253    def setItems(self, items):
254        text = " ".join(items)
255        self.setPlainText(text)
256
257    def items(self):
258        if self._items is None:
259            self._items = self._getItems()
260        return self._items
261
262    def keyPressEvent(self, event):
263        # TODO: in Qt 4.8 QPlainTextEdit uses inputMethodEvent for
264        # non-ascii input
265
266        if self._completer.popup().isVisible():
267            if event.key() in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape,
268                               Qt.Key_Tab, Qt.Key_Backtab]:
269                # These need to propagate to the completer.
270                event.ignore()
271                return
272
273        QPlainTextEdit.keyPressEvent(self, event)
274
275        if not len(event.text()) or not is_printable(unicode(event.text())[0]):
276            return
277
278        text = unicode(self.toPlainText())
279        cursor = self.textCursor()
280        pos = cursor.position()
281
282        if pos == len(text) or not(text[pos].strip()):
283            # cursor is at end of text or whitespace
284            # find the beginning of the current word
285            whitespace = " \t\n\r\f\v"
286            start = max([text.rfind(c, 0, pos) for c in whitespace]) + 1
287
288            prefix = text[start:pos]
289
290            if prefix:
291                if self._completer.completionPrefix() != prefix:
292                    self._completer.setCompletionPrefix(text[start:pos])
293
294                rect = self.cursorRect()
295                popup = self._completer.popup()
296                if popup.isVisible():
297                    rect.setWidth(popup.width())
298                else:
299                    rect.setWidth(popup.sizeHintForColumn(0) +
300                                  popup.verticalScrollBar().sizeHint().width())
301
302                # Popup the completer list
303                self._completer.complete(rect)
304
305                # Inline completion of a common prefix
306                inline = self._commonCompletionPrefix()
307                inline = inline[len(prefix):]
308
309                self._completionState = \
310                    _CompletionState(start, pos, pos + len(inline))
311
312                cursor.insertText(inline)
313                cursor.setPosition(pos, QTextCursor.KeepAnchor)
314                self.setTextCursor(cursor)
315
316            elif self._completer.popup().isVisible():
317                self._stopCompletion()
318
319    def _cursorPositionChanged(self):
320        cursor = self.textCursor()
321        pos = cursor.position()
322        start, _, _ = self._completionState
323
324        if start == -1:
325            # completion not in progress
326            return
327
328        if pos <= start:
329            # cursor moved before the start of the prefix
330            self._stopCompletion()
331            return
332
333        text = unicode(self.toPlainText())
334        # Find the end of the word started by completion prefix
335        word_end = len(text)
336        for i in range(start, len(text)):
337            if text[i] in " \t\n\r\f\v":
338                word_end = i
339                break
340
341        if pos > word_end:
342            # cursor moved past the word boundary
343            self._stopCompletion()
344
345        # TODO: Update the prefix when moving the cursor
346        # inside the word
347
348    def _insertCompletion(self, item):
349        if isinstance(item, list):
350            completion = " ".join(item)
351        else:
352            completion = unicode(item)
353
354        start, _, end = self._completionState
355
356        self._stopCompletion()
357
358        cursor = self.textCursor()
359        # Replace the prefix+inline with the full completion
360        # (correcting for the case-insensitive search).
361        cursor.setPosition(min(end, self.document().characterCount()))
362        cursor.setPosition(start, QTextCursor.KeepAnchor)
363
364        cursor.insertText(completion + " ")
365
366    def _commonCompletionPrefix(self):
367        model = self._completer.completionModel()
368        column = self._completer.completionColumn()
369        role = self._completer.completionRole()
370        items = [toString(model.index(i, column).data(role))
371                 for i in range(model.rowCount())]
372
373        if not items:
374            return ""
375
376        first = min(items)
377        last = max(items)
378        for i, c in enumerate(first):
379            if c != last[i]:
380                return first[:i]
381
382        return first
383
384    def _stopCompletion(self):
385        self._completionState = _CompletionState(-1, -1, -1)
386        if self._completer.popup().isVisible():
387            self._completer.popup().hide()
388
389    def _textChanged(self):
390        items = self._getItems()
391        if self._items != items:
392            self._items = items
393            self.itemsChanged.emit(items)
394
395    def _getItems(self):
396        text = unicode(self.toPlainText())
397        if self._completionState[0] != -1:
398            # Remove the inline completion text from the text
399            _, pos, end = self._completionState
400            text = text[:pos] + text[end:]
401        return [item for item in text.split() if item.strip()]
402
403
404class NameHighlight(QSyntaxHighlighter):
405    def __init__(self, parent=None, **kwargs):
406        super(NameHighlight, self).__init__(parent, **kwargs)
407
408        self._names = set()
409
410        self._format = QTextCharFormat()
411        self._format.setForeground(Qt.blue)
412
413        self._unrecognized_format = QTextCharFormat()
414#         self._unrecognized_format.setFontStrikeOut(True)
415
416    def setNames(self, names):
417        self._names = set(names)
418        self.rehighlight()
419
420    def names(self):
421        return set(self._names)
422
423    def highlightBlock(self, text):
424        text = unicode(text)
425        pattern = re.compile(r"\S+")
426        for match in pattern.finditer(text):
427            name = text[match.start(): match.end()]
428            match_len = match.end() - match.start()
429
430            if not name.strip():
431                continue
432
433            if name in self._names:
434                format = self._format
435            else:
436                format = self._unrecognized_format
437
438            self.setFormat(match.start(), match_len, format)
439
440
441def toString(variant):
442    if isinstance(variant, QVariant):
443        return unicode(variant.toString())
444    else:
445        return unicode(variant)
446
447
448@contextmanager
449def signals_blocked(obj):
450    blocked = obj.signalsBlocked()
451    obj.blockSignals(True)
452    try:
453        yield
454    finally:
455        obj.blockSignals(blocked)
456
457
458class ListCompleter(QCompleter):
459    activated = Signal(list)
460
461    def __init__(self, *args, **kwargs):
462        QCompleter.__init__(self, *args, **kwargs)
463
464        popup = QListView()
465        popup.setEditTriggers(QListView.NoEditTriggers)
466        popup.setSelectionMode(QListView.ExtendedSelection)
467
468        self.setPopup(popup)
469
470    def setPopup(self, popup):
471        QCompleter.setPopup(self, popup)
472
473        popup.viewport().installEventFilter(self)
474        popup.doubleClicked.connect(self._complete)
475
476    def eventFilter(self, receiver, event):
477        if event.type() == QEvent.KeyPress and receiver is self.popup():
478            if event.key() in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab]:
479                self._complete()
480                return True
481
482        elif event.type() == QEvent.MouseButtonRelease and \
483                receiver is self.popup().viewport():
484            # Process the event without emitting 'clicked', ... signal to
485            # override the default QCompleter behavior
486            with signals_blocked(self.popup()):
487                QApplication.sendEvent(self.popup(), event)
488                return True
489
490        return QCompleter.eventFilter(self, receiver, event)
491
492    def _complete(self):
493        selection = self.popup().selectionModel().selection()
494        indexes = selection.indexes()
495
496        items = [toString(index.data(self.completionRole()))
497                 for index in indexes]
498
499        if self.popup().isVisible():
500            self.popup().hide()
501
502        if items:
503            self.activated.emit(items)
504
505
506# All control character categories.
507_control = set(["Cc", "Cf", "Cs", "Co", "Cn"])
508
509
510def is_printable(unichar):
511    """
512    Return True if the unicode character `unichar` is a printable character.
513    """
514    return unicodedata.category(unichar) not in _control
515
516
517def test():
518    app = QApplication([])
519    w = OWSelectGenes()
520    data = Orange.data.Table("brown-selected")
521    w.set_data(data)
522    w.show()
523    app.exec_()
524    w.deleteLater()
525    del w
526    app.processEvents()
527
528if __name__ == "__main__":
529    test()
Note: See TracBrowser for help on using the repository browser.