mirror of
https://github.com/vale981/tdesktop
synced 2025-03-05 09:41:41 -05:00
Replace FlatTextarea with InputField.
This commit is contained in:
parent
30dd8fe070
commit
017ec87d60
29 changed files with 1646 additions and 2272 deletions
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
};
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
61
Telegram/SourceFiles/rpl/skip.h
Normal file
61
Telegram/SourceFiles/rpl/skip.h
Normal 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
|
|
@ -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
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Reference in a new issue