Fix editing text with custom emoji and formatting.

This commit is contained in:
John Preston 2022-07-01 12:55:26 +04:00
parent e6b3951b40
commit 6bd7518109
4 changed files with 213 additions and 58 deletions

View file

@ -1527,6 +1527,29 @@ bool CutPart(TextWithEntities &sending, TextWithEntities &left, int32 limit) {
return true;
}
MentionNameFields MentionNameDataToFields(QStringView data) {
const auto components = data.split('.');
if (components.size() != 2) {
return {};
}
const auto parts = components[1].split(':');
if (parts.size() != 2) {
return {};
}
return {
.selfId = parts[1].toULongLong(),
.userId = components[0].toULongLong(),
.accessHash = parts[0].toULongLong(),
};
}
QString MentionNameDataFromFields(const MentionNameFields &fields) {
return u"%1.%2:%3"_q
.arg(fields.userId)
.arg(fields.accessHash)
.arg(fields.selfId);
}
TextWithEntities ParseEntities(const QString &text, int32 flags) {
auto result = TextWithEntities{ text, EntitiesInText() };
ParseEntities(result, flags);
@ -1929,7 +1952,14 @@ bool IsMentionLink(QStringView link) {
return link.startsWith(kMentionTagStart);
}
[[nodiscard]] bool IsSeparateTag(QStringView tag) {
QString MentionEntityData(QStringView link) {
const auto match = qthelp::regex_match(
"^(\\d+\\.\\d+:\\d+)(/|$)",
base::StringViewMid(link, kMentionTagStart.size()));
return match ? match->captured(1) : QString();
}
bool IsSeparateTag(QStringView tag) {
return (tag == Ui::InputField::kTagCode)
|| (tag == Ui::InputField::kTagPre);
}
@ -1953,8 +1983,8 @@ QString JoinTag(const QList<QStringView> &list) {
return result;
}
QList<QStringView> SplitTags(const QString &tag) {
return QStringView(tag).split(kTagSeparator);
QList<QStringView> SplitTags(QStringView tag) {
return tag.split(kTagSeparator);
}
QString TagWithRemoved(const QString &tag, const QString &removed) {
@ -2073,11 +2103,9 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
openType(EntityType::CustomEmoji, data);
}
} else if (IsMentionLink(nextState.link)) {
const auto match = qthelp::regex_match(
"^(\\d+\\.\\d+)(/|$)",
base::StringViewMid(nextState.link, kMentionTagStart.size()));
if (match) {
openType(EntityType::MentionName, match->captured(1));
const auto data = MentionEntityData(nextState.link);
if (!data.isEmpty()) {
openType(EntityType::MentionName, data);
}
} else {
openType(EntityType::CustomUrl, nextState.link);
@ -2169,8 +2197,8 @@ TextWithTags::Tags ConvertEntitiesToTextTags(
};
switch (entity.type()) {
case EntityType::MentionName: {
auto match = QRegularExpression(
R"(^(\d+\.\d+)$)"
const auto match = QRegularExpression(
"^(\\d+\\.\\d+:\\d+)$"
).match(entity.data());
if (match.hasMatch()) {
push(kMentionTagStart + entity.data());
@ -2184,7 +2212,12 @@ TextWithTags::Tags ConvertEntitiesToTextTags(
}
} break;
case EntityType::CustomEmoji: {
push(Ui::InputField::CustomEmojiLink(entity.data()));
const auto match = QRegularExpression(
"^(\\d+\\.\\d+:\\d+/\\d+)$"
).match(entity.data());
if (match.hasMatch()) {
push(Ui::InputField::CustomEmojiLink(entity.data()));
}
} break;
case EntityType::Bold: push(Ui::InputField::kTagBold); break;
//case EntityType::Semibold: // Semibold is for UI parts only.

View file

@ -306,31 +306,13 @@ QStringList PrepareSearchWords(const QString &query, const QRegularExpression *S
bool CutPart(TextWithEntities &sending, TextWithEntities &left, int limit);
struct MentionNameFields {
MentionNameFields(uint64 userId = 0, uint64 accessHash = 0)
: userId(userId), accessHash(accessHash) {
}
uint64 selfId = 0;
uint64 userId = 0;
uint64 accessHash = 0;
};
inline MentionNameFields MentionNameDataToFields(const QString &data) {
auto components = data.split('.');
if (!components.isEmpty()) {
return {
components.at(0).toULongLong(),
(components.size() > 1) ? components.at(1).toULongLong() : 0
};
}
return MentionNameFields{};
}
inline QString MentionNameDataFromFields(const MentionNameFields &fields) {
auto result = QString::number(fields.userId);
if (fields.accessHash) {
result += '.' + QString::number(fields.accessHash);
}
return result;
}
[[nodiscard]] MentionNameFields MentionNameDataToFields(QStringView data);
[[nodiscard]] QString MentionNameDataFromFields(
const MentionNameFields &fields);
// New entities are added to the ones that are already in result.
// Changes text if (flags & TextParseMarkdown).
@ -362,12 +344,13 @@ void ApplyServerCleaning(TextWithEntities &result);
[[nodiscard]] QString TagsMimeType();
[[nodiscard]] QString TagsTextMimeType();
inline const auto kMentionTagStart = qstr("mention://user.");
inline const auto kMentionTagStart = qstr("mention://");
[[nodiscard]] bool IsMentionLink(QStringView link);
[[nodiscard]] QString MentionEntityData(QStringView link);
[[nodiscard]] bool IsSeparateTag(QStringView tag);
[[nodiscard]] QString JoinTag(const QList<QStringView> &list);
[[nodiscard]] QList<QStringView> SplitTags(const QString &tag);
[[nodiscard]] QList<QStringView> SplitTags(QStringView tag);
[[nodiscard]] QString TagWithRemoved(
const QString &tag,
const QString &removed);

View file

@ -38,6 +38,7 @@ constexpr auto kTagProperty = QTextFormat::UserProperty + 4;
constexpr auto kCustomEmojiFormat = QTextFormat::UserObject + 1;
constexpr auto kCustomEmojiText = QTextFormat::UserProperty + 5;
constexpr auto kCustomEmojiLink = QTextFormat::UserProperty + 6;
constexpr auto kCustomEmojiId = QTextFormat::UserProperty + 7;
const auto kObjectReplacementCh = QChar(QChar::ObjectReplacementCharacter);
const auto kObjectReplacement = QString::fromRawData(
&kObjectReplacementCh,
@ -127,6 +128,19 @@ bool IsNewline(QChar ch) {
.arg(++GlobalCustomEmojiCounter);
}
[[nodiscard]] uint64 CustomEmojiIdFromLink(QStringView link) {
const auto skip = Ui::InputField::kCustomEmojiTagStart.size();
if (const auto i = link.indexOf('/', skip + 1); i > 0) {
const auto j = link.indexOf('?', i + 1);
return base::StringViewMid(
link,
i + 1,
(j > i) ? (j - i - 1) : -1
).toULongLong();
}
return 0;
}
[[nodiscard]] QString CheckFullTextTag(
const TextWithTags &textWithTags,
const QString &tag) {
@ -733,6 +747,9 @@ QTextCharFormat PrepareTagFormat(
replaceWith = MakeUniqueCustomEmojiLink(tag);
result.setObjectType(kCustomEmojiFormat);
result.setProperty(kCustomEmojiLink, replaceWith);
result.setProperty(
kCustomEmojiId,
CustomEmojiIdFromLink(replaceWith));
} else if (IsValidMarkdownLink(tag)) {
color = st::defaultTextPalette.linkFg;
} else if (tag == kTagBold) {
@ -762,10 +779,40 @@ QTextCharFormat PrepareTagFormat(
: std::move(tag).replace(replaceWhat, replaceWith)));
if (bg) {
result.setBackground(*bg);
} else {
result.setBackground(QBrush());
}
return result;
}
[[nodiscard]] QString TagWithoutCustomEmoji(QStringView tag) {
auto tags = TextUtilities::SplitTags(tag);
for (auto i = tags.begin(); i != tags.end();) {
if (IsCustomEmojiLink(*i)) {
i = tags.erase(i);
} else {
++i;
}
}
return TextUtilities::JoinTag(tags);
}
void RemoveCustomEmojiTag(
const style::InputField &st,
not_null<QTextDocument*> document,
const QString &existingTags,
int from,
int end) {
auto cursor = QTextCursor(document);
cursor.setPosition(from);
cursor.setPosition(end, QTextCursor::KeepAnchor);
auto format = PrepareTagFormat(st, TagWithoutCustomEmoji(existingTags));
format.setProperty(kCustomEmojiLink, QString());
format.setProperty(kCustomEmojiId, QString());
cursor.mergeCharFormat(format);
}
void ApplyTagFormat(QTextCharFormat &to, const QTextCharFormat &from) {
to.setProperty(kTagProperty, from.property(kTagProperty));
to.setProperty(kReplaceTagId, from.property(kReplaceTagId));
@ -781,7 +828,7 @@ int ProcessInsertedTags(
int changedPosition,
int changedEnd,
const TextWithTags::Tags &tags,
InputField::TagMimeProcessor *processor) {
Fn<QString(QStringView)> processor) {
int firstTagStart = changedEnd;
int applyNoTagFrom = changedEnd;
for (const auto &tag : tags) {
@ -789,7 +836,7 @@ int ProcessInsertedTags(
int tagTo = tagFrom + tag.length;
accumulate_max(tagFrom, changedPosition);
accumulate_min(tagTo, changedEnd);
auto tagId = processor ? processor->tagFromMimeTag(tag.id) : tag.id;
auto tagId = processor ? processor(tag.id) : tag.id;
if (tagTo > tagFrom && !tagId.isEmpty()) {
accumulate_min(firstTagStart, tagFrom);
@ -833,7 +880,9 @@ bool WasInsertTillTheEndOfTag(
const auto outsideInsertion = (position >= insertionEnd);
if (outsideInsertion) {
const auto format = fragment.charFormat();
return (format.property(kTagProperty) != insertTagName);
const auto tag = format.property(kTagProperty).toString();
return TagWithoutCustomEmoji(tag)
!= TagWithoutCustomEmoji(insertTagName.toString());
}
const auto end = position + fragment.length();
const auto notFullFragmentInserted = (end > insertionEnd);
@ -857,6 +906,7 @@ struct FormattingAction {
Invalid,
InsertEmoji,
InsertCustomEmoji,
RemoveCustomEmoji,
TildeFont,
RemoveTag,
RemoveNewline,
@ -867,6 +917,7 @@ struct FormattingAction {
EmojiPtr emoji = nullptr;
bool isTilde = false;
QString tildeTag;
QString existingTags;
QString customEmojiText;
QString customEmojiLink;
int intervalStart = 0;
@ -949,6 +1000,7 @@ void InsertCustomEmojiAtCursor(
format.setObjectType(kCustomEmojiFormat);
format.setProperty(kCustomEmojiText, text);
format.setProperty(kCustomEmojiLink, MakeUniqueCustomEmojiLink(link));
format.setProperty(kCustomEmojiId, CustomEmojiIdFromLink(link));
format.setVerticalAlignment(QTextCharFormat::AlignBottom);
ApplyTagFormat(format, currentFormat);
cursor.insertText(kObjectReplacement, format);
@ -1315,9 +1367,14 @@ void FlatInput::onTextChange(const QString &text) {
Integration::Instance().textActionsUpdated();
}
CustomEmojiObject::CustomEmojiObject(QObject *parent) : QObject(parent) {
CustomEmojiObject::CustomEmojiObject(Factory factory, Fn<bool()> paused)
: _factory(std::move(factory))
, _paused(std::move(paused))
, _now(crl::now()) {
}
CustomEmojiObject::~CustomEmojiObject() = default;
QSizeF CustomEmojiObject::intrinsicSize(
QTextDocument *doc,
int posInDocument,
@ -1326,7 +1383,7 @@ QSizeF CustomEmojiObject::intrinsicSize(
const auto size = Emoji::GetSizeNormal() / factor;
const auto width = size + st::emojiPadding * 2.;
const auto font = format.toCharFormat().font();
const auto height = std::max(QFontMetrics(font).height() * 1., size);
const auto height = std::min(QFontMetrics(font).height() * 1., size);
return { width, height };
}
@ -1336,7 +1393,36 @@ void CustomEmojiObject::drawObject(
QTextDocument *doc,
int posInDocument,
const QTextFormat &format) {
painter->fillRect(rect, QColor(0, 128, 0, 128));
const auto id = format.property(kCustomEmojiId).toULongLong();
if (!id) {
return;
}
auto i = _emoji.find(id);
if (i == end(_emoji)) {
const auto link = format.property(kCustomEmojiLink).toString();
const auto data = InputField::CustomEmojiEntityData(link);
if (auto emoji = _factory(data)) {
i = _emoji.emplace(id, std::move(emoji)).first;
}
}
if (i == end(_emoji)) {
return;
}
i->second->paint(
*painter,
int(base::SafeRound(rect.x())) + st::emojiPadding,
int(base::SafeRound(rect.y())),
_now,
st::defaultTextPalette.spoilerActiveBg->c,
_paused());
}
void CustomEmojiObject::clear() {
_emoji.clear();
}
void CustomEmojiObject::setNow(crl::time now) {
_now = now;
}
InputField::InputField(
@ -1388,10 +1474,6 @@ InputField::InputField(
setAttribute(Qt::WA_OpaquePaintEvent);
}
_inner->document()->documentLayout()->registerHandler(
kCustomEmojiFormat,
new CustomEmojiObject(this));
_inner->setFont(_st.font->f);
_inner->setAlignment(_st.textAlign);
if (_mode == Mode::SingleLine) {
@ -1473,6 +1555,8 @@ bool InputField::viewportEventInner(QEvent *e) {
if (ev->device()->type() == base::TouchDevice::TouchScreen) {
handleTouchEvent(ev);
}
} else if (e->type() == QEvent::Paint && _customEmojiObject) {
_customEmojiObject->setNow(crl::now());
}
return _inner->QTextEdit::viewportEvent(e);
}
@ -1566,11 +1650,22 @@ void InputField::setMarkdownReplacesEnabled(rpl::producer<bool> enabled) {
}, lifetime());
}
void InputField::setTagMimeProcessor(
std::unique_ptr<TagMimeProcessor> &&processor) {
void InputField::setTagMimeProcessor(Fn<QString(QStringView)> processor) {
_tagMimeProcessor = std::move(processor);
}
void InputField::setCustomEmojiFactory(
CustomEmojiFactory factory,
Fn<bool()> paused) {
_customEmojiObject = std::make_unique<CustomEmojiObject>([=](
QStringView data) {
return factory(data, [=] { _inner->update(); });
}, std::move(paused));
_inner->document()->documentLayout()->registerHandler(
kCustomEmojiFormat,
_customEmojiObject.get());
}
void InputField::setAdditionalMargin(int margin) {
_additionalMargin = margin;
QResizeEvent e(size(), size());
@ -2105,8 +2200,8 @@ void InputField::processFormatting(int insertPosition, int insertEnd) {
auto document = _inner->document();
// Apply inserted tags.
auto insertedTagsProcessor = _insertedTagsAreFromMime
? _tagMimeProcessor.get()
const auto insertedTagsProcessor = _insertedTagsAreFromMime
? _tagMimeProcessor
: nullptr;
const auto breakTagOnNotLetterTill = ProcessInsertedTags(
_st,
@ -2176,6 +2271,7 @@ void InputField::processFormatting(int insertPosition, int insertEnd) {
action.customEmojiText = fragmentText;
action.customEmojiLink = format.property(
kCustomEmojiLink).toString();
break;
}
const auto with = format.property(kInstantReplaceWithId);
@ -2193,6 +2289,15 @@ void InputField::processFormatting(int insertPosition, int insertEnd) {
}
}
if (format.hasProperty(kCustomEmojiLink)
&& !format.property(kCustomEmojiLink).toString().isEmpty()) {
action.type = ActionType::RemoveCustomEmoji;
action.existingTags = format.property(kTagProperty).toString();
action.intervalStart = fragmentPosition;
action.intervalEnd = fragmentPosition
+ fragmentText.size();
break;
}
if (!startTagFound) {
startTagFound = true;
auto tagName = format.property(kTagProperty).toString();
@ -2319,6 +2424,13 @@ void InputField::processFormatting(int insertPosition, int insertEnd) {
document,
action.intervalStart,
action.intervalEnd);
} else if (action.type == ActionType::RemoveCustomEmoji) {
RemoveCustomEmojiTag(
_st,
document,
action.existingTags,
action.intervalStart,
action.intervalEnd);
} else if (action.type == ActionType::TildeFont) {
auto format = QTextCharFormat();
format.setFont(action.isTilde
@ -2476,6 +2588,11 @@ void InputField::handleContentsChanged() {
checkContentHeight();
}
startPlaceholderAnimation();
if (_lastTextWithTags.text.isEmpty()) {
if (const auto object = _customEmojiObject.get()) {
object->clear();
}
}
Integration::Instance().textActionsUpdated();
}
@ -2751,6 +2868,9 @@ TextWithTags InputField::getTextWithAppliedMarkdown() const {
void InputField::clear() {
_inner->clear();
startPlaceholderAnimation();
if (const auto object = _customEmojiObject.get()) {
object->clear();
}
}
bool InputField::hasFocus() const {
@ -3460,7 +3580,7 @@ QString InputField::CustomEmojiLink(QStringView entityData) {
QString InputField::CustomEmojiEntityData(QStringView link) {
const auto match = qthelp::regex_match(
"^(\\d+\\.\\d+/\\d+)(\\?|$)",
"^(\\d+\\.\\d+:\\d+/\\d+)(\\?|$)",
base::StringViewMid(link, kCustomEmojiTagStart.size()));
return match ? match->captured(1) : QString();
}

View file

@ -23,6 +23,10 @@
class QTouchEvent;
class Painter;
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Ui {
const auto kClearFormatSequence = QKeySequence("ctrl+shift+n");
@ -31,6 +35,10 @@ const auto kMonospaceSequence = QKeySequence("ctrl+shift+m");
const auto kEditLinkSequence = QKeySequence("ctrl+k");
const auto kSpoilerSequence = QKeySequence("ctrl+shift+p");
using CustomEmojiFactory = Fn<std::unique_ptr<Text::CustomEmoji>(
QStringView,
Fn<void()>)>;
class PopupMenu;
void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji);
@ -149,7 +157,10 @@ class CustomEmojiObject : public QObject, public QTextObjectInterface {
Q_INTERFACES(QTextObjectInterface)
public:
explicit CustomEmojiObject(QObject *parent);
using Factory = Fn<std::unique_ptr<Text::CustomEmoji>(QStringView)>;
CustomEmojiObject(Factory factory, Fn<bool()> paused);
~CustomEmojiObject();
QSizeF intrinsicSize(
QTextDocument *doc,
@ -162,6 +173,15 @@ public:
int posInDocument,
const QTextFormat &format) override;
void setNow(crl::time now);
void clear();
private:
Factory _factory;
Fn<bool()> _paused;
base::flat_map<uint64, std::unique_ptr<Text::CustomEmoji>> _emoji;
crl::time _now = 0;
};
class InputField : public RpWidget {
@ -245,12 +265,10 @@ public:
// 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 tagFromMimeTag(const QString &mimeTag) = 0;
virtual ~TagMimeProcessor() = default;
};
void setTagMimeProcessor(std::unique_ptr<TagMimeProcessor> &&processor);
void setTagMimeProcessor(Fn<QString(QStringView)> processor);
void setCustomEmojiFactory(
CustomEmojiFactory factory,
Fn<bool()> paused);
struct EditLinkSelection {
int from = 0;
@ -528,7 +546,8 @@ private:
// before _documentContentsChanges fire.
int _emojiSurrogateAmount = 0;
std::unique_ptr<TagMimeProcessor> _tagMimeProcessor;
Fn<QString(QStringView)> _tagMimeProcessor;
std::unique_ptr<CustomEmojiObject> _customEmojiObject;
SubmitSettings _submitSettings = SubmitSettings::Enter;
bool _markdownEnabled = false;