How to do a wrapped text finder? - c++

I am working on a text finder in QtCreator(c++). I used a "QtextEdit::find" function and it finds every occurrence of the searched word until the document reaches the end, when a NEXT button is pushed. Now I want to add a "QCheckBox" that when checked restart the "finder" at the beginning of the document and continues the search.
Do you have any suggestions how to do that, considering that the "QTextEdit::find" returns a bool?
Here is the my find function:
void textFinder(const QString& textToFind, bool rev, bool wrappedSearch)
{
QTextDocument *document = this->document();
QTextCursor cursor (document);
QTextDocument::FindFlags flag;
if (rev == flag)
flag |= QTextDocument::FindBackward;
bool found = find(textToFind, flag);
if (wrappedSearch == true){
cursor.movePosition(QTextCursor::Start);
}
}
The tool bar that I did contains a QLineEdit, QPushButton, and a QCheckBox.

Thank you for your answears ! Those helped me a lot. For my code, it worked like this:
if (searchWrapped == true && !found){
if (rev == false && cursor.atStart() == true){
cursor.movePosition(QTextCursor::End);
found = find (text, flag);
}
if (rev && cursor.atEnd() == true){
cursor.movePosition(QTextCursor::Start);
found = find (text, flag);
}
}
setTextCursor(cursor);
}

If I have understood the question you want help with implementing wrapped search. Then I think it will suffice to perform another search after having moved the cursor to either start or end of document depending on whether reversed search or not:
void textFinder(const QString& textToFind, bool rev, bool wrappedSearch)
{
QTextDocument *document = this->document();
QTextCursor cursor (document);
QTextDocument::FindFlags flag;
if (rev == flag)
flag |= QTextDocument::FindBackward;
bool found = find(textToFind, flag);
if (!found&&wrappedSearch == true)
{
cursor.movePosition(rev?QTextCursor::End:QTextCursor::Start);
found = find(textToFind, flag);
}
}

Theres a few problems with your code and the logic inside it.
First of all, I don't understand why you are comparing your rev bool to the flag variable. Shouldn't you compare it against true? I'd also set the initial value of the flag variable to 0.
Second of all, if the wrapped search is set to true, you should only wrap it if QTextEdit::find returned false. Otherwise you'd be stuck at the beginning of the document.
Then you should also check if the value rev is true before setting the cursor to the beginning. If you are searching backwards, you should set the cursor to the end of the document when wrapping.
And you also have to set the text cursor back to your text edit after modifying it.
Here is my working search function, I didn't subclass QTextEdit because I was lazy, but the logic is the same.
void MainWindow::findText(const QString &text, bool rev, bool wrapped)
{
QTextDocument::FindFlags flag = 0;
if(rev)
flag = flag | QTextDocument::FindBackward;
bool found = ui->textEdit->find(text, flag);
if(!found && wrapped)
{
QTextCursor cursor = ui->textEdit->textCursor();
if(!rev)
cursor.movePosition(QTextCursor::Start);
else
cursor.movePosition(QTextCursor::End);
ui->textEdit->setTextCursor(cursor);
ui->textEdit->find(text, flag);
}
}

Related

QSyntaxHighlighter and multiline comments

I am using Qt's QSyntaxHighlighter to color some C like syntax in a QML TextEdit
Everything works great except for multiline comments.
I am detecting them this way :
void highlightBlock(QString const& text) override {
bool inMultilineComment = previousBlockState() == STATES::COMMENT;
bool inSingleLineComment = false;
int previousIndex = 0;
QRegularExpression expr("(\\/\\*|\\*\\/|\\/\\/|\n)"); // will match either /**, /**, // or \n
QRegularExpressionMatchIterator it = expr.globalMatch(text);
while(it.hasNext()) {
QRegularExpressionMatch match = it.next();
const QString captured = match.captured(1);
if(captured == "/*" && !inSingleLineComment) {
inMultilineComment = true;
previousIndex = match.capturedStart(1);
}
if(captured == "*/" && inMultilineComment) {
inMultilineComment = false;
setFormat(previousIndex, match.capturedEnd(1) - previousIndex, _commentFormat);
}
if(captured == "//" && !inMultilineComment) {
inSingleLineComment = true;
}
if(captured == "\n" && inSingleLineComment) {
inSingleLineComment = false;
}
}
if(inMultilineComment) {
setFormat(previousIndex, text.size() - previousIndex, _commentFormat);
setCurrentBlockState(STATES::COMMENT);
}
else {
setCurrentBlockState(STATES::NONE);
}
}
It works until I take a multiline comment already colored and I remove the /* at the begining. Only the block that contains the /* is processed and recolored, but not the following ones, which means that they continue to appear commented when they are not.
Is there an easy way to tell QSyntaxHighlighter to re-process the following blocks to prevent such mis-colorations ?
I ran into this same problem recently and discovered that Qt actually should be handling this for you, assuming that you set your blockState correctly.
If you look at the sourceCode for QSyntaxHighlighterPrivate::reformatBlocks in the Qt5 source code, you'll see
while (block.isValid() && (block.position() < endPosition || forceHighlightOfNextBlock)) {
const int stateBeforeHighlight = block.userState();
reformatBlock(block);
forceHighlightOfNextBlock = (block.userState() != stateBeforeHighlight);
block = block.next();
}
retrieved from https://code.woboq.org/qt5/qtbase/src/gui/text/qsyntaxhighlighter.cpp.html#165
That code (which is fired by a contentsChange signal from the QTextDocument your highlighter is on) will iterate through each block (line) starting from the block that was just modified. Assuming that the state of the block changed based on the typing change that just happened, it will continue to process the following blocks. This means that you need to get your userState correct for every line and Qt should handle the rest.
Given the example
/*
* This is a comment
*
* That I made
*/
You would want to start in the condition where every line had the STATES::COMMENT set except for the last line which should be set to STATES::NONE. Once you do something like deleting the initial /* you need to make sure that the block state is reset to STATES::NONE. That will trigger Qt to rerun the next block, which will also need to change its state, etc.
In my (python) code, I ended up using a combination of print statements and real debugging to track the propagation of state changes and figured out where it was not correctly updating and breaking the chain of updates. Your code looks superficially correct, though I did not try to compile and run it, but I suspect there is a some case being triggered where the state is not being updated correctly after an edit.

QPlainTextEdit - searches the document to the end and again from the beginning

I want to search in QPlainTextEdit for a string from the current cursor to the end. If nothing is found I want to continue searching from the start. Only in this stage, if nothing is found, a message will appear.
This is the code:
void BasicEdit::findString(QString s, bool reverse, bool casesens, bool words) {
QTextDocument::FindFlags flag;
if (reverse) flag |= QTextDocument::FindBackward;
if (casesens) flag |= QTextDocument::FindCaseSensitively;
if (words) flag |= QTextDocument::FindWholeWords;
QTextCursor cursor = this->textCursor();
if (!find(s, flag)) {
//nothing is found | jump to start/end
cursor.movePosition(reverse?QTextCursor::End:QTextCursor::Start);
setTextCursor(cursor); //!!!!!!
if (!find(s, flag)) {
//no match in whole document
QMessageBox msgBox;
msgBox.setText(tr("String not found."));
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setDefaultButton(QMessageBox::Ok);
msgBox.exec();
}
}
}
The problem is line setTextCursor(cursor);
without this line, the search does not continue from the beginning/end
with this line everything is fine except that when string is not found the cursor is positioned at the beginning / end of the document, and the current position in document for user is lost.
How to search for a string in a document and do not change current position if none is found?
Update
Thanks to IAmInPLS the code looks like below.
I added value preservation for verticalScrollBar.
Even so, there is a short flickering when nothing is found generated by: cursor.movePosition(reverse?QTextCursor::End:QTextCursor::Start);
How can we get rid of it?
How can look like a professional editor?
It is an idea to create another invisible QPlainTextEdit element to search in it?
void BasicEdit::findString(QString s, bool reverse, bool casesens, bool words)
{
QTextDocument::FindFlags flag;
if (reverse) flag |= QTextDocument::FindBackward;
if (casesens) flag |= QTextDocument::FindCaseSensitively;
if (words) flag |= QTextDocument::FindWholeWords;
QTextCursor cursor = this->textCursor();
// here we save the cursor position and the verticalScrollBar value
QTextCursor cursorSaved = cursor;
int scroll = verticalScrollBar()->value();
if (!find(s, flag))
{
//nothing is found | jump to start/end
cursor.movePosition(reverse?QTextCursor::End:QTextCursor::Start);
setTextCursor(cursor);
if (!find(s, flag))
{
// word not found : we set the cursor back to its initial position and restore verticalScrollBar value
setTextCursor(cursorSaved);
verticalScrollBar()->setValue(scroll);
QMessageBox msgBox(this);
msgBox.setText(tr("String not found."));
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setDefaultButton(QMessageBox::Ok);
msgBox.exec();
}
}
}
The idea is to keep the cursor position you have before starting to search for the word. Then, after the research, you will set the cursor back to the position you saved.
void BasicEdit::findString(QString s, bool reverse, bool casesens, bool words)
{
QTextDocument::FindFlags flag;
if (reverse) flag |= QTextDocument::FindBackward;
if (casesens) flag |= QTextDocument::FindCaseSensitively;
if (words) flag |= QTextDocument::FindWholeWords;
QTextCursor cursor = this->textCursor();
// here , you save the cursor position
QTextCursor cursorSaved = cursor;
if (!find(s, flag))
{
//nothing is found | jump to start/end
cursor.movePosition(reverse?QTextCursor::End:QTextCursor::Start);
/* following line :
- the cursor is set at the beginning/end of the document (if search is reverse or not)
- in the next "find", if the word is found, now you will change the cursor position
*/
setTextCursor(cursor);
if (!find(s, flag))
{
//no match in whole document
QMessageBox msgBox;
msgBox.setText(tr("String not found."));
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setDefaultButton(QMessageBox::Ok);
msgBox.exec();
// word not found : we set the cursor back to its initial position
setTextCursor(cursorSaved);
}
}
}

How to disable middle button functionality of QTextEdit?

I don't want mouse middle button to paste text in my QTextEdit. This code doesn't work. TextEdit inherits QTextEdit. After mouse middle button pastes it pastes copied text.
void TextEdit::mousePressEvent ( QMouseEvent * e ) {
if (e->button() == Qt::MidButton) {
e->accept();
return;
};
QTextEdit::mousePressEvent(e);
}
As mouse clicks are usually registered when the button is released, you should redefine the mouseReleaseEvent function.
You don't even need to redefine mousePressEvent, because the middle button isn't handled at all by that function.
I'm assuming you're using Linux here; right clicking in the window is likely to be triggering an insertion of mime data before you get to handle the mouse event, which is why it is still pasting text.
Therefore, according to Qt docs for paste: - " to modify what QTextEdit can paste and how it is being pasted, reimplement the virtual canInsertFromMimeData() and insertFromMimeData() functions."
I've been in the same case, that is to say: having parts of my CustomQTextEdit required to be non-editable.
As I truly love the middle mouse button paste feature, I did not wanted to disable it. So, here is the (more or less quick and dirty coded) workaround I used:
void QTextEditHighlighter::mouseReleaseEvent(QMouseEvent *e)
{
QString prev_text;
if (e->button() == Qt::MidButton) {
// Backup the text as it is before middle button click
prev_text = this->toPlainText();
// And let the paste operation occure...
// e->accept();
// return;
}
// !!!!
QTextEdit::mouseReleaseEvent(e);
// !!!!
if (e->button() == Qt::MidButton) {
/*
* Keep track of the editbale ranges (up to you).
* My way is a single one range inbetween the unique
* tags "//# BEGIN_EDIT" and "//# END_EDIT"...
*/
QRegExp begin_regexp = QRegExp("(^|\n)(\\s)*//# BEGIN_EDIT[^\n]*(?=\n|$)");
QRegExp end_regexp = QRegExp("(^|\n)(\\s)*//# END_EDIT[^\n]*(?=\n|$)");
QTextCursor from = QTextCursor(this->document());
from.movePosition(QTextCursor::Start);
QTextCursor cursor_begin = this->document()->find(begin_regexp, from);
QTextCursor cursor_end = this->document()->find(end_regexp, from);
cursor_begin.movePosition(QTextCursor::EndOfBlock);
cursor_end.movePosition(QTextCursor::StartOfBlock);
int begin_pos = cursor_begin.position();
int end_pos = cursor_end.position();
if (!(cursor_begin.isNull() || cursor_end.isNull())) {
// Deduce the insertion index by finding the position
// of the first character that changed between previous
// text and the current "after-paste" text
int insert_pos; //, end_insert_pos;
std::string s_cur = this->toPlainText().toStdString();
std::string s_prev = prev_text.toStdString();
int i_max = std::min(s_cur.length(), s_prev.length());
for (insert_pos=0; insert_pos < i_max; insert_pos++) {
if (s_cur[insert_pos] != s_prev[insert_pos])
break;
}
// If the insertion point is not in my editable area: just restore the
// text as it was before the paste occured
if (insert_pos < begin_pos+1 || insert_pos > end_pos) {
// Restore text (ghostly)
((MainWindow *)this->topLevelWidget())->disconnect(this, SIGNAL(textChanged()), ((MainWindow *)this->topLevelWidget()), SLOT(on_textEdit_CustomMacro_textChanged()));
this->setText(prev_text);
((MainWindow *)this->topLevelWidget())->connect(this, SIGNAL(textChanged()), ((MainWindow *)this->topLevelWidget()), SLOT(on_textEdit_CustomMacro_textChanged()));
}
}
}
}

QTextEdit. How to select text manually?

There are functions like textEdit->textCursor()->selectionStart() and textEdit->textCursor()->selectionEnd(), but there are no functions setSelectionStart, setSelectionEnd.
Is there any way to select some part of text manually?
QTextCursor c = textEdit->textCursor();
c.setPosition(startPos);
c.setPosition(endPos, QTextCursor::KeepAnchor);
textEdit->setTextCursor(c);
This piece of code moves the cursor to the start position of the selection using setPosition, then moves it to the end of the selection, but leaves the selection anchor at the old position by specifying a MoveMode as the second parameter.
The last line sets the selection to be visible inside the edit control, so you should skip it if you just want to do some manipulations with the selected text.
Also, if you don't have the exact positions, movePosition is helpful: you can move the cursor in various ways, such as one word to the right or down one line.
I encountered a similar problem.
In Windows 10, there might be a bug of 'drag/move'. We use QT_NO_DRAGANDDROP as a compiler option, which makes text selection in QTextEdit not work anymore.
Solution:
void QTextEditEx::mouseMoveEvent(QMouseEvent *event)
{
QTextEdit::mouseMoveEvent(event);
if (event->buttons() & Qt::LeftButton)
{
QTextCursor cursor = textCursor();
QTextCursor endCursor = cursorForPosition(event->pos()); // key point
cursor.setPosition(pos, QTextCursor::MoveAnchor);
cursor.setPosition(endCursor.position(), QTextCursor::KeepAnchor);
setTextCursor(cursor);
}
}
void QTextEditEx::mousePressEvent(QMouseEvent *event)
{
QTextEdit::mousePressEvent(event);
if (event->buttons() & Qt::LeftButton)
{
QTextCursor cursor = cursorForPosition(event->pos());
// int pos; member variable
pos = cursor.position();
cursor.clearSelection();
setTextCursor(cursor);
}
}
reference:
Two existing answers
QTextEdit: get word under the mouse pointer?
Try to use:
QTextCursor cur = tw->textCursor();
cur.clearSelection();
tw->setTextCursor(cur);

Why does this code not highlight the search term once found?

The code below does not highlight the search term when it is found. In fact the cursor disappears from the QPlainTextEdit (called ui->Editor) after pressing the 'next' button. What's causing it?
void TextEditor::findNextInstanceOfSearchTerm()
{
QString searchTerm = this->edtFind->text();
if(this->TextDocument == NULL)
{
this->TextDocument = ui->Editor->document();
}
QTextCursor documentCursor(this->TextDocument);
documentCursor = this->TextDocument->find(searchTerm,documentCursor);
if(!documentCursor.isNull())
{
documentCursor.select(QTextCursor::WordUnderCursor);
}else
{
ui->statusbar->showMessage("\""+searchTerm+"\" could not be found",MESSAGE_DURATION);
}
}
Firstly, your code creates a new cursor at the beginning of the document each time you press the next button, so you will always search from the beginning. Secondly, you must understand that the cursor you manipulate has nothing to do with the one in your QPlainTextEdit: you manipulate a copy. If you want to impact the text edit, you must modify its cursor using setTextCursor. Here is a working solution:
void TextEditor::findNextInstanceOfSearchTerm()
{
QString searchTerm = this->edtFind->text();
if(this->TextDocument == NULL)
{
this->TextDocument = ui->Editor->document();
}
// get the current cursor
QTextCursor documentCursor = ui->Editor->textCursor();
documentCursor = this->TextDocument->find(searchTerm,documentCursor);
if(!documentCursor.isNull())
{
// needed only if you want the entire word to be selected
documentCursor.select(QTextCursor::WordUnderCursor);
// modify the text edit cursor
ui->Editor->setTextCursor(documentCursor);
}
else
{
ui->statusbar->showMessage(
"\""+searchTerm+"\" could not be found",MESSAGE_DURATION);
}
}
As a side note, you might want to know that QPlainTextEdit provides a find method, so this might be an easier way to achieve what you want:
void TextEditor::findNextInstanceOfSearchTerm()
{
QString searchTerm = this->edtFind->text();
bool found = ui->Editor->find(searchTerm);
if (found)
{
QTextCursor cursor = ui->Editor->textCursor();
cursor.select(QTextCursor::WordUnderCursor);
ui->Editor->setTextCursor(cursor);
}
else
{
// set message in status bar
}
}
Use QTextCursor::EndOfWord
Use QPlainTextEdit::setExtraSelections to select/highlight something in QPlainTextEdit
Simply you already have cursor that would highlight word, but you didn't apply it to text edit