source: orange-bioinformatics/orangecontrib/bio/widgets/OWSelectGenes.py @ 1882:5f55ddc19cea

Revision 1882:5f55ddc19cea, 28.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Added an option to select genes in a secondary data table input.

RevLine 
[1846]1import re
2import unicodedata
[1853]3from collections import defaultdict, namedtuple
[1859]4from xml.sax.saxutils import escape
[1851]5
[1850]6from contextlib import contextmanager
[1846]7
8from PyQt4.QtGui import (
[1882]9    QFrame, QHBoxLayout, QPlainTextEdit, QSyntaxHighlighter, QTextCharFormat,
10    QTextCursor, QCompleter, QStringListModel, QStandardItemModel,
11    QStandardItem, QListView, QStyle, QStyledItemDelegate,
12    QStyleOptionViewItemV4, QPalette, QColor, QApplication,
13    QAction, QToolButton, QSizePolicy, QItemSelectionModel,
14    QPlainTextDocumentLayout, QTextDocument, QRadioButton,
15    QButtonGroup, QStyleOptionButton
[1846]16)
17
[1882]18from PyQt4.QtCore import Qt, QEvent, QVariant, pyqtSignal as Signal
[1846]19
20import Orange
21
[1882]22from Orange.OrangeWidgets.OWWidget import \
23    OWWidget, DomainContextHandler, Default
24
[1846]25from Orange.OrangeWidgets.OWItemModels import VariableListModel
26from Orange.OrangeWidgets import OWGUI
27
28
29NAME = "Select Genes"
30DESCRIPTION = "Select a specified subset of the input genes."
31ICON = "icons/SelectGenes.svg"
32
[1882]33INPUTS = [("Data", Orange.data.Table, "setData", Default),
34          ("Gene Subset", Orange.data.Table, "setGeneSubset")]
35
[1846]36OUTPUTS = [("Selected Data", Orange.data.Table)]
37
38
[1857]39def toString(variant):
40    if isinstance(variant, QVariant):
41        return unicode(variant.toString())
42    else:
43        return unicode(variant)
44
45
46def toBool(variant):
47    if isinstance(variant, QVariant):
48        return bool(variant.toPyObject())
49    else:
50        return bool(variant)
51
52
53class SaveSlot(QStandardItem):
54    ModifiedRole = next(OWGUI.OrangeUserRole)
55
56    def __init__(self, name, savedata=None, modified=False):
57        super(SaveSlot, self).__init__(name)
58
59        self.savedata = savedata
60        self.modified = modified
61        self.document = None
62
63    @property
64    def name(self):
65        return unicode(self.text())
66
67    @property
68    def modified(self):
69        return toBool(self.data(SaveSlot.ModifiedRole))
70
71    @modified.setter
72    def modified(self, state):
73        self.setData(bool(state), SaveSlot.ModifiedRole)
74
75
76class SavedSlotDelegate(QStyledItemDelegate):
77
78    def paint(self, painter, option, index):
79        option = QStyleOptionViewItemV4(option)
80        self.initStyleOption(option, index)
81
82        modified = toBool(index.data(SaveSlot.ModifiedRole))
83        if modified:
84            option.palette.setColor(QPalette.Text, QColor(Qt.red))
85            option.palette.setColor(QPalette.Highlight, QColor(Qt.darkRed))
86            option.text = "*" + option.text
87
88        if option.widget:
89            widget = option.widget
90            style = widget.style()
91        else:
92            widget = None
93            style = QApplication.style()
94
95        style.drawControl(QStyle.CE_ItemViewItem, option, painter, widget)
96
97
[1882]98def radio_indicator_width(button):
99    button.ensurePolished()
100    style = button.style()
101    option = QStyleOptionButton()
102    button.initStyleOption(option)
103
104    w = style.pixelMetric(QStyle.PM_ExclusiveIndicatorWidth, option, button)
105    return w
106
107
[1846]108class OWSelectGenes(OWWidget):
109
110    contextHandlers = {
111        "": DomainContextHandler(
[1857]112            "", ["geneIndex"]
[1882]113        ),
114        "subset": DomainContextHandler(
115            "subset", ["subsetGeneIndex"]
[1846]116        )
117    }
118
[1857]119    settingsList = ["autoCommit", "preserveOrder", "savedSelections",
[1882]120                    "selectedSelectionIndex", "selectedSource"]
121
122    SelectInput, SelectCustom = 0, 1
[1846]123
124    def __init__(self, parent=None, signalManager=None, title=NAME):
125        OWWidget.__init__(self, parent, signalManager, title,
126                          wantMainArea=False)
127
128        self.geneIndex = None
129        self.autoCommit = False
[1851]130        self.preserveOrder = True
[1857]131        self.savedSelections = [
132            ("Example", ["MRE11A", "RAD51", "MLH1", "MSH2", "DMC1"])
133        ]
134
135        self.selectedSelectionIndex = -1
[1882]136        self.selectedSource = OWSelectGenes.SelectCustom
[1846]137
138        self.loadSettings()
139
140        # Input variables that could contain names
141        self.variables = VariableListModel()
142        # All gene names from the input (in self.geneIndex column)
[1848]143        self.geneNames = []
[1846]144        # Output changed flag
145        self._changedFlag = False
[1857]146        # Current gene names
147        self.selection = []
[1882]148        # Input data
149        self.data = None
150        self.subsetData = None
151        # Input variables that could contain gene names from "Gene Subset"
152        self.subsetVariables = VariableListModel()
153        # Selected subset variable index
154        self.subsetGeneIndex = -1
[1846]155
156        box = OWGUI.widgetBox(self.controlArea, "Gene Attribute")
[1882]157        box.setToolTip("Column with gene names")
[1846]158        self.attrsCombo = OWGUI.comboBox(
159            box, self, "geneIndex",
160            callback=self._onGeneIndexChanged,
161        )
162        self.attrsCombo.setModel(self.variables)
163
164        box = OWGUI.widgetBox(self.controlArea, "Gene Selection")
[1882]165
166        button1 = QRadioButton("Select genes from 'Gene Subset' input")
167        button2 = QRadioButton("Select specified genes")
168
169        box.layout().addWidget(button1)
170
171        # Subset gene variable selection
172        self.subsetbox = OWGUI.widgetBox(box, None)
173        offset = radio_indicator_width(button1)
174        self.subsetbox.layout().setContentsMargins(offset, 0, 0, 0)
175        self.subsetbox.setEnabled(
176            self.selectedSource == OWSelectGenes.SelectInput)
177
178        self.subsetVarCombo = OWGUI.comboBox(
179            OWGUI.widgetBox(self.subsetbox, "Gene Attribute", flat=True),
180            self, "subsetGeneIndex",
181            callback=self._onSubsetGeneIndexChanged
182        )
183        self.subsetVarCombo.setModel(self.subsetVariables)
184        self.subsetVarCombo.setToolTip(
185            "Column with gene names in the 'Gene Subset' input"
186        )
187
188        box.layout().addWidget(button2)
189
190        group = QButtonGroup(box)
191        group.addButton(button1, OWSelectGenes.SelectInput)
192        group.addButton(button2, OWSelectGenes.SelectCustom)
193        group.buttonClicked[int].connect(self._selectionSourceChanged)
194
195        if self.selectedSource == OWSelectGenes.SelectInput:
196            button1.setChecked(True)
197        else:
198            button2.setChecked(True)
199
200        self.entrybox = OWGUI.widgetBox(box, None)
201        offset = radio_indicator_width(button2)
202        self.entrybox.layout().setContentsMargins(offset, 0, 0, 0)
203
204        self.entrybox.setEnabled(
205            self.selectedSource == OWSelectGenes.SelectCustom)
206
207        box = OWGUI.widgetBox(self.entrybox, "Select Genes", flat=True)
208        box.setToolTip("Enter gene names to select")
209
[1846]210        self.entryField = ListTextEdit(box)
211        self.entryField.setTabChangesFocus(True)
[1857]212        self.entryField.setDocument(self._createDocument())
213        self.entryField.itemsChanged.connect(self._onItemsChanged)
[1846]214
215        box.layout().addWidget(self.entryField)
216
[1857]217        completer = ListCompleter()
[1846]218        completer.setCompletionMode(QCompleter.PopupCompletion)
219        completer.setCaseSensitivity(Qt.CaseInsensitive)
[1850]220        completer.setMaxVisibleItems(10)
[1846]221        completer.popup().setAlternatingRowColors(True)
[1848]222        completer.setModel(QStringListModel([], self))
223
[1846]224        self.entryField.setCompleter(completer)
225
[1882]226        box = OWGUI.widgetBox(self.entrybox, "Saved Selections", flat=True)
227        box.setToolTip("Save/Select/Update saved gene selections")
[1857]228        box.layout().setSpacing(1)
229
230        self.selectionsModel = QStandardItemModel()
231        self.selectionsView = QListView()
232        self.selectionsView.setAlternatingRowColors(True)
233        self.selectionsView.setModel(self.selectionsModel)
234        self.selectionsView.setItemDelegate(SavedSlotDelegate(self))
235        self.selectionsView.selectionModel().selectionChanged.connect(
236            self._onSelectedSaveSlotChanged
237        )
238
239        box.layout().addWidget(self.selectionsView)
240
[1882]241        self.actionSave = QAction(
242            "Save", self,
243            toolTip="Save/Update the current selection")
244
245        self.actionAdd = QAction(
246            "+", self,
247            toolTip="Create a new saved selection")
248
249        self.actionRemove = QAction(
250            "-", self,
251            toolTip="Delete the current saved selection")
[1857]252
253        toolbar = QFrame()
254        layout = QHBoxLayout()
255        layout.setContentsMargins(0, 0, 0, 0)
256        layout.setSpacing(1)
257
258        def button(action):
259            b = QToolButton()
260            b.setDefaultAction(action)
261            return b
262
263        b = button(self.actionAdd)
264        layout.addWidget(b)
265
266        b = button(self.actionSave)
267        b.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
268        layout.addWidget(b, stretch=10)
269
270        b = button(self.actionRemove)
271        layout.addWidget(b)
272
273        toolbar.setLayout(layout)
274
275        box.layout().addWidget(toolbar)
276
277        self.actionSave.triggered.connect(self.saveSelection)
278        self.actionAdd.triggered.connect(self.addSelection)
279        self.actionRemove.triggered.connect(self.removeSelection)
[1846]280
281        box = OWGUI.widgetBox(self.controlArea, "Output")
[1851]282        OWGUI.checkBox(box, self, "preserveOrder", "Preserve input order",
283                       tooltip="Preserve the order of the input data "
284                               "instances.",
285                       callback=self.invalidateOutput)
[1846]286        cb = OWGUI.checkBox(box, self, "autoCommit", "Auto commit")
287        button = OWGUI.button(box, self, "Commit", callback=self.commit)
288
289        OWGUI.setStopper(self, button, cb, "_changedFlag", self.commit)
290
[1857]291        # restore saved selections model.
292        for name, names in self.savedSelections:
293            item = SaveSlot(name, names)
294            self.selectionsModel.appendRow([item])
295
296        if self.selectedSelectionIndex != -1:
297            self.selectionsView.selectionModel().select(
298                self.selectionsModel.index(self.selectedSelectionIndex, 0),
299                QItemSelectionModel.Select
300            )
301        self._updateActions()
302
[1858]303    def setData(self, data):
[1846]304        """
305        Set the input data.
306        """
307        self.closeContext("")
[1882]308        self.warning(0)
[1846]309        self.data = data
310        if data is not None:
311            attrs = gene_candidates(data)
312            self.variables[:] = attrs
313            self.attrsCombo.setCurrentIndex(0)
[1854]314            if attrs:
315                self.geneIndex = 0
316            else:
317                self.geneIndex = -1
318                self.warning(0, "No suitable columns for gene names.")
[1846]319        else:
320            self.variables[:] = []
321            self.geneIndex = -1
322
323        self._changedFlag = True
324        self._updateCompletionModel()
325
326        self.openContext("", data)
327
328        self.commit()
329
[1882]330    def setGeneSubset(self, data):
331        """
332        Set the gene subset input.
333        """
334        self.closeContext("subset")
335        self.warning(1)
336        self.subsetData = data
337        if data is not None:
338            variables = gene_candidates(data)
339            self.subsetVariables[:] = variables
340            self.subsetVarCombo.setCurrentIndex(0)
341            if variables:
342                self.subsetGeneIndex = 0
343            else:
344                self.subsetGeneIndex = -1
345                self.warning(1, "No suitable column for subset gene names.")
346        else:
347            self.subsetVariables[:] = []
348            self.subsetGeneIndex = -1
349
350        self.openContext("subset", data)
351
352        if self.selectedSource == OWSelectGenes.SelectInput:
353            self.commit()
354
[1846]355    @property
356    def geneVar(self):
[1858]357        """
358        Current gene attribute or None if none available.
359        """
[1882]360        index = self.attrsCombo.currentIndex()
361        if self.data is not None and index >= 0:
362            return self.variables[index]
363        else:
364            return None
365
366    @property
367    def subsetGeneVar(self):
368        """
369        Current subset gene attribute or None if not available.
370        """
371        index = self.subsetVarCombo.currentIndex()
372        if self.subsetData is not None and index >= 0:
373            return self.subsetVariables[index]
[1846]374        else:
375            return None
376
377    def invalidateOutput(self):
378        if self.autoCommit:
379            self.commit()
380        else:
381            self._changedFlag = True
382
[1882]383    def selectedGenes(self):
384        """
385        Return the names of the current selected genes.
386        """
387        selection = []
388        if self.selectedSource == OWSelectGenes.SelectInput:
389            var = self.subsetGeneVar
390            if var is not None:
391                values = [inst[var] for inst in self.subsetData]
392                selection = [str(val) for val in values
393                             if not val.is_special()]
394        else:
395            selection = self.selection
396        return selection
397
[1846]398    def commit(self):
[1858]399        """
400        Send the selected data subset to the output.
401        """
[1882]402        selection = self.selectedGenes()
[1846]403
[1882]404        if self.geneVar is not None:
405            data = select_by_genes(self.data, self.geneVar,
406                                   gene_list=selection,
[1858]407                                   preserve_order=self.preserveOrder)
[1846]408        else:
409            data = None
410
411        self.send("Selected Data", data)
412        self._changedFlag = False
413
[1882]414    def _selectionSourceChanged(self, source):
415        if self.selectedSource != source:
416            self.selectedSource = source
417            self.subsetbox.setEnabled(source == OWSelectGenes.SelectInput)
418            self.entrybox.setEnabled(source == OWSelectGenes.SelectCustom)
419            self.invalidateOutput()
420
[1846]421    def _updateCompletionModel(self):
422        var = self.geneVar
423        if var is not None:
424            names = [str(inst[var]) for inst in self.data
[1882]425                     if not inst[var].is_special()]
[1846]426        else:
427            names = []
428
429        self.geneNames = names
[1848]430        self.entryField.completer().model().setStringList(sorted(set(names)))
[1857]431        self.entryField.document().highlighter.setNames(names)
[1846]432
433    def _onGeneIndexChanged(self):
434        self._updateCompletionModel()
435        self.invalidateOutput()
436
[1882]437    def _onSubsetGeneIndexChanged(self):
438        if self.selectedSource == OWSelectGenes.SelectInput:
439            self.invalidateOutput()
440
[1857]441    def _onItemsChanged(self, names):
[1846]442        selection = set(names).intersection(self.geneNames)
443        curr_selection = set(self.selection).intersection(self.geneNames)
[1848]444
[1857]445        self.selection = names
446
[1846]447        if selection != curr_selection:
448            self.invalidateOutput()
[1857]449            to_complete = sorted(set(self.geneNames) - set(names))
450            self.entryField.completer().model().setStringList(to_complete)
[1846]451
[1857]452        item = self._selectedSaveSlot()
453        if item:
454            item.modified = item.savedata != names
455
456    def _selectedSaveSlot(self):
457        """
458        Return the current selected saved selection slot.
459        """
460        indexes = self.selectionsView.selectedIndexes()
461        if indexes:
462            return self.selectionsModel.item(indexes[0].row())
463        else:
464            return None
465
466    def saveSelection(self):
467        """
468        Save (update) the items in the current selected selection.
469        """
470        item = self._selectedSaveSlot()
471        if item:
472            item.savedata = self.entryField.items()
473            item.modified = False
474
475    def addSelection(self, name=None):
476        """
477        Add a new saved selection entry initialized by the current items.
478
479        The new slot will be selected.
480
481        """
482        item = SaveSlot(name or "New selection")
483        item.savedata = self.entryField.items()
484        self.selectionsModel.appendRow([item])
485        self.selectionsView.setCurrentIndex(item.index())
486
487        if not name:
488            self.selectionsView.edit(item.index())
489
490    def removeSelection(self):
491        """
492        Remove the current selected save slot.
493        """
494        item = self._selectedSaveSlot()
495        if item:
496            self.selectionsModel.removeRow(item.row())
497
498    def _onSelectedSaveSlotChanged(self):
499        item = self._selectedSaveSlot()
500        if item:
501            if not item.document:
502                item.document = self._createDocument()
503                if item.savedata:
504                    item.document.setPlainText(" ".join(item.savedata))
505
506            item.document.highlighter.setNames(self.geneNames)
507
508            self.entryField.setDocument(item.document)
509
510        self._updateActions()
511
512    def _createDocument(self):
513        """
514        Create and new QTextDocument instance for editing gene names.
515        """
516        doc = QTextDocument(self)
517        doc.setDocumentLayout(QPlainTextDocumentLayout(doc))
518        doc.highlighter = NameHighlight(doc)
519        return doc
520
521    def _updateActions(self):
522        """
523        Update the Save/remove action enabled state.
524        """
525        selected = bool(self._selectedSaveSlot())
526        self.actionRemove.setEnabled(selected)
527        self.actionSave.setEnabled(selected)
528
529    def getSettings(self, *args, **kwargs):
530        # copy the saved selections model back to widget settings.
531        selections = []
532        for i in range(self.selectionsModel.rowCount()):
533            item = self.selectionsModel.item(i)
534            selections.append((item.name, item.savedata))
535        self.savedSelections = selections
536
537        item = self._selectedSaveSlot()
538        if item is None:
539            self.selectedSelectionIndex = -1
540        else:
541            self.selectedSelectionIndex = item.row()
542
543        return OWWidget.getSettings(self, *args, **kwargs)
[1848]544
[1859]545    def sendReport(self):
546        report = []
547        if self.data is not None:
548            report.append("%i instances on input." % len(self.data))
549        else:
550            report.append("No data on input.")
551
552        if self.geneVar is not None:
553            report.append("Gene names taken from %r attribute." %
554                          escape(self.geneVar.name))
555
556        self.reportSection("Input")
557        self.startReportList()
558        for item in report:
559            self.addToReportList(item)
560        self.finishReportList()
[1882]561        report = []
562        selection = self.selectedGenes()
563        if self.selectedSource == OWSelectGenes.SelectInput:
564            self.reportRaw(
565                "<p>Gene Selection (from 'Gene Subset' input): %s</p>" %
566                escape(" ".join(selection))
567            )
568        else:
569            self.reportRaw(
570                "<p>Gene Selection: %s</p>" %
571                escape(" ".join(selection))
572            )
[1859]573        self.reportSettings(
574            "Settings",
575            [("Preserve order", self.preserveOrder)]
576        )
577
[1846]578
579def is_string(feature):
580    return isinstance(feature, Orange.feature.String)
581
582
583def domain_variables(domain):
[1858]584    """
585    Return all feature descriptors from the domain.
586    """
[1846]587    vars = (domain.features +
588            domain.class_vars +
589            domain.getmetas().values())
590    return vars
591
592
593def gene_candidates(data):
[1858]594    """
595    Return features that could contain gene names.
596    """
[1846]597    vars = domain_variables(data.domain)
598    vars = filter(is_string, vars)
599    return vars
600
601
[1858]602def select_by_genes(data, gene_feature, gene_list, preserve_order=True):
603    if preserve_order:
604        selection = set(gene_list)
605        sel = [inst for inst in data
606               if str(inst[gene_feature]) in selection]
607    else:
608        by_genes = defaultdict(list)
609        for inst in data:
610            by_genes[str(inst[gene_feature])].append(inst)
611
612        sel = []
613        for name in gene_list:
614            sel.extend(by_genes.get(name, []))
615
616    if sel:
617        data = Orange.data.Table(data.domain, sel)
618    else:
619        data = Orange.data.Table(data.domain)
620
621    return data
622
623
[1853]624_CompletionState = namedtuple(
625    "_CompletionState",
626    ["start",  # completion prefix start position
627     "pos",  # cursor position
628     "anchor"]  # anchor position (inline completion end)
629)
630
631
[1846]632class ListTextEdit(QPlainTextEdit):
[1858]633    """
634    A text editor specialized for editing a list of items.
635    """
636    #: Emitted when the list items change.
[1853]637    itemsChanged = Signal(list)
638
[1846]639    def __init__(self, parent=None, **kwargs):
640        QPlainTextEdit.__init__(self, parent, **kwargs)
641
[1853]642        self._items = None
[1846]643        self._completer = None
[1853]644        self._completionState = _CompletionState(-1, -1, -1)
645
646        self.cursorPositionChanged.connect(self._cursorPositionChanged)
647        self.textChanged.connect(self._textChanged)
[1846]648
649    def setCompleter(self, completer):
650        """
651        Set a completer for list items.
652        """
653        if self._completer is not None:
654            self._completer.setWidget(None)
[1850]655            self._completer.activated.disconnect(self._insertCompletion)
[1846]656
657        self._completer = completer
658
659        if self._completer:
660            self._completer.setWidget(self)
[1850]661            self._completer.activated.connect(self._insertCompletion)
[1846]662
663    def completer(self):
664        """
665        Return the completer.
666        """
667        return self._completer
668
[1853]669    def setItems(self, items):
670        text = " ".join(items)
[1846]671        self.setPlainText(text)
672
[1853]673    def items(self):
674        if self._items is None:
675            self._items = self._getItems()
676        return self._items
[1846]677
678    def keyPressEvent(self, event):
[1853]679        # TODO: in Qt 4.8 QPlainTextEdit uses inputMethodEvent for
680        # non-ascii input
681
[1846]682        if self._completer.popup().isVisible():
683            if event.key() in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape,
684                               Qt.Key_Tab, Qt.Key_Backtab]:
685                # These need to propagate to the completer.
686                event.ignore()
687                return
688
689        QPlainTextEdit.keyPressEvent(self, event)
690
691        if not len(event.text()) or not is_printable(unicode(event.text())[0]):
692            return
693
694        text = unicode(self.toPlainText())
[1853]695        cursor = self.textCursor()
696        pos = cursor.position()
[1846]697
698        if pos == len(text) or not(text[pos].strip()):
[1853]699            # cursor is at end of text or whitespace
700            # find the beginning of the current word
701            whitespace = " \t\n\r\f\v"
702            start = max([text.rfind(c, 0, pos) for c in whitespace]) + 1
[1846]703
704            prefix = text[start:pos]
705
706            if prefix:
707                if self._completer.completionPrefix() != prefix:
708                    self._completer.setCompletionPrefix(text[start:pos])
709
710                rect = self.cursorRect()
711                popup = self._completer.popup()
712                if popup.isVisible():
713                    rect.setWidth(popup.width())
714                else:
715                    rect.setWidth(popup.sizeHintForColumn(0) +
716                                  popup.verticalScrollBar().sizeHint().width())
717
[1853]718                # Popup the completer list
[1846]719                self._completer.complete(rect)
720
[1853]721                # Inline completion of a common prefix
722                inline = self._commonCompletionPrefix()
723                inline = inline[len(prefix):]
724
725                self._completionState = \
726                    _CompletionState(start, pos, pos + len(inline))
727
728                cursor.insertText(inline)
729                cursor.setPosition(pos, QTextCursor.KeepAnchor)
730                self.setTextCursor(cursor)
731
[1846]732            elif self._completer.popup().isVisible():
[1853]733                self._stopCompletion()
734
735    def _cursorPositionChanged(self):
736        cursor = self.textCursor()
737        pos = cursor.position()
738        start, _, _ = self._completionState
739
740        if start == -1:
741            # completion not in progress
742            return
743
744        if pos <= start:
745            # cursor moved before the start of the prefix
746            self._stopCompletion()
747            return
748
749        text = unicode(self.toPlainText())
750        # Find the end of the word started by completion prefix
751        word_end = len(text)
752        for i in range(start, len(text)):
753            if text[i] in " \t\n\r\f\v":
754                word_end = i
755                break
756
757        if pos > word_end:
758            # cursor moved past the word boundary
759            self._stopCompletion()
760
761        # TODO: Update the prefix when moving the cursor
762        # inside the word
[1846]763
764    def _insertCompletion(self, item):
[1850]765        if isinstance(item, list):
766            completion = " ".join(item)
767        else:
768            completion = unicode(item)
769
[1853]770        start, _, end = self._completionState
771
772        self._stopCompletion()
[1846]773
774        cursor = self.textCursor()
[1853]775        # Replace the prefix+inline with the full completion
776        # (correcting for the case-insensitive search).
777        cursor.setPosition(min(end, self.document().characterCount()))
778        cursor.setPosition(start, QTextCursor.KeepAnchor)
[1846]779
[1847]780        cursor.insertText(completion + " ")
[1846]781
[1853]782    def _commonCompletionPrefix(self):
[1858]783        """
784        Return the common prefix of items in the current completion model.
785        """
[1853]786        model = self._completer.completionModel()
787        column = self._completer.completionColumn()
788        role = self._completer.completionRole()
789        items = [toString(model.index(i, column).data(role))
790                 for i in range(model.rowCount())]
791
792        if not items:
793            return ""
794
795        first = min(items)
796        last = max(items)
797        for i, c in enumerate(first):
798            if c != last[i]:
799                return first[:i]
800
801        return first
802
803    def _stopCompletion(self):
804        self._completionState = _CompletionState(-1, -1, -1)
805        if self._completer.popup().isVisible():
806            self._completer.popup().hide()
807
808    def _textChanged(self):
809        items = self._getItems()
810        if self._items != items:
811            self._items = items
812            self.itemsChanged.emit(items)
813
814    def _getItems(self):
[1858]815        """
816        Return the current items (a list of strings).
817
818        .. note:: The inline completion text is not included.
819
820        """
[1853]821        text = unicode(self.toPlainText())
822        if self._completionState[0] != -1:
[1858]823            # Remove the inline completion text
[1853]824            _, pos, end = self._completionState
825            text = text[:pos] + text[end:]
826        return [item for item in text.split() if item.strip()]
827
[1846]828
829class NameHighlight(QSyntaxHighlighter):
830    def __init__(self, parent=None, **kwargs):
[1853]831        super(NameHighlight, self).__init__(parent, **kwargs)
[1846]832
833        self._names = set()
834
835        self._format = QTextCharFormat()
836        self._format.setForeground(Qt.blue)
837
838        self._unrecognized_format = QTextCharFormat()
839#         self._unrecognized_format.setFontStrikeOut(True)
840
841    def setNames(self, names):
842        self._names = set(names)
843        self.rehighlight()
844
845    def names(self):
846        return set(self._names)
847
848    def highlightBlock(self, text):
849        text = unicode(text)
850        pattern = re.compile(r"\S+")
851        for match in pattern.finditer(text):
852            name = text[match.start(): match.end()]
853            match_len = match.end() - match.start()
854
855            if not name.strip():
856                continue
857
858            if name in self._names:
859                format = self._format
860            else:
861                format = self._unrecognized_format
862
863            self.setFormat(match.start(), match_len, format)
864
865
[1850]866@contextmanager
867def signals_blocked(obj):
868    blocked = obj.signalsBlocked()
869    obj.blockSignals(True)
870    try:
871        yield
872    finally:
873        obj.blockSignals(blocked)
874
875
[1846]876class ListCompleter(QCompleter):
[1858]877    """
878    A completer supporting selection of multiple list items.
879    """
[1850]880    activated = Signal(list)
[1846]881
882    def __init__(self, *args, **kwargs):
883        QCompleter.__init__(self, *args, **kwargs)
884
885        popup = QListView()
[1850]886        popup.setEditTriggers(QListView.NoEditTriggers)
[1846]887        popup.setSelectionMode(QListView.ExtendedSelection)
[1850]888
[1846]889        self.setPopup(popup)
890
891    def setPopup(self, popup):
892        QCompleter.setPopup(self, popup)
893
[1850]894        popup.viewport().installEventFilter(self)
895        popup.doubleClicked.connect(self._complete)
[1846]896
[1850]897    def eventFilter(self, receiver, event):
898        if event.type() == QEvent.KeyPress and receiver is self.popup():
899            if event.key() in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab]:
900                self._complete()
901                return True
902
903        elif event.type() == QEvent.MouseButtonRelease and \
904                receiver is self.popup().viewport():
905            # Process the event without emitting 'clicked', ... signal to
906            # override the default QCompleter behavior
907            with signals_blocked(self.popup()):
908                QApplication.sendEvent(self.popup(), event)
909                return True
910
911        return QCompleter.eventFilter(self, receiver, event)
912
913    def _complete(self):
[1846]914        selection = self.popup().selectionModel().selection()
915        indexes = selection.indexes()
916
917        items = [toString(index.data(self.completionRole()))
918                 for index in indexes]
919
[1850]920        if self.popup().isVisible():
921            self.popup().hide()
922
[1853]923        if items:
924            self.activated.emit(items)
[1846]925
926
927# All control character categories.
928_control = set(["Cc", "Cf", "Cs", "Co", "Cn"])
929
930
931def is_printable(unichar):
932    """
933    Return True if the unicode character `unichar` is a printable character.
934    """
935    return unicodedata.category(unichar) not in _control
936
937
938def test():
939    app = QApplication([])
940    w = OWSelectGenes()
941    data = Orange.data.Table("brown-selected")
[1858]942    w.setData(data)
[1882]943    w.setGeneSubset(Orange.data.Table(data[:10]))
[1846]944    w.show()
945    app.exec_()
[1857]946    w.saveSettings()
[1846]947    w.deleteLater()
948    del w
949    app.processEvents()
950
951if __name__ == "__main__":
952    test()
Note: See TracBrowser for help on using the repository browser.