fix(StatusTextMessage): Allow user to copy parts of a message containing URLs

The root cause of this issue is that the `TextEdit.text` was replaced on hover to add specific styling for the hovered state. As a result the selection was dropped.

To fix this I've moved the highlighted hyperlink style to the StatusSyntaxHighlighter.
This commit is contained in:
Alex Jbanca 2024-03-13 10:59:40 +02:00 committed by Alex Jbanca
parent 4ca7e9b32d
commit 3b17134451
4 changed files with 189 additions and 72 deletions

View File

@ -50,6 +50,16 @@ SplitView {
isAReply: true isAReply: true
trustIndicator: StatusContactVerificationIcons.TrustedType.None trustIndicator: StatusContactVerificationIcons.TrustedType.None
} }
ListElement {
timestamp: 1667937930489
senderId: "zqdeadbeef"
senderDisplayName: "replicator.stateofus.eth"
contentType: StatusMessage.ContentType.Text
message: "Test message with a link https://github.com/. Try to copy the link!"
isContact: true
isAReply: true
trustIndicator: StatusContactVerificationIcons.TrustedType.None
}
} }
readonly property var colorHash: ListModel { readonly property var colorHash: ListModel {
ListElement { colorId: 13; segmentLength: 5 } ListElement { colorId: 13; segmentLength: 5 }

View File

@ -3,6 +3,7 @@
#include <QQmlParserStatus> #include <QQmlParserStatus>
#include <QRegularExpression> #include <QRegularExpression>
#include <QSyntaxHighlighter> #include <QSyntaxHighlighter>
#include <QFlags>
class QQuickTextDocument; class QQuickTextDocument;
class QTextCharFormat; class QTextCharFormat;
@ -26,9 +27,25 @@ class StatusSyntaxHighlighter : public QSyntaxHighlighter, public QQmlParserStat
Q_PROPERTY(QString highlightedHyperlink READ highlightedHyperlink WRITE setHighlightedHyperlink NOTIFY Q_PROPERTY(QString highlightedHyperlink READ highlightedHyperlink WRITE setHighlightedHyperlink NOTIFY
highlightedHyperlinkChanged) highlightedHyperlinkChanged)
Q_PROPERTY(Features features READ features WRITE setFeatures NOTIFY featuresChanged)
Q_INTERFACES(QQmlParserStatus) Q_INTERFACES(QQmlParserStatus)
public: public:
enum FeatureFlags {
None = 0,
SingleLineBold = 1 << 0,
SingleLineItalic = 1 << 1,
Code = 1 << 2,
CodeBlock = 1 << 3,
SingleLineStrikeThrough = 1 << 4,
Hyperlink = 1 << 5,
HighlightedHyperlink = 1 << 6,
All = SingleLineBold | SingleLineItalic | Code | CodeBlock | SingleLineStrikeThrough | Hyperlink | HighlightedHyperlink
};
Q_DECLARE_FLAGS(Features, FeatureFlags)
Q_FLAG(Features)
explicit StatusSyntaxHighlighter(QObject* parent = nullptr); explicit StatusSyntaxHighlighter(QObject* parent = nullptr);
QQuickTextDocument* quickTextDocument() const; QQuickTextDocument* quickTextDocument() const;
@ -47,6 +64,7 @@ signals:
void hyperlinkHoverColorChanged(); void hyperlinkHoverColorChanged();
void hyperlinksChanged(); void hyperlinksChanged();
void highlightedHyperlinkChanged(); void highlightedHyperlinkChanged();
void featuresChanged();
private: private:
QQuickTextDocument* m_quicktextdocument{nullptr}; QQuickTextDocument* m_quicktextdocument{nullptr};
@ -80,8 +98,15 @@ private:
QStringList getPossibleUrlFormats(const QUrl& url) const; QStringList getPossibleUrlFormats(const QUrl& url) const;
QRegularExpression buildHyperlinkRegex(QStringList hyperlinks) const; QRegularExpression buildHyperlinkRegex(QStringList hyperlinks) const;
Features features() const;
void setFeatures(Features features);
void buildRules();
int findRuleIndex(FeatureFlags flag) const;
struct HighlightingRule struct HighlightingRule
{ {
int id;
QRegularExpression pattern; QRegularExpression pattern;
QRegularExpression::MatchType matchType{QRegularExpression::PartialPreferCompleteMatch}; QRegularExpression::MatchType matchType{QRegularExpression::PartialPreferCompleteMatch};
QTextCharFormat format; QTextCharFormat format;
@ -94,4 +119,8 @@ private:
QTextCharFormat singleLineStrikeThroughFormat; QTextCharFormat singleLineStrikeThroughFormat;
QTextCharFormat hyperlinkFormat; QTextCharFormat hyperlinkFormat;
QTextCharFormat highlightedHyperlinkFormat; QTextCharFormat highlightedHyperlinkFormat;
Features m_features{All};
}; };
Q_DECLARE_OPERATORS_FOR_FLAGS(StatusSyntaxHighlighter::Features)

View File

@ -1,8 +1,9 @@
import QtQuick 2.13 import QtQuick 2.15
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 import StatusQ.Core.Utils 0.1
@ -50,7 +51,7 @@ Item {
const editedMessage = formattedMessage.slice(0, index) const editedMessage = formattedMessage.slice(0, index)
+ ` <span class="isEdited">` + qsTr("(edited)") + `</span>` + ` <span class="isEdited">` + qsTr("(edited)") + `</span>`
+ formattedMessage.slice(index); + formattedMessage.slice(index);
return Utils.getMessageWithStyle(Emoji.parse(editedMessage), d.hoveredLink) return Utils.getMessageWithStyle(Emoji.parse(editedMessage))
} }
if (root.convertToSingleLine || isQuote) if (root.convertToSingleLine || isQuote)
@ -66,7 +67,7 @@ Item {
// short return not to add styling when no html // short return not to add styling when no html
return formattedMessage return formattedMessage
return Utils.getMessageWithStyle(formattedMessage, d.hoveredLink) return Utils.getMessageWithStyle(formattedMessage)
} }
} }
@ -106,8 +107,14 @@ Item {
} }
} }
// Horizontal crop mask StatusSyntaxHighlighter {
quickTextDocument: chatText.textDocument
hyperlinkHoverColor: Theme.palette.primaryColor3
highlightedHyperlink: d.hoveredLink
features: StatusSyntaxHighlighter.HighlightedHyperlink
}
// Horizontal crop mask
Loader { Loader {
id: horizontalClipMask id: horizontalClipMask
anchors.fill: chatText anchors.fill: chatText

View File

@ -9,86 +9,133 @@ StatusSyntaxHighlighter::StatusSyntaxHighlighter(QObject* parent)
void StatusSyntaxHighlighter::componentComplete() void StatusSyntaxHighlighter::componentComplete()
{ {
HighlightingRule rule; buildRules();
//BOLD connect(this, &StatusSyntaxHighlighter::hyperlinksChanged, this, [this](){
singlelineBoldFormat.setFontWeight(QFont::Bold); const auto index = findRuleIndex(StatusSyntaxHighlighter::Hyperlink);
rule.pattern = QRegularExpression(QStringLiteral("(\\*\\*(.*?)\\*\\*)|(\\_\\_(.*?)\\_\\_)")); if (index == -1) return;
rule.format = singlelineBoldFormat;
highlightingRules.append(rule);
//BOLD
//ITALIC highlightingRules[index].pattern = hyperlinksRegularExpression();
singleLineItalicFormat.setFontItalic(true);
rule.pattern = QRegularExpression(QStringLiteral("(\\*(.*?)\\*)|(\\_(.*?)\\_)"));
rule.format = singleLineItalicFormat;
highlightingRules.append(rule);
//ITALIC
//STRIKETHROUGH
singleLineStrikeThroughFormat.setFontStrikeOut(true);
rule.pattern = QRegularExpression(QStringLiteral("\\~\\~(.*?)\\~\\~"));
rule.format = singleLineStrikeThroughFormat;
highlightingRules.append(rule);
//STRIKETHROUGH
//CODE (`foo`)
codeFormat.setFontFamily(QStringLiteral("Roboto Mono"));
codeFormat.setBackground(m_codeBackgroundColor);
codeFormat.setForeground(m_codeForegroundColor);
rule.pattern = QRegularExpression(QStringLiteral("\\`{1}(.+)\\`{1}"),
// to not match single backtick pair inside a triple backtick block below
QRegularExpression::InvertedGreedinessOption);
rule.format = codeFormat;
highlightingRules.append(rule);
//CODE
//CODEBLOCK (```\nfoo\nbar```)
rule.pattern = QRegularExpression(QStringLiteral("\\`{3}(.+)\\`{3}"));
rule.format = codeFormat;
highlightingRules.append(rule);
//CODEBLOCK
//HYPERLINKS
//QRegularExpression to match any hyperlink in m_hyperlinks
hyperlinkFormat.setForeground(m_hyperlinkColor);
rule.pattern = hyperlinksRegularExpression();
rule.format = hyperlinkFormat;
rule.matchType = QRegularExpression::NormalMatch;
highlightingRules.append(rule);
const int hyperlinksRuleIndex = highlightingRules.size() - 1;
//HIGHLIGHTED
highlightedHyperlinkFormat.setForeground(m_hyperlinkColor);
highlightedHyperlinkFormat.setBackground(m_hyperlinkHoverColor);
rule.pattern = highlightedHyperlinkRegularExpression();
rule.format = highlightedHyperlinkFormat;
rule.matchType = QRegularExpression::NormalMatch;
highlightingRules.append(rule);
const int highlightedHyperlinkRuleIndex = highlightingRules.size() - 1;
connect(this, &StatusSyntaxHighlighter::hyperlinksChanged, this, [hyperlinksRuleIndex, this](){
highlightingRules[hyperlinksRuleIndex].pattern = hyperlinksRegularExpression();
rehighlight(); rehighlight();
}); });
connect(this, &StatusSyntaxHighlighter::hyperlinkColorChanged, this, [hyperlinksRuleIndex, this](){ connect(this, &StatusSyntaxHighlighter::hyperlinkColorChanged, this, [this](){
const auto index = findRuleIndex(StatusSyntaxHighlighter::Hyperlink);
if (index == -1) return;
hyperlinkFormat.setForeground(m_hyperlinkColor); hyperlinkFormat.setForeground(m_hyperlinkColor);
highlightedHyperlinkFormat.setForeground(m_hyperlinkColor); highlightedHyperlinkFormat.setForeground(m_hyperlinkColor);
highlightingRules[hyperlinksRuleIndex].format = hyperlinkFormat; highlightingRules[index].format = hyperlinkFormat;
rehighlight(); rehighlight();
}); });
connect(this, &StatusSyntaxHighlighter::highlightedHyperlinkChanged, this, [highlightedHyperlinkRuleIndex, this](){ connect(this, &StatusSyntaxHighlighter::highlightedHyperlinkChanged, this, [this](){
highlightingRules[highlightedHyperlinkRuleIndex].pattern = highlightedHyperlinkRegularExpression(); const auto index = findRuleIndex(StatusSyntaxHighlighter::HighlightedHyperlink);
if (index == -1) return;
highlightingRules[index].pattern = highlightedHyperlinkRegularExpression();
rehighlight(); rehighlight();
}); });
connect(this, &StatusSyntaxHighlighter::hyperlinkHoverColorChanged, this, [highlightedHyperlinkRuleIndex, this](){ connect(this, &StatusSyntaxHighlighter::hyperlinkHoverColorChanged, this, [this](){
const auto index = findRuleIndex(StatusSyntaxHighlighter::HighlightedHyperlink);
if (index == -1) return;
highlightedHyperlinkFormat.setBackground(m_hyperlinkHoverColor); highlightedHyperlinkFormat.setBackground(m_hyperlinkHoverColor);
highlightingRules[highlightedHyperlinkRuleIndex].format = highlightedHyperlinkFormat; highlightingRules[index].format = highlightedHyperlinkFormat;
rehighlight(); rehighlight();
}); });
connect(this, &StatusSyntaxHighlighter::featuresChanged, this, [this](){
buildRules();
rehighlight();
});
}
void StatusSyntaxHighlighter::buildRules()
{
HighlightingRule rule;
highlightingRules.clear();
if (m_features & StatusSyntaxHighlighter::SingleLineBold)
{
//BOLD
singlelineBoldFormat.setFontWeight(QFont::Bold);
rule.id = StatusSyntaxHighlighter::SingleLineBold;
rule.pattern = QRegularExpression(QStringLiteral("(\\*\\*(.*?)\\*\\*)|(\\_\\_(.*?)\\_\\_)"));
rule.format = singlelineBoldFormat;
highlightingRules.append(rule);
//BOLD
}
if (m_features & StatusSyntaxHighlighter::SingleLineItalic)
{
//ITALIC
singleLineItalicFormat.setFontItalic(true);
rule.id = StatusSyntaxHighlighter::SingleLineItalic;
rule.pattern = QRegularExpression(QStringLiteral("(\\*(.*?)\\*)|(\\_(.*?)\\_)"));
rule.format = singleLineItalicFormat;
highlightingRules.append(rule);
//ITALIC
}
if (m_features & StatusSyntaxHighlighter::SingleLineStrikeThrough)
{
//STRIKETHROUGH
singleLineStrikeThroughFormat.setFontStrikeOut(true);
rule.id = StatusSyntaxHighlighter::SingleLineStrikeThrough;
rule.pattern = QRegularExpression(QStringLiteral("\\~\\~(.*?)\\~\\~"));
rule.format = singleLineStrikeThroughFormat;
highlightingRules.append(rule);
//STRIKETHROUGH
}
if (m_features & StatusSyntaxHighlighter::Code)
{
//CODE (`foo`)
codeFormat.setFontFamily(QStringLiteral("Roboto Mono"));
codeFormat.setBackground(m_codeBackgroundColor);
codeFormat.setForeground(m_codeForegroundColor);
rule.id = StatusSyntaxHighlighter::Code;
rule.pattern = QRegularExpression(QStringLiteral("\\`{1}(.+)\\`{1}"),
// to not match single backtick pair inside a triple backtick block below
QRegularExpression::InvertedGreedinessOption);
rule.format = codeFormat;
highlightingRules.append(rule);
//CODE
}
if (m_features & StatusSyntaxHighlighter::CodeBlock)
{
//CODEBLOCK (```\nfoo\nbar```)
rule.id = StatusSyntaxHighlighter::CodeBlock;
rule.pattern = QRegularExpression(QStringLiteral("\\`{3}(.+)\\`{3}"));
rule.format = codeFormat;
highlightingRules.append(rule);
//CODEBLOCK
}
if (m_features & StatusSyntaxHighlighter::Hyperlink)
{
//HYPERLINKS
hyperlinkFormat.setForeground(m_hyperlinkColor);
rule.id = StatusSyntaxHighlighter::Hyperlink;
rule.pattern = hyperlinksRegularExpression();
rule.format = hyperlinkFormat;
rule.matchType = QRegularExpression::NormalMatch;
highlightingRules.append(rule);
//HYPERLINKS
}
if (m_features & StatusSyntaxHighlighter::HighlightedHyperlink)
{
//HIGHLIGHTED
highlightedHyperlinkFormat.setForeground(m_hyperlinkColor);
highlightedHyperlinkFormat.setBackground(m_hyperlinkHoverColor);
rule.id = StatusSyntaxHighlighter::HighlightedHyperlink;
rule.pattern = highlightedHyperlinkRegularExpression();
rule.format = highlightedHyperlinkFormat;
rule.matchType = QRegularExpression::NormalMatch;
highlightingRules.append(rule);
}
} }
void StatusSyntaxHighlighter::highlightBlock(const QString& text) void StatusSyntaxHighlighter::highlightBlock(const QString& text)
@ -217,6 +264,7 @@ QStringList StatusSyntaxHighlighter::getPossibleUrlFormats(const QUrl& url) cons
result.append(QRegularExpression::escape(url.toString())); result.append(QRegularExpression::escape(url.toString()));
result.append(QRegularExpression::escape(url.toString(QUrl::EncodeUnicode))); result.append(QRegularExpression::escape(url.toString(QUrl::EncodeUnicode)));
result.append(QRegularExpression::escape(url.toString(QUrl::FullyEncoded))); result.append(QRegularExpression::escape(url.toString(QUrl::FullyEncoded)));
return result; return result;
} }
@ -225,9 +273,32 @@ QRegularExpression StatusSyntaxHighlighter::buildHyperlinkRegex(QStringList hype
hyperlinks.removeAll(QString()); hyperlinks.removeAll(QString());
if(hyperlinks.isEmpty()) if(hyperlinks.isEmpty())
return QRegularExpression("(?!)"); return QRegularExpression(QStringLiteral("(?!)"));
QString matchHyperlinks = QStringLiteral("(?:^|(?<=\\s))(") + hyperlinks.join("|") + QStringLiteral(")(?:(?=\\s)|$)"); QString matchHyperlinks = QStringLiteral("(?:^|(?<=\\s))(") + hyperlinks.join('|') + QStringLiteral(")(?:(?=\\s|[[:punct:]])|$)");
auto regex = QRegularExpression(matchHyperlinks, QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption | QRegularExpression::MultilineOption); auto regex = QRegularExpression(matchHyperlinks, QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption | QRegularExpression::MultilineOption);
regex.optimize(); regex.optimize();
return regex; return regex;
}
StatusSyntaxHighlighter::Features StatusSyntaxHighlighter::features() const
{
return m_features;
}
void StatusSyntaxHighlighter::setFeatures(Features features)
{
if(features == m_features) return;
m_features = features;
emit featuresChanged();
}
int StatusSyntaxHighlighter::findRuleIndex(FeatureFlags flag) const
{
for (int i = 0; i < highlightingRules.size(); ++i)
{
if (highlightingRules[i].id == flag)
return i;
}
return -1;
} }