Support serializing / deserializing custom emoji as tags.
This commit is contained in:
parent
32cf0968a1
commit
e6b3951b40
3 changed files with 165 additions and 12 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue