c++qtqplaintexteditqtextcursor

Parent function terminates whenever I try to call QTextCharFormat on QTextCursor selection


I recently ran into a weird issue where my QPlainTextEdit::selectionChanged handler function terminates prematurely whenever QTextCursor::mergeCharFormat/setCharFormat/setBlockCharFormat is called. Additionally, after terminating it gets called again and runs into the same issue, leading to an infinite loop.

I'm trying to replicate a feature present in many text editors (such as Notepad++) where upon selecting a word, all similar words in the entire document are highlighted. My TextEditor class is overloaded from QPlainTextEdit.

The minimal reproducible example is as follows:

main.cpp:

#include "mainWindow.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    mainWindow w;
    w.show();
    return a.exec();
}

MainWindow.h:

#pragma once
#include <QtWidgets/QMainWindow>
#include "ui_mainWindow.h"
#include "TextEditor.h"

class mainWindow : public QMainWindow
{
    Q_OBJECT

public:
    mainWindow(QWidget *parent = Q_NULLPTR) : QMainWindow(parent)
    {
        ui.setupUi(this);

        auto textEdit = new TextEditor(this);
        textEdit->setPlainText("test lorem ipsum test\n dolor sit test");
        ui.tabWidget->addTab(textEdit, "Editor"); //Or any other way of adding the widget to the window
    }

private:
    Ui::mainWindowClass ui;
};

TextEditor.h:

The regex highlighter part is based on this SO answer.

#pragma once
#include <QPlainTextEdit>

class TextEditor : public QPlainTextEdit
{
    Q_OBJECT

public:
    TextEditor(QWidget* parent) : QPlainTextEdit(parent)
    {
        connect(this, &QPlainTextEdit::selectionChanged, this, &TextEditor::selectChangeHandler);
    }

private:
    void selectChangeHandler()
    {
        //Ignore empty selections
        if (textCursor().selectionStart() >= textCursor().selectionEnd())
            return;

        //We only care about fully selected words (nonalphanumerical characters on either side of selection)
        auto plaintext = toPlainText();
        auto prevChar = plaintext.mid(textCursor().selectionStart() - 1, 1).toStdString()[0];
        auto nextChar = plaintext.mid(textCursor().selectionEnd(), 1).toStdString()[0];
        if (isalnum(prevChar) || isalnum(nextChar))
            return;

        auto qselection = textCursor().selectedText();
        auto selection = qselection.toStdString();

        //We also only care about selections that do not themselves contain nonalphanumerical characters
        if (std::find_if(selection.begin(), selection.end(), [](char c) { return !isalnum(c); }) != selection.end())
            return;

        //Prepare text format
        QTextCharFormat format;
        format.setBackground(Qt::green);

        //Find all words in our document that match the selected word and apply the background format to them
        size_t pos = 0;
        auto reg = QRegExp(qselection);
        auto cur = textCursor();
        auto index = reg.indexIn(plaintext, pos);
        while (index >= 0)
        {
            //Select matched text and apply format
            cur.setPosition(index);
            cur.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor, 1);
            cur.mergeCharFormat(format); //This causes the selectChangeHandler function to terminate and then execute again, causing an infinite loop leading to a stack overflow

            //Move to next match
            pos = index + (size_t)reg.matchedLength();
            index = reg.indexIn(plaintext, pos);
        }
    }
};

I suspect the format fails to apply for some reason, possibly causing an exception that gets caught inside Qt and terminates the parent function. I tried adding my own try-catch handler around the problematic area, but it did nothing (as expected).

I'm not sure whether this is my fault or a bug inside Qt. Does anybody know what I'm doing wrong or how to work around this issue?


Solution

  • An infinite loop is being generated because it seems that getting the text changes also changes the selection. One possible solution is to block the signals using QSignalBlocker:

    void selectChangeHandler()
    {
        const QSignalBlocker blocker(this); // <--- this line
        //Ignore empty selections
        if (textCursor().selectionStart() >= textCursor().selectionEnd())
            return;
        // ...