Qt highlighting selected line overwrites highlighting of individual words - c++

I have a QPlainTextEdit where I want to highlight the current line the user is on as well as all words similar to the word the user selected. This word highlight works fine on all lines except the currently selected one, because the "selected line" background style overrides the "selected word" style applied to the selected words.
My question is how can I make sure the word highlighing is done after the line highlight is done, so that they can both be active at the same time?
Screenshot to illustrate:
The yellow line is the current line being highlighted. The first 'test' is selected, so all others should have the lightblue background applied. All except the 'test' on the highlighted line do.
Minimal reproducible example:
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 test dolor sit test\test amet test");
ui.tabWidget->addTab(textEdit, "Editor");
}
private:
Ui::mainWindowClass ui;
};
TextEditor.h
#pragma once
#include <QPlainTextEdit>
class TextEditor : public QPlainTextEdit
{
Q_OBJECT
public:
TextEditor(QWidget* parent) : QPlainTextEdit(parent)
{
connect(this, &QPlainTextEdit::selectionChanged, this, &TextEditor::selectChangeHandler);
connect(this, &QPlainTextEdit::cursorPositionChanged, this, &TextEditor::highlightCurrentLine);
}
private:
std::vector<std::pair<int, QTextCharFormat>> highlightedWords_;
//Highlights the current line
void highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> extraSelections;
if (!isReadOnly())
{
QTextEdit::ExtraSelection selection;
selection.format.setBackground(Qt::yellow);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
}
setExtraSelections(extraSelections);
}
//Highlights all words similar to the currently selected word
void selectChangeHandler()
{
//Unset previous selection
resetHighlightedWords();
//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;
//Highlight all matches of the given word in the editor
blockSignals(true);
highlightWord(qselection);
blockSignals(false);
}
//Removes highlight from selected words
void resetHighlightedWords()
{
if (highlightedWords_.empty())
return;
blockSignals(true);
auto cur = textCursor();
for (const auto& [index, oldFormat] : highlightedWords_)
{
cur.setPosition(index);
cur.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor, 1);
cur.setCharFormat(oldFormat);
}
blockSignals(false);
highlightedWords_.clear();
}
//Applies the highlight style to all appearances of the given word
void highlightWord(const QString& word)
{
auto plaintext = toPlainText();
//Prepare text format
QTextCharFormat format;
format.setBackground(QColor::fromRgb(0x70, 0xED, 0xE0));
//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("\\b" + word + "\\b");
auto cur = textCursor();
auto index = reg.indexIn(plaintext, pos);
while (index >= 0)
{
//Select matched text
cur.setPosition(index);
//Save old text style
highlightedWords_.push_back(std::make_pair(index, cur.charFormat()));
//Apply format
cur.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor, 1);
cur.mergeCharFormat(format);
//Move to next match
auto len = (size_t)reg.matchedLength();
pos = index + (size_t)reg.matchedLength();
index = reg.indexIn(plaintext, pos);
}
}
};

Avoiding inheritance as a first resort was the right thing to do IMO, but in this particular case, it may be the simplest approach.
#include <QPainter>
TextEditor::TextEditor( QWidget* parent ) : QPlainTextEdit( parent )
{
connect( this, SIGNAL( cursorPositionChanged() ), viewport(), SLOT( update() ) );
//Just for brevity. Instead of repainting the whole thing on every cursor change,
//you'll want to filter for changes to the current block/line and only update the.
//changed portions. And accommodate resize, etc.
}
void TextEditor::paintEvent( QPaintEvent* pEvent )
{
QPainter painter( viewport() );
QRect r = cursorRect();
r.setLeft( 0 ); r.setRight( width() - 1 ); //Or more!
painter.setPen( Qt::NoPen );
painter.setBrush( QColor( 228, 242, 244, 200 ) );
painter.drawRect( r );
QPlainTextEdit::paintEvent( pEvent );
}
A background hint behind the cursor's block is a really nice UX improvement, as it makes the cursor position more immediately apparent at a glance in all sorts of scenarios. If you have several text editors up together, little details like that become even more important.
At first glance, setExtraSelections() looks like a fast and simple way to get there. And it is... But I find that it falls short when you want to take it to the next level, and, as you discovered, it doesn't play well with ANY other highlighting.
I suspect the built-in ExtraSelection method is meant to be a kind of quick and dirty brute force tool for quickly showing errors or breakpoints, i.e. things that are meant to really visually stand out for the user. It's basically like a secondary selection highlight behind the cursor selection, and so like all other selection highlights it will render behind text but in front of everything else. That means it will also eclipse any custom text background formatting you do using QTextFormat or QTextCharFormat, or even QSyntaxHighlighter. I don't find that acceptable, personally.
Another smaller problem with built-in selections or highlighting in general for this kind of background hint use case is that they don't cover the entire background of the text block. They stop a few pixels shy at the text area boundary or worse depending on margins, etc., making it look clunky to my eyes.
In terms of UI design, a current line indication generally needs to be subtler than most other indications and all other highlights, with lower contrast, and towards the very far background. It needs to look more like a part of the widget than part of the text. It's a hint not a selection, and I've found that balancing it all visually required more than ExtraSelections or regular text formatting was able to provide.
BTW, If you plan on making this more complex, e.g. a code editor, I would also recommend that you look into using QSyntaxHighlighter for your selected word pattern highlight. ( It will remove a LOT of the cursor control logic and ALL of the signal interruption. It will also scale better for when (if?) you add keyword, variable, comment, search terms, etc. Right now, your highlighting involves editing your document/data model directly, which is fine for your post or for simple text input, but likely to be problematic for other cases. )
EDIT
Here's more code that shows using an extended highlighter together with the paintEvent override. I'm going to use headers to hopefully make it clearer how this approach might integrate with your actual projects' classes.
First, the highlighter:
#include <QSyntaxHighlighter>
class QTextDocument;
class CQSyntaxHighlighterSelectionMatch : public QSyntaxHighlighter
{
Q_OBJECT
public:
explicit CQSyntaxHighlighterSelectionMatch( QTextDocument *parent = 0 );
public slots:
void SetSelectionTerm( QString term );
protected:
virtual void highlightBlock( const QString &text );
void ApplySelectionTermHighlight( const QString &text );
private:
QString m_strSelectionTerm;
struct HighlightingRule {
QRegExp pattern;
QTextCharFormat format;
};
HighlightingRule m_HighlightRuleSelectionTerm;
};
A quick and dirty implementation:
CQSyntaxHighlighterSelectionMatch::CQSyntaxHighlighterSelectionMatch( QTextDocument *parent )
: QSyntaxHighlighter( parent )
{
m_strSelectionTerm.clear();
m_HighlightRuleSelectionTerm.format.setBackground( QColor(255, 210, 120 ) );
//m_HighlightRuleSelectionTerm.format.setFontWeight( QFont::Bold ); //or italic, etc...
}
void CQSyntaxHighlighterSelectionMatch::SetSelectionTerm( QString txtIn )
{
if( txtIn == m_strSelectionTerm )
return;
if( !txtIn.isEmpty() )
{
txtIn = "\\b" + txtIn + "\\b";
if( txtIn == m_strSelectionTerm )
return;
}
m_strSelectionTerm = txtIn;
Qt::CaseSensitivity cs = Qt::CaseSensitive;
m_HighlightRuleSelectionTerm.pattern = QRegExp( m_strSelectionTerm, cs );
rehighlight();
}
void CQSyntaxHighlighterSelectionMatch::highlightBlock( const QString &text )
{
if( m_strSelectionTerm.length() > 1 )
ApplySelectionTermHighlight( text );
}
void CQSyntaxHighlighterSelectionMatch::ApplySelectionTermHighlight( const QString &text )
{
QRegExp expression( m_HighlightRuleSelectionTerm.pattern );
int index, length;
index = expression.indexIn( text );
while ( index >= 0 )
{
length = expression.matchedLength();
setFormat( index, length, m_HighlightRuleSelectionTerm.format );
index = expression.indexIn( text, index + length );
}
}
And here's how a QPlainTextEdit derived class might use something like the above:
#include <QPlainTextEdit>
class TextEditor : public QPlainTextEdit
{
Q_OBJECT
public:
TextEditor( QWidget *parent = 0 );
protected:
virtual void paintEvent( QPaintEvent *event );
private slots:
void CheckForCurrentBlockChange();
void FilterSelectionForSingleWholeWord();
private:
unsigned int m_uiCurrentBlock;
CQSyntaxHighlighterSelectionMatch *m_pHighlighter;
};
#include <QPainter>
TextEditor::TextEditor(QWidget *parent)
: QPlainTextEdit(parent)
{
//Instead of repainting on every cursor change, we can filter for changes to the current block/line
//connect( this, SIGNAL(cursorPositionChanged()), viewport(), SLOT(update()) );
connect( this, SIGNAL(cursorPositionChanged()), this, SLOT(CheckForCurrentBlockChange()) );
m_pHighlighter = new CQSyntaxHighlighterSelectionMatch( document() );
connect( this, SIGNAL(selectionChanged()), this, SLOT(FilterSelectionForSingleWholeWord()) );
}
void TextEditor::paintEvent( QPaintEvent* pEvent )
{
QPainter painter( viewport() );
QRect r = cursorRect();
r.setLeft( 0 );
r.setRight( width()-1 );
painter.setPen( Qt::NoPen );
painter.setBrush( QColor( 228, 242, 244 ) );
painter.drawRect( r );
QPlainTextEdit::paintEvent( pEvent );
}
void TextEditor::CheckForCurrentBlockChange()
{
QTextCursor tc = textCursor();
unsigned int b = (unsigned int)tc.blockNumber();
if( b == m_uiCurrentBlock )
return;
m_uiCurrentBlock = b;
viewport()->update(); //I'll just brute force paint everything for this example. Your real code can be smarter with it's repainting it matters...
}
void TextEditor::FilterSelectionForSingleWholeWord()
{
QTextCursor tc = textCursor();
QString currentSelection = tc.selectedText();
QStringList list = currentSelection.split(QRegExp("\\s+"), QString::SkipEmptyParts);
if( list.count() > 1 )
{
m_pHighlighter->SetSelectionTerm( "" );
return;
}
tc.movePosition( QTextCursor::StartOfWord );
tc.movePosition( QTextCursor::EndOfWord, QTextCursor::KeepAnchor );
QString word = tc.selectedText();
if( currentSelection != word )
{
m_pHighlighter->SetSelectionTerm( "" );
return;
}
m_pHighlighter->SetSelectionTerm( currentSelection );
}
This is the simplest way I know to provide the selection term functionality you are after while solving the issue of the background hint interfering with the selection term highlight when they are on the same block.

Related

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?
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;
// ...

How to display graphics objects behind or foreground of text inside QTextEdit in Qt?

I would like to display a rectangle behind a word I selected like Qt Creator does here:
I am experimenting with the example of QSyntaxHighlighter. I am able to change styles based on keyword patterns. I would like to have graphics or widgets for custom autocompletion lists.
For autocompletion follow the Custom Completer Example or the Completer Example.
The code below follows the first one, which I blatantly, unashamedly copied and integrated into the BackgroundHighlighter class and main.cpp.
This answer will contain five files within a project along with a Qt Resource File.
highlighter.h (Highlighter Class for Syntax)
highlighter.cpp
backgroundHighlighter.h (BackgroundHighlighter Class)
backgroundHighlighter.cpp
main.cpp
res.qrc (optional, not needed, you can hardcode your text)
res (directory) (optional)
|- symbols.txt (optional, you can set your own default text)
|- wordlist.txt (optional, copied from example but you could use your own line-delimited word list and set this in main.cpp with a QStringListModel)
Note that the implementation of the Highlighter class for (1) and (2) can be found in the Qt Syntax Highlighter Example. I will leave its implementation as an exercise for the reader.
In calling the BackgroundHighlighter class, one can pass it a file name to load text from a file. (This wasn't in the OP's specification, but was convenient to implement due to the large amount of text I wanted to test.)
Also note that I integrated the Custom Completer Example into the class.
Here's backgroundHighlighter.h (3) (~45 lines, ~60 lines with completer):
#ifndef BACKGROUNDHIGHLIGHTER_H
#define BACKGROUNDHIGHLIGHTER_H
#include <QtWidgets>
#include <QtGui>
// this is the file to your highlighter
#include "myhighlighter.h"
class BackgroundHighlighter : public QTextEdit
{
Q_OBJECT
public:
BackgroundHighlighter(const QString &fileName = QString(), QWidget *parent = nullptr);
void loadFile(const QString &fileName);
void setCompleter(QCompleter *completer);
QCompleter *completer() const;
protected:
void keyPressEvent(QKeyEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
public slots:
void onCursorPositionChanged();
private slots:
void insertCompletion(const QString &completion);
private:
// this is your syntax highlighter
Highlighter *syntaxHighlighter;
// stores the symbol being highlighted
QString highlightSymbol;
// stores the position (front of selection) where the cursor was originally placed
int mainHighlightPosition;
// stores character formats to be used
QTextCharFormat mainFmt; // refers to format block directly under the cursor
QTextCharFormat subsidiaryFmt; // refers to the formatting blocks on matching words
QTextCharFormat defaultFmt; // refers to the default format of the **entire** document which will be used in resetting the format
void setWordFormat(const int &position, const QTextCharFormat &format);
void runHighlight();
void clearHighlights();
void highlightMatchingSymbols(const QString &symbol);
// completer, copied from example
QString textUnderCursor() const;
QCompleter *c;
};
#endif // BACKGROUNDHIGHLIGHTER_H
And here's backgroundHighlighter.cpp (4) (~160 lines, ~250 lines with completer):
#include "backgroundhighlighter.h"
#include <QDebug>
// constructor
BackgroundHighlighter::BackgroundHighlighter(const QString &fileName, QWidget *parent) :
QTextEdit(parent)
{
// I like Monaco
setFont(QFont("Monaco"));
setMinimumSize(QSize(500, 200));
// load initial text from a file OR from a hardcoded default
if (!fileName.isEmpty())
loadFile(fileName);
else
{
QString defaultText = "This is a default text implemented by "
"a stackoverflow user. Please upvote the answer "
"at https://stackoverflow.com/a/53351512/10239789.";
setPlainText(defaultText);
}
// set the highlighter here
QTextDocument *doc = document();
syntaxHighlighter = new Highlighter(doc);
// TODO change brush/colours to match theme
mainFmt.setBackground(Qt::yellow);
subsidiaryFmt.setBackground(Qt::lightGray);
defaultFmt.setBackground(Qt::white);
// connect the signal to our handler
connect(this, &QTextEdit::cursorPositionChanged, this, &BackgroundHighlighter::onCursorPositionChanged);
}
// convenience function for reading a file
void BackgroundHighlighter::loadFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly))
return;
// the file could be in Plain Text OR Html
setText(file.readAll());
}
void BackgroundHighlighter::setCompleter(QCompleter *completer)
{
if (c)
QObject::disconnect(c, 0, this, 0);
c = completer;
if (!c)
return;
c->setWidget(this);
c->setCompletionMode(QCompleter::PopupCompletion);
c->setCaseSensitivity(Qt::CaseInsensitive);
QObject::connect(c, SIGNAL(activated(QString)),
this, SLOT(insertCompletion(QString)));
}
QCompleter *BackgroundHighlighter::completer() const
{
return c;
}
void BackgroundHighlighter::keyPressEvent(QKeyEvent *e)
{
if (c && c->popup()->isVisible()) {
// The following keys are forwarded by the completer to the widget
switch (e->key()) {
case Qt::Key_Enter:
case Qt::Key_Return:
case Qt::Key_Escape:
case Qt::Key_Tab:
case Qt::Key_Backtab:
e->ignore();
return; // let the completer do default behavior
default:
break;
}
}
bool isShortcut = ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_E); // CTRL+E
if (!c || !isShortcut) // do not process the shortcut when we have a completer
QTextEdit::keyPressEvent(e);
const bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier);
if (!c || (ctrlOrShift && e->text().isEmpty()))
return;
static QString eow("~!##$%^&*()_+{}|:\"<>?,./;'[]\\-="); // end of word
bool hasModifier = (e->modifiers() != Qt::NoModifier) && !ctrlOrShift;
QString completionPrefix = textUnderCursor();
if (!isShortcut && (hasModifier || e->text().isEmpty()|| completionPrefix.length() < 3
|| eow.contains(e->text().right(1)))) {
c->popup()->hide();
return;
}
if (completionPrefix != c->completionPrefix()) {
c->setCompletionPrefix(completionPrefix);
c->popup()->setCurrentIndex(c->completionModel()->index(0, 0));
}
QRect cr = cursorRect();
cr.setWidth(c->popup()->sizeHintForColumn(0)
+ c->popup()->verticalScrollBar()->sizeHint().width());
c->complete(cr); // pop it up!
}
void BackgroundHighlighter::focusInEvent(QFocusEvent *e)
{
if (c)
c->setWidget(this);
QTextEdit::focusInEvent(e);
}
// convenience function for setting a `charFmt` at a `position`
void BackgroundHighlighter::setWordFormat(const int &position, const QTextCharFormat &charFmt)
{
QTextCursor cursor = textCursor();
cursor.setPosition(position);
cursor.select(QTextCursor::WordUnderCursor);
cursor.setCharFormat(charFmt);
}
// this will handle the `QTextEdit::cursorPositionChanged()` signal
void BackgroundHighlighter::onCursorPositionChanged()
{
// if cursor landed on different format, the `currentCharFormat` will be changed
// we need to change it back to white
setCurrentCharFormat(defaultFmt);
// this is the function you're looking for
runHighlight();
}
void BackgroundHighlighter::insertCompletion(const QString &completion)
{
if (c->widget() != this)
return;
QTextCursor tc = textCursor();
int extra = completion.length() - c->completionPrefix().length();
tc.movePosition(QTextCursor::Left);
tc.movePosition(QTextCursor::EndOfWord);
tc.insertText(completion.right(extra));
setTextCursor(tc);
}
QString BackgroundHighlighter::textUnderCursor() const
{
QTextCursor tc = textCursor();
tc.select(QTextCursor::WordUnderCursor);
return tc.selectedText();
}
/**
* BRIEF
* Check if new highlighting is needed
* Clear previous highlights
* Check if the word under the cursor is a symbol (i.e. matches ^[A-Za-z0-9_]+$)
* Highlight all relevant symbols
*/
void BackgroundHighlighter::runHighlight()
{
// retrieve cursor
QTextCursor cursor = textCursor();
// retrieve word under cursor
cursor.select(QTextCursor::WordUnderCursor);
QString wordUnder = cursor.selectedText();
qDebug() << "Word Under Cursor:" << wordUnder;
// get front of cursor, used later for storing in `highlightPositions` or `mainHighlightPosition`
int cursorFront = cursor.selectionStart();
// if the word under cursor is the same, then save time
// by skipping the process
if (wordUnder == highlightSymbol)
{
// switch formats
setWordFormat(mainHighlightPosition, subsidiaryFmt); // change previous main to subsidiary
setWordFormat(cursorFront, mainFmt); // change position under cursor to main
// update main position
mainHighlightPosition = cursorFront;
// jump the gun
return;
}
// clear previous highlights
if (mainHighlightPosition != -1)
clearHighlights();
// check if selected word is a symbol
if (!wordUnder.contains(QRegularExpression("^[A-Za-z0-9_]+$")))
{
qDebug() << wordUnder << "is not a symbol!";
return;
}
// set the highlight symbol
highlightSymbol = wordUnder;
// store the cursor position to check later
mainHighlightPosition = cursorFront;
// highlight all relevant symbols
highlightMatchingSymbols(wordUnder);
qDebug() << "Highlight done\n\n";
}
// clear previously highlights
void BackgroundHighlighter::clearHighlights()
{
QTextCursor cursor = textCursor();
// wipe the ENTIRE document with the default background, this should be REALLY fast
// WARNING: this may have unintended consequences if you have other backgrounds you want to keep
cursor.select(QTextCursor::Document);
cursor.setCharFormat(defaultFmt);
// reset variables
mainHighlightPosition = -1;
highlightSymbol.clear();
}
// highlight all matching symbols
void BackgroundHighlighter::highlightMatchingSymbols(const QString &symbol)
{
// highlight background of congruent symbols
QString docText = toPlainText();
// use a regex with \\b to look for standalone symbols
QRegularExpression regexp("\\b" + symbol + "\\b");
// loop through all matches in the text
int matchPosition = docText.indexOf(regexp);
while (matchPosition != -1)
{
// if the position
setWordFormat(matchPosition, matchPosition == mainHighlightPosition ? mainFmt : subsidiaryFmt);
// find next match
matchPosition = docText.indexOf(regexp, matchPosition + 1);
}
}
Finally, here's main.cpp (5) (~10 lines, ~45 lines with completer)
#include <QApplication>
#include <backgroundhighlighter.h>
QAbstractItemModel *modelFromFile(const QString& fileName, QCompleter *completer)
{
QFile file(fileName);
if (!file.open(QFile::ReadOnly))
return new QStringListModel(completer);
#ifndef QT_NO_CURSOR
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
#endif
QStringList words;
while (!file.atEnd()) {
QByteArray line = file.readLine();
if (!line.isEmpty())
words << line.trimmed();
}
#ifndef QT_NO_CURSOR
QApplication::restoreOverrideCursor();
#endif
return new QStringListModel(words, completer);
}
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
BackgroundHighlighter bh(":/res/symbols.txt");
QCompleter *completer = new QCompleter();
completer->setModel(modelFromFile(":/res/wordlist.txt", completer));
// use this and comment the above if you don't have or don't want to use wordlist.txt
// QStringListModel *model = new QStringListModel(QStringList() << "aaaaaaa" << "aaaaab" << "aaaabb" << "aaacccc",
completer);
// completer->setModel(model);
completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setWrapAround(false);
bh.setCompleter(completer);
bh.show();
return a.exec();
}
In res.qrc add a / prefix and add files (res/symbols.txt, res/wordlist.txt) from the res/ subdirectory.
I have tested with a symbols.txt file resembling
symbol1 symbol2 symbol3 symbol4 symbol5
symbol1 symbol2 symbol3 symbol4 symbol5
symbol1 symbol2 symbol3 symbol4 symbol5
// ... ditto 500 lines
It takes about 1 second, which probably isn't ideal (100ms is probably more ideal).
However, you might want to watch over for the line count as it grows. With the same text file at 1000 lines, the program will start to take approx. 3 seconds for highlighting.
Note that... I haven't optimised it entirely. There could possibly be a better implementation which formats only when the symbol scrolls into the user's view. This is just a suggestion. How to implement it I don't know.
Notes
For reference, I've attached symbols.txt and wordlist.txt on github.
If you want to change the background colour of formatting, go to lines 27 to 29 of backgroundhighlighter.cpp. There, you can see that I centralised the formatting.
BackgroundHighlighter::clearHighlights() might clear away any background highlights originally added as it sets the ENTIRE document's character background to the default format. This may be an unintended consequence of the result.

Limit QKeySequence/QKeySequenceEdit to only one shortcut

Is it possible to limit QKeySequence to show only one shortcut in QKeySequenceEdit? Currently now it supports up to 4 shortcuts. My application supports key sequences of only one shortcut, e.g. Ctrl+A or Ctrl+C and not e.g. Ctrl+A, D or Ctrl+C, X, Z.
Is it possible to limit QKeySequence or QKeySequenceEdit to just one key sequence?
Solved it, not the best solution but quick...If you want something more customize, I think you have to build it yourself...
customkeysequenceedit.h:
#ifndef CUSTOMKEYSEQUENCEEDIT_H
#define CUSTOMKEYSEQUENCEEDIT_H
#include <QKeySequenceEdit>
class QKeyEvent;
class CustomKeySequenceEdit : public QKeySequenceEdit
{
Q_OBJECT
public:
explicit CustomKeySequenceEdit(QWidget *parent = 0);
~CustomKeySequenceEdit();
protected:
void keyPressEvent(QKeyEvent *pEvent);
};
#endif // CUSTOMKEYSEQUENCEEDIT_H
customkeysequenceedit.cpp:
#include "customkeysequenceedit.h"
#include <QKeyEvent>
CustomKeySequenceEdit::CustomKeySequenceEdit(QWidget *parent) : QKeySequenceEdit(parent) { }
CustomKeySequenceEdit::~CustomKeySequenceEdit() { }
void CustomKeySequenceEdit::keyPressEvent(QKeyEvent *pEvent)
{
QKeySequenceEdit::keyPressEvent(pEvent);
QKeySequence seq(QKeySequence::fromString(keySequence().toString().split(", ").first()));
setKeySequence(seq);
}
You can use the [] operator of QKeySequence: http://doc.qt.io/qt-5/qkeysequence.html#operator-5b-5d
So in your interface constructor, write this:
connect(ui->editShortcut, &QKeySequenceEdit::editingFinished,
this, &dialog::truncateShortcut);
And add this private method to your dialog class:
void dialog::truncateShortcut()
{
int value = ui->editShortcut->keySequence()[0];
QKeySequence shortcut(value);
ui->editShortcut->setKeySequence(shortcut);
}
Doing that, you fully respect the API and don't depend on the , character, which is quite risky.
Most answers is to truncate shortcut after the input done. Anyway, it will show more than one shorcut in the process of inputing, which is kind of annoying.
I found a solutioin that will not even show more than one shorcut.
After one shortcut is inputed, finish the input via clear focus and setKeySequence via override QKeySequenceEdit class keyPressEvent function.
What's more, this method is very easy and graceful!
First create a class myKeySequenceEdit inheriated from QKeySequenceEdit, and below is the codes:
mykeysequenceedit.h:
#ifndef MYKEYSEQUENCEEDIT_H
#define MYKEYSEQUENCEEDIT_H
#include <QKeySequenceEdit>
#include <QWidget>
class myKeySequenceEdit : public QKeySequenceEdit
{
Q_OBJECT
public:
myKeySequenceEdit(QWidget *parent = nullptr);
void keyPressEvent(QKeyEvent *) override;
};
#endif // MYKEYSEQUENCEEDIT_H
mykeysequenceedit.cpp:
#include "mykeysequenceedit.h"
myKeySequenceEdit::myKeySequenceEdit(QWidget *parent) : QKeySequenceEdit(parent) {}
void myKeySequenceEdit::keyPressEvent(QKeyEvent *event)
{
QKeySequenceEdit::keyPressEvent(event);
if (this->keySequence().count() > 0) {
QKeySequenceEdit::setKeySequence(this->keySequence());
emit editingFinished(); // Optinal, depend on if you need the editingFinished signal to be triggered
}
}
My take on this: why would we like to wait in edit mode, if just singular shortcut is wanted. Thus interrupting edit immediately on success:
inline bool QKeySequence_valid( const QKeySequence& accelerator ){
return !accelerator.isEmpty() && accelerator[0] != Qt::Key_unknown;
}
class Single_QKeySequenceEdit : public QKeySequenceEdit
{
protected:
void keyPressEvent( QKeyEvent *e ) override {
QKeySequenceEdit::keyPressEvent( e );
if( QKeySequence_valid( keySequence() ) )
editingFinished();
}
};
Real implementation, because i was struggling to find one of this sort:
class Single_QKeySequenceEdit : public QKeySequenceEdit
{
protected:
void keyPressEvent( QKeyEvent *e ) override {
QKeySequenceEdit::keyPressEvent( e );
if( QKeySequence_valid( keySequence() ) )
clearFocus(); // trigger editingFinished(); via losing focus 🙉
// because this can still receive focus loss b4 getting deleted (practically because modal msgbox)
// and two editingFinished(); b no good
}
void focusOutEvent( QFocusEvent *event ) override {
editingFinished();
}
bool event( QEvent *event ) override { // comsume ALL key presses including Tab
if( event->type() == QEvent::KeyPress ){
keyPressEvent( static_cast<QKeyEvent*>( event ) );
return true;
}
return QKeySequenceEdit::event( event );
}
};
void accelerator_edit( QTreeWidgetItem *item ){
auto edit = new Single_QKeySequenceEdit;
QObject::connect( edit, &QKeySequenceEdit::editingFinished, [item, edit](){
const QKeySequence accelerator = edit->keySequence();
item->treeWidget()->setItemWidget( item, 1, nullptr );
if( QKeySequence_valid( accelerator ) )
accelerator_alter( item, accelerator );
} );
item->treeWidget()->setItemWidget( item, 1, edit );
edit->setFocus(); // track sanity gently via edit being focused property
}

How to find the row from which drag has started QTableView

I was working on QTableView and I want to achieve drag and drop on it.
The way I want it is:
When I drag, I want to take out the row which is to be dragged and place an empty place holder there, this is how I have reimplemented dragEnterEvent:
void dragEnterEvent ( QDragEnterEvent * event )
{
if( event->source() == this )
{
// Get the row to be dragged
int mItemDraggedRowPosition = rowAt( event->pos().y() );
auto currentModel = model();
auto standardModel = boost::polymorphic_cast< QStandardItemModel *>( currentModel );
// Take out the dragged row
QList<QStandardItem *> mDraggedItem = standardModel->takeRow ( ( mItemDraggedRowPosition ) );
// Insert an empty row there
standardModel->insertRow( ( mItemDraggedRowPosition ), new QStandardItem() );
}
event->accept();
}
}
Now the problem I am facing here is the row number which I am getting is incorrect some times.
Am I not doing it correctly .?
Is there any alternative way to find out the row from which the drag was started,
seems like the way I am trying to get it:
int mItemDraggedRowPosition = rowAt( event->pos().y() );
does not return correct value always.
I finally reproduced problem and I got incorrect values too (drag very close to the joining area of 2 rows). So I solved this issue with next event filter:
ui->tableView->setDragEnabled(true);//somewhere in constructor
ui->tableView->setAcceptDrops(true);
//...
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (obj == ui->tableView->viewport() && event->type() == QEvent::DragEnter)
{
QDragEnterEvent * e = static_cast<QDragEnterEvent *>(event);
qDebug( ) << ui->tableView->rowAt(e->pos().y());
}
return QObject::eventFilter(obj, event);
}
To use eventFilter you should also:
protected:
bool eventFilter(QObject *obj, QEvent *event);//in header
and
qApp->installEventFilter(this);//in constructor
I finally ended up overriding "mousePressEvent" and saving the start position there.
I use the same saved position in "dragEnterEvent".
This one works perfectly for me.
void
PrePressView::TableWidget::mousePressEvent( QMouseEvent * inEvent )
{
if ( inEvent->button() == Qt::LeftButton )
{
int rowToMove = rowAt( inEvent->y() );
}
}

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()));
}
}
}
}