4295 lines
120 KiB
C++
4295 lines
120 KiB
C++
/*
|
|
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
|
|
*/
|
|
#include "ui/widgets/input_fields.h"
|
|
|
|
#include "ui/widgets/popup_menu.h"
|
|
#include "ui/countryinput.h"
|
|
#include "ui/emoji_config.h"
|
|
#include "emoji_suggestions_data.h"
|
|
#include "chat_helpers/emoji_suggestions_helper.h"
|
|
#include "window/themes/window_theme.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "data/data_user.h"
|
|
#include "mainwindow.h"
|
|
#include "numbers.h"
|
|
#include "auth_session.h"
|
|
#include "messenger.h"
|
|
|
|
namespace Ui {
|
|
namespace {
|
|
|
|
constexpr auto kMaxUsernameLength = 32;
|
|
constexpr auto kInstantReplaceRandomId = QTextFormat::UserProperty;
|
|
constexpr auto kInstantReplaceWhatId = QTextFormat::UserProperty + 1;
|
|
constexpr auto kInstantReplaceWithId = QTextFormat::UserProperty + 2;
|
|
constexpr auto kReplaceTagId = QTextFormat::UserProperty + 3;
|
|
constexpr auto kTagProperty = QTextFormat::UserProperty + 4;
|
|
const auto kObjectReplacementCh = QChar(QChar::ObjectReplacementCharacter);
|
|
const auto kObjectReplacement = QString::fromRawData(
|
|
&kObjectReplacementCh,
|
|
1);
|
|
const auto &kTagBold = InputField::kTagBold;
|
|
const auto &kTagItalic = InputField::kTagItalic;
|
|
const auto &kTagCode = InputField::kTagCode;
|
|
const auto &kTagPre = InputField::kTagPre;
|
|
const auto kNewlineChars = QString("\r\n")
|
|
+ QChar(0xfdd0) // QTextBeginningOfFrame
|
|
+ QChar(0xfdd1) // QTextEndOfFrame
|
|
+ QChar(QChar::ParagraphSeparator)
|
|
+ QChar(QChar::LineSeparator);
|
|
const auto kClearFormatSequence = QKeySequence("ctrl+shift+n");
|
|
const auto kMonospaceSequence = QKeySequence("ctrl+shift+m");
|
|
const auto kEditLinkSequence = QKeySequence("ctrl+k");
|
|
|
|
class InputDocument : public QTextDocument {
|
|
public:
|
|
InputDocument(QObject *parent, const style::InputField &st);
|
|
|
|
protected:
|
|
QVariant loadResource(int type, const QUrl &name) override;
|
|
|
|
private:
|
|
const style::InputField &_st;
|
|
std::map<QUrl, QVariant> _emojiCache;
|
|
rpl::lifetime _lifetime;
|
|
|
|
};
|
|
|
|
InputDocument::InputDocument(QObject *parent, const style::InputField &st)
|
|
: QTextDocument(parent)
|
|
, _st(st) {
|
|
Ui::Emoji::Updated(
|
|
) | rpl::start_with_next([=] {
|
|
_emojiCache.clear();
|
|
}, _lifetime);
|
|
}
|
|
|
|
QVariant InputDocument::loadResource(int type, const QUrl &name) {
|
|
if (type != QTextDocument::ImageResource
|
|
|| name.scheme() != qstr("emoji")) {
|
|
return QTextDocument::loadResource(type, name);
|
|
}
|
|
const auto i = _emojiCache.find(name);
|
|
if (i != _emojiCache.end()) {
|
|
return i->second;
|
|
}
|
|
auto result = [&] {
|
|
if (const auto emoji = Ui::Emoji::FromUrl(name.toDisplayString())) {
|
|
const auto height = _st.font->height;
|
|
return QVariant(Ui::Emoji::SinglePixmap(emoji, height));
|
|
}
|
|
return QVariant();
|
|
}();
|
|
_emojiCache.emplace(name, result);
|
|
return result;
|
|
}
|
|
|
|
bool IsNewline(QChar ch) {
|
|
return (kNewlineChars.indexOf(ch) >= 0);
|
|
}
|
|
|
|
QString GetFullSimpleTextTag(const TextWithTags &textWithTags) {
|
|
const auto &text = textWithTags.text;
|
|
const auto &tags = textWithTags.tags;
|
|
const auto tag = (tags.size() == 1) ? tags[0] : TextWithTags::Tag();
|
|
auto from = 0;
|
|
auto till = int(text.size());
|
|
for (; from != till; ++from) {
|
|
if (!IsNewline(text[from]) && !chIsSpace(text[from])) {
|
|
break;
|
|
}
|
|
}
|
|
while (till != from) {
|
|
if (!IsNewline(text[till - 1]) && !chIsSpace(text[till - 1])) {
|
|
break;
|
|
}
|
|
--till;
|
|
}
|
|
return ((tag.offset <= from)
|
|
&& (tag.offset + tag.length >= till))
|
|
? (tag.id == kTagPre ? kTagCode : tag.id)
|
|
: QString();
|
|
}
|
|
|
|
class TagAccumulator {
|
|
public:
|
|
TagAccumulator(TextWithTags::Tags &tags) : _tags(tags) {
|
|
}
|
|
|
|
bool changed() const {
|
|
return _changed;
|
|
}
|
|
|
|
void feed(const QString &randomTagId, int currentPosition) {
|
|
if (randomTagId == _currentTagId) {
|
|
return;
|
|
}
|
|
|
|
if (!_currentTagId.isEmpty()) {
|
|
const auto tag = TextWithTags::Tag {
|
|
_currentStart,
|
|
currentPosition - _currentStart,
|
|
_currentTagId
|
|
};
|
|
if (tag.length > 0) {
|
|
if (_currentTag >= _tags.size()) {
|
|
_changed = true;
|
|
_tags.push_back(tag);
|
|
} else if (_tags[_currentTag] != tag) {
|
|
_changed = true;
|
|
_tags[_currentTag] = tag;
|
|
}
|
|
++_currentTag;
|
|
}
|
|
}
|
|
_currentTagId = randomTagId;
|
|
_currentStart = currentPosition;
|
|
};
|
|
|
|
void finish() {
|
|
if (_currentTag < _tags.size()) {
|
|
_tags.resize(_currentTag);
|
|
_changed = true;
|
|
}
|
|
}
|
|
|
|
private:
|
|
TextWithTags::Tags &_tags;
|
|
bool _changed = false;
|
|
|
|
int _currentTag = 0;
|
|
int _currentStart = 0;
|
|
QString _currentTagId;
|
|
|
|
};
|
|
|
|
struct TagStartExpression {
|
|
QString tag;
|
|
QString goodBefore;
|
|
QString badAfter;
|
|
};
|
|
|
|
constexpr auto kTagBoldIndex = 0;
|
|
constexpr auto kTagItalicIndex = 1;
|
|
constexpr auto kTagCodeIndex = 2;
|
|
constexpr auto kTagPreIndex = 3;
|
|
constexpr auto kInvalidPosition = std::numeric_limits<int>::max() / 2;
|
|
|
|
class TagSearchItem {
|
|
public:
|
|
enum class Edge {
|
|
Open,
|
|
Close,
|
|
};
|
|
|
|
int matchPosition(Edge edge) const {
|
|
return (_position >= 0) ? _position : kInvalidPosition;
|
|
}
|
|
|
|
void applyOffset(int offset) {
|
|
if (_position < offset) {
|
|
_position = -1;
|
|
}
|
|
accumulate_max(_offset, offset);
|
|
}
|
|
|
|
void fill(
|
|
const QString &text,
|
|
Edge edge,
|
|
const TagStartExpression &expression) {
|
|
const auto length = text.size();
|
|
const auto &tag = expression.tag;
|
|
const auto tagLength = tag.size();
|
|
const auto isGood = [&](QChar ch) {
|
|
return (expression.goodBefore.indexOf(ch) >= 0);
|
|
};
|
|
const auto isBad = [&](QChar ch) {
|
|
return (expression.badAfter.indexOf(ch) >= 0);
|
|
};
|
|
const auto check = [&](Edge edge) {
|
|
if (_position > 0) {
|
|
const auto before = text[_position - 1];
|
|
if ((edge == Edge::Open && !isGood(before))
|
|
|| (edge == Edge::Close && isBad(before))) {
|
|
return false;
|
|
}
|
|
}
|
|
if (_position + tagLength < length) {
|
|
const auto after = text[_position + tagLength];
|
|
if ((edge == Edge::Open && isBad(after))
|
|
|| (edge == Edge::Close && !isGood(after))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
const auto edgeIndex = static_cast<int>(edge);
|
|
if (_position >= 0) {
|
|
if (_checked[edgeIndex]) {
|
|
return;
|
|
} else if (check(edge)) {
|
|
_checked[edgeIndex] = true;
|
|
return;
|
|
} else {
|
|
_checked = { { false, false } };
|
|
}
|
|
}
|
|
while (true) {
|
|
_position = text.indexOf(tag, _offset);
|
|
if (_position < 0) {
|
|
_offset = _position = kInvalidPosition;
|
|
break;
|
|
}
|
|
_offset = _position + tagLength;
|
|
if (check(edge)) {
|
|
break;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
if (_position == kInvalidPosition) {
|
|
_checked = { { true, true } };
|
|
} else {
|
|
_checked = { { false, false } };
|
|
_checked[edgeIndex] = true;
|
|
}
|
|
}
|
|
|
|
private:
|
|
int _offset = 0;
|
|
int _position = -1;
|
|
std::array<bool, 2> _checked = { { false, false } };
|
|
|
|
};
|
|
|
|
const std::vector<TagStartExpression> &TagStartExpressions() {
|
|
static auto cached = std::vector<TagStartExpression> {
|
|
{
|
|
kTagBold,
|
|
TextUtilities::MarkdownBoldGoodBefore(),
|
|
TextUtilities::MarkdownBoldBadAfter()
|
|
},
|
|
{
|
|
kTagItalic,
|
|
TextUtilities::MarkdownItalicGoodBefore(),
|
|
TextUtilities::MarkdownItalicBadAfter()
|
|
},
|
|
{
|
|
kTagCode,
|
|
TextUtilities::MarkdownCodeGoodBefore(),
|
|
TextUtilities::MarkdownCodeBadAfter()
|
|
},
|
|
{
|
|
kTagPre,
|
|
TextUtilities::MarkdownPreGoodBefore(),
|
|
TextUtilities::MarkdownPreBadAfter()
|
|
},
|
|
};
|
|
return cached;
|
|
}
|
|
|
|
const std::map<QString, int> &TagIndices() {
|
|
static auto cached = std::map<QString, int> {
|
|
{ kTagBold, kTagBoldIndex },
|
|
{ kTagItalic, kTagItalicIndex },
|
|
{ kTagCode, kTagCodeIndex },
|
|
{ kTagPre, kTagPreIndex },
|
|
};
|
|
return cached;
|
|
}
|
|
|
|
bool DoesTagFinishByNewline(const QString &tag) {
|
|
return (tag == kTagCode);
|
|
}
|
|
|
|
class MarkdownTagAccumulator {
|
|
public:
|
|
using Edge = TagSearchItem::Edge;
|
|
|
|
MarkdownTagAccumulator(std::vector<InputField::MarkdownTag> *tags)
|
|
: _tags(tags)
|
|
, _expressions(TagStartExpressions())
|
|
, _tagIndices(TagIndices())
|
|
, _items(_expressions.size()) {
|
|
}
|
|
|
|
// Here we use the fact that text either contains only emoji
|
|
// { adjustedTextLength = text.size() * (emojiLength - 1) }
|
|
// or contains no emoji at all and can have tag edges in the middle
|
|
// { adjustedTextLength = 0 }.
|
|
//
|
|
// Otherwise we would have to pass emoji positions inside text.
|
|
void feed(
|
|
const QString &text,
|
|
int adjustedTextLength,
|
|
const QString &textTag) {
|
|
if (!_tags) {
|
|
return;
|
|
}
|
|
const auto guard = gsl::finally([&] {
|
|
_currentInternalLength += text.size();
|
|
_currentAdjustedLength += adjustedTextLength;
|
|
});
|
|
if (!textTag.isEmpty()) {
|
|
finishTags();
|
|
return;
|
|
}
|
|
for (auto &item : _items) {
|
|
item = TagSearchItem();
|
|
}
|
|
auto tryFinishTag = _currentTag;
|
|
while (true) {
|
|
for (; tryFinishTag != _currentFreeTag; ++tryFinishTag) {
|
|
auto &tag = (*_tags)[tryFinishTag];
|
|
if (tag.internalLength >= 0) {
|
|
continue;
|
|
}
|
|
|
|
const auto i = _tagIndices.find(tag.tag);
|
|
Assert(i != end(_tagIndices));
|
|
const auto tagIndex = i->second;
|
|
|
|
const auto atLeastOffset =
|
|
tag.internalStart
|
|
+ tag.tag.size()
|
|
+ 1
|
|
- _currentInternalLength;
|
|
_items[tagIndex].applyOffset(atLeastOffset);
|
|
|
|
fillItem(
|
|
tagIndex,
|
|
text,
|
|
Edge::Close);
|
|
if (finishByNewline(tryFinishTag, text, tagIndex)) {
|
|
continue;
|
|
}
|
|
const auto position = matchPosition(tagIndex, Edge::Close);
|
|
if (position < kInvalidPosition) {
|
|
const auto till = position + tag.tag.size();
|
|
finishTag(tryFinishTag, till, true);
|
|
_items[tagIndex].applyOffset(till);
|
|
}
|
|
}
|
|
for (auto i = 0, count = int(_items.size()); i != count; ++i) {
|
|
fillItem(i, text, Edge::Open);
|
|
}
|
|
const auto min = minIndex(Edge::Open);
|
|
if (min < 0) {
|
|
return;
|
|
}
|
|
startTag(matchPosition(min, Edge::Open), _expressions[min].tag);
|
|
}
|
|
}
|
|
|
|
void finish() {
|
|
if (!_tags) {
|
|
return;
|
|
}
|
|
finishTags();
|
|
if (_currentTag < _tags->size()) {
|
|
_tags->resize(_currentTag);
|
|
}
|
|
}
|
|
|
|
private:
|
|
void finishTag(int index, int offsetFromAccumulated, bool closed) {
|
|
Expects(_tags != nullptr);
|
|
Expects(index >= 0 && index < _tags->size());
|
|
|
|
auto &tag = (*_tags)[index];
|
|
if (tag.internalLength < 0) {
|
|
tag.internalLength = _currentInternalLength
|
|
+ offsetFromAccumulated
|
|
- tag.internalStart;
|
|
tag.adjustedLength = _currentAdjustedLength
|
|
+ offsetFromAccumulated
|
|
- tag.adjustedStart;
|
|
tag.closed = closed;
|
|
}
|
|
if (index == _currentTag) {
|
|
++_currentTag;
|
|
}
|
|
}
|
|
bool finishByNewline(
|
|
int index,
|
|
const QString &text,
|
|
int tagIndex) {
|
|
Expects(_tags != nullptr);
|
|
Expects(index >= 0 && index < _tags->size());
|
|
|
|
auto &tag = (*_tags)[index];
|
|
|
|
if (!DoesTagFinishByNewline(tag.tag)) {
|
|
return false;
|
|
}
|
|
const auto endPosition = newlinePosition(
|
|
text,
|
|
std::max(0, tag.internalStart + 1 - _currentInternalLength));
|
|
if (matchPosition(tagIndex, Edge::Close) <= endPosition) {
|
|
return false;
|
|
}
|
|
finishTag(index, endPosition, false);
|
|
return true;
|
|
}
|
|
void finishTags() {
|
|
while (_currentTag != _currentFreeTag) {
|
|
finishTag(_currentTag, 0, false);
|
|
}
|
|
}
|
|
void startTag(int offsetFromAccumulated, const QString &tag) {
|
|
Expects(_tags != nullptr);
|
|
|
|
const auto newTag = InputField::MarkdownTag{
|
|
_currentInternalLength + offsetFromAccumulated,
|
|
-1,
|
|
_currentAdjustedLength + offsetFromAccumulated,
|
|
-1,
|
|
false,
|
|
tag
|
|
};
|
|
if (_currentFreeTag < _tags->size()) {
|
|
(*_tags)[_currentFreeTag] = newTag;
|
|
} else {
|
|
_tags->push_back(newTag);
|
|
}
|
|
++_currentFreeTag;
|
|
}
|
|
void fillItem(int index, const QString &text, Edge edge) {
|
|
Expects(index >= 0 && index < _items.size());
|
|
|
|
_items[index].fill(text, edge, _expressions[index]);
|
|
}
|
|
int matchPosition(int index, Edge edge) const {
|
|
Expects(index >= 0 && index < _items.size());
|
|
|
|
return _items[index].matchPosition(edge);
|
|
}
|
|
int newlinePosition(const QString &text, int offset) const {
|
|
const auto length = text.size();
|
|
if (offset < length) {
|
|
const auto begin = text.data();
|
|
const auto end = begin + length;
|
|
for (auto ch = begin + offset; ch != end; ++ch) {
|
|
if (IsNewline(*ch)) {
|
|
return (ch - begin);
|
|
}
|
|
}
|
|
}
|
|
return kInvalidPosition;
|
|
}
|
|
int minIndex(Edge edge) const {
|
|
auto result = -1;
|
|
auto minPosition = kInvalidPosition;
|
|
for (auto i = 0, count = int(_items.size()); i != count; ++i) {
|
|
const auto position = matchPosition(i, edge);
|
|
if (position < minPosition) {
|
|
minPosition = position;
|
|
result = i;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
int minIndexForFinish(const std::vector<int> &indices) const {
|
|
const auto tagIndex = indices[0];
|
|
auto result = -1;
|
|
auto minPosition = kInvalidPosition;
|
|
for (auto i : indices) {
|
|
const auto edge = (i == tagIndex) ? Edge::Close : Edge::Open;
|
|
const auto position = matchPosition(i, edge);
|
|
if (position < minPosition) {
|
|
minPosition = position;
|
|
result = i;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<InputField::MarkdownTag> *_tags = nullptr;
|
|
const std::vector<TagStartExpression> &_expressions;
|
|
const std::map<QString, int> &_tagIndices;
|
|
std::vector<TagSearchItem> _items;
|
|
|
|
int _currentTag = 0;
|
|
int _currentFreeTag = 0;
|
|
int _currentInternalLength = 0;
|
|
int _currentAdjustedLength = 0;
|
|
|
|
};
|
|
|
|
template <typename InputClass>
|
|
class InputStyle : public QCommonStyle {
|
|
public:
|
|
InputStyle() {
|
|
setParent(QCoreApplication::instance());
|
|
}
|
|
|
|
void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget = 0) const {
|
|
}
|
|
QRect subElementRect(SubElement r, const QStyleOption *opt, const QWidget *widget = 0) const {
|
|
switch (r) {
|
|
case SE_LineEditContents:
|
|
const InputClass *w = widget ? qobject_cast<const InputClass*>(widget) : 0;
|
|
return w ? w->getTextRect() : QCommonStyle::subElementRect(r, opt, widget);
|
|
break;
|
|
}
|
|
return QCommonStyle::subElementRect(r, opt, widget);
|
|
}
|
|
|
|
static InputStyle<InputClass> *instance() {
|
|
if (!_instance) {
|
|
if (!QGuiApplication::instance()) {
|
|
return nullptr;
|
|
}
|
|
_instance = new InputStyle<InputClass>();
|
|
}
|
|
return _instance;
|
|
}
|
|
|
|
~InputStyle() {
|
|
_instance = nullptr;
|
|
}
|
|
|
|
private:
|
|
static InputStyle<InputClass> *_instance;
|
|
|
|
};
|
|
|
|
template <typename InputClass>
|
|
InputStyle<InputClass> *InputStyle<InputClass>::_instance = nullptr;
|
|
|
|
template <typename Iterator>
|
|
QString AccumulateText(Iterator begin, Iterator end) {
|
|
auto result = QString();
|
|
result.reserve(end - begin);
|
|
for (auto i = end; i != begin;) {
|
|
result.push_back(*--i);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QTextImageFormat PrepareEmojiFormat(EmojiPtr emoji, const QFont &font) {
|
|
const auto factor = cIntRetinaFactor();
|
|
const auto width = Ui::Emoji::GetSizeNormal()
|
|
+ st::emojiPadding * factor * 2;
|
|
const auto height = QFontMetrics(font).height() * factor;
|
|
auto result = QTextImageFormat();
|
|
result.setWidth(width / factor);
|
|
result.setHeight(height / factor);
|
|
result.setName(emoji->toUrl());
|
|
result.setVerticalAlignment(QTextCharFormat::AlignBaseline);
|
|
return result;
|
|
}
|
|
|
|
// Optimization: with null page size document does not re-layout
|
|
// on each insertText / mergeCharFormat.
|
|
void PrepareFormattingOptimization(not_null<QTextDocument*> document) {
|
|
if (!document->pageSize().isNull()) {
|
|
document->setPageSize(QSizeF(0, 0));
|
|
}
|
|
}
|
|
|
|
void RemoveDocumentTags(
|
|
const style::InputField &st,
|
|
not_null<QTextDocument*> document,
|
|
int from,
|
|
int end) {
|
|
auto cursor = QTextCursor(document->docHandle(), from);
|
|
cursor.setPosition(end, QTextCursor::KeepAnchor);
|
|
|
|
QTextCharFormat format;
|
|
format.setProperty(kTagProperty, QString());
|
|
format.setProperty(kReplaceTagId, QString());
|
|
format.setForeground(st.textFg);
|
|
format.setFont(st.font);
|
|
cursor.mergeCharFormat(format);
|
|
}
|
|
|
|
style::font AdjustFont(
|
|
const style::font &font,
|
|
const style::font &original) {
|
|
return (font->size() != original->size()
|
|
|| font->flags() != original->flags())
|
|
? style::font(original->size(), original->flags(), font->family())
|
|
: font;
|
|
}
|
|
|
|
bool IsValidMarkdownLink(const QString &link) {
|
|
return (link.indexOf('.') >= 0) || (link.indexOf(':') >= 0);
|
|
}
|
|
|
|
QTextCharFormat PrepareTagFormat(
|
|
const style::InputField &st,
|
|
QString tag) {
|
|
auto result = QTextCharFormat();
|
|
if (IsValidMarkdownLink(tag)) {
|
|
result.setForeground(st::defaultTextPalette.linkFg);
|
|
result.setFont(st.font);
|
|
} else if (tag == kTagBold) {
|
|
auto semibold = st::semiboldFont;
|
|
if (semibold->size() != st.font->size()
|
|
|| semibold->flags() != st.font->flags()) {
|
|
semibold = style::font(
|
|
st.font->size(),
|
|
st.font->flags(),
|
|
semibold->family());
|
|
}
|
|
result.setForeground(st.textFg);
|
|
result.setFont(AdjustFont(st::semiboldFont, st.font));
|
|
} else if (tag == kTagItalic) {
|
|
result.setForeground(st.textFg);
|
|
result.setFont(st.font->italic());
|
|
} else if (tag == kTagCode || tag == kTagPre) {
|
|
result.setForeground(st::defaultTextPalette.monoFg);
|
|
result.setFont(AdjustFont(App::monofont(), st.font));
|
|
} else {
|
|
result.setForeground(st.textFg);
|
|
result.setFont(st.font);
|
|
}
|
|
result.setProperty(kTagProperty, tag);
|
|
return result;
|
|
}
|
|
|
|
void ApplyTagFormat(QTextCharFormat &to, const QTextCharFormat &from) {
|
|
to.setProperty(kTagProperty, from.property(kTagProperty));
|
|
to.setProperty(kReplaceTagId, from.property(kReplaceTagId));
|
|
to.setFont(from.font());
|
|
to.setForeground(from.foreground());
|
|
}
|
|
|
|
// Returns the position of the first inserted tag or "changedEnd" value if none found.
|
|
int ProcessInsertedTags(
|
|
const style::InputField &st,
|
|
not_null<QTextDocument*> document,
|
|
int changedPosition,
|
|
int changedEnd,
|
|
const TextWithTags::Tags &tags,
|
|
InputField::TagMimeProcessor *processor) {
|
|
int firstTagStart = changedEnd;
|
|
int applyNoTagFrom = changedEnd;
|
|
for (const auto &tag : tags) {
|
|
int tagFrom = changedPosition + tag.offset;
|
|
int tagTo = tagFrom + tag.length;
|
|
accumulate_max(tagFrom, changedPosition);
|
|
accumulate_min(tagTo, changedEnd);
|
|
auto tagId = processor ? processor->tagFromMimeTag(tag.id) : tag.id;
|
|
if (tagTo > tagFrom && !tagId.isEmpty()) {
|
|
accumulate_min(firstTagStart, tagFrom);
|
|
|
|
PrepareFormattingOptimization(document);
|
|
|
|
if (applyNoTagFrom < tagFrom) {
|
|
RemoveDocumentTags(
|
|
st,
|
|
document,
|
|
applyNoTagFrom,
|
|
tagFrom);
|
|
}
|
|
QTextCursor c(document->docHandle(), 0);
|
|
c.setPosition(tagFrom);
|
|
c.setPosition(tagTo, QTextCursor::KeepAnchor);
|
|
|
|
c.mergeCharFormat(PrepareTagFormat(st, tagId));
|
|
|
|
applyNoTagFrom = tagTo;
|
|
}
|
|
}
|
|
if (applyNoTagFrom < changedEnd) {
|
|
RemoveDocumentTags(st, document, applyNoTagFrom, changedEnd);
|
|
}
|
|
|
|
return firstTagStart;
|
|
}
|
|
|
|
// When inserting a part of text inside a tag we need to have
|
|
// a way to know if the insertion replaced the end of the tag
|
|
// or it was strictly inside (in the middle) of the tag.
|
|
bool WasInsertTillTheEndOfTag(
|
|
QTextBlock block,
|
|
QTextBlock::iterator fragmentIt,
|
|
int insertionEnd) {
|
|
const auto format = fragmentIt.fragment().charFormat();
|
|
const auto insertTagName = format.property(kTagProperty);
|
|
while (true) {
|
|
for (; !fragmentIt.atEnd(); ++fragmentIt) {
|
|
const auto fragment = fragmentIt.fragment();
|
|
const auto position = fragment.position();
|
|
const auto outsideInsertion = (position >= insertionEnd);
|
|
if (outsideInsertion) {
|
|
const auto format = fragment.charFormat();
|
|
return (format.property(kTagProperty) != insertTagName);
|
|
}
|
|
const auto end = position + fragment.length();
|
|
const auto notFullFragmentInserted = (end > insertionEnd);
|
|
if (notFullFragmentInserted) {
|
|
return false;
|
|
}
|
|
}
|
|
if (block.isValid()) {
|
|
fragmentIt = block.begin();
|
|
block = block.next();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
// Insertion goes till the end of the text => not strictly inside a tag.
|
|
return true;
|
|
}
|
|
|
|
struct FormattingAction {
|
|
enum class Type {
|
|
Invalid,
|
|
InsertEmoji,
|
|
TildeFont,
|
|
RemoveTag,
|
|
RemoveNewline,
|
|
ClearInstantReplace,
|
|
};
|
|
|
|
Type type = Type::Invalid;
|
|
EmojiPtr emoji = nullptr;
|
|
bool isTilde = false;
|
|
QString tildeTag;
|
|
int intervalStart = 0;
|
|
int intervalEnd = 0;
|
|
|
|
};
|
|
|
|
} // namespace
|
|
|
|
const QString InputField::kTagBold = qsl("**");
|
|
const QString InputField::kTagItalic = qsl("__");
|
|
const QString InputField::kTagCode = qsl("`");
|
|
const QString InputField::kTagPre = qsl("```");
|
|
|
|
class InputField::Inner final : public QTextEdit {
|
|
public:
|
|
Inner(not_null<InputField*> parent) : QTextEdit(parent) {
|
|
}
|
|
|
|
protected:
|
|
bool viewportEvent(QEvent *e) override {
|
|
return outer()->viewportEventInner(e);
|
|
}
|
|
void focusInEvent(QFocusEvent *e) override {
|
|
return outer()->focusInEventInner(e);
|
|
}
|
|
void focusOutEvent(QFocusEvent *e) override {
|
|
return outer()->focusOutEventInner(e);
|
|
}
|
|
void keyPressEvent(QKeyEvent *e) override {
|
|
return outer()->keyPressEventInner(e);
|
|
}
|
|
void contextMenuEvent(QContextMenuEvent *e) override {
|
|
return outer()->contextMenuEventInner(e);
|
|
}
|
|
void dropEvent(QDropEvent *e) override {
|
|
return outer()->dropEventInner(e);
|
|
}
|
|
void inputMethodEvent(QInputMethodEvent *e) override {
|
|
return outer()->inputMethodEventInner(e);
|
|
}
|
|
|
|
bool canInsertFromMimeData(const QMimeData *source) const override {
|
|
return outer()->canInsertFromMimeDataInner(source);
|
|
}
|
|
void insertFromMimeData(const QMimeData *source) override {
|
|
return outer()->insertFromMimeDataInner(source);
|
|
}
|
|
QMimeData *createMimeDataFromSelection() const override {
|
|
return outer()->createMimeDataFromSelectionInner();
|
|
}
|
|
|
|
private:
|
|
not_null<InputField*> outer() const {
|
|
return static_cast<InputField*>(parentWidget());
|
|
}
|
|
friend class InputField;
|
|
|
|
};
|
|
|
|
void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji) {
|
|
const auto currentFormat = cursor.charFormat();
|
|
auto format = PrepareEmojiFormat(emoji, currentFormat.font());
|
|
ApplyTagFormat(format, currentFormat);
|
|
cursor.insertText(kObjectReplacement, format);
|
|
}
|
|
|
|
void InstantReplaces::add(const QString &what, const QString &with) {
|
|
auto node = &reverseMap;
|
|
for (auto i = what.end(), b = what.begin(); i != b;) {
|
|
node = &node->tail.emplace(*--i, Node()).first->second;
|
|
}
|
|
node->text = with;
|
|
accumulate_max(maxLength, int(what.size()));
|
|
}
|
|
|
|
const InstantReplaces &InstantReplaces::Default() {
|
|
static const auto result = [] {
|
|
auto result = InstantReplaces();
|
|
result.add("--", QString(1, QChar(8212)));
|
|
result.add("<<", QString(1, QChar(171)));
|
|
result.add(">>", QString(1, QChar(187)));
|
|
result.add(
|
|
":shrug:",
|
|
QChar(175) + QString("\\_(") + QChar(12484) + ")_/" + QChar(175));
|
|
result.add(":o ", QString(1, QChar(0xD83D)) + QChar(0xDE28));
|
|
result.add("xD ", QString(1, QChar(0xD83D)) + QChar(0xDE06));
|
|
const auto &replacements = Emoji::internal::GetAllReplacements();
|
|
for (const auto &one : replacements) {
|
|
const auto with = Emoji::QStringFromUTF16(one.emoji);
|
|
const auto what = Emoji::QStringFromUTF16(one.replacement);
|
|
result.add(what, with);
|
|
}
|
|
const auto &pairs = Emoji::internal::GetReplacementPairs();
|
|
for (const auto &[what, index] : pairs) {
|
|
const auto emoji = Emoji::internal::ByIndex(index);
|
|
Assert(emoji != nullptr);
|
|
result.add(what, emoji->text());
|
|
}
|
|
return result;
|
|
}();
|
|
return result;
|
|
}
|
|
|
|
FlatInput::FlatInput(QWidget *parent, const style::FlatInput &st, Fn<QString()> placeholderFactory, const QString &v) : TWidgetHelper<QLineEdit>(v, parent)
|
|
, _oldtext(v)
|
|
, _placeholderFactory(std::move(placeholderFactory))
|
|
, _placeholderVisible(!v.length())
|
|
, _st(st)
|
|
, _textMrg(_st.textMrg) {
|
|
setCursor(style::cur_text);
|
|
resize(_st.width, _st.height);
|
|
|
|
setFont(_st.font->f);
|
|
setAlignment(_st.align);
|
|
|
|
subscribe(Lang::Current().updated(), [this] { refreshPlaceholder(); });
|
|
refreshPlaceholder();
|
|
|
|
subscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) {
|
|
if (update.paletteChanged()) {
|
|
updatePalette();
|
|
}
|
|
});
|
|
updatePalette();
|
|
|
|
connect(this, SIGNAL(textChanged(const QString &)), this, SLOT(onTextChange(const QString &)));
|
|
connect(this, SIGNAL(textEdited(const QString &)), this, SLOT(onTextEdited()));
|
|
if (App::wnd()) connect(this, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu()));
|
|
|
|
setStyle(InputStyle<FlatInput>::instance());
|
|
QLineEdit::setTextMargins(0, 0, 0, 0);
|
|
setContentsMargins(0, 0, 0, 0);
|
|
|
|
setAttribute(Qt::WA_AcceptTouchEvents);
|
|
_touchTimer.setSingleShot(true);
|
|
connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
|
|
}
|
|
|
|
void FlatInput::updatePalette() {
|
|
auto p = palette();
|
|
p.setColor(QPalette::Text, _st.textColor->c);
|
|
setPalette(p);
|
|
}
|
|
|
|
void FlatInput::customUpDown(bool custom) {
|
|
_customUpDown = custom;
|
|
}
|
|
|
|
void FlatInput::onTouchTimer() {
|
|
_touchRightButton = true;
|
|
}
|
|
|
|
bool FlatInput::event(QEvent *e) {
|
|
if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) {
|
|
QTouchEvent *ev = static_cast<QTouchEvent*>(e);
|
|
if (ev->device()->type() == QTouchDevice::TouchScreen) {
|
|
touchEvent(ev);
|
|
return QLineEdit::event(e);
|
|
}
|
|
}
|
|
return QLineEdit::event(e);
|
|
}
|
|
|
|
void FlatInput::touchEvent(QTouchEvent *e) {
|
|
switch (e->type()) {
|
|
case QEvent::TouchBegin: {
|
|
if (_touchPress || e->touchPoints().isEmpty()) return;
|
|
_touchTimer.start(QApplication::startDragTime());
|
|
_touchPress = true;
|
|
_touchMove = _touchRightButton = false;
|
|
_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
|
|
} break;
|
|
|
|
case QEvent::TouchUpdate: {
|
|
if (!_touchPress || e->touchPoints().isEmpty()) return;
|
|
if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
|
|
_touchMove = true;
|
|
}
|
|
} break;
|
|
|
|
case QEvent::TouchEnd: {
|
|
if (!_touchPress) return;
|
|
auto weak = make_weak(this);
|
|
if (!_touchMove && window()) {
|
|
Qt::MouseButton btn(_touchRightButton ? Qt::RightButton : Qt::LeftButton);
|
|
QPoint mapped(mapFromGlobal(_touchStart)), winMapped(window()->mapFromGlobal(_touchStart));
|
|
|
|
if (_touchRightButton) {
|
|
QContextMenuEvent contextEvent(QContextMenuEvent::Mouse, mapped, _touchStart);
|
|
contextMenuEvent(&contextEvent);
|
|
}
|
|
}
|
|
if (weak) {
|
|
_touchTimer.stop();
|
|
_touchPress = _touchMove = _touchRightButton = false;
|
|
}
|
|
} break;
|
|
|
|
case QEvent::TouchCancel: {
|
|
_touchPress = false;
|
|
_touchTimer.stop();
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void FlatInput::setTextMrg(const QMargins &textMrg) {
|
|
_textMrg = textMrg;
|
|
refreshPlaceholder();
|
|
update();
|
|
}
|
|
|
|
QRect FlatInput::getTextRect() const {
|
|
return rect().marginsRemoved(_textMrg + QMargins(-2, -1, -2, -1));
|
|
}
|
|
|
|
void FlatInput::paintEvent(QPaintEvent *e) {
|
|
Painter p(this);
|
|
|
|
auto ms = getms();
|
|
auto placeholderFocused = _a_placeholderFocused.current(ms, _focused ? 1. : 0.);
|
|
|
|
auto pen = anim::pen(_st.borderColor, _st.borderActive, placeholderFocused);
|
|
pen.setWidth(_st.borderWidth);
|
|
p.setPen(pen);
|
|
p.setBrush(anim::brush(_st.bgColor, _st.bgActive, placeholderFocused));
|
|
{
|
|
PainterHighQualityEnabler hq(p);
|
|
p.drawRoundedRect(QRectF(0, 0, width(), height()).marginsRemoved(QMarginsF(_st.borderWidth / 2., _st.borderWidth / 2., _st.borderWidth / 2., _st.borderWidth / 2.)), st::buttonRadius - (_st.borderWidth / 2.), st::buttonRadius - (_st.borderWidth / 2.));
|
|
}
|
|
|
|
if (!_st.icon.empty()) {
|
|
_st.icon.paint(p, 0, 0, width());
|
|
}
|
|
|
|
auto placeholderOpacity = _a_placeholderVisible.current(ms, _placeholderVisible ? 1. : 0.);
|
|
if (placeholderOpacity > 0.) {
|
|
p.setOpacity(placeholderOpacity);
|
|
|
|
auto left = anim::interpolate(_st.phShift, 0, placeholderOpacity);
|
|
|
|
p.save();
|
|
p.setClipRect(rect());
|
|
QRect phRect(placeholderRect());
|
|
phRect.moveLeft(phRect.left() + left);
|
|
phPrepare(p, placeholderFocused);
|
|
p.drawText(phRect, _placeholder, QTextOption(_st.phAlign));
|
|
p.restore();
|
|
}
|
|
QLineEdit::paintEvent(e);
|
|
}
|
|
|
|
void FlatInput::focusInEvent(QFocusEvent *e) {
|
|
if (!_focused) {
|
|
_focused = true;
|
|
_a_placeholderFocused.start([this] { update(); }, 0., 1., _st.phDuration);
|
|
update();
|
|
}
|
|
QLineEdit::focusInEvent(e);
|
|
emit focused();
|
|
}
|
|
|
|
void FlatInput::focusOutEvent(QFocusEvent *e) {
|
|
if (_focused) {
|
|
_focused = false;
|
|
_a_placeholderFocused.start([this] { update(); }, 1., 0., _st.phDuration);
|
|
update();
|
|
}
|
|
QLineEdit::focusOutEvent(e);
|
|
emit blurred();
|
|
}
|
|
|
|
void FlatInput::resizeEvent(QResizeEvent *e) {
|
|
refreshPlaceholder();
|
|
return QLineEdit::resizeEvent(e);
|
|
}
|
|
|
|
void FlatInput::setPlaceholder(Fn<QString()> placeholderFactory) {
|
|
_placeholderFactory = std::move(placeholderFactory);
|
|
refreshPlaceholder();
|
|
}
|
|
|
|
void FlatInput::refreshPlaceholder() {
|
|
auto availw = width() - _textMrg.left() - _textMrg.right() - _st.phPos.x() - 1;
|
|
auto placeholderText = _placeholderFactory ? _placeholderFactory() : QString();
|
|
if (_st.font->width(placeholderText) > availw) {
|
|
_placeholder = _st.font->elided(placeholderText, availw);
|
|
} else {
|
|
_placeholder = placeholderText;
|
|
}
|
|
update();
|
|
}
|
|
|
|
void FlatInput::contextMenuEvent(QContextMenuEvent *e) {
|
|
if (auto menu = createStandardContextMenu()) {
|
|
(new Ui::PopupMenu(this, menu))->popup(e->globalPos());
|
|
}
|
|
}
|
|
|
|
QSize FlatInput::sizeHint() const {
|
|
return geometry().size();
|
|
}
|
|
|
|
QSize FlatInput::minimumSizeHint() const {
|
|
return geometry().size();
|
|
}
|
|
|
|
void FlatInput::updatePlaceholder() {
|
|
auto hasText = !text().isEmpty();
|
|
if (!hasText) {
|
|
hasText = _lastPreEditTextNotEmpty;
|
|
} else {
|
|
_lastPreEditTextNotEmpty = false;
|
|
}
|
|
auto placeholderVisible = !hasText;
|
|
if (_placeholderVisible != placeholderVisible) {
|
|
_placeholderVisible = placeholderVisible;
|
|
_a_placeholderVisible.start([this] { update(); }, _placeholderVisible ? 0. : 1., _placeholderVisible ? 1. : 0., _st.phDuration);
|
|
}
|
|
}
|
|
|
|
void FlatInput::inputMethodEvent(QInputMethodEvent *e) {
|
|
QLineEdit::inputMethodEvent(e);
|
|
auto lastPreEditTextNotEmpty = !e->preeditString().isEmpty();
|
|
if (_lastPreEditTextNotEmpty != lastPreEditTextNotEmpty) {
|
|
_lastPreEditTextNotEmpty = lastPreEditTextNotEmpty;
|
|
updatePlaceholder();
|
|
}
|
|
}
|
|
|
|
QRect FlatInput::placeholderRect() const {
|
|
return QRect(_textMrg.left() + _st.phPos.x(), _textMrg.top() + _st.phPos.y(), width() - _textMrg.left() - _textMrg.right(), height() - _textMrg.top() - _textMrg.bottom());
|
|
}
|
|
|
|
void FlatInput::correctValue(const QString &was, QString &now) {
|
|
}
|
|
|
|
void FlatInput::phPrepare(Painter &p, float64 placeholderFocused) {
|
|
p.setFont(_st.font);
|
|
p.setPen(anim::pen(_st.phColor, _st.phFocusColor, placeholderFocused));
|
|
}
|
|
|
|
void FlatInput::keyPressEvent(QKeyEvent *e) {
|
|
QString wasText(_oldtext);
|
|
|
|
if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) {
|
|
e->ignore();
|
|
} else {
|
|
QLineEdit::keyPressEvent(e);
|
|
}
|
|
|
|
QString newText(text());
|
|
if (wasText == newText) { // call correct manually
|
|
correctValue(wasText, newText);
|
|
_oldtext = newText;
|
|
if (wasText != _oldtext) emit changed();
|
|
updatePlaceholder();
|
|
}
|
|
if (e->key() == Qt::Key_Escape) {
|
|
emit cancelled();
|
|
} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
|
|
emit submitted(e->modifiers());
|
|
#ifdef Q_OS_MAC
|
|
} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
|
|
auto selected = selectedText();
|
|
if (!selected.isEmpty() && echoMode() == QLineEdit::Normal) {
|
|
QApplication::clipboard()->setText(selected, QClipboard::FindBuffer);
|
|
}
|
|
#endif // Q_OS_MAC
|
|
}
|
|
}
|
|
|
|
void FlatInput::onTextEdited() {
|
|
QString wasText(_oldtext), newText(text());
|
|
|
|
correctValue(wasText, newText);
|
|
_oldtext = newText;
|
|
if (wasText != _oldtext) emit changed();
|
|
updatePlaceholder();
|
|
|
|
if (App::wnd()) App::wnd()->updateGlobalMenu();
|
|
}
|
|
|
|
void FlatInput::onTextChange(const QString &text) {
|
|
_oldtext = text;
|
|
if (App::wnd()) App::wnd()->updateGlobalMenu();
|
|
}
|
|
|
|
InputField::InputField(
|
|
QWidget *parent,
|
|
const style::InputField &st,
|
|
Fn<QString()> placeholderFactory,
|
|
const QString &value)
|
|
: InputField(
|
|
parent,
|
|
st,
|
|
Mode::SingleLine,
|
|
std::move(placeholderFactory),
|
|
{ value, {} }) {
|
|
}
|
|
|
|
InputField::InputField(
|
|
QWidget *parent,
|
|
const style::InputField &st,
|
|
Mode mode,
|
|
Fn<QString()> placeholderFactory,
|
|
const QString &value)
|
|
: InputField(
|
|
parent,
|
|
st,
|
|
mode,
|
|
std::move(placeholderFactory),
|
|
{ value, {} }) {
|
|
}
|
|
|
|
InputField::InputField(
|
|
QWidget *parent,
|
|
const style::InputField &st,
|
|
Mode mode,
|
|
Fn<QString()> placeholderFactory,
|
|
const TextWithTags &value)
|
|
: RpWidget(parent)
|
|
, _st(st)
|
|
, _mode(mode)
|
|
, _minHeight(st.heightMin)
|
|
, _maxHeight(st.heightMax)
|
|
, _inner(std::make_unique<Inner>(this))
|
|
, _lastTextWithTags(value)
|
|
, _placeholderFactory(std::move(placeholderFactory)) {
|
|
_inner->setDocument(Ui::CreateChild<InputDocument>(_inner.get(), _st));
|
|
|
|
_inner->setAcceptRichText(false);
|
|
resize(_st.width, _minHeight);
|
|
|
|
if (_st.textBg->c.alphaF() >= 1.) {
|
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
|
}
|
|
|
|
_inner->setFont(_st.font->f);
|
|
_inner->setAlignment(_st.textAlign);
|
|
if (_mode == Mode::SingleLine) {
|
|
_inner->setWordWrapMode(QTextOption::NoWrap);
|
|
}
|
|
|
|
subscribe(Lang::Current().updated(), [=] { refreshPlaceholder(); });
|
|
refreshPlaceholder();
|
|
|
|
subscribe(Window::Theme::Background(), [=](
|
|
const Window::Theme::BackgroundUpdate &update) {
|
|
if (update.paletteChanged()) {
|
|
updatePalette();
|
|
}
|
|
});
|
|
|
|
_defaultCharFormat = _inner->textCursor().charFormat();
|
|
updatePalette();
|
|
_inner->textCursor().setCharFormat(_defaultCharFormat);
|
|
|
|
_inner->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
_inner->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
|
_inner->setFrameStyle(QFrame::NoFrame | QFrame::Plain);
|
|
_inner->viewport()->setAutoFillBackground(false);
|
|
|
|
_inner->setContentsMargins(0, 0, 0, 0);
|
|
_inner->document()->setDocumentMargin(0);
|
|
|
|
setAttribute(Qt::WA_AcceptTouchEvents);
|
|
_inner->viewport()->setAttribute(Qt::WA_AcceptTouchEvents);
|
|
_touchTimer.setSingleShot(true);
|
|
connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
|
|
|
|
connect(_inner->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(onDocumentContentsChange(int,int,int)));
|
|
connect(_inner.get(), SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool)));
|
|
connect(_inner.get(), SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool)));
|
|
connect(_inner.get(), SIGNAL(cursorPositionChanged()), this, SLOT(onCursorPositionChanged()));
|
|
if (App::wnd()) {
|
|
connect(_inner.get(), SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu()));
|
|
}
|
|
|
|
const auto bar = _inner->verticalScrollBar();
|
|
_scrollTop = bar->value();
|
|
connect(bar, &QScrollBar::valueChanged, [=] {
|
|
_scrollTop = bar->value();
|
|
});
|
|
|
|
setCursor(style::cur_text);
|
|
heightAutoupdated();
|
|
|
|
if (!_lastTextWithTags.text.isEmpty()) {
|
|
setTextWithTags(_lastTextWithTags, HistoryAction::Clear);
|
|
}
|
|
|
|
startBorderAnimation();
|
|
startPlaceholderAnimation();
|
|
finishAnimating();
|
|
}
|
|
|
|
const rpl::variable<int> &InputField::scrollTop() const {
|
|
return _scrollTop;
|
|
}
|
|
|
|
int InputField::scrollTopMax() const {
|
|
return _inner->verticalScrollBar()->maximum();
|
|
}
|
|
|
|
void InputField::scrollTo(int top) {
|
|
_inner->verticalScrollBar()->setValue(top);
|
|
}
|
|
|
|
bool InputField::viewportEventInner(QEvent *e) {
|
|
if (e->type() == QEvent::TouchBegin
|
|
|| e->type() == QEvent::TouchUpdate
|
|
|| e->type() == QEvent::TouchEnd
|
|
|| e->type() == QEvent::TouchCancel) {
|
|
const auto ev = static_cast<QTouchEvent*>(e);
|
|
if (ev->device()->type() == QTouchDevice::TouchScreen) {
|
|
handleTouchEvent(ev);
|
|
}
|
|
}
|
|
return _inner->QTextEdit::viewportEvent(e);
|
|
}
|
|
|
|
void InputField::updatePalette() {
|
|
auto p = _inner->palette();
|
|
p.setColor(QPalette::Text, _st.textFg->c);
|
|
_inner->setPalette(p);
|
|
|
|
_defaultCharFormat.merge(PrepareTagFormat(_st, QString()));
|
|
auto cursor = textCursor();
|
|
|
|
const auto document = _inner->document();
|
|
auto block = document->begin();
|
|
const auto end = document->end();
|
|
for (; block != end; block = block.next()) {
|
|
auto till = block.position();
|
|
for (auto i = block.begin(); !i.atEnd();) {
|
|
for (; !i.atEnd(); ++i) {
|
|
const auto fragment = i.fragment();
|
|
if (!fragment.isValid() || fragment.position() < till) {
|
|
continue;
|
|
}
|
|
till = fragment.position() + fragment.length();
|
|
|
|
auto format = fragment.charFormat();
|
|
const auto tag = format.property(kTagProperty).toString();
|
|
format.setForeground(PrepareTagFormat(_st, tag).foreground());
|
|
cursor.setPosition(fragment.position());
|
|
cursor.setPosition(till, QTextCursor::KeepAnchor);
|
|
cursor.mergeCharFormat(format);
|
|
i = block.begin();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
cursor = textCursor();
|
|
if (!cursor.hasSelection()) {
|
|
auto format = cursor.charFormat();
|
|
format.merge(PrepareTagFormat(
|
|
_st,
|
|
format.property(kTagProperty).toString()));
|
|
cursor.setCharFormat(format);
|
|
setTextCursor(cursor);
|
|
}
|
|
}
|
|
|
|
void InputField::onTouchTimer() {
|
|
_touchRightButton = true;
|
|
}
|
|
|
|
void InputField::setInstantReplaces(const InstantReplaces &replaces) {
|
|
_mutableInstantReplaces = replaces;
|
|
}
|
|
|
|
void InputField::setInstantReplacesEnabled(rpl::producer<bool> enabled) {
|
|
std::move(
|
|
enabled
|
|
) | rpl::start_with_next([=](bool value) {
|
|
_instantReplacesEnabled = value;
|
|
}, lifetime());
|
|
}
|
|
|
|
void InputField::setMarkdownReplacesEnabled(rpl::producer<bool> enabled) {
|
|
std::move(
|
|
enabled
|
|
) | rpl::start_with_next([=](bool value) {
|
|
if (_markdownEnabled != value) {
|
|
_markdownEnabled = value;
|
|
if (_markdownEnabled) {
|
|
handleContentsChanged();
|
|
} else {
|
|
_lastMarkdownTags = {};
|
|
}
|
|
}
|
|
}, lifetime());
|
|
}
|
|
|
|
void InputField::setTagMimeProcessor(
|
|
std::unique_ptr<TagMimeProcessor> &&processor) {
|
|
_tagMimeProcessor = std::move(processor);
|
|
}
|
|
|
|
void InputField::setAdditionalMargin(int margin) {
|
|
_inner->setStyleSheet(qsl("QTextEdit { margin: %1px; }").arg(margin));
|
|
_additionalMargin = margin;
|
|
checkContentHeight();
|
|
}
|
|
|
|
void InputField::setMaxLength(int length) {
|
|
if (_maxLength != length) {
|
|
_maxLength = length;
|
|
if (_maxLength > 0) {
|
|
const auto document = _inner->document();
|
|
_correcting = true;
|
|
QTextCursor(document->docHandle(), 0).joinPreviousEditBlock();
|
|
const auto guard = gsl::finally([&] {
|
|
_correcting = false;
|
|
QTextCursor(document->docHandle(), 0).endEditBlock();
|
|
handleContentsChanged();
|
|
});
|
|
|
|
auto cursor = QTextCursor(document->docHandle(), 0);
|
|
cursor.movePosition(QTextCursor::End);
|
|
chopByMaxLength(0, cursor.position());
|
|
}
|
|
}
|
|
}
|
|
|
|
void InputField::setMinHeight(int height) {
|
|
_minHeight = height;
|
|
}
|
|
|
|
void InputField::setMaxHeight(int height) {
|
|
_maxHeight = height;
|
|
}
|
|
|
|
void InputField::insertTag(const QString &text, QString tagId) {
|
|
auto cursor = textCursor();
|
|
const auto position = cursor.position();
|
|
|
|
const auto document = _inner->document();
|
|
auto block = document->findBlock(position);
|
|
for (auto iter = block.begin(); !iter.atEnd(); ++iter) {
|
|
auto fragment = iter.fragment();
|
|
Assert(fragment.isValid());
|
|
|
|
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;
|
|
}
|
|
|
|
auto mentionInCommand = false;
|
|
const auto fragmentText = fragment.text();
|
|
for (auto i = position - fragmentPosition; i > 0; --i) {
|
|
const auto previous = fragmentText[i - 1];
|
|
if (previous == '@' || previous == '#' || previous == '/') {
|
|
if ((i == position - fragmentPosition
|
|
|| (previous == '/'
|
|
? fragmentText[i].isLetterOrNumber()
|
|
: fragmentText[i].isLetter())
|
|
|| previous == '#') &&
|
|
(i < 2 || !(fragmentText[i - 2].isLetterOrNumber()
|
|
|| fragmentText[i - 2] == '_'))) {
|
|
cursor.setPosition(fragmentPosition + i - 1);
|
|
auto till = fragmentPosition + i;
|
|
for (; (till < fragmentEnd && till < position); ++till) {
|
|
const auto ch = fragmentText[till - fragmentPosition];
|
|
if (!ch.isLetterOrNumber() && ch != '_' && ch != '@') {
|
|
break;
|
|
}
|
|
}
|
|
if (till < fragmentEnd
|
|
&& fragmentText[till - fragmentPosition] == ' ') {
|
|
++till;
|
|
}
|
|
cursor.setPosition(till, QTextCursor::KeepAnchor);
|
|
break;
|
|
} else if ((i == position - fragmentPosition
|
|
|| fragmentText[i].isLetter())
|
|
&& fragmentText[i - 1] == '@'
|
|
&& (i > 2)
|
|
&& (fragmentText[i - 2].isLetterOrNumber()
|
|
|| fragmentText[i - 2] == '_')
|
|
&& !mentionInCommand) {
|
|
mentionInCommand = true;
|
|
--i;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (position - fragmentPosition - i > 127
|
|
|| (!mentionInCommand
|
|
&& (position - fragmentPosition - i > 63))
|
|
|| (!fragmentText[i - 1].isLetterOrNumber()
|
|
&& fragmentText[i - 1] != '_')) {
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if (tagId.isEmpty()) {
|
|
cursor.insertText(text + ' ', _defaultCharFormat);
|
|
} else {
|
|
_insertedTags.clear();
|
|
_insertedTags.push_back({ 0, text.size(), tagId });
|
|
_insertedTagsAreFromMime = false;
|
|
cursor.insertText(text + ' ');
|
|
_insertedTags.clear();
|
|
}
|
|
}
|
|
|
|
bool InputField::heightAutoupdated() {
|
|
if (_minHeight < 0
|
|
|| _maxHeight < 0
|
|
|| _inHeightCheck
|
|
|| _mode == Mode::SingleLine) {
|
|
return false;
|
|
}
|
|
_inHeightCheck = true;
|
|
const auto guard = gsl::finally([&] { _inHeightCheck = false; });
|
|
|
|
SendPendingMoveResizeEvents(this);
|
|
|
|
const auto contentHeight = int(std::ceil(document()->size().height()))
|
|
+ _st.textMargins.top()
|
|
+ _st.textMargins.bottom()
|
|
+ 2 * _additionalMargin;
|
|
const auto newHeight = snap(contentHeight, _minHeight, _maxHeight);
|
|
if (height() != newHeight) {
|
|
resize(width(), newHeight);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void InputField::checkContentHeight() {
|
|
if (heightAutoupdated()) {
|
|
emit resized();
|
|
}
|
|
}
|
|
|
|
void InputField::handleTouchEvent(QTouchEvent *e) {
|
|
switch (e->type()) {
|
|
case QEvent::TouchBegin: {
|
|
if (_touchPress || e->touchPoints().isEmpty()) return;
|
|
_touchTimer.start(QApplication::startDragTime());
|
|
_touchPress = true;
|
|
_touchMove = _touchRightButton = false;
|
|
_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
|
|
} break;
|
|
|
|
case QEvent::TouchUpdate: {
|
|
if (!_touchPress || e->touchPoints().isEmpty()) return;
|
|
if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
|
|
_touchMove = true;
|
|
}
|
|
} break;
|
|
|
|
case QEvent::TouchEnd: {
|
|
if (!_touchPress) return;
|
|
auto weak = make_weak(this);
|
|
if (!_touchMove && window()) {
|
|
Qt::MouseButton btn(_touchRightButton ? Qt::RightButton : Qt::LeftButton);
|
|
QPoint mapped(mapFromGlobal(_touchStart)), winMapped(window()->mapFromGlobal(_touchStart));
|
|
|
|
if (_touchRightButton) {
|
|
QContextMenuEvent contextEvent(QContextMenuEvent::Mouse, mapped, _touchStart);
|
|
contextMenuEvent(&contextEvent);
|
|
}
|
|
}
|
|
if (weak) {
|
|
_touchTimer.stop();
|
|
_touchPress = _touchMove = _touchRightButton = false;
|
|
}
|
|
} break;
|
|
|
|
case QEvent::TouchCancel: {
|
|
_touchPress = false;
|
|
_touchTimer.stop();
|
|
} break;
|
|
}
|
|
}
|
|
|
|
void InputField::paintEvent(QPaintEvent *e) {
|
|
Painter p(this);
|
|
|
|
auto ms = getms();
|
|
auto r = rect().intersected(e->rect());
|
|
if (_st.textBg->c.alphaF() > 0.) {
|
|
p.fillRect(r, _st.textBg);
|
|
}
|
|
if (_st.border) {
|
|
p.fillRect(0, height() - _st.border, width(), _st.border, _st.borderFg);
|
|
}
|
|
auto errorDegree = _a_error.current(ms, _error ? 1. : 0.);
|
|
auto focusedDegree = _a_focused.current(ms, _focused ? 1. : 0.);
|
|
auto borderShownDegree = _a_borderShown.current(ms, 1.);
|
|
auto borderOpacity = _a_borderOpacity.current(ms, _borderVisible ? 1. : 0.);
|
|
if (_st.borderActive && (borderOpacity > 0.)) {
|
|
auto borderStart = snap(_borderAnimationStart, 0, width());
|
|
auto borderFrom = qRound(borderStart * (1. - borderShownDegree));
|
|
auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree);
|
|
if (borderTo > borderFrom) {
|
|
auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree);
|
|
p.setOpacity(borderOpacity);
|
|
p.fillRect(borderFrom, height() - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg);
|
|
p.setOpacity(1);
|
|
}
|
|
}
|
|
|
|
if (_st.placeholderScale > 0. && !_placeholderPath.isEmpty()) {
|
|
auto placeholderShiftDegree = _a_placeholderShifted.current(ms, _placeholderShifted ? 1. : 0.);
|
|
p.save();
|
|
p.setClipRect(r);
|
|
|
|
auto placeholderTop = anim::interpolate(0, _st.placeholderShift, placeholderShiftDegree);
|
|
|
|
QRect r(rect().marginsRemoved(_st.textMargins + _st.placeholderMargins));
|
|
r.moveTop(r.top() + placeholderTop);
|
|
if (rtl()) r.moveLeft(width() - r.left() - r.width());
|
|
|
|
auto placeholderScale = 1. - (1. - _st.placeholderScale) * placeholderShiftDegree;
|
|
auto placeholderFg = anim::color(_st.placeholderFg, _st.placeholderFgActive, focusedDegree);
|
|
placeholderFg = anim::color(placeholderFg, _st.placeholderFgError, errorDegree);
|
|
|
|
PainterHighQualityEnabler hq(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(placeholderFg);
|
|
p.translate(r.topLeft());
|
|
p.scale(placeholderScale, placeholderScale);
|
|
p.drawPath(_placeholderPath);
|
|
|
|
p.restore();
|
|
} else if (!_placeholder.isEmpty()) {
|
|
auto placeholderHiddenDegree = _a_placeholderShifted.current(ms, _placeholderShifted ? 1. : 0.);
|
|
if (placeholderHiddenDegree < 1.) {
|
|
p.setOpacity(1. - placeholderHiddenDegree);
|
|
p.save();
|
|
p.setClipRect(r);
|
|
|
|
auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree);
|
|
|
|
QRect r(rect().marginsRemoved(_st.textMargins + _st.placeholderMargins));
|
|
r.moveLeft(r.left() + placeholderLeft);
|
|
if (rtl()) r.moveLeft(width() - r.left() - r.width());
|
|
|
|
p.setFont(_st.placeholderFont);
|
|
p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree));
|
|
|
|
if (_st.placeholderAlign == style::al_topleft && _placeholderAfterSymbols > 0) {
|
|
const auto skipWidth = placeholderSkipWidth();
|
|
p.drawText(
|
|
_st.textMargins.left() + _st.placeholderMargins.left() + skipWidth,
|
|
_st.textMargins.top() + _st.placeholderMargins.top() + _st.placeholderFont->ascent,
|
|
_placeholder);
|
|
} else {
|
|
auto r = rect().marginsRemoved(_st.textMargins + _st.placeholderMargins);
|
|
r.moveLeft(r.left() + placeholderLeft);
|
|
if (rtl()) r.moveLeft(width() - r.left() - r.width());
|
|
p.drawText(r, _placeholder, _st.placeholderAlign);
|
|
}
|
|
|
|
p.restore();
|
|
}
|
|
}
|
|
TWidget::paintEvent(e);
|
|
}
|
|
|
|
int InputField::placeholderSkipWidth() const {
|
|
if (!_placeholderAfterSymbols) {
|
|
return 0;
|
|
}
|
|
const auto &text = getTextWithTags().text;
|
|
auto result = _st.font->width(text.mid(0, _placeholderAfterSymbols));
|
|
if (_placeholderAfterSymbols > text.size()) {
|
|
result += _st.font->spacew;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void InputField::startBorderAnimation() {
|
|
auto borderVisible = (_error || _focused);
|
|
if (_borderVisible != borderVisible) {
|
|
_borderVisible = borderVisible;
|
|
if (_borderVisible) {
|
|
if (_a_borderOpacity.animating()) {
|
|
_a_borderOpacity.start([this] { update(); }, 0., 1., _st.duration);
|
|
} else {
|
|
_a_borderShown.start([this] { update(); }, 0., 1., _st.duration);
|
|
}
|
|
} else {
|
|
_a_borderOpacity.start([this] { update(); }, 1., 0., _st.duration);
|
|
}
|
|
}
|
|
}
|
|
|
|
void InputField::focusInEvent(QFocusEvent *e) {
|
|
_borderAnimationStart = (e->reason() == Qt::MouseFocusReason)
|
|
? mapFromGlobal(QCursor::pos()).x()
|
|
: (width() / 2);
|
|
QTimer::singleShot(0, this, SLOT(onFocusInner()));
|
|
}
|
|
|
|
void InputField::mousePressEvent(QMouseEvent *e) {
|
|
_borderAnimationStart = e->pos().x();
|
|
QTimer::singleShot(0, this, SLOT(onFocusInner()));
|
|
}
|
|
|
|
void InputField::onFocusInner() {
|
|
auto borderStart = _borderAnimationStart;
|
|
_inner->setFocus();
|
|
_borderAnimationStart = borderStart;
|
|
}
|
|
|
|
int InputField::borderAnimationStart() const {
|
|
return _borderAnimationStart;
|
|
}
|
|
|
|
void InputField::contextMenuEvent(QContextMenuEvent *e) {
|
|
_inner->contextMenuEvent(e);
|
|
}
|
|
|
|
void InputField::focusInEventInner(QFocusEvent *e) {
|
|
_borderAnimationStart = (e->reason() == Qt::MouseFocusReason)
|
|
? mapFromGlobal(QCursor::pos()).x()
|
|
: (width() / 2);
|
|
setFocused(true);
|
|
_inner->QTextEdit::focusInEvent(e);
|
|
emit focused();
|
|
}
|
|
|
|
void InputField::focusOutEventInner(QFocusEvent *e) {
|
|
setFocused(false);
|
|
_inner->QTextEdit::focusOutEvent(e);
|
|
emit blurred();
|
|
}
|
|
|
|
void InputField::setFocused(bool focused) {
|
|
if (_focused != focused) {
|
|
_focused = focused;
|
|
_a_focused.start([this] { update(); }, _focused ? 0. : 1., _focused ? 1. : 0., _st.duration);
|
|
startPlaceholderAnimation();
|
|
startBorderAnimation();
|
|
}
|
|
}
|
|
|
|
QSize InputField::sizeHint() const {
|
|
return geometry().size();
|
|
}
|
|
|
|
QSize InputField::minimumSizeHint() const {
|
|
return geometry().size();
|
|
}
|
|
|
|
bool InputField::hasText() const {
|
|
const auto document = _inner->document();
|
|
const auto from = document->begin();
|
|
const auto till = document->end();
|
|
|
|
if (from == till) {
|
|
return false;
|
|
}
|
|
|
|
for (auto item = from.begin(); !item.atEnd(); ++item) {
|
|
const auto fragment = item.fragment();
|
|
if (!fragment.isValid()) {
|
|
continue;
|
|
} else if (!fragment.text().isEmpty()) {
|
|
return true;
|
|
}
|
|
}
|
|
return (from.next() != till);
|
|
}
|
|
|
|
QString InputField::getTextPart(
|
|
int start,
|
|
int end,
|
|
TagList &outTagsList,
|
|
bool &outTagsChanged,
|
|
std::vector<MarkdownTag> *outMarkdownTags) const {
|
|
Expects((start == 0 && end < 0) || outMarkdownTags == nullptr);
|
|
|
|
if (end >= 0 && end <= start) {
|
|
outTagsChanged = !outTagsList.isEmpty();
|
|
outTagsList.clear();
|
|
return QString();
|
|
}
|
|
|
|
if (start < 0) {
|
|
start = 0;
|
|
}
|
|
const auto full = (start == 0 && end < 0);
|
|
|
|
auto lastTag = QString();
|
|
TagAccumulator tagAccumulator(outTagsList);
|
|
MarkdownTagAccumulator markdownTagAccumulator(outMarkdownTags);
|
|
const auto newline = outMarkdownTags ? QString(1, '\n') : QString();
|
|
|
|
const auto document = _inner->document();
|
|
const auto from = full ? document->begin() : document->findBlock(start);
|
|
auto till = (end < 0) ? document->end() : document->findBlock(end);
|
|
if (till.isValid()) {
|
|
till = till.next();
|
|
}
|
|
|
|
auto possibleLength = 0;
|
|
for (auto block = from; block != till; block = block.next()) {
|
|
possibleLength += block.length();
|
|
}
|
|
auto result = QString();
|
|
result.reserve(possibleLength);
|
|
if (!full && end < 0) {
|
|
end = possibleLength;
|
|
}
|
|
|
|
for (auto block = from; block != till;) {
|
|
for (auto item = block.begin(); !item.atEnd(); ++item) {
|
|
const auto fragment = item.fragment();
|
|
if (!fragment.isValid()) {
|
|
continue;
|
|
}
|
|
|
|
const auto fragmentPosition = full ? 0 : fragment.position();
|
|
const auto fragmentEnd = full
|
|
? 0
|
|
: (fragmentPosition + fragment.length());
|
|
const auto format = fragment.charFormat();
|
|
if (!full) {
|
|
if (fragmentPosition == end) {
|
|
tagAccumulator.feed(
|
|
format.property(kTagProperty).toString(),
|
|
result.size());
|
|
break;
|
|
} else if (fragmentPosition > end) {
|
|
break;
|
|
} else if (fragmentEnd <= start) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const auto emojiText = [&] {
|
|
if (format.isImageFormat()) {
|
|
const auto imageName = format.toImageFormat().name();
|
|
if (const auto emoji = Ui::Emoji::FromUrl(imageName)) {
|
|
return emoji->text();
|
|
}
|
|
}
|
|
return QString();
|
|
}();
|
|
auto text = [&] {
|
|
const auto result = fragment.text();
|
|
if (!full) {
|
|
if (fragmentPosition < start) {
|
|
return result.mid(start - fragmentPosition, end - start);
|
|
} else if (fragmentEnd > end) {
|
|
return result.mid(0, end - fragmentPosition);
|
|
}
|
|
}
|
|
return result;
|
|
}();
|
|
|
|
if (full || !text.isEmpty()) {
|
|
lastTag = format.property(kTagProperty).toString();
|
|
tagAccumulator.feed(lastTag, result.size());
|
|
}
|
|
|
|
auto begin = text.data();
|
|
auto ch = begin;
|
|
auto adjustedLength = text.size();
|
|
for (const auto end = begin + text.size(); ch != end; ++ch) {
|
|
if (IsNewline(*ch) && ch->unicode() != '\r') {
|
|
*ch = QLatin1Char('\n');
|
|
} else switch (ch->unicode()) {
|
|
case QChar::Nbsp: {
|
|
*ch = QLatin1Char(' ');
|
|
} break;
|
|
case QChar::ObjectReplacementCharacter: {
|
|
if (ch > begin) {
|
|
result.append(begin, ch - begin);
|
|
}
|
|
adjustedLength += (emojiText.size() - 1);
|
|
if (!emojiText.isEmpty()) {
|
|
result.append(emojiText);
|
|
}
|
|
begin = ch + 1;
|
|
} break;
|
|
}
|
|
}
|
|
if (ch > begin) {
|
|
result.append(begin, ch - begin);
|
|
}
|
|
|
|
if (full || !text.isEmpty()) {
|
|
markdownTagAccumulator.feed(text, adjustedLength, lastTag);
|
|
}
|
|
}
|
|
|
|
block = block.next();
|
|
if (block != till) {
|
|
result.append('\n');
|
|
markdownTagAccumulator.feed(newline, 1, lastTag);
|
|
}
|
|
}
|
|
|
|
tagAccumulator.feed(QString(), result.size());
|
|
tagAccumulator.finish();
|
|
markdownTagAccumulator.finish();
|
|
|
|
outTagsChanged = tagAccumulator.changed();
|
|
return result;
|
|
}
|
|
|
|
bool InputField::isUndoAvailable() const {
|
|
return _undoAvailable;
|
|
}
|
|
|
|
bool InputField::isRedoAvailable() const {
|
|
return _redoAvailable;
|
|
}
|
|
|
|
void InputField::processFormatting(int insertPosition, int insertEnd) {
|
|
// Tilde formatting.
|
|
const auto tildeFormatting = (_st.font->f.pixelSize() * cIntRetinaFactor() == 13)
|
|
&& (_st.font->f.family() == qstr("Open Sans"));
|
|
auto isTildeFragment = false;
|
|
const auto tildeFixedFont = AdjustFont(st::semiboldFont, _st.font);
|
|
|
|
// First tag handling (the one we inserted text to).
|
|
bool startTagFound = false;
|
|
bool breakTagOnNotLetter = false;
|
|
|
|
auto document = _inner->document();
|
|
|
|
// Apply inserted tags.
|
|
auto insertedTagsProcessor = _insertedTagsAreFromMime
|
|
? _tagMimeProcessor.get()
|
|
: nullptr;
|
|
const auto breakTagOnNotLetterTill = ProcessInsertedTags(
|
|
_st,
|
|
document,
|
|
insertPosition,
|
|
insertEnd,
|
|
_insertedTags,
|
|
insertedTagsProcessor);
|
|
using ActionType = FormattingAction::Type;
|
|
while (true) {
|
|
FormattingAction action;
|
|
|
|
auto fromBlock = document->findBlock(insertPosition);
|
|
auto tillBlock = document->findBlock(insertEnd);
|
|
if (tillBlock.isValid()) tillBlock = tillBlock.next();
|
|
|
|
for (auto block = fromBlock; block != tillBlock; block = block.next()) {
|
|
for (auto fragmentIt = block.begin(); !fragmentIt.atEnd(); ++fragmentIt) {
|
|
auto fragment = fragmentIt.fragment();
|
|
Assert(fragment.isValid());
|
|
|
|
int fragmentPosition = fragment.position();
|
|
if (insertPosition >= fragmentPosition + fragment.length()) {
|
|
continue;
|
|
}
|
|
int changedPositionInFragment = insertPosition - fragmentPosition; // Can be negative.
|
|
int changedEndInFragment = insertEnd - fragmentPosition;
|
|
if (changedEndInFragment <= 0) {
|
|
break;
|
|
}
|
|
|
|
auto format = fragment.charFormat();
|
|
if (!format.hasProperty(kTagProperty)) {
|
|
action.type = ActionType::RemoveTag;
|
|
action.intervalStart = fragmentPosition;
|
|
action.intervalEnd = fragmentPosition + fragment.length();
|
|
break;
|
|
}
|
|
if (tildeFormatting) {
|
|
isTildeFragment = (format.font() == tildeFixedFont);
|
|
}
|
|
|
|
auto fragmentText = fragment.text();
|
|
auto *textStart = fragmentText.constData();
|
|
auto *textEnd = textStart + fragmentText.size();
|
|
|
|
const auto with = format.property(kInstantReplaceWithId);
|
|
if (with.isValid()) {
|
|
const auto string = with.toString();
|
|
if (fragmentText != string) {
|
|
action.type = ActionType::ClearInstantReplace;
|
|
action.intervalStart = fragmentPosition
|
|
+ (fragmentText.startsWith(string)
|
|
? string.size()
|
|
: 0);
|
|
action.intervalEnd = fragmentPosition
|
|
+ fragmentText.size();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!startTagFound) {
|
|
startTagFound = true;
|
|
auto tagName = format.property(kTagProperty).toString();
|
|
if (!tagName.isEmpty()) {
|
|
breakTagOnNotLetter = WasInsertTillTheEndOfTag(
|
|
block,
|
|
fragmentIt,
|
|
insertEnd);
|
|
}
|
|
}
|
|
|
|
auto *ch = textStart + qMax(changedPositionInFragment, 0);
|
|
for (; ch < textEnd; ++ch) {
|
|
const auto removeNewline = (_mode == Mode::SingleLine)
|
|
&& (IsNewline(*ch));
|
|
if (removeNewline) {
|
|
if (action.type == ActionType::Invalid) {
|
|
action.type = ActionType::RemoveNewline;
|
|
action.intervalStart = fragmentPosition + (ch - textStart);
|
|
action.intervalEnd = action.intervalStart + 1;
|
|
}
|
|
break;
|
|
}
|
|
|
|
auto emojiLength = 0;
|
|
if (const auto emoji = Ui::Emoji::Find(ch, textEnd, &emojiLength)) {
|
|
// Replace emoji if no current action is prepared.
|
|
if (action.type == ActionType::Invalid) {
|
|
action.type = ActionType::InsertEmoji;
|
|
action.emoji = emoji;
|
|
action.intervalStart = fragmentPosition + (ch - textStart);
|
|
action.intervalEnd = action.intervalStart + emojiLength;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (breakTagOnNotLetter && !ch->isLetter()) {
|
|
// Remove tag name till the end if no current action is prepared.
|
|
if (action.type != ActionType::Invalid) {
|
|
break;
|
|
}
|
|
breakTagOnNotLetter = false;
|
|
if (fragmentPosition + (ch - textStart) < breakTagOnNotLetterTill) {
|
|
action.type = ActionType::RemoveTag;
|
|
action.intervalStart = fragmentPosition + (ch - textStart);
|
|
action.intervalEnd = breakTagOnNotLetterTill;
|
|
break;
|
|
}
|
|
}
|
|
if (tildeFormatting) { // Tilde symbol fix in OpenSans.
|
|
bool tilde = (ch->unicode() == '~');
|
|
if ((tilde && !isTildeFragment) || (!tilde && isTildeFragment)) {
|
|
if (action.type == ActionType::Invalid) {
|
|
action.type = ActionType::TildeFont;
|
|
action.intervalStart = fragmentPosition + (ch - textStart);
|
|
action.intervalEnd = action.intervalStart + 1;
|
|
action.tildeTag = format.property(kTagProperty).toString();
|
|
action.isTilde = tilde;
|
|
} else {
|
|
++action.intervalEnd;
|
|
}
|
|
} else if (action.type == ActionType::TildeFont) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ch + 1 < textEnd && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) {
|
|
++ch;
|
|
}
|
|
}
|
|
if (action.type != ActionType::Invalid) {
|
|
break;
|
|
}
|
|
}
|
|
if (action.type != ActionType::Invalid) {
|
|
break;
|
|
} else if (_mode == Mode::SingleLine
|
|
&& block.next() != document->end()) {
|
|
action.type = ActionType::RemoveNewline;
|
|
action.intervalStart = block.next().position() - 1;
|
|
action.intervalEnd = action.intervalStart + 1;
|
|
break;
|
|
}
|
|
}
|
|
if (action.type != ActionType::Invalid) {
|
|
PrepareFormattingOptimization(document);
|
|
|
|
auto cursor = QTextCursor(
|
|
document->docHandle(),
|
|
action.intervalStart);
|
|
cursor.setPosition(action.intervalEnd, QTextCursor::KeepAnchor);
|
|
if (action.type == ActionType::InsertEmoji) {
|
|
InsertEmojiAtCursor(cursor, action.emoji);
|
|
insertPosition = action.intervalStart + 1;
|
|
if (insertEnd >= action.intervalEnd) {
|
|
insertEnd -= action.intervalEnd
|
|
- action.intervalStart
|
|
- 1;
|
|
}
|
|
} else if (action.type == ActionType::RemoveTag) {
|
|
RemoveDocumentTags(
|
|
_st,
|
|
document,
|
|
action.intervalStart,
|
|
action.intervalEnd);
|
|
} else if (action.type == ActionType::TildeFont) {
|
|
auto format = QTextCharFormat();
|
|
format.setFont(action.isTilde
|
|
? tildeFixedFont
|
|
: PrepareTagFormat(_st, action.tildeTag).font());
|
|
cursor.mergeCharFormat(format);
|
|
insertPosition = action.intervalEnd;
|
|
} else if (action.type == ActionType::ClearInstantReplace) {
|
|
auto format = _defaultCharFormat;
|
|
ApplyTagFormat(format, cursor.charFormat());
|
|
cursor.setCharFormat(format);
|
|
} else if (action.type == ActionType::RemoveNewline) {
|
|
cursor.removeSelectedText();
|
|
insertPosition = action.intervalStart;
|
|
if (insertEnd >= action.intervalEnd) {
|
|
insertEnd -= action.intervalEnd - action.intervalStart;
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void InputField::onDocumentContentsChange(
|
|
int position,
|
|
int charsRemoved,
|
|
int charsAdded) {
|
|
if (_correcting) {
|
|
return;
|
|
}
|
|
|
|
const auto document = _inner->document();
|
|
|
|
// Qt bug workaround https://bugreports.qt.io/browse/QTBUG-49062
|
|
if (!position) {
|
|
auto cursor = QTextCursor(document->docHandle(), 0);
|
|
cursor.movePosition(QTextCursor::End);
|
|
if (position + charsAdded > cursor.position()) {
|
|
const auto delta = position + charsAdded - cursor.position();
|
|
if (charsRemoved >= delta) {
|
|
charsAdded -= delta;
|
|
charsRemoved -= delta;
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto insertPosition = (_realInsertPosition >= 0)
|
|
? _realInsertPosition
|
|
: position;
|
|
const auto insertLength = (_realInsertPosition >= 0)
|
|
? _realCharsAdded
|
|
: charsAdded;
|
|
|
|
const auto removePosition = position;
|
|
const auto removeLength = charsRemoved;
|
|
|
|
_correcting = true;
|
|
QTextCursor(document->docHandle(), 0).joinPreviousEditBlock();
|
|
const auto guard = gsl::finally([&] {
|
|
_correcting = false;
|
|
QTextCursor(document->docHandle(), 0).endEditBlock();
|
|
handleContentsChanged();
|
|
});
|
|
|
|
chopByMaxLength(insertPosition, insertLength);
|
|
|
|
if (document->availableRedoSteps() == 0 && insertLength > 0) {
|
|
const auto pageSize = document->pageSize();
|
|
processFormatting(insertPosition, insertPosition + insertLength);
|
|
if (document->pageSize() != pageSize) {
|
|
document->setPageSize(pageSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
void InputField::onCursorPositionChanged() {
|
|
auto cursor = textCursor();
|
|
if (!cursor.hasSelection() && !cursor.position()) {
|
|
cursor.setCharFormat(_defaultCharFormat);
|
|
setTextCursor(cursor);
|
|
}
|
|
}
|
|
|
|
void InputField::chopByMaxLength(int insertPosition, int insertLength) {
|
|
Expects(_correcting);
|
|
|
|
if (_maxLength < 0) {
|
|
return;
|
|
}
|
|
|
|
auto cursor = QTextCursor(document()->docHandle(), 0);
|
|
cursor.movePosition(QTextCursor::End);
|
|
const auto fullSize = cursor.position();
|
|
const auto toRemove = fullSize - _maxLength;
|
|
if (toRemove > 0) {
|
|
if (toRemove > insertLength) {
|
|
if (insertLength) {
|
|
cursor.setPosition(insertPosition);
|
|
cursor.setPosition(
|
|
(insertPosition + insertLength),
|
|
QTextCursor::KeepAnchor);
|
|
cursor.removeSelectedText();
|
|
}
|
|
cursor.setPosition(fullSize - (toRemove - insertLength));
|
|
cursor.setPosition(fullSize, QTextCursor::KeepAnchor);
|
|
cursor.removeSelectedText();
|
|
} else {
|
|
cursor.setPosition(
|
|
insertPosition + (insertLength - toRemove));
|
|
cursor.setPosition(
|
|
insertPosition + insertLength,
|
|
QTextCursor::KeepAnchor);
|
|
cursor.removeSelectedText();
|
|
}
|
|
}
|
|
}
|
|
|
|
void InputField::handleContentsChanged() {
|
|
setErrorShown(false);
|
|
|
|
auto tagsChanged = false;
|
|
const auto currentText = getTextPart(
|
|
0,
|
|
-1,
|
|
_lastTextWithTags.tags,
|
|
tagsChanged,
|
|
_markdownEnabled ? &_lastMarkdownTags : nullptr);
|
|
|
|
//highlightMarkdown();
|
|
|
|
if (tagsChanged || (_lastTextWithTags.text != currentText)) {
|
|
_lastTextWithTags.text = currentText;
|
|
const auto weak = make_weak(this);
|
|
emit changed();
|
|
if (!weak) {
|
|
return;
|
|
}
|
|
checkContentHeight();
|
|
}
|
|
startPlaceholderAnimation();
|
|
if (App::wnd()) App::wnd()->updateGlobalMenu();
|
|
}
|
|
|
|
void InputField::highlightMarkdown() {
|
|
// Highlighting may interfere with markdown parsing -> inaccurate.
|
|
// For debug.
|
|
auto from = 0;
|
|
auto applyColor = [&](int a, int b, QColor color) {
|
|
auto cursor = textCursor();
|
|
cursor.setPosition(a);
|
|
cursor.setPosition(b, QTextCursor::KeepAnchor);
|
|
auto format = QTextCharFormat();
|
|
format.setForeground(color);
|
|
cursor.mergeCharFormat(format);
|
|
from = b;
|
|
};
|
|
for (const auto &tag : _lastMarkdownTags) {
|
|
if (tag.internalStart > from) {
|
|
applyColor(from, tag.internalStart, QColor(0, 0, 0));
|
|
} else if (tag.internalStart < from) {
|
|
continue;
|
|
}
|
|
applyColor(
|
|
tag.internalStart,
|
|
tag.internalStart + tag.internalLength,
|
|
(tag.closed
|
|
? QColor(0, 128, 0)
|
|
: QColor(128, 0, 0)));
|
|
}
|
|
auto cursor = textCursor();
|
|
cursor.movePosition(QTextCursor::End);
|
|
if (const auto till = cursor.position(); till > from) {
|
|
applyColor(from, till, QColor(0, 0, 0));
|
|
}
|
|
}
|
|
|
|
void InputField::onUndoAvailable(bool avail) {
|
|
_undoAvailable = avail;
|
|
if (App::wnd()) App::wnd()->updateGlobalMenu();
|
|
}
|
|
|
|
void InputField::onRedoAvailable(bool avail) {
|
|
_redoAvailable = avail;
|
|
if (App::wnd()) App::wnd()->updateGlobalMenu();
|
|
}
|
|
|
|
void InputField::setDisplayFocused(bool focused) {
|
|
setFocused(focused);
|
|
finishAnimating();
|
|
}
|
|
|
|
void InputField::selectAll() {
|
|
auto cursor = _inner->textCursor();
|
|
cursor.setPosition(0);
|
|
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
|
_inner->setTextCursor(cursor);
|
|
}
|
|
|
|
void InputField::finishAnimating() {
|
|
_a_focused.finish();
|
|
_a_error.finish();
|
|
_a_placeholderShifted.finish();
|
|
_a_borderShown.finish();
|
|
_a_borderOpacity.finish();
|
|
update();
|
|
}
|
|
|
|
void InputField::setPlaceholderHidden(bool forcePlaceholderHidden) {
|
|
_forcePlaceholderHidden = forcePlaceholderHidden;
|
|
startPlaceholderAnimation();
|
|
}
|
|
|
|
void InputField::startPlaceholderAnimation() {
|
|
const auto textLength = [&] {
|
|
return getTextWithTags().text.size() + _lastPreEditText.size();
|
|
};
|
|
const auto placeholderShifted = _forcePlaceholderHidden
|
|
|| (_focused && _st.placeholderScale > 0.)
|
|
|| (textLength() > _placeholderAfterSymbols);
|
|
if (_placeholderShifted != placeholderShifted) {
|
|
_placeholderShifted = placeholderShifted;
|
|
_a_placeholderShifted.start(
|
|
[=] { update(); },
|
|
_placeholderShifted ? 0. : 1.,
|
|
_placeholderShifted ? 1. : 0.,
|
|
_st.duration);
|
|
}
|
|
}
|
|
|
|
QMimeData *InputField::createMimeDataFromSelectionInner() const {
|
|
auto result = std::make_unique<QMimeData>();
|
|
const auto cursor = _inner->textCursor();
|
|
const auto start = cursor.selectionStart();
|
|
const auto end = cursor.selectionEnd();
|
|
if (end > start) {
|
|
auto textWithTags = getTextWithTagsPart(start, end);
|
|
result->setText(textWithTags.text);
|
|
if (!textWithTags.tags.isEmpty()) {
|
|
if (_tagMimeProcessor) {
|
|
for (auto &tag : textWithTags.tags) {
|
|
tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id);
|
|
}
|
|
}
|
|
result->setData(
|
|
TextUtilities::TagsMimeType(),
|
|
TextUtilities::SerializeTags(textWithTags.tags));
|
|
}
|
|
}
|
|
return result.release();
|
|
}
|
|
|
|
void InputField::customUpDown(bool isCustom) {
|
|
_customUpDown = isCustom;
|
|
}
|
|
|
|
void InputField::customTab(bool isCustom) {
|
|
_customTab = isCustom;
|
|
}
|
|
|
|
void InputField::setSubmitSettings(SubmitSettings settings) {
|
|
_submitSettings = settings;
|
|
}
|
|
|
|
not_null<QTextDocument*> InputField::document() {
|
|
return _inner->document();
|
|
}
|
|
|
|
not_null<const QTextDocument*> InputField::document() const {
|
|
return _inner->document();
|
|
}
|
|
|
|
void InputField::setTextCursor(const QTextCursor &cursor) {
|
|
return _inner->setTextCursor(cursor);
|
|
}
|
|
|
|
QTextCursor InputField::textCursor() const {
|
|
return _inner->textCursor();
|
|
}
|
|
|
|
void InputField::setCursorPosition(int pos) {
|
|
auto cursor = _inner->textCursor();
|
|
cursor.setPosition(pos);
|
|
_inner->setTextCursor(cursor);
|
|
}
|
|
|
|
void InputField::setText(const QString &text) {
|
|
setTextWithTags({ text, {} });
|
|
}
|
|
|
|
void InputField::setTextWithTags(
|
|
const TextWithTags &textWithTags,
|
|
HistoryAction historyAction) {
|
|
_insertedTags = textWithTags.tags;
|
|
_insertedTagsAreFromMime = false;
|
|
_realInsertPosition = 0;
|
|
_realCharsAdded = textWithTags.text.size();
|
|
const auto document = _inner->document();
|
|
auto cursor = QTextCursor(document->docHandle(), 0);
|
|
if (historyAction == HistoryAction::Clear) {
|
|
document->setUndoRedoEnabled(false);
|
|
cursor.beginEditBlock();
|
|
} else if (historyAction == HistoryAction::MergeEntry) {
|
|
cursor.joinPreviousEditBlock();
|
|
} else {
|
|
cursor.beginEditBlock();
|
|
}
|
|
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
|
cursor.insertText(textWithTags.text);
|
|
cursor.movePosition(QTextCursor::End);
|
|
cursor.endEditBlock();
|
|
if (historyAction == HistoryAction::Clear) {
|
|
document->setUndoRedoEnabled(true);
|
|
}
|
|
_insertedTags.clear();
|
|
_realInsertPosition = -1;
|
|
finishAnimating();
|
|
}
|
|
|
|
TextWithTags InputField::getTextWithTagsPart(int start, int end) const {
|
|
auto changed = false;
|
|
auto result = TextWithTags();
|
|
result.text = getTextPart(start, end, result.tags, changed);
|
|
return result;
|
|
}
|
|
|
|
TextWithTags InputField::getTextWithAppliedMarkdown() const {
|
|
if (!_markdownEnabled || _lastMarkdownTags.empty()) {
|
|
return getTextWithTags();
|
|
}
|
|
const auto &originalText = _lastTextWithTags.text;
|
|
const auto &originalTags = _lastTextWithTags.tags;
|
|
|
|
// Ignore tags that partially intersect some http-links.
|
|
// This will allow sending http://test.com/__test__/test correctly.
|
|
const auto links = TextUtilities::ParseEntities(
|
|
originalText,
|
|
0).entities;
|
|
|
|
auto result = TextWithTags();
|
|
result.text.reserve(originalText.size());
|
|
result.tags.reserve(originalTags.size() + _lastMarkdownTags.size());
|
|
auto removed = 0;
|
|
auto originalTag = originalTags.begin();
|
|
const auto originalTagsEnd = originalTags.end();
|
|
const auto addOriginalTagsUpTill = [&](int offset) {
|
|
while (originalTag != originalTagsEnd
|
|
&& originalTag->offset + originalTag->length <= offset) {
|
|
result.tags.push_back(*originalTag++);
|
|
result.tags.back().offset -= removed;
|
|
}
|
|
};
|
|
auto from = 0;
|
|
const auto addOriginalTextUpTill = [&](int offset) {
|
|
if (offset > from) {
|
|
result.text.append(originalText.midRef(from, offset - from));
|
|
}
|
|
};
|
|
auto link = links.begin();
|
|
const auto linksEnd = links.end();
|
|
for (const auto &tag : _lastMarkdownTags) {
|
|
const auto tagLength = int(tag.tag.size());
|
|
if (!tag.closed || tag.adjustedStart < from) {
|
|
continue;
|
|
}
|
|
auto entityLength = tag.adjustedLength - 2 * tagLength;
|
|
if (entityLength <= 0) {
|
|
continue;
|
|
}
|
|
addOriginalTagsUpTill(tag.adjustedStart);
|
|
const auto tagAdjustedEnd = tag.adjustedStart + tag.adjustedLength;
|
|
if (originalTag != originalTagsEnd
|
|
&& originalTag->offset < tagAdjustedEnd) {
|
|
continue;
|
|
}
|
|
while (link != linksEnd
|
|
&& link->offset() + link->length() <= tag.adjustedStart) {
|
|
++link;
|
|
}
|
|
if (link != linksEnd
|
|
&& link->offset() < tagAdjustedEnd
|
|
&& (link->offset() + link->length() > tagAdjustedEnd
|
|
|| link->offset() < tag.adjustedStart)) {
|
|
continue;
|
|
}
|
|
addOriginalTextUpTill(tag.adjustedStart);
|
|
|
|
auto entityStart = tag.adjustedStart + tagLength;
|
|
if (tag.tag == kTagPre) {
|
|
// Remove redundant newlines for pre.
|
|
// If ``` is on a separate line add only one newline.
|
|
if (IsNewline(originalText[entityStart])
|
|
&& (result.text.isEmpty()
|
|
|| IsNewline(result.text[result.text.size() - 1]))) {
|
|
++entityStart;
|
|
--entityLength;
|
|
}
|
|
const auto entityEnd = entityStart + entityLength;
|
|
if (IsNewline(originalText[entityEnd - 1])
|
|
&& (originalText.size() <= entityEnd + tagLength
|
|
|| IsNewline(originalText[entityEnd + tagLength]))) {
|
|
--entityLength;
|
|
}
|
|
}
|
|
|
|
if (entityLength > 0) {
|
|
// Add tag text and entity.
|
|
result.tags.push_back(TextWithTags::Tag{
|
|
int(result.text.size()),
|
|
entityLength,
|
|
tag.tag });
|
|
result.text.append(originalText.midRef(
|
|
entityStart,
|
|
entityLength));
|
|
}
|
|
|
|
from = tag.adjustedStart + tag.adjustedLength;
|
|
removed += (tag.adjustedLength - entityLength);
|
|
}
|
|
addOriginalTagsUpTill(originalText.size());
|
|
addOriginalTextUpTill(originalText.size());
|
|
return result;
|
|
}
|
|
|
|
void InputField::clear() {
|
|
_inner->clear();
|
|
startPlaceholderAnimation();
|
|
}
|
|
|
|
bool InputField::hasFocus() const {
|
|
return _inner->hasFocus();
|
|
}
|
|
|
|
void InputField::setFocus() {
|
|
_inner->setFocus();
|
|
}
|
|
|
|
void InputField::clearFocus() {
|
|
_inner->clearFocus();
|
|
}
|
|
|
|
void InputField::ensureCursorVisible() {
|
|
_inner->ensureCursorVisible();
|
|
}
|
|
|
|
not_null<QTextEdit*> InputField::rawTextEdit() {
|
|
return _inner.get();
|
|
}
|
|
|
|
not_null<const QTextEdit*> InputField::rawTextEdit() const {
|
|
return _inner.get();
|
|
}
|
|
|
|
bool InputField::ShouldSubmit(
|
|
SubmitSettings settings,
|
|
Qt::KeyboardModifiers modifiers) {
|
|
const auto shift = modifiers.testFlag(Qt::ShiftModifier);
|
|
const auto ctrl = modifiers.testFlag(Qt::ControlModifier)
|
|
|| modifiers.testFlag(Qt::MetaModifier);
|
|
return (ctrl && shift)
|
|
|| (ctrl
|
|
&& settings != SubmitSettings::None
|
|
&& settings != SubmitSettings::Enter)
|
|
|| (!ctrl
|
|
&& !shift
|
|
&& settings != SubmitSettings::None
|
|
&& settings != SubmitSettings::CtrlEnter);
|
|
}
|
|
|
|
void InputField::keyPressEventInner(QKeyEvent *e) {
|
|
bool shift = e->modifiers().testFlag(Qt::ShiftModifier), alt = e->modifiers().testFlag(Qt::AltModifier);
|
|
bool macmeta = (cPlatform() == dbipMac || cPlatform() == dbipMacOld) && e->modifiers().testFlag(Qt::ControlModifier) && !e->modifiers().testFlag(Qt::MetaModifier) && !e->modifiers().testFlag(Qt::AltModifier);
|
|
bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier);
|
|
bool enterSubmit = (_mode == Mode::SingleLine)
|
|
|| ShouldSubmit(_submitSettings, e->modifiers());
|
|
bool enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return);
|
|
if (e->key() == Qt::Key_Left
|
|
|| e->key() == Qt::Key_Right
|
|
|| e->key() == Qt::Key_Up
|
|
|| e->key() == Qt::Key_Down
|
|
|| e->key() == Qt::Key_Home
|
|
|| e->key() == Qt::Key_End) {
|
|
_reverseMarkdownReplacement = false;
|
|
}
|
|
|
|
if (macmeta && e->key() == Qt::Key_Backspace) {
|
|
QTextCursor tc(textCursor()), start(tc);
|
|
start.movePosition(QTextCursor::StartOfLine);
|
|
tc.setPosition(start.position(), QTextCursor::KeepAnchor);
|
|
tc.removeSelectedText();
|
|
} else if (e->key() == Qt::Key_Backspace
|
|
&& e->modifiers() == 0
|
|
&& revertFormatReplace()) {
|
|
e->accept();
|
|
} else if (enter && enterSubmit) {
|
|
emit submitted(e->modifiers());
|
|
} else if (e->key() == Qt::Key_Escape) {
|
|
e->ignore();
|
|
emit cancelled();
|
|
} else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab) {
|
|
if (alt || ctrl) {
|
|
e->ignore();
|
|
} else if (_customTab) {
|
|
emit tabbed();
|
|
} else if (!focusNextPrevChild(e->key() == Qt::Key_Tab && !shift)) {
|
|
e->ignore();
|
|
}
|
|
} else if (e->key() == Qt::Key_Search || e == QKeySequence::Find) {
|
|
e->ignore();
|
|
} else if (handleMarkdownKey(e)) {
|
|
e->accept();
|
|
} else if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) {
|
|
e->ignore();
|
|
#ifdef Q_OS_MAC
|
|
} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
|
|
const auto cursor = textCursor();
|
|
const auto start = cursor.selectionStart();
|
|
const auto end = cursor.selectionEnd();
|
|
if (end > start) {
|
|
QApplication::clipboard()->setText(
|
|
getTextWithTagsPart(start, end).text,
|
|
QClipboard::FindBuffer);
|
|
}
|
|
#endif // Q_OS_MAC
|
|
} else {
|
|
const auto text = e->text();
|
|
const auto oldPosition = textCursor().position();
|
|
if (enter && ctrl) {
|
|
e->setModifiers(e->modifiers() & ~Qt::ControlModifier);
|
|
}
|
|
_inner->QTextEdit::keyPressEvent(e);
|
|
auto cursor = textCursor();
|
|
if (cursor.position() == oldPosition) {
|
|
bool check = false;
|
|
if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) {
|
|
cursor.movePosition(QTextCursor::Start, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor);
|
|
check = true;
|
|
} else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_Down) {
|
|
cursor.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor);
|
|
check = true;
|
|
} else if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Right || e->key() == Qt::Key_Backspace) {
|
|
e->ignore();
|
|
}
|
|
if (check) {
|
|
if (oldPosition == cursor.position()) {
|
|
e->ignore();
|
|
} else {
|
|
setTextCursor(cursor);
|
|
}
|
|
}
|
|
}
|
|
if (!processMarkdownReplaces(text)) {
|
|
processInstantReplaces(text);
|
|
}
|
|
}
|
|
}
|
|
|
|
TextWithTags InputField::getTextWithTagsSelected() const {
|
|
const auto cursor = textCursor();
|
|
const auto start = cursor.selectionStart();
|
|
const auto end = cursor.selectionEnd();
|
|
return (end > start) ? getTextWithTagsPart(start, end) : TextWithTags();
|
|
}
|
|
|
|
bool InputField::handleMarkdownKey(QKeyEvent *e) {
|
|
if (!_markdownEnabled) {
|
|
return false;
|
|
}
|
|
const auto matches = [&](const QKeySequence &sequence) {
|
|
const auto searchKey = (e->modifiers() | e->key())
|
|
& ~(Qt::KeypadModifier | Qt::GroupSwitchModifier);
|
|
const auto events = QKeySequence(searchKey);
|
|
return sequence.matches(events) == QKeySequence::ExactMatch;
|
|
};
|
|
if (e == QKeySequence::Bold) {
|
|
toggleSelectionMarkdown(kTagBold);
|
|
} else if (e == QKeySequence::Italic) {
|
|
toggleSelectionMarkdown(kTagItalic);
|
|
} else if (matches(kMonospaceSequence)) {
|
|
toggleSelectionMarkdown(kTagCode);
|
|
} else if (matches(kClearFormatSequence)) {
|
|
clearSelectionMarkdown();
|
|
} else if (matches(kEditLinkSequence) && _editLinkCallback) {
|
|
const auto cursor = textCursor();
|
|
editMarkdownLink({
|
|
cursor.selectionStart(),
|
|
cursor.selectionEnd()
|
|
});
|
|
} else {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
auto InputField::selectionEditLinkData(EditLinkSelection selection) const
|
|
-> EditLinkData {
|
|
Expects(_editLinkCallback != nullptr);
|
|
|
|
const auto position = (selection.from == selection.till
|
|
&& selection.from > 0)
|
|
? (selection.from - 1)
|
|
: selection.from;
|
|
const auto link = [&] {
|
|
return (position != selection.till)
|
|
? GetFullSimpleTextTag(
|
|
getTextWithTagsPart(position, selection.till))
|
|
: QString();
|
|
}();
|
|
const auto simple = EditLinkData {
|
|
selection.from,
|
|
selection.till,
|
|
QString()
|
|
};
|
|
if (!_editLinkCallback(selection, {}, link, EditLinkAction::Check)) {
|
|
return simple;
|
|
}
|
|
Assert(!link.isEmpty());
|
|
|
|
struct State {
|
|
QTextBlock block;
|
|
QTextBlock::iterator i;
|
|
};
|
|
const auto document = _inner->document();
|
|
const auto skipInvalid = [&](State &state) {
|
|
if (state.block == document->end()) {
|
|
return false;
|
|
}
|
|
while (state.i.atEnd()) {
|
|
state.block = state.block.next();
|
|
if (state.block == document->end()) {
|
|
return false;
|
|
}
|
|
state.i = state.block.begin();
|
|
}
|
|
return true;
|
|
};
|
|
const auto moveToNext = [&](State &state) {
|
|
Expects(state.block != document->end());
|
|
Expects(!state.i.atEnd());
|
|
|
|
++state.i;
|
|
};
|
|
const auto moveToPrevious = [&](State &state) {
|
|
Expects(state.block != document->end());
|
|
Expects(!state.i.atEnd());
|
|
|
|
while (state.i == state.block.begin()) {
|
|
if (state.block == document->begin()) {
|
|
state.block = document->end();
|
|
return false;
|
|
}
|
|
state.block = state.block.previous();
|
|
state.i = state.block.end();
|
|
}
|
|
--state.i;
|
|
return true;
|
|
};
|
|
const auto stateTag = [&](const State &state) {
|
|
const auto format = state.i.fragment().charFormat();
|
|
return format.property(kTagProperty).toString();
|
|
};
|
|
const auto stateStart = [&](const State &state) {
|
|
return state.i.fragment().position();
|
|
};
|
|
const auto stateEnd = [&](const State &state) {
|
|
const auto fragment = state.i.fragment();
|
|
return fragment.position() + fragment.length();
|
|
};
|
|
auto state = State{ document->findBlock(position) };
|
|
if (state.block != document->end()) {
|
|
state.i = state.block.begin();
|
|
}
|
|
for (; skipInvalid(state); moveToNext(state)) {
|
|
const auto fragmentStart = stateStart(state);
|
|
const auto fragmentEnd = stateEnd(state);
|
|
if (fragmentEnd <= position) {
|
|
continue;
|
|
} else if (fragmentStart >= selection.till) {
|
|
break;
|
|
}
|
|
if (stateTag(state) == link) {
|
|
auto start = fragmentStart;
|
|
auto finish = fragmentEnd;
|
|
auto copy = state;
|
|
while (moveToPrevious(copy) && (stateTag(copy) == link)) {
|
|
start = stateStart(copy);
|
|
}
|
|
while (skipInvalid(state) && (stateTag(state) == link)) {
|
|
finish = stateEnd(state);
|
|
moveToNext(state);
|
|
}
|
|
return { start, finish, link };
|
|
}
|
|
}
|
|
return simple;
|
|
}
|
|
|
|
auto InputField::editLinkSelection(QContextMenuEvent *e) const
|
|
-> EditLinkSelection {
|
|
const auto cursor = textCursor();
|
|
if (!cursor.hasSelection() && e->reason() == QContextMenuEvent::Mouse) {
|
|
const auto clickCursor = _inner->cursorForPosition(
|
|
_inner->viewport()->mapFromGlobal(e->globalPos()));
|
|
if (!clickCursor.isNull() && !clickCursor.hasSelection()) {
|
|
return {
|
|
clickCursor.position(),
|
|
clickCursor.position()
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
cursor.selectionStart(),
|
|
cursor.selectionEnd()
|
|
};
|
|
}
|
|
|
|
void InputField::editMarkdownLink(EditLinkSelection selection) {
|
|
if (!_editLinkCallback) {
|
|
return;
|
|
}
|
|
const auto data = selectionEditLinkData(selection);
|
|
_editLinkCallback(
|
|
selection,
|
|
getTextWithTagsPart(data.from, data.till).text,
|
|
data.link,
|
|
EditLinkAction::Edit);
|
|
}
|
|
|
|
void InputField::inputMethodEventInner(QInputMethodEvent *e) {
|
|
const auto preedit = e->preeditString();
|
|
if (_lastPreEditText != preedit) {
|
|
_lastPreEditText = preedit;
|
|
startPlaceholderAnimation();
|
|
}
|
|
const auto text = e->commitString();
|
|
_inner->QTextEdit::inputMethodEvent(e);
|
|
if (!processMarkdownReplaces(text)) {
|
|
processInstantReplaces(text);
|
|
}
|
|
}
|
|
|
|
const InstantReplaces &InputField::instantReplaces() const {
|
|
return _mutableInstantReplaces;
|
|
}
|
|
|
|
// Disable markdown instant replacement.
|
|
bool InputField::processMarkdownReplaces(const QString &appended) {
|
|
//if (appended.size() != 1 || !_markdownEnabled) {
|
|
// return false;
|
|
//}
|
|
//const auto ch = appended[0];
|
|
//if (ch == '`') {
|
|
// return processMarkdownReplace(kTagCode)
|
|
// || processMarkdownReplace(kTagPre);
|
|
//} else if (ch == '*') {
|
|
// return processMarkdownReplace(kTagBold);
|
|
//} else if (ch == '_') {
|
|
// return processMarkdownReplace(kTagItalic);
|
|
//}
|
|
return false;
|
|
}
|
|
|
|
//bool InputField::processMarkdownReplace(const QString &tag) {
|
|
// const auto position = textCursor().position();
|
|
// const auto tagLength = tag.size();
|
|
// const auto start = [&] {
|
|
// for (const auto &possible : _lastMarkdownTags) {
|
|
// const auto end = possible.start + possible.length;
|
|
// if (possible.start + 2 * tagLength >= position) {
|
|
// return MarkdownTag();
|
|
// } else if (end >= position || end + tagLength == position) {
|
|
// if (possible.tag == tag) {
|
|
// return possible;
|
|
// }
|
|
// }
|
|
// }
|
|
// return MarkdownTag();
|
|
// }();
|
|
// if (start.tag.isEmpty()) {
|
|
// return false;
|
|
// }
|
|
// return commitMarkdownReplacement(start.start, position, tag, tag);
|
|
//}
|
|
|
|
void InputField::processInstantReplaces(const QString &appended) {
|
|
const auto &replaces = instantReplaces();
|
|
if (appended.size() != 1
|
|
|| !_instantReplacesEnabled
|
|
|| !replaces.maxLength) {
|
|
return;
|
|
}
|
|
const auto it = replaces.reverseMap.tail.find(appended[0]);
|
|
if (it == end(replaces.reverseMap.tail)) {
|
|
return;
|
|
}
|
|
const auto position = textCursor().position();
|
|
for (const auto &tag : _lastMarkdownTags) {
|
|
if (tag.internalStart < position
|
|
&& tag.internalStart + tag.internalLength >= position
|
|
&& (tag.tag == kTagCode || tag.tag == kTagPre)) {
|
|
return;
|
|
}
|
|
}
|
|
const auto typed = getTextWithTagsPart(
|
|
std::max(position - replaces.maxLength, 0),
|
|
position - 1).text;
|
|
auto node = &it->second;
|
|
auto i = typed.size();
|
|
do {
|
|
if (!node->text.isEmpty()) {
|
|
applyInstantReplace(typed.mid(i) + appended, node->text);
|
|
return;
|
|
} else if (!i) {
|
|
return;
|
|
}
|
|
const auto it = node->tail.find(typed[--i]);
|
|
if (it == end(node->tail)) {
|
|
return;
|
|
}
|
|
node = &it->second;
|
|
} while (true);
|
|
}
|
|
|
|
void InputField::applyInstantReplace(
|
|
const QString &what,
|
|
const QString &with) {
|
|
const auto length = int(what.size());
|
|
const auto cursor = textCursor();
|
|
const auto position = cursor.position();
|
|
if (cursor.hasSelection()) {
|
|
return;
|
|
} else if (position < length) {
|
|
return;
|
|
}
|
|
commitInstantReplacement(position - length, position, with, what);
|
|
}
|
|
|
|
void InputField::commitInstantReplacement(
|
|
int from,
|
|
int till,
|
|
const QString &with,
|
|
std::optional<QString> checkOriginal) {
|
|
const auto original = getTextWithTagsPart(from, till).text;
|
|
if (checkOriginal
|
|
&& checkOriginal->compare(original, Qt::CaseInsensitive) != 0) {
|
|
return;
|
|
}
|
|
|
|
auto cursor = textCursor();
|
|
const auto currentTag = cursor.charFormat().property(kTagProperty);
|
|
if (currentTag == kTagPre || currentTag == kTagCode) {
|
|
return;
|
|
}
|
|
cursor.setPosition(from);
|
|
cursor.setPosition(till, QTextCursor::KeepAnchor);
|
|
|
|
auto format = [&]() -> QTextCharFormat {
|
|
auto emojiLength = 0;
|
|
const auto emoji = Ui::Emoji::Find(with, &emojiLength);
|
|
if (!emoji || with.size() != emojiLength) {
|
|
return _defaultCharFormat;
|
|
}
|
|
const auto use = [&] {
|
|
if (!emoji->hasVariants()) {
|
|
return emoji;
|
|
}
|
|
const auto nonColored = emoji->nonColoredId();
|
|
const auto it = cEmojiVariants().constFind(nonColored);
|
|
return (it != cEmojiVariants().cend())
|
|
? emoji->variant(it.value())
|
|
: emoji;
|
|
}();
|
|
Ui::Emoji::AddRecent(use);
|
|
return PrepareEmojiFormat(use, _st.font);
|
|
}();
|
|
const auto replacement = format.isImageFormat()
|
|
? kObjectReplacement
|
|
: with;
|
|
format.setProperty(kInstantReplaceWhatId, original);
|
|
format.setProperty(kInstantReplaceWithId, replacement);
|
|
format.setProperty(kInstantReplaceRandomId, rand_value<uint32>());
|
|
ApplyTagFormat(format, cursor.charFormat());
|
|
cursor.insertText(replacement, format);
|
|
}
|
|
|
|
bool InputField::commitMarkdownReplacement(
|
|
int from,
|
|
int till,
|
|
const QString &tag,
|
|
const QString &edge) {
|
|
const auto end = [&] {
|
|
auto cursor = QTextCursor(document()->docHandle(), 0);
|
|
cursor.movePosition(QTextCursor::End);
|
|
return cursor.position();
|
|
}();
|
|
|
|
// In case of 'pre' tag extend checked text by one symbol.
|
|
// So that we'll know if we need to insert additional newlines.
|
|
// "Test ```test``` Test" should become three-line text.
|
|
const auto blocktag = (tag == kTagPre);
|
|
const auto extendLeft = (blocktag && from > 0) ? 1 : 0;
|
|
const auto extendRight = (blocktag && till < end) ? 1 : 0;
|
|
const auto extended = getTextWithTagsPart(
|
|
from - extendLeft,
|
|
till + extendRight).text;
|
|
const auto outer = extended.midRef(
|
|
extendLeft,
|
|
extended.size() - extendLeft - extendRight);
|
|
if ((outer.size() <= 2 * edge.size())
|
|
|| (!edge.isEmpty()
|
|
&& !(outer.startsWith(edge) && outer.endsWith(edge)))) {
|
|
return false;
|
|
}
|
|
|
|
// In case of 'pre' tag check if we need to remove one of two newlines.
|
|
// "Test\n```\ntest\n```" should become two-line text + newline.
|
|
const auto innerRight = edge.size();
|
|
const auto checkIfTwoNewlines = blocktag
|
|
&& (extendLeft > 0)
|
|
&& IsNewline(extended[0]);
|
|
const auto innerLeft = [&] {
|
|
const auto simple = edge.size();
|
|
if (!checkIfTwoNewlines) {
|
|
return simple;
|
|
}
|
|
const auto last = outer.size() - innerRight;
|
|
for (auto check = simple; check != last; ++check) {
|
|
const auto ch = outer.at(check);
|
|
if (IsNewline(ch)) {
|
|
return check + 1;
|
|
} else if (!chIsSpace(ch)) {
|
|
break;
|
|
}
|
|
}
|
|
return simple;
|
|
}();
|
|
const auto innerLength = outer.size() - innerLeft - innerRight;
|
|
|
|
// Prepare the final "insert" replacement for the "outer" text part.
|
|
const auto newlineleft = blocktag
|
|
&& (extendLeft > 0)
|
|
&& !IsNewline(extended[0])
|
|
&& !IsNewline(outer.at(innerLeft));
|
|
const auto newlineright = blocktag
|
|
&& (!extendRight || !IsNewline(extended[extended.size() - 1]))
|
|
&& !IsNewline(outer.at(outer.size() - innerRight - 1));
|
|
const auto insert = (newlineleft ? "\n" : "")
|
|
+ outer.mid(innerLeft, innerLength).toString()
|
|
+ (newlineright ? "\n" : "");
|
|
|
|
// Trim inserted tag, so that all newlines are left outside.
|
|
_insertedTags.clear();
|
|
auto tagFrom = newlineleft ? 1 : 0;
|
|
auto tagTill = insert.size() - (newlineright ? 1 : 0);
|
|
for (; tagFrom != tagTill; ++tagFrom) {
|
|
const auto ch = insert.at(tagFrom);
|
|
if (!IsNewline(ch)) {
|
|
break;
|
|
}
|
|
}
|
|
for (; tagTill != tagFrom; --tagTill) {
|
|
const auto ch = insert.at(tagTill - 1);
|
|
if (!IsNewline(ch)) {
|
|
break;
|
|
}
|
|
}
|
|
if (tagTill > tagFrom) {
|
|
_insertedTags.push_back({
|
|
tagFrom,
|
|
tagTill - tagFrom,
|
|
tag,
|
|
});
|
|
}
|
|
|
|
// Replace.
|
|
auto cursor = _inner->textCursor();
|
|
cursor.setPosition(from);
|
|
cursor.setPosition(till, QTextCursor::KeepAnchor);
|
|
auto format = _defaultCharFormat;
|
|
if (!edge.isEmpty()) {
|
|
format.setProperty(kReplaceTagId, edge);
|
|
_reverseMarkdownReplacement = true;
|
|
}
|
|
_insertedTagsAreFromMime = false;
|
|
cursor.insertText(insert, format);
|
|
_insertedTags.clear();
|
|
|
|
cursor.setCharFormat(_defaultCharFormat);
|
|
_inner->setTextCursor(cursor);
|
|
return true;
|
|
}
|
|
|
|
bool InputField::IsValidMarkdownLink(const QString &link) {
|
|
return ::Ui::IsValidMarkdownLink(link);
|
|
}
|
|
|
|
void InputField::commitMarkdownLinkEdit(
|
|
EditLinkSelection selection,
|
|
const QString &text,
|
|
const QString &link) {
|
|
if (text.isEmpty()
|
|
|| !IsValidMarkdownLink(link)
|
|
|| !_editLinkCallback) {
|
|
return;
|
|
}
|
|
_insertedTags.clear();
|
|
_insertedTags.push_back({ 0, text.size(), link });
|
|
|
|
auto cursor = textCursor();
|
|
const auto editData = selectionEditLinkData(selection);
|
|
cursor.setPosition(editData.from);
|
|
cursor.setPosition(editData.till, QTextCursor::KeepAnchor);
|
|
auto format = _defaultCharFormat;
|
|
_insertedTagsAreFromMime = false;
|
|
cursor.insertText(
|
|
(editData.from == editData.till) ? (text + QChar(' ')) : text,
|
|
_defaultCharFormat);
|
|
_insertedTags.clear();
|
|
|
|
_reverseMarkdownReplacement = false;
|
|
cursor.setCharFormat(_defaultCharFormat);
|
|
_inner->setTextCursor(cursor);
|
|
}
|
|
|
|
void InputField::toggleSelectionMarkdown(const QString &tag) {
|
|
_reverseMarkdownReplacement = false;
|
|
const auto cursor = textCursor();
|
|
const auto position = cursor.position();
|
|
const auto from = cursor.selectionStart();
|
|
const auto till = cursor.selectionEnd();
|
|
if (from == till) {
|
|
return;
|
|
}
|
|
if (tag.isEmpty()
|
|
|| GetFullSimpleTextTag(getTextWithTagsSelected()) == tag) {
|
|
RemoveDocumentTags(_st, document(), from, till);
|
|
return;
|
|
}
|
|
const auto commitTag = [&] {
|
|
if (tag != kTagCode) {
|
|
return tag;
|
|
}
|
|
const auto leftForBlock = [&] {
|
|
if (!from) {
|
|
return true;
|
|
}
|
|
const auto text = getTextWithTagsPart(from - 1, from + 1).text;
|
|
return text.isEmpty()
|
|
|| IsNewline(text[0])
|
|
|| IsNewline(text[text.size() - 1]);
|
|
}();
|
|
const auto rightForBlock = [&] {
|
|
const auto text = getTextWithTagsPart(till - 1, till + 1).text;
|
|
return text.isEmpty()
|
|
|| IsNewline(text[0])
|
|
|| IsNewline(text[text.size() - 1]);
|
|
}();
|
|
return (leftForBlock && rightForBlock) ? kTagPre : kTagCode;
|
|
}();
|
|
commitMarkdownReplacement(from, till, commitTag);
|
|
auto restorePosition = textCursor();
|
|
restorePosition.setPosition((position == till) ? from : till);
|
|
restorePosition.setPosition(position, QTextCursor::KeepAnchor);
|
|
setTextCursor(restorePosition);
|
|
}
|
|
|
|
void InputField::clearSelectionMarkdown() {
|
|
toggleSelectionMarkdown(QString());
|
|
}
|
|
|
|
bool InputField::revertFormatReplace() {
|
|
const auto cursor = textCursor();
|
|
const auto position = cursor.position();
|
|
if (position <= 0 || cursor.hasSelection()) {
|
|
return false;
|
|
}
|
|
const auto inside = position - 1;
|
|
const auto document = _inner->document();
|
|
const auto block = document->findBlock(inside);
|
|
if (block == document->end()) {
|
|
return false;
|
|
}
|
|
for (auto i = block.begin(); !i.atEnd(); ++i) {
|
|
const auto fragment = i.fragment();
|
|
const auto fragmentStart = fragment.position();
|
|
const auto fragmentEnd = fragmentStart + fragment.length();
|
|
if (fragmentEnd <= inside) {
|
|
continue;
|
|
} else if (fragmentStart > inside || fragmentEnd != position) {
|
|
return false;
|
|
}
|
|
const auto current = fragment.charFormat();
|
|
if (current.hasProperty(kInstantReplaceWithId)) {
|
|
const auto with = current.property(kInstantReplaceWithId);
|
|
const auto string = with.toString();
|
|
if (fragment.text() != string) {
|
|
return false;
|
|
}
|
|
auto replaceCursor = cursor;
|
|
replaceCursor.setPosition(fragmentStart);
|
|
replaceCursor.setPosition(fragmentEnd, QTextCursor::KeepAnchor);
|
|
const auto what = current.property(kInstantReplaceWhatId);
|
|
auto format = _defaultCharFormat;
|
|
ApplyTagFormat(format, current);
|
|
replaceCursor.insertText(what.toString(), format);
|
|
return true;
|
|
} else if (_reverseMarkdownReplacement
|
|
&& current.hasProperty(kReplaceTagId)) {
|
|
const auto tag = current.property(kReplaceTagId).toString();
|
|
if (tag.isEmpty()) {
|
|
return false;
|
|
} else if (auto test = i; !(++test).atEnd()) {
|
|
const auto format = test.fragment().charFormat();
|
|
if (format.property(kReplaceTagId).toString() == tag) {
|
|
return false;
|
|
}
|
|
} else if (auto test = block; test.next() != document->end()) {
|
|
const auto begin = test.begin();
|
|
if (begin != test.end()) {
|
|
const auto format = begin.fragment().charFormat();
|
|
if (format.property(kReplaceTagId).toString() == tag) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto first = [&] {
|
|
auto checkBlock = block;
|
|
auto checkLast = i;
|
|
while (true) {
|
|
for (auto j = checkLast; j != checkBlock.begin();) {
|
|
--j;
|
|
const auto format = j.fragment().charFormat();
|
|
if (format.property(kReplaceTagId) != tag) {
|
|
return ++j;
|
|
}
|
|
}
|
|
if (checkBlock == document->begin()) {
|
|
return checkBlock.begin();
|
|
}
|
|
checkBlock = checkBlock.previous();
|
|
checkLast = checkBlock.end();
|
|
}
|
|
}();
|
|
const auto from = first.fragment().position();
|
|
const auto till = fragmentEnd;
|
|
auto replaceCursor = cursor;
|
|
replaceCursor.setPosition(from);
|
|
replaceCursor.setPosition(till, QTextCursor::KeepAnchor);
|
|
replaceCursor.insertText(
|
|
tag + getTextWithTagsPart(from, till).text + tag,
|
|
_defaultCharFormat);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void InputField::contextMenuEventInner(QContextMenuEvent *e) {
|
|
if (const auto menu = _inner->createStandardContextMenu()) {
|
|
addMarkdownActions(menu, e);
|
|
_contextMenu = base::make_unique_q<Ui::PopupMenu>(this, menu);
|
|
_contextMenu->popup(e->globalPos());
|
|
}
|
|
}
|
|
|
|
void InputField::addMarkdownActions(
|
|
not_null<QMenu*> menu,
|
|
QContextMenuEvent *e) {
|
|
if (!_markdownEnabled) {
|
|
return;
|
|
}
|
|
const auto formatting = new QAction(lang(lng_menu_formatting), menu);
|
|
addMarkdownMenuAction(menu, formatting);
|
|
|
|
const auto submenu = new QMenu(menu);
|
|
formatting->setMenu(submenu);
|
|
|
|
const auto textWithTags = getTextWithTagsSelected();
|
|
const auto &text = textWithTags.text;
|
|
const auto &tags = textWithTags.tags;
|
|
const auto hasText = !text.isEmpty();
|
|
const auto hasTags = !tags.isEmpty();
|
|
const auto disabled = (!_editLinkCallback && !hasText);
|
|
formatting->setDisabled(disabled);
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
const auto fullTag = GetFullSimpleTextTag(textWithTags);
|
|
const auto add = [&](
|
|
LangKey key,
|
|
QKeySequence sequence,
|
|
bool disabled,
|
|
auto callback) {
|
|
const auto add = sequence.isEmpty()
|
|
? QString()
|
|
: QChar('\t') + sequence.toString(QKeySequence::NativeText);
|
|
const auto action = new QAction(lang(key) + add, submenu);
|
|
connect(action, &QAction::triggered, this, callback);
|
|
action->setDisabled(disabled);
|
|
submenu->addAction(action);
|
|
};
|
|
const auto addtag = [&](
|
|
LangKey key,
|
|
QKeySequence sequence,
|
|
const QString &tag) {
|
|
const auto disabled = (fullTag == tag)
|
|
|| (fullTag == kTagPre && tag == kTagCode);
|
|
add(key, sequence, (!hasText || fullTag == tag), [=] {
|
|
toggleSelectionMarkdown(tag);
|
|
});
|
|
};
|
|
const auto addlink = [&] {
|
|
const auto selection = editLinkSelection(e);
|
|
const auto data = selectionEditLinkData(selection);
|
|
const auto key = data.link.isEmpty()
|
|
? lng_menu_formatting_link_create
|
|
: lng_menu_formatting_link_edit;
|
|
add(key, kEditLinkSequence, false, [=] {
|
|
editMarkdownLink(selection);
|
|
});
|
|
};
|
|
const auto addclear = [&] {
|
|
const auto disabled = !hasText || !hasTags;
|
|
add(lng_menu_formatting_clear, kClearFormatSequence, disabled, [=] {
|
|
clearSelectionMarkdown();
|
|
});
|
|
};
|
|
|
|
addtag(lng_menu_formatting_bold, QKeySequence::Bold, kTagBold);
|
|
addtag(lng_menu_formatting_italic, QKeySequence::Italic, kTagItalic);
|
|
addtag(lng_menu_formatting_monospace, kMonospaceSequence, kTagCode);
|
|
|
|
if (_editLinkCallback) {
|
|
submenu->addSeparator();
|
|
addlink();
|
|
}
|
|
|
|
submenu->addSeparator();
|
|
addclear();
|
|
}
|
|
|
|
void InputField::addMarkdownMenuAction(
|
|
not_null<QMenu*> menu,
|
|
not_null<QAction*> action) {
|
|
const auto actions = menu->actions();
|
|
const auto before = [&] {
|
|
auto seenAfter = false;
|
|
for (const auto action : actions) {
|
|
if (seenAfter) {
|
|
return action;
|
|
} else if (action->objectName() == qstr("edit-delete")) {
|
|
seenAfter = true;
|
|
}
|
|
}
|
|
return (QAction*)nullptr;
|
|
}();
|
|
menu->insertSeparator(before);
|
|
menu->insertAction(before, action);
|
|
}
|
|
|
|
void InputField::dropEventInner(QDropEvent *e) {
|
|
_inDrop = true;
|
|
_inner->QTextEdit::dropEvent(e);
|
|
_inDrop = false;
|
|
_insertedTags.clear();
|
|
_realInsertPosition = -1;
|
|
window()->activateWindow();
|
|
}
|
|
|
|
bool InputField::canInsertFromMimeDataInner(const QMimeData *source) const {
|
|
if (source
|
|
&& _mimeDataHook
|
|
&& _mimeDataHook(source, MimeAction::Check)) {
|
|
return true;
|
|
}
|
|
return _inner->QTextEdit::canInsertFromMimeData(source);
|
|
}
|
|
|
|
void InputField::insertFromMimeDataInner(const QMimeData *source) {
|
|
if (source
|
|
&& _mimeDataHook
|
|
&& _mimeDataHook(source, MimeAction::Insert)) {
|
|
return;
|
|
}
|
|
auto mime = TextUtilities::TagsMimeType();
|
|
auto text = source->text();
|
|
if (source->hasFormat(mime)) {
|
|
auto tagsData = source->data(mime);
|
|
_insertedTags = TextUtilities::DeserializeTags(
|
|
tagsData,
|
|
text.size());
|
|
_insertedTagsAreFromMime = true;
|
|
} else {
|
|
_insertedTags.clear();
|
|
}
|
|
auto cursor = textCursor();
|
|
_realInsertPosition = cursor.selectionStart();
|
|
_realCharsAdded = text.size();
|
|
_inner->QTextEdit::insertFromMimeData(source);
|
|
if (!_inDrop) {
|
|
_insertedTags.clear();
|
|
_realInsertPosition = -1;
|
|
}
|
|
}
|
|
|
|
void InputField::resizeEvent(QResizeEvent *e) {
|
|
refreshPlaceholder();
|
|
_inner->setGeometry(rect().marginsRemoved(_st.textMargins));
|
|
_borderAnimationStart = width() / 2;
|
|
TWidget::resizeEvent(e);
|
|
checkContentHeight();
|
|
}
|
|
|
|
void InputField::refreshPlaceholder() {
|
|
auto placeholderText = _placeholderFactory ? _placeholderFactory() : QString();
|
|
auto availableWidth = width() - _st.textMargins.left() - _st.textMargins.right() - _st.placeholderMargins.left() - _st.placeholderMargins.right() - 1;
|
|
if (_st.placeholderScale > 0.) {
|
|
auto placeholderFont = _st.placeholderFont->f;
|
|
placeholderFont.setStyleStrategy(QFont::PreferMatch);
|
|
auto metrics = QFontMetrics(placeholderFont);
|
|
_placeholder = metrics.elidedText(placeholderText, Qt::ElideRight, availableWidth);
|
|
_placeholderPath = QPainterPath();
|
|
if (!_placeholder.isEmpty()) {
|
|
_placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder);
|
|
}
|
|
} else {
|
|
_placeholder = _st.placeholderFont->elided(placeholderText, availableWidth);
|
|
}
|
|
update();
|
|
}
|
|
|
|
void InputField::setPlaceholder(
|
|
Fn<QString()> placeholderFactory,
|
|
int afterSymbols) {
|
|
_placeholderFactory = std::move(placeholderFactory);
|
|
if (_placeholderAfterSymbols != afterSymbols) {
|
|
_placeholderAfterSymbols = afterSymbols;
|
|
startPlaceholderAnimation();
|
|
}
|
|
refreshPlaceholder();
|
|
}
|
|
|
|
void InputField::setEditLinkCallback(
|
|
Fn<bool(
|
|
EditLinkSelection selection,
|
|
QString text,
|
|
QString link,
|
|
EditLinkAction action)> callback) {
|
|
_editLinkCallback = std::move(callback);
|
|
}
|
|
|
|
void InputField::showError() {
|
|
setErrorShown(true);
|
|
if (!hasFocus()) {
|
|
_inner->setFocus();
|
|
}
|
|
}
|
|
|
|
void InputField::setErrorShown(bool error) {
|
|
if (_error != error) {
|
|
_error = error;
|
|
_a_error.start([this] { update(); }, _error ? 0. : 1., _error ? 1. : 0., _st.duration);
|
|
startBorderAnimation();
|
|
}
|
|
}
|
|
|
|
InputField::~InputField() = default;
|
|
|
|
MaskedInputField::MaskedInputField(
|
|
QWidget *parent,
|
|
const style::InputField &st,
|
|
Fn<QString()> placeholderFactory,
|
|
const QString &val)
|
|
: Parent(val, parent)
|
|
, _st(st)
|
|
, _oldtext(val)
|
|
, _placeholderFactory(std::move(placeholderFactory)) {
|
|
resize(_st.width, _st.heightMin);
|
|
|
|
setFont(_st.font);
|
|
setAlignment(_st.textAlign);
|
|
|
|
subscribe(Lang::Current().updated(), [this] { refreshPlaceholder(); });
|
|
refreshPlaceholder();
|
|
|
|
subscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) {
|
|
if (update.paletteChanged()) {
|
|
updatePalette();
|
|
}
|
|
});
|
|
updatePalette();
|
|
|
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
|
|
|
connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(onTextChange(const QString&)));
|
|
connect(this, SIGNAL(cursorPositionChanged(int,int)), this, SLOT(onCursorPositionChanged(int,int)));
|
|
|
|
connect(this, SIGNAL(textEdited(const QString&)), this, SLOT(onTextEdited()));
|
|
if (App::wnd()) connect(this, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu()));
|
|
|
|
setStyle(InputStyle<MaskedInputField>::instance());
|
|
QLineEdit::setTextMargins(0, 0, 0, 0);
|
|
setContentsMargins(0, 0, 0, 0);
|
|
|
|
setAttribute(Qt::WA_AcceptTouchEvents);
|
|
_touchTimer.setSingleShot(true);
|
|
connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
|
|
|
|
setTextMargins(_st.textMargins);
|
|
|
|
startPlaceholderAnimation();
|
|
startBorderAnimation();
|
|
finishAnimating();
|
|
}
|
|
|
|
void MaskedInputField::updatePalette() {
|
|
auto p = palette();
|
|
p.setColor(QPalette::Text, _st.textFg->c);
|
|
setPalette(p);
|
|
}
|
|
|
|
void MaskedInputField::setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos) {
|
|
if (newPos < 0 || newPos > newText.size()) {
|
|
newPos = newText.size();
|
|
}
|
|
auto updateText = (newText != now);
|
|
if (updateText) {
|
|
now = newText;
|
|
setText(now);
|
|
startPlaceholderAnimation();
|
|
}
|
|
auto updateCursorPosition = (newPos != nowCursor) || updateText;
|
|
if (updateCursorPosition) {
|
|
nowCursor = newPos;
|
|
setCursorPosition(nowCursor);
|
|
}
|
|
}
|
|
|
|
void MaskedInputField::customUpDown(bool custom) {
|
|
_customUpDown = custom;
|
|
}
|
|
|
|
int MaskedInputField::borderAnimationStart() const {
|
|
return _borderAnimationStart;
|
|
}
|
|
|
|
void MaskedInputField::setTextMargins(const QMargins &mrg) {
|
|
_textMargins = mrg;
|
|
refreshPlaceholder();
|
|
}
|
|
|
|
void MaskedInputField::onTouchTimer() {
|
|
_touchRightButton = true;
|
|
}
|
|
|
|
bool MaskedInputField::eventHook(QEvent *e) {
|
|
auto type = e->type();
|
|
if (type == QEvent::TouchBegin
|
|
|| type == QEvent::TouchUpdate
|
|
|| type == QEvent::TouchEnd
|
|
|| type == QEvent::TouchCancel) {
|
|
auto event = static_cast<QTouchEvent*>(e);
|
|
if (event->device()->type() == QTouchDevice::TouchScreen) {
|
|
touchEvent(event);
|
|
}
|
|
}
|
|
return Parent::eventHook(e);
|
|
}
|
|
|
|
void MaskedInputField::touchEvent(QTouchEvent *e) {
|
|
switch (e->type()) {
|
|
case QEvent::TouchBegin: {
|
|
if (_touchPress || e->touchPoints().isEmpty()) return;
|
|
_touchTimer.start(QApplication::startDragTime());
|
|
_touchPress = true;
|
|
_touchMove = _touchRightButton = false;
|
|
_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
|
|
} break;
|
|
|
|
case QEvent::TouchUpdate: {
|
|
if (!_touchPress || e->touchPoints().isEmpty()) return;
|
|
if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
|
|
_touchMove = true;
|
|
}
|
|
} break;
|
|
|
|
case QEvent::TouchEnd: {
|
|
if (!_touchPress) return;
|
|
auto weak = make_weak(this);
|
|
if (!_touchMove && window()) {
|
|
Qt::MouseButton btn(_touchRightButton ? Qt::RightButton : Qt::LeftButton);
|
|
QPoint mapped(mapFromGlobal(_touchStart)), winMapped(window()->mapFromGlobal(_touchStart));
|
|
|
|
if (_touchRightButton) {
|
|
QContextMenuEvent contextEvent(QContextMenuEvent::Mouse, mapped, _touchStart);
|
|
contextMenuEvent(&contextEvent);
|
|
}
|
|
}
|
|
if (weak) {
|
|
_touchTimer.stop();
|
|
_touchPress = _touchMove = _touchRightButton = false;
|
|
}
|
|
} break;
|
|
|
|
case QEvent::TouchCancel: {
|
|
_touchPress = false;
|
|
_touchTimer.stop();
|
|
} break;
|
|
}
|
|
}
|
|
|
|
QRect MaskedInputField::getTextRect() const {
|
|
return rect().marginsRemoved(_textMargins + QMargins(-2, -1, -2, -1));
|
|
}
|
|
|
|
void MaskedInputField::paintEvent(QPaintEvent *e) {
|
|
Painter p(this);
|
|
|
|
auto ms = getms();
|
|
auto r = rect().intersected(e->rect());
|
|
p.fillRect(r, _st.textBg);
|
|
if (_st.border) {
|
|
p.fillRect(0, height() - _st.border, width(), _st.border, _st.borderFg->b);
|
|
}
|
|
auto errorDegree = _a_error.current(ms, _error ? 1. : 0.);
|
|
auto focusedDegree = _a_focused.current(ms, _focused ? 1. : 0.);
|
|
auto borderShownDegree = _a_borderShown.current(ms, 1.);
|
|
auto borderOpacity = _a_borderOpacity.current(ms, _borderVisible ? 1. : 0.);
|
|
if (_st.borderActive && (borderOpacity > 0.)) {
|
|
auto borderStart = snap(_borderAnimationStart, 0, width());
|
|
auto borderFrom = qRound(borderStart * (1. - borderShownDegree));
|
|
auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree);
|
|
if (borderTo > borderFrom) {
|
|
auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree);
|
|
p.setOpacity(borderOpacity);
|
|
p.fillRect(borderFrom, height() - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg);
|
|
p.setOpacity(1);
|
|
}
|
|
}
|
|
|
|
p.setClipRect(r);
|
|
if (_st.placeholderScale > 0. && !_placeholderPath.isEmpty()) {
|
|
auto placeholderShiftDegree = _a_placeholderShifted.current(ms, _placeholderShifted ? 1. : 0.);
|
|
p.save();
|
|
p.setClipRect(r);
|
|
|
|
auto placeholderTop = anim::interpolate(0, _st.placeholderShift, placeholderShiftDegree);
|
|
|
|
QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins));
|
|
r.moveTop(r.top() + placeholderTop);
|
|
if (rtl()) r.moveLeft(width() - r.left() - r.width());
|
|
|
|
auto placeholderScale = 1. - (1. - _st.placeholderScale) * placeholderShiftDegree;
|
|
auto placeholderFg = anim::color(_st.placeholderFg, _st.placeholderFgActive, focusedDegree);
|
|
placeholderFg = anim::color(placeholderFg, _st.placeholderFgError, errorDegree);
|
|
|
|
PainterHighQualityEnabler hq(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(placeholderFg);
|
|
p.translate(r.topLeft());
|
|
p.scale(placeholderScale, placeholderScale);
|
|
p.drawPath(_placeholderPath);
|
|
|
|
p.restore();
|
|
} else if (!_placeholder.isEmpty()) {
|
|
auto placeholderHiddenDegree = _a_placeholderShifted.current(ms, _placeholderShifted ? 1. : 0.);
|
|
if (placeholderHiddenDegree < 1.) {
|
|
p.setOpacity(1. - placeholderHiddenDegree);
|
|
p.save();
|
|
p.setClipRect(r);
|
|
|
|
auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree);
|
|
|
|
QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins));
|
|
r.moveLeft(r.left() + placeholderLeft);
|
|
if (rtl()) r.moveLeft(width() - r.left() - r.width());
|
|
|
|
p.setFont(_st.placeholderFont);
|
|
p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree));
|
|
p.drawText(r, _placeholder, _st.placeholderAlign);
|
|
|
|
p.restore();
|
|
p.setOpacity(1.);
|
|
}
|
|
}
|
|
|
|
paintAdditionalPlaceholder(p, ms);
|
|
QLineEdit::paintEvent(e);
|
|
}
|
|
|
|
void MaskedInputField::startBorderAnimation() {
|
|
auto borderVisible = (_error || _focused);
|
|
if (_borderVisible != borderVisible) {
|
|
_borderVisible = borderVisible;
|
|
if (_borderVisible) {
|
|
if (_a_borderOpacity.animating()) {
|
|
_a_borderOpacity.start([this] { update(); }, 0., 1., _st.duration);
|
|
} else {
|
|
_a_borderShown.start([this] { update(); }, 0., 1., _st.duration);
|
|
}
|
|
} else if (qFuzzyCompare(_a_borderShown.current(1.), 0.)) {
|
|
_a_borderShown.finish();
|
|
_a_borderOpacity.finish();
|
|
} else {
|
|
_a_borderOpacity.start([this] { update(); }, 1., 0., _st.duration);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MaskedInputField::focusInEvent(QFocusEvent *e) {
|
|
_borderAnimationStart = (e->reason() == Qt::MouseFocusReason) ? mapFromGlobal(QCursor::pos()).x() : (width() / 2);
|
|
setFocused(true);
|
|
QLineEdit::focusInEvent(e);
|
|
emit focused();
|
|
}
|
|
|
|
void MaskedInputField::focusOutEvent(QFocusEvent *e) {
|
|
setFocused(false);
|
|
QLineEdit::focusOutEvent(e);
|
|
emit blurred();
|
|
}
|
|
|
|
void MaskedInputField::setFocused(bool focused) {
|
|
if (_focused != focused) {
|
|
_focused = focused;
|
|
_a_focused.start([this] { update(); }, _focused ? 0. : 1., _focused ? 1. : 0., _st.duration);
|
|
startPlaceholderAnimation();
|
|
startBorderAnimation();
|
|
}
|
|
}
|
|
|
|
void MaskedInputField::resizeEvent(QResizeEvent *e) {
|
|
refreshPlaceholder();
|
|
_borderAnimationStart = width() / 2;
|
|
QLineEdit::resizeEvent(e);
|
|
}
|
|
|
|
void MaskedInputField::refreshPlaceholder() {
|
|
auto placeholderText = _placeholderFactory ? _placeholderFactory() : QString();
|
|
auto availableWidth = width() - _textMargins.left() - _textMargins.right() - _st.placeholderMargins.left() - _st.placeholderMargins.right() - 1;
|
|
if (_st.placeholderScale > 0.) {
|
|
auto placeholderFont = _st.placeholderFont->f;
|
|
placeholderFont.setStyleStrategy(QFont::PreferMatch);
|
|
auto metrics = QFontMetrics(placeholderFont);
|
|
_placeholder = metrics.elidedText(placeholderText, Qt::ElideRight, availableWidth);
|
|
_placeholderPath = QPainterPath();
|
|
if (!_placeholder.isEmpty()) {
|
|
_placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder);
|
|
}
|
|
} else {
|
|
_placeholder = _st.placeholderFont->elided(placeholderText, availableWidth);
|
|
}
|
|
update();
|
|
}
|
|
|
|
void MaskedInputField::setPlaceholder(Fn<QString()> placeholderFactory) {
|
|
_placeholderFactory = std::move(placeholderFactory);
|
|
refreshPlaceholder();
|
|
}
|
|
|
|
void MaskedInputField::contextMenuEvent(QContextMenuEvent *e) {
|
|
if (auto menu = createStandardContextMenu()) {
|
|
(new Ui::PopupMenu(this, menu))->popup(e->globalPos());
|
|
}
|
|
}
|
|
|
|
void MaskedInputField::inputMethodEvent(QInputMethodEvent *e) {
|
|
QLineEdit::inputMethodEvent(e);
|
|
_lastPreEditText = e->preeditString();
|
|
update();
|
|
}
|
|
|
|
void MaskedInputField::showError() {
|
|
setErrorShown(true);
|
|
if (!hasFocus()) {
|
|
setFocus();
|
|
}
|
|
}
|
|
|
|
void MaskedInputField::setErrorShown(bool error) {
|
|
if (_error != error) {
|
|
_error = error;
|
|
_a_error.start([this] { update(); }, _error ? 0. : 1., _error ? 1. : 0., _st.duration);
|
|
startBorderAnimation();
|
|
}
|
|
}
|
|
|
|
QSize MaskedInputField::sizeHint() const {
|
|
return geometry().size();
|
|
}
|
|
|
|
QSize MaskedInputField::minimumSizeHint() const {
|
|
return geometry().size();
|
|
}
|
|
|
|
void MaskedInputField::setDisplayFocused(bool focused) {
|
|
setFocused(focused);
|
|
finishAnimating();
|
|
}
|
|
|
|
void MaskedInputField::finishAnimating() {
|
|
_a_focused.finish();
|
|
_a_error.finish();
|
|
_a_placeholderShifted.finish();
|
|
_a_borderShown.finish();
|
|
_a_borderOpacity.finish();
|
|
update();
|
|
}
|
|
|
|
void MaskedInputField::setPlaceholderHidden(bool forcePlaceholderHidden) {
|
|
_forcePlaceholderHidden = forcePlaceholderHidden;
|
|
startPlaceholderAnimation();
|
|
}
|
|
|
|
void MaskedInputField::startPlaceholderAnimation() {
|
|
auto placeholderShifted = _forcePlaceholderHidden || (_focused && _st.placeholderScale > 0.) || !getLastText().isEmpty();
|
|
if (_placeholderShifted != placeholderShifted) {
|
|
_placeholderShifted = placeholderShifted;
|
|
_a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration);
|
|
}
|
|
}
|
|
|
|
QRect MaskedInputField::placeholderRect() const {
|
|
return rect().marginsRemoved(_textMargins + _st.placeholderMargins);
|
|
}
|
|
|
|
void MaskedInputField::placeholderAdditionalPrepare(Painter &p, TimeMs ms) {
|
|
p.setFont(_st.font);
|
|
p.setPen(_st.placeholderFg);
|
|
}
|
|
|
|
void MaskedInputField::keyPressEvent(QKeyEvent *e) {
|
|
QString wasText(_oldtext);
|
|
int32 wasCursor(_oldcursor);
|
|
|
|
if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) {
|
|
e->ignore();
|
|
} else {
|
|
QLineEdit::keyPressEvent(e);
|
|
}
|
|
|
|
auto newText = text();
|
|
auto newCursor = cursorPosition();
|
|
if (wasText == newText && wasCursor == newCursor) { // call correct manually
|
|
correctValue(wasText, wasCursor, newText, newCursor);
|
|
_oldtext = newText;
|
|
_oldcursor = newCursor;
|
|
if (wasText != _oldtext) emit changed();
|
|
startPlaceholderAnimation();
|
|
}
|
|
if (e->key() == Qt::Key_Escape) {
|
|
e->ignore();
|
|
emit cancelled();
|
|
} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
|
|
emit submitted(e->modifiers());
|
|
#ifdef Q_OS_MAC
|
|
} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
|
|
auto selected = selectedText();
|
|
if (!selected.isEmpty() && echoMode() == QLineEdit::Normal) {
|
|
QApplication::clipboard()->setText(selected, QClipboard::FindBuffer);
|
|
}
|
|
#endif // Q_OS_MAC
|
|
}
|
|
}
|
|
|
|
void MaskedInputField::onTextEdited() {
|
|
QString wasText(_oldtext), newText(text());
|
|
int32 wasCursor(_oldcursor), newCursor(cursorPosition());
|
|
|
|
correctValue(wasText, wasCursor, newText, newCursor);
|
|
_oldtext = newText;
|
|
_oldcursor = newCursor;
|
|
if (wasText != _oldtext) emit changed();
|
|
startPlaceholderAnimation();
|
|
|
|
if (App::wnd()) App::wnd()->updateGlobalMenu();
|
|
}
|
|
|
|
void MaskedInputField::onTextChange(const QString &text) {
|
|
_oldtext = QLineEdit::text();
|
|
setErrorShown(false);
|
|
if (App::wnd()) App::wnd()->updateGlobalMenu();
|
|
}
|
|
|
|
void MaskedInputField::onCursorPositionChanged(int oldPosition, int position) {
|
|
_oldcursor = position;
|
|
}
|
|
|
|
CountryCodeInput::CountryCodeInput(QWidget *parent, const style::InputField &st) : MaskedInputField(parent, st)
|
|
, _nosignal(false) {
|
|
}
|
|
|
|
void CountryCodeInput::startErasing(QKeyEvent *e) {
|
|
setFocus();
|
|
keyPressEvent(e);
|
|
}
|
|
|
|
void CountryCodeInput::codeSelected(const QString &code) {
|
|
auto wasText = getLastText();
|
|
auto wasCursor = cursorPosition();
|
|
auto newText = '+' + code;
|
|
auto newCursor = newText.size();
|
|
setText(newText);
|
|
_nosignal = true;
|
|
correctValue(wasText, wasCursor, newText, newCursor);
|
|
_nosignal = false;
|
|
emit changed();
|
|
}
|
|
|
|
void CountryCodeInput::correctValue(
|
|
const QString &was,
|
|
int wasCursor,
|
|
QString &now,
|
|
int &nowCursor) {
|
|
QString newText, addToNumber;
|
|
int oldPos(nowCursor), newPos(-1), oldLen(now.length()), start = 0, digits = 5;
|
|
newText.reserve(oldLen + 1);
|
|
if (oldLen && now[0] == '+') {
|
|
if (start == oldPos) {
|
|
newPos = newText.length();
|
|
}
|
|
++start;
|
|
}
|
|
newText += '+';
|
|
for (int i = start; i < oldLen; ++i) {
|
|
if (i == oldPos) {
|
|
newPos = newText.length();
|
|
}
|
|
auto ch = now[i];
|
|
if (ch.isDigit()) {
|
|
if (!digits || !--digits) {
|
|
addToNumber += ch;
|
|
} else {
|
|
newText += ch;
|
|
}
|
|
}
|
|
}
|
|
if (!addToNumber.isEmpty()) {
|
|
auto validCode = findValidCode(newText.mid(1));
|
|
addToNumber = newText.mid(1 + validCode.length()) + addToNumber;
|
|
newText = '+' + validCode;
|
|
}
|
|
setCorrectedText(now, nowCursor, newText, newPos);
|
|
|
|
if (!_nosignal && was != newText) {
|
|
emit codeChanged(newText.mid(1));
|
|
}
|
|
if (!addToNumber.isEmpty()) {
|
|
emit addedToNumber(addToNumber);
|
|
}
|
|
}
|
|
|
|
PhonePartInput::PhonePartInput(QWidget *parent, const style::InputField &st) : MaskedInputField(parent, st/*, lang(lng_phone_ph)*/) {
|
|
}
|
|
|
|
void PhonePartInput::paintAdditionalPlaceholder(Painter &p, TimeMs ms) {
|
|
if (!_pattern.isEmpty()) {
|
|
auto t = getDisplayedText();
|
|
auto ph = _additionalPlaceholder.mid(t.size());
|
|
if (!ph.isEmpty()) {
|
|
p.setClipRect(rect());
|
|
auto phRect = placeholderRect();
|
|
int tw = phFont()->width(t);
|
|
if (tw < phRect.width()) {
|
|
phRect.setLeft(phRect.left() + tw);
|
|
placeholderAdditionalPrepare(p, ms);
|
|
p.drawText(phRect, ph, style::al_topleft);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PhonePartInput::keyPressEvent(QKeyEvent *e) {
|
|
if (e->key() == Qt::Key_Backspace && getLastText().isEmpty()) {
|
|
emit voidBackspace(e);
|
|
} else {
|
|
MaskedInputField::keyPressEvent(e);
|
|
}
|
|
}
|
|
|
|
void PhonePartInput::correctValue(
|
|
const QString &was,
|
|
int wasCursor,
|
|
QString &now,
|
|
int &nowCursor) {
|
|
QString newText;
|
|
int oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = 0;
|
|
for (int i = 0; i < oldLen; ++i) {
|
|
if (now[i].isDigit()) {
|
|
++digitCount;
|
|
}
|
|
}
|
|
if (digitCount > MaxPhoneTailLength) digitCount = MaxPhoneTailLength;
|
|
|
|
bool inPart = !_pattern.isEmpty();
|
|
int curPart = -1, leftInPart = 0;
|
|
newText.reserve(oldLen);
|
|
for (int i = 0; i < oldLen; ++i) {
|
|
if (i == oldPos && newPos < 0) {
|
|
newPos = newText.length();
|
|
}
|
|
|
|
auto ch = now[i];
|
|
if (ch.isDigit()) {
|
|
if (!digitCount--) {
|
|
break;
|
|
}
|
|
if (inPart) {
|
|
if (leftInPart) {
|
|
--leftInPart;
|
|
} else {
|
|
newText += ' ';
|
|
++curPart;
|
|
inPart = curPart < _pattern.size();
|
|
leftInPart = inPart ? (_pattern.at(curPart) - 1) : 0;
|
|
|
|
++oldPos;
|
|
}
|
|
}
|
|
newText += ch;
|
|
} else if (ch == ' ' || ch == '-' || ch == '(' || ch == ')') {
|
|
if (inPart) {
|
|
if (leftInPart) {
|
|
} else {
|
|
newText += ch;
|
|
++curPart;
|
|
inPart = curPart < _pattern.size();
|
|
leftInPart = inPart ? _pattern.at(curPart) : 0;
|
|
}
|
|
} else {
|
|
newText += ch;
|
|
}
|
|
}
|
|
}
|
|
auto newlen = newText.size();
|
|
while (newlen > 0 && newText.at(newlen - 1).isSpace()) {
|
|
--newlen;
|
|
}
|
|
if (newlen < newText.size()) {
|
|
newText = newText.mid(0, newlen);
|
|
}
|
|
setCorrectedText(now, nowCursor, newText, newPos);
|
|
}
|
|
|
|
void PhonePartInput::addedToNumber(const QString &added) {
|
|
setFocus();
|
|
auto wasText = getLastText();
|
|
auto wasCursor = cursorPosition();
|
|
auto newText = added + wasText;
|
|
auto newCursor = newText.size();
|
|
setText(newText);
|
|
setCursorPosition(added.length());
|
|
correctValue(wasText, wasCursor, newText, newCursor);
|
|
startPlaceholderAnimation();
|
|
}
|
|
|
|
void PhonePartInput::onChooseCode(const QString &code) {
|
|
_pattern = phoneNumberParse(code);
|
|
if (!_pattern.isEmpty() && _pattern.at(0) == code.size()) {
|
|
_pattern.pop_front();
|
|
} else {
|
|
_pattern.clear();
|
|
}
|
|
_additionalPlaceholder = QString();
|
|
if (!_pattern.isEmpty()) {
|
|
_additionalPlaceholder.reserve(20);
|
|
for (int i = 0, l = _pattern.size(); i < l; ++i) {
|
|
_additionalPlaceholder.append(' ');
|
|
_additionalPlaceholder.append(QString(_pattern.at(i), QChar(0x2212)));
|
|
}
|
|
}
|
|
setPlaceholderHidden(!_additionalPlaceholder.isEmpty());
|
|
|
|
auto wasText = getLastText();
|
|
auto wasCursor = cursorPosition();
|
|
auto newText = getLastText();
|
|
auto newCursor = newText.size();
|
|
correctValue(wasText, wasCursor, newText, newCursor);
|
|
|
|
startPlaceholderAnimation();
|
|
}
|
|
|
|
PasswordInput::PasswordInput(QWidget *parent, const style::InputField &st, Fn<QString()> placeholderFactory, const QString &val) : MaskedInputField(parent, st, std::move(placeholderFactory), val) {
|
|
setEchoMode(QLineEdit::Password);
|
|
}
|
|
|
|
PortInput::PortInput(QWidget *parent, const style::InputField &st, Fn<QString()> placeholderFactory, const QString &val) : MaskedInputField(parent, st, std::move(placeholderFactory), val) {
|
|
if (!val.toInt() || val.toInt() > 65535) {
|
|
setText(QString());
|
|
}
|
|
}
|
|
|
|
void PortInput::correctValue(
|
|
const QString &was,
|
|
int wasCursor,
|
|
QString &now,
|
|
int &nowCursor) {
|
|
QString newText;
|
|
newText.reserve(now.size());
|
|
auto newPos = nowCursor;
|
|
for (auto i = 0, l = now.size(); i < l; ++i) {
|
|
if (now.at(i).isDigit()) {
|
|
newText.append(now.at(i));
|
|
} else if (i < nowCursor) {
|
|
--newPos;
|
|
}
|
|
}
|
|
if (!newText.toInt()) {
|
|
newText = QString();
|
|
newPos = 0;
|
|
} else if (newText.toInt() > 65535) {
|
|
newText = was;
|
|
newPos = wasCursor;
|
|
}
|
|
setCorrectedText(now, nowCursor, newText, newPos);
|
|
}
|
|
|
|
HexInput::HexInput(QWidget *parent, const style::InputField &st, Fn<QString()> placeholderFactory, const QString &val) : MaskedInputField(parent, st, std::move(placeholderFactory), val) {
|
|
if (!QRegularExpression("^[a-fA-F0-9]+$").match(val).hasMatch()) {
|
|
setText(QString());
|
|
}
|
|
}
|
|
|
|
void HexInput::correctValue(
|
|
const QString &was,
|
|
int wasCursor,
|
|
QString &now,
|
|
int &nowCursor) {
|
|
QString newText;
|
|
newText.reserve(now.size());
|
|
auto newPos = nowCursor;
|
|
for (auto i = 0, l = now.size(); i < l; ++i) {
|
|
const auto ch = now[i];
|
|
if ((ch >= '0' && ch <= '9')
|
|
|| (ch >= 'a' && ch <= 'f')
|
|
|| (ch >= 'A' && ch <= 'F')) {
|
|
newText.append(ch);
|
|
} else if (i < nowCursor) {
|
|
--newPos;
|
|
}
|
|
}
|
|
setCorrectedText(now, nowCursor, newText, newPos);
|
|
}
|
|
|
|
UsernameInput::UsernameInput(QWidget *parent, const style::InputField &st, Fn<QString()> placeholderFactory, const QString &val, bool isLink) : MaskedInputField(parent, st, std::move(placeholderFactory), val) {
|
|
setLinkPlaceholder(isLink ? Messenger::Instance().createInternalLink(QString()) : QString());
|
|
}
|
|
|
|
void UsernameInput::setLinkPlaceholder(const QString &placeholder) {
|
|
_linkPlaceholder = placeholder;
|
|
if (!_linkPlaceholder.isEmpty()) {
|
|
setTextMargins(style::margins(_st.textMargins.left() + _st.font->width(_linkPlaceholder), _st.textMargins.top(), _st.textMargins.right(), _st.textMargins.bottom()));
|
|
setPlaceholderHidden(true);
|
|
}
|
|
}
|
|
|
|
void UsernameInput::paintAdditionalPlaceholder(Painter &p, TimeMs ms) {
|
|
if (!_linkPlaceholder.isEmpty()) {
|
|
p.setFont(_st.font);
|
|
p.setPen(_st.placeholderFg);
|
|
p.drawText(QRect(_st.textMargins.left(), _st.textMargins.top(), width(), height() - _st.textMargins.top() - _st.textMargins.bottom()), _linkPlaceholder, style::al_topleft);
|
|
}
|
|
}
|
|
|
|
void UsernameInput::correctValue(
|
|
const QString &was,
|
|
int wasCursor,
|
|
QString &now,
|
|
int &nowCursor) {
|
|
auto newPos = nowCursor;
|
|
auto from = 0, len = now.size();
|
|
for (; from < len; ++from) {
|
|
if (!now.at(from).isSpace()) {
|
|
break;
|
|
}
|
|
if (newPos > 0) --newPos;
|
|
}
|
|
len -= from;
|
|
if (len > kMaxUsernameLength) {
|
|
len = kMaxUsernameLength + (now.at(from) == '@' ? 1 : 0);
|
|
}
|
|
for (int32 to = from + len; to > from;) {
|
|
--to;
|
|
if (!now.at(to).isSpace()) {
|
|
break;
|
|
}
|
|
--len;
|
|
}
|
|
setCorrectedText(now, nowCursor, now.mid(from, len), newPos);
|
|
}
|
|
|
|
PhoneInput::PhoneInput(QWidget *parent, const style::InputField &st, Fn<QString()> placeholderFactory, const QString &val) : MaskedInputField(parent, st, std::move(placeholderFactory), val) {
|
|
QString phone(val);
|
|
if (phone.isEmpty()) {
|
|
clearText();
|
|
} else {
|
|
int32 pos = phone.size();
|
|
correctValue(QString(), 0, phone, pos);
|
|
}
|
|
}
|
|
|
|
void PhoneInput::focusInEvent(QFocusEvent *e) {
|
|
MaskedInputField::focusInEvent(e);
|
|
setSelection(cursorPosition(), cursorPosition());
|
|
}
|
|
|
|
void PhoneInput::clearText() {
|
|
QString phone;
|
|
if (AuthSession::Exists()) {
|
|
const auto self = Auth().user();
|
|
QVector<int> newPattern = phoneNumberParse(self->phone());
|
|
if (!newPattern.isEmpty()) {
|
|
phone = self->phone().mid(0, newPattern.at(0));
|
|
}
|
|
}
|
|
setText(phone);
|
|
int32 pos = phone.size();
|
|
correctValue(QString(), 0, phone, pos);
|
|
}
|
|
|
|
void PhoneInput::paintAdditionalPlaceholder(Painter &p, TimeMs ms) {
|
|
if (!_pattern.isEmpty()) {
|
|
auto t = getDisplayedText();
|
|
auto ph = _additionalPlaceholder.mid(t.size());
|
|
if (!ph.isEmpty()) {
|
|
p.setClipRect(rect());
|
|
auto phRect = placeholderRect();
|
|
int tw = phFont()->width(t);
|
|
if (tw < phRect.width()) {
|
|
phRect.setLeft(phRect.left() + tw);
|
|
placeholderAdditionalPrepare(p, ms);
|
|
p.drawText(phRect, ph, style::al_topleft);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PhoneInput::correctValue(
|
|
const QString &was,
|
|
int wasCursor,
|
|
QString &now,
|
|
int &nowCursor) {
|
|
auto digits = now;
|
|
digits.replace(QRegularExpression(qsl("[^\\d]")), QString());
|
|
_pattern = phoneNumberParse(digits);
|
|
|
|
QString newPlaceholder;
|
|
if (_pattern.isEmpty()) {
|
|
newPlaceholder = QString();
|
|
} else if (_pattern.size() == 1 && _pattern.at(0) == digits.size()) {
|
|
newPlaceholder = QString(_pattern.at(0) + 2, ' ') + lang(lng_contact_phone);
|
|
} else {
|
|
newPlaceholder.reserve(20);
|
|
for (int i = 0, l = _pattern.size(); i < l; ++i) {
|
|
if (i) {
|
|
newPlaceholder.append(' ');
|
|
} else {
|
|
newPlaceholder.append('+');
|
|
}
|
|
newPlaceholder.append(i ? QString(_pattern.at(i), QChar(0x2212)) : digits.mid(0, _pattern.at(i)));
|
|
}
|
|
}
|
|
if (_additionalPlaceholder != newPlaceholder) {
|
|
_additionalPlaceholder = newPlaceholder;
|
|
setPlaceholderHidden(!_additionalPlaceholder.isEmpty());
|
|
update();
|
|
}
|
|
|
|
QString newText;
|
|
int oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = qMin(digits.size(), MaxPhoneCodeLength + MaxPhoneTailLength);
|
|
|
|
bool inPart = !_pattern.isEmpty(), plusFound = false;
|
|
int curPart = 0, leftInPart = inPart ? _pattern.at(curPart) : 0;
|
|
newText.reserve(oldLen + 1);
|
|
newText.append('+');
|
|
for (int i = 0; i < oldLen; ++i) {
|
|
if (i == oldPos && newPos < 0) {
|
|
newPos = newText.length();
|
|
}
|
|
|
|
QChar ch(now[i]);
|
|
if (ch.isDigit()) {
|
|
if (!digitCount--) {
|
|
break;
|
|
}
|
|
if (inPart) {
|
|
if (leftInPart) {
|
|
--leftInPart;
|
|
} else {
|
|
newText += ' ';
|
|
++curPart;
|
|
inPart = curPart < _pattern.size();
|
|
leftInPart = inPart ? (_pattern.at(curPart) - 1) : 0;
|
|
|
|
++oldPos;
|
|
}
|
|
}
|
|
newText += ch;
|
|
} else if (ch == ' ' || ch == '-' || ch == '(' || ch == ')') {
|
|
if (inPart) {
|
|
if (leftInPart) {
|
|
} else {
|
|
newText += ch;
|
|
++curPart;
|
|
inPart = curPart < _pattern.size();
|
|
leftInPart = inPart ? _pattern.at(curPart) : 0;
|
|
}
|
|
} else {
|
|
newText += ch;
|
|
}
|
|
} else if (ch == '+') {
|
|
plusFound = true;
|
|
}
|
|
}
|
|
if (!plusFound && newText == qstr("+")) {
|
|
newText = QString();
|
|
newPos = 0;
|
|
}
|
|
int32 newlen = newText.size();
|
|
while (newlen > 0 && newText.at(newlen - 1).isSpace()) {
|
|
--newlen;
|
|
}
|
|
if (newlen < newText.size()) {
|
|
newText = newText.mid(0, newlen);
|
|
}
|
|
setCorrectedText(now, nowCursor, newText, newPos);
|
|
}
|
|
|
|
} // namespace Ui
|