source: orange-bioinformatics/orangecontrib/bio/widgets/OWSelectGenes.py @ 1884:bbd363ba9498

Revision 1884:bbd363ba9498, 30.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Added option to copy/append the names from "Gene Subset" input to saved selections.

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