Beispiel #1
0
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)
Beispiel #2
0
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))