diff --git a/ui/text/text_entity.cpp b/ui/text/text_entity.cpp index 3c95679..96fa427 100644 --- a/ui/text/text_entity.cpp +++ b/ui/text/text_entity.cpp @@ -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); diff --git a/ui/widgets/input_fields.cpp b/ui/widgets/input_fields.cpp index 43a09b4..6e4cf08 100644 --- a/ui/widgets/input_fields.cpp +++ b/ui/widgets/input_fields.cpp @@ -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 #include @@ -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(); auto bg = std::optional(); + 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, diff --git a/ui/widgets/input_fields.h b/ui/widgets/input_fields.h index 01fd43e..736ea03 100644 --- a/ui/widgets/input_fields.h +++ b/ui/widgets/input_fields.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -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;