Ejemplo n.º 1
0
class AnnotationDialog(QMainWindow):
    """ The dialog shown to the user to do the annotation/labeling."""

    def __init__(self, textmodel):
        print("Showing annotation dialog...")
        app = QApplication([])
        super().__init__()
        self.setWindowIcon(self.get_icon("icon.ico"))
        self.textmodel = textmodel
        self.layout_controls()
        self.setup_and_wire_navigator_incl_buttons()
        self.setup_and_wire_shortcuts()
        self.show()
        app.exec_()

    @staticmethod
    def get_icon(file):
        return QIcon(
            os.path.join(
                os.path.abspath(os.path.dirname(__file__)),
                "../resources/{}".format(file),
            )
        )

    def layout_controls(self):
        # window
        self.setWindowTitle("neanno")
        screen = QDesktopWidget().screenGeometry()
        self.setGeometry(0, 0, screen.width() * 0.75, screen.height() * 0.75)
        mysize = self.geometry()
        horizontal_position = (screen.width() - mysize.width()) / 2
        vertical_position = (screen.height() - mysize.height()) / 6
        self.move(horizontal_position, vertical_position)

        # text edit
        self.textedit = QPlainTextEdit()
        self.textedit.setStyleSheet(
            """QPlainTextEdit {
                font-size: 14pt;
                font-family: Consolas;
                color: lightgrey;
                background-color: black
            }"""
        )
        self.textedit_highlighter = TextEditHighlighter(
            self.textedit.document(), config.named_entity_definitions
        )
        self.textedit.textChanged.connect(self.textedit_text_changed)

        # annotation monitor
        self.annotation_monitor = QPlainTextEdit()
        self.annotation_monitor.setReadOnly(True)
        self.annotation_monitor.setStyleSheet(
            """
                font-size: 14pt;
                font-family: Consolas;
                color: lightgrey;
                background-color: black
            """
        )

        # navigation / about / shortcuts buttons
        navigation_buttons_layout = QHBoxLayout()
        self.backward_button = QPushButton(self.get_icon("backward.png"), None)
        self.backward_button.setToolTip("Backward")
        navigation_buttons_layout.addWidget(self.backward_button)
        self.forward_button = QPushButton(self.get_icon("forward.png"), None)
        self.forward_button.setToolTip("Forward")
        navigation_buttons_layout.addWidget(self.forward_button)
        navigation_buttons_layout.addStretch()
        self.first_button = QPushButton(self.get_icon("first.png"), None)
        self.first_button.setToolTip("First")
        navigation_buttons_layout.addWidget(self.first_button)
        self.previous_button = QPushButton(self.get_icon("previous.png"), None)
        self.previous_button.setToolTip("Previous")
        navigation_buttons_layout.addWidget(self.previous_button)
        self.next_button = QPushButton(self.get_icon("next.png"), None)
        self.next_button.setToolTip("Next")
        navigation_buttons_layout.addWidget(self.next_button)
        self.last_button = QPushButton(self.get_icon("last.png"), None)
        self.last_button.setToolTip("Last")
        navigation_buttons_layout.addWidget(self.last_button)
        self.goto_button = QPushButton(self.get_icon("goto.png"), None)
        self.goto_button.setToolTip("Go to index")
        navigation_buttons_layout.addWidget(self.goto_button)
        self.search_button = QPushButton(self.get_icon("search.png"), None)
        self.search_button.setToolTip("Search a text")
        navigation_buttons_layout.addWidget(self.search_button)
        self.submit_next_best_button = QPushButton(
            self.get_icon("submit_next_best.png"), None
        )
        self.submit_next_best_button.setToolTip("Submit and go to next best text")
        navigation_buttons_layout.addWidget(self.submit_next_best_button)
        navigation_buttons_layout.addStretch()
        if config.has_instructions:
            instructions_button = QPushButton("Instructions")
            instructions_button.setToolTip("Instructions")
            navigation_buttons_layout.addWidget(instructions_button)
            instructions_button.clicked.connect(
                lambda: QMessageBox.information(
                    self, "Instructions", config.instructions, QMessageBox.Ok
                )
            )
        shortcuts_button = QPushButton("Shortcuts")
        shortcuts_button.setToolTip("Show general shortcuts")
        shortcuts_button.clicked.connect(lambda: show_shortcuts_dialog(self))
        navigation_buttons_layout.addWidget(shortcuts_button)
        about_button = QPushButton("About")
        about_button.setToolTip("About")
        about_button.clicked.connect(lambda: show_about_dialog(self))
        navigation_buttons_layout.addWidget(about_button)

        # progress bar
        self.progressbar = QProgressBar()
        self.progressbar.setValue(0)

        # categories
        # note: CategoriesSelectorWidget populates itself (mostly due to the QTableWidget control, might be improved in future)
        if config.is_categories_enabled:
            self.categories_selector = CategoriesSelectorWidget(config, self.textmodel)
            self.categories_selector.selectionModel().selectionChanged.connect(
                self.update_annotation_monitor
            )
            categories_groupbox_layout = QHBoxLayout()
            categories_groupbox_layout.addWidget(self.categories_selector)
            categories_groupbox_layout.setSizeConstraint(QLayout.SetFixedSize)
            categories_groupbox = QGroupBox("Categories")
            categories_groupbox.setLayout(categories_groupbox_layout)

        # key_terms
        if config.is_key_terms_enabled:
            key_terms_layout = QHBoxLayout()
            key_terms_layout.addWidget(
                QLabel(
                    "Select a text and press\n- {} to mark a key term.\n- {} to mark with consolidating terms.".format(
                        config.key_terms_shortcut_mark_standalone,
                        config.key_terms_shortcut_mark_parented,
                    )
                )
            )
            key_terms_groupbox = QGroupBox("Key Terms")
            key_terms_groupbox.setLayout(key_terms_layout)

        # entity shortcuts / counts
        if config.is_named_entities_enabled:
            named_entity_infos_layout = QHBoxLayout()
            self.named_entity_infos_markup_control = QLabel()
            self.named_entity_infos_markup_control.setTextFormat(Qt.RichText)
            named_entity_infos_layout.addWidget(self.named_entity_infos_markup_control)
            named_entities_groupbox = QGroupBox("Named Entities")
            named_entities_groupbox.setLayout(named_entity_infos_layout)

        # language
        if config.uses_languages:
            language_layout = QHBoxLayout()
            self.language_combobox = QComboBox()
            self.language_combobox.addItems(config.languages_available_for_selection)
            language_layout.addWidget(self.language_combobox)
            language_groupbox = QGroupBox("Language")
            language_groupbox.setLayout(language_layout)

        # predictors / modelling
        if config.prediction_pipeline.has_predictors():
            predictors_from_vertical_layout = QVBoxLayout()

            manage_predictors_button = QPushButton("Manage Predictors")
            manage_predictors_button.clicked.connect(self.configure_predictors)
            predictors_from_vertical_layout.addWidget(manage_predictors_button)

            trigger_batch_trainings_button = QPushButton("Trigger Batch Training(s)")
            trigger_batch_trainings_button.clicked.connect(self.trigger_batch_trainings)
            predictors_from_vertical_layout.addWidget(trigger_batch_trainings_button)

            export_pipeline_model_button = QPushButton("Export Pipeline Model")
            export_pipeline_model_button.clicked.connect(self.export_pipeline_model)
            predictors_from_vertical_layout.addWidget(export_pipeline_model_button)
            predictors_from_groupbox = QGroupBox("Predictors")

            predictors_from_groupbox.setLayout(predictors_from_vertical_layout)

        # dataset
        dataset_grid = QGridLayout()
        dataset_grid.addWidget(QLabel("Annotated"), 0, 0)
        dataset_grid.addWidget(self.progressbar, 0, 1)
        dataset_grid.addWidget(QLabel("Annotated Texts"), 1, 0)
        self.annotated_texts_label = QLabel()
        dataset_grid.addWidget(self.annotated_texts_label, 1, 1)
        dataset_grid.addWidget(QLabel("Total Texts"), 2, 0)
        self.total_texts_label = QLabel()
        dataset_grid.addWidget(self.total_texts_label, 2, 1)
        dataset_grid.addWidget(QLabel("Current Index"), 3, 0)
        self.current_text_index_label = QLabel()
        dataset_grid.addWidget(self.current_text_index_label, 3, 1)
        dataset_grid.addWidget(QLabel("Is Annotated"), 4, 0)
        self.is_annotated_label = QLabel()
        dataset_grid.addWidget(self.is_annotated_label, 4, 1)
        if config.dataset_source_friendly:
            dataset_grid.addWidget(QLabel("Source"), 5, 0)
            self.dataset_source_friendly_label = QLabel(config.dataset_source_friendly)
            dataset_grid.addWidget(self.dataset_source_friendly_label, 5, 1)
        if config.dataset_target_friendly:
            dataset_grid.addWidget(QLabel("Target"), 6, 0)
            self.dataset_target_friendly_label = QLabel(config.dataset_target_friendly)
            dataset_grid.addWidget(self.dataset_target_friendly_label, 6, 1)
        dataset_groupbox = QGroupBox("Dataset")
        dataset_groupbox.setLayout(dataset_grid)

        # close
        close_button = QPushButton("Close")
        close_button.setToolTip("Close")
        close_button.clicked.connect(self.close)

        # remaining layouts
        # left panel
        left_panel_layout = QVBoxLayout()
        left_panel_layout_splitter = QSplitter(Qt.Vertical)
        left_panel_layout_splitter.addWidget(self.textedit)
        left_panel_layout_splitter.addWidget(self.annotation_monitor)
        left_panel_layout_splitter.setSizes([400, 100])
        left_panel_layout.addWidget(left_panel_layout_splitter)
        # right panel
        right_panel_layout = QVBoxLayout()
        if config.is_categories_enabled:
            right_panel_layout.addWidget(categories_groupbox)
        if config.is_key_terms_enabled:
            right_panel_layout.addWidget(key_terms_groupbox)
        if config.is_named_entities_enabled:
            right_panel_layout.addWidget(named_entities_groupbox)
        if config.uses_languages:
            right_panel_layout.addWidget(language_groupbox)
        if config.prediction_pipeline.has_predictors():
            right_panel_layout.addWidget(predictors_from_groupbox)
        right_panel_layout.addWidget(dataset_groupbox)
        right_panel_layout.addStretch()
        right_buttons_layout = QHBoxLayout()
        right_buttons_layout.addStretch()
        right_buttons_layout.addWidget(close_button)
        # main
        main_grid = QGridLayout()
        main_grid.setSpacing(10)
        main_grid.setColumnStretch(0, 1)
        main_grid.setColumnStretch(1, 0)
        main_grid.addLayout(left_panel_layout, 0, 0)
        main_grid.addLayout(right_panel_layout, 0, 1)
        main_grid.addLayout(navigation_buttons_layout, 1, 0)
        main_grid.addLayout(right_buttons_layout, 1, 1)
        central_widget = QWidget()
        central_widget.setLayout(main_grid)
        self.setCentralWidget(central_widget)

        # update the dataset-related controls so they show up
        self.update_dataset_related_controls()

    def setup_and_wire_shortcuts(self):
        # annotate key terms shortcuts
        if config.is_key_terms_enabled:
            register_shortcut(
                self,
                config.key_terms_shortcut_mark_parented,
                self.annotate_parented_key_term,
            )
            register_shortcut(
                self,
                config.key_terms_shortcut_mark_standalone,
                self.annotate_standalone_key_term,
            )
        # named entity shortcuts
        for named_entity_definition in config.named_entity_definitions:
            # standalone
            register_shortcut(
                self,
                named_entity_definition.key_sequence,
                self.annotate_standalone_named_entity,
            )
            # parented
            register_shortcut(
                self,
                "Shift+{}".format(named_entity_definition.key_sequence),
                self.annotate_parented_named_entity,
            )
        # submit next
        register_shortcut(self, SHORTCUT_SUBMIT_NEXT, self.submit_and_go_to_next)
        # submit next best
        register_shortcut(
            self, SHORTCUT_SUBMIT_NEXT_BEST, self.submit_and_go_to_next_best
        )
        # navigation shortcuts
        register_shortcut(self, SHORTCUT_BACKWARD, self.navigator.backward)
        register_shortcut(self, SHORTCUT_FORWARD, self.navigator.forward)
        register_shortcut(self, SHORTCUT_FIRST, self.navigator.toFirst)
        register_shortcut(self, SHORTCUT_PREVIOUS, self.navigator.toPrevious)
        register_shortcut(self, SHORTCUT_NEXT, self.navigator.toNext)
        register_shortcut(self, SHORTCUT_LAST, self.navigator.toLast)
        register_shortcut(self, SHORTCUT_GOTO, self.go_to_index)
        register_shortcut(self, SHORTCUT_SEARCH, self.search)
        # remove/reset/revert/... shortcuts
        register_shortcut(
            self, SHORTCUT_REMOVE_ANNOTATION_AT_CURSOR, self.remove_annotation
        )
        register_shortcut(
            self, SHORTCUT_REMOVE_ALL_FOR_CURRENT_TEXT, self.remove_all_annotations
        )
        register_shortcut(
            self, SHORTCUT_RESET_IS_ANNOTATED_FLAG, self.reset_is_annotated_flag
        )
        register_shortcut(
            self,
            SHORTCUT_RESET_ALL_IS_ANNOTATED_FLAGS,
            self.reset_all_is_annotated_flags,
        )
        register_shortcut(self, SHORTCUT_REVERT_CHANGES, self.revert_changes)

    def setup_and_wire_navigator_incl_buttons(self):
        self.navigator = TextNavigator(self)
        self.navigator.setSubmitPolicy(QDataWidgetMapper.ManualSubmit)
        self.navigator.setModel(self.textmodel)
        if config.uses_languages:
            self.navigator.addMapping(self.language_combobox, 0)
        self.navigator.addMapping(self.textedit, 1)
        if config.is_categories_enabled:
            self.navigator.addMapping(
                self.categories_selector,
                2,
                QByteArray().insert(0, "selected_categories_text"),
            )
        self.navigator.addMapping(
            self.is_annotated_label, 3, QByteArray().insert(0, "text")
        )
        self.navigator.currentIndexChanged.connect(
            self.update_navigation_related_controls
        )
        self.navigator.setCurrentIndex(self.textmodel.get_next_best_row_index(-1))
        self.backward_button.clicked.connect(self.navigator.backward)
        self.forward_button.clicked.connect(self.navigator.forward)
        self.first_button.clicked.connect(self.navigator.toFirst)
        self.previous_button.clicked.connect(self.navigator.toPrevious)
        self.next_button.clicked.connect(self.navigator.toNext)
        self.last_button.clicked.connect(self.navigator.toLast)
        self.goto_button.clicked.connect(self.go_to_index)
        self.search_button.clicked.connect(self.search)
        self.submit_next_best_button.clicked.connect(self.submit_and_go_to_next_best)

    def update_navigation_related_controls(self):
        # current index
        self.current_text_index_label.setText(str(self.navigator.currentIndex()))
        # remove focus from controls
        if config.uses_languages:
            self.language_combobox.clearFocus()
        # textedit
        self.textedit.clearFocus()
        # categories_selector
        if config.is_categories_enabled:
            self.categories_selector.clearFocus()

    def update_dataset_related_controls(self):
        # annotated texts count
        annotated_texts_count = self.textmodel.get_annotated_texts_count()
        self.annotated_texts_label.setText(str(annotated_texts_count))
        # total texts count
        total_texts_count = self.textmodel.rowCount()
        self.total_texts_label.setText(str(total_texts_count))
        # categories frequency
        if config.is_categories_enabled:
            self.categories_selector.update_categories_distribution()
        # entity infos markup
        if config.is_named_entities_enabled:
            entity_infos_markup = "<table width='100%'>"
            for named_entity_definition in config.named_entity_definitions:
                entity_infos_markup += "<tr>"
                entity_infos_markup += "<td style='background-color:{}; padding-left: 5'> </td>".format(
                    named_entity_definition.maincolor
                )
                entity_infos_markup += "<td style='padding-left: 5'>{}</td>".format(
                    named_entity_definition.code
                )
                entity_infos_markup += "<td style='padding-left: 5'>{}</td>".format(
                    named_entity_definition.key_sequence
                )
                entity_infos_markup += "<td style='width: 100%; text-align: right'>{}</td>".format(
                    str(
                        self.textmodel.named_entity_distribution[
                            named_entity_definition.code
                        ]
                    )
                    if named_entity_definition.code
                    in self.textmodel.named_entity_distribution
                    else "0"
                )
                entity_infos_markup += "</tr>"
            entity_infos_markup += "</table>"
            entity_infos_markup += "<p>Add Shift key to add consolidating terms.</p>"
            self.named_entity_infos_markup_control.setText(entity_infos_markup)
        # progress
        new_progress_value = (
            self.textmodel.get_annotated_texts_count() * 100 / self.textmodel.rowCount()
        )
        self.progressbar.setValue(new_progress_value)

    def textedit_text_changed(self):
        self.sync_parented_annotations()
        self.update_annotation_monitor()

    def sync_parented_annotations(self):
        # get annotation at current cursor position
        annotation_at_current_cursor_pos = get_annotation_at_position(
            self.textedit.toPlainText(), self.textedit.textCursor().position()
        )
        # update annotation for same parented keyterm
        if annotation_at_current_cursor_pos is not None and annotation_at_current_cursor_pos[
            "type"
        ] in [
            "parented_key_term",
            "parented_named_entity",
        ]:
            if annotation_at_current_cursor_pos["type"] == "parented_key_term":
                text_to_replace_pattern = r"`{}``PK``.*?`´".format(
                    re.escape(annotation_at_current_cursor_pos["term"])
                )
                replace_against_text = "`{}``PK``{}`´".format(
                    annotation_at_current_cursor_pos["term"],
                    annotation_at_current_cursor_pos["parent_terms_raw"],
                )
            if annotation_at_current_cursor_pos["type"] == "parented_named_entity":
                text_to_replace_pattern = r"`{}``PN``{}``.*?`´".format(
                    re.escape(annotation_at_current_cursor_pos["term"]),
                    re.escape(annotation_at_current_cursor_pos["entity_code"]),
                )
                replace_against_text = "`{}``PN``{}``{}`´".format(
                    annotation_at_current_cursor_pos["term"],
                    annotation_at_current_cursor_pos["entity_code"],
                    annotation_at_current_cursor_pos["parent_terms_raw"],
                )
            self.replace_pattern_in_textedit(
                text_to_replace_pattern, replace_against_text
            )

    def update_annotation_monitor(self):
        self.annotation_monitor.setPlainText(
            extract_annotations_as_text(
                self.textedit.toPlainText(),
                entity_codes_to_extract=config.named_entity_codes,
            )
        )

    def go_to_index(self):
        new_index, is_not_canceled = QInputDialog.getInt(
            self, "Goto Index", "Enter an index:"
        )
        if is_not_canceled:
            self.navigator.setCurrentIndex(new_index)

    def search(self):
        searched_text, is_not_canceled = QInputDialog.getText(
            self,
            "Search a text",
            'Enter the substring or regex pattern by which you want to find another text.\n\nIf you prefix with "regex:", your input will be interpreted as a regex pattern.',
        )
        if is_not_canceled:
            new_index = self.textmodel.get_index_of_next_text_which_contains_substring(
                searched_text, self.navigator.currentIndex()
            )
            if new_index is not None:
                self.navigator.setCurrentIndex(new_index)
            else:
                QMessageBox.information(
                    self,
                    "Information",
                    "Could not find a text containing '{}'".format(searched_text),
                    QMessageBox.Ok,
                )

    def annotate_standalone_key_term(self):
        text_cursor = self.textedit.textCursor()
        if text_cursor.hasSelection():
            text_to_replace = text_cursor.selectedText()
            text_to_replace_pattern = r"(?<!`){}(?!``SK`´)".format(
                re.escape(text_to_replace)
            )
            replace_against_text = "`{}``SK`´".format(
                remove_all_annotations_from_text(text_to_replace)
            )
            self.replace_pattern_in_textedit(
                text_to_replace_pattern, replace_against_text
            )

    def annotate_parented_key_term(self):
        text_cursor = self.textedit.textCursor()
        if text_cursor.hasSelection():
            text_to_replace = text_cursor.selectedText()
            text_to_replace_pattern = r"(?<!`){}(?!``PK``.*?`´)".format(
                re.escape(text_to_replace)
            )
            orig_selection_start = text_cursor.selectionStart()
            new_selection_start = orig_selection_start + len(
                "`{}``PK``".format(remove_all_annotations_from_text(text_to_replace))
            )
            new_selection_end = new_selection_start + len(DEFAULT_PARENT_KEY_TERM)
            replace_against_text = "`{}``PK``{}`´".format(
                remove_all_annotations_from_text(text_to_replace),
                DEFAULT_PARENT_KEY_TERM,
            )
            self.replace_pattern_in_textedit(
                text_to_replace_pattern, replace_against_text
            )
            text_cursor.setPosition(new_selection_start)
            text_cursor.setPosition(new_selection_end, QTextCursor.KeepAnchor)
            self.textedit.setTextCursor(text_cursor)

    def annotate_standalone_named_entity(self):
        text_cursor = self.textedit.textCursor()
        if text_cursor.hasSelection():
            selected_text = text_cursor.selectedText()
            term = remove_all_annotations_from_text(selected_text)
            named_entity_definition = ConfigManager.get_named_entity_definition_by_key_sequence(
                self.sender().key().toString()
            )
            entity_code = named_entity_definition.code
            parent_terms_candidate = config.prediction_pipeline.get_parent_terms_for_named_entity(
                term, entity_code
            )
            if parent_terms_candidate:
                text_cursor.insertText(
                    "`{}``PN``{}``{}`´".format(
                        term, named_entity_definition.code, parent_terms_candidate
                    )
                )
            else:
                text_cursor.insertText(
                    "`{}``SN``{}`´".format(term, named_entity_definition.code)
                )

    def annotate_parented_named_entity(self):
        text_cursor = self.textedit.textCursor()
        if text_cursor.hasSelection():
            named_entity_definition = ConfigManager.get_named_entity_definition_by_key_sequence(
                self.sender().key().toString()
            )
            entity_code = named_entity_definition.code
            text_to_replace = text_cursor.selectedText()
            text_to_replace_pattern = r"(?<!`){}(?!``PN``{}``.*?`´)".format(
                re.escape(text_to_replace), re.escape(entity_code)
            )
            orig_selection_start = text_cursor.selectionStart()
            term = remove_all_annotations_from_text(text_to_replace)
            new_selection_start = orig_selection_start + len(
                "`{}``PN``{}``".format(term, entity_code)
            )
            parent_terms_candidate = config.prediction_pipeline.get_parent_terms_for_named_entity(
                term, entity_code
            )
            parent_terms = (
                parent_terms_candidate
                if parent_terms_candidate is not None
                else DEFAULT_PARENT_KEY_TERM
            )
            new_selection_end = new_selection_start + len(parent_terms)
            replace_against_text = "`{}``PN``{}``{}`´".format(
                remove_all_annotations_from_text(text_to_replace),
                named_entity_definition.code,
                parent_terms,
            )
            self.replace_pattern_in_textedit(
                text_to_replace_pattern, replace_against_text
            )
            if not parent_terms_candidate:
                text_cursor.setPosition(new_selection_start)
                text_cursor.setPosition(new_selection_end, QTextCursor.KeepAnchor)
            self.textedit.setTextCursor(text_cursor)

    def mark_annotation_for_removal(self, annotation):
        if annotation["type"] in ["standalone_key_term", "parented_key_term"]:
            config.prediction_pipeline.mark_key_term_for_removal(annotation["term"])
        if annotation["type"] in ["standalone_named_entity", "parented_named_entity"]:
            config.prediction_pipeline.mark_named_entity_term_for_removal(
                annotation["term"], annotation["entity_code"]
            )

    def remove_annotation(self):
        annotation = get_annotation_at_position(
            self.textedit.toPlainText(), self.textedit.textCursor().position()
        )
        # ensure there is an annotation to remove
        if annotation is not None:
            # get the replace pattern and replace against text
            if annotation["type"] in ["standalone_key_term", "parented_key_term"]:
                text_to_replace_pattern = r"`{}``(SK|PK).*?`´".format(
                    re.escape(annotation["term"])
                )
            if annotation["type"] in [
                "standalone_named_entity",
                "parented_named_entity",
            ]:
                text_to_replace_pattern = r"`{}``(SN|PN)``{}.*?`´".format(
                    re.escape(annotation["term"]), re.escape(annotation["entity_code"])
                )
            replace_against_text = annotation["term"]
            # do the replace
            self.replace_pattern_in_textedit(
                text_to_replace_pattern, replace_against_text
            )
            # mark annotation for removal
            self.mark_annotation_for_removal(annotation)

    def remove_all_annotations(self):
        # mark all key terms and named entities for removal
        for annotation in extract_annotations_as_generator(self.textedit.toPlainText()):
            self.mark_annotation_for_removal(annotation)

        # update text
        self.textedit.setPlainText(
            remove_all_annotations_from_text(self.textedit.toPlainText())
        )

        # clear categories if categories are
        if config.is_categories_enabled:
            self.categories_selector.set_selected_categories_by_text("")

        # reset is annotated flag
        self.reset_is_annotated_flag()

    def reset_is_annotated_flag(self):
        self.textmodel.unset_is_annotated_for_index(self.navigator.getCurrentIndex())
        self.is_annotated_label.setText("False")

    def reset_all_is_annotated_flags(self):
        if (
            QMessageBox.question(
                self,
                "Confirmation",
                "This will reset all Is Annotated flags and save the dataset to its target. You may end up in big trouble if you don't know what you are doing.\n\nAre you sure you want to the reset/save?",
                QMessageBox.Yes | QMessageBox.No,
            )
            == QMessageBox.Yes
        ):
            self.textmodel.reset_is_annotated_flags()
            self.is_annotated_label.setText("False")
            self.textmodel.save()
            self.navigator.toFirst()

    def revert_changes(self):
        self.navigator.revert()

    def submit(self):
        # submit changes of model-bound controls
        self.navigator.submit()
        # update controls
        self.update_dataset_related_controls()
        self.update_navigation_related_controls()
        # show "all messages annotated" if all texts are annotated
        if not self.textmodel.is_texts_left_for_annotation():
            QMessageBox.information(
                self,
                "Congratulations",
                "You have annotated all {} texts. There are no more texts to annotate.".format(
                    self.textmodel.rowCount()
                ),
                QMessageBox.Ok,
            )

    def submit_and_go_to_next(self):
        # submit
        self.submit()
        # identify and go to next text
        self.navigator.setCurrentIndex(
            self.textmodel.get_next_row_index(self.navigator.currentIndex())
        )

    def submit_and_go_to_next_best(self):
        # submit
        self.submit()
        # identify and go to next best text
        self.navigator.setCurrentIndex(
            self.textmodel.get_next_best_row_index(self.navigator.currentIndex())
        )

    def configure_predictors(self):
        PredictorSelectionDialog.show(self)

    def trigger_batch_trainings(self):
        config.prediction_pipeline.learn_from_annotated_dataset_async(
            config.dataset_to_edit,
            config.text_column,
            config.is_annotated_column,
            config.language_column,
            config.categories_column,
            config.categories_names_list,
            config.named_entity_codes,
        )

    def export_pipeline_model(self):
        QMessageBox.information(
            self,
            "Unfortunately...",
            "...this feature has not been implemented yet but some predictors save their own models already. Check back soon.",
            QMessageBox.Ok,
        )

    def replace_pattern_in_textedit(self, replace_pattern, replace_against_text):
        text_cursor = self.textedit.textCursor()
        text_cursor_backup = self.textedit.textCursor()
        compiled_pattern = re.compile(replace_pattern, flags=re.DOTALL)
        current_position = 0
        match = compiled_pattern.search(self.textedit.toPlainText(), current_position)
        while match is not None:
            if match.group() != replace_against_text:
                text_cursor.setPosition(match.start())
                text_cursor.setPosition(match.end(), QTextCursor.KeepAnchor)
                text_cursor.insertText(replace_against_text)
            current_position = match.end()
            match = compiled_pattern.search(
                self.textedit.toPlainText(), current_position
            )
        self.textedit.setTextCursor(text_cursor_backup)