def showPopup(self): # type: () -> None """ Reimplemented from QComboBox.showPopup Popup up a customized view and filter edit line. Note ---- The .popup(), .lineEdit(), .completer() of the base class are not used. """ if self.__popup is not None: # We have user entered state that cannot be disturbed # (entered filter text, scroll offset, ...) return # pragma: no cover if self.count() == 0: return opt = QStyleOptionComboBox() self.initStyleOption(opt) popup = QListView( uniformItemSizes=True, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, verticalScrollBarPolicy=Qt.ScrollBarAsNeeded, iconSize=self.iconSize(), ) popup.setFocusProxy(self.__searchline) popup.setParent(self, Qt.Popup | Qt.FramelessWindowHint) popup.setItemDelegate(_ComboBoxListDelegate(popup)) proxy = QSortFilterProxyModel( popup, filterCaseSensitivity=Qt.CaseInsensitive ) proxy.setFilterKeyColumn(self.modelColumn()) proxy.setSourceModel(self.model()) popup.setModel(proxy) root = proxy.mapFromSource(self.rootModelIndex()) popup.setRootIndex(root) self.__popup = popup self.__proxy = proxy self.__searchline.setText("") self.__searchline.setPlaceholderText("Filter...") self.__searchline.setVisible(True) self.__searchline.textEdited.connect(proxy.setFilterFixedString) style = self.style() # type: QStyle popuprect_origin = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxListBoxPopup, self ) # type: QRect popuprect_origin = QRect( self.mapToGlobal(popuprect_origin.topLeft()), popuprect_origin.size() ) editrect = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self ) # type: QRect self.__searchline.setGeometry(editrect) desktop = QApplication.desktop() screenrect = desktop.availableGeometry(self) # type: QRect # get the height for the view listrect = QRect() for i in range(min(proxy.rowCount(root), self.maxVisibleItems())): index = proxy.index(i, self.modelColumn(), root) if index.isValid(): listrect = listrect.united(popup.visualRect(index)) if listrect.height() >= screenrect.height(): break window = popup.window() # type: QWidget window.ensurePolished() if window.layout() is not None: window.layout().activate() else: QApplication.sendEvent(window, QEvent(QEvent.LayoutRequest)) margins = qwidget_margin_within(popup.viewport(), window) height = (listrect.height() + 2 * popup.spacing() + margins.top() + margins.bottom()) popup_size = (QSize(popuprect_origin.width(), height) .expandedTo(window.minimumSize()) .boundedTo(window.maximumSize()) .boundedTo(screenrect.size())) popuprect = QRect(popuprect_origin.bottomLeft(), popup_size) popuprect = dropdown_popup_geometry( popuprect, popuprect_origin, screenrect) popup.setGeometry(popuprect) current = proxy.mapFromSource( self.model().index(self.currentIndex(), self.modelColumn(), self.rootModelIndex())) popup.setCurrentIndex(current) popup.scrollTo(current, QAbstractItemView.EnsureVisible) popup.show() popup.setFocus(Qt.PopupFocusReason) popup.installEventFilter(self) popup.viewport().installEventFilter(self) popup.viewport().setMouseTracking(True) self.update() self.__popupTimer.restart()
def showPopup(self): # type: () -> None """ Reimplemented from QComboBox.showPopup Popup up a customized view and filter edit line. Note ---- The .popup(), .lineEdit(), .completer() of the base class are not used. """ if self.__popup is not None: # We have user entered state that cannot be disturbed # (entered filter text, scroll offset, ...) return # pragma: no cover if self.count() == 0: return opt = QStyleOptionComboBox() self.initStyleOption(opt) popup = QListView( uniformItemSizes=True, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, verticalScrollBarPolicy=Qt.ScrollBarAsNeeded, iconSize=self.iconSize(), ) popup.setFocusProxy(self.__searchline) popup.setParent(self, Qt.Popup | Qt.FramelessWindowHint) popup.setItemDelegate(_ComboBoxListDelegate(popup)) proxy = QSortFilterProxyModel( popup, filterCaseSensitivity=Qt.CaseInsensitive ) proxy.setFilterKeyColumn(self.modelColumn()) proxy.setSourceModel(self.model()) popup.setModel(proxy) root = proxy.mapFromSource(self.rootModelIndex()) popup.setRootIndex(root) self.__popup = popup self.__proxy = proxy self.__searchline.setText("") self.__searchline.setPlaceholderText("Filter...") self.__searchline.setVisible(True) self.__searchline.textEdited.connect(proxy.setFilterFixedString) style = self.style() # type: QStyle popuprect_origin = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxListBoxPopup, self ) # type: QRect popuprect_origin = QRect( self.mapToGlobal(popuprect_origin.topLeft()), popuprect_origin.size() ) editrect = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self ) # type: QRect self.__searchline.setGeometry(editrect) desktop = QApplication.desktop() screenrect = desktop.availableGeometry(self) # type: QRect # get the height for the view listrect = QRect() for i in range(min(proxy.rowCount(root), self.maxVisibleItems())): index = proxy.index(i, self.modelColumn(), root) if index.isValid(): listrect = listrect.united(popup.visualRect(index)) if listrect.height() >= screenrect.height(): break window = popup.window() # type: QWidget window.ensurePolished() if window.layout() is not None: window.layout().activate() else: QApplication.sendEvent(window, QEvent(QEvent.LayoutRequest)) margins = qwidget_margin_within(popup.viewport(), window) height = (listrect.height() + 2 * popup.spacing() + margins.top() + margins.bottom()) popup_size = (QSize(popuprect_origin.width(), height) .expandedTo(window.minimumSize()) .boundedTo(window.maximumSize()) .boundedTo(screenrect.size())) popuprect = QRect(popuprect_origin.bottomLeft(), popup_size) popuprect = dropdown_popup_geometry( popuprect, popuprect_origin, screenrect) popup.setGeometry(popuprect) current = proxy.mapFromSource( self.model().index(self.currentIndex(), self.modelColumn(), self.rootModelIndex())) popup.setCurrentIndex(current) popup.scrollTo(current, QAbstractItemView.EnsureVisible) popup.show() popup.setFocus(Qt.PopupFocusReason) popup.installEventFilter(self) popup.viewport().installEventFilter(self) popup.viewport().setMouseTracking(True) self.update() self.__popupTimer.restart()
class OWCreateInstance(OWWidget): name = "Create Instance" description = "Interactively create a data instance from sample dataset." icon = "icons/CreateInstance.svg" category = "Data" keywords = ["simulator"] priority = 4000 class Inputs: data = Input("Data", Table) reference = Input("Reference", Table) class Outputs: data = Output("Data", Table) class Information(OWWidget.Information): nans_removed = Msg("Variables with only missing values were " "removed from the list.") want_main_area = False ACTIONS = ["median", "mean", "random", "input"] HEADER = [["name", "Variable"], ["variable", "Value"]] Header = namedtuple("header", [tag for tag, _ in HEADER])(*range(len(HEADER))) values: Dict[str, Union[float, str]] = Setting({}, schema_only=True) append_to_data = Setting(True) auto_commit = Setting(True) def __init__(self): super().__init__() self.data: Optional[Table] = None self.reference: Optional[Table] = None self.filter_edit = QLineEdit(textChanged=self.__filter_edit_changed, placeholderText="Filter...") self.view = QTableView(sortingEnabled=True, contextMenuPolicy=Qt.CustomContextMenu, selectionMode=QTableView.NoSelection) self.view.customContextMenuRequested.connect(self.__menu_requested) self.view.setItemDelegateForColumn(self.Header.variable, VariableDelegate(self)) self.view.verticalHeader().hide() self.view.horizontalHeader().setStretchLastSection(True) self.view.horizontalHeader().setMaximumSectionSize(350) self.model = VariableItemModel(self) self.model.setHorizontalHeaderLabels([x for _, x in self.HEADER]) self.model.dataChanged.connect(self.__table_data_changed) self.model.dataHasNanColumn.connect(self.Information.nans_removed) self.proxy_model = QSortFilterProxyModel() self.proxy_model.setFilterKeyColumn(-1) self.proxy_model.setFilterCaseSensitivity(False) self.proxy_model.setSourceModel(self.model) self.view.setModel(self.proxy_model) vbox = gui.vBox(self.controlArea, box=True) vbox.layout().addWidget(self.filter_edit) vbox.layout().addWidget(self.view) box = gui.hBox(vbox) gui.rubber(box) for name in self.ACTIONS: gui.button(box, self, name.capitalize(), lambda *args, fun=name: self._initialize_values(fun), autoDefault=False) gui.rubber(box) box = gui.auto_apply(self.controlArea, self, "auto_commit") box.button.setFixedWidth(180) box.layout().insertStretch(0) # pylint: disable=unnecessary-lambda append = gui.checkBox(None, self, "append_to_data", "Append this instance to input data", callback=lambda: self.commit()) box.layout().insertWidget(0, append) self._set_input_summary() self._set_output_summary() self.settingsAboutToBePacked.connect(self.pack_settings) def __filter_edit_changed(self): self.proxy_model.setFilterFixedString(self.filter_edit.text().strip()) def __table_data_changed(self): self.commit() def __menu_requested(self, point: QPoint): index = self.view.indexAt(point) model: QSortFilterProxyModel = index.model() source_index = model.mapToSource(index) menu = QMenu(self) for action in self._create_actions(source_index): menu.addAction(action) menu.popup(self.view.viewport().mapToGlobal(point)) def _create_actions(self, index: QModelIndex) -> List[QAction]: actions = [] for name in self.ACTIONS: action = QAction(name.capitalize(), self) action.triggered.connect( lambda *args, fun=name: self._initialize_values(fun, [index])) actions.append(action) return actions def _initialize_values(self, fun: str, indices: List[QModelIndex] = None): cont_fun = { "median": np.nanmedian, "mean": np.nanmean, "random": cont_random, "input": np.nanmean }.get(fun, NotImplemented) disc_fun = { "median": majority, "mean": majority, "random": disc_random, "input": majority }.get(fun, NotImplemented) if not self.data or fun == "input" and not self.reference: return self.model.dataChanged.disconnect(self.__table_data_changed) rows = range(self.proxy_model.rowCount()) if indices is None else \ [index.row() for index in indices] for row in rows: index = self.model.index(row, self.Header.variable) variable = self.model.data(index, VariableRole) if fun == "input": if variable not in self.reference.domain: continue values = self.reference.get_column_view(variable)[0] if variable.is_primitive(): values = values.astype(float) if all(np.isnan(values)): continue else: values = self.model.data(index, ValuesRole) if variable.is_continuous: value = cont_fun(values) value = round(value, variable.number_of_decimals) elif variable.is_discrete: value = disc_fun(values) elif variable.is_string: value = "" else: raise NotImplementedError self.model.setData(index, value, ValueRole) self.model.dataChanged.connect(self.__table_data_changed) self.commit() @Inputs.data def set_data(self, data: Table): self.data = data self._set_input_summary() self._set_model_data() self.unconditional_commit() def _set_model_data(self): self.Information.nans_removed.clear() self.model.removeRows(0, self.model.rowCount()) if not self.data: return self.model.set_data(self.data, self.values) self.values = {} self.view.horizontalHeader().setStretchLastSection(False) self.view.resizeColumnsToContents() self.view.resizeRowsToContents() self.view.horizontalHeader().setStretchLastSection(True) @Inputs.reference def set_reference(self, data: Table): self.reference = data self._set_input_summary() def _set_input_summary(self): n_data = len(self.data) if self.data else 0 n_refs = len(self.reference) if self.reference else 0 summary, details, kwargs = self.info.NoInput, "", {} if self.data or self.reference: summary = f"{self.info.format_number(n_data)}, " \ f"{self.info.format_number(n_refs)}" data_list = [("Data", self.data), ("Reference", self.reference)] details = format_multiple_summaries(data_list) kwargs = {"format": Qt.RichText} self.info.set_input_summary(summary, details, **kwargs) def _set_output_summary(self, data: Optional[Table] = None): if data: summary, details = len(data), format_summary_details(data) else: summary, details = self.info.NoOutput, "" self.info.set_output_summary(summary, details) def commit(self): output_data = None if self.data: output_data = self._create_data_from_values() if self.append_to_data: output_data = self._append_to_data(output_data) self._set_output_summary(output_data) self.Outputs.data.send(output_data) def _create_data_from_values(self) -> Table: data = Table.from_domain(self.data.domain, 1) data.name = "created" data.X[:] = np.nan data.Y[:] = np.nan for i, m in enumerate(self.data.domain.metas): data.metas[:, i] = "" if m.is_string else np.nan values = self._get_values() for var_name, value in values.items(): data[:, var_name] = value return data def _append_to_data(self, data: Table) -> Table: assert self.data assert len(data) == 1 var = DiscreteVariable("Source ID", values=(self.data.name, data.name)) data = Table.concatenate([self.data, data], axis=0) domain = Domain(data.domain.attributes, data.domain.class_vars, data.domain.metas + (var, )) data = data.transform(domain) data.metas[:len(self.data), -1] = 0 data.metas[len(self.data):, -1] = 1 return data def _get_values(self) -> Dict[str, Union[str, float]]: values = {} for row in range(self.model.rowCount()): index = self.model.index(row, self.Header.variable) values[self.model.data(index, VariableRole).name] = \ self.model.data(index, ValueRole) return values def send_report(self): if not self.data: return self.report_domain("Input", self.data.domain) self.report_domain("Output", self.data.domain) items = [] values: Dict = self._get_values() for var in self.data.domain.variables + self.data.domain.metas: val = values.get(var.name, np.nan) if var.is_primitive(): val = var.repr_val(val) items.append([f"{var.name}:", val]) self.report_table("Values", items) @staticmethod def sizeHint(): return QSize(600, 500) def pack_settings(self): self.values: Dict[str, Union[str, float]] = self._get_values()
class ListViewSearch(QListView): """ An QListView with an implicit and transparent row filtering. """ def __init__(self, *a, preferred_size=None, **ak): super().__init__(*a, **ak) self.__search = QLineEdit(self, placeholderText="Filter...") self.__search.textEdited.connect(self.__setFilterString) # Use an QSortFilterProxyModel for filtering. Note that this is # never set on the view, only its rows insertes/removed signals are # connected to observe an update row hidden state. self.__pmodel = QSortFilterProxyModel( self, filterCaseSensitivity=Qt.CaseInsensitive) self.__pmodel.rowsAboutToBeRemoved.connect( self.__filter_rowsAboutToBeRemoved) self.__pmodel.rowsInserted.connect(self.__filter_rowsInserted) self.__layout() self.preferred_size = preferred_size self.setMinimumHeight(100) def setFilterPlaceholderText(self, text: str): self.__search.setPlaceholderText(text) def filterPlaceholderText(self) -> str: return self.__search.placeholderText() def setFilterProxyModel(self, proxy: QSortFilterProxyModel) -> None: """ Set an instance of QSortFilterProxyModel that will be used for filtering the model. The `proxy` must be a filtering proxy only; it MUST not sort the row of the model. The FilterListView takes ownership of the proxy. """ self.__pmodel.rowsAboutToBeRemoved.disconnect( self.__filter_rowsAboutToBeRemoved) self.__pmodel.rowsInserted.disconnect(self.__filter_rowsInserted) self.__pmodel = proxy proxy.setParent(self) self.__pmodel.rowsAboutToBeRemoved.connect( self.__filter_rowsAboutToBeRemoved) self.__pmodel.rowsInserted.connect(self.__filter_rowsInserted) self.__pmodel.setSourceModel(self.model()) self.__filter_reset() def filterProxyModel(self) -> QSortFilterProxyModel: return self.__pmodel def setModel(self, model: QAbstractItemModel) -> None: super().setModel(model) self.__pmodel.setSourceModel(model) self.__filter_reset() self.model().rowsInserted.connect(self.__model_rowInserted) def setRootIndex(self, index: QModelIndex) -> None: super().setRootIndex(index) self.__filter_reset() def __filter_reset(self): root = self.rootIndex() self.__filter(range(self.__pmodel.rowCount(root))) def __setFilterString(self, string: str): self.__pmodel.setFilterFixedString(string) def setFilterString(self, string: str): """Set the filter string.""" self.__search.setText(string) self.__pmodel.setFilterFixedString(string) def filterString(self): """Return the filter string.""" return self.__search.text() def __filter(self, rows: Iterable[int]) -> None: """Set hidden state for rows based on filter string""" root = self.rootIndex() pm = self.__pmodel for r in rows: self.setRowHidden(r, not pm.filterAcceptsRow(r, root)) def __filter_set(self, rows: Iterable[int], state: bool): for r in rows: self.setRowHidden(r, state) def __filter_rowsAboutToBeRemoved(self, parent: QModelIndex, start: int, end: int) -> None: fmodel = self.__pmodel mrange = QItemSelection(fmodel.index(start, 0, parent), fmodel.index(end, 0, parent)) mranges = fmodel.mapSelectionToSource(mrange) for mrange in mranges: self.__filter_set(range(mrange.top(), mrange.bottom() + 1), True) def __filter_rowsInserted(self, parent: QModelIndex, start: int, end: int) -> None: fmodel = self.__pmodel mrange = QItemSelection(fmodel.index(start, 0, parent), fmodel.index(end, 0, parent)) mranges = fmodel.mapSelectionToSource(mrange) for mrange in mranges: self.__filter_set(range(mrange.top(), mrange.bottom() + 1), False) def __model_rowInserted(self, _, start: int, end: int) -> None: """ Filter elements when inserted in list - proxy model's rowsAboutToBeRemoved is not called on elements that are hidden when inserting """ self.__filter(range(start, end + 1)) def resizeEvent(self, event: QResizeEvent) -> None: super().resizeEvent(event) def updateGeometries(self) -> None: super().updateGeometries() self.__layout() def __layout(self): margins = self.viewportMargins() search = self.__search sh = search.sizeHint() size = self.size() margins.setTop(sh.height()) vscroll = self.verticalScrollBar() style = self.style() transient = style.styleHint(QStyle.SH_ScrollBar_Transient, None, vscroll) w = size.width() if vscroll.isVisibleTo(self) and not transient: w = w - vscroll.width() - 1 search.setGeometry(0, 0, w, sh.height()) self.setViewportMargins(margins) def sizeHint(self): return (self.preferred_size if self.preferred_size is not None else super().sizeHint())