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;
}
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)
@ -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(stickerChosen(DocumentData*,FieldAutocomplete::ChooseMethod)), this, SLOT(onStickerSend(DocumentData*)));
_field.installEventFilter(_fieldAutocomplete);
_field.setTagMimeProcessor(std_::make_unique<FieldTagMimeProcessor>());
updateFieldSubmitSettings();
_field.hide();
@ -2940,7 +2967,7 @@ void HistoryWidget::onMentionInsert(UserData *user) {
} else {
replacement = '@' + user->username;
}
_field.insertMentionHashtagOrBotCommand(replacement, entityTag);
_field.insertTag(replacement, entityTag);
}
void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::ChooseMethod method) {
@ -2949,7 +2976,7 @@ void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::
App::sendBotCommand(_peer, nullptr, str);
setFieldText(_field.getLastText().mid(_field.textCursor().position()));
} else {
_field.insertMentionHashtagOrBotCommand(str);
_field.insertTag(str);
}
}

View File

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

View File

@ -92,7 +92,16 @@ public:
const TagList &getLastTags() const {
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:
@ -177,6 +186,8 @@ private:
int _realInsertPosition = -1;
int _realCharsAdded = 0;
std_::unique_ptr<TagMimeProcessor> _tagMimeProcessor;
style::flatTextarea _st;
bool _undoAvailable = false;
@ -194,8 +205,13 @@ private:
bool _correcting = false;
typedef QPair<int, int> LinkRange;
typedef QList<LinkRange> LinkRanges;
struct LinkRange {
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;
};
@ -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) {
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);
}