Example #1
0
def main(argv=[]):
    app = QApplication(argv)
    w = ToolBox()

    style = app.style()
    icon = QIcon(style.standardIcon(QStyle.SP_FileIcon))

    p1 = QLabel("A Label")
    p2 = QListView()
    p3 = QLabel("Another\nlabel")
    p4 = QSpinBox()

    i1 = w.addItem(p1, "Tab 1", icon)
    i2 = w.addItem(p2, "Tab 2", icon, "The second tab")
    i3 = w.addItem(p3, "Tab 3")
    i4 = w.addItem(p4, "Tab 4")

    p6 = QTextBrowser()
    p6.setHtml(
        "<h1>Hello Visitor</h1>"
        "<p>Are you interested in some of our wares?</p>"
    )
    w.insertItem(2, p6, "Dear friend")
    w.show()
    return app.exec_()
class OWMarkerGenes(widget.OWWidget):
    name = "Marker Genes"
    icon = 'icons/OWMarkerGenes.svg'
    priority = 130

    replaces = ['orangecontrib.single_cell.widgets.owmarkergenes.OWMarkerGenes']

    class Warning(widget.OWWidget.Warning):
        using_local_files = widget.Msg("Can't connect to serverfiles. Using cached files.")

    class Outputs:
        genes = widget.Output("Genes", Table)

    want_main_area = True
    want_control_area = True

    auto_commit = Setting(True)
    selected_source = Setting("")
    selected_organism = Setting("")
    selected_root_attribute = Setting(0)

    settingsHandler = MarkerGroupContextHandler()  # noqa: N815
    selected_genes = settings.ContextSetting([])

    settings_version = 2

    _data = None
    _available_sources = None

    def __init__(self) -> None:
        super().__init__()

        # define the layout
        main_area = QWidget(self.mainArea)
        self.mainArea.layout().addWidget(main_area)
        layout = QGridLayout()
        main_area.setLayout(layout)
        layout.setContentsMargins(4, 4, 4, 4)

        # filter line edit
        self.filter_line_edit = QLineEdit()
        self.filter_line_edit.setPlaceholderText("Filter marker genes")
        layout.addWidget(self.filter_line_edit, 0, 0, 1, 3)

        # define available markers view
        self.available_markers_view = TreeView()
        box = gui.vBox(self.mainArea, "Available markers", addToLayout=False)
        box.layout().addWidget(self.available_markers_view)
        layout.addWidget(box, 1, 0, 2, 1)

        # create selected markers view
        self.selected_markers_view = TreeView()
        box = gui.vBox(self.mainArea, "Selected markers", addToLayout=False)
        box.layout().addWidget(self.selected_markers_view)
        layout.addWidget(box, 1, 2, 2, 1)

        self.available_markers_view.otherView = self.selected_markers_view
        self.selected_markers_view.otherView = self.available_markers_view

        # buttons
        box = gui.vBox(self.mainArea, addToLayout=False, margin=0)
        layout.addWidget(box, 1, 1, 1, 1)
        self.move_button = gui.button(box, self, ">", callback=self._move_selected)

        self._init_description_area(layout)
        self._init_control_area()
        self._load_data()

    def _init_description_area(self, layout: QLayout) -> None:
        """
        Function define an info area with description of the genes and add it to the layout.
        """
        box = gui.widgetBox(self.mainArea, "Description", addToLayout=False)
        self.descriptionlabel = QTextBrowser(
            openExternalLinks=True, textInteractionFlags=(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse)
        )
        box.setMaximumHeight(self.descriptionlabel.fontMetrics().height() * (NUM_LINES_TEXT + 3))

        # description filed
        self.descriptionlabel.setText("Select a gene to see information.")
        self.descriptionlabel.setFrameStyle(QTextBrowser.NoFrame)
        # no (white) text background
        self.descriptionlabel.viewport().setAutoFillBackground(False)
        box.layout().addWidget(self.descriptionlabel)
        layout.addWidget(box, 3, 0, 1, 3)

    def _init_control_area(self) -> None:
        """
        Function defines dropdowns and the button in the control area.
        """
        box = gui.widgetBox(self.controlArea, 'Database', margin=0)
        self.source_index = -1
        self.db_source_cb = gui.comboBox(box, self, 'source_index')
        self.db_source_cb.activated[int].connect(self._set_db_source_index)

        box = gui.widgetBox(self.controlArea, 'Organism', margin=0)
        self.organism_index = -1
        self.group_cb = gui.comboBox(box, self, 'organism_index')
        self.group_cb.activated[int].connect(self._set_group_index)

        box = gui.widgetBox(self.controlArea, 'Group by', margin=0)
        self.group_by_cb = gui.comboBox(
            box, self, 'selected_root_attribute', items=GROUP_BY_ITEMS, callback=self._setup
        )

        gui.rubber(self.controlArea)
        gui.auto_commit(self.controlArea, self, "auto_commit", "Commit")

    def sizeHint(self):
        return super().sizeHint().expandedTo(QSize(900, 500))

    @property
    def available_sources(self) -> dict:
        return self._available_sources

    @available_sources.setter
    def available_sources(self, value: dict) -> None:
        """
        Set _available_sources variable, add them to dropdown, and select the source that was previously.
        """
        self._available_sources = value

        items = sorted(list(value.keys()), reverse=True)  # panglao first
        try:
            idx = items.index(self.selected_source)
        except ValueError:
            idx = -1

        self.db_source_cb.clear()
        self.db_source_cb.addItems(items)

        if idx != -1:
            self.source_index = idx
            self.selected_source = items[idx]
        elif items:
            self.source_index = min(max(self.source_index, 0), len(items) - 1)

        self._set_db_source_index(self.source_index)

    @property
    def data(self) -> Table:
        return self._data

    @data.setter
    def data(self, value: Table):
        """
        Set the source data. The data is then filtered on the first meta column (group).
        Select set dropdown with the groups and select the one that was selected previously.
        """
        self._data = value
        domain = value.domain

        if domain.metas:
            group = domain.metas[0]
            groupcol, _ = value.get_column_view(group)

            if group.is_string:
                group_values = list(set(groupcol))
            elif group.is_discrete:
                group_values = group.values
            else:
                raise TypeError("Invalid column type")
            group_values = sorted(group_values)  # human first

            try:
                idx = group_values.index(self.selected_organism)
            except ValueError:
                idx = -1

            self.group_cb.clear()
            self.group_cb.addItems(group_values)

            if idx != -1:
                self.organism_index = idx
                self.selected_organism = group_values[idx]
            elif group_values:
                self.organism_index = min(max(self.organism_index, 0), len(group_values) - 1)

            self._set_group_index(self.organism_index)

    def _load_data(self) -> None:
        """
        Collect available data sources (marker genes data sets).
        """
        self.Warning.using_local_files.clear()

        found_sources = {}
        try:
            found_sources.update(serverfiles.ServerFiles().allinfo(SERVER_FILES_DOMAIN))
        except requests.exceptions.ConnectionError:
            found_sources.update(serverfiles.allinfo(SERVER_FILES_DOMAIN))
            self.Warning.using_local_files()

        self.available_sources = {item.get('title').split(': ')[-1]: item for item in found_sources.values()}

    def _source_changed(self) -> None:
        """
        Respond on change of the source and download the data.
        """
        if self.available_sources:
            file_name = self.available_sources[self.selected_source]['filename']

            try:
                serverfiles.update(SERVER_FILES_DOMAIN, file_name)
            except requests.exceptions.ConnectionError:
                # try to update file. Ignore network errors.
                pass

            try:
                file_path = serverfiles.localpath_download(SERVER_FILES_DOMAIN, file_name)
            except requests.exceptions.ConnectionError as err:
                # Unexpected error.
                raise err
            self.data = Table.from_file(file_path)

    def _setup(self) -> None:
        """
        Setup the views with data.
        """
        self.closeContext()
        self.selected_genes = []
        self.openContext((self.selected_organism, self.selected_source))
        data_not_selected, data_selected = self._filter_data_group(self.data)

        # add model to available markers view
        group_by = GROUP_BY_ITEMS[self.selected_root_attribute]
        tree_model = TreeModel(data_not_selected, group_by)
        proxy_model = FilterProxyModel(self.filter_line_edit)
        proxy_model.setSourceModel(tree_model)

        self.available_markers_view.setModel(proxy_model)
        self.available_markers_view.selectionModel().selectionChanged.connect(
            partial(self._on_selection_changed, self.available_markers_view)
        )

        tree_model = TreeModel(data_selected, group_by)
        proxy_model = FilterProxyModel(self.filter_line_edit)
        proxy_model.setSourceModel(tree_model)
        self.selected_markers_view.setModel(proxy_model)

        self.selected_markers_view.selectionModel().selectionChanged.connect(
            partial(self._on_selection_changed, self.selected_markers_view)
        )
        self.selected_markers_view.model().sourceModel().data_added.connect(self._selected_markers_changed)
        self.selected_markers_view.model().sourceModel().data_removed.connect(self._selected_markers_changed)

        # update output and messages
        self._selected_markers_changed()

    def _filter_data_group(self, data: Table) -> Tuple[Table, Tuple]:
        """
        Function filter the table based on the selected group (Mouse, Human) and divide them in two groups based on
        selected_data variable.

        Parameters
        ----------
        data
            Table to be filtered

        Returns
        -------
        data_not_selected
            Data that will initially be in available markers view.
        data_selected
            Data that will initially be in selected markers view.
        """
        group = data.domain.metas[0]
        gvec = data.get_column_view(group)[0]

        if group.is_string:
            mask = gvec == self.selected_organism
        else:
            mask = gvec == self.organism_index
        data = data[mask]

        # divide data based on selected_genes variable (context)
        unique_gene_names = np.core.defchararray.add(
            data.get_column_view("Entrez ID")[0].astype(str), data.get_column_view("Cell Type")[0].astype(str)
        )
        mask = np.isin(unique_gene_names, self.selected_genes)
        data_not_selected = data[~mask]
        data_selected = data[mask]
        return data_not_selected, data_selected

    def commit(self) -> None:
        rows = self.selected_markers_view.model().sourceModel().rootItem.get_data_rows()
        if len(rows) > 0:
            metas = [r.metas for r in rows]
            data = Table.from_numpy(self.data.domain, np.empty((len(metas), 0)), metas=np.array(metas))
            # always false for marker genes data tables in single cell
            data.attributes[GENE_AS_ATTRIBUTE_NAME] = False
            # set taxonomy id in data.attributes
            data.attributes[TAX_ID] = MAP_GROUP_TO_TAX_ID.get(self.selected_organism, '')
            # set column id flag
            data.attributes[GENE_ID_COLUMN] = "Entrez ID"
            data.name = 'Marker Genes'
        else:
            data = None
        self.Outputs.genes.send(data)

    def _update_description(self, view: TreeView) -> None:
        """
        Upate the description about the gene. Only in case when one gene is selected.
        """
        selection = self._selected_rows(view)
        qmodel = view.model().sourceModel()

        if len(selection) > 1 or len(selection) == 0 or qmodel.node_from_index(selection[0]).data_row is None:
            self.descriptionlabel.setText("Select a gene to see information.")
        else:
            data_row = qmodel.node_from_index(selection[0]).data_row
            self.descriptionlabel.setHtml(
                f"<b>Gene name:</b> {data_row['Name']}<br/>"
                f"<b>Entrez ID:</b> {data_row['Entrez ID']}<br/>"
                f"<b>Cell Type:</b> {data_row['Cell Type']}<br/>"
                f"<b>Function:</b> {data_row['Function']}<br/>"
                f"<b>Reference:</b> <a href='{data_row['URL']}'>{data_row['Reference']}</a>"
            )

    def _update_data_info(self) -> None:
        """
        Updates output info in the control area.
        """
        sel_model = self.selected_markers_view.model().sourceModel()
        self.info.set_output_summary(f"Selected: {str(len(sel_model))}")

    # callback functions

    def _selected_markers_changed(self) -> None:
        """
        This function is called when markers in the selected view are added or removed.
        """
        rows = self.selected_markers_view.model().sourceModel().rootItem.get_data_rows()
        self.selected_genes = [row["Entrez ID"].value + row["Cell Type"].value for row in rows]
        self._update_data_info()
        self.commit()

    def _on_selection_changed(self, view: TreeView) -> None:
        """
        When selection in one of the view changes in a view button should change a sign in the correct direction and
        other view should reset the selection. Also gene description is updated.
        """
        self.move_button.setText(">" if view is self.available_markers_view else "<")
        if view is self.available_markers_view:
            self.selected_markers_view.clearSelection()
        else:
            self.available_markers_view.clearSelection()
        self._update_description(view)

    def _set_db_source_index(self, source_index: int) -> None:
        """
        Set the index of selected database source - index in a dropdown.
        """
        self.source_index = source_index
        self.selected_source = self.db_source_cb.itemText(source_index)
        self._source_changed()

    def _set_group_index(self, group_index: int) -> None:
        """
        Set the index of organism - index in a dropdown.
        """
        self.organism_index = group_index
        self.selected_organism = self.group_cb.itemText(group_index)
        self._setup()

    def _move_selected(self) -> None:
        """
        Move selected genes when button clicked.
        """
        if self._selected_rows(self.selected_markers_view):
            self._move_selected_from_to(self.selected_markers_view, self.available_markers_view)
        elif self._selected_rows(self.available_markers_view):
            self._move_selected_from_to(self.available_markers_view, self.selected_markers_view)

    # support functions for callbacks

    def _move_selected_from_to(self, src: TreeView, dst: TreeView) -> None:
        """
        Function moves items from src model to dst model.
        """
        selected_items = self._selected_rows(src)

        src_model = src.model().sourceModel()
        dst_model = dst.model().sourceModel()

        # move data as mimeData from source to destination tree view
        mime_data = src_model.mimeData(selected_items)
        # remove nodes from the source view
        src_model.remove_node_list(selected_items)
        dst_model.dropMimeData(mime_data, Qt.MoveAction, -1, -1)

    @staticmethod
    def _selected_rows(view: TreeView) -> List[QModelIndex]:
        """
        Return the selected rows in the view.
        """
        rows = view.selectionModel().selectedRows()
        return list(map(view.model().mapToSource, rows))

    @classmethod
    def migrate_settings(cls, settings, version=0):
        def migrate_to_version_2():
            settings["selected_source"] = settings.pop("selected_db_source", "")
            settings["selected_organism"] = settings.pop("selected_group", "")
            if "context_settings" in settings:
                for co in settings["context_settings"]:
                    co.values["selected_genes"] = [g[0] + g[1] for g in co.values["selected_genes"]]

        if version < 2:
            migrate_to_version_2()