Mention names support added to FlatTextarea, messages.

Copy of mention names to clipboard done, pasting started.
This commit is contained in:
John Preston 2016-04-30 20:04:14 +03:00
parent b4bc515079
commit 21f462a77e
18 changed files with 584 additions and 227 deletions

View File

@ -213,6 +213,14 @@ private:
};
inline QString str_const_latin1_toString(const str_const &str) {
return QString::fromLatin1(str.c_str(), str.size());
}
inline QString str_const_utf8_toString(const str_const &str) {
return QString::fromUtf8(str.c_str(), str.size());
}
template <typename T>
inline void accumulate_max(T &a, const T &b) { if (a < b) a = b; }

View File

@ -126,6 +126,29 @@ EntityInText MentionClickHandler::getEntityInText(int offset, const QStringRef &
return EntityInText(EntityInTextMention, offset, textPart.size());
}
void MentionNameClickHandler::onClick(Qt::MouseButton button) const {
if (button == Qt::LeftButton || button == Qt::MiddleButton) {
if (auto user = App::userLoaded(_userId)) {
Ui::showPeerProfile(user);
}
}
}
EntityInText MentionNameClickHandler::getEntityInText(int offset, const QStringRef &textPart) const {
auto data = QString::number(_userId) + '.' + QString::number(_accessHash);
return EntityInText(EntityInTextMentionName, offset, textPart.size(), data);
}
QString MentionNameClickHandler::tooltip() const {
if (auto user = App::userLoaded(_userId)) {
auto name = App::peerName(user);
if (name != _text) {
return name;
}
}
return QString();
}
QString HashtagClickHandler::copyToClipboardContextItemText() const {
return lang(lng_context_copy_hashtag);
}

View File

@ -147,6 +147,27 @@ private:
};
class MentionNameClickHandler : public ClickHandler {
public:
MentionNameClickHandler(QString text, UserId userId, uint64 accessHash)
: _text(text)
, _userId(userId)
, _accessHash(accessHash) {
}
void onClick(Qt::MouseButton button) const override;
EntityInText getEntityInText(int offset, const QStringRef &textPart) const override;
QString tooltip() const override;
private:
QString _text;
UserId _userId;
uint64 _accessHash;
};
class HashtagClickHandler : public TextClickHandler {
public:
HashtagClickHandler(const QString &tag) : _tag(tag) {

View File

@ -24,7 +24,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
#define BETA_VERSION_MACRO (0ULL)
static constexpr int AppVersion = 9046;
static constexpr str_const AppVersionStr = "0.9.46";
static constexpr bool AppAlphaVersion = true;
static constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO;
constexpr int AppVersion = 9046;
constexpr str_const AppVersionStr = "0.9.46";
constexpr bool AppAlphaVersion = true;
constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO;

View File

@ -236,6 +236,10 @@ void autoplayMediaInlineAsync(const FullMsgId &msgId) {
}
}
void showPeerProfile(const PeerId &peer) {
if (MainWidget *m = App::main()) m->showPeerProfile(App::peer(peer));
}
void showPeerHistory(const PeerId &peer, MsgId msgId, bool back) {
if (MainWidget *m = App::main()) m->ui_showPeerHistory(peer, msgId, back);
}

View File

@ -67,6 +67,14 @@ void repaintInlineItem(const InlineBots::Layout::ItemBase *layout);
bool isInlineItemVisible(const InlineBots::Layout::ItemBase *reader);
void autoplayMediaInlineAsync(const FullMsgId &msgId);
void showPeerProfile(const PeerId &peer);
inline void showPeerProfile(const PeerData *peer) {
showPeerProfile(peer->id);
}
inline void showPeerProfile(const History *history) {
showPeerProfile(history->peer->id);
}
void showPeerHistory(const PeerId &peer, MsgId msgId, bool back = false);
inline void showPeerHistory(const PeerData *peer, MsgId msgId, bool back = false) {
showPeerHistory(peer->id, msgId, back);

View File

@ -1029,7 +1029,8 @@ HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction,
}
if (badMedia == 1) {
QString text(lng_message_unsupported(lt_link, qsl("https://desktop.telegram.org")));
EntitiesInText entities = textParseEntities(text, _historyTextNoMonoOptions.flags);
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);
} else if (badMedia) {

View File

@ -2735,6 +2735,25 @@ QPoint SilentToggle::tooltipPos() const {
return QCursor::pos();
}
EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) {
EntitiesInText result;
if (tags.isEmpty()) {
return result;
}
result.reserve(tags.size());
auto mentionStart = qstr("mention://user.");
for_const (auto &tag, tags) {
if (tag.id.startsWith(mentionStart)) {
auto match = QRegularExpression("^(\\d+\\.\\d+)(/|$)").match(tag.id.midRef(mentionStart.size()));
if (match.hasMatch()) {
result.push_back(EntityInText(EntityInTextMentionName, tag.offset, tag.length, match.captured(1)));
}
}
}
return result;
}
HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent)
, _fieldBarCancel(this, st::replyCancel)
, _scroll(this, st::historyScroll, false)
@ -2917,7 +2936,7 @@ void HistoryWidget::onMentionInsert(UserData *user) {
QString replacement, entityTag;
if (user->username.isEmpty()) {
replacement = App::peerName(user);
entityTag = qsl("mention://peer.") + QString::number(user->id);
entityTag = qsl("mention://user.") + QString::number(user->bareId()) + '.' + QString::number(user->access);
} else {
replacement = '@' + user->username;
}
@ -4737,8 +4756,11 @@ void HistoryWidget::saveEditMsg() {
WebPageId webPageId = _previewCancelled ? CancelledWebPageId : ((_previewData && _previewData->pendingTill >= 0) ? _previewData->id : 0);
EntitiesInText sendingEntities, leftEntities;
QString sendingText, leftText = prepareTextWithEntities(_field.getLastText(), leftEntities, itemTextOptions(_history, App::self()).flags);
auto fieldText = _field.getLastText();
auto fieldTags = _field.getLastTags();
auto prepareFlags = itemTextOptions(_history, App::self()).flags;
EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(fieldTags);
QString sendingText, leftText = prepareTextWithEntities(fieldText, prepareFlags, &leftEntities);
if (!textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) {
_field.selectAll();
@ -4814,7 +4836,15 @@ void HistoryWidget::onSend(bool ctrlShiftEnter, MsgId replyTo) {
WebPageId webPageId = _previewCancelled ? CancelledWebPageId : ((_previewData && _previewData->pendingTill >= 0) ? _previewData->id : 0);
App::main()->sendMessage(_history, _field.getLastText(), replyTo, _broadcast.checked(), _silent.checked(), webPageId);
MainWidget::MessageToSend message;
message.history = _history;
message.text = _field.getLastText();
message.entities = _field.getLastTags();
message.replyTo = replyTo;
message.broadcast = _broadcast.checked();
message.silent = _silent.checked();
message.webPageId = webPageId;
App::main()->sendMessage(message);
clearFieldText();
_saveDraftText = true;
@ -5320,7 +5350,13 @@ void HistoryWidget::sendBotCommand(PeerData *peer, UserData *bot, const QString
toSend += '@' + username;
}
App::main()->sendMessage(_history, toSend, replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? replyTo : -1) : 0, false, false);
MainWidget::MessageToSend message;
message.history = _history;
message.text = toSend;
message.replyTo = replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? replyTo : -1) : 0;
message.broadcast = false;
message.silent = false;
App::main()->sendMessage(message);
if (replyTo) {
if (_replyToId == replyTo) {
cancelReply();

View File

@ -486,6 +486,8 @@ public:
};
EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags);
enum TextUpdateEventsFlags {
TextUpdateEventsSaveDraft = 0x01,
TextUpdateEventsSendTyping = 0x02,

View File

@ -1084,50 +1084,54 @@ void executeParsedCommand(const QString &command) {
}
} // namespace
void MainWidget::sendMessage(History *hist, const QString &text, MsgId replyTo, bool broadcast, bool silent, WebPageId webPageId) {
readServerHistory(hist, false);
_history->fastShowAtEnd(hist);
void MainWidget::sendMessage(const MessageToSend &message) {
auto history = message.history;
const auto &text = message.text;
if (!hist || !_history->canSendMessages(hist->peer)) {
readServerHistory(history, false);
_history->fastShowAtEnd(history);
if (!history || !_history->canSendMessages(history->peer)) {
return;
}
saveRecentHashtags(text);
EntitiesInText sendingEntities, leftEntities;
QString sendingText, leftText = prepareTextWithEntities(text, leftEntities, itemTextOptions(hist, App::self()).flags);
EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(message.entities);
auto prepareFlags = itemTextOptions(history, App::self()).flags;
QString sendingText, leftText = prepareTextWithEntities(text, prepareFlags, &leftEntities);
QString command = parseCommandFromMessage(hist, text);
QString command = parseCommandFromMessage(history, text);
HistoryItem *lastMessage = nullptr;
if (replyTo < 0) replyTo = _history->replyToId();
MsgId replyTo = (message.replyTo < 0) ? _history->replyToId() : 0;
while (command.isEmpty() && textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) {
FullMsgId newId(peerToChannel(hist->peer->id), clientMsgId());
FullMsgId newId(peerToChannel(history->peer->id), clientMsgId());
uint64 randomId = rand_value<uint64>();
trimTextWithEntities(sendingText, sendingEntities);
trimTextWithEntities(sendingText, &sendingEntities);
App::historyRegRandom(randomId, newId);
App::historyRegSentData(randomId, hist->peer->id, sendingText);
App::historyRegSentData(randomId, history->peer->id, sendingText);
MTPstring msgText(MTP_string(sendingText));
MTPDmessage::Flags flags = newMessageFlags(hist->peer) | MTPDmessage::Flag::f_entities; // unread, out
MTPDmessage::Flags flags = newMessageFlags(history->peer) | MTPDmessage::Flag::f_entities; // unread, out
MTPmessages_SendMessage::Flags sendFlags = 0;
if (replyTo) {
flags |= MTPDmessage::Flag::f_reply_to_msg_id;
sendFlags |= MTPmessages_SendMessage::Flag::f_reply_to_msg_id;
}
MTPMessageMedia media = MTP_messageMediaEmpty();
if (webPageId == CancelledWebPageId) {
if (message.webPageId == CancelledWebPageId) {
sendFlags |= MTPmessages_SendMessage::Flag::f_no_webpage;
} else if (webPageId) {
WebPageData *page = App::webPage(webPageId);
} else if (message.webPageId) {
WebPageData *page = App::webPage(message.webPageId);
media = MTP_messageMediaWebPage(MTP_webPagePending(MTP_long(page->id), MTP_int(page->pendingTill)));
flags |= MTPDmessage::Flag::f_media;
}
bool channelPost = hist->peer->isChannel() && !hist->peer->isMegagroup() && hist->peer->asChannel()->canPublish() && (hist->peer->asChannel()->isBroadcast() || broadcast);
bool showFromName = !channelPost || hist->peer->asChannel()->addsSignature();
bool silentPost = channelPost && silent;
bool channelPost = history->peer->isChannel() && !history->peer->isMegagroup() && history->peer->asChannel()->canPublish() && (history->peer->asChannel()->isBroadcast() || message.broadcast);
bool showFromName = !channelPost || history->peer->asChannel()->addsSignature();
bool silentPost = channelPost && message.silent;
if (channelPost) {
sendFlags |= MTPmessages_SendMessage::Flag::f_broadcast;
flags |= MTPDmessage::Flag::f_views;
@ -1143,13 +1147,13 @@ void MainWidget::sendMessage(History *hist, const QString &text, MsgId replyTo,
if (!sentEntities.c_vector().v.isEmpty()) {
sendFlags |= MTPmessages_SendMessage::Flag::f_entities;
}
lastMessage = hist->addNewMessage(MTP_message(MTP_flags(flags), MTP_int(newId.msg), MTP_int(showFromName ? MTP::authedId() : 0), peerToMTP(hist->peer->id), MTPnullFwdHeader, MTPint(), MTP_int(replyTo), MTP_int(unixtime()), msgText, media, MTPnullMarkup, localEntities, MTP_int(1), MTPint()), NewMessageUnread);
hist->sendRequestId = MTP::send(MTPmessages_SendMessage(MTP_flags(sendFlags), hist->peer->input, MTP_int(replyTo), msgText, MTP_long(randomId), MTPnullMarkup, sentEntities), rpcDone(&MainWidget::sentUpdatesReceived, randomId), rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId);
lastMessage = history->addNewMessage(MTP_message(MTP_flags(flags), MTP_int(newId.msg), MTP_int(showFromName ? MTP::authedId() : 0), peerToMTP(history->peer->id), MTPnullFwdHeader, MTPint(), MTP_int(replyTo), MTP_int(unixtime()), msgText, media, MTPnullMarkup, localEntities, MTP_int(1), MTPint()), NewMessageUnread);
history->sendRequestId = MTP::send(MTPmessages_SendMessage(MTP_flags(sendFlags), history->peer->input, MTP_int(replyTo), msgText, MTP_long(randomId), MTPnullMarkup, sentEntities), rpcDone(&MainWidget::sentUpdatesReceived, randomId), rpcFail(&MainWidget::sendMessageFail), 0, 0, history->sendRequestId);
}
hist->lastSentMsg = lastMessage;
history->lastSentMsg = lastMessage;
finishForwarding(hist, broadcast, silent);
finishForwarding(history, message.broadcast, message.silent);
executeParsedCommand(command);
}
@ -1728,7 +1732,8 @@ void MainWidget::dialogsCancelled() {
void MainWidget::serviceNotification(const QString &msg, const MTPMessageMedia &media) {
MTPDmessage::Flags flags = MTPDmessage::Flag::f_unread | MTPDmessage::Flag::f_entities | MTPDmessage::Flag::f_from_id;
QString sendingText, leftText = msg;
EntitiesInText sendingEntities, leftEntities = textParseEntities(leftText, _historyTextNoMonoOptions.flags);
EntitiesInText sendingEntities, leftEntities;
textParseEntities(leftText, _historyTextNoMonoOptions.flags, &leftEntities);
HistoryItem *item = 0;
while (textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) {
MTPVector<MTPMessageEntity> localEntities = linksToMTP(sendingEntities);

View File

@ -281,7 +281,16 @@ public:
Dialogs::IndexedList *contactsList();
Dialogs::IndexedList *dialogsList();
void sendMessage(History *hist, const QString &text, MsgId replyTo, bool broadcast, bool silent, WebPageId webPageId = 0);
struct MessageToSend {
History *history = nullptr;
QString text;
FlatTextarea::TagList entities;
MsgId replyTo = 0;
bool broadcast = false;
bool silent = false;
WebPageId webPageId = 0;
};
void sendMessage(const MessageToSend &message);
void saveRecentHashtags(const QString &text);
void readServerHistory(History *history, bool force = true);

View File

@ -84,7 +84,13 @@ void MacPrivate::notifyClicked(unsigned long long peer, int msgid) {
void MacPrivate::notifyReplied(unsigned long long peer, int msgid, const char *str) {
History *history = App::history(PeerId(peer));
App::main()->sendMessage(history, QString::fromUtf8(str), (msgid > 0 && !history->peer->isUser()) ? msgid : 0, false, false);
MainWidget::MessageToSend message;
message.history = history;
message.text = QString::fromUtf8(str);
message.replyTo = (msgid > 0 && !history->peer->isUser()) ? msgid : 0;
message.broadcast = false;
message.silent = false;
App::main()->sendMessage(message);
}
PsMainWindow::PsMainWindow(QWidget *parent) : QMainWindow(parent),

View File

@ -165,9 +165,9 @@ inline bool emojiEdge(const QChar *ch) {
return false;
}
inline void appendPartToResult(QString &result, const QChar *start, const QChar *from, const QChar *to, EntitiesInText &entities) {
inline void appendPartToResult(QString &result, const QChar *start, const QChar *from, const QChar *to, EntitiesInText *inOutEntities) {
if (to > from) {
for (auto &entity : entities) {
for (auto &entity : *inOutEntities) {
if (entity.offset() >= to - start) break;
if (entity.offset() + entity.length() < from - start) continue;
if (entity.offset() >= from - start) {
@ -181,9 +181,9 @@ inline void appendPartToResult(QString &result, const QChar *start, const QChar
}
}
inline QString replaceEmojis(const QString &text, EntitiesInText &entities) {
inline QString replaceEmojis(const QString &text, EntitiesInText *inOutEntities) {
QString result;
auto currentEntity = entities.begin(), entitiesEnd = entities.end();
auto currentEntity = inOutEntities->begin(), entitiesEnd = inOutEntities->end();
const QChar *emojiStart = text.constData(), *emojiEnd = emojiStart, *e = text.constData() + text.size();
bool canFindEmoji = true;
for (const QChar *ch = emojiEnd; ch != e;) {
@ -204,7 +204,7 @@ inline QString replaceEmojis(const QString &text, EntitiesInText &entities) {
) {
if (result.isEmpty()) result.reserve(text.size());
appendPartToResult(result, emojiStart, emojiEnd, ch, entities);
appendPartToResult(result, emojiStart, emojiEnd, ch, inOutEntities);
if (emoji->color) {
EmojiColorVariants::const_iterator it = cEmojiVariants().constFind(emoji->code);
@ -232,7 +232,7 @@ inline QString replaceEmojis(const QString &text, EntitiesInText &entities) {
}
if (result.isEmpty()) return text;
appendPartToResult(result, emojiStart, emojiEnd, e, entities);
appendPartToResult(result, emojiStart, emojiEnd, e, inOutEntities);
return result;
}

View File

@ -23,6 +23,64 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
#include "mainwindow.h"
namespace {
QByteArray serializeTagsList(const FlatTextarea::TagList &tags) {
if (tags.isEmpty()) {
return QByteArray();
}
QByteArray tagsSerialized;
{
QBuffer buffer(&tagsSerialized);
buffer.open(QIODevice::WriteOnly);
QDataStream stream(&buffer);
stream.setVersion(QDataStream::Qt_5_1);
stream << qint32(tags.size());
for_const (auto &tag, tags) {
stream << qint32(tag.offset) << qint32(tag.length) << tag.id;
}
}
return tagsSerialized;
}
FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
FlatTextarea::TagList result;
QBuffer buffer(&data);
buffer.open(QIODevice::ReadOnly);
QDataStream stream(&buffer);
stream.setVersion(QDataStream::Qt_5_1);
qint32 tagCount = 0;
stream >> tagCount;
if (stream.status() != QDataStream::Ok) {
return result;
}
if (tagCount <= 0 || tagCount > textSize) {
return result;
}
for (int i = 0; i < tagCount; ++i) {
qint32 offset = 0, length = 0;
QString id;
stream >> offset >> length >> id;
if (stream.status() != QDataStream::Ok) {
return result;
}
if (offset < 0 || length <= 0 || offset + length > textSize) {
return result;
}
result.push_back({ offset, length, id });
}
return result;
}
constexpr str_const TagsMimeType = "application/x-td-field-tags";
} // namespace
FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &pholder, const QString &v) : QTextEdit(parent)
, _oldtext(v)
, _phVisible(!v.length())
@ -62,7 +120,7 @@ FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const
_touchTimer.setSingleShot(true);
connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
connect(document(), SIGNAL(contentsChange(int, int, int)), this, SLOT(onDocumentContentsChange(int, int, int)));
connect(document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(onDocumentContentsChange(int,int,int)));
connect(document(), SIGNAL(contentsChanged()), this, SLOT(onDocumentContentsChanged()));
connect(this, SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool)));
connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool)));
@ -360,7 +418,7 @@ QString FlatTextarea::getMentionHashtagBotCommandPart(bool &start) const {
return QString();
}
void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const QString &entityTag) {
void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const QString &tagId) {
QTextCursor c(textCursor());
int32 pos = c.position();
@ -405,13 +463,15 @@ void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const Q
}
break;
}
if (entityTag.isEmpty()) {
if (tagId.isEmpty()) {
c.insertText(data + ' ');
} else {
QTextCharFormat fmt;
fmt.setForeground(st::defaultTextStyle.linkFg);
c.insertText(data, fmt);
c.insertText(qsl(" "));
QTextCharFormat defaultFormat = c.charFormat(), linkFormat = defaultFormat;
linkFormat.setAnchor(true);
linkFormat.setAnchorName(tagId + '/' + QString::number(rand_value<uint32>()));
linkFormat.setForeground(st::defaultTextStyle.linkFg);
c.insertText(data, linkFormat);
c.insertText(qsl(" "), defaultFormat);
}
}
@ -471,12 +531,59 @@ void FlatTextarea::removeSingleEmoji() {
}
}
QString FlatTextarea::getText(int32 start, int32 end) const {
namespace {
class TagAccumulator {
public:
TagAccumulator(FlatTextarea::TagList *tags) : _tags(tags) {
}
bool changed() const {
return _changed;
}
void feed(const QString &tagId, int currentPosition) {
if (tagId == _currentTagId) return;
if (!_currentTagId.isEmpty()) {
FlatTextarea::Tag tag = {
_currentStart,
currentPosition - _currentStart,
_currentTagId,
};
if (_currentTag >= _tags->size()) {
_changed = true;
_tags->push_back(tag);
} else if (_tags->at(_currentTag) != tag) {
_changed = true;
(*_tags)[_currentTag] = tag;
}
++_currentTag;
}
_currentTagId = tagId;
_currentStart = currentPosition;
};
private:
FlatTextarea::TagList *_tags;
bool _changed = false;
int _currentTag = 0;
int _currentStart = 0;
QString _currentTagId;
};
} // namespace
QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const {
if (end >= 0 && end <= start) return QString();
if (start < 0) start = 0;
bool full = (start == 0) && (end < 0);
TagAccumulator tagAccumulator(outTagsList);
QTextDocument *doc(document());
QTextBlock from = full ? doc->begin() : doc->findBlock(start), till = (end < 0) ? doc->end() : doc->findBlock(end);
if (till.isValid()) till = till.next();
@ -491,17 +598,28 @@ QString FlatTextarea::getText(int32 start, int32 end) const {
end = possibleLen;
}
for (QTextBlock b = from; b != till; b = b.next()) {
for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) {
bool tillFragmentEnd = full;
for (auto b = from; b != till; b = b.next()) {
for (auto iter = b.begin(); !iter.atEnd(); ++iter) {
QTextFragment fragment(iter.fragment());
if (!fragment.isValid()) continue;
int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length());
if (!full) {
if (p >= end || e <= start) {
tillFragmentEnd = (e <= end);
if (p == end && outTagsList) {
tagAccumulator.feed(fragment.charFormat().anchorName(), result.size());
}
if (p >= end) {
break;
}
if (e <= start) {
continue;
}
}
if (outTagsList && (full || p >= start)) {
tagAccumulator.feed(fragment.charFormat().anchorName(), result.size());
}
QTextCharFormat f = fragment.charFormat();
QString emojiText;
@ -545,6 +663,13 @@ QString FlatTextarea::getText(int32 start, int32 end) const {
result.append('\n');
}
result.chop(1);
if (outTagsList) {
if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size());
if (outTagsChanged) {
*outTagsChanged = tagAccumulator.changed();
}
}
return result;
}
@ -666,11 +791,17 @@ QStringList FlatTextarea::linksList() const {
}
void FlatTextarea::insertFromMimeData(const QMimeData *source) {
auto mime = str_const_latin1_toString(TagsMimeType);
if (source->hasFormat(mime)) {
auto tagsData = source->data(mime);
_settingTags = deserializeTagsList(tagsData, source->text().size());
} else {
_settingTags.clear();
}
QTextEdit::insertFromMimeData(source);
if (!_inDrop) emit spacedReturnedPasted();
}
_settingTags.clear();
void FlatTextarea::correctValue(const QString &was, QString &now) {
if (!_inDrop) emit spacedReturnedPasted();
}
void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) {
@ -680,6 +811,7 @@ void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) {
imageFormat.setHeight(eh / cIntRetinaFactor());
imageFormat.setName(qsl("emoji://e.") + QString::number(emojiKey(emoji), 16));
imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline);
imageFormat.setAnchorName(c.charFormat().anchorName());
static QString objectReplacement(QChar::ObjectReplacementCharacter);
c.insertText(objectReplacement, imageFormat);
@ -703,7 +835,7 @@ void FlatTextarea::checkContentHeight() {
void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) {
int32 replacePosition = -1, replaceLen = 0;
const EmojiData *emoji = 0;
const EmojiData *emoji = nullptr;
static QString regular = qsl("Open Sans"), semibold = qsl("Open Sans Semibold");
bool checkTilde = !cRetina() && (font().pixelSize() == 13) && (font().family() == regular), wasTildeFragment = false;
@ -731,7 +863,7 @@ void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) {
QString t(fragment.text());
const QChar *ch = t.constData(), *e = ch + t.size();
for (; ch != e; ++ch, ++fp) {
int32 emojiLen = 0;
int emojiLen = 0;
emoji = emojiFromText(ch, e, &emojiLen);
if (emoji) {
if (replacePosition >= 0) {
@ -767,9 +899,12 @@ void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) {
if (replacePosition >= 0) break;
}
if (replacePosition >= 0) {
// Optimization: with null page size document does not re-layout
// on each insertText / mergeCharFormat.
if (!document()->pageSize().isNull()) {
document()->setPageSize(QSizeF(0, 0));
}
QTextCursor c(doc->docHandle(), replacePosition);
c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor);
if (emoji) {
@ -782,7 +917,7 @@ void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) {
charsAdded -= replacePosition + replaceLen - position;
position = replacePosition + (emoji ? 1 : replaceLen);
emoji = 0;
emoji = nullptr;
replacePosition = -1;
} else {
break;
@ -821,7 +956,7 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int
if (!_links.isEmpty()) {
bool changed = false;
for (LinkRanges::iterator i = _links.begin(); i != _links.end();) {
for (auto i = _links.begin(); i != _links.end();) {
if (i->first + i->second <= position) {
++i;
} else if (i->first >= position + charsRemoved) {
@ -867,11 +1002,15 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int
void FlatTextarea::onDocumentContentsChanged() {
if (_correcting) return;
QString curText(getText());
auto tagsChanged = false;
auto curText = getText(0, -1, &_oldtags, &tagsChanged);
_correcting = true;
correctValue(_oldtext, curText);
correctValue(_oldtext, curText, _oldtags);
_correcting = false;
if (_oldtext != curText) {
bool textOrTagsChanged = tagsChanged || (_oldtext != curText);
if (textOrTagsChanged) {
_oldtext = curText;
emit changed();
checkContentHeight();
@ -934,7 +1073,11 @@ QMimeData *FlatTextarea::createMimeDataFromSelection() const {
QTextCursor c(textCursor());
int32 start = c.selectionStart(), end = c.selectionEnd();
if (end > start) {
result->setText(getText(start, end));
TagList tags;
result->setText(getText(start, end, &tags, nullptr));
if (!tags.isEmpty()) {
result->setData(qsl("application/x-td-field-tags"), serializeTagsList(tags));
}
}
return result;
}

View File

@ -33,17 +33,6 @@ public:
FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &ph = QString(), const QString &val = QString());
bool viewportEvent(QEvent *e) override;
void touchEvent(QTouchEvent *e);
void paintEvent(QPaintEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void focusOutEvent(QFocusEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void dropEvent(QDropEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void setMaxLength(int32 maxLength);
void setMinHeight(int32 minHeight);
void setMaxHeight(int32 maxHeight);
@ -51,6 +40,7 @@ public:
const QString &getLastText() const {
return _oldtext;
}
void setPlaceholder(const QString &ph, int32 afterSymbols = 0);
void updatePlaceholder();
void finishPlaceholder();
@ -94,7 +84,15 @@ public:
void setTextFast(const QString &text, bool clearUndoHistory = true);
void insertMentionHashtagOrBotCommand(const QString &data, const QString &entityTag = QString());
struct Tag {
int offset, length;
QString id;
};
using TagList = QVector<Tag>;
const TagList &getLastTags() const {
return _oldtags;
}
void insertMentionHashtagOrBotCommand(const QString &data, const QString &tagId = QString());
public slots:
@ -118,8 +116,19 @@ signals:
protected:
QString getText(int32 start = 0, int32 end = -1) const;
virtual void correctValue(const QString &was, QString &now);
bool viewportEvent(QEvent *e) override;
void touchEvent(QTouchEvent *e);
void paintEvent(QPaintEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void focusOutEvent(QFocusEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void dropEvent(QDropEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
virtual void correctValue(const QString &was, QString &now, TagList &nowTags) {
}
void insertEmoji(EmojiPtr emoji, QTextCursor c);
@ -129,6 +138,10 @@ protected:
private:
// "start" and "end" are in coordinates of text where emoji are replaced by ObjectReplacementCharacter.
// If "end" = -1 means get text till the end. "outTagsList" and "outTagsChanged" may be nullptr.
QString getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const;
void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const;
void processDocumentContentsChange(int position, int charsAdded);
bool heightAutoupdated();
@ -139,6 +152,7 @@ private:
SubmitSettings _submitSettings = SubmitSettings::Enter;
QString _ph, _phelided, _oldtext;
TagList _oldtags;
int _phAfter = 0;
bool _phVisible;
anim::ivalue a_phLeft;
@ -146,6 +160,9 @@ private:
anim::cvalue a_phColor;
Animation _a_appearance;
// Tags list which we should apply while setText() call or insert from mime data.
TagList _settingTags;
style::flatTextarea _st;
bool _undoAvailable = false;
@ -167,3 +184,10 @@ private:
typedef QList<LinkRange> LinkRanges;
LinkRanges _links;
};
inline bool operator==(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) {
return (a.offset == b.offset) && (a.length == b.length) && (a.id == b.id);
}
inline bool operator!=(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) {
return !(a == b);
}

View File

@ -246,27 +246,6 @@ public:
createBlock();
}
void getLinkData(const QString &original, QString &result, int32 &fullDisplayed) {
if (!original.isEmpty() && original.at(0) == '/') {
result = original;
fullDisplayed = -4; // bot command
} else if (!original.isEmpty() && original.at(0) == '@') {
result = original;
fullDisplayed = -3; // mention
} else if (!original.isEmpty() && original.at(0) == '#') {
result = original;
fullDisplayed = -2; // hashtag
} else if (reMailStart().match(original).hasMatch()) {
result = original;
fullDisplayed = -1; // email
} else {
QUrl url(original), good(url.isValid() ? url.toEncoded() : "");
QString readable = good.isValid() ? good.toDisplayString() : original;
result = _t->_font->elided(readable, st::linkCropLimit);
fullDisplayed = (result == readable) ? 1 : 0;
}
}
bool checkCommand() {
bool result = false;
for (QChar c = ((ptr < end) ? *ptr : 0); c == TextCommand; c = ((ptr < end) ? *ptr : 0)) {
@ -300,17 +279,11 @@ public:
return;
}
bool lnk = false;
int32 startFlags = 0;
int32 fullDisplayed;
QString lnkUrl, lnkText;
auto type = waitingEntity->type();
if (type == EntityInTextCustomUrl) {
lnk = true;
lnkUrl = waitingEntity->data();
lnkText = QString(start + waitingEntity->offset(), waitingEntity->length());
fullDisplayed = -5;
} else if (type == EntityInTextBold) {
QString linkData, linkText;
auto type = waitingEntity->type(), linkType = EntityInTextInvalid;
LinkDisplayStatus linkDisplayStatus = LinkDisplayedFull;
if (type == EntityInTextBold) {
startFlags = TextBlockFSemibold;
} else if (type == EntityInTextItalic) {
startFlags = TextBlockFItalic;
@ -322,21 +295,36 @@ public:
if (!_t->_blocks.isEmpty() && _t->_blocks.back()->type() != TextBlockTNewline) {
createNewlineBlock();
}
} else {
lnk = true;
lnkUrl = QString(start + waitingEntity->offset(), waitingEntity->length());
getLinkData(lnkUrl, lnkText, fullDisplayed);
} else if (type == EntityInTextUrl
|| type == EntityInTextEmail
|| type == EntityInTextMention
|| type == EntityInTextHashtag
|| type == EntityInTextBotCommand) {
linkType = type;
linkData = QString(start + waitingEntity->offset(), waitingEntity->length());
if (linkType == EntityInTextUrl) {
computeLinkText(linkData, &linkText, &linkDisplayStatus);
} else {
linkText = linkData;
}
} else if (type == EntityInTextCustomUrl || type == EntityInTextMentionName) {
linkType = type;
linkData = waitingEntity->data();
linkText = QString(start + waitingEntity->offset(), waitingEntity->length());
}
if (lnk) {
if (linkType != EntityInTextInvalid) {
createBlock();
links.push_back(TextLinkData(lnkUrl, fullDisplayed));
links.push_back(TextLinkData(linkType, linkText, linkData, linkDisplayStatus));
lnkIndex = 0x8000 + links.size();
_t->_text += lnkText;
ptr = start + waitingEntity->offset() + waitingEntity->length();
for (auto entityEnd = start + waitingEntity->offset() + waitingEntity->length(); ptr < entityEnd; ++ptr) {
parseCurrentChar();
parseEmojiFromCurrent();
if (sumFinished || _t->_text.size() >= 0x8000) break; // 32k max
}
createBlock();
lnkIndex = 0;
@ -461,7 +449,7 @@ public:
case TextCommandLinkText: {
createBlock();
int32 len = ptr->unicode();
links.push_back(TextLinkData(QString(++ptr, len), false));
links.push_back(TextLinkData(EntityInTextCustomUrl, QString(), QString(++ptr, len), LinkDisplayedFull));
lnkIndex = 0x8000 + links.size();
} break;
@ -565,7 +553,7 @@ public:
lnkIndex(0),
stopAfterWidth(QFIXED_MAX) {
if (options.flags & TextParseLinks) {
entities = textParseEntities(src, options.flags, rich);
textParseEntities(src, options.flags, &entities, rich);
}
parse(options);
}
@ -664,32 +652,44 @@ public:
lnkIndex = maxLnkIndex + (b->lnkIndex() - 0x8000);
if (_t->_links.size() < lnkIndex) {
_t->_links.resize(lnkIndex);
const TextLinkData &data(links[lnkIndex - maxLnkIndex - 1]);
ClickHandlerPtr lnk;
if (data.fullDisplayed < -4) { // hidden link
lnk.reset(new HiddenUrlClickHandler(data.url));
} else if (data.fullDisplayed < -3) { // bot command
lnk.reset(new BotCommandClickHandler(data.url));
} else if (data.fullDisplayed < -2) { // mention
const TextLinkData &link(links[lnkIndex - maxLnkIndex - 1]);
ClickHandlerPtr handler;
switch (link.type) {
case EntityInTextCustomUrl: handler.reset(new HiddenUrlClickHandler(link.data)); break;
case EntityInTextEmail:
case EntityInTextUrl: handler.reset(new UrlClickHandler(link.data, link.displayStatus == LinkDisplayedFull)); break;
case EntityInTextBotCommand: handler.reset(new BotCommandClickHandler(link.data)); break;
case EntityInTextHashtag:
if (options.flags & TextTwitterMentions) {
lnk.reset(new UrlClickHandler(qsl("https://twitter.com/") + data.url.mid(1), true));
handler.reset(new UrlClickHandler(qsl("https://twitter.com/hashtag/") + link.data.mid(1) + qsl("?src=hash"), true));
} else if (options.flags & TextInstagramMentions) {
lnk.reset(new UrlClickHandler(qsl("https://instagram.com/") + data.url.mid(1) + '/', true));
handler.reset(new UrlClickHandler(qsl("https://instagram.com/explore/tags/") + link.data.mid(1) + '/', true));
} else {
lnk.reset(new MentionClickHandler(data.url));
handler.reset(new HashtagClickHandler(link.data));
}
} else if (data.fullDisplayed < -1) { // hashtag
break;
case EntityInTextMention:
if (options.flags & TextTwitterMentions) {
lnk.reset(new UrlClickHandler(qsl("https://twitter.com/hashtag/") + data.url.mid(1) + qsl("?src=hash"), true));
handler.reset(new UrlClickHandler(qsl("https://twitter.com/") + link.data.mid(1), true));
} else if (options.flags & TextInstagramMentions) {
lnk.reset(new UrlClickHandler(qsl("https://instagram.com/explore/tags/") + data.url.mid(1) + '/', true));
handler.reset(new UrlClickHandler(qsl("https://instagram.com/") + link.data.mid(1) + '/', true));
} else {
lnk.reset(new HashtagClickHandler(data.url));
handler.reset(new MentionClickHandler(link.data));
}
} else { // email or url
lnk.reset(new UrlClickHandler(data.url, data.fullDisplayed != 0));
break;
case EntityInTextMentionName: {
UserId userId = 0;
uint64 accessHash = 0;
if (mentionNameToFields(link.data, &userId, &accessHash)) {
handler.reset(new MentionNameClickHandler(link.text, userId, accessHash));
} else {
LOG(("Bad mention name: %1").arg(link.data));
}
} break;
}
_t->setLink(lnkIndex, lnk);
t_assert(!handler.isNull());
_t->setLink(lnkIndex, handler);
}
b->setLnkIndex(lnkIndex);
}
@ -701,6 +701,30 @@ public:
private:
enum LinkDisplayStatus {
LinkDisplayedFull,
LinkDisplayedElided,
};
struct TextLinkData {
TextLinkData() = default;
TextLinkData(EntityInTextType type, const QString &text, const QString &data, LinkDisplayStatus displayStatus)
: type(type)
, text(text)
, data(data)
, displayStatus(displayStatus) {
}
EntityInTextType type = EntityInTextInvalid;
QString text, data;
LinkDisplayStatus displayStatus = LinkDisplayedFull;
};
void computeLinkText(const QString &linkData, QString *outLinkText, LinkDisplayStatus *outDisplayStatus) {
QUrl url(linkData), good(url.isValid() ? url.toEncoded() : "");
QString readable = good.isValid() ? good.toDisplayString() : linkData;
*outLinkText = _t->_font->elided(readable, st::linkCropLimit);
*outDisplayStatus = (*outLinkText == readable) ? LinkDisplayedFull : LinkDisplayedElided;
}
Text *_t;
QString src;
const QChar *start, *end, *ptr;
@ -709,12 +733,6 @@ private:
EntitiesInText entities;
EntitiesInText::const_iterator waitingEntity, entitiesEnd;
struct TextLinkData {
TextLinkData(const QString &url = QString(), int32 fullDisplayed = 1) : url(url), fullDisplayed(fullDisplayed) {
}
QString url;
int32 fullDisplayed; // -5 - custom text link, -4 - bot command, -3 - mention, -2 - hashtag, -1 - email
};
typedef QVector<TextLinkData> TextLinks;
TextLinks links;

View File

@ -1364,6 +1364,21 @@ EntitiesInText entitiesFromMTP(const QVector<MTPMessageEntity> &entities) {
case mtpc_messageEntityHashtag: { const auto &d(entity.c_messageEntityHashtag()); result.push_back(EntityInText(EntityInTextHashtag, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityMention: { const auto &d(entity.c_messageEntityMention()); result.push_back(EntityInText(EntityInTextMention, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityMentionName: { const auto &d(entity.c_messageEntityMentionName()); result.push_back(EntityInText(EntityInTextMentionName, d.voffset.v, d.vlength.v, QString::number(d.vuser_id.v))); } break;
case mtpc_inputMessageEntityMentionName: {
const auto &d(entity.c_inputMessageEntityMentionName());
auto data = ([&d]() -> QString {
if (d.vuser_id.type() == mtpc_inputUserSelf) {
return QString::number(MTP::authedId());
} else if (d.vuser_id.type() == mtpc_inputUser) {
const auto &user(d.vuser_id.c_inputUser());
return QString::number(user.vuser_id.v) + '.' + QString::number(user.vaccess_hash.v);
}
return QString();
})();
if (!data.isEmpty()) {
result.push_back(EntityInText(EntityInTextMentionName, d.voffset.v, d.vlength.v, data));
}
} break;
case mtpc_messageEntityBotCommand: { const auto &d(entity.c_messageEntityBotCommand()); result.push_back(EntityInText(EntityInTextBotCommand, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityBold: { const auto &d(entity.c_messageEntityBold()); result.push_back(EntityInText(EntityInTextBold, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityItalic: { const auto &d(entity.c_messageEntityItalic()); result.push_back(EntityInText(EntityInTextItalic, d.voffset.v, d.vlength.v)); } break;
@ -1380,7 +1395,12 @@ MTPVector<MTPMessageEntity> linksToMTP(const EntitiesInText &links, bool sending
auto &v = result._vector().v;
for_const (const auto &link, links) {
if (link.length() <= 0) continue;
if (sending && link.type() != EntityInTextCode && link.type() != EntityInTextPre) continue;
if (sending
&& link.type() != EntityInTextCode
&& link.type() != EntityInTextPre
&& link.type() != EntityInTextMentionName) {
continue;
}
auto offset = MTP_int(link.offset()), length = MTP_int(link.length());
switch (link.type()) {
@ -1389,7 +1409,22 @@ MTPVector<MTPMessageEntity> linksToMTP(const EntitiesInText &links, bool sending
case EntityInTextEmail: v.push_back(MTP_messageEntityEmail(offset, length)); break;
case EntityInTextHashtag: v.push_back(MTP_messageEntityHashtag(offset, length)); break;
case EntityInTextMention: v.push_back(MTP_messageEntityMention(offset, length)); break;
case EntityInTextMentionName: v.push_back(MTP_messageEntityMentionName(offset, length, MTP_int(link.data().toInt()))); break;
case EntityInTextMentionName: {
auto inputUser = ([](const QString &data) -> MTPInputUser {
UserId userId = 0;
uint64 accessHash = 0;
if (mentionNameToFields(data, &userId, &accessHash)) {
if (userId == MTP::authedId()) {
return MTP_inputUserSelf();
}
return MTP_inputUser(MTP_int(userId), MTP_long(accessHash));
}
return MTP_inputUserEmpty();
})(link.data());
if (inputUser.type() != mtpc_inputUserEmpty) {
v.push_back(MTP_inputMessageEntityMentionName(offset, length, inputUser));
}
} break;
case EntityInTextBotCommand: v.push_back(MTP_messageEntityBotCommand(offset, length)); break;
case EntityInTextBold: v.push_back(MTP_messageEntityBold(offset, length)); break;
case EntityInTextItalic: v.push_back(MTP_messageEntityItalic(offset, length)); break;
@ -1400,8 +1435,9 @@ MTPVector<MTPMessageEntity> linksToMTP(const EntitiesInText &links, bool sending
return result;
}
EntitiesInText textParseEntities(QString &text, int32 flags, bool rich) { // some code is duplicated in flattextarea.cpp!
EntitiesInText result, mono;
// Some code is duplicated in flattextarea.cpp!
void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities, bool rich) {
EntitiesInText mono;
bool withHashtags = (flags & TextParseHashtags);
bool withMentions = (flags & TextParseMentions);
@ -1701,20 +1737,18 @@ EntitiesInText textParseEntities(QString &text, int32 flags, bool rich) { // som
}
for (; monoEntity < monoCount && mono[monoEntity].offset() <= lnkStart; ++monoEntity) {
monoTill = qMax(monoTill, mono[monoEntity].offset() + mono[monoEntity].length());
result.push_back(mono[monoEntity]);
inOutEntities->push_back(mono[monoEntity]);
}
if (lnkStart >= monoTill) {
result.push_back(EntityInText(lnkType, lnkStart, lnkLength));
inOutEntities->push_back(EntityInText(lnkType, lnkStart, lnkLength));
}
offset = matchOffset = lnkStart + lnkLength;
}
for (; monoEntity < monoCount; ++monoEntity) {
monoTill = qMax(monoTill, mono[monoEntity].offset() + mono[monoEntity].length());
result.push_back(mono[monoEntity]);
inOutEntities->push_back(mono[monoEntity]);
}
return result;
}
QString textApplyEntities(const QString &text, const EntitiesInText &entities) {
@ -1769,71 +1803,11 @@ QString textApplyEntities(const QString &text, const EntitiesInText &entities) {
return result;
}
void replaceStringWithEntities(const QLatin1String &from, QChar to, QString &result, EntitiesInText &entities, bool checkSpace = false) {
int32 len = from.size(), s = result.size(), offset = 0, length = 0;
EntitiesInText::iterator i = entities.begin(), e = entities.end();
for (QChar *start = result.data(); offset < s;) {
int32 nextOffset = result.indexOf(from, offset);
if (nextOffset < 0) {
moveStringPart(start, length, offset, s - offset, entities);
break;
}
if (checkSpace) {
bool spaceBefore = (nextOffset > 0) && (start + nextOffset - 1)->isSpace();
bool spaceAfter = (nextOffset + len < s) && (start + nextOffset + len)->isSpace();
if (!spaceBefore && !spaceAfter) {
moveStringPart(start, length, offset, nextOffset - offset + len + 1, entities);
continue;
}
}
bool 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) {
moveStringPart(start, length, offset, nextOffset - offset + len, entities);
continue;
}
moveStringPart(start, length, offset, nextOffset - offset, entities);
*(start + length) = to;
++length;
offset += len;
}
if (length < s) result.resize(length);
}
QString prepareTextWithEntities(QString result, EntitiesInText &entities, int32 flags) {
cleanTextWithEntities(result, entities);
if (flags) {
entities = textParseEntities(result, flags);
}
replaceStringWithEntities(qstr("--"), QChar(8212), result, entities, true);
replaceStringWithEntities(qstr("<<"), QChar(171), result, entities);
replaceStringWithEntities(qstr(">>"), QChar(187), result, entities);
if (cReplaceEmojis()) {
result = replaceEmojis(result, entities);
}
trimTextWithEntities(result, entities);
return result;
}
void moveStringPart(QChar *start, int32 &to, int32 &from, int32 count, EntitiesInText &entities) {
void moveStringPart(QChar *start, int32 &to, int32 &from, int32 count, EntitiesInText *inOutEntities) {
if (count > 0) {
if (to < from) {
memmove(start + to, start + from, count * sizeof(QChar));
for (auto &entity : entities) {
for (auto &entity : *inOutEntities) {
if (entity.offset() >= from + count) break;
if (entity.offset() + entity.length() < from) continue;
if (entity.offset() >= from) {
@ -1849,24 +1823,84 @@ void moveStringPart(QChar *start, int32 &to, int32 &from, int32 count, EntitiesI
}
}
void replaceStringWithEntities(const QLatin1String &from, QChar to, QString &result, EntitiesInText *inOutEntities, bool checkSpace = false) {
int32 len = from.size(), s = result.size(), offset = 0, length = 0;
EntitiesInText::iterator i = inOutEntities->begin(), e = inOutEntities->end();
for (QChar *start = result.data(); offset < s;) {
int32 nextOffset = result.indexOf(from, offset);
if (nextOffset < 0) {
moveStringPart(start, length, offset, s - offset, inOutEntities);
break;
}
if (checkSpace) {
bool spaceBefore = (nextOffset > 0) && (start + nextOffset - 1)->isSpace();
bool spaceAfter = (nextOffset + len < s) && (start + nextOffset + len)->isSpace();
if (!spaceBefore && !spaceAfter) {
moveStringPart(start, length, offset, nextOffset - offset + len + 1, inOutEntities);
continue;
}
}
bool 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) {
moveStringPart(start, length, offset, nextOffset - offset + len, inOutEntities);
continue;
}
moveStringPart(start, length, offset, nextOffset - offset, inOutEntities);
*(start + length) = to;
++length;
offset += len;
}
if (length < s) result.resize(length);
}
QString prepareTextWithEntities(QString result, int32 flags, EntitiesInText *inOutEntities) {
cleanTextWithEntities(result, inOutEntities);
if (flags) {
textParseEntities(result, flags, inOutEntities);
}
replaceStringWithEntities(qstr("--"), QChar(8212), result, inOutEntities, true);
replaceStringWithEntities(qstr("<<"), QChar(171), result, inOutEntities);
replaceStringWithEntities(qstr(">>"), QChar(187), result, inOutEntities);
if (cReplaceEmojis()) {
result = replaceEmojis(result, inOutEntities);
}
trimTextWithEntities(result, inOutEntities);
return result;
}
// replace bad symbols with space and remove \r
void cleanTextWithEntities(QString &result, EntitiesInText &entities) {
void cleanTextWithEntities(QString &result, EntitiesInText *inOutEntities) {
result = result.replace('\t', qstr(" "));
int32 len = result.size(), to = 0, from = 0;
QChar *start = result.data();
for (QChar *ch = start, *end = start + len; ch < end; ++ch) {
if (ch->unicode() == '\r') {
moveStringPart(start, to, from, (ch - start) - from, entities);
moveStringPart(start, to, from, (ch - start) - from, inOutEntities);
++from;
} else if (chReplacedBySpace(*ch)) {
*ch = ' ';
}
}
moveStringPart(start, to, from, len - from, entities);
moveStringPart(start, to, from, len - from, inOutEntities);
if (to < len) result.resize(to);
}
void trimTextWithEntities(QString &result, EntitiesInText &entities) {
void trimTextWithEntities(QString &result, EntitiesInText *inOutEntities) {
bool foundNotTrimmedChar = false;
// right trim
@ -1875,7 +1909,7 @@ void trimTextWithEntities(QString &result, EntitiesInText &entities) {
if (!chIsTrimmed(*ch)) {
if (ch + 1 < e) {
int32 l = ch + 1 - s;
for (auto &entity : entities) {
for (auto &entity : *inOutEntities) {
entity.updateTextEnd(l);
}
result.resize(l);
@ -1886,18 +1920,18 @@ void trimTextWithEntities(QString &result, EntitiesInText &entities) {
}
if (!foundNotTrimmedChar) {
result.clear();
entities.clear();
inOutEntities->clear();
return;
}
int firstMonospaceOffset = EntityInText::firstMonospaceOffset(entities, result.size());
int firstMonospaceOffset = EntityInText::firstMonospaceOffset(*inOutEntities, result.size());
// left trim
for (QChar *s = result.data(), *ch = s, *e = s + result.size(); ch != e; ++ch) {
if (!chIsTrimmed(*ch) || (ch - s) == firstMonospaceOffset) {
if (ch > s) {
int32 l = ch - s;
for (auto &entity : entities) {
for (auto &entity : *inOutEntities) {
entity.shiftLeft(l);
}
result = result.mid(l);

View File

@ -133,21 +133,36 @@ enum {
TextInstagramHashtags = 0x800,
};
inline bool mentionNameToFields(const QString &data, int32 *outUserId, uint64 *outAccessHash) {
auto components = data.split('.');
if (!components.isEmpty()) {
*outUserId = components.at(0).toInt();
*outAccessHash = (components.size() > 1) ? components.at(1).toULongLong() : 0;
return (*outUserId != 0);
}
return false;
}
inline QString mentionNameFromFields(int32 userId, uint64 accessHash) {
return QString::number(userId) + '.' + QString::number(accessHash);
}
EntitiesInText entitiesFromMTP(const QVector<MTPMessageEntity> &entities);
MTPVector<MTPMessageEntity> linksToMTP(const EntitiesInText &links, bool sending = false);
EntitiesInText textParseEntities(QString &text, int32 flags, bool rich = false); // changes text if (flags & TextParseMono)
// New entities are added to the ones that are already in inOutEntities.
// Changes text if (flags & TextParseMono).
void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities, bool rich = false);
QString textApplyEntities(const QString &text, const EntitiesInText &entities);
QString prepareTextWithEntities(QString result, EntitiesInText &entities, int32 flags);
QString prepareTextWithEntities(QString result, int32 flags, EntitiesInText *inOutEntities);
inline QString prepareText(QString result, bool checkLinks = false) {
EntitiesInText entities;
return prepareTextWithEntities(result, entities, checkLinks ? (TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands) : 0);
auto prepareFlags = checkLinks ? (TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands) : 0;
return prepareTextWithEntities(result, prepareFlags, &entities);
}
void moveStringPart(QChar *start, int32 &to, int32 &from, int32 count, EntitiesInText &entities);
// replace bad symbols with space and remove \r
void cleanTextWithEntities(QString &result, EntitiesInText &entities);
void trimTextWithEntities(QString &result, EntitiesInText &entities);
void cleanTextWithEntities(QString &result, EntitiesInText *inOutEntities);
void trimTextWithEntities(QString &result, EntitiesInText *inOutEntities);