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
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 {
ListElement { colorId: 13; segmentLength: 5 }

View File

@ -3,6 +3,7 @@
#include <QQmlParserStatus>
#include <QRegularExpression>
#include <QSyntaxHighlighter>
#include <QFlags>
class QQuickTextDocument;
class QTextCharFormat;
@ -26,9 +27,25 @@ class StatusSyntaxHighlighter : public QSyntaxHighlighter, public QQmlParserStat
Q_PROPERTY(QString highlightedHyperlink READ highlightedHyperlink WRITE setHighlightedHyperlink NOTIFY
highlightedHyperlinkChanged)
Q_PROPERTY(Features features READ features WRITE setFeatures NOTIFY featuresChanged)
Q_INTERFACES(QQmlParserStatus)
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);
QQuickTextDocument* quickTextDocument() const;
@ -47,6 +64,7 @@ signals:
void hyperlinkHoverColorChanged();
void hyperlinksChanged();
void highlightedHyperlinkChanged();
void featuresChanged();
private:
QQuickTextDocument* m_quicktextdocument{nullptr};
@ -80,8 +98,15 @@ private:
QStringList getPossibleUrlFormats(const QUrl& url) const;
QRegularExpression buildHyperlinkRegex(QStringList hyperlinks) const;
Features features() const;
void setFeatures(Features features);
void buildRules();
int findRuleIndex(FeatureFlags flag) const;
struct HighlightingRule
{
int id;
QRegularExpression pattern;
QRegularExpression::MatchType matchType{QRegularExpression::PartialPreferCompleteMatch};
QTextCharFormat format;
@ -94,4 +119,8 @@ private:
QTextCharFormat singleLineStrikeThroughFormat;
QTextCharFormat hyperlinkFormat;
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 StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
@ -50,7 +51,7 @@ Item {
const editedMessage = formattedMessage.slice(0, index)
+ ` <span class="isEdited">` + qsTr("(edited)") + `</span>`
+ formattedMessage.slice(index);
return Utils.getMessageWithStyle(Emoji.parse(editedMessage), d.hoveredLink)
return Utils.getMessageWithStyle(Emoji.parse(editedMessage))
}
if (root.convertToSingleLine || isQuote)
@ -66,7 +67,7 @@ Item {
// short return not to add styling when no html
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 {
id: horizontalClipMask
anchors.fill: chatText

View File

@ -9,86 +9,133 @@ StatusSyntaxHighlighter::StatusSyntaxHighlighter(QObject* parent)
void StatusSyntaxHighlighter::componentComplete()
{
HighlightingRule rule;
buildRules();
//BOLD
singlelineBoldFormat.setFontWeight(QFont::Bold);
rule.pattern = QRegularExpression(QStringLiteral("(\\*\\*(.*?)\\*\\*)|(\\_\\_(.*?)\\_\\_)"));
rule.format = singlelineBoldFormat;
highlightingRules.append(rule);
//BOLD
connect(this, &StatusSyntaxHighlighter::hyperlinksChanged, this, [this](){
const auto index = findRuleIndex(StatusSyntaxHighlighter::Hyperlink);
if (index == -1) return;
//ITALIC
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();
highlightingRules[index].pattern = hyperlinksRegularExpression();
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);
highlightedHyperlinkFormat.setForeground(m_hyperlinkColor);
highlightingRules[hyperlinksRuleIndex].format = hyperlinkFormat;
highlightingRules[index].format = hyperlinkFormat;
rehighlight();
});
connect(this, &StatusSyntaxHighlighter::highlightedHyperlinkChanged, this, [highlightedHyperlinkRuleIndex, this](){
highlightingRules[highlightedHyperlinkRuleIndex].pattern = highlightedHyperlinkRegularExpression();
connect(this, &StatusSyntaxHighlighter::highlightedHyperlinkChanged, this, [this](){
const auto index = findRuleIndex(StatusSyntaxHighlighter::HighlightedHyperlink);
if (index == -1) return;
highlightingRules[index].pattern = highlightedHyperlinkRegularExpression();
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);
highlightingRules[highlightedHyperlinkRuleIndex].format = highlightedHyperlinkFormat;
highlightingRules[index].format = highlightedHyperlinkFormat;
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)
@ -217,6 +264,7 @@ QStringList StatusSyntaxHighlighter::getPossibleUrlFormats(const QUrl& url) cons
result.append(QRegularExpression::escape(url.toString()));
result.append(QRegularExpression::escape(url.toString(QUrl::EncodeUnicode)));
result.append(QRegularExpression::escape(url.toString(QUrl::FullyEncoded)));
return result;
}
@ -225,9 +273,32 @@ QRegularExpression StatusSyntaxHighlighter::buildHyperlinkRegex(QStringList hype
hyperlinks.removeAll(QString());
if(hyperlinks.isEmpty())
return QRegularExpression("(?!)");
QString matchHyperlinks = QStringLiteral("(?:^|(?<=\\s))(") + hyperlinks.join("|") + QStringLiteral(")(?:(?=\\s)|$)");
return QRegularExpression(QStringLiteral("(?!)"));
QString matchHyperlinks = QStringLiteral("(?:^|(?<=\\s))(") + hyperlinks.join('|') + QStringLiteral(")(?:(?=\\s|[[:punct:]])|$)");
auto regex = QRegularExpression(matchHyperlinks, QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption | QRegularExpression::MultilineOption);
regex.optimize();
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;
}