Support serializing / deserializing custom emoji as tags.

This commit is contained in:
John Preston 2022-06-30 13:22:03 +04:00
parent 32cf0968a1
commit e6b3951b40
3 changed files with 165 additions and 12 deletions

View file

@ -2052,7 +2052,9 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
const auto processState = [&](State nextState) {
const auto linkChanged = (nextState.link != state.link);
if (linkChanged) {
if (IsMentionLink(state.link)) {
if (Ui::InputField::IsCustomEmojiLink(state.link)) {
closeType(EntityType::CustomEmoji);
} else if (IsMentionLink(state.link)) {
closeType(EntityType::MentionName);
} else {
closeType(EntityType::CustomUrl);
@ -2064,7 +2066,13 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
}
}
if (linkChanged && !nextState.link.isEmpty()) {
if (IsMentionLink(nextState.link)) {
if (Ui::InputField::IsCustomEmojiLink(nextState.link)) {
const auto data = Ui::InputField::CustomEmojiEntityData(
nextState.link);
if (!data.isEmpty()) {
openType(EntityType::CustomEmoji, data);
}
} else if (IsMentionLink(nextState.link)) {
const auto match = qthelp::regex_match(
"^(\\d+\\.\\d+)(/|$)",
base::StringViewMid(nextState.link, kMentionTagStart.size()));
@ -2175,6 +2183,9 @@ TextWithTags::Tags ConvertEntitiesToTextTags(
push(url);
}
} break;
case EntityType::CustomEmoji: {
push(Ui::InputField::CustomEmojiLink(entity.data()));
} break;
case EntityType::Bold: push(Ui::InputField::kTagBold); break;
//case EntityType::Semibold: // Semibold is for UI parts only.
// push(Ui::InputField::kTagSemibold);

View file

@ -14,8 +14,9 @@
#include "base/random.h"
#include "base/platform/base_platform_info.h"
#include "emoji_suggestions_helper.h"
#include "styles/palette.h"
#include "base/qthelp_regex.h"
#include "base/qt/qt_common_adapters.h"
#include "styles/palette.h"
#include <QtWidgets/QCommonStyle>
#include <QtWidgets/QScrollBar>
@ -34,6 +35,9 @@ constexpr auto kInstantReplaceWhatId = QTextFormat::UserProperty + 1;
constexpr auto kInstantReplaceWithId = QTextFormat::UserProperty + 2;
constexpr auto kReplaceTagId = QTextFormat::UserProperty + 3;
constexpr auto kTagProperty = QTextFormat::UserProperty + 4;
constexpr auto kCustomEmojiFormat = QTextFormat::UserObject + 1;
constexpr auto kCustomEmojiText = QTextFormat::UserProperty + 5;
constexpr auto kCustomEmojiLink = QTextFormat::UserProperty + 6;
const auto kObjectReplacementCh = QChar(QChar::ObjectReplacementCharacter);
const auto kObjectReplacement = QString::fromRawData(
&kObjectReplacementCh,
@ -45,13 +49,17 @@ const auto &kTagStrikeOut = InputField::kTagStrikeOut;
const auto &kTagCode = InputField::kTagCode;
const auto &kTagPre = InputField::kTagPre;
const auto &kTagSpoiler = InputField::kTagSpoiler;
const auto kTagCheckLinkMeta = QString("^:/:/:^");
const auto kTagCheckLinkMeta = u"^:/:/:^"_q;
const auto kNewlineChars = QString("\r\n")
+ QChar(0xfdd0) // QTextBeginningOfFrame
+ QChar(0xfdd1) // QTextEndOfFrame
+ QChar(QChar::ParagraphSeparator)
+ QChar(QChar::LineSeparator);
// We need unique tags otherwise same custom emoji would join in a single
// QTextCharFormat with the same properties, including kCustomEmojiText.
auto GlobalCustomEmojiCounter = 0;
class InputDocument : public QTextDocument {
public:
InputDocument(QObject *parent, const style::InputField &st);
@ -105,6 +113,20 @@ bool IsNewline(QChar ch) {
return (link.indexOf('.') >= 0) || (link.indexOf(':') >= 0);
}
[[nodiscard]] bool IsCustomEmojiLink(QStringView link) {
return link.startsWith(Ui::InputField::kCustomEmojiTagStart);
}
[[nodiscard]] QString MakeUniqueCustomEmojiLink(QStringView link) {
if (!IsCustomEmojiLink(link)) {
return link.toString();
}
const auto index = link.indexOf('?');
return u"%1?%2"_q
.arg((index < 0) ? link : base::StringViewMid(link, 0, index))
.arg(++GlobalCustomEmojiCounter);
}
[[nodiscard]] QString CheckFullTextTag(
const TextWithTags &textWithTags,
const QString &tag) {
@ -703,8 +725,15 @@ QTextCharFormat PrepareTagFormat(
auto font = st.font;
auto color = std::optional<style::color>();
auto bg = std::optional<style::color>();
auto replaceWhat = QString();
auto replaceWith = QString();
const auto applyOne = [&](QStringView tag) {
if (IsValidMarkdownLink(tag)) {
if (IsCustomEmojiLink(tag)) {
replaceWhat = tag.toString();
replaceWith = MakeUniqueCustomEmojiLink(tag);
result.setObjectType(kCustomEmojiFormat);
result.setProperty(kCustomEmojiLink, replaceWith);
} else if (IsValidMarkdownLink(tag)) {
color = st::defaultTextPalette.linkFg;
} else if (tag == kTagBold) {
font = font->bold();
@ -726,7 +755,11 @@ QTextCharFormat PrepareTagFormat(
}
result.setFont(font);
result.setForeground(color.value_or(st.textFg));
result.setProperty(kTagProperty, tag);
result.setProperty(
kTagProperty,
(replaceWhat.isEmpty()
? tag
: std::move(tag).replace(replaceWhat, replaceWith)));
if (bg) {
result.setBackground(*bg);
}
@ -772,7 +805,6 @@ int ProcessInsertedTags(
QTextCursor c(document);
c.setPosition(tagFrom);
c.setPosition(tagTo, QTextCursor::KeepAnchor);
c.mergeCharFormat(PrepareTagFormat(st, tagId));
applyNoTagFrom = tagTo;
@ -824,6 +856,7 @@ struct FormattingAction {
enum class Type {
Invalid,
InsertEmoji,
InsertCustomEmoji,
TildeFont,
RemoveTag,
RemoveNewline,
@ -834,6 +867,8 @@ struct FormattingAction {
EmojiPtr emoji = nullptr;
bool isTilde = false;
QString tildeTag;
QString customEmojiText;
QString customEmojiLink;
int intervalStart = 0;
int intervalEnd = 0;
@ -850,6 +885,7 @@ const QString InputField::kTagStrikeOut = QStringLiteral("~~");
const QString InputField::kTagCode = QStringLiteral("`");
const QString InputField::kTagPre = QStringLiteral("```");
const QString InputField::kTagSpoiler = QStringLiteral("||");
const QString InputField::kCustomEmojiTagStart = u"custom-emoji://"_q;
class InputField::Inner final : public QTextEdit {
public:
@ -904,6 +940,20 @@ void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji) {
cursor.insertText(kObjectReplacement, format);
}
void InsertCustomEmojiAtCursor(
QTextCursor cursor,
const QString &text,
const QString &link) {
const auto currentFormat = cursor.charFormat();
auto format = QTextCharFormat();
format.setObjectType(kCustomEmojiFormat);
format.setProperty(kCustomEmojiText, text);
format.setProperty(kCustomEmojiLink, MakeUniqueCustomEmojiLink(link));
format.setVerticalAlignment(QTextCharFormat::AlignBottom);
ApplyTagFormat(format, currentFormat);
cursor.insertText(kObjectReplacement, format);
}
void InstantReplaces::add(const QString &what, const QString &with) {
auto node = &reverseMap;
for (auto i = what.end(), b = what.begin(); i != b;) {
@ -1265,6 +1315,30 @@ void FlatInput::onTextChange(const QString &text) {
Integration::Instance().textActionsUpdated();
}
CustomEmojiObject::CustomEmojiObject(QObject *parent) : QObject(parent) {
}
QSizeF CustomEmojiObject::intrinsicSize(
QTextDocument *doc,
int posInDocument,
const QTextFormat &format) {
const auto factor = style::DevicePixelRatio() * 1.;
const auto size = Emoji::GetSizeNormal() / factor;
const auto width = size + st::emojiPadding * 2.;
const auto font = format.toCharFormat().font();
const auto height = std::max(QFontMetrics(font).height() * 1., size);
return { width, height };
}
void CustomEmojiObject::drawObject(
QPainter *painter,
const QRectF &rect,
QTextDocument *doc,
int posInDocument,
const QTextFormat &format) {
painter->fillRect(rect, QColor(0, 128, 0, 128));
}
InputField::InputField(
QWidget *parent,
const style::InputField &st,
@ -1314,6 +1388,10 @@ InputField::InputField(
setAttribute(Qt::WA_OpaquePaintEvent);
}
_inner->document()->documentLayout()->registerHandler(
kCustomEmojiFormat,
new CustomEmojiObject(this));
_inner->setFont(_st.font->f);
_inner->setAlignment(_st.textAlign);
if (_mode == Mode::SingleLine) {
@ -1940,7 +2018,7 @@ QString InputField::getTextPart(
return emoji->text();
}
}
return QString();
return format.property(kCustomEmojiText).toString();
}();
auto text = [&] {
const auto result = fragment.text();
@ -2086,6 +2164,20 @@ void InputField::processFormatting(int insertPosition, int insertEnd) {
auto *textStart = fragmentText.constData();
auto *textEnd = textStart + fragmentText.size();
if (format.objectType() == kCustomEmojiFormat) {
if (fragmentText == kObjectReplacement) {
checkedTill = fragmentEnd;
continue;
}
action.type = ActionType::InsertCustomEmoji;
action.intervalStart = fragmentPosition;
action.intervalEnd = fragmentPosition
+ fragmentText.size();
action.customEmojiText = fragmentText;
action.customEmojiLink = format.property(
kCustomEmojiLink).toString();
}
const auto with = format.property(kInstantReplaceWithId);
if (with.isValid()) {
const auto string = with.toString();
@ -2205,8 +2297,16 @@ void InputField::processFormatting(int insertPosition, int insertEnd) {
auto cursor = QTextCursor(document);
cursor.setPosition(action.intervalStart);
cursor.setPosition(action.intervalEnd, QTextCursor::KeepAnchor);
if (action.type == ActionType::InsertEmoji) {
InsertEmojiAtCursor(cursor, action.emoji);
if (action.type == ActionType::InsertEmoji
|| action.type == ActionType::InsertCustomEmoji) {
if (action.type == ActionType::InsertEmoji) {
InsertEmojiAtCursor(cursor, action.emoji);
} else {
InsertCustomEmojiAtCursor(
cursor,
action.customEmojiText,
action.customEmojiLink);
}
insertPosition = action.intervalStart + 1;
if (insertEnd >= action.intervalEnd) {
insertEnd -= action.intervalEnd
@ -2858,7 +2958,7 @@ auto InputField::selectionEditLinkData(EditLinkSelection selection) const
kTagCheckLinkMeta)
: QString();
}();
const auto simple = EditLinkData {
const auto simple = EditLinkData{
selection.from,
selection.till,
QString()
@ -3348,6 +3448,23 @@ bool InputField::IsValidMarkdownLink(QStringView link) {
return ::Ui::IsValidMarkdownLink(link);
}
bool InputField::IsCustomEmojiLink(QStringView link) {
return ::Ui::IsCustomEmojiLink(link);
}
QString InputField::CustomEmojiLink(QStringView entityData) {
return MakeUniqueCustomEmojiLink(u"%1%2"_q
.arg(kCustomEmojiTagStart)
.arg(entityData));
}
QString InputField::CustomEmojiEntityData(QStringView link) {
const auto match = qthelp::regex_match(
"^(\\d+\\.\\d+/\\d+)(\\?|$)",
base::StringViewMid(link, kCustomEmojiTagStart.size()));
return match ? match->captured(1) : QString();
}
void InputField::commitMarkdownLinkEdit(
EditLinkSelection selection,
const QString &text,

View file

@ -15,6 +15,7 @@
#include <QContextMenuEvent>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QTextEdit>
#include <QtGui/QTextObjectInterface>
#include <QtCore/QTimer>
#include <rpl/variable.h>
@ -143,6 +144,26 @@ private:
};
class CustomEmojiObject : public QObject, public QTextObjectInterface {
Q_OBJECT
Q_INTERFACES(QTextObjectInterface)
public:
explicit CustomEmojiObject(QObject *parent);
QSizeF intrinsicSize(
QTextDocument *doc,
int posInDocument,
const QTextFormat &format) override;
void drawObject(
QPainter *painter,
const QRectF &rect,
QTextDocument *doc,
int posInDocument,
const QTextFormat &format) override;
};
class InputField : public RpWidget {
Q_OBJECT
@ -173,6 +194,7 @@ public:
static const QString kTagCode;
static const QString kTagPre;
static const QString kTagSpoiler;
static const QString kCustomEmojiTagStart;
InputField(
QWidget *parent,
@ -261,7 +283,10 @@ public:
EditLinkSelection selection,
const QString &text,
const QString &link);
static bool IsValidMarkdownLink(QStringView link);
[[nodiscard]] static bool IsValidMarkdownLink(QStringView link);
[[nodiscard]] static bool IsCustomEmojiLink(QStringView link);
[[nodiscard]] static QString CustomEmojiLink(QStringView entityData);
[[nodiscard]] static QString CustomEmojiEntityData(QStringView link);
const QString &getLastText() const {
return _lastTextWithTags.text;