From 3e5f51f45a995e26b1367d89a884062531c33355 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 May 2016 20:33:48 +0300 Subject: [PATCH] Everywhere TextWithTags and TextWithEntities are used. Copy tags from messages to clipboard, to drag mime data. Sorting entities while processing (links, monospace, mentions). --- Telegram/SourceFiles/app.cpp | 4 +- Telegram/SourceFiles/boxes/photosendbox.cpp | 5 +- Telegram/SourceFiles/core/click_handler.cpp | 10 +- Telegram/SourceFiles/core/click_handler.h | 11 +- .../SourceFiles/core/click_handler_types.cpp | 53 +++-- .../SourceFiles/core/click_handler_types.h | 12 +- Telegram/SourceFiles/history.cpp | 206 ++++++++++-------- Telegram/SourceFiles/history.h | 82 ++++--- Telegram/SourceFiles/historywidget.cpp | 171 +++++++++------ Telegram/SourceFiles/historywidget.h | 4 +- Telegram/SourceFiles/mainwidget.cpp | 5 +- Telegram/SourceFiles/mediaview.cpp | 2 +- .../SourceFiles/overview/overview_layout.cpp | 6 +- Telegram/SourceFiles/ui/flatinput.cpp | 3 +- Telegram/SourceFiles/ui/flattextarea.cpp | 23 +- Telegram/SourceFiles/ui/text/text.cpp | 201 +++++++++-------- Telegram/SourceFiles/ui/text/text.h | 11 +- Telegram/SourceFiles/ui/text/text_entity.cpp | 85 ++++++-- Telegram/SourceFiles/ui/text/text_entity.h | 17 +- 19 files changed, 542 insertions(+), 369 deletions(-) diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp index 1d9f2082f..5c9ab777c 100644 --- a/Telegram/SourceFiles/app.cpp +++ b/Telegram/SourceFiles/app.cpp @@ -968,7 +968,9 @@ namespace { peerId = peerFromUser(m.vfrom_id); } if (HistoryItem *existing = App::histItemById(peerToChannel(peerId), m.vid.v)) { - existing->setText(qs(m.vmessage), m.has_entities() ? entitiesFromMTP(m.ventities.c_vector().v) : EntitiesInText()); + auto text = qs(m.vmessage); + auto entities = m.has_entities() ? entitiesFromMTP(m.ventities.c_vector().v) : EntitiesInText(); + existing->setText({ text, entities }); existing->updateMedia(m.has_media() ? (&m.vmedia) : nullptr); existing->setViewsCount(m.has_views() ? m.vviews.v : -1); existing->addToOverview(AddToOverviewNew); diff --git a/Telegram/SourceFiles/boxes/photosendbox.cpp b/Telegram/SourceFiles/boxes/photosendbox.cpp index 987295b9c..f3e6b47d0 100644 --- a/Telegram/SourceFiles/boxes/photosendbox.cpp +++ b/Telegram/SourceFiles/boxes/photosendbox.cpp @@ -414,7 +414,7 @@ EditCaptionBox::EditCaptionBox(HistoryItem *msg) : AbstractBox(st::boxWideWidth) image = doc->thumb; } break; } - caption = media->getCaption(); + caption = media->getCaption().text; } if ((!_animated && (dimensions.isEmpty() || doc)) || image->isNull()) { _animated = false; @@ -492,7 +492,8 @@ EditCaptionBox::EditCaptionBox(HistoryItem *msg) : AbstractBox(st::boxWideWidth) _field->setMaxLength(MaxPhotoCaption); _field->setCtrlEnterSubmit(CtrlEnterSubmitBoth); } else { - QString text = textApplyEntities(msg->originalText(), msg->originalEntities()); + auto original = msg->originalText(); + QString text = textApplyEntities(original.text, original.entities); _field = new InputArea(this, st::editTextArea, lang(lng_photo_caption), text); // _field->setMaxLength(MaxMessageSize); // entities can make text in input field larger but still valid _field->setCtrlEnterSubmit(cCtrlEnter() ? CtrlEnterSubmitCtrlEnter : CtrlEnterSubmitEnter); diff --git a/Telegram/SourceFiles/core/click_handler.cpp b/Telegram/SourceFiles/core/click_handler.cpp index 99b6b5a42..82cc2231c 100644 --- a/Telegram/SourceFiles/core/click_handler.cpp +++ b/Telegram/SourceFiles/core/click_handler.cpp @@ -66,6 +66,12 @@ QString ClickHandler::getExpandedLinkText(ExpandLinksMode mode, const QStringRef return QString(); } -EntityInText ClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - return EntityInText(EntityInTextInvalid, offset, 0); +TextWithEntities ClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + return { QString(), EntitiesInText() }; +} + +TextWithEntities ClickHandler::simpleTextWithEntity(const EntityInText &entity) const { + TextWithEntities result; + result.entities.push_back(entity); + return result; } diff --git a/Telegram/SourceFiles/core/click_handler.h b/Telegram/SourceFiles/core/click_handler.h index b40ff9b5d..c0199fac9 100644 --- a/Telegram/SourceFiles/core/click_handler.h +++ b/Telegram/SourceFiles/core/click_handler.h @@ -42,9 +42,9 @@ protected: }; class EntityInText; +struct TextWithEntities; class ClickHandler { public: - virtual ~ClickHandler() { } @@ -71,8 +71,7 @@ public: // This method returns empty string if just textPart should be used (nothing to expand). virtual QString getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const; - - virtual EntityInText getEntityInText(int offset, const QStringRef &textPart) const; + virtual TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const; // This method should be called on mouse over a click handler. // It returns true if the active handler was changed or false otherwise. @@ -152,8 +151,12 @@ public: } } -private: +protected: + // For click handlers like mention or hashtag in getExpandedLinkTextWithEntities() + // we return just an empty string ("use original string part") with single entity. + TextWithEntities simpleTextWithEntity(const EntityInText &entity) const; +private: static NeverFreedPointer _active; static NeverFreedPointer _pressed; static ClickHandlerHost *_activeHost; diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 3b5e82cad..05043abf2 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -75,18 +75,23 @@ void UrlClickHandler::doOpen(QString url) { } QString UrlClickHandler::getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const { - if (mode == ExpandLinksNone) { - return QString(); + QString result; + if (mode != ExpandLinksNone) { + result = _originalUrl; } - return _originalUrl; + return result; } -EntityInText UrlClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - auto u = _originalUrl; - if (isEmail(u)) { - return EntityInText(EntityInTextUrl, offset, u.size()); +TextWithEntities UrlClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + TextWithEntities result; + auto entityType = isEmail(_originalUrl) ? EntityInTextEmail : EntityInTextUrl; + int entityLength = textPart.size(); + if (mode != ExpandLinksNone) { + result.text = _originalUrl; + entityLength = _originalUrl.size(); } - return EntityInText(EntityInTextUrl, offset, u.size()); + result.entities.push_back({ entityType, entityOffset, entityLength }); + return result; } void HiddenUrlClickHandler::onClick(Qt::MouseButton button) const { @@ -102,14 +107,20 @@ void HiddenUrlClickHandler::onClick(Qt::MouseButton button) const { } QString HiddenUrlClickHandler::getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const { - if (mode != ExpandLinksAll) { - return QString(); + QString result; + if (mode == ExpandLinksAll) { + result = textPart.toString() + qsl(" (") + url() + ')'; } - return textPart.toString() + qsl(" (") + url() + ')'; + return result; } -EntityInText HiddenUrlClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - return EntityInText(EntityInTextCustomUrl, offset, textPart.size(), url()); +TextWithEntities HiddenUrlClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + TextWithEntities result; + result.entities.push_back({ EntityInTextCustomUrl, entityOffset, textPart.size(), url() }); + if (mode == ExpandLinksAll) { + result.text = textPart.toString() + qsl(" (") + url() + ')'; + } + return result; } QString MentionClickHandler::copyToClipboardContextItemText() const { @@ -122,8 +133,8 @@ void MentionClickHandler::onClick(Qt::MouseButton button) const { } } -EntityInText MentionClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - return EntityInText(EntityInTextMention, offset, textPart.size()); +TextWithEntities MentionClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + return simpleTextWithEntity({ EntityInTextMention, entityOffset, textPart.size() }); } void MentionNameClickHandler::onClick(Qt::MouseButton button) const { @@ -134,9 +145,9 @@ void MentionNameClickHandler::onClick(Qt::MouseButton button) const { } } -EntityInText MentionNameClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { +TextWithEntities MentionNameClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { auto data = QString::number(_userId) + '.' + QString::number(_accessHash); - return EntityInText(EntityInTextMentionName, offset, textPart.size(), data); + return simpleTextWithEntity({ EntityInTextMentionName, entityOffset, textPart.size(), data }); } QString MentionNameClickHandler::tooltip() const { @@ -159,8 +170,8 @@ void HashtagClickHandler::onClick(Qt::MouseButton button) const { } } -EntityInText HashtagClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - return EntityInText(EntityInTextHashtag, offset, textPart.size()); +TextWithEntities HashtagClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + return simpleTextWithEntity({ EntityInTextHashtag, entityOffset, textPart.size() }); } void BotCommandClickHandler::onClick(Qt::MouseButton button) const { @@ -180,6 +191,6 @@ void BotCommandClickHandler::onClick(Qt::MouseButton button) const { } } -EntityInText BotCommandClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - return EntityInText(EntityInTextHashtag, offset, textPart.size()); +TextWithEntities BotCommandClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + return simpleTextWithEntity({ EntityInTextHashtag, entityOffset, textPart.size() }); } diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 316b1f275..d25078278 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -70,7 +70,7 @@ public: } QString getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const override; - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; static void doOpen(QString url); void onClick(Qt::MouseButton button) const override { @@ -118,7 +118,7 @@ public: void onClick(Qt::MouseButton button) const override; QString getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const override; - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; }; @@ -135,7 +135,7 @@ public: QString copyToClipboardContextItemText() const override; - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; protected: QString url() const override { @@ -157,7 +157,7 @@ public: void onClick(Qt::MouseButton button) const override; - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; QString tooltip() const override; @@ -181,7 +181,7 @@ public: QString copyToClipboardContextItemText() const override; - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; protected: QString url() const override { @@ -204,7 +204,7 @@ public: return _cmd; } - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; protected: QString url() const override { diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 7907cdc2d..60d53120a 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -1032,7 +1032,7 @@ HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction, EntitiesInText entities; textParseEntities(text, _historyTextNoMonoOptions.flags, &entities); entities.push_front(EntityInText(EntityInTextItalic, 0, text.size())); - result = HistoryMessage::create(this, m.vid.v, m.vflags.v, m.vreply_to_msg_id.v, m.vvia_bot_id.v, date(m.vdate), m.vfrom_id.v, text, entities); + result = HistoryMessage::create(this, m.vid.v, m.vflags.v, m.vreply_to_msg_id.v, m.vvia_bot_id.v, date(m.vdate), m.vfrom_id.v, { text, entities }); } else if (badMedia) { result = HistoryService::create(this, m.vid.v, date(m.vdate), lang(lng_message_empty), m.vflags.v, m.has_from_id() ? m.vfrom_id.v : 0); } else { @@ -3095,22 +3095,21 @@ void HistoryItem::setId(MsgId newId) { } bool HistoryItem::canEdit(const QDateTime &cur) const { - int32 s = date.secsTo(cur); auto channel = _history->peer->asChannel(); if (!channel || id < 0 || date.secsTo(cur) >= Global::EditTimeLimit()) return false; - if (const HistoryMessage *msg = toHistoryMessage()) { + if (auto msg = toHistoryMessage()) { if (msg->Has() || msg->Has()) return false; if (HistoryMedia *media = msg->getMedia()) { - HistoryMediaType t = media->type(); - if (t != MediaTypePhoto && - t != MediaTypeVideo && - t != MediaTypeFile && - t != MediaTypeGif && - t != MediaTypeMusicFile && - t != MediaTypeVoiceFile && - t != MediaTypeWebPage) { + auto type = media->type(); + if (type != MediaTypePhoto && + type != MediaTypeVideo && + type != MediaTypeFile && + type != MediaTypeGif && + type != MediaTypeMusicFile && + type != MediaTypeVoiceFile && + type != MediaTypeWebPage) { return false; } } @@ -3304,18 +3303,24 @@ int32 gifMaxStatusWidth(DocumentData *document) { return result; } -QString captionedSelectedText(const QString &attachType, const Text &caption, TextSelection selection) { +TextWithEntities captionedSelectedText(const QString &attachType, const Text &caption, TextSelection selection) { if (selection != FullSelection) { - return caption.original(selection, ExpandLinksAll); + return caption.originalTextWithEntities(selection, ExpandLinksAll); } - QString result; - result.reserve(5 + attachType.size() + caption.length()); - result.append(qstr("[ ")).append(attachType).append(qstr(" ]")); + + TextWithEntities result, original; if (!caption.isEmpty()) { - result.append(qstr("\n")).append(caption.original(AllTextSelection)); + original = caption.originalTextWithEntities(AllTextSelection, ExpandLinksAll); + } + result.text.reserve(5 + attachType.size() + original.text.size()); + result.text.append(qstr("[ ")).append(attachType).append(qstr(" ]")); + if (!caption.isEmpty()) { + result.text.append(qstr("\n")); + appendTextWithEntities(result, std_::move(original)); } return result; } + } // namespace void HistoryFileMedia::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { @@ -3735,10 +3740,10 @@ void HistoryPhoto::detachFromParent() { } QString HistoryPhoto::inDialogsText() const { - return _caption.isEmpty() ? lang(lng_in_dlg_photo) : _caption.original(AllTextSelection, ExpandLinksNone); + return _caption.isEmpty() ? lang(lng_in_dlg_photo) : _caption.originalText(AllTextSelection, ExpandLinksNone); } -QString HistoryPhoto::selectedText(TextSelection selection) const { +TextWithEntities HistoryPhoto::selectedText(TextSelection selection) const { return captionedSelectedText(lang(lng_in_dlg_photo), _caption, selection); } @@ -3981,10 +3986,10 @@ void HistoryVideo::setStatusSize(int32 newSize) const { } QString HistoryVideo::inDialogsText() const { - return _caption.isEmpty() ? lang(lng_in_dlg_video) : _caption.original(AllTextSelection, ExpandLinksNone); + return _caption.isEmpty() ? lang(lng_in_dlg_video) : _caption.originalText(AllTextSelection, ExpandLinksNone); } -QString HistoryVideo::selectedText(TextSelection selection) const { +TextWithEntities HistoryVideo::selectedText(TextSelection selection) const { return captionedSelectedText(lang(lng_in_dlg_video), _caption, selection); } @@ -4472,13 +4477,13 @@ QString HistoryDocument::inDialogsText() const { } if (auto captioned = Get()) { if (!captioned->_caption.isEmpty()) { - result.append(' ').append(captioned->_caption.original(AllTextSelection, ExpandLinksNone)); + result.append(' ').append(captioned->_caption.originalText(AllTextSelection, ExpandLinksNone)); } } return result; } -QString HistoryDocument::selectedText(TextSelection selection) const { +TextWithEntities HistoryDocument::selectedText(TextSelection selection) const { const Text emptyCaption; const Text *caption = &emptyCaption; if (auto captioned = Get()) { @@ -4930,10 +4935,10 @@ HistoryTextState HistoryGif::getState(int x, int y, HistoryStateRequest request) } QString HistoryGif::inDialogsText() const { - return qsl("GIF") + (_caption.isEmpty() ? QString() : (' ' + _caption.original(AllTextSelection, ExpandLinksNone))); + return qsl("GIF") + (_caption.isEmpty() ? QString() : (' ' + _caption.originalText(AllTextSelection, ExpandLinksNone))); } -QString HistoryGif::selectedText(TextSelection selection) const { +TextWithEntities HistoryGif::selectedText(TextSelection selection) const { return captionedSelectedText(qsl("GIF"), _caption, selection); } @@ -5250,11 +5255,11 @@ QString HistorySticker::inDialogsText() const { return _emoji.isEmpty() ? lang(lng_in_dlg_sticker) : lng_in_dlg_sticker_emoji(lt_emoji, _emoji); } -QString HistorySticker::selectedText(TextSelection selection) const { +TextWithEntities HistorySticker::selectedText(TextSelection selection) const { if (selection != FullSelection) { - return QString(); + return TextWithEntities(); } - return qsl("[ ") + inDialogsText() + qsl(" ]"); + return { qsl("[ ") + inDialogsText() + qsl(" ]"), EntitiesInText() }; } void HistorySticker::attachToParent() { @@ -5435,11 +5440,11 @@ QString HistoryContact::inDialogsText() const { return lang(lng_in_dlg_contact); } -QString HistoryContact::selectedText(TextSelection selection) const { +TextWithEntities HistoryContact::selectedText(TextSelection selection) const { if (selection != FullSelection) { - return QString(); + return TextWithEntities(); } - return qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" ]\n") + _name.original() + '\n' + _phone; + return { qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" ]\n") + _name.originalText() + '\n' + _phone, EntitiesInText() }; } void HistoryContact::attachToParent() { @@ -5956,18 +5961,21 @@ QString HistoryWebPage::inDialogsText() const { return QString(); } -QString HistoryWebPage::selectedText(TextSelection selection) const { +TextWithEntities HistoryWebPage::selectedText(TextSelection selection) const { if (selection == FullSelection) { - return QString(); + return TextWithEntities(); } - auto titleResult = _title.original(selection, ExpandLinksAll); - auto descriptionResult = _description.original(toDescriptionSelection(selection), ExpandLinksAll); - if (titleResult.isEmpty()) { + auto titleResult = _title.originalTextWithEntities(selection, ExpandLinksAll); + auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection), ExpandLinksAll); + if (titleResult.text.isEmpty()) { return descriptionResult; - } else if (descriptionResult.isEmpty()) { + } else if (descriptionResult.text.isEmpty()) { return titleResult; } - return titleResult + '\n' + descriptionResult; + + titleResult.text += '\n'; + appendTextWithEntities(titleResult, std_::move(descriptionResult)); + return titleResult; } ImagePtr HistoryWebPage::replyPreview() { @@ -6399,24 +6407,31 @@ TextSelection HistoryLocation::adjustSelection(TextSelection selection, TextSele } QString HistoryLocation::inDialogsText() const { - return _title.isEmpty() ? lang(lng_maps_point) : _title.original(AllTextSelection); + return _title.isEmpty() ? lang(lng_maps_point) : _title.originalText(AllTextSelection); } -QString HistoryLocation::selectedText(TextSelection selection) const { +TextWithEntities HistoryLocation::selectedText(TextSelection selection) const { if (selection == FullSelection) { - auto result = qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"); + TextWithEntities result = { qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"), EntitiesInText() }; auto info = selectedText(AllTextSelection); - if (!info.isEmpty()) result.append(info).append('\n'); - return result + _link->dragText(); + if (!info.text.isEmpty()) { + appendTextWithEntities(result, std_::move(info)); + result.text.append('\n'); + } + result.text += _link->dragText(); + return result; } - auto titleResult = _title.original(selection); - auto descriptionResult = _description.original(toDescriptionSelection(selection)); - if (titleResult.isEmpty()) { + + auto titleResult = _title.originalTextWithEntities(selection); + auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection)); + if (titleResult.text.isEmpty()) { return descriptionResult; - } else if (descriptionResult.isEmpty()) { + } else if (descriptionResult.text.isEmpty()) { return titleResult; } - return titleResult + '\n' + descriptionResult; + titleResult.text += '\n'; + appendTextWithEntities(titleResult, std_::move(descriptionResult)); + return titleResult; } int32 HistoryLocation::fullWidth() const { @@ -6729,7 +6744,12 @@ HistoryMessage::HistoryMessage(History *history, const MTPDmessage &msg) QString text(textClean(qs(msg.vmessage))); initMedia(msg.has_media() ? (&msg.vmedia) : nullptr, text); - setText(text, msg.has_entities() ? entitiesFromMTP(msg.ventities.c_vector().v) : EntitiesInText()); + + TextWithEntities textWithEntities = { text, EntitiesInText() }; + if (msg.has_entities()) { + textWithEntities.entities = entitiesFromMTP(msg.ventities.c_vector().v); + } + setText(textWithEntities); } HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *fwd) @@ -6755,14 +6775,14 @@ HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags fl if (HistoryMedia *mediaOriginal = fwd->getMedia()) { _media.reset(mediaOriginal->clone(this)); } - setText(fwd->originalText(), fwd->originalEntities()); + setText(fwd->originalText()); } -HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const QString &msg, const EntitiesInText &entities) +HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const TextWithEntities &textWithEntities) : HistoryItem(history, id, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, MTPnullMarkup); - setText(msg, entities); + setText(textWithEntities); } HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) @@ -6770,7 +6790,7 @@ HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags createComponentsHelper(flags, replyTo, viaBotId, markup); initMediaFromDocument(doc, caption); - setText(QString(), EntitiesInText()); + setText(TextWithEntities()); } HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) @@ -6778,7 +6798,7 @@ HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags createComponentsHelper(flags, replyTo, viaBotId, markup); _media.reset(new HistoryPhoto(this, photo, caption)); - setText(QString(), EntitiesInText()); + setText(TextWithEntities()); } void HistoryMessage::createComponentsHelper(MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, const MTPReplyMarkup &markup) { @@ -7069,11 +7089,6 @@ void HistoryMessage::applyEdition(const MTPDmessage &message) { } } - EntitiesInText entities; - if (message.has_entities()) { - entities = entitiesFromMTP(message.ventities.c_vector().v); - } - if (message.has_edit_date()) { _flags |= MTPDmessage::Flag::f_edit_date; if (!Has()) { @@ -7083,7 +7098,11 @@ void HistoryMessage::applyEdition(const MTPDmessage &message) { initTime(); } - setText(qs(message.vmessage), entities); + TextWithEntities textWithEntities = { qs(message.vmessage), EntitiesInText() }; + if (message.has_entities()) { + textWithEntities.entities = entitiesFromMTP(message.ventities.c_vector().v); + } + setText(textWithEntities); setMedia(message.has_media() ? (&message.vmedia) : nullptr); setReplyMarkup(message.has_reply_markup() ? (&message.vreply_markup) : nullptr); setViewsCount(message.has_views() ? message.vviews.v : -1); @@ -7157,36 +7176,44 @@ void HistoryMessage::eraseFromOverview() { } } -QString HistoryMessage::selectedText(TextSelection selection) const { - QString result, textResult, mediaResult; +TextWithEntities HistoryMessage::selectedText(TextSelection selection) const { + TextWithEntities result, textResult, mediaResult; if (selection == FullSelection) { - textResult = _text.original(AllTextSelection, ExpandLinksAll); + textResult = _text.originalTextWithEntities(AllTextSelection, ExpandLinksAll); } else { - textResult = _text.original(selection, ExpandLinksAll); + textResult = _text.originalTextWithEntities(selection, ExpandLinksAll); } if (_media) { mediaResult = _media->selectedText(toMediaSelection(selection)); } - if (textResult.isEmpty()) { + if (textResult.text.isEmpty()) { result = mediaResult; - } else if (mediaResult.isEmpty()) { + } else if (mediaResult.text.isEmpty()) { result = textResult; } else { - result = textResult + qstr("\n\n") + mediaResult; + result.text = textResult.text + qstr("\n\n"); + result.entities = textResult.entities; + appendTextWithEntities(result, std_::move(mediaResult)); } if (auto fwd = Get()) { if (selection == FullSelection) { - QString fwdinfo = fwd->_text.original(AllTextSelection, ExpandLinksAll), wrapped; - wrapped.reserve(fwdinfo.size() + 4 + result.size()); - wrapped.append('[').append(fwdinfo).append(qsl("]\n")).append(result); + auto fwdinfo = fwd->_text.originalTextWithEntities(AllTextSelection, ExpandLinksAll); + TextWithEntities wrapped; + wrapped.text.reserve(fwdinfo.text.size() + 4 + result.text.size()); + wrapped.entities.reserve(fwdinfo.entities.size() + result.entities.size()); + wrapped.text.append('['); + appendTextWithEntities(wrapped, std_::move(fwdinfo)); + wrapped.text.append(qsl("]\n")); + appendTextWithEntities(wrapped, std_::move(result)); result = wrapped; } } if (auto reply = Get()) { if (selection == FullSelection && reply->replyToMsg) { - QString wrapped; - wrapped.reserve(lang(lng_in_reply_to).size() + reply->replyToMsg->author()->name.size() + 4 + result.size()); - wrapped.append('[').append(lang(lng_in_reply_to)).append(' ').append(reply->replyToMsg->author()->name).append(qsl("]\n")).append(result); + TextWithEntities wrapped; + wrapped.text.reserve(lang(lng_in_reply_to).size() + reply->replyToMsg->author()->name.size() + 4 + result.text.size()); + wrapped.text.append('[').append(lang(lng_in_reply_to)).append(' ').append(reply->replyToMsg->author()->name).append(qsl("]\n")); + appendTextWithEntities(wrapped, std_::move(result)); result = wrapped; } } @@ -7194,7 +7221,7 @@ QString HistoryMessage::selectedText(TextSelection selection) const { } QString HistoryMessage::inDialogsText() const { - return emptyText() ? (_media ? _media->inDialogsText() : QString()) : _text.original(AllTextSelection, ExpandLinksNone); + return emptyText() ? (_media ? _media->inDialogsText() : QString()) : _text.originalText(AllTextSelection, ExpandLinksNone); } HistoryMedia *HistoryMessage::getMedia() const { @@ -7222,16 +7249,16 @@ void HistoryMessage::setMedia(const MTPMessageMedia *media) { } } -void HistoryMessage::setText(const QString &text, const EntitiesInText &entities) { +void HistoryMessage::setText(const TextWithEntities &textWithEntities) { textstyleSet(&((out() && !isPost()) ? st::outTextStyle : st::inTextStyle)); if (_media && _media->isDisplayed()) { - _text.setMarkedText(st::msgFont, text, entities, itemTextOptions(this)); + _text.setMarkedText(st::msgFont, textWithEntities, itemTextOptions(this)); } else { - _text.setMarkedText(st::msgFont, text + skipBlock(), entities, itemTextOptions(this)); + _text.setMarkedText(st::msgFont, { textWithEntities.text + skipBlock(), textWithEntities.entities }, itemTextOptions(this)); } textstyleRestore(); - for_const (const auto &entity, entities) { + for_const (auto &entity, textWithEntities.entities) { auto type = entity.type(); if (type == EntityInTextUrl || type == EntityInTextCustomUrl || type == EntityInTextEmail) { _flags |= MTPDmessage_ClientFlag::f_has_text_links; @@ -7286,15 +7313,14 @@ void HistoryMessage::setReplyMarkup(const MTPReplyMarkup *markup) { } } -QString HistoryMessage::originalText() const { - return emptyText() ? QString() : _text.original(); +TextWithEntities HistoryMessage::originalText() const { + if (emptyText()) { + return { QString(), EntitiesInText() }; + } + return _text.originalTextWithEntities(); } -EntitiesInText HistoryMessage::originalEntities() const { - return emptyText() ? EntitiesInText() : _text.originalEntities(); -} - -bool HistoryMessage::textHasLinks() { +bool HistoryMessage::textHasLinks() const { return emptyText() ? false : _text.hasLinks(); } @@ -8114,7 +8140,7 @@ bool HistoryService::updatePinnedText(const QString *pfrom, QString *ptext) { case MediaTypeVoiceFile: mediaText = lang(lng_action_pinned_media_voice); break; } if (mediaText.isEmpty()) { - QString original = pinned->msg->originalText(); + QString original = pinned->msg->originalText().text; int32 cutat = 0, limit = PinnedMessageTextLimit, size = original.size(); for (; limit > 0;) { --limit; @@ -8192,12 +8218,12 @@ void HistoryService::countPositionAndSize(int32 &left, int32 &width) const { width = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); } -QString HistoryService::selectedText(TextSelection selection) const { - return _text.original((selection == FullSelection) ? AllTextSelection : selection); +TextWithEntities HistoryService::selectedText(TextSelection selection) const { + return _text.originalTextWithEntities((selection == FullSelection) ? AllTextSelection : selection); } QString HistoryService::inDialogsText() const { - return _text.original(AllTextSelection, ExpandLinksNone); + return _text.originalText(AllTextSelection, ExpandLinksNone); } QString HistoryService::inReplyText() const { @@ -8381,7 +8407,7 @@ void HistoryService::drawInDialog(Painter &p, const QRect &r, bool act, const Hi } QString HistoryService::notificationText() const { - QString msg = _text.original(); + QString msg = _text.originalText(); if (msg.size() > 0xFF) msg = msg.mid(0, 0xFF) + qsl("..."); return msg; } diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index fd61a44bc..7a8d8c7cc 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -1293,8 +1293,8 @@ public: } virtual void previousItemChanged(); - virtual QString selectedText(TextSelection selection) const { - return qsl("[-]"); + virtual TextWithEntities selectedText(TextSelection selection) const { + return { qsl("[-]"), EntitiesInText() }; } virtual QString inDialogsText() const { return qsl("-"); @@ -1302,6 +1302,9 @@ public: virtual QString inReplyText() const { return inDialogsText(); } + virtual TextWithEntities originalText() const { + return { QString(), EntitiesInText() }; + } virtual void drawInfo(Painter &p, int32 right, int32 bottom, int32 width, bool selected, InfoDisplayType type) const { } @@ -1363,15 +1366,9 @@ public: virtual HistoryMedia *getMedia() const { return nullptr; } - virtual void setText(const QString &text, const EntitiesInText &links) { + virtual void setText(const TextWithEntities &textWithEntities) { } - virtual QString originalText() const { - return QString(); - } - virtual EntitiesInText originalEntities() const { - return EntitiesInText(); - } - virtual bool textHasLinks() { + virtual bool textHasLinks() const { return false; } @@ -1663,7 +1660,7 @@ public: virtual HistoryMediaType type() const = 0; virtual QString inDialogsText() const = 0; - virtual QString selectedText(TextSelection selection) const = 0; + virtual TextWithEntities selectedText(TextSelection selection) const = 0; bool hasPoint(int x, int y) const { return (x >= 0 && y >= 0 && x < _width && y < _height); @@ -1750,8 +1747,8 @@ public: virtual ImagePtr replyPreview() { return ImagePtr(); } - virtual QString getCaption() const { - return QString(); + virtual TextWithEntities getCaption() const { + return TextWithEntities(); } virtual bool needsBubble() const = 0; virtual bool customInfoLayout() const = 0; @@ -1900,7 +1897,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; PhotoData *photo() const { return _data; @@ -1917,8 +1914,8 @@ public: } ImagePtr replyPreview() override; - QString getCaption() const override { - return _caption.original(); + TextWithEntities getCaption() const override { + return _caption.originalTextWithEntities(); } bool needsBubble() const override { if (!_caption.isEmpty()) { @@ -1980,7 +1977,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; DocumentData *getDocument() override { return _data; @@ -2000,8 +1997,8 @@ public: } ImagePtr replyPreview() override; - QString getCaption() const override { - return _caption.original(); + TextWithEntities getCaption() const override { + return _caption.originalTextWithEntities(); } bool needsBubble() const override { if (!_caption.isEmpty()) { @@ -2103,7 +2100,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; bool uploading() const override { return _data->uploading(); @@ -2124,11 +2121,11 @@ public: } ImagePtr replyPreview() override; - QString getCaption() const override { + TextWithEntities getCaption() const override { if (const HistoryDocumentCaptioned *captioned = Get()) { - return captioned->_caption.original(); + return captioned->_caption.originalTextWithEntities(); } - return QString(); + return TextWithEntities(); } bool needsBubble() const override { return true; @@ -2190,7 +2187,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; bool uploading() const override { return _data->uploading(); @@ -2217,8 +2214,8 @@ public: } ImagePtr replyPreview() override; - QString getCaption() const override { - return _caption.original(); + TextWithEntities getCaption() const override { + return _caption.originalTextWithEntities(); } bool needsBubble() const override { if (!_caption.isEmpty()) { @@ -2288,7 +2285,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; DocumentData *getDocument() override { return _data; @@ -2357,7 +2354,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; void attachToParent() override; void detachFromParent() override; @@ -2425,7 +2422,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override; void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override; @@ -2559,7 +2556,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; bool needsBubble() const override { if (!_title.isEmpty() || !_description.isEmpty()) { @@ -2613,8 +2610,8 @@ public: static HistoryMessage *create(History *history, MsgId msgId, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *fwd) { return _create(history, msgId, flags, date, from, fwd); } - static HistoryMessage *create(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const QString &msg, const EntitiesInText &entities) { - return _create(history, msgId, flags, replyTo, viaBotId, date, from, msg, entities); + static HistoryMessage *create(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const TextWithEntities &textWithEntities) { + return _create(history, msgId, flags, replyTo, viaBotId, date, from, textWithEntities); } static HistoryMessage *create(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) { return _create(history, msgId, flags, replyTo, viaBotId, date, from, doc, caption, markup); @@ -2685,13 +2682,12 @@ public: int32 addToOverview(AddToOverviewMethod method) override; void eraseFromOverview(); - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; QString inDialogsText() const override; HistoryMedia *getMedia() const override; - void setText(const QString &text, const EntitiesInText &entities) override; - QString originalText() const override; - EntitiesInText originalEntities() const override; - bool textHasLinks() override; + void setText(const TextWithEntities &textWithEntities) override; + TextWithEntities originalText() const override; + bool textHasLinks() const override; int32 infoWidth() const override { int32 result = _timeWidth; @@ -2753,7 +2749,7 @@ private: HistoryMessage(History *history, const MTPDmessage &msg); HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *fwd); // local forwarded - HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const QString &msg, const EntitiesInText &entities); // local message + HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const TextWithEntities &textWithEntities); // local message HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup); // local document HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup); // local photo friend class HistoryItemInstantiated; @@ -2902,7 +2898,7 @@ public: bool serviceMsg() const override { return true; } - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; QString inDialogsText() const override; QString inReplyText() const override; @@ -2939,8 +2935,8 @@ public: HistoryTextState getState(int x, int y, HistoryStateRequest request) const override; - QString selectedText(TextSelection selection) const override { - return QString(); + TextWithEntities selectedText(TextSelection selection) const override { + return { QString(), EntitiesInText() }; } HistoryItemType type() const override { return HistoryItemGroup; @@ -2989,8 +2985,8 @@ public: void draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const override; HistoryTextState getState(int x, int y, HistoryStateRequest request) const override; - QString selectedText(TextSelection selection) const override { - return QString(); + TextWithEntities selectedText(TextSelection selection) const override { + return { QString(), EntitiesInText() }; } HistoryItemType type() const override { return HistoryItemCollapse; diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 448dbf54b..14f0c9be5 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -39,6 +39,54 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "window/top_bar_widget.h" #include "playerwidget.h" +namespace { + +QString mimeTagFromTag(const QString &tagId) { + if (tagId.startsWith(qstr("mention://"))) { + return tagId + ':' + QString::number(MTP::authedId()); + } + return tagId; +} + +QMimeData *mimeDataFromTextWithEntities(const TextWithEntities &forClipboard) { + if (forClipboard.text.isEmpty()) { + return nullptr; + } + + auto result = new QMimeData(); + result->setText(forClipboard.text); + auto tags = textTagsFromEntities(forClipboard.entities); + if (!tags.isEmpty()) { + for (auto &tag : tags) { + tag.id = mimeTagFromTag(tag.id); + } + result->setData(FlatTextarea::tagsMimeType(), FlatTextarea::serializeTagsList(tags)); + } + return result; +} + +// For mention tags save and validate userId, ignore tags for different userId. +class FieldTagMimeProcessor : public FlatTextarea::TagMimeProcessor { +public: + QString mimeTagFromTag(const QString &tagId) override { + return ::mimeTagFromTag(tagId); + } + + QString tagFromMimeTag(const QString &mimeTag) override { + if (mimeTag.startsWith(qstr("mention://"))) { + auto match = QRegularExpression(":(\\d+)$").match(mimeTag); + if (!match.hasMatch() || match.capturedRef(1).toInt() != MTP::authedId()) { + return QString(); + } + return mimeTag.mid(0, mimeTag.size() - match.capturedLength()); + } + return mimeTag; + } + +}; + +} // namespace + // flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html HistoryInner::HistoryInner(HistoryWidget *historyWidget, ScrollArea *scroll, History *history) : TWidget(nullptr) @@ -704,24 +752,21 @@ void HistoryInner::onDragExec() { } } ClickHandlerPtr pressedHandler = ClickHandler::getPressed(); - QString sel; + TextWithEntities sel; QList urls; if (uponSelected) { sel = getSelectedText(); } else if (pressedHandler) { - sel = pressedHandler->dragText(); + sel = { pressedHandler->dragText(), EntitiesInText() }; //if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') { // urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o //} } - if (!sel.isEmpty()) { + if (auto mimeData = mimeDataFromTextWithEntities(sel)) { updateDragSelection(0, 0, false); _widget->noSelectingScroll(); QDrag *drag = new QDrag(App::wnd()); - QMimeData *mimeData = new QMimeData; - - mimeData->setText(sel); if (!urls.isEmpty()) mimeData->setUrls(urls); if (uponSelected && !_selected.isEmpty() && _selected.cbegin().value() == FullSelection && !Adaptive::OneColumn()) { mimeData->setData(qsl("application/x-td-forward-selected"), "1"); @@ -748,7 +793,7 @@ void HistoryInner::onDragExec() { } if (!forwardMimeType.isEmpty()) { QDrag *drag = new QDrag(App::wnd()); - QMimeData *mimeData = new QMimeData; + QMimeData *mimeData = new QMimeData(); mimeData->setData(forwardMimeType, "1"); if (DocumentData *document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) { @@ -1124,10 +1169,7 @@ void HistoryInner::onMenuDestroy(QObject *obj) { } void HistoryInner::copySelectedText() { - QString sel = getSelectedText(); - if (!sel.isEmpty()) { - QApplication::clipboard()->setText(sel); - } + setToClipboard(getSelectedText()); } void HistoryInner::copyContextUrl() { @@ -1217,9 +1259,12 @@ void HistoryInner::copyContextText() { return; } - QString contextMenuText = item->selectedText(FullSelection); - if (!contextMenuText.isEmpty()) { - QApplication::clipboard()->setText(contextMenuText); + setToClipboard(item->selectedText(FullSelection)); +} + +void HistoryInner::setToClipboard(const TextWithEntities &forClipboard) { + if (auto data = mimeDataFromTextWithEntities(forClipboard)) { + QApplication::clipboard()->setMimeData(data); } } @@ -1227,42 +1272,48 @@ void HistoryInner::resizeEvent(QResizeEvent *e) { onUpdateSelected(); } -QString HistoryInner::getSelectedText() const { +TextWithEntities HistoryInner::getSelectedText() const { SelectedItems sel = _selected; if (_dragAction == Selecting && _dragSelFrom && _dragSelTo) { applyDragSelection(&sel); } - if (sel.isEmpty()) return QString(); + if (sel.isEmpty()) { + return TextWithEntities(); + } if (sel.cbegin().value() != FullSelection) { return sel.cbegin().key()->selectedText(sel.cbegin().value()); } - int32 fullSize = 0, mtop = migratedTop(), htop = historyTop(); + int fullSize = 0; QString timeFormat(qsl(", [dd.MM.yy hh:mm]\n")); - QMap texts; - for (SelectedItems::const_iterator i = sel.cbegin(), e = sel.cend(); i != e; ++i) { + QMap texts; + for (auto i = sel.cbegin(), e = sel.cend(); i != e; ++i) { HistoryItem *item = i.key(); if (item->detached()) continue; - QString text, sel = item->selectedText(FullSelection), time = item->date.toString(timeFormat); - int32 size = item->author()->name.size() + time.size() + sel.size(); - text.reserve(size); + QString time = item->date.toString(timeFormat); + TextWithEntities part, unwrapped = item->selectedText(FullSelection); + int size = item->author()->name.size() + time.size() + unwrapped.text.size(); + part.text.reserve(size); - int32 y = itemTop(item); + int y = itemTop(item); if (y >= 0) { - texts.insert(y, text.append(item->author()->name).append(time).append(sel)); + part.text.append(item->author()->name).append(time); + appendTextWithEntities(part, std_::move(unwrapped)); + texts.insert(y, part); fullSize += size; } } - QString result, sep(qsl("\n\n")); - result.reserve(fullSize + (texts.size() - 1) * 2); - for (QMap::const_iterator i = texts.cbegin(), e = texts.cend(); i != e; ++i) { - result.append(i.value()); + TextWithEntities result; + auto sep = qsl("\n\n"); + result.text.reserve(fullSize + (texts.size() - 1) * sep.size()); + for (auto i = texts.begin(), e = texts.end(); i != e; ++i) { + appendTextWithEntities(result, std_::move(i.value())); if (i + 1 != e) { - result.append(sep); + result.text.append(sep); } } return result; @@ -2027,7 +2078,7 @@ QString HistoryInner::tooltipText() const { } else if (_dragCursorState == HistoryInForwardedCursorState && _dragAction == NoDrag) { if (App::hoveredItem()) { if (HistoryMessageForwarded *fwd = App::hoveredItem()->Get()) { - return fwd->_text.original(AllTextSelection, ExpandLinksNone); + return fwd->_text.originalText(AllTextSelection, ExpandLinksNone); } } } else if (ClickHandlerPtr lnk = ClickHandler::getActive()) { @@ -2679,7 +2730,7 @@ bool HistoryHider::offerPeer(PeerId peer) { } QString HistoryHider::offeredText() const { - return toText.original(); + return toText.originalText(); } bool HistoryHider::wasOffered() const { @@ -2772,32 +2823,6 @@ TextWithTags::Tags textTagsFromEntities(const EntitiesInText &entities) { return result; } -namespace { - -// For mention tags save and validate userId, ignore tags for different userId. -class FieldTagMimeProcessor : public FlatTextarea::TagMimeProcessor { -public: - QString mimeTagFromTag(const QString &tagId) override { - if (tagId.startsWith(qstr("mention://"))) { - return tagId + ':' + QString::number(MTP::authedId()); - } - return tagId; - } - - QString tagFromMimeTag(const QString &mimeTag) override { - if (mimeTag.startsWith(qstr("mention://"))) { - auto match = QRegularExpression(":(\\d+)$").match(mimeTag); - if (!match.hasMatch() || match.capturedRef(1).toInt() != MTP::authedId()) { - return QString(); - } - return mimeTag.mid(0, mimeTag.size() - match.capturedLength()); - } - return mimeTag; - } -}; - -} // namespace - HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent) , _fieldBarCancel(this, st::replyCancel) , _scroll(this, st::historyScroll, false) @@ -6258,8 +6283,8 @@ void HistoryWidget::onPhotoUploaded(const FullMsgId &newId, bool silent, const M if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - QString caption = item->getMedia() ? item->getMedia()->getCaption() : QString(); - hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedPhoto(file, MTP_string(caption)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); + auto caption = item->getMedia() ? item->getMedia()->getCaption() : TextWithEntities(); + hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedPhoto(file, MTP_string(caption.text)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); } } @@ -6310,8 +6335,8 @@ void HistoryWidget::onDocumentUploaded(const FullMsgId &newId, bool silent, cons if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - QString caption = item->getMedia() ? item->getMedia()->getCaption() : QString(); - hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedDocument(file, MTP_string(document->mime), _composeDocumentAttributes(document), MTP_string(caption)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); + auto caption = item->getMedia() ? item->getMedia()->getCaption() : TextWithEntities(); + hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedDocument(file, MTP_string(document->mime), _composeDocumentAttributes(document), MTP_string(caption.text)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); } } } @@ -6339,8 +6364,8 @@ void HistoryWidget::onThumbDocumentUploaded(const FullMsgId &newId, bool silent, if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - QString caption = item->getMedia() ? item->getMedia()->getCaption() : QString(); - hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedThumbDocument(file, thumb, MTP_string(document->mime), _composeDocumentAttributes(document), MTP_string(caption)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); + auto caption = item->getMedia() ? item->getMedia()->getCaption() : TextWithEntities(); + hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedThumbDocument(file, thumb, MTP_string(document->mime), _composeDocumentAttributes(document), MTP_string(caption.text)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); } } } @@ -7414,11 +7439,12 @@ void HistoryWidget::onEditMessage() { _history->clearMsgDraft(); } - auto originalText = to->originalText(); - auto originalEntities = to->originalEntities(); - TextWithTags original = { textApplyEntities(originalText, originalEntities), textTagsFromEntities(originalEntities) }; - MessageCursor cursor = { original.text.size(), original.text.size(), QFIXED_MAX }; - _history->setEditDraft(std_::make_unique(original, to->id, cursor, false)); + auto original = to->originalText(); + auto editText = textApplyEntities(original.text, original.entities); + auto editTags = textTagsFromEntities(original.entities); + TextWithTags editData = { editText, editTags }; + MessageCursor cursor = { editText.size(), editText.size(), QFIXED_MAX }; + _history->setEditDraft(std_::make_unique(editData, to->id, cursor, false)); applyDraft(false); _previewData = 0; @@ -7755,10 +7781,11 @@ void HistoryWidget::onCancel() { if (_inlineBotCancel) { onInlineBotCancel(); } else if (_editMsgId) { - auto originalText = _replyEditMsg ? _replyEditMsg->originalText() : QString(); - auto originalEntities = _replyEditMsg ? _replyEditMsg->originalEntities() : EntitiesInText(); - TextWithTags original = { textApplyEntities(originalText, originalEntities), textTagsFromEntities(originalEntities) }; - if (_replyEditMsg && original != _field.getTextWithTags()) { + auto original = _replyEditMsg ? _replyEditMsg->originalText() : TextWithEntities(); + auto editText = textApplyEntities(original.text, original.entities); + auto editTags = textTagsFromEntities(original.entities); + TextWithTags editData = { editText, editTags }; + if (_replyEditMsg && editData != _field.getTextWithTags()) { auto box = new ConfirmBox(lang(lng_cancel_edit_post_sure), lang(lng_cancel_edit_post_yes), st::defaultBoxButton, lang(lng_cancel_edit_post_no)); connect(box, SIGNAL(confirmed()), this, SLOT(onFieldBarCancel())); Ui::showLayer(box); diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index 76093067c..5ed237aa6 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -57,7 +57,7 @@ public: void keyPressEvent(QKeyEvent *e) override; void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false); - QString getSelectedText() const; + TextWithEntities getSelectedText() const; void dragActionStart(const QPoint &screenPos, Qt::MouseButton button = Qt::LeftButton); void dragActionUpdate(const QPoint &screenPos); @@ -144,6 +144,8 @@ private: HistoryItem *nextItem(HistoryItem *item); void updateDragSelection(HistoryItem *dragSelFrom, HistoryItem *dragSelTo, bool dragSelecting, bool force = false); + void setToClipboard(const TextWithEntities &forClipboard); + PeerData *_peer = nullptr; History *_migrated = nullptr; History *_history = nullptr; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index a724cec8f..53cf80b79 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -3947,8 +3947,9 @@ void MainWidget::feedUpdates(const MTPUpdates &updates, uint64 randomId) { feedUpdate(MTP_updateMessageID(d.vid, MTP_long(randomId))); // ignore real date if (peerId) { - if (HistoryItem *item = App::histItemById(peerToChannel(peerId), d.vid.v)) { - item->setText(text, d.has_entities() ? entitiesFromMTP(d.ventities.c_vector().v) : EntitiesInText()); + if (auto item = App::histItemById(peerToChannel(peerId), d.vid.v)) { + auto entities = d.has_entities() ? entitiesFromMTP(d.ventities.c_vector().v) : EntitiesInText(); + item->setText({ text, entities }); item->updateMedia(d.has_media() ? (&d.vmedia) : nullptr); item->addToOverview(AddToOverviewNew); } diff --git a/Telegram/SourceFiles/mediaview.cpp b/Telegram/SourceFiles/mediaview.cpp index 005e78a09..0c8c0b804 100644 --- a/Telegram/SourceFiles/mediaview.cpp +++ b/Telegram/SourceFiles/mediaview.cpp @@ -912,7 +912,7 @@ void MediaView::displayPhoto(PhotoData *photo, HistoryItem *item) { _caption = Text(); if (HistoryMessage *itemMsg = item ? item->toHistoryMessage() : nullptr) { if (HistoryPhoto *photoMsg = dynamic_cast(itemMsg->getMedia())) { - _caption.setText(st::mvCaptionFont, photoMsg->getCaption(), (item->author()->isUser() && item->author()->asUser()->botInfo) ? _captionBotOptions : _captionTextOptions); + _caption.setMarkedText(st::mvCaptionFont, photoMsg->getCaption(), (item->author()->isUser() && item->author()->asUser()->botInfo) ? _captionBotOptions : _captionTextOptions); } } diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index 77358278a..42c69b4cc 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -898,9 +898,11 @@ bool Document::updateStatusText() const { Link::Link(HistoryMedia *media, HistoryItem *parent) : ItemBase(parent) { AddComponents(Info::Bit()); - QString text = _parent->originalText(), mainUrl; - EntitiesInText entities = _parent->originalEntities(); + const auto textWithEntities = _parent->originalText(); + QString mainUrl; + auto text = textWithEntities.text; + auto &entities = textWithEntities.entities; int32 from = 0, till = text.size(), lnk = entities.size(); for_const (const auto &entity, entities) { auto type = entity.type(); diff --git a/Telegram/SourceFiles/ui/flatinput.cpp b/Telegram/SourceFiles/ui/flatinput.cpp index 0a3d90f02..1afe8650e 100644 --- a/Telegram/SourceFiles/ui/flatinput.cpp +++ b/Telegram/SourceFiles/ui/flatinput.cpp @@ -1011,7 +1011,8 @@ void InputArea::processDocumentContentsChange(int position, int charsAdded) { if (!_inner.document()->pageSize().isNull()) { _inner.document()->setPageSize(QSizeF(0, 0)); } - QTextCursor c(doc->docHandle(), replacePosition); + QTextCursor c(doc->docHandle(), 0); + c.setPosition(replacePosition); c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor); if (emoji) { insertEmoji(emoji, c); diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp index 9486b8dbf..ccababd19 100644 --- a/Telegram/SourceFiles/ui/flattextarea.cpp +++ b/Telegram/SourceFiles/ui/flattextarea.cpp @@ -463,7 +463,7 @@ void FlatTextarea::insertTag(const QString &text, QString tagId) { if (previousChar == '@' || previousChar == '#' || previousChar == '/') { if ((i == pos - fragmentPosition || (previousChar == '/' ? fragmentText.at(i).isLetterOrNumber() : fragmentText.at(i).isLetter()) || previousChar == '#') && (i < 2 || !(fragmentText.at(i - 2).isLetterOrNumber() || fragmentText.at(i - 2) == '_'))) { - cursor.setPosition(fragmentPosition + i - 1, QTextCursor::MoveAnchor); + cursor.setPosition(fragmentPosition + i - 1); int till = fragmentPosition + i; for (; (till < fragmentEnd) && (till - fragmentPosition - i + 1 < text.size()); ++till) { if (fragmentText.at(till - fragmentPosition).toLower() != text.at(till - fragmentPosition - i + 1).toLower()) { @@ -608,6 +608,13 @@ public: _currentStart = currentPosition; }; + void finish() { + if (_currentTag < _tags->size()) { + _tags->resize(_currentTag); + _changed = true; + } + } + private: FlatTextarea::TagList *_tags; bool _changed = false; @@ -709,6 +716,8 @@ QString FlatTextarea::getTextPart(int start, int end, TagList *outTagsList, bool result.chop(1); if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size()); + tagAccumulator.finish(); + if (outTagsChanged) { *outTagsChanged = tagAccumulator.changed(); } @@ -842,7 +851,8 @@ void FlatTextarea::insertFromMimeData(const QMimeData *source) { } else { _insertedTags.clear(); } - _realInsertPosition = textCursor().position(); + auto cursor = textCursor(); + _realInsertPosition = qMin(cursor.position(), cursor.anchor()); _realCharsAdded = text.size(); QTextEdit::insertFromMimeData(source); if (!_inDrop) { @@ -895,7 +905,8 @@ void prepareFormattingOptimization(QTextDocument *document) { } void removeTags(QTextDocument *document, int from, int end) { - QTextCursor c(document->docHandle(), from); + QTextCursor c(document->docHandle(), 0); + c.setPosition(from); c.setPosition(end, QTextCursor::KeepAnchor); QTextCharFormat format; @@ -923,7 +934,8 @@ int processInsertedTags(QTextDocument *document, int changedPosition, int change if (applyNoTagFrom < tagFrom) { removeTags(document, applyNoTagFrom, tagFrom); } - QTextCursor c(document->docHandle(), tagFrom); + QTextCursor c(document->docHandle(), 0); + c.setPosition(tagFrom); c.setPosition(tagTo, QTextCursor::KeepAnchor); QTextCharFormat format; @@ -1098,7 +1110,8 @@ void FlatTextarea::processFormatting(int insertPosition, int insertEnd) { if (action.type != ActionType::Invalid) { prepareFormattingOptimization(doc); - QTextCursor c(doc->docHandle(), action.intervalStart); + QTextCursor c(doc->docHandle(), 0); + c.setPosition(action.intervalStart); c.setPosition(action.intervalEnd, QTextCursor::KeepAnchor); if (action.type == ActionType::InsertEmoji) { insertEmoji(action.emoji, c); diff --git a/Telegram/SourceFiles/ui/text/text.cpp b/Telegram/SourceFiles/ui/text/text.cpp index 79b72b78f..f828a2f28 100644 --- a/Telegram/SourceFiles/ui/text/text.cpp +++ b/Telegram/SourceFiles/ui/text/text.cpp @@ -555,14 +555,15 @@ public: } parse(options); } - TextParser(Text *t, const QString &text, const EntitiesInText &preparsed, const TextParseOptions &options) : _t(t), - src(text), + TextParser(Text *t, const TextWithEntities &textWithEntities, const TextParseOptions &options) : _t(t), + src(textWithEntities.text), rich(options.flags & TextParseRichText), multiline(options.flags & TextParseMultiline), maxLnkIndex(0), flags(0), lnkIndex(0), stopAfterWidth(QFIXED_MAX) { + auto preparsed = textWithEntities.entities; if ((options.flags & TextParseLinks) && !preparsed.isEmpty()) { bool parseMentions = (options.flags & TextParseMentions); bool parseHashtags = (options.flags & TextParseHashtags); @@ -573,7 +574,7 @@ public: } else { int32 i = 0, l = preparsed.size(); entities.reserve(l); - const QChar s = text.size(); + const QChar s = src.size(); for (; i < l; ++i) { auto type = preparsed.at(i).type(); if (((type == EntityInTextMention || type == EntityInTextMentionName) && !parseMentions) || @@ -2497,7 +2498,7 @@ void Text::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { } } -void Text::setMarkedText(style::font font, const QString &text, const EntitiesInText &entities, const TextParseOptions &options) { +void Text::setMarkedText(style::font font, const TextWithEntities &textWithEntities, const TextParseOptions &options) { if (!_textStyle) initDefault(); _font = font; clear(); @@ -2532,7 +2533,7 @@ void Text::setMarkedText(style::font font, const QString &text, const EntitiesIn // newText.append("\n\n").append(text); // TextParser parser(this, newText, EntitiesInText(), options); - TextParser parser(this, text, entities, options); + TextParser parser(this, textWithEntities, options); } recountNaturalSize(true, options.dir); } @@ -2932,117 +2933,133 @@ TextSelection Text::adjustSelection(TextSelection selection, TextSelectType sele return { from, to }; } -QString Text::original(TextSelection selection, ExpandLinksMode mode) const { +template +void Text::enumerateText(TextSelection selection, AppendPartCallback appendPartCallback, ClickHandlerStartCallback clickHandlerStartCallback, ClickHandlerFinishCallback clickHandlerFinishCallback, FlagsChangeCallback flagsChangeCallback) const { if (isEmpty() || selection.empty()) { - return QString(); + return; } - QString result, emptyurl; - result.reserve(_text.size()); - - int32 lnkFrom = 0, lnkIndex = 0; - for (TextBlocks::const_iterator i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { - int32 blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex(); - int32 blockFrom = (i == e) ? _text.size() : (*i)->from(); - if (blockLnkIndex && !_links.at(blockLnkIndex - 1)) { // ignore empty links - blockLnkIndex = 0; - } - if (blockLnkIndex != lnkIndex) { - int32 rangeFrom = qMax(int32(selection.from), lnkFrom), rangeTo = qMin(blockFrom, int32(selection.to)); - if (lnkIndex && rangeTo > rangeFrom) { // write link - QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); - if (lnkFrom != rangeFrom || blockFrom != rangeTo) { - result += r; - } else { - QString expanded = _links.at(lnkIndex - 1)->getExpandedLinkText(mode, r); - if (expanded.isEmpty()) { - result += r; - } else { - result += expanded; - } - } - } - lnkIndex = blockLnkIndex; - lnkFrom = blockFrom; - } - if (i == e) break; - - TextBlockType type = (*i)->type(); - if (type == TextBlockTSkip) continue; - - if (!blockLnkIndex) { - int32 rangeFrom = qMax(selection.from, (*i)->from()), rangeTo = qMin(selection.to, uint16((*i)->from() + TextPainter::_blockLength(this, i, e))); - if (rangeTo > rangeFrom) { - result += _text.midRef(rangeFrom, rangeTo - rangeFrom); - } - } - } - return result; -} - -EntitiesInText Text::originalEntities() const { - EntitiesInText result; - int32 originalLength = 0, lnkStart = 0, italicStart = 0, boldStart = 0, codeStart = 0, preStart = 0; - int32 lnkFrom = 0, lnkIndex = 0, flags = 0; - for (TextBlocks::const_iterator i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { - int32 blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex(); - int32 blockFrom = (i == e) ? _text.size() : (*i)->from(); + int lnkIndex = 0; + uint16 lnkFrom = 0; + int32 flags = 0; + for (auto i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { + int blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex(); + uint16 blockFrom = (i == e) ? _text.size() : (*i)->from(); int32 blockFlags = (i == e) ? 0 : (*i)->flags(); - if (blockFlags != flags) { - if ((flags & TextBlockFItalic) && !(blockFlags & TextBlockFItalic)) { // write italic - result.push_back(EntityInText(EntityInTextItalic, italicStart, originalLength - italicStart)); - } else if ((blockFlags & TextBlockFItalic) && !(flags & TextBlockFItalic)) { - italicStart = originalLength; - } - if ((flags & TextBlockFSemibold) && !(blockFlags & TextBlockFSemibold)) { - result.push_back(EntityInText(EntityInTextBold, boldStart, originalLength - boldStart)); - } else if ((blockFlags & TextBlockFSemibold) && !(flags & TextBlockFSemibold)) { - boldStart = originalLength; - } - if ((flags & TextBlockFCode) && !(blockFlags & TextBlockFCode)) { - result.push_back(EntityInText(EntityInTextCode, codeStart, originalLength - codeStart)); - } else if ((blockFlags & TextBlockFCode) && !(flags & TextBlockFCode)) { - codeStart = originalLength; - } - if ((flags & TextBlockFPre) && !(blockFlags & TextBlockFPre)) { - result.push_back(EntityInText(EntityInTextPre, preStart, originalLength - preStart)); - } else if ((blockFlags & TextBlockFPre) && !(flags & TextBlockFPre)) { - preStart = originalLength; - } + + bool checkBlockFlags = (blockFrom >= selection.from) && (blockFrom <= selection.to); + if (checkBlockFlags && blockFlags != flags) { + flagsChangeCallback(flags, blockFlags); flags = blockFlags; } if (blockLnkIndex && !_links.at(blockLnkIndex - 1)) { // ignore empty links blockLnkIndex = 0; } if (blockLnkIndex != lnkIndex) { - int32 rangeFrom = lnkFrom, rangeTo = blockFrom; - if (lnkIndex && rangeTo > rangeFrom) { // write link - QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); - if (auto entity = _links.at(lnkIndex - 1)->getEntityInText(lnkStart, r)) { - result.push_back(entity); - originalLength += entity.length(); - } else { - originalLength += r.size(); + if (lnkIndex) { + auto rangeFrom = qMax(selection.from, lnkFrom); + auto rangeTo = qMin(blockFrom, selection.to); + if (rangeTo > rangeFrom) { // handle click handler + QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); + if (lnkFrom != rangeFrom || blockFrom != rangeTo) { + appendPartCallback(r); + } else { + clickHandlerFinishCallback(r, _links.at(lnkIndex - 1)); + } } } lnkIndex = blockLnkIndex; if (lnkIndex) { lnkFrom = blockFrom; - lnkStart = originalLength; + clickHandlerStartCallback(); } } - if (i == e) break; + if (i == e || blockFrom >= selection.to) break; - TextBlockType type = (*i)->type(); - if (type == TextBlockTSkip) continue; + if ((*i)->type() == TextBlockTSkip) continue; if (!blockLnkIndex) { - int32 rangeFrom = (*i)->from(), rangeTo = uint16((*i)->from() + TextPainter::_blockLength(this, i, e)); + auto rangeFrom = qMax(selection.from, blockFrom); + auto rangeTo = qMin(selection.to, uint16(blockFrom + TextPainter::_blockLength(this, i, e))); if (rangeTo > rangeFrom) { - originalLength += rangeTo - rangeFrom; + appendPartCallback(_text.midRef(rangeFrom, rangeTo - rangeFrom)); } } } +} + +TextWithEntities Text::originalTextWithEntities(TextSelection selection, ExpandLinksMode mode) const { + TextWithEntities result; + result.text.reserve(_text.size()); + + int lnkStart = 0, italicStart = 0, boldStart = 0, codeStart = 0, preStart = 0; + auto flagsChangeCallback = [&result, &italicStart, &boldStart, &codeStart, &preStart](int32 oldFlags, int32 newFlags) { + if ((oldFlags & TextBlockFItalic) && !(newFlags & TextBlockFItalic)) { // write italic + result.entities.push_back(EntityInText(EntityInTextItalic, italicStart, result.text.size() - italicStart)); + } else if ((newFlags & TextBlockFItalic) && !(oldFlags & TextBlockFItalic)) { + italicStart = result.text.size(); + } + if ((oldFlags & TextBlockFSemibold) && !(newFlags & TextBlockFSemibold)) { + result.entities.push_back(EntityInText(EntityInTextBold, boldStart, result.text.size() - boldStart)); + } else if ((newFlags & TextBlockFSemibold) && !(oldFlags & TextBlockFSemibold)) { + boldStart = result.text.size(); + } + if ((oldFlags & TextBlockFCode) && !(newFlags & TextBlockFCode)) { + result.entities.push_back(EntityInText(EntityInTextCode, codeStart, result.text.size() - codeStart)); + } else if ((newFlags & TextBlockFCode) && !(oldFlags & TextBlockFCode)) { + codeStart = result.text.size(); + } + if ((oldFlags & TextBlockFPre) && !(newFlags & TextBlockFPre)) { + result.entities.push_back(EntityInText(EntityInTextPre, preStart, result.text.size() - preStart)); + } else if ((newFlags & TextBlockFPre) && !(oldFlags & TextBlockFPre)) { + preStart = result.text.size(); + } + }; + auto clickHandlerStartCallback = [&result, &lnkStart]() { + lnkStart = result.text.size(); + }; + auto clickHandlerFinishCallback = [mode, &result, &lnkStart](const QStringRef &r, const ClickHandlerPtr &handler) { + auto expanded = handler->getExpandedLinkTextWithEntities(mode, lnkStart, r); + if (expanded.text.isEmpty()) { + result.text += r; + } else { + result.text += expanded.text; + } + if (!expanded.entities.isEmpty()) { + result.entities.append(expanded.entities); + } + }; + auto appendPartCallback = [&result](const QStringRef &r) { + result.text += r; + }; + + enumerateText(selection, appendPartCallback, clickHandlerStartCallback, clickHandlerFinishCallback, flagsChangeCallback); + + return result; +} + +QString Text::originalText(TextSelection selection, ExpandLinksMode mode) const { + QString result; + result.reserve(_text.size()); + + auto appendPartCallback = [&result](const QStringRef &r) { + result += r; + }; + auto clickHandlerStartCallback = []() { + }; + auto clickHandlerFinishCallback = [mode, &result](const QStringRef &r, const ClickHandlerPtr &handler) { + auto expanded = handler->getExpandedLinkText(mode, r); + if (expanded.isEmpty()) { + result += r; + } else { + result += expanded; + } + }; + auto flagsChangeCallback = [](int32 oldFlags, int32 newFlags) { + }; + + enumerateText(selection, appendPartCallback, clickHandlerStartCallback, clickHandlerFinishCallback, flagsChangeCallback); + return result; } diff --git a/Telegram/SourceFiles/ui/text/text.h b/Telegram/SourceFiles/ui/text/text.h index 691e076b5..d9ea6e798 100644 --- a/Telegram/SourceFiles/ui/text/text.h +++ b/Telegram/SourceFiles/ui/text/text.h @@ -97,7 +97,7 @@ public: int32 countHeight(int32 width) const; void setText(style::font font, const QString &text, const TextParseOptions &options = _defaultOptions); void setRichText(style::font font, const QString &text, TextParseOptions options = _defaultOptions, const TextCustomTagsMap &custom = TextCustomTagsMap()); - void setMarkedText(style::font font, const QString &text, const EntitiesInText &entities, const TextParseOptions &options = _defaultOptions); + void setMarkedText(style::font font, const TextWithEntities &textWithEntities, const TextParseOptions &options = _defaultOptions); void setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk); bool hasLinks() const; @@ -178,8 +178,9 @@ public: int length() const { return _text.size(); } - QString original(TextSelection selection = AllTextSelection, ExpandLinksMode mode = ExpandLinksShortened) const; - EntitiesInText originalEntities() const; + + TextWithEntities originalTextWithEntities(TextSelection selection = AllTextSelection, ExpandLinksMode mode = ExpandLinksShortened) const; + QString originalText(TextSelection selection = AllTextSelection, ExpandLinksMode mode = ExpandLinksShortened) const; bool lastDots(int32 dots, int32 maxdots = 3) { // hack for typing animation if (_text.size() < maxdots) return false; @@ -207,6 +208,10 @@ public: private: + // Template method for originalText(), originalTextWithEntities(). + template + void enumerateText(TextSelection selection, AppendPartCallback appendPartCallback, ClickHandlerStartCallback clickHandlerStartCallback, ClickHandlerFinishCallback clickHandlerFinishCallback, FlagsChangeCallback flagsChangeCallback) const; + void recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir = Qt::LayoutDirectionAuto); // clear() deletes all blocks and calls this method diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp index 7e4170cb6..d3a79d030 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.cpp +++ b/Telegram/SourceFiles/ui/text/text_entity.cpp @@ -1437,7 +1437,7 @@ MTPVector linksToMTP(const EntitiesInText &links, bool sending // Some code is duplicated in flattextarea.cpp! void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities, bool rich) { - EntitiesInText mono; + EntitiesInText result; bool withHashtags = (flags & TextParseHashtags); bool withMentions = (flags & TextParseMentions); @@ -1445,6 +1445,9 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities bool withMono = (flags & TextParseMono); if (withMono) { // parse mono entities (code and pre) + int existingEntityIndex = 0, existingEntitiesCount = inOutEntities->size(); + int existingEntityShiftLeft = 0; + QString newText; int32 offset = 0, matchOffset = offset, len = text.size(), commandOffset = rich ? 0 : len; @@ -1468,7 +1471,7 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities auto mCode = _reCode.match(text, matchOffset); if (!mPre.hasMatch() && !mCode.hasMatch()) break; - int32 preStart = mPre.hasMatch() ? mPre.capturedStart() : INT_MAX, + int preStart = mPre.hasMatch() ? mPre.capturedStart() : INT_MAX, preEnd = mPre.hasMatch() ? mPre.capturedEnd() : INT_MAX, codeStart = mCode.hasMatch() ? mCode.capturedStart() : INT_MAX, codeEnd = mCode.hasMatch() ? mCode.capturedEnd() : INT_MAX, @@ -1506,11 +1509,25 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities continue; } - if (newText.isEmpty()) newText.reserve(text.size()); - bool addNewlineBefore = false, addNewlineAfter = false; int32 outerStart = tagStart, outerEnd = tagEnd; int32 innerStart = tagStart + mTag.capturedLength(2), innerEnd = tagEnd - mTag.capturedLength(3); + + // Check if start or end sequences intersect any existing entity. + int intersectedEntityEnd = 0; + for_const (auto &entity, *inOutEntities) { + if (qMin(innerStart, entity.offset() + entity.length()) > qMax(outerStart, entity.offset()) || + qMin(outerEnd, entity.offset() + entity.length()) > qMax(innerEnd, entity.offset())) { + intersectedEntityEnd = entity.offset() + entity.length(); + break; + } + } + if (intersectedEntityEnd > 0) { + matchOffset = qMax(innerStart, intersectedEntityEnd); + continue; + } + + if (newText.isEmpty()) newText.reserve(text.size()); if (pre) { while (outerStart > 0 && chIsSpace(*(start + outerStart - 1), rich) && !chIsNewline(*(start + outerStart - 1))) { --outerStart; @@ -1540,14 +1557,27 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities } addNewlineAfter = (outerEnd < len && !chIsNewline(*(start + outerEnd))); } + + for (; existingEntityIndex < existingEntitiesCount && inOutEntities->at(existingEntityIndex).offset() < innerStart; ++existingEntityIndex) { + auto &entity = inOutEntities->at(existingEntityIndex); + result.push_back(entity); + result.back().shiftLeft(existingEntityShiftLeft); + } if (outerStart > offset) newText.append(start + offset, outerStart - offset); if (addNewlineBefore) newText.append('\n'); + existingEntityShiftLeft += (innerStart - outerStart) - (addNewlineBefore ? 1 : 0); - int32 tagLength = innerEnd - innerStart; - mono.push_back(EntityInText(pre ? EntityInTextPre : EntityInTextCode, newText.size(), tagLength)); + int entityStart = newText.size(), entityLength = innerEnd - innerStart; + result.push_back(EntityInText(pre ? EntityInTextPre : EntityInTextCode, entityStart, entityLength)); - newText.append(start + innerStart, tagLength); + for (; existingEntityIndex < existingEntitiesCount && inOutEntities->at(existingEntityIndex).offset() <= innerEnd; ++existingEntityIndex) { + auto &entity = inOutEntities->at(existingEntityIndex); + result.push_back(entity); + result.back().shiftLeft(existingEntityShiftLeft); + } + newText.append(start + innerStart, entityLength); if (addNewlineAfter) newText.append('\n'); + existingEntityShiftLeft += (outerEnd - innerEnd) - (addNewlineAfter ? 1 : 0); offset = matchOffset = outerEnd; } @@ -1555,8 +1585,19 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities newText.append(start + offset, len - offset); text = newText; } + if (!result.isEmpty()) { + for (; existingEntityIndex < existingEntitiesCount; ++existingEntityIndex) { + auto &entity = inOutEntities->at(existingEntityIndex); + result.push_back(entity); + result.back().shiftLeft(existingEntityShiftLeft); + } + *inOutEntities = result; + result = EntitiesInText(); + } } - int32 monoEntity = 0, monoCount = mono.size(), monoTill = 0; + + int existingEntityIndex = 0, existingEntitiesCount = inOutEntities->size(); + int existingEntityEnd = 0; initLinkSets(); int32 len = text.size(), commandOffset = rich ? 0 : len; @@ -1572,11 +1613,11 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities } } } - QRegularExpressionMatch mDomain = _reDomain.match(text, matchOffset); - QRegularExpressionMatch mExplicitDomain = _reExplicitDomain.match(text, matchOffset); - QRegularExpressionMatch mHashtag = withHashtags ? _reHashtag.match(text, matchOffset) : QRegularExpressionMatch(); - QRegularExpressionMatch mMention = withMentions ? _reMention.match(text, qMax(mentionSkip, matchOffset)) : QRegularExpressionMatch(); - QRegularExpressionMatch mBotCommand = withBotCommands ? _reBotCommand.match(text, matchOffset) : QRegularExpressionMatch(); + auto mDomain = _reDomain.match(text, matchOffset); + auto mExplicitDomain = _reExplicitDomain.match(text, matchOffset); + auto mHashtag = withHashtags ? _reHashtag.match(text, matchOffset) : QRegularExpressionMatch(); + auto mMention = withMentions ? _reMention.match(text, qMax(mentionSkip, matchOffset)) : QRegularExpressionMatch(); + auto mBotCommand = withBotCommands ? _reBotCommand.match(text, matchOffset) : QRegularExpressionMatch(); EntityInTextType lnkType = EntityInTextUrl; int32 lnkStart = 0, lnkLength = 0; @@ -1735,19 +1776,23 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities lnkLength = (p - start) - lnkStart; } } - for (; monoEntity < monoCount && mono[monoEntity].offset() <= lnkStart; ++monoEntity) { - monoTill = qMax(monoTill, mono[monoEntity].offset() + mono[monoEntity].length()); - inOutEntities->push_back(mono[monoEntity]); + for (; existingEntityIndex < existingEntitiesCount && inOutEntities->at(existingEntityIndex).offset() <= lnkStart; ++existingEntityIndex) { + auto &entity = inOutEntities->at(existingEntityIndex); + accumulate_max(existingEntityEnd, entity.offset() + entity.length()); + result.push_back(entity); } - if (lnkStart >= monoTill) { + if (lnkStart >= existingEntityEnd) { inOutEntities->push_back(EntityInText(lnkType, lnkStart, lnkLength)); } offset = matchOffset = lnkStart + lnkLength; } - for (; monoEntity < monoCount; ++monoEntity) { - monoTill = qMax(monoTill, mono[monoEntity].offset() + mono[monoEntity].length()); - inOutEntities->push_back(mono[monoEntity]); + if (!result.isEmpty()) { + for (; existingEntityIndex < existingEntitiesCount; ++existingEntityIndex) { + auto &entity = inOutEntities->at(existingEntityIndex); + result.push_back(entity); + } + *inOutEntities = result; } } diff --git a/Telegram/SourceFiles/ui/text/text_entity.h b/Telegram/SourceFiles/ui/text/text_entity.h index fe2f36c7f..ae9439ac3 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.h +++ b/Telegram/SourceFiles/ui/text/text_entity.h @@ -79,6 +79,9 @@ public: } } } + void shiftRight(int shift) { + _offset += shift; + } void updateTextEnd(int textEnd) { if (_offset > textEnd) { _offset = textEnd; @@ -108,7 +111,19 @@ private: QString _data; }; -typedef QList EntitiesInText; + +struct TextWithEntities { + QString text; + EntitiesInText entities; +}; +inline void appendTextWithEntities(TextWithEntities &to, TextWithEntities &&append) { + int entitiesShiftRight = to.text.size(); + for (auto &entity : append.entities) { + entity.shiftRight(entitiesShiftRight); + } + to.text += append.text; + to.entities += append.entities; +} // text preprocess QString textClean(const QString &text);