From 30dd8fe070e910aa4225d6e597e5511af5261498 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 20 May 2018 20:42:30 +0300 Subject: [PATCH] Unite InputField and InputArea. Also support and use instant replaces in InputField-s. --- .../SourceFiles/boxes/add_contact_box.cpp | 27 +- Telegram/SourceFiles/boxes/add_contact_box.h | 7 +- .../SourceFiles/boxes/edit_caption_box.cpp | 14 +- Telegram/SourceFiles/boxes/edit_caption_box.h | 4 +- .../boxes/peers/edit_peer_info_box.cpp | 10 +- Telegram/SourceFiles/boxes/rate_call_box.cpp | 6 +- Telegram/SourceFiles/boxes/rate_call_box.h | 4 +- Telegram/SourceFiles/boxes/report_box.cpp | 6 +- Telegram/SourceFiles/boxes/report_box.h | 4 +- Telegram/SourceFiles/boxes/send_files_box.cpp | 19 +- Telegram/SourceFiles/boxes/send_files_box.h | 4 +- .../chat_helpers/emoji_suggestions_widget.h | 1 - .../chat_helpers/message_field.cpp | 35 +- .../SourceFiles/chat_helpers/message_field.h | 2 +- .../SourceFiles/history/history_widget.cpp | 2 +- Telegram/SourceFiles/storage/localstorage.cpp | 27 +- Telegram/SourceFiles/ui/text/text_entity.cpp | 54 + Telegram/SourceFiles/ui/text/text_entity.h | 4 + .../SourceFiles/ui/widgets/input_fields.cpp | 1454 ++++++----------- .../SourceFiles/ui/widgets/input_fields.h | 347 ++-- .../window/notifications_manager_default.cpp | 7 +- .../window/notifications_manager_default.h | 4 +- 22 files changed, 771 insertions(+), 1271 deletions(-) diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index c87b88d7b..8533b8eae 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -322,7 +322,11 @@ void GroupInfoBox::prepare() { _title->setMaxLength(kMaxGroupChannelTitle); if (_creating == CreatingGroupChannel) { - _description.create(this, st::newGroupDescription, langFactory(lng_create_group_description)); + _description.create( + this, + st::newGroupDescription, + Ui::InputField::Mode::MultiLine, + langFactory(lng_create_group_description)); _description->show(); _description->setMaxLength(kMaxChannelDescription); @@ -1052,7 +1056,12 @@ bool EditNameBox::saveSelfFail(const RPCError &error) { EditBioBox::EditBioBox(QWidget*, not_null self) : BoxContent() , _dynamicFieldStyle(CreateBioFieldStyle()) , _self(self) -, _bio(this, _dynamicFieldStyle, langFactory(lng_bio_placeholder), _self->about()) +, _bio( + this, + _dynamicFieldStyle, + Ui::InputField::Mode::MultiLine, + langFactory(lng_bio_placeholder), + _self->about()) , _countdown(this, QString(), Ui::FlatLabel::InitType::Simple, st::editBioCountdownLabel) , _about(this, lang(lng_bio_about), Ui::FlatLabel::InitType::Simple, st::aboutRevokePublicLabel) { } @@ -1067,9 +1076,10 @@ void EditBioBox::prepare() { auto cursor = _bio->textCursor(); cursor.setPosition(_bio->getLastText().size()); _bio->setTextCursor(cursor); - connect(_bio, &Ui::InputArea::submitted, this, [this](bool ctrlShiftEnter) { save(); }); - connect(_bio, &Ui::InputArea::resized, this, [this] { updateMaxHeight(); }); - connect(_bio, &Ui::InputArea::changed, this, [this] { handleBioUpdated(); }); + connect(_bio, &Ui::InputField::submitted, this, [this](bool ctrlShiftEnter) { save(); }); + connect(_bio, &Ui::InputField::resized, this, [this] { updateMaxHeight(); }); + connect(_bio, &Ui::InputField::changed, this, [this] { handleBioUpdated(); }); + _bio->setInstantReplaces(Ui::InstantReplaces::Default()); handleBioUpdated(); updateMaxHeight(); } @@ -1122,7 +1132,12 @@ void EditBioBox::save() { EditChannelBox::EditChannelBox(QWidget*, not_null channel) : _channel(channel) , _title(this, st::defaultInputField, langFactory(_channel->isMegagroup() ? lng_dlg_new_group_name : lng_dlg_new_channel_name), _channel->name) -, _description(this, st::newGroupDescription, langFactory(lng_create_group_description), _channel->about()) +, _description( + this, + st::newGroupDescription, + Ui::InputField::Mode::MultiLine, + langFactory(lng_create_group_description), + _channel->about()) , _sign(this, lang(lng_edit_sign_messages), channel->addsSignature(), st::defaultBoxCheckbox) , _inviteGroup(std::make_shared>(channel->anyoneCanAddMembers() ? Invites::Everybody : Invites::OnlyAdmins)) , _inviteEverybody(this, _inviteGroup, Invites::Everybody, lang(lng_edit_group_invites_everybody)) diff --git a/Telegram/SourceFiles/boxes/add_contact_box.h b/Telegram/SourceFiles/boxes/add_contact_box.h index 37b1e697a..c13517585 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.h +++ b/Telegram/SourceFiles/boxes/add_contact_box.h @@ -18,7 +18,6 @@ namespace Ui { class FlatLabel; class InputField; class PhoneInput; -class InputArea; class UsernameInput; class Checkbox; template @@ -111,7 +110,7 @@ private: object_ptr _photo = { nullptr }; object_ptr _title = { nullptr }; - object_ptr _description = { nullptr }; + object_ptr _description = { nullptr }; // group / channel creation mtpRequestId _creationRequestId = 0; @@ -231,7 +230,7 @@ private: style::InputField _dynamicFieldStyle; not_null _self; - object_ptr _bio; + object_ptr _bio; object_ptr _countdown; object_ptr _about; mtpRequestId _requestId = 0; @@ -280,7 +279,7 @@ private: not_null _channel; object_ptr _title; - object_ptr _description; + object_ptr _description; object_ptr _sign; enum class Invites { diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index f5e3db2c1..7cb8e69b5 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -130,9 +130,15 @@ EditCaptionBox::EditCaptionBox( } Assert(_animated || _photo || _doc); - _field.create(this, st::confirmCaptionArea, langFactory(lng_photo_caption), caption); + _field.create( + this, + st::confirmCaptionArea, + Ui::InputField::Mode::MultiLine, + langFactory(lng_photo_caption), + caption); _field->setMaxLength(MaxPhotoCaption); _field->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); + _field->setInstantReplaces(Ui::InstantReplaces::Default()); } void EditCaptionBox::prepareGifPreview(DocumentData *document) { @@ -177,11 +183,11 @@ void EditCaptionBox::prepare() { addButton(langFactory(lng_cancel), [this] { closeBox(); }); updateBoxSize(); - connect(_field, &Ui::InputArea::submitted, this, [this] { save(); }); - connect(_field, &Ui::InputArea::cancelled, this, [this] { + connect(_field, &Ui::InputField::submitted, this, [this] { save(); }); + connect(_field, &Ui::InputField::cancelled, this, [this] { closeBox(); }); - connect(_field, &Ui::InputArea::resized, this, [this] { + connect(_field, &Ui::InputField::resized, this, [this] { captionResized(); }); diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h index 14c4c037f..091c4fbe4 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.h +++ b/Telegram/SourceFiles/boxes/edit_caption_box.h @@ -14,7 +14,7 @@ class Media; } // namespace Data namespace Ui { -class InputArea; +class InputField; } // namespace Ui class EditCaptionBox : public BoxContent, public RPCSender { @@ -49,7 +49,7 @@ private: QPixmap _thumb; Media::Clip::ReaderPointer _gifPreview; - object_ptr _field = { nullptr }; + object_ptr _field = { nullptr }; int _thumbx = 0; int _thumbw = 0; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index b82496332..716a99b09 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -71,7 +71,7 @@ private: }; struct Controls { Ui::InputField *title = nullptr; - Ui::InputArea *description = nullptr; + Ui::InputField *description = nullptr; Ui::UserpicButton *photo = nullptr; rpl::lifetime initialPhotoImageWaiting; @@ -317,19 +317,21 @@ object_ptr Controller::createDescriptionEdit() { return nullptr; } - auto result = object_ptr>( + auto result = object_ptr>( _wrap, - object_ptr( + object_ptr( _wrap, st::editPeerDescription, + Ui::InputField::Mode::MultiLine, langFactory(lng_create_group_description), channel->about()), st::editPeerDescriptionMargins); result->entity()->setMaxLength(kMaxChannelDescription); + result->entity()->setInstantReplaces(Ui::InstantReplaces::Default()); QObject::connect( result->entity(), - &Ui::InputArea::submitted, + &Ui::InputField::submitted, [this] { submitDescription(); }); _controls.description = result->entity(); diff --git a/Telegram/SourceFiles/boxes/rate_call_box.cpp b/Telegram/SourceFiles/boxes/rate_call_box.cpp index 4abe7227c..0b811f755 100644 --- a/Telegram/SourceFiles/boxes/rate_call_box.cpp +++ b/Telegram/SourceFiles/boxes/rate_call_box.cpp @@ -71,7 +71,11 @@ void RateCallBox::ratingChanged(int value) { } if (value < kMaxRating) { if (!_comment) { - _comment.create(this, st::callRatingComment, langFactory(lng_call_rate_comment)); + _comment.create( + this, + st::callRatingComment, + Ui::InputField::Mode::MultiLine, + langFactory(lng_call_rate_comment)); _comment->show(); _comment->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); _comment->setMaxLength(MaxPhotoCaption); diff --git a/Telegram/SourceFiles/boxes/rate_call_box.h b/Telegram/SourceFiles/boxes/rate_call_box.h index 8ce0a5c08..e187e7ea2 100644 --- a/Telegram/SourceFiles/boxes/rate_call_box.h +++ b/Telegram/SourceFiles/boxes/rate_call_box.h @@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/sender.h" namespace Ui { -class InputArea; +class InputField; class FlatLabel; class IconButton; } // namespace Ui @@ -44,7 +44,7 @@ private: int _rating = 0; std::vector> _stars; - object_ptr _comment = { nullptr }; + object_ptr _comment = { nullptr }; mtpRequestId _requestId = 0; diff --git a/Telegram/SourceFiles/boxes/report_box.cpp b/Telegram/SourceFiles/boxes/report_box.cpp index 8d5775c6a..54e163acf 100644 --- a/Telegram/SourceFiles/boxes/report_box.cpp +++ b/Telegram/SourceFiles/boxes/report_box.cpp @@ -82,7 +82,11 @@ void ReportBox::resizeEvent(QResizeEvent *e) { void ReportBox::reasonChanged(Reason reason) { if (reason == Reason::Other) { if (!_reasonOtherText) { - _reasonOtherText.create(this, st::profileReportReasonOther, langFactory(lng_report_reason_description)); + _reasonOtherText.create( + this, + st::profileReportReasonOther, + Ui::InputField::Mode::MultiLine, + langFactory(lng_report_reason_description)); _reasonOtherText->show(); _reasonOtherText->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); _reasonOtherText->setMaxLength(MaxPhotoCaption); diff --git a/Telegram/SourceFiles/boxes/report_box.h b/Telegram/SourceFiles/boxes/report_box.h index d6e80328b..38b71151f 100644 --- a/Telegram/SourceFiles/boxes/report_box.h +++ b/Telegram/SourceFiles/boxes/report_box.h @@ -14,7 +14,7 @@ template class RadioenumGroup; template class Radioenum; -class InputArea; +class InputField; } // namespace Ui class ReportBox : public BoxContent, public RPCSender { @@ -58,7 +58,7 @@ private: object_ptr> _reasonViolence = { nullptr }; object_ptr> _reasonPornography = { nullptr }; object_ptr> _reasonOther = { nullptr }; - object_ptr _reasonOtherText = { nullptr }; + object_ptr _reasonOtherText = { nullptr }; mtpRequestId _requestId = 0; diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index c371383c4..69fa8ff57 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -1550,29 +1550,34 @@ void SendFilesBox::setupCaption() { return; } - _caption.create(this, st::confirmCaptionArea, FieldPlaceholder(_list)); + _caption.create( + this, + st::confirmCaptionArea, + Ui::InputField::Mode::MultiLine, + FieldPlaceholder(_list)); _caption->setMaxLength(MaxPhotoCaption); _caption->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); - connect(_caption, &Ui::InputArea::resized, this, [this] { + connect(_caption, &Ui::InputField::resized, this, [this] { captionResized(); }); - connect(_caption, &Ui::InputArea::submitted, this, [this]( + connect(_caption, &Ui::InputField::submitted, this, [this]( bool ctrlShiftEnter) { send(ctrlShiftEnter); }); - connect(_caption, &Ui::InputArea::cancelled, this, [this] { + connect(_caption, &Ui::InputField::cancelled, this, [this] { closeBox(); }); _caption->setMimeDataHook([this]( not_null data, - Ui::InputArea::MimeAction action) { - if (action == Ui::InputArea::MimeAction::Check) { + Ui::InputField::MimeAction action) { + if (action == Ui::InputField::MimeAction::Check) { return canAddFiles(data); - } else if (action == Ui::InputArea::MimeAction::Insert) { + } else if (action == Ui::InputField::MimeAction::Insert) { return addFiles(data); } Unexpected("action in MimeData hook."); }); + _caption->setInstantReplaces(Ui::InstantReplaces::Default()); } void SendFilesBox::captionResized() { diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 3c87ec0ff..fee61dfff 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -18,7 +18,7 @@ class Radioenum; template class RadioenumGroup; class RoundButton; -class InputArea; +class InputField; struct GroupMediaLayout; } // namespace Ui @@ -103,7 +103,7 @@ private: base::lambda _cancelledCallback; bool _confirmed = false; - object_ptr _caption = { nullptr }; + object_ptr _caption = { nullptr }; object_ptr> _sendAlbum = { nullptr }; object_ptr> _sendPhotos = { nullptr }; object_ptr> _sendFiles = { nullptr }; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h index 4a3c3cc03..abc89727a 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h @@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class InnerDropdown; -class FlatTextarea; namespace Emoji { diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 9df9c8c12..7e2b12028 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -9,12 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_widget.h" #include "base/qthelp_regex.h" -#include "styles/style_history.h" #include "window/window_controller.h" -#include "emoji_suggestions_data.h" -#include "chat_helpers/emoji_suggestions_helper.h" #include "mainwindow.h" #include "auth_session.h" +#include "styles/style_history.h" namespace { @@ -28,7 +26,8 @@ public: QString tagFromMimeTag(const QString &mimeTag) override { if (mimeTag.startsWith(qstr("mention://"))) { auto match = QRegularExpression(":(\\d+)$").match(mimeTag); - if (!match.hasMatch() || match.capturedRef(1).toInt() != Auth().userId()) { + if (!match.hasMatch() + || match.capturedRef(1).toInt() != Auth().userId()) { return QString(); } return mimeTag.mid(0, mimeTag.size() - match.capturedLength()); @@ -97,8 +96,8 @@ std::unique_ptr MimeDataFromTextWithEntities( tag.id = ConvertTagToMimeTag(tag.id); } result->setData( - Ui::FlatTextarea::tagsMimeType(), - Ui::FlatTextarea::serializeTagsList(tags)); + TextUtilities::TagsMimeType(), + TextUtilities::SerializeTags(tags)); } return result; } @@ -111,33 +110,15 @@ void SetClipboardWithEntities( } } -MessageField::MessageField(QWidget *parent, not_null controller, const style::FlatTextarea &st, base::lambda placeholderFactory, const QString &val) : Ui::FlatTextarea(parent, st, std::move(placeholderFactory), val) +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); setTagMimeProcessor(std::make_unique()); - addInstantReplace("--", QString(1, QChar(8212))); - addInstantReplace("<<", QString(1, QChar(171))); - addInstantReplace(">>", QString(1, QChar(187))); - addInstantReplace( - ":shrug:", - QChar(175) + QString("\\_(") + QChar(12484) + ")_/" + QChar(175)); - addInstantReplace(":o ", QString(1, QChar(0xD83D)) + QChar(0xDE28)); - addInstantReplace("xD ", QString(1, QChar(0xD83D)) + QChar(0xDE06)); - const auto &replacements = Ui::Emoji::internal::GetAllReplacements(); - for (const auto &one : replacements) { - const auto with = Ui::Emoji::QStringFromUTF16(one.emoji); - const auto what = Ui::Emoji::QStringFromUTF16(one.replacement); - addInstantReplace(what, with); - } - const auto &pairs = Ui::Emoji::internal::GetReplacementPairs(); - for (const auto &[what, index] : pairs) { - const auto emoji = Ui::Emoji::internal::ByIndex(index); - Assert(emoji != nullptr); - addInstantReplace(what, emoji->text()); - } + setInstantReplaces(Ui::InstantReplaces::Default()); enableInstantReplaces(Global::ReplaceEmoji()); subscribe(Global::RefReplaceEmojiChanged(), [=] { enableInstantReplaces(Global::ReplaceEmoji()); diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index 9f1079397..0cb9b0434 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -29,7 +29,7 @@ class MessageField final : public Ui::FlatTextarea { Q_OBJECT public: - MessageField(QWidget *parent, not_null controller, const style::FlatTextarea &st, base::lambda placeholderFactory = base::lambda(), const QString &val = QString()); + MessageField(QWidget *parent, not_null controller, const style::FlatTextarea &st, base::lambda placeholderFactory = nullptr); bool hasSendText() const; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index e7bb87c85..6efb1c4e0 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -516,7 +516,7 @@ HistoryWidget::HistoryWidget( int from, int till, const QString &replacement) { - _field->commmitInstantReplacement(from, till, replacement); + _field->commitInstantReplacement(from, till, replacement); }); updateFieldSubmitSettings(); diff --git a/Telegram/SourceFiles/storage/localstorage.cpp b/Telegram/SourceFiles/storage/localstorage.cpp index 88cf68c83..94fa9c8a2 100644 --- a/Telegram/SourceFiles/storage/localstorage.cpp +++ b/Telegram/SourceFiles/storage/localstorage.cpp @@ -20,7 +20,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "lang/lang_keys.h" #include "media/media_audio.h" -#include "ui/widgets/input_fields.h" #include "mtproto/dc_options.h" #include "messenger.h" #include "application.h" @@ -2679,8 +2678,10 @@ void writeDrafts(const PeerId &peer, const MessageDraft &localDraft, const Messa _writeMap(WriteMapWhen::Fast); } - auto msgTags = Ui::FlatTextarea::serializeTagsList(localDraft.textWithTags.tags); - auto editTags = Ui::FlatTextarea::serializeTagsList(editDraft.textWithTags.tags); + auto msgTags = TextUtilities::SerializeTags( + localDraft.textWithTags.tags); + auto editTags = TextUtilities::SerializeTags( + editDraft.textWithTags.tags); int size = sizeof(quint64); size += Serialize::stringSize(localDraft.textWithTags.text) + Serialize::bytearraySize(msgTags) + 2 * sizeof(qint32); @@ -2786,8 +2787,12 @@ void readDraftsWithCursors(History *h) { return; } - msgData.tags = Ui::FlatTextarea::deserializeTagsList(msgTagsSerialized, msgData.text.size()); - editData.tags = Ui::FlatTextarea::deserializeTagsList(editTagsSerialized, editData.text.size()); + msgData.tags = TextUtilities::DeserializeTags( + msgTagsSerialized, + msgData.text.size()); + editData.tags = TextUtilities::DeserializeTags( + editTagsSerialized, + editData.text.size()); MessageCursor msgCursor, editCursor; _readDraftCursors(peer, msgCursor, editCursor); @@ -2796,13 +2801,21 @@ void readDraftsWithCursors(History *h) { if (msgData.text.isEmpty() && !msgReplyTo) { h->clearLocalDraft(); } else { - h->setLocalDraft(std::make_unique(msgData, msgReplyTo, msgCursor, msgPreviewCancelled)); + h->setLocalDraft(std::make_unique( + msgData, + msgReplyTo, + msgCursor, + msgPreviewCancelled)); } } if (!editMsgId) { h->clearEditDraft(); } else { - h->setEditDraft(std::make_unique(editData, editMsgId, editCursor, editPreviewCancelled)); + h->setEditDraft(std::make_unique( + editData, + editMsgId, + editCursor, + editPreviewCancelled)); } } diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp index 17f425f71..4b96607b1 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.cpp +++ b/Telegram/SourceFiles/ui/text/text_entity.cpp @@ -2241,6 +2241,60 @@ void Trim(TextWithEntities &result) { } } +QByteArray SerializeTags(const TextWithTags::Tags &tags) { + if (tags.isEmpty()) { + return QByteArray(); + } + + QByteArray tagsSerialized; + { + QDataStream stream(&tagsSerialized, QIODevice::WriteOnly); + stream.setVersion(QDataStream::Qt_5_1); + stream << qint32(tags.size()); + for (const auto &tag : tags) { + stream << qint32(tag.offset) << qint32(tag.length) << tag.id; + } + } + return tagsSerialized; +} + +TextWithTags::Tags DeserializeTags(QByteArray data, int textLength) { + auto result = TextWithTags::Tags(); + if (data.isEmpty()) { + return result; + } + + QDataStream stream(data); + stream.setVersion(QDataStream::Qt_5_1); + + qint32 tagCount = 0; + stream >> tagCount; + if (stream.status() != QDataStream::Ok) { + return result; + } + if (tagCount <= 0 || tagCount > textLength) { + return result; + } + + for (auto i = 0; i != tagCount; ++i) { + qint32 offset = 0, length = 0; + QString id; + stream >> offset >> length >> id; + if (stream.status() != QDataStream::Ok) { + return result; + } + if (offset < 0 || length <= 0 || offset + length > textLength) { + return result; + } + result.push_back({ offset, length, id }); + } + return result; +} + +QString TagsMimeType() { + return qsl("application/x-td-field-tags"); +} + } // namespace TextUtilities namespace Lang { diff --git a/Telegram/SourceFiles/ui/text/text_entity.h b/Telegram/SourceFiles/ui/text/text_entity.h index 1dcc9a796..ff3268753 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.h +++ b/Telegram/SourceFiles/ui/text/text_entity.h @@ -237,6 +237,10 @@ inline QString PrepareForSending(const QString &text, PrepareTextOption option = // Replace bad symbols with space and remove '\r'. void ApplyServerCleaning(TextWithEntities &result); +QByteArray SerializeTags(const TextWithTags::Tags &tags); +TextWithTags::Tags DeserializeTags(QByteArray data, int textLength); +QString TagsMimeType(); + } // namespace TextUtilities namespace Lang { diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp index 86dfc4f3e..de6539b34 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp +++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp @@ -8,10 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/input_fields.h" #include "ui/widgets/popup_menu.h" -#include "mainwindow.h" #include "ui/countryinput.h" +#include "emoji_suggestions_data.h" +#include "chat_helpers/emoji_suggestions_helper.h" #include "window/themes/window_theme.h" #include "lang/lang_keys.h" +#include "mainwindow.h" #include "numbers.h" #include "messenger.h" @@ -92,60 +94,87 @@ QTextImageFormat PrepareEmojiFormat(EmojiPtr emoji, const style::font &f) { } // namespace -QByteArray FlatTextarea::serializeTagsList(const TagList &tags) { - if (tags.isEmpty()) { - return QByteArray(); +class InputField::Inner final : public QTextEdit { +public: + Inner(not_null parent) : QTextEdit(parent) { } - QByteArray tagsSerialized; - { - QDataStream stream(&tagsSerialized, QIODevice::WriteOnly); - stream.setVersion(QDataStream::Qt_5_1); - stream << qint32(tags.size()); - for_const (auto &tag, tags) { - stream << qint32(tag.offset) << qint32(tag.length) << tag.id; - } + QVariant loadResource(int type, const QUrl &name) { + return outer()->loadResource(type, name); } - return tagsSerialized; + +protected: + bool viewportEvent(QEvent *e) override { + return outer()->viewportEventInner(e); + } + void focusInEvent(QFocusEvent *e) override { + return outer()->focusInEventInner(e); + } + void focusOutEvent(QFocusEvent *e) override { + return outer()->focusOutEventInner(e); + } + void keyPressEvent(QKeyEvent *e) override { + return outer()->keyPressEventInner(e); + } + void contextMenuEvent(QContextMenuEvent *e) override { + return outer()->contextMenuEventInner(e); + } + + bool canInsertFromMimeData(const QMimeData *source) const override { + return outer()->canInsertFromMimeDataInner(source); + } + void insertFromMimeData(const QMimeData *source) override { + return outer()->insertFromMimeDataInner(source); + } + QMimeData *createMimeDataFromSelection() const override { + return outer()->createMimeDataFromSelectionInner(); + } + +private: + not_null outer() const { + return static_cast(parentWidget()); + } + friend class InputField; + +}; + +void InstantReplaces::add(const QString &what, const QString &with) { + auto node = &reverseMap; + for (auto i = what.end(), b = what.begin(); i != b;) { + node = &node->tail.emplace(*--i, Node()).first->second; + } + node->text = with; + accumulate_max(maxLength, int(what.size())); } -FlatTextarea::TagList FlatTextarea::deserializeTagsList(QByteArray data, int textLength) { - TagList result; - if (data.isEmpty()) { - return result; - } - - QDataStream stream(data); - stream.setVersion(QDataStream::Qt_5_1); - - qint32 tagCount = 0; - stream >> tagCount; - if (stream.status() != QDataStream::Ok) { - return result; - } - if (tagCount <= 0 || tagCount > textLength) { - return result; - } - - for (int i = 0; i < tagCount; ++i) { - qint32 offset = 0, length = 0; - QString id; - stream >> offset >> length >> id; - if (stream.status() != QDataStream::Ok) { - return result; +const InstantReplaces &InstantReplaces::Default() { + static const auto result = [] { + auto result = InstantReplaces(); + result.add("--", QString(1, QChar(8212))); + result.add("<<", QString(1, QChar(171))); + result.add(">>", QString(1, QChar(187))); + result.add( + ":shrug:", + QChar(175) + QString("\\_(") + QChar(12484) + ")_/" + QChar(175)); + result.add(":o ", QString(1, QChar(0xD83D)) + QChar(0xDE28)); + result.add("xD ", QString(1, QChar(0xD83D)) + QChar(0xDE06)); + const auto &replacements = Emoji::internal::GetAllReplacements(); + for (const auto &one : replacements) { + const auto with = Emoji::QStringFromUTF16(one.emoji); + const auto what = Emoji::QStringFromUTF16(one.replacement); + result.add(what, with); } - if (offset < 0 || length <= 0 || offset + length > textLength) { - return result; + const auto &pairs = Emoji::internal::GetReplacementPairs(); + for (const auto &[what, index] : pairs) { + const auto emoji = Emoji::internal::ByIndex(index); + Assert(emoji != nullptr); + result.add(what, emoji->text()); } - result.push_back({ offset, length, id }); - } + return result; + }(); return result; } -QString FlatTextarea::tagsMimeType() { - return qsl("application/x-td-field-tags"); -} - 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()) @@ -200,15 +229,8 @@ FlatTextarea::FlatTextarea(QWidget *parent, const style::FlatTextarea &st, base: } } -void FlatTextarea::addInstantReplace( - const QString &what, - const QString &with) { - auto node = &_reverseInstantReplaces; - for (auto i = what.end(), b = what.begin(); i != b;) { - node = &node->tail.emplace(*--i, InstantReplaceNode()).first->second; - } - node->text = with; - accumulate_max(_instantReplaceMaxLength, int(what.size())); +void FlatTextarea::setInstantReplaces(const InstantReplaces &replaces) { + _mutableInstantReplaces = replaces; } void FlatTextarea::enableInstantReplaces(bool enabled) { @@ -952,11 +974,13 @@ QStringList FlatTextarea::linksList() const { } void FlatTextarea::insertFromMimeData(const QMimeData *source) { - auto mime = tagsMimeType(); + auto mime = TextUtilities::TagsMimeType(); auto text = source->text(); if (source->hasFormat(mime)) { auto tagsData = source->data(mime); - _insertedTags = deserializeTagsList(tagsData, text.size()); + _insertedTags = TextUtilities::DeserializeTags( + tagsData, + text.size()); _insertedTagsAreFromMime = true; } else { _insertedTags.clear(); @@ -1399,7 +1423,9 @@ QMimeData *FlatTextarea::createMimeDataFromSelection() const { tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id); } } - result->setData(tagsMimeType(), serializeTagsList(tags)); + result->setData( + TextUtilities::TagsMimeType(), + TextUtilities::SerializeTags(tags)); } } return result; @@ -1502,20 +1528,25 @@ void FlatTextarea::keyPressEvent(QKeyEvent *e) { } } +const InstantReplaces &FlatTextarea::instantReplaces() const { + return _mutableInstantReplaces; +} + void FlatTextarea::processInstantReplaces(const QString &text) { + const auto &replaces = instantReplaces(); if (text.size() != 1 - || !_instantReplaceMaxLength - || !_instantReplacesEnabled) { + || !_instantReplacesEnabled + || !replaces.maxLength) { return; } - const auto it = _reverseInstantReplaces.tail.find(text[0]); - if (it == end(_reverseInstantReplaces.tail)) { + 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 - _instantReplaceMaxLength, 0), + std::max(position - replaces.maxLength, 0), position - 1, &tags); auto node = &it->second; @@ -1546,10 +1577,10 @@ void FlatTextarea::applyInstantReplace( } else if (position < length) { return; } - commmitInstantReplacement(position - length, position, with, what); + commitInstantReplacement(position - length, position, with, what); } -void FlatTextarea::commmitInstantReplacement( +void FlatTextarea::commitInstantReplacement( int from, int till, const QString &with, @@ -1943,27 +1974,63 @@ void FlatInput::onTextChange(const QString &text) { if (App::wnd()) App::wnd()->updateGlobalMenu(); } -InputArea::InputArea( +InputField::InputField( QWidget *parent, const style::InputField &st, base::lambda placeholderFactory, - const QString &val) + const QString &value) +: InputField( + parent, + st, + Mode::SingleLine, + std::move(placeholderFactory), + { value, {} }) { +} + +InputField::InputField( + QWidget *parent, + const style::InputField &st, + Mode mode, + base::lambda placeholderFactory, + const QString &value) +: InputField( + parent, + st, + mode, + std::move(placeholderFactory), + { value, {} }) { +} + +InputField::InputField( + QWidget *parent, + const style::InputField &st, + Mode mode, + base::lambda placeholderFactory, + const TextWithTags &value) : RpWidget(parent) , _st(st) +, _mode(mode) , _inner(this) -, _oldtext(val) +, _lastTextWithTags(value) , _placeholderFactory(std::move(placeholderFactory)) { _inner->setAcceptRichText(false); resize(_st.width, _st.heightMin); - setAttribute(Qt::WA_OpaquePaintEvent); + if (_st.textBg->c.alphaF() >= 1.) { + setAttribute(Qt::WA_OpaquePaintEvent); + } _inner->setFont(_st.font->f); + _inner->setAlignment(_st.textAlign); + if (_mode == Mode::SingleLine) { + _inner->setWordWrapMode(QTextOption::NoWrap); + } - subscribe(Lang::Current().updated(), [this] { refreshPlaceholder(); }); + subscribe(Lang::Current().updated(), [=] { refreshPlaceholder(); }); refreshPlaceholder(); - subscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) { + subscribe(Window::Theme::Background(), [=]( + const Window::Theme::BackgroundUpdate &update) { if (update.paletteChanged()) { updatePalette(); } @@ -1993,29 +2060,65 @@ InputArea::InputArea( setCursor(style::cur_text); heightAutoupdated(); - if (!val.isEmpty()) { - _inner->setPlainText(val); + if (!_lastTextWithTags.text.isEmpty()) { + setTextWithTags(_lastTextWithTags, HistoryAction::Clear); } - _inner->document()->clearUndoRedoStacks(); + + _defaultCharFormat = _inner->textCursor().charFormat(); startBorderAnimation(); startPlaceholderAnimation(); finishAnimating(); } -void InputArea::updatePalette() { +bool InputField::viewportEventInner(QEvent *e) { + if (e->type() == QEvent::TouchBegin + || e->type() == QEvent::TouchUpdate + || e->type() == QEvent::TouchEnd + || e->type() == QEvent::TouchCancel) { + const auto ev = static_cast(e); + if (ev->device()->type() == QTouchDevice::TouchScreen) { + handleTouchEvent(ev); + } + } + return _inner->QTextEdit::viewportEvent(e); +} + +QVariant InputField::loadResource(int type, const QUrl &name) { + const auto imageName = name.toDisplayString(); + if (const auto emoji = Ui::Emoji::FromUrl(imageName)) { + return QVariant(App::emojiSingle(emoji, _st.font->height)); + } + return _inner->QTextEdit::loadResource(type, name); +} + +void InputField::updatePalette() { auto p = palette(); p.setColor(QPalette::Text, _st.textFg->c); setPalette(p); } -void InputArea::onTouchTimer() { +void InputField::onTouchTimer() { _touchRightButton = true; } -bool InputArea::heightAutoupdated() { - if (_st.heightMin < 0 || _st.heightMax < 0 || _inHeightCheck) return false; +void InputField::setInstantReplaces(const InstantReplaces &replaces) { + _mutableInstantReplaces = replaces; +} + +void InputField::enableInstantReplaces(bool enabled) { + _instantReplacesEnabled = enabled; +} + +bool InputField::heightAutoupdated() { + if (_st.heightMin < 0 + || _st.heightMax < 0 + || _inHeightCheck + || _mode == Mode::SingleLine) { + return false; + } _inHeightCheck = true; + const auto guard = gsl::finally([&] { _inHeightCheck = false; }); SendPendingMoveResizeEvents(this); @@ -2027,34 +2130,18 @@ bool InputArea::heightAutoupdated() { } if (height() != newh) { resize(width(), newh); - _inHeightCheck = false; return true; } - _inHeightCheck = false; return false; } -void InputArea::checkContentHeight() { +void InputField::checkContentHeight() { if (heightAutoupdated()) { emit resized(); } } -InputArea::Inner::Inner(InputArea *parent) : QTextEdit(parent) { -} - -bool InputArea::Inner::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) { - qobject_cast(parentWidget())->touchEvent(ev); - return QTextEdit::viewportEvent(e); - } - } - return QTextEdit::viewportEvent(e); -} - -void InputArea::touchEvent(QTouchEvent *e) { +void InputField::handleTouchEvent(QTouchEvent *e) { switch (e->type()) { case QEvent::TouchBegin: { if (_touchPress || e->touchPoints().isEmpty()) return; @@ -2096,12 +2183,14 @@ void InputArea::touchEvent(QTouchEvent *e) { } } -void InputArea::paintEvent(QPaintEvent *e) { +void InputField::paintEvent(QPaintEvent *e) { Painter p(this); auto ms = getms(); auto r = rect().intersected(e->rect()); - p.fillRect(r, _st.textBg); + if (_st.textBg->c.alphaF() > 0.) { + p.fillRect(r, _st.textBg); + } if (_st.border) { p.fillRect(0, height() - _st.border, width(), _st.border, _st.borderFg); } @@ -2167,7 +2256,7 @@ void InputArea::paintEvent(QPaintEvent *e) { TWidget::paintEvent(e); } -void InputArea::startBorderAnimation() { +void InputField::startBorderAnimation() { auto borderVisible = (_error || _focused); if (_borderVisible != borderVisible) { _borderVisible = borderVisible; @@ -2183,742 +2272,6 @@ void InputArea::startBorderAnimation() { } } -void InputArea::focusInEvent(QFocusEvent *e) { - _borderAnimationStart = (e->reason() == Qt::MouseFocusReason) ? mapFromGlobal(QCursor::pos()).x() : (width() / 2); - QTimer::singleShot(0, this, SLOT(onFocusInner())); -} - -void InputArea::mousePressEvent(QMouseEvent *e) { - _borderAnimationStart = e->pos().x(); - QTimer::singleShot(0, this, SLOT(onFocusInner())); -} - -void InputArea::onFocusInner() { - auto borderStart = _borderAnimationStart; - _inner->setFocus(); - _borderAnimationStart = borderStart; -} - -void InputArea::contextMenuEvent(QContextMenuEvent *e) { - _inner->contextMenuEvent(e); -} - -void InputArea::Inner::focusInEvent(QFocusEvent *e) { - f()->focusInInner(e->reason() == Qt::MouseFocusReason); - QTextEdit::focusInEvent(e); - emit f()->focused(); -} - -void InputArea::focusInInner(bool focusByMouse) { - _borderAnimationStart = focusByMouse ? mapFromGlobal(QCursor::pos()).x() : (width() / 2); - setFocused(true); -} - -void InputArea::Inner::focusOutEvent(QFocusEvent *e) { - f()->focusOutInner(); - QTextEdit::focusOutEvent(e); - emit f()->blurred(); -} - -void InputArea::focusOutInner() { - setFocused(false); -} - -void InputArea::setFocused(bool focused) { - if (_focused != focused) { - _focused = focused; - _a_focused.start([this] { update(); }, _focused ? 0. : 1., _focused ? 1. : 0., _st.duration); - startPlaceholderAnimation(); - startBorderAnimation(); - } -} - -QSize InputArea::sizeHint() const { - return geometry().size(); -} - -QSize InputArea::minimumSizeHint() const { - return geometry().size(); -} - -QString InputArea::getText(int32 start, int32 end) const { - if (end >= 0 && end <= start) return QString(); - - if (start < 0) start = 0; - bool 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(); - - 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; - } - - 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; - - int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length()); - if (!full) { - if (p >= end || e <= start) { - continue; - } - } - - QTextCharFormat f = fragment.charFormat(); - QString emojiText; - QString 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()) { - auto imageName = static_cast(&f)->name(); - if (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); - return result; -} - -bool InputArea::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; - } - return (from.next() != till); -} - -bool InputArea::isUndoAvailable() const { - return _undoAvailable; -} - -bool InputArea::isRedoAvailable() const { - return _redoAvailable; -} - -void InputArea::insertEmoji(EmojiPtr emoji, QTextCursor c) { - const auto format = PrepareEmojiFormat(emoji, _st.font); - c.insertText(kObjectReplacement, format); -} - -QVariant InputArea::Inner::loadResource(int type, const QUrl &name) { - auto imageName = name.toDisplayString(); - if (auto emoji = Ui::Emoji::FromUrl(imageName)) { - return QVariant(App::emojiSingle(emoji, f()->_st.font->height)); - } - return QVariant(); -} - -void InputArea::processDocumentContentsChange(int position, int charsAdded) { - int32 replacePosition = -1, replaceLen = 0; - EmojiPtr emoji = nullptr; - - // 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(); - while (true) { - int32 start = position, end = position + charsAdded; - 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 fragment(iter.fragment()); - if (!fragment.isValid()) continue; - - int32 fp = fragment.position(), fe = fp + fragment.length(); - if (fp >= end || fe <= start) { - continue; - } - - if (tildeFormatting) { - isTildeFragment = (fragment.charFormat().fontFamily() == tildeFixedFont); - } - - QString t(fragment.text()); - const QChar *ch = t.constData(), *e = ch + t.size(); - for (; ch != e; ++ch, ++fp) { - int32 emojiLen = 0; - emoji = Ui::Emoji::Find(ch, e, &emojiLen); - if (emoji) { - if (replacePosition >= 0) { - emoji = 0; // replace tilde char format first - } else { - replacePosition = fp; - replaceLen = emojiLen; - } - break; - } - - if (tildeFormatting && fp >= position) { // tilde fix in OpenSans - bool tilde = (ch->unicode() == '~'); - if ((tilde && !isTildeFragment) || (!tilde && isTildeFragment)) { - if (replacePosition < 0) { - replacePosition = fp; - replaceLen = 1; - } else { - ++replaceLen; - } - } else if (replacePosition >= 0) { - break; - } - } - - if (ch + 1 < e && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) { - ++ch; - ++fp; - } - } - if (replacePosition >= 0) break; - } - if (replacePosition >= 0) break; - } - if (replacePosition >= 0) { - if (!_inner->document()->pageSize().isNull()) { - _inner->document()->setPageSize(QSizeF(0, 0)); - } - QTextCursor c(doc->docHandle(), 0); - c.setPosition(replacePosition); - c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor); - if (emoji) { - insertEmoji(emoji, c); - } else { - QTextCharFormat format; - format.setFontFamily(isTildeFragment ? tildeRegularFont : tildeFixedFont); - c.mergeCharFormat(format); - } - charsAdded -= replacePosition + replaceLen - position; - position = replacePosition + (emoji ? 1 : replaceLen); - - emoji = 0; - replacePosition = -1; - } else { - break; - } - } - c.endEditBlock(); -} - -void InputArea::onDocumentContentsChange(int position, int charsRemoved, int charsAdded) { - if (_correcting) return; - - QString oldtext(_oldtext); - QTextCursor(_inner->document()->docHandle(), 0).joinPreviousEditBlock(); - - 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; - } - } - } - - _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; - } - } - } - _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 InputArea::onDocumentContentsChanged() { - if (_correcting) return; - - setErrorShown(false); - - auto curText = getText(); - if (_oldtext != curText) { - _oldtext = curText; - emit changed(); - checkContentHeight(); - } - startPlaceholderAnimation(); - if (App::wnd()) App::wnd()->updateGlobalMenu(); -} - -void InputArea::onUndoAvailable(bool avail) { - _undoAvailable = avail; - if (App::wnd()) App::wnd()->updateGlobalMenu(); -} - -void InputArea::onRedoAvailable(bool avail) { - _redoAvailable = avail; - if (App::wnd()) App::wnd()->updateGlobalMenu(); -} - -void InputArea::setDisplayFocused(bool focused) { - setFocused(focused); - finishAnimating(); -} - -void InputArea::finishAnimating() { - _a_focused.finish(); - _a_error.finish(); - _a_placeholderShifted.finish(); - _a_borderShown.finish(); - _a_borderOpacity.finish(); - update(); -} - -void InputArea::startPlaceholderAnimation() { - auto placeholderShifted = (_focused && _st.placeholderScale > 0.) || !getLastText().isEmpty(); - if (_placeholderShifted != placeholderShifted) { - _placeholderShifted = placeholderShifted; - _a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration); - } -} - -QMimeData *InputArea::Inner::createMimeDataFromSelection() const { - QMimeData *result = new QMimeData(); - QTextCursor c(textCursor()); - int32 start = c.selectionStart(), end = c.selectionEnd(); - if (end > start) { - result->setText(f()->getText(start, end)); - } - return result; -} - -void InputArea::customUpDown(bool custom) { - _customUpDown = custom; -} - -void InputArea::setCtrlEnterSubmit(CtrlEnterSubmit ctrlEnterSubmit) { - _ctrlEnterSubmit = ctrlEnterSubmit; -} - -void InputArea::Inner::keyPressEvent(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 = (ctrl && shift) || - (ctrl && (f()->_ctrlEnterSubmit == CtrlEnterSubmit::CtrlEnter || f()->_ctrlEnterSubmit == CtrlEnterSubmit::Both)) || - (!ctrl && !shift && (f()->_ctrlEnterSubmit == CtrlEnterSubmit::Enter || f()->_ctrlEnterSubmit == CtrlEnterSubmit::Both)); - 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 (enter && ctrlGood) { - emit f()->submitted(ctrl && shift); - } else if (e->key() == Qt::Key_Escape) { - e->ignore(); - emit f()->cancelled(); - } else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab) { - if (alt || ctrl) { - e->ignore(); - } else { - if (!focusNextPrevChild(e->key() == Qt::Key_Tab && !shift)) { - e->ignore(); - } - } - } else if (e->key() == Qt::Key_Search || e == QKeySequence::Find) { - e->ignore(); - } else if (f()->_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) { - 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) { - QApplication::clipboard()->setText(f()->getText(start, end), QClipboard::FindBuffer); - } -#endif // Q_OS_MAC - } else { - QTextCursor tc(textCursor()); - if (enter && ctrl) { - e->setModifiers(e->modifiers() & ~Qt::ControlModifier); - } - QTextEdit::keyPressEvent(e); - if (tc == textCursor()) { - bool check = false; - if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) { - tc.movePosition(QTextCursor::Start, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); - check = true; - } else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_Down) { - tc.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); - check = true; - } - if (check) { - if (tc == textCursor()) { - e->ignore(); - } else { - setTextCursor(tc); - } - } - } - } -} - -void InputArea::Inner::contextMenuEvent(QContextMenuEvent *e) { - if (auto menu = createStandardContextMenu()) { - (new Ui::PopupMenu(nullptr, menu))->popup(e->globalPos()); - } -} - -bool InputArea::Inner::canInsertFromMimeData(const QMimeData *source) const { - if (source - && f()->_mimeDataHook - && f()->_mimeDataHook(source, MimeAction::Check)) { - return true; - } - return QTextEdit::canInsertFromMimeData(source); -} - -void InputArea::Inner::insertFromMimeData(const QMimeData *source) { - if (source - && f()->_mimeDataHook - && f()->_mimeDataHook(source, MimeAction::Insert)) { - return; - } - return QTextEdit::insertFromMimeData(source); -} - -void InputArea::resizeEvent(QResizeEvent *e) { - refreshPlaceholder(); - _inner->setGeometry(rect().marginsRemoved(_st.textMargins)); - _borderAnimationStart = width() / 2; - TWidget::resizeEvent(e); - checkContentHeight(); -} - -void InputArea::refreshPlaceholder() { - auto placeholderText = _placeholderFactory ? _placeholderFactory() : QString(); - auto availableWidth = width() - _st.textMargins.left() - _st.textMargins.right() - _st.placeholderMargins.left() - _st.placeholderMargins.right() - 1; - if (_st.placeholderScale > 0.) { - auto placeholderFont = _st.placeholderFont->f; - placeholderFont.setStyleStrategy(QFont::PreferMatch); - auto metrics = QFontMetrics(placeholderFont); - _placeholder = metrics.elidedText(placeholderText, Qt::ElideRight, availableWidth); - _placeholderPath = QPainterPath(); - if (!_placeholder.isEmpty()) { - _placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder); - } - } else { - _placeholder = _st.placeholderFont->elided(placeholderText, availableWidth); - } - update(); -} - -void InputArea::setPlaceholder(base::lambda placeholderFactory) { - _placeholderFactory = std::move(placeholderFactory); - refreshPlaceholder(); -} - -void InputArea::showError() { - setErrorShown(true); - if (!hasFocus()) { - _inner->setFocus(); - } -} - -void InputArea::setErrorShown(bool error) { - if (_error != error) { - _error = error; - _a_error.start([this] { update(); }, _error ? 0. : 1., _error ? 1. : 0., _st.duration); - startBorderAnimation(); - } -} - -InputField::InputField( - QWidget *parent, - const style::InputField &st, - base::lambda placeholderFactory, - const QString &val) -: RpWidget(parent) -, _st(st) -, _inner(std::make_unique(this)) -, _oldtext(val) -, _placeholderFactory(std::move(placeholderFactory)) { - _inner->setAcceptRichText(false); - resize(_st.width, _st.heightMin); - - _inner->setWordWrapMode(QTextOption::NoWrap); - - if (_st.textBg->c.alphaF() >= 1.) { - setAttribute(Qt::WA_OpaquePaintEvent); - } - - _inner->setFont(_st.font->f); - _inner->setAlignment(_st.textAlign); - - subscribe(Lang::Current().updated(), [this] { refreshPlaceholder(); }); - refreshPlaceholder(); - - subscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) { - if (update.paletteChanged()) { - updatePalette(); - } - }); - updatePalette(); - - _inner->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - _inner->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - _inner->setFrameStyle(QFrame::NoFrame | QFrame::Plain); - _inner->viewport()->setAutoFillBackground(false); - - _inner->setContentsMargins(0, 0, 0, 0); - _inner->document()->setDocumentMargin(0); - - setAttribute(Qt::WA_AcceptTouchEvents); - _inner->viewport()->setAttribute(Qt::WA_AcceptTouchEvents); - _touchTimer.setSingleShot(true); - connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer())); - - connect(_inner->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(onDocumentContentsChange(int,int,int))); - connect(_inner->document(), SIGNAL(contentsChanged()), this, SLOT(onDocumentContentsChanged())); - connect(_inner.get(), SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool))); - connect(_inner.get(), SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool))); - if (App::wnd()) connect(_inner.get(), SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu())); - - setCursor(style::cur_text); - if (!val.isEmpty()) { - _inner->setPlainText(val); - } - _inner->document()->clearUndoRedoStacks(); - - startPlaceholderAnimation(); - startBorderAnimation(); - finishAnimating(); -} - -void InputField::updatePalette() { - auto p = palette(); - p.setColor(QPalette::Text, _st.textFg->c); - setPalette(p); -} - -void InputField::onTouchTimer() { - _touchRightButton = true; -} - -InputField::Inner::Inner(InputField *parent) : QTextEdit(parent) { -} - -bool InputField::Inner::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) { - qobject_cast(parentWidget())->touchEvent(ev); - return QTextEdit::viewportEvent(e); - } - } - return QTextEdit::viewportEvent(e); -} - -void InputField::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; - } -} - -void InputField::paintEvent(QPaintEvent *e) { - Painter p(this); - - auto ms = getms(); - QRect r(rect().intersected(e->rect())); - if (_st.textBg->c.alphaF() > 0.) { - p.fillRect(r, _st.textBg); - } - if (_st.border) { - p.fillRect(0, height() - _st.border, width(), _st.border, _st.borderFg); - } - auto errorDegree = _a_error.current(ms, _error ? 1. : 0.); - auto focusedDegree = _a_focused.current(ms, _focused ? 1. : 0.); - auto borderShownDegree = _a_borderShown.current(ms, 1.); - auto borderOpacity = _a_borderOpacity.current(ms, _borderVisible ? 1. : 0.); - if (_st.borderActive && (borderOpacity > 0.)) { - auto borderStart = snap(_borderAnimationStart, 0, width()); - auto borderFrom = qRound(borderStart * (1. - borderShownDegree)); - auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree); - if (borderTo > borderFrom) { - auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree); - p.setOpacity(borderOpacity); - p.fillRect(borderFrom, height() - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg); - p.setOpacity(1); - } - } - - if (_st.placeholderScale > 0. && !_placeholderPath.isEmpty()) { - auto placeholderShiftDegree = _a_placeholderShifted.current(ms, _placeholderShifted ? 1. : 0.); - p.save(); - p.setClipRect(r); - - auto placeholderTop = anim::interpolate(0, _st.placeholderShift, placeholderShiftDegree); - - QRect r(rect().marginsRemoved(_st.textMargins + _st.placeholderMargins)); - r.moveTop(r.top() + placeholderTop); - if (rtl()) r.moveLeft(width() - r.left() - r.width()); - - auto placeholderScale = 1. - (1. - _st.placeholderScale) * placeholderShiftDegree; - auto placeholderFg = anim::color(_st.placeholderFg, _st.placeholderFgActive, focusedDegree); - placeholderFg = anim::color(placeholderFg, _st.placeholderFgError, errorDegree); - - PainterHighQualityEnabler hq(p); - p.setPen(Qt::NoPen); - p.setBrush(placeholderFg); - p.translate(r.topLeft()); - p.scale(placeholderScale, placeholderScale); - p.drawPath(_placeholderPath); - - p.restore(); - } else if (!_placeholder.isEmpty()) { - auto placeholderHiddenDegree = _a_placeholderShifted.current(ms, _placeholderShifted ? 1. : 0.); - if (placeholderHiddenDegree < 1.) { - p.setOpacity(1. - placeholderHiddenDegree); - p.save(); - p.setClipRect(r); - - auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree); - - QRect 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); - - p.restore(); - } - } - TWidget::paintEvent(e); -} - void InputField::focusInEvent(QFocusEvent *e) { _borderAnimationStart = (e->reason() == Qt::MouseFocusReason) ? mapFromGlobal(QCursor::pos()).x() : (width() / 2); QTimer::singleShot(0, this, SLOT(onFocusInner())); @@ -2939,25 +2292,19 @@ void InputField::contextMenuEvent(QContextMenuEvent *e) { _inner->contextMenuEvent(e); } -void InputField::Inner::focusInEvent(QFocusEvent *e) { - f()->focusInInner(e->reason() == Qt::MouseFocusReason); - QTextEdit::focusInEvent(e); - emit f()->focused(); -} - -void InputField::focusInInner(bool focusByMouse) { - _borderAnimationStart = focusByMouse ? mapFromGlobal(QCursor::pos()).x() : (width() / 2); +void InputField::focusInEventInner(QFocusEvent *e) { + _borderAnimationStart = (e->reason() == Qt::MouseFocusReason) + ? mapFromGlobal(QCursor::pos()).x() + : (width() / 2); setFocused(true); + _inner->QTextEdit::focusInEvent(e); + emit focused(); } -void InputField::Inner::focusOutEvent(QFocusEvent *e) { - f()->focusOutInner(); - QTextEdit::focusOutEvent(e); - emit f()->blurred(); -} - -void InputField::focusOutInner() { +void InputField::focusOutEventInner(QFocusEvent *e) { setFocused(false); + _inner->QTextEdit::focusOutEvent(e); + emit blurred(); } void InputField::setFocused(bool focused) { @@ -2969,32 +2316,6 @@ void InputField::setFocused(bool focused) { } } -void InputField::startPlaceholderAnimation() { - auto placeholderShifted = _forcePlaceholderHidden - || (_focused && _st.placeholderScale > 0.) - || !getLastText().isEmpty(); - if (_placeholderShifted != placeholderShifted) { - _placeholderShifted = placeholderShifted; - _a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration); - } -} - -void InputField::startBorderAnimation() { - auto borderVisible = (_error || _focused); - if (_borderVisible != borderVisible) { - _borderVisible = borderVisible; - if (_borderVisible) { - if (_a_borderOpacity.animating()) { - _a_borderOpacity.start([this] { update(); }, 0., 1., _st.duration); - } else { - _a_borderShown.start([this] { update(); }, 0., 1., _st.duration); - } - } else { - _a_borderOpacity.start([this] { update(); }, 1., 0., _st.duration); - } - } -} - QSize InputField::sizeHint() const { return geometry().size(); } @@ -3105,19 +2426,7 @@ void InputField::insertEmoji(EmojiPtr emoji, QTextCursor c) { c.insertText(kObjectReplacement, format); } -QVariant InputField::Inner::loadResource(int type, const QUrl &name) { - QString imageName = name.toDisplayString(); - if (auto emoji = Ui::Emoji::FromUrl(imageName)) { - return QVariant(App::emojiSingle(emoji, f()->_st.font->height)); - } - return QVariant(); -} - void InputField::processDocumentContentsChange(int position, int charsAdded) { - int32 replacePosition = -1, replaceLen = 0; - EmojiPtr emoji = nullptr; - bool newlineFound = false; - // Tilde formatting. auto tildeFormatting = !cRetina() && (font().pixelSize() == 13) && (font().family() == qstr("Open Sans")); auto isTildeFragment = false; @@ -3127,33 +2436,65 @@ void InputField::processDocumentContentsChange(int position, int charsAdded) { QTextDocument *doc(_inner->document()); QTextCursor c(_inner->textCursor()); c.joinPreviousEditBlock(); + + 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(); - for (QTextBlock b = from; b != till; b = b.next()) { - for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) { - QTextFragment fragment(iter.fragment()); + 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; - int32 fp = fragment.position(), fe = fp + fragment.length(); + auto fp = fragment.position(); + auto fe = fp + fragment.length(); if (fp >= end || fe <= start) { continue; } + auto format = fragment.charFormat(); if (tildeFormatting) { - isTildeFragment = (fragment.charFormat().fontFamily() == tildeFixedFont); + isTildeFragment = (format.fontFamily() == tildeFixedFont); } - QString t(fragment.text()); - const QChar *ch = t.constData(), *e = ch + t.size(); - for (; ch != e; ++ch, ++fp) { + 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 = fp + + (fragmentText.startsWith(string) + ? string.size() + : 0); + action.intervalEnd = fp + + fragmentText.size(); + break; + } + } + auto *ch = textStart;// +qMax(changedPositionInFragment, 0); + for (; ch != textEnd; ++ch, ++fp) { // QTextBeginningOfFrame // QTextEndOfFrame - newlineFound = (ch->unicode() == 0xfdd0 || ch->unicode() == 0xfdd1 || ch->unicode() == QChar::ParagraphSeparator || ch->unicode() == QChar::LineSeparator || ch->unicode() == '\n' || ch->unicode() == '\r'); - if (newlineFound) { + removeNewline = (_mode == Mode::SingleLine) + && (ch->unicode() == 0xfdd0 + || ch->unicode() == 0xfdd1 + || ch->unicode() == QChar::ParagraphSeparator + || ch->unicode() == QChar::LineSeparator + || ch->unicode() == '\n' + || ch->unicode() == '\r'); + if (removeNewline) { if (replacePosition >= 0) { - newlineFound = false; // replace tilde char format first + removeNewline = false; // replace tilde char format first } else { replacePosition = fp; replaceLen = 1; @@ -3162,7 +2503,7 @@ void InputField::processDocumentContentsChange(int position, int charsAdded) { } auto emojiLen = 0; - emoji = Ui::Emoji::Find(ch, e, &emojiLen); + emoji = Ui::Emoji::Find(ch, textEnd, &emojiLen); if (emoji) { if (replacePosition >= 0) { emoji = 0; // replace tilde char format first @@ -3187,29 +2528,37 @@ void InputField::processDocumentContentsChange(int position, int charsAdded) { } } - if (ch + 1 < e && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) { + if (ch + 1 < textEnd && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) { ++ch; ++fp; } } if (replacePosition >= 0) break; } - if (replacePosition >= 0) break; + if (replacePosition >= 0 || action.type != ActionType::Invalid) { + break; + } - if (b.next() != doc->end()) { - newlineFound = true; + if (_mode == Mode::SingleLine && b.next() != doc->end()) { + removeNewline = true; replacePosition = b.next().position() - 1; replaceLen = 1; break; } } - if (replacePosition >= 0) { - if (!_inner->document()->pageSize().isNull()) { - _inner->document()->setPageSize(QSizeF(0, 0)); - } + + 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 (newlineFound) { + if (removeNewline) { QTextCharFormat format; format.setFontFamily(font().family()); c.mergeCharFormat(format); @@ -3222,11 +2571,7 @@ void InputField::processDocumentContentsChange(int position, int charsAdded) { c.mergeCharFormat(format); } charsAdded -= replacePosition + replaceLen - position; - position = replacePosition + ((emoji || newlineFound) ? 1 : replaceLen); - - newlineFound = false; - emoji = 0; - replacePosition = -1; + position = replacePosition + ((emoji || removeNewline) ? 1 : replaceLen); } else { break; } @@ -3237,7 +2582,6 @@ void InputField::processDocumentContentsChange(int position, int charsAdded) { void InputField::onDocumentContentsChange(int position, int charsRemoved, int charsAdded) { if (_correcting) return; - QString oldtext(_oldtext); QTextCursor(_inner->document()->docHandle(), 0).joinPreviousEditBlock(); if (!position) { // Qt bug workaround https://bugreports.qt.io/browse/QTBUG-49062 @@ -3309,9 +2653,10 @@ void InputField::onDocumentContentsChanged() { setErrorShown(false); auto curText = getText(); - if (_oldtext != curText) { - _oldtext = curText; + if (_lastTextWithTags.text != curText) { + _lastTextWithTags.text = curText; emit changed(); + checkContentHeight(); } startPlaceholderAnimation(); if (App::wnd()) App::wnd()->updateGlobalMenu(); @@ -3327,6 +2672,11 @@ void InputField::onRedoAvailable(bool avail) { if (App::wnd()) App::wnd()->updateGlobalMenu(); } +void InputField::setDisplayFocused(bool focused) { + setFocused(focused); + finishAnimating(); +} + void InputField::selectAll() { auto cursor = _inner->textCursor(); cursor.setPosition(0); @@ -3334,11 +2684,6 @@ void InputField::selectAll() { _inner->setTextCursor(cursor); } -void InputField::setDisplayFocused(bool focused) { - setFocused(focused); - finishAnimating(); -} - void InputField::finishAnimating() { _a_focused.finish(); _a_error.finish(); @@ -3353,25 +2698,88 @@ void InputField::setPlaceholderHidden(bool forcePlaceholderHidden) { startPlaceholderAnimation(); } -QMimeData *InputField::Inner::createMimeDataFromSelection() const { - auto result = new QMimeData(); - auto cursor = textCursor(); - auto start = cursor.selectionStart(); - auto end = cursor.selectionEnd(); - if (end > start) { - result->setText(f()->getText(start, end)); +void InputField::startPlaceholderAnimation() { + auto placeholderShifted = _forcePlaceholderHidden + || (_focused && _st.placeholderScale > 0.) + || !getLastText().isEmpty(); + if (_placeholderShifted != placeholderShifted) { + _placeholderShifted = placeholderShifted; + _a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration); } - return result; +} + +QMimeData *InputField::createMimeDataFromSelectionInner() const { + auto result = std::make_unique(); + const auto cursor = _inner->textCursor(); + const auto start = cursor.selectionStart(); + const auto end = cursor.selectionEnd(); + if (end > start) { + result->setText(getText(start, end)); + } + return result.release(); } void InputField::customUpDown(bool custom) { _customUpDown = custom; } -void InputField::Inner::keyPressEvent(QKeyEvent *e) { +void InputField::setCtrlEnterSubmit(CtrlEnterSubmit ctrlEnterSubmit) { + _ctrlEnterSubmit = ctrlEnterSubmit; +} + +void InputField::setTextCursor(const QTextCursor &cursor) { + return _inner->setTextCursor(cursor); +} + +QTextCursor InputField::textCursor() const { + return _inner->textCursor(); +} + +void InputField::setCursorPosition(int pos) { + auto cursor = _inner->textCursor(); + cursor.setPosition(pos); + _inner->setTextCursor(cursor); +} + +void InputField::setText(const QString &text) { + setTextWithTags({ text, {} }); +} + +void InputField::setTextWithTags( + const TextWithTags &textWithTags, + HistoryAction historyAction) { + _inner->setPlainText(textWithTags.text); // #TODO + startPlaceholderAnimation(); + if (historyAction == HistoryAction::Clear) { + _inner->document()->clearUndoRedoStacks(); + } +} + +void InputField::clear() { + _inner->clear(); + startPlaceholderAnimation(); +} + +bool InputField::hasFocus() const { + return _inner->hasFocus(); +} + +void InputField::setFocus() { + _inner->setFocus(); +} + +void InputField::clearFocus() { + _inner->clearFocus(); +} + +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), ctrlGood = true; + bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier); + bool ctrlGood = (_mode == Mode::SingleLine) + || (ctrl && shift) + || (ctrl && (_ctrlEnterSubmit == CtrlEnterSubmit::CtrlEnter || _ctrlEnterSubmit == CtrlEnterSubmit::Both)) + || (!ctrl && !shift && (_ctrlEnterSubmit == CtrlEnterSubmit::Enter || _ctrlEnterSubmit == CtrlEnterSubmit::Both)); bool enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return); if (macmeta && e->key() == Qt::Key_Backspace) { @@ -3379,11 +2787,15 @@ void InputField::Inner::keyPressEvent(QKeyEvent *e) { 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 && ctrlGood) { - emit f()->submitted(ctrl && shift); + emit submitted(ctrl && shift); } else if (e->key() == Qt::Key_Escape) { e->ignore(); - emit f()->cancelled(); + emit cancelled(); } else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab) { if (alt || ctrl) { e->ignore(); @@ -3394,7 +2806,7 @@ void InputField::Inner::keyPressEvent(QKeyEvent *e) { } } else if (e->key() == Qt::Key_Search || e == QKeySequence::Find) { e->ignore(); - } else if (f()->_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) { + } else if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) { e->ignore(); #ifdef Q_OS_MAC } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) { @@ -3405,47 +2817,197 @@ void InputField::Inner::keyPressEvent(QKeyEvent *e) { } #endif // Q_OS_MAC } else { - auto oldCursorPosition = textCursor().position(); + const auto text = e->text(); + const auto oldPosition = textCursor().position(); if (enter && ctrl) { e->setModifiers(e->modifiers() & ~Qt::ControlModifier); } - QTextEdit::keyPressEvent(e); - auto currentCursor = textCursor(); - if (textCursor().position() == oldCursorPosition) { + _inner->QTextEdit::keyPressEvent(e); + auto cursor = textCursor(); + if (cursor.position() == oldPosition) { bool check = false; if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) { - oldCursorPosition = currentCursor.position(); - currentCursor.movePosition(QTextCursor::Start, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); + cursor.movePosition(QTextCursor::Start, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); check = true; } else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_Down) { - oldCursorPosition = currentCursor.position(); - currentCursor.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); + cursor.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); check = true; } else if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Right || e->key() == Qt::Key_Backspace) { e->ignore(); } if (check) { - if (oldCursorPosition == currentCursor.position()) { + if (oldPosition == cursor.position()) { e->ignore(); } else { - setTextCursor(currentCursor); + setTextCursor(cursor); } } } + processInstantReplaces(text); } } -void InputField::Inner::contextMenuEvent(QContextMenuEvent *e) { - if (auto menu = createStandardContextMenu()) { +const InstantReplaces &InputField::instantReplaces() const { + return _mutableInstantReplaces; +} + +void InputField::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(); + const auto typed = getText( + std::max(position - replaces.maxLength, 0), + position - 1); + 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 InputField::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 InputField::commitInstantReplacement( + int from, + int till, + const QString &with, + base::optional checkOriginal) { + const auto original = getText(from, till); + 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 InputField::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 = _inner->document()->findBlock(inside); + if (block == _inner->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 InputField::contextMenuEventInner(QContextMenuEvent *e) { + if (const auto menu = _inner->createStandardContextMenu()) { (new Ui::PopupMenu(nullptr, menu))->popup(e->globalPos()); } } +bool InputField::canInsertFromMimeDataInner(const QMimeData *source) const { + if (source + && _mimeDataHook + && _mimeDataHook(source, MimeAction::Check)) { + return true; + } + return _inner->QTextEdit::canInsertFromMimeData(source); +} + +void InputField::insertFromMimeDataInner(const QMimeData *source) { + if (source + && _mimeDataHook + && _mimeDataHook(source, MimeAction::Insert)) { + return; + } + return _inner->QTextEdit::insertFromMimeData(source); +} + void InputField::resizeEvent(QResizeEvent *e) { refreshPlaceholder(); _inner->setGeometry(rect().marginsRemoved(_st.textMargins)); _borderAnimationStart = width() / 2; TWidget::resizeEvent(e); + checkContentHeight(); } void InputField::refreshPlaceholder() { diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.h b/Telegram/SourceFiles/ui/widgets/input_fields.h index ad35691d6..ba012761c 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.h +++ b/Telegram/SourceFiles/ui/widgets/input_fields.h @@ -16,25 +16,36 @@ namespace Ui { static UserData * const LookingUpInlineBot = SharedMemoryLocation(); +struct InstantReplaces { + struct Node { + QString text; + std::map tail; + }; + + void add(const QString &what, const QString &with); + + static const InstantReplaces &Default(); + + int maxLength = 0; + Node reverseMap; + +}; + class FlatTextarea : public TWidgetHelper, protected base::Subscriber { Q_OBJECT public: using TagList = TextWithTags::Tags; - static QByteArray serializeTagsList(const TagList &tags); - static TagList deserializeTagsList(QByteArray data, int textLength); - static QString tagsMimeType(); - 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 addInstantReplace(const QString &what, const QString &with); - void commmitInstantReplacement( + void commitInstantReplacement( int from, int till, const QString &with, @@ -150,10 +161,6 @@ protected: void checkContentHeight(); private: - struct InstantReplaceNode { - QString text; - std::map tail; - }; void updatePalette(); void refreshPlaceholder(); @@ -172,6 +179,9 @@ private: // 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(); @@ -234,8 +244,7 @@ private: QTextCharFormat _defaultCharFormat; - int _instantReplaceMaxLength = 0; - InstantReplaceNode _reverseInstantReplaces; + InstantReplaces _mutableInstantReplaces; bool _instantReplacesEnabled = true; }; @@ -334,15 +343,33 @@ enum class CtrlEnterSubmit { Both, }; -class InputArea : public RpWidget, private base::Subscriber { +class InputField : public RpWidget, private base::Subscriber { Q_OBJECT public: - InputArea( + enum class Mode { + SingleLine, + MultiLine, + }; + using TagList = TextWithTags::Tags; + + InputField( QWidget *parent, const style::InputField &st, - base::lambda placeholderFactory = base::lambda(), - const QString &val = QString()); + base::lambda placeholderFactory, + const QString &value = QString()); + InputField( + QWidget *parent, + const style::InputField &st, + Mode mode, + base::lambda placeholderFactory, + const QString &value); + InputField( + QWidget *parent, + const style::InputField &st, + Mode mode = Mode::SingleLine, + base::lambda placeholderFactory = nullptr, + const TextWithTags &value = TextWithTags()); void showError(); @@ -350,10 +377,28 @@ public: _maxLength = maxLength; } + enum class HistoryAction { + NewEntry, + MergeEntry, + Clear, + }; + void setTextWithTags( + const TextWithTags &textWithTags, + HistoryAction historyAction = HistoryAction::NewEntry); + + void setInstantReplaces(const InstantReplaces &replaces); + void enableInstantReplaces(bool enabled); + void commitInstantReplacement( + int from, + int till, + const QString &with, + base::optional checkOriginal = base::none); + const QString &getLastText() const { - return _oldtext; + return _lastTextWithTags.text; } void setPlaceholder(base::lambda placeholderFactory); + void setPlaceholderHidden(bool forcePlaceholderHidden); void setDisplayFocused(bool focused); void finishAnimating(); void setFocusFast() { @@ -366,6 +411,7 @@ public: QString getText(int start = 0, int end = -1) const; bool hasText() const; + void selectAll(); bool isUndoAvailable() const; bool isRedoAvailable() const; @@ -373,29 +419,14 @@ public: void customUpDown(bool isCustom); void setCtrlEnterSubmit(CtrlEnterSubmit ctrlEnterSubmit); - void setTextCursor(const QTextCursor &cursor) { - return _inner->setTextCursor(cursor); - } - QTextCursor textCursor() const { - return _inner->textCursor(); - } - void setText(const QString &text) { - _inner->setText(text); - startPlaceholderAnimation(); - } - void clear() { - _inner->clear(); - startPlaceholderAnimation(); - } - bool hasFocus() const { - return _inner->hasFocus(); - } - void setFocus() { - _inner->setFocus(); - } - void clearFocus() { - _inner->clearFocus(); - } + void setTextCursor(const QTextCursor &cursor); + void setCursorPosition(int position); + QTextCursor textCursor() const; + void setText(const QString &text); + void clear(); + bool hasFocus() const; + void setFocus(); + void clearFocus(); enum class MimeAction { Check, @@ -441,7 +472,6 @@ protected: return qobject_cast(parentWidget()); } - void touchEvent(QTouchEvent *e); void paintEvent(QPaintEvent *e) override; void focusInEvent(QFocusEvent *e) override; void mousePressEvent(QMouseEvent *e) override; @@ -449,32 +479,13 @@ protected: void resizeEvent(QResizeEvent *e) override; private: - class Inner : public QTextEdit { - public: - Inner(InputArea *parent); - - QVariant loadResource(int type, const QUrl &name) override; - - protected: - bool viewportEvent(QEvent *e) override; - void focusInEvent(QFocusEvent *e) override; - void focusOutEvent(QFocusEvent *e) override; - void keyPressEvent(QKeyEvent *e) override; - void contextMenuEvent(QContextMenuEvent *e) override; - - bool canInsertFromMimeData(const QMimeData *source) const override; - void insertFromMimeData(const QMimeData *source) override; - QMimeData *createMimeDataFromSelection() const override; - - private: - InputArea *f() const { - return static_cast(parentWidget()); - } - friend class InputArea; - - }; + class Inner; friend class Inner; + bool viewportEventInner(QEvent *e); + QVariant loadResource(int type, const QUrl &name); + void handleTouchEvent(QTouchEvent *e); + void updatePalette(); void refreshPlaceholder(); @@ -482,19 +493,34 @@ private: void checkContentHeight(); void setErrorShown(bool error); - void focusInInner(bool focusByMouse); - void focusOutInner(); + void focusInEventInner(QFocusEvent *e); + void focusOutEventInner(QFocusEvent *e); void setFocused(bool focused); + void keyPressEventInner(QKeyEvent *e); + void contextMenuEventInner(QContextMenuEvent *e); + + QMimeData *createMimeDataFromSelectionInner() const; + bool canInsertFromMimeDataInner(const QMimeData *source) const; + void insertFromMimeDataInner(const QMimeData *source); void processDocumentContentsChange(int position, int charsAdded); + // 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(); + const style::InputField &_st; + Mode _mode = Mode::SingleLine; int _maxLength = -1; + bool _forcePlaceholderHidden = false; object_ptr _inner; - QString _oldtext; + TextWithTags _lastTextWithTags; CtrlEnterSubmit _ctrlEnterSubmit = CtrlEnterSubmit::CtrlEnter; bool _undoAvailable = false; @@ -529,190 +555,11 @@ private: bool _correcting = false; MimeDataHook _mimeDataHook; -}; + QTextCharFormat _defaultCharFormat; -class InputField : public RpWidget, private base::Subscriber { - Q_OBJECT + InstantReplaces _mutableInstantReplaces; + bool _instantReplacesEnabled = true; -public: - InputField(QWidget *parent, const style::InputField &st, base::lambda placeholderFactory = base::lambda(), const QString &val = QString()); - - void setMaxLength(int maxLength) { - _maxLength = maxLength; - } - - void showError(); - - const QString &getLastText() const { - return _oldtext; - } - void setPlaceholder(base::lambda placeholderFactory); - void setPlaceholderHidden(bool forcePlaceholderHidden); - void setDisplayFocused(bool focused); - void finishAnimating(); - void setFocusFast() { - setDisplayFocused(true); - setFocus(); - } - - QSize sizeHint() const override; - QSize minimumSizeHint() const override; - - QString getText(int start = 0, int end = -1) const; - bool hasText() const; - - bool isUndoAvailable() const; - bool isRedoAvailable() const; - - void customUpDown(bool isCustom); - - void setTextCursor(const QTextCursor &cursor) { - return _inner->setTextCursor(cursor); - } - QTextCursor textCursor() const { - return _inner->textCursor(); - } - void setText(const QString &text) { - _inner->setText(text); - startPlaceholderAnimation(); - } - void clear() { - _inner->clear(); - startPlaceholderAnimation(); - } - bool hasFocus() const { - return _inner->hasFocus(); - } - void setFocus() { - _inner->setFocus(); - auto cursor = _inner->textCursor(); - cursor.movePosition(QTextCursor::End); - _inner->setTextCursor(cursor); - } - void clearFocus() { - _inner->clearFocus(); - } - void setCursorPosition(int pos) { - auto cursor = _inner->textCursor(); - cursor.setPosition(pos); - _inner->setTextCursor(cursor); - } - -public slots: - void selectAll(); - -private slots: - void onTouchTimer(); - - void onDocumentContentsChange(int position, int charsRemoved, int charsAdded); - void onDocumentContentsChanged(); - - void onUndoAvailable(bool avail); - void onRedoAvailable(bool avail); - - void onFocusInner(); - -signals: - void changed(); - void submitted(bool ctrlShiftEnter); - void cancelled(); - void tabbed(); - - void focused(); - void blurred(); - -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 touchEvent(QTouchEvent *e); - void paintEvent(QPaintEvent *e) override; - void focusInEvent(QFocusEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void contextMenuEvent(QContextMenuEvent *e) override; - void resizeEvent(QResizeEvent *e) override; - -private: - class Inner : public QTextEdit { - public: - Inner(InputField *parent); - - QVariant loadResource(int type, const QUrl &name) override; - - protected: - bool viewportEvent(QEvent *e) override; - void focusInEvent(QFocusEvent *e) override; - void focusOutEvent(QFocusEvent *e) override; - void keyPressEvent(QKeyEvent *e) override; - void contextMenuEvent(QContextMenuEvent *e) override; - - QMimeData *createMimeDataFromSelection() const override; - - private: - InputField *f() const { - return static_cast(parentWidget()); - } - friend class InputField; - - }; - friend class Inner; - - void updatePalette(); - void refreshPlaceholder(); - void setErrorShown(bool error); - - void focusInInner(bool focusByMouse); - void focusOutInner(); - void setFocused(bool focused); - - void processDocumentContentsChange(int position, int charsAdded); - - const style::InputField &_st; - - std::unique_ptr _inner; - - int _maxLength = -1; - bool _forcePlaceholderHidden = false; - - QString _oldtext; - - bool _undoAvailable = false; - bool _redoAvailable = false; - - bool _customUpDown = true; - - QString _placeholder; - base::lambda _placeholderFactory; - Animation _a_placeholderShifted; - bool _placeholderShifted = false; - QPainterPath _placeholderPath; - - Animation _a_borderShown; - int _borderAnimationStart = 0; - Animation _a_borderOpacity; - bool _borderVisible = false; - - Animation _a_focused; - Animation _a_error; - - bool _focused = false; - bool _error = false; - - QTimer _touchTimer; - bool _touchPress = false; - bool _touchRightButton = false; - bool _touchMove = false; - QPoint _touchStart; - - bool _correcting = false; }; class MaskedInputField diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index 7cc8fd80a..c0a297342 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -760,13 +760,18 @@ void Notification::showReplyField() { _background->setGeometry(0, st::notifyMinHeight, width(), st::notifySendReply.height + st::notifyBorderWidth); _background->show(); - _replyArea.create(this, st::notifyReplyArea, langFactory(lng_message_ph), QString()); + _replyArea.create( + this, + st::notifyReplyArea, + Ui::InputField::Mode::MultiLine, + langFactory(lng_message_ph)); _replyArea->resize(width() - st::notifySendReply.width - 2 * st::notifyBorderWidth, st::notifySendReply.height); _replyArea->moveToLeft(st::notifyBorderWidth, st::notifyMinHeight); _replyArea->show(); _replyArea->setFocus(); _replyArea->setMaxLength(MaxMessageSize); _replyArea->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both); + _replyArea->setInstantReplaces(Ui::InstantReplaces::Default()); // Catch mouse press event to activate the window. QCoreApplication::instance()->installEventFilter(this); diff --git a/Telegram/SourceFiles/window/notifications_manager_default.h b/Telegram/SourceFiles/window/notifications_manager_default.h index 7d847c4fc..8f3b97841 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.h +++ b/Telegram/SourceFiles/window/notifications_manager_default.h @@ -13,7 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class IconButton; class RoundButton; -class InputArea; +class InputField; } // namespace Ui namespace Window { @@ -233,7 +233,7 @@ private: object_ptr _close; object_ptr _reply; object_ptr _background = { nullptr }; - object_ptr _replyArea = { nullptr }; + object_ptr _replyArea = { nullptr }; object_ptr _replySend = { nullptr }; bool _waitingForInput = true;