Saving FlatTextarea tags to drafts, applying them in setText.

Now instead of plain text a TextWithTags struct is used almost
everywhere. Started writing and reading serialized tags to drafts
from 9048, switched version to 0.9.48 for testing.
This commit is contained in:
John Preston 2016-05-05 19:04:17 +03:00
parent 5a47d8e29b
commit 463450e607
18 changed files with 324 additions and 205 deletions

View File

@ -34,8 +34,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 0,9,47,0 FILEVERSION 0,9,48,0
PRODUCTVERSION 0,9,47,0 PRODUCTVERSION 0,9,48,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -51,10 +51,10 @@ BEGIN
BLOCK "040904b0" BLOCK "040904b0"
BEGIN BEGIN
VALUE "CompanyName", "Telegram Messenger LLP" VALUE "CompanyName", "Telegram Messenger LLP"
VALUE "FileVersion", "0.9.47.0" VALUE "FileVersion", "0.9.48.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2016" VALUE "LegalCopyright", "Copyright (C) 2014-2016"
VALUE "ProductName", "Telegram Desktop" VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "0.9.47.0" VALUE "ProductVersion", "0.9.48.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View File

@ -25,8 +25,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 0,9,47,0 FILEVERSION 0,9,48,0
PRODUCTVERSION 0,9,47,0 PRODUCTVERSION 0,9,48,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -43,10 +43,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Telegram Messenger LLP" VALUE "CompanyName", "Telegram Messenger LLP"
VALUE "FileDescription", "Telegram Updater" VALUE "FileDescription", "Telegram Updater"
VALUE "FileVersion", "0.9.47.0" VALUE "FileVersion", "0.9.48.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2016" VALUE "LegalCopyright", "Copyright (C) 2014-2016"
VALUE "ProductName", "Telegram Desktop" VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "0.9.47.0" VALUE "ProductVersion", "0.9.48.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View File

@ -166,6 +166,16 @@ template <typename T, size_t N> char(&ArraySizeHelper(T(&array)[N]))[N];
#define qsl(s) QStringLiteral(s) #define qsl(s) QStringLiteral(s)
#define qstr(s) QLatin1String(s, sizeof(s) - 1) #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<Flags::enum_type> operator|(Flags::enum_type f1, Flags::enum_type f2) Q_DECL_NOTHROW; \
friend QFlags<Flags::enum_type> operator|(Flags::enum_type f1, QFlags<Flags::enum_type> 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 // 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 // it is important for the copy-on-write Qt containers
// if you have "QVector<T*> v" then "for (T * const p : v)" will still call QVector::detach(), // if you have "QVector<T*> v" then "for (T * const p : v)" will still call QVector::detach(),

View File

@ -24,7 +24,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
#define BETA_VERSION_MACRO (0ULL) #define BETA_VERSION_MACRO (0ULL)
constexpr int AppVersion = 9047; constexpr int AppVersion = 9048;
constexpr str_const AppVersionStr = "0.9.47"; constexpr str_const AppVersionStr = "0.9.48";
constexpr bool AppAlphaVersion = true; constexpr bool AppAlphaVersion = true;
constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO; constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO;

View File

@ -151,22 +151,23 @@ struct SendAction {
int32 progress; int32 progress;
}; };
using TextWithTags = FlatTextarea::TextWithTags;
struct HistoryDraft { struct HistoryDraft {
HistoryDraft() : msgId(0), previewCancelled(false) { HistoryDraft() : msgId(0), previewCancelled(false) {
} }
HistoryDraft(const QString &text, MsgId msgId, const MessageCursor &cursor, bool previewCancelled) HistoryDraft(const TextWithTags &textWithTags, MsgId msgId, const MessageCursor &cursor, bool previewCancelled)
: text(text) : textWithTags(textWithTags)
, msgId(msgId) , msgId(msgId)
, cursor(cursor) , cursor(cursor)
, previewCancelled(previewCancelled) { , previewCancelled(previewCancelled) {
} }
HistoryDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled) HistoryDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled)
: text(field.getLastText()) : textWithTags(field.getTextWithTags())
, msgId(msgId) , msgId(msgId)
, cursor(field) , cursor(field)
, previewCancelled(previewCancelled) { , previewCancelled(previewCancelled) {
} }
QString text; TextWithTags textWithTags;
MsgId msgId; // replyToId for message draft, editMsgId for edit draft MsgId msgId; // replyToId for message draft, editMsgId for edit draft
MessageCursor cursor; MessageCursor cursor;
bool previewCancelled; bool previewCancelled;
@ -176,8 +177,8 @@ struct HistoryEditDraft : public HistoryDraft {
: HistoryDraft() : HistoryDraft()
, saveRequest(0) { , saveRequest(0) {
} }
HistoryEditDraft(const QString &text, MsgId msgId, const MessageCursor &cursor, bool previewCancelled, mtpRequestId saveRequest = 0) HistoryEditDraft(const TextWithTags &textWithTags, MsgId msgId, const MessageCursor &cursor, bool previewCancelled, mtpRequestId saveRequest = 0)
: HistoryDraft(text, msgId, cursor, previewCancelled) : HistoryDraft(textWithTags, msgId, cursor, previewCancelled)
, saveRequest(saveRequest) { , saveRequest(saveRequest) {
} }
HistoryEditDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled, mtpRequestId saveRequest = 0) HistoryEditDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled, mtpRequestId saveRequest = 0)
@ -373,7 +374,7 @@ public:
} }
void takeMsgDraft(History *from) { void takeMsgDraft(History *from) {
if (auto &draft = from->_msgDraft) { if (auto &draft = from->_msgDraft) {
if (!draft->text.isEmpty() && !_msgDraft) { if (!draft->textWithTags.text.isEmpty() && !_msgDraft) {
_msgDraft = std_::move(draft); _msgDraft = std_::move(draft);
_msgDraft->msgId = 0; // edit and reply to drafts can't migrate _msgDraft->msgId = 0; // edit and reply to drafts can't migrate
} }

View File

@ -2053,8 +2053,8 @@ MessageField::MessageField(HistoryWidget *history, const style::flatTextarea &st
} }
bool MessageField::hasSendText() const { bool MessageField::hasSendText() const {
const QString &text(getLastText()); auto &text(getTextWithTags().text);
for (const QChar *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) { for (auto *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) {
ushort code = ch->unicode(); ushort code = ch->unicode();
if (code != ' ' && code != '\n' && code != '\r' && !chReplacedBySpace(code)) { if (code != ' ' && code != '\n' && code != '\r' && !chReplacedBySpace(code)) {
return true; return true;
@ -2735,7 +2735,7 @@ QPoint SilentToggle::tooltipPos() const {
return QCursor::pos(); return QCursor::pos();
} }
EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) { EntitiesInText entitiesFromTextTags(const FlatTextarea::TagList &tags) {
EntitiesInText result; EntitiesInText result;
if (tags.isEmpty()) { if (tags.isEmpty()) {
return result; return result;
@ -2754,6 +2754,24 @@ EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) {
return result; 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 { namespace {
// For mention tags save and validate userId, ignore tags for different userId. // 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. // Send bot command at once, if it was not inserted by pressing Tab.
if (str.at(0) == '/' && method != FieldAutocomplete::ChooseMethod::ByTab) { if (str.at(0) == '/' && method != FieldAutocomplete::ChooseMethod::ByTab) {
App::sendBotCommand(_peer, nullptr, str); App::sendBotCommand(_peer, nullptr, str);
setFieldText(_field.getLastText().mid(_field.textCursor().position())); setFieldText(_field.getTextWithTagsPart(_field.textCursor().position()));
} else { } else {
_field.insertTag(str); _field.insertTag(str);
} }
@ -3022,15 +3040,16 @@ void HistoryWidget::updateInlineBotQuery() {
void HistoryWidget::updateStickersByEmoji() { void HistoryWidget::updateStickersByEmoji() {
int32 len = 0; int32 len = 0;
if (EmojiPtr emoji = emojiFromText(_field.getLastText(), &len)) { auto &text = _field.getTextWithTags().text;
if (_field.getLastText().size() <= len) { if (EmojiPtr emoji = emojiFromText(text, &len)) {
_fieldAutocomplete->showStickers(emoji); if (text.size() > len) {
} else {
len = 0; len = 0;
} else {
_fieldAutocomplete->showStickers(emoji);
} }
} }
if (!len) { if (!len) {
_fieldAutocomplete->showStickers(EmojiPtr(0)); _fieldAutocomplete->showStickers(nullptr);
} }
} }
@ -3039,7 +3058,7 @@ void HistoryWidget::onTextChange() {
updateStickersByEmoji(); updateStickersByEmoji();
if (_peer && (!_peer->isChannel() || _peer->isMegagroup() || !_peer->asChannel()->canPublish() || (!_peer->asChannel()->isBroadcast() && !_broadcast.checked()))) { 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); updateSendAction(_history, SendActionTyping);
} }
} }
@ -3065,13 +3084,13 @@ void HistoryWidget::onTextChange() {
update(); update();
} }
if (!_peer || !(_textUpdateEventsFlags & TextUpdateEventsSaveDraft)) return; if (!_peer || !(_textUpdateEvents.testFlag(TextUpdateEvent::SaveDraft))) return;
_saveDraftText = true; _saveDraftText = true;
onDraftSave(true); onDraftSave(true);
} }
void HistoryWidget::onDraftSaveDelayed() { 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 (!_field.textCursor().anchor() && !_field.textCursor().position() && !_field.verticalScrollBar()->value()) {
if (!Local::hasDraftCursors(_peer->id)) { if (!Local::hasDraftCursors(_peer->id)) {
return; return;
@ -3106,17 +3125,17 @@ void HistoryWidget::writeDrafts(HistoryDraft **msgDraft, HistoryEditDraft **edit
Local::MessageDraft localMsgDraft, localEditDraft; Local::MessageDraft localMsgDraft, localEditDraft;
if (msgDraft) { if (msgDraft) {
if (*msgDraft) { if (*msgDraft) {
localMsgDraft = Local::MessageDraft((*msgDraft)->msgId, (*msgDraft)->text, (*msgDraft)->previewCancelled); localMsgDraft = Local::MessageDraft((*msgDraft)->msgId, (*msgDraft)->textWithTags, (*msgDraft)->previewCancelled);
} }
} else { } else {
localMsgDraft = Local::MessageDraft(_replyToId, _field.getLastText(), _previewCancelled); localMsgDraft = Local::MessageDraft(_replyToId, _field.getTextWithTags(), _previewCancelled);
} }
if (editDraft) { if (editDraft) {
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) { } else if (_editMsgId) {
localEditDraft = Local::MessageDraft(_editMsgId, _field.getLastText(), _previewCancelled); localEditDraft = Local::MessageDraft(_editMsgId, _field.getTextWithTags(), _previewCancelled);
} }
Local::writeDrafts(_peer->id, localMsgDraft, localEditDraft); Local::writeDrafts(_peer->id, localMsgDraft, localEditDraft);
if (_migrated) { if (_migrated) {
@ -3152,11 +3171,11 @@ void HistoryWidget::writeDrafts(History *history) {
Local::MessageDraft localMsgDraft, localEditDraft; Local::MessageDraft localMsgDraft, localEditDraft;
MessageCursor msgCursor, editCursor; MessageCursor msgCursor, editCursor;
if (auto msgDraft = history->msgDraft()) { 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; msgCursor = msgDraft->cursor;
} }
if (auto editDraft = history->editDraft()) { 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; editCursor = editDraft->cursor;
} }
Local::writeDrafts(history->peer->id, localMsgDraft, localEditDraft); Local::writeDrafts(history->peer->id, localMsgDraft, localEditDraft);
@ -3331,8 +3350,9 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(const QString &query) {
} }
bot->botInfo->inlineReturnPeerId = 0; bot->botInfo->inlineReturnPeerId = 0;
History *h = App::history(toPeerId); History *h = App::history(toPeerId);
auto text = '@' + bot->username + ' ' + query; TextWithTags textWithTags = { '@' + bot->username + ' ' + query, TextWithTags::Tags() };
h->setMsgDraft(std_::make_unique<HistoryDraft>(text, 0, MessageCursor(text.size(), text.size(), QFIXED_MAX), false)); MessageCursor cursor = { textWithTags.text.size(), textWithTags.text.size(), QFIXED_MAX };
h->setMsgDraft(std_::make_unique<HistoryDraft>(textWithTags, 0, cursor, false));
if (h == _history) { if (h == _history) {
applyDraft(); applyDraft();
} else { } else {
@ -3633,11 +3653,11 @@ void HistoryWidget::applyDraft(bool parseLinks) {
return; return;
} }
_textUpdateEventsFlags = 0; _textUpdateEvents = 0;
setFieldText(draft->text); setFieldText(draft->textWithTags);
_field.setFocus(); _field.setFocus();
draft->cursor.applyTo(_field); draft->cursor.applyTo(_field);
_textUpdateEventsFlags = TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping; _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;
_previewCancelled = draft->previewCancelled; _previewCancelled = draft->previewCancelled;
if (auto editDraft = _history->editDraft()) { if (auto editDraft = _history->editDraft()) {
_editMsgId = editDraft->msgId; _editMsgId = editDraft->msgId;
@ -3730,7 +3750,7 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re
if (_editMsgId) { if (_editMsgId) {
_history->setEditDraft(std_::make_unique<HistoryEditDraft>(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId)); _history->setEditDraft(std_::make_unique<HistoryEditDraft>(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId));
} else { } else {
if (_replyToId || !_field.getLastText().isEmpty()) { if (_replyToId || !_field.isEmpty()) {
_history->setMsgDraft(std_::make_unique<HistoryDraft>(_field, _replyToId, _previewCancelled)); _history->setMsgDraft(std_::make_unique<HistoryDraft>(_field, _replyToId, _previewCancelled));
} else { } else {
_history->clearMsgDraft(); _history->clearMsgDraft();
@ -4738,11 +4758,11 @@ void HistoryWidget::preloadHistoryIfNeeded() {
} }
void HistoryWidget::onInlineBotCancel() { void HistoryWidget::onInlineBotCancel() {
QString text = _field.getLastText(); auto &textWithTags = _field.getTextWithTags();
if (text.size() > _inlineBotUsername.size() + 2) { if (textWithTags.text.size() > _inlineBotUsername.size() + 2) {
setFieldText('@' + _inlineBotUsername + ' ', TextUpdateEventsSaveDraft, false); setFieldText({ '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
} else { } 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); WebPageId webPageId = _previewCancelled ? CancelledWebPageId : ((_previewData && _previewData->pendingTill >= 0) ? _previewData->id : 0);
auto fieldText = _field.getLastText(); auto &textWithTags = _field.getTextWithTags();
auto fieldTags = _field.getLastTags();
auto prepareFlags = itemTextOptions(_history, App::self()).flags; auto prepareFlags = itemTextOptions(_history, App::self()).flags;
EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(fieldTags); EntitiesInText sendingEntities, leftEntities = entitiesFromTextTags(textWithTags.tags);
QString sendingText, leftText = prepareTextWithEntities(fieldText, prepareFlags, &leftEntities); QString sendingText, leftText = prepareTextWithEntities(textWithTags.text, prepareFlags, &leftEntities);
if (!textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) { if (!textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) {
_field.selectAll(); _field.selectAll();
@ -4865,8 +4884,7 @@ void HistoryWidget::onSend(bool ctrlShiftEnter, MsgId replyTo) {
MainWidget::MessageToSend message; MainWidget::MessageToSend message;
message.history = _history; message.history = _history;
message.text = _field.getLastText(); message.textWithTags = _field.getTextWithTags();
message.entities = _field.getLastTags();
message.replyTo = replyTo; message.replyTo = replyTo;
message.broadcast = _broadcast.checked(); message.broadcast = _broadcast.checked();
message.silent = _silent.checked(); message.silent = _silent.checked();
@ -5379,7 +5397,7 @@ void HistoryWidget::sendBotCommand(PeerData *peer, UserData *bot, const QString
MainWidget::MessageToSend message; MainWidget::MessageToSend message;
message.history = _history; message.history = _history;
message.text = toSend; message.textWithTags = { toSend, TextWithTags::Tags() };
message.replyTo = replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? replyTo : -1) : 0; message.replyTo = replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? replyTo : -1) : 0;
message.broadcast = false; message.broadcast = false;
message.silent = false; message.silent = false;
@ -5460,8 +5478,9 @@ bool HistoryWidget::botCallbackFail(BotCallbackInfo info, const RPCError &error,
bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) { bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) {
if (!_history) return false; if (!_history) return false;
bool insertingInlineBot = !cmd.isEmpty() && (cmd.at(0) == '@');
QString toInsert = cmd; QString toInsert = cmd;
if (!toInsert.isEmpty() && toInsert.at(0) != '@') { if (!toInsert.isEmpty() && !insertingInlineBot) {
PeerData *bot = _peer->isUser() ? _peer : (App::hoveredLinkItem() ? App::hoveredLinkItem()->fromOriginal() : 0); PeerData *bot = _peer->isUser() ? _peer : (App::hoveredLinkItem() ? App::hoveredLinkItem()->fromOriginal() : 0);
if (!bot->isUser() || !bot->asUser()->botInfo) bot = 0; if (!bot->isUser() || !bot->asUser()->botInfo) bot = 0;
QString username = bot ? bot->asUser()->username : QString(); QString username = bot ? bot->asUser()->username : QString();
@ -5472,28 +5491,33 @@ bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) {
} }
toInsert += ' '; toInsert += ' ';
if (toInsert.at(0) != '@') { if (!insertingInlineBot) {
QString text = _field.getLastText(); auto &textWithTags = _field.getTextWithTags();
if (specialGif) { if (specialGif) {
if (text.trimmed() == '@' + cInlineGifBotUsername() && text.at(0) == '@') { if (textWithTags.text.trimmed() == '@' + cInlineGifBotUsername() && textWithTags.text.at(0) == '@') {
clearFieldText(TextUpdateEventsSaveDraft, false); clearFieldText(TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
} }
} else { } 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()) { if (m.hasMatch()) {
text = toInsert + text.mid(m.capturedLength()); textWithTagsToSet = _field.getTextWithTagsPart(m.capturedLength());
} else { } 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()); QTextCursor cur(_field.textCursor());
cur.movePosition(QTextCursor::End); cur.movePosition(QTextCursor::End);
_field.setTextCursor(cur); _field.setTextCursor(cur);
} }
} else { } else {
if (!specialGif || _field.getLastText().isEmpty()) { if (!specialGif || _field.isEmpty()) {
setFieldText(toInsert, TextUpdateEventsSaveDraft, false); setFieldText({ toInsert, TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
_field.setFocus(); _field.setFocus();
return true; return true;
} }
@ -5787,7 +5811,7 @@ void HistoryWidget::onKbToggle(bool manual) {
} }
void HistoryWidget::onCmdStart() { void HistoryWidget::onCmdStart() {
setFieldText(qsl("/")); setFieldText({ qsl("/"), TextWithTags::Tags() }, 0, FlatTextarea::AddToUndoHistory);
} }
void HistoryWidget::contextMenuEvent(QContextMenuEvent *e) { void HistoryWidget::contextMenuEvent(QContextMenuEvent *e) {
@ -6998,7 +7022,7 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) {
} else if (e->key() == Qt::Key_Up) { } else if (e->key() == Qt::Key_Up) {
if (!(e->modifiers() & (Qt::ShiftModifier | Qt::MetaModifier | Qt::ControlModifier))) { if (!(e->modifiers() & (Qt::ShiftModifier | Qt::MetaModifier | Qt::ControlModifier))) {
if (_history && _history->lastSentMsg && _history->lastSentMsg->canEdit(::date(unixtime()))) { if (_history && _history->lastSentMsg && _history->lastSentMsg->canEdit(::date(unixtime()))) {
if (_field.getLastText().isEmpty() && !_editMsgId && !_replyToId) { if (_field.isEmpty() && !_editMsgId && !_replyToId) {
App::contextItem(_history->lastSentMsg); App::contextItem(_history->lastSentMsg);
onEditMessage(); onEditMessage();
} }
@ -7308,11 +7332,11 @@ void HistoryWidget::sendExistingPhoto(PhotoData *photo, const QString &caption)
_field.setFocus(); _field.setFocus();
} }
void HistoryWidget::setFieldText(const QString &text, int32 textUpdateEventsFlags, bool clearUndoHistory) { void HistoryWidget::setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events, FlatTextarea::UndoHistoryAction undoHistoryAction) {
_textUpdateEventsFlags = textUpdateEventsFlags; _textUpdateEvents = events;
_field.setTextFast(text, clearUndoHistory); _field.setTextWithTags(textWithTags, undoHistoryAction);
_field.moveCursor(QTextCursor::End); _field.moveCursor(QTextCursor::End);
_textUpdateEventsFlags = TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping; _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;
_previewCancelled = false; _previewCancelled = false;
_previewData = nullptr; _previewData = nullptr;
@ -7351,7 +7375,7 @@ void HistoryWidget::onReplyToMessage() {
if (auto msgDraft = _history->msgDraft()) { if (auto msgDraft = _history->msgDraft()) {
msgDraft->msgId = to->id; msgDraft->msgId = to->id;
} else { } else {
_history->setMsgDraft(std_::make_unique<HistoryDraft>(QString(), to->id, MessageCursor(), false)); _history->setMsgDraft(std_::make_unique<HistoryDraft>(TextWithTags(), to->id, MessageCursor(), false));
} }
} else { } else {
_replyEditMsg = to; _replyEditMsg = to;
@ -7384,14 +7408,17 @@ void HistoryWidget::onEditMessage() {
} else { } else {
delete box; delete box;
if (_replyToId || !_field.getLastText().isEmpty()) { if (_replyToId || !_field.isEmpty()) {
_history->setMsgDraft(std_::make_unique<HistoryDraft>(_field, _replyToId, _previewCancelled)); _history->setMsgDraft(std_::make_unique<HistoryDraft>(_field, _replyToId, _previewCancelled));
} else { } else {
_history->clearMsgDraft(); _history->clearMsgDraft();
} }
QString text(textApplyEntities(to->originalText(), to->originalEntities())); auto originalText = to->originalText();
_history->setEditDraft(std_::make_unique<HistoryEditDraft>(text, to->id, MessageCursor(text.size(), text.size(), QFIXED_MAX), false)); 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<HistoryEditDraft>(original, to->id, cursor, false));
applyDraft(false); applyDraft(false);
_previewData = 0; _previewData = 0;
@ -7510,7 +7537,7 @@ void HistoryWidget::cancelReply(bool lastKeyboardUsed) {
update(); update();
} else if (auto msgDraft = (_history ? _history->msgDraft() : nullptr)) { } else if (auto msgDraft = (_history ? _history->msgDraft() : nullptr)) {
if (msgDraft->msgId) { if (msgDraft->msgId) {
if (msgDraft->text.isEmpty()) { if (msgDraft->textWithTags.text.isEmpty()) {
_history->clearMsgDraft(); _history->clearMsgDraft();
} else { } else {
msgDraft->msgId = 0; msgDraft->msgId = 0;
@ -7533,7 +7560,7 @@ void HistoryWidget::cancelEdit() {
if (!_editMsgId) return; if (!_editMsgId) return;
_editMsgId = 0; _editMsgId = 0;
_replyEditMsg = 0; _replyEditMsg = nullptr;
_history->clearEditDraft(); _history->clearEditDraft();
applyDraft(); applyDraft();
@ -7546,21 +7573,21 @@ void HistoryWidget::cancelEdit() {
_saveDraftStart = getms(); _saveDraftStart = getms();
onDraftSave(); onDraftSave();
mouseMoveEvent(0); mouseMoveEvent(nullptr);
if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !replyToId()) { if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !replyToId()) {
_fieldBarCancel.hide(); _fieldBarCancel.hide();
updateMouseTracking(); updateMouseTracking();
} }
int32 old = _textUpdateEventsFlags; auto old = _textUpdateEvents;
_textUpdateEventsFlags = 0; _textUpdateEvents = 0;
onTextChange(); onTextChange();
_textUpdateEventsFlags = old; _textUpdateEvents = old;
updateBotKeyboard(); updateBotKeyboard();
updateFieldPlaceholder(); updateFieldPlaceholder();
resizeEvent(0); resizeEvent(nullptr);
update(); update();
} }
@ -7728,7 +7755,10 @@ void HistoryWidget::onCancel() {
if (_inlineBotCancel) { if (_inlineBotCancel) {
onInlineBotCancel(); onInlineBotCancel();
} else if (_editMsgId) { } 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)); 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())); connect(box, SIGNAL(confirmed()), this, SLOT(onFieldBarCancel()));
Ui::showLayer(box); Ui::showLayer(box);

View File

@ -486,12 +486,8 @@ public:
}; };
EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags); EntitiesInText entitiesFromTextTags(const TextWithTags::Tags &tags);
TextWithTags::Tags textTagsFromEntities(const EntitiesInText &entities);
enum TextUpdateEventsFlags {
TextUpdateEventsSaveDraft = 0x01,
TextUpdateEventsSendTyping = 0x02,
};
class HistoryWidget : public TWidget, public RPCSender { class HistoryWidget : public TWidget, public RPCSender {
Q_OBJECT Q_OBJECT
@ -954,11 +950,18 @@ private:
void savedGifsGot(const MTPmessages_SavedGifs &gifs); void savedGifsGot(const MTPmessages_SavedGifs &gifs);
bool savedGifsFailed(const RPCError &error); 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(HistoryDraft **msgDraft, HistoryEditDraft **editDraft);
void writeDrafts(History *history); void writeDrafts(History *history);
void setFieldText(const QString &text, int32 textUpdateEventsFlags = 0, bool clearUndoHistory = true); void setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events = 0, FlatTextarea::UndoHistoryAction undoHistoryAction = FlatTextarea::ClearUndoHistory);
void clearFieldText(int32 textUpdateEventsFlags = 0, bool clearUndoHistory = true) { void clearFieldText(TextUpdateEvents events = 0, FlatTextarea::UndoHistoryAction undoHistoryAction = FlatTextarea::ClearUndoHistory) {
setFieldText(QString()); setFieldText(TextWithTags(), events, undoHistoryAction);
} }
QStringList getMediasFromMime(const QMimeData *d); QStringList getMediasFromMime(const QMimeData *d);
@ -1062,7 +1065,7 @@ private:
int32 _selCount; // < 0 - text selected, focus list, not _field int32 _selCount; // < 0 - text selected, focus list, not _field
TaskQueue _fileLoader; TaskQueue _fileLoader;
int32 _textUpdateEventsFlags = (TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping); TextUpdateEvents _textUpdateEvents = (TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping);
int64 _serviceImageCacheSize = 0; int64 _serviceImageCacheSize = 0;
QString _confirmSource; QString _confirmSource;
@ -1095,3 +1098,4 @@ private:
}; };
Q_DECLARE_OPERATORS_FOR_FLAGS(HistoryWidget::TextUpdateEvents)

View File

@ -122,14 +122,6 @@ namespace {
return true; 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; QByteArray _settingsSalt, _passKeySalt, _passKeyEncrypted;
MTP::AuthKey _oldKey, _settingsKey, _passKey, _localKey; MTP::AuthKey _oldKey, _settingsKey, _passKey, _localKey;
@ -628,18 +620,18 @@ namespace {
size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(i.value().name()); size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(i.value().name());
if (AppVersion > 9013) { if (AppVersion > 9013) {
// bookmark // bookmark
size += _bytearraySize(i.value().bookmark()); size += Serialize::bytearraySize(i.value().bookmark());
} }
// date + size // date + size
size += _dateTimeSize() + sizeof(quint32); size += Serialize::dateTimeSize() + sizeof(quint32);
} }
//end mark //end mark
size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(QString()); size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(QString());
if (AppVersion > 9013) { if (AppVersion > 9013) {
size += _bytearraySize(QByteArray()); size += Serialize::bytearraySize(QByteArray());
} }
size += _dateTimeSize() + sizeof(quint32); size += Serialize::dateTimeSize() + sizeof(quint32);
size += sizeof(quint32); // aliases count size += sizeof(quint32); // aliases count
for (FileLocationAliases::const_iterator i = _fileLocationAliases.cbegin(), e = _fileLocationAliases.cend(); i != e; ++i) { 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)); 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) + (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) + cEmojiVariants().size() * (sizeof(uint32) + sizeof(uint64));
size += sizeof(quint32) + sizeof(qint32) + (cRecentStickersPreload().isEmpty() ? cGetRecentStickers().size() : cRecentStickersPreload().size()) * (sizeof(uint64) + sizeof(ushort)); 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) { void writeDrafts(const PeerId &peer, const MessageDraft &msgDraft, const MessageDraft &editDraft) {
if (!_working()) return; if (!_working()) return;
if (msgDraft.msgId <= 0 && msgDraft.text.isEmpty() && editDraft.msgId <= 0) { if (msgDraft.msgId <= 0 && msgDraft.textWithTags.text.isEmpty() && editDraft.msgId <= 0) {
DraftsMap::iterator i = _draftsMap.find(peer); auto i = _draftsMap.find(peer);
if (i != _draftsMap.cend()) { if (i != _draftsMap.cend()) {
clearKey(i.value()); clearKey(i.value());
_draftsMap.erase(i); _draftsMap.erase(i);
@ -2292,17 +2284,26 @@ namespace Local {
_draftsNotReadMap.remove(peer); _draftsNotReadMap.remove(peer);
} else { } else {
DraftsMap::const_iterator i = _draftsMap.constFind(peer); auto i = _draftsMap.constFind(peer);
if (i == _draftsMap.cend()) { if (i == _draftsMap.cend()) {
i = _draftsMap.insert(peer, genKey()); i = _draftsMap.insert(peer, genKey());
_mapChanged = true; _mapChanged = true;
_writeMap(WriteMapFast); _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 << quint64(peer);
data.stream << msgDraft.text << qint32(msgDraft.msgId) << qint32(msgDraft.previewCancelled ? 1 : 0); data.stream << msgDraft.textWithTags.text << msgTags;
data.stream << editDraft.text << qint32(editDraft.msgId) << qint32(editDraft.previewCancelled ? 1 : 0); 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()); FileWriteDescriptor file(i.value());
file.writeEncrypted(data); file.writeEncrypted(data);
@ -2370,15 +2371,23 @@ namespace Local {
} }
quint64 draftPeer = 0; quint64 draftPeer = 0;
QString msgText, editText; TextWithTags msgData, editData;
QByteArray msgTagsSerialized, editTagsSerialized;
qint32 msgReplyTo = 0, msgPreviewCancelled = 0, editMsgId = 0, editPreviewCancelled = 0; 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) { if (draft.version >= 7021) {
draft.stream >> msgReplyTo; draft.stream >> msgReplyTo;
if (draft.version >= 8001) { if (draft.version >= 8001) {
draft.stream >> msgPreviewCancelled; draft.stream >> msgPreviewCancelled;
if (!draft.stream.atEnd()) { 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; return;
} }
msgData.tags = FlatTextarea::deserializeTagsList(msgTagsSerialized, msgData.text.size());
editData.tags = FlatTextarea::deserializeTagsList(editTagsSerialized, editData.text.size());
MessageCursor msgCursor, editCursor; MessageCursor msgCursor, editCursor;
_readDraftCursors(peer, msgCursor, editCursor); _readDraftCursors(peer, msgCursor, editCursor);
if (msgText.isEmpty() && !msgReplyTo) { if (msgData.text.isEmpty() && !msgReplyTo) {
h->clearMsgDraft(); h->clearMsgDraft();
} else { } else {
h->setMsgDraft(std_::make_unique<HistoryDraft>(msgText, msgReplyTo, msgCursor, msgPreviewCancelled)); h->setMsgDraft(std_::make_unique<HistoryDraft>(msgData, msgReplyTo, msgCursor, msgPreviewCancelled));
} }
if (!editMsgId) { if (!editMsgId) {
h->clearEditDraft(); h->clearEditDraft();
} else { } else {
h->setEditDraft(std_::make_unique<HistoryEditDraft>(editText, editMsgId, editCursor, editPreviewCancelled)); h->setEditDraft(std_::make_unique<HistoryEditDraft>(editData, editMsgId, editCursor, editPreviewCancelled));
} }
} }
@ -3019,7 +3031,7 @@ namespace Local {
} else { } else {
int32 setsCount = 0; int32 setsCount = 0;
QByteArray hashToWrite; QByteArray hashToWrite;
quint32 size = sizeof(quint32) + _bytearraySize(hashToWrite); quint32 size = sizeof(quint32) + Serialize::bytearraySize(hashToWrite);
for (auto i = sets.cbegin(); i != sets.cend(); ++i) { for (auto i = sets.cbegin(); i != sets.cend(); ++i) {
bool notLoaded = (i->flags & MTPDstickerSet_ClientFlag::f_not_loaded); bool notLoaded = (i->flags & MTPDstickerSet_ClientFlag::f_not_loaded);
if (notLoaded) { if (notLoaded) {
@ -3682,7 +3694,7 @@ namespace Local {
} }
quint32 size = sizeof(quint32); quint32 size = sizeof(quint32);
for (SavedPeers::const_iterator i = saved.cbegin(); i != saved.cend(); ++i) { 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); EncryptedDescriptor data(size);

View File

@ -105,11 +105,15 @@ namespace Local {
int32 oldSettingsVersion(); int32 oldSettingsVersion();
using TextWithTags = FlatTextarea::TextWithTags;
struct MessageDraft { 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; MsgId msgId;
QString text; TextWithTags textWithTags;
bool previewCancelled; bool previewCancelled;
}; };
void writeDrafts(const PeerId &peer, const MessageDraft &msgDraft, const MessageDraft &editDraft); void writeDrafts(const PeerId &peer, const MessageDraft &msgDraft, const MessageDraft &editDraft);

View File

@ -159,7 +159,9 @@ bool MainWidget::onShareUrl(const PeerId &peer, const QString &url, const QStrin
return false; return false;
} }
History *h = App::history(peer); History *h = App::history(peer);
h->setMsgDraft(std_::make_unique<HistoryDraft>(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<HistoryDraft>(textWithTags, 0, cursor, false));
h->clearEditDraft(); h->clearEditDraft();
bool opened = _history->peer() && (_history->peer()->id == peer); bool opened = _history->peer() && (_history->peer()->id == peer);
if (opened) { if (opened) {
@ -177,7 +179,9 @@ bool MainWidget::onInlineSwitchChosen(const PeerId &peer, const QString &botAndQ
return false; return false;
} }
History *h = App::history(peer); History *h = App::history(peer);
h->setMsgDraft(std_::make_unique<HistoryDraft>(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<HistoryDraft>(textWithTags, 0, cursor, false));
h->clearEditDraft(); h->clearEditDraft();
bool opened = _history->peer() && (_history->peer()->id == peer); bool opened = _history->peer() && (_history->peer()->id == peer);
if (opened) { if (opened) {
@ -1086,7 +1090,7 @@ void executeParsedCommand(const QString &command) {
void MainWidget::sendMessage(const MessageToSend &message) { void MainWidget::sendMessage(const MessageToSend &message) {
auto history = message.history; auto history = message.history;
const auto &text = message.text; const auto &textWithTags = message.textWithTags;
readServerHistory(history, false); readServerHistory(history, false);
_history->fastShowAtEnd(history); _history->fastShowAtEnd(history);
@ -1095,13 +1099,13 @@ void MainWidget::sendMessage(const MessageToSend &message) {
return; 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; 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; HistoryItem *lastMessage = nullptr;
MsgId replyTo = (message.replyTo < 0) ? _history->replyToId() : 0; MsgId replyTo = (message.replyTo < 0) ? _history->replyToId() : 0;

View File

@ -283,8 +283,7 @@ public:
struct MessageToSend { struct MessageToSend {
History *history = nullptr; History *history = nullptr;
QString text; TextWithTags textWithTags;
FlatTextarea::TagList entities;
MsgId replyTo = 0; MsgId replyTo = 0;
bool broadcast = false; bool broadcast = false;
bool silent = false; bool silent = false;

View File

@ -86,7 +86,7 @@ void MacPrivate::notifyReplied(unsigned long long peer, int msgid, const char *s
MainWidget::MessageToSend message; MainWidget::MessageToSend message;
message.history = history; 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.replyTo = (msgid > 0 && !history->peer->isUser()) ? msgid : 0;
message.broadcast = false; message.broadcast = false;
message.silent = false; message.silent = false;

View File

@ -23,10 +23,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
namespace Serialize { namespace Serialize {
int stringSize(const QString &str) {
return sizeof(quint32) + str.size() * sizeof(ushort);
}
void writeStorageImageLocation(QDataStream &stream, const StorageImageLocation &loc) { void writeStorageImageLocation(QDataStream &stream, const StorageImageLocation &loc) {
stream << qint32(loc.width()) << qint32(loc.height()); stream << qint32(loc.width()) << qint32(loc.height());
stream << qint32(loc.dc()) << quint64(loc.volume()) << qint32(loc.local()) << quint64(loc.secret()); stream << qint32(loc.dc()) << quint64(loc.volume()) << qint32(loc.local()) << quint64(loc.secret());

View File

@ -24,7 +24,17 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
namespace Serialize { 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); void writeStorageImageLocation(QDataStream &stream, const StorageImageLocation &loc);
StorageImageLocation readStorageImageLocation(QDataStream &stream); StorageImageLocation readStorageImageLocation(QDataStream &stream);

View File

@ -23,9 +23,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
#include "mainwindow.h" #include "mainwindow.h"
namespace { QByteArray FlatTextarea::serializeTagsList(const TagList &tags) {
QByteArray serializeTagsList(const FlatTextarea::TagList &tags) {
if (tags.isEmpty()) { if (tags.isEmpty()) {
return QByteArray(); return QByteArray();
} }
@ -44,8 +42,11 @@ QByteArray serializeTagsList(const FlatTextarea::TagList &tags) {
return tagsSerialized; return tagsSerialized;
} }
FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) { FlatTextarea::TagList FlatTextarea::deserializeTagsList(QByteArray data, int textLength) {
FlatTextarea::TagList result; TagList result;
if (data.isEmpty()) {
return result;
}
QBuffer buffer(&data); QBuffer buffer(&data);
buffer.open(QIODevice::ReadOnly); buffer.open(QIODevice::ReadOnly);
@ -58,7 +59,7 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
if (stream.status() != QDataStream::Ok) { if (stream.status() != QDataStream::Ok) {
return result; return result;
} }
if (tagCount <= 0 || tagCount > textSize) { if (tagCount <= 0 || tagCount > textLength) {
return result; return result;
} }
@ -69,7 +70,7 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
if (stream.status() != QDataStream::Ok) { if (stream.status() != QDataStream::Ok) {
return result; return result;
} }
if (offset < 0 || length <= 0 || offset + length > textSize) { if (offset < 0 || length <= 0 || offset + length > textLength) {
return result; return result;
} }
result.push_back({ offset, length, id }); result.push_back({ offset, length, id });
@ -77,12 +78,12 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
return result; 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, const TagList &tags) : QTextEdit(parent)
, _lastTextWithTags { v, tags }
FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &pholder, const QString &v) : QTextEdit(parent)
, _oldtext(v)
, _phVisible(!v.length()) , _phVisible(!v.length())
, a_phLeft(_phVisible ? 0 : st.phShift) , a_phLeft(_phVisible ? 0 : st.phShift)
, a_phAlpha(_phVisible ? 1 : 0) , 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))); connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool)));
if (App::wnd()) connect(this, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu())); if (App::wnd()) connect(this, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu()));
if (!v.isEmpty()) { if (!_lastTextWithTags.text.isEmpty()) {
setTextFast(v); setTextWithTags(_lastTextWithTags, ClearUndoHistory);
} }
} }
void FlatTextarea::setTextFast(const QString &text, bool clearUndoHistory) { FlatTextarea::TextWithTags FlatTextarea::getTextWithTagsPart(int start, int end) {
if (clearUndoHistory) { TextWithTags result;
setPlainText(text); 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 { } else {
QTextCursor c(document()->docHandle(), 0); cursor.beginEditBlock();
c.joinPreviousEditBlock();
c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
c.insertText(text);
c.movePosition(QTextCursor::End);
c.endEditBlock();
} }
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(); finishPlaceholder();
} }
@ -266,7 +286,8 @@ void FlatTextarea::paintEvent(QPaintEvent *e) {
p.setFont(_st.font); p.setFont(_st.font);
p.setPen(a_phColor.current()); p.setPen(a_phColor.current());
if (_st.phAlign == style::al_topleft && _phAfter > 0) { 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 { } 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()); 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)); p.drawText(phRect, _ph, QTextOption(_st.phAlign));
@ -317,7 +338,7 @@ QString FlatTextarea::getInlineBotQuery(UserData **outInlineBot, QString *outInl
t_assert(outInlineBot != nullptr); t_assert(outInlineBot != nullptr);
t_assert(outInlineBotUsername != nullptr); t_assert(outInlineBotUsername != nullptr);
const QString &text(getLastText()); auto &text = getTextWithTags().text;
int32 inlineUsernameStart = 1, inlineUsernameLength = 0, size = text.size(); int32 inlineUsernameStart = 1, inlineUsernameLength = 0, size = text.size();
if (size > 2 && text.at(0) == '@' && text.at(1).isLetter()) { 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); cursor.insertText(text + ' ', format);
} else { } else {
_insertedTags.clear(); _insertedTags.clear();
if (_tagMimeProcessor) {
tagId = _tagMimeProcessor->mimeTagFromTag(tagId);
}
_insertedTags.push_back({ 0, text.size(), tagId }); _insertedTags.push_back({ 0, text.size(), tagId });
_insertedTagsAreFromMime = false;
cursor.insertText(text + ' '); cursor.insertText(text + ' ');
_insertedTags.clear(); _insertedTags.clear();
} }
@ -601,7 +620,7 @@ private:
} // namespace } // 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 (end >= 0 && end <= start) return QString();
if (start < 0) start = 0; 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()); int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length());
if (!full) { if (!full) {
tillFragmentEnd = (e <= end); tillFragmentEnd = (e <= end);
if (p == end && outTagsList) { if (p == end) {
tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); tagAccumulator.feed(fragment.charFormat().anchorName(), result.size());
} }
if (p >= end) { if (p >= end) {
@ -642,7 +661,7 @@ QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *ou
continue; continue;
} }
} }
if (outTagsList && (full || p >= start)) { if (full || p >= start) {
tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); 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); result.chop(1);
if (outTagsList) { if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size());
if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size()); if (outTagsChanged) {
if (outTagsChanged) { *outTagsChanged = tagAccumulator.changed();
*outTagsChanged = tagAccumulator.changed();
}
} }
return result; return result;
} }
@ -816,11 +833,12 @@ QStringList FlatTextarea::linksList() const {
} }
void FlatTextarea::insertFromMimeData(const QMimeData *source) { void FlatTextarea::insertFromMimeData(const QMimeData *source) {
auto mime = str_const_toString(TagsMimeType); auto mime = tagsMimeType();
auto text = source->text(); auto text = source->text();
if (source->hasFormat(mime)) { if (source->hasFormat(mime)) {
auto tagsData = source->data(mime); auto tagsData = source->data(mime);
_insertedTags = deserializeTagsList(tagsData, text.size()); _insertedTags = deserializeTagsList(tagsData, text.size());
_insertedTagsAreFromMime = true;
} else { } else {
_insertedTags.clear(); _insertedTags.clear();
} }
@ -982,7 +1000,9 @@ void FlatTextarea::processFormatting(int insertPosition, int insertEnd) {
auto doc = document(); auto doc = document();
// Apply inserted tags. // 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; using ActionType = FormattingAction::Type;
while (true) { while (true) {
FormattingAction action; FormattingAction action;
@ -1181,15 +1201,15 @@ void FlatTextarea::onDocumentContentsChanged() {
if (_correcting) return; if (_correcting) return;
auto tagsChanged = false; auto tagsChanged = false;
auto curText = getText(0, -1, &_oldtags, &tagsChanged); auto curText = getTextPart(0, -1, &_lastTextWithTags.tags, &tagsChanged);
_correcting = true; _correcting = true;
correctValue(_oldtext, curText, _oldtags); correctValue(_lastTextWithTags.text, curText, _lastTextWithTags.tags);
_correcting = false; _correcting = false;
bool textOrTagsChanged = tagsChanged || (_oldtext != curText); bool textOrTagsChanged = tagsChanged || (_lastTextWithTags.text != curText);
if (textOrTagsChanged) { if (textOrTagsChanged) {
_oldtext = curText; _lastTextWithTags.text = curText;
emit changed(); emit changed();
checkContentHeight(); checkContentHeight();
} }
@ -1231,12 +1251,16 @@ void FlatTextarea::setPlaceholder(const QString &ph, int32 afterSymbols) {
_phAfter = afterSymbols; _phAfter = afterSymbols;
updatePlaceholder(); 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(); if (_phVisible) update();
} }
void FlatTextarea::updatePlaceholder() { void FlatTextarea::updatePlaceholder() {
bool vis = (getLastText().size() <= _phAfter); bool vis = (getTextWithTags().text.size() <= _phAfter);
if (vis == _phVisible) return; if (vis == _phVisible) return;
a_phLeft.start(vis ? 0 : _st.phShift); a_phLeft.start(vis ? 0 : _st.phShift);
@ -1252,14 +1276,14 @@ QMimeData *FlatTextarea::createMimeDataFromSelection() const {
int32 start = c.selectionStart(), end = c.selectionEnd(); int32 start = c.selectionStart(), end = c.selectionEnd();
if (end > start) { if (end > start) {
TagList tags; TagList tags;
result->setText(getText(start, end, &tags, nullptr)); result->setText(getTextPart(start, end, &tags));
if (!tags.isEmpty()) { if (!tags.isEmpty()) {
if (_tagMimeProcessor) { if (_tagMimeProcessor) {
for (auto &tag : tags) { for (auto &tag : tags) {
tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id); tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id);
} }
} }
result->setData(str_const_toString(TagsMimeType), serializeTagsList(tags)); result->setData(tagsMimeType(), serializeTagsList(tags));
} }
} }
return result; return result;

View File

@ -31,16 +31,27 @@ class FlatTextarea : public QTextEdit {
public: 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<Tag>;
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 setMaxLength(int32 maxLength);
void setMinHeight(int32 minHeight); void setMinHeight(int32 minHeight);
void setMaxHeight(int32 maxHeight); void setMaxHeight(int32 maxHeight);
const QString &getLastText() const {
return _oldtext;
}
void setPlaceholder(const QString &ph, int32 afterSymbols = 0); void setPlaceholder(const QString &ph, int32 afterSymbols = 0);
void updatePlaceholder(); void updatePlaceholder();
void finishPlaceholder(); void finishPlaceholder();
@ -82,18 +93,23 @@ public:
}; };
void setSubmitSettings(SubmitSettings settings); void setSubmitSettings(SubmitSettings settings);
void setTextFast(const QString &text, bool clearUndoHistory = true); const TextWithTags &getTextWithTags() const {
return _lastTextWithTags;
struct Tag {
int offset, length;
QString id;
};
using TagList = QVector<Tag>;
const TagList &getLastTags() const {
return _oldtags;
} }
TextWithTags getTextWithTagsPart(int start, int end = -1);
void insertTag(const QString &text, QString tagId = QString()); 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 // 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. // (and then to clipboard or to drag-n-drop object), here is a strategy for that.
class TagMimeProcessor { class TagMimeProcessor {
@ -147,9 +163,9 @@ protected:
private: private:
// "start" and "end" are in coordinates of text where emoji are replaced by ObjectReplacementCharacter. // "start" and "end" are in coordinates of text where emoji are replaced
// If "end" = -1 means get text till the end. "outTagsList" and "outTagsChanged" may be nullptr. // by ObjectReplacementCharacter. If "end" = -1 means get text till the end.
QString getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const; QString getTextPart(int start, int end, TagList *outTagsList, bool *outTagsChanged = nullptr) const;
void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const; void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const;
@ -169,8 +185,7 @@ private:
int _maxLength = -1; int _maxLength = -1;
SubmitSettings _submitSettings = SubmitSettings::Enter; SubmitSettings _submitSettings = SubmitSettings::Enter;
QString _ph, _phelided, _oldtext; QString _ph, _phelided;
TagList _oldtags;
int _phAfter = 0; int _phAfter = 0;
bool _phVisible; bool _phVisible;
anim::ivalue a_phLeft; anim::ivalue a_phLeft;
@ -178,8 +193,11 @@ private:
anim::cvalue a_phColor; anim::cvalue a_phColor;
Animation _a_appearance; Animation _a_appearance;
TextWithTags _lastTextWithTags;
// Tags list which we should apply while setText() call or insert from mime data. // Tags list which we should apply while setText() call or insert from mime data.
TagList _insertedTags; TagList _insertedTags;
bool _insertedTagsAreFromMime;
// Override insert position and charsAdded from complex text editing // Override insert position and charsAdded from complex text editing
// (like drag-n-drop in the same text edit field). // (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); 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) { inline bool operator==(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) {
return (a.start == b.start) && (a.length == b.length); return (a.start == b.start) && (a.length == b.length);
} }

View File

@ -2023,7 +2023,7 @@
SDKROOT = macosx; SDKROOT = macosx;
SYMROOT = ./../Mac; SYMROOT = ./../Mac;
TDESKTOP_MAJOR_VERSION = 0.9; TDESKTOP_MAJOR_VERSION = 0.9;
TDESKTOP_VERSION = 0.9.47; TDESKTOP_VERSION = 0.9.48;
}; };
name = Release; name = Release;
}; };
@ -2164,7 +2164,7 @@
SDKROOT = macosx; SDKROOT = macosx;
SYMROOT = ./../Mac; SYMROOT = ./../Mac;
TDESKTOP_MAJOR_VERSION = 0.9; TDESKTOP_MAJOR_VERSION = 0.9;
TDESKTOP_VERSION = 0.9.47; TDESKTOP_VERSION = 0.9.48;
}; };
name = Debug; name = Debug;
}; };

View File

@ -1,6 +1,6 @@
AppVersion 9047 AppVersion 9048
AppVersionStrMajor 0.9 AppVersionStrMajor 0.9
AppVersionStrSmall 0.9.47 AppVersionStrSmall 0.9.48
AppVersionStr 0.9.47 AppVersionStr 0.9.48
AlphaChannel 1 AlphaChannel 1
BetaVersion 0 BetaVersion 0