From 017ec87d60b38890bd9415aba0f009362fc41ff9 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 22 May 2018 00:31:46 +0300 Subject: [PATCH] Replace FlatTextarea with InputField. --- .../SourceFiles/boxes/add_contact_box.cpp | 2 +- .../SourceFiles/boxes/change_phone_box.cpp | 3 - .../SourceFiles/boxes/edit_caption_box.cpp | 2 +- Telegram/SourceFiles/boxes/rate_call_box.cpp | 2 +- Telegram/SourceFiles/boxes/report_box.cpp | 2 +- Telegram/SourceFiles/boxes/send_files_box.cpp | 2 +- .../chat_helpers/field_autocomplete.cpp | 17 +- .../chat_helpers/message_field.cpp | 352 ++- .../SourceFiles/chat_helpers/message_field.h | 81 +- Telegram/SourceFiles/data/data_drafts.cpp | 2 +- Telegram/SourceFiles/data/data_drafts.h | 4 +- Telegram/SourceFiles/data/data_types.cpp | 25 +- Telegram/SourceFiles/data/data_types.h | 18 +- Telegram/SourceFiles/history/history.style | 30 +- .../SourceFiles/history/history_widget.cpp | 349 ++- Telegram/SourceFiles/history/history_widget.h | 47 +- .../SourceFiles/info/info_wrap_widget.cpp | 3 - .../SourceFiles/mtproto/connection_tcp.cpp | 9 +- Telegram/SourceFiles/mtproto/connection_tcp.h | 2 +- Telegram/SourceFiles/rpl/operators_tests.cpp | 32 + Telegram/SourceFiles/rpl/rpl.h | 1 + Telegram/SourceFiles/rpl/skip.h | 61 + Telegram/SourceFiles/rpl/take.h | 3 +- .../SourceFiles/ui/widgets/input_fields.cpp | 2509 +++++------------ .../SourceFiles/ui/widgets/input_fields.h | 329 +-- Telegram/SourceFiles/ui/widgets/widgets.style | 16 - .../window/notifications_manager_default.cpp | 2 +- .../window/themes/window_theme_preview.cpp | 12 +- Telegram/gyp/tests/tests.gyp | 1 + 29 files changed, 1646 insertions(+), 2272 deletions(-) create mode 100644 Telegram/SourceFiles/rpl/skip.h diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 8533b8eaed..8290aa5d20 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -1072,7 +1072,7 @@ void EditBioBox::prepare() { addButton(langFactory(lng_settings_save), [this] { save(); }); addButton(langFactory(lng_cancel), [this] { closeBox(); }); _bio->setMaxLength(kMaxBioLength); - _bio->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); + _bio->setSubmitSettings(Ui::InputField::SubmitSettings::Both); auto cursor = _bio->textCursor(); cursor.setPosition(_bio->getLastText().size()); _bio->setTextCursor(cursor); diff --git a/Telegram/SourceFiles/boxes/change_phone_box.cpp b/Telegram/SourceFiles/boxes/change_phone_box.cpp index 5bf11ade85..2e5293dbab 100644 --- a/Telegram/SourceFiles/boxes/change_phone_box.cpp +++ b/Telegram/SourceFiles/boxes/change_phone_box.cpp @@ -7,9 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/change_phone_box.h" -#include -#include -#include #include "lang/lang_keys.h" #include "styles/style_boxes.h" #include "ui/widgets/labels.h" diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 7cb8e69b59..43c1485c6d 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -137,7 +137,7 @@ EditCaptionBox::EditCaptionBox( langFactory(lng_photo_caption), caption); _field->setMaxLength(MaxPhotoCaption); - _field->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); + _field->setSubmitSettings(Ui::InputField::SubmitSettings::Both); _field->setInstantReplaces(Ui::InstantReplaces::Default()); } diff --git a/Telegram/SourceFiles/boxes/rate_call_box.cpp b/Telegram/SourceFiles/boxes/rate_call_box.cpp index 0b811f7555..9babfefd51 100644 --- a/Telegram/SourceFiles/boxes/rate_call_box.cpp +++ b/Telegram/SourceFiles/boxes/rate_call_box.cpp @@ -77,7 +77,7 @@ void RateCallBox::ratingChanged(int value) { Ui::InputField::Mode::MultiLine, langFactory(lng_call_rate_comment)); _comment->show(); - _comment->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); + _comment->setSubmitSettings(Ui::InputField::SubmitSettings::Both); _comment->setMaxLength(MaxPhotoCaption); _comment->resize(width() - (st::callRatingPadding.left() + st::callRatingPadding.right()), _comment->height()); diff --git a/Telegram/SourceFiles/boxes/report_box.cpp b/Telegram/SourceFiles/boxes/report_box.cpp index 54e163acfd..5b5cee6fef 100644 --- a/Telegram/SourceFiles/boxes/report_box.cpp +++ b/Telegram/SourceFiles/boxes/report_box.cpp @@ -88,7 +88,7 @@ void ReportBox::reasonChanged(Reason reason) { Ui::InputField::Mode::MultiLine, langFactory(lng_report_reason_description)); _reasonOtherText->show(); - _reasonOtherText->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); + _reasonOtherText->setSubmitSettings(Ui::InputField::SubmitSettings::Both); _reasonOtherText->setMaxLength(MaxPhotoCaption); _reasonOtherText->resize(width() - (st::boxPadding.left() + st::boxOptionListPadding.left() + st::boxPadding.right()), _reasonOtherText->height()); diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 69fa8ff576..354824d497 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -1556,7 +1556,7 @@ void SendFilesBox::setupCaption() { Ui::InputField::Mode::MultiLine, FieldPlaceholder(_list)); _caption->setMaxLength(MaxPhotoCaption); - _caption->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); + _caption->setSubmitSettings(Ui::InputField::SubmitSettings::Both); connect(_caption, &Ui::InputField::resized, this, [this] { captionResized(); }); diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 105a2fdf8f..1c786f6c91 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -523,10 +523,19 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) { QRect r(e->rect()); if (r != rect()) p.setClipRect(r); - int32 atwidth = st::mentionFont->width('@'), hashwidth = st::mentionFont->width('#'); - int32 mentionleft = 2 * st::mentionPadding.left() + st::mentionPhotoSize; - int32 mentionwidth = width() - mentionleft - 2 * st::mentionPadding.right(); - int32 htagleft = st::historyAttach.width + st::historyComposeField.textMrg.left() - st::lineWidth, htagwidth = width() - st::mentionPadding.right() - htagleft - st::mentionScroll.width; + auto atwidth = st::mentionFont->width('@'); + auto hashwidth = st::mentionFont->width('#'); + auto mentionleft = 2 * st::mentionPadding.left() + st::mentionPhotoSize; + auto mentionwidth = width() + - mentionleft + - 2 * st::mentionPadding.right(); + auto htagleft = st::historyAttach.width + + st::historyComposeField.textMargins.left() + - st::lineWidth; + auto htagwidth = width() + - st::mentionPadding.right() + - htagleft + - st::mentionScroll.width; if (!_srows->empty()) { int32 rows = rowscount(_srows->size(), _stickersPerRow); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 7e2b12028b..f53abfa620 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -16,8 +16,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { +constexpr auto kParseLinksTimeout = TimeMs(1000); + // For mention tags save and validate userId, ignore tags for different userId. -class FieldTagMimeProcessor : public Ui::FlatTextarea::TagMimeProcessor { +class FieldTagMimeProcessor : public Ui::InputField::TagMimeProcessor { public: QString mimeTagFromTag(const QString &tagId) override { return ConvertTagToMimeTag(tagId); @@ -110,66 +112,336 @@ void SetClipboardWithEntities( } } -MessageField::MessageField(QWidget *parent, not_null controller, const style::FlatTextarea &st, base::lambda placeholderFactory) -: FlatTextarea(parent, st, std::move(placeholderFactory)) -, _controller(controller) { - setMinHeight(st::historySendSize.height() - 2 * st::historySendPadding); - setMaxHeight(st::historyComposeFieldMaxHeight); +void InitMessageField(not_null field) { + field->setMinHeight(st::historySendSize.height() - 2 * st::historySendPadding); + field->setMaxHeight(st::historyComposeFieldMaxHeight); - setTagMimeProcessor(std::make_unique()); + field->setTagMimeProcessor(std::make_unique()); - setInstantReplaces(Ui::InstantReplaces::Default()); - enableInstantReplaces(Global::ReplaceEmoji()); - subscribe(Global::RefReplaceEmojiChanged(), [=] { - enableInstantReplaces(Global::ReplaceEmoji()); - }); + field->document()->setDocumentMargin(4.); + const auto additional = convertScale(4) - 4; + field->rawTextEdit()->setStyleSheet( + qsl("QTextEdit { margin: %1px; }").arg(additional)); + + field->setInstantReplaces(Ui::InstantReplaces::Default()); + field->enableInstantReplaces(Global::ReplaceEmoji()); + auto &changed = Global::RefReplaceEmojiChanged(); + Ui::AttachAsChild(field, changed.add_subscription([=] { + field->enableInstantReplaces(Global::ReplaceEmoji()); + })); + field->window()->activateWindow(); } -bool MessageField::hasSendText() const { - auto &text = getTextWithTags().text; - for (auto *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) { - auto code = ch->unicode(); - if (code != ' ' && code != '\n' && code != '\r' && !chReplacedBySpace(code)) { +bool HasSendText(not_null field) { + const auto &text = field->getTextWithTags().text; + for (const auto ch : text) { + const auto code = ch.unicode(); + if (code != ' ' + && code != '\n' + && code != '\r' + && !chReplacedBySpace(code)) { return true; } } return false; } -void MessageField::onEmojiInsert(EmojiPtr emoji) { - if (isHidden()) return; - insertEmoji(emoji, textCursor()); -} +InlineBotQuery ParseInlineBotQuery(not_null field) { + auto result = InlineBotQuery(); -void MessageField::dropEvent(QDropEvent *e) { - FlatTextarea::dropEvent(e); - if (e->isAccepted()) { - _controller->window()->activateWindow(); + const auto &text = field->getTextWithTags().text; + const auto textLength = text.size(); + + auto inlineUsernameStart = 1; + auto inlineUsernameLength = 0; + if (textLength > 2 && text[0] == '@' && text[1].isLetter()) { + inlineUsernameLength = 1; + for (auto i = inlineUsernameStart + 1; i != textLength; ++i) { + const auto ch = text[i]; + if (ch.isLetterOrNumber() || ch.unicode() == '_') { + ++inlineUsernameLength; + continue; + } else if (!ch.isSpace()) { + inlineUsernameLength = 0; + } + break; + } + auto inlineUsernameEnd = inlineUsernameStart + inlineUsernameLength; + auto inlineUsernameEqualsText = (inlineUsernameEnd == textLength); + auto validInlineUsername = false; + if (inlineUsernameEqualsText) { + validInlineUsername = text.endsWith(qstr("bot")); + } else if (inlineUsernameEnd < textLength && inlineUsernameLength) { + validInlineUsername = text[inlineUsernameEnd].isSpace(); + } + if (validInlineUsername) { + auto username = text.midRef(inlineUsernameStart, inlineUsernameLength); + if (username != result.username) { + result.username = username.toString(); + if (const auto peer = App::peerByName(result.username)) { + if (const auto user = peer->asUser()) { + result.bot = peer->asUser(); + } else { + result.bot = nullptr; + } + result.lookingUpBot = false; + } else { + result.bot = nullptr; + result.lookingUpBot = true; + } + } + if (result.lookingUpBot) { + result.query = QString(); + return result; + } else if (result.bot && (!result.bot->botInfo + || result.bot->botInfo->inlinePlaceholder.isEmpty())) { + result.bot = nullptr; + } else { + result.query = inlineUsernameEqualsText + ? QString() + : text.mid(inlineUsernameEnd + 1); + return result; + } + } else { + inlineUsernameLength = 0; + } } + if (inlineUsernameLength < 3) { + result.bot = nullptr; + result.username = QString(); + } + result.query = QString(); + return result; } -bool MessageField::canInsertFromMimeData(const QMimeData *source) const { - if (source->hasUrls()) { - int32 files = 0; - for (int32 i = 0; i < source->urls().size(); ++i) { - if (source->urls().at(i).isLocalFile()) { - ++files; +AutocompleteQuery ParseMentionHashtagBotCommandQuery( + not_null field) { + auto result = AutocompleteQuery(); + + const auto cursor = field->textCursor(); + const auto position = cursor.position(); + if (cursor.anchor() != position) { + return result; + } + + const auto document = field->document(); + const auto block = document->findBlock(position); + for (auto item = block.begin(); !item.atEnd(); ++item) { + const auto fragment = item.fragment(); + if (!fragment.isValid()) { + continue; + } + + const auto fragmentPosition = fragment.position(); + const auto fragmentEnd = fragmentPosition + fragment.length(); + if (fragmentPosition >= position || fragmentEnd < position) { + continue; + } + + const auto format = fragment.charFormat(); + if (format.isImageFormat()) { + continue; + } + + bool mentionInCommand = false; + const auto text = fragment.text(); + for (auto i = position - fragmentPosition; i != 0; --i) { + if (text[i - 1] == '@') { + if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_'))) { + result.fromStart = (i == 1) && (fragmentPosition == 0); + result.query = text.mid(i - 1, position - fragmentPosition - i + 1); + } else if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && i > 2 && (text[i - 2].isLetterOrNumber() || text[i - 2] == '_') && !mentionInCommand) { + mentionInCommand = true; + --i; + continue; + } + return result; + } else if (text[i - 1] == '#') { + if (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_')) { + result.fromStart = (i == 1) && (fragmentPosition == 0); + result.query = text.mid(i - 1, position - fragmentPosition - i + 1); + } + return result; + } else if (text[i - 1] == '/') { + if (i < 2) { + result.fromStart = (i == 1) && (fragmentPosition == 0); + result.query = text.mid(i - 1, position - fragmentPosition - i + 1); + } + return result; + } + if (position - fragmentPosition - i > 127 || (!mentionInCommand && (position - fragmentPosition - i > 63))) { + break; + } + if (!text[i - 1].isLetterOrNumber() && text[i - 1] != '_') { + break; } } - if (files > 1) return false; // multiple confirm with "compressed" checkbox + break; } - if (source->hasImage()) return true; - return FlatTextarea::canInsertFromMimeData(source); + return result; } -void MessageField::insertFromMimeData(const QMimeData *source) { - if (_insertFromMimeDataHook && _insertFromMimeDataHook(source)) { +QtConnectionOwner::QtConnectionOwner(QMetaObject::Connection connection) +: _data(connection) { +} + +QtConnectionOwner::QtConnectionOwner(QtConnectionOwner &&other) +: _data(base::take(other._data)) { +} + +QtConnectionOwner &QtConnectionOwner::operator=(QtConnectionOwner &&other) { + disconnect(); + _data = base::take(other._data); + return *this; +} + +void QtConnectionOwner::disconnect() { + QObject::disconnect(base::take(_data)); +} + +QtConnectionOwner::~QtConnectionOwner() { + disconnect(); +} + +MessageLinksParser::MessageLinksParser(not_null field) +: _field(field) +, _timer([=] { parse(); }) { + _connection = QObject::connect(_field, &Ui::InputField::changed, [=] { + const auto length = _field->getTextWithTags().text.size(); + const auto timeout = (std::abs(length - _lastLength) > 2) + ? 0 + : kParseLinksTimeout; + if (!_timer.isActive() || timeout < _timer.remainingTime()) { + _timer.callOnce(timeout); + } + _lastLength = length; + }); + _field->installEventFilter(this); +} + +bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) { + if (object == _field) { + if (event->type() == QEvent::KeyPress) { + const auto text = static_cast(event)->text(); + if (!text.isEmpty() && text.size() < 3) { + const auto ch = text[0]; + if (false + || ch == '\n' + || ch == '\r' + || ch.isSpace() + || ch == QChar::LineSeparator) { + _timer.callOnce(0); + } + } + } else if (event->type() == QEvent::Drop) { + _timer.callOnce(0); + } + } + return QObject::eventFilter(object, event); +} + +const rpl::variable &MessageLinksParser::list() const { + return _list; +} + +void MessageLinksParser::parse() { + const auto &text = _field->getTextWithTags().text; + if (text.isEmpty()) { + _list = QStringList(); return; } - FlatTextarea::insertFromMimeData(source); + + auto ranges = QVector(); + const auto len = text.size(); + const QChar *start = text.unicode(), *end = start + text.size(); + for (auto offset = 0, matchOffset = offset; offset < len;) { + auto m = TextUtilities::RegExpDomain().match(text, matchOffset); + if (!m.hasMatch()) break; + + auto domainOffset = m.capturedStart(); + + auto protocol = m.captured(1).toLower(); + auto topDomain = m.captured(3).toLower(); + auto isProtocolValid = protocol.isEmpty() || TextUtilities::IsValidProtocol(protocol); + auto isTopDomainValid = !protocol.isEmpty() || TextUtilities::IsValidTopDomain(topDomain); + + if (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) { + auto forMailName = text.mid(offset, domainOffset - offset - 1); + auto mMailName = TextUtilities::RegExpMailNameAtEnd().match(forMailName); + if (mMailName.hasMatch()) { + offset = matchOffset = m.capturedEnd(); + continue; + } + } + if (!isProtocolValid || !isTopDomainValid) { + offset = matchOffset = m.capturedEnd(); + continue; + } + + QStack parenth; + const QChar *domainEnd = start + m.capturedEnd(), *p = domainEnd; + for (; p < end; ++p) { + QChar ch(*p); + if (chIsLinkEnd(ch)) break; // link finished + if (chIsAlmostLinkEnd(ch)) { + const QChar *endTest = p + 1; + while (endTest < end && chIsAlmostLinkEnd(*endTest)) { + ++endTest; + } + if (endTest >= end || chIsLinkEnd(*endTest)) { + break; // link finished at p + } + p = endTest; + ch = *p; + } + if (ch == '(' || ch == '[' || ch == '{' || ch == '<') { + parenth.push(p); + } else if (ch == ')' || ch == ']' || ch == '}' || ch == '>') { + if (parenth.isEmpty()) break; + const QChar *q = parenth.pop(), open(*q); + if ((ch == ')' && open != '(') || (ch == ']' && open != '[') || (ch == '}' && open != '{') || (ch == '>' && open != '<')) { + p = q; + break; + } + } + } + if (p > domainEnd) { // check, that domain ended + if (domainEnd->unicode() != '/' && domainEnd->unicode() != '?') { + matchOffset = domainEnd - start; + continue; + } + } + ranges.push_back({ domainOffset, static_cast(p - start - domainOffset) }); + offset = matchOffset = p - start; + } + + apply(text, ranges); } -void MessageField::focusInEvent(QFocusEvent *e) { - FlatTextarea::focusInEvent(e); - emit focused(); +void MessageLinksParser::apply( + const QString &text, + const QVector &ranges) { + const auto count = int(ranges.size()); + const auto current = _list.current(); + const auto changed = [&] { + if (current.size() != count) { + return true; + } + for (auto i = 0; i != count; ++i) { + const auto &range = ranges[i]; + if (text.midRef(range.start, range.length) != current[i]) { + return true; + } + } + return false; + }(); + if (!changed) { + return; + } + auto parsed = QStringList(); + parsed.reserve(count); + for (const auto &range : ranges) { + parsed.push_back(text.mid(range.start, range.length)); + } + _list = std::move(parsed); } diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 0cb9b0434f..45053a6753 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "ui/widgets/input_fields.h" +#include "base/timer.h" class HistoryWidget; namespace Window { @@ -25,32 +26,66 @@ void SetClipboardWithEntities( const TextWithEntities &forClipboard, QClipboard::Mode mode = QClipboard::Clipboard); -class MessageField final : public Ui::FlatTextarea { - Q_OBJECT +void InitMessageField(not_null field); +bool HasSendText(not_null field); +struct InlineBotQuery { + QString query; + QString username; + UserData *bot = nullptr; + bool lookingUpBot = false; +}; +InlineBotQuery ParseInlineBotQuery(not_null field); + +struct AutocompleteQuery { + QString query; + bool fromStart = false; +}; +AutocompleteQuery ParseMentionHashtagBotCommandQuery( + not_null field); + +class QtConnectionOwner { public: - MessageField(QWidget *parent, not_null controller, const style::FlatTextarea &st, base::lambda placeholderFactory = nullptr); - - bool hasSendText() const; - - void setInsertFromMimeDataHook(base::lambda hook) { - _insertFromMimeDataHook = std::move(hook); - } - -public slots: - void onEmojiInsert(EmojiPtr emoji); - -signals: - void focused(); - -protected: - void focusInEvent(QFocusEvent *e) override; - void dropEvent(QDropEvent *e) override; - bool canInsertFromMimeData(const QMimeData *source) const override; - void insertFromMimeData(const QMimeData *source) override; + QtConnectionOwner(QMetaObject::Connection connection = {}); + QtConnectionOwner(QtConnectionOwner &&other); + QtConnectionOwner &operator=(QtConnectionOwner &&other); + ~QtConnectionOwner(); private: - not_null _controller; - base::lambda _insertFromMimeDataHook; + void disconnect(); + + QMetaObject::Connection _data; }; + +class MessageLinksParser : private QObject { +public: + MessageLinksParser(not_null field); + + const rpl::variable &list() const; + +protected: + bool eventFilter(QObject *object, QEvent *event) override; + +private: + struct LinkRange { + int start; + int length; + }; + friend inline bool operator==(const LinkRange &a, const LinkRange &b) { + return (a.start == b.start) && (a.length == b.length); + } + friend inline bool operator!=(const LinkRange &a, const LinkRange &b) { + return !(a == b); + } + + void parse(); + void apply(const QString &text, const QVector &ranges); + + not_null _field; + rpl::variable _list; + int _lastLength = 0; + base::Timer _timer; + QtConnectionOwner _connection; + +}; \ No newline at end of file diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp index d5b441552d..e6630b824e 100644 --- a/Telegram/SourceFiles/data/data_drafts.cpp +++ b/Telegram/SourceFiles/data/data_drafts.cpp @@ -33,7 +33,7 @@ Draft::Draft( } Draft::Draft( - not_null field, + not_null field, MsgId msgId, bool previewCancelled, mtpRequestId saveRequestId) diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h index cd3c84d747..84bc4859ff 100644 --- a/Telegram/SourceFiles/data/data_drafts.h +++ b/Telegram/SourceFiles/data/data_drafts.h @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once namespace Ui { -class FlatTextarea; +class InputField; } // namespace Ui namespace Data { @@ -25,7 +25,7 @@ struct Draft { bool previewCancelled, mtpRequestId saveRequestId = 0); Draft( - not_null field, + not_null field, MsgId msgId, bool previewCancelled, mtpRequestId saveRequestId = 0); diff --git a/Telegram/SourceFiles/data/data_types.cpp b/Telegram/SourceFiles/data/data_types.cpp index 9438d4b9f9..3e71bc1cc5 100644 --- a/Telegram/SourceFiles/data/data_types.cpp +++ b/Telegram/SourceFiles/data/data_types.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_types.h" #include "data/data_document.h" +#include "ui/widgets/input_fields.h" void AudioMsgId::setTypeFromAudio() { if (_audio->isVoiceMessage() || _audio->isVideoMessage()) { @@ -21,24 +22,20 @@ void AudioMsgId::setTypeFromAudio() { } } -void MessageCursor::fillFrom(const QTextEdit *edit) { - QTextCursor c = edit->textCursor(); - position = c.position(); - anchor = c.anchor(); - QScrollBar *s = edit->verticalScrollBar(); - scroll = (s && (s->value() != s->maximum())) - ? s->value() - : QFIXED_MAX; +void MessageCursor::fillFrom(not_null field) { + const auto cursor = field->textCursor(); + position = cursor.position(); + anchor = cursor.anchor(); + const auto top = field->scrollTop().current(); + scroll = (top != field->scrollTopMax()) ? top : QFIXED_MAX; } -void MessageCursor::applyTo(QTextEdit *edit) { - auto cursor = edit->textCursor(); +void MessageCursor::applyTo(not_null field) { + auto cursor = field->textCursor(); cursor.setPosition(anchor, QTextCursor::MoveAnchor); cursor.setPosition(position, QTextCursor::KeepAnchor); - edit->setTextCursor(cursor); - if (auto scrollbar = edit->verticalScrollBar()) { - scrollbar->setValue(scroll); - } + field->setTextCursor(cursor); + field->scrollTo(scroll); } HistoryItem *FileClickHandler::getActionItem() const { diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 32b92e19ba..1a8f2723f4 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class HistoryItem; using HistoryItemsList = std::vector>; +namespace Ui { +class InputField; +} // namespace Ui + namespace Data { struct UploadState { @@ -384,16 +388,16 @@ inline MsgId clientMsgId() { struct MessageCursor { MessageCursor() = default; MessageCursor(int position, int anchor, int scroll) - : position(position) - , anchor(anchor) - , scroll(scroll) { + : position(position) + , anchor(anchor) + , scroll(scroll) { } - MessageCursor(const QTextEdit *edit) { - fillFrom(edit); + MessageCursor(not_null field) { + fillFrom(field); } - void fillFrom(const QTextEdit *edit); - void applyTo(QTextEdit *edit); + void fillFrom(not_null field); + void applyTo(not_null field); int position = 0; int anchor = 0; diff --git a/Telegram/SourceFiles/history/history.style b/Telegram/SourceFiles/history/history.style index e98609e1a4..a4f085cdcf 100644 --- a/Telegram/SourceFiles/history/history.style +++ b/Telegram/SourceFiles/history/history.style @@ -131,19 +131,25 @@ historyViewsOutIcon: icon {{ "history_views", historyOutIconFg }}; historyViewsOutSelectedIcon: icon {{ "history_views", historyOutIconFgSelected }}; historyViewsInvertedIcon: icon {{ "history_views", historySendingInvertedIconFg }}; -historyComposeField: FlatTextarea { - textColor: historyComposeAreaFg; - bgColor: historyComposeAreaBg; - align: align(left); - textMrg: margins(5px, 5px, 5px, 5px); +historyComposeField: InputField(defaultInputField) { font: msgFont; - - phColor: placeholderFg; - phFocusColor: placeholderFgActive; - phAlign: align(topleft); - phPos: point(2px, 0px); - phShift: 50px; - phDuration: 100; + textMargins: margins(0px, 0px, 0px, 0px); + textAlign: align(left); + textFg: historyComposeAreaFg; + textBg: historyComposeAreaBg; + heightMin: 36px; + heightMax: 72px; + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(7px, 5px, 7px, 5px); + placeholderAlign: align(topleft); + placeholderScale: 0.; + placeholderFont: normalFont; + placeholderShift: -50px; + border: 0px; + borderActive: 0px; + duration: 100; } historyComposeFieldMaxHeight: 224px; // historyMinHeight: 56px; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 6efb1c4e02..67aa0c7308 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -110,6 +110,12 @@ void ActivateWindowDelayed(not_null controller) { }); } +void InsertEmojiToField(not_null field, EmojiPtr emoji) { + if (!field->isHidden()) { + Ui::InsertEmojiAtCursor(field->textCursor(), emoji); + } +} + } // namespace ReportSpamPanel::ReportSpamPanel(QWidget *parent) : TWidget(parent), @@ -406,6 +412,7 @@ HistoryWidget::HistoryWidget( not_null controller) : Window::AbstractSectionWidget(parent, controller) , _fieldBarCancel(this, st::historyReplyCancel) +, _previewTimer([=] { requestPreview(); }) , _topBar(this, controller) , _scroll(this, st::historyScroll, false) , _historyDown(_scroll, st::historyToDown) @@ -421,7 +428,11 @@ HistoryWidget::HistoryWidget( , _botKeyboardShow(this, st::historyBotKeyboardShow) , _botKeyboardHide(this, st::historyBotKeyboardHide) , _botCommandStart(this, st::historyBotCommandStart) -, _field(this, controller, st::historyComposeField, langFactory(lng_message_ph)) +, _field( + this, + st::historyComposeField, + Ui::InputField::Mode::MultiLine, + langFactory(lng_message_ph)) , _recordCancelWidth(st::historyRecordFont->width(lang(lng_record_cancel))) , _a_recording(animation(this, &HistoryWidget::step_recording)) , _kbScroll(this, st::botKbScroll) @@ -444,21 +455,22 @@ HistoryWidget::HistoryWidget( connect(_botStart, SIGNAL(clicked()), this, SLOT(onBotStart())); connect(_joinChannel, SIGNAL(clicked()), this, SLOT(onJoinChannel())); connect(_muteUnmute, SIGNAL(clicked()), this, SLOT(onMuteUnmute())); - connect(_field, SIGNAL(submitted(bool)), this, SLOT(onSend(bool))); + connect(_field, SIGNAL(submitted(bool)), this, SLOT(onSend())); connect(_field, SIGNAL(cancelled()), this, SLOT(onCancel())); connect(_field, SIGNAL(tabbed()), this, SLOT(onFieldTabbed())); connect(_field, SIGNAL(resized()), this, SLOT(onFieldResize())); connect(_field, SIGNAL(focused()), this, SLOT(onFieldFocused())); connect(_field, SIGNAL(changed()), this, SLOT(onTextChange())); - connect(_field, SIGNAL(spacedReturnedPasted()), this, SLOT(onPreviewParse())); - connect(_field, SIGNAL(linksChanged()), this, SLOT(onPreviewCheck())); connect(App::wnd()->windowHandle(), SIGNAL(visibleChanged(bool)), this, SLOT(onWindowVisibleChanged())); connect(&_scrollTimer, SIGNAL(timeout()), this, SLOT(onScrollTimer())); - connect(_tabbedSelector, SIGNAL(emojiSelected(EmojiPtr)), _field, SLOT(onEmojiInsert(EmojiPtr))); + connect( + _tabbedSelector, + &TabbedSelector::emojiSelected, + _field, + [=](EmojiPtr emoji) { InsertEmojiToField(_field, emoji); }); connect(_tabbedSelector, SIGNAL(stickerSelected(DocumentData*)), this, SLOT(onStickerSend(DocumentData*))); connect(_tabbedSelector, SIGNAL(photoSelected(PhotoData*)), this, SLOT(onPhotoSend(PhotoData*))); connect(_tabbedSelector, SIGNAL(inlineResultSelected(InlineBots::Result*,UserData*)), this, SLOT(onInlineResultSend(InlineBots::Result*,UserData*))); - connect(&_previewTimer, SIGNAL(timeout()), this, SLOT(onPreviewTimeout())); connect(Media::Capture::instance(), SIGNAL(error()), this, SLOT(onRecordError())); connect(Media::Capture::instance(), SIGNAL(updated(quint16,qint32)), this, SLOT(onRecordUpdate(quint16,qint32))); connect(Media::Capture::instance(), SIGNAL(done(QByteArray,VoiceWaveform,qint32)), this, SLOT(onRecordDone(QByteArray,VoiceWaveform,qint32))); @@ -481,9 +493,12 @@ HistoryWidget::HistoryWidget( connect(&_saveDraftTimer, SIGNAL(timeout()), this, SLOT(onDraftSave())); _saveCloudDraftTimer.setSingleShot(true); connect(&_saveCloudDraftTimer, SIGNAL(timeout()), this, SLOT(onCloudDraftSave())); - connect(_field->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(onDraftSaveDelayed())); - connect(_field, SIGNAL(cursorPositionChanged()), this, SLOT(onDraftSaveDelayed())); - connect(_field, SIGNAL(cursorPositionChanged()), this, SLOT(onCheckFieldAutocomplete()), Qt::QueuedConnection); + _field->scrollTop().changes( + ) | rpl::start_with_next([=] { + onDraftSaveDelayed(); + }, _field->lifetime()); + connect(_field->rawTextEdit(), SIGNAL(cursorPositionChanged()), this, SLOT(onDraftSaveDelayed())); + connect(_field->rawTextEdit(), SIGNAL(cursorPositionChanged()), this, SLOT(onCheckFieldAutocomplete()), Qt::QueuedConnection); _fieldBarCancel->hide(); @@ -498,20 +513,35 @@ HistoryWidget::HistoryWidget( _historyDown->installEventFilter(this); _unreadMentions->installEventFilter(this); + InitMessageField(_field); _fieldAutocomplete->hide(); connect(_fieldAutocomplete, SIGNAL(mentionChosen(UserData*,FieldAutocomplete::ChooseMethod)), this, SLOT(onMentionInsert(UserData*))); connect(_fieldAutocomplete, SIGNAL(hashtagChosen(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(moderateKeyActivate(int,bool*)), this, SLOT(onModerateKeyActivate(int,bool*))); - _field->installEventFilter(_fieldAutocomplete); - _field->setInsertFromMimeDataHook([this](const QMimeData *data) { - return confirmSendingFiles( - data, - CompressConfirm::Auto, - data->text()); + _fieldLinksParser = std::make_unique(_field); + _fieldLinksParser->list().changes( + ) | rpl::start_with_next([=](QStringList &&parsed) { + _parsedLinks = std::move(parsed); + checkPreview(); + }, lifetime()); + _field->rawTextEdit()->installEventFilter(_fieldAutocomplete); + _field->setMimeDataHook([=]( + not_null data, + Ui::InputField::MimeAction action) { + if (action == Ui::InputField::MimeAction::Check) { + return canSendFiles(data); + } else if (action == Ui::InputField::MimeAction::Insert) { + return confirmSendingFiles( + data, + CompressConfirm::Auto, + data->text()); + } + Unexpected("action in MimeData hook."); }); - _emojiSuggestions.create(this, _field.data()); + + _emojiSuggestions.create(this, _field->rawTextEdit()); _emojiSuggestions->setReplaceCallback([=]( int from, int till, @@ -622,7 +652,7 @@ HistoryWidget::HistoryWidget( subscribe(Notify::PeerUpdated(), Notify::PeerUpdatedHandler(changes, [this](const Notify::PeerUpdate &update) { if (update.peer == _peer) { if (update.flags & UpdateFlag::ChannelRightsChanged) { - onPreviewCheck(); + checkPreview(); } if (update.flags & UpdateFlag::UnreadMentionsChanged) { updateUnreadMentionsVisibility(); @@ -995,36 +1025,41 @@ void HistoryWidget::onHashtagOrBotCommandInsert( } void HistoryWidget::updateInlineBotQuery() { - UserData *bot = nullptr; - QString inlineBotUsername; - QString query = _field->getInlineBotQuery(&bot, &inlineBotUsername); - if (inlineBotUsername != _inlineBotUsername) { - _inlineBotUsername = inlineBotUsername; + const auto query = ParseInlineBotQuery(_field); + if (_inlineBotUsername != query.username) { + _inlineBotUsername = query.username; if (_inlineBotResolveRequestId) { // Notify::inlineBotRequesting(false); MTP::cancel(_inlineBotResolveRequestId); _inlineBotResolveRequestId = 0; } - if (bot == Ui::LookingUpInlineBot) { - _inlineBot = Ui::LookingUpInlineBot; + if (query.lookingUpBot) { + _inlineBot = nullptr; + _inlineLookingUpBot = true; // Notify::inlineBotRequesting(true); - _inlineBotResolveRequestId = MTP::send(MTPcontacts_ResolveUsername(MTP_string(_inlineBotUsername)), rpcDone(&HistoryWidget::inlineBotResolveDone), rpcFail(&HistoryWidget::inlineBotResolveFail, _inlineBotUsername)); - return; + _inlineBotResolveRequestId = MTP::send( + MTPcontacts_ResolveUsername(MTP_string(_inlineBotUsername)), + rpcDone(&HistoryWidget::inlineBotResolveDone), + rpcFail( + &HistoryWidget::inlineBotResolveFail, + _inlineBotUsername)); + } else { + applyInlineBotQuery(query.bot, query.query); } - } else if (bot == Ui::LookingUpInlineBot) { - if (_inlineBot == Ui::LookingUpInlineBot) { - return; + } else if (query.lookingUpBot) { + if (!_inlineLookingUpBot) { + applyInlineBotQuery(_inlineBot, query.query); } - bot = _inlineBot; + } else { + applyInlineBotQuery(query.bot, query.query); } - - applyInlineBotQuery(bot, query); } void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) { if (bot) { if (_inlineBot != bot) { _inlineBot = bot; + _inlineLookingUpBot = false; inlineBotChanged(); } if (!_inlineResults) { @@ -1125,15 +1160,21 @@ void HistoryWidget::onTextChange() { } _saveCloudDraftTimer.stop(); - if (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) return; + if (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) { + return; + } _saveDraftText = true; onDraftSave(true); } void HistoryWidget::onDraftSaveDelayed() { - if (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) return; - if (!_field->textCursor().anchor() && !_field->textCursor().position() && !_field->verticalScrollBar()->value()) { + if (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) { + return; + } + if (!_field->textCursor().anchor() + && !_field->textCursor().position() + && !_field->scrollTop().current()) { if (!Local::hasDraftCursors(_peer->id)) { return; } @@ -1161,7 +1202,7 @@ void HistoryWidget::saveFieldToHistoryLocalDraft() { if (_editMsgId) { _history->setEditDraft(std::make_unique(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId)); } else { - if (_replyToId || !_field->isEmpty()) { + if (_replyToId || !_field->empty()) { _history->setLocalDraft(std::make_unique(_field, _replyToId, _previewCancelled)); } else { _history->clearLocalDraft(); @@ -1595,12 +1636,12 @@ void HistoryWidget::fastShowAtEnd(not_null history) { } } -void HistoryWidget::applyDraft(bool parseLinks, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction) { +void HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { auto draft = _history ? _history->draft() : nullptr; auto fieldAvailable = canWriteMessage(); if (!draft || (!_history->editDraft() && !fieldAvailable)) { auto fieldWillBeHiddenAfterEdit = (!fieldAvailable && _editMsgId != 0); - clearFieldText(0, undoHistoryAction); + clearFieldText(0, fieldHistoryAction); _field->setFocus(); _replyEditMsg = nullptr; _editMsgId = _replyToId = 0; @@ -1612,7 +1653,7 @@ void HistoryWidget::applyDraft(bool parseLinks, Ui::FlatTextarea::UndoHistoryAct } _textUpdateEvents = 0; - setFieldText(draft->textWithTags, 0, undoHistoryAction); + setFieldText(draft->textWithTags, 0, fieldHistoryAction); _field->setFocus(); draft->cursor.applyTo(_field); _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; @@ -1628,9 +1669,6 @@ void HistoryWidget::applyDraft(bool parseLinks, Ui::FlatTextarea::UndoHistoryAct updateControlsVisibility(); updateControlsGeometry(); - if (parseLinks) { - onPreviewParse(); - } if (_editMsgId || _replyToId) { updateReplyEditTexts(); if (!_replyEditMsg) { @@ -1644,7 +1682,7 @@ void HistoryWidget::applyDraft(bool parseLinks, Ui::FlatTextarea::UndoHistoryAct void HistoryWidget::applyCloudDraft(History *history) { if (_history == history && !_editMsgId) { - applyDraft(true, Ui::FlatTextarea::AddToUndoHistory); + applyDraft(Ui::InputField::HistoryAction::NewEntry); updateControlsVisibility(); updateControlsGeometry(); @@ -1849,15 +1887,12 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re _migrated->clearEditDraft(); _history->takeLocalDraft(_migrated); } - applyDraft(false); + applyDraft(); _send->finishAnimating(); _tabbedSelector->showMegagroupSet(_peer->asMegagroup()); updateControlsGeometry(); - if (!_previewCancelled) { - onPreviewParse(); - } connect(_scroll, SIGNAL(geometryChanged()), _list, SLOT(onParentGeometryChanged())); @@ -1910,11 +1945,11 @@ void HistoryWidget::clearAllLoadRequests() { } void HistoryWidget::updateFieldSubmitSettings() { - auto settings = Ui::FlatTextarea::SubmitSettings::Enter; + auto settings = Ui::InputField::SubmitSettings::Enter; if (_isInlineBot) { - settings = Ui::FlatTextarea::SubmitSettings::None; + settings = Ui::InputField::SubmitSettings::None; } else if (cCtrlEnter()) { - settings = Ui::FlatTextarea::SubmitSettings::CtrlEnter; + settings = Ui::InputField::SubmitSettings::CtrlEnter; } _field->setSubmitSettings(settings); } @@ -2819,9 +2854,14 @@ void HistoryWidget::checkReplyReturns() { void HistoryWidget::onInlineBotCancel() { auto &textWithTags = _field->getTextWithTags(); if (textWithTags.text.size() > _inlineBotUsername.size() + 2) { - setFieldText({ '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, Ui::FlatTextarea::AddToUndoHistory); + setFieldText( + { '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }, + TextUpdateEvent::SaveDraft, + Ui::InputField::HistoryAction::NewEntry); } else { - clearFieldText(TextUpdateEvent::SaveDraft, Ui::FlatTextarea::AddToUndoHistory); + clearFieldText( + TextUpdateEvent::SaveDraft, + Ui::InputField::HistoryAction::NewEntry); } } @@ -2938,7 +2978,7 @@ void HistoryWidget::hideSelectorControlsAnimated() { } } -void HistoryWidget::onSend(bool ctrlShiftEnter) { +void HistoryWidget::onSend() { if (!_history) return; if (_editMsgId) { @@ -3525,7 +3565,10 @@ bool HistoryWidget::insertBotCommand(const QString &cmd) { cur.movePosition(QTextCursor::End); _field->setTextCursor(cur); } else { - setFieldText({ toInsert, TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, Ui::FlatTextarea::AddToUndoHistory); + setFieldText( + { toInsert, TextWithTags::Tags() }, + TextUpdateEvent::SaveDraft, + Ui::InputField::HistoryAction::NewEntry); _field->setFocus(); return true; } @@ -3587,33 +3630,29 @@ bool HistoryWidget::hasSilentToggle() const { && !_peer->notifySettingsUnknown(); } -void HistoryWidget::inlineBotResolveDone(const MTPcontacts_ResolvedPeer &result) { +void HistoryWidget::inlineBotResolveDone( + const MTPcontacts_ResolvedPeer &result) { + Expects(result.type() == mtpc_contacts_resolvedPeer); + _inlineBotResolveRequestId = 0; + const auto &data = result.c_contacts_resolvedPeer(); // Notify::inlineBotRequesting(false); - UserData *resolvedBot = nullptr; - if (result.type() == mtpc_contacts_resolvedPeer) { - const auto &d(result.c_contacts_resolvedPeer()); - resolvedBot = App::feedUsers(d.vusers); - if (resolvedBot) { - if (!resolvedBot->botInfo || resolvedBot->botInfo->inlinePlaceholder.isEmpty()) { - resolvedBot = nullptr; + const auto resolvedBot = [&]() -> UserData* { + if (const auto result = App::feedUsers(data.vusers)) { + if (result->botInfo + && !result->botInfo->inlinePlaceholder.isEmpty()) { + return result; } } - App::feedChats(d.vchats); - } + return nullptr; + }(); + App::feedChats(data.vchats); - UserData *bot = nullptr; - QString inlineBotUsername; - auto query = _field->getInlineBotQuery(&bot, &inlineBotUsername); - if (inlineBotUsername == _inlineBotUsername) { - if (bot == Ui::LookingUpInlineBot) { - bot = resolvedBot; - } - } else { - bot = nullptr; - } - if (bot) { - applyInlineBotQuery(bot, query); + const auto query = ParseInlineBotQuery(_field); + if (_inlineBotUsername == query.username) { + applyInlineBotQuery( + query.lookingUpBot ? resolvedBot : query.bot, + query.query); } else { clearInlineBot(); } @@ -3657,11 +3696,11 @@ bool HistoryWidget::isMuteUnmute() const { } bool HistoryWidget::showRecordButton() const { - return Media::Capture::instance()->available() && !_field->hasSendText() && !readyToForward() && !_editMsgId; + return Media::Capture::instance()->available() && !HasSendText(_field) && !readyToForward() && !_editMsgId; } bool HistoryWidget::showInlineBotCancel() const { - return _inlineBot && (_inlineBot != Ui::LookingUpInlineBot); + return _inlineBot && !_inlineLookingUpBot; } void HistoryWidget::updateSendButtonType() { @@ -3683,7 +3722,7 @@ bool HistoryWidget::updateCmdStartShown() { bool cmdStartShown = false; if (_history && _peer && ((_peer->isChat() && _peer->asChat()->botStatus > 0) || (_peer->isMegagroup() && _peer->asChannel()->mgInfo->botStatus > 0) || (_peer->isUser() && _peer->asUser()->botInfo))) { if (!isBotStart() && !isBlocked() && !_keyboard->hasMarkup() && !_keyboard->forceReply()) { - if (!_field->hasSendText()) { + if (!HasSendText(_field)) { cmdStartShown = true; } } @@ -3791,7 +3830,10 @@ void HistoryWidget::onKbToggle(bool manual) { } void HistoryWidget::onCmdStart() { - setFieldText({ qsl("/"), TextWithTags::Tags() }, 0, Ui::FlatTextarea::AddToUndoHistory); + setFieldText( + { qsl("/"), TextWithTags::Tags() }, + 0, + Ui::InputField::HistoryAction::NewEntry); } void HistoryWidget::setMembersShowAreaActive(bool active) { @@ -3974,8 +4016,9 @@ void HistoryWidget::updateFieldSize() { void HistoryWidget::clearInlineBot() { if (_inlineBot) { _inlineBot = nullptr; + _inlineLookingUpBot = false; inlineBotChanged(); - _field->finishPlaceholder(); + _field->finishAnimating(); } if (_inlineResults) { _inlineResults->clearInlineBot(); @@ -4006,24 +4049,39 @@ void HistoryWidget::onFieldFocused() { } void HistoryWidget::onCheckFieldAutocomplete() { - if (!_history || _a_show.animating()) return; - - auto start = false; - auto isInlineBot = _inlineBot && (_inlineBot != Ui::LookingUpInlineBot); - auto query = isInlineBot ? QString() : _field->getMentionHashtagBotCommandPart(start); - if (!query.isEmpty()) { - if (query.at(0) == '#' && cRecentWriteHashtags().isEmpty() && cRecentSearchHashtags().isEmpty()) Local::readRecentHashtagsAndBots(); - if (query.at(0) == '@' && cRecentInlineBots().isEmpty()) Local::readRecentHashtagsAndBots(); - if (query.at(0) == '/' && _peer->isUser() && !_peer->asUser()->botInfo) return; + if (!_history || _a_show.animating()) { + return; } - _fieldAutocomplete->showFiltered(_peer, query, start); + + const auto isInlineBot = _inlineBot && !_inlineLookingUpBot; + const auto autocomplete = isInlineBot + ? AutocompleteQuery() + : ParseMentionHashtagBotCommandQuery(_field); + if (!autocomplete.query.isEmpty()) { + if (autocomplete.query[0] == '#' + && cRecentWriteHashtags().isEmpty() + && cRecentSearchHashtags().isEmpty()) { + Local::readRecentHashtagsAndBots(); + } else if (autocomplete.query[0] == '@' + && cRecentInlineBots().isEmpty()) { + Local::readRecentHashtagsAndBots(); + } else if (autocomplete.query[0] == '/' + && _peer->isUser() + && !_peer->asUser()->botInfo) { + return; + } + } + _fieldAutocomplete->showFiltered( + _peer, + autocomplete.query, + autocomplete.fromStart); } void HistoryWidget::updateFieldPlaceholder() { if (_editMsgId) { _field->setPlaceholder(langFactory(lng_edit_message_text)); } else { - if (_inlineBot && _inlineBot != Ui::LookingUpInlineBot) { + if (_inlineBot && !_inlineLookingUpBot) { auto text = _inlineBot->botInfo->inlinePlaceholder.mid(1); _field->setPlaceholder([text] { return text; }, _inlineBot->username.size() + 2); } else { @@ -4076,7 +4134,7 @@ bool HistoryWidget::confirmSendingFiles(const QStringList &files) { return confirmSendingFiles(files, CompressConfirm::Auto); } -bool HistoryWidget::confirmSendingFiles(const QMimeData *data) { +bool HistoryWidget::confirmSendingFiles(not_null data) { return confirmSendingFiles(data, CompressConfirm::Auto); } @@ -4157,8 +4215,29 @@ bool HistoryWidget::confirmSendingFiles( insertTextOnCancel); } +bool HistoryWidget::canSendFiles(not_null data) const { + if (!canWriteMessage()) { + return false; + } + if (const auto urls = data->urls(); !urls.empty()) { + if (ranges::find_if( + urls, + [](const QUrl &url) { return !url.isLocalFile(); } + ) == urls.end()) { + return true; + } + } + if (data->hasImage()) { + const auto image = qvariant_cast(data->imageData()); + if (!image.isNull()) { + return true; + } + } + return false; +} + bool HistoryWidget::confirmSendingFiles( - const QMimeData *data, + not_null data, CompressConfirm compressed, const QString &insertTextOnCancel) { if (!canWriteMessage()) { @@ -4961,7 +5040,7 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { if (_keyboard->singleUse() && _keyboard->hasMarkup() && _keyboard->forMsgId() == FullMsgId(_channel, _history->lastKeyboardId) && _history->lastKeyboardUsed) { _history->lastKeyboardHiddenId = _history->lastKeyboardId; } - if (!isBotStart() && !isBlocked() && _canSendMessages && (wasVisible || (_replyToId && _replyEditMsg) || (!_field->hasSendText() && !kbWasHidden()))) { + if (!isBotStart() && !isBlocked() && _canSendMessages && (wasVisible || (_replyToId && _replyEditMsg) || (!HasSendText(_field) && !kbWasHidden()))) { if (!_a_show.animating()) { if (hasMarkup) { _kbScroll->show(); @@ -5155,7 +5234,7 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) { : nullptr; if (item && item->allowsEdit(unixtime()) - && _field->isEmpty() + && _field->empty() && !_editMsgId && !_replyToId) { editMessage(item); @@ -5638,21 +5717,30 @@ void HistoryWidget::sendExistingPhoto( _field->setFocus(); } -void HistoryWidget::setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction) { +void HistoryWidget::setFieldText( + const TextWithTags &textWithTags, + TextUpdateEvents events, + FieldHistoryAction fieldHistoryAction) { _textUpdateEvents = events; - _field->setTextWithTags(textWithTags, undoHistoryAction); - _field->moveCursor(QTextCursor::End); - _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; + _field->setTextWithTags(textWithTags, fieldHistoryAction); + auto cursor = _field->textCursor(); + cursor.movePosition(QTextCursor::End); + _field->setTextCursor(cursor); + _textUpdateEvents = TextUpdateEvent::SaveDraft + | TextUpdateEvent::SendTyping; _previewCancelled = false; _previewData = nullptr; - if (_previewRequest) { - MTP::cancel(_previewRequest); - _previewRequest = 0; - } + MTP::cancel(base::take(_previewRequest)); _previewLinks.clear(); } +void HistoryWidget::clearFieldText( + TextUpdateEvents events, + FieldHistoryAction fieldHistoryAction) { + setFieldText(TextWithTags(), events, fieldHistoryAction); +} + void HistoryWidget::replyToMessage(FullMsgId itemId) { if (const auto item = App::histItemById(itemId)) { replyToMessage(item); @@ -5739,7 +5827,7 @@ void HistoryWidget::editMessage(not_null item) { _send->clearState(); } if (!_editMsgId) { - if (_replyToId || !_field->isEmpty()) { + if (_replyToId || !_field->empty()) { _history->setLocalDraft(std::make_unique(_field, _replyToId, _previewCancelled)); } else { _history->clearLocalDraft(); @@ -5761,7 +5849,7 @@ void HistoryWidget::editMessage(not_null item) { item->id, cursor, false)); - applyDraft(false); + applyDraft(); _previewData = nullptr; if (const auto media = item->media()) { @@ -5770,9 +5858,6 @@ void HistoryWidget::editMessage(not_null item) { updatePreview(); } } - if (!_previewData) { - onPreviewParse(); - } updateBotKeyboard(); @@ -5996,12 +6081,7 @@ void HistoryWidget::previewCancel() { } } -void HistoryWidget::onPreviewParse() { - if (_previewCancelled) return; - _field->parseLinks(); -} - -void HistoryWidget::onPreviewCheck() { +void HistoryWidget::checkPreview() { auto previewRestricted = [this] { if (auto megagroup = _peer ? _peer->asMegagroup() : nullptr) { if (megagroup->restricted(ChannelRestriction::f_embed_links)) { @@ -6017,15 +6097,16 @@ void HistoryWidget::onPreviewCheck() { update(); return; } - auto linksList = _field->linksList(); - auto newLinks = linksList.join(' '); - if (newLinks != _previewLinks) { + const auto newLinks = _parsedLinks.join(' '); + if (_previewLinks != newLinks) { MTP::cancel(base::take(_previewRequest)); _previewLinks = newLinks; if (_previewLinks.isEmpty()) { - if (_previewData && _previewData->pendingTill >= 0) previewCancel(); + if (_previewData && _previewData->pendingTill >= 0) { + previewCancel(); + } } else { - PreviewCache::const_iterator i = _previewCache.constFind(_previewLinks); + const auto i = _previewCache.constFind(_previewLinks); if (i == _previewCache.cend()) { _previewRequest = MTP::send( MTPmessages_GetWebPagePreview( @@ -6043,17 +6124,18 @@ void HistoryWidget::onPreviewCheck() { } } -void HistoryWidget::onPreviewTimeout() { - if (_previewData - && (_previewData->pendingTill > 0) - && !_previewLinks.isEmpty()) { - _previewRequest = MTP::send( - MTPmessages_GetWebPagePreview( - MTP_flags(0), - MTP_string(_previewLinks), - MTPnullEntities), - rpcDone(&HistoryWidget::gotPreview, _previewLinks)); +void HistoryWidget::requestPreview() { + if (!_previewData + || (_previewData->pendingTill <= 0) + || _previewLinks.isEmpty()) { + return; } + _previewRequest = MTP::send( + MTPmessages_GetWebPagePreview( + MTP_flags(0), + MTP_string(_previewLinks), + MTPnullEntities), + rpcDone(&HistoryWidget::gotPreview, _previewLinks)); } void HistoryWidget::gotPreview(QString links, const MTPMessageMedia &result, mtpRequestId req) { @@ -6084,7 +6166,7 @@ void HistoryWidget::gotPreview(QString links, const MTPMessageMedia &result, mtp } void HistoryWidget::updatePreview() { - _previewTimer.stop(); + _previewTimer.cancel(); if (_previewData && _previewData->pendingTill >= 0) { _fieldBarCancel->show(); updateMouseTracking(); @@ -6103,9 +6185,8 @@ void HistoryWidget::updatePreview() { TextUtilities::Clean(linkText), Ui::DialogTextOptions()); - int32 t = (_previewData->pendingTill - unixtime()) * 1000; - if (t <= 0) t = 1; - _previewTimer.start(t); + const auto timeout = (_previewData->pendingTill - unixtime()); + _previewTimer.callOnce(std::max(timeout, 0) * TimeMs(1000)); } else { QString title, desc; if (_previewData->siteName.isEmpty()) { diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 48be1db798..e227ad1be3 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -22,6 +22,7 @@ struct FileMediaInformation; struct SendingAlbum; enum class SendMediaType; enum class CompressConfirm; +class MessageLinksParser; namespace InlineBots { namespace Layout { @@ -176,6 +177,8 @@ class HistoryWidget final : public Window::AbstractSectionWidget, public RPCSend Q_OBJECT public: + using FieldHistoryAction = Ui::InputField::HistoryAction; + HistoryWidget(QWidget *parent, not_null controller); void start(); @@ -219,7 +222,7 @@ public: void updateStickersByEmoji(); bool confirmSendingFiles(const QStringList &files); - bool confirmSendingFiles(const QMimeData *data); + bool confirmSendingFiles(not_null data); void sendFileConfirmed(const std::shared_ptr &file); void updateControlsVisibility(); @@ -297,7 +300,8 @@ public: void updateBotKeyboard(History *h = nullptr, bool force = false); void fastShowAtEnd(not_null history); - void applyDraft(bool parseLinks = true, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction = Ui::FlatTextarea::ClearUndoHistory); + void applyDraft( + FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear); void showHistory(const PeerId &peer, MsgId showAtMsgId, bool reload = false); void clearDelayedShowAt(); void clearAllLoadRequests(); @@ -374,10 +378,6 @@ public slots: void onPinnedHide(); void onFieldBarCancel(); - void onPreviewParse(); - void onPreviewCheck(); - void onPreviewTimeout(); - void onPhotoUploaded(const FullMsgId &msgId, bool silent, const MTPInputFile &file); void onDocumentUploaded(const FullMsgId &msgId, bool silent, const MTPInputFile &file); void onThumbDocumentUploaded(const FullMsgId &msgId, bool silent, const MTPInputFile &file, const MTPInputFile &thumb); @@ -433,7 +433,7 @@ public slots: void preloadHistoryIfNeeded(); private slots: - void onSend(bool ctrlShiftEnter = false); + void onSend(); void onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::ChooseMethod method); void onMentionInsert(UserData *user); @@ -490,6 +490,7 @@ private: void unreadMentionsAnimationFinish(); void sendButtonClicked(); + bool canSendFiles(not_null data) const; bool confirmSendingFiles( const QStringList &files, CompressConfirm compressed, @@ -500,7 +501,7 @@ private: CompressConfirm compressed, const QString &insertTextOnCancel = QString()); bool confirmSendingFiles( - const QMimeData *data, + not_null data, CompressConfirm compressed, const QString &insertTextOnCancel = QString()); bool confirmSendingFiles( @@ -607,14 +608,20 @@ private: void saveEditMsgDone(History *history, const MTPUpdates &updates, mtpRequestId req); bool saveEditMsgFail(History *history, const RPCError &error, mtpRequestId req); - static const mtpRequestId ReportSpamRequestNeeded = -1; - DBIPeerReportSpamStatus _reportSpamStatus = dbiprsUnknown; - mtpRequestId _reportSpamSettingRequestId = ReportSpamRequestNeeded; void updateReportSpamStatus(); void requestReportSpamSetting(); void reportSpamSettingDone(const MTPPeerSettings &result, mtpRequestId req); bool reportSpamSettingFail(const RPCError &error, mtpRequestId req); + void checkPreview(); + void requestPreview(); + void gotPreview(QString links, const MTPMessageMedia &media, mtpRequestId req); + + static const mtpRequestId ReportSpamRequestNeeded = -1; + DBIPeerReportSpamStatus _reportSpamStatus = dbiprsUnknown; + mtpRequestId _reportSpamSettingRequestId = ReportSpamRequestNeeded; + + QStringList _parsedLinks; QString _previewLinks; WebPageData *_previewData = nullptr; typedef QMap PreviewCache; @@ -622,9 +629,8 @@ private: mtpRequestId _previewRequest = 0; Text _previewTitle; Text _previewDescription; - SingleTimer _previewTimer; + base::Timer _previewTimer; bool _previewCancelled = false; - void gotPreview(QString links, const MTPMessageMedia &media, mtpRequestId req); bool _replyForwardPressed = false; @@ -695,10 +701,13 @@ private: void writeDrafts(Data::Draft **localDraft, Data::Draft **editDraft); void writeDrafts(History *history); - void setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events = 0, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction = Ui::FlatTextarea::ClearUndoHistory); - void clearFieldText(TextUpdateEvents events = 0, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction = Ui::FlatTextarea::ClearUndoHistory) { - setFieldText(TextWithTags(), events, undoHistoryAction); - } + void setFieldText( + const TextWithTags &textWithTags, + TextUpdateEvents events = 0, + FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear); + void clearFieldText( + TextUpdateEvents events = 0, + FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear); HistoryItem *getItemFromHistoryOrMigrated(MsgId genericMsgId) const; void animatedScrollToItem(MsgId msgId); @@ -759,9 +768,11 @@ private: object_ptr _unreadMentions; object_ptr _fieldAutocomplete; + std::unique_ptr _fieldLinksParser; UserData *_inlineBot = nullptr; QString _inlineBotUsername; + bool _inlineLookingUpBot = false; mtpRequestId _inlineBotResolveRequestId = 0; bool _isInlineBot = false; void inlineBotResolveDone(const MTPcontacts_ResolvedPeer &result); @@ -796,7 +807,7 @@ private: object_ptr _botCommandStart; object_ptr _silent = { nullptr }; bool _cmdStartShown = false; - object_ptr _field; + object_ptr _field; bool _recording = false; bool _inField = false; bool _inReplyEditForward = false; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 2c6ae9e81d..cee5368d56 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -7,9 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/info_wrap_widget.h" -#include -#include -#include #include "info/profile/info_profile_widget.h" #include "info/profile/info_profile_values.h" #include "info/media/info_media_widget.h" diff --git a/Telegram/SourceFiles/mtproto/connection_tcp.cpp b/Telegram/SourceFiles/mtproto/connection_tcp.cpp index ef45b9ba7f..2a4ea0286c 100644 --- a/Telegram/SourceFiles/mtproto/connection_tcp.cpp +++ b/Telegram/SourceFiles/mtproto/connection_tcp.cpp @@ -171,7 +171,14 @@ mtpBuffer TcpConnection::handleResponse(const char *packet, uint32 length) { const mtpPrime *packetdata = reinterpret_cast(packet + (length - len)); TCP_LOG(("TCP Info: packet received, size = %1").arg(size * sizeof(mtpPrime))); if (size == 1) { - LOG(("TCP Error: error packet received, code = %1").arg(*packetdata)); + LOG(("TCP Error: " + "error packet received, endpoint: '%1:%2', " + "protocolDcId: %3, secret_len: %4, code = %5" + ).arg(_address.isEmpty() ? ("proxy_" + _proxy.host) : _address + ).arg(_address.isEmpty() ? _proxy.port : _port + ).arg(_protocolDcId + ).arg(_protocolSecret.size() + ).arg(*packetdata)); return mtpBuffer(1, *packetdata); } diff --git a/Telegram/SourceFiles/mtproto/connection_tcp.h b/Telegram/SourceFiles/mtproto/connection_tcp.h index d9e5a13c22..3957325a40 100644 --- a/Telegram/SourceFiles/mtproto/connection_tcp.h +++ b/Telegram/SourceFiles/mtproto/connection_tcp.h @@ -56,7 +56,7 @@ private: void socketError(QAbstractSocket::SocketError e); void handleTimeout(); - static mtpBuffer handleResponse(const char *packet, uint32 length); + mtpBuffer handleResponse(const char *packet, uint32 length); static void handleError(QAbstractSocket::SocketError e, QTcpSocket &sock); static uint32 fourCharsToUInt(char ch1, char ch2, char ch3, char ch4) { char ch[4] = { ch1, ch2, ch3, ch4 }; diff --git a/Telegram/SourceFiles/rpl/operators_tests.cpp b/Telegram/SourceFiles/rpl/operators_tests.cpp index 704c447f2a..f7d141a2c3 100644 --- a/Telegram/SourceFiles/rpl/operators_tests.cpp +++ b/Telegram/SourceFiles/rpl/operators_tests.cpp @@ -461,4 +461,36 @@ TEST_CASE("basic operators tests", "[rpl::operators]") { } REQUIRE(*sum == "012done012done012done"); } + + SECTION("skip test") { + auto sum = std::make_shared(""); + { + rpl::lifetime lifetime; + ints(10) | skip(5) + | start_with_next_done([=](int value) { + *sum += std::to_string(value); + }, [=] { + *sum += "done"; + }, lifetime); + } + { + rpl::lifetime lifetime; + ints(3) | skip(3) + | start_with_next_done([=](int value) { + *sum += std::to_string(value); + }, [=] { + *sum += "done"; + }, lifetime); + } + { + rpl::lifetime lifetime; + ints(3) | skip(10) + | start_with_next_done([=](int value) { + *sum += std::to_string(value); + }, [=] { + *sum += "done"; + }, lifetime); + } + REQUIRE(*sum == "56789donedonedone"); + } } diff --git a/Telegram/SourceFiles/rpl/rpl.h b/Telegram/SourceFiles/rpl/rpl.h index f2ebcfa669..58e55d5048 100644 --- a/Telegram/SourceFiles/rpl/rpl.h +++ b/Telegram/SourceFiles/rpl/rpl.h @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include #include #include #include diff --git a/Telegram/SourceFiles/rpl/skip.h b/Telegram/SourceFiles/rpl/skip.h new file mode 100644 index 0000000000..8cfe5774b3 --- /dev/null +++ b/Telegram/SourceFiles/rpl/skip.h @@ -0,0 +1,61 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace rpl { +namespace details { + +class skip_helper { +public: + skip_helper(int count) : _count(count) { + } + + template < + typename Value, + typename Error, + typename Generator> + auto operator()(producer &&initial) { + return make_producer([ + initial = std::move(initial), + skipping = _count + ](const auto &consumer) mutable { + auto count = consumer.template make_state(skipping); + auto initial_consumer = make_consumer( + [consumer, count](auto &&value) { + if (*count) { + --*count; + } else { + consumer.put_next_forward( + std::forward(value)); + } + }, [consumer](auto &&error) { + consumer.put_error_forward( + std::forward(error)); + }, [consumer] { + consumer.put_done(); + }); + consumer.add_lifetime(initial_consumer.terminator()); + return std::move(initial).start_existing(initial_consumer); + }); + } + +private: + int _count = 0; + +}; + +} // namespace details + +inline auto skip(int count) +-> details::skip_helper { + Expects(count >= 0); + + return details::skip_helper(count); +} + +} // namespace rpl diff --git a/Telegram/SourceFiles/rpl/take.h b/Telegram/SourceFiles/rpl/take.h index a54d0434b6..6164de4ad2 100644 --- a/Telegram/SourceFiles/rpl/take.h +++ b/Telegram/SourceFiles/rpl/take.h @@ -53,11 +53,12 @@ private: }; } // namespace details + inline auto take(int count) -> details::take_helper { Expects(count >= 0); + return details::take_helper(count); } - } // namespace rpl diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp index de6539b34d..201cb1ada4 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp +++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp @@ -29,6 +29,67 @@ const auto kObjectReplacement = QString::fromRawData( &kObjectReplacementCh, 1); +class TagAccumulator { +public: + TagAccumulator(TextWithTags::Tags &tags) : _tags(tags) { + } + + bool changed() const { + return _changed; + } + + void feed(const QString &randomTagId, int currentPosition) { + if (randomTagId == _currentTagId) return; + + if (!_currentTagId.isEmpty()) { + int randomPartPosition = _currentTagId.lastIndexOf('/'); + Assert(randomPartPosition > 0); + + bool tagChanged = true; + if (_currentTag < _tags.size()) { + auto &alreadyTag = _tags[_currentTag]; + if (alreadyTag.offset == _currentStart && + alreadyTag.length == currentPosition - _currentStart && + alreadyTag.id == _currentTagId.midRef(0, randomPartPosition)) { + tagChanged = false; + } + } + if (tagChanged) { + _changed = true; + const auto tag = TextWithTags::Tag { + _currentStart, + currentPosition - _currentStart, + _currentTagId.mid(0, randomPartPosition), + }; + if (_currentTag < _tags.size()) { + _tags[_currentTag] = tag; + } else { + _tags.push_back(tag); + } + } + ++_currentTag; + } + _currentTagId = randomTagId; + _currentStart = currentPosition; + }; + + void finish() { + if (_currentTag < _tags.size()) { + _tags.resize(_currentTag); + _changed = true; + } + } + +private: + TextWithTags::Tags &_tags; + bool _changed = false; + + int _currentTag = 0; + int _currentStart = 0; + QString _currentTagId; + +}; + template class InputStyle : public QCommonStyle { public: @@ -80,10 +141,10 @@ QString AccumulateText(Iterator begin, Iterator end) { return result; } -QTextImageFormat PrepareEmojiFormat(EmojiPtr emoji, const style::font &f) { +QTextImageFormat PrepareEmojiFormat(EmojiPtr emoji, const QFont &font) { const auto factor = cIntRetinaFactor(); const auto width = Ui::Emoji::Size() + st::emojiPadding * factor * 2; - const auto height = f->height * factor; + const auto height = QFontMetrics(font).height() * factor; auto result = QTextImageFormat(); result.setWidth(width / factor); result.setHeight(height / factor); @@ -92,6 +153,123 @@ QTextImageFormat PrepareEmojiFormat(EmojiPtr emoji, const style::font &f) { return result; } +// Optimization: with null page size document does not re-layout +// on each insertText / mergeCharFormat. +void PrepareFormattingOptimization(not_null document) { + if (!document->pageSize().isNull()) { + document->setPageSize(QSizeF(0, 0)); + } +} + +void RemoveDocumentTags( + style::color textFg, + not_null document, + int from, + int end) { + auto cursor = QTextCursor(document->docHandle(), from); + cursor.setPosition(end, QTextCursor::KeepAnchor); + + QTextCharFormat format; + format.setAnchor(false); + format.setAnchorName(QString()); + format.setForeground(textFg); + cursor.mergeCharFormat(format); +} + +// Returns the position of the first inserted tag or "changedEnd" value if none found. +int ProcessInsertedTags( + style::color textFg, + QTextDocument *document, + int changedPosition, + int changedEnd, + const TextWithTags::Tags &tags, + InputField::TagMimeProcessor *processor) { + int firstTagStart = changedEnd; + int applyNoTagFrom = changedEnd; + for (const auto &tag : tags) { + int tagFrom = changedPosition + tag.offset; + int tagTo = tagFrom + tag.length; + accumulate_max(tagFrom, changedPosition); + accumulate_min(tagTo, changedEnd); + auto tagId = processor ? processor->tagFromMimeTag(tag.id) : tag.id; + if (tagTo > tagFrom && !tagId.isEmpty()) { + accumulate_min(firstTagStart, tagFrom); + + PrepareFormattingOptimization(document); + + if (applyNoTagFrom < tagFrom) { + RemoveDocumentTags(textFg, document, applyNoTagFrom, tagFrom); + } + QTextCursor c(document->docHandle(), 0); + c.setPosition(tagFrom); + c.setPosition(tagTo, QTextCursor::KeepAnchor); + + QTextCharFormat format; + format.setAnchor(true); + format.setAnchorName(tagId + '/' + QString::number(rand_value())); + format.setForeground(st::defaultTextPalette.linkFg); + c.mergeCharFormat(format); + + applyNoTagFrom = tagTo; + } + } + if (applyNoTagFrom < changedEnd) { + RemoveDocumentTags(textFg, document, applyNoTagFrom, changedEnd); + } + + return firstTagStart; +} + +// When inserting a part of text inside a tag we need to have +// a way to know if the insertion replaced the end of the tag +// or it was strictly inside (in the middle) of the tag. +bool WasInsertTillTheEndOfTag( + QTextBlock block, + QTextBlock::iterator fragmentIt, + int insertionEnd) { + auto insertTagName = fragmentIt.fragment().charFormat().anchorName(); + while (true) { + for (; !fragmentIt.atEnd(); ++fragmentIt) { + auto fragment = fragmentIt.fragment(); + bool fragmentOutsideInsertion = (fragment.position() >= insertionEnd); + if (fragmentOutsideInsertion) { + return (fragment.charFormat().anchorName() != insertTagName); + } + int fragmentEnd = fragment.position() + fragment.length(); + bool notFullFragmentInserted = (fragmentEnd > insertionEnd); + if (notFullFragmentInserted) { + return false; + } + } + if (block.isValid()) { + fragmentIt = block.begin(); + block = block.next(); + } else { + break; + } + } + // Insertion goes till the end of the text => not strictly inside a tag. + return true; +} + +struct FormattingAction { + enum class Type { + Invalid, + InsertEmoji, + TildeFont, + RemoveTag, + RemoveNewline, + ClearInstantReplace, + }; + + Type type = Type::Invalid; + EmojiPtr emoji = nullptr; + bool isTilde = false; + int intervalStart = 0; + int intervalEnd = 0; + +}; + } // namespace class InputField::Inner final : public QTextEdit { @@ -119,6 +297,9 @@ protected: void contextMenuEvent(QContextMenuEvent *e) override { return outer()->contextMenuEventInner(e); } + void dropEvent(QDropEvent *e) override { + return outer()->dropEventInner(e); + } bool canInsertFromMimeData(const QMimeData *source) const override { return outer()->canInsertFromMimeDataInner(source); @@ -138,6 +319,17 @@ private: }; +void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji) { + const auto currentFormat = cursor.charFormat(); + auto format = PrepareEmojiFormat(emoji, currentFormat.font()); + if (currentFormat.isAnchor()) { + format.setAnchor(true); + format.setAnchorName(currentFormat.anchorName()); + format.setForeground(st::defaultTextPalette.linkFg); + } + cursor.insertText(kObjectReplacement, format); +} + void InstantReplaces::add(const QString &what, const QString &with) { auto node = &reverseMap; for (auto i = what.end(), b = what.begin(); i != b;) { @@ -175,1519 +367,6 @@ const InstantReplaces &InstantReplaces::Default() { return result; } -FlatTextarea::FlatTextarea(QWidget *parent, const style::FlatTextarea &st, base::lambda placeholderFactory, const QString &v, const TagList &tags) : TWidgetHelper(parent) -, _placeholderFactory(std::move(placeholderFactory)) -, _placeholderVisible(!v.length()) -, _lastTextWithTags { v, tags } -, _st(st) { - _defaultCharFormat = textCursor().charFormat(); - - setCursor(style::cur_text); - setAcceptRichText(false); - resize(_st.width, _st.font->height); - - setFont(_st.font->f); - setAlignment(_st.align); - - subscribe(Lang::Current().updated(), [this] { refreshPlaceholder(); }); - refreshPlaceholder(); - - subscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) { - if (update.paletteChanged()) { - updatePalette(); - } - }); - updatePalette(); - - setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - setFrameStyle(QFrame::NoFrame | QFrame::Plain); - viewport()->setAutoFillBackground(false); - - setContentsMargins(0, 0, 0, 0); - - switch (cScale()) { - case dbisOneAndQuarter: _fakeMargin = 1; break; - case dbisOneAndHalf: _fakeMargin = 2; break; - case dbisTwo: _fakeMargin = 4; break; - } - setStyleSheet(qsl("QTextEdit { margin: %1px; }").arg(_fakeMargin)); - - viewport()->setAttribute(Qt::WA_AcceptTouchEvents); - _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(contentsChanged()), this, SLOT(onDocumentContentsChanged())); - connect(this, SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool))); - connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool))); - if (App::wnd()) connect(this, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu())); - - if (!_lastTextWithTags.text.isEmpty()) { - setTextWithTags(_lastTextWithTags, ClearUndoHistory); - } -} - -void FlatTextarea::setInstantReplaces(const InstantReplaces &replaces) { - _mutableInstantReplaces = replaces; -} - -void FlatTextarea::enableInstantReplaces(bool enabled) { - _instantReplacesEnabled = enabled; -} - -void FlatTextarea::updatePalette() { - auto p = palette(); - p.setColor(QPalette::Text, _st.textColor->c); - setPalette(p); -} - -TextWithTags FlatTextarea::getTextWithTagsPart(int start, int end) { - TextWithTags result; - result.text = getTextPart(start, end, &result.tags); - return result; -} - -void FlatTextarea::setTextWithTags(const TextWithTags &textWithTags, UndoHistoryAction undoHistoryAction) { - _insertedTags = textWithTags.tags; - _insertedTagsAreFromMime = false; - _realInsertPosition = 0; - _realCharsAdded = textWithTags.text.size(); - auto doc = document(); - auto cursor = QTextCursor(doc->docHandle(), 0); - if (undoHistoryAction == ClearUndoHistory) { - doc->setUndoRedoEnabled(false); - cursor.beginEditBlock(); - } else if (undoHistoryAction == MergeWithUndoHistory) { - cursor.joinPreviousEditBlock(); - } else { - cursor.beginEditBlock(); - } - cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); - cursor.insertText(textWithTags.text); - cursor.movePosition(QTextCursor::End); - cursor.endEditBlock(); - if (undoHistoryAction == ClearUndoHistory) { - doc->setUndoRedoEnabled(true); - } - _insertedTags.clear(); - _realInsertPosition = -1; - finishPlaceholder(); -} - -void FlatTextarea::finishPlaceholder() { - _a_placeholderFocused.finish(); - _a_placeholderVisible.finish(); - update(); -} - -void FlatTextarea::setMaxLength(int32 maxLength) { - _maxLength = maxLength; -} - -void FlatTextarea::setMinHeight(int32 minHeight) { - _minHeight = minHeight; - heightAutoupdated(); -} - -void FlatTextarea::setMaxHeight(int32 maxHeight) { - _maxHeight = maxHeight; - heightAutoupdated(); -} - -bool FlatTextarea::heightAutoupdated() { - if (_minHeight < 0 || _maxHeight < 0 || _inHeightCheck) return false; - _inHeightCheck = true; - - SendPendingMoveResizeEvents(this); - - int newh = ceil(document()->size().height()) + 2 * fakeMargin(); - if (newh > _maxHeight) { - newh = _maxHeight; - } else if (newh < _minHeight) { - newh = _minHeight; - } - if (height() != newh) { - resize(width(), newh); - _inHeightCheck = false; - return true; - } - _inHeightCheck = false; - return false; -} - -void FlatTextarea::onTouchTimer() { - _touchRightButton = true; -} - -bool FlatTextarea::viewportEvent(QEvent *e) { - if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) { - QTouchEvent *ev = static_cast(e); - if (ev->device()->type() == QTouchDevice::TouchScreen) { - touchEvent(ev); - return QTextEdit::viewportEvent(e); - } - } - return QTextEdit::viewportEvent(e); -} - -void FlatTextarea::touchEvent(QTouchEvent *e) { - switch (e->type()) { - case QEvent::TouchBegin: { - if (_touchPress || e->touchPoints().isEmpty()) return; - _touchTimer.start(QApplication::startDragTime()); - _touchPress = true; - _touchMove = _touchRightButton = false; - _touchStart = e->touchPoints().cbegin()->screenPos().toPoint(); - } break; - - case QEvent::TouchUpdate: { - if (!_touchPress || e->touchPoints().isEmpty()) return; - if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) { - _touchMove = true; - } - } break; - - case QEvent::TouchEnd: { - if (!_touchPress) return; - auto weak = make_weak(this); - if (!_touchMove && window()) { - Qt::MouseButton btn(_touchRightButton ? Qt::RightButton : Qt::LeftButton); - QPoint mapped(mapFromGlobal(_touchStart)), winMapped(window()->mapFromGlobal(_touchStart)); - - if (_touchRightButton) { - QContextMenuEvent contextEvent(QContextMenuEvent::Mouse, mapped, _touchStart); - contextMenuEvent(&contextEvent); - } - } - if (weak) { - _touchTimer.stop(); - _touchPress = _touchMove = _touchRightButton = false; - } - } break; - - case QEvent::TouchCancel: { - _touchPress = false; - _touchTimer.stop(); - } break; - } -} - -QRect FlatTextarea::getTextRect() const { - return rect().marginsRemoved(_st.textMrg + st::textRectMargins); -} - -int32 FlatTextarea::fakeMargin() const { - return _fakeMargin; -} - -void FlatTextarea::paintEvent(QPaintEvent *e) { - Painter p(viewport()); - auto ms = getms(); - auto r = rect().intersected(e->rect()); - p.fillRect(r, _st.bgColor); - auto placeholderOpacity = _a_placeholderVisible.current(ms, _placeholderVisible ? 1. : 0.); - if (placeholderOpacity > 0.) { - p.setOpacity(placeholderOpacity); - - auto placeholderLeft = anim::interpolate(_st.phShift, 0, placeholderOpacity); - - p.save(); - p.setClipRect(r); - p.setFont(_st.font); - p.setPen(anim::pen(_st.phColor, _st.phFocusColor, _a_placeholderFocused.current(ms, _focused ? 1. : 0.))); - if (_st.phAlign == style::al_topleft && _placeholderAfterSymbols > 0) { - int skipWidth = placeholderSkipWidth(); - p.drawText(_st.textMrg.left() - _fakeMargin + placeholderLeft + skipWidth, _st.textMrg.top() - _fakeMargin - st::lineWidth + _st.font->ascent, _placeholder); - } else { - QRect phRect(_st.textMrg.left() - _fakeMargin + _st.phPos.x() + placeholderLeft, _st.textMrg.top() - _fakeMargin + _st.phPos.y(), width() - _st.textMrg.left() - _st.textMrg.right(), height() - _st.textMrg.top() - _st.textMrg.bottom()); - p.drawText(phRect, _placeholder, QTextOption(_st.phAlign)); - } - p.restore(); - p.setOpacity(1); - } - QTextEdit::paintEvent(e); -} - -int FlatTextarea::placeholderSkipWidth() const { - if (!_placeholderAfterSymbols) { - return 0; - } - auto text = getTextWithTags().text; - auto result = _st.font->width(text.mid(0, _placeholderAfterSymbols)); - if (_placeholderAfterSymbols > text.size()) { - result += _st.font->spacew; - } - return result; -} - -void FlatTextarea::focusInEvent(QFocusEvent *e) { - if (!_focused) { - _focused = true; - _a_placeholderFocused.start([this] { update(); }, 0., 1., _st.phDuration); - update(); - } - QTextEdit::focusInEvent(e); -} - -void FlatTextarea::focusOutEvent(QFocusEvent *e) { - if (_focused) { - _focused = false; - _a_placeholderFocused.start([this] { update(); }, 1., 0., _st.phDuration); - update(); - } - QTextEdit::focusOutEvent(e); -} - -QSize FlatTextarea::sizeHint() const { - return geometry().size(); -} - -QSize FlatTextarea::minimumSizeHint() const { - return geometry().size(); -} - -EmojiPtr FlatTextarea::getSingleEmoji() const { - QString text; - QTextFragment fragment; - - getSingleEmojiFragment(text, fragment); - - if (!text.isEmpty()) { - auto format = fragment.charFormat(); - auto imageName = static_cast(&format)->name(); - return Ui::Emoji::FromUrl(imageName); - } - return nullptr; -} - -QString FlatTextarea::getInlineBotQuery(UserData **outInlineBot, QString *outInlineBotUsername) const { - Assert(outInlineBot != nullptr); - Assert(outInlineBotUsername != nullptr); - - auto &text = getTextWithTags().text; - auto textLength = text.size(); - - int inlineUsernameStart = 1, inlineUsernameLength = 0; - if (textLength > 2 && text.at(0) == '@' && text.at(1).isLetter()) { - inlineUsernameLength = 1; - for (int i = inlineUsernameStart + 1; i != textLength; ++i) { - if (text.at(i).isLetterOrNumber() || text.at(i).unicode() == '_') { - ++inlineUsernameLength; - continue; - } - if (!text.at(i).isSpace()) { - inlineUsernameLength = 0; - } - break; - } - auto inlineUsernameEnd = inlineUsernameStart + inlineUsernameLength; - auto inlineUsernameEqualsText = (inlineUsernameEnd == textLength); - auto validInlineUsername = false; - if (inlineUsernameEqualsText) { - validInlineUsername = text.endsWith(qstr("bot")); - } else if (inlineUsernameEnd < textLength && inlineUsernameLength) { - validInlineUsername = text.at(inlineUsernameEnd).isSpace(); - } - if (validInlineUsername) { - auto username = text.midRef(inlineUsernameStart, inlineUsernameLength); - if (username != *outInlineBotUsername) { - *outInlineBotUsername = username.toString(); - auto peer = App::peerByName(*outInlineBotUsername); - if (peer) { - if (peer->isUser()) { - *outInlineBot = peer->asUser(); - } else { - *outInlineBot = nullptr; - } - } else { - *outInlineBot = LookingUpInlineBot; - } - } - if (*outInlineBot == LookingUpInlineBot) return QString(); - - if (*outInlineBot && (!(*outInlineBot)->botInfo || (*outInlineBot)->botInfo->inlinePlaceholder.isEmpty())) { - *outInlineBot = nullptr; - } else { - return inlineUsernameEqualsText ? QString() : text.mid(inlineUsernameEnd + 1); - } - } else { - inlineUsernameLength = 0; - } - } - if (inlineUsernameLength < 3) { - *outInlineBot = nullptr; - *outInlineBotUsername = QString(); - } - return QString(); -} - -QString FlatTextarea::getMentionHashtagBotCommandPart(bool &start) const { - start = false; - - int32 pos = textCursor().position(); - if (textCursor().anchor() != pos) return QString(); - - // check mention / hashtag / bot command - 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; - - int32 p = fr.position(), e = (p + fr.length()); - if (p >= pos || e < pos) continue; - - const auto f = fr.charFormat(); - if (f.isImageFormat()) continue; - - bool mentionInCommand = false; - QString t(fr.text()); - for (int i = pos - p; i > 0; --i) { - if (t.at(i - 1) == '@') { - if ((pos - p - i < 1 || t.at(i).isLetter()) && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) { - start = (i == 1) && (p == 0); - return t.mid(i - 1, pos - p - i + 1); - } else if ((pos - p - i < 1 || t.at(i).isLetter()) && i > 2 && (t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_') && !mentionInCommand) { - mentionInCommand = true; - --i; - continue; - } - return QString(); - } else if (t.at(i - 1) == '#') { - if (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_')) { - start = (i == 1) && (p == 0); - return t.mid(i - 1, pos - p - i + 1); - } - return QString(); - } else if (t.at(i - 1) == '/') { - if (i < 2) { - start = (i == 1) && (p == 0); - return t.mid(i - 1, pos - p - i + 1); - } - return QString(); - } - if (pos - p - i > 127 || (!mentionInCommand && (pos - p - i > 63))) break; - if (!t.at(i - 1).isLetterOrNumber() && t.at(i - 1) != '_') break; - } - break; - } - return QString(); -} - -void FlatTextarea::insertTag(const QString &text, QString tagId) { - auto cursor = textCursor(); - int32 pos = cursor.position(); - - auto doc = document(); - auto block = doc->findBlock(pos); - for (auto iter = block.begin(); !iter.atEnd(); ++iter) { - auto fragment = iter.fragment(); - Assert(fragment.isValid()); - - int fragmentPosition = fragment.position(); - int fragmentEnd = (fragmentPosition + fragment.length()); - if (fragmentPosition >= pos || fragmentEnd < pos) continue; - - auto format = fragment.charFormat(); - if (format.isImageFormat()) continue; - - bool mentionInCommand = false; - 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); - int till = fragmentPosition + i; - for (; (till < fragmentEnd && till < pos); ++till) { - auto ch = fragmentText.at(till - fragmentPosition); - if (!ch.isLetterOrNumber() && ch != '_' && ch != '@') { - break; - } - } - if (till < fragmentEnd && fragmentText.at(till - fragmentPosition) == ' ') { - ++till; - } - cursor.setPosition(till, QTextCursor::KeepAnchor); - break; - } 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 - fragmentPosition - i > 127 || (!mentionInCommand && (pos - fragmentPosition - i > 63))) break; - if (!fragmentText.at(i - 1).isLetterOrNumber() && fragmentText.at(i - 1) != '_') break; - } - break; - } - if (tagId.isEmpty()) { - cursor.insertText(text + ' ', _defaultCharFormat); - } else { - _insertedTags.clear(); - _insertedTags.push_back({ 0, text.size(), tagId }); - _insertedTagsAreFromMime = false; - cursor.insertText(text + ' '); - _insertedTags.clear(); - } -} - -void FlatTextarea::setTagMimeProcessor(std::unique_ptr &&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; - - if (start < 0) start = 0; - - QTextDocument *doc(document()); - QTextBlock from = doc->findBlock(start), till = doc->findBlock(end); - if (till.isValid()) till = till.next(); - - for (QTextBlock b = from; b != till; b = b.next()) { - for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) { - QTextFragment fr(iter.fragment()); - if (!fr.isValid()) continue; - - int32 p = fr.position(), e = (p + fr.length()); - if (p >= end || e <= start) { - continue; - } - - const auto f = fr.charFormat(); - auto t = fr.text(); - if (p < start) { - t = t.mid(start - p, end - start); - } else if (e > end) { - t = t.mid(0, end - p); - } - if (f.isImageFormat() - && !t.isEmpty() - && t[0] == kObjectReplacementCh) { - const auto imageName = static_cast( - &f)->name(); - if (Ui::Emoji::FromUrl(imageName)) { - fragment = fr; - text = t; - return; - } - } - return; - } - } - return; -} - -void FlatTextarea::removeSingleEmoji() { - QString text; - QTextFragment fragment; - - getSingleEmojiFragment(text, fragment); - - if (!text.isEmpty()) { - QTextCursor t(textCursor()); - t.setPosition(fragment.position()); - t.setPosition(fragment.position() + fragment.length(), QTextCursor::KeepAnchor); - t.removeSelectedText(); - setTextCursor(t); - } -} - -namespace { - -class TagAccumulator { -public: - TagAccumulator(FlatTextarea::TagList *tags) : _tags(tags) { - } - - bool changed() const { - return _changed; - } - - void feed(const QString &randomTagId, int currentPosition) { - if (randomTagId == _currentTagId) return; - - if (!_currentTagId.isEmpty()) { - int randomPartPosition = _currentTagId.lastIndexOf('/'); - 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; - TextWithTags::Tag tag = { - _currentStart, - currentPosition - _currentStart, - _currentTagId.mid(0, randomPartPosition), - }; - if (_currentTag < _tags->size()) { - (*_tags)[_currentTag] = tag; - } else { - _tags->push_back(tag); - } - } - ++_currentTag; - } - _currentTagId = randomTagId; - _currentStart = currentPosition; - }; - - void finish() { - if (_currentTag < _tags->size()) { - _tags->resize(_currentTag); - _changed = true; - } - } - -private: - FlatTextarea::TagList *_tags; - bool _changed = false; - - int _currentTag = 0; - int _currentStart = 0; - QString _currentTagId; - -}; - -} // namespace - -QString FlatTextarea::getTextPart(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(); - - int32 possibleLen = 0; - for (QTextBlock b = from; b != till; b = b.next()) { - possibleLen += b.length(); - } - QString result; - result.reserve(possibleLen + 1); - if (!full && end < 0) { - end = possibleLen; - } - - 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) { - tillFragmentEnd = (e <= end); - if (p == end) { - tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); - } - if (p >= end) { - break; - } - if (e <= start) { - continue; - } - } - if (full || p >= start) { - tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); - } - - const auto f = fragment.charFormat(); - QString emojiText; - auto t = fragment.text(); - if (!full) { - if (p < start) { - t = t.mid(start - p, end - start); - } else if (e > end) { - t = t.mid(0, end - p); - } - } - QChar *ub = t.data(), *uc = ub, *ue = uc + t.size(); - for (; uc != ue; ++uc) { - switch (uc->unicode()) { - case 0xfdd0: // QTextBeginningOfFrame - case 0xfdd1: // QTextEndOfFrame - case QChar::ParagraphSeparator: - case QChar::LineSeparator: { - *uc = QLatin1Char('\n'); - } break; - case QChar::Nbsp: { - *uc = QLatin1Char(' '); - } break; - case QChar::ObjectReplacementCharacter: { - if (emojiText.isEmpty() && f.isImageFormat()) { - const auto imageName = static_cast(&f)->name(); - if (const auto emoji = Ui::Emoji::FromUrl(imageName)) { - emojiText = emoji->text(); - } - } - if (uc > ub) result.append(ub, uc - ub); - if (!emojiText.isEmpty()) result.append(emojiText); - ub = uc + 1; - } break; - } - } - if (uc > ub) result.append(ub, uc - ub); - } - result.append('\n'); - } - result.chop(1); - - if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size()); - tagAccumulator.finish(); - - if (outTagsChanged) { - *outTagsChanged = tagAccumulator.changed(); - } - return result; -} - -bool FlatTextarea::hasText() const { - QTextDocument *doc(document()); - QTextBlock from = doc->begin(), till = doc->end(); - - if (from == till) return false; - - for (QTextBlock::Iterator iter = from.begin(); !iter.atEnd(); ++iter) { - QTextFragment fragment(iter.fragment()); - if (!fragment.isValid()) continue; - if (!fragment.text().isEmpty()) return true; - } - return (from.next() != till); -} - -bool FlatTextarea::isUndoAvailable() const { - return _undoAvailable; -} - -bool FlatTextarea::isRedoAvailable() const { - return _redoAvailable; -} - -void FlatTextarea::parseLinks() { // some code is duplicated in text.cpp! - LinkRanges newLinks; - - QString text(toPlainText()); - if (text.isEmpty()) { - if (!_links.isEmpty()) { - _links.clear(); - emit linksChanged(); - } - return; - } - - auto len = text.size(); - const QChar *start = text.unicode(), *end = start + text.size(); - for (auto offset = 0, matchOffset = offset; offset < len;) { - auto m = TextUtilities::RegExpDomain().match(text, matchOffset); - if (!m.hasMatch()) break; - - auto domainOffset = m.capturedStart(); - - auto protocol = m.captured(1).toLower(); - auto topDomain = m.captured(3).toLower(); - auto isProtocolValid = protocol.isEmpty() || TextUtilities::IsValidProtocol(protocol); - auto isTopDomainValid = !protocol.isEmpty() || TextUtilities::IsValidTopDomain(topDomain); - - if (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) { - auto forMailName = text.mid(offset, domainOffset - offset - 1); - auto mMailName = TextUtilities::RegExpMailNameAtEnd().match(forMailName); - if (mMailName.hasMatch()) { - offset = matchOffset = m.capturedEnd(); - continue; - } - } - if (!isProtocolValid || !isTopDomainValid) { - offset = matchOffset = m.capturedEnd(); - continue; - } - - QStack parenth; - const QChar *domainEnd = start + m.capturedEnd(), *p = domainEnd; - for (; p < end; ++p) { - QChar ch(*p); - if (chIsLinkEnd(ch)) break; // link finished - if (chIsAlmostLinkEnd(ch)) { - const QChar *endTest = p + 1; - while (endTest < end && chIsAlmostLinkEnd(*endTest)) { - ++endTest; - } - if (endTest >= end || chIsLinkEnd(*endTest)) { - break; // link finished at p - } - p = endTest; - ch = *p; - } - if (ch == '(' || ch == '[' || ch == '{' || ch == '<') { - parenth.push(p); - } else if (ch == ')' || ch == ']' || ch == '}' || ch == '>') { - if (parenth.isEmpty()) break; - const QChar *q = parenth.pop(), open(*q); - if ((ch == ')' && open != '(') || (ch == ']' && open != '[') || (ch == '}' && open != '{') || (ch == '>' && open != '<')) { - p = q; - break; - } - } - } - if (p > domainEnd) { // check, that domain ended - if (domainEnd->unicode() != '/' && domainEnd->unicode() != '?') { - matchOffset = domainEnd - start; - continue; - } - } - newLinks.push_back({ domainOffset - 1, static_cast(p - start - domainOffset + 2) }); - offset = matchOffset = p - start; - } - - if (newLinks != _links) { - _links = newLinks; - emit linksChanged(); - } -} - -QStringList FlatTextarea::linksList() const { - QStringList result; - if (!_links.isEmpty()) { - QString text(toPlainText()); - for_const (auto &link, _links) { - result.push_back(text.mid(link.start + 1, link.length - 2)); - } - } - return result; -} - -void FlatTextarea::insertFromMimeData(const QMimeData *source) { - auto mime = TextUtilities::TagsMimeType(); - auto text = source->text(); - if (source->hasFormat(mime)) { - auto tagsData = source->data(mime); - _insertedTags = TextUtilities::DeserializeTags( - tagsData, - text.size()); - _insertedTagsAreFromMime = true; - } else { - _insertedTags.clear(); - } - auto cursor = textCursor(); - _realInsertPosition = qMin(cursor.position(), cursor.anchor()); - _realCharsAdded = text.size(); - QTextEdit::insertFromMimeData(source); - if (!_inDrop) { - emit spacedReturnedPasted(); - _insertedTags.clear(); - _realInsertPosition = -1; - } -} - -void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) { - auto format = PrepareEmojiFormat(emoji, _st.font); - if (c.charFormat().isAnchor()) { - format.setAnchor(true); - format.setAnchorName(c.charFormat().anchorName()); - format.setForeground(st::defaultTextPalette.linkFg); - } - c.insertText(kObjectReplacement, format); -} - -QVariant FlatTextarea::loadResource(int type, const QUrl &name) { - auto imageName = name.toDisplayString(); - if (auto emoji = Ui::Emoji::FromUrl(imageName)) { - return QVariant(App::emojiSingle(emoji, _st.font->height)); - } - return QVariant(); -} - -void FlatTextarea::checkContentHeight() { - if (heightAutoupdated()) { - emit resized(); - } -} - -namespace { - -// Optimization: with null page size document does not re-layout -// on each insertText / mergeCharFormat. -void prepareFormattingOptimization(QTextDocument *document) { - if (!document->pageSize().isNull()) { - document->setPageSize(QSizeF(0, 0)); - } -} - -void removeTags(style::color textFg, QTextDocument *document, int from, int end) { - QTextCursor c(document->docHandle(), 0); - c.setPosition(from); - c.setPosition(end, QTextCursor::KeepAnchor); - - QTextCharFormat format; - format.setAnchor(false); - format.setAnchorName(QString()); - format.setForeground(textFg); - c.mergeCharFormat(format); -} - -// Returns the position of the first inserted tag or "changedEnd" value if none found. -int processInsertedTags(style::color textFg, QTextDocument *document, int changedPosition, int changedEnd, const FlatTextarea::TagList &tags, FlatTextarea::TagMimeProcessor *processor) { - int firstTagStart = changedEnd; - int applyNoTagFrom = changedEnd; - for_const (auto &tag, tags) { - int tagFrom = changedPosition + tag.offset; - int tagTo = tagFrom + tag.length; - accumulate_max(tagFrom, changedPosition); - accumulate_min(tagTo, changedEnd); - auto tagId = processor ? processor->tagFromMimeTag(tag.id) : tag.id; - if (tagTo > tagFrom && !tagId.isEmpty()) { - accumulate_min(firstTagStart, tagFrom); - - prepareFormattingOptimization(document); - - if (applyNoTagFrom < tagFrom) { - removeTags(textFg, document, applyNoTagFrom, tagFrom); - } - QTextCursor c(document->docHandle(), 0); - c.setPosition(tagFrom); - c.setPosition(tagTo, QTextCursor::KeepAnchor); - - QTextCharFormat format; - format.setAnchor(true); - format.setAnchorName(tagId + '/' + QString::number(rand_value())); - format.setForeground(st::defaultTextPalette.linkFg); - c.mergeCharFormat(format); - - applyNoTagFrom = tagTo; - } - } - if (applyNoTagFrom < changedEnd) { - removeTags(textFg, document, applyNoTagFrom, changedEnd); - } - - return firstTagStart; -} - -// When inserting a part of text inside a tag we need to have -// a way to know if the insertion replaced the end of the tag -// or it was strictly inside (in the middle) of the tag. -bool wasInsertTillTheEndOfTag(QTextBlock block, QTextBlock::iterator fragmentIt, int insertionEnd) { - auto insertTagName = fragmentIt.fragment().charFormat().anchorName(); - while (true) { - for (; !fragmentIt.atEnd(); ++fragmentIt) { - auto fragment = fragmentIt.fragment(); - bool fragmentOutsideInsertion = (fragment.position() >= insertionEnd); - if (fragmentOutsideInsertion) { - return (fragment.charFormat().anchorName() != insertTagName); - } - int fragmentEnd = fragment.position() + fragment.length(); - bool notFullFragmentInserted = (fragmentEnd > insertionEnd); - if (notFullFragmentInserted) { - return false; - } - } - if (block.isValid()) { - fragmentIt = block.begin(); - block = block.next(); - } else { - break; - } - } - // Insertion goes till the end of the text => not strictly inside a tag. - return true; -} - -struct FormattingAction { - enum class Type { - Invalid, - InsertEmoji, - TildeFont, - RemoveTag, - ClearInstantReplace, - }; - Type type = Type::Invalid; - EmojiPtr emoji = nullptr; - bool isTilde = false; - int intervalStart = 0; - int intervalEnd = 0; -}; - -} // namespace - -void FlatTextarea::processFormatting(int insertPosition, int insertEnd) { - // Tilde formatting. - auto tildeFormatting = !cRetina() && (font().pixelSize() == 13) && (font().family() == qstr("Open Sans")); - auto isTildeFragment = false; - auto tildeRegularFont = tildeFormatting ? qsl("Open Sans") : QString(); - auto tildeFixedFont = tildeFormatting ? Fonts::GetOverride(qsl("Open Sans Semibold")) : QString(); - - // First tag handling (the one we inserted text to). - bool startTagFound = false; - bool breakTagOnNotLetter = false; - - auto doc = document(); - - // Apply inserted tags. - auto insertedTagsProcessor = _insertedTagsAreFromMime ? _tagMimeProcessor.get() : nullptr; - int breakTagOnNotLetterTill = processInsertedTags(_st.textColor, doc, insertPosition, insertEnd, - _insertedTags, insertedTagsProcessor); - using ActionType = FormattingAction::Type; - while (true) { - FormattingAction action; - - 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()) { - for (auto fragmentIt = block.begin(); !fragmentIt.atEnd(); ++fragmentIt) { - auto fragment = fragmentIt.fragment(); - Assert(fragment.isValid()); - - int fragmentPosition = fragment.position(); - if (insertPosition >= fragmentPosition + fragment.length()) { - continue; - } - int changedPositionInFragment = insertPosition - fragmentPosition; // Can be negative. - int changedEndInFragment = insertEnd - fragmentPosition; - if (changedEndInFragment <= 0) { - break; - } - - auto format = fragment.charFormat(); - if (tildeFormatting) { - isTildeFragment = (format.fontFamily() == tildeFixedFont); - } - - auto fragmentText = fragment.text(); - auto *textStart = fragmentText.constData(); - auto *textEnd = textStart + fragmentText.size(); - - const auto with = format.property(kInstantReplaceWithId); - if (with.isValid()) { - const auto string = with.toString(); - if (fragmentText != string) { - action.type = ActionType::ClearInstantReplace; - action.intervalStart = fragmentPosition - + (fragmentText.startsWith(string) - ? string.size() - : 0); - action.intervalEnd = fragmentPosition - + fragmentText.size(); - break; - } - } - - if (!startTagFound) { - startTagFound = true; - auto tagName = format.anchorName(); - if (!tagName.isEmpty()) { - breakTagOnNotLetter = wasInsertTillTheEndOfTag(block, fragmentIt, insertEnd); - } - } - - auto *ch = textStart + qMax(changedPositionInFragment, 0); - for (; ch < textEnd; ++ch) { - int emojiLength = 0; - if (auto emoji = Ui::Emoji::Find(ch, textEnd, &emojiLength)) { - // Replace emoji if no current action is prepared. - if (action.type == ActionType::Invalid) { - action.type = ActionType::InsertEmoji; - action.emoji = emoji; - action.intervalStart = fragmentPosition + (ch - textStart); - action.intervalEnd = action.intervalStart + emojiLength; - } - break; - } - - if (breakTagOnNotLetter && !ch->isLetter()) { - // Remove tag name till the end if no current action is prepared. - if (action.type != ActionType::Invalid) { - break; - } - breakTagOnNotLetter = false; - if (fragmentPosition + (ch - textStart) < breakTagOnNotLetterTill) { - action.type = ActionType::RemoveTag; - action.intervalStart = fragmentPosition + (ch - textStart); - action.intervalEnd = breakTagOnNotLetterTill; - break; - } - } - if (tildeFormatting) { // Tilde symbol fix in OpenSans. - bool tilde = (ch->unicode() == '~'); - if ((tilde && !isTildeFragment) || (!tilde && isTildeFragment)) { - if (action.type == ActionType::Invalid) { - action.type = ActionType::TildeFont; - action.intervalStart = fragmentPosition + (ch - textStart); - action.intervalEnd = action.intervalStart + 1; - action.isTilde = tilde; - } else { - ++action.intervalEnd; - } - } else if (action.type == ActionType::TildeFont) { - break; - } - } - - if (ch + 1 < textEnd && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) { - ++ch; - ++fragmentPosition; - } - } - if (action.type != ActionType::Invalid) break; - } - if (action.type != ActionType::Invalid) break; - } - if (action.type != ActionType::Invalid) { - prepareFormattingOptimization(doc); - - QTextCursor c(doc->docHandle(), 0); - c.setPosition(action.intervalStart); - c.setPosition(action.intervalEnd, QTextCursor::KeepAnchor); - if (action.type == ActionType::InsertEmoji) { - insertEmoji(action.emoji, c); - insertPosition = action.intervalStart + 1; - } else if (action.type == ActionType::RemoveTag) { - QTextCharFormat format; - format.setAnchor(false); - format.setAnchorName(QString()); - format.setForeground(_st.textColor); - c.mergeCharFormat(format); - } else if (action.type == ActionType::TildeFont) { - QTextCharFormat format; - format.setFontFamily(action.isTilde ? tildeFixedFont : tildeRegularFont); - c.mergeCharFormat(format); - insertPosition = action.intervalEnd; - } else if (action.type == ActionType::ClearInstantReplace) { - c.setCharFormat(_defaultCharFormat); - } - } else { - break; - } - } -} - -void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int charsAdded) { - if (_correcting) return; - - 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; - if (_maxLength >= 0) { - QTextCursor c(document()->docHandle(), 0); - c.movePosition(QTextCursor::End); - int32 fullSize = c.position(), toRemove = fullSize - _maxLength; - if (toRemove > 0) { - if (toRemove > insertLength) { - if (insertLength) { - c.setPosition(insertPosition); - c.setPosition((insertPosition + insertLength), QTextCursor::KeepAnchor); - c.removeSelectedText(); - } - c.setPosition(fullSize - (toRemove - insertLength)); - c.setPosition(fullSize, QTextCursor::KeepAnchor); - c.removeSelectedText(); - } else { - c.setPosition(insertPosition + (insertLength - toRemove)); - c.setPosition(insertPosition + insertLength, QTextCursor::KeepAnchor); - c.removeSelectedText(); - } - } - } - _correcting = false; - - 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(); - } - } else { - parseLinks(); - } - - if (document()->availableRedoSteps() > 0) { - QTextCursor(document()->docHandle(), 0).endEditBlock(); - return; - } - - if (insertLength <= 0) { - QTextCursor(document()->docHandle(), 0).endEditBlock(); - return; - } - - _correcting = true; - auto pageSize = document()->pageSize(); - processFormatting(insertPosition, insertPosition + insertLength); - if (document()->pageSize() != pageSize) { - document()->setPageSize(pageSize); - } - _correcting = false; - - QTextCursor(document()->docHandle(), 0).endEditBlock(); -} - -void FlatTextarea::onDocumentContentsChanged() { - if (_correcting) return; - - auto tagsChanged = false; - auto curText = getTextPart(0, -1, &_lastTextWithTags.tags, &tagsChanged); - - _correcting = true; - correctValue(_lastTextWithTags.text, curText, _lastTextWithTags.tags); - _correcting = false; - - bool textOrTagsChanged = tagsChanged || (_lastTextWithTags.text != curText); - if (textOrTagsChanged) { - _lastTextWithTags.text = curText; - emit changed(); - checkContentHeight(); - } - updatePlaceholder(); - if (App::wnd()) App::wnd()->updateGlobalMenu(); -} - -void FlatTextarea::onUndoAvailable(bool avail) { - _undoAvailable = avail; - if (App::wnd()) App::wnd()->updateGlobalMenu(); -} - -void FlatTextarea::onRedoAvailable(bool avail) { - _redoAvailable = avail; - if (App::wnd()) App::wnd()->updateGlobalMenu(); -} - -void FlatTextarea::setPlaceholder(base::lambda placeholderFactory, int afterSymbols) { - _placeholderFactory = std::move(placeholderFactory); - if (_placeholderAfterSymbols != afterSymbols) { - _placeholderAfterSymbols = afterSymbols; - updatePlaceholder(); - } - refreshPlaceholder(); -} - -void FlatTextarea::refreshPlaceholder() { - auto skipWidth = placeholderSkipWidth(); - auto placeholderText = _placeholderFactory ? _placeholderFactory() : QString(); - _placeholder = _st.font->elided(placeholderText, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1 - skipWidth); - update(); -} - -void FlatTextarea::updatePlaceholder() { - auto textSize = (getTextWithTags().text.size() + textCursor().block().layout()->preeditAreaText().size()); - auto placeholderVisible = (textSize <= _placeholderAfterSymbols); - if (_placeholderVisible != placeholderVisible) { - _placeholderVisible = placeholderVisible; - _a_placeholderVisible.start([this] { update(); }, _placeholderVisible ? 0. : 1., _placeholderVisible ? 1. : 0., _st.phDuration); - } -} - -QMimeData *FlatTextarea::createMimeDataFromSelection() const { - QMimeData *result = new QMimeData(); - QTextCursor c(textCursor()); - int32 start = c.selectionStart(), end = c.selectionEnd(); - if (end > start) { - TagList tags; - result->setText(getTextPart(start, end, &tags)); - if (!tags.isEmpty()) { - if (_tagMimeProcessor) { - for (auto &tag : tags) { - tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id); - } - } - result->setData( - TextUtilities::TagsMimeType(), - TextUtilities::SerializeTags(tags)); - } - } - return result; -} - -void FlatTextarea::setSubmitSettings(SubmitSettings settings) { - _submitSettings = settings; -} - -void FlatTextarea::keyPressEvent(QKeyEvent *e) { - bool shift = e->modifiers().testFlag(Qt::ShiftModifier); - bool macmeta = (cPlatform() == dbipMac || cPlatform() == dbipMacOld) && e->modifiers().testFlag(Qt::ControlModifier) && !e->modifiers().testFlag(Qt::MetaModifier) && !e->modifiers().testFlag(Qt::AltModifier); - bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier); - bool enterSubmit = (ctrl && shift); - if (ctrl && _submitSettings != SubmitSettings::None && _submitSettings != SubmitSettings::Enter) { - enterSubmit = true; - } - if (!ctrl && !shift && _submitSettings != SubmitSettings::None && _submitSettings != SubmitSettings::CtrlEnter) { - enterSubmit = true; - } - bool enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return); - - if (macmeta && e->key() == Qt::Key_Backspace) { - QTextCursor tc(textCursor()), start(tc); - start.movePosition(QTextCursor::StartOfLine); - tc.setPosition(start.position(), QTextCursor::KeepAnchor); - tc.removeSelectedText(); - } else if (e->key() == Qt::Key_Backspace - && e->modifiers() == 0 - && revertInstantReplace()) { - e->accept(); - } else if (enter && enterSubmit) { - emit submitted(ctrl && shift); - } else if (e->key() == Qt::Key_Escape) { - emit cancelled(); - } else if (e->key() == Qt::Key_Tab || (ctrl && e->key() == Qt::Key_Backtab)) { - if (ctrl) { - e->ignore(); - } else { - emit tabbed(); - } - } else if (e->key() == Qt::Key_Search || e == QKeySequence::Find) { - e->ignore(); -#ifdef Q_OS_MAC - } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) { - auto cursor = textCursor(); - int start = cursor.selectionStart(), end = cursor.selectionEnd(); - if (end > start) { - TagList tags; - QApplication::clipboard()->setText(getTextPart(start, end, &tags), QClipboard::FindBuffer); - } -#endif // Q_OS_MAC - } else { - const auto text = e->text(); - const auto key = e->key(); - auto cursor = textCursor(); - if (enter && ctrl) { - e->setModifiers(e->modifiers() & ~Qt::ControlModifier); - } - bool spaceOrReturn = false; - if (!text.isEmpty() && text.size() < 3) { - const auto ch = text[0]; - if (ch == '\n' - || ch == '\r' - || ch.isSpace() - || ch == QChar::LineSeparator) { - spaceOrReturn = true; - } - } - QTextEdit::keyPressEvent(e); - if (cursor == textCursor()) { - bool check = false; - if (key == Qt::Key_PageUp || key == Qt::Key_Up) { - cursor.movePosition( - QTextCursor::Start, - (e->modifiers().testFlag(Qt::ShiftModifier) - ? QTextCursor::KeepAnchor - : QTextCursor::MoveAnchor)); - check = true; - } else if (key == Qt::Key_PageDown || key == Qt::Key_Down) { - cursor.movePosition( - QTextCursor::End, - (e->modifiers().testFlag(Qt::ShiftModifier) - ? QTextCursor::KeepAnchor - : QTextCursor::MoveAnchor)); - check = true; - } - if (check) { - if (cursor == textCursor()) { - e->ignore(); - } else { - setTextCursor(cursor); - } - } - } - processInstantReplaces(text); - if (spaceOrReturn) { - emit spacedReturnedPasted(); - } - } -} - -const InstantReplaces &FlatTextarea::instantReplaces() const { - return _mutableInstantReplaces; -} - -void FlatTextarea::processInstantReplaces(const QString &text) { - const auto &replaces = instantReplaces(); - if (text.size() != 1 - || !_instantReplacesEnabled - || !replaces.maxLength) { - return; - } - const auto it = replaces.reverseMap.tail.find(text[0]); - if (it == end(replaces.reverseMap.tail)) { - return; - } - const auto position = textCursor().position(); - auto tags = QVector(); - const auto typed = getTextPart( - std::max(position - replaces.maxLength, 0), - position - 1, - &tags); - auto node = &it->second; - auto i = typed.size(); - do { - if (!node->text.isEmpty()) { - applyInstantReplace(typed.mid(i) + text, node->text); - return; - } else if (!i) { - return; - } - const auto it = node->tail.find(typed[--i]); - if (it == end(node->tail)) { - return; - } - node = &it->second; - } while (true); -} - -void FlatTextarea::applyInstantReplace( - const QString &what, - const QString &with) { - const auto length = int(what.size()); - const auto cursor = textCursor(); - const auto position = cursor.position(); - if (cursor.anchor() != position) { - return; - } else if (position < length) { - return; - } - commitInstantReplacement(position - length, position, with, what); -} - -void FlatTextarea::commitInstantReplacement( - int from, - int till, - const QString &with, - base::optional checkOriginal) { - auto tags = QVector(); - const auto original = getTextPart(from, till, &tags); - if (checkOriginal - && checkOriginal->compare(original, Qt::CaseInsensitive) != 0) { - return; - } - - auto format = [&]() -> QTextCharFormat { - auto emojiLength = 0; - const auto emoji = Ui::Emoji::Find(with, &emojiLength); - if (!emoji || with.size() != emojiLength) { - return _defaultCharFormat; - } - const auto use = [&] { - if (!emoji->hasVariants()) { - return emoji; - } - const auto nonColored = emoji->nonColoredId(); - const auto it = cEmojiVariants().constFind(nonColored); - return (it != cEmojiVariants().cend()) - ? emoji->variant(it.value()) - : emoji; - }(); - Ui::Emoji::AddRecent(use); - return PrepareEmojiFormat(use, _st.font); - }(); - const auto replacement = format.isImageFormat() - ? kObjectReplacement - : with; - format.setProperty(kInstantReplaceWhatId, original); - format.setProperty(kInstantReplaceWithId, replacement); - format.setProperty(kInstantReplaceRandomId, rand_value()); - auto cursor = textCursor(); - cursor.setPosition(from); - cursor.setPosition(till, QTextCursor::KeepAnchor); - cursor.insertText(replacement, format); -} - -bool FlatTextarea::revertInstantReplace() { - const auto cursor = textCursor(); - const auto position = cursor.position(); - if (position <= 0 || cursor.anchor() != position) { - return false; - } - const auto inside = position - 1; - const auto block = document()->findBlock(inside); - if (block == document()->end()) { - return false; - } - for (auto i = block.begin(); !i.atEnd(); ++i) { - const auto fragment = i.fragment(); - const auto fragmentStart = fragment.position(); - const auto fragmentEnd = fragmentStart + fragment.length(); - if (fragmentEnd <= inside) { - continue; - } else if (fragmentStart > inside || fragmentEnd != position) { - return false; - } - const auto format = fragment.charFormat(); - const auto with = format.property(kInstantReplaceWithId); - if (!with.isValid()) { - return false; - } - const auto string = with.toString(); - if (fragment.text() != string) { - return false; - } - auto replaceCursor = cursor; - replaceCursor.setPosition(fragmentStart); - replaceCursor.setPosition(fragmentEnd, QTextCursor::KeepAnchor); - const auto what = format.property(kInstantReplaceWhatId).toString(); - replaceCursor.insertText(what, _defaultCharFormat); - return true; - } - return false; -} - -void FlatTextarea::resizeEvent(QResizeEvent *e) { - refreshPlaceholder(); - QTextEdit::resizeEvent(e); - checkContentHeight(); -} - -void FlatTextarea::mousePressEvent(QMouseEvent *e) { - QTextEdit::mousePressEvent(e); -} - -void FlatTextarea::dropEvent(QDropEvent *e) { - _inDrop = true; - QTextEdit::dropEvent(e); - _inDrop = false; - _insertedTags.clear(); - _realInsertPosition = -1; - - emit spacedReturnedPasted(); -} - -void FlatTextarea::contextMenuEvent(QContextMenuEvent *e) { - if (auto menu = createStandardContextMenu()) { - (new Ui::PopupMenu(nullptr, menu))->popup(e->globalPos()); - } -} - FlatInput::FlatInput(QWidget *parent, const style::FlatInput &st, base::lambda placeholderFactory, const QString &v) : TWidgetHelper(v, parent) , _oldtext(v) , _placeholderFactory(std::move(placeholderFactory)) @@ -2010,11 +689,13 @@ InputField::InputField( : RpWidget(parent) , _st(st) , _mode(mode) +, _minHeight(st.heightMin) +, _maxHeight(st.heightMax) , _inner(this) , _lastTextWithTags(value) , _placeholderFactory(std::move(placeholderFactory)) { _inner->setAcceptRichText(false); - resize(_st.width, _st.heightMin); + resize(_st.width, _minHeight); if (_st.textBg->c.alphaF() >= 1.) { setAttribute(Qt::WA_OpaquePaintEvent); @@ -2057,6 +738,12 @@ InputField::InputField( connect(_inner, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool))); if (App::wnd()) connect(_inner, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu())); + const auto bar = _inner->verticalScrollBar(); + _scrollTop = bar->value(); + connect(bar, &QScrollBar::valueChanged, [=] { + _scrollTop = bar->value(); + }); + setCursor(style::cur_text); heightAutoupdated(); @@ -2071,6 +758,18 @@ InputField::InputField( finishAnimating(); } +const rpl::variable &InputField::scrollTop() const { + return _scrollTop; +} + +int InputField::scrollTopMax() const { + return _inner->verticalScrollBar()->maximum(); +} + +void InputField::scrollTo(int top) { + _inner->verticalScrollBar()->setValue(top); +} + bool InputField::viewportEventInner(QEvent *e) { if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate @@ -2110,9 +809,107 @@ void InputField::enableInstantReplaces(bool enabled) { _instantReplacesEnabled = enabled; } +void InputField::setTagMimeProcessor( + std::unique_ptr &&processor) { + _tagMimeProcessor = std::move(processor); +} + +void InputField::setMaxLength(int length) { + _maxLength = length; +} + +void InputField::setMinHeight(int height) { + _minHeight = height; +} + +void InputField::setMaxHeight(int height) { + _maxHeight = height; +} + +void InputField::insertTag(const QString &text, QString tagId) { + auto cursor = textCursor(); + const auto position = cursor.position(); + + const auto document = _inner->document(); + auto block = document->findBlock(position); + for (auto iter = block.begin(); !iter.atEnd(); ++iter) { + auto fragment = iter.fragment(); + Assert(fragment.isValid()); + + const auto fragmentPosition = fragment.position(); + const auto fragmentEnd = (fragmentPosition + fragment.length()); + if (fragmentPosition >= position || fragmentEnd < position) { + continue; + } + + const auto format = fragment.charFormat(); + if (format.isImageFormat()) { + continue; + } + + auto mentionInCommand = false; + const auto fragmentText = fragment.text(); + for (auto i = position - fragmentPosition; i > 0; --i) { + const auto previous = fragmentText[i - 1]; + if (previous == '@' || previous == '#' || previous == '/') { + if ((i == position - fragmentPosition + || (previous == '/' + ? fragmentText[i].isLetterOrNumber() + : fragmentText[i].isLetter()) + || previous == '#') && + (i < 2 || !(fragmentText[i - 2].isLetterOrNumber() + || fragmentText[i - 2] == '_'))) { + cursor.setPosition(fragmentPosition + i - 1); + auto till = fragmentPosition + i; + for (; (till < fragmentEnd && till < position); ++till) { + const auto ch = fragmentText[till - fragmentPosition]; + if (!ch.isLetterOrNumber() && ch != '_' && ch != '@') { + break; + } + } + if (till < fragmentEnd + && fragmentText[till - fragmentPosition] == ' ') { + ++till; + } + cursor.setPosition(till, QTextCursor::KeepAnchor); + break; + } else if ((i == position - fragmentPosition + || fragmentText[i].isLetter()) + && fragmentText[i - 1] == '@' + && (i > 2) + && (fragmentText[i - 2].isLetterOrNumber() + || fragmentText[i - 2] == '_') + && !mentionInCommand) { + mentionInCommand = true; + --i; + continue; + } + break; + } + if (position - fragmentPosition - i > 127 + || (!mentionInCommand + && (position - fragmentPosition - i > 63)) + || (!fragmentText[i - 1].isLetterOrNumber() + && fragmentText[i - 1] != '_')) { + break; + } + } + break; + } + if (tagId.isEmpty()) { + cursor.insertText(text + ' ', _defaultCharFormat); + } else { + _insertedTags.clear(); + _insertedTags.push_back({ 0, text.size(), tagId }); + _insertedTagsAreFromMime = false; + cursor.insertText(text + ' '); + _insertedTags.clear(); + } +} + bool InputField::heightAutoupdated() { - if (_st.heightMin < 0 - || _st.heightMax < 0 + if (_minHeight < 0 + || _maxHeight < 0 || _inHeightCheck || _mode == Mode::SingleLine) { return false; @@ -2122,11 +919,11 @@ bool InputField::heightAutoupdated() { SendPendingMoveResizeEvents(this); - int newh = qCeil(_inner->document()->size().height()) + _st.textMargins.top() + _st.textMargins.bottom(); - if (newh > _st.heightMax) { - newh = _st.heightMax; - } else if (newh < _st.heightMin) { - newh = _st.heightMin; + int newh = ceil(document()->size().height()) + _st.textMargins.top() + _st.textMargins.bottom() + 2 * _fakeMargin; + if (newh > _maxHeight) { + newh = _maxHeight; + } else if (newh < _minHeight) { + newh = _minHeight; } if (height() != newh) { resize(width(), newh); @@ -2242,13 +1039,21 @@ void InputField::paintEvent(QPaintEvent *e) { auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree); - auto r = rect().marginsRemoved(_st.textMargins + _st.placeholderMargins); - r.moveLeft(r.left() + placeholderLeft); - if (rtl()) r.moveLeft(width() - r.left() - r.width()); - p.setFont(_st.font); p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree)); - p.drawText(r, _placeholder, _st.placeholderAlign); + + if (_st.placeholderAlign == style::al_topleft && _placeholderAfterSymbols > 0) { + const auto skipWidth = placeholderSkipWidth(); + p.drawText( + _st.textMargins.left() + _st.placeholderMargins.left() + skipWidth, + _st.textMargins.top() + _st.placeholderMargins.top() + _st.placeholderFont->ascent, + _placeholder); + } else { + auto r = rect().marginsRemoved(_st.textMargins + _st.placeholderMargins); + r.moveLeft(r.left() + placeholderLeft); + if (rtl()) r.moveLeft(width() - r.left() - r.width()); + p.drawText(r, _placeholder, _st.placeholderAlign); + } p.restore(); } @@ -2256,6 +1061,18 @@ void InputField::paintEvent(QPaintEvent *e) { TWidget::paintEvent(e); } +int InputField::placeholderSkipWidth() const { + if (!_placeholderAfterSymbols) { + return 0; + } + const auto &text = getTextWithTags().text; + auto result = _st.font->width(text.mid(0, _placeholderAfterSymbols)); + if (_placeholderAfterSymbols > text.size()) { + result += _st.font->spacew; + } + return result; +} + void InputField::startBorderAnimation() { auto borderVisible = (_error || _focused); if (_borderVisible != borderVisible) { @@ -2324,46 +1141,94 @@ QSize InputField::minimumSizeHint() const { return geometry().size(); } -QString InputField::getText(int32 start, int32 end) const { - if (end >= 0 && end <= start) return QString(); +bool InputField::hasText() const { + const auto document = _inner->document(); + const auto from = document->begin(); + const auto till = document->end(); + + if (from == till) { + return false; + } + + for (auto item = from.begin(); !item.atEnd(); ++item) { + const auto fragment = item.fragment(); + if (!fragment.isValid()) { + continue; + } else if (!fragment.text().isEmpty()) { + return true; + } + } + return (from.next() != till); +} + +QString InputField::getTextPart( + int start, + int end, + TagList &outTagsList, + bool &outTagsChanged) const { + if (end >= 0 && end <= start) { + outTagsChanged = !outTagsList.isEmpty(); + outTagsList.clear(); + return QString(); + } if (start < 0) start = 0; - bool full = (start == 0) && (end < 0); + const auto full = (start == 0 && end < 0); - QTextDocument *doc(_inner->document()); - QTextBlock from = full ? doc->begin() : doc->findBlock(start), till = (end < 0) ? doc->end() : doc->findBlock(end); - if (till.isValid()) till = till.next(); + TagAccumulator tagAccumulator(outTagsList); - int32 possibleLen = 0; - for (QTextBlock b = from; b != till; b = b.next()) { - possibleLen += b.length(); + const auto document = _inner->document(); + const auto from = full ? document->begin() : document->findBlock(start); + auto till = (end < 0) ? document->end() : document->findBlock(end); + if (till.isValid()) { + till = till.next(); } - QString result; - result.reserve(possibleLen + 1); + + auto possibleLength = 0; + for (auto block = from; block != till; block = block.next()) { + possibleLength += block.length(); + } + auto result = QString(); + result.reserve(possibleLength + 1); if (!full && end < 0) { - end = possibleLen; + end = possibleLength; } - for (QTextBlock b = from; b != till; b = b.next()) { - for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) { - QTextFragment fragment(iter.fragment()); - if (!fragment.isValid()) continue; + bool tillFragmentEnd = full; + for (auto block = from; block != till; block = block.next()) { + for (auto item = block.begin(); !item.atEnd(); ++item) { + const auto fragment = item.fragment(); + if (!fragment.isValid()) { + continue; + } - int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length()); + const auto fragmentPosition = full ? 0 : fragment.position(); + const auto fragmentEnd = full + ? 0 + : (fragmentPosition + fragment.length()); + const auto format = fragment.charFormat(); if (!full) { - if (p >= end || e <= start) { + tillFragmentEnd = (fragmentEnd <= end); + if (fragmentPosition == end) { + tagAccumulator.feed(format.anchorName(), result.size()); + } + if (fragmentPosition >= end) { + break; + } else if (fragmentEnd <= start) { continue; } } + if (full || fragmentPosition >= start) { + tagAccumulator.feed(format.anchorName(), result.size()); + } - QTextCharFormat f = fragment.charFormat(); QString emojiText; - QString t(fragment.text()); + auto t = fragment.text(); if (!full) { - if (p < start) { - t = t.mid(start - p, end - start); - } else if (e > end) { - t = t.mid(0, end - p); + if (fragmentPosition < start) { + t = t.mid(start - fragmentPosition, end - start); + } else if (fragmentEnd > end) { + t = t.mid(0, end - fragmentPosition); } } QChar *ub = t.data(), *uc = ub, *ue = uc + t.size(); @@ -2379,9 +1244,9 @@ QString InputField::getText(int32 start, int32 end) const { *uc = QLatin1Char(' '); } break; case QChar::ObjectReplacementCharacter: { - if (emojiText.isEmpty() && f.isImageFormat()) { - auto imageName = static_cast(&f)->name(); - if (auto emoji = Ui::Emoji::FromUrl(imageName)) { + if (emojiText.isEmpty() && format.isImageFormat()) { + const auto imageName = format.toImageFormat().name(); + if (const auto emoji = Ui::Emoji::FromUrl(imageName)) { emojiText = emoji->text(); } } @@ -2396,21 +1261,14 @@ QString InputField::getText(int32 start, int32 end) const { result.append('\n'); } result.chop(1); - return result; -} -bool InputField::hasText() const { - QTextDocument *doc(_inner->document()); - QTextBlock from = doc->begin(), till = doc->end(); - - if (from == till) return false; - - for (QTextBlock::Iterator iter = from.begin(); !iter.atEnd(); ++iter) { - QTextFragment fragment(iter.fragment()); - if (!fragment.isValid()) continue; - if (!fragment.text().isEmpty()) return true; + if (tillFragmentEnd) { + tagAccumulator.feed(QString(), result.size()); } - return (from.next() != till); + tagAccumulator.finish(); + + outTagsChanged = tagAccumulator.changed(); + return result; } bool InputField::isUndoAvailable() const { @@ -2421,43 +1279,50 @@ bool InputField::isRedoAvailable() const { return _redoAvailable; } -void InputField::insertEmoji(EmojiPtr emoji, QTextCursor c) { - const auto format = PrepareEmojiFormat(emoji, _st.font); - c.insertText(kObjectReplacement, format); -} - -void InputField::processDocumentContentsChange(int position, int charsAdded) { +void InputField::processFormatting(int insertPosition, int insertEnd) { // Tilde formatting. auto tildeFormatting = !cRetina() && (font().pixelSize() == 13) && (font().family() == qstr("Open Sans")); auto isTildeFragment = false; auto tildeRegularFont = tildeFormatting ? qsl("Open Sans") : QString(); auto tildeFixedFont = tildeFormatting ? Fonts::GetOverride(qsl("Open Sans Semibold")) : QString(); - QTextDocument *doc(_inner->document()); - QTextCursor c(_inner->textCursor()); - c.joinPreviousEditBlock(); + // First tag handling (the one we inserted text to). + bool startTagFound = false; + bool breakTagOnNotLetter = false; + auto document = _inner->document(); + + // Apply inserted tags. + auto insertedTagsProcessor = _insertedTagsAreFromMime ? _tagMimeProcessor.get() : nullptr; + const auto breakTagOnNotLetterTill = ProcessInsertedTags( + _st.textFg, + document, + insertPosition, + insertEnd, + _insertedTags, + insertedTagsProcessor); using ActionType = FormattingAction::Type; while (true) { FormattingAction action; - int32 replacePosition = -1, replaceLen = 0; - EmojiPtr emoji = nullptr; - bool removeNewline = false; - int32 start = position, end = position + charsAdded; - QTextBlock from = doc->findBlock(start), till = doc->findBlock(end); - if (till.isValid()) till = till.next(); + auto fromBlock = document->findBlock(insertPosition); + auto tillBlock = document->findBlock(insertEnd); + if (tillBlock.isValid()) tillBlock = tillBlock.next(); - for (auto b = from; b != till; b = b.next()) { - for (auto iter = b.begin(); !iter.atEnd(); ++iter) { - const auto fragment = iter.fragment(); - if (!fragment.isValid()) continue; + for (auto block = fromBlock; block != tillBlock; block = block.next()) { + for (auto fragmentIt = block.begin(); !fragmentIt.atEnd(); ++fragmentIt) { + auto fragment = fragmentIt.fragment(); + Assert(fragment.isValid()); - auto fp = fragment.position(); - auto fe = fp + fragment.length(); - if (fp >= end || fe <= start) { + int fragmentPosition = fragment.position(); + if (insertPosition >= fragmentPosition + fragment.length()) { continue; } + int changedPositionInFragment = insertPosition - fragmentPosition; // Can be negative. + int changedEndInFragment = insertEnd - fragmentPosition; + if (changedEndInFragment <= 0) { + break; + } auto format = fragment.charFormat(); if (tildeFormatting) { @@ -2473,178 +1338,238 @@ void InputField::processDocumentContentsChange(int position, int charsAdded) { const auto string = with.toString(); if (fragmentText != string) { action.type = ActionType::ClearInstantReplace; - action.intervalStart = fp + action.intervalStart = fragmentPosition + (fragmentText.startsWith(string) ? string.size() : 0); - action.intervalEnd = fp + action.intervalEnd = fragmentPosition + fragmentText.size(); break; } } - auto *ch = textStart;// +qMax(changedPositionInFragment, 0); - for (; ch != textEnd; ++ch, ++fp) { - // QTextBeginningOfFrame // QTextEndOfFrame - removeNewline = (_mode == Mode::SingleLine) - && (ch->unicode() == 0xfdd0 - || ch->unicode() == 0xfdd1 + + if (!startTagFound) { + startTagFound = true; + auto tagName = format.anchorName(); + if (!tagName.isEmpty()) { + breakTagOnNotLetter = WasInsertTillTheEndOfTag( + block, + fragmentIt, + insertEnd); + } + } + + auto *ch = textStart + qMax(changedPositionInFragment, 0); + for (; ch < textEnd; ++ch) { + const auto removeNewline = (_mode == Mode::SingleLine) + && (false + || ch->unicode() == 0xfdd0 // QTextBeginningOfFrame + || ch->unicode() == 0xfdd1 // QTextEndOfFrame || ch->unicode() == QChar::ParagraphSeparator || ch->unicode() == QChar::LineSeparator || ch->unicode() == '\n' || ch->unicode() == '\r'); if (removeNewline) { - if (replacePosition >= 0) { - removeNewline = false; // replace tilde char format first - } else { - replacePosition = fp; - replaceLen = 1; + if (action.type == ActionType::Invalid) { + action.type = ActionType::RemoveNewline; + action.intervalStart = fragmentPosition + (ch - textStart); + action.intervalEnd = action.intervalStart + 1; } break; } - auto emojiLen = 0; - emoji = Ui::Emoji::Find(ch, textEnd, &emojiLen); - if (emoji) { - if (replacePosition >= 0) { - emoji = 0; // replace tilde char format first - } else { - replacePosition = fp; - replaceLen = emojiLen; + auto emojiLength = 0; + if (const auto emoji = Ui::Emoji::Find(ch, textEnd, &emojiLength)) { + // Replace emoji if no current action is prepared. + if (action.type == ActionType::Invalid) { + action.type = ActionType::InsertEmoji; + action.emoji = emoji; + action.intervalStart = fragmentPosition + (ch - textStart); + action.intervalEnd = action.intervalStart + emojiLength; } break; } - if (tildeFormatting && fp >= position) { // tilde fix in OpenSans + if (breakTagOnNotLetter && !ch->isLetter()) { + // Remove tag name till the end if no current action is prepared. + if (action.type != ActionType::Invalid) { + break; + } + breakTagOnNotLetter = false; + if (fragmentPosition + (ch - textStart) < breakTagOnNotLetterTill) { + action.type = ActionType::RemoveTag; + action.intervalStart = fragmentPosition + (ch - textStart); + action.intervalEnd = breakTagOnNotLetterTill; + break; + } + } + if (tildeFormatting) { // Tilde symbol fix in OpenSans. bool tilde = (ch->unicode() == '~'); if ((tilde && !isTildeFragment) || (!tilde && isTildeFragment)) { - if (replacePosition < 0) { - replacePosition = fp; - replaceLen = 1; + if (action.type == ActionType::Invalid) { + action.type = ActionType::TildeFont; + action.intervalStart = fragmentPosition + (ch - textStart); + action.intervalEnd = action.intervalStart + 1; + action.isTilde = tilde; } else { - ++replaceLen; + ++action.intervalEnd; } - } else if (replacePosition >= 0) { + } else if (action.type == ActionType::TildeFont) { break; } } if (ch + 1 < textEnd && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) { ++ch; - ++fp; + ++fragmentPosition; } } - if (replacePosition >= 0) break; + if (action.type != ActionType::Invalid) { + break; + } } - if (replacePosition >= 0 || action.type != ActionType::Invalid) { + if (action.type != ActionType::Invalid) { break; - } - - if (_mode == Mode::SingleLine && b.next() != doc->end()) { - removeNewline = true; - replacePosition = b.next().position() - 1; - replaceLen = 1; + } else if (_mode == Mode::SingleLine + && block.next() != document->end()) { + action.type = ActionType::RemoveNewline; + action.intervalStart = block.next().position() - 1; + action.intervalEnd = action.intervalStart + 1; break; } } + if (action.type != ActionType::Invalid) { + PrepareFormattingOptimization(document); - if (action.type == ActionType::ClearInstantReplace) { - prepareFormattingOptimization(doc); - - QTextCursor c(doc->docHandle(), action.intervalStart); - c.setPosition(action.intervalEnd, QTextCursor::KeepAnchor); - c.setCharFormat(_defaultCharFormat); - } else if (replacePosition >= 0) { - prepareFormattingOptimization(doc); - - QTextCursor c(doc->docHandle(), replacePosition); - c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor); - if (removeNewline) { + auto cursor = QTextCursor( + document->docHandle(), + action.intervalStart); + cursor.setPosition(action.intervalEnd, QTextCursor::KeepAnchor); + if (action.type == ActionType::InsertEmoji) { + InsertEmojiAtCursor(cursor, action.emoji); + insertPosition = action.intervalStart + 1; + if (insertEnd >= action.intervalEnd) { + insertEnd -= action.intervalEnd + - action.intervalStart + - 1; + } + } else if (action.type == ActionType::RemoveTag) { QTextCharFormat format; - format.setFontFamily(font().family()); - c.mergeCharFormat(format); - c.insertText(" "); - } else if (emoji) { - insertEmoji(emoji, c); - } else { + format.setAnchor(false); + format.setAnchorName(QString()); + format.setForeground(_st.textFg); + cursor.mergeCharFormat(format); + } else if (action.type == ActionType::TildeFont) { QTextCharFormat format; - format.setFontFamily(isTildeFragment ? tildeRegularFont : tildeFixedFont); - c.mergeCharFormat(format); + format.setFontFamily(action.isTilde ? tildeFixedFont : tildeRegularFont); + cursor.mergeCharFormat(format); + insertPosition = action.intervalEnd; + } else if (action.type == ActionType::ClearInstantReplace) { + auto format = _defaultCharFormat; + const auto current = cursor.charFormat(); + if (current.isAnchor()) { + format.setAnchor(true); + format.setAnchorName(current.anchorName()); + format.setForeground(st::defaultTextPalette.linkFg); + } + cursor.setCharFormat(format); + } else if (action.type == ActionType::RemoveNewline) { + cursor.removeSelectedText(); + insertPosition = action.intervalStart; + if (insertEnd >= action.intervalEnd) { + insertEnd -= action.intervalEnd - action.intervalStart; + } } - charsAdded -= replacePosition + replaceLen - position; - position = replacePosition + ((emoji || removeNewline) ? 1 : replaceLen); } else { break; } } - c.endEditBlock(); } -void InputField::onDocumentContentsChange(int position, int charsRemoved, int charsAdded) { +void InputField::onDocumentContentsChange( + int position, + int charsRemoved, + int charsAdded) { if (_correcting) return; - QTextCursor(_inner->document()->docHandle(), 0).joinPreviousEditBlock(); + const auto document = _inner->document(); - if (!position) { // Qt bug workaround https://bugreports.qt.io/browse/QTBUG-49062 - QTextCursor c(_inner->document()->docHandle(), 0); - c.movePosition(QTextCursor::End); - if (position + charsAdded > c.position()) { - int32 toSubstract = position + charsAdded - c.position(); - if (charsRemoved >= toSubstract) { - charsAdded -= toSubstract; - charsRemoved -= toSubstract; + // Qt bug workaround https://bugreports.qt.io/browse/QTBUG-49062 + if (!position) { + auto cursor = QTextCursor(document->docHandle(), 0); + cursor.movePosition(QTextCursor::End); + if (position + charsAdded > cursor.position()) { + const auto delta = position + charsAdded - cursor.position(); + if (charsRemoved >= delta) { + charsAdded -= delta; + charsRemoved -= delta; } } } + const auto insertPosition = (_realInsertPosition >= 0) + ? _realInsertPosition + : position; + const auto insertLength = (_realInsertPosition >= 0) + ? _realCharsAdded + : charsAdded; + + const auto removePosition = position; + const auto removeLength = charsRemoved; + + QTextCursor(document->docHandle(), 0).joinPreviousEditBlock(); + const auto guard = gsl::finally([&] { + QTextCursor(document->docHandle(), 0).endEditBlock(); + }); + + chopByMaxLength(insertPosition, insertLength); + + if (document->availableRedoSteps() > 0 || insertLength <= 0) { + return; + } + _correcting = true; - if (_maxLength >= 0) { - QTextCursor c(_inner->document()->docHandle(), 0); - c.movePosition(QTextCursor::End); - int32 fullSize = c.position(), toRemove = fullSize - _maxLength; - if (toRemove > 0) { - if (toRemove > charsAdded) { - if (charsAdded) { - c.setPosition(position); - c.setPosition((position + charsAdded), QTextCursor::KeepAnchor); - c.removeSelectedText(); - } - c.setPosition(fullSize - (toRemove - charsAdded)); - c.setPosition(fullSize, QTextCursor::KeepAnchor); - c.removeSelectedText(); - position = _maxLength; - charsAdded = 0; - charsRemoved += toRemove; - } else { - c.setPosition(position + (charsAdded - toRemove)); - c.setPosition(position + charsAdded, QTextCursor::KeepAnchor); - c.removeSelectedText(); - charsAdded -= toRemove; + auto pageSize = document->pageSize(); + processFormatting(insertPosition, insertPosition + insertLength); + if (document->pageSize() != pageSize) { + document->setPageSize(pageSize); + } + _correcting = false; +} + +void InputField::chopByMaxLength(int insertPosition, int insertLength) { + if (_maxLength < 0) { + return; + } + + _correcting = true; + const auto guard = gsl::finally([&] { _correcting = false; }); + + auto cursor = QTextCursor(document()->docHandle(), 0); + cursor.movePosition(QTextCursor::End); + const auto fullSize = cursor.position(); + const auto toRemove = fullSize - _maxLength; + if (toRemove > 0) { + if (toRemove > insertLength) { + if (insertLength) { + cursor.setPosition(insertPosition); + cursor.setPosition( + (insertPosition + insertLength), + QTextCursor::KeepAnchor); + cursor.removeSelectedText(); } + cursor.setPosition(fullSize - (toRemove - insertLength)); + cursor.setPosition(fullSize, QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + } else { + cursor.setPosition( + insertPosition + (insertLength - toRemove)); + cursor.setPosition( + insertPosition + insertLength, + QTextCursor::KeepAnchor); + cursor.removeSelectedText(); } } - _correcting = false; - - QTextCursor(_inner->document()->docHandle(), 0).endEditBlock(); - - if (_inner->document()->availableRedoSteps() > 0) return; - - const int takeBack = 3; - - position -= takeBack; - charsAdded += takeBack; - if (position < 0) { - charsAdded += position; - position = 0; - } - if (charsAdded <= 0) return; - - _correcting = true; - QSizeF s = _inner->document()->pageSize(); - processDocumentContentsChange(position, charsAdded); - if (_inner->document()->pageSize() != s) { - _inner->document()->setPageSize(s); - } - _correcting = false; } void InputField::onDocumentContentsChanged() { @@ -2652,9 +1577,15 @@ void InputField::onDocumentContentsChanged() { setErrorShown(false); - auto curText = getText(); - if (_lastTextWithTags.text != curText) { - _lastTextWithTags.text = curText; + auto tagsChanged = false; + const auto currentText = getTextPart( + 0, + -1, + _lastTextWithTags.tags, + tagsChanged); + + if (tagsChanged || (_lastTextWithTags.text != currentText)) { + _lastTextWithTags.text = currentText; emit changed(); checkContentHeight(); } @@ -2699,12 +1630,21 @@ void InputField::setPlaceholderHidden(bool forcePlaceholderHidden) { } void InputField::startPlaceholderAnimation() { - auto placeholderShifted = _forcePlaceholderHidden + const auto textLength = [&] { + const auto layout = textCursor().block().layout(); + return getTextWithTags().text.size() + + (layout ? layout->preeditAreaText().size() : 0); + }; + const auto placeholderShifted = _forcePlaceholderHidden || (_focused && _st.placeholderScale > 0.) - || !getLastText().isEmpty(); + || (textLength() > _placeholderAfterSymbols); if (_placeholderShifted != placeholderShifted) { _placeholderShifted = placeholderShifted; - _a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration); + _a_placeholderShifted.start( + [=] { update(); }, + _placeholderShifted ? 0. : 1., + _placeholderShifted ? 1. : 0., + _st.duration); } } @@ -2714,7 +1654,18 @@ QMimeData *InputField::createMimeDataFromSelectionInner() const { const auto start = cursor.selectionStart(); const auto end = cursor.selectionEnd(); if (end > start) { - result->setText(getText(start, end)); + auto textWithTags = getTextWithTagsPart(start, end); + result->setText(textWithTags.text); + if (!textWithTags.tags.isEmpty()) { + if (_tagMimeProcessor) { + for (auto &tag : textWithTags.tags) { + tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id); + } + } + result->setData( + TextUtilities::TagsMimeType(), + TextUtilities::SerializeTags(textWithTags.tags)); + } } return result.release(); } @@ -2723,8 +1674,16 @@ void InputField::customUpDown(bool custom) { _customUpDown = custom; } -void InputField::setCtrlEnterSubmit(CtrlEnterSubmit ctrlEnterSubmit) { - _ctrlEnterSubmit = ctrlEnterSubmit; +void InputField::setSubmitSettings(SubmitSettings settings) { + _submitSettings = settings; +} + +not_null InputField::document() { + return _inner->document(); +} + +not_null InputField::document() const { + return _inner->document(); } void InputField::setTextCursor(const QTextCursor &cursor) { @@ -2748,11 +1707,37 @@ void InputField::setText(const QString &text) { void InputField::setTextWithTags( const TextWithTags &textWithTags, HistoryAction historyAction) { - _inner->setPlainText(textWithTags.text); // #TODO - startPlaceholderAnimation(); + _insertedTags = textWithTags.tags; + _insertedTagsAreFromMime = false; + _realInsertPosition = 0; + _realCharsAdded = textWithTags.text.size(); + const auto document = _inner->document(); + auto cursor = QTextCursor(document->docHandle(), 0); if (historyAction == HistoryAction::Clear) { - _inner->document()->clearUndoRedoStacks(); + document->setUndoRedoEnabled(false); + cursor.beginEditBlock(); + } else if (historyAction == HistoryAction::MergeEntry) { + cursor.joinPreviousEditBlock(); + } else { + cursor.beginEditBlock(); } + cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); + cursor.insertText(textWithTags.text); + cursor.movePosition(QTextCursor::End); + cursor.endEditBlock(); + if (historyAction == HistoryAction::Clear) { + document->setUndoRedoEnabled(true); + } + _insertedTags.clear(); + _realInsertPosition = -1; + finishAnimating(); +} + +TextWithTags InputField::getTextWithTagsPart(int start, int end) const { + auto changed = false; + auto result = TextWithTags(); + result.text = getTextPart(start, end, result.tags, changed); + return result; } void InputField::clear() { @@ -2772,14 +1757,27 @@ void InputField::clearFocus() { _inner->clearFocus(); } +not_null InputField::rawTextEdit() { + return _inner; +} + +not_null InputField::rawTextEdit() const { + return _inner; +} + void InputField::keyPressEventInner(QKeyEvent *e) { bool shift = e->modifiers().testFlag(Qt::ShiftModifier), alt = e->modifiers().testFlag(Qt::AltModifier); bool macmeta = (cPlatform() == dbipMac || cPlatform() == dbipMacOld) && e->modifiers().testFlag(Qt::ControlModifier) && !e->modifiers().testFlag(Qt::MetaModifier) && !e->modifiers().testFlag(Qt::AltModifier); bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier); - bool ctrlGood = (_mode == Mode::SingleLine) + bool enterSubmit = (_mode == Mode::SingleLine) || (ctrl && shift) - || (ctrl && (_ctrlEnterSubmit == CtrlEnterSubmit::CtrlEnter || _ctrlEnterSubmit == CtrlEnterSubmit::Both)) - || (!ctrl && !shift && (_ctrlEnterSubmit == CtrlEnterSubmit::Enter || _ctrlEnterSubmit == CtrlEnterSubmit::Both)); + || (ctrl + && _submitSettings != SubmitSettings::None + && _submitSettings != SubmitSettings::Enter) + || (!ctrl + && !shift + && _submitSettings != SubmitSettings::None + && _submitSettings != SubmitSettings::CtrlEnter); bool enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return); if (macmeta && e->key() == Qt::Key_Backspace) { @@ -2791,18 +1789,16 @@ void InputField::keyPressEventInner(QKeyEvent *e) { && e->modifiers() == 0 && revertInstantReplace()) { e->accept(); - } else if (enter && ctrlGood) { + } else if (enter && enterSubmit) { emit submitted(ctrl && shift); } else if (e->key() == Qt::Key_Escape) { e->ignore(); emit cancelled(); - } else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab) { + } else if (e->key() == Qt::Key_Tab || (ctrl && e->key() == Qt::Key_Backtab)) { if (alt || ctrl) { e->ignore(); } else { - if (!focusNextPrevChild(e->key() == Qt::Key_Tab && !shift)) { - e->ignore(); - } + emit tabbed(); } } else if (e->key() == Qt::Key_Search || e == QKeySequence::Find) { e->ignore(); @@ -2810,10 +1806,13 @@ void InputField::keyPressEventInner(QKeyEvent *e) { e->ignore(); #ifdef Q_OS_MAC } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) { - auto cursor = textCursor(); - int start = cursor.selectionStart(), end = cursor.selectionEnd(); + const auto cursor = textCursor(); + const auto start = cursor.selectionStart(); + const auto end = cursor.selectionEnd(); if (end > start) { - QApplication::clipboard()->setText(f()->getText(start, end), QClipboard::FindBuffer); + QApplication::clipboard()->setText( + getTextWithTagsPart(start, end).text, + QClipboard::FindBuffer); } #endif // Q_OS_MAC } else { @@ -2863,9 +1862,9 @@ void InputField::processInstantReplaces(const QString &text) { return; } const auto position = textCursor().position(); - const auto typed = getText( + const auto typed = getTextWithTagsPart( std::max(position - replaces.maxLength, 0), - position - 1); + position - 1).text; auto node = &it->second; auto i = typed.size(); do { @@ -2902,7 +1901,7 @@ void InputField::commitInstantReplacement( int till, const QString &with, base::optional checkOriginal) { - const auto original = getText(from, till); + const auto original = getTextWithTagsPart(from, till).text; if (checkOriginal && checkOriginal->compare(original, Qt::CaseInsensitive) != 0) { return; @@ -2936,6 +1935,12 @@ void InputField::commitInstantReplacement( auto cursor = textCursor(); cursor.setPosition(from); cursor.setPosition(till, QTextCursor::KeepAnchor); + const auto current = cursor.charFormat(); + if (current.isAnchor()) { + format.setAnchor(true); + format.setAnchorName(current.anchorName()); + format.setForeground(st::defaultTextPalette.linkFg); + } cursor.insertText(replacement, format); } @@ -2972,7 +1977,13 @@ bool InputField::revertInstantReplace() { replaceCursor.setPosition(fragmentStart); replaceCursor.setPosition(fragmentEnd, QTextCursor::KeepAnchor); const auto what = format.property(kInstantReplaceWhatId).toString(); - replaceCursor.insertText(what, _defaultCharFormat); + auto replaceFormat = _defaultCharFormat; + if (format.isAnchor()) { + replaceFormat.setAnchor(true); + replaceFormat.setAnchorName(format.anchorName()); + replaceFormat.setForeground(st::defaultTextPalette.linkFg); + } + replaceCursor.insertText(what, replaceFormat); return true; } return false; @@ -2984,6 +1995,14 @@ void InputField::contextMenuEventInner(QContextMenuEvent *e) { } } +void InputField::dropEventInner(QDropEvent *e) { + _inDrop = true; + _inner->QTextEdit::dropEvent(e); + _inDrop = false; + _insertedTags.clear(); + _realInsertPosition = -1; +} + bool InputField::canInsertFromMimeDataInner(const QMimeData *source) const { if (source && _mimeDataHook @@ -2999,7 +2018,25 @@ void InputField::insertFromMimeDataInner(const QMimeData *source) { && _mimeDataHook(source, MimeAction::Insert)) { return; } - return _inner->QTextEdit::insertFromMimeData(source); + auto mime = TextUtilities::TagsMimeType(); + auto text = source->text(); + if (source->hasFormat(mime)) { + auto tagsData = source->data(mime); + _insertedTags = TextUtilities::DeserializeTags( + tagsData, + text.size()); + _insertedTagsAreFromMime = true; + } else { + _insertedTags.clear(); + } + auto cursor = textCursor(); + _realInsertPosition = qMin(cursor.position(), cursor.anchor()); + _realCharsAdded = text.size(); + _inner->QTextEdit::insertFromMimeData(source); + if (!_inDrop) { + _insertedTags.clear(); + _realInsertPosition = -1; + } } void InputField::resizeEvent(QResizeEvent *e) { @@ -3028,8 +2065,14 @@ void InputField::refreshPlaceholder() { update(); } -void InputField::setPlaceholder(base::lambda placeholderFactory) { +void InputField::setPlaceholder( + base::lambda placeholderFactory, + int afterSymbols) { _placeholderFactory = std::move(placeholderFactory); + if (_placeholderAfterSymbols != afterSymbols) { + _placeholderAfterSymbols = afterSymbols; + startPlaceholderAnimation(); + } refreshPlaceholder(); } diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.h b/Telegram/SourceFiles/ui/widgets/input_fields.h index ba012761c1..e5e06bd248 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.h +++ b/Telegram/SourceFiles/ui/widgets/input_fields.h @@ -14,7 +14,7 @@ class UserData; namespace Ui { -static UserData * const LookingUpInlineBot = SharedMemoryLocation(); +void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji); struct InstantReplaces { struct Node { @@ -31,231 +31,6 @@ struct InstantReplaces { }; -class FlatTextarea : public TWidgetHelper, protected base::Subscriber { - Q_OBJECT - -public: - using TagList = TextWithTags::Tags; - - FlatTextarea(QWidget *parent, const style::FlatTextarea &st, base::lambda placeholderFactory = base::lambda(), const QString &val = QString(), const TagList &tags = TagList()); - - void setMaxLength(int maxLength); - void setMinHeight(int minHeight); - void setMaxHeight(int maxHeight); - - void setInstantReplaces(const InstantReplaces &replaces); - void enableInstantReplaces(bool enabled); - void commitInstantReplacement( - int from, - int till, - const QString &with, - base::optional checkOriginal = base::none); - - void setPlaceholder(base::lambda placeholderFactory, int afterSymbols = 0); - void updatePlaceholder(); - void finishPlaceholder(); - - QRect getTextRect() const; - int fakeMargin() const; - - QSize sizeHint() const override; - QSize minimumSizeHint() const override; - - EmojiPtr getSingleEmoji() const; - QString getMentionHashtagBotCommandPart(bool &start) const; - - // Get the current inline bot and request string for it. - // The *outInlineBot can be filled by LookingUpInlineBot shared ptr. - // In that case the caller should lookup the bot by *outInlineBotUsername. - QString getInlineBotQuery(UserData **outInlineBot, QString *outInlineBotUsername) const; - - void removeSingleEmoji(); - bool hasText() const; - - bool isUndoAvailable() const; - bool isRedoAvailable() const; - - void parseLinks(); - QStringList linksList() const; - - void insertFromMimeData(const QMimeData *source) override; - - QMimeData *createMimeDataFromSelection() const override; - - enum class SubmitSettings { - None, - Enter, - CtrlEnter, - Both, - }; - void setSubmitSettings(SubmitSettings settings); - - const TextWithTags &getTextWithTags() const { - return _lastTextWithTags; - } - TextWithTags getTextWithTagsPart(int start, int end = -1); - void insertTag(const QString &text, QString tagId = QString()); - - bool isEmpty() const { - return _lastTextWithTags.text.isEmpty(); - } - - enum UndoHistoryAction { - AddToUndoHistory, - MergeWithUndoHistory, - ClearUndoHistory - }; - void setTextWithTags(const TextWithTags &textWithTags, UndoHistoryAction undoHistoryAction = AddToUndoHistory); - - // 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; - virtual ~TagMimeProcessor() { - } - }; - void setTagMimeProcessor(std::unique_ptr &&processor); - -public slots: - void onTouchTimer(); - - void onDocumentContentsChange(int position, int charsRemoved, int charsAdded); - void onDocumentContentsChanged(); - - void onUndoAvailable(bool avail); - void onRedoAvailable(bool avail); - -signals: - void resized(); - void changed(); - void submitted(bool ctrlShiftEnter); - void cancelled(); - void tabbed(); - void spacedReturnedPasted(); - void linksChanged(); - -protected: - 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); - - QVariant loadResource(int type, const QUrl &name) override; - - void checkContentHeight(); - -private: - void updatePalette(); - void refreshPlaceholder(); - - // "start" and "end" are in coordinates of text where emoji are replaced - // by ObjectReplacementCharacter. If "end" = -1 means get text till the end. - QString getTextPart(int start, int end, TagList *outTagsList, bool *outTagsChanged = nullptr) const; - - void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const; - - // After any characters added we must postprocess them. This includes: - // 1. Replacing font family to semibold for ~ characters, if we used Open Sans 13px. - // 2. Replacing font family from semibold for all non-~ characters, if we used ... - // 3. Replacing emoji code sequences by ObjectReplacementCharacters with emoji pics. - // 4. Interrupting tags in which the text was inserted by any char except a letter. - // 5. Applying tags from "_insertedTags" in case we pasted text with tags, not just text. - // Rule 4 applies only if we inserted chars not in the middle of a tag (but at the end). - void processFormatting(int changedPosition, int changedEnd); - - // We don't want accidentally detach InstantReplaces map. - // So we access it only by const reference from this method. - const InstantReplaces &instantReplaces() const; - void processInstantReplaces(const QString &text); - void applyInstantReplace(const QString &what, const QString &with); - bool revertInstantReplace(); - - bool heightAutoupdated(); - - int placeholderSkipWidth() const; - - int _minHeight = -1; // < 0 - no autosize - int _maxHeight = -1; - int _maxLength = -1; - SubmitSettings _submitSettings = SubmitSettings::Enter; - - QString _placeholder; - base::lambda _placeholderFactory; - int _placeholderAfterSymbols = 0; - bool _focused = false; - bool _placeholderVisible = true; - Animation _a_placeholderFocused; - Animation _a_placeholderVisible; - - TextWithTags _lastTextWithTags; - - // Tags list which we should apply while setText() call or insert from mime data. - TagList _insertedTags; - bool _insertedTagsAreFromMime; - - // Override insert position and charsAdded from complex text editing - // (like drag-n-drop in the same text edit field). - int _realInsertPosition = -1; - int _realCharsAdded = 0; - - std::unique_ptr _tagMimeProcessor; - - const style::FlatTextarea &_st; - - bool _undoAvailable = false; - bool _redoAvailable = false; - bool _inDrop = false; - bool _inHeightCheck = false; - - int _fakeMargin = 0; - - QTimer _touchTimer; - bool _touchPress = false; - bool _touchRightButton = false; - bool _touchMove = false; - QPoint _touchStart; - - bool _correcting = false; - - 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; - LinkRanges _links; - - QTextCharFormat _defaultCharFormat; - - InstantReplaces _mutableInstantReplaces; - bool _instantReplacesEnabled = true; - -}; - -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); -} - class FlatInput : public TWidgetHelper, private base::Subscriber { Q_OBJECT @@ -337,12 +112,6 @@ private: QPoint _touchStart; }; -enum class CtrlEnterSubmit { - Enter, - CtrlEnter, - Both, -}; - class InputField : public RpWidget, private base::Subscriber { Q_OBJECT @@ -373,10 +142,18 @@ public: void showError(); - void setMaxLength(int maxLength) { - _maxLength = maxLength; - } + void setMaxLength(int maxLength); + void setMinHeight(int minHeight); + void setMaxHeight(int maxHeight); + const TextWithTags &getTextWithTags() const { + return _lastTextWithTags; + } + TextWithTags getTextWithTagsPart(int start, int end = -1) const; + void insertTag(const QString &text, QString tagId = QString()); + bool empty() const { + return _lastTextWithTags.text.isEmpty(); + } enum class HistoryAction { NewEntry, MergeEntry, @@ -386,6 +163,17 @@ public: const TextWithTags &textWithTags, HistoryAction historyAction = HistoryAction::NewEntry); + // 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; + virtual ~TagMimeProcessor() { + } + }; + void setTagMimeProcessor(std::unique_ptr &&processor); + void setInstantReplaces(const InstantReplaces &replaces); void enableInstantReplaces(bool enabled); void commitInstantReplacement( @@ -397,7 +185,9 @@ public: const QString &getLastText() const { return _lastTextWithTags.text; } - void setPlaceholder(base::lambda placeholderFactory); + void setPlaceholder( + base::lambda placeholderFactory, + int afterSymbols = 0); void setPlaceholderHidden(bool forcePlaceholderHidden); void setDisplayFocused(bool focused); void finishAnimating(); @@ -409,16 +199,23 @@ public: QSize sizeHint() const override; QSize minimumSizeHint() const override; - QString getText(int start = 0, int end = -1) const; bool hasText() const; void selectAll(); bool isUndoAvailable() const; bool isRedoAvailable() const; + enum class SubmitSettings { + None, + Enter, + CtrlEnter, + Both, + }; + void setSubmitSettings(SubmitSettings settings); void customUpDown(bool isCustom); - void setCtrlEnterSubmit(CtrlEnterSubmit ctrlEnterSubmit); + not_null document(); + not_null document() const; void setTextCursor(const QTextCursor &cursor); void setCursorPosition(int position); QTextCursor textCursor() const; @@ -427,6 +224,8 @@ public: bool hasFocus() const; void setFocus(); void clearFocus(); + not_null rawTextEdit(); + not_null rawTextEdit() const; enum class MimeAction { Check, @@ -439,6 +238,10 @@ public: _mimeDataHook = std::move(hook); } + const rpl::variable &scrollTop() const; + int scrollTopMax() const; + void scrollTo(int top); + private slots: void onTouchTimer(); @@ -455,7 +258,6 @@ signals: void submitted(bool ctrlShiftEnter); void cancelled(); void tabbed(); - void focused(); void blurred(); void resized(); @@ -464,14 +266,6 @@ protected: void startPlaceholderAnimation(); void startBorderAnimation(); - void insertEmoji(EmojiPtr emoji, QTextCursor c); - TWidget *tparent() { - return qobject_cast(parentWidget()); - } - const TWidget *tparent() const { - return qobject_cast(parentWidget()); - } - void paintEvent(QPaintEvent *e) override; void focusInEvent(QFocusEvent *e) override; void mousePressEvent(QMouseEvent *e) override; @@ -488,6 +282,7 @@ private: void updatePalette(); void refreshPlaceholder(); + int placeholderSkipWidth() const; bool heightAutoupdated(); void checkContentHeight(); @@ -498,12 +293,30 @@ private: void setFocused(bool focused); void keyPressEventInner(QKeyEvent *e); void contextMenuEventInner(QContextMenuEvent *e); + void dropEventInner(QDropEvent *e); QMimeData *createMimeDataFromSelectionInner() const; bool canInsertFromMimeDataInner(const QMimeData *source) const; void insertFromMimeDataInner(const QMimeData *source); - void processDocumentContentsChange(int position, int charsAdded); + // "start" and "end" are in coordinates of text where emoji are replaced + // by ObjectReplacementCharacter. If "end" = -1 means get text till the end. + QString getTextPart( + int start, + int end, + TagList &outTagsList, + bool &outTagsChanged) const; + + // After any characters added we must postprocess them. This includes: + // 1. Replacing font family to semibold for ~ characters, if we used Open Sans 13px. + // 2. Replacing font family from semibold for all non-~ characters, if we used ... + // 3. Replacing emoji code sequences by ObjectReplacementCharacters with emoji pics. + // 4. Interrupting tags in which the text was inserted by any char except a letter. + // 5. Applying tags from "_insertedTags" in case we pasted text with tags, not just text. + // Rule 4 applies only if we inserted chars not in the middle of a tag (but at the end). + void processFormatting(int changedPosition, int changedEnd); + + void chopByMaxLength(int insertPosition, int insertLength); // We don't want accidentally detach InstantReplaces map. // So we access it only by const reference from this method. @@ -516,21 +329,37 @@ private: Mode _mode = Mode::SingleLine; int _maxLength = -1; + int _minHeight = -1; + int _maxHeight = -1; bool _forcePlaceholderHidden = false; object_ptr _inner; TextWithTags _lastTextWithTags; - CtrlEnterSubmit _ctrlEnterSubmit = CtrlEnterSubmit::CtrlEnter; + // Tags list which we should apply while setText() call or insert from mime data. + TagList _insertedTags; + bool _insertedTagsAreFromMime; + + // Override insert position and charsAdded from complex text editing + // (like drag-n-drop in the same text edit field). + int _realInsertPosition = -1; + int _realCharsAdded = 0; + + std::unique_ptr _tagMimeProcessor; + + SubmitSettings _submitSettings = SubmitSettings::Enter; bool _undoAvailable = false; bool _redoAvailable = false; + bool _inDrop = false; bool _inHeightCheck = false; + int _fakeMargin = 0; bool _customUpDown = false; QString _placeholder; base::lambda _placeholderFactory; + int _placeholderAfterSymbols = 0; Animation _a_placeholderShifted; bool _placeholderShifted = false; QPainterPath _placeholderPath; @@ -557,6 +386,8 @@ private: QTextCharFormat _defaultCharFormat; + rpl::variable _scrollTop; + InstantReplaces _mutableInstantReplaces; bool _instantReplacesEnabled = true; diff --git a/Telegram/SourceFiles/ui/widgets/widgets.style b/Telegram/SourceFiles/ui/widgets/widgets.style index 272d6ebff6..1ed7c6ae05 100644 --- a/Telegram/SourceFiles/ui/widgets/widgets.style +++ b/Telegram/SourceFiles/ui/widgets/widgets.style @@ -180,22 +180,6 @@ ScrollArea { hiding: int; } -FlatTextarea { - textColor: color; - bgColor: color; - width: pixels; - textMrg: margins; - align: align; - font: font; - - phColor: color; - phFocusColor: color; - phPos: point; - phAlign: align; - phShift: pixels; - phDuration: int; -} - FlatInput { textColor: color; bgColor: color; diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index c0a2973422..ff2325ec30 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -770,7 +770,7 @@ void Notification::showReplyField() { _replyArea->show(); _replyArea->setFocus(); _replyArea->setMaxLength(MaxMessageSize); - _replyArea->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); + _replyArea->setSubmitSettings(Ui::InputField::SubmitSettings::Both); _replyArea->setInstantReplaces(Ui::InstantReplaces::Default()); // Catch mouse press event to activate the window. diff --git a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp index ecb0ea1ad7..5c7a6ffa8f 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp @@ -501,15 +501,19 @@ void Generator::paintComposeArea() { auto fieldWidth = _composeArea.width() - st::historyAttach.width - st::historySendSize.width() - st::historySendRight - st::historyAttachEmoji.width - 2 * fakeMargin; auto fieldHeight = st::historySendSize.height() - 2 * st::historySendPadding - 2 * fakeMargin; auto field = QRect(fieldLeft, fieldTop, fieldWidth, fieldHeight); - _p->fillRect(field, st::historyComposeField.bgColor[_palette]); + _p->fillRect(field, st::historyComposeField.textBg[_palette]); _p->save(); _p->setClipRect(field); _p->setFont(st::historyComposeField.font); - _p->setPen(st::historyComposeField.phColor[_palette]); + _p->setPen(st::historyComposeField.placeholderFg[_palette]); - auto phRect = QRect(field.x() + st::historyComposeField.textMrg.left() - fakeMargin + st::historyComposeField.phPos.x(), field.y() + st::historyComposeField.textMrg.top() - fakeMargin + st::historyComposeField.phPos.y(), field.width() - st::historyComposeField.textMrg.left() - st::historyComposeField.textMrg.right(), field.height() - st::historyComposeField.textMrg.top() - st::historyComposeField.textMrg.bottom()); - _p->drawText(phRect, lang(lng_message_ph), QTextOption(st::historyComposeField.phAlign)); + auto placeholderRect = QRect( + field.x() + st::historyComposeField.textMargins.left() - fakeMargin + st::historyComposeField.placeholderMargins.left(), + field.y() + st::historyComposeField.textMargins.top() - fakeMargin + st::historyComposeField.placeholderMargins.top(), + field.width() - st::historyComposeField.textMargins.left() - st::historyComposeField.textMargins.right(), + field.height() - st::historyComposeField.textMargins.top() - st::historyComposeField.textMargins.bottom()); + _p->drawText(placeholderRect, lang(lng_message_ph), QTextOption(st::historyComposeField.textAlign)); _p->restore(); _p->setClipping(false); diff --git a/Telegram/gyp/tests/tests.gyp b/Telegram/gyp/tests/tests.gyp index cc83feb481..4bb9be165a 100644 --- a/Telegram/gyp/tests/tests.gyp +++ b/Telegram/gyp/tests/tests.gyp @@ -109,6 +109,7 @@ '<(src_loc)/rpl/producer_tests.cpp', '<(src_loc)/rpl/range.h', '<(src_loc)/rpl/rpl.h', + '<(src_loc)/rpl/skip.h', '<(src_loc)/rpl/take.h', '<(src_loc)/rpl/then.h', '<(src_loc)/rpl/type_erased.h',