diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 6352baeaf..6df5730f8 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -34,8 +34,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 0,9,47,0 - PRODUCTVERSION 0,9,47,0 + FILEVERSION 0,9,48,0 + PRODUCTVERSION 0,9,48,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -51,10 +51,10 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "CompanyName", "Telegram Messenger LLP" - VALUE "FileVersion", "0.9.47.0" + VALUE "FileVersion", "0.9.48.0" VALUE "LegalCopyright", "Copyright (C) 2014-2016" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "0.9.47.0" + VALUE "ProductVersion", "0.9.48.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 182787008..5a58c3a01 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -25,8 +25,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 0,9,47,0 - PRODUCTVERSION 0,9,47,0 + FILEVERSION 0,9,48,0 + PRODUCTVERSION 0,9,48,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -43,10 +43,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram Messenger LLP" VALUE "FileDescription", "Telegram Updater" - VALUE "FileVersion", "0.9.47.0" + VALUE "FileVersion", "0.9.48.0" VALUE "LegalCopyright", "Copyright (C) 2014-2016" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "0.9.47.0" + VALUE "ProductVersion", "0.9.48.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/core/basic_types.h b/Telegram/SourceFiles/core/basic_types.h index 0e2d05e64..d828ffd20 100644 --- a/Telegram/SourceFiles/core/basic_types.h +++ b/Telegram/SourceFiles/core/basic_types.h @@ -166,6 +166,16 @@ template char(&ArraySizeHelper(T(&array)[N]))[N]; #define qsl(s) QStringLiteral(s) #define qstr(s) QLatin1String(s, sizeof(s) - 1) +// For QFlags<> declared in private section of a class we need to declare +// operators from Q_DECLARE_OPERATORS_FOR_FLAGS as friend functions. +#define Q_DECLARE_FRIEND_INCOMPATIBLE_FLAGS(Flags) \ +friend QIncompatibleFlag operator|(Flags::enum_type f1, int f2) Q_DECL_NOTHROW; + +#define Q_DECLARE_FRIEND_OPERATORS_FOR_FLAGS(Flags) \ +friend QFlags operator|(Flags::enum_type f1, Flags::enum_type f2) Q_DECL_NOTHROW; \ +friend QFlags operator|(Flags::enum_type f1, QFlags f2) Q_DECL_NOTHROW; \ +Q_DECLARE_FRIEND_INCOMPATIBLE_FLAGS(Flags) + // using for_const instead of plain range-based for loop to ensure usage of const_iterator // it is important for the copy-on-write Qt containers // if you have "QVector v" then "for (T * const p : v)" will still call QVector::detach(), diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 4f4c0a710..cb1108299 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -24,7 +24,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #define BETA_VERSION_MACRO (0ULL) -constexpr int AppVersion = 9047; -constexpr str_const AppVersionStr = "0.9.47"; +constexpr int AppVersion = 9048; +constexpr str_const AppVersionStr = "0.9.48"; constexpr bool AppAlphaVersion = true; constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO; diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index cc8a8bec1..fd61a44bc 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -151,22 +151,23 @@ struct SendAction { int32 progress; }; +using TextWithTags = FlatTextarea::TextWithTags; struct HistoryDraft { HistoryDraft() : msgId(0), previewCancelled(false) { } - HistoryDraft(const QString &text, MsgId msgId, const MessageCursor &cursor, bool previewCancelled) - : text(text) + HistoryDraft(const TextWithTags &textWithTags, MsgId msgId, const MessageCursor &cursor, bool previewCancelled) + : textWithTags(textWithTags) , msgId(msgId) , cursor(cursor) , previewCancelled(previewCancelled) { } HistoryDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled) - : text(field.getLastText()) + : textWithTags(field.getTextWithTags()) , msgId(msgId) , cursor(field) , previewCancelled(previewCancelled) { } - QString text; + TextWithTags textWithTags; MsgId msgId; // replyToId for message draft, editMsgId for edit draft MessageCursor cursor; bool previewCancelled; @@ -176,8 +177,8 @@ struct HistoryEditDraft : public HistoryDraft { : HistoryDraft() , saveRequest(0) { } - HistoryEditDraft(const QString &text, MsgId msgId, const MessageCursor &cursor, bool previewCancelled, mtpRequestId saveRequest = 0) - : HistoryDraft(text, msgId, cursor, previewCancelled) + HistoryEditDraft(const TextWithTags &textWithTags, MsgId msgId, const MessageCursor &cursor, bool previewCancelled, mtpRequestId saveRequest = 0) + : HistoryDraft(textWithTags, msgId, cursor, previewCancelled) , saveRequest(saveRequest) { } HistoryEditDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled, mtpRequestId saveRequest = 0) @@ -373,7 +374,7 @@ public: } void takeMsgDraft(History *from) { if (auto &draft = from->_msgDraft) { - if (!draft->text.isEmpty() && !_msgDraft) { + if (!draft->textWithTags.text.isEmpty() && !_msgDraft) { _msgDraft = std_::move(draft); _msgDraft->msgId = 0; // edit and reply to drafts can't migrate } diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 949c04817..448dbf54b 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -2053,8 +2053,8 @@ MessageField::MessageField(HistoryWidget *history, const style::flatTextarea &st } bool MessageField::hasSendText() const { - const QString &text(getLastText()); - for (const QChar *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) { + auto &text(getTextWithTags().text); + for (auto *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) { ushort code = ch->unicode(); if (code != ' ' && code != '\n' && code != '\r' && !chReplacedBySpace(code)) { return true; @@ -2735,7 +2735,7 @@ QPoint SilentToggle::tooltipPos() const { return QCursor::pos(); } -EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) { +EntitiesInText entitiesFromTextTags(const FlatTextarea::TagList &tags) { EntitiesInText result; if (tags.isEmpty()) { return result; @@ -2754,6 +2754,24 @@ EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) { return result; } +TextWithTags::Tags textTagsFromEntities(const EntitiesInText &entities) { + TextWithTags::Tags result; + if (entities.isEmpty()) { + return result; + } + + result.reserve(entities.size()); + for_const (auto &entity, entities) { + if (entity.type() == EntityInTextMentionName) { + auto match = QRegularExpression("^(\\d+\\.\\d+)$").match(entity.data()); + if (match.hasMatch()) { + result.push_back({ entity.offset(), entity.length(), qstr("mention://user.") + entity.data() }); + } + } + } + return result; +} + namespace { // For mention tags save and validate userId, ignore tags for different userId. @@ -2974,7 +2992,7 @@ void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete:: // Send bot command at once, if it was not inserted by pressing Tab. if (str.at(0) == '/' && method != FieldAutocomplete::ChooseMethod::ByTab) { App::sendBotCommand(_peer, nullptr, str); - setFieldText(_field.getLastText().mid(_field.textCursor().position())); + setFieldText(_field.getTextWithTagsPart(_field.textCursor().position())); } else { _field.insertTag(str); } @@ -3022,15 +3040,16 @@ void HistoryWidget::updateInlineBotQuery() { void HistoryWidget::updateStickersByEmoji() { int32 len = 0; - if (EmojiPtr emoji = emojiFromText(_field.getLastText(), &len)) { - if (_field.getLastText().size() <= len) { - _fieldAutocomplete->showStickers(emoji); - } else { + auto &text = _field.getTextWithTags().text; + if (EmojiPtr emoji = emojiFromText(text, &len)) { + if (text.size() > len) { len = 0; + } else { + _fieldAutocomplete->showStickers(emoji); } } if (!len) { - _fieldAutocomplete->showStickers(EmojiPtr(0)); + _fieldAutocomplete->showStickers(nullptr); } } @@ -3039,7 +3058,7 @@ void HistoryWidget::onTextChange() { updateStickersByEmoji(); if (_peer && (!_peer->isChannel() || _peer->isMegagroup() || !_peer->asChannel()->canPublish() || (!_peer->asChannel()->isBroadcast() && !_broadcast.checked()))) { - if (!_inlineBot && !_editMsgId && (_textUpdateEventsFlags & TextUpdateEventsSendTyping)) { + if (!_inlineBot && !_editMsgId && (_textUpdateEvents.testFlag(TextUpdateEvent::SendTyping))) { updateSendAction(_history, SendActionTyping); } } @@ -3065,13 +3084,13 @@ void HistoryWidget::onTextChange() { update(); } - if (!_peer || !(_textUpdateEventsFlags & TextUpdateEventsSaveDraft)) return; + if (!_peer || !(_textUpdateEvents.testFlag(TextUpdateEvent::SaveDraft))) return; _saveDraftText = true; onDraftSave(true); } void HistoryWidget::onDraftSaveDelayed() { - if (!_peer || !(_textUpdateEventsFlags & TextUpdateEventsSaveDraft)) return; + if (!_peer || !(_textUpdateEvents.testFlag(TextUpdateEvent::SaveDraft))) return; if (!_field.textCursor().anchor() && !_field.textCursor().position() && !_field.verticalScrollBar()->value()) { if (!Local::hasDraftCursors(_peer->id)) { return; @@ -3106,17 +3125,17 @@ void HistoryWidget::writeDrafts(HistoryDraft **msgDraft, HistoryEditDraft **edit Local::MessageDraft localMsgDraft, localEditDraft; if (msgDraft) { if (*msgDraft) { - localMsgDraft = Local::MessageDraft((*msgDraft)->msgId, (*msgDraft)->text, (*msgDraft)->previewCancelled); + localMsgDraft = Local::MessageDraft((*msgDraft)->msgId, (*msgDraft)->textWithTags, (*msgDraft)->previewCancelled); } } else { - localMsgDraft = Local::MessageDraft(_replyToId, _field.getLastText(), _previewCancelled); + localMsgDraft = Local::MessageDraft(_replyToId, _field.getTextWithTags(), _previewCancelled); } if (editDraft) { if (*editDraft) { - localEditDraft = Local::MessageDraft((*editDraft)->msgId, (*editDraft)->text, (*editDraft)->previewCancelled); + localEditDraft = Local::MessageDraft((*editDraft)->msgId, (*editDraft)->textWithTags, (*editDraft)->previewCancelled); } } else if (_editMsgId) { - localEditDraft = Local::MessageDraft(_editMsgId, _field.getLastText(), _previewCancelled); + localEditDraft = Local::MessageDraft(_editMsgId, _field.getTextWithTags(), _previewCancelled); } Local::writeDrafts(_peer->id, localMsgDraft, localEditDraft); if (_migrated) { @@ -3152,11 +3171,11 @@ void HistoryWidget::writeDrafts(History *history) { Local::MessageDraft localMsgDraft, localEditDraft; MessageCursor msgCursor, editCursor; if (auto msgDraft = history->msgDraft()) { - localMsgDraft = Local::MessageDraft(msgDraft->msgId, msgDraft->text, msgDraft->previewCancelled); + localMsgDraft = Local::MessageDraft(msgDraft->msgId, msgDraft->textWithTags, msgDraft->previewCancelled); msgCursor = msgDraft->cursor; } if (auto editDraft = history->editDraft()) { - localEditDraft = Local::MessageDraft(editDraft->msgId, editDraft->text, editDraft->previewCancelled); + localEditDraft = Local::MessageDraft(editDraft->msgId, editDraft->textWithTags, editDraft->previewCancelled); editCursor = editDraft->cursor; } Local::writeDrafts(history->peer->id, localMsgDraft, localEditDraft); @@ -3331,8 +3350,9 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(const QString &query) { } bot->botInfo->inlineReturnPeerId = 0; History *h = App::history(toPeerId); - auto text = '@' + bot->username + ' ' + query; - h->setMsgDraft(std_::make_unique(text, 0, MessageCursor(text.size(), text.size(), QFIXED_MAX), false)); + TextWithTags textWithTags = { '@' + bot->username + ' ' + query, TextWithTags::Tags() }; + MessageCursor cursor = { textWithTags.text.size(), textWithTags.text.size(), QFIXED_MAX }; + h->setMsgDraft(std_::make_unique(textWithTags, 0, cursor, false)); if (h == _history) { applyDraft(); } else { @@ -3633,11 +3653,11 @@ void HistoryWidget::applyDraft(bool parseLinks) { return; } - _textUpdateEventsFlags = 0; - setFieldText(draft->text); + _textUpdateEvents = 0; + setFieldText(draft->textWithTags); _field.setFocus(); draft->cursor.applyTo(_field); - _textUpdateEventsFlags = TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping; + _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; _previewCancelled = draft->previewCancelled; if (auto editDraft = _history->editDraft()) { _editMsgId = editDraft->msgId; @@ -3730,7 +3750,7 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re if (_editMsgId) { _history->setEditDraft(std_::make_unique(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId)); } else { - if (_replyToId || !_field.getLastText().isEmpty()) { + if (_replyToId || !_field.isEmpty()) { _history->setMsgDraft(std_::make_unique(_field, _replyToId, _previewCancelled)); } else { _history->clearMsgDraft(); @@ -4738,11 +4758,11 @@ void HistoryWidget::preloadHistoryIfNeeded() { } void HistoryWidget::onInlineBotCancel() { - QString text = _field.getLastText(); - if (text.size() > _inlineBotUsername.size() + 2) { - setFieldText('@' + _inlineBotUsername + ' ', TextUpdateEventsSaveDraft, false); + auto &textWithTags = _field.getTextWithTags(); + if (textWithTags.text.size() > _inlineBotUsername.size() + 2) { + setFieldText({ '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory); } else { - clearFieldText(TextUpdateEventsSaveDraft, false); + clearFieldText(TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory); } } @@ -4783,11 +4803,10 @@ void HistoryWidget::saveEditMsg() { WebPageId webPageId = _previewCancelled ? CancelledWebPageId : ((_previewData && _previewData->pendingTill >= 0) ? _previewData->id : 0); - auto fieldText = _field.getLastText(); - auto fieldTags = _field.getLastTags(); + auto &textWithTags = _field.getTextWithTags(); auto prepareFlags = itemTextOptions(_history, App::self()).flags; - EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(fieldTags); - QString sendingText, leftText = prepareTextWithEntities(fieldText, prepareFlags, &leftEntities); + EntitiesInText sendingEntities, leftEntities = entitiesFromTextTags(textWithTags.tags); + QString sendingText, leftText = prepareTextWithEntities(textWithTags.text, prepareFlags, &leftEntities); if (!textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) { _field.selectAll(); @@ -4865,8 +4884,7 @@ void HistoryWidget::onSend(bool ctrlShiftEnter, MsgId replyTo) { MainWidget::MessageToSend message; message.history = _history; - message.text = _field.getLastText(); - message.entities = _field.getLastTags(); + message.textWithTags = _field.getTextWithTags(); message.replyTo = replyTo; message.broadcast = _broadcast.checked(); message.silent = _silent.checked(); @@ -5379,7 +5397,7 @@ void HistoryWidget::sendBotCommand(PeerData *peer, UserData *bot, const QString MainWidget::MessageToSend message; message.history = _history; - message.text = toSend; + message.textWithTags = { toSend, TextWithTags::Tags() }; message.replyTo = replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? replyTo : -1) : 0; message.broadcast = false; message.silent = false; @@ -5460,8 +5478,9 @@ bool HistoryWidget::botCallbackFail(BotCallbackInfo info, const RPCError &error, bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) { if (!_history) return false; + bool insertingInlineBot = !cmd.isEmpty() && (cmd.at(0) == '@'); QString toInsert = cmd; - if (!toInsert.isEmpty() && toInsert.at(0) != '@') { + if (!toInsert.isEmpty() && !insertingInlineBot) { PeerData *bot = _peer->isUser() ? _peer : (App::hoveredLinkItem() ? App::hoveredLinkItem()->fromOriginal() : 0); if (!bot->isUser() || !bot->asUser()->botInfo) bot = 0; QString username = bot ? bot->asUser()->username : QString(); @@ -5472,28 +5491,33 @@ bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) { } toInsert += ' '; - if (toInsert.at(0) != '@') { - QString text = _field.getLastText(); + if (!insertingInlineBot) { + auto &textWithTags = _field.getTextWithTags(); if (specialGif) { - if (text.trimmed() == '@' + cInlineGifBotUsername() && text.at(0) == '@') { - clearFieldText(TextUpdateEventsSaveDraft, false); + if (textWithTags.text.trimmed() == '@' + cInlineGifBotUsername() && textWithTags.text.at(0) == '@') { + clearFieldText(TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory); } } else { - QRegularExpressionMatch m = QRegularExpression(qsl("^/[A-Za-z_0-9]{0,64}(@[A-Za-z_0-9]{0,32})?(\\s|$)")).match(text); + TextWithTags textWithTagsToSet; + QRegularExpressionMatch m = QRegularExpression(qsl("^/[A-Za-z_0-9]{0,64}(@[A-Za-z_0-9]{0,32})?(\\s|$)")).match(textWithTags.text); if (m.hasMatch()) { - text = toInsert + text.mid(m.capturedLength()); + textWithTagsToSet = _field.getTextWithTagsPart(m.capturedLength()); } else { - text = toInsert + text; + textWithTagsToSet = textWithTags; } - _field.setTextFast(text); + textWithTagsToSet.text = toInsert + textWithTagsToSet.text; + for (auto &tag : textWithTagsToSet.tags) { + tag.offset += toInsert.size(); + } + _field.setTextWithTags(textWithTagsToSet); QTextCursor cur(_field.textCursor()); cur.movePosition(QTextCursor::End); _field.setTextCursor(cur); } } else { - if (!specialGif || _field.getLastText().isEmpty()) { - setFieldText(toInsert, TextUpdateEventsSaveDraft, false); + if (!specialGif || _field.isEmpty()) { + setFieldText({ toInsert, TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory); _field.setFocus(); return true; } @@ -5787,7 +5811,7 @@ void HistoryWidget::onKbToggle(bool manual) { } void HistoryWidget::onCmdStart() { - setFieldText(qsl("/")); + setFieldText({ qsl("/"), TextWithTags::Tags() }, 0, FlatTextarea::AddToUndoHistory); } void HistoryWidget::contextMenuEvent(QContextMenuEvent *e) { @@ -6998,7 +7022,7 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) { } else if (e->key() == Qt::Key_Up) { if (!(e->modifiers() & (Qt::ShiftModifier | Qt::MetaModifier | Qt::ControlModifier))) { if (_history && _history->lastSentMsg && _history->lastSentMsg->canEdit(::date(unixtime()))) { - if (_field.getLastText().isEmpty() && !_editMsgId && !_replyToId) { + if (_field.isEmpty() && !_editMsgId && !_replyToId) { App::contextItem(_history->lastSentMsg); onEditMessage(); } @@ -7308,11 +7332,11 @@ void HistoryWidget::sendExistingPhoto(PhotoData *photo, const QString &caption) _field.setFocus(); } -void HistoryWidget::setFieldText(const QString &text, int32 textUpdateEventsFlags, bool clearUndoHistory) { - _textUpdateEventsFlags = textUpdateEventsFlags; - _field.setTextFast(text, clearUndoHistory); +void HistoryWidget::setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events, FlatTextarea::UndoHistoryAction undoHistoryAction) { + _textUpdateEvents = events; + _field.setTextWithTags(textWithTags, undoHistoryAction); _field.moveCursor(QTextCursor::End); - _textUpdateEventsFlags = TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping; + _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; _previewCancelled = false; _previewData = nullptr; @@ -7351,7 +7375,7 @@ void HistoryWidget::onReplyToMessage() { if (auto msgDraft = _history->msgDraft()) { msgDraft->msgId = to->id; } else { - _history->setMsgDraft(std_::make_unique(QString(), to->id, MessageCursor(), false)); + _history->setMsgDraft(std_::make_unique(TextWithTags(), to->id, MessageCursor(), false)); } } else { _replyEditMsg = to; @@ -7384,14 +7408,17 @@ void HistoryWidget::onEditMessage() { } else { delete box; - if (_replyToId || !_field.getLastText().isEmpty()) { + if (_replyToId || !_field.isEmpty()) { _history->setMsgDraft(std_::make_unique(_field, _replyToId, _previewCancelled)); } else { _history->clearMsgDraft(); } - QString text(textApplyEntities(to->originalText(), to->originalEntities())); - _history->setEditDraft(std_::make_unique(text, to->id, MessageCursor(text.size(), text.size(), QFIXED_MAX), false)); + auto originalText = to->originalText(); + auto originalEntities = to->originalEntities(); + TextWithTags original = { textApplyEntities(originalText, originalEntities), textTagsFromEntities(originalEntities) }; + MessageCursor cursor = { original.text.size(), original.text.size(), QFIXED_MAX }; + _history->setEditDraft(std_::make_unique(original, to->id, cursor, false)); applyDraft(false); _previewData = 0; @@ -7510,7 +7537,7 @@ void HistoryWidget::cancelReply(bool lastKeyboardUsed) { update(); } else if (auto msgDraft = (_history ? _history->msgDraft() : nullptr)) { if (msgDraft->msgId) { - if (msgDraft->text.isEmpty()) { + if (msgDraft->textWithTags.text.isEmpty()) { _history->clearMsgDraft(); } else { msgDraft->msgId = 0; @@ -7533,7 +7560,7 @@ void HistoryWidget::cancelEdit() { if (!_editMsgId) return; _editMsgId = 0; - _replyEditMsg = 0; + _replyEditMsg = nullptr; _history->clearEditDraft(); applyDraft(); @@ -7546,21 +7573,21 @@ void HistoryWidget::cancelEdit() { _saveDraftStart = getms(); onDraftSave(); - mouseMoveEvent(0); + mouseMoveEvent(nullptr); if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !replyToId()) { _fieldBarCancel.hide(); updateMouseTracking(); } - int32 old = _textUpdateEventsFlags; - _textUpdateEventsFlags = 0; + auto old = _textUpdateEvents; + _textUpdateEvents = 0; onTextChange(); - _textUpdateEventsFlags = old; + _textUpdateEvents = old; updateBotKeyboard(); updateFieldPlaceholder(); - resizeEvent(0); + resizeEvent(nullptr); update(); } @@ -7728,7 +7755,10 @@ void HistoryWidget::onCancel() { if (_inlineBotCancel) { onInlineBotCancel(); } else if (_editMsgId) { - if (_replyEditMsg && textApplyEntities(_replyEditMsg->originalText(), _replyEditMsg->originalEntities()) != _field.getLastText()) { + auto originalText = _replyEditMsg ? _replyEditMsg->originalText() : QString(); + auto originalEntities = _replyEditMsg ? _replyEditMsg->originalEntities() : EntitiesInText(); + TextWithTags original = { textApplyEntities(originalText, originalEntities), textTagsFromEntities(originalEntities) }; + if (_replyEditMsg && original != _field.getTextWithTags()) { auto box = new ConfirmBox(lang(lng_cancel_edit_post_sure), lang(lng_cancel_edit_post_yes), st::defaultBoxButton, lang(lng_cancel_edit_post_no)); connect(box, SIGNAL(confirmed()), this, SLOT(onFieldBarCancel())); Ui::showLayer(box); diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index 189afb9e4..76093067c 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -486,12 +486,8 @@ public: }; -EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags); - -enum TextUpdateEventsFlags { - TextUpdateEventsSaveDraft = 0x01, - TextUpdateEventsSendTyping = 0x02, -}; +EntitiesInText entitiesFromTextTags(const TextWithTags::Tags &tags); +TextWithTags::Tags textTagsFromEntities(const EntitiesInText &entities); class HistoryWidget : public TWidget, public RPCSender { Q_OBJECT @@ -954,11 +950,18 @@ private: void savedGifsGot(const MTPmessages_SavedGifs &gifs); bool savedGifsFailed(const RPCError &error); + enum class TextUpdateEvent { + SaveDraft = 0x01, + SendTyping = 0x02, + }; + Q_DECLARE_FLAGS(TextUpdateEvents, TextUpdateEvent); + Q_DECLARE_FRIEND_OPERATORS_FOR_FLAGS(TextUpdateEvents); + void writeDrafts(HistoryDraft **msgDraft, HistoryEditDraft **editDraft); void writeDrafts(History *history); - void setFieldText(const QString &text, int32 textUpdateEventsFlags = 0, bool clearUndoHistory = true); - void clearFieldText(int32 textUpdateEventsFlags = 0, bool clearUndoHistory = true) { - setFieldText(QString()); + void setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events = 0, FlatTextarea::UndoHistoryAction undoHistoryAction = FlatTextarea::ClearUndoHistory); + void clearFieldText(TextUpdateEvents events = 0, FlatTextarea::UndoHistoryAction undoHistoryAction = FlatTextarea::ClearUndoHistory) { + setFieldText(TextWithTags(), events, undoHistoryAction); } QStringList getMediasFromMime(const QMimeData *d); @@ -1062,7 +1065,7 @@ private: int32 _selCount; // < 0 - text selected, focus list, not _field TaskQueue _fileLoader; - int32 _textUpdateEventsFlags = (TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping); + TextUpdateEvents _textUpdateEvents = (TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping); int64 _serviceImageCacheSize = 0; QString _confirmSource; @@ -1095,3 +1098,4 @@ private: }; +Q_DECLARE_OPERATORS_FOR_FLAGS(HistoryWidget::TextUpdateEvents) diff --git a/Telegram/SourceFiles/localstorage.cpp b/Telegram/SourceFiles/localstorage.cpp index 1aee7a285..8df89e4c0 100644 --- a/Telegram/SourceFiles/localstorage.cpp +++ b/Telegram/SourceFiles/localstorage.cpp @@ -122,14 +122,6 @@ namespace { return true; } - uint32 _dateTimeSize() { - return (sizeof(qint64) + sizeof(quint32) + sizeof(qint8)); - } - - uint32 _bytearraySize(const QByteArray &arr) { - return sizeof(quint32) + arr.size(); - } - QByteArray _settingsSalt, _passKeySalt, _passKeyEncrypted; MTP::AuthKey _oldKey, _settingsKey, _passKey, _localKey; @@ -628,18 +620,18 @@ namespace { size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(i.value().name()); if (AppVersion > 9013) { // bookmark - size += _bytearraySize(i.value().bookmark()); + size += Serialize::bytearraySize(i.value().bookmark()); } // date + size - size += _dateTimeSize() + sizeof(quint32); + size += Serialize::dateTimeSize() + sizeof(quint32); } //end mark size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(QString()); if (AppVersion > 9013) { - size += _bytearraySize(QByteArray()); + size += Serialize::bytearraySize(QByteArray()); } - size += _dateTimeSize() + sizeof(quint32); + size += Serialize::dateTimeSize() + sizeof(quint32); size += sizeof(quint32); // aliases count for (FileLocationAliases::const_iterator i = _fileLocationAliases.cbegin(), e = _fileLocationAliases.cend(); i != e; ++i) { @@ -1530,7 +1522,7 @@ namespace { } uint32 size = 16 * (sizeof(quint32) + sizeof(qint32)); - size += sizeof(quint32) + Serialize::stringSize(cAskDownloadPath() ? QString() : cDownloadPath()) + _bytearraySize(cAskDownloadPath() ? QByteArray() : cDownloadPathBookmark()); + size += sizeof(quint32) + Serialize::stringSize(cAskDownloadPath() ? QString() : cDownloadPath()) + Serialize::bytearraySize(cAskDownloadPath() ? QByteArray() : cDownloadPathBookmark()); size += sizeof(quint32) + sizeof(qint32) + (cRecentEmojisPreload().isEmpty() ? cGetRecentEmojis().size() : cRecentEmojisPreload().size()) * (sizeof(uint64) + sizeof(ushort)); size += sizeof(quint32) + sizeof(qint32) + cEmojiVariants().size() * (sizeof(uint32) + sizeof(uint64)); size += sizeof(quint32) + sizeof(qint32) + (cRecentStickersPreload().isEmpty() ? cGetRecentStickers().size() : cRecentStickersPreload().size()) * (sizeof(uint64) + sizeof(ushort)); @@ -2281,8 +2273,8 @@ namespace Local { void writeDrafts(const PeerId &peer, const MessageDraft &msgDraft, const MessageDraft &editDraft) { if (!_working()) return; - if (msgDraft.msgId <= 0 && msgDraft.text.isEmpty() && editDraft.msgId <= 0) { - DraftsMap::iterator i = _draftsMap.find(peer); + if (msgDraft.msgId <= 0 && msgDraft.textWithTags.text.isEmpty() && editDraft.msgId <= 0) { + auto i = _draftsMap.find(peer); if (i != _draftsMap.cend()) { clearKey(i.value()); _draftsMap.erase(i); @@ -2292,17 +2284,26 @@ namespace Local { _draftsNotReadMap.remove(peer); } else { - DraftsMap::const_iterator i = _draftsMap.constFind(peer); + auto i = _draftsMap.constFind(peer); if (i == _draftsMap.cend()) { i = _draftsMap.insert(peer, genKey()); _mapChanged = true; _writeMap(WriteMapFast); } - EncryptedDescriptor data(sizeof(quint64) + Serialize::stringSize(msgDraft.text) + 2 * sizeof(qint32) + Serialize::stringSize(editDraft.text) + 2 * sizeof(qint32)); + auto msgTags = FlatTextarea::serializeTagsList(msgDraft.textWithTags.tags); + auto editTags = FlatTextarea::serializeTagsList(editDraft.textWithTags.tags); + + int size = sizeof(quint64); + size += Serialize::stringSize(msgDraft.textWithTags.text) + Serialize::bytearraySize(msgTags) + 2 * sizeof(qint32); + size += Serialize::stringSize(editDraft.textWithTags.text) + Serialize::bytearraySize(editTags) + 2 * sizeof(qint32); + + EncryptedDescriptor data(size); data.stream << quint64(peer); - data.stream << msgDraft.text << qint32(msgDraft.msgId) << qint32(msgDraft.previewCancelled ? 1 : 0); - data.stream << editDraft.text << qint32(editDraft.msgId) << qint32(editDraft.previewCancelled ? 1 : 0); + data.stream << msgDraft.textWithTags.text << msgTags; + data.stream << qint32(msgDraft.msgId) << qint32(msgDraft.previewCancelled ? 1 : 0); + data.stream << editDraft.textWithTags.text << editTags; + data.stream << qint32(editDraft.msgId) << qint32(editDraft.previewCancelled ? 1 : 0); FileWriteDescriptor file(i.value()); file.writeEncrypted(data); @@ -2370,15 +2371,23 @@ namespace Local { } quint64 draftPeer = 0; - QString msgText, editText; + TextWithTags msgData, editData; + QByteArray msgTagsSerialized, editTagsSerialized; qint32 msgReplyTo = 0, msgPreviewCancelled = 0, editMsgId = 0, editPreviewCancelled = 0; - draft.stream >> draftPeer >> msgText; + draft.stream >> draftPeer >> msgData.text; + if (draft.version >= 9048) { + draft.stream >> msgTagsSerialized; + } if (draft.version >= 7021) { draft.stream >> msgReplyTo; if (draft.version >= 8001) { draft.stream >> msgPreviewCancelled; if (!draft.stream.atEnd()) { - draft.stream >> editText >> editMsgId >> editPreviewCancelled; + draft.stream >> editData.text; + if (draft.version >= 9048) { + draft.stream >> editTagsSerialized; + } + draft.stream >> editMsgId >> editPreviewCancelled; } } } @@ -2389,18 +2398,21 @@ namespace Local { return; } + msgData.tags = FlatTextarea::deserializeTagsList(msgTagsSerialized, msgData.text.size()); + editData.tags = FlatTextarea::deserializeTagsList(editTagsSerialized, editData.text.size()); + MessageCursor msgCursor, editCursor; _readDraftCursors(peer, msgCursor, editCursor); - if (msgText.isEmpty() && !msgReplyTo) { + if (msgData.text.isEmpty() && !msgReplyTo) { h->clearMsgDraft(); } else { - h->setMsgDraft(std_::make_unique(msgText, msgReplyTo, msgCursor, msgPreviewCancelled)); + h->setMsgDraft(std_::make_unique(msgData, msgReplyTo, msgCursor, msgPreviewCancelled)); } if (!editMsgId) { h->clearEditDraft(); } else { - h->setEditDraft(std_::make_unique(editText, editMsgId, editCursor, editPreviewCancelled)); + h->setEditDraft(std_::make_unique(editData, editMsgId, editCursor, editPreviewCancelled)); } } @@ -3019,7 +3031,7 @@ namespace Local { } else { int32 setsCount = 0; QByteArray hashToWrite; - quint32 size = sizeof(quint32) + _bytearraySize(hashToWrite); + quint32 size = sizeof(quint32) + Serialize::bytearraySize(hashToWrite); for (auto i = sets.cbegin(); i != sets.cend(); ++i) { bool notLoaded = (i->flags & MTPDstickerSet_ClientFlag::f_not_loaded); if (notLoaded) { @@ -3682,7 +3694,7 @@ namespace Local { } quint32 size = sizeof(quint32); for (SavedPeers::const_iterator i = saved.cbegin(); i != saved.cend(); ++i) { - size += _peerSize(i.key()) + _dateTimeSize(); + size += _peerSize(i.key()) + Serialize::dateTimeSize(); } EncryptedDescriptor data(size); diff --git a/Telegram/SourceFiles/localstorage.h b/Telegram/SourceFiles/localstorage.h index 10264515a..ee5e8ba6e 100644 --- a/Telegram/SourceFiles/localstorage.h +++ b/Telegram/SourceFiles/localstorage.h @@ -105,11 +105,15 @@ namespace Local { int32 oldSettingsVersion(); + using TextWithTags = FlatTextarea::TextWithTags; struct MessageDraft { - MessageDraft(MsgId msgId = 0, QString text = QString(), bool previewCancelled = false) : msgId(msgId), text(text), previewCancelled(previewCancelled) { + MessageDraft(MsgId msgId = 0, TextWithTags textWithTags = TextWithTags(), bool previewCancelled = false) + : msgId(msgId) + , textWithTags(textWithTags) + , previewCancelled(previewCancelled) { } MsgId msgId; - QString text; + TextWithTags textWithTags; bool previewCancelled; }; void writeDrafts(const PeerId &peer, const MessageDraft &msgDraft, const MessageDraft &editDraft); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index e59cae222..a724cec8f 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -159,7 +159,9 @@ bool MainWidget::onShareUrl(const PeerId &peer, const QString &url, const QStrin return false; } History *h = App::history(peer); - h->setMsgDraft(std_::make_unique(url + '\n' + text, 0, MessageCursor(url.size() + 1, url.size() + 1 + text.size(), QFIXED_MAX), false)); + TextWithTags textWithTags = { url + '\n' + text, TextWithTags::Tags() }; + MessageCursor cursor = { url.size() + 1, url.size() + 1 + text.size(), QFIXED_MAX }; + h->setMsgDraft(std_::make_unique(textWithTags, 0, cursor, false)); h->clearEditDraft(); bool opened = _history->peer() && (_history->peer()->id == peer); if (opened) { @@ -177,7 +179,9 @@ bool MainWidget::onInlineSwitchChosen(const PeerId &peer, const QString &botAndQ return false; } History *h = App::history(peer); - h->setMsgDraft(std_::make_unique(botAndQuery, 0, MessageCursor(botAndQuery.size(), botAndQuery.size(), QFIXED_MAX), false)); + TextWithTags textWithTags = { botAndQuery, TextWithTags::Tags() }; + MessageCursor cursor = { botAndQuery.size(), botAndQuery.size(), QFIXED_MAX }; + h->setMsgDraft(std_::make_unique(textWithTags, 0, cursor, false)); h->clearEditDraft(); bool opened = _history->peer() && (_history->peer()->id == peer); if (opened) { @@ -1086,7 +1090,7 @@ void executeParsedCommand(const QString &command) { void MainWidget::sendMessage(const MessageToSend &message) { auto history = message.history; - const auto &text = message.text; + const auto &textWithTags = message.textWithTags; readServerHistory(history, false); _history->fastShowAtEnd(history); @@ -1095,13 +1099,13 @@ void MainWidget::sendMessage(const MessageToSend &message) { return; } - saveRecentHashtags(text); + saveRecentHashtags(textWithTags.text); - EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(message.entities); + EntitiesInText sendingEntities, leftEntities = entitiesFromTextTags(textWithTags.tags); auto prepareFlags = itemTextOptions(history, App::self()).flags; - QString sendingText, leftText = prepareTextWithEntities(text, prepareFlags, &leftEntities); + QString sendingText, leftText = prepareTextWithEntities(textWithTags.text, prepareFlags, &leftEntities); - QString command = parseCommandFromMessage(history, text); + QString command = parseCommandFromMessage(history, textWithTags.text); HistoryItem *lastMessage = nullptr; MsgId replyTo = (message.replyTo < 0) ? _history->replyToId() : 0; diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 17af829aa..7da61f427 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -283,8 +283,7 @@ public: struct MessageToSend { History *history = nullptr; - QString text; - FlatTextarea::TagList entities; + TextWithTags textWithTags; MsgId replyTo = 0; bool broadcast = false; bool silent = false; diff --git a/Telegram/SourceFiles/pspecific_mac.cpp b/Telegram/SourceFiles/pspecific_mac.cpp index e1e94fbd2..6ebacc89f 100644 --- a/Telegram/SourceFiles/pspecific_mac.cpp +++ b/Telegram/SourceFiles/pspecific_mac.cpp @@ -86,7 +86,7 @@ void MacPrivate::notifyReplied(unsigned long long peer, int msgid, const char *s MainWidget::MessageToSend message; message.history = history; - message.text = QString::fromUtf8(str); + message.textWithTags = { QString::fromUtf8(str), TextWithTags::Tags() }; message.replyTo = (msgid > 0 && !history->peer->isUser()) ? msgid : 0; message.broadcast = false; message.silent = false; diff --git a/Telegram/SourceFiles/serialize/serialize_common.cpp b/Telegram/SourceFiles/serialize/serialize_common.cpp index 0d7ee09f6..ea9d68e17 100644 --- a/Telegram/SourceFiles/serialize/serialize_common.cpp +++ b/Telegram/SourceFiles/serialize/serialize_common.cpp @@ -23,10 +23,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org namespace Serialize { -int stringSize(const QString &str) { - return sizeof(quint32) + str.size() * sizeof(ushort); -} - void writeStorageImageLocation(QDataStream &stream, const StorageImageLocation &loc) { stream << qint32(loc.width()) << qint32(loc.height()); stream << qint32(loc.dc()) << quint64(loc.volume()) << qint32(loc.local()) << quint64(loc.secret()); diff --git a/Telegram/SourceFiles/serialize/serialize_common.h b/Telegram/SourceFiles/serialize/serialize_common.h index 7412104fe..dc57155f1 100644 --- a/Telegram/SourceFiles/serialize/serialize_common.h +++ b/Telegram/SourceFiles/serialize/serialize_common.h @@ -24,7 +24,17 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org namespace Serialize { -int stringSize(const QString &str); +inline int stringSize(const QString &str) { + return sizeof(quint32) + str.size() * sizeof(ushort); +} + +inline int bytearraySize(const QByteArray &arr) { + return sizeof(quint32) + arr.size(); +} + +inline int dateTimeSize() { + return (sizeof(qint64) + sizeof(quint32) + sizeof(qint8)); +} void writeStorageImageLocation(QDataStream &stream, const StorageImageLocation &loc); StorageImageLocation readStorageImageLocation(QDataStream &stream); diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp index 3e7b4c4ca..9486b8dbf 100644 --- a/Telegram/SourceFiles/ui/flattextarea.cpp +++ b/Telegram/SourceFiles/ui/flattextarea.cpp @@ -23,9 +23,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "mainwindow.h" -namespace { - -QByteArray serializeTagsList(const FlatTextarea::TagList &tags) { +QByteArray FlatTextarea::serializeTagsList(const TagList &tags) { if (tags.isEmpty()) { return QByteArray(); } @@ -44,8 +42,11 @@ QByteArray serializeTagsList(const FlatTextarea::TagList &tags) { return tagsSerialized; } -FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) { - FlatTextarea::TagList result; +FlatTextarea::TagList FlatTextarea::deserializeTagsList(QByteArray data, int textLength) { + TagList result; + if (data.isEmpty()) { + return result; + } QBuffer buffer(&data); buffer.open(QIODevice::ReadOnly); @@ -58,7 +59,7 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) { if (stream.status() != QDataStream::Ok) { return result; } - if (tagCount <= 0 || tagCount > textSize) { + if (tagCount <= 0 || tagCount > textLength) { return result; } @@ -69,7 +70,7 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) { if (stream.status() != QDataStream::Ok) { return result; } - if (offset < 0 || length <= 0 || offset + length > textSize) { + if (offset < 0 || length <= 0 || offset + length > textLength) { return result; } result.push_back({ offset, length, id }); @@ -77,12 +78,12 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) { return result; } -constexpr str_const TagsMimeType = "application/x-td-field-tags"; +QString FlatTextarea::tagsMimeType() { + return qsl("application/x-td-field-tags"); +} -} // namespace - -FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &pholder, const QString &v) : QTextEdit(parent) -, _oldtext(v) +FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &pholder, const QString &v, const TagList &tags) : QTextEdit(parent) +, _lastTextWithTags { v, tags } , _phVisible(!v.length()) , a_phLeft(_phVisible ? 0 : st.phShift) , a_phAlpha(_phVisible ? 1 : 0) @@ -126,22 +127,41 @@ FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool))); if (App::wnd()) connect(this, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu())); - if (!v.isEmpty()) { - setTextFast(v); + if (!_lastTextWithTags.text.isEmpty()) { + setTextWithTags(_lastTextWithTags, ClearUndoHistory); } } -void FlatTextarea::setTextFast(const QString &text, bool clearUndoHistory) { - if (clearUndoHistory) { - setPlainText(text); +FlatTextarea::TextWithTags FlatTextarea::getTextWithTagsPart(int start, int end) { + TextWithTags result; + result.text = getTextPart(start, end, &result.tags); + return result; +} + +void FlatTextarea::setTextWithTags(const TextWithTags &textWithTags, UndoHistoryAction undoHistoryAction) { + _insertedTags = textWithTags.tags; + _insertedTagsAreFromMime = false; + _realInsertPosition = 0; + _realCharsAdded = textWithTags.text.size(); + auto doc = document(); + auto cursor = QTextCursor(doc->docHandle(), 0); + if (undoHistoryAction == ClearUndoHistory) { + doc->setUndoRedoEnabled(false); + cursor.beginEditBlock(); + } else if (undoHistoryAction == MergeWithUndoHistory) { + cursor.joinPreviousEditBlock(); } else { - QTextCursor c(document()->docHandle(), 0); - c.joinPreviousEditBlock(); - c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); - c.insertText(text); - c.movePosition(QTextCursor::End); - c.endEditBlock(); + cursor.beginEditBlock(); } + cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); + cursor.insertText(textWithTags.text); + cursor.movePosition(QTextCursor::End); + cursor.endEditBlock(); + if (undoHistoryAction == ClearUndoHistory) { + doc->setUndoRedoEnabled(true); + } + _insertedTags.clear(); + _realInsertPosition = -1; finishPlaceholder(); } @@ -266,7 +286,8 @@ void FlatTextarea::paintEvent(QPaintEvent *e) { p.setFont(_st.font); p.setPen(a_phColor.current()); if (_st.phAlign == style::al_topleft && _phAfter > 0) { - p.drawText(_st.textMrg.left() - _fakeMargin + a_phLeft.current() + _st.font->width(getLastText().mid(0, _phAfter)), _st.textMrg.top() - _fakeMargin - st::lineWidth + _st.font->ascent, _ph); + int skipWidth = _st.font->width(getTextWithTags().text.mid(0, _phAfter)); + p.drawText(_st.textMrg.left() - _fakeMargin + a_phLeft.current() + skipWidth, _st.textMrg.top() - _fakeMargin - st::lineWidth + _st.font->ascent, _ph); } else { QRect phRect(_st.textMrg.left() - _fakeMargin + _st.phPos.x() + a_phLeft.current(), _st.textMrg.top() - _fakeMargin + _st.phPos.y(), width() - _st.textMrg.left() - _st.textMrg.right(), height() - _st.textMrg.top() - _st.textMrg.bottom()); p.drawText(phRect, _ph, QTextOption(_st.phAlign)); @@ -317,7 +338,7 @@ QString FlatTextarea::getInlineBotQuery(UserData **outInlineBot, QString *outInl t_assert(outInlineBot != nullptr); t_assert(outInlineBotUsername != nullptr); - const QString &text(getLastText()); + auto &text = getTextWithTags().text; int32 inlineUsernameStart = 1, inlineUsernameLength = 0, size = text.size(); if (size > 2 && text.at(0) == '@' && text.at(1).isLetter()) { @@ -474,10 +495,8 @@ void FlatTextarea::insertTag(const QString &text, QString tagId) { cursor.insertText(text + ' ', format); } else { _insertedTags.clear(); - if (_tagMimeProcessor) { - tagId = _tagMimeProcessor->mimeTagFromTag(tagId); - } _insertedTags.push_back({ 0, text.size(), tagId }); + _insertedTagsAreFromMime = false; cursor.insertText(text + ' '); _insertedTags.clear(); } @@ -601,7 +620,7 @@ private: } // namespace -QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const { +QString FlatTextarea::getTextPart(int start, int end, TagList *outTagsList, bool *outTagsChanged) const { if (end >= 0 && end <= start) return QString(); if (start < 0) start = 0; @@ -632,7 +651,7 @@ QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *ou int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length()); if (!full) { tillFragmentEnd = (e <= end); - if (p == end && outTagsList) { + if (p == end) { tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); } if (p >= end) { @@ -642,7 +661,7 @@ QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *ou continue; } } - if (outTagsList && (full || p >= start)) { + if (full || p >= start) { tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); } @@ -689,11 +708,9 @@ QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *ou } result.chop(1); - if (outTagsList) { - if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size()); - if (outTagsChanged) { - *outTagsChanged = tagAccumulator.changed(); - } + if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size()); + if (outTagsChanged) { + *outTagsChanged = tagAccumulator.changed(); } return result; } @@ -816,11 +833,12 @@ QStringList FlatTextarea::linksList() const { } void FlatTextarea::insertFromMimeData(const QMimeData *source) { - auto mime = str_const_toString(TagsMimeType); + auto mime = tagsMimeType(); auto text = source->text(); if (source->hasFormat(mime)) { auto tagsData = source->data(mime); _insertedTags = deserializeTagsList(tagsData, text.size()); + _insertedTagsAreFromMime = true; } else { _insertedTags.clear(); } @@ -982,7 +1000,9 @@ void FlatTextarea::processFormatting(int insertPosition, int insertEnd) { auto doc = document(); // Apply inserted tags. - int breakTagOnNotLetterTill = processInsertedTags(doc, insertPosition, insertEnd, _insertedTags, _tagMimeProcessor.get()); + auto insertedTagsProcessor = _insertedTagsAreFromMime ? _tagMimeProcessor.get() : nullptr; + int breakTagOnNotLetterTill = processInsertedTags(doc, insertPosition, insertEnd, + _insertedTags, insertedTagsProcessor); using ActionType = FormattingAction::Type; while (true) { FormattingAction action; @@ -1181,15 +1201,15 @@ void FlatTextarea::onDocumentContentsChanged() { if (_correcting) return; auto tagsChanged = false; - auto curText = getText(0, -1, &_oldtags, &tagsChanged); + auto curText = getTextPart(0, -1, &_lastTextWithTags.tags, &tagsChanged); _correcting = true; - correctValue(_oldtext, curText, _oldtags); + correctValue(_lastTextWithTags.text, curText, _lastTextWithTags.tags); _correcting = false; - bool textOrTagsChanged = tagsChanged || (_oldtext != curText); + bool textOrTagsChanged = tagsChanged || (_lastTextWithTags.text != curText); if (textOrTagsChanged) { - _oldtext = curText; + _lastTextWithTags.text = curText; emit changed(); checkContentHeight(); } @@ -1231,12 +1251,16 @@ void FlatTextarea::setPlaceholder(const QString &ph, int32 afterSymbols) { _phAfter = afterSymbols; updatePlaceholder(); } - _phelided = _st.font->elided(_ph, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1 - (_phAfter ? _st.font->width(getLastText().mid(0, _phAfter)) : 0)); + int skipWidth = 0; + if (_phAfter) { + skipWidth = _st.font->width(getTextWithTags().text.mid(0, _phAfter)); + } + _phelided = _st.font->elided(_ph, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1 - skipWidth); if (_phVisible) update(); } void FlatTextarea::updatePlaceholder() { - bool vis = (getLastText().size() <= _phAfter); + bool vis = (getTextWithTags().text.size() <= _phAfter); if (vis == _phVisible) return; a_phLeft.start(vis ? 0 : _st.phShift); @@ -1252,14 +1276,14 @@ QMimeData *FlatTextarea::createMimeDataFromSelection() const { int32 start = c.selectionStart(), end = c.selectionEnd(); if (end > start) { TagList tags; - result->setText(getText(start, end, &tags, nullptr)); + result->setText(getTextPart(start, end, &tags)); if (!tags.isEmpty()) { if (_tagMimeProcessor) { for (auto &tag : tags) { tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id); } } - result->setData(str_const_toString(TagsMimeType), serializeTagsList(tags)); + result->setData(tagsMimeType(), serializeTagsList(tags)); } } return result; diff --git a/Telegram/SourceFiles/ui/flattextarea.h b/Telegram/SourceFiles/ui/flattextarea.h index aac48d4df..8315022ea 100644 --- a/Telegram/SourceFiles/ui/flattextarea.h +++ b/Telegram/SourceFiles/ui/flattextarea.h @@ -31,16 +31,27 @@ class FlatTextarea : public QTextEdit { public: - FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &ph = QString(), const QString &val = QString()); + struct Tag { + int offset, length; + QString id; + }; + using TagList = QVector; + struct TextWithTags { + using Tags = FlatTextarea::TagList; + QString text; + Tags tags; + }; + + static QByteArray serializeTagsList(const TagList &tags); + static TagList deserializeTagsList(QByteArray data, int textLength); + static QString tagsMimeType(); + + FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &ph = QString(), const QString &val = QString(), const TagList &tags = TagList()); void setMaxLength(int32 maxLength); void setMinHeight(int32 minHeight); void setMaxHeight(int32 maxHeight); - const QString &getLastText() const { - return _oldtext; - } - void setPlaceholder(const QString &ph, int32 afterSymbols = 0); void updatePlaceholder(); void finishPlaceholder(); @@ -82,18 +93,23 @@ public: }; void setSubmitSettings(SubmitSettings settings); - void setTextFast(const QString &text, bool clearUndoHistory = true); - - struct Tag { - int offset, length; - QString id; - }; - using TagList = QVector; - const TagList &getLastTags() const { - return _oldtags; + const TextWithTags &getTextWithTags() const { + return _lastTextWithTags; } + TextWithTags getTextWithTagsPart(int start, int end = -1); void insertTag(const QString &text, QString tagId = QString()); + bool isEmpty() const { + return _lastTextWithTags.text.isEmpty(); + } + + enum UndoHistoryAction { + AddToUndoHistory, + MergeWithUndoHistory, + ClearUndoHistory + }; + void setTextWithTags(const TextWithTags &textWithTags, UndoHistoryAction undoHistoryAction = AddToUndoHistory); + // If you need to make some preparations of tags before putting them to QMimeData // (and then to clipboard or to drag-n-drop object), here is a strategy for that. class TagMimeProcessor { @@ -147,9 +163,9 @@ protected: private: - // "start" and "end" are in coordinates of text where emoji are replaced by ObjectReplacementCharacter. - // If "end" = -1 means get text till the end. "outTagsList" and "outTagsChanged" may be nullptr. - QString getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const; + // "start" and "end" are in coordinates of text where emoji are replaced + // by ObjectReplacementCharacter. If "end" = -1 means get text till the end. + QString getTextPart(int start, int end, TagList *outTagsList, bool *outTagsChanged = nullptr) const; void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const; @@ -169,8 +185,7 @@ private: int _maxLength = -1; SubmitSettings _submitSettings = SubmitSettings::Enter; - QString _ph, _phelided, _oldtext; - TagList _oldtags; + QString _ph, _phelided; int _phAfter = 0; bool _phVisible; anim::ivalue a_phLeft; @@ -178,8 +193,11 @@ private: anim::cvalue a_phColor; Animation _a_appearance; + TextWithTags _lastTextWithTags; + // Tags list which we should apply while setText() call or insert from mime data. TagList _insertedTags; + bool _insertedTagsAreFromMime; // Override insert position and charsAdded from complex text editing // (like drag-n-drop in the same text edit field). @@ -222,6 +240,13 @@ inline bool operator!=(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) { return !(a == b); } +inline bool operator==(const FlatTextarea::TextWithTags &a, const FlatTextarea::TextWithTags &b) { + return (a.text == b.text) && (a.tags == b.tags); +} +inline bool operator!=(const FlatTextarea::TextWithTags &a, const FlatTextarea::TextWithTags &b) { + return !(a == b); +} + inline bool operator==(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) { return (a.start == b.start) && (a.length == b.length); } diff --git a/Telegram/Telegram.xcodeproj/project.pbxproj b/Telegram/Telegram.xcodeproj/project.pbxproj index 41b3df598..9bcf3780d 100644 --- a/Telegram/Telegram.xcodeproj/project.pbxproj +++ b/Telegram/Telegram.xcodeproj/project.pbxproj @@ -2023,7 +2023,7 @@ SDKROOT = macosx; SYMROOT = ./../Mac; TDESKTOP_MAJOR_VERSION = 0.9; - TDESKTOP_VERSION = 0.9.47; + TDESKTOP_VERSION = 0.9.48; }; name = Release; }; @@ -2164,7 +2164,7 @@ SDKROOT = macosx; SYMROOT = ./../Mac; TDESKTOP_MAJOR_VERSION = 0.9; - TDESKTOP_VERSION = 0.9.47; + TDESKTOP_VERSION = 0.9.48; }; name = Debug; }; diff --git a/Telegram/build/version b/Telegram/build/version index b2d958b19..406870c55 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,6 +1,6 @@ -AppVersion 9047 +AppVersion 9048 AppVersionStrMajor 0.9 -AppVersionStrSmall 0.9.47 -AppVersionStr 0.9.47 +AppVersionStrSmall 0.9.48 +AppVersionStr 0.9.48 AlphaChannel 1 BetaVersion 0