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:
parent
4ca7e9b32d
commit
3b17134451
|
@ -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 }
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue