class TestListViewSearch(GuiTest): def setUp(self) -> None: super().setUp() self.lv = ListViewSearch() s = ["one", "two", "three", "four"] model = QStringListModel(s) self.lv.setModel(model) def tearDown(self) -> None: super().tearDown() self.lv.deleteLater() self.lv = None def test_list_view(self): num_items = 4 self.assertEqual(num_items, self.lv.model().rowCount()) filter_row = self.lv.findChild(QLineEdit) filter_row.grab() self.lv.grab() QTest.keyClick(filter_row, Qt.Key_E, delay=-1) self.assertListEqual( [False, True, False, True], [self.lv.isRowHidden(i) for i in range(num_items)], ) QTest.keyClick(filter_row, Qt.Key_Backspace) self.assertListEqual( [False] * 4, [self.lv.isRowHidden(i) for i in range(num_items)] ) QTest.keyClick(filter_row, Qt.Key_F) self.assertListEqual( [True, True, True, False], [self.lv.isRowHidden(i) for i in range(num_items)], ) QTest.keyClick(filter_row, Qt.Key_Backspace) QTest.keyClick(filter_row, Qt.Key_T) self.assertListEqual( [True, False, False, True], [self.lv.isRowHidden(i) for i in range(num_items)], ) QTest.keyClick(filter_row, Qt.Key_H) self.assertListEqual( [True, True, False, True], [self.lv.isRowHidden(i) for i in range(num_items)], ) def test_empty(self): self.lv.setModel(QStringListModel([])) self.assertEqual(0, self.lv.model().rowCount()) filter_row = self.lv.findChild(QLineEdit) filter_row.grab() self.lv.grab() QTest.keyClick(filter_row, Qt.Key_T) QTest.keyClick(filter_row, Qt.Key_Backspace)
class OWOntology(OWWidget, ConcurrentWidgetMixin): name = "Ontology" description = "" icon = "icons/Ontology.svg" priority = 1110 keywords = [] CACHED, LIBRARY = range(2) # library list modification types RUN_BUTTON, INC_BUTTON = "Generate", "Include" settingsHandler = DomainContextHandler() ontology_library: List[Dict] = Setting([ {"name": Ontology.generate_name([]), "ontology": {}}, ]) ontology_index: int = Setting(0) ontology: OntoType = Setting((), schema_only=True) include_children = Setting(True) auto_commit = Setting(True) class Inputs: words = Input("Words", Table) class Outputs: words = Output("Words", Table, dynamic=False) class Warning(OWWidget.Warning): no_words_column = Msg("Input is missing 'Words' column.") class Error(OWWidget.Error): load_error = Msg("{}") def __init__(self): OWWidget.__init__(self) ConcurrentWidgetMixin.__init__(self) self.__onto_handler = OntologyHandler() flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable self.__model = PyListModel([], self, flags=flags) self.__input_model = QStandardItemModel() self.__library_view: QListView = None self.__input_view: ListViewSearch = None self.__ontology_view: EditableTreeView = None self.ontology_info = "" self._setup_gui() self._restore_state() self.settingsAboutToBePacked.connect(self._save_state) def _setup_gui(self): # control area library_box: QGroupBox = gui.vBox(self.controlArea, "Library") library_box.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) edit_triggers = QListView.DoubleClicked | QListView.EditKeyPressed self.__library_view = QListView( editTriggers=int(edit_triggers), minimumWidth=200, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding), ) self.__library_view.setFixedHeight(100) self.__library_view.setItemDelegate(LibraryItemDelegate(self)) self.__library_view.setModel(self.__model) self.__library_view.selectionModel().selectionChanged.connect( self.__on_selection_changed ) actions_widget = ModelActionsWidget() actions_widget.layout().setSpacing(1) tool_tip = "Add a new ontology to the library" action = QAction("+", self, toolTip=tool_tip) action.triggered.connect(self.__on_add) actions_widget.addAction(action) tool_tip = "Remove the ontology from the library" action = QAction("\N{MINUS SIGN}", self, toolTip=tool_tip) action.triggered.connect(self.__on_remove) actions_widget.addAction(action) tool_tip = "Save changes in the editor to the library" action = QAction("Update", self, toolTip=tool_tip) action.triggered.connect(self.__on_update) actions_widget.addAction(action) gui.rubber(actions_widget) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Ontology from File", self) new_from_file.triggered.connect(self.__on_import_file) new_from_url = QAction("Import Ontology from URL", self) new_from_url.triggered.connect(self.__on_import_url) save_to_file = QAction("Save Ontology to File", self) save_to_file.triggered.connect(self.__on_save) menu = QMenu(actions_widget) menu.addAction(new_from_file) menu.addAction(new_from_url) menu.addAction(save_to_file) action.setMenu(menu) button = actions_widget.addAction(action) button.setPopupMode(QToolButton.InstantPopup) vlayout = QVBoxLayout() vlayout.setSpacing(1) vlayout.setContentsMargins(0, 0, 0, 0) vlayout.addWidget(self.__library_view) vlayout.addWidget(actions_widget) library_box.layout().setSpacing(1) library_box.layout().addLayout(vlayout) input_box: QGroupBox = gui.vBox(self.controlArea, "Input") self.__input_view = ListViewSearch( sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding), selectionMode=QListView.ExtendedSelection, dragEnabled=True, ) self.__input_view.setModel(self.__input_model) self.__input_view.selectionModel().selectionChanged.connect( self._enable_include_button ) self.__inc_button = gui.button( None, self, self.INC_BUTTON, enabled=False, toolTip="Include selected words into the ontology", autoDefault=False, callback=self.__on_toggle_include ) input_box.layout().setSpacing(1) input_box.layout().addWidget(self.__input_view) input_box.layout().addWidget(self.__inc_button) self.__run_button = gui.button( self.controlArea, self, self.RUN_BUTTON, callback=self.__on_toggle_run ) gui.checkBox( self.controlArea, self, "include_children", "Include subtree", box="Output", callback=self.commit.deferred ) box = gui.vBox(self.controlArea, "Ontology info") gui.label(box, self, "%(ontology_info)s") gui.auto_send(self.buttonsArea, self, "auto_commit") # main area ontology_box: QGroupBox = gui.vBox(self.mainArea, box=True) self.__ontology_view = EditableTreeView(self) self.__ontology_view.dataChanged.connect( self.__on_ontology_data_changed ) self.__ontology_view.selectionChanged.connect(self.commit.deferred) ontology_box.layout().setSpacing(1) ontology_box.layout().addWidget(self.__ontology_view) self._enable_include_button() def __on_selection_changed(self, selection: QItemSelection, *_): self.Error.load_error.clear() if selection.indexes(): self.ontology_index = row = selection.indexes()[0].row() data = self.__model[row].cached_word_tree self.__ontology_view.set_data(data) self.__update_score() error_msg = self.__model[row].error_msg if error_msg: self.Error.load_error(error_msg) def __on_add(self): name = Ontology.generate_name([l.name for l in self.__model]) data = self.__ontology_view.get_data() self.__model.append(Ontology(name, data)) self.__set_selected_row(len(self.__model) - 1) def __on_remove(self): index = self.__get_selected_row() if index is not None: del self.__model[index] self.__set_selected_row(max(index - 1, 0)) def __on_update(self): self.__set_current_modified(self.LIBRARY) def __on_import_file(self): ontology = read_from_file(self) self._import_ontology(ontology) def __on_import_url(self): ontology = read_from_url(self) self._import_ontology(ontology) def __on_save(self): index = self.__get_selected_row() if index is not None: filename = self.__model[index].filename if filename: filename, _ = os.path.splitext(filename) else: filename = os.path.expanduser("~/") save_ontology(self, filename, self.__ontology_view.get_data()) QApplication.setActiveWindow(self) def __on_toggle_include(self): if self.task is not None: self._cancel_tasks() else: self._run_insert() def __on_toggle_run(self): if self.task is not None: self._cancel_tasks() else: self._run() def __on_ontology_data_changed(self): self.__set_current_modified(self.CACHED) self.__update_score() self._enable_include_button() self.commit.deferred() @Inputs.words def set_words(self, words: Optional[Table]): self.Warning.no_words_column.clear() self.__input_model.clear() if words: if WORDS_COLUMN_NAME in words.domain and words.domain[ WORDS_COLUMN_NAME].attributes.get("type") == "words": for word in words.get_column_view(WORDS_COLUMN_NAME)[0]: self.__input_model.appendRow(QStandardItem(word)) else: self.Warning.no_words_column() @gui.deferred def commit(self): if self.include_children: words = self.__ontology_view.get_selected_words_with_children() else: words = self.__ontology_view.get_selected_words() words_table = self._create_output_table(sorted(words)) self.Outputs.words.send(words_table) @staticmethod def _create_output_table(words: List[str]) -> Optional[Table]: if not words: return None return create_words_table(words) def _cancel_tasks(self): self.cancel() self.__inc_button.setText(self.INC_BUTTON) self.__run_button.setText(self.RUN_BUTTON) def _run(self): self.__run_button.setText("Stop") words = self.__ontology_view.get_words() handler = self.__onto_handler.generate self.start(_run, handler, (words,)) def _run_insert(self): self.__inc_button.setText("Stop") tree = self.__ontology_view.get_data() words = self.__get_selected_input_words() handler = self.__onto_handler.insert self.start(_run, handler, (tree, words)) def on_done(self, data: Dict): self.__inc_button.setText(self.INC_BUTTON) self.__run_button.setText(self.RUN_BUTTON) self.__ontology_view.set_data(data, keep_history=True) self.__set_current_modified(self.CACHED) self.__update_score() def __update_score(self): tree = self.__ontology_view.get_data() score = round(self.__onto_handler.score(tree), 2) \ if len(tree) == 1 and list(tree.values())[0] else "/" self.ontology_info = f"Score: {score}" def on_exception(self, ex: Exception): raise ex def on_partial_result(self, _: Any): pass def onDeleteWidget(self): self.shutdown() super().onDeleteWidget() def __set_selected_row(self, row: int): self.__library_view.selectionModel().select( self.__model.index(row, 0), QItemSelectionModel.ClearAndSelect ) def __get_selected_row(self) -> Optional[int]: rows = self.__library_view.selectionModel().selectedRows() return rows[0].row() if rows else None def __set_current_modified(self, mod_type: int): index = self.__get_selected_row() if index is not None: if mod_type == self.LIBRARY: ontology = self.__ontology_view.get_data() self.__model[index].word_tree = ontology self.__model[index].cached_word_tree = ontology self.__model[index].update_rule_flag = Ontology.NotModified elif mod_type == self.CACHED: ontology = self.__ontology_view.get_data() self.__model[index].cached_word_tree = ontology else: raise NotImplementedError self.__model.emitDataChanged(index) self.__library_view.repaint() def __get_selected_input_words(self) -> List[str]: return [self.__input_view.model().data(index) for index in self.__input_view.selectedIndexes()] def _import_ontology(self, ontology: Ontology): if ontology is not None: self.__model.append(ontology) self.__set_selected_row(len(self.__model) - 1) QApplication.setActiveWindow(self) def _restore_state(self): source = [Ontology.from_dict(s) for s in self.ontology_library] self.__model.wrap(source) self.__set_selected_row(self.ontology_index) if self.ontology: self.__ontology_view.set_data(self.ontology) self.__set_current_modified(self.CACHED) self.__update_score() self.commit.now() def _save_state(self): self.ontology_library = [s.as_dict() for s in self.__model] self.ontology = self.__ontology_view.get_data(with_selection=True) def _enable_include_button(self): tree = self.__ontology_view.get_data() words = self.__get_selected_input_words() enabled = len(tree) == 1 and len(words) > 0 self.__inc_button.setEnabled(enabled) def send_report(self): model = self.__model library = model[self.ontology_index].name if model else "/" self.report_items("Settings", [("Library", library)]) ontology = self.__ontology_view.get_data() style = """ <style> ul { padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 20px; } </style> """ self.report_raw("Ontology", style + _tree_to_html(ontology))