Commits

Flashpoint committed 36c847a Merge

Merged biolab/orange-bioinformatics into default

Comments (0)

Files changed (6)

_bioinformatics/obiGeneSetSig.py

         gene_sets = select_genesets(nm, self.gene_sets, self.min_size, self.max_size, self.min_part)
 
         #build a new domain
+        print "WHOLE"
         newfeatures = self.build_features(data, gene_sets)
         newdomain = Orange.data.Domain(newfeatures, data.domain.class_var)
 
                 cvi = self.cv(data)
             data_cv = [ [] for _ in range(len(data)) ]
             for f in set(cvi):
+                print "FOLD", f
                 learn = data.select(cvi, f, negate=True)
                 test = data.select(cvi, f)
                 lf = self.build_features(learn, gene_sets)
                 for ex, pos in \
                     zip(trans_test, [ i for i,n in enumerate(cvi) if n == f ]):
                     data_cv[pos] = ex.native(0)
+            print data_cv[0]
             return Orange.data.Table(newdomain, data_cv)
 
     def build_features(self, data, gene_sets):
         else:
             break
         
-    return sortedinds[:bg] #FIXED - one too many was taken
+    return sortedinds[:bg]
 
 class CORGs(ParametrizedTransformation):
     """
 
         ind_names = dict( (a,b) for b,a in name_ind.items() )
         selected_genes = sorted(set([to_geneset[ind_names[i]] for i in indices]))
-            
+    
         def t(ex, w, corg=selected_genes): #copy od the data
             nm2, name_ind2, genes2 = self._match_instance(ex, corg, None)
             exvalues = [ vou(ex, gn, name_ind2) for gn in genes2 ]
 
     ass = LLR(data, matcher=matcher, gene_sets=gsets, class_values=choosen_cv, min_part=0.0, normalize=True)
     #ass = LLR_slow(data, matcher=matcher, gene_sets=gsets, class_values=choosen_cv, min_part=0.0)
+    ass = CORGs(data, matcher=matcher, gene_sets=gsets, class_values=choosen_cv, min_part=0.0, cv=True)
     ar = to_old_dic(ass.domain, data[:5])
     pp2(ar)

_bioinformatics/widgets/OWFeatureSelection.py

 """
-<name>Gene Selection</name>
+<name>Differential expression</name>
 <description>Gene differential expression scoring and selection.</description>
 <priority>1010</priority>
 <icon>icons/GeneSelection.svg</icon>
         if self.data is not None  and \
                 not (self.attribute_targets or self.class_targets):
             # If both attr. labels and classes are missing, show an error
-            self.error(1, "Cannot compute gene scores! Gene Selection widget "
+            self.error(1, "Cannot compute gene scores! Differential expression widget "
                           "requires a data-set with a discrete class variable "
                           "or attribute labels!")
             self.data = None

_bioinformatics/widgets/OWPIPAx.py

             # Sort attributes
             sortOrder = self.columnsSortingWidget.sortingOrder
 
+            all_values = defaultdict(set)
+            for at in table.domain.attributes:
+                atts = at.attributes
+                for name in sortOrder:
+                    all_values[name].add(atts.get(reverse_header_dict[name], ""))
+
+            isnum = {}
+            for at, vals in all_values.items():
+                vals = filter(None, vals)
+                try:
+                    for a in vals:
+                        float(a)
+                    isnum[at] = True
+                except:
+                    isnum[at] = False
+
+            def optfloat(x, at):
+                if x == "":
+                    return ""
+                else:
+                    return float(x) if isnum[at] else x
+
             def sorting_key(attr):
                 atts = attr.attributes
-                return tuple([atts.get(reverse_header_dict[name], "") \
+                return tuple([optfloat(atts.get(reverse_header_dict[name], ""), name) \
                               for name in sortOrder])
 
             attributes = sorted(table.domain.attributes,

_bioinformatics/widgets/OWSelectGenes.py

 import re
 import unicodedata
+from collections import defaultdict, namedtuple
+from xml.sax.saxutils import escape
+
+from contextlib import contextmanager
 
 from PyQt4.QtGui import (
     QLabel, QWidget, QPlainTextEdit, QSyntaxHighlighter, QTextCharFormat,
     QTextCursor, QCompleter, QStringListModel, QListView
 )
 
-from PyQt4.QtCore import Qt, pyqtSignal as Signal
+from PyQt4.QtCore import Qt, QEvent, pyqtSignal as Signal
 
 import Orange
 
 DESCRIPTION = "Select a specified subset of the input genes."
 ICON = "icons/SelectGenes.svg"
 
-INPUTS = [("Data", Orange.data.Table, "set_data")]
+INPUTS = [("Data", Orange.data.Table, "setData")]
 OUTPUTS = [("Selected Data", Orange.data.Table)]
 
 
+def toString(variant):
+    if isinstance(variant, QVariant):
+        return unicode(variant.toString())
+    else:
+        return unicode(variant)
+
+
+def toBool(variant):
+    if isinstance(variant, QVariant):
+        return bool(variant.toPyObject())
+    else:
+        return bool(variant)
+
+
+class SaveSlot(QStandardItem):
+    ModifiedRole = next(OWGUI.OrangeUserRole)
+
+    def __init__(self, name, savedata=None, modified=False):
+        super(SaveSlot, self).__init__(name)
+
+        self.savedata = savedata
+        self.modified = modified
+        self.document = None
+
+    @property
+    def name(self):
+        return unicode(self.text())
+
+    @property
+    def modified(self):
+        return toBool(self.data(SaveSlot.ModifiedRole))
+
+    @modified.setter
+    def modified(self, state):
+        self.setData(bool(state), SaveSlot.ModifiedRole)
+
+
+class SavedSlotDelegate(QStyledItemDelegate):
+
+    def paint(self, painter, option, index):
+        option = QStyleOptionViewItemV4(option)
+        self.initStyleOption(option, index)
+
+        modified = toBool(index.data(SaveSlot.ModifiedRole))
+        if modified:
+            option.palette.setColor(QPalette.Text, QColor(Qt.red))
+            option.palette.setColor(QPalette.Highlight, QColor(Qt.darkRed))
+            option.text = "*" + option.text
+
+        if option.widget:
+            widget = option.widget
+            style = widget.style()
+        else:
+            widget = None
+            style = QApplication.style()
+
+        style.drawControl(QStyle.CE_ItemViewItem, option, painter, widget)
+
+
 class OWSelectGenes(OWWidget):
 
     contextHandlers = {
         "": DomainContextHandler(
-            "", ["geneIndex", "selection"]
+            "", ["geneIndex"]
         )
     }
 
-    settingsList = ["autoCommit"]
+    settingsList = ["autoCommit", "preserveOrder", "savedSelections",
+                    "selectedSelectionIndex"]
 
     def __init__(self, parent=None, signalManager=None, title=NAME):
         OWWidget.__init__(self, parent, signalManager, title,
                           wantMainArea=False)
 
-        self.selection = []
         self.geneIndex = None
         self.autoCommit = False
+        self.preserveOrder = True
+        self.savedSelections = [
+            ("Example", ["MRE11A", "RAD51", "MLH1", "MSH2", "DMC1"])
+        ]
+
+        self.selectedSelectionIndex = -1
 
         self.loadSettings()
 
         # Input variables that could contain names
         self.variables = VariableListModel()
         # All gene names from the input (in self.geneIndex column)
-        self.geneNames = set([])
+        self.geneNames = []
         # Output changed flag
         self._changedFlag = False
+        self.data = None
+        # Current gene names
+        self.selection = []
 
         box = OWGUI.widgetBox(self.controlArea, "Gene Attribute")
         self.attrsCombo = OWGUI.comboBox(
         self.entryField = ListTextEdit(box)
         self.entryField.setTabChangesFocus(True)
         self.entryField.setToolTip("Enter selected gene names")
-        self.entryField.textChanged.connect(self._textChanged)
+        self.entryField.setDocument(self._createDocument())
+        self.entryField.itemsChanged.connect(self._onItemsChanged)
 
         box.layout().addWidget(self.entryField)
 
-        completer = QCompleter(self)
+        completer = ListCompleter()
         completer.setCompletionMode(QCompleter.PopupCompletion)
         completer.setCaseSensitivity(Qt.CaseInsensitive)
+        completer.setMaxVisibleItems(10)
         completer.popup().setAlternatingRowColors(True)
+        completer.setModel(QStringListModel([], self))
+
         self.entryField.setCompleter(completer)
 
-        self.hightlighter = NameHighlight(self.entryField.document())
+        box = OWGUI.widgetBox(self.controlArea, "Saved Selections")
+        box.layout().setSpacing(1)
+
+        self.selectionsModel = QStandardItemModel()
+        self.selectionsView = QListView()
+        self.selectionsView.setAlternatingRowColors(True)
+        self.selectionsView.setModel(self.selectionsModel)
+        self.selectionsView.setItemDelegate(SavedSlotDelegate(self))
+        self.selectionsView.selectionModel().selectionChanged.connect(
+            self._onSelectedSaveSlotChanged
+        )
+
+        box.layout().addWidget(self.selectionsView)
+
+        self.actionSave = QAction("Save", self)
+        self.actionAdd = QAction("+", self)
+        self.actionRemove = QAction("-", self)
+
+        toolbar = QFrame()
+        layout = QHBoxLayout()
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.setSpacing(1)
+
+        def button(action):
+            b = QToolButton()
+            b.setDefaultAction(action)
+            return b
+
+        b = button(self.actionAdd)
+        layout.addWidget(b)
+
+        b = button(self.actionSave)
+        b.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+        layout.addWidget(b, stretch=10)
+
+        b = button(self.actionRemove)
+        layout.addWidget(b)
+
+        toolbar.setLayout(layout)
+
+        box.layout().addWidget(toolbar)
+
+        self.actionSave.triggered.connect(self.saveSelection)
+        self.actionAdd.triggered.connect(self.addSelection)
+        self.actionRemove.triggered.connect(self.removeSelection)
 
         box = OWGUI.widgetBox(self.controlArea, "Output")
-
+        OWGUI.checkBox(box, self, "preserveOrder", "Preserve input order",
+                       tooltip="Preserve the order of the input data "
+                               "instances.",
+                       callback=self.invalidateOutput)
         cb = OWGUI.checkBox(box, self, "autoCommit", "Auto commit")
         button = OWGUI.button(box, self, "Commit", callback=self.commit)
 
         OWGUI.setStopper(self, button, cb, "_changedFlag", self.commit)
 
-    def set_data(self, data):
+        # restore saved selections model.
+        for name, names in self.savedSelections:
+            item = SaveSlot(name, names)
+            self.selectionsModel.appendRow([item])
+
+        if self.selectedSelectionIndex != -1:
+            self.selectionsView.selectionModel().select(
+                self.selectionsModel.index(self.selectedSelectionIndex, 0),
+                QItemSelectionModel.Select
+            )
+        self._updateActions()
+
+    def setData(self, data):
         """
         Set the input data.
         """
             attrs = gene_candidates(data)
             self.variables[:] = attrs
             self.attrsCombo.setCurrentIndex(0)
-            self.geneIndex = 0
-            self.selection = []
+            if attrs:
+                self.geneIndex = 0
+            else:
+                self.geneIndex = -1
+                self.warning(0, "No suitable columns for gene names.")
         else:
             self.variables[:] = []
             self.geneIndex = -1
-            self.warning(0, "No suitable columns for gene names.")
 
         self._changedFlag = True
         self._updateCompletionModel()
 
         self.openContext("", data)
 
-        self.entryField.setPlainText(" ".join(self.selection))
-
         self.commit()
 
     @property
     def geneVar(self):
+        """
+        Current gene attribute or None if none available.
+        """
         if self.data is not None and self.geneIndex >= 0:
             return self.variables[self.geneIndex]
         else:
             self._changedFlag = True
 
     def commit(self):
+        """
+        Send the selected data subset to the output.
+        """
         gene = self.geneVar
 
         if gene is not None:
-            selection = set(self.selection)
-
-            sel = [inst for inst in self.data
-                   if str(inst[gene]) in selection]
-
-            if sel:
-                data = Orange.data.Table(self.data.domain, sel)
-            else:
-                data = Orange.data.Table(self.data.domain)
-
+            data = select_by_genes(self.data, gene,
+                                   gene_list=self.selection,
+                                   preserve_order=self.preserveOrder)
         else:
             data = None
 
             names = []
 
         self.geneNames = names
-        self._completerModel = QStringListModel(names)
-        self.entryField.completer().setModel(self._completerModel)
-        self.hightlighter.setNames(names)
+        self.entryField.completer().model().setStringList(sorted(set(names)))
+        self.entryField.document().highlighter.setNames(names)
 
     def _onGeneIndexChanged(self):
         self._updateCompletionModel()
         self.invalidateOutput()
 
-    def _textChanged(self):
-        names = self.entryField.list()
+    def _onItemsChanged(self, names):
         selection = set(names).intersection(self.geneNames)
         curr_selection = set(self.selection).intersection(self.geneNames)
+
+        self.selection = names
+
         if selection != curr_selection:
-            self.selection = names
             self.invalidateOutput()
+            to_complete = sorted(set(self.geneNames) - set(names))
+            self.entryField.completer().model().setStringList(to_complete)
+
+        item = self._selectedSaveSlot()
+        if item:
+            item.modified = item.savedata != names
+
+    def _selectedSaveSlot(self):
+        """
+        Return the current selected saved selection slot.
+        """
+        indexes = self.selectionsView.selectedIndexes()
+        if indexes:
+            return self.selectionsModel.item(indexes[0].row())
+        else:
+            return None
+
+    def saveSelection(self):
+        """
+        Save (update) the items in the current selected selection.
+        """
+        item = self._selectedSaveSlot()
+        if item:
+            item.savedata = self.entryField.items()
+            item.modified = False
+
+    def addSelection(self, name=None):
+        """
+        Add a new saved selection entry initialized by the current items.
+
+        The new slot will be selected.
+
+        """
+        item = SaveSlot(name or "New selection")
+        item.savedata = self.entryField.items()
+        self.selectionsModel.appendRow([item])
+        self.selectionsView.setCurrentIndex(item.index())
+
+        if not name:
+            self.selectionsView.edit(item.index())
+
+    def removeSelection(self):
+        """
+        Remove the current selected save slot.
+        """
+        item = self._selectedSaveSlot()
+        if item:
+            self.selectionsModel.removeRow(item.row())
+
+    def _onSelectedSaveSlotChanged(self):
+        item = self._selectedSaveSlot()
+        if item:
+            if not item.document:
+                item.document = self._createDocument()
+                if item.savedata:
+                    item.document.setPlainText(" ".join(item.savedata))
+
+            item.document.highlighter.setNames(self.geneNames)
+
+            self.entryField.setDocument(item.document)
+
+        self._updateActions()
+
+    def _createDocument(self):
+        """
+        Create and new QTextDocument instance for editing gene names.
+        """
+        doc = QTextDocument(self)
+        doc.setDocumentLayout(QPlainTextDocumentLayout(doc))
+        doc.highlighter = NameHighlight(doc)
+        return doc
+
+    def _updateActions(self):
+        """
+        Update the Save/remove action enabled state.
+        """
+        selected = bool(self._selectedSaveSlot())
+        self.actionRemove.setEnabled(selected)
+        self.actionSave.setEnabled(selected)
+
+    def getSettings(self, *args, **kwargs):
+        # copy the saved selections model back to widget settings.
+        selections = []
+        for i in range(self.selectionsModel.rowCount()):
+            item = self.selectionsModel.item(i)
+            selections.append((item.name, item.savedata))
+        self.savedSelections = selections
+
+        item = self._selectedSaveSlot()
+        if item is None:
+            self.selectedSelectionIndex = -1
+        else:
+            self.selectedSelectionIndex = item.row()
+
+        return OWWidget.getSettings(self, *args, **kwargs)
+
+    def sendReport(self):
+        report = []
+        if self.data is not None:
+            report.append("%i instances on input." % len(self.data))
+        else:
+            report.append("No data on input.")
+
+        if self.geneVar is not None:
+            report.append("Gene names taken from %r attribute." %
+                          escape(self.geneVar.name))
+
+        self.reportSection("Input")
+        self.startReportList()
+        for item in report:
+            self.addToReportList(item)
+        self.finishReportList()
+        self.reportRaw(
+            "<p>Gene Selection: %s</p>" %
+            escape(" ".join(self.selection))
+        )
+        self.reportSettings(
+            "Settings",
+            [("Preserve order", self.preserveOrder)]
+        )
 
 
 def is_string(feature):
 
 
 def domain_variables(domain):
+    """
+    Return all feature descriptors from the domain.
+    """
     vars = (domain.features +
             domain.class_vars +
             domain.getmetas().values())
 
 
 def gene_candidates(data):
+    """
+    Return features that could contain gene names.
+    """
     vars = domain_variables(data.domain)
     vars = filter(is_string, vars)
     return vars
 
 
+def select_by_genes(data, gene_feature, gene_list, preserve_order=True):
+    if preserve_order:
+        selection = set(gene_list)
+        sel = [inst for inst in data
+               if str(inst[gene_feature]) in selection]
+    else:
+        by_genes = defaultdict(list)
+        for inst in data:
+            by_genes[str(inst[gene_feature])].append(inst)
+
+        sel = []
+        for name in gene_list:
+            sel.extend(by_genes.get(name, []))
+
+    if sel:
+        data = Orange.data.Table(data.domain, sel)
+    else:
+        data = Orange.data.Table(data.domain)
+
+    return data
+
+
+_CompletionState = namedtuple(
+    "_CompletionState",
+    ["start",  # completion prefix start position
+     "pos",  # cursor position
+     "anchor"]  # anchor position (inline completion end)
+)
+
+
 class ListTextEdit(QPlainTextEdit):
-    listChanged = Signal()
+    """
+    A text editor specialized for editing a list of items.
+    """
+    #: Emitted when the list items change.
+    itemsChanged = Signal(list)
 
     def __init__(self, parent=None, **kwargs):
         QPlainTextEdit.__init__(self, parent, **kwargs)
 
+        self._items = None
         self._completer = None
+        self._completionState = _CompletionState(-1, -1, -1)
+
+        self.cursorPositionChanged.connect(self._cursorPositionChanged)
+        self.textChanged.connect(self._textChanged)
 
     def setCompleter(self, completer):
         """
         """
         if self._completer is not None:
             self._completer.setWidget(None)
-            if isinstance(self._completer, ListCompleter):
-                self._completer.activatedList.disconnect(self._insertCompletion)
-            else:
-                self._completer.activated.disconnect(self._insertCompletion)
+            self._completer.activated.disconnect(self._insertCompletion)
 
         self._completer = completer
 
         if self._completer:
             self._completer.setWidget(self)
-            if isinstance(self._completer, ListCompleter):
-                self._completer.activatedList.connect(self._insertCompletion)
-            else:
-                self._completer.activated.connect(self._insertCompletion)
+            self._completer.activated.connect(self._insertCompletion)
 
     def completer(self):
         """
         """
         return self._completer
 
-    def setList(self, list):
-        text = " ".join(list)
+    def setItems(self, items):
+        text = " ".join(items)
         self.setPlainText(text)
 
-    def list(self):
-        return [name for name in unicode(self.toPlainText()).split()
-                if name.strip()]
+    def items(self):
+        if self._items is None:
+            self._items = self._getItems()
+        return self._items
 
     def keyPressEvent(self, event):
+        # TODO: in Qt 4.8 QPlainTextEdit uses inputMethodEvent for
+        # non-ascii input
+
         if self._completer.popup().isVisible():
             if event.key() in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape,
                                Qt.Key_Tab, Qt.Key_Backtab]:
             return
 
         text = unicode(self.toPlainText())
-        pos = self.textCursor().position()
+        cursor = self.textCursor()
+        pos = cursor.position()
 
         if pos == len(text) or not(text[pos].strip()):
-            # At end of text or whitespace
-            # TODO: Match all whitespace characters.
-            start_sp = text.rfind(" ", 0, pos) + 1
-            start_n = text.rfind("\n", 0, pos) + 1
-            start = max(start_sp, start_n)
+            # cursor is at end of text or whitespace
+            # find the beginning of the current word
+            whitespace = " \t\n\r\f\v"
+            start = max([text.rfind(c, 0, pos) for c in whitespace]) + 1
 
             prefix = text[start:pos]
 
                     rect.setWidth(popup.sizeHintForColumn(0) +
                                   popup.verticalScrollBar().sizeHint().width())
 
+                # Popup the completer list
                 self._completer.complete(rect)
 
+                # Inline completion of a common prefix
+                inline = self._commonCompletionPrefix()
+                inline = inline[len(prefix):]
+
+                self._completionState = \
+                    _CompletionState(start, pos, pos + len(inline))
+
+                cursor.insertText(inline)
+                cursor.setPosition(pos, QTextCursor.KeepAnchor)
+                self.setTextCursor(cursor)
+
             elif self._completer.popup().isVisible():
-                self._completer.popup().hide()
+                self._stopCompletion()
+
+    def _cursorPositionChanged(self):
+        cursor = self.textCursor()
+        pos = cursor.position()
+        start, _, _ = self._completionState
+
+        if start == -1:
+            # completion not in progress
+            return
+
+        if pos <= start:
+            # cursor moved before the start of the prefix
+            self._stopCompletion()
+            return
+
+        text = unicode(self.toPlainText())
+        # Find the end of the word started by completion prefix
+        word_end = len(text)
+        for i in range(start, len(text)):
+            if text[i] in " \t\n\r\f\v":
+                word_end = i
+                break
+
+        if pos > word_end:
+            # cursor moved past the word boundary
+            self._stopCompletion()
+
+        # TODO: Update the prefix when moving the cursor
+        # inside the word
 
     def _insertCompletion(self, item):
-        completion = unicode(item)
-        prefix = self._completer.completionPrefix()
+        if isinstance(item, list):
+            completion = " ".join(item)
+        else:
+            completion = unicode(item)
+
+        start, _, end = self._completionState
+
+        self._stopCompletion()
 
         cursor = self.textCursor()
-        # Replace the prefix with the full completion (correcting for the
-        # case-insensitive search).
-        cursor.setPosition(cursor.position() - len(prefix),
-                           QTextCursor.KeepAnchor)
+        # Replace the prefix+inline with the full completion
+        # (correcting for the case-insensitive search).
+        cursor.setPosition(min(end, self.document().characterCount()))
+        cursor.setPosition(start, QTextCursor.KeepAnchor)
 
-        cursor.insertText(completion)
+        cursor.insertText(completion + " ")
+
+    def _commonCompletionPrefix(self):
+        """
+        Return the common prefix of items in the current completion model.
+        """
+        model = self._completer.completionModel()
+        column = self._completer.completionColumn()
+        role = self._completer.completionRole()
+        items = [toString(model.index(i, column).data(role))
+                 for i in range(model.rowCount())]
+
+        if not items:
+            return ""
+
+        first = min(items)
+        last = max(items)
+        for i, c in enumerate(first):
+            if c != last[i]:
+                return first[:i]
+
+        return first
+
+    def _stopCompletion(self):
+        self._completionState = _CompletionState(-1, -1, -1)
+        if self._completer.popup().isVisible():
+            self._completer.popup().hide()
+
+    def _textChanged(self):
+        items = self._getItems()
+        if self._items != items:
+            self._items = items
+            self.itemsChanged.emit(items)
+
+    def _getItems(self):
+        """
+        Return the current items (a list of strings).
+
+        .. note:: The inline completion text is not included.
+
+        """
+        text = unicode(self.toPlainText())
+        if self._completionState[0] != -1:
+            # Remove the inline completion text
+            _, pos, end = self._completionState
+            text = text[:pos] + text[end:]
+        return [item for item in text.split() if item.strip()]
 
 
 class NameHighlight(QSyntaxHighlighter):
     def __init__(self, parent=None, **kwargs):
-        super(NameHighlight, self).__init__(parent)
+        super(NameHighlight, self).__init__(parent, **kwargs)
 
         self._names = set()
 
             self.setFormat(match.start(), match_len, format)
 
 
-def toString(variant):
-    if isinstance(variant, QVariant):
-        return unicode(variant.toString())
-    else:
-        return unicode(variant)
+@contextmanager
+def signals_blocked(obj):
+    blocked = obj.signalsBlocked()
+    obj.blockSignals(True)
+    try:
+        yield
+    finally:
+        obj.blockSignals(blocked)
 
 
 class ListCompleter(QCompleter):
-    activatedList = Signal(list)
+    """
+    A completer supporting selection of multiple list items.
+    """
+    activated = Signal(list)
 
     def __init__(self, *args, **kwargs):
         QCompleter.__init__(self, *args, **kwargs)
 
         popup = QListView()
+        popup.setEditTriggers(QListView.NoEditTriggers)
         popup.setSelectionMode(QListView.ExtendedSelection)
+
         self.setPopup(popup)
 
     def setPopup(self, popup):
         QCompleter.setPopup(self, popup)
 
-        popup.selectionModel().selectionChanged.connect(
-            self._completionSelected)
+        popup.viewport().installEventFilter(self)
+        popup.doubleClicked.connect(self._complete)
 
-    def _completionSelected(self, selected, deselected):
+    def eventFilter(self, receiver, event):
+        if event.type() == QEvent.KeyPress and receiver is self.popup():
+            if event.key() in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab]:
+                self._complete()
+                return True
+
+        elif event.type() == QEvent.MouseButtonRelease and \
+                receiver is self.popup().viewport():
+            # Process the event without emitting 'clicked', ... signal to
+            # override the default QCompleter behavior
+            with signals_blocked(self.popup()):
+                QApplication.sendEvent(self.popup(), event)
+                return True
+
+        return QCompleter.eventFilter(self, receiver, event)
+
+    def _complete(self):
         selection = self.popup().selectionModel().selection()
         indexes = selection.indexes()
 
         items = [toString(index.data(self.completionRole()))
                  for index in indexes]
 
-        self.activatedList.emit(items)
+        if self.popup().isVisible():
+            self.popup().hide()
+
+        if items:
+            self.activated.emit(items)
 
 
 # All control character categories.
     app = QApplication([])
     w = OWSelectGenes()
     data = Orange.data.Table("brown-selected")
-    w.set_data(data)
+    w.setData(data)
     w.show()
     app.exec_()
+    w.saveSettings()
     w.deleteLater()
     del w
     app.processEvents()

_bioinformatics/widgets/icons/SelectGenes.svg

+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xml:space="preserve" enable-background="new 0 0 48 48" y="0px" x="0px" width="48px" height="48px" viewBox="0 0 48 48">
+<g transform="matrix(0.85314283,0.76541739,-0.60993003,0.70357337,20.0103,-23.565583)">
+	<path fill="#333" d="M31.498,17.374c-1.463-6.536-0.359-10.374-0.359-10.374s-3.279,1.797-3.033,5.549c0.478,7.261,4.177,11.802,1.185,16.311-0.001-0.001,4.681-0.431,2.207-11.486z"/>
+	<path fill="#b3b3b3" d="m38.542,18.24s-8.642-8.644-8.644-8.645c-0.043-0.044-0.301,0.53-0.317,0.593-0.054,0.196-0.11,0.32-0.126,0.521-0.007,0.092-0.029,0.438,0.039,0.513l8.032,8.033,1.016-1.015z"/>
+	<path fill="#333" d="m17.54,41s2.911-2.115,2.632-7.022c-0.278-4.908-3.181-13.991-0.309-16.116l0.943-1.151s-4.306,2.303-4.225,8.25c0.083,5.945,2.594,8.618,0.959,16.039z"/>
+	<path fill="#b3b3b3" d="m22.465,17.345,8.604,8.606c0.125-0.318,0.228-0.648,0.264-0.99,0.021-0.209,0.057-0.722-0.113-0.89l-7.739-7.742-1.016,1.016z"/>
+	<path fill="#b3b3b3" d="m17.739,23.548,7.363,7.396,1.016-1.016-8.244-8.251c-0.161,0.625-0.179,1.231-0.135,1.871z"/>
+	<path fill="#b3b3b3" d="m18.654,19.001,10.458,10.473,1.016-1.016-10.653-10.671-0.06,0.079s-0.644,0.594-0.771,1.033c-0.005,0.019,0.01,0.102,0.01,0.102z"/>
+	<path fill="#666" d="m30.745,16.382c6.357,2.114,10.375,0.359,10.375,0.359s-1.799,3.279-5.548,3.031c-1.067-0.07-2.131-0.204-3.174-0.444-0.765-0.176-1.506-0.411-2.242-0.682-0.624-0.229-1.238-0.486-1.869-0.692-0.678-0.221-1.38-0.39-2.086-0.476-1.251-0.152-2.543-0.204-3.794-0.065-0.774,0.086-1.581,0.286-2.294,0.622-0.531,0.249-1.012,0.594-1.467,0.962-0.114,0.093-0.229,0.187-0.342,0.279-0.074,0.061-0.255,0.156-0.297,0.241,0.001,0.001,3.095-6.343,12.738-3.135z"/>
+	<path fill="#b3b3b3" d="m10.202,29.099s8.587,8.589,8.604,8.605c0.104-0.066,0.18-0.287,0.219-0.406,0.074-0.224,0.117-0.612,0.121-0.848,0.003-0.144,0.039-0.416-0.076-0.529l-7.852-7.839-1.016,1.017z"/>
+	<path fill="#666" d="m6.88,30.581s2.115-2.912,7.022-2.635c4.906,0.279,12.258,2.691,15.72,0.561,0.716-0.44,1.795-1.396,1.795-1.396s-2.382,4.531-8.498,4.428c-5.944-0.101-8.616-2.595-16.039-0.958z"/>
+</g><g transform="matrix(0.38580747,0.76586005,-0.71780818,0.07442885,25.895552,14.459649)">
+	<path fill="#333" d="M31.498,17.374c-1.463-6.536-0.359-10.374-0.359-10.374s-3.279,1.797-3.033,5.549c0.478,7.261,4.177,11.802,1.185,16.311-0.001-0.001,4.681-0.431,2.207-11.486z"/>
+	<path fill="#b3b3b3" d="m38.542,18.24s-8.642-8.644-8.644-8.645c-0.043-0.044-0.301,0.53-0.317,0.593-0.054,0.196-0.11,0.32-0.126,0.521-0.007,0.092-0.029,0.438,0.039,0.513l8.032,8.033,1.016-1.015z"/>
+	<path fill="#333" d="m17.54,41s2.911-2.115,2.632-7.022c-0.278-4.908-3.181-13.991-0.309-16.116l0.943-1.151s-4.306,2.303-4.225,8.25c0.083,5.945,2.594,8.618,0.959,16.039z"/>
+	<path fill="#b3b3b3" d="m22.465,17.345,8.604,8.606c0.125-0.318,0.228-0.648,0.264-0.99,0.021-0.209,0.057-0.722-0.113-0.89l-7.739-7.742-1.016,1.016z"/>
+	<path fill="#b3b3b3" d="m17.739,23.548,7.363,7.396,1.016-1.016-8.244-8.251c-0.161,0.625-0.179,1.231-0.135,1.871z"/>
+	<path fill="#b3b3b3" d="m18.654,19.001,10.458,10.473,1.016-1.016-10.653-10.671-0.06,0.079s-0.644,0.594-0.771,1.033c-0.005,0.019,0.01,0.102,0.01,0.102z"/>
+	<path fill="#666" d="m30.745,16.382c6.357,2.114,10.375,0.359,10.375,0.359s-1.799,3.279-5.548,3.031c-1.067-0.07-2.131-0.204-3.174-0.444-0.765-0.176-1.506-0.411-2.242-0.682-0.624-0.229-1.238-0.486-1.869-0.692-0.678-0.221-1.38-0.39-2.086-0.476-1.251-0.152-2.543-0.204-3.794-0.065-0.774,0.086-1.581,0.286-2.294,0.622-0.531,0.249-1.012,0.594-1.467,0.962-0.114,0.093-0.229,0.187-0.342,0.279-0.074,0.061-0.255,0.156-0.297,0.241,0.001,0.001,3.095-6.343,12.738-3.135z"/>
+	<path fill="#b3b3b3" d="m10.202,29.099s8.587,8.589,8.604,8.605c0.104-0.066,0.18-0.287,0.219-0.406,0.074-0.224,0.117-0.612,0.121-0.848,0.003-0.144,0.039-0.416-0.076-0.529l-7.852-7.839-1.016,1.017z"/>
+	<path fill="#666" d="m6.88,30.581s2.115-2.912,7.022-2.635c4.906,0.279,12.258,2.691,15.72,0.561,0.716-0.44,1.795-1.396,1.795-1.396s-2.382,4.531-8.498,4.428c-5.944-0.101-8.616-2.595-16.039-0.958z"/>
+</g><g transform="matrix(0.90443566,0.42661006,-0.42661006,0.90443566,18.271744,-19.892411)">
+	<path fill="#333" d="m43.684,29.709c-0.851-1.15-1.752-2.261-2.665-3.359-0.378-0.452-0.763-0.898-1.146-1.349-0.265-0.312-0.506-0.661-0.856-0.867-0.076-0.045-0.158-0.086-0.246-0.112-0.457-0.153-0.978-0.133-1.417,0.066-0.06,0.025-0.117,0.056-0.17,0.09-0.173,0.114-0.298,0.271-0.368,0.479-0.109,0.325,0.021,0.71-0.08,1.016-0.031,0.091-0.081,0.157-0.145,0.197-0.287,0.19-0.851-0.057-1.148-0.039-0.275,0.013-0.538,0.104-0.764,0.253-0.16,0.105-0.301,0.241-0.413,0.4-0.152,0.22-0.258,0.521-0.263,0.812-0.004,0.202,0.186,0.592-0.014,0.724-0.018,0.012-0.038,0.019-0.061,0.028-0.207,0.063-0.753-0.344-0.972-0.377-0.323-0.047-0.714-0.083-0.982,0.095-0.007,0.005-0.017,0.011-0.021,0.015-0.531,0.351-0.755,1.078-0.608,1.722,0.104,0.462,0.433,0.77,0.773,1.06,0.004,0.006,0.029,0.041,0.034,0.047-0.005-0.006,0.002,0.009-0.002,0.004-0.617-0.524-1.438-1.155-2.099-1.623-0.729-0.515-3.815-2.895-4.733-2.711-0.166,0.032-0.319,0.103-0.459,0.194-0.164,0.108-0.306,0.252-0.417,0.414-0.203,0.292-0.297,0.645-0.27,0.999,0.095,1.145,1.575,2.018,2.382,2.59,1.86,1.318,2.391,1.711,4.156,3.153,0.595,0.487,2.634,2.269,3.076,2.865-0.837,0.149-2.252,0.089-3.059,0.622-0.146,0.098-0.276,0.216-0.375,0.36-0.278,0.399-0.35,0.91-0.192,1.371,0.076,0.225,0.208,0.457,0.389,0.618,0.993,0.882,2.555,0.597,3.76,0.493,1.064-0.093,2.12-0.199,3.19-0.211,0.726-0.007,1.45,0.009,2.174-0.022,0.453-0.018,1.339,0.048,1.755-0.228,0.018-0.012,0.036-0.025,0.055-0.039,0.258-0.21,0.449-0.712,0.646-0.991,0.269-0.386,0.536-0.771,0.806-1.157,0.68-0.977,1.353-1.958,2.038-2.93,0.34-0.479,1.007-1.043,0.835-1.7-0.18-0.678-0.912-1.235-1.268-1.831-0.244-0.41-0.576-0.765-0.856-1.141z"/>
+	<path fill="#FFF" d="m45.819,30.87c-0.17-0.196-0.329-0.382-0.422-0.537-0.193-0.321-0.423-0.607-0.625-0.858-0.098-0.12-0.194-0.241-0.285-0.363-0.907-1.227-1.873-2.408-2.699-3.401-0.282-0.339-0.569-0.673-0.854-1.007l-0.301-0.351-0.142-0.174c-0.237-0.29-0.533-0.654-0.97-0.909-0.126-0.077-0.276-0.147-0.434-0.2-0.703-0.234-1.484-0.196-2.148,0.104-0.106,0.048-0.21,0.104-0.307,0.168-0.363,0.24-0.628,0.584-0.765,0.995-0.063,0.181-0.087,0.357-0.097,0.517-0.121-0.018-0.249-0.029-0.384-0.021-0.447,0.021-0.884,0.166-1.261,0.415-0.268,0.177-0.497,0.4-0.681,0.664-0.185,0.263-0.316,0.571-0.387,0.89-0.493-0.044-0.993,0.09-1.457,0.391-0.409,0.271-0.721,0.622-0.927,1.017-0.014-0.011-0.027-0.02-0.042-0.029-0.091-0.065-0.22-0.16-0.378-0.274-2.916-2.133-4.211-2.79-5.131-2.605-0.288,0.059-0.564,0.173-0.817,0.341-0.268,0.178-0.5,0.405-0.688,0.678-0.332,0.477-0.49,1.064-0.444,1.648,0.123,1.484,1.557,2.468,2.506,3.118,0.104,0.073,0.203,0.142,0.292,0.205,1.848,1.311,2.361,1.692,4.102,3.111,0.373,0.306,0.977,0.829,1.542,1.343-0.509,0.096-1.015,0.253-1.441,0.535-0.26,0.172-0.478,0.382-0.645,0.623-0.46,0.659-0.579,1.507-0.316,2.269,0.102,0.295,0.301,0.714,0.669,1.041,1.206,1.071,2.873,0.902,4.091,0.782,0.144-0.016,0.283-0.028,0.42-0.041l0.478-0.041c0.907-0.082,1.766-0.157,2.637-0.167,0.224-0.002,0.45-0.002,0.676-0.003,0.51,0.001,1.019,0.001,1.528-0.019,0.076-0.004,0.17-0.004,0.272-0.005,0.595-0.001,1.408-0.004,1.991-0.389,0.048-0.032,0.095-0.066,0.137-0.103,0.317-0.256,0.52-0.628,0.68-0.927,0.054-0.097,0.101-0.189,0.151-0.26l0.808-1.162,0.746-1.074c0.43-0.617,0.857-1.235,1.29-1.851,0.06-0.083,0.136-0.172,0.21-0.265,0.391-0.471,1.038-1.257,0.773-2.265-0.166-0.641-0.584-1.127-0.951-1.554zm-0.012,1.809c0.174,0.657-0.493,1.221-0.834,1.701-0.684,0.973-1.357,1.952-2.037,2.928-0.27,0.387-0.536,0.771-0.807,1.158-0.196,0.28-0.387,0.78-0.647,0.992-0.016,0.012-0.033,0.026-0.053,0.037-0.416,0.275-1.302,0.21-1.755,0.228-0.725,0.031-1.448,0.017-2.175,0.023-1.068,0.011-2.126,0.119-3.188,0.211-1.206,0.104-2.768,0.389-3.763-0.493-0.179-0.161-0.309-0.394-0.387-0.618-0.158-0.461-0.085-0.972,0.191-1.372,0.102-0.143,0.229-0.261,0.378-0.36,0.805-0.532,2.221-0.472,3.057-0.622-0.442-0.596-2.481-2.377-3.077-2.864-1.765-1.442-2.298-1.835-4.156-3.153-0.805-0.573-2.286-1.445-2.381-2.589-0.026-0.354,0.066-0.708,0.269-1,0.113-0.161,0.256-0.303,0.418-0.41,0.139-0.092,0.295-0.159,0.461-0.193,0.919-0.184,4.007,2.2,4.737,2.716,0.661,0.467,1.486,1.106,2.104,1.632-0.341-0.29-0.712-0.663-0.816-1.127-0.146-0.644,0.077-1.371,0.608-1.722,0.007-0.006,0.015-0.01,0.021-0.015,0.267-0.178,0.657-0.142,0.981-0.094,0.22,0.032,0.722,0.376,0.929,0.309,0.022-0.005,0.065,0.019,0.082,0.007,0.198-0.13,0.021-0.504,0.025-0.706,0.005-0.291,0.115-0.585,0.269-0.804,0.112-0.159,0.255-0.292,0.415-0.398,0.226-0.148,0.49-0.237,0.765-0.249,0.298-0.018,0.863,0.229,1.15,0.039,0.063-0.04,0.113-0.106,0.144-0.197,0.102-0.304-0.029-0.689,0.082-1.015,0.067-0.208,0.196-0.364,0.367-0.479,0.053-0.033,0.11-0.064,0.17-0.091,0.438-0.199,0.96-0.219,1.418-0.066,0.086,0.028,0.169,0.068,0.245,0.114,0.351,0.206,0.59,0.556,0.855,0.866,0.383,0.45,0.769,0.897,1.146,1.351,0.914,1.1,1.818,2.208,2.666,3.358,0.279,0.376,0.611,0.732,0.855,1.138,0.357,0.592,1.089,1.151,1.268,1.829z"/>
+</g></svg>
 NAME = 'Orange-Bioinformatics'
 DOCUMENTATION_NAME = 'Orange Bioinformatics'
 
-VERSION = '2.5a8'
+VERSION = '2.5a9'
 
 DESCRIPTION = 'Orange Bioinformatics add-on for Orange data mining software package.'
 LONG_DESCRIPTION = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read()