QSyntaxHighlighter does not create QTextFragments - c++

I'm working on a syntax highlighter with Qt and I wanted to add unit tests on it to check if formats are well applied.
But I don't manage to get the block divided by formats. I use QTextBlock and QTextFragment but it's not working while the doc of QTextFragment says :
A text fragment describes a piece of text that is stored with a single character format.
Here is the code in a runnable main.cpp file :
#include <QApplication>
#include <QTextEdit>
#include <QSyntaxHighlighter>
#include <QRegularExpression>
#include <QDebug>
class Highlighter : public QSyntaxHighlighter
{
public:
Highlighter(QTextDocument *parent)
: QSyntaxHighlighter(parent)
{}
protected:
void highlightBlock(const QString& text) override
{
QTextCharFormat classFormat;
classFormat.setFontWeight(QFont::Bold);
QRegularExpression pattern { "\\bclass\\b" };
QRegularExpressionMatchIterator matchIterator = pattern.globalMatch(text);
while (matchIterator.hasNext())
{
QRegularExpressionMatch match = matchIterator.next();
setFormat(match.capturedStart(), match.capturedLength(), classFormat);
}
// ==== TESTS ==== //
qDebug() << "--------------------------------";
QTextDocument *doc = document();
QTextBlock currentBlock = doc->firstBlock();
while (currentBlock.isValid()) {
qDebug() << "BLOCK" << currentBlock.text();
QTextBlockFormat blockFormat = currentBlock.blockFormat();
QTextCharFormat charFormat = currentBlock.charFormat();
QFont font = charFormat.font();
// each QTextBlock holds multiple fragments of text, so iterate over it:
QTextBlock::iterator it;
for (it = currentBlock.begin(); !(it.atEnd()); ++it) {
QTextFragment currentFragment = it.fragment();
if (currentFragment.isValid()) {
// a text fragment also has a char format with font:
QTextCharFormat fragmentCharFormat = currentFragment.charFormat();
QFont fragmentFont = fragmentCharFormat.font();
qDebug() << "FRAGMENT" << currentFragment.text();
}
}
currentBlock = currentBlock.next();
}
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
auto *textEdit = new QTextEdit;
auto *highlighter = new Highlighter(textEdit->document());
Q_UNUSED(highlighter);
textEdit->setText("a class for test");
textEdit->show();
return a.exec();
}
And it outputs only one block "a class for test" and one format "a class for test" while the class keyword is in bold.
Thanks for your help !

Ok I found this from the documentation of QSyntaxHighlighter::setFormat :
Note that the document itself remains unmodified by the format set through this function.
The formats applied by the syntax highlighter are not stored in QTextBlock::charFormat but in the additional formats :
QVector<QTextLayout::FormatRange> formats = textEdit->textCursor().block().layout()->formats();

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

gtkmm : Drawing text with cairo

I want to draw a simple text with Cairo in an app using Gtkmm. I want to give the font style (it can be Pango::FontDescription or Pango::Context and so on ...) directly to draw text with Cairo when the Gtk::FontButton is clicked (in other words when the signal_font_set signal is issued). In the example below, I have a Gtk::HeaderBar that contains a Gtk::FontButton that sends a Glib::RefPtr<<Pango::Context>> to the drawing class when clicked. I want something like this:
MyWindow.cpp:
#include <iostream>
#include "MyWindow.h"
MyWindow::MyWindow() {
set_default_size(1000, 1000);
set_position(Gtk::WIN_POS_CENTER);
header.set_show_close_button(true);
header.pack_start(fontButton);;
set_titlebar(header);
fontButton.signal_font_set().connect([&] {
drawingArea.select_font(fontButton.get_pango_context());
});
scrolledWindow.add(drawingArea);
add(scrolledWindow);
show_all();
}
MyDrawing.h:
#ifndef DRAWING_H
#define DRAWING_H
#include <gtkmm.h>
#include <cairo/cairo.h>
class MyDrawing : public Gtk::Layout
{
public:
MyDrawing();
~MyDrawing() override;
void select_font(Glib::RefPtr<Pango::Context> p_pangoContext);
private:
bool draw_text(const Cairo::RefPtr<::Cairo::Context>& p_context);
Glib::RefPtr<Pango::Context> m_pangoContext;
};
#endif // DRAWING_H
and MyDrawing.cpp:
#include <iostream>
#include <utility>
#include "MyDrawing.h"
#include <cairomm/context.h>
#include <cairomm/surface.h>
MyDrawing::MyDrawing() {
this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_text));
}
MyDrawing::~MyDrawing() = default;
bool MyDrawing::draw_text(const Cairo::RefPtr<::Cairo::Context> &p_context) {
auto layout = create_pango_layout("hello ");
if(m_pangoContext) {
layout->set_font_description(m_pangoContext->get_font_description());
}
else {
Pango::FontDescription fontDescription;
layout->set_font_description(fontDescription);
}
p_context->save();
p_context->set_font_size(30);
p_context->set_source_rgb(0.1, 0.1, 0.1);
p_context->move_to(40, 40);
layout->show_in_cairo_context(p_context);
p_context->restore();
return true;
}
void MyDrawing::select_font(Glib::RefPtr<Pango::Context> p_pangoContext) {
this->m_pangoContext = std::move(p_pangoContext);
queue_draw();
}
when I set up Pango::FontDescription manually like :
Pango::FontDescription fontDescription;
fontDescription.set_weight(Pango::WEIGHT_BOLD);
fontDescription.set_style(Pango::STYLE_ITALIC);
layout->set_font_description(fontDescription);
it works:
Here is a way that works: instead of using a Pango::Context, you can get the font name from Gtk::FontButton::get_font_name and then create a Pango::FontDescription from it. Here is a minimal example to show this:
#include <iostream>
#include <gtkmm.h>
class MyDrawing : public Gtk::Layout
{
public:
MyDrawing();
void set_font(const std::string& p_selectedFont);
private:
bool draw_text(const Cairo::RefPtr<::Cairo::Context>& p_context);
std::string m_selectedFont;
Glib::RefPtr<Pango::Layout> m_pangoLayout;
};
MyDrawing::MyDrawing()
{
signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_text));
m_pangoLayout = Pango::Layout::create(get_pango_context());
m_pangoLayout->set_text(
"This text's appearance should change\n"
"when font description changes."
);
}
void MyDrawing::set_font(const std::string& p_selectedFont)
{
// Record the font selected by the user (as a string... Ugh!):
m_selectedFont = p_selectedFont;
std::cout << "Selected font: " << m_selectedFont << std::endl;
// Request a redraw:
queue_draw();
}
bool MyDrawing::draw_text(const Cairo::RefPtr<::Cairo::Context>& p_context)
{
// Create a font description from what was selected by the user earlier,
// and set it:
const Pango::FontDescription description{m_selectedFont};
m_pangoLayout->set_font_description(description);
p_context->save();
p_context->set_font_size(30);
p_context->set_source_rgb(0.1, 0.1, 0.1);
p_context->move_to(40, 40);
m_pangoLayout->show_in_cairo_context(p_context);
p_context->restore();
return true;
}
class MyWindow : public Gtk::ApplicationWindow
{
public:
MyWindow();
private:
Gtk::HeaderBar m_headerBar;
Gtk::FontButton m_fontButton;
MyDrawing m_drawingArea;
};
MyWindow::MyWindow()
{
m_headerBar.set_show_close_button(true);
m_headerBar.pack_start(m_fontButton);;
set_titlebar(m_headerBar);
m_fontButton.signal_font_set().connect(
[this]()
{
m_drawingArea.set_font(m_fontButton.get_font_name());
}
);
add(m_drawingArea);
show_all();
}
int main(int argc, char *argv[])
{
auto app = Gtk::Application::create(argc, argv, "org.gtkmm.examples.base");
MyWindow window;
window.show_all();
return app->run(window);
}
I am not a Pango expert, so I don't know if there is a better way using your initial strategy. Furthermore, the string representing the font names seem to need to follow some convention. From the documentation of Pango::FontDescription::FontDescription(const Glib::ustring& font_name) (Gtkmm 3.24):
font_name must have the form "[FAMILY-LIST] [STYLE-OPTIONS] [SIZE]",
where FAMILY-LIST is a comma separated list of families optionally
terminated by a comma, STYLE_OPTIONS is a whitespace separated list of
words where each WORD describes one of style, variant, weight, or
stretch, and SIZE is an decimal number (size in points). Any one of
the options may be absent. If FAMILY-LIST is absent, then the
family_name field of the resulting font description will be
initialized to 0. If STYLE-OPTIONS is missing, then all style options
will be set to the default values. If SIZE is missing, the size in the
resulting font description will be set to 0.
so you may have to do some testing of your own to make sure this works for all fonts. In my case, all 10-15 fonts I have tested seemed to work fine.

How can I get an array of all currency symbols and currency abbreviations in C++ or Qt?

I need to get all possible currency symbols and currency abbreviations in my program in an array. How can I achieve this with C++ or possibly with Qt?
Parse Existing Up-To-Date Currencies List
I found the currency codes JSON list https://gist.github.com/Fluidbyte/2973986.
This list is regularly updated.
How the list could be parsed. Download the raw JSON text from the link. Save it to an UTF-8 encoded text file. Parse it somehow about written below.
#include <QtWidgets/QApplication>
#include <QVariantMap>
#include <QStringList>
#include <QJsonDocument>
#include <QJsonObject>
#include <QFile>
#include <QDebug>
#include <QPlainTextEdit>
using CurrencyMap = QMap<QString, QVariantMap>; // Use three-letter code as a key
using CurrencyMapIterator = QMapIterator<QString, QVariantMap>;
CurrencyMap parseCurrencyDataFromJson(QByteArray utf8Json)
{
CurrencyMap ret;
QJsonParseError parseError;
QJsonDocument document = QJsonDocument::fromJson(utf8Json, &parseError);
if (parseError.error != QJsonParseError::NoError)
{
qWarning() << parseError.errorString();
}
if (!document.isNull())
{
QJsonObject jobject = document.object();
// Iterate over all the currencies
for (QJsonValue val : jobject)
{
// Object of a given currency
QJsonObject obj = val.toObject();
// Three-letter code of the currency
QString name = obj.value("code").toString();
// All the data available for the given currency
QVariantMap fields = obj.toVariantMap();
ret.insert(name, fields);
}
}
return ret;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPlainTextEdit plainTextEdit;
plainTextEdit.show();
// File saved from the GitHub repo
QFile file("curr.json");
if (file.open(QIODevice::ReadOnly))
{
QByteArray jsonData = file.readAll();
CurrencyMap currencyMap = parseCurrencyDataFromJson(jsonData);
// Output all the available currency symbols to the plainTextEdit
CurrencyMapIterator currency(currencyMap);
while (currency.hasNext())
{
currency.next();
QString code = currency.key();
QVariantMap fileds = currency.value();
QStringList currencyInfo
{
code,
fileds.value("symbol").toString(),
fileds.value("symbol_native").toString()
};
plainTextEdit.appendPlainText(currencyInfo.join('\t'));
}
QString total = QString("\nTotal Available Currencies Count = %1")
.arg(currencyMap.count());
plainTextEdit.appendPlainText(total);
}
return a.exec();
}
QChar-Limited Solution
QChar and Unicode based solution. But be note that QChar itself supports codes up to 0xFFFF maximum only. Therefore only limited amount of symbols can be retrieved this way.
// In Qt, Unicode characters are 16-bit entities.
// QChar itself supports only codes with 0xFFFF maximum
QList<QChar> getAllUnicodeSymbolsForCategory(QChar::Category cat)
{
QList<QChar> ret;
// QChar actually stores 16-bit values
const static quint16 QCharMaximum = 0xFFFF;
for (quint16 val = 0; val < QCharMaximum; val++)
{
QChar ch(val);
if (ch.category() == cat)
{
ret.append(ch);
}
}
return ret;
}
Usage
QList<QChar> currencies = getAllUnicodeSymbolsForCategory(QChar::Symbol_Currency);

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.

How to Get Absolute Path of Currently Selected Item in QTreeWidget on mouse Clicked

I have a simple QTreeWidget pointing to the root directory:
#include <QTreeWidget>
#include <QStringList>
#include <QApplication>
int main(int argc, char **argv)
{
QApplication application(argc, argv);
QStringList fileNames{"TEST/branch", "trunk"};
QTreeWidget treeWidget;
treeWidget.setColumnCount(1);
for (const auto& filename : fileNames)
{
QTreeWidgetItem *parentTreeItem = new QTreeWidgetItem(&treeWidget);
parentTreeItem->setText(0, filename.split('/').first());
QStringList filenameParts = filename.split('/').mid(1);
for(const auto& filenamePart : filenameParts)
{
QTreeWidgetItem *treeItem = new QTreeWidgetItem();
treeItem->setText(0, filenamePart);
parentTreeItem->addChild(treeItem);
parentTreeItem = treeItem;
}
}
treeWidget.show();
return application.exec();
}
Output:
The item I have selected above is /TEST/branches. How can I get the absolute path of the currently selected item?
Well, I don't think there is a built in function does that but you can write a function yourself like
QString treeItemToFullPath(QTreeWidgetItem* treeItem)
{
QString fullPath= treeItem->text(0);
while (treeItem->parent() != NULL)
{
fullPath= treeItem->parent()->text(0) + "/" + fullPath;
treeItem = treeItem->parent();
}
return fullPath;
}
edit:
Input treeItem is the selected tree item that you want to show its path. if you are sure at least one item is selected, you can get it by
treeWidget.selectedItems().first();
Another mehtod is using tooltips. You can add tip for each item, while you are adding them to tree, but you can do this after you add them in their final place.
change this
for(const auto& filenamePart : filenameParts)
{
QTreeWidgetItem *treeItem = new QTreeWidgetItem();
treeItem->setText(0, filenamePart);
parentTreeItem->addChild(treeItem);
parentTreeItem = treeItem;
}
as this
for(const auto& filenamePart : filenameParts)
{
QTreeWidgetItem *treeItem = new QTreeWidgetItem();
treeItem->setText(0, filenamePart);
parentTreeItem->addChild(treeItem);
parentTreeItem = treeItem;
treeItem->setToolTip(0, treeItemToFullPath(treeItem));
}
this way you will see the full path whenever you hover the mouse on the item.
To get notified of the current item change, one can use QTreeWidget::currentItemChanged or QItemSelectionModel::currentChanged.
There are two main approaches to obtaining the full path:
Iterate up the tree from the selected item and reconstruct the path. This keeps the data model normalized - without redundant information.
Store full path to each item.
If the tree is large, storing the model normalized will use less memory. Given that selection of the items is presumably rare because it's done on explicit user input, the cost of iterating the tree to extract the full path is minuscule. Humans aren't all that fast when it comes to mashing the keys or the mouse button.
The example demonstrates both approaches:
// https://github.com/KubaO/stackoverflown/tree/master/questions/tree-path-41037995
#include <QtWidgets>
QTreeWidgetItem *get(QTreeWidgetItem *parent, const QString &text) {
for (int i = 0; i < parent->childCount(); ++i) {
auto child = parent->child(i);
if (child->text(0) == text)
return child;
}
return new QTreeWidgetItem(parent, {text});
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QStringList filenames{"TEST/branch", "TEST/foo", "trunk"};
QWidget window;
QVBoxLayout layout(&window);
QTreeWidget treeWidget;
QLabel label1, label2;
for (const auto &filename : filenames) {
QString path;
auto item = treeWidget.invisibleRootItem();
for (auto const &chunk : filename.split('/')) {
item = get(item, chunk);
path.append(QStringLiteral("/%1").arg(chunk));
item->setData(0, Qt::UserRole, path);
}
}
QObject::connect(&treeWidget, &QTreeWidget::currentItemChanged, [&](const QTreeWidgetItem *item){
QString path;
for (; item; item = item->parent())
path.prepend(QStringLiteral("/%1").arg(item->text(0)));
label1.setText(path);
});
QObject::connect(&treeWidget, &QTreeWidget::currentItemChanged, [&](const QTreeWidgetItem *item){
label2.setText(item->data(0, Qt::UserRole).toString());
});
layout.addWidget(&treeWidget);
layout.addWidget(&label1);
layout.addWidget(&label2);
window.show();
return app.exec();
}