Beispiel #1
0
    def move_rows(self, view: QListView, offset: int, roles=(Qt.EditRole,)):
        rows = [idx.row() for idx in view.selectionModel().selectedRows()]
        model = view.model()  # type: QAbstractItemModel
        rowcount = model.rowCount()
        newrows = [min(max(0, row + offset), rowcount - 1) for row in rows]

        def itemData(index):
            return {role: model.data(index, role) for role in roles}

        for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0):
            d1 = itemData(model.index(row, 0))
            d2 = itemData(model.index(newrow, 0))
            model.setItemData(model.index(row, 0), d2)
            model.setItemData(model.index(newrow, 0), d1)

        selection = QItemSelection()
        for nrow in newrows:
            index = model.index(nrow, 0)
            selection.select(index, index)
        view.selectionModel().select(
            selection, QItemSelectionModel.ClearAndSelect)

        self.commit()
class OWQualityControl(widget.OWWidget):
    name = "Quality Control"
    description = "Experiment quality control"
    icon = "../widgets/icons/QualityControl.svg"
    priority = 5000

    inputs = [("Experiment Data", Orange.data.Table, "set_data")]
    outputs = []

    DISTANCE_FUNCTIONS = [("Distance from Pearson correlation",
                           dist_pcorr),
                          ("Euclidean distance",
                           dist_eucl),
                          ("Distance from Spearman correlation",
                           dist_spearman)]

    settingsHandler = SetContextHandler()

    split_by_labels = settings.ContextSetting({})
    sort_by_labels = settings.ContextSetting({})

    selected_distance_index = settings.Setting(0)

    def __init__(self, parent=None):
        super().__init__(parent)

        ## Attributes
        self.data = None
        self.distances = None
        self.groups = None
        self.unique_pos = None
        self.base_group_index = 0

        ## GUI
        box = gui.widgetBox(self.controlArea, "Info")
        self.info_box = gui.widgetLabel(box, "\n")

        ## Separate By box
        box = gui.widgetBox(self.controlArea, "Separate By")
        self.split_by_model = itemmodels.PyListModel(parent=self)
        self.split_by_view = QListView()
        self.split_by_view.setSelectionMode(QListView.ExtendedSelection)
        self.split_by_view.setModel(self.split_by_model)
        box.layout().addWidget(self.split_by_view)

        self.split_by_view.selectionModel().selectionChanged.connect(
            self.on_split_key_changed)

        ## Sort By box
        box = gui.widgetBox(self.controlArea, "Sort By")
        self.sort_by_model = itemmodels.PyListModel(parent=self)
        self.sort_by_view = QListView()
        self.sort_by_view.setSelectionMode(QListView.ExtendedSelection)
        self.sort_by_view.setModel(self.sort_by_model)
        box.layout().addWidget(self.sort_by_view)

        self.sort_by_view.selectionModel().selectionChanged.connect(
            self.on_sort_key_changed)

        ## Distance box
        box = gui.widgetBox(self.controlArea, "Distance Measure")
        gui.comboBox(box, self, "selected_distance_index",
                     items=[name for name, _ in self.DISTANCE_FUNCTIONS],
                     callback=self.on_distance_measure_changed)

        self.scene = QGraphicsScene()
        self.scene_view = QGraphicsView(self.scene)
        self.scene_view.setRenderHints(QPainter.Antialiasing)
        self.scene_view.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        self.mainArea.layout().addWidget(self.scene_view)

        self.scene_view.installEventFilter(self)

        self._disable_updates = False
        self._cached_distances = {}
        self._base_index_hints = {}
        self.main_widget = None

        self.resize(800, 600)

    def clear(self):
        """Clear the widget state."""
        self.data = None
        self.distances = None
        self.groups = None
        self.unique_pos = None

        with disable_updates(self):
            self.split_by_model[:] = []
            self.sort_by_model[:] = []

        self.main_widget = None
        self.scene.clear()
        self.info_box.setText("\n")
        self._cached_distances = {}

    def set_data(self, data=None):
        """Set input experiment data."""
        self.closeContext()
        self.clear()

        self.error(0)
        self.warning(0)

        if data is not None:
            keys = self.get_suitable_keys(data)
            if not keys:
                self.error(0, "Data has no suitable feature labels.")
                data = None

        self.data = data
        if data is not None:
            self.on_new_data()

    def update_label_candidates(self):
        """Update the label candidates selection GUI 
        (Group/Sort By views).

        """
        keys = self.get_suitable_keys(self.data)
        with disable_updates(self):
            self.split_by_model[:] = keys
            self.sort_by_model[:] = keys

    def get_suitable_keys(self, data):
        """ Return suitable attr label keys from the data where
        the key has at least two unique values in the data.

        """
        attrs = [attr.attributes.items() for attr in data.domain.attributes]
        attrs = reduce(operator.iadd, attrs, [])
        # in case someone put non string values in attributes dict
        attrs = [(str(key), str(value)) for key, value in attrs]
        attrs = set(attrs)
        values = defaultdict(set)
        for key, value in attrs:
            values[key].add(value)
        keys = [key for key in values if len(values[key]) > 1]
        return keys

    def selected_split_by_labels(self):
        """Return the current selected split labels.
        """
        sel_m = self.split_by_view.selectionModel()
        indices = [r.row() for r in sel_m.selectedRows()]
        return [self.sort_by_model[i] for i in indices]

    def selected_sort_by_labels(self):
        """Return the current selected sort labels
        """
        sel_m = self.sort_by_view.selectionModel()
        indices = [r.row() for r in sel_m.selectedRows()]
        return [self.sort_by_model[i] for i in indices]

    def selected_distance(self):
        """Return the selected distance function.
        """
        return self.DISTANCE_FUNCTIONS[self.selected_distance_index][1]

    def selected_base_group_index(self):
        """Return the selected base group index
        """
        return self.base_group_index

    def selected_base_indices(self, base_group_index=None):
        indices = []
        for g, ind in self.groups:
            if base_group_index is None:
                label = group_label(self.selected_split_by_labels(), g)
                ind = [i for i in ind if i is not None]
                i = self._base_index_hints.get(label, ind[0] if ind else None)
            else:
                i = ind[base_group_index]
            indices.append(i)
        return indices

    def on_new_data(self):
        """We have new data and need to recompute all.
        """
        self.closeContext()

        self.update_label_candidates()
        self.info_box.setText(
            "%s genes \n%s experiments" %
            (len(self.data),  len(self.data.domain.attributes))
        )

        self.base_group_index = 0

        keys = self.get_suitable_keys(self.data)
        self.openContext(keys)

        ## Restore saved context settings (split/sort selection)
        split_by_labels = self.split_by_labels
        sort_by_labels = self.sort_by_labels

        def select(model, selection_model, selected_items):
            """Select items in a Qt item model view
            """
            all_items = list(model)
            try:
                indices = [all_items.index(item) for item in selected_items]
            except:
                indices = []
            for ind in indices:
                selection_model.select(model.index(ind),
                                       QItemSelectionModel.Select)

        with disable_updates(self):
            select(self.split_by_view.model(),
                   self.split_by_view.selectionModel(),
                   split_by_labels)

            select(self.sort_by_view.model(),
                   self.sort_by_view.selectionModel(),
                   sort_by_labels)

        with widget_disable(self):
            self.split_and_update()

    def on_split_key_changed(self, *args):
        """Split key has changed
        """
        with widget_disable(self):
            if not self._disable_updates:
                self.base_group_index = 0
                self.split_by_labels = self.selected_split_by_labels()
                self.split_and_update()

    def on_sort_key_changed(self, *args):
        """Sort key has changed
        """
        with widget_disable(self):
            if not self._disable_updates:
                self.base_group_index = 0
                self.sort_by_labels = self.selected_sort_by_labels()
                self.split_and_update()

    def on_distance_measure_changed(self):
        """Distance measure has changed
        """
        if self.data is not None:
            with widget_disable(self):
                self.update_distances()
                self.replot_experiments()

    def on_view_resize(self, size):
        """The view with the quality plot has changed
        """
        if self.main_widget:
            current = self.main_widget.size()
            self.main_widget.resize(size.width() - 6,
                                    current.height())

            self.scene.setSceneRect(self.scene.itemsBoundingRect())

    def on_rug_item_clicked(self, item):
        """An ``item`` in the quality plot has been clicked.
        """
        update = False
        sort_by_labels = self.selected_sort_by_labels()
        if sort_by_labels and item.in_group:
            ## The item is part of the group
            if item.group_index != self.base_group_index:
                self.base_group_index = item.group_index
                update = True

        else:
            if sort_by_labels:
                # If the user clicked on an background item it
                # invalidates the sorted labels selection
                with disable_updates(self):
                    self.sort_by_view.selectionModel().clear()
                    update = True

            index = item.index
            group = item.group
            label = group_label(self.selected_split_by_labels(), group)

            if self._base_index_hints.get(label, 0) != index:
                self._base_index_hints[label] = index
                update = True

        if update:
            with widget_disable(self):
                self.split_and_update()

    def eventFilter(self, obj, event):
        if obj is self.scene_view and event.type() == QEvent.Resize:
            self.on_view_resize(event.size())
        return super().eventFilter(obj, event)

    def split_and_update(self):
        """
        Split the data based on the selected sort/split labels
        and update the quality plot.

        """
        split_labels = self.selected_split_by_labels()
        sort_labels = self.selected_sort_by_labels()

        self.warning(0)
        if not split_labels:
            self.warning(0, "No separate by label selected.")

        self.groups, self.unique_pos = \
                exp.separate_by(self.data, split_labels,
                                consider=sort_labels,
                                add_empty=True)

        self.groups = sorted(self.groups.items(),
                             key=lambda t: list(map(float_if_posible, t[0])))
        self.unique_pos = sorted(self.unique_pos.items(),
                                 key=lambda t: list(map(float_if_posible, t[0])))

        if self.groups:
            if sort_labels:
                group_base = self.selected_base_group_index()
                base_indices = self.selected_base_indices(group_base)
            else:
                base_indices = self.selected_base_indices()
            self.update_distances(base_indices)
            self.replot_experiments()

    def get_cached_distances(self, measure):
        if measure not in self._cached_distances:
            attrs = self.data.domain.attributes
            mat = numpy.zeros((len(attrs), len(attrs)))

            self._cached_distances[measure] = \
                (mat, set(zip(range(len(attrs)), range(len(attrs)))))

        return self._cached_distances[measure]

    def get_cached_distance(self, measure, i, j):
        matrix, computed = self.get_cached_distances(measure)
        key = (i, j) if i < j else (j, i)
        if key in computed:
            return matrix[i, j]
        else:
            return None

    def get_distance(self, measure, i, j):
        d = self.get_cached_distance(measure, i, j)
        if d is None:
            vec_i = take_columns(self.data, [i])
            vec_j = take_columns(self.data, [j])
            d = measure(vec_i, vec_j)

            mat, computed = self.get_cached_distances(measure)
            mat[i, j] = d
            key = key = (i, j) if i < j else (j, i)
            computed.add(key)
        return d

    def store_distance(self, measure, i, j, dist):
        matrix, computed = self.get_cached_distances(measure)
        key = (i, j) if i < j else (j, i)
        matrix[j, i] = matrix[i, j] = dist
        computed.add(key)

    def update_distances(self, base_indices=()):
        """Recompute the experiment distances.
        """
        distance = self.selected_distance()
        if base_indices == ():
            base_group_index = self.selected_base_group_index()
            base_indices = [ind[base_group_index] \
                            for _, ind in self.groups]

        assert(len(base_indices) == len(self.groups))

        base_distances = []
        attributes = self.data.domain.attributes
        pb = gui.ProgressBar(self, len(self.groups) * len(attributes))

        for (group, indices), base_index in zip(self.groups, base_indices):
            # Base column of the group
            if base_index is not None:
                base_vec = take_columns(self.data, [base_index])
                distances = []
                # Compute the distances between base column
                # and all the rest data columns.
                for i in range(len(attributes)):
                    if i == base_index:
                        distances.append(0.0)
                    elif self.get_cached_distance(distance, i, base_index) is not None:
                        distances.append(self.get_cached_distance(distance, i, base_index))
                    else:
                        vec_i = take_columns(self.data, [i])
                        dist = distance(base_vec, vec_i)
                        self.store_distance(distance, i, base_index, dist)
                        distances.append(dist)
                    pb.advance()

                base_distances.append(distances)
            else:
                base_distances.append(None)

        pb.finish()
        self.distances = base_distances

    def replot_experiments(self):
        """Replot the whole quality plot.
        """
        self.scene.clear()
        labels = []

        max_dist = numpy.nanmax(list(filter(None, self.distances)))
        rug_widgets = []

        group_pen = QPen(Qt.black)
        group_pen.setWidth(2)
        group_pen.setCapStyle(Qt.RoundCap)
        background_pen = QPen(QColor(0, 0, 250, 150))
        background_pen.setWidth(1)
        background_pen.setCapStyle(Qt.RoundCap)

        main_widget = QGraphicsWidget()
        layout = QGraphicsGridLayout()
        attributes = self.data.domain.attributes
        if self.data is not None:
            for (group, indices), dist_vec in zip(self.groups, self.distances):
                indices_set = set(indices)
                rug_items = []
                if dist_vec is not None:
                    for i, attr in enumerate(attributes):
                        # Is this a within group distance or background
                        in_group = i in indices_set
                        if in_group:
                            rug_item = ClickableRugItem(dist_vec[i] / max_dist,
                                           1.0, self.on_rug_item_clicked)
                            rug_item.setPen(group_pen)
                            tooltip = experiment_description(attr)
                            rug_item.setToolTip(tooltip)
                            rug_item.group_index = indices.index(i)
                            rug_item.setZValue(rug_item.zValue() + 1)
                        else:
                            rug_item = ClickableRugItem(dist_vec[i] / max_dist,
                                           0.85, self.on_rug_item_clicked)
                            rug_item.setPen(background_pen)
                            tooltip = experiment_description(attr)
                            rug_item.setToolTip(tooltip)

                        rug_item.group = group
                        rug_item.index = i
                        rug_item.in_group = in_group

                        rug_items.append(rug_item)

                rug_widget = RugGraphicsWidget(parent=main_widget)
                rug_widget.set_rug(rug_items)

                rug_widgets.append(rug_widget)

                label = group_label(self.selected_split_by_labels(), group)
                label_item = QGraphicsSimpleTextItem(label, main_widget)
                label_item = GraphicsSimpleTextLayoutItem(label_item, parent=layout)
                label_item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
                labels.append(label_item)

        for i, (label, rug_w) in enumerate(zip(labels, rug_widgets)):
            layout.addItem(label, i, 0, Qt.AlignVCenter)
            layout.addItem(rug_w, i, 1)
            layout.setRowMaximumHeight(i, 30)

        main_widget.setLayout(layout)
        self.scene.addItem(main_widget)
        self.main_widget = main_widget
        self.rug_widgets = rug_widgets
        self.labels = labels
        self.on_view_resize(self.scene_view.size())
Beispiel #3
0
 def _set_selection(view: QListView, indices: List[int]):
     view.clearSelection()
     sm = view.selectionModel()
     model = view.model()
     for ind in indices:
         sm.select(model.index(ind), QItemSelectionModel.Select)
Beispiel #4
0
class SortedListWidget(QWidget):
    sortingOrderChanged = Signal()

    class _MyItemDelegate(QStyledItemDelegate):

        def __init__(self, sortingModel, parent):
            QStyledItemDelegate.__init__(self, parent)
            self.sortingModel = sortingModel

        def sizeHint(self, option, index):
            size = QStyledItemDelegate.sizeHint(self, option, index)
            return QSize(size.width(), size.height() + 4)

        def createEditor(self, parent, option, index):
            cb = QComboBox(parent)
            cb.setModel(self.sortingModel)
            cb.showPopup()
            return cb

        def setEditorData(self, editor, index):
            pass  # TODO: sensible default

        def setModelData(self, editor, model, index):
            text = editor.currentText()
            model.setData(index, text)

    def __init__(self, *args):
        QWidget.__init__(self, *args)
        self.setContentsMargins(0, 0, 0, 0)
        gridLayout = QGridLayout()
        gridLayout.setContentsMargins(0, 0, 0, 0)
        gridLayout.setSpacing(1)

        model = QStandardItemModel(self)
        model.rowsInserted.connect(self.__changed)
        model.rowsRemoved.connect(self.__changed)
        model.dataChanged.connect(self.__changed)

        self._listView = QListView(self)
        self._listView.setModel(model)
#        self._listView.setDragEnabled(True)
        self._listView.setDropIndicatorShown(True)
        self._listView.setDragDropMode(QListView.InternalMove)
        self._listView.viewport().setAcceptDrops(True)
        self._listView.setMinimumHeight(100)

        gridLayout.addWidget(self._listView, 0, 0, 2, 2)

        vButtonLayout = QVBoxLayout()

        self._upAction = QAction(
            "\u2191", self, toolTip="Move up")

        self._upButton = QToolButton(self)
        self._upButton.setDefaultAction(self._upAction)
        self._upButton.setSizePolicy(
            QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)

        self._downAction = QAction(
            "\u2193", self, toolTip="Move down")

        self._downButton = QToolButton(self)
        self._downButton.setDefaultAction(self._downAction)
        self._downButton.setSizePolicy(
            QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)

        vButtonLayout.addWidget(self._upButton)
        vButtonLayout.addWidget(self._downButton)

        gridLayout.addLayout(vButtonLayout, 0, 2, 2, 1)

        hButtonLayout = QHBoxLayout()

        self._addAction = QAction("+", self)
        self._addButton = QToolButton(self)
        self._addButton.setDefaultAction(self._addAction)

        self._removeAction = QAction("-", self)
        self._removeButton = QToolButton(self)
        self._removeButton.setDefaultAction(self._removeAction)
        hButtonLayout.addWidget(self._addButton)
        hButtonLayout.addWidget(self._removeButton)
        hButtonLayout.addStretch(10)
        gridLayout.addLayout(hButtonLayout, 2, 0, 1, 2)

        self.setLayout(gridLayout)
        self._addAction.triggered.connect(self._onAddAction)
        self._removeAction.triggered.connect(self._onRemoveAction)
        self._upAction.triggered.connect(self._onUpAction)
        self._downAction.triggered.connect(self._onDownAction)

    def sizeHint(self):
        size = QWidget.sizeHint(self)
        return QSize(size.width(), 100)

    def _onAddAction(self):
        item = QStandardItem("")
        item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled)
        self._listView.model().appendRow(item)
        self._listView.setCurrentIndex(item.index())
        self._listView.edit(item.index())

    def _onRemoveAction(self):
        current = self._listView.currentIndex()
        self._listView.model().takeRow(current.row())

    def _onUpAction(self):
        row = self._listView.currentIndex().row()
        model = self._listView.model()
        if row > 0:
            items = model.takeRow(row)
            model.insertRow(row - 1, items)
            self._listView.setCurrentIndex(model.index(row - 1, 0))

    def _onDownAction(self):
        row = self._listView.currentIndex().row()
        model = self._listView.model()
        if row < model.rowCount() and row >= 0:
            items = model.takeRow(row)
            if row == model.rowCount():
                model.appendRow(items)
            else:
                model.insertRow(row + 1, items)
            self._listView.setCurrentIndex(model.index(row + 1, 0))

    def setModel(self, model):
        """ Set a model to select items from
        """
        self._model = model
        self._listView.setItemDelegate(self._MyItemDelegate(self._model, self))

    def addItem(self, *args):
        """ Add a new entry in the list
        """
        item = QStandardItem(*args)
        item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled)
        self._listView.model().appendRow(item)

    def setItems(self, items):
        self._listView.model().clear()
        for item in items:
            self.addItem(item)

    def items(self):
        order = []
        for row in range(self._listView.model().rowCount()):
            order.append(str(self._listView.model().item(row, 0).text()))
        return order

    def __changed(self):
        self.sortingOrderChanged.emit()

    sortingOrder = property(items, setItems)
class OWGenotypeDistances(widget.OWWidget):
    name = "Expression Profile Distances"
    description = ("Compute distances between expression profiles of "
                   "different experimental factors.")
    icon = "../widgets/icons/GenotypeDistances.svg"
    priority = 1050

    inputs = [("Data", Orange.data.Table, "set_data")]
    outputs = [("Distances", Orange.misc.DistMatrix),
               ("Sorted Data", Orange.data.Table)]

    settingsHandler = SetContextHandler()

    separate_keys = settings.ContextSetting({})
    relevant_keys = settings.ContextSetting({})

    distance_measure = settings.Setting(0)
    auto_commit = settings.Setting(False)

    DISTANCE_FUNCTIONS = [
        ("Distance from Pearson correlation", dist_pcorr),
        ("Euclidean distance", dist_eucl),
        ("Distance from Spearman correlation", dist_spearman)
    ]

    def __init__(self, parent=None):
        super().__init__(self, parent)

        self.data = None
        self.partitions = []
        self.matrix = None
        self.split_groups = []
        self._disable_updates = False

        ########
        # GUI
        ########

        box = gui.widgetBox(self.controlArea, "Input")

        self.info_box = gui.widgetLabel(box, "No data on input\n")

        box = gui.widgetBox(self.controlArea, "Separate By",
                            addSpace=True)

        self.separate_view = QListView(
            selectionMode=QListView.MultiSelection
        )
        box.layout().addWidget(self.separate_view)

        box = gui.widgetBox(self.controlArea, "Sort By",
                            addSpace=True)
        self.relevant_view = QListView(
            selectionMode=QListView.MultiSelection)

        box.layout().addWidget(self.relevant_view)

        self.distance_view = gui.comboBox(
            self.controlArea, self, "distance_measure",
            box="Distance Measure",
            items=[name for name, _ in self.DISTANCE_FUNCTIONS])

        gui.rubber(self.controlArea)

        gui.auto_commit(self.controlArea, self, "auto_commit", "Commit")
        self.groups_box = gui.widgetBox(self.mainArea, "Groups")
        self.groups_scroll_area = QScrollArea()
        self.groups_box.layout().addWidget(self.groups_scroll_area)

    def sizeHint(self):
        return QSize(800, 600)

    def clear(self):
        self.data = None
        self.partitions = []
        self.split_groups = []
        self.matrix = None

    def get_suitable_keys(self, data):
        """Return suitable attr label keys from the data where the key has at least
        two unique values in the data.

        """
        attrs = [attr.attributes.items() for attr in data.domain.attributes]
        attrs = reduce(operator.iadd, attrs, [])
        # in case someone put non string values in attributes dict
        attrs = [(str(key), str(value)) for key, value in attrs]
        attrs = set(attrs)
        values = defaultdict(set)
        for key, value in attrs:
            values[key].add(value)
        keys = [key for key in values if len(values[key]) > 1]
        return keys

    def set_data(self, data=None):
        """Set the input data table.
        """
        self.closeContext()
        self.clear()
        self.error(0)
        self.warning(0)
        if data and not self.get_suitable_keys(data):
            self.error(0, "Data has no suitable column labels.")
            data = None

        self.data = data

        if data:
            self.info_box.setText("{0} genes\n{1} experiments"
                                  .format(len(data), len(data.domain)))
            self.update_control()
            self.split_data()
        else:
            self.separate_view.setModel(itemmodels.PyListModel([]))
            self.relevant_view.setModel(itemmodels.PyListModel([]))
            self.groups_scroll_area.setWidget(QWidget())
            self.info_box.setText("No data on input.\n")
        self.commit()

    def update_control(self):
        """Update the control area of the widget. Populate the list
        views with keys from attribute labels.
        """
        keys = self.get_suitable_keys(self.data)

        model = itemmodels.PyListModel(keys)
        self.separate_view.setModel(model)
        self.separate_view.selectionModel().selectionChanged.connect(
            self.on_separate_key_changed)

        model = itemmodels.PyListModel(keys)
        self.relevant_view.setModel(model)
        self.relevant_view.selectionModel().selectionChanged.connect(
            self.on_relevant_key_changed)

        self.openContext(keys)

        # Get the selected keys from the open context
        separate_keys = self.separate_keys
        relevant_keys = self.relevant_keys

        def select(model, selection_model, selected_items):
            all_items = list(model)
            try:
                indices = [all_items.index(item) for item in selected_items]
            except:
                indices = []
            selection = QItemSelection()
            for ind in indices:
                index = model.index(ind)
                selection.select(index, index)

            selection_model.select(selection, QItemSelectionModel.Select)

        self._disable_updates = True
        try:
            select(self.relevant_view.model(),
                   self.relevant_view.selectionModel(),
                   relevant_keys)

            select(self.separate_view.model(),
                   self.separate_view.selectionModel(),
                   separate_keys)
        finally:
            self._disable_updates = False

    def on_separate_key_changed(self, *args):
        if not self._disable_updates:
            self.separate_keys = self.selected_separeate_by_keys()
            self.split_data()

    def on_relevant_key_changed(self, *args):
        if not self._disable_updates:
            self.relevant_keys = self.selected_relevant_keys()
            self.split_data()

    def selected_separeate_by_keys(self):
        """Return the currently selected separate by keys
        """
        rows = self.separate_view.selectionModel().selectedRows()
        rows = sorted([idx.row() for idx in rows])
        keys = [self.separate_view.model()[row] for row in rows]
        return keys

    def selected_relevant_keys(self):
        """Return the currently selected relevant keys
        """
        rows = self.relevant_view.selectionModel().selectedRows()
        rows = sorted([idx.row() for idx in rows])
        keys = [self.relevant_view.model()[row] for row in rows]
        return keys

    def split_data(self):
        """Split the data and update the Groups widget
        """
        separate_keys = self.selected_separeate_by_keys()
        relevant_keys = self.selected_relevant_keys()

        self.warning(0)
        if not separate_keys:
            self.warning(0, "No separate by column selected.")

        partitions, uniquepos = separate_by(
            self.data, separate_keys, consider=relevant_keys)
        partitions = partitions.items()

        all_values = defaultdict(set)
        for a in [at.attributes for at in self.data.domain.attributes]:
            for k, v in a.items():
                all_values[k].add(v)

        # sort groups
        pkeys = [key for key, _ in partitions]
        types = [data_type([a[i] for a in pkeys])
                 for i in range(len(pkeys[0]))]

        partitions = sorted(partitions, key=lambda x:
                    tuple(types[i](v) for i,v in enumerate(x[0])))

        split_groups = []

        # Collect relevant key value pairs for all columns
        relevant_items = None

        for keys, indices in partitions:
            if relevant_items == None:
                relevant_items = [defaultdict(set) for _ in indices]
            for i, ind in enumerate(indices):
                if ind is not None:
                    attr = self.data.domain[ind]
                    for key in relevant_keys:
                        relevant_items[i][key].add(attr.attributes[key])

        #those with different values between rows are not relevant
        for d in relevant_items:
            for k, s in list(d.items()):
                if len(s) > 1:
                    del d[k]
                else:
                    d[k] = s.pop()

        def get_attr(attr_index, i):
            if attr_index is None:
                attr = Orange.data.ContinuousVariable(next(missing_name_gen), 
                    compute_value=lambda x: None)
                attr.attributes.update(relevant_items[i])
                return attr
            else:
                return self.data.domain[attr_index]

        for keys, indices in partitions:
            attrs = [get_attr(attr_index, i)
                     for i, attr_index in enumerate(indices)]
            for attr in attrs:
                attr.attributes.update(zip(separate_keys, keys))
            domain = Orange.data.Domain(attrs, [], self.data.domain.metas)
            split_groups.append((keys, domain))

        self.set_groups(separate_keys, split_groups, relevant_keys,
                        relevant_items, all_values, uniquepos)

        self.partitions = partitions
        self.split_groups = split_groups

        self.commit()

    def set_groups(self, keys, groups, relevant_keys, relevant_items,
                   all_values, uniquepos):
        """Set the current data groups and update the Group widget
        """
        layout = QVBoxLayout()
        header_widths = []
        header_views = []
        palette = self.palette()
        all_values = all_values.keys()

        def for_print(rd):
            attrs = []
            for d in rd:
                attr = Orange.data.ContinuousVariable(next(inactive_name_gen))
                attr.attributes.update(d)
                attrs.append(attr)
            return Orange.data.Domain(attrs, None)

        for separatev, domain in [(None, for_print(relevant_items))] + groups:
            label = None
            if separatev is not None:
                ann_vals = " <b>|</b> ".join(["<b>{0}</b> = {1}".format(key,val) \
                     for key, val in zip(keys, separatev)])
                label = QLabel(ann_vals)

            model = QStandardItemModel()
            for i, attr in enumerate(domain.attributes):
                item = QStandardItem()
                if separatev is not None:
                    isunique = uniquepos[separatev][i]
                else:
                    isunique = all(a[i] for a in uniquepos.values())

                if str(attr.name).startswith("!!missing "):  # TODO: Change this to not depend on name
                    header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \
                                   for key in all_values if key not in relevant_items[i]]
                    header_text = "\n".join(header_text) if header_text else "Empty"
                    item.setData(header_text, Qt.DisplayRole)
                    item.setFlags(Qt.NoItemFlags)
                    item.setData(QColor(Qt.red), Qt.ForegroundRole)
                    item.setData(palette.color(QPalette.Disabled, QPalette.Window), Qt.BackgroundRole)
                    item.setData("Missing feature.", Qt.ToolTipRole)
                elif str(attr.name).startswith("!!inactive "):
                    header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \
                                   for key in all_values if key in relevant_items[i]]
                    header_text = "\n".join(header_text) if header_text else "No descriptor"
                    item.setData(header_text, Qt.DisplayRole)
                    item.setData(palette.color(QPalette.Disabled, QPalette.Window), Qt.BackgroundRole)
                else:
                    header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \
                                   for key in all_values if key not in relevant_items[i]]
                    header_text = "\n".join(header_text) if header_text else "Empty"
                    item.setData(header_text, Qt.DisplayRole)
                    item.setData(attr.name, Qt.ToolTipRole)

                if not isunique:
                    item.setData(QColor(Qt.red), Qt.ForegroundRole)

                model.setHorizontalHeaderItem(i, item)
            attr_count = len(domain.attributes)
            view = MyHeaderView(Qt.Horizontal)
            view.setResizeMode(QHeaderView.Fixed)
            view.setModel(model)
            hint = view.sizeHint()
            view.setMaximumHeight(hint.height())

            widths = [view.sectionSizeHint(i) for i in range(attr_count)]
            header_widths.append(widths)
            header_views.append(view)

            if label:
                layout.addWidget(label)
            layout.addWidget(view)
            layout.addSpacing(8)

        # Make all header sections the same width
        width_sum = 0
        max_header_count = max([h.count() for h in header_views])
        for i in range(max_header_count):
            max_width = max([w[i] for w in header_widths if i < len(w)] or [0])
            for view in header_views:
                if i < view.count():
                    view.resizeSection(i, max_width)
            width_sum += max_width + 2

        for h in header_views:
            h.setMinimumWidth(h.length() + 4)

        widget = QWidget()
        widget.setLayout(layout)
        widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum)
        layout.activate()

        max_width = max(h.length() for h in header_views) + 20

        left, _, right, _ = self.getContentsMargins()
        widget.setMinimumWidth(width_sum)
        widget.setMinimumWidth(max_width + left + right)
        self.groups_scroll_area.setWidget(widget)

    def compute_distances(self, separate_keys, partitions, data):
        """Compute the distances between genotypes.
        """
        if separate_keys and partitions:
            self.progressBarInit()
#             matrix = Orange.misc.DistMatrix(len(partitions))
            matrix = numpy.zeros((len(partitions), len(partitions)))

            profiles = [linearize(data, indices) for _, indices in partitions]
            dist_func = self.DISTANCE_FUNCTIONS[self.distance_measure][1]
#             from Orange.utils import progress_bar_milestones
            count = (len(profiles) * len(profiles) - 1) / 2
#             milestones = progress_bar_milestones(count)
            iter_count = 0
            for i in range(len(profiles)):
                for j in range(i + 1, len(profiles)):
                    matrix[i, j] = dist_func(profiles[i], profiles[j])
                    matrix[j, i] = matrix[i, j]
                    iter_count += 1
#                     if iter_count in milestones:
                    self.progressBarSet(100.0 * iter_count / count)
            self.progressBarFinished()

            items = [["{0}={1}".format(key, value)
                      for key, value in zip(separate_keys, values)]
                     for values, _ in partitions]
            items = [" | ".join(item) for item in items]
#             matrix.setattr("items", items)
            matrix = Orange.misc.DistMatrix(matrix)
        else:
            matrix = None

        self.matrix = matrix

    def commit(self):
        separate_keys = self.selected_separeate_by_keys()
        self.compute_distances(separate_keys,
                               self.partitions,
                               self.data)
        if self.split_groups:
            all_attrs = []
            for group, domain in self.split_groups:
                attrs = []
                group_name = " | ".join("{0}={1}".format(*item) for item in
                                        zip(separate_keys, group))
                for attr in domain.attributes:
                    newattr = clone_attr(attr)
                    newattr.attributes["<GENOTYPE GROUP>"] = group_name # Need a better way to pass the groups to downstream widgets.
                    attrs.append(newattr)

                all_attrs.extend(attrs)

            domain = Orange.data.Domain(all_attrs, self.data.domain.class_vars,
                                        self.data.domain.metas)
            data = Orange.data.Table(domain, self.data)
        else:
            data = None
        self.send("Sorted Data", data)
        self.send("Distances", self.matrix)

    def send_report(self):
        self.report_items((
            ("Separate By", ", ".join(self.selected_separeate_by_keys())),
            ("Sort By", ", ".join(self.selected_relevant_keys())),
            ("Distance Measure", self.DISTANCE_FUNCTIONS[self.distance_measure][0])
        ))
        layout = self.groups_scroll_area.widget().layout()
        html = "<table>"
        for i in range(layout.count()):
            item = layout.itemAt(i)
            if isinstance(item, QSpacerItem):
                html += "<tr><td></td></tr>"
            elif isinstance(item, QWidgetItem):
                hor = item.widget()
                if isinstance(hor, QLabel):
                    label = hor.text()
                    html += "<tr><td><b>%s</b></td></tr>" % label
                elif isinstance(hor, QHeaderView):
                    model = hor.model()
                    content = (model.horizontalHeaderItem(col) for col in range(model.columnCount()))
                    content = (item.text().replace('\n', "<br/>") for item in content)
                    html += "<tr>" + ''.join("<td>{}</td>".format(item) for item in content) + "</tr>"
        html += "</table>"
        self.report_raw("Groups", html)
Beispiel #6
0
class OWGenotypeDistances(widget.OWWidget):
    name = "Expression Profile Distances"
    description = ("Compute distances between expression profiles of "
                   "different experimental factors.")
    icon = "../widgets/icons/GenotypeDistances.svg"
    priority = 1050

    inputs = [("Data", Orange.data.Table, "set_data")]
    outputs = [("Distances", Orange.misc.DistMatrix),
               ("Sorted Data", Orange.data.Table)]

    settingsHandler = SetContextHandler()

    separate_keys = settings.ContextSetting({})
    relevant_keys = settings.ContextSetting({})

    distance_measure = settings.Setting(0)
    auto_commit = settings.Setting(False)

    DISTANCE_FUNCTIONS = [("Distance from Pearson correlation", dist_pcorr),
                          ("Euclidean distance", dist_eucl),
                          ("Distance from Spearman correlation", dist_spearman)
                          ]

    def __init__(self, parent=None):
        super().__init__(self, parent)

        self.data = None
        self.partitions = []
        self.matrix = None
        self.split_groups = []
        self._disable_updates = False

        ########
        # GUI
        ########

        box = gui.widgetBox(self.controlArea, "Input")

        self.info_box = gui.widgetLabel(box, "No data on input\n")

        box = gui.widgetBox(self.controlArea, "Separate By", addSpace=True)

        self.separate_view = QListView(selectionMode=QListView.MultiSelection)
        box.layout().addWidget(self.separate_view)

        box = gui.widgetBox(self.controlArea, "Sort By", addSpace=True)
        self.relevant_view = QListView(selectionMode=QListView.MultiSelection)

        box.layout().addWidget(self.relevant_view)

        self.distance_view = gui.comboBox(
            self.controlArea,
            self,
            "distance_measure",
            box="Distance Measure",
            items=[name for name, _ in self.DISTANCE_FUNCTIONS])

        gui.rubber(self.controlArea)

        gui.auto_commit(self.controlArea, self, "auto_commit", "Commit")
        self.groups_box = gui.widgetBox(self.mainArea, "Groups")
        self.groups_scroll_area = QScrollArea()
        self.groups_box.layout().addWidget(self.groups_scroll_area)

    def sizeHint(self):
        return QSize(800, 600)

    def clear(self):
        self.data = None
        self.partitions = []
        self.split_groups = []
        self.matrix = None

    def get_suitable_keys(self, data):
        """Return suitable attr label keys from the data where the key has at least
        two unique values in the data.

        """
        attrs = [attr.attributes.items() for attr in data.domain.attributes]
        attrs = reduce(operator.iadd, attrs, [])
        # in case someone put non string values in attributes dict
        attrs = [(str(key), str(value)) for key, value in attrs]
        attrs = set(attrs)
        values = defaultdict(set)
        for key, value in attrs:
            values[key].add(value)
        keys = [key for key in values if len(values[key]) > 1]
        return keys

    def set_data(self, data=None):
        """Set the input data table.
        """
        self.closeContext()
        self.clear()
        self.error(0)
        self.warning(0)
        if data and not self.get_suitable_keys(data):
            self.error(0, "Data has no suitable column labels.")
            data = None

        self.data = data

        if data:
            self.info_box.setText("{0} genes\n{1} experiments".format(
                len(data), len(data.domain)))
            self.update_control()
            self.split_data()
        else:
            self.separate_view.setModel(itemmodels.PyListModel([]))
            self.relevant_view.setModel(itemmodels.PyListModel([]))
            self.groups_scroll_area.setWidget(QWidget())
            self.info_box.setText("No data on input.\n")
        self.commit()

    def update_control(self):
        """Update the control area of the widget. Populate the list
        views with keys from attribute labels.
        """
        keys = self.get_suitable_keys(self.data)

        model = itemmodels.PyListModel(keys)
        self.separate_view.setModel(model)
        self.separate_view.selectionModel().selectionChanged.connect(
            self.on_separate_key_changed)

        model = itemmodels.PyListModel(keys)
        self.relevant_view.setModel(model)
        self.relevant_view.selectionModel().selectionChanged.connect(
            self.on_relevant_key_changed)

        self.openContext(keys)

        # Get the selected keys from the open context
        separate_keys = self.separate_keys
        relevant_keys = self.relevant_keys

        def select(model, selection_model, selected_items):
            all_items = list(model)
            try:
                indices = [all_items.index(item) for item in selected_items]
            except:
                indices = []
            selection = QItemSelection()
            for ind in indices:
                index = model.index(ind)
                selection.select(index, index)

            selection_model.select(selection, QItemSelectionModel.Select)

        self._disable_updates = True
        try:
            select(self.relevant_view.model(),
                   self.relevant_view.selectionModel(), relevant_keys)

            select(self.separate_view.model(),
                   self.separate_view.selectionModel(), separate_keys)
        finally:
            self._disable_updates = False

    def on_separate_key_changed(self, *args):
        if not self._disable_updates:
            self.separate_keys = self.selected_separeate_by_keys()
            self.split_data()

    def on_relevant_key_changed(self, *args):
        if not self._disable_updates:
            self.relevant_keys = self.selected_relevant_keys()
            self.split_data()

    def selected_separeate_by_keys(self):
        """Return the currently selected separate by keys
        """
        rows = self.separate_view.selectionModel().selectedRows()
        rows = sorted([idx.row() for idx in rows])
        keys = [self.separate_view.model()[row] for row in rows]
        return keys

    def selected_relevant_keys(self):
        """Return the currently selected relevant keys
        """
        rows = self.relevant_view.selectionModel().selectedRows()
        rows = sorted([idx.row() for idx in rows])
        keys = [self.relevant_view.model()[row] for row in rows]
        return keys

    def split_data(self):
        """Split the data and update the Groups widget
        """
        separate_keys = self.selected_separeate_by_keys()
        relevant_keys = self.selected_relevant_keys()

        self.warning(0)
        if not separate_keys:
            self.warning(0, "No separate by column selected.")

        partitions, uniquepos = separate_by(self.data,
                                            separate_keys,
                                            consider=relevant_keys)
        partitions = partitions.items()

        all_values = defaultdict(set)
        for a in [at.attributes for at in self.data.domain.attributes]:
            for k, v in a.items():
                all_values[k].add(v)

        # sort groups
        pkeys = [key for key, _ in partitions]
        types = [
            data_type([a[i] for a in pkeys]) for i in range(len(pkeys[0]))
        ]

        partitions = sorted(partitions,
                            key=lambda x: tuple(types[i](v)
                                                for i, v in enumerate(x[0])))

        split_groups = []

        # Collect relevant key value pairs for all columns
        relevant_items = None

        for keys, indices in partitions:
            if relevant_items == None:
                relevant_items = [defaultdict(set) for _ in indices]
            for i, ind in enumerate(indices):
                if ind is not None:
                    attr = self.data.domain[ind]
                    for key in relevant_keys:
                        relevant_items[i][key].add(attr.attributes[key])

        #those with different values between rows are not relevant
        for d in relevant_items:
            for k, s in list(d.items()):
                if len(s) > 1:
                    del d[k]
                else:
                    d[k] = s.pop()

        def get_attr(attr_index, i):
            if attr_index is None:
                attr = Orange.data.ContinuousVariable(
                    next(missing_name_gen), compute_value=lambda x: None)
                attr.attributes.update(relevant_items[i])
                return attr
            else:
                return self.data.domain[attr_index]

        for keys, indices in partitions:
            attrs = [
                get_attr(attr_index, i) for i, attr_index in enumerate(indices)
            ]
            for attr in attrs:
                attr.attributes.update(zip(separate_keys, keys))
            domain = Orange.data.Domain(attrs, [], self.data.domain.metas)
            split_groups.append((keys, domain))

        self.set_groups(separate_keys, split_groups, relevant_keys,
                        relevant_items, all_values, uniquepos)

        self.partitions = partitions
        self.split_groups = split_groups

        self.commit()

    def set_groups(self, keys, groups, relevant_keys, relevant_items,
                   all_values, uniquepos):
        """Set the current data groups and update the Group widget
        """
        layout = QVBoxLayout()
        header_widths = []
        header_views = []
        palette = self.palette()
        all_values = all_values.keys()

        def for_print(rd):
            attrs = []
            for d in rd:
                attr = Orange.data.ContinuousVariable(next(inactive_name_gen))
                attr.attributes.update(d)
                attrs.append(attr)
            return Orange.data.Domain(attrs, None)

        for separatev, domain in [(None, for_print(relevant_items))] + groups:
            label = None
            if separatev is not None:
                ann_vals = " <b>|</b> ".join(["<b>{0}</b> = {1}".format(key,val) \
                     for key, val in zip(keys, separatev)])
                label = QLabel(ann_vals)

            model = QStandardItemModel()
            for i, attr in enumerate(domain.attributes):
                item = QStandardItem()
                if separatev is not None:
                    isunique = uniquepos[separatev][i]
                else:
                    isunique = all(a[i] for a in uniquepos.values())

                if str(attr.name).startswith(
                        "!!missing "
                ):  # TODO: Change this to not depend on name
                    header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \
                                   for key in all_values if key not in relevant_items[i]]
                    header_text = "\n".join(
                        header_text) if header_text else "Empty"
                    item.setData(header_text, Qt.DisplayRole)
                    item.setFlags(Qt.NoItemFlags)
                    item.setData(QColor(Qt.red), Qt.ForegroundRole)
                    item.setData(
                        palette.color(QPalette.Disabled, QPalette.Window),
                        Qt.BackgroundRole)
                    item.setData("Missing feature.", Qt.ToolTipRole)
                elif str(attr.name).startswith("!!inactive "):
                    header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \
                                   for key in all_values if key in relevant_items[i]]
                    header_text = "\n".join(
                        header_text) if header_text else "No descriptor"
                    item.setData(header_text, Qt.DisplayRole)
                    item.setData(
                        palette.color(QPalette.Disabled, QPalette.Window),
                        Qt.BackgroundRole)
                else:
                    header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \
                                   for key in all_values if key not in relevant_items[i]]
                    header_text = "\n".join(
                        header_text) if header_text else "Empty"
                    item.setData(header_text, Qt.DisplayRole)
                    item.setData(attr.name, Qt.ToolTipRole)

                if not isunique:
                    item.setData(QColor(Qt.red), Qt.ForegroundRole)

                model.setHorizontalHeaderItem(i, item)
            attr_count = len(domain.attributes)
            view = MyHeaderView(Qt.Horizontal)
            view.setResizeMode(QHeaderView.Fixed)
            view.setModel(model)
            hint = view.sizeHint()
            view.setMaximumHeight(hint.height())

            widths = [view.sectionSizeHint(i) for i in range(attr_count)]
            header_widths.append(widths)
            header_views.append(view)

            if label:
                layout.addWidget(label)
            layout.addWidget(view)
            layout.addSpacing(8)

        # Make all header sections the same width
        width_sum = 0
        max_header_count = max([h.count() for h in header_views])
        for i in range(max_header_count):
            max_width = max([w[i] for w in header_widths if i < len(w)] or [0])
            for view in header_views:
                if i < view.count():
                    view.resizeSection(i, max_width)
            width_sum += max_width + 2

        for h in header_views:
            h.setMinimumWidth(h.length() + 4)

        widget = QWidget()
        widget.setLayout(layout)
        widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum)
        layout.activate()

        max_width = max(h.length() for h in header_views) + 20

        left, _, right, _ = self.getContentsMargins()
        widget.setMinimumWidth(width_sum)
        widget.setMinimumWidth(max_width + left + right)
        self.groups_scroll_area.setWidget(widget)

    def compute_distances(self, separate_keys, partitions, data):
        """Compute the distances between genotypes.
        """
        if separate_keys and partitions:
            self.progressBarInit()
            #             matrix = Orange.misc.DistMatrix(len(partitions))
            matrix = numpy.zeros((len(partitions), len(partitions)))

            profiles = [linearize(data, indices) for _, indices in partitions]
            dist_func = self.DISTANCE_FUNCTIONS[self.distance_measure][1]
            #             from Orange.utils import progress_bar_milestones
            count = (len(profiles) * len(profiles) - 1) / 2
            #             milestones = progress_bar_milestones(count)
            iter_count = 0
            for i in range(len(profiles)):
                for j in range(i + 1, len(profiles)):
                    matrix[i, j] = dist_func(profiles[i], profiles[j])
                    matrix[j, i] = matrix[i, j]
                    iter_count += 1
                    #                     if iter_count in milestones:
                    self.progressBarSet(100.0 * iter_count / count)
            self.progressBarFinished()

            items = [[
                "{0}={1}".format(key, value)
                for key, value in zip(separate_keys, values)
            ] for values, _ in partitions]
            items = [" | ".join(item) for item in items]
            #             matrix.setattr("items", items)
            matrix = Orange.misc.DistMatrix(matrix)
        else:
            matrix = None

        self.matrix = matrix

    def commit(self):
        separate_keys = self.selected_separeate_by_keys()
        self.compute_distances(separate_keys, self.partitions, self.data)
        if self.split_groups:
            all_attrs = []
            for group, domain in self.split_groups:
                attrs = []
                group_name = " | ".join("{0}={1}".format(*item)
                                        for item in zip(separate_keys, group))
                for attr in domain.attributes:
                    newattr = clone_attr(attr)
                    newattr.attributes[
                        "<GENOTYPE GROUP>"] = group_name  # Need a better way to pass the groups to downstream widgets.
                    attrs.append(newattr)

                all_attrs.extend(attrs)

            domain = Orange.data.Domain(all_attrs, self.data.domain.class_vars,
                                        self.data.domain.metas)
            data = Orange.data.Table(domain, self.data)
        else:
            data = None
        self.send("Sorted Data", data)
        self.send("Distances", self.matrix)

    def send_report(self):
        self.report_items(
            (("Separate By", ", ".join(self.selected_separeate_by_keys())),
             ("Sort By", ", ".join(self.selected_relevant_keys())),
             ("Distance Measure",
              self.DISTANCE_FUNCTIONS[self.distance_measure][0])))
        layout = self.groups_scroll_area.widget().layout()
        html = "<table>"
        for i in range(layout.count()):
            item = layout.itemAt(i)
            if isinstance(item, QSpacerItem):
                html += "<tr><td></td></tr>"
            elif isinstance(item, QWidgetItem):
                hor = item.widget()
                if isinstance(hor, QLabel):
                    label = hor.text()
                    html += "<tr><td><b>%s</b></td></tr>" % label
                elif isinstance(hor, QHeaderView):
                    model = hor.model()
                    content = (model.horizontalHeaderItem(col)
                               for col in range(model.columnCount()))
                    content = (item.text().replace('\n', "<br/>")
                               for item in content)
                    html += "<tr>" + ''.join("<td>{}</td>".format(item)
                                             for item in content) + "</tr>"
        html += "</table>"
        self.report_raw("Groups", html)
Beispiel #7
0
class SortedListWidget(QWidget):
    sortingOrderChanged = Signal()

    class _MyItemDelegate(QStyledItemDelegate):
        def __init__(self, sortingModel, parent):
            QStyledItemDelegate.__init__(self, parent)
            self.sortingModel = sortingModel

        def sizeHint(self, option, index):
            size = QStyledItemDelegate.sizeHint(self, option, index)
            return QSize(size.width(), size.height() + 4)

        def createEditor(self, parent, option, index):
            cb = QComboBox(parent)
            cb.setModel(self.sortingModel)
            cb.showPopup()
            return cb

        def setEditorData(self, editor, index):
            pass  # TODO: sensible default

        def setModelData(self, editor, model, index):
            text = editor.currentText()
            model.setData(index, text)

    def __init__(self, *args):
        QWidget.__init__(self, *args)
        self.setContentsMargins(0, 0, 0, 0)
        gridLayout = QGridLayout()
        gridLayout.setContentsMargins(0, 0, 0, 0)
        gridLayout.setSpacing(1)

        model = QStandardItemModel(self)
        model.rowsInserted.connect(self.__changed)
        model.rowsRemoved.connect(self.__changed)
        model.dataChanged.connect(self.__changed)

        self._listView = QListView(self)
        self._listView.setModel(model)
        #        self._listView.setDragEnabled(True)
        self._listView.setDropIndicatorShown(True)
        self._listView.setDragDropMode(QListView.InternalMove)
        self._listView.viewport().setAcceptDrops(True)
        self._listView.setMinimumHeight(100)

        gridLayout.addWidget(self._listView, 0, 0, 2, 2)

        vButtonLayout = QVBoxLayout()

        self._upAction = QAction("\u2191", self, toolTip="Move up")

        self._upButton = QToolButton(self)
        self._upButton.setDefaultAction(self._upAction)
        self._upButton.setSizePolicy(QSizePolicy.Fixed,
                                     QSizePolicy.MinimumExpanding)

        self._downAction = QAction("\u2193", self, toolTip="Move down")

        self._downButton = QToolButton(self)
        self._downButton.setDefaultAction(self._downAction)
        self._downButton.setSizePolicy(QSizePolicy.Fixed,
                                       QSizePolicy.MinimumExpanding)

        vButtonLayout.addWidget(self._upButton)
        vButtonLayout.addWidget(self._downButton)

        gridLayout.addLayout(vButtonLayout, 0, 2, 2, 1)

        hButtonLayout = QHBoxLayout()

        self._addAction = QAction("+", self)
        self._addButton = QToolButton(self)
        self._addButton.setDefaultAction(self._addAction)

        self._removeAction = QAction("-", self)
        self._removeButton = QToolButton(self)
        self._removeButton.setDefaultAction(self._removeAction)
        hButtonLayout.addWidget(self._addButton)
        hButtonLayout.addWidget(self._removeButton)
        hButtonLayout.addStretch(10)
        gridLayout.addLayout(hButtonLayout, 2, 0, 1, 2)

        self.setLayout(gridLayout)
        self._addAction.triggered.connect(self._onAddAction)
        self._removeAction.triggered.connect(self._onRemoveAction)
        self._upAction.triggered.connect(self._onUpAction)
        self._downAction.triggered.connect(self._onDownAction)

    def sizeHint(self):
        size = QWidget.sizeHint(self)
        return QSize(size.width(), 100)

    def _onAddAction(self):
        item = QStandardItem("")
        item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled)
        self._listView.model().appendRow(item)
        self._listView.setCurrentIndex(item.index())
        self._listView.edit(item.index())

    def _onRemoveAction(self):
        current = self._listView.currentIndex()
        self._listView.model().takeRow(current.row())

    def _onUpAction(self):
        row = self._listView.currentIndex().row()
        model = self._listView.model()
        if row > 0:
            items = model.takeRow(row)
            model.insertRow(row - 1, items)
            self._listView.setCurrentIndex(model.index(row - 1, 0))

    def _onDownAction(self):
        row = self._listView.currentIndex().row()
        model = self._listView.model()
        if row < model.rowCount() and row >= 0:
            items = model.takeRow(row)
            if row == model.rowCount():
                model.appendRow(items)
            else:
                model.insertRow(row + 1, items)
            self._listView.setCurrentIndex(model.index(row + 1, 0))

    def setModel(self, model):
        """ Set a model to select items from
        """
        self._model = model
        self._listView.setItemDelegate(self._MyItemDelegate(self._model, self))

    def addItem(self, *args):
        """ Add a new entry in the list
        """
        item = QStandardItem(*args)
        item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled)
        self._listView.model().appendRow(item)

    def setItems(self, items):
        self._listView.model().clear()
        for item in items:
            self.addItem(item)

    def items(self):
        order = []
        for row in range(self._listView.model().rowCount()):
            order.append(str(self._listView.model().item(row, 0).text()))
        return order

    def __changed(self):
        self.sortingOrderChanged.emit()

    sortingOrder = property(items, setItems)