Marking tags by random values only inside of FlatTextarea.

Added a strategy to convert tags to and from tags-for-mime-data.
This commit is contained in:
John Preston 2016-05-04 23:38:37 +03:00
parent 45143c40c9
commit 5a47d8e29b
3 changed files with 162 additions and 76 deletions

View File

@ -2754,6 +2754,32 @@ EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) {
return result; 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) HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent)
, _fieldBarCancel(this, st::replyCancel) , _fieldBarCancel(this, st::replyCancel)
, _scroll(this, st::historyScroll, false) , _scroll(this, st::historyScroll, false)
@ -2874,6 +2900,7 @@ HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent)
connect(_fieldAutocomplete, SIGNAL(botCommandChosen(QString,FieldAutocomplete::ChooseMethod)), this, SLOT(onHashtagOrBotCommandInsert(QString,FieldAutocomplete::ChooseMethod))); connect(_fieldAutocomplete, SIGNAL(botCommandChosen(QString,FieldAutocomplete::ChooseMethod)), this, SLOT(onHashtagOrBotCommandInsert(QString,FieldAutocomplete::ChooseMethod)));
connect(_fieldAutocomplete, SIGNAL(stickerChosen(DocumentData*,FieldAutocomplete::ChooseMethod)), this, SLOT(onStickerSend(DocumentData*))); connect(_fieldAutocomplete, SIGNAL(stickerChosen(DocumentData*,FieldAutocomplete::ChooseMethod)), this, SLOT(onStickerSend(DocumentData*)));
_field.installEventFilter(_fieldAutocomplete); _field.installEventFilter(_fieldAutocomplete);
_field.setTagMimeProcessor(std_::make_unique<FieldTagMimeProcessor>());
updateFieldSubmitSettings(); updateFieldSubmitSettings();
_field.hide(); _field.hide();
@ -2940,7 +2967,7 @@ void HistoryWidget::onMentionInsert(UserData *user) {
} else { } else {
replacement = '@' + user->username; replacement = '@' + user->username;
} }
_field.insertMentionHashtagOrBotCommand(replacement, entityTag); _field.insertTag(replacement, entityTag);
} }
void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::ChooseMethod method) { void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::ChooseMethod method) {
@ -2949,7 +2976,7 @@ void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::
App::sendBotCommand(_peer, nullptr, str); App::sendBotCommand(_peer, nullptr, str);
setFieldText(_field.getLastText().mid(_field.textCursor().position())); setFieldText(_field.getLastText().mid(_field.textCursor().position()));
} else { } else {
_field.insertMentionHashtagOrBotCommand(str); _field.insertTag(str);
} }
} }

View File

@ -418,65 +418,75 @@ QString FlatTextarea::getMentionHashtagBotCommandPart(bool &start) const {
return QString(); return QString();
} }
void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const QString &tagId) { void FlatTextarea::insertTag(const QString &text, QString tagId) {
QTextCursor c(textCursor()); auto cursor = textCursor();
int32 pos = c.position(); int32 pos = cursor.position();
QTextDocument *doc(document()); auto doc = document();
QTextBlock block = doc->findBlock(pos); auto block = doc->findBlock(pos);
for (QTextBlock::Iterator iter = block.begin(); !iter.atEnd(); ++iter) { for (auto iter = block.begin(); !iter.atEnd(); ++iter) {
QTextFragment fr(iter.fragment()); auto fragment = iter.fragment();
if (!fr.isValid()) continue; t_assert(fragment.isValid());
int32 p = fr.position(), e = (p + fr.length()); int fragmentPosition = fragment.position();
if (p >= pos || e < pos) continue; int fragmentEnd = (fragmentPosition + fragment.length());
if (fragmentPosition >= pos || fragmentEnd < pos) continue;
QTextCharFormat f = fr.charFormat(); auto format = fragment.charFormat();
if (f.isImageFormat()) continue; if (format.isImageFormat()) continue;
bool mentionInCommand = false; bool mentionInCommand = false;
QString t(fr.text()); auto fragmentText = fragment.text();
for (int i = pos - p; i > 0; --i) { for (int i = pos - fragmentPosition; i > 0; --i) {
if (t.at(i - 1) == '@' || t.at(i - 1) == '#' || t.at(i - 1) == '/') { auto previousChar = fragmentText.at(i - 1);
if ((i == pos - p || (t.at(i - 1) == '/' ? t.at(i).isLetterOrNumber() : t.at(i).isLetter()) || t.at(i - 1) == '#') && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) { if (previousChar == '@' || previousChar == '#' || previousChar == '/') {
c.setPosition(p + i - 1, QTextCursor::MoveAnchor); if ((i == pos - fragmentPosition || (previousChar == '/' ? fragmentText.at(i).isLetterOrNumber() : fragmentText.at(i).isLetter()) || previousChar == '#') &&
int till = p + i; (i < 2 || !(fragmentText.at(i - 2).isLetterOrNumber() || fragmentText.at(i - 2) == '_'))) {
for (; (till < e) && (till - p - i + 1 < data.size()); ++till) { cursor.setPosition(fragmentPosition + i - 1, QTextCursor::MoveAnchor);
if (t.at(till - p).toLower() != data.at(till - p - i + 1).toLower()) { 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()) {
break; break;
} }
} }
if (till - p - i + 1 == data.size() && till < e && t.at(till - p) == ' ') { if (till - fragmentPosition - i + 1 == text.size() && till < fragmentEnd && fragmentText.at(till - fragmentPosition) == ' ') {
++till; ++till;
} }
c.setPosition(till, QTextCursor::KeepAnchor); cursor.setPosition(till, QTextCursor::KeepAnchor);
break; break;
} else if ((i == pos - p || t.at(i).isLetter()) && t.at(i - 1) == '@' && i > 2 && (t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_') && !mentionInCommand) { } else if ((i == pos - fragmentPosition || fragmentText.at(i).isLetter()) && fragmentText.at(i - 1) == '@' && i > 2 && (fragmentText.at(i - 2).isLetterOrNumber() || fragmentText.at(i - 2) == '_') && !mentionInCommand) {
mentionInCommand = true; mentionInCommand = true;
--i; --i;
continue; continue;
} }
break; break;
} }
if (pos - p - i > 127 || (!mentionInCommand && (pos - p - i > 63))) break; if (pos - fragmentPosition - i > 127 || (!mentionInCommand && (pos - fragmentPosition - i > 63))) break;
if (!t.at(i - 1).isLetterOrNumber() && t.at(i - 1) != '_') break; if (!fragmentText.at(i - 1).isLetterOrNumber() && fragmentText.at(i - 1) != '_') break;
} }
break; break;
} }
if (tagId.isEmpty()) { if (tagId.isEmpty()) {
QTextCharFormat format = c.charFormat(); QTextCharFormat format = cursor.charFormat();
format.setAnchor(false); format.setAnchor(false);
format.setAnchorName(QString()); format.setAnchorName(QString());
format.clearForeground(); format.clearForeground();
c.insertText(data + ' ', format); cursor.insertText(text + ' ', format);
} else { } else {
_insertedTags.clear(); _insertedTags.clear();
_insertedTags.push_back({ 0, data.size(), tagId + '/' + QString::number(rand_value<uint32>()) }); if (_tagMimeProcessor) {
c.insertText(data + ' '); tagId = _tagMimeProcessor->mimeTagFromTag(tagId);
}
_insertedTags.push_back({ 0, text.size(), tagId });
cursor.insertText(text + ' ');
_insertedTags.clear(); _insertedTags.clear();
} }
} }
void FlatTextarea::setTagMimeProcessor(std_::unique_ptr<TagMimeProcessor> &&processor) {
_tagMimeProcessor = std_::move(processor);
}
void FlatTextarea::getSingleEmojiFragment(QString &text, QTextFragment &fragment) const { void FlatTextarea::getSingleEmojiFragment(QString &text, QTextFragment &fragment) const {
int32 end = textCursor().position(), start = end - 1; int32 end = textCursor().position(), start = end - 1;
if (textCursor().anchor() != end) return; if (textCursor().anchor() != end) return;
@ -544,25 +554,38 @@ public:
return _changed; return _changed;
} }
void feed(const QString &tagId, int currentPosition) { void feed(const QString &randomTagId, int currentPosition) {
if (tagId == _currentTagId) return; if (randomTagId == _currentTagId) return;
if (!_currentTagId.isEmpty()) { if (!_currentTagId.isEmpty()) {
FlatTextarea::Tag tag = { int randomPartPosition = _currentTagId.lastIndexOf('/');
_currentStart, t_assert(randomPartPosition > 0);
currentPosition - _currentStart,
_currentTagId, bool tagChanged = true;
}; if (_currentTag < _tags->size()) {
if (_currentTag >= _tags->size()) { auto &alreadyTag = _tags->at(_currentTag);
if (alreadyTag.offset == _currentStart &&
alreadyTag.length == currentPosition - _currentStart &&
alreadyTag.id == _currentTagId.midRef(0, randomPartPosition)) {
tagChanged = false;
}
}
if (tagChanged) {
_changed = true; _changed = true;
_tags->push_back(tag); FlatTextarea::Tag tag = {
} else if (_tags->at(_currentTag) != tag) { _currentStart,
_changed = true; currentPosition - _currentStart,
(*_tags)[_currentTag] = tag; _currentTagId.mid(0, randomPartPosition),
};
if (_currentTag < _tags->size()) {
(*_tags)[_currentTag] = tag;
} else {
_tags->push_back(tag);
}
} }
++_currentTag; ++_currentTag;
} }
_currentTagId = tagId; _currentTagId = randomTagId;
_currentStart = currentPosition; _currentStart = currentPosition;
}; };
@ -771,7 +794,7 @@ void FlatTextarea::parseLinks() { // some code is duplicated in text.cpp!
continue; continue;
} }
} }
newLinks.push_back(qMakePair(domainOffset - 1, p - start - domainOffset + 2)); newLinks.push_back({ domainOffset - 1, p - start - domainOffset + 2 });
offset = matchOffset = p - start; offset = matchOffset = p - start;
} }
@ -785,8 +808,8 @@ QStringList FlatTextarea::linksList() const {
QStringList result; QStringList result;
if (!_links.isEmpty()) { if (!_links.isEmpty()) {
QString text(toPlainText()); QString text(toPlainText());
for (LinkRanges::const_iterator i = _links.cbegin(), e = _links.cend(); i != e; ++i) { for_const (auto &link, _links) {
result.push_back(text.mid(i->first + 1, i->second - 2)); result.push_back(text.mid(link.start + 1, link.length - 2));
} }
} }
return result; return result;
@ -865,7 +888,7 @@ void removeTags(QTextDocument *document, int from, int end) {
} }
// Returns the position of the first inserted tag or "changedEnd" value if none found. // Returns the position of the first inserted tag or "changedEnd" value if none found.
int processInsertedTags(QTextDocument *document, int changedPosition, int changedEnd, const FlatTextarea::TagList &tags) { int processInsertedTags(QTextDocument *document, int changedPosition, int changedEnd, const FlatTextarea::TagList &tags, FlatTextarea::TagMimeProcessor *processor) {
int firstTagStart = changedEnd; int firstTagStart = changedEnd;
int applyNoTagFrom = changedEnd; int applyNoTagFrom = changedEnd;
for_const (auto &tag, tags) { for_const (auto &tag, tags) {
@ -873,7 +896,8 @@ int processInsertedTags(QTextDocument *document, int changedPosition, int change
int tagTo = tagFrom + tag.length; int tagTo = tagFrom + tag.length;
accumulate_max(tagFrom, changedPosition); accumulate_max(tagFrom, changedPosition);
accumulate_min(tagTo, changedEnd); accumulate_min(tagTo, changedEnd);
if (tagTo > tagFrom) { auto tagId = processor ? processor->tagFromMimeTag(tag.id) : tag.id;
if (tagTo > tagFrom && !tagId.isEmpty()) {
accumulate_min(firstTagStart, tagFrom); accumulate_min(firstTagStart, tagFrom);
prepareFormattingOptimization(document); prepareFormattingOptimization(document);
@ -886,7 +910,7 @@ int processInsertedTags(QTextDocument *document, int changedPosition, int change
QTextCharFormat format; QTextCharFormat format;
format.setAnchor(true); format.setAnchor(true);
format.setAnchorName(tag.id); format.setAnchorName(tagId + '/' + QString::number(rand_value<uint32>()));
format.setForeground(st::defaultTextStyle.linkFg); format.setForeground(st::defaultTextStyle.linkFg);
c.mergeCharFormat(format); c.mergeCharFormat(format);
@ -945,7 +969,7 @@ struct FormattingAction {
} // namespace } // namespace
void FlatTextarea::processFormatting(int changedPosition, int changedEnd) { void FlatTextarea::processFormatting(int insertPosition, int insertEnd) {
// Tilde formatting. // Tilde formatting.
auto regularFont = qsl("Open Sans"), semiboldFont = qsl("Open Sans Semibold"); auto regularFont = qsl("Open Sans"), semiboldFont = qsl("Open Sans Semibold");
bool tildeFormatting = !cRetina() && (font().pixelSize() == 13) && (font().family() == regularFont); bool tildeFormatting = !cRetina() && (font().pixelSize() == 13) && (font().family() == regularFont);
@ -958,13 +982,13 @@ void FlatTextarea::processFormatting(int changedPosition, int changedEnd) {
auto doc = document(); auto doc = document();
// Apply inserted tags. // Apply inserted tags.
int breakTagOnNotLetterTill = processInsertedTags(doc, changedPosition, changedEnd, _insertedTags); int breakTagOnNotLetterTill = processInsertedTags(doc, insertPosition, insertEnd, _insertedTags, _tagMimeProcessor.get());
using ActionType = FormattingAction::Type; using ActionType = FormattingAction::Type;
while (true) { while (true) {
FormattingAction action; FormattingAction action;
auto fromBlock = doc->findBlock(changedPosition); auto fromBlock = doc->findBlock(insertPosition);
auto tillBlock = doc->findBlock(changedEnd); auto tillBlock = doc->findBlock(insertEnd);
if (tillBlock.isValid()) tillBlock = tillBlock.next(); if (tillBlock.isValid()) tillBlock = tillBlock.next();
for (auto block = fromBlock; block != tillBlock; block = block.next()) { for (auto block = fromBlock; block != tillBlock; block = block.next()) {
@ -973,11 +997,11 @@ void FlatTextarea::processFormatting(int changedPosition, int changedEnd) {
t_assert(fragment.isValid()); t_assert(fragment.isValid());
int fragmentPosition = fragment.position(); int fragmentPosition = fragment.position();
if (changedPosition >= fragmentPosition + fragment.length()) { if (insertPosition >= fragmentPosition + fragment.length()) {
continue; continue;
} }
int changedPositionInFragment = changedPosition - fragmentPosition; // Can be negative. int changedPositionInFragment = insertPosition - fragmentPosition; // Can be negative.
int changedEndInFragment = changedEnd - fragmentPosition; int changedEndInFragment = insertEnd - fragmentPosition;
if (changedEndInFragment <= 0) { if (changedEndInFragment <= 0) {
break; break;
} }
@ -995,7 +1019,7 @@ void FlatTextarea::processFormatting(int changedPosition, int changedEnd) {
startTagFound = true; startTagFound = true;
auto tagName = charFormat.anchorName(); auto tagName = charFormat.anchorName();
if (!tagName.isEmpty()) { if (!tagName.isEmpty()) {
breakTagOnNotLetter = wasInsertTillTheEndOfTag(block, fragmentIt, changedEnd); breakTagOnNotLetter = wasInsertTillTheEndOfTag(block, fragmentIt, insertEnd);
} }
} }
@ -1058,7 +1082,7 @@ void FlatTextarea::processFormatting(int changedPosition, int changedEnd) {
c.setPosition(action.intervalEnd, QTextCursor::KeepAnchor); c.setPosition(action.intervalEnd, QTextCursor::KeepAnchor);
if (action.type == ActionType::InsertEmoji) { if (action.type == ActionType::InsertEmoji) {
insertEmoji(action.emoji, c); insertEmoji(action.emoji, c);
changedPosition = action.intervalStart + 1; insertPosition = action.intervalStart + 1;
} else if (action.type == ActionType::RemoveTag) { } else if (action.type == ActionType::RemoveTag) {
QTextCharFormat format; QTextCharFormat format;
format.setAnchor(false); format.setAnchor(false);
@ -1069,7 +1093,7 @@ void FlatTextarea::processFormatting(int changedPosition, int changedEnd) {
QTextCharFormat format; QTextCharFormat format;
format.setFontFamily(action.isTilde ? semiboldFont : regularFont); format.setFontFamily(action.isTilde ? semiboldFont : regularFont);
c.mergeCharFormat(format); c.mergeCharFormat(format);
changedPosition = action.intervalEnd; insertPosition = action.intervalEnd;
} }
} else { } else {
break; break;
@ -1083,6 +1107,9 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int
int insertPosition = (_realInsertPosition >= 0) ? _realInsertPosition : position; int insertPosition = (_realInsertPosition >= 0) ? _realInsertPosition : position;
int insertLength = (_realInsertPosition >= 0) ? _realCharsAdded : charsAdded; int insertLength = (_realInsertPosition >= 0) ? _realCharsAdded : charsAdded;
int removePosition = position;
int removeLength = charsRemoved;
QTextCursor(document()->docHandle(), 0).joinPreviousEditBlock(); QTextCursor(document()->docHandle(), 0).joinPreviousEditBlock();
_correcting = true; _correcting = true;
@ -1109,20 +1136,24 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int
} }
_correcting = false; _correcting = false;
if (!_links.isEmpty()) { if (insertPosition == removePosition) {
bool changed = false; if (!_links.isEmpty()) {
for (auto i = _links.begin(); i != _links.end();) { bool changed = false;
if (i->first + i->second <= insertPosition) { for (auto i = _links.begin(); i != _links.end();) {
++i; if (i->start + i->length <= insertPosition) {
} else if (i->first >= insertPosition + charsRemoved) { ++i;
i->first += insertLength - charsRemoved; } else if (i->start >= removePosition + removeLength) {
++i; i->start += insertLength - removeLength;
} else { ++i;
i = _links.erase(i); } else {
changed = true; i = _links.erase(i);
changed = true;
}
} }
if (changed) emit linksChanged();
} }
if (changed) emit linksChanged(); } else {
parseLinks();
} }
if (document()->availableRedoSteps() > 0) { if (document()->availableRedoSteps() > 0) {
@ -1223,7 +1254,12 @@ QMimeData *FlatTextarea::createMimeDataFromSelection() const {
TagList tags; TagList tags;
result->setText(getText(start, end, &tags, nullptr)); result->setText(getText(start, end, &tags, nullptr));
if (!tags.isEmpty()) { if (!tags.isEmpty()) {
result->setData(qsl("application/x-td-field-tags"), serializeTagsList(tags)); if (_tagMimeProcessor) {
for (auto &tag : tags) {
tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id);
}
}
result->setData(str_const_toString(TagsMimeType), serializeTagsList(tags));
} }
} }
return result; return result;

View File

@ -92,7 +92,16 @@ public:
const TagList &getLastTags() const { const TagList &getLastTags() const {
return _oldtags; return _oldtags;
} }
void insertMentionHashtagOrBotCommand(const QString &data, const QString &tagId = QString()); void insertTag(const QString &text, QString tagId = QString());
// If you need to make some preparations of tags before putting them to QMimeData
// (and then to clipboard or to drag-n-drop object), here is a strategy for that.
class TagMimeProcessor {
public:
virtual QString mimeTagFromTag(const QString &tagId) = 0;
virtual QString tagFromMimeTag(const QString &mimeTag) = 0;
};
void setTagMimeProcessor(std_::unique_ptr<TagMimeProcessor> &&processor);
public slots: public slots:
@ -177,6 +186,8 @@ private:
int _realInsertPosition = -1; int _realInsertPosition = -1;
int _realCharsAdded = 0; int _realCharsAdded = 0;
std_::unique_ptr<TagMimeProcessor> _tagMimeProcessor;
style::flatTextarea _st; style::flatTextarea _st;
bool _undoAvailable = false; bool _undoAvailable = false;
@ -194,8 +205,13 @@ private:
bool _correcting = false; bool _correcting = false;
typedef QPair<int, int> LinkRange; struct LinkRange {
typedef QList<LinkRange> LinkRanges; int start;
int length;
};
friend bool operator==(const LinkRange &a, const LinkRange &b);
friend bool operator!=(const LinkRange &a, const LinkRange &b);
using LinkRanges = QVector<LinkRange>;
LinkRanges _links; LinkRanges _links;
}; };
@ -205,3 +221,10 @@ inline bool operator==(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) {
inline bool operator!=(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) { inline bool operator!=(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) {
return !(a == b); return !(a == b);
} }
inline bool operator==(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) {
return (a.start == b.start) && (a.length == b.length);
}
inline bool operator!=(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) {
return !(a == b);
}