class OWWordList(OWWidget): name = "Word List" description = "Create a list of words." icon = "icons/WordList.svg" priority = 1000 class Inputs: words = Input("Words", Table) class Outputs: selected_words = Output("Selected Words", Table) words = Output("Words", Table) class Warning(OWWidget.Warning): no_string_vars = Msg("Input needs at least one Text variable.") NONE, CACHED, LIBRARY = range(3) # library list modification types want_main_area = False resizing_enabled = True settingsHandler = DomainContextHandler() word_list_library: List[Dict] = Setting([ { "name": WordList.generate_word_list_name([]), "words": [] }, ]) word_list_index: int = Setting(0) words_var: Optional[StringVariable] = ContextSetting(None) update_rule_index: int = Setting(UpdateRules.INTERSECT) words: List[str] = Setting(None, schema_only=True) selected_words: Set[str] = Setting(set(), schema_only=True) def __init__(self): super().__init__(self) flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable self.library_model = PyListModel([], self, flags=flags) self.words_model = PyListModel([], self, flags=flags, enable_dnd=True) self.library_view: QListView = None self.words_view: ListView = None self.__input_words_model = DomainModel(valid_types=(StringVariable, )) self.__input_words: Optional[Table] = None self.__library_box: QGroupBox = gui.vBox(None, "Library") self.__input_box: QGroupBox = gui.vBox(None, "Input") self.__words_box: QGroupBox = gui.vBox(None, box=True) self.__update_rule_rb: QRadioButton = None self.__add_word_action: QAction = None self.__remove_word_action: QAction = None self._setup_gui() self._restore_state() self.settingsAboutToBePacked.connect(self._save_state) def _setup_gui(self): layout = QGridLayout() gui.widgetBox(self.controlArea, orientation=layout) self._setup_library_box() self._setup_input_box() self._setup_words_box() layout.addWidget(self.__library_box, 0, 0) layout.addWidget(self.__input_box, 1, 0) layout.addWidget(self.__words_box, 0, 1, 0, 1) def _setup_library_box(self): self.library_view = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, minimumWidth=200, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding), ) self.library_view.setItemDelegate(WordListItemDelegate(self)) self.library_view.setModel(self.library_model) self.library_view.selectionModel().selectionChanged.connect( self.__on_library_selection_changed) self.__library_box.layout().setSpacing(1) self.__library_box.layout().addWidget(self.library_view) actions_widget = ModelActionsWidget() actions_widget.layout().setSpacing(1) action = QAction("+", self) action.setToolTip("Add a new word list to the library") action.triggered.connect(self.__on_add_word_list) actions_widget.addAction(action) action = QAction("\N{MINUS SIGN}", self) action.setToolTip("Remove word list from library") action.triggered.connect(self.__on_remove_word_list) actions_widget.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.__on_update_word_list) actions_widget.addAction(action) gui.rubber(actions_widget.layout()) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Words from File", self) new_from_file.triggered.connect(self.__on_import_word_list) save_to_file = QAction("Save Words to File", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) save_to_file.triggered.connect(self.__on_save_word_list) menu = QMenu(actions_widget) menu.addAction(new_from_file) menu.addAction(save_to_file) action.setMenu(menu) button = actions_widget.addAction(action) button.setPopupMode(QToolButton.InstantPopup) self.__library_box.layout().addWidget(actions_widget) def __on_library_selection_changed(self, selected: QItemSelection, *_): index = [i.row() for i in selected.indexes()] if index: current = index[0] word_list: WordList = self.library_model[current] self.word_list_index = current self.selected_words = set() self.words_model.wrap(list(word_list.cached_words)) self._apply_update_rule() def __on_add_word_list(self): taken = [l.name for l in self.library_model] name = WordList.generate_word_list_name(taken) word_list = WordList(name, self.words_model[:]) self.library_model.append(word_list) self._set_selected_word_list(len(self.library_model) - 1) def __on_remove_word_list(self): index = self._get_selected_word_list_index() if index is not None: del self.library_model[index] self._set_selected_word_list(max(index - 1, 0)) self._apply_update_rule() def __on_update_word_list(self): self._set_word_list_modified(mod_type=self.LIBRARY) def __on_import_word_list(self): filename, _ = QFileDialog.getOpenFileName( self, "Open Word List", os.path.expanduser("~/"), "Text files (*.txt)\nAll files(*.*)") if filename: name = os.path.basename(filename) with open(filename, encoding="utf-8") as f: words = [line.strip() for line in f.readlines()] self.library_model.append(WordList(name, words, filename=filename)) self._set_selected_word_list(len(self.library_model) - 1) self._apply_update_rule() def __on_save_word_list(self): index = self._get_selected_word_list_index() if index is not None: word_list = self.library_model[index] filename = word_list.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, "Save Word List", filename, "Text files (*.txt)\nAll files(*.*)") if filename: head, tail = os.path.splitext(filename) if not tail: filename = head + ".txt" with open(filename, "w", encoding="utf-8") as f: for word in self.words_model: f.write(f"{word}\n") def _setup_input_box(self): gui.comboBox(self.__input_box, self, "words_var", label="Word variable:", orientation=Qt.Vertical, model=self.__input_words_model, callback=self._apply_update_rule) gui.radioButtons(self.__input_box, self, "update_rule_index", UpdateRules.ITEMS, label="Update: ", orientation=Qt.Vertical, callback=self.__on_update_rule_changed) self.__input_box.setEnabled(False) def __on_update_rule_changed(self): self._enable_words_actions() self._apply_update_rule() def _setup_words_box(self): self.words_view = ListView() self.words_view.drop_finished.connect(self.__on_words_data_changed) self.words_view.setModel(self.words_model) self.words_view.selectionModel().selectionChanged.connect( self.__on_words_selection_changed) self.words_model.dataChanged.connect(self.__on_words_data_changed) self.__words_box.layout().setSpacing(1) self.__words_box.layout().addWidget(self.words_view) actions_widget = ModelActionsWidget() actions_widget.layout().setSpacing(1) action = QAction("+", self.words_view, toolTip="Add a new word") action.triggered.connect(self.__on_add_word) actions_widget.addAction(action) self.__add_word_action = action action = QAction("\N{MINUS SIGN}", self, toolTip="Remove word") action.triggered.connect(self.__on_remove_word) actions_widget.addAction(action) self.__remove_word_action = action gui.rubber(actions_widget) action = QAction("Sort", self) action.setToolTip("Sort words alphabetically") action.triggered.connect(self.__on_apply_sorting) actions_widget.addAction(action) self.__words_box.layout().addWidget(actions_widget) def __on_words_data_changed(self): self._set_word_list_modified(mod_type=self.CACHED) self.commit() def __on_words_selection_changed(self): self.commit() def __on_add_word(self): row = self.words_model.rowCount() if not self.words_model.insertRow(self.words_model.rowCount()): return with disconnected(self.words_view.selectionModel().selectionChanged, self.__on_words_selection_changed): self._set_selected_words([0]) index = self.words_model.index(row, 0) self.words_view.setCurrentIndex(index) self.words_model.setItemData(index, {Qt.EditRole: ""}) self.words_view.edit(index) def __on_remove_word(self): rows = self.words_view.selectionModel().selectedRows(0) if not rows: return indices = sorted([row.row() for row in rows], reverse=True) with disconnected(self.words_view.selectionModel().selectionChanged, self.__on_words_selection_changed): for index in indices: self.words_model.removeRow(index) if self.words_model: self._set_selected_words([max(0, indices[-1] - 1)]) self.__on_words_data_changed() def __on_apply_sorting(self): if not self.words_model: return words = self.words_model[:] mask = np.zeros(len(words), dtype=bool) selection = self._get_selected_words_indices() if selection: mask[selection] = True indices = np.argsort(words) self.words_model.wrap([words[i] for i in indices]) self._set_word_list_modified(mod_type=self.CACHED) if selection: self._set_selected_words(list(np.flatnonzero(mask[indices]))) else: self.commit() @Inputs.words def set_words(self, words: Optional[Table]): self.closeContext() self.__input_words = words self._check_input_words() self._init_controls() self.openContext(self.__input_words) self._apply_update_rule() def _check_input_words(self): self.Warning.no_string_vars.clear() if self.__input_words: metas = self.__input_words.domain.metas if not any(isinstance(m, StringVariable) for m in metas): self.Warning.no_string_vars() self.__input_words = None def _init_controls(self): words = self.__input_words domain = words.domain if words is not None else None self.__input_words_model.set_domain(domain) if len(self.__input_words_model) > 0: self.words_var = self.__input_words_model[0] self.__input_box.setEnabled(bool(self.__input_words_model)) self._enable_words_actions() def _enable_words_actions(self): if bool(self.__input_words_model) \ and self.update_rule_index != UpdateRules.LIBRARY: self.words_view.setEditTriggers(QListView.NoEditTriggers) self.__add_word_action.setEnabled(False) self.__remove_word_action.setEnabled(False) else: self.words_view.setEditTriggers(QListView.DoubleClicked | QListView.EditKeyPressed) self.__add_word_action.setEnabled(True) self.__remove_word_action.setEnabled(True) def _apply_update_rule(self): lib_index = self._get_selected_word_list_index() lib_words, in_words, update_rule = [], [], UpdateRules.LIBRARY if lib_index is not None: lib_words = self.library_model[lib_index].cached_words else: lib_words = self.words_model[:] if self.__input_words is not None: in_words = self.__input_words.get_column_view(self.words_var)[0] in_words = list(in_words) update_rule = self.update_rule_index UpdateRules.update(self.words_model, lib_words, in_words, update_rule) if lib_index is not None: cached = self.library_model[lib_index].cached_words modified = WordList.NotModified if cached == self.words_model[:] \ else WordList.Modified self.library_model[lib_index].update_rule_flag = modified self._set_word_list_modified(mod_type=self.NONE) self.library_view.repaint() # Apply selection. selection_changed invokes commit(). # If there is no selection, call commit explicitly. if any(w in self.selected_words for w in self.words_model): self.set_selected_words() self.words_view.repaint() else: self.commit() def commit(self): selection = self._get_selected_words_indices() self.selected_words = set(np.array(self.words_model)[selection]) words, selected_words = None, None if self.words_model: words_var = StringVariable("Words") words_var.attributes = {"type": "words"} domain = Domain([], metas=[words_var]) _words = Table.from_list(domain, [[w] for w in self.words_model]) _words.name = "Words" if selection: selected_words = _words[selection] words = create_annotated_table(_words, selection) self.Outputs.words.send(words) self.Outputs.selected_words.send(selected_words) def _set_word_list_modified(self, mod_type): index = self._get_selected_word_list_index() if index is not None: if mod_type == self.LIBRARY: self.library_model[index].words = self.words_model[:] self.library_model[index].cached_words = self.words_model[:] self.library_model[index].update_rule_flag \ = WordList.NotModified elif mod_type == self.CACHED: self.library_model[index].cached_words = self.words_model[:] elif mod_type == self.NONE: pass else: raise NotImplementedError self.library_model.emitDataChanged(index) self.library_view.repaint() def _set_selected_word_list(self, index: int): sel_model: QItemSelectionModel = self.library_view.selectionModel() sel_model.select(self.library_model.index(index, 0), QItemSelectionModel.ClearAndSelect) def _get_selected_word_list_index(self) -> Optional[int]: rows = self.library_view.selectionModel().selectedRows() return rows[0].row() if rows else None def _set_selected_words(self, indices: List[int]): selection = QItemSelection() sel_model: QItemSelectionModel = self.words_view.selectionModel() for i in indices: selection.append(QItemSelectionRange(self.words_model.index(i, 0))) sel_model.select(selection, QItemSelectionModel.ClearAndSelect) def _get_selected_words_indices(self) -> List[int]: rows = self.words_view.selectionModel().selectedRows() return [row.row() for row in rows] def set_selected_words(self): if self.selected_words: indices = [ i for i, w in enumerate(self.words_model) if w in self.selected_words ] self._set_selected_words(indices) def _restore_state(self): source = [WordList.from_dict(s) for s in self.word_list_library] self.library_model.wrap(source) # __on_library_selection_changed() (invoked by _set_selected_word_list) # clears self.selected_words selected_words = self.selected_words self._set_selected_word_list(self.word_list_index) if self.words is not None: self.words_model.wrap(list(self.words)) self._set_word_list_modified(mod_type=self.CACHED) if selected_words: self.selected_words = selected_words self.set_selected_words() elif len(self.word_list_library) > self.word_list_index and \ self.word_list_library[self.word_list_index] != self.words: self.commit() def _save_state(self): self.word_list_library = [s.as_dict() for s in self.library_model] self.words = self.words_model[:] def send_report(self): library = self.library_model[self.word_list_index].name \ if self.library_model else "/" settings = [("Library", library)] if self.__input_words: self.report_data("Input Words", self.__input_words) settings.append(("Word variable", self.words_var)) rule = UpdateRules.ITEMS[self.update_rule_index] settings.append(("Update", rule)) self.report_items("Settings", settings) self.report_paragraph("Words", ", ".join(self.words_model[:]))
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))