Replace FlatTextarea with InputField.

This commit is contained in:
John Preston 2018-05-22 00:31:46 +03:00
parent 30dd8fe070
commit 017ec87d60
29 changed files with 1646 additions and 2272 deletions

View File

@ -1072,7 +1072,7 @@ void EditBioBox::prepare() {
addButton(langFactory(lng_settings_save), [this] { save(); });
addButton(langFactory(lng_cancel), [this] { closeBox(); });
_bio->setMaxLength(kMaxBioLength);
_bio->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both);
_bio->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
auto cursor = _bio->textCursor();
cursor.setPosition(_bio->getLastText().size());
_bio->setTextCursor(cursor);

View File

@ -7,9 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/change_phone_box.h"
#include <rpl/filter.h>
#include <rpl/mappers.h>
#include <rpl/take.h>
#include "lang/lang_keys.h"
#include "styles/style_boxes.h"
#include "ui/widgets/labels.h"

View File

@ -137,7 +137,7 @@ EditCaptionBox::EditCaptionBox(
langFactory(lng_photo_caption),
caption);
_field->setMaxLength(MaxPhotoCaption);
_field->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both);
_field->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
_field->setInstantReplaces(Ui::InstantReplaces::Default());
}

View File

@ -77,7 +77,7 @@ void RateCallBox::ratingChanged(int value) {
Ui::InputField::Mode::MultiLine,
langFactory(lng_call_rate_comment));
_comment->show();
_comment->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both);
_comment->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
_comment->setMaxLength(MaxPhotoCaption);
_comment->resize(width() - (st::callRatingPadding.left() + st::callRatingPadding.right()), _comment->height());

View File

@ -88,7 +88,7 @@ void ReportBox::reasonChanged(Reason reason) {
Ui::InputField::Mode::MultiLine,
langFactory(lng_report_reason_description));
_reasonOtherText->show();
_reasonOtherText->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both);
_reasonOtherText->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
_reasonOtherText->setMaxLength(MaxPhotoCaption);
_reasonOtherText->resize(width() - (st::boxPadding.left() + st::boxOptionListPadding.left() + st::boxPadding.right()), _reasonOtherText->height());

View File

@ -1556,7 +1556,7 @@ void SendFilesBox::setupCaption() {
Ui::InputField::Mode::MultiLine,
FieldPlaceholder(_list));
_caption->setMaxLength(MaxPhotoCaption);
_caption->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both);
_caption->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
connect(_caption, &Ui::InputField::resized, this, [this] {
captionResized();
});

View File

@ -523,10 +523,19 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) {
QRect r(e->rect());
if (r != rect()) p.setClipRect(r);
int32 atwidth = st::mentionFont->width('@'), hashwidth = st::mentionFont->width('#');
int32 mentionleft = 2 * st::mentionPadding.left() + st::mentionPhotoSize;
int32 mentionwidth = width() - mentionleft - 2 * st::mentionPadding.right();
int32 htagleft = st::historyAttach.width + st::historyComposeField.textMrg.left() - st::lineWidth, htagwidth = width() - st::mentionPadding.right() - htagleft - st::mentionScroll.width;
auto atwidth = st::mentionFont->width('@');
auto hashwidth = st::mentionFont->width('#');
auto mentionleft = 2 * st::mentionPadding.left() + st::mentionPhotoSize;
auto mentionwidth = width()
- mentionleft
- 2 * st::mentionPadding.right();
auto htagleft = st::historyAttach.width
+ st::historyComposeField.textMargins.left()
- st::lineWidth;
auto htagwidth = width()
- st::mentionPadding.right()
- htagleft
- st::mentionScroll.width;
if (!_srows->empty()) {
int32 rows = rowscount(_srows->size(), _stickersPerRow);

View File

@ -16,8 +16,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
constexpr auto kParseLinksTimeout = TimeMs(1000);
// For mention tags save and validate userId, ignore tags for different userId.
class FieldTagMimeProcessor : public Ui::FlatTextarea::TagMimeProcessor {
class FieldTagMimeProcessor : public Ui::InputField::TagMimeProcessor {
public:
QString mimeTagFromTag(const QString &tagId) override {
return ConvertTagToMimeTag(tagId);
@ -110,66 +112,336 @@ void SetClipboardWithEntities(
}
}
MessageField::MessageField(QWidget *parent, not_null<Window::Controller*> controller, const style::FlatTextarea &st, base::lambda<QString()> placeholderFactory)
: FlatTextarea(parent, st, std::move(placeholderFactory))
, _controller(controller) {
setMinHeight(st::historySendSize.height() - 2 * st::historySendPadding);
setMaxHeight(st::historyComposeFieldMaxHeight);
void InitMessageField(not_null<Ui::InputField*> field) {
field->setMinHeight(st::historySendSize.height() - 2 * st::historySendPadding);
field->setMaxHeight(st::historyComposeFieldMaxHeight);
setTagMimeProcessor(std::make_unique<FieldTagMimeProcessor>());
field->setTagMimeProcessor(std::make_unique<FieldTagMimeProcessor>());
setInstantReplaces(Ui::InstantReplaces::Default());
enableInstantReplaces(Global::ReplaceEmoji());
subscribe(Global::RefReplaceEmojiChanged(), [=] {
enableInstantReplaces(Global::ReplaceEmoji());
});
field->document()->setDocumentMargin(4.);
const auto additional = convertScale(4) - 4;
field->rawTextEdit()->setStyleSheet(
qsl("QTextEdit { margin: %1px; }").arg(additional));
field->setInstantReplaces(Ui::InstantReplaces::Default());
field->enableInstantReplaces(Global::ReplaceEmoji());
auto &changed = Global::RefReplaceEmojiChanged();
Ui::AttachAsChild(field, changed.add_subscription([=] {
field->enableInstantReplaces(Global::ReplaceEmoji());
}));
field->window()->activateWindow();
}
bool MessageField::hasSendText() const {
auto &text = getTextWithTags().text;
for (auto *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) {
auto code = ch->unicode();
if (code != ' ' && code != '\n' && code != '\r' && !chReplacedBySpace(code)) {
bool HasSendText(not_null<const Ui::InputField*> field) {
const auto &text = field->getTextWithTags().text;
for (const auto ch : text) {
const auto code = ch.unicode();
if (code != ' '
&& code != '\n'
&& code != '\r'
&& !chReplacedBySpace(code)) {
return true;
}
}
return false;
}
void MessageField::onEmojiInsert(EmojiPtr emoji) {
if (isHidden()) return;
insertEmoji(emoji, textCursor());
}
InlineBotQuery ParseInlineBotQuery(not_null<const Ui::InputField*> field) {
auto result = InlineBotQuery();
void MessageField::dropEvent(QDropEvent *e) {
FlatTextarea::dropEvent(e);
if (e->isAccepted()) {
_controller->window()->activateWindow();
const auto &text = field->getTextWithTags().text;
const auto textLength = text.size();
auto inlineUsernameStart = 1;
auto inlineUsernameLength = 0;
if (textLength > 2 && text[0] == '@' && text[1].isLetter()) {
inlineUsernameLength = 1;
for (auto i = inlineUsernameStart + 1; i != textLength; ++i) {
const auto ch = text[i];
if (ch.isLetterOrNumber() || ch.unicode() == '_') {
++inlineUsernameLength;
continue;
} else if (!ch.isSpace()) {
inlineUsernameLength = 0;
}
break;
}
auto inlineUsernameEnd = inlineUsernameStart + inlineUsernameLength;
auto inlineUsernameEqualsText = (inlineUsernameEnd == textLength);
auto validInlineUsername = false;
if (inlineUsernameEqualsText) {
validInlineUsername = text.endsWith(qstr("bot"));
} else if (inlineUsernameEnd < textLength && inlineUsernameLength) {
validInlineUsername = text[inlineUsernameEnd].isSpace();
}
if (validInlineUsername) {
auto username = text.midRef(inlineUsernameStart, inlineUsernameLength);
if (username != result.username) {
result.username = username.toString();
if (const auto peer = App::peerByName(result.username)) {
if (const auto user = peer->asUser()) {
result.bot = peer->asUser();
} else {
result.bot = nullptr;
}
result.lookingUpBot = false;
} else {
result.bot = nullptr;
result.lookingUpBot = true;
}
}
if (result.lookingUpBot) {
result.query = QString();
return result;
} else if (result.bot && (!result.bot->botInfo
|| result.bot->botInfo->inlinePlaceholder.isEmpty())) {
result.bot = nullptr;
} else {
result.query = inlineUsernameEqualsText
? QString()
: text.mid(inlineUsernameEnd + 1);
return result;
}
} else {
inlineUsernameLength = 0;
}
}
if (inlineUsernameLength < 3) {
result.bot = nullptr;
result.username = QString();
}
result.query = QString();
return result;
}
bool MessageField::canInsertFromMimeData(const QMimeData *source) const {
if (source->hasUrls()) {
int32 files = 0;
for (int32 i = 0; i < source->urls().size(); ++i) {
if (source->urls().at(i).isLocalFile()) {
++files;
AutocompleteQuery ParseMentionHashtagBotCommandQuery(
not_null<const Ui::InputField*> field) {
auto result = AutocompleteQuery();
const auto cursor = field->textCursor();
const auto position = cursor.position();
if (cursor.anchor() != position) {
return result;
}
const auto document = field->document();
const auto block = document->findBlock(position);
for (auto item = block.begin(); !item.atEnd(); ++item) {
const auto fragment = item.fragment();
if (!fragment.isValid()) {
continue;
}
const auto fragmentPosition = fragment.position();
const auto fragmentEnd = fragmentPosition + fragment.length();
if (fragmentPosition >= position || fragmentEnd < position) {
continue;
}
const auto format = fragment.charFormat();
if (format.isImageFormat()) {
continue;
}
bool mentionInCommand = false;
const auto text = fragment.text();
for (auto i = position - fragmentPosition; i != 0; --i) {
if (text[i - 1] == '@') {
if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_'))) {
result.fromStart = (i == 1) && (fragmentPosition == 0);
result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
} else if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && i > 2 && (text[i - 2].isLetterOrNumber() || text[i - 2] == '_') && !mentionInCommand) {
mentionInCommand = true;
--i;
continue;
}
return result;
} else if (text[i - 1] == '#') {
if (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_')) {
result.fromStart = (i == 1) && (fragmentPosition == 0);
result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
}
return result;
} else if (text[i - 1] == '/') {
if (i < 2) {
result.fromStart = (i == 1) && (fragmentPosition == 0);
result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
}
return result;
}
if (position - fragmentPosition - i > 127 || (!mentionInCommand && (position - fragmentPosition - i > 63))) {
break;
}
if (!text[i - 1].isLetterOrNumber() && text[i - 1] != '_') {
break;
}
}
if (files > 1) return false; // multiple confirm with "compressed" checkbox
break;
}
if (source->hasImage()) return true;
return FlatTextarea::canInsertFromMimeData(source);
return result;
}
void MessageField::insertFromMimeData(const QMimeData *source) {
if (_insertFromMimeDataHook && _insertFromMimeDataHook(source)) {
QtConnectionOwner::QtConnectionOwner(QMetaObject::Connection connection)
: _data(connection) {
}
QtConnectionOwner::QtConnectionOwner(QtConnectionOwner &&other)
: _data(base::take(other._data)) {
}
QtConnectionOwner &QtConnectionOwner::operator=(QtConnectionOwner &&other) {
disconnect();
_data = base::take(other._data);
return *this;
}
void QtConnectionOwner::disconnect() {
QObject::disconnect(base::take(_data));
}
QtConnectionOwner::~QtConnectionOwner() {
disconnect();
}
MessageLinksParser::MessageLinksParser(not_null<Ui::InputField*> field)
: _field(field)
, _timer([=] { parse(); }) {
_connection = QObject::connect(_field, &Ui::InputField::changed, [=] {
const auto length = _field->getTextWithTags().text.size();
const auto timeout = (std::abs(length - _lastLength) > 2)
? 0
: kParseLinksTimeout;
if (!_timer.isActive() || timeout < _timer.remainingTime()) {
_timer.callOnce(timeout);
}
_lastLength = length;
});
_field->installEventFilter(this);
}
bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
if (object == _field) {
if (event->type() == QEvent::KeyPress) {
const auto text = static_cast<QKeyEvent*>(event)->text();
if (!text.isEmpty() && text.size() < 3) {
const auto ch = text[0];
if (false
|| ch == '\n'
|| ch == '\r'
|| ch.isSpace()
|| ch == QChar::LineSeparator) {
_timer.callOnce(0);
}
}
} else if (event->type() == QEvent::Drop) {
_timer.callOnce(0);
}
}
return QObject::eventFilter(object, event);
}
const rpl::variable<QStringList> &MessageLinksParser::list() const {
return _list;
}
void MessageLinksParser::parse() {
const auto &text = _field->getTextWithTags().text;
if (text.isEmpty()) {
_list = QStringList();
return;
}
FlatTextarea::insertFromMimeData(source);
auto ranges = QVector<LinkRange>();
const auto len = text.size();
const QChar *start = text.unicode(), *end = start + text.size();
for (auto offset = 0, matchOffset = offset; offset < len;) {
auto m = TextUtilities::RegExpDomain().match(text, matchOffset);
if (!m.hasMatch()) break;
auto domainOffset = m.capturedStart();
auto protocol = m.captured(1).toLower();
auto topDomain = m.captured(3).toLower();
auto isProtocolValid = protocol.isEmpty() || TextUtilities::IsValidProtocol(protocol);
auto isTopDomainValid = !protocol.isEmpty() || TextUtilities::IsValidTopDomain(topDomain);
if (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) {
auto forMailName = text.mid(offset, domainOffset - offset - 1);
auto mMailName = TextUtilities::RegExpMailNameAtEnd().match(forMailName);
if (mMailName.hasMatch()) {
offset = matchOffset = m.capturedEnd();
continue;
}
}
if (!isProtocolValid || !isTopDomainValid) {
offset = matchOffset = m.capturedEnd();
continue;
}
QStack<const QChar*> parenth;
const QChar *domainEnd = start + m.capturedEnd(), *p = domainEnd;
for (; p < end; ++p) {
QChar ch(*p);
if (chIsLinkEnd(ch)) break; // link finished
if (chIsAlmostLinkEnd(ch)) {
const QChar *endTest = p + 1;
while (endTest < end && chIsAlmostLinkEnd(*endTest)) {
++endTest;
}
if (endTest >= end || chIsLinkEnd(*endTest)) {
break; // link finished at p
}
p = endTest;
ch = *p;
}
if (ch == '(' || ch == '[' || ch == '{' || ch == '<') {
parenth.push(p);
} else if (ch == ')' || ch == ']' || ch == '}' || ch == '>') {
if (parenth.isEmpty()) break;
const QChar *q = parenth.pop(), open(*q);
if ((ch == ')' && open != '(') || (ch == ']' && open != '[') || (ch == '}' && open != '{') || (ch == '>' && open != '<')) {
p = q;
break;
}
}
}
if (p > domainEnd) { // check, that domain ended
if (domainEnd->unicode() != '/' && domainEnd->unicode() != '?') {
matchOffset = domainEnd - start;
continue;
}
}
ranges.push_back({ domainOffset, static_cast<int>(p - start - domainOffset) });
offset = matchOffset = p - start;
}
apply(text, ranges);
}
void MessageField::focusInEvent(QFocusEvent *e) {
FlatTextarea::focusInEvent(e);
emit focused();
void MessageLinksParser::apply(
const QString &text,
const QVector<LinkRange> &ranges) {
const auto count = int(ranges.size());
const auto current = _list.current();
const auto changed = [&] {
if (current.size() != count) {
return true;
}
for (auto i = 0; i != count; ++i) {
const auto &range = ranges[i];
if (text.midRef(range.start, range.length) != current[i]) {
return true;
}
}
return false;
}();
if (!changed) {
return;
}
auto parsed = QStringList();
parsed.reserve(count);
for (const auto &range : ranges) {
parsed.push_back(text.mid(range.start, range.length));
}
_list = std::move(parsed);
}

View File

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "ui/widgets/input_fields.h"
#include "base/timer.h"
class HistoryWidget;
namespace Window {
@ -25,32 +26,66 @@ void SetClipboardWithEntities(
const TextWithEntities &forClipboard,
QClipboard::Mode mode = QClipboard::Clipboard);
class MessageField final : public Ui::FlatTextarea {
Q_OBJECT
void InitMessageField(not_null<Ui::InputField*> field);
bool HasSendText(not_null<const Ui::InputField*> field);
struct InlineBotQuery {
QString query;
QString username;
UserData *bot = nullptr;
bool lookingUpBot = false;
};
InlineBotQuery ParseInlineBotQuery(not_null<const Ui::InputField*> field);
struct AutocompleteQuery {
QString query;
bool fromStart = false;
};
AutocompleteQuery ParseMentionHashtagBotCommandQuery(
not_null<const Ui::InputField*> field);
class QtConnectionOwner {
public:
MessageField(QWidget *parent, not_null<Window::Controller*> controller, const style::FlatTextarea &st, base::lambda<QString()> placeholderFactory = nullptr);
bool hasSendText() const;
void setInsertFromMimeDataHook(base::lambda<bool(const QMimeData *data)> hook) {
_insertFromMimeDataHook = std::move(hook);
}
public slots:
void onEmojiInsert(EmojiPtr emoji);
signals:
void focused();
protected:
void focusInEvent(QFocusEvent *e) override;
void dropEvent(QDropEvent *e) override;
bool canInsertFromMimeData(const QMimeData *source) const override;
void insertFromMimeData(const QMimeData *source) override;
QtConnectionOwner(QMetaObject::Connection connection = {});
QtConnectionOwner(QtConnectionOwner &&other);
QtConnectionOwner &operator=(QtConnectionOwner &&other);
~QtConnectionOwner();
private:
not_null<Window::Controller*> _controller;
base::lambda<bool(const QMimeData *data)> _insertFromMimeDataHook;
void disconnect();
QMetaObject::Connection _data;
};
class MessageLinksParser : private QObject {
public:
MessageLinksParser(not_null<Ui::InputField*> field);
const rpl::variable<QStringList> &list() const;
protected:
bool eventFilter(QObject *object, QEvent *event) override;
private:
struct LinkRange {
int start;
int length;
};
friend inline bool operator==(const LinkRange &a, const LinkRange &b) {
return (a.start == b.start) && (a.length == b.length);
}
friend inline bool operator!=(const LinkRange &a, const LinkRange &b) {
return !(a == b);
}
void parse();
void apply(const QString &text, const QVector<LinkRange> &ranges);
not_null<Ui::InputField*> _field;
rpl::variable<QStringList> _list;
int _lastLength = 0;
base::Timer _timer;
QtConnectionOwner _connection;
};

View File

@ -33,7 +33,7 @@ Draft::Draft(
}
Draft::Draft(
not_null<const Ui::FlatTextarea*> field,
not_null<const Ui::InputField*> field,
MsgId msgId,
bool previewCancelled,
mtpRequestId saveRequestId)

View File

@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
namespace Ui {
class FlatTextarea;
class InputField;
} // namespace Ui
namespace Data {
@ -25,7 +25,7 @@ struct Draft {
bool previewCancelled,
mtpRequestId saveRequestId = 0);
Draft(
not_null<const Ui::FlatTextarea*> field,
not_null<const Ui::InputField*> field,
MsgId msgId,
bool previewCancelled,
mtpRequestId saveRequestId = 0);

View File

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_types.h"
#include "data/data_document.h"
#include "ui/widgets/input_fields.h"
void AudioMsgId::setTypeFromAudio() {
if (_audio->isVoiceMessage() || _audio->isVideoMessage()) {
@ -21,24 +22,20 @@ void AudioMsgId::setTypeFromAudio() {
}
}
void MessageCursor::fillFrom(const QTextEdit *edit) {
QTextCursor c = edit->textCursor();
position = c.position();
anchor = c.anchor();
QScrollBar *s = edit->verticalScrollBar();
scroll = (s && (s->value() != s->maximum()))
? s->value()
: QFIXED_MAX;
void MessageCursor::fillFrom(not_null<const Ui::InputField*> field) {
const auto cursor = field->textCursor();
position = cursor.position();
anchor = cursor.anchor();
const auto top = field->scrollTop().current();
scroll = (top != field->scrollTopMax()) ? top : QFIXED_MAX;
}
void MessageCursor::applyTo(QTextEdit *edit) {
auto cursor = edit->textCursor();
void MessageCursor::applyTo(not_null<Ui::InputField*> field) {
auto cursor = field->textCursor();
cursor.setPosition(anchor, QTextCursor::MoveAnchor);
cursor.setPosition(position, QTextCursor::KeepAnchor);
edit->setTextCursor(cursor);
if (auto scrollbar = edit->verticalScrollBar()) {
scrollbar->setValue(scroll);
}
field->setTextCursor(cursor);
field->scrollTo(scroll);
}
HistoryItem *FileClickHandler::getActionItem() const {

View File

@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class HistoryItem;
using HistoryItemsList = std::vector<not_null<HistoryItem*>>;
namespace Ui {
class InputField;
} // namespace Ui
namespace Data {
struct UploadState {
@ -384,16 +388,16 @@ inline MsgId clientMsgId() {
struct MessageCursor {
MessageCursor() = default;
MessageCursor(int position, int anchor, int scroll)
: position(position)
, anchor(anchor)
, scroll(scroll) {
: position(position)
, anchor(anchor)
, scroll(scroll) {
}
MessageCursor(const QTextEdit *edit) {
fillFrom(edit);
MessageCursor(not_null<const Ui::InputField*> field) {
fillFrom(field);
}
void fillFrom(const QTextEdit *edit);
void applyTo(QTextEdit *edit);
void fillFrom(not_null<const Ui::InputField*> field);
void applyTo(not_null<Ui::InputField*> field);
int position = 0;
int anchor = 0;

View File

@ -131,19 +131,25 @@ historyViewsOutIcon: icon {{ "history_views", historyOutIconFg }};
historyViewsOutSelectedIcon: icon {{ "history_views", historyOutIconFgSelected }};
historyViewsInvertedIcon: icon {{ "history_views", historySendingInvertedIconFg }};
historyComposeField: FlatTextarea {
textColor: historyComposeAreaFg;
bgColor: historyComposeAreaBg;
align: align(left);
textMrg: margins(5px, 5px, 5px, 5px);
historyComposeField: InputField(defaultInputField) {
font: msgFont;
phColor: placeholderFg;
phFocusColor: placeholderFgActive;
phAlign: align(topleft);
phPos: point(2px, 0px);
phShift: 50px;
phDuration: 100;
textMargins: margins(0px, 0px, 0px, 0px);
textAlign: align(left);
textFg: historyComposeAreaFg;
textBg: historyComposeAreaBg;
heightMin: 36px;
heightMax: 72px;
placeholderFg: placeholderFg;
placeholderFgActive: placeholderFgActive;
placeholderFgError: placeholderFgActive;
placeholderMargins: margins(7px, 5px, 7px, 5px);
placeholderAlign: align(topleft);
placeholderScale: 0.;
placeholderFont: normalFont;
placeholderShift: -50px;
border: 0px;
borderActive: 0px;
duration: 100;
}
historyComposeFieldMaxHeight: 224px;
// historyMinHeight: 56px;

View File

@ -110,6 +110,12 @@ void ActivateWindowDelayed(not_null<Window::Controller*> controller) {
});
}
void InsertEmojiToField(not_null<Ui::InputField*> field, EmojiPtr emoji) {
if (!field->isHidden()) {
Ui::InsertEmojiAtCursor(field->textCursor(), emoji);
}
}
} // namespace
ReportSpamPanel::ReportSpamPanel(QWidget *parent) : TWidget(parent),
@ -406,6 +412,7 @@ HistoryWidget::HistoryWidget(
not_null<Window::Controller*> controller)
: Window::AbstractSectionWidget(parent, controller)
, _fieldBarCancel(this, st::historyReplyCancel)
, _previewTimer([=] { requestPreview(); })
, _topBar(this, controller)
, _scroll(this, st::historyScroll, false)
, _historyDown(_scroll, st::historyToDown)
@ -421,7 +428,11 @@ HistoryWidget::HistoryWidget(
, _botKeyboardShow(this, st::historyBotKeyboardShow)
, _botKeyboardHide(this, st::historyBotKeyboardHide)
, _botCommandStart(this, st::historyBotCommandStart)
, _field(this, controller, st::historyComposeField, langFactory(lng_message_ph))
, _field(
this,
st::historyComposeField,
Ui::InputField::Mode::MultiLine,
langFactory(lng_message_ph))
, _recordCancelWidth(st::historyRecordFont->width(lang(lng_record_cancel)))
, _a_recording(animation(this, &HistoryWidget::step_recording))
, _kbScroll(this, st::botKbScroll)
@ -444,21 +455,22 @@ HistoryWidget::HistoryWidget(
connect(_botStart, SIGNAL(clicked()), this, SLOT(onBotStart()));
connect(_joinChannel, SIGNAL(clicked()), this, SLOT(onJoinChannel()));
connect(_muteUnmute, SIGNAL(clicked()), this, SLOT(onMuteUnmute()));
connect(_field, SIGNAL(submitted(bool)), this, SLOT(onSend(bool)));
connect(_field, SIGNAL(submitted(bool)), this, SLOT(onSend()));
connect(_field, SIGNAL(cancelled()), this, SLOT(onCancel()));
connect(_field, SIGNAL(tabbed()), this, SLOT(onFieldTabbed()));
connect(_field, SIGNAL(resized()), this, SLOT(onFieldResize()));
connect(_field, SIGNAL(focused()), this, SLOT(onFieldFocused()));
connect(_field, SIGNAL(changed()), this, SLOT(onTextChange()));
connect(_field, SIGNAL(spacedReturnedPasted()), this, SLOT(onPreviewParse()));
connect(_field, SIGNAL(linksChanged()), this, SLOT(onPreviewCheck()));
connect(App::wnd()->windowHandle(), SIGNAL(visibleChanged(bool)), this, SLOT(onWindowVisibleChanged()));
connect(&_scrollTimer, SIGNAL(timeout()), this, SLOT(onScrollTimer()));
connect(_tabbedSelector, SIGNAL(emojiSelected(EmojiPtr)), _field, SLOT(onEmojiInsert(EmojiPtr)));
connect(
_tabbedSelector,
&TabbedSelector::emojiSelected,
_field,
[=](EmojiPtr emoji) { InsertEmojiToField(_field, emoji); });
connect(_tabbedSelector, SIGNAL(stickerSelected(DocumentData*)), this, SLOT(onStickerSend(DocumentData*)));
connect(_tabbedSelector, SIGNAL(photoSelected(PhotoData*)), this, SLOT(onPhotoSend(PhotoData*)));
connect(_tabbedSelector, SIGNAL(inlineResultSelected(InlineBots::Result*,UserData*)), this, SLOT(onInlineResultSend(InlineBots::Result*,UserData*)));
connect(&_previewTimer, SIGNAL(timeout()), this, SLOT(onPreviewTimeout()));
connect(Media::Capture::instance(), SIGNAL(error()), this, SLOT(onRecordError()));
connect(Media::Capture::instance(), SIGNAL(updated(quint16,qint32)), this, SLOT(onRecordUpdate(quint16,qint32)));
connect(Media::Capture::instance(), SIGNAL(done(QByteArray,VoiceWaveform,qint32)), this, SLOT(onRecordDone(QByteArray,VoiceWaveform,qint32)));
@ -481,9 +493,12 @@ HistoryWidget::HistoryWidget(
connect(&_saveDraftTimer, SIGNAL(timeout()), this, SLOT(onDraftSave()));
_saveCloudDraftTimer.setSingleShot(true);
connect(&_saveCloudDraftTimer, SIGNAL(timeout()), this, SLOT(onCloudDraftSave()));
connect(_field->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(onDraftSaveDelayed()));
connect(_field, SIGNAL(cursorPositionChanged()), this, SLOT(onDraftSaveDelayed()));
connect(_field, SIGNAL(cursorPositionChanged()), this, SLOT(onCheckFieldAutocomplete()), Qt::QueuedConnection);
_field->scrollTop().changes(
) | rpl::start_with_next([=] {
onDraftSaveDelayed();
}, _field->lifetime());
connect(_field->rawTextEdit(), SIGNAL(cursorPositionChanged()), this, SLOT(onDraftSaveDelayed()));
connect(_field->rawTextEdit(), SIGNAL(cursorPositionChanged()), this, SLOT(onCheckFieldAutocomplete()), Qt::QueuedConnection);
_fieldBarCancel->hide();
@ -498,20 +513,35 @@ HistoryWidget::HistoryWidget(
_historyDown->installEventFilter(this);
_unreadMentions->installEventFilter(this);
InitMessageField(_field);
_fieldAutocomplete->hide();
connect(_fieldAutocomplete, SIGNAL(mentionChosen(UserData*,FieldAutocomplete::ChooseMethod)), this, SLOT(onMentionInsert(UserData*)));
connect(_fieldAutocomplete, SIGNAL(hashtagChosen(QString,FieldAutocomplete::ChooseMethod)), this, SLOT(onHashtagOrBotCommandInsert(QString,FieldAutocomplete::ChooseMethod)));
connect(_fieldAutocomplete, SIGNAL(botCommandChosen(QString,FieldAutocomplete::ChooseMethod)), this, SLOT(onHashtagOrBotCommandInsert(QString,FieldAutocomplete::ChooseMethod)));
connect(_fieldAutocomplete, SIGNAL(stickerChosen(DocumentData*,FieldAutocomplete::ChooseMethod)), this, SLOT(onStickerSend(DocumentData*)));
connect(_fieldAutocomplete, SIGNAL(moderateKeyActivate(int,bool*)), this, SLOT(onModerateKeyActivate(int,bool*)));
_field->installEventFilter(_fieldAutocomplete);
_field->setInsertFromMimeDataHook([this](const QMimeData *data) {
return confirmSendingFiles(
data,
CompressConfirm::Auto,
data->text());
_fieldLinksParser = std::make_unique<MessageLinksParser>(_field);
_fieldLinksParser->list().changes(
) | rpl::start_with_next([=](QStringList &&parsed) {
_parsedLinks = std::move(parsed);
checkPreview();
}, lifetime());
_field->rawTextEdit()->installEventFilter(_fieldAutocomplete);
_field->setMimeDataHook([=](
not_null<const QMimeData*> data,
Ui::InputField::MimeAction action) {
if (action == Ui::InputField::MimeAction::Check) {
return canSendFiles(data);
} else if (action == Ui::InputField::MimeAction::Insert) {
return confirmSendingFiles(
data,
CompressConfirm::Auto,
data->text());
}
Unexpected("action in MimeData hook.");
});
_emojiSuggestions.create(this, _field.data());
_emojiSuggestions.create(this, _field->rawTextEdit());
_emojiSuggestions->setReplaceCallback([=](
int from,
int till,
@ -622,7 +652,7 @@ HistoryWidget::HistoryWidget(
subscribe(Notify::PeerUpdated(), Notify::PeerUpdatedHandler(changes, [this](const Notify::PeerUpdate &update) {
if (update.peer == _peer) {
if (update.flags & UpdateFlag::ChannelRightsChanged) {
onPreviewCheck();
checkPreview();
}
if (update.flags & UpdateFlag::UnreadMentionsChanged) {
updateUnreadMentionsVisibility();
@ -995,36 +1025,41 @@ void HistoryWidget::onHashtagOrBotCommandInsert(
}
void HistoryWidget::updateInlineBotQuery() {
UserData *bot = nullptr;
QString inlineBotUsername;
QString query = _field->getInlineBotQuery(&bot, &inlineBotUsername);
if (inlineBotUsername != _inlineBotUsername) {
_inlineBotUsername = inlineBotUsername;
const auto query = ParseInlineBotQuery(_field);
if (_inlineBotUsername != query.username) {
_inlineBotUsername = query.username;
if (_inlineBotResolveRequestId) {
// Notify::inlineBotRequesting(false);
MTP::cancel(_inlineBotResolveRequestId);
_inlineBotResolveRequestId = 0;
}
if (bot == Ui::LookingUpInlineBot) {
_inlineBot = Ui::LookingUpInlineBot;
if (query.lookingUpBot) {
_inlineBot = nullptr;
_inlineLookingUpBot = true;
// Notify::inlineBotRequesting(true);
_inlineBotResolveRequestId = MTP::send(MTPcontacts_ResolveUsername(MTP_string(_inlineBotUsername)), rpcDone(&HistoryWidget::inlineBotResolveDone), rpcFail(&HistoryWidget::inlineBotResolveFail, _inlineBotUsername));
return;
_inlineBotResolveRequestId = MTP::send(
MTPcontacts_ResolveUsername(MTP_string(_inlineBotUsername)),
rpcDone(&HistoryWidget::inlineBotResolveDone),
rpcFail(
&HistoryWidget::inlineBotResolveFail,
_inlineBotUsername));
} else {
applyInlineBotQuery(query.bot, query.query);
}
} else if (bot == Ui::LookingUpInlineBot) {
if (_inlineBot == Ui::LookingUpInlineBot) {
return;
} else if (query.lookingUpBot) {
if (!_inlineLookingUpBot) {
applyInlineBotQuery(_inlineBot, query.query);
}
bot = _inlineBot;
} else {
applyInlineBotQuery(query.bot, query.query);
}
applyInlineBotQuery(bot, query);
}
void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) {
if (bot) {
if (_inlineBot != bot) {
_inlineBot = bot;
_inlineLookingUpBot = false;
inlineBotChanged();
}
if (!_inlineResults) {
@ -1125,15 +1160,21 @@ void HistoryWidget::onTextChange() {
}
_saveCloudDraftTimer.stop();
if (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) return;
if (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) {
return;
}
_saveDraftText = true;
onDraftSave(true);
}
void HistoryWidget::onDraftSaveDelayed() {
if (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) return;
if (!_field->textCursor().anchor() && !_field->textCursor().position() && !_field->verticalScrollBar()->value()) {
if (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) {
return;
}
if (!_field->textCursor().anchor()
&& !_field->textCursor().position()
&& !_field->scrollTop().current()) {
if (!Local::hasDraftCursors(_peer->id)) {
return;
}
@ -1161,7 +1202,7 @@ void HistoryWidget::saveFieldToHistoryLocalDraft() {
if (_editMsgId) {
_history->setEditDraft(std::make_unique<Data::Draft>(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId));
} else {
if (_replyToId || !_field->isEmpty()) {
if (_replyToId || !_field->empty()) {
_history->setLocalDraft(std::make_unique<Data::Draft>(_field, _replyToId, _previewCancelled));
} else {
_history->clearLocalDraft();
@ -1595,12 +1636,12 @@ void HistoryWidget::fastShowAtEnd(not_null<History*> history) {
}
}
void HistoryWidget::applyDraft(bool parseLinks, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction) {
void HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
auto draft = _history ? _history->draft() : nullptr;
auto fieldAvailable = canWriteMessage();
if (!draft || (!_history->editDraft() && !fieldAvailable)) {
auto fieldWillBeHiddenAfterEdit = (!fieldAvailable && _editMsgId != 0);
clearFieldText(0, undoHistoryAction);
clearFieldText(0, fieldHistoryAction);
_field->setFocus();
_replyEditMsg = nullptr;
_editMsgId = _replyToId = 0;
@ -1612,7 +1653,7 @@ void HistoryWidget::applyDraft(bool parseLinks, Ui::FlatTextarea::UndoHistoryAct
}
_textUpdateEvents = 0;
setFieldText(draft->textWithTags, 0, undoHistoryAction);
setFieldText(draft->textWithTags, 0, fieldHistoryAction);
_field->setFocus();
draft->cursor.applyTo(_field);
_textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;
@ -1628,9 +1669,6 @@ void HistoryWidget::applyDraft(bool parseLinks, Ui::FlatTextarea::UndoHistoryAct
updateControlsVisibility();
updateControlsGeometry();
if (parseLinks) {
onPreviewParse();
}
if (_editMsgId || _replyToId) {
updateReplyEditTexts();
if (!_replyEditMsg) {
@ -1644,7 +1682,7 @@ void HistoryWidget::applyDraft(bool parseLinks, Ui::FlatTextarea::UndoHistoryAct
void HistoryWidget::applyCloudDraft(History *history) {
if (_history == history && !_editMsgId) {
applyDraft(true, Ui::FlatTextarea::AddToUndoHistory);
applyDraft(Ui::InputField::HistoryAction::NewEntry);
updateControlsVisibility();
updateControlsGeometry();
@ -1849,15 +1887,12 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re
_migrated->clearEditDraft();
_history->takeLocalDraft(_migrated);
}
applyDraft(false);
applyDraft();
_send->finishAnimating();
_tabbedSelector->showMegagroupSet(_peer->asMegagroup());
updateControlsGeometry();
if (!_previewCancelled) {
onPreviewParse();
}
connect(_scroll, SIGNAL(geometryChanged()), _list, SLOT(onParentGeometryChanged()));
@ -1910,11 +1945,11 @@ void HistoryWidget::clearAllLoadRequests() {
}
void HistoryWidget::updateFieldSubmitSettings() {
auto settings = Ui::FlatTextarea::SubmitSettings::Enter;
auto settings = Ui::InputField::SubmitSettings::Enter;
if (_isInlineBot) {
settings = Ui::FlatTextarea::SubmitSettings::None;
settings = Ui::InputField::SubmitSettings::None;
} else if (cCtrlEnter()) {
settings = Ui::FlatTextarea::SubmitSettings::CtrlEnter;
settings = Ui::InputField::SubmitSettings::CtrlEnter;
}
_field->setSubmitSettings(settings);
}
@ -2819,9 +2854,14 @@ void HistoryWidget::checkReplyReturns() {
void HistoryWidget::onInlineBotCancel() {
auto &textWithTags = _field->getTextWithTags();
if (textWithTags.text.size() > _inlineBotUsername.size() + 2) {
setFieldText({ '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, Ui::FlatTextarea::AddToUndoHistory);
setFieldText(
{ '@' + _inlineBotUsername + ' ', TextWithTags::Tags() },
TextUpdateEvent::SaveDraft,
Ui::InputField::HistoryAction::NewEntry);
} else {
clearFieldText(TextUpdateEvent::SaveDraft, Ui::FlatTextarea::AddToUndoHistory);
clearFieldText(
TextUpdateEvent::SaveDraft,
Ui::InputField::HistoryAction::NewEntry);
}
}
@ -2938,7 +2978,7 @@ void HistoryWidget::hideSelectorControlsAnimated() {
}
}
void HistoryWidget::onSend(bool ctrlShiftEnter) {
void HistoryWidget::onSend() {
if (!_history) return;
if (_editMsgId) {
@ -3525,7 +3565,10 @@ bool HistoryWidget::insertBotCommand(const QString &cmd) {
cur.movePosition(QTextCursor::End);
_field->setTextCursor(cur);
} else {
setFieldText({ toInsert, TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, Ui::FlatTextarea::AddToUndoHistory);
setFieldText(
{ toInsert, TextWithTags::Tags() },
TextUpdateEvent::SaveDraft,
Ui::InputField::HistoryAction::NewEntry);
_field->setFocus();
return true;
}
@ -3587,33 +3630,29 @@ bool HistoryWidget::hasSilentToggle() const {
&& !_peer->notifySettingsUnknown();
}
void HistoryWidget::inlineBotResolveDone(const MTPcontacts_ResolvedPeer &result) {
void HistoryWidget::inlineBotResolveDone(
const MTPcontacts_ResolvedPeer &result) {
Expects(result.type() == mtpc_contacts_resolvedPeer);
_inlineBotResolveRequestId = 0;
const auto &data = result.c_contacts_resolvedPeer();
// Notify::inlineBotRequesting(false);
UserData *resolvedBot = nullptr;
if (result.type() == mtpc_contacts_resolvedPeer) {
const auto &d(result.c_contacts_resolvedPeer());
resolvedBot = App::feedUsers(d.vusers);
if (resolvedBot) {
if (!resolvedBot->botInfo || resolvedBot->botInfo->inlinePlaceholder.isEmpty()) {
resolvedBot = nullptr;
const auto resolvedBot = [&]() -> UserData* {
if (const auto result = App::feedUsers(data.vusers)) {
if (result->botInfo
&& !result->botInfo->inlinePlaceholder.isEmpty()) {
return result;
}
}
App::feedChats(d.vchats);
}
return nullptr;
}();
App::feedChats(data.vchats);
UserData *bot = nullptr;
QString inlineBotUsername;
auto query = _field->getInlineBotQuery(&bot, &inlineBotUsername);
if (inlineBotUsername == _inlineBotUsername) {
if (bot == Ui::LookingUpInlineBot) {
bot = resolvedBot;
}
} else {
bot = nullptr;
}
if (bot) {
applyInlineBotQuery(bot, query);
const auto query = ParseInlineBotQuery(_field);
if (_inlineBotUsername == query.username) {
applyInlineBotQuery(
query.lookingUpBot ? resolvedBot : query.bot,
query.query);
} else {
clearInlineBot();
}
@ -3657,11 +3696,11 @@ bool HistoryWidget::isMuteUnmute() const {
}
bool HistoryWidget::showRecordButton() const {
return Media::Capture::instance()->available() && !_field->hasSendText() && !readyToForward() && !_editMsgId;
return Media::Capture::instance()->available() && !HasSendText(_field) && !readyToForward() && !_editMsgId;
}
bool HistoryWidget::showInlineBotCancel() const {
return _inlineBot && (_inlineBot != Ui::LookingUpInlineBot);
return _inlineBot && !_inlineLookingUpBot;
}
void HistoryWidget::updateSendButtonType() {
@ -3683,7 +3722,7 @@ bool HistoryWidget::updateCmdStartShown() {
bool cmdStartShown = false;
if (_history && _peer && ((_peer->isChat() && _peer->asChat()->botStatus > 0) || (_peer->isMegagroup() && _peer->asChannel()->mgInfo->botStatus > 0) || (_peer->isUser() && _peer->asUser()->botInfo))) {
if (!isBotStart() && !isBlocked() && !_keyboard->hasMarkup() && !_keyboard->forceReply()) {
if (!_field->hasSendText()) {
if (!HasSendText(_field)) {
cmdStartShown = true;
}
}
@ -3791,7 +3830,10 @@ void HistoryWidget::onKbToggle(bool manual) {
}
void HistoryWidget::onCmdStart() {
setFieldText({ qsl("/"), TextWithTags::Tags() }, 0, Ui::FlatTextarea::AddToUndoHistory);
setFieldText(
{ qsl("/"), TextWithTags::Tags() },
0,
Ui::InputField::HistoryAction::NewEntry);
}
void HistoryWidget::setMembersShowAreaActive(bool active) {
@ -3974,8 +4016,9 @@ void HistoryWidget::updateFieldSize() {
void HistoryWidget::clearInlineBot() {
if (_inlineBot) {
_inlineBot = nullptr;
_inlineLookingUpBot = false;
inlineBotChanged();
_field->finishPlaceholder();
_field->finishAnimating();
}
if (_inlineResults) {
_inlineResults->clearInlineBot();
@ -4006,24 +4049,39 @@ void HistoryWidget::onFieldFocused() {
}
void HistoryWidget::onCheckFieldAutocomplete() {
if (!_history || _a_show.animating()) return;
auto start = false;
auto isInlineBot = _inlineBot && (_inlineBot != Ui::LookingUpInlineBot);
auto query = isInlineBot ? QString() : _field->getMentionHashtagBotCommandPart(start);
if (!query.isEmpty()) {
if (query.at(0) == '#' && cRecentWriteHashtags().isEmpty() && cRecentSearchHashtags().isEmpty()) Local::readRecentHashtagsAndBots();
if (query.at(0) == '@' && cRecentInlineBots().isEmpty()) Local::readRecentHashtagsAndBots();
if (query.at(0) == '/' && _peer->isUser() && !_peer->asUser()->botInfo) return;
if (!_history || _a_show.animating()) {
return;
}
_fieldAutocomplete->showFiltered(_peer, query, start);
const auto isInlineBot = _inlineBot && !_inlineLookingUpBot;
const auto autocomplete = isInlineBot
? AutocompleteQuery()
: ParseMentionHashtagBotCommandQuery(_field);
if (!autocomplete.query.isEmpty()) {
if (autocomplete.query[0] == '#'
&& cRecentWriteHashtags().isEmpty()
&& cRecentSearchHashtags().isEmpty()) {
Local::readRecentHashtagsAndBots();
} else if (autocomplete.query[0] == '@'
&& cRecentInlineBots().isEmpty()) {
Local::readRecentHashtagsAndBots();
} else if (autocomplete.query[0] == '/'
&& _peer->isUser()
&& !_peer->asUser()->botInfo) {
return;
}
}
_fieldAutocomplete->showFiltered(
_peer,
autocomplete.query,
autocomplete.fromStart);
}
void HistoryWidget::updateFieldPlaceholder() {
if (_editMsgId) {
_field->setPlaceholder(langFactory(lng_edit_message_text));
} else {
if (_inlineBot && _inlineBot != Ui::LookingUpInlineBot) {
if (_inlineBot && !_inlineLookingUpBot) {
auto text = _inlineBot->botInfo->inlinePlaceholder.mid(1);
_field->setPlaceholder([text] { return text; }, _inlineBot->username.size() + 2);
} else {
@ -4076,7 +4134,7 @@ bool HistoryWidget::confirmSendingFiles(const QStringList &files) {
return confirmSendingFiles(files, CompressConfirm::Auto);
}
bool HistoryWidget::confirmSendingFiles(const QMimeData *data) {
bool HistoryWidget::confirmSendingFiles(not_null<const QMimeData*> data) {
return confirmSendingFiles(data, CompressConfirm::Auto);
}
@ -4157,8 +4215,29 @@ bool HistoryWidget::confirmSendingFiles(
insertTextOnCancel);
}
bool HistoryWidget::canSendFiles(not_null<const QMimeData*> data) const {
if (!canWriteMessage()) {
return false;
}
if (const auto urls = data->urls(); !urls.empty()) {
if (ranges::find_if(
urls,
[](const QUrl &url) { return !url.isLocalFile(); }
) == urls.end()) {
return true;
}
}
if (data->hasImage()) {
const auto image = qvariant_cast<QImage>(data->imageData());
if (!image.isNull()) {
return true;
}
}
return false;
}
bool HistoryWidget::confirmSendingFiles(
const QMimeData *data,
not_null<const QMimeData*> data,
CompressConfirm compressed,
const QString &insertTextOnCancel) {
if (!canWriteMessage()) {
@ -4961,7 +5040,7 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) {
if (_keyboard->singleUse() && _keyboard->hasMarkup() && _keyboard->forMsgId() == FullMsgId(_channel, _history->lastKeyboardId) && _history->lastKeyboardUsed) {
_history->lastKeyboardHiddenId = _history->lastKeyboardId;
}
if (!isBotStart() && !isBlocked() && _canSendMessages && (wasVisible || (_replyToId && _replyEditMsg) || (!_field->hasSendText() && !kbWasHidden()))) {
if (!isBotStart() && !isBlocked() && _canSendMessages && (wasVisible || (_replyToId && _replyEditMsg) || (!HasSendText(_field) && !kbWasHidden()))) {
if (!_a_show.animating()) {
if (hasMarkup) {
_kbScroll->show();
@ -5155,7 +5234,7 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) {
: nullptr;
if (item
&& item->allowsEdit(unixtime())
&& _field->isEmpty()
&& _field->empty()
&& !_editMsgId
&& !_replyToId) {
editMessage(item);
@ -5638,21 +5717,30 @@ void HistoryWidget::sendExistingPhoto(
_field->setFocus();
}
void HistoryWidget::setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction) {
void HistoryWidget::setFieldText(
const TextWithTags &textWithTags,
TextUpdateEvents events,
FieldHistoryAction fieldHistoryAction) {
_textUpdateEvents = events;
_field->setTextWithTags(textWithTags, undoHistoryAction);
_field->moveCursor(QTextCursor::End);
_textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;
_field->setTextWithTags(textWithTags, fieldHistoryAction);
auto cursor = _field->textCursor();
cursor.movePosition(QTextCursor::End);
_field->setTextCursor(cursor);
_textUpdateEvents = TextUpdateEvent::SaveDraft
| TextUpdateEvent::SendTyping;
_previewCancelled = false;
_previewData = nullptr;
if (_previewRequest) {
MTP::cancel(_previewRequest);
_previewRequest = 0;
}
MTP::cancel(base::take(_previewRequest));
_previewLinks.clear();
}
void HistoryWidget::clearFieldText(
TextUpdateEvents events,
FieldHistoryAction fieldHistoryAction) {
setFieldText(TextWithTags(), events, fieldHistoryAction);
}
void HistoryWidget::replyToMessage(FullMsgId itemId) {
if (const auto item = App::histItemById(itemId)) {
replyToMessage(item);
@ -5739,7 +5827,7 @@ void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
_send->clearState();
}
if (!_editMsgId) {
if (_replyToId || !_field->isEmpty()) {
if (_replyToId || !_field->empty()) {
_history->setLocalDraft(std::make_unique<Data::Draft>(_field, _replyToId, _previewCancelled));
} else {
_history->clearLocalDraft();
@ -5761,7 +5849,7 @@ void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
item->id,
cursor,
false));
applyDraft(false);
applyDraft();
_previewData = nullptr;
if (const auto media = item->media()) {
@ -5770,9 +5858,6 @@ void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
updatePreview();
}
}
if (!_previewData) {
onPreviewParse();
}
updateBotKeyboard();
@ -5996,12 +6081,7 @@ void HistoryWidget::previewCancel() {
}
}
void HistoryWidget::onPreviewParse() {
if (_previewCancelled) return;
_field->parseLinks();
}
void HistoryWidget::onPreviewCheck() {
void HistoryWidget::checkPreview() {
auto previewRestricted = [this] {
if (auto megagroup = _peer ? _peer->asMegagroup() : nullptr) {
if (megagroup->restricted(ChannelRestriction::f_embed_links)) {
@ -6017,15 +6097,16 @@ void HistoryWidget::onPreviewCheck() {
update();
return;
}
auto linksList = _field->linksList();
auto newLinks = linksList.join(' ');
if (newLinks != _previewLinks) {
const auto newLinks = _parsedLinks.join(' ');
if (_previewLinks != newLinks) {
MTP::cancel(base::take(_previewRequest));
_previewLinks = newLinks;
if (_previewLinks.isEmpty()) {
if (_previewData && _previewData->pendingTill >= 0) previewCancel();
if (_previewData && _previewData->pendingTill >= 0) {
previewCancel();
}
} else {
PreviewCache::const_iterator i = _previewCache.constFind(_previewLinks);
const auto i = _previewCache.constFind(_previewLinks);
if (i == _previewCache.cend()) {
_previewRequest = MTP::send(
MTPmessages_GetWebPagePreview(
@ -6043,17 +6124,18 @@ void HistoryWidget::onPreviewCheck() {
}
}
void HistoryWidget::onPreviewTimeout() {
if (_previewData
&& (_previewData->pendingTill > 0)
&& !_previewLinks.isEmpty()) {
_previewRequest = MTP::send(
MTPmessages_GetWebPagePreview(
MTP_flags(0),
MTP_string(_previewLinks),
MTPnullEntities),
rpcDone(&HistoryWidget::gotPreview, _previewLinks));
void HistoryWidget::requestPreview() {
if (!_previewData
|| (_previewData->pendingTill <= 0)
|| _previewLinks.isEmpty()) {
return;
}
_previewRequest = MTP::send(
MTPmessages_GetWebPagePreview(
MTP_flags(0),
MTP_string(_previewLinks),
MTPnullEntities),
rpcDone(&HistoryWidget::gotPreview, _previewLinks));
}
void HistoryWidget::gotPreview(QString links, const MTPMessageMedia &result, mtpRequestId req) {
@ -6084,7 +6166,7 @@ void HistoryWidget::gotPreview(QString links, const MTPMessageMedia &result, mtp
}
void HistoryWidget::updatePreview() {
_previewTimer.stop();
_previewTimer.cancel();
if (_previewData && _previewData->pendingTill >= 0) {
_fieldBarCancel->show();
updateMouseTracking();
@ -6103,9 +6185,8 @@ void HistoryWidget::updatePreview() {
TextUtilities::Clean(linkText),
Ui::DialogTextOptions());
int32 t = (_previewData->pendingTill - unixtime()) * 1000;
if (t <= 0) t = 1;
_previewTimer.start(t);
const auto timeout = (_previewData->pendingTill - unixtime());
_previewTimer.callOnce(std::max(timeout, 0) * TimeMs(1000));
} else {
QString title, desc;
if (_previewData->siteName.isEmpty()) {

View File

@ -22,6 +22,7 @@ struct FileMediaInformation;
struct SendingAlbum;
enum class SendMediaType;
enum class CompressConfirm;
class MessageLinksParser;
namespace InlineBots {
namespace Layout {
@ -176,6 +177,8 @@ class HistoryWidget final : public Window::AbstractSectionWidget, public RPCSend
Q_OBJECT
public:
using FieldHistoryAction = Ui::InputField::HistoryAction;
HistoryWidget(QWidget *parent, not_null<Window::Controller*> controller);
void start();
@ -219,7 +222,7 @@ public:
void updateStickersByEmoji();
bool confirmSendingFiles(const QStringList &files);
bool confirmSendingFiles(const QMimeData *data);
bool confirmSendingFiles(not_null<const QMimeData*> data);
void sendFileConfirmed(const std::shared_ptr<FileLoadResult> &file);
void updateControlsVisibility();
@ -297,7 +300,8 @@ public:
void updateBotKeyboard(History *h = nullptr, bool force = false);
void fastShowAtEnd(not_null<History*> history);
void applyDraft(bool parseLinks = true, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction = Ui::FlatTextarea::ClearUndoHistory);
void applyDraft(
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
void showHistory(const PeerId &peer, MsgId showAtMsgId, bool reload = false);
void clearDelayedShowAt();
void clearAllLoadRequests();
@ -374,10 +378,6 @@ public slots:
void onPinnedHide();
void onFieldBarCancel();
void onPreviewParse();
void onPreviewCheck();
void onPreviewTimeout();
void onPhotoUploaded(const FullMsgId &msgId, bool silent, const MTPInputFile &file);
void onDocumentUploaded(const FullMsgId &msgId, bool silent, const MTPInputFile &file);
void onThumbDocumentUploaded(const FullMsgId &msgId, bool silent, const MTPInputFile &file, const MTPInputFile &thumb);
@ -433,7 +433,7 @@ public slots:
void preloadHistoryIfNeeded();
private slots:
void onSend(bool ctrlShiftEnter = false);
void onSend();
void onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::ChooseMethod method);
void onMentionInsert(UserData *user);
@ -490,6 +490,7 @@ private:
void unreadMentionsAnimationFinish();
void sendButtonClicked();
bool canSendFiles(not_null<const QMimeData*> data) const;
bool confirmSendingFiles(
const QStringList &files,
CompressConfirm compressed,
@ -500,7 +501,7 @@ private:
CompressConfirm compressed,
const QString &insertTextOnCancel = QString());
bool confirmSendingFiles(
const QMimeData *data,
not_null<const QMimeData*> data,
CompressConfirm compressed,
const QString &insertTextOnCancel = QString());
bool confirmSendingFiles(
@ -607,14 +608,20 @@ private:
void saveEditMsgDone(History *history, const MTPUpdates &updates, mtpRequestId req);
bool saveEditMsgFail(History *history, const RPCError &error, mtpRequestId req);
static const mtpRequestId ReportSpamRequestNeeded = -1;
DBIPeerReportSpamStatus _reportSpamStatus = dbiprsUnknown;
mtpRequestId _reportSpamSettingRequestId = ReportSpamRequestNeeded;
void updateReportSpamStatus();
void requestReportSpamSetting();
void reportSpamSettingDone(const MTPPeerSettings &result, mtpRequestId req);
bool reportSpamSettingFail(const RPCError &error, mtpRequestId req);
void checkPreview();
void requestPreview();
void gotPreview(QString links, const MTPMessageMedia &media, mtpRequestId req);
static const mtpRequestId ReportSpamRequestNeeded = -1;
DBIPeerReportSpamStatus _reportSpamStatus = dbiprsUnknown;
mtpRequestId _reportSpamSettingRequestId = ReportSpamRequestNeeded;
QStringList _parsedLinks;
QString _previewLinks;
WebPageData *_previewData = nullptr;
typedef QMap<QString, WebPageId> PreviewCache;
@ -622,9 +629,8 @@ private:
mtpRequestId _previewRequest = 0;
Text _previewTitle;
Text _previewDescription;
SingleTimer _previewTimer;
base::Timer _previewTimer;
bool _previewCancelled = false;
void gotPreview(QString links, const MTPMessageMedia &media, mtpRequestId req);
bool _replyForwardPressed = false;
@ -695,10 +701,13 @@ private:
void writeDrafts(Data::Draft **localDraft, Data::Draft **editDraft);
void writeDrafts(History *history);
void setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events = 0, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction = Ui::FlatTextarea::ClearUndoHistory);
void clearFieldText(TextUpdateEvents events = 0, Ui::FlatTextarea::UndoHistoryAction undoHistoryAction = Ui::FlatTextarea::ClearUndoHistory) {
setFieldText(TextWithTags(), events, undoHistoryAction);
}
void setFieldText(
const TextWithTags &textWithTags,
TextUpdateEvents events = 0,
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
void clearFieldText(
TextUpdateEvents events = 0,
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
HistoryItem *getItemFromHistoryOrMigrated(MsgId genericMsgId) const;
void animatedScrollToItem(MsgId msgId);
@ -759,9 +768,11 @@ private:
object_ptr<Ui::HistoryDownButton> _unreadMentions;
object_ptr<FieldAutocomplete> _fieldAutocomplete;
std::unique_ptr<MessageLinksParser> _fieldLinksParser;
UserData *_inlineBot = nullptr;
QString _inlineBotUsername;
bool _inlineLookingUpBot = false;
mtpRequestId _inlineBotResolveRequestId = 0;
bool _isInlineBot = false;
void inlineBotResolveDone(const MTPcontacts_ResolvedPeer &result);
@ -796,7 +807,7 @@ private:
object_ptr<Ui::IconButton> _botCommandStart;
object_ptr<Ui::SilentToggle> _silent = { nullptr };
bool _cmdStartShown = false;
object_ptr<MessageField> _field;
object_ptr<Ui::InputField> _field;
bool _recording = false;
bool _inField = false;
bool _inReplyEditForward = false;

View File

@ -7,9 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/info_wrap_widget.h"
#include <rpl/flatten_latest.h>
#include <rpl/take.h>
#include <rpl/combine.h>
#include "info/profile/info_profile_widget.h"
#include "info/profile/info_profile_values.h"
#include "info/media/info_media_widget.h"

View File

@ -171,7 +171,14 @@ mtpBuffer TcpConnection::handleResponse(const char *packet, uint32 length) {
const mtpPrime *packetdata = reinterpret_cast<const mtpPrime*>(packet + (length - len));
TCP_LOG(("TCP Info: packet received, size = %1").arg(size * sizeof(mtpPrime)));
if (size == 1) {
LOG(("TCP Error: error packet received, code = %1").arg(*packetdata));
LOG(("TCP Error: "
"error packet received, endpoint: '%1:%2', "
"protocolDcId: %3, secret_len: %4, code = %5"
).arg(_address.isEmpty() ? ("proxy_" + _proxy.host) : _address
).arg(_address.isEmpty() ? _proxy.port : _port
).arg(_protocolDcId
).arg(_protocolSecret.size()
).arg(*packetdata));
return mtpBuffer(1, *packetdata);
}

View File

@ -56,7 +56,7 @@ private:
void socketError(QAbstractSocket::SocketError e);
void handleTimeout();
static mtpBuffer handleResponse(const char *packet, uint32 length);
mtpBuffer handleResponse(const char *packet, uint32 length);
static void handleError(QAbstractSocket::SocketError e, QTcpSocket &sock);
static uint32 fourCharsToUInt(char ch1, char ch2, char ch3, char ch4) {
char ch[4] = { ch1, ch2, ch3, ch4 };

View File

@ -461,4 +461,36 @@ TEST_CASE("basic operators tests", "[rpl::operators]") {
}
REQUIRE(*sum == "012done012done012done");
}
SECTION("skip test") {
auto sum = std::make_shared<std::string>("");
{
rpl::lifetime lifetime;
ints(10) | skip(5)
| start_with_next_done([=](int value) {
*sum += std::to_string(value);
}, [=] {
*sum += "done";
}, lifetime);
}
{
rpl::lifetime lifetime;
ints(3) | skip(3)
| start_with_next_done([=](int value) {
*sum += std::to_string(value);
}, [=] {
*sum += "done";
}, lifetime);
}
{
rpl::lifetime lifetime;
ints(3) | skip(10)
| start_with_next_done([=](int value) {
*sum += std::to_string(value);
}, [=] {
*sum += "done";
}, lifetime);
}
REQUIRE(*sum == "56789donedonedone");
}
}

View File

@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <rpl/never.h>
#include <rpl/take.h>
#include <rpl/skip.h>
#include <rpl/then.h>
#include <rpl/deferred.h>
#include <rpl/map.h>

View File

@ -0,0 +1,61 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace rpl {
namespace details {
class skip_helper {
public:
skip_helper(int count) : _count(count) {
}
template <
typename Value,
typename Error,
typename Generator>
auto operator()(producer<Value, Error, Generator> &&initial) {
return make_producer<Value, Error>([
initial = std::move(initial),
skipping = _count
](const auto &consumer) mutable {
auto count = consumer.template make_state<int>(skipping);
auto initial_consumer = make_consumer<Value, Error>(
[consumer, count](auto &&value) {
if (*count) {
--*count;
} else {
consumer.put_next_forward(
std::forward<decltype(value)>(value));
}
}, [consumer](auto &&error) {
consumer.put_error_forward(
std::forward<decltype(error)>(error));
}, [consumer] {
consumer.put_done();
});
consumer.add_lifetime(initial_consumer.terminator());
return std::move(initial).start_existing(initial_consumer);
});
}
private:
int _count = 0;
};
} // namespace details
inline auto skip(int count)
-> details::skip_helper {
Expects(count >= 0);
return details::skip_helper(count);
}
} // namespace rpl

View File

@ -53,11 +53,12 @@ private:
};
} // namespace details
inline auto take(int count)
-> details::take_helper {
Expects(count >= 0);
return details::take_helper(count);
}
} // namespace rpl

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ class UserData;
namespace Ui {
static UserData * const LookingUpInlineBot = SharedMemoryLocation<UserData, 0>();
void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji);
struct InstantReplaces {
struct Node {
@ -31,231 +31,6 @@ struct InstantReplaces {
};
class FlatTextarea : public TWidgetHelper<QTextEdit>, protected base::Subscriber {
Q_OBJECT
public:
using TagList = TextWithTags::Tags;
FlatTextarea(QWidget *parent, const style::FlatTextarea &st, base::lambda<QString()> placeholderFactory = base::lambda<QString()>(), const QString &val = QString(), const TagList &tags = TagList());
void setMaxLength(int maxLength);
void setMinHeight(int minHeight);
void setMaxHeight(int maxHeight);
void setInstantReplaces(const InstantReplaces &replaces);
void enableInstantReplaces(bool enabled);
void commitInstantReplacement(
int from,
int till,
const QString &with,
base::optional<QString> checkOriginal = base::none);
void setPlaceholder(base::lambda<QString()> placeholderFactory, int afterSymbols = 0);
void updatePlaceholder();
void finishPlaceholder();
QRect getTextRect() const;
int fakeMargin() const;
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
EmojiPtr getSingleEmoji() const;
QString getMentionHashtagBotCommandPart(bool &start) const;
// Get the current inline bot and request string for it.
// The *outInlineBot can be filled by LookingUpInlineBot shared ptr.
// In that case the caller should lookup the bot by *outInlineBotUsername.
QString getInlineBotQuery(UserData **outInlineBot, QString *outInlineBotUsername) const;
void removeSingleEmoji();
bool hasText() const;
bool isUndoAvailable() const;
bool isRedoAvailable() const;
void parseLinks();
QStringList linksList() const;
void insertFromMimeData(const QMimeData *source) override;
QMimeData *createMimeDataFromSelection() const override;
enum class SubmitSettings {
None,
Enter,
CtrlEnter,
Both,
};
void setSubmitSettings(SubmitSettings settings);
const TextWithTags &getTextWithTags() const {
return _lastTextWithTags;
}
TextWithTags getTextWithTagsPart(int start, int end = -1);
void insertTag(const QString &text, QString tagId = QString());
bool isEmpty() const {
return _lastTextWithTags.text.isEmpty();
}
enum UndoHistoryAction {
AddToUndoHistory,
MergeWithUndoHistory,
ClearUndoHistory
};
void setTextWithTags(const TextWithTags &textWithTags, UndoHistoryAction undoHistoryAction = AddToUndoHistory);
// If you need to make some preparations of tags before putting them to QMimeData
// (and then to clipboard or to drag-n-drop object), here is a strategy for that.
class TagMimeProcessor {
public:
virtual QString mimeTagFromTag(const QString &tagId) = 0;
virtual QString tagFromMimeTag(const QString &mimeTag) = 0;
virtual ~TagMimeProcessor() {
}
};
void setTagMimeProcessor(std::unique_ptr<TagMimeProcessor> &&processor);
public slots:
void onTouchTimer();
void onDocumentContentsChange(int position, int charsRemoved, int charsAdded);
void onDocumentContentsChanged();
void onUndoAvailable(bool avail);
void onRedoAvailable(bool avail);
signals:
void resized();
void changed();
void submitted(bool ctrlShiftEnter);
void cancelled();
void tabbed();
void spacedReturnedPasted();
void linksChanged();
protected:
bool viewportEvent(QEvent *e) override;
void touchEvent(QTouchEvent *e);
void paintEvent(QPaintEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void focusOutEvent(QFocusEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void dropEvent(QDropEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
virtual void correctValue(
const QString &was,
QString &now,
TagList &nowTags) {
}
void insertEmoji(EmojiPtr emoji, QTextCursor c);
QVariant loadResource(int type, const QUrl &name) override;
void checkContentHeight();
private:
void updatePalette();
void refreshPlaceholder();
// "start" and "end" are in coordinates of text where emoji are replaced
// by ObjectReplacementCharacter. If "end" = -1 means get text till the end.
QString getTextPart(int start, int end, TagList *outTagsList, bool *outTagsChanged = nullptr) const;
void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const;
// After any characters added we must postprocess them. This includes:
// 1. Replacing font family to semibold for ~ characters, if we used Open Sans 13px.
// 2. Replacing font family from semibold for all non-~ characters, if we used ...
// 3. Replacing emoji code sequences by ObjectReplacementCharacters with emoji pics.
// 4. Interrupting tags in which the text was inserted by any char except a letter.
// 5. Applying tags from "_insertedTags" in case we pasted text with tags, not just text.
// Rule 4 applies only if we inserted chars not in the middle of a tag (but at the end).
void processFormatting(int changedPosition, int changedEnd);
// We don't want accidentally detach InstantReplaces map.
// So we access it only by const reference from this method.
const InstantReplaces &instantReplaces() const;
void processInstantReplaces(const QString &text);
void applyInstantReplace(const QString &what, const QString &with);
bool revertInstantReplace();
bool heightAutoupdated();
int placeholderSkipWidth() const;
int _minHeight = -1; // < 0 - no autosize
int _maxHeight = -1;
int _maxLength = -1;
SubmitSettings _submitSettings = SubmitSettings::Enter;
QString _placeholder;
base::lambda<QString()> _placeholderFactory;
int _placeholderAfterSymbols = 0;
bool _focused = false;
bool _placeholderVisible = true;
Animation _a_placeholderFocused;
Animation _a_placeholderVisible;
TextWithTags _lastTextWithTags;
// Tags list which we should apply while setText() call or insert from mime data.
TagList _insertedTags;
bool _insertedTagsAreFromMime;
// Override insert position and charsAdded from complex text editing
// (like drag-n-drop in the same text edit field).
int _realInsertPosition = -1;
int _realCharsAdded = 0;
std::unique_ptr<TagMimeProcessor> _tagMimeProcessor;
const style::FlatTextarea &_st;
bool _undoAvailable = false;
bool _redoAvailable = false;
bool _inDrop = false;
bool _inHeightCheck = false;
int _fakeMargin = 0;
QTimer _touchTimer;
bool _touchPress = false;
bool _touchRightButton = false;
bool _touchMove = false;
QPoint _touchStart;
bool _correcting = false;
struct LinkRange {
int start;
int length;
};
friend bool operator==(const LinkRange &a, const LinkRange &b);
friend bool operator!=(const LinkRange &a, const LinkRange &b);
using LinkRanges = QVector<LinkRange>;
LinkRanges _links;
QTextCharFormat _defaultCharFormat;
InstantReplaces _mutableInstantReplaces;
bool _instantReplacesEnabled = true;
};
inline bool operator==(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) {
return (a.start == b.start) && (a.length == b.length);
}
inline bool operator!=(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) {
return !(a == b);
}
class FlatInput : public TWidgetHelper<QLineEdit>, private base::Subscriber {
Q_OBJECT
@ -337,12 +112,6 @@ private:
QPoint _touchStart;
};
enum class CtrlEnterSubmit {
Enter,
CtrlEnter,
Both,
};
class InputField : public RpWidget, private base::Subscriber {
Q_OBJECT
@ -373,10 +142,18 @@ public:
void showError();
void setMaxLength(int maxLength) {
_maxLength = maxLength;
}
void setMaxLength(int maxLength);
void setMinHeight(int minHeight);
void setMaxHeight(int maxHeight);
const TextWithTags &getTextWithTags() const {
return _lastTextWithTags;
}
TextWithTags getTextWithTagsPart(int start, int end = -1) const;
void insertTag(const QString &text, QString tagId = QString());
bool empty() const {
return _lastTextWithTags.text.isEmpty();
}
enum class HistoryAction {
NewEntry,
MergeEntry,
@ -386,6 +163,17 @@ public:
const TextWithTags &textWithTags,
HistoryAction historyAction = HistoryAction::NewEntry);
// If you need to make some preparations of tags before putting them to QMimeData
// (and then to clipboard or to drag-n-drop object), here is a strategy for that.
class TagMimeProcessor {
public:
virtual QString mimeTagFromTag(const QString &tagId) = 0;
virtual QString tagFromMimeTag(const QString &mimeTag) = 0;
virtual ~TagMimeProcessor() {
}
};
void setTagMimeProcessor(std::unique_ptr<TagMimeProcessor> &&processor);
void setInstantReplaces(const InstantReplaces &replaces);
void enableInstantReplaces(bool enabled);
void commitInstantReplacement(
@ -397,7 +185,9 @@ public:
const QString &getLastText() const {
return _lastTextWithTags.text;
}
void setPlaceholder(base::lambda<QString()> placeholderFactory);
void setPlaceholder(
base::lambda<QString()> placeholderFactory,
int afterSymbols = 0);
void setPlaceholderHidden(bool forcePlaceholderHidden);
void setDisplayFocused(bool focused);
void finishAnimating();
@ -409,16 +199,23 @@ public:
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
QString getText(int start = 0, int end = -1) const;
bool hasText() const;
void selectAll();
bool isUndoAvailable() const;
bool isRedoAvailable() const;
enum class SubmitSettings {
None,
Enter,
CtrlEnter,
Both,
};
void setSubmitSettings(SubmitSettings settings);
void customUpDown(bool isCustom);
void setCtrlEnterSubmit(CtrlEnterSubmit ctrlEnterSubmit);
not_null<QTextDocument*> document();
not_null<const QTextDocument*> document() const;
void setTextCursor(const QTextCursor &cursor);
void setCursorPosition(int position);
QTextCursor textCursor() const;
@ -427,6 +224,8 @@ public:
bool hasFocus() const;
void setFocus();
void clearFocus();
not_null<QTextEdit*> rawTextEdit();
not_null<const QTextEdit*> rawTextEdit() const;
enum class MimeAction {
Check,
@ -439,6 +238,10 @@ public:
_mimeDataHook = std::move(hook);
}
const rpl::variable<int> &scrollTop() const;
int scrollTopMax() const;
void scrollTo(int top);
private slots:
void onTouchTimer();
@ -455,7 +258,6 @@ signals:
void submitted(bool ctrlShiftEnter);
void cancelled();
void tabbed();
void focused();
void blurred();
void resized();
@ -464,14 +266,6 @@ protected:
void startPlaceholderAnimation();
void startBorderAnimation();
void insertEmoji(EmojiPtr emoji, QTextCursor c);
TWidget *tparent() {
return qobject_cast<TWidget*>(parentWidget());
}
const TWidget *tparent() const {
return qobject_cast<const TWidget*>(parentWidget());
}
void paintEvent(QPaintEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
@ -488,6 +282,7 @@ private:
void updatePalette();
void refreshPlaceholder();
int placeholderSkipWidth() const;
bool heightAutoupdated();
void checkContentHeight();
@ -498,12 +293,30 @@ private:
void setFocused(bool focused);
void keyPressEventInner(QKeyEvent *e);
void contextMenuEventInner(QContextMenuEvent *e);
void dropEventInner(QDropEvent *e);
QMimeData *createMimeDataFromSelectionInner() const;
bool canInsertFromMimeDataInner(const QMimeData *source) const;
void insertFromMimeDataInner(const QMimeData *source);
void processDocumentContentsChange(int position, int charsAdded);
// "start" and "end" are in coordinates of text where emoji are replaced
// by ObjectReplacementCharacter. If "end" = -1 means get text till the end.
QString getTextPart(
int start,
int end,
TagList &outTagsList,
bool &outTagsChanged) const;
// After any characters added we must postprocess them. This includes:
// 1. Replacing font family to semibold for ~ characters, if we used Open Sans 13px.
// 2. Replacing font family from semibold for all non-~ characters, if we used ...
// 3. Replacing emoji code sequences by ObjectReplacementCharacters with emoji pics.
// 4. Interrupting tags in which the text was inserted by any char except a letter.
// 5. Applying tags from "_insertedTags" in case we pasted text with tags, not just text.
// Rule 4 applies only if we inserted chars not in the middle of a tag (but at the end).
void processFormatting(int changedPosition, int changedEnd);
void chopByMaxLength(int insertPosition, int insertLength);
// We don't want accidentally detach InstantReplaces map.
// So we access it only by const reference from this method.
@ -516,21 +329,37 @@ private:
Mode _mode = Mode::SingleLine;
int _maxLength = -1;
int _minHeight = -1;
int _maxHeight = -1;
bool _forcePlaceholderHidden = false;
object_ptr<Inner> _inner;
TextWithTags _lastTextWithTags;
CtrlEnterSubmit _ctrlEnterSubmit = CtrlEnterSubmit::CtrlEnter;
// Tags list which we should apply while setText() call or insert from mime data.
TagList _insertedTags;
bool _insertedTagsAreFromMime;
// Override insert position and charsAdded from complex text editing
// (like drag-n-drop in the same text edit field).
int _realInsertPosition = -1;
int _realCharsAdded = 0;
std::unique_ptr<TagMimeProcessor> _tagMimeProcessor;
SubmitSettings _submitSettings = SubmitSettings::Enter;
bool _undoAvailable = false;
bool _redoAvailable = false;
bool _inDrop = false;
bool _inHeightCheck = false;
int _fakeMargin = 0;
bool _customUpDown = false;
QString _placeholder;
base::lambda<QString()> _placeholderFactory;
int _placeholderAfterSymbols = 0;
Animation _a_placeholderShifted;
bool _placeholderShifted = false;
QPainterPath _placeholderPath;
@ -557,6 +386,8 @@ private:
QTextCharFormat _defaultCharFormat;
rpl::variable<int> _scrollTop;
InstantReplaces _mutableInstantReplaces;
bool _instantReplacesEnabled = true;

View File

@ -180,22 +180,6 @@ ScrollArea {
hiding: int;
}
FlatTextarea {
textColor: color;
bgColor: color;
width: pixels;
textMrg: margins;
align: align;
font: font;
phColor: color;
phFocusColor: color;
phPos: point;
phAlign: align;
phShift: pixels;
phDuration: int;
}
FlatInput {
textColor: color;
bgColor: color;

View File

@ -770,7 +770,7 @@ void Notification::showReplyField() {
_replyArea->show();
_replyArea->setFocus();
_replyArea->setMaxLength(MaxMessageSize);
_replyArea->setCtrlEnterSubmit(Ui::CtrlEnterSubmit::Both);
_replyArea->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
_replyArea->setInstantReplaces(Ui::InstantReplaces::Default());
// Catch mouse press event to activate the window.

View File

@ -501,15 +501,19 @@ void Generator::paintComposeArea() {
auto fieldWidth = _composeArea.width() - st::historyAttach.width - st::historySendSize.width() - st::historySendRight - st::historyAttachEmoji.width - 2 * fakeMargin;
auto fieldHeight = st::historySendSize.height() - 2 * st::historySendPadding - 2 * fakeMargin;
auto field = QRect(fieldLeft, fieldTop, fieldWidth, fieldHeight);
_p->fillRect(field, st::historyComposeField.bgColor[_palette]);
_p->fillRect(field, st::historyComposeField.textBg[_palette]);
_p->save();
_p->setClipRect(field);
_p->setFont(st::historyComposeField.font);
_p->setPen(st::historyComposeField.phColor[_palette]);
_p->setPen(st::historyComposeField.placeholderFg[_palette]);
auto phRect = QRect(field.x() + st::historyComposeField.textMrg.left() - fakeMargin + st::historyComposeField.phPos.x(), field.y() + st::historyComposeField.textMrg.top() - fakeMargin + st::historyComposeField.phPos.y(), field.width() - st::historyComposeField.textMrg.left() - st::historyComposeField.textMrg.right(), field.height() - st::historyComposeField.textMrg.top() - st::historyComposeField.textMrg.bottom());
_p->drawText(phRect, lang(lng_message_ph), QTextOption(st::historyComposeField.phAlign));
auto placeholderRect = QRect(
field.x() + st::historyComposeField.textMargins.left() - fakeMargin + st::historyComposeField.placeholderMargins.left(),
field.y() + st::historyComposeField.textMargins.top() - fakeMargin + st::historyComposeField.placeholderMargins.top(),
field.width() - st::historyComposeField.textMargins.left() - st::historyComposeField.textMargins.right(),
field.height() - st::historyComposeField.textMargins.top() - st::historyComposeField.textMargins.bottom());
_p->drawText(placeholderRect, lang(lng_message_ph), QTextOption(st::historyComposeField.textAlign));
_p->restore();
_p->setClipping(false);

View File

@ -109,6 +109,7 @@
'<(src_loc)/rpl/producer_tests.cpp',
'<(src_loc)/rpl/range.h',
'<(src_loc)/rpl/rpl.h',
'<(src_loc)/rpl/skip.h',
'<(src_loc)/rpl/take.h',
'<(src_loc)/rpl/then.h',
'<(src_loc)/rpl/type_erased.h',