From 4b763a76df514c6f98bc588b66b8df070ec66d85 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 13 May 2018 18:14:02 +0300 Subject: [PATCH] Instant in-field emoji and text replaces. Fixes #4410. Fixes #522. --- .../calls/calls_emoji_fingerprint.cpp | 12 +- .../chat_helpers/emoji_suggestions_helper.h | 8 +- .../chat_helpers/message_field.cpp | 18 ++ Telegram/SourceFiles/codegen/emoji/data.cpp | 2 +- .../SourceFiles/codegen/emoji/generator.cpp | 18 ++ Telegram/SourceFiles/ui/text/text_entity.cpp | 49 --- .../SourceFiles/ui/widgets/input_fields.cpp | 295 ++++++++++++++---- .../SourceFiles/ui/widgets/input_fields.h | 16 + 8 files changed, 302 insertions(+), 116 deletions(-) diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp index 7dddb8582..aebf8efb0 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Calls { namespace { -ushort Data[] = { +const ushort Data[] = { 0xd83d, 0xde09, 0xd83d, 0xde0d, 0xd83d, 0xde1b, 0xd83d, 0xde2d, 0xd83d, 0xde31, 0xd83d, 0xde21, 0xd83d, 0xde0e, 0xd83d, 0xde34, 0xd83d, 0xde35, 0xd83d, 0xde08, 0xd83d, 0xde2c, 0xd83d, 0xde07, 0xd83d, 0xde0f, 0xd83d, 0xdc6e, 0xd83d, 0xdc77, 0xd83d, 0xdc82, 0xd83d, 0xdc76, 0xd83d, 0xdc68, @@ -69,7 +69,7 @@ ushort Data[] = { 0x0030, 0x20e3, 0xd83d, 0xdd1f, 0x2757, 0x2753, 0x2665, 0x2666, 0xd83d, 0xdcaf, 0xd83d, 0xdd17, 0xd83d, 0xdd31, 0xd83d, 0xdd34, 0xd83d, 0xdd35, 0xd83d, 0xdd36, 0xd83d, 0xdd37 }; -ushort Offsets[] = { +const ushort Offsets[] = { 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, @@ -119,7 +119,9 @@ std::vector ComputeEmojiFingerprint(not_null call) { for (auto index = 0; index != EmojiCount; ++index) { auto offset = Offsets[index]; auto size = Offsets[index + 1] - offset; - auto string = QString::fromRawData(reinterpret_cast(Data + offset), size); + auto string = QString::fromRawData( + reinterpret_cast(Data + offset), + size); auto emoji = Ui::Emoji::Find(string); Assert(emoji != nullptr); } @@ -131,7 +133,9 @@ std::vector ComputeEmojiFingerprint(not_null call) { auto index = value % EmojiCount; auto offset = Offsets[index]; auto size = Offsets[index + 1] - offset; - auto string = QString::fromRawData(reinterpret_cast(Data + offset), size); + auto string = QString::fromRawData( + reinterpret_cast(Data + offset), + size); auto emoji = Ui::Emoji::Find(string); Assert(emoji != nullptr); result.push_back(emoji); diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_helper.h b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_helper.h index f7df9bdeb..0b4b55796 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_helper.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_helper.h @@ -14,11 +14,15 @@ namespace Ui { namespace Emoji { inline utf16string QStringToUTF16(const QString &string) { - return utf16string(reinterpret_cast(string.constData()), string.size()); + return utf16string( + reinterpret_cast(string.constData()), + string.size()); } inline QString QStringFromUTF16(utf16string string) { - return QString::fromRawData(reinterpret_cast(string.data()), string.size()); + return QString::fromRawData( + reinterpret_cast(string.data()), + string.size()); } constexpr auto kSuggestionMaxLength = internal::kReplacementMaxLength; diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index d0d76938a..a2268935a 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qthelp_regex.h" #include "styles/style_history.h" #include "window/window_controller.h" +#include "emoji_suggestions_data.h" +#include "chat_helpers/emoji_suggestions_helper.h" #include "mainwindow.h" #include "auth_session.h" @@ -115,6 +117,22 @@ MessageField::MessageField(QWidget *parent, not_null contro setMaxHeight(st::historyComposeFieldMaxHeight); setTagMimeProcessor(std::make_unique()); + + addInstantReplace("--", QString(1, QChar(8212))); + addInstantReplace("<<", QString(1, QChar(171))); + addInstantReplace(">>", QString(1, QChar(187))); + const auto &replacements = Ui::Emoji::internal::GetAllReplacements(); + for (const auto &one : replacements) { + const auto with = Ui::Emoji::QStringFromUTF16(one.emoji); + const auto what = Ui::Emoji::QStringFromUTF16(one.replacement); + addInstantReplace(what, with); + } + const auto &pairs = Ui::Emoji::internal::GetReplacementPairs(); + for (const auto &[what, index] : pairs) { + const auto emoji = Ui::Emoji::internal::ByIndex(index); + Assert(emoji != nullptr); + addInstantReplace(what, emoji->text()); + } } bool MessageField::hasSendText() const { diff --git a/Telegram/SourceFiles/codegen/emoji/data.cpp b/Telegram/SourceFiles/codegen/emoji/data.cpp index a880621a3..e5407b58e 100644 --- a/Telegram/SourceFiles/codegen/emoji/data.cpp +++ b/Telegram/SourceFiles/codegen/emoji/data.cpp @@ -57,7 +57,7 @@ Replace Replaces[] = { { { 0xD83DDE22U }, ":'(" }, { { 0xD83DDE2DU }, ":_(" }, { { 0xD83DDE29U }, ":((" }, - { { 0xD83DDE28U }, ":o" }, +// { { 0xD83DDE28U }, ":o" }, // Conflicts with typing :ok... { { 0xD83DDE10U }, ":|" }, { { 0xD83DDE0CU }, "3-)" }, { { 0xD83DDE20U }, ">(" }, diff --git a/Telegram/SourceFiles/codegen/emoji/generator.cpp b/Telegram/SourceFiles/codegen/emoji/generator.cpp index a6b9e960e..610bbe3c7 100644 --- a/Telegram/SourceFiles/codegen/emoji/generator.cpp +++ b/Telegram/SourceFiles/codegen/emoji/generator.cpp @@ -335,6 +335,10 @@ EmojiPtr FindReplace(const QChar *start, const QChar *end, int *outLength) {\n\ return index ? &Items[index - 1] : nullptr;\n\ }\n\ \n\ +const std::vector> GetReplacementPairs() {\n\ + return ReplacementPairs;\n\ +}\n\ +\n\ EmojiPtr Find(const QChar *start, const QChar *end, int *outLength) {\n\ auto index = FindIndex(start, end, outLength);\n\ return index ? &Items[index - 1] : nullptr;\n\ @@ -389,6 +393,7 @@ inline bool IsReplaceEdge(const QChar *ch) {\n\ // return false;\n\ }\n\ \n\ +const std::vector> GetReplacementPairs();\n\ EmojiPtr FindReplace(const QChar *ch, const QChar *end, int *outLength = nullptr);\n\ \n"; header->popNamespace().stream() << "\ @@ -591,6 +596,14 @@ EmojiPack GetSection(Section section) {\n\ bool Generator::writeFindReplace() { source_->stream() << "\ \n\ +const std::vector> ReplacementPairs = {\n"; + for (const auto &[what, index] : data_.replaces) { + source_->stream() << "\ + { qsl(\"" << what << "\"), " << index << " },\n"; + } + source_->stream() << "\ +};\n\ +\n\ int FindReplaceIndex(const QChar *start, const QChar *end, int *outLength) {\n\ auto ch = start;\n\ \n"; @@ -783,6 +796,7 @@ struct Replacement {\n\ constexpr auto kReplacementMaxLength = " << maxLength << ";\n\ \n\ void InitReplacements();\n\ +const std::vector &GetAllReplacements();\n\ const std::vector *GetReplacements(utf16char first);\n\ utf16string GetReplacementEmoji(utf16string replacement);\n\ \n"; @@ -923,6 +937,10 @@ const std::vector *GetReplacements(utf16char first) {\n\ return (it == ReplacementsMap.cend()) ? nullptr : &it->second;\n\ }\n\ \n\ +const std::vector &GetAllReplacements() {\n\ + return Replacements;\n\ +}\n\ +\n\ utf16string GetReplacementEmoji(utf16string replacement) {\n\ auto code = internal::countChecksum(replacement.data(), replacement.size() * sizeof(utf16char));\n\ auto it = ReplacementsHash.find(code);\n\ diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp index f4f65d53e..17f425f71 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.cpp +++ b/Telegram/SourceFiles/ui/text/text_entity.cpp @@ -2153,47 +2153,6 @@ void MovePartAndGoForward(TextWithEntities &result, int &to, int &from, int coun from += count; } -void ReplaceStringWithChar(const QLatin1String &from, QChar to, TextWithEntities &result, bool checkSpace = false) { - Expects(from.size() > 1); - auto len = from.size(), s = result.text.size(), offset = 0, length = 0; - auto i = result.entities.begin(), e = result.entities.end(); - for (auto start = result.text.data(); offset < s;) { - auto nextOffset = result.text.indexOf(from, offset); - if (nextOffset < 0) { - MovePartAndGoForward(result, length, offset, s - offset); - break; - } - - if (checkSpace) { - bool spaceBefore = (nextOffset > 0) && (start + nextOffset - 1)->isSpace(); - bool spaceAfter = (nextOffset + len < s) && (start + nextOffset + len)->isSpace(); - if (!spaceBefore && !spaceAfter) { - MovePartAndGoForward(result, length, offset, nextOffset - offset + len + 1); - continue; - } - } - - auto skip = false; - for (; i != e; ++i) { // find and check next finishing entity - if (i->offset() + i->length() > nextOffset) { - skip = (i->offset() < nextOffset + len); - break; - } - } - if (skip) { - MovePartAndGoForward(result, length, offset, nextOffset - offset + len); - continue; - } - - MovePartAndGoForward(result, length, offset, nextOffset - offset); - - *(start + length) = to; - ++length; - offset += len; - } - if (length < s) result.text.resize(length); -} - void PrepareForSending(TextWithEntities &result, int32 flags) { ApplyServerCleaning(result); @@ -2201,14 +2160,6 @@ void PrepareForSending(TextWithEntities &result, int32 flags) { ParseEntities(result, flags); } - ReplaceStringWithChar(qstr("--"), QChar(8212), result, true); - ReplaceStringWithChar(qstr("<<"), QChar(171), result); - ReplaceStringWithChar(qstr(">>"), QChar(187), result); - - if (cReplaceEmojis()) { - Ui::Emoji::ReplaceInText(result); - } - Trim(result); } diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp index 42813eb9d..cb0370423 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp +++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp @@ -19,6 +19,13 @@ namespace Ui { namespace { constexpr auto kMaxUsernameLength = 32; +constexpr auto kInstantReplaceRandomId = QTextFormat::UserProperty; +constexpr auto kInstantReplaceWhatId = QTextFormat::UserProperty + 1; +constexpr auto kInstantReplaceWithId = QTextFormat::UserProperty + 2; +const auto kObjectReplacementCh = QChar(QChar::ObjectReplacementCharacter); +const auto kObjectReplacement = QString::fromRawData( + &kObjectReplacementCh, + 1); template class InputStyle : public QCommonStyle { @@ -61,6 +68,28 @@ private: template InputStyle *InputStyle::_instance = nullptr; +template +QString AccumulateText(Iterator begin, Iterator end) { + auto result = QString(); + result.reserve(end - begin); + for (auto i = end; i != begin;) { + result.push_back(*--i); + } + return result; +} + +QTextImageFormat PrepareEmojiFormat(EmojiPtr emoji, const style::font &f) { + const auto factor = cIntRetinaFactor(); + const auto width = Ui::Emoji::Size() + st::emojiPadding * factor * 2; + const auto height = f->height * factor; + auto result = QTextImageFormat(); + result.setWidth(width / factor); + result.setHeight(height / factor); + result.setName(emoji->toUrl()); + result.setVerticalAlignment(QTextCharFormat::AlignBaseline); + return result; +} + } // namespace QByteArray FlatTextarea::serializeTagsList(const TagList &tags) { @@ -122,6 +151,7 @@ FlatTextarea::FlatTextarea(QWidget *parent, const style::FlatTextarea &st, base: , _placeholderVisible(!v.length()) , _lastTextWithTags { v, tags } , _st(st) { + _defaultCharFormat = textCursor().charFormat(); setCursor(style::cur_text); setAcceptRichText(false); @@ -170,6 +200,17 @@ FlatTextarea::FlatTextarea(QWidget *parent, const style::FlatTextarea &st, base: } } +void FlatTextarea::addInstantReplace( + const QString &what, + const QString &with) { + auto node = &_reverseInstantReplaces; + for (const auto ch : base::reversed(what)) { + node = &node->tail.emplace(ch, InstantReplaceNode()).first->second; + } + node->text = with; + accumulate_max(_instantReplaceMaxLength, int(what.size())); +} + void FlatTextarea::updatePalette() { auto p = palette(); p.setColor(QPalette::Text, _st.textColor->c); @@ -472,7 +513,7 @@ QString FlatTextarea::getMentionHashtagBotCommandPart(bool &start) const { int32 p = fr.position(), e = (p + fr.length()); if (p >= pos || e < pos) continue; - QTextCharFormat f = fr.charFormat(); + const auto f = fr.charFormat(); if (f.isImageFormat()) continue; bool mentionInCommand = false; @@ -559,11 +600,7 @@ void FlatTextarea::insertTag(const QString &text, QString tagId) { break; } if (tagId.isEmpty()) { - QTextCharFormat format = cursor.charFormat(); - format.setAnchor(false); - format.setAnchorName(QString()); - format.clearForeground(); - cursor.insertText(text + ' ', format); + cursor.insertText(text + ' ', _defaultCharFormat); } else { _insertedTags.clear(); _insertedTags.push_back({ 0, text.size(), tagId }); @@ -597,15 +634,18 @@ void FlatTextarea::getSingleEmojiFragment(QString &text, QTextFragment &fragment continue; } - QTextCharFormat f = fr.charFormat(); - QString t(fr.text()); + const auto f = fr.charFormat(); + auto t = fr.text(); if (p < start) { t = t.mid(start - p, end - start); } else if (e > end) { t = t.mid(0, end - p); } - if (f.isImageFormat() && !t.isEmpty() && t.at(0).unicode() == QChar::ObjectReplacementCharacter) { - auto imageName = static_cast(&f)->name(); + if (f.isImageFormat() + && !t.isEmpty() + && t[0] == kObjectReplacementCh) { + const auto imageName = static_cast( + &f)->name(); if (Ui::Emoji::FromUrl(imageName)) { fragment = fr; text = t; @@ -743,9 +783,9 @@ QString FlatTextarea::getTextPart(int start, int end, TagList *outTagsList, bool tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); } - QTextCharFormat f = fragment.charFormat(); + const auto f = fragment.charFormat(); QString emojiText; - QString t(fragment.text()); + auto t = fragment.text(); if (!full) { if (p < start) { t = t.mid(start - p, end - start); @@ -767,8 +807,8 @@ QString FlatTextarea::getTextPart(int start, int end, TagList *outTagsList, bool } break; case QChar::ObjectReplacementCharacter: { if (emojiText.isEmpty() && f.isImageFormat()) { - auto imageName = static_cast(&f)->name(); - if (auto emoji = Ui::Emoji::FromUrl(imageName)) { + const auto imageName = static_cast(&f)->name(); + if (const auto emoji = Ui::Emoji::FromUrl(imageName)) { emojiText = emoji->text(); } } @@ -929,20 +969,13 @@ void FlatTextarea::insertFromMimeData(const QMimeData *source) { } void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) { - QTextImageFormat imageFormat; - auto ew = Ui::Emoji::Size() + st::emojiPadding * cIntRetinaFactor() * 2; - auto eh = _st.font->height * cIntRetinaFactor(); - imageFormat.setWidth(ew / cIntRetinaFactor()); - imageFormat.setHeight(eh / cIntRetinaFactor()); - imageFormat.setName(emoji->toUrl()); - imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline); + auto format = PrepareEmojiFormat(emoji, _st.font); if (c.charFormat().isAnchor()) { - imageFormat.setAnchor(true); - imageFormat.setAnchorName(c.charFormat().anchorName()); - imageFormat.setForeground(st::defaultTextPalette.linkFg); + format.setAnchor(true); + format.setAnchorName(c.charFormat().anchorName()); + format.setForeground(st::defaultTextPalette.linkFg); } - static QString objectReplacement(QChar::ObjectReplacementCharacter); - c.insertText(objectReplacement, imageFormat); + c.insertText(kObjectReplacement, format); } QVariant FlatTextarea::loadResource(int type, const QUrl &name) { @@ -1054,6 +1087,7 @@ struct FormattingAction { InsertEmoji, TildeFont, RemoveTag, + ClearInstantReplace, }; Type type = Type::Invalid; EmojiPtr emoji = nullptr; @@ -1104,18 +1138,33 @@ void FlatTextarea::processFormatting(int insertPosition, int insertEnd) { break; } - auto charFormat = fragment.charFormat(); + auto format = fragment.charFormat(); if (tildeFormatting) { - isTildeFragment = (charFormat.fontFamily() == tildeFixedFont); + isTildeFragment = (format.fontFamily() == tildeFixedFont); } auto fragmentText = fragment.text(); auto *textStart = fragmentText.constData(); auto *textEnd = textStart + fragmentText.size(); + const auto with = format.property(kInstantReplaceWithId); + if (with.isValid()) { + const auto string = with.toString(); + if (fragmentText != string) { + action.type = ActionType::ClearInstantReplace; + action.intervalStart = fragmentPosition + + (fragmentText.startsWith(string) + ? string.size() + : 0); + action.intervalEnd = fragmentPosition + + fragmentText.size(); + break; + } + } + if (!startTagFound) { startTagFound = true; - auto tagName = charFormat.anchorName(); + auto tagName = format.anchorName(); if (!tagName.isEmpty()) { breakTagOnNotLetter = wasInsertTillTheEndOfTag(block, fragmentIt, insertEnd); } @@ -1193,6 +1242,8 @@ void FlatTextarea::processFormatting(int insertPosition, int insertEnd) { format.setFontFamily(action.isTilde ? tildeFixedFont : tildeRegularFont); c.mergeCharFormat(format); insertPosition = action.intervalEnd; + } else if (action.type == ActionType::ClearInstantReplace) { + c.setCharFormat(_defaultCharFormat); } } else { break; @@ -1372,6 +1423,10 @@ void FlatTextarea::keyPressEvent(QKeyEvent *e) { start.movePosition(QTextCursor::StartOfLine); tc.setPosition(start.position(), QTextCursor::KeepAnchor); tc.removeSelectedText(); + } else if (e->key() == Qt::Key_Backspace + && e->modifiers() == 0 + && revertInstantReplace()) { + e->accept(); } else if (enter && enterSubmit) { emit submitted(ctrl && shift); } else if (e->key() == Qt::Key_Escape) { @@ -1394,39 +1449,174 @@ void FlatTextarea::keyPressEvent(QKeyEvent *e) { } #endif // Q_OS_MAC } else { - QTextCursor tc(textCursor()); + const auto text = e->text(); + const auto key = e->key(); + auto cursor = textCursor(); if (enter && ctrl) { e->setModifiers(e->modifiers() & ~Qt::ControlModifier); } bool spaceOrReturn = false; - QString t(e->text()); - if (!t.isEmpty() && t.size() < 3) { - if (t.at(0) == '\n' || t.at(0) == '\r' || t.at(0).isSpace() || t.at(0) == QChar::LineSeparator) { + if (!text.isEmpty() && text.size() < 3) { + const auto ch = text[0]; + if (ch == '\n' + || ch == '\r' + || ch.isSpace() + || ch == QChar::LineSeparator) { spaceOrReturn = true; } } QTextEdit::keyPressEvent(e); - if (tc == textCursor()) { + if (cursor == textCursor()) { bool check = false; - if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) { - tc.movePosition(QTextCursor::Start, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); + if (key == Qt::Key_PageUp || key == Qt::Key_Up) { + cursor.movePosition( + QTextCursor::Start, + (e->modifiers().testFlag(Qt::ShiftModifier) + ? QTextCursor::KeepAnchor + : QTextCursor::MoveAnchor)); check = true; - } else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_Down) { - tc.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); + } else if (key == Qt::Key_PageDown || key == Qt::Key_Down) { + cursor.movePosition( + QTextCursor::End, + (e->modifiers().testFlag(Qt::ShiftModifier) + ? QTextCursor::KeepAnchor + : QTextCursor::MoveAnchor)); check = true; } if (check) { - if (tc == textCursor()) { + if (cursor == textCursor()) { e->ignore(); } else { - setTextCursor(tc); + setTextCursor(cursor); } } } - if (spaceOrReturn) emit spacedReturnedPasted(); + processInstantReplaces(text); + if (spaceOrReturn) { + emit spacedReturnedPasted(); + } } } +void FlatTextarea::processInstantReplaces(const QString &text) { + if (text.size() != 1 || !_instantReplaceMaxLength) { + return; + } + const auto it = _reverseInstantReplaces.tail.find(text[0]); + if (it == end(_reverseInstantReplaces.tail)) { + return; + } + const auto position = textCursor().position(); + auto tags = QVector(); + const auto typed = getTextPart( + std::max(position - _instantReplaceMaxLength, 0), + position - 1, + &tags); + auto node = &it->second; + auto i = typed.size(); + do { + if (!node->text.isEmpty()) { + applyInstantReplace(typed.mid(i) + text, node->text); + return; + } else if (!i) { + return; + } + const auto it = node->tail.find(typed[--i]); + if (it == end(node->tail)) { + return; + } + node = &it->second; + } while (true); +} + +void FlatTextarea::applyInstantReplace( + const QString &what, + const QString &with) { + const auto length = int(what.size()); + const auto cursor = textCursor(); + const auto position = cursor.position(); + if (cursor.anchor() != position) { + return; + } else if (position < length) { + return; + } + auto tags = QVector(); + const auto original = getTextPart(position - length, position, &tags); + if (what.compare(original, Qt::CaseInsensitive) != 0) { + return; + } + + auto format = [&]() -> QTextCharFormat { + auto emojiLength = 0; + const auto emoji = Ui::Emoji::Find(with, &emojiLength); + if (!emoji || with.size() != emojiLength) { + return cursor.charFormat(); + } + const auto use = [&] { + if (!emoji->hasVariants()) { + return emoji; + } + const auto nonColored = emoji->nonColoredId(); + const auto it = cEmojiVariants().constFind(nonColored); + return (it != cEmojiVariants().cend()) + ? emoji->variant(it.value()) + : emoji; + }(); + return PrepareEmojiFormat(use, _st.font); + }(); + const auto replacement = format.isImageFormat() + ? kObjectReplacement + : with; + format.setProperty(kInstantReplaceWhatId, original); + format.setProperty(kInstantReplaceWithId, replacement); + format.setProperty(kInstantReplaceRandomId, rand_value()); + auto replaceCursor = cursor; + replaceCursor.setPosition(position - length); + replaceCursor.setPosition(position, QTextCursor::KeepAnchor); + replaceCursor.insertText( + replacement, + format); +} + +bool FlatTextarea::revertInstantReplace() { + const auto cursor = textCursor(); + const auto position = cursor.position(); + if (position <= 0 || cursor.anchor() != position) { + return false; + } + const auto inside = position - 1; + const auto block = document()->findBlock(inside); + if (block == document()->end()) { + return false; + } + for (auto i = block.begin(); !i.atEnd(); ++i) { + const auto fragment = i.fragment(); + const auto fragmentStart = fragment.position(); + const auto fragmentEnd = fragmentStart + fragment.length(); + if (fragmentEnd <= inside) { + continue; + } else if (fragmentStart > inside || fragmentEnd != position) { + return false; + } + const auto format = fragment.charFormat(); + const auto with = format.property(kInstantReplaceWithId); + if (!with.isValid()) { + return false; + } + const auto string = with.toString(); + if (fragment.text() != string) { + return false; + } + auto replaceCursor = cursor; + replaceCursor.setPosition(fragmentStart); + replaceCursor.setPosition(fragmentEnd, QTextCursor::KeepAnchor); + const auto what = format.property(kInstantReplaceWhatId).toString(); + replaceCursor.insertText(what, _defaultCharFormat); + return true; + } + return false; +} + void FlatTextarea::resizeEvent(QResizeEvent *e) { refreshPlaceholder(); QTextEdit::resizeEvent(e); @@ -2135,16 +2325,8 @@ bool InputArea::isRedoAvailable() const { } void InputArea::insertEmoji(EmojiPtr emoji, QTextCursor c) { - QTextImageFormat imageFormat; - auto ew = Ui::Emoji::Size() + st::emojiPadding * cIntRetinaFactor() * 2; - auto eh = _st.font->height * cIntRetinaFactor(); - imageFormat.setWidth(ew / cIntRetinaFactor()); - imageFormat.setHeight(eh / cIntRetinaFactor()); - imageFormat.setName(emoji->toUrl()); - imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline); - - static QString objectReplacement(QChar::ObjectReplacementCharacter); - c.insertText(objectReplacement, imageFormat); + const auto format = PrepareEmojiFormat(emoji, _st.font); + c.insertText(kObjectReplacement, format); } QVariant InputArea::Inner::loadResource(int type, const QUrl &name) { @@ -2905,15 +3087,8 @@ bool InputField::isRedoAvailable() const { } void InputField::insertEmoji(EmojiPtr emoji, QTextCursor c) { - QTextImageFormat imageFormat; - auto ew = Ui::Emoji::Size() + st::emojiPadding * cIntRetinaFactor() * 2, eh = _st.font->height * cIntRetinaFactor(); - imageFormat.setWidth(ew / cIntRetinaFactor()); - imageFormat.setHeight(eh / cIntRetinaFactor()); - imageFormat.setName(emoji->toUrl()); - imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline); - - static QString objectReplacement(QChar::ObjectReplacementCharacter); - c.insertText(objectReplacement, imageFormat); + const auto format = PrepareEmojiFormat(emoji, _st.font); + c.insertText(kObjectReplacement, format); } QVariant InputField::Inner::loadResource(int type, const QUrl &name) { diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.h b/Telegram/SourceFiles/ui/widgets/input_fields.h index c3d6bb3fa..b31f476b4 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.h +++ b/Telegram/SourceFiles/ui/widgets/input_fields.h @@ -32,6 +32,8 @@ public: void setMinHeight(int minHeight); void setMaxHeight(int maxHeight); + void addInstantReplace(const QString &what, const QString &with); + void setPlaceholder(base::lambda placeholderFactory, int afterSymbols = 0); void updatePlaceholder(); void finishPlaceholder(); @@ -142,6 +144,10 @@ protected: void checkContentHeight(); private: + struct InstantReplaceNode { + QString text; + std::map tail; + }; void updatePalette(); void refreshPlaceholder(); @@ -160,6 +166,10 @@ private: // Rule 4 applies only if we inserted chars not in the middle of a tag (but at the end). void processFormatting(int changedPosition, int changedEnd); + void processInstantReplaces(const QString &text); + void applyInstantReplace(const QString &what, const QString &with); + bool revertInstantReplace(); + bool heightAutoupdated(); int placeholderSkipWidth() const; @@ -215,6 +225,12 @@ private: friend bool operator!=(const LinkRange &a, const LinkRange &b); using LinkRanges = QVector; LinkRanges _links; + + QTextCharFormat _defaultCharFormat; + + int _instantReplaceMaxLength = 0; + InstantReplaceNode _reverseInstantReplaces; + }; inline bool operator==(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) {