def createEditor(self, parent, options, index): nonlocal owwidget edit = QLineEdit(parent) wordlist = [''] + ToLatLon.valid_values( owwidget.ID_TYPE[owwidget.str_type]) edit.setCompleter( QCompleter(wordlist, edit, caseSensitivity=Qt.CaseInsensitive, filterMode=Qt.MatchContains)) def save_and_commit(): if edit.text() and edit.text() in wordlist: model = index.model() pindex = QPersistentModelIndex(index) if pindex.isValid(): new_index = pindex.sibling(pindex.row(), pindex.column()) save = model.setData(new_index, edit.text(), Qt.EditRole) if save: owwidget.commit() return edit.clear() edit.editingFinished.connect(save_and_commit) return edit
def createEditor(self, p_int, parent): nonlocal owwidget edit = QLineEdit(parent) wordlist = [''] + ToLatLon.valid_values( owwidget.ID_TYPE[owwidget.str_type]) edit.setCompleter( QCompleter(wordlist, edit, caseSensitivity=Qt.CaseInsensitive, filterMode=Qt.MatchContains)) return edit
def createEditor(self, parent, options, index: QModelIndex): model = index.model() # type: EditableTableModel if not model.is_discrete(index.column()): return super().createEditor(parent, options, index) vals = model.discrete_vals(index.column()) edit = QLineEdit(parent) edit.setCompleter(QCompleter(vals, edit, filterMode=Qt.MatchContains)) def save(): if edit.text(): model.setData(index, edit.text()) edit.editingFinished.connect(save) return edit
class OWSelectAttributes(widget.OWWidget): name = "Select Columns" description = "Select columns from the data table and assign them to " \ "data features, classes or meta variables." icon = "icons/SelectColumns.svg" priority = 100 inputs = [("Data", Table, "set_data")] outputs = [("Data", Table), ("Features", widget.AttributeList)] want_main_area = False want_control_area = True settingsHandler = SelectAttributesDomainContextHandler() domain_role_hints = ContextSetting({}) auto_commit = Setting(False) def __init__(self): super().__init__() self.controlArea = QWidget(self.controlArea) self.layout().addWidget(self.controlArea) layout = QGridLayout() self.controlArea.setLayout(layout) layout.setContentsMargins(4, 4, 4, 4) box = gui.vBox(self.controlArea, "Available Variables", addToLayout=False) self.filter_edit = QLineEdit() self.filter_edit.setToolTip("Filter the list of available variables.") box.layout().addWidget(self.filter_edit) if hasattr(self.filter_edit, "setPlaceholderText"): self.filter_edit.setPlaceholderText("Filter") self.completer = QCompleter() self.completer.setCompletionMode(QCompleter.InlineCompletion) self.completer_model = QStringListModel() self.completer.setModel(self.completer_model) self.completer.setModelSorting( QCompleter.CaseSensitivelySortedModel) self.filter_edit.setCompleter(self.completer) self.completer_navigator = CompleterNavigator(self) self.filter_edit.installEventFilter(self.completer_navigator) def dropcompleted(action): if action == Qt.MoveAction: self.commit() self.available_attrs = VariableListModel(enable_dnd=True) self.available_attrs_proxy = VariableFilterProxyModel() self.available_attrs_proxy.setSourceModel(self.available_attrs) self.available_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.available_attrs_view.setModel(self.available_attrs_proxy) aa = self.available_attrs aa.dataChanged.connect(self.update_completer_model) aa.rowsInserted.connect(self.update_completer_model) aa.rowsRemoved.connect(self.update_completer_model) self.available_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.available_attrs_view)) self.available_attrs_view.dragDropActionDidComplete.connect(dropcompleted) self.filter_edit.textChanged.connect(self.update_completer_prefix) self.filter_edit.textChanged.connect( self.available_attrs_proxy.set_filter_string) box.layout().addWidget(self.available_attrs_view) layout.addWidget(box, 0, 0, 3, 1) box = gui.vBox(self.controlArea, "Features", addToLayout=False) self.used_attrs = VariableListModel(enable_dnd=True) self.used_attrs_view = VariablesListItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.used_attrs_view.setModel(self.used_attrs) self.used_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.used_attrs_view)) self.used_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.used_attrs_view) layout.addWidget(box, 0, 2, 1, 1) box = gui.vBox(self.controlArea, "Target Variable", addToLayout=False) self.class_attrs = ClassVarListItemModel(enable_dnd=True) self.class_attrs_view = ClassVariableItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.class_attrs_view.setModel(self.class_attrs) self.class_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.class_attrs_view)) self.class_attrs_view.dragDropActionDidComplete.connect(dropcompleted) self.class_attrs_view.setMaximumHeight(24) box.layout().addWidget(self.class_attrs_view) layout.addWidget(box, 1, 2, 1, 1) box = gui.vBox(self.controlArea, "Meta Attributes", addToLayout=False) self.meta_attrs = VariableListModel(enable_dnd=True) self.meta_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.meta_attrs_view.setModel(self.meta_attrs) self.meta_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.meta_attrs_view)) self.meta_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.meta_attrs_view) layout.addWidget(box, 2, 2, 1, 1) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 0, 1, 1, 1) self.up_attr_button = gui.button(bbox, self, "Up", callback=partial(self.move_up, self.used_attrs_view)) self.move_attr_button = gui.button(bbox, self, ">", callback=partial(self.move_selected, self.used_attrs_view)) self.down_attr_button = gui.button(bbox, self, "Down", callback=partial(self.move_down, self.used_attrs_view)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 1, 1, 1, 1) self.move_class_button = gui.button(bbox, self, ">", callback=partial(self.move_selected, self.class_attrs_view, exclusive=True)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 2, 1, 1, 1) self.up_meta_button = gui.button(bbox, self, "Up", callback=partial(self.move_up, self.meta_attrs_view)) self.move_meta_button = gui.button(bbox, self, ">", callback=partial(self.move_selected, self.meta_attrs_view)) self.down_meta_button = gui.button(bbox, self, "Down", callback=partial(self.move_down, self.meta_attrs_view)) autobox = gui.auto_commit(None, self, "auto_commit", "Send") layout.addWidget(autobox, 3, 0, 1, 3) reset = gui.button(None, self, "Reset", callback=self.reset) autobox.layout().insertWidget(0, self.report_button) autobox.layout().insertWidget(1, reset) autobox.layout().insertSpacing(2, 10) reset.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.report_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) layout.setRowStretch(0, 4) layout.setRowStretch(1, 0) layout.setRowStretch(2, 2) layout.setHorizontalSpacing(0) self.controlArea.setLayout(layout) self.data = None self.output_data = None self.original_completer_items = [] self.resize(500, 600) def set_data(self, data=None): self.update_domain_role_hints() self.closeContext() self.data = data if data is not None: self.openContext(data) all_vars = data.domain.variables + data.domain.metas var_sig = lambda attr: (attr.name, vartype(attr)) domain_hints = {var_sig(attr): ("attribute", i) for i, attr in enumerate(data.domain.attributes)} domain_hints.update({var_sig(attr): ("meta", i) for i, attr in enumerate(data.domain.metas)}) if data.domain.class_vars: domain_hints.update( {var_sig(attr): ("class", i) for i, attr in enumerate(data.domain.class_vars)}) # update the hints from context settings domain_hints.update(self.domain_role_hints) attrs_for_role = lambda role: [ (domain_hints[var_sig(attr)][1], attr) for attr in all_vars if domain_hints[var_sig(attr)][0] == role] attributes = [ attr for place, attr in sorted(attrs_for_role("attribute"), key=lambda a: a[0])] classes = [ attr for place, attr in sorted(attrs_for_role("class"), key=lambda a: a[0])] metas = [ attr for place, attr in sorted(attrs_for_role("meta"), key=lambda a: a[0])] available = [ attr for place, attr in sorted(attrs_for_role("available"), key=lambda a: a[0])] self.used_attrs[:] = attributes self.class_attrs[:] = classes self.meta_attrs[:] = metas self.available_attrs[:] = available else: self.used_attrs[:] = [] self.class_attrs[:] = [] self.meta_attrs[:] = [] self.available_attrs[:] = [] self.unconditional_commit() def update_domain_role_hints(self): """ Update the domain hints to be stored in the widgets settings. """ hints_from_model = lambda role, model: [ ((attr.name, vartype(attr)), (role, i)) for i, attr in enumerate(model)] hints = dict(hints_from_model("available", self.available_attrs)) hints.update(hints_from_model("attribute", self.used_attrs)) hints.update(hints_from_model("class", self.class_attrs)) hints.update(hints_from_model("meta", self.meta_attrs)) self.domain_role_hints = hints def selected_rows(self, view): """ Return the selected rows in the view. """ rows = view.selectionModel().selectedRows() model = view.model() if isinstance(model, QSortFilterProxyModel): rows = [model.mapToSource(r) for r in rows] return [r.row() for r in rows] def move_rows(self, view, rows, offset): model = view.model() newrows = [min(max(0, row + offset), len(model) - 1) for row in rows] for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0): model[row], model[newrow] = model[newrow], model[row] selection = QItemSelection() for nrow in newrows: index = model.index(nrow, 0) selection.select(index, index) view.selectionModel().select( selection, QItemSelectionModel.ClearAndSelect) self.commit() def move_up(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, -1) def move_down(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, 1) def move_selected(self, view, exclusive=False): if self.selected_rows(view): self.move_selected_from_to(view, self.available_attrs_view) elif self.selected_rows(self.available_attrs_view): self.move_selected_from_to(self.available_attrs_view, view, exclusive) def move_selected_from_to(self, src, dst, exclusive=False): self.move_from_to(src, dst, self.selected_rows(src), exclusive) def move_from_to(self, src, dst, rows, exclusive=False): src_model = source_model(src) attrs = [src_model[r] for r in rows] if exclusive and len(attrs) != 1: return for s1, s2 in reversed(list(slices(rows))): del src_model[s1:s2] dst_model = source_model(dst) if exclusive and len(dst_model) > 0: src_model.append(dst_model[0]) del dst_model[0] dst_model.extend(attrs) self.commit() def update_interface_state(self, focus=None, selected=None, deselected=None): for view in [self.available_attrs_view, self.used_attrs_view, self.class_attrs_view, self.meta_attrs_view]: if view is not focus and not view.hasFocus() and self.selected_rows(view): view.selectionModel().clear() def selected_vars(view): model = source_model(view) return [model[i] for i in self.selected_rows(view)] available_selected = selected_vars(self.available_attrs_view) attrs_selected = selected_vars(self.used_attrs_view) class_selected = selected_vars(self.class_attrs_view) meta_selected = selected_vars(self.meta_attrs_view) available_types = set(map(type, available_selected)) all_primitive = all(var.is_primitive() for var in available_types) move_attr_enabled = (available_selected and all_primitive) or \ attrs_selected self.move_attr_button.setEnabled(bool(move_attr_enabled)) if move_attr_enabled: self.move_attr_button.setText(">" if available_selected else "<") move_class_enabled = (len(available_selected) == 1 and all_primitive) or \ class_selected self.move_class_button.setEnabled(bool(move_class_enabled)) if move_class_enabled: self.move_class_button.setText(">" if available_selected else "<") move_meta_enabled = available_selected or meta_selected self.move_meta_button.setEnabled(bool(move_meta_enabled)) if move_meta_enabled: self.move_meta_button.setText(">" if available_selected else "<") def update_completer_model(self, *_): """ This gets called when the model for available attributes changes through either drag/drop or the left/right button actions. """ vars = list(self.available_attrs) items = [var.name for var in vars] items += ["%s=%s" % item for v in vars for item in v.attributes.items()] new = sorted(set(items)) if new != self.original_completer_items: self.original_completer_items = new self.completer_model.setStringList(self.original_completer_items) def update_completer_prefix(self, filter): """ Prefixes all items in the completer model with the current already done completion to enable the completion of multiple keywords. """ prefix = str(self.completer.completionPrefix()) if not prefix.endswith(" ") and " " in prefix: prefix, _ = prefix.rsplit(" ", 1) items = [prefix + " " + item for item in self.original_completer_items] else: items = self.original_completer_items old = list(map(str, self.completer_model.stringList())) if set(old) != set(items): self.completer_model.setStringList(items) def commit(self): self.update_domain_role_hints() if self.data is not None: attributes = list(self.used_attrs) class_var = list(self.class_attrs) metas = list(self.meta_attrs) domain = Orange.data.Domain(attributes, class_var, metas) newdata = self.data.from_table(domain, self.data) self.output_data = newdata self.send("Data", newdata) self.send("Features", widget.AttributeList(attributes)) else: self.output_data = None self.send("Data", None) self.send("Features", None) def reset(self): if self.data is not None: self.available_attrs[:] = [] self.used_attrs[:] = self.data.domain.attributes self.class_attrs[:] = self.data.domain.class_vars self.meta_attrs[:] = self.data.domain.metas self.update_domain_role_hints() self.commit() def send_report(self): if not self.data or not self.output_data: return in_domain, out_domain = self.data.domain, self.output_data.domain self.report_domain("Input data", self.data.domain) if (in_domain.attributes, in_domain.class_vars, in_domain.metas) == ( out_domain.attributes, out_domain.class_vars, out_domain.metas): self.report_paragraph("Output data", "No changes.") else: self.report_domain("Output data", self.output_data.domain) diff = list(set(in_domain.variables + in_domain.metas) - set(out_domain.variables + out_domain.metas)) if diff: text = "%i (%s)" % (len(diff), ", ".join(x.name for x in diff)) self.report_items((("Removed", text),))
class OWSelectAttributes(widget.OWWidget): name = "Select Columns" description = "Select columns from the data table and assign them to " \ "data features, classes or meta variables." icon = "icons/SelectColumns.svg" priority = 100 class Inputs: data = Input("Data", Table) class Outputs: data = Output("Data", Table) features = Output("Features", widget.AttributeList, dynamic=False) want_main_area = False want_control_area = True settingsHandler = SelectAttributesDomainContextHandler() domain_role_hints = ContextSetting({}) auto_commit = Setting(True) def __init__(self): super().__init__() self.controlArea = QWidget(self.controlArea) self.layout().addWidget(self.controlArea) layout = QGridLayout() self.controlArea.setLayout(layout) layout.setContentsMargins(4, 4, 4, 4) box = gui.vBox(self.controlArea, "Available Variables", addToLayout=False) self.filter_edit = QLineEdit() self.filter_edit.setToolTip("Filter the list of available variables.") box.layout().addWidget(self.filter_edit) if hasattr(self.filter_edit, "setPlaceholderText"): self.filter_edit.setPlaceholderText("Filter") self.completer = QCompleter() self.completer.setCompletionMode(QCompleter.InlineCompletion) self.completer_model = QStringListModel() self.completer.setModel(self.completer_model) self.completer.setModelSorting(QCompleter.CaseSensitivelySortedModel) self.filter_edit.setCompleter(self.completer) self.completer_navigator = CompleterNavigator(self) self.filter_edit.installEventFilter(self.completer_navigator) def dropcompleted(action): if action == Qt.MoveAction: self.commit() self.available_attrs = VariableListModel(enable_dnd=True) self.available_attrs_proxy = VariableFilterProxyModel() self.available_attrs_proxy.setSourceModel(self.available_attrs) self.available_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.available_attrs_view.setModel(self.available_attrs_proxy) aa = self.available_attrs aa.dataChanged.connect(self.update_completer_model) aa.rowsInserted.connect(self.update_completer_model) aa.rowsRemoved.connect(self.update_completer_model) self.available_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.available_attrs_view)) self.available_attrs_view.dragDropActionDidComplete.connect( dropcompleted) self.filter_edit.textChanged.connect(self.update_completer_prefix) self.filter_edit.textChanged.connect( self.available_attrs_proxy.set_filter_string) box.layout().addWidget(self.available_attrs_view) layout.addWidget(box, 0, 0, 3, 1) box = gui.vBox(self.controlArea, "Features", addToLayout=False) self.used_attrs = VariableListModel(enable_dnd=True) self.used_attrs_view = VariablesListItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.used_attrs_view.setModel(self.used_attrs) self.used_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.used_attrs_view)) self.used_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.used_attrs_view) layout.addWidget(box, 0, 2, 1, 1) box = gui.vBox(self.controlArea, "Target Variable", addToLayout=False) self.class_attrs = ClassVarListItemModel(enable_dnd=True) self.class_attrs_view = ClassVariableItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.class_attrs_view.setModel(self.class_attrs) self.class_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.class_attrs_view)) self.class_attrs_view.dragDropActionDidComplete.connect(dropcompleted) self.class_attrs_view.setMaximumHeight(72) box.layout().addWidget(self.class_attrs_view) layout.addWidget(box, 1, 2, 1, 1) box = gui.vBox(self.controlArea, "Meta Attributes", addToLayout=False) self.meta_attrs = VariableListModel(enable_dnd=True) self.meta_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.meta_attrs_view.setModel(self.meta_attrs) self.meta_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.meta_attrs_view)) self.meta_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.meta_attrs_view) layout.addWidget(box, 2, 2, 1, 1) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 0, 1, 1, 1) self.up_attr_button = gui.button(bbox, self, "Up", callback=partial( self.move_up, self.used_attrs_view)) self.move_attr_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.used_attrs_view)) self.down_attr_button = gui.button(bbox, self, "Down", callback=partial( self.move_down, self.used_attrs_view)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 1, 1, 1, 1) self.up_class_button = gui.button(bbox, self, "Up", callback=partial( self.move_up, self.class_attrs_view)) self.move_class_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.class_attrs_view, exclusive=False)) self.down_class_button = gui.button(bbox, self, "Down", callback=partial( self.move_down, self.class_attrs_view)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 2, 1, 1, 1) self.up_meta_button = gui.button(bbox, self, "Up", callback=partial( self.move_up, self.meta_attrs_view)) self.move_meta_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.meta_attrs_view)) self.down_meta_button = gui.button(bbox, self, "Down", callback=partial( self.move_down, self.meta_attrs_view)) autobox = gui.auto_commit(None, self, "auto_commit", "Send") layout.addWidget(autobox, 3, 0, 1, 3) reset = gui.button(None, self, "Reset", callback=self.reset) autobox.layout().insertWidget(0, self.report_button) autobox.layout().insertWidget(1, reset) autobox.layout().insertSpacing(2, 10) reset.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.report_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) layout.setRowStretch(0, 4) layout.setRowStretch(1, 0) layout.setRowStretch(2, 2) layout.setHorizontalSpacing(0) self.controlArea.setLayout(layout) self.data = None self.output_data = None self.original_completer_items = [] self.resize(500, 600) @Inputs.data def set_data(self, data=None): self.update_domain_role_hints() self.closeContext() self.data = data if data is not None: self.openContext(data) all_vars = data.domain.variables + data.domain.metas var_sig = lambda attr: (attr.name, vartype(attr)) domain_hints = { var_sig(attr): ("attribute", i) for i, attr in enumerate(data.domain.attributes) } domain_hints.update({ var_sig(attr): ("meta", i) for i, attr in enumerate(data.domain.metas) }) if data.domain.class_vars: domain_hints.update({ var_sig(attr): ("class", i) for i, attr in enumerate(data.domain.class_vars) }) # update the hints from context settings domain_hints.update(self.domain_role_hints) attrs_for_role = lambda role: [ (domain_hints[var_sig(attr)][1], attr) for attr in all_vars if domain_hints[var_sig(attr)][0] == role ] attributes = [ attr for place, attr in sorted(attrs_for_role("attribute"), key=lambda a: a[0]) ] classes = [ attr for place, attr in sorted(attrs_for_role("class"), key=lambda a: a[0]) ] metas = [ attr for place, attr in sorted(attrs_for_role("meta"), key=lambda a: a[0]) ] available = [ attr for place, attr in sorted(attrs_for_role("available"), key=lambda a: a[0]) ] self.used_attrs[:] = attributes self.class_attrs[:] = classes self.meta_attrs[:] = metas self.available_attrs[:] = available else: self.used_attrs[:] = [] self.class_attrs[:] = [] self.meta_attrs[:] = [] self.available_attrs[:] = [] self.unconditional_commit() def update_domain_role_hints(self): """ Update the domain hints to be stored in the widgets settings. """ hints_from_model = lambda role, model: [( (attr.name, vartype(attr)), (role, i)) for i, attr in enumerate(model)] hints = dict(hints_from_model("available", self.available_attrs)) hints.update(hints_from_model("attribute", self.used_attrs)) hints.update(hints_from_model("class", self.class_attrs)) hints.update(hints_from_model("meta", self.meta_attrs)) self.domain_role_hints = hints def selected_rows(self, view): """ Return the selected rows in the view. """ rows = view.selectionModel().selectedRows() model = view.model() if isinstance(model, QSortFilterProxyModel): rows = [model.mapToSource(r) for r in rows] return [r.row() for r in rows] def move_rows(self, view, rows, offset): model = view.model() newrows = [min(max(0, row + offset), len(model) - 1) for row in rows] for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0): model[row], model[newrow] = model[newrow], model[row] selection = QItemSelection() for nrow in newrows: index = model.index(nrow, 0) selection.select(index, index) view.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect) self.commit() def move_up(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, -1) def move_down(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, 1) def move_selected(self, view, exclusive=False): if self.selected_rows(view): self.move_selected_from_to(view, self.available_attrs_view) elif self.selected_rows(self.available_attrs_view): self.move_selected_from_to(self.available_attrs_view, view, exclusive) def move_selected_from_to(self, src, dst, exclusive=False): self.move_from_to(src, dst, self.selected_rows(src), exclusive) def move_from_to(self, src, dst, rows, exclusive=False): src_model = source_model(src) attrs = [src_model[r] for r in rows] for s1, s2 in reversed(list(slices(rows))): del src_model[s1:s2] dst_model = source_model(dst) dst_model.extend(attrs) self.commit() def update_interface_state(self, focus=None, selected=None, deselected=None): for view in [ self.available_attrs_view, self.used_attrs_view, self.class_attrs_view, self.meta_attrs_view ]: if view is not focus and not view.hasFocus( ) and self.selected_rows(view): view.selectionModel().clear() def selected_vars(view): model = source_model(view) return [model[i] for i in self.selected_rows(view)] available_selected = selected_vars(self.available_attrs_view) attrs_selected = selected_vars(self.used_attrs_view) class_selected = selected_vars(self.class_attrs_view) meta_selected = selected_vars(self.meta_attrs_view) available_types = set(map(type, available_selected)) all_primitive = all(var.is_primitive() for var in available_types) move_attr_enabled = (available_selected and all_primitive) or \ attrs_selected self.move_attr_button.setEnabled(bool(move_attr_enabled)) if move_attr_enabled: self.move_attr_button.setText(">" if available_selected else "<") move_class_enabled = (all_primitive and available_selected) or class_selected self.move_class_button.setEnabled(bool(move_class_enabled)) if move_class_enabled: self.move_class_button.setText(">" if available_selected else "<") move_meta_enabled = available_selected or meta_selected self.move_meta_button.setEnabled(bool(move_meta_enabled)) if move_meta_enabled: self.move_meta_button.setText(">" if available_selected else "<") def update_completer_model(self, *_): """ This gets called when the model for available attributes changes through either drag/drop or the left/right button actions. """ vars = list(self.available_attrs) items = [var.name for var in vars] items += [ "%s=%s" % item for v in vars for item in v.attributes.items() ] new = sorted(set(items)) if new != self.original_completer_items: self.original_completer_items = new self.completer_model.setStringList(self.original_completer_items) def update_completer_prefix(self, filter): """ Prefixes all items in the completer model with the current already done completion to enable the completion of multiple keywords. """ prefix = str(self.completer.completionPrefix()) if not prefix.endswith(" ") and " " in prefix: prefix, _ = prefix.rsplit(" ", 1) items = [ prefix + " " + item for item in self.original_completer_items ] else: items = self.original_completer_items old = list(map(str, self.completer_model.stringList())) if set(old) != set(items): self.completer_model.setStringList(items) def commit(self): self.update_domain_role_hints() if self.data is not None: attributes = list(self.used_attrs) class_var = list(self.class_attrs) metas = list(self.meta_attrs) domain = Orange.data.Domain(attributes, class_var, metas) newdata = self.data.transform(domain) self.output_data = newdata self.Outputs.data.send(newdata) self.Outputs.features.send(widget.AttributeList(attributes)) else: self.output_data = None self.Outputs.data.send(None) self.Outputs.features.send(None) def reset(self): if self.data is not None: self.available_attrs[:] = [] self.used_attrs[:] = self.data.domain.attributes self.class_attrs[:] = self.data.domain.class_vars self.meta_attrs[:] = self.data.domain.metas self.update_domain_role_hints() self.commit() def send_report(self): if not self.data or not self.output_data: return in_domain, out_domain = self.data.domain, self.output_data.domain self.report_domain("Input data", self.data.domain) if (in_domain.attributes, in_domain.class_vars, in_domain.metas) == (out_domain.attributes, out_domain.class_vars, out_domain.metas): self.report_paragraph("Output data", "No changes.") else: self.report_domain("Output data", self.output_data.domain) diff = list( set(in_domain.variables + in_domain.metas) - set(out_domain.variables + out_domain.metas)) if diff: text = "%i (%s)" % (len(diff), ", ".join(x.name for x in diff)) self.report_items((("Removed", text), ))
class OWDatabasesUpdate(OWWidget): name = "Databases Update" description = "Update local systems biology databases." icon = "../widgets/icons/Databases.svg" priority = 10 inputs = [] outputs = [] want_main_area = False def __init__(self, parent=None, signalManager=None, name="Databases update"): OWWidget.__init__(self, parent, signalManager, name, wantMainArea=False) self.searchString = "" fbox = gui.widgetBox(self.controlArea, "Filter") self.completer = TokenListCompleter( self, caseSensitivity=Qt.CaseInsensitive) self.lineEditFilter = QLineEdit(textChanged=self.SearchUpdate) self.lineEditFilter.setCompleter(self.completer) fbox.layout().addWidget(self.lineEditFilter) box = gui.widgetBox(self.controlArea, "Files") self.filesView = QTreeWidget(self) self.filesView.setHeaderLabels( ["", "Data Source", "Update", "Last Updated", "Size"]) self.filesView.setRootIsDecorated(False) self.filesView.setUniformRowHeights(True) self.filesView.setSelectionMode(QAbstractItemView.NoSelection) self.filesView.setSortingEnabled(True) self.filesView.sortItems(1, Qt.AscendingOrder) self.filesView.setItemDelegateForColumn( 0, UpdateOptionsItemDelegate(self.filesView)) self.filesView.model().layoutChanged.connect(self.SearchUpdate) box.layout().addWidget(self.filesView) box = gui.widgetBox(self.controlArea, orientation="horizontal") self.updateButton = gui.button( box, self, "Update all", callback=self.UpdateAll, tooltip="Update all updatable files", ) self.downloadButton = gui.button( box, self, "Download all", callback=self.DownloadFiltered, tooltip="Download all filtered files shown" ) self.cancelButton = gui.button( box, self, "Cancel", callback=self.Cancel, tooltip="Cancel scheduled downloads/updates." ) self.retryButton = gui.button( box, self, "Reconnect", callback=self.RetrieveFilesList ) self.retryButton.hide() gui.rubber(box) self.warning(0) box = gui.widgetBox(self.controlArea, orientation="horizontal") gui.rubber(box) self.infoLabel = QLabel() self.infoLabel.setAlignment(Qt.AlignCenter) self.controlArea.layout().addWidget(self.infoLabel) self.infoLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.updateItems = [] self.resize(800, 600) self.progress = ProgressState(self, maximum=3) self.progress.valueChanged.connect(self._updateProgress) self.progress.rangeChanged.connect(self._updateProgress) self.executor = ThreadExecutor( threadPool=QThreadPool(maxThreadCount=2) ) task = Task(self, function=self.RetrieveFilesList) task.exceptionReady.connect(self.HandleError) task.start() self._tasks = [] self._haveProgress = False def RetrieveFilesList(self): self.retryButton.hide() self.warning(0) self.progress.setRange(0, 3) task = Task(function=partial(retrieveFilesList, methodinvoke(self.progress, "advance"))) task.resultReady.connect(self.SetFilesList) task.exceptionReady.connect(self.HandleError) self.executor.submit(task) self.setEnabled(False) def SetFilesList(self, serverInfo): """ Set the files to show. """ self.setEnabled(True) localInfo = serverfiles.allinfo() all_tags = set() self.filesView.clear() self.updateItems = [] for item in join_info_dict(localInfo, serverInfo): tree_item = UpdateTreeWidgetItem(item) options_widget = UpdateOptionsWidget(item.state) options_widget.item = item options_widget.installClicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) options_widget.removeClicked.connect( partial(self.SubmitRemoveTask, item.domain, item.filename) ) self.updateItems.append((item, tree_item, options_widget)) all_tags.update(item.tags) self.filesView.addTopLevelItems( [tree_item for _, tree_item, _ in self.updateItems] ) for item, tree_item, options_widget in self.updateItems: self.filesView.setItemWidget(tree_item, 0, options_widget) # Add an update button if the file is updateable if item.state == OUTDATED: button = QToolButton( None, text="Update", maximumWidth=120, minimumHeight=20, maximumHeight=20 ) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) button.clicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) self.filesView.setItemWidget(tree_item, 2, button) self.progress.advance() self.filesView.setColumnWidth(0, self.filesView.sizeHintForColumn(0)) for column in range(1, 4): contents_hint = self.filesView.sizeHintForColumn(column) header_hint = self.filesView.header().sectionSizeHint(column) width = max(min(contents_hint, 400), header_hint) self.filesView.setColumnWidth(column, width) hints = [hint for hint in sorted(all_tags) if not hint.startswith("#")] self.completer.setTokenList(hints) self.SearchUpdate() self.UpdateInfoLabel() self.toggleButtons() self.cancelButton.setEnabled(False) self.progress.setRange(0, 0) def buttonCheck(self, selected_items, state, button): for item in selected_items: if item.state != state: button.setEnabled(False) else: button.setEnabled(True) break def toggleButtons(self): selected_items = [item for item, tree_item, _ in self.updateItems if not tree_item.isHidden()] self.buttonCheck(selected_items, OUTDATED, self.updateButton) self.buttonCheck(selected_items, AVAILABLE, self.downloadButton) def HandleError(self, exception): if isinstance(exception, ConnectionError): self.warning(0, "Could not connect to server! Check your connection " "and try to reconnect.") self.SetFilesList({}) self.retryButton.show() else: sys.excepthook(type(exception), exception, None) self.progress.setRange(0, 0) self.setEnabled(True) def UpdateInfoLabel(self): local = [item for item, tree_item, _ in self.updateItems if item.state != AVAILABLE and not tree_item.isHidden()] size = sum(float(item.size) for item in local) onServer = [item for item, tree_item, _ in self.updateItems if not tree_item.isHidden()] sizeOnServer = sum(float(item.size) for item in onServer) text = ("%i items, %s (on server: %i items, %s)" % (len(local), sizeof_fmt(size), len(onServer), sizeof_fmt(sizeOnServer))) self.infoLabel.setText(text) def UpdateAll(self): self.warning(0) for item, tree_item, _ in self.updateItems: if item.state == OUTDATED and not tree_item.isHidden(): self.SubmitDownloadTask(item.domain, item.filename) def DownloadFiltered(self): # TODO: submit items in the order shown. for item, tree_item, _ in self.updateItems: if not tree_item.isHidden() and item.state in \ [AVAILABLE, OUTDATED]: self.SubmitDownloadTask(item.domain, item.filename) def SearchUpdate(self, searchString=None): strings = str(self.lineEditFilter.text()).split() for item, tree_item, _ in self.updateItems: hide = not all(UpdateItem_match(item, string) for string in strings) tree_item.setHidden(hide) self.UpdateInfoLabel() self.toggleButtons() def SubmitDownloadTask(self, domain, filename): """ Submit the (domain, filename) to be downloaded/updated. """ self.cancelButton.setEnabled(True) index = self.updateItemIndex(domain, filename) _, tree_item, opt_widget = self.updateItems[index] sf = LocalFiles(serverfiles.PATH, serverfiles.ServerFiles()) task = DownloadTask(domain, filename, sf) self.progress.adjustRange(0, 100) pb = ItemProgressBar(self.filesView) pb.setRange(0, 100) pb.setTextVisible(False) task.advanced.connect(pb.advance) task.advanced.connect(self.progress.advance) task.finished.connect(pb.hide) task.finished.connect(self.onDownloadFinished, Qt.QueuedConnection) task.exception.connect(self.onDownloadError, Qt.QueuedConnection) self.filesView.setItemWidget(tree_item, 2, pb) # Clear the text so it does not show behind the progress bar. tree_item.setData(2, Qt.DisplayRole, "") pb.show() # Disable the options widget opt_widget.setEnabled(False) self._tasks.append(task) self.executor.submit(task) def EndDownloadTask(self, task): future = task.future() index = self.updateItemIndex(task.domain, task.filename) item, tree_item, opt_widget = self.updateItems[index] self.filesView.removeItemWidget(tree_item, 2) opt_widget.setEnabled(True) if future.cancelled(): # Restore the previous state tree_item.setUpdateItem(item) opt_widget.setState(item.state) elif future.exception(): tree_item.setUpdateItem(item) opt_widget.setState(item.state) # Show the exception string in the size column. self.warning(0, "Error while downloading. Check your connection " "and retry.") # recreate button for download button = QToolButton( None, text="Retry", maximumWidth=120, minimumHeight=20, maximumHeight=20 ) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) button.clicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) self.filesView.setItemWidget(tree_item, 2, button) else: # get the new updated info dict and replace the the old item self.warning(0) info = serverfiles.info(item.domain, item.filename) new_item = update_item_from_info(item.domain, item.filename, info, info) self.updateItems[index] = (new_item, tree_item, opt_widget) tree_item.setUpdateItem(new_item) opt_widget.setState(new_item.state) self.UpdateInfoLabel() def SubmitRemoveTask(self, domain, filename): serverfiles.LOCALFILES.remove(domain, filename) index = self.updateItemIndex(domain, filename) item, tree_item, opt_widget = self.updateItems[index] if item.info_server: new_item = item._replace(state=AVAILABLE, local=None, info_local=None) else: new_item = item._replace(local=None, info_local=None) # Disable the options widget. No more actions can be performed # for the item. opt_widget.setEnabled(False) tree_item.setUpdateItem(new_item) opt_widget.setState(new_item.state) self.updateItems[index] = (new_item, tree_item, opt_widget) self.UpdateInfoLabel() def Cancel(self): """ Cancel all pending update/download tasks (that have not yet started). """ for task in self._tasks: task.future().cancel() def onDeleteWidget(self): self.Cancel() self.executor.shutdown(wait=False) OWWidget.onDeleteWidget(self) def onDownloadFinished(self): # on download completed/canceled/error assert QThread.currentThread() is self.thread() for task in list(self._tasks): future = task.future() if future.done(): self.EndDownloadTask(task) self._tasks.remove(task) if not self._tasks: # Clear/reset the overall progress self.progress.setRange(0, 0) self.cancelButton.setEnabled(False) def onDownloadError(self, exc_info): sys.excepthook(*exc_info) self.warning(0, "Error while downloading. Check your connection and " "retry.") def updateItemIndex(self, domain, filename): for i, (item, _, _) in enumerate(self.updateItems): if item.domain == domain and item.filename == filename: return i raise ValueError("%r, %r not in update list" % (domain, filename)) def _updateProgress(self, *args): rmin, rmax = self.progress.range() if rmin != rmax: if not self._haveProgress: self._haveProgress = True self.progressBarInit() self.progressBarSet(self.progress.ratioCompleted() * 100, processEvents=None) if rmin == rmax: self._haveProgress = False self.progressBarFinished()
def variables_filter(model, parent=None): """ GUI components: ListView with a lineedit which works as a filter. One can write a variable name in a edit box and possible matches are then shown in a listview. """ def update_completer_model(): """ This gets called when the model for available attributes changes through either drag/drop or the left/right button actions. """ nonlocal original_completer_items items = [var.name for var in model] items += [ "%s=%s" % item for v in model for item in v.attributes.items() ] new = sorted(set(items)) if new != original_completer_items: original_completer_items = new completer_model.setStringList(original_completer_items) def update_completer_prefix(): """ Prefixes all items in the completer model with the current already done completion to enable the completion of multiple keywords. """ nonlocal original_completer_items prefix = str(completer.completionPrefix()) if not prefix.endswith(" ") and " " in prefix: prefix, _ = prefix.rsplit(" ", 1) items = [prefix + " " + item for item in original_completer_items] else: items = original_completer_items old = list(map(str, completer_model.stringList())) if set(old) != set(items): completer_model.setStringList(items) original_completer_items = [] filter_edit = QLineEdit() filter_edit.setToolTip("Filter the list of available variables.") filter_edit.setPlaceholderText("Filter") completer_model = QStringListModel() completer = QCompleter(completer_model) completer.setCompletionMode(QCompleter.InlineCompletion) completer.setModelSorting(QCompleter.CaseSensitivelySortedModel) filter_edit.setCompleter(completer) completer_navigator = CompleterNavigator(parent) filter_edit.installEventFilter(completer_navigator) proxy = VariableFilterProxyModel() proxy.setSourceModel(model) view = VariablesListItemView(acceptedType=Orange.data.Variable) view.setModel(proxy) model.dataChanged.connect(update_completer_model) model.rowsInserted.connect(update_completer_model) model.rowsRemoved.connect(update_completer_model) filter_edit.textChanged.connect(update_completer_prefix) filter_edit.textChanged.connect(proxy.set_filter_string) return filter_edit, view
class SelectionSetsWidget(QFrame): """ Widget for managing multiple stored item selections """ selectionModified = Signal(bool) def __init__(self, parent): QFrame.__init__(self, parent) self.setContentsMargins(0, 0, 0, 0) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(1) self._setNameLineEdit = QLineEdit(self) layout.addWidget(self._setNameLineEdit) self._setListView = QListView(self) self._listModel = QStandardItemModel(self) self._proxyModel = QSortFilterProxyModel(self) self._proxyModel.setSourceModel(self._listModel) self._setListView.setModel(self._proxyModel) self._setListView.setItemDelegate(ListItemDelegate(self)) self._setNameLineEdit.textChanged.connect( self._proxyModel.setFilterFixedString) self._completer = QCompleter(self._listModel, self) self._setNameLineEdit.setCompleter(self._completer) self._listModel.itemChanged.connect(self._onSetNameChange) layout.addWidget(self._setListView) buttonLayout = QHBoxLayout() self._addAction = QAction( "+", self, toolTip="Add a new sort key") self._updateAction = QAction( "Update", self, toolTip="Update/save current selection") self._removeAction = QAction( "\u2212", self, toolTip="Remove selected sort key.") self._addToolButton = QToolButton(self) self._updateToolButton = QToolButton(self) self._removeToolButton = QToolButton(self) self._updateToolButton.setSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) self._addToolButton.setDefaultAction(self._addAction) self._updateToolButton.setDefaultAction(self._updateAction) self._removeToolButton.setDefaultAction(self._removeAction) buttonLayout.addWidget(self._addToolButton) buttonLayout.addWidget(self._updateToolButton) buttonLayout.addWidget(self._removeToolButton) layout.addLayout(buttonLayout) self.setLayout(layout) self._addAction.triggered.connect(self.addCurrentSelection) self._updateAction.triggered.connect(self.updateSelectedSelection) self._removeAction.triggered.connect(self.removeSelectedSelection) self._setListView.selectionModel().selectionChanged.connect( self._onListViewSelectionChanged) self.selectionModel = None self._selections = [] def sizeHint(self): size = QFrame.sizeHint(self) return QSize(size.width(), 150) def _onSelectionChanged(self, selected, deselected): self.setSelectionModified(True) def _onListViewSelectionChanged(self, selected, deselected): try: index = self._setListView.selectedIndexes()[0] except IndexError: return self.commitSelection(self._proxyModel.mapToSource(index).row()) def _onSetNameChange(self, item): self.selections[item.row()].name = str(item.text()) def _setButtonStates(self, val): self._updateToolButton.setEnabled(val) def setSelectionModel(self, selectionModel): if self.selectionModel: self.selectionModel.selectionChanged.disconnect( self._onSelectionChanged) self.selectionModel = selectionModel self.selectionModel.selectionChanged.connect(self._onSelectionChanged) def addCurrentSelection(self): item = self.addSelection( SelectionByKey(self.selectionModel.selection(), name="New selection", key=(1, 2, 3, 10))) index = self._proxyModel.mapFromSource(item.index()) self._setListView.setCurrentIndex(index) self._setListView.edit(index) self.setSelectionModified(False) def removeSelectedSelection(self): i = self._proxyModel.mapToSource(self._setListView.currentIndex()).row() self._listModel.takeRow(i) del self.selections[i] def updateCurentSelection(self): i = self._proxyModel.mapToSource(self._setListView.selectedIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def addSelection(self, selection, name=""): self._selections.append(selection) item = QStandardItem(selection.name) item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listModel.appendRow(item) self.setSelectionModified(False) return item def updateSelectedSelection(self): i = self._proxyModel.mapToSource(self._setListView.currentIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def setSelectionModified(self, val): self._selectionModified = val self._setButtonStates(val) self.selectionModified.emit(bool(val)) def commitSelection(self, index): selection = self.selections[index] selection.select(self.selectionModel) def setSelections(self, selections): self._listModel.clear() for selection in selections: self.addSelection(selection) def selections(self): return self._selections selections = property(selections, setSelections)
class OWDatabasesUpdate(OWWidget): name = "Databases Update" description = "Update local systems biology databases." icon = "../widgets/icons/OWDatabasesUpdate.svg" priority = 1 inputs = [] outputs = [] want_main_area = False def __init__(self, parent=None, signalManager=None, name="Databases update"): OWWidget.__init__(self, parent, signalManager, name, wantMainArea=False) self.searchString = "" fbox = gui.widgetBox(self.controlArea, "Filter") self.completer = TokenListCompleter(self, caseSensitivity=Qt.CaseInsensitive) self.lineEditFilter = QLineEdit(textChanged=self.search_update) self.lineEditFilter.setCompleter(self.completer) fbox.layout().addWidget(self.lineEditFilter) box = gui.widgetBox(self.controlArea, "Files") self.filesView = QTreeWidget(self) self.filesView.setHeaderLabels(header_labels) self.filesView.setRootIsDecorated(False) self.filesView.setUniformRowHeights(True) self.filesView.setSelectionMode(QAbstractItemView.NoSelection) self.filesView.setSortingEnabled(True) self.filesView.sortItems(header.Title, Qt.AscendingOrder) self.filesView.setItemDelegateForColumn( 0, UpdateOptionsItemDelegate(self.filesView)) self.filesView.model().layoutChanged.connect(self.search_update) box.layout().addWidget(self.filesView) layout = QHBoxLayout() gui.widgetBox(self.controlArea, margin=0, orientation=layout) self.updateButton = gui.button( box, self, "Update all", callback=self.update_all, tooltip="Update all updatable files", ) self.downloadButton = gui.button( box, self, "Download all", callback=self.download_filtered, tooltip="Download all filtered files shown") self.cancelButton = gui.button( box, self, "Cancel", callback=self.cancel_active_threads, tooltip="Cancel scheduled downloads/updates.") self.addButton = gui.button(box, self, "Add ...", callback=self.__handle_dialog, tooltip="Add files for personal use.") layout.addWidget(self.updateButton) layout.addWidget(self.downloadButton) layout.addWidget(self.cancelButton) layout.addStretch() layout.addWidget(self.addButton) # Enable retryButton once connection is established # self.retryButton = gui.button( # box, self, "Reconnect", callback=self.initialize_files_view # ) # self.retryButton.hide() self.resize(800, 600) self.update_items = [] self._dialog = None self.progress_bar = None # threads self.threadpool = QThreadPool(self) #self.threadpool.setMaxThreadCount(1) self.workers = list() self.initialize_files_view() def __handle_dialog(self): if not self._dialog: self._dialog = FileUploadHelper(self) self._dialog.show() def __progress_advance(self): # GUI should be updated in main thread. That's why we are calling advance method here if self.progress_bar: self.progress_bar.advance() def handle_worker_exception(self, ex): self.progress_bar.finish() self.setStatusMessage('') if isinstance(ex, ConnectionError): # TODO: set warning messages pass print(ex) def initialize_files_view(self): # self.retryButton.hide() # clear view self.filesView.clear() # init progress bar self.progress_bar = gui.ProgressBar(self, iterations=3) # status message self.setStatusMessage('initializing') worker = Worker(evaluate_files_state, progress_callback=True) worker.signals.progress.connect(self.__progress_advance) worker.signals.result.connect(self.set_files_list) worker.signals.error.connect(self.handle_worker_exception) # move download process to worker thread self.threadpool.start(worker) self.setEnabled(False) def __create_action_button(self, fs, retry=None): if not fs.state not in [OUTDATED, USER_FILE] or not retry: self.filesView.setItemWidget(fs.tree_item, header.Update, None) button = QToolButton(None) if not retry: if fs.state == OUTDATED: button.setText('Update') button.clicked.connect( partial(self.submit_download_task, fs.domain, fs.filename, True)) elif fs.state == USER_FILE: if not fs.info_server: button.setText('Remove') button.clicked.connect( partial(self.submit_remove_task, fs.domain, fs.filename)) else: button.setText('Use server version') button.clicked.connect( partial(self.submit_download_task, fs.domain, fs.filename, True)) else: button.setText('Retry') button.clicked.connect( partial(self.submit_download_task, fs.domain, fs.filename, True)) button.setMaximumWidth(120) button.setMaximumHeight(20) button.setMinimumHeight(20) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) self.filesView.setItemWidget(fs.tree_item, header.Update, button) def set_files_list(self, result): """ Set the files to show. """ assert threading.current_thread() == threading.main_thread() self.progress_bar.finish() self.setStatusMessage('') self.setEnabled(True) self.update_items = result all_tags = set() for fs in self.update_items: fs.tree_item = FileStateItem(fs) fs.download_option = DownloadOption(state=fs.state) fs.download_option.download_clicked.connect( partial(self.submit_download_task, fs.domain, fs.filename)) fs.download_option.remove_clicked.connect( partial(self.submit_remove_task, fs.domain, fs.filename)) # add widget items to the QTreeWidget self.filesView.addTopLevelItems( [fs.tree_item for fs in self.update_items]) # add action widgets to tree items for fs in self.update_items: self.filesView.setItemWidget(fs.tree_item, header.Download, fs.download_option) if fs.state in [USER_FILE, OUTDATED]: self.__create_action_button(fs) all_tags.update(fs.tags) self.filesView.setColumnWidth( header.Download, self.filesView.sizeHintForColumn(header.Download)) for column in range(1, len(header_labels)): self.filesView.resizeColumnToContents(column) hints = [hint for hint in sorted(all_tags) if not hint.startswith("#")] self.completer.setTokenList(hints) self.search_update() self.toggle_action_buttons() self.cancelButton.setEnabled(False) def toggle_action_buttons(self): selected_items = [ fs for fs in self.update_items if not fs.tree_item.isHidden() ] def button_check(sel_items, state, button): for item in sel_items: if item.state != state: button.setEnabled(False) else: button.setEnabled(True) break button_check(selected_items, OUTDATED, self.updateButton) button_check(selected_items, AVAILABLE, self.downloadButton) def search_update(self, searchString=None): strings = str(self.lineEditFilter.text()).split() for fs in self.update_items: hide = not all(UpdateItem_match(fs, string) for string in strings) fs.tree_item.setHidden(hide) self.toggle_action_buttons() def update_all(self): for fs in self.update_items: if fs.state == OUTDATED and not fs.tree_item.isHidden(): self.submit_download_task(fs.domain, fs.filename) def download_filtered(self): for fs in self.update_items: if not fs.tree_item.isHidden() and fs.state in [ AVAILABLE, OUTDATED ]: self.submit_download_task(fs.domain, fs.filename, start=False) self.run_download_tasks() def submit_download_task(self, domain, filename, start=True): """ Submit the (domain, filename) to be downloaded/updated. """ # get selected tree item index = self.tree_item_index(domain, filename) fs = self.update_items[index] worker = Worker(download_server_file, fs, index, progress_callback=True) worker.signals.progress.connect(self.__progress_advance) worker.signals.result.connect(self.on_download_finished) worker.signals.error.connect(self.on_download_exception) self.workers.append(worker) if start: self.run_download_tasks() def run_download_tasks(self): self.cancelButton.setEnabled(True) # init progress bar self.progress_bar = gui.ProgressBar(self, iterations=len(self.workers) * 100) # status message self.setStatusMessage('downloading') # move workers to threadpool [self.threadpool.start(worker) for worker in self.workers] self.filesView.setDisabled(True) # reset list of workers self.workers = list() def on_download_exception(self, ex): assert threading.current_thread() == threading.main_thread() self.progress_bar.finish() self.setStatusMessage('') print(ex) if isinstance(ex, ValueError): fs, index = ex.args # restore state and retry fs.refresh_state() fs.tree_item.update_data(fs) fs.download_option.state = fs.state self.__create_action_button(fs, retry=True) def on_download_finished(self, result): assert threading.current_thread() == threading.main_thread() # We check if all workers have completed. If not, continue if self.progress_bar.count == 100 or self.threadpool.activeThreadCount( ) == 0: self.filesView.setDisabled(False) self.progress_bar.finish() self.setStatusMessage('') fs, index = result # re-evaluate File State info = serverfiles.info(fs.domain, fs.filename) fs.refresh_state(info_local=info, info_server=info) # reinitialize treeWidgetItem fs.tree_item.update_data(fs) # reinitialize OptionWidget fs.download_option.state = fs.state self.filesView.setItemWidget(fs.tree_item, header.Update, None) self.toggle_action_buttons() for column in range(1, len(header_labels)): self.filesView.resizeColumnToContents(column) def submit_remove_task(self, domain, filename): serverfiles.LOCALFILES.remove(domain, filename) index = self.tree_item_index(domain, filename) fs = self.update_items[index] if fs.state == USER_FILE: self.filesView.takeTopLevelItem( self.filesView.indexOfTopLevelItem(fs.tree_item)) self.update_items.remove(fs) # self.filesView.removeItemWidget(index) else: # refresh item state fs.info_local = None fs.refresh_state() # reinitialize treeWidgetItem fs.tree_item.update_data(fs) # reinitialize OptionWidget fs.download_option.state = fs.state self.toggle_action_buttons() def cancel_active_threads(self): """ Cancel all pending update/download tasks (that have not yet started). """ if self.threadpool: self.threadpool.clear() def tree_item_index(self, domain, filename): for i, fs in enumerate(self.update_items): if fs.domain == domain and fs.filename == filename: return i raise ValueError("%r, %r not in update list" % (domain, filename)) def onDeleteWidget(self): self.cancel_active_threads() OWWidget.onDeleteWidget(self)
class OWGEODatasets(OWWidget): name = "GEO Data Sets" description = DESCRIPTION icon = "../widgets/icons/GEODataSets.svg" priority = PRIORITY inputs = [] outputs = [("Expression Data", Orange.data.Table)] settingsList = [ "outputRows", "mergeSpots", "gdsSelectionStates", "splitterSettings", "currentGds", "autoCommit", "datasetNames" ] outputRows = Setting(True) mergeSpots = Setting(True) gdsSelectionStates = Setting({}) currentGds = Setting(None) datasetNames = Setting({}) splitterSettings = Setting(( b'\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\xea\x00\x00\x00\xd7\x01\x00\x00\x00\x07\x01\x00\x00\x00\x02', b'\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\xb5\x00\x00\x02\x10\x01\x00\x00\x00\x07\x01\x00\x00\x00\x01' )) autoCommit = Setting(False) def __init__(self, parent=None, signalManager=None, name=" GEO Data Sets"): OWWidget.__init__(self, parent, signalManager, name) self.selectionChanged = False self.filterString = "" self.datasetName = "" ## GUI box = gui.widgetBox(self.controlArea, "Info", addSpace=True) self.infoBox = gui.widgetLabel(box, "Initializing\n\n") box = gui.widgetBox(self.controlArea, "Output", addSpace=True) gui.radioButtonsInBox(box, self, "outputRows", ["Genes in rows", "Samples in rows"], "Rows", callback=self.commitIf) gui.checkBox(box, self, "mergeSpots", "Merge spots of same gene", callback=self.commitIf) gui.separator(box) self.nameEdit = gui.lineEdit( box, self, "datasetName", "Data set name", tooltip="Override the default output data set name", callback=self.onNameEdited) self.nameEdit.setPlaceholderText("") if sys.version_info < (3, ): box = gui.widgetBox(self.controlArea, "Commit", addSpace=True) self.commitButton = gui.button(box, self, "Commit", callback=self.commit) cb = gui.checkBox(box, self, "autoCommit", "Commit on any change") gui.setStopper(self, self.commitButton, cb, "selectionChanged", self.commit) else: gui.auto_commit(self.controlArea, self, "autoCommit", "Commit", box="Commit") self.commitIf = self.commit gui.rubber(self.controlArea) gui.widgetLabel(self.mainArea, "Filter") self.filterLineEdit = QLineEdit(textChanged=self.filter) self.completer = TokenListCompleter(self, caseSensitivity=Qt.CaseInsensitive) self.filterLineEdit.setCompleter(self.completer) self.mainArea.layout().addWidget(self.filterLineEdit) splitter = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(splitter) self.treeWidget = QTreeView(splitter) self.treeWidget.setSelectionMode(QTreeView.SingleSelection) self.treeWidget.setRootIsDecorated(False) self.treeWidget.setSortingEnabled(True) self.treeWidget.setAlternatingRowColors(True) self.treeWidget.setUniformRowHeights(True) self.treeWidget.setEditTriggers(QTreeView.NoEditTriggers) linkdelegate = LinkStyledItemDelegate(self.treeWidget) self.treeWidget.setItemDelegateForColumn(1, linkdelegate) self.treeWidget.setItemDelegateForColumn(8, linkdelegate) self.treeWidget.setItemDelegateForColumn( 0, gui.IndicatorItemDelegate(self.treeWidget, role=Qt.DisplayRole)) proxyModel = MySortFilterProxyModel(self.treeWidget) self.treeWidget.setModel(proxyModel) self.treeWidget.selectionModel().selectionChanged.connect( self.updateSelection) self.treeWidget.viewport().setMouseTracking(True) splitterH = QSplitter(Qt.Horizontal, splitter) box = gui.widgetBox(splitterH, "Description") self.infoGDS = gui.widgetLabel(box, "") self.infoGDS.setWordWrap(True) gui.rubber(box) box = gui.widgetBox(splitterH, "Sample Annotations") self.annotationsTree = QTreeWidget(box) self.annotationsTree.setHeaderLabels( ["Type (Sample annotations)", "Sample count"]) self.annotationsTree.setRootIsDecorated(True) box.layout().addWidget(self.annotationsTree) self.annotationsTree.itemChanged.connect( self.annotationSelectionChanged) self._annotationsUpdating = False self.splitters = splitter, splitterH for sp, setting in zip(self.splitters, self.splitterSettings): sp.splitterMoved.connect(self.splitterMoved) sp.restoreState(setting) self.searchKeys = [ "dataset_id", "title", "platform_organism", "description" ] self.gds = [] self.gds_info = None self.resize(1000, 600) self.setBlocking(True) self.setEnabled(False) self.progressBarInit() self._executor = ThreadExecutor() func = partial(get_gds_model, methodinvoke(self, "_setProgress", (float, ))) self._inittask = Task(function=func) self._inittask.finished.connect(self._initializemodel) self._executor.submit(self._inittask) self._datatask = None @Slot(float) def _setProgress(self, value): self.progressBarValue = value def _initializemodel(self): assert self.thread() is QThread.currentThread() model, self.gds_info, self.gds = self._inittask.result() model.setParent(self) proxy = self.treeWidget.model() proxy.setFilterKeyColumn(0) proxy.setFilterRole(TextFilterRole) proxy.setFilterCaseSensitivity(False) proxy.setFilterFixedString(self.filterString) proxy.setSourceModel(model) proxy.sort(0, Qt.DescendingOrder) self.progressBarFinished() self.setBlocking(False) self.setEnabled(True) filter_items = " ".join(gds[key] for gds in self.gds for key in self.searchKeys) tr_chars = ",.:;!?(){}[]_-+\\|/%#@$^&*<>~`" tr_table = str.maketrans(tr_chars, " " * len(tr_chars)) filter_items = filter_items.translate(tr_table) filter_items = sorted(set(filter_items.split(" "))) filter_items = [item for item in filter_items if len(item) > 3] self.completer.setTokenList(filter_items) if self.currentGds: current_id = self.currentGds["dataset_id"] gdss = [(i, qunpack(proxy.data(proxy.index(i, 1), Qt.DisplayRole))) for i in range(proxy.rowCount())] current = [i for i, data in gdss if data and data == current_id] if current: current_index = proxy.index(current[0], 0) self.treeWidget.selectionModel().select( current_index, QItemSelectionModel.Select | QItemSelectionModel.Rows) self.treeWidget.scrollTo(current_index, QTreeView.PositionAtCenter) for i in range(8): self.treeWidget.resizeColumnToContents(i) self.treeWidget.setColumnWidth( 1, min(self.treeWidget.columnWidth(1), 300)) self.treeWidget.setColumnWidth( 2, min(self.treeWidget.columnWidth(2), 200)) self.updateInfo() def updateInfo(self): gds_info = self.gds_info text = ("%i datasets\n%i datasets cached\n" % (len(gds_info), len(glob.glob(serverfiles.localpath("GEO") + "/GDS*")))) filtered = self.treeWidget.model().rowCount() if len(self.gds) != filtered: text += ("%i after filtering") % filtered self.infoBox.setText(text) def updateSelection(self, *args): current = self.treeWidget.selectedIndexes() mapToSource = self.treeWidget.model().mapToSource current = [mapToSource(index).row() for index in current] if current: self.currentGds = self.gds[current[0]] self.setAnnotations(self.currentGds) self.infoGDS.setText(self.currentGds.get("description", "")) self.nameEdit.setPlaceholderText(self.currentGds["title"]) self.datasetName = \ self.datasetNames.get(self.currentGds["dataset_id"], "") else: self.currentGds = None self.nameEdit.setPlaceholderText("") self.datasetName = "" self.commitIf() def setAnnotations(self, gds): self._annotationsUpdating = True self.annotationsTree.clear() annotations = defaultdict(set) subsetscount = {} for desc in gds["subsets"]: annotations[desc["type"]].add(desc["description"]) subsetscount[desc["description"]] = str(len(desc["sample_id"])) for type, subsets in annotations.items(): key = (gds["dataset_id"], type) subsetItem = QTreeWidgetItem(self.annotationsTree, [type]) subsetItem.setFlags(subsetItem.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsTristate) subsetItem.setCheckState( 0, self.gdsSelectionStates.get(key, Qt.Checked)) subsetItem.key = key for subset in subsets: key = (gds["dataset_id"], type, subset) item = QTreeWidgetItem( subsetItem, [subset, subsetscount.get(subset, "")]) item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState( 0, self.gdsSelectionStates.get(key, Qt.Checked)) item.key = key self._annotationsUpdating = False self.annotationsTree.expandAll() for i in range(self.annotationsTree.columnCount()): self.annotationsTree.resizeColumnToContents(i) def annotationSelectionChanged(self, item, column): if self._annotationsUpdating: return for i in range(self.annotationsTree.topLevelItemCount()): item = self.annotationsTree.topLevelItem(i) self.gdsSelectionStates[item.key] = item.checkState(0) for j in range(item.childCount()): child = item.child(j) self.gdsSelectionStates[child.key] = child.checkState(0) def filter(self): filter_string = unicode(self.filterLineEdit.text()) proxyModel = self.treeWidget.model() if proxyModel: strings = filter_string.lower().strip().split() proxyModel.setFilterFixedStrings(strings) self.updateInfo() def selectedSamples(self): """ Return the currently selected sample annotations. The return value is a list of selected (sample type, sample value) tuples. .. note:: if some Sample annotation type has no selected values. this method will return all values for it. """ samples = [] unused_types = [] used_types = [] for stype in childiter(self.annotationsTree.invisibleRootItem()): selected_values = [] all_values = [] for sval in childiter(stype): value = (str(stype.text(0)), str(sval.text(0))) if self.gdsSelectionStates.get(sval.key, True): selected_values.append(value) all_values.append(value) if selected_values: samples.extend(selected_values) used_types.append(str(stype.text(0))) else: # If no sample of sample type is selected we don't filter # on it. samples.extend(all_values) unused_types.append(str(stype.text(0))) return samples, used_types def commitIf(self): if self.autoCommit: self.commit() else: self.selectionChanged = True @Slot(int, int) def progressCompleted(self, value, total): if total > 0: self.progressBarSet(100. * value / total, processEvents=False) else: pass # TODO: report 'indeterminate progress' def commit(self): if self.currentGds: self.error(0) sample_type = None self.progressBarInit(processEvents=None) _, groups = self.selectedSamples() if len(groups) == 1 and self.outputRows: sample_type = groups[0] self.setEnabled(False) self.setBlocking(True) progress = methodinvoke(self, "progressCompleted", (int, int)) def get_data(gds_id, report_genes, transpose, sample_type, title): gds_ensure_downloaded(gds_id, progress) gds = geo.GDS(gds_id) data = gds.getdata(report_genes=report_genes, transpose=transpose, sample_type=sample_type) data.name = title return data get_data = partial(get_data, self.currentGds["dataset_id"], report_genes=self.mergeSpots, transpose=self.outputRows, sample_type=sample_type, title=self.datasetName or self.currentGds["title"]) self._datatask = Task(function=get_data) self._datatask.finished.connect(self._on_dataready) self._executor.submit(self._datatask) def _on_dataready(self): self.setEnabled(True) self.setBlocking(False) self.progressBarFinished(processEvents=False) try: data = self._datatask.result() except urlrequest.URLError as error: self.error(0, ("Error while connecting to the NCBI ftp server! " "'%s'" % error)) sys.excepthook(type(error), error, getattr(error, "__traceback__")) return finally: self._datatask = None data_name = data.name samples, _ = self.selectedSamples() self.warning(0) message = None if self.outputRows: def samplesinst(ex): out = [] for meta in data.domain.metas: out.append((meta.name, ex[meta].value)) if data.domain.class_var.name != 'class': out.append((data.domain.class_var.name, ex[data.domain.class_var].value)) return out samples = set(samples) mask = [samples.issuperset(samplesinst(ex)) for ex in data] data = data[numpy.array(mask, dtype=bool)] if len(data) == 0: message = "No samples with selected sample annotations." else: samples = set(samples) domain = Orange.data.Domain([ attr for attr in data.domain.attributes if samples.issuperset(attr.attributes.items()) ], data.domain.class_var, data.domain.metas) # domain.addmetas(data.domain.getmetas()) if len(domain.attributes) == 0: message = "No samples with selected sample annotations." stypes = set(s[0] for s in samples) for attr in domain.attributes: attr.attributes = dict( (key, value) for key, value in attr.attributes.items() if key in stypes) data = Orange.data.Table(domain, data) if message is not None: self.warning(0, message) data_hints.set_hint(data, "taxid", self.currentGds.get("taxid", ""), 10.0) data_hints.set_hint(data, "genesinrows", self.outputRows, 10.0) data.name = data_name self.send("Expression Data", data) model = self.treeWidget.model().sourceModel() row = self.gds.index(self.currentGds) model.setData(model.index(row, 0), " ", Qt.DisplayRole) self.updateInfo() self.selectionChanged = False def splitterMoved(self, *args): self.splitterSettings = [ bytes(sp.saveState()) for sp in self.splitters ] def send_report(self): self.report_items("GEO Dataset", [("ID", self.currentGds['dataset_id']), ("Title", self.currentGds['title']), ("Organism", self.currentGds['sample_organism'])]) self.report_items("Data", [("Samples", self.currentGds['sample_count']), ("Features", self.currentGds['feature_count']), ("Genes", self.currentGds['gene_count'])]) self.report_name("Sample annotations") subsets = defaultdict(list) for subset in self.currentGds['subsets']: subsets[subset['type']].append( (subset['description'], len(subset['sample_id']))) self.report_html += "<ul>" for type in subsets: self.report_html += "<b>" + type + ":</b></br>" for desc, count in subsets[type]: self.report_html += 9 * " " + "<b>{}:</b> {}</br>".format( desc, count) self.report_html += "</ul>" def onDeleteWidget(self): if self._inittask: self._inittask.future().cancel() self._inittask.finished.disconnect(self._initializemodel) if self._datatask: self._datatask.future().cancel() self._datatask.finished.disconnect(self._on_dataready) self._executor.shutdown(wait=False) super(OWGEODatasets, self).onDeleteWidget() def onNameEdited(self): if self.currentGds: gds_id = self.currentGds["dataset_id"] self.datasetNames[gds_id] = unicode(self.nameEdit.text()) self.commitIf()
class OWDatabasesUpdate(OWWidget): name = "Databases Update" description = "Update local systems biology databases." icon = "../widgets/icons/OWDatabasesUpdate.svg" priority = 1 inputs = [] outputs = [] want_main_area = False def __init__(self, parent=None, signalManager=None, name="Databases update"): OWWidget.__init__(self, parent, signalManager, name, wantMainArea=False) self.searchString = "" fbox = gui.widgetBox(self.controlArea, "Filter") self.completer = TokenListCompleter( self, caseSensitivity=Qt.CaseInsensitive) self.lineEditFilter = QLineEdit(textChanged=self.SearchUpdate) self.lineEditFilter.setCompleter(self.completer) fbox.layout().addWidget(self.lineEditFilter) box = gui.widgetBox(self.controlArea, "Files") self.filesView = QTreeWidget(self) self.filesView.setHeaderLabels( ["", "Data Source", "Update", "Last Updated", "Size"]) self.filesView.setRootIsDecorated(False) self.filesView.setUniformRowHeights(True) self.filesView.setSelectionMode(QAbstractItemView.NoSelection) self.filesView.setSortingEnabled(True) self.filesView.sortItems(1, Qt.AscendingOrder) self.filesView.setItemDelegateForColumn( 0, UpdateOptionsItemDelegate(self.filesView)) self.filesView.model().layoutChanged.connect(self.SearchUpdate) box.layout().addWidget(self.filesView) box = gui.widgetBox(self.controlArea, orientation="horizontal") self.updateButton = gui.button( box, self, "Update all", callback=self.UpdateAll, tooltip="Update all updatable files", ) self.downloadButton = gui.button( box, self, "Download all", callback=self.DownloadFiltered, tooltip="Download all filtered files shown" ) self.cancelButton = gui.button( box, self, "Cancel", callback=self.Cancel, tooltip="Cancel scheduled downloads/updates." ) self.retryButton = gui.button( box, self, "Reconnect", callback=self.RetrieveFilesList ) self.retryButton.hide() gui.rubber(box) self.warning(0) box = gui.widgetBox(self.controlArea, orientation="horizontal") gui.rubber(box) self.infoLabel = QLabel() self.infoLabel.setAlignment(Qt.AlignCenter) self.controlArea.layout().addWidget(self.infoLabel) self.infoLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.updateItems = [] self.resize(800, 600) self.progress = ProgressState(self, maximum=3) self.progress.valueChanged.connect(self._updateProgress) self.progress.rangeChanged.connect(self._updateProgress) self.executor = ThreadExecutor( threadPool=QThreadPool(maxThreadCount=2) ) task = Task(self, function=self.RetrieveFilesList) task.exceptionReady.connect(self.HandleError) task.start() self._tasks = [] self._haveProgress = False def RetrieveFilesList(self): self.retryButton.hide() self.warning(0) self.progress.setRange(0, 3) task = Task(function=partial(retrieveFilesList, methodinvoke(self.progress, "advance"))) task.resultReady.connect(self.SetFilesList) task.exceptionReady.connect(self.HandleError) self.executor.submit(task) self.setEnabled(False) def SetFilesList(self, serverInfo): """ Set the files to show. """ self.setEnabled(True) localInfo = serverfiles.allinfo() all_tags = set() self.filesView.clear() self.updateItems = [] for item in join_info_dict(localInfo, serverInfo): tree_item = UpdateTreeWidgetItem(item) options_widget = UpdateOptionsWidget(item.state) options_widget.item = item options_widget.installClicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) options_widget.removeClicked.connect( partial(self.SubmitRemoveTask, item.domain, item.filename) ) self.updateItems.append((item, tree_item, options_widget)) all_tags.update(item.tags) self.filesView.addTopLevelItems( [tree_item for _, tree_item, _ in self.updateItems] ) for item, tree_item, options_widget in self.updateItems: self.filesView.setItemWidget(tree_item, 0, options_widget) # Add an update button if the file is updateable if item.state == OUTDATED: button = QToolButton( None, text="Update", maximumWidth=120, minimumHeight=20, maximumHeight=20 ) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) button.clicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) self.filesView.setItemWidget(tree_item, 2, button) self.progress.advance() self.filesView.setColumnWidth(0, self.filesView.sizeHintForColumn(0)) for column in range(1, 4): contents_hint = self.filesView.sizeHintForColumn(column) header_hint = self.filesView.header().sectionSizeHint(column) width = max(min(contents_hint, 400), header_hint) self.filesView.setColumnWidth(column, width) hints = [hint for hint in sorted(all_tags) if not hint.startswith("#")] self.completer.setTokenList(hints) self.SearchUpdate() self.UpdateInfoLabel() self.toggleButtons() self.cancelButton.setEnabled(False) self.progress.setRange(0, 0) def buttonCheck(self, selected_items, state, button): for item in selected_items: if item.state != state: button.setEnabled(False) else: button.setEnabled(True) break def toggleButtons(self): selected_items = [item for item, tree_item, _ in self.updateItems if not tree_item.isHidden()] self.buttonCheck(selected_items, OUTDATED, self.updateButton) self.buttonCheck(selected_items, AVAILABLE, self.downloadButton) def HandleError(self, exception): if isinstance(exception, ConnectionError): self.warning(0, "Could not connect to server! Check your connection " "and try to reconnect.") self.SetFilesList({}) self.retryButton.show() else: sys.excepthook(type(exception), exception, None) self.progress.setRange(0, 0) self.setEnabled(True) def UpdateInfoLabel(self): local = [item for item, tree_item, _ in self.updateItems if item.state != AVAILABLE and not tree_item.isHidden()] size = sum(float(item.size) for item in local) onServer = [item for item, tree_item, _ in self.updateItems if not tree_item.isHidden()] sizeOnServer = sum(float(item.size) for item in onServer) text = ("%i items, %s (on server: %i items, %s)" % (len(local), serverfiles.sizeformat(size), len(onServer), serverfiles.sizeformat(sizeOnServer))) self.infoLabel.setText(text) def UpdateAll(self): self.warning(0) for item, tree_item, _ in self.updateItems: if item.state == OUTDATED and not tree_item.isHidden(): self.SubmitDownloadTask(item.domain, item.filename) def DownloadFiltered(self): # TODO: submit items in the order shown. for item, tree_item, _ in self.updateItems: if not tree_item.isHidden() and item.state in \ [AVAILABLE, OUTDATED]: self.SubmitDownloadTask(item.domain, item.filename) def SearchUpdate(self, searchString=None): strings = str(self.lineEditFilter.text()).split() for item, tree_item, _ in self.updateItems: hide = not all(UpdateItem_match(item, string) for string in strings) tree_item.setHidden(hide) self.UpdateInfoLabel() self.toggleButtons() def SubmitDownloadTask(self, domain, filename): """ Submit the (domain, filename) to be downloaded/updated. """ self.cancelButton.setEnabled(True) index = self.updateItemIndex(domain, filename) _, tree_item, opt_widget = self.updateItems[index] task = DownloadTask(domain, filename, serverfiles.LOCALFILES) self.progress.adjustRange(0, 100) pb = ItemProgressBar(self.filesView) pb.setRange(0, 100) pb.setTextVisible(False) task.advanced.connect(pb.advance) task.advanced.connect(self.progress.advance) task.finished.connect(pb.hide) task.finished.connect(self.onDownloadFinished, Qt.QueuedConnection) task.exception.connect(self.onDownloadError, Qt.QueuedConnection) self.filesView.setItemWidget(tree_item, 2, pb) # Clear the text so it does not show behind the progress bar. tree_item.setData(2, Qt.DisplayRole, "") pb.show() # Disable the options widget opt_widget.setEnabled(False) self._tasks.append(task) self.executor.submit(task) def EndDownloadTask(self, task): future = task.future() index = self.updateItemIndex(task.domain, task.filename) item, tree_item, opt_widget = self.updateItems[index] self.filesView.removeItemWidget(tree_item, 2) opt_widget.setEnabled(True) if future.cancelled(): # Restore the previous state tree_item.setUpdateItem(item) opt_widget.setState(item.state) elif future.exception(): tree_item.setUpdateItem(item) opt_widget.setState(item.state) # Show the exception string in the size column. self.warning(0, "Error while downloading. Check your connection " "and retry.") # recreate button for download button = QToolButton( None, text="Retry", maximumWidth=120, minimumHeight=20, maximumHeight=20 ) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) button.clicked.connect( partial(self.SubmitDownloadTask, item.domain, item.filename) ) self.filesView.setItemWidget(tree_item, 2, button) else: # get the new updated info dict and replace the the old item self.warning(0) info = serverfiles.info(item.domain, item.filename) new_item = update_item_from_info(item.domain, item.filename, info, info) self.updateItems[index] = (new_item, tree_item, opt_widget) tree_item.setUpdateItem(new_item) opt_widget.setState(new_item.state) self.UpdateInfoLabel() def SubmitRemoveTask(self, domain, filename): serverfiles.LOCALFILES.remove(domain, filename) index = self.updateItemIndex(domain, filename) item, tree_item, opt_widget = self.updateItems[index] if item.info_server: new_item = item._replace(state=AVAILABLE, local=None, info_local=None) else: new_item = item._replace(local=None, info_local=None) # Disable the options widget. No more actions can be performed # for the item. opt_widget.setEnabled(False) tree_item.setUpdateItem(new_item) opt_widget.setState(new_item.state) self.updateItems[index] = (new_item, tree_item, opt_widget) self.UpdateInfoLabel() def Cancel(self): """ Cancel all pending update/download tasks (that have not yet started). """ for task in self._tasks: task.future().cancel() def onDeleteWidget(self): self.Cancel() self.executor.shutdown(wait=False) OWWidget.onDeleteWidget(self) def onDownloadFinished(self): # on download completed/canceled/error assert QThread.currentThread() is self.thread() for task in list(self._tasks): future = task.future() if future.done(): self.EndDownloadTask(task) self._tasks.remove(task) if not self._tasks: # Clear/reset the overall progress self.progress.setRange(0, 0) self.cancelButton.setEnabled(False) def onDownloadError(self, exc_info): sys.excepthook(*exc_info) self.warning(0, "Error while downloading. Check your connection and " "retry.") def updateItemIndex(self, domain, filename): for i, (item, _, _) in enumerate(self.updateItems): if item.domain == domain and item.filename == filename: return i raise ValueError("%r, %r not in update list" % (domain, filename)) def _updateProgress(self, *args): rmin, rmax = self.progress.range() if rmin != rmax: if not self._haveProgress: self._haveProgress = True self.progressBarInit() self.progressBarSet(self.progress.ratioCompleted() * 100, processEvents=None) if rmin == rmax: self._haveProgress = False self.progressBarFinished()
class OWSetEnrichment(widget.OWWidget): name = "Set Enrichment" description = "" icon = "../widgets/icons/GeneSetEnrichment.svg" priority = 5000 inputs = [("Data", Orange.data.Table, "setData", widget.Default), ("Reference", Orange.data.Table, "setReference")] outputs = [("Data subset", Orange.data.Table)] settingsHandler = settings.DomainContextHandler() taxid = settings.ContextSetting(None) speciesIndex = settings.ContextSetting(0) genesinrows = settings.ContextSetting(False) geneattr = settings.ContextSetting(0) categoriesCheckState = settings.ContextSetting({}) useReferenceData = settings.Setting(False) useMinCountFilter = settings.Setting(True) useMaxPValFilter = settings.Setting(True) useMaxFDRFilter = settings.Setting(True) minClusterCount = settings.Setting(3) maxPValue = settings.Setting(0.01) maxFDR = settings.Setting(0.01) autocommit = settings.Setting(False) Ready, Initializing, Loading, RunningEnrichment = 0, 1, 2, 4 def __init__(self, parent=None): super().__init__(parent) self.geneMatcherSettings = [False, False, True, False] self.data = None self.referenceData = None self.taxid_list = [] self.__genematcher = (None, fulfill(gene.matcher([]))) self.__invalidated = False self.currentAnnotatedCategories = [] self.state = None self.__state = OWSetEnrichment.Initializing box = gui.widgetBox(self.controlArea, "Info") self.infoBox = gui.widgetLabel(box, "Info") self.infoBox.setText("No data on input.\n") self.speciesComboBox = gui.comboBox( self.controlArea, self, "speciesIndex", "Species", callback=self.__on_speciesIndexChanged) box = gui.widgetBox(self.controlArea, "Entity names") self.geneAttrComboBox = gui.comboBox(box, self, "geneattr", "Entity feature", sendSelectedValue=0, callback=self.updateAnnotations) cb = gui.checkBox(box, self, "genesinrows", "Use feature names", callback=self.updateAnnotations, disables=[(-1, self.geneAttrComboBox)]) cb.makeConsistent() # gui.button(box, self, "Gene matcher settings", # callback=self.updateGeneMatcherSettings, # tooltip="Open gene matching settings dialog") self.referenceRadioBox = gui.radioButtonsInBox( self.controlArea, self, "useReferenceData", ["All entities", "Reference set (input)"], tooltips=[ "Use entire genome (for gene set enrichment) or all " + "available entities for reference", "Use entities from Reference Examples input signal " + "as reference" ], box="Reference", callback=self.updateAnnotations) box = gui.widgetBox(self.controlArea, "Entity Sets") self.groupsWidget = QTreeWidget(self) self.groupsWidget.setHeaderLabels(["Category"]) box.layout().addWidget(self.groupsWidget) hLayout = QHBoxLayout() hLayout.setSpacing(10) hWidget = gui.widgetBox(self.mainArea, orientation=hLayout) gui.spin(hWidget, self, "minClusterCount", 0, 100, label="Entities", tooltip="Minimum entity count", callback=self.filterAnnotationsChartView, callbackOnReturn=True, checked="useMinCountFilter", checkCallback=self.filterAnnotationsChartView) pvalfilterbox = gui.widgetBox(hWidget, orientation="horizontal") cb = gui.checkBox(pvalfilterbox, self, "useMaxPValFilter", "p-value", callback=self.filterAnnotationsChartView) sp = gui.doubleSpin( pvalfilterbox, self, "maxPValue", 0.0, 1.0, 0.0001, tooltip="Maximum p-value", callback=self.filterAnnotationsChartView, callbackOnReturn=True, ) sp.setEnabled(self.useMaxFDRFilter) cb.toggled[bool].connect(sp.setEnabled) pvalfilterbox.layout().setAlignment(cb, Qt.AlignRight) pvalfilterbox.layout().setAlignment(sp, Qt.AlignLeft) fdrfilterbox = gui.widgetBox(hWidget, orientation="horizontal") cb = gui.checkBox(fdrfilterbox, self, "useMaxFDRFilter", "FDR", callback=self.filterAnnotationsChartView) sp = gui.doubleSpin( fdrfilterbox, self, "maxFDR", 0.0, 1.0, 0.0001, tooltip="Maximum False discovery rate", callback=self.filterAnnotationsChartView, callbackOnReturn=True, ) sp.setEnabled(self.useMaxFDRFilter) cb.toggled[bool].connect(sp.setEnabled) fdrfilterbox.layout().setAlignment(cb, Qt.AlignRight) fdrfilterbox.layout().setAlignment(sp, Qt.AlignLeft) self.filterLineEdit = QLineEdit(self, placeholderText="Filter ...") self.filterCompleter = QCompleter(self.filterLineEdit) self.filterCompleter.setCaseSensitivity(Qt.CaseInsensitive) self.filterLineEdit.setCompleter(self.filterCompleter) hLayout.addWidget(self.filterLineEdit) self.mainArea.layout().addWidget(hWidget) self.filterLineEdit.textChanged.connect( self.filterAnnotationsChartView) self.annotationsChartView = QTreeView( alternatingRowColors=True, sortingEnabled=True, selectionMode=QTreeView.ExtendedSelection, rootIsDecorated=False, editTriggers=QTreeView.NoEditTriggers, ) self.annotationsChartView.viewport().setMouseTracking(True) self.mainArea.layout().addWidget(self.annotationsChartView) contextEventFilter = gui.VisibleHeaderSectionContextEventFilter( self.annotationsChartView) self.annotationsChartView.header().installEventFilter( contextEventFilter) self.groupsWidget.itemClicked.connect(self.subsetSelectionChanged) gui.auto_commit(self.controlArea, self, "autocommit", "Commit") self.setBlocking(True) task = EnsureDownloaded([(taxonomy.Taxonomy.DOMAIN, taxonomy.Taxonomy.FILENAME), (geneset.sfdomain, "index.pck")]) task.finished.connect(self.__initialize_finish) self.setStatusMessage("Initializing") self._executor = ThreadExecutor(parent=self, threadPool=QThreadPool(self)) self._executor.submit(task) def sizeHint(self): return QSize(1024, 600) def __initialize_finish(self): # Finalize the the widget's initialization (preferably after # ensuring all required databases have been downloaded. sets = geneset.list_all() taxids = set(taxonomy.common_taxids() + list(filter(None, [tid for _, tid, _ in sets]))) organisms = [(tid, name_or_none(tid)) for tid in taxids] organisms = [(tid, name) for tid, name in organisms if name is not None] organisms = [(None, "None")] + sorted(organisms) taxids = [tid for tid, _ in organisms] names = [name for _, name in organisms] self.taxid_list = taxids self.speciesComboBox.clear() self.speciesComboBox.addItems(names) self.genesets = sets if self.taxid in self.taxid_list: taxid = self.taxid else: taxid = self.taxid_list[0] self.taxid = None self.setCurrentOrganism(taxid) self.setBlocking(False) self.__state = OWSetEnrichment.Ready self.setStatusMessage("") def setCurrentOrganism(self, taxid): """Set the current organism `taxid`.""" if taxid not in self.taxid_list: taxid = self.taxid_list[min(self.speciesIndex, len(self.taxid_list) - 1)] if self.taxid != taxid: self.taxid = taxid self.speciesIndex = self.taxid_list.index(taxid) self.refreshHierarchy() self._invalidateGeneMatcher() self._invalidate() def currentOrganism(self): """Return the current organism taxid""" return self.taxid def __on_speciesIndexChanged(self): taxid = self.taxid_list[self.speciesIndex] self.taxid = "< Do not look >" self.setCurrentOrganism(taxid) if self.__invalidated and self.data is not None: self.updateAnnotations() def clear(self): """Clear/reset the widget state.""" self._cancelPending() self.state = None self.__state = self.__state & ~OWSetEnrichment.RunningEnrichment self._clearView() if self.annotationsChartView.model() is not None: self.annotationsChartView.model().clear() self.geneAttrComboBox.clear() self.geneAttrs = [] self._updatesummary() def _cancelPending(self): """Cancel pending tasks.""" if self.state is not None: self.state.results.cancel() self.state.namematcher.cancel() self.state.cancelled = True def _clearView(self): """Clear the enrichment report view (main area).""" if self.annotationsChartView.model() is not None: self.annotationsChartView.model().clear() def setData(self, data=None): """Set the input dataset with query gene names""" if self.__state & OWSetEnrichment.Initializing: self.__initialize_finish() self.error(0) self.closeContext() self.clear() self.groupsWidget.clear() self.data = data if data is not None: varlist = [ var for var in data.domain.variables + data.domain.metas if isinstance(var, Orange.data.StringVariable) ] self.geneAttrs = varlist for var in varlist: self.geneAttrComboBox.addItem(*gui.attributeItem(var)) oldtaxid = self.taxid self.geneattr = min(self.geneattr, len(self.geneAttrs) - 1) taxid = data_hints.get_hint(data, "taxid", "") if taxid in self.taxid_list: self.speciesIndex = self.taxid_list.index(taxid) self.taxid = taxid self.genesinrows = data_hints.get_hint(data, "genesinrows", self.genesinrows) self.openContext(data) if oldtaxid != self.taxid: self.taxid = "< Do not look >" self.setCurrentOrganism(taxid) self.refreshHierarchy() self._invalidate() def setReference(self, data=None): """Set the (optional) input dataset with reference gene names.""" self.referenceData = data self.referenceRadioBox.setEnabled(bool(data)) if self.useReferenceData: self._invalidate() def handleNewSignals(self): if self.__invalidated: self.updateAnnotations() def _invalidateGeneMatcher(self): _, f = self.__genematcher f.cancel() self.__genematcher = (None, fulfill(gene.matcher([]))) def _invalidate(self): self.__invalidated = True def genesFromTable(self, table): if self.genesinrows: genes = [attr.name for attr in table.domain.attributes] else: geneattr = self.geneAttrs[self.geneattr] genes = [str(ex[geneattr]) for ex in table] return genes def getHierarchy(self, taxid): def recursive_dict(): return defaultdict(recursive_dict) collection = recursive_dict() def collect(col, hier): if hier: collect(col[hier[0]], hier[1:]) for hierarchy, t_id, _ in self.genesets: collect(collection[t_id], hierarchy) return (taxid, collection[taxid]), (None, collection[None]) def setHierarchy(self, hierarchy, hierarchy_noorg): self.groupsWidgetItems = {} def fill(col, parent, full=(), org=""): for key, value in sorted(col.items()): full_cat = full + (key, ) item = QTreeWidgetItem(parent, [key]) item.setFlags(item.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsEnabled) if value: item.setFlags(item.flags() | Qt.ItemIsTristate) checked = self.categoriesCheckState.get((full_cat, org), Qt.Checked) item.setData(0, Qt.CheckStateRole, checked) item.setExpanded(True) item.category = full_cat item.organism = org self.groupsWidgetItems[full_cat] = item fill(value, item, full_cat, org=org) self.groupsWidget.clear() fill(hierarchy[1], self.groupsWidget, org=hierarchy[0]) fill(hierarchy_noorg[1], self.groupsWidget, org=hierarchy_noorg[0]) def refreshHierarchy(self): self.setHierarchy(*self.getHierarchy( taxid=self.taxid_list[self.speciesIndex])) def selectedCategories(self): """ Return a list of currently selected hierarchy keys. A key is a tuple of identifiers from the root to the leaf of the hierarchy tree. """ return [ key for key, check in self.getHierarchyCheckState().items() if check == Qt.Checked ] def getHierarchyCheckState(self): def collect(item, full=()): checked = item.checkState(0) name = str(item.data(0, Qt.DisplayRole)) full_cat = full + (name, ) result = [((full_cat, item.organism), checked)] for i in range(item.childCount()): result.extend(collect(item.child(i), full_cat)) return result items = [ self.groupsWidget.topLevelItem(i) for i in range(self.groupsWidget.topLevelItemCount()) ] states = itertools.chain(*(collect(item) for item in items)) return dict(states) def subsetSelectionChanged(self, item, column): # The selected geneset (hierarchy) subset has been changed by the # user. Update the displayed results. # Update the stored state (persistent settings) self.categoriesCheckState = self.getHierarchyCheckState() categories = self.selectedCategories() if self.data is not None: if self._nogenematching() or \ not set(categories) <= set(self.currentAnnotatedCategories): self.updateAnnotations() else: self.filterAnnotationsChartView() def updateGeneMatcherSettings(self): raise NotImplementedError from .OWGOEnrichmentAnalysis import GeneMatcherDialog dialog = GeneMatcherDialog(self, defaults=self.geneMatcherSettings, enabled=[True] * 4, modal=True) if dialog.exec_(): self.geneMatcherSettings = [ getattr(dialog, item[0]) for item in dialog.items ] self._invalidateGeneMatcher() if self.data is not None: self.updateAnnotations() def _genematcher(self): """ Return a Future[gene.SequenceMatcher] """ taxid = self.taxid_list[self.speciesIndex] current, matcher_f = self.__genematcher if taxid == current and \ not matcher_f.cancelled(): return matcher_f self._invalidateGeneMatcher() if taxid is None: self.__genematcher = (None, fulfill(gene.matcher([]))) return self.__genematcher[1] matchers = [gene.GMGO, gene.GMKEGG, gene.GMNCBI, gene.GMAffy] matchers = [ m for m, use in zip(matchers, self.geneMatcherSettings) if use ] def create(): return gene.matcher([m(taxid) for m in matchers]) matcher_f = self._executor.submit(create) self.__genematcher = (taxid, matcher_f) return self.__genematcher[1] def _nogenematching(self): return self.taxid is None or not any(self.geneMatcherSettings) def updateAnnotations(self): if self.data is None: return assert not self.__state & OWSetEnrichment.Initializing self._cancelPending() self._clearView() self.information(0) self.warning(0) self.error(0) if not self.genesinrows and len(self.geneAttrs) == 0: self.error(0, "Input data contains no columns with gene names") return self.__state = OWSetEnrichment.RunningEnrichment taxid = self.taxid_list[self.speciesIndex] self.taxid = taxid categories = self.selectedCategories() clusterGenes = self.genesFromTable(self.data) if self.referenceData is not None and self.useReferenceData: referenceGenes = self.genesFromTable(self.referenceData) else: referenceGenes = None self.currentAnnotatedCategories = categories genematcher = self._genematcher() self.progressBarInit() ## Load collections in a worker thread # TODO: Use cached collections if already loaded and # use ensure_genesetsdownloaded with progress report (OWSelectGenes) collections = self._executor.submit(geneset.collections, *categories) def refset_null(): """Return the default background reference set""" col = collections.result() return reduce(operator.ior, (set(g.genes) for g in col), set()) def refset_ncbi(): """Return all NCBI gene names""" geneinfo = gene.NCBIGeneInfo(taxid) return set(geneinfo.keys()) def namematcher(): matcher = genematcher.result() match = matcher.set_targets(ref_set.result()) match.umatch = memoize(match.umatch) return match def map_unames(): matcher = namematcher.result() query = list(filter(None, map(matcher.umatch, querynames))) reference = list( filter(None, map(matcher.umatch, ref_set.result()))) return query, reference if self._nogenematching(): if referenceGenes is None: ref_set = self._executor.submit(refset_null) else: ref_set = fulfill(referenceGenes) else: if referenceGenes == None: ref_set = self._executor.submit(refset_ncbi) else: ref_set = fulfill(referenceGenes) namematcher = self._executor.submit(namematcher) querynames = clusterGenes state = types.SimpleNamespace() state.query_set = clusterGenes state.reference_set = referenceGenes state.namematcher = namematcher state.query_count = len(set(clusterGenes)) state.reference_count = (len(set(referenceGenes)) if referenceGenes is not None else None) state.cancelled = False progress = methodinvoke(self, "_setProgress", (float, )) info = methodinvoke(self, "_setRunInfo", (str, )) @withtraceback def run(): info("Loading data") match = namematcher.result() query, reference = map_unames() gscollections = collections.result() results = [] info("Running enrichment") p = 0 for i, gset in enumerate(gscollections): genes = set(filter(None, map(match.umatch, gset.genes))) enr = set_enrichment(genes, reference, query) results.append((gset, enr)) if state.cancelled: raise UserInteruptException pnew = int(100 * i / len(gscollections)) if pnew != p: progress(pnew) p = pnew progress(100) info("") return query, reference, results task = Task(function=run) task.resultReady.connect(self.__on_enrichment_finished) task.exceptionReady.connect(self.__on_enrichment_failed) result = self._executor.submit(task) state.results = result self.state = state self._updatesummary() def __on_enrichment_failed(self, exception): if not isinstance(exception, UserInteruptException): print("ERROR:", exception, file=sys.stderr) print(exception._traceback, file=sys.stderr) self.progressBarFinished() self.setStatusMessage("") self.__state &= ~OWSetEnrichment.RunningEnrichment def __on_enrichment_finished(self, results): assert QThread.currentThread() is self.thread() self.__state &= ~OWSetEnrichment.RunningEnrichment query, reference, results = results if self.annotationsChartView.model(): self.annotationsChartView.model().clear() nquery = len(query) nref = len(reference) maxcount = max((len(e.query_mapped) for _, e in results), default=1) maxrefcount = max((len(e.reference_mapped) for _, e in results), default=1) nspaces = int(math.ceil(math.log10(maxcount or 1))) refspaces = int(math.ceil(math.log(maxrefcount or 1))) query_fmt = "%" + str(nspaces) + "s (%.2f%%)" ref_fmt = "%" + str(refspaces) + "s (%.2f%%)" def fmt_count(fmt, count, total): return fmt % (count, 100.0 * count / (total or 1)) fmt_query_count = partial(fmt_count, query_fmt) fmt_ref_count = partial(fmt_count, ref_fmt) linkFont = QFont(self.annotationsChartView.viewOptions().font) linkFont.setUnderline(True) def item(value=None, tooltip=None, user=None): si = QStandardItem() if value is not None: si.setData(value, Qt.DisplayRole) if tooltip is not None: si.setData(tooltip, Qt.ToolTipRole) if user is not None: si.setData(user, Qt.UserRole) else: si.setData(value, Qt.UserRole) return si model = QStandardItemModel() model.setSortRole(Qt.UserRole) model.setHorizontalHeaderLabels([ "Category", "Term", "Count", "Reference count", "p-value", "FDR", "Enrichment" ]) for i, (gset, enrich) in enumerate(results): if len(enrich.query_mapped) == 0: continue nquery_mapped = len(enrich.query_mapped) nref_mapped = len(enrich.reference_mapped) row = [ item(", ".join(gset.hierarchy)), item(gsname(gset), tooltip=gset.link), item(fmt_query_count(nquery_mapped, nquery), tooltip=nquery_mapped, user=nquery_mapped), item(fmt_ref_count(nref_mapped, nref), tooltip=nref_mapped, user=nref_mapped), item(fmtp(enrich.p_value), user=enrich.p_value), item( ), # column 5, FDR, is computed in filterAnnotationsChartView item(enrich.enrichment_score, tooltip="%.3f" % enrich.enrichment_score, user=enrich.enrichment_score) ] row[0].geneset = gset row[0].enrichment = enrich row[1].setData(gset.link, gui.LinkRole) row[1].setFont(linkFont) row[1].setForeground(QColor(Qt.blue)) model.appendRow(row) self.annotationsChartView.setModel(model) self.annotationsChartView.selectionModel().selectionChanged.connect( self.commit) if not model.rowCount(): self.warning(0, "No enriched sets found.") else: self.warning(0) allnames = set( gsname(geneset) for geneset, (count, _, _, _) in results if count) allnames |= reduce(operator.ior, (set(word_split(name)) for name in allnames), set()) self.filterCompleter.setModel(None) self.completerModel = QStringListModel(sorted(allnames)) self.filterCompleter.setModel(self.completerModel) if results: max_score = max( (e.enrichment_score for _, e in results if np.isfinite(e.enrichment_score)), default=1) self.annotationsChartView.setItemDelegateForColumn( 6, BarItemDelegate(self, scale=(0.0, max_score))) self.annotationsChartView.setItemDelegateForColumn( 1, gui.LinkStyledItemDelegate(self.annotationsChartView)) header = self.annotationsChartView.header() for i in range(model.columnCount()): sh = self.annotationsChartView.sizeHintForColumn(i) sh = max(sh, header.sectionSizeHint(i)) self.annotationsChartView.setColumnWidth(i, max(min(sh, 300), 30)) # self.annotationsChartView.resizeColumnToContents(i) self.filterAnnotationsChartView() self.progressBarFinished() self.setStatusMessage("") def _updatesummary(self): state = self.state if state is None: self.error(0, ) self.warning(0) self.infoBox.setText("No data on input.\n") return text = "{.query_count} unique names on input\n".format(state) if state.results.done() and not state.results.exception(): mapped, _, _ = state.results.result() ratio_mapped = (len(mapped) / state.query_count if state.query_count else 0) text += ("%i (%.1f%%) gene names matched" % (len(mapped), 100.0 * ratio_mapped)) elif not state.results.done(): text += "..." else: text += "<Error {}>".format(str(state.results.exception())) self.infoBox.setText(text) # TODO: warn on no enriched sets found (i.e no query genes # mapped to any set) def filterAnnotationsChartView(self, filterString=""): if self.__state & OWSetEnrichment.RunningEnrichment: return # TODO: Move filtering to a filter proxy model # TODO: Re-enable string search categories = set(", ".join(cat) for cat, _ in self.selectedCategories()) # filterString = str(self.filterLineEdit.text()).lower() model = self.annotationsChartView.model() def ishidden(index): # Is item at index (row) hidden item = model.item(index) item_cat = item.data(Qt.DisplayRole) return item_cat not in categories hidemask = [ishidden(i) for i in range(model.rowCount())] # compute FDR according the selected categories pvals = [ model.item(i, 4).data(Qt.UserRole) for i, hidden in enumerate(hidemask) if not hidden ] fdrs = utils.stats.FDR(pvals) # update FDR for the selected collections and apply filtering rules itemsHidden = [] fdriter = iter(fdrs) for index, hidden in enumerate(hidemask): if not hidden: fdr = next(fdriter) pval = model.index(index, 4).data(Qt.UserRole) count = model.index(index, 2).data(Qt.ToolTipRole) hidden = (self.useMinCountFilter and count < self.minClusterCount) or \ (self.useMaxPValFilter and pval > self.maxPValue) or \ (self.useMaxFDRFilter and fdr > self.maxFDR) if not hidden: fdr_item = model.item(index, 5) fdr_item.setData(fmtpdet(fdr), Qt.ToolTipRole) fdr_item.setData(fmtp(fdr), Qt.DisplayRole) fdr_item.setData(fdr, Qt.UserRole) self.annotationsChartView.setRowHidden(index, QModelIndex(), hidden) itemsHidden.append(hidden) if model.rowCount() and all(itemsHidden): self.information(0, "All sets were filtered out.") else: self.information(0) self._updatesummary() @Slot(float) def _setProgress(self, value): assert QThread.currentThread() is self.thread() self.progressBarSet(value, processEvents=None) @Slot(str) def _setRunInfo(self, text): self.setStatusMessage(text) def commit(self): if self.data is None or \ self.__state & OWSetEnrichment.RunningEnrichment: return model = self.annotationsChartView.model() rows = self.annotationsChartView.selectionModel().selectedRows(0) selected = [model.item(index.row(), 0) for index in rows] mapped = reduce(operator.ior, (set(item.enrichment.query_mapped) for item in selected), set()) assert self.state.namematcher.done() matcher = self.state.namematcher.result() axis = 1 if self.genesinrows else 0 if axis == 1: mapped = [ attr for attr in self.data.domain.attributes if matcher.umatch(attr.name) in mapped ] newdomain = Orange.data.Domain(mapped, self.data.domain.class_vars, self.data.domain.metas) data = self.data.from_table(newdomain, self.data) else: geneattr = self.geneAttrs[self.geneattr] selected = [ i for i, ex in enumerate(self.data) if matcher.umatch(str(ex[geneattr])) in mapped ] data = self.data[selected] self.send("Data subset", data) def onDeleteWidget(self): if self.state is not None: self._cancelPending() self.state = None self._executor.shutdown(wait=False)
class OWGEODatasets(OWWidget): name = "GEO Data Sets" description = DESCRIPTION icon = "../widgets/icons/GEODataSets.svg" priority = PRIORITY inputs = [] outputs = [("Expression Data", Orange.data.Table)] settingsList = ["outputRows", "mergeSpots", "gdsSelectionStates", "splitterSettings", "currentGds", "autoCommit", "datasetNames"] outputRows = Setting(True) mergeSpots = Setting(True) gdsSelectionStates = Setting({}) currentGds = Setting(None) datasetNames = Setting({}) splitterSettings = Setting( (b'\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\xea\x00\x00\x00\xd7\x01\x00\x00\x00\x07\x01\x00\x00\x00\x02', b'\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\xb5\x00\x00\x02\x10\x01\x00\x00\x00\x07\x01\x00\x00\x00\x01') ) autoCommit = Setting(False) def __init__(self, parent=None, signalManager=None, name=" GEO Data Sets"): OWWidget.__init__(self, parent, signalManager, name) self.selectionChanged = False self.filterString = "" self.datasetName = "" ## GUI box = gui.widgetBox(self.controlArea, "Info", addSpace=True) self.infoBox = gui.widgetLabel(box, "Initializing\n\n") box = gui.widgetBox(self.controlArea, "Output", addSpace=True) gui.radioButtonsInBox(box, self, "outputRows", ["Genes in rows", "Samples in rows"], "Rows", callback=self.commitIf) gui.checkBox(box, self, "mergeSpots", "Merge spots of same gene", callback=self.commitIf) gui.separator(box) self.nameEdit = gui.lineEdit( box, self, "datasetName", "Data set name", tooltip="Override the default output data set name", callback=self.onNameEdited ) self.nameEdit.setPlaceholderText("") if sys.version_info < (3, ): box = gui.widgetBox(self.controlArea, "Commit", addSpace=True) self.commitButton = gui.button( box, self, "Commit", callback=self.commit) cb = gui.checkBox(box, self, "autoCommit", "Commit on any change") gui.setStopper(self, self.commitButton, cb, "selectionChanged", self.commit) else: gui.auto_commit(self.controlArea, self, "autoCommit", "Commit", box="Commit") self.commitIf = self.commit gui.rubber(self.controlArea) gui.widgetLabel(self.mainArea, "Filter") self.filterLineEdit = QLineEdit( textChanged=self.filter ) self.completer = TokenListCompleter( self, caseSensitivity=Qt.CaseInsensitive ) self.filterLineEdit.setCompleter(self.completer) self.mainArea.layout().addWidget(self.filterLineEdit) splitter = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(splitter) self.treeWidget = QTreeView(splitter) self.treeWidget.setSelectionMode(QTreeView.SingleSelection) self.treeWidget.setRootIsDecorated(False) self.treeWidget.setSortingEnabled(True) self.treeWidget.setAlternatingRowColors(True) self.treeWidget.setUniformRowHeights(True) self.treeWidget.setEditTriggers(QTreeView.NoEditTriggers) linkdelegate = LinkStyledItemDelegate(self.treeWidget) self.treeWidget.setItemDelegateForColumn(1, linkdelegate) self.treeWidget.setItemDelegateForColumn(8, linkdelegate) self.treeWidget.setItemDelegateForColumn( 0, gui.IndicatorItemDelegate(self.treeWidget, role=Qt.DisplayRole)) proxyModel = MySortFilterProxyModel(self.treeWidget) self.treeWidget.setModel(proxyModel) self.treeWidget.selectionModel().selectionChanged.connect( self.updateSelection ) self.treeWidget.viewport().setMouseTracking(True) splitterH = QSplitter(Qt.Horizontal, splitter) box = gui.widgetBox(splitterH, "Description") self.infoGDS = gui.widgetLabel(box, "") self.infoGDS.setWordWrap(True) gui.rubber(box) box = gui.widgetBox(splitterH, "Sample Annotations") self.annotationsTree = QTreeWidget(box) self.annotationsTree.setHeaderLabels( ["Type (Sample annotations)", "Sample count"] ) self.annotationsTree.setRootIsDecorated(True) box.layout().addWidget(self.annotationsTree) self.annotationsTree.itemChanged.connect( self.annotationSelectionChanged ) self._annotationsUpdating = False self.splitters = splitter, splitterH for sp, setting in zip(self.splitters, self.splitterSettings): sp.splitterMoved.connect(self.splitterMoved) sp.restoreState(setting) self.searchKeys = ["dataset_id", "title", "platform_organism", "description"] self.gds = [] self.gds_info = None self.resize(1000, 600) self.setBlocking(True) self.setEnabled(False) self.progressBarInit() self._executor = ThreadExecutor() func = partial(get_gds_model, methodinvoke(self, "_setProgress", (float,))) self._inittask = Task(function=func) self._inittask.finished.connect(self._initializemodel) self._executor.submit(self._inittask) self._datatask = None @Slot(float) def _setProgress(self, value): self.progressBarValue = value def _initializemodel(self): assert self.thread() is QThread.currentThread() model, self.gds_info, self.gds = self._inittask.result() model.setParent(self) proxy = self.treeWidget.model() proxy.setFilterKeyColumn(0) proxy.setFilterRole(TextFilterRole) proxy.setFilterCaseSensitivity(False) proxy.setFilterFixedString(self.filterString) proxy.setSourceModel(model) proxy.sort(0, Qt.DescendingOrder) self.progressBarFinished() self.setBlocking(False) self.setEnabled(True) filter_items = " ".join( gds[key] for gds in self.gds for key in self.searchKeys ) tr_chars = ",.:;!?(){}[]_-+\\|/%#@$^&*<>~`" tr_table = str.maketrans(tr_chars, " " * len(tr_chars)) filter_items = filter_items.translate(tr_table) filter_items = sorted(set(filter_items.split(" "))) filter_items = [item for item in filter_items if len(item) > 3] self.completer.setTokenList(filter_items) if self.currentGds: current_id = self.currentGds["dataset_id"] gdss = [(i, qunpack(proxy.data(proxy.index(i, 1), Qt.DisplayRole))) for i in range(proxy.rowCount())] current = [i for i, data in gdss if data and data == current_id] if current: current_index = proxy.index(current[0], 0) self.treeWidget.selectionModel().select( current_index, QItemSelectionModel.Select | QItemSelectionModel.Rows ) self.treeWidget.scrollTo( current_index, QTreeView.PositionAtCenter) for i in range(8): self.treeWidget.resizeColumnToContents(i) self.treeWidget.setColumnWidth( 1, min(self.treeWidget.columnWidth(1), 300)) self.treeWidget.setColumnWidth( 2, min(self.treeWidget.columnWidth(2), 200)) self.updateInfo() def updateInfo(self): gds_info = self.gds_info text = ("%i datasets\n%i datasets cached\n" % (len(gds_info), len(glob.glob(serverfiles.localpath("GEO") + "/GDS*")))) filtered = self.treeWidget.model().rowCount() if len(self.gds) != filtered: text += ("%i after filtering") % filtered self.infoBox.setText(text) def updateSelection(self, *args): current = self.treeWidget.selectedIndexes() mapToSource = self.treeWidget.model().mapToSource current = [mapToSource(index).row() for index in current] if current: self.currentGds = self.gds[current[0]] self.setAnnotations(self.currentGds) self.infoGDS.setText(self.currentGds.get("description", "")) self.nameEdit.setPlaceholderText(self.currentGds["title"]) self.datasetName = \ self.datasetNames.get(self.currentGds["dataset_id"], "") else: self.currentGds = None self.nameEdit.setPlaceholderText("") self.datasetName = "" self.commitIf() def setAnnotations(self, gds): self._annotationsUpdating = True self.annotationsTree.clear() annotations = defaultdict(set) subsetscount = {} for desc in gds["subsets"]: annotations[desc["type"]].add(desc["description"]) subsetscount[desc["description"]] = str(len(desc["sample_id"])) for type, subsets in annotations.items(): key = (gds["dataset_id"], type) subsetItem = QTreeWidgetItem(self.annotationsTree, [type]) subsetItem.setFlags(subsetItem.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsTristate) subsetItem.setCheckState( 0, self.gdsSelectionStates.get(key, Qt.Checked) ) subsetItem.key = key for subset in subsets: key = (gds["dataset_id"], type, subset) item = QTreeWidgetItem( subsetItem, [subset, subsetscount.get(subset, "")] ) item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState( 0, self.gdsSelectionStates.get(key, Qt.Checked) ) item.key = key self._annotationsUpdating = False self.annotationsTree.expandAll() for i in range(self.annotationsTree.columnCount()): self.annotationsTree.resizeColumnToContents(i) def annotationSelectionChanged(self, item, column): if self._annotationsUpdating: return for i in range(self.annotationsTree.topLevelItemCount()): item = self.annotationsTree.topLevelItem(i) self.gdsSelectionStates[item.key] = item.checkState(0) for j in range(item.childCount()): child = item.child(j) self.gdsSelectionStates[child.key] = child.checkState(0) def filter(self): filter_string = unicode(self.filterLineEdit.text()) proxyModel = self.treeWidget.model() if proxyModel: strings = filter_string.lower().strip().split() proxyModel.setFilterFixedStrings(strings) self.updateInfo() def selectedSamples(self): """ Return the currently selected sample annotations. The return value is a list of selected (sample type, sample value) tuples. .. note:: if some Sample annotation type has no selected values. this method will return all values for it. """ samples = [] unused_types = [] used_types = [] for stype in childiter(self.annotationsTree.invisibleRootItem()): selected_values = [] all_values = [] for sval in childiter(stype): value = (str(stype.text(0)), str(sval.text(0))) if self.gdsSelectionStates.get(sval.key, True): selected_values.append(value) all_values.append(value) if selected_values: samples.extend(selected_values) used_types.append(str(stype.text(0))) else: # If no sample of sample type is selected we don't filter # on it. samples.extend(all_values) unused_types.append(str(stype.text(0))) return samples, used_types def commitIf(self): if self.autoCommit: self.commit() else: self.selectionChanged = True @Slot(int, int) def progressCompleted(self, value, total): if total > 0: self.progressBarSet(100. * value / total, processEvents=False) else: pass # TODO: report 'indeterminate progress' def commit(self): if self.currentGds: self.error(0) sample_type = None self.progressBarInit(processEvents=None) _, groups = self.selectedSamples() if len(groups) == 1 and self.outputRows: sample_type = groups[0] self.setEnabled(False) self.setBlocking(True) progress = methodinvoke(self, "progressCompleted", (int, int)) def get_data(gds_id, report_genes, transpose, sample_type, title): gds_ensure_downloaded(gds_id, progress) gds = geo.GDS(gds_id) data = gds.getdata( report_genes=report_genes, transpose=transpose, sample_type=sample_type ) data.name = title return data get_data = partial( get_data, self.currentGds["dataset_id"], report_genes=self.mergeSpots, transpose=self.outputRows, sample_type=sample_type, title=self.datasetName or self.currentGds["title"] ) self._datatask = Task(function=get_data) self._datatask.finished.connect(self._on_dataready) self._executor.submit(self._datatask) def _on_dataready(self): self.setEnabled(True) self.setBlocking(False) self.progressBarFinished(processEvents=False) try: data = self._datatask.result() except urlrequest.URLError as error: self.error(0, ("Error while connecting to the NCBI ftp server! " "'%s'" % error)) sys.excepthook(type(error), error, getattr(error, "__traceback__")) return finally: self._datatask = None data_name = data.name samples, _ = self.selectedSamples() self.warning(0) message = None if self.outputRows: def samplesinst(ex): out = [] for meta in data.domain.metas: out.append((meta.name, ex[meta].value)) if data.domain.class_var.name != 'class': out.append((data.domain.class_var.name, ex[data.domain.class_var].value)) return out samples = set(samples) mask = [samples.issuperset(samplesinst(ex)) for ex in data] data = data[numpy.array(mask, dtype=bool)] if len(data) == 0: message = "No samples with selected sample annotations." else: samples = set(samples) domain = Orange.data.Domain( [attr for attr in data.domain.attributes if samples.issuperset(attr.attributes.items())], data.domain.class_var, data.domain.metas ) # domain.addmetas(data.domain.getmetas()) if len(domain.attributes) == 0: message = "No samples with selected sample annotations." stypes = set(s[0] for s in samples) for attr in domain.attributes: attr.attributes = dict( (key, value) for key, value in attr.attributes.items() if key in stypes ) data = Orange.data.Table(domain, data) if message is not None: self.warning(0, message) data_hints.set_hint(data, "taxid", self.currentGds.get("taxid", ""), 10.0) data_hints.set_hint(data, "genesinrows", self.outputRows, 10.0) data.name = data_name self.send("Expression Data", data) model = self.treeWidget.model().sourceModel() row = self.gds.index(self.currentGds) model.setData(model.index(row, 0), " ", Qt.DisplayRole) self.updateInfo() self.selectionChanged = False def splitterMoved(self, *args): self.splitterSettings = [bytes(sp.saveState()) for sp in self.splitters] def send_report(self): self.report_items("GEO Dataset", [("ID", self.currentGds['dataset_id']), ("Title", self.currentGds['title']), ("Organism", self.currentGds['sample_organism'])]) self.report_items("Data", [("Samples", self.currentGds['sample_count']), ("Features", self.currentGds['feature_count']), ("Genes", self.currentGds['gene_count'])]) self.report_name("Sample annotations") subsets = defaultdict(list) for subset in self.currentGds['subsets']: subsets[subset['type']].append((subset['description'], len(subset['sample_id']))) self.report_html += "<ul>" for type in subsets: self.report_html += "<b>" + type + ":</b></br>" for desc, count in subsets[type]: self.report_html += 9 * " " + "<b>{}:</b> {}</br>".format(desc, count) self.report_html += "</ul>" def onDeleteWidget(self): if self._inittask: self._inittask.future().cancel() self._inittask.finished.disconnect(self._initializemodel) if self._datatask: self._datatask.future().cancel() self._datatask.finished.disconnect(self._on_dataready) self._executor.shutdown(wait=False) super(OWGEODatasets, self).onDeleteWidget() def onNameEdited(self): if self.currentGds: gds_id = self.currentGds["dataset_id"] self.datasetNames[gds_id] = unicode(self.nameEdit.text()) self.commitIf()
class OWSetEnrichment(widget.OWWidget): name = "Set Enrichment" description = "" icon = "../widgets/icons/GeneSetEnrichment.svg" priority = 5000 inputs = [("Data", Orange.data.Table, "setData", widget.Default), ("Reference", Orange.data.Table, "setReference")] outputs = [("Data subset", Orange.data.Table)] settingsHandler = settings.DomainContextHandler() taxid = settings.ContextSetting(None) speciesIndex = settings.ContextSetting(0) genesinrows = settings.ContextSetting(False) geneattr = settings.ContextSetting(0) categoriesCheckState = settings.ContextSetting({}) useReferenceData = settings.Setting(False) useMinCountFilter = settings.Setting(True) useMaxPValFilter = settings.Setting(True) useMaxFDRFilter = settings.Setting(True) minClusterCount = settings.Setting(3) maxPValue = settings.Setting(0.01) maxFDR = settings.Setting(0.01) autocommit = settings.Setting(False) Ready, Initializing, Loading, RunningEnrichment = 0, 1, 2, 4 def __init__(self, parent=None): super().__init__(parent) self.geneMatcherSettings = [False, False, True, False] self.data = None self.referenceData = None self.taxid_list = [] self.__genematcher = (None, fulfill(gene.matcher([]))) self.__invalidated = False self.currentAnnotatedCategories = [] self.state = None self.__state = OWSetEnrichment.Initializing box = gui.widgetBox(self.controlArea, "Info") self.infoBox = gui.widgetLabel(box, "Info") self.infoBox.setText("No data on input.\n") self.speciesComboBox = gui.comboBox( self.controlArea, self, "speciesIndex", "Species", callback=self.__on_speciesIndexChanged) box = gui.widgetBox(self.controlArea, "Entity names") self.geneAttrComboBox = gui.comboBox( box, self, "geneattr", "Entity feature", sendSelectedValue=0, callback=self.updateAnnotations) cb = gui.checkBox( box, self, "genesinrows", "Use feature names", callback=self.updateAnnotations, disables=[(-1, self.geneAttrComboBox)]) cb.makeConsistent() # gui.button(box, self, "Gene matcher settings", # callback=self.updateGeneMatcherSettings, # tooltip="Open gene matching settings dialog") self.referenceRadioBox = gui.radioButtonsInBox( self.controlArea, self, "useReferenceData", ["All entities", "Reference set (input)"], tooltips=["Use entire genome (for gene set enrichment) or all " + "available entities for reference", "Use entities from Reference Examples input signal " + "as reference"], box="Reference", callback=self.updateAnnotations) box = gui.widgetBox(self.controlArea, "Entity Sets") self.groupsWidget = QTreeWidget(self) self.groupsWidget.setHeaderLabels(["Category"]) box.layout().addWidget(self.groupsWidget) hLayout = QHBoxLayout() hLayout.setSpacing(10) hWidget = gui.widgetBox(self.mainArea, orientation=hLayout) gui.spin(hWidget, self, "minClusterCount", 0, 100, label="Entities", tooltip="Minimum entity count", callback=self.filterAnnotationsChartView, callbackOnReturn=True, checked="useMinCountFilter", checkCallback=self.filterAnnotationsChartView) pvalfilterbox = gui.widgetBox(hWidget, orientation="horizontal") cb = gui.checkBox( pvalfilterbox, self, "useMaxPValFilter", "p-value", callback=self.filterAnnotationsChartView) sp = gui.doubleSpin( pvalfilterbox, self, "maxPValue", 0.0, 1.0, 0.0001, tooltip="Maximum p-value", callback=self.filterAnnotationsChartView, callbackOnReturn=True, ) sp.setEnabled(self.useMaxFDRFilter) cb.toggled[bool].connect(sp.setEnabled) pvalfilterbox.layout().setAlignment(cb, Qt.AlignRight) pvalfilterbox.layout().setAlignment(sp, Qt.AlignLeft) fdrfilterbox = gui.widgetBox(hWidget, orientation="horizontal") cb = gui.checkBox( fdrfilterbox, self, "useMaxFDRFilter", "FDR", callback=self.filterAnnotationsChartView) sp = gui.doubleSpin( fdrfilterbox, self, "maxFDR", 0.0, 1.0, 0.0001, tooltip="Maximum False discovery rate", callback=self.filterAnnotationsChartView, callbackOnReturn=True, ) sp.setEnabled(self.useMaxFDRFilter) cb.toggled[bool].connect(sp.setEnabled) fdrfilterbox.layout().setAlignment(cb, Qt.AlignRight) fdrfilterbox.layout().setAlignment(sp, Qt.AlignLeft) self.filterLineEdit = QLineEdit( self, placeholderText="Filter ...") self.filterCompleter = QCompleter(self.filterLineEdit) self.filterCompleter.setCaseSensitivity(Qt.CaseInsensitive) self.filterLineEdit.setCompleter(self.filterCompleter) hLayout.addWidget(self.filterLineEdit) self.mainArea.layout().addWidget(hWidget) self.filterLineEdit.textChanged.connect( self.filterAnnotationsChartView) self.annotationsChartView = QTreeView( alternatingRowColors=True, sortingEnabled=True, selectionMode=QTreeView.ExtendedSelection, rootIsDecorated=False, editTriggers=QTreeView.NoEditTriggers, ) self.annotationsChartView.viewport().setMouseTracking(True) self.mainArea.layout().addWidget(self.annotationsChartView) contextEventFilter = gui.VisibleHeaderSectionContextEventFilter( self.annotationsChartView) self.annotationsChartView.header().installEventFilter(contextEventFilter) self.groupsWidget.itemClicked.connect(self.subsetSelectionChanged) gui.auto_commit(self.controlArea, self, "autocommit", "Commit") self.setBlocking(True) task = EnsureDownloaded( [(taxonomy.Taxonomy.DOMAIN, taxonomy.Taxonomy.FILENAME), (geneset.sfdomain, "index.pck")] ) task.finished.connect(self.__initialize_finish) self.setStatusMessage("Initializing") self._executor = ThreadExecutor( parent=self, threadPool=QThreadPool(self)) self._executor.submit(task) def sizeHint(self): return QSize(1024, 600) def __initialize_finish(self): # Finalize the the widget's initialization (preferably after # ensuring all required databases have been downloaded. sets = geneset.list_all() taxids = set(taxonomy.common_taxids() + list(filter(None, [tid for _, tid, _ in sets]))) organisms = [(tid, name_or_none(tid)) for tid in taxids] organisms = [(tid, name) for tid, name in organisms if name is not None] organisms = [(None, "None")] + sorted(organisms) taxids = [tid for tid, _ in organisms] names = [name for _, name in organisms] self.taxid_list = taxids self.speciesComboBox.clear() self.speciesComboBox.addItems(names) self.genesets = sets if self.taxid in self.taxid_list: taxid = self.taxid else: taxid = self.taxid_list[0] self.taxid = None self.setCurrentOrganism(taxid) self.setBlocking(False) self.__state = OWSetEnrichment.Ready self.setStatusMessage("") def setCurrentOrganism(self, taxid): """Set the current organism `taxid`.""" if taxid not in self.taxid_list: taxid = self.taxid_list[min(self.speciesIndex, len(self.taxid_list) - 1)] if self.taxid != taxid: self.taxid = taxid self.speciesIndex = self.taxid_list.index(taxid) self.refreshHierarchy() self._invalidateGeneMatcher() self._invalidate() def currentOrganism(self): """Return the current organism taxid""" return self.taxid def __on_speciesIndexChanged(self): taxid = self.taxid_list[self.speciesIndex] self.taxid = "< Do not look >" self.setCurrentOrganism(taxid) if self.__invalidated and self.data is not None: self.updateAnnotations() def clear(self): """Clear/reset the widget state.""" self._cancelPending() self.state = None self.__state = self.__state & ~OWSetEnrichment.RunningEnrichment self._clearView() if self.annotationsChartView.model() is not None: self.annotationsChartView.model().clear() self.geneAttrComboBox.clear() self.geneAttrs = [] self._updatesummary() def _cancelPending(self): """Cancel pending tasks.""" if self.state is not None: self.state.results.cancel() self.state.namematcher.cancel() self.state.cancelled = True def _clearView(self): """Clear the enrichment report view (main area).""" if self.annotationsChartView.model() is not None: self.annotationsChartView.model().clear() def setData(self, data=None): """Set the input dataset with query gene names""" if self.__state & OWSetEnrichment.Initializing: self.__initialize_finish() self.error(0) self.closeContext() self.clear() self.groupsWidget.clear() self.data = data if data is not None: varlist = [var for var in data.domain.variables + data.domain.metas if isinstance(var, Orange.data.StringVariable)] self.geneAttrs = varlist for var in varlist: self.geneAttrComboBox.addItem(*gui.attributeItem(var)) oldtaxid = self.taxid self.geneattr = min(self.geneattr, len(self.geneAttrs) - 1) taxid = data_hints.get_hint(data, "taxid", "") if taxid in self.taxid_list: self.speciesIndex = self.taxid_list.index(taxid) self.taxid = taxid self.genesinrows = data_hints.get_hint( data, "genesinrows", self.genesinrows) self.openContext(data) if oldtaxid != self.taxid: self.taxid = "< Do not look >" self.setCurrentOrganism(taxid) self.refreshHierarchy() self._invalidate() def setReference(self, data=None): """Set the (optional) input dataset with reference gene names.""" self.referenceData = data self.referenceRadioBox.setEnabled(bool(data)) if self.useReferenceData: self._invalidate() def handleNewSignals(self): if self.__invalidated: self.updateAnnotations() def _invalidateGeneMatcher(self): _, f = self.__genematcher f.cancel() self.__genematcher = (None, fulfill(gene.matcher([]))) def _invalidate(self): self.__invalidated = True def genesFromTable(self, table): if self.genesinrows: genes = [attr.name for attr in table.domain.attributes] else: geneattr = self.geneAttrs[self.geneattr] genes = [str(ex[geneattr]) for ex in table] return genes def getHierarchy(self, taxid): def recursive_dict(): return defaultdict(recursive_dict) collection = recursive_dict() def collect(col, hier): if hier: collect(col[hier[0]], hier[1:]) for hierarchy, t_id, _ in self.genesets: collect(collection[t_id], hierarchy) return (taxid, collection[taxid]), (None, collection[None]) def setHierarchy(self, hierarchy, hierarchy_noorg): self.groupsWidgetItems = {} def fill(col, parent, full=(), org=""): for key, value in sorted(col.items()): full_cat = full + (key,) item = QTreeWidgetItem(parent, [key]) item.setFlags(item.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsEnabled) if value: item.setFlags(item.flags() | Qt.ItemIsTristate) checked = self.categoriesCheckState.get( (full_cat, org), Qt.Checked) item.setData(0, Qt.CheckStateRole, checked) item.setExpanded(True) item.category = full_cat item.organism = org self.groupsWidgetItems[full_cat] = item fill(value, item, full_cat, org=org) self.groupsWidget.clear() fill(hierarchy[1], self.groupsWidget, org=hierarchy[0]) fill(hierarchy_noorg[1], self.groupsWidget, org=hierarchy_noorg[0]) def refreshHierarchy(self): self.setHierarchy(*self.getHierarchy(taxid=self.taxid_list[self.speciesIndex])) def selectedCategories(self): """ Return a list of currently selected hierarchy keys. A key is a tuple of identifiers from the root to the leaf of the hierarchy tree. """ return [key for key, check in self.getHierarchyCheckState().items() if check == Qt.Checked] def getHierarchyCheckState(self): def collect(item, full=()): checked = item.checkState(0) name = str(item.data(0, Qt.DisplayRole)) full_cat = full + (name,) result = [((full_cat, item.organism), checked)] for i in range(item.childCount()): result.extend(collect(item.child(i), full_cat)) return result items = [self.groupsWidget.topLevelItem(i) for i in range(self.groupsWidget.topLevelItemCount())] states = itertools.chain(*(collect(item) for item in items)) return dict(states) def subsetSelectionChanged(self, item, column): # The selected geneset (hierarchy) subset has been changed by the # user. Update the displayed results. # Update the stored state (persistent settings) self.categoriesCheckState = self.getHierarchyCheckState() categories = self.selectedCategories() if self.data is not None: if self._nogenematching() or \ not set(categories) <= set(self.currentAnnotatedCategories): self.updateAnnotations() else: self.filterAnnotationsChartView() def updateGeneMatcherSettings(self): raise NotImplementedError from .OWGOEnrichmentAnalysis import GeneMatcherDialog dialog = GeneMatcherDialog(self, defaults=self.geneMatcherSettings, enabled=[True] * 4, modal=True) if dialog.exec_(): self.geneMatcherSettings = [getattr(dialog, item[0]) for item in dialog.items] self._invalidateGeneMatcher() if self.data is not None: self.updateAnnotations() def _genematcher(self): """ Return a Future[gene.SequenceMatcher] """ taxid = self.taxid_list[self.speciesIndex] current, matcher_f = self.__genematcher if taxid == current and \ not matcher_f.cancelled(): return matcher_f self._invalidateGeneMatcher() if taxid is None: self.__genematcher = (None, fulfill(gene.matcher([]))) return self.__genematcher[1] matchers = [gene.GMGO, gene.GMKEGG, gene.GMNCBI, gene.GMAffy] matchers = [m for m, use in zip(matchers, self.geneMatcherSettings) if use] def create(): return gene.matcher([m(taxid) for m in matchers]) matcher_f = self._executor.submit(create) self.__genematcher = (taxid, matcher_f) return self.__genematcher[1] def _nogenematching(self): return self.taxid is None or not any(self.geneMatcherSettings) def updateAnnotations(self): if self.data is None: return assert not self.__state & OWSetEnrichment.Initializing self._cancelPending() self._clearView() self.information(0) self.warning(0) self.error(0) if not self.genesinrows and len(self.geneAttrs) == 0: self.error(0, "Input data contains no columns with gene names") return self.__state = OWSetEnrichment.RunningEnrichment taxid = self.taxid_list[self.speciesIndex] self.taxid = taxid categories = self.selectedCategories() clusterGenes = self.genesFromTable(self.data) if self.referenceData is not None and self.useReferenceData: referenceGenes = self.genesFromTable(self.referenceData) else: referenceGenes = None self.currentAnnotatedCategories = categories genematcher = self._genematcher() self.progressBarInit() ## Load collections in a worker thread # TODO: Use cached collections if already loaded and # use ensure_genesetsdownloaded with progress report (OWSelectGenes) collections = self._executor.submit(geneset.collections, *categories) def refset_null(): """Return the default background reference set""" col = collections.result() return reduce(operator.ior, (set(g.genes) for g in col), set()) def refset_ncbi(): """Return all NCBI gene names""" geneinfo = gene.NCBIGeneInfo(taxid) return set(geneinfo.keys()) def namematcher(): matcher = genematcher.result() match = matcher.set_targets(ref_set.result()) match.umatch = memoize(match.umatch) return match def map_unames(): matcher = namematcher.result() query = list(filter(None, map(matcher.umatch, querynames))) reference = list(filter(None, map(matcher.umatch, ref_set.result()))) return query, reference if self._nogenematching(): if referenceGenes is None: ref_set = self._executor.submit(refset_null) else: ref_set = fulfill(referenceGenes) else: if referenceGenes == None: ref_set = self._executor.submit(refset_ncbi) else: ref_set = fulfill(referenceGenes) namematcher = self._executor.submit(namematcher) querynames = clusterGenes state = types.SimpleNamespace() state.query_set = clusterGenes state.reference_set = referenceGenes state.namematcher = namematcher state.query_count = len(set(clusterGenes)) state.reference_count = (len(set(referenceGenes)) if referenceGenes is not None else None) state.cancelled = False progress = methodinvoke(self, "_setProgress", (float,)) info = methodinvoke(self, "_setRunInfo", (str,)) @withtraceback def run(): info("Loading data") match = namematcher.result() query, reference = map_unames() gscollections = collections.result() results = [] info("Running enrichment") p = 0 for i, gset in enumerate(gscollections): genes = set(filter(None, map(match.umatch, gset.genes))) enr = set_enrichment(genes, reference, query) results.append((gset, enr)) if state.cancelled: raise UserInteruptException pnew = int(100 * i / len(gscollections)) if pnew != p: progress(pnew) p = pnew progress(100) info("") return query, reference, results task = Task(function=run) task.resultReady.connect(self.__on_enrichment_finished) task.exceptionReady.connect(self.__on_enrichment_failed) result = self._executor.submit(task) state.results = result self.state = state self._updatesummary() def __on_enrichment_failed(self, exception): if not isinstance(exception, UserInteruptException): print("ERROR:", exception, file=sys.stderr) print(exception._traceback, file=sys.stderr) self.progressBarFinished() self.setStatusMessage("") self.__state &= ~OWSetEnrichment.RunningEnrichment def __on_enrichment_finished(self, results): assert QThread.currentThread() is self.thread() self.__state &= ~OWSetEnrichment.RunningEnrichment query, reference, results = results if self.annotationsChartView.model(): self.annotationsChartView.model().clear() nquery = len(query) nref = len(reference) maxcount = max((len(e.query_mapped) for _, e in results), default=1) maxrefcount = max((len(e.reference_mapped) for _, e in results), default=1) nspaces = int(math.ceil(math.log10(maxcount or 1))) refspaces = int(math.ceil(math.log(maxrefcount or 1))) query_fmt = "%" + str(nspaces) + "s (%.2f%%)" ref_fmt = "%" + str(refspaces) + "s (%.2f%%)" def fmt_count(fmt, count, total): return fmt % (count, 100.0 * count / (total or 1)) fmt_query_count = partial(fmt_count, query_fmt) fmt_ref_count = partial(fmt_count, ref_fmt) linkFont = QFont(self.annotationsChartView.viewOptions().font) linkFont.setUnderline(True) def item(value=None, tooltip=None, user=None): si = QStandardItem() if value is not None: si.setData(value, Qt.DisplayRole) if tooltip is not None: si.setData(tooltip, Qt.ToolTipRole) if user is not None: si.setData(user, Qt.UserRole) else: si.setData(value, Qt.UserRole) return si model = QStandardItemModel() model.setSortRole(Qt.UserRole) model.setHorizontalHeaderLabels( ["Category", "Term", "Count", "Reference count", "p-value", "FDR", "Enrichment"]) for i, (gset, enrich) in enumerate(results): if len(enrich.query_mapped) == 0: continue nquery_mapped = len(enrich.query_mapped) nref_mapped = len(enrich.reference_mapped) row = [ item(", ".join(gset.hierarchy)), item(gsname(gset), tooltip=gset.link), item(fmt_query_count(nquery_mapped, nquery), tooltip=nquery_mapped, user=nquery_mapped), item(fmt_ref_count(nref_mapped, nref), tooltip=nref_mapped, user=nref_mapped), item(fmtp(enrich.p_value), user=enrich.p_value), item(), # column 5, FDR, is computed in filterAnnotationsChartView item(enrich.enrichment_score, tooltip="%.3f" % enrich.enrichment_score, user=enrich.enrichment_score) ] row[0].geneset = gset row[0].enrichment = enrich row[1].setData(gset.link, gui.LinkRole) row[1].setFont(linkFont) row[1].setForeground(QColor(Qt.blue)) model.appendRow(row) self.annotationsChartView.setModel(model) self.annotationsChartView.selectionModel().selectionChanged.connect( self.commit ) if not model.rowCount(): self.warning(0, "No enriched sets found.") else: self.warning(0) allnames = set(gsname(geneset) for geneset, (count, _, _, _) in results if count) allnames |= reduce(operator.ior, (set(word_split(name)) for name in allnames), set()) self.filterCompleter.setModel(None) self.completerModel = QStringListModel(sorted(allnames)) self.filterCompleter.setModel(self.completerModel) if results: max_score = max((e.enrichment_score for _, e in results if np.isfinite(e.enrichment_score)), default=1) self.annotationsChartView.setItemDelegateForColumn( 6, BarItemDelegate(self, scale=(0.0, max_score)) ) self.annotationsChartView.setItemDelegateForColumn( 1, gui.LinkStyledItemDelegate(self.annotationsChartView) ) header = self.annotationsChartView.header() for i in range(model.columnCount()): sh = self.annotationsChartView.sizeHintForColumn(i) sh = max(sh, header.sectionSizeHint(i)) self.annotationsChartView.setColumnWidth(i, max(min(sh, 300), 30)) # self.annotationsChartView.resizeColumnToContents(i) self.filterAnnotationsChartView() self.progressBarFinished() self.setStatusMessage("") def _updatesummary(self): state = self.state if state is None: self.error(0,) self.warning(0) self.infoBox.setText("No data on input.\n") return text = "{.query_count} unique names on input\n".format(state) if state.results.done() and not state.results.exception(): mapped, _, _ = state.results.result() ratio_mapped = (len(mapped) / state.query_count if state.query_count else 0) text += ("%i (%.1f%%) gene names matched" % (len(mapped), 100.0 * ratio_mapped)) elif not state.results.done(): text += "..." else: text += "<Error {}>".format(str(state.results.exception())) self.infoBox.setText(text) # TODO: warn on no enriched sets found (i.e no query genes # mapped to any set) def filterAnnotationsChartView(self, filterString=""): if self.__state & OWSetEnrichment.RunningEnrichment: return # TODO: Move filtering to a filter proxy model # TODO: Re-enable string search categories = set(", ".join(cat) for cat, _ in self.selectedCategories()) # filterString = str(self.filterLineEdit.text()).lower() model = self.annotationsChartView.model() def ishidden(index): # Is item at index (row) hidden item = model.item(index) item_cat = item.data(Qt.DisplayRole) return item_cat not in categories hidemask = [ishidden(i) for i in range(model.rowCount())] # compute FDR according the selected categories pvals = [model.item(i, 4).data(Qt.UserRole) for i, hidden in enumerate(hidemask) if not hidden] fdrs = utils.stats.FDR(pvals) # update FDR for the selected collections and apply filtering rules itemsHidden = [] fdriter = iter(fdrs) for index, hidden in enumerate(hidemask): if not hidden: fdr = next(fdriter) pval = model.index(index, 4).data(Qt.UserRole) count = model.index(index, 2).data(Qt.ToolTipRole) hidden = (self.useMinCountFilter and count < self.minClusterCount) or \ (self.useMaxPValFilter and pval > self.maxPValue) or \ (self.useMaxFDRFilter and fdr > self.maxFDR) if not hidden: fdr_item = model.item(index, 5) fdr_item.setData(fmtpdet(fdr), Qt.ToolTipRole) fdr_item.setData(fmtp(fdr), Qt.DisplayRole) fdr_item.setData(fdr, Qt.UserRole) self.annotationsChartView.setRowHidden( index, QModelIndex(), hidden) itemsHidden.append(hidden) if model.rowCount() and all(itemsHidden): self.information(0, "All sets were filtered out.") else: self.information(0) self._updatesummary() @Slot(float) def _setProgress(self, value): assert QThread.currentThread() is self.thread() self.progressBarSet(value, processEvents=None) @Slot(str) def _setRunInfo(self, text): self.setStatusMessage(text) def commit(self): if self.data is None or \ self.__state & OWSetEnrichment.RunningEnrichment: return model = self.annotationsChartView.model() rows = self.annotationsChartView.selectionModel().selectedRows(0) selected = [model.item(index.row(), 0) for index in rows] mapped = reduce(operator.ior, (set(item.enrichment.query_mapped) for item in selected), set()) assert self.state.namematcher.done() matcher = self.state.namematcher.result() axis = 1 if self.genesinrows else 0 if axis == 1: mapped = [attr for attr in self.data.domain.attributes if matcher.umatch(attr.name) in mapped] newdomain = Orange.data.Domain( mapped, self.data.domain.class_vars, self.data.domain.metas) data = self.data.from_table(newdomain, self.data) else: geneattr = self.geneAttrs[self.geneattr] selected = [i for i, ex in enumerate(self.data) if matcher.umatch(str(ex[geneattr])) in mapped] data = self.data[selected] self.send("Data subset", data) def onDeleteWidget(self): if self.state is not None: self._cancelPending() self.state = None self._executor.shutdown(wait=False)
def variables_filter(model, parent=None, accepted_type=Orange.data.Variable, view_type=VariablesListItemView): """ GUI components: ListView with a lineedit which works as a filter. One can write a variable name in a edit box and possible matches are then shown in a listview. """ def update_completer_model(): """ This gets called when the model for available attributes changes through either drag/drop or the left/right button actions. """ nonlocal original_completer_items items = ["%s=%s" % item for v in model for item in v.attributes.items()] new = sorted(set(items)) if new != original_completer_items: original_completer_items = new completer_model.setStringList(original_completer_items) def update_completer_prefix(): """ Prefixes all items in the completer model with the current already done completion to enable the completion of multiple keywords. """ nonlocal original_completer_items prefix = str(completer.completionPrefix()) if not prefix.endswith(" ") and " " in prefix: prefix, _ = prefix.rsplit(" ", 1) items = [prefix + " " + item for item in original_completer_items] else: items = original_completer_items old = list(map(str, completer_model.stringList())) if set(old) != set(items): completer_model.setStringList(items) original_completer_items = [] filter_edit = QLineEdit() filter_edit.setToolTip("Filter the list of available variables.") filter_edit.setPlaceholderText("Filter") completer_model = QStringListModel() completer = QCompleter(completer_model, filter_edit) completer.setCompletionMode(QCompleter.InlineCompletion) completer.setModelSorting(QCompleter.CaseSensitivelySortedModel) filter_edit.setCompleter(completer) completer_navigator = CompleterNavigator(parent) filter_edit.installEventFilter(completer_navigator) proxy = VariableFilterProxyModel() proxy.setSourceModel(model) view = view_type(acceptedType=accepted_type) view.setModel(proxy) model.dataChanged.connect(update_completer_model) model.rowsInserted.connect(update_completer_model) model.rowsRemoved.connect(update_completer_model) filter_edit.textChanged.connect(update_completer_prefix) filter_edit.textChanged.connect(proxy.set_filter_string) return filter_edit, view
class SelectionSetsWidget(QFrame): """ Widget for managing multiple stored item selections """ selectionModified = Signal(bool) def __init__(self, parent): QFrame.__init__(self, parent) self.setContentsMargins(0, 0, 0, 0) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(1) self._setNameLineEdit = QLineEdit(self) layout.addWidget(self._setNameLineEdit) self._setListView = QListView(self) self._listModel = QStandardItemModel(self) self._proxyModel = QSortFilterProxyModel(self) self._proxyModel.setSourceModel(self._listModel) self._setListView.setModel(self._proxyModel) self._setListView.setItemDelegate(ListItemDelegate(self)) self._setNameLineEdit.textChanged.connect( self._proxyModel.setFilterFixedString) self._completer = QCompleter(self._listModel, self) self._setNameLineEdit.setCompleter(self._completer) self._listModel.itemChanged.connect(self._onSetNameChange) layout.addWidget(self._setListView) buttonLayout = QHBoxLayout() self._addAction = QAction("+", self, toolTip="Add a new sort key") self._updateAction = QAction("Update", self, toolTip="Update/save current selection") self._removeAction = QAction("\u2212", self, toolTip="Remove selected sort key.") self._addToolButton = QToolButton(self) self._updateToolButton = QToolButton(self) self._removeToolButton = QToolButton(self) self._updateToolButton.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) self._addToolButton.setDefaultAction(self._addAction) self._updateToolButton.setDefaultAction(self._updateAction) self._removeToolButton.setDefaultAction(self._removeAction) buttonLayout.addWidget(self._addToolButton) buttonLayout.addWidget(self._updateToolButton) buttonLayout.addWidget(self._removeToolButton) layout.addLayout(buttonLayout) self.setLayout(layout) self._addAction.triggered.connect(self.addCurrentSelection) self._updateAction.triggered.connect(self.updateSelectedSelection) self._removeAction.triggered.connect(self.removeSelectedSelection) self._setListView.selectionModel().selectionChanged.connect( self._onListViewSelectionChanged) self.selectionModel = None self._selections = [] def sizeHint(self): size = QFrame.sizeHint(self) return QSize(size.width(), 150) def _onSelectionChanged(self, selected, deselected): self.setSelectionModified(True) def _onListViewSelectionChanged(self, selected, deselected): try: index = self._setListView.selectedIndexes()[0] except IndexError: return self.commitSelection(self._proxyModel.mapToSource(index).row()) def _onSetNameChange(self, item): self.selections[item.row()].name = str(item.text()) def _setButtonStates(self, val): self._updateToolButton.setEnabled(val) def setSelectionModel(self, selectionModel): if self.selectionModel: self.selectionModel.selectionChanged.disconnect( self._onSelectionChanged) self.selectionModel = selectionModel self.selectionModel.selectionChanged.connect(self._onSelectionChanged) def addCurrentSelection(self): item = self.addSelection( SelectionByKey(self.selectionModel.selection(), name="New selection", key=(1, 2, 3, 10))) index = self._proxyModel.mapFromSource(item.index()) self._setListView.setCurrentIndex(index) self._setListView.edit(index) self.setSelectionModified(False) def removeSelectedSelection(self): i = self._proxyModel.mapToSource( self._setListView.currentIndex()).row() self._listModel.takeRow(i) del self.selections[i] def updateCurentSelection(self): i = self._proxyModel.mapToSource( self._setListView.selectedIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def addSelection(self, selection, name=""): self._selections.append(selection) item = QStandardItem(selection.name) item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listModel.appendRow(item) self.setSelectionModified(False) return item def updateSelectedSelection(self): i = self._proxyModel.mapToSource( self._setListView.currentIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def setSelectionModified(self, val): self._selectionModified = val self._setButtonStates(val) self.selectionModified.emit(bool(val)) def commitSelection(self, index): selection = self.selections[index] selection.select(self.selectionModel) def setSelections(self, selections): self._listModel.clear() for selection in selections: self.addSelection(selection) def selections(self): return self._selections selections = property(selections, setSelections)