3564 lines
		
	
	
	
		
			100 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			3564 lines
		
	
	
	
		
			100 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 "emoji_suggestions_data.h"
 | |
| #include "chat_helpers/emoji_suggestions_helper.h"
 | |
| #include "window/themes/window_theme.h"
 | |
| #include "lang/lang_keys.h"
 | |
| #include "mainwindow.h"
 | |
| #include "numbers.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);
 | |
| 
 | |
| bool IsNewline(QChar ch) {
 | |
| 	return (kNewlineChars.indexOf(ch) >= 0);
 | |
| }
 | |
| 
 | |
| 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 randomPartPosition = _currentTagId.lastIndexOf('/');
 | |
| 			const auto tagId = _currentTagId.midRef(
 | |
| 				0,
 | |
| 				(randomPartPosition > 0
 | |
| 					? randomPartPosition
 | |
| 					: _currentTagId.size()));
 | |
| 
 | |
| 			bool tagChanged = true;
 | |
| 			if (_currentTag < _tags.size()) {
 | |
| 				auto &alreadyTag = _tags[_currentTag];
 | |
| 				if (alreadyTag.offset == _currentStart &&
 | |
| 					alreadyTag.length == currentPosition - _currentStart &&
 | |
| 					alreadyTag.id == tagId) {
 | |
| 					tagChanged = false;
 | |
| 				}
 | |
| 			}
 | |
| 			if (tagChanged) {
 | |
| 				_changed = true;
 | |
| 				const auto tag = TextWithTags::Tag {
 | |
| 					_currentStart,
 | |
| 					currentPosition - _currentStart,
 | |
| 					tagId.toString()
 | |
| 				};
 | |
| 				if (_currentTag < _tags.size()) {
 | |
| 					_tags[_currentTag] = tag;
 | |
| 				} else {
 | |
| 					_tags.push_back(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;
 | |
| };
 | |
| 
 | |
| struct TagStartItem {
 | |
| 	int offset = 0;
 | |
| 	int position = -1;
 | |
| };
 | |
| 
 | |
| 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;
 | |
| 
 | |
| 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, std::vector<int>> &TagFinishIndices() {
 | |
| 	static auto cached = std::map<QString, std::vector<int>> {
 | |
| 		{ kTagBold, { kTagBoldIndex, kTagCodeIndex, kTagPreIndex } },
 | |
| 		{ kTagItalic, { kTagItalicIndex, kTagCodeIndex, kTagPreIndex } },
 | |
| 		{ kTagCode, { kTagCodeIndex, kTagPreIndex } },
 | |
| 		{ kTagPre, { kTagCodeIndex, kTagPreIndex } },
 | |
| 	};
 | |
| 	return cached;
 | |
| }
 | |
| 
 | |
| bool DoesTagFinishByNewline(const QString &tag) {
 | |
| 	return (tag == kTagCode);
 | |
| }
 | |
| 
 | |
| class PossibleTagAccumulator {
 | |
| public:
 | |
| 	PossibleTagAccumulator(std::vector<InputField::PossibleTag> *tags)
 | |
| 	: _tags(tags)
 | |
| 	, _expressions(TagStartExpressions())
 | |
| 	, _finishIndices(TagFinishIndices())
 | |
| 	, _items(_expressions.size()) {
 | |
| 	}
 | |
| 
 | |
| 	void feed(const QString &text, const QString &textTag) {
 | |
| 		if (!_tags) {
 | |
| 			return;
 | |
| 		}
 | |
| 		const auto guard = gsl::finally([&] {
 | |
| 			_currentLength += text.size();
 | |
| 		});
 | |
| 		if (!textTag.isEmpty()) {
 | |
| 			finishTags();
 | |
| 			return;
 | |
| 		}
 | |
| 		for (auto &item : _items) {
 | |
| 			item = TagStartItem();
 | |
| 		}
 | |
| 		auto tagIndex = _currentTag;
 | |
| 		while (true) {
 | |
| 			for (; tagIndex != _currentFreeTag; ++tagIndex) {
 | |
| 				auto &tag = (*_tags)[tagIndex];
 | |
| 				bumpOffsetByTag(tag, tag.start + 1);
 | |
| 
 | |
| 				const auto finishIt = _finishIndices.find(tag.tag);
 | |
| 				Assert(finishIt != end(_finishIndices));
 | |
| 				const auto &finishingIndices = finishIt->second;
 | |
| 				for (const auto index : finishingIndices) {
 | |
| 					fillItem(index, text);
 | |
| 				}
 | |
| 				if (finishByNewline(tagIndex, text, finishingIndices)) {
 | |
| 					continue;
 | |
| 				}
 | |
| 				const auto min = minIndex(finishingIndices);
 | |
| 				if (min >= 0) {
 | |
| 					const auto minPosition = matchPosition(min);
 | |
| 					finishTag(tagIndex, _currentLength + minPosition);
 | |
| 				} else if (tag.tag == kTagPre || tag.tag == kTagCode) {
 | |
| 					// We can't finish a mono tag, so we ignore all others.
 | |
| 					return;
 | |
| 				}
 | |
| 			}
 | |
| 			for (auto i = 0, count = int(_items.size()); i != count; ++i) {
 | |
| 				fillItem(i, text);
 | |
| 			}
 | |
| 			const auto min = minIndex();
 | |
| 			if (min < 0) {
 | |
| 				return;
 | |
| 			}
 | |
| 			startTag(
 | |
| 				_currentLength + matchPosition(min),
 | |
| 				_expressions[min].tag);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	void finish() {
 | |
| 		if (!_tags) {
 | |
| 			return;
 | |
| 		}
 | |
| 		finishTags();
 | |
| 		if (_currentTag < _tags->size()) {
 | |
| 			_tags->resize(_currentTag);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| private:
 | |
| 	void finishTag(int index, int end) {
 | |
| 		Expects(_tags != nullptr);
 | |
| 		Expects(index >= 0 && index < _tags->size());
 | |
| 
 | |
| 		auto &tag = (*_tags)[index];
 | |
| 		if (tag.length < 0) {
 | |
| 			tag.length = end - tag.start;
 | |
| 		}
 | |
| 		if (index == _currentTag) {
 | |
| 			++_currentTag;
 | |
| 		}
 | |
| 	}
 | |
| 	bool finishByNewline(
 | |
| 			int index,
 | |
| 			const QString &text,
 | |
| 			const std::vector<int> &finishingIndices) {
 | |
| 		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.start + 1 - _currentLength));
 | |
| 		for (const auto finishingIndex : finishingIndices) {
 | |
| 			if (matchPosition(finishingIndex) <= endPosition) {
 | |
| 				return false;
 | |
| 			}
 | |
| 		}
 | |
| 		finishTag(index, _currentLength + endPosition);
 | |
| 		return true;
 | |
| 	}
 | |
| 	void bumpOffsetByTag(const InputField::PossibleTag &tag, int end) {
 | |
| 		const auto offset = end - _currentLength;
 | |
| 		if (tag.tag == kTagPre || tag.tag == kTagCode) {
 | |
| 			for (auto &item : _items) {
 | |
| 				applyOffset(item, offset);
 | |
| 			}
 | |
| 		} else if (tag.tag == kTagBold) {
 | |
| 			applyOffset(_items[kTagBoldIndex], offset);
 | |
| 		} else if (tag.tag == kTagItalic) {
 | |
| 			applyOffset(_items[kTagItalicIndex], offset);
 | |
| 		} else {
 | |
| 			Unexpected("Unsupported tag.");
 | |
| 		}
 | |
| 	}
 | |
| 	void applyOffset(TagStartItem &item, int offset) {
 | |
| 		if (matchPosition(item) < offset) {
 | |
| 			item.position = -1;
 | |
| 		}
 | |
| 		accumulate_max(item.offset, offset);
 | |
| 	}
 | |
| 	void finishTags() {
 | |
| 		while (_currentTag != _currentFreeTag) {
 | |
| 			finishTag(_currentTag, _currentLength);
 | |
| 		}
 | |
| 	}
 | |
| 	void startTag(int offset, const QString &tag) {
 | |
| 		Expects(_tags != nullptr);
 | |
| 
 | |
| 		if (_currentFreeTag < _tags->size()) {
 | |
| 			(*_tags)[_currentFreeTag] = { offset, -1, tag };
 | |
| 		} else {
 | |
| 			_tags->push_back({ offset, -1, tag });
 | |
| 		}
 | |
| 		++_currentFreeTag;
 | |
| 	}
 | |
| 	void fillItem(int index, const QString &text) {
 | |
| 		Expects(index >= 0 && index < _items.size());
 | |
| 
 | |
| 		auto &item = _items[index];
 | |
| 		if (item.position >= 0) {
 | |
| 			return;
 | |
| 		}
 | |
| 		const auto length = text.size();
 | |
| 		const auto &expression = _expressions[index];
 | |
| 		const auto &tag = expression.tag;
 | |
| 		const auto &goodBefore = expression.goodBefore;
 | |
| 		const auto &badAfter = expression.badAfter;
 | |
| 		const auto tagLength = tag.size();
 | |
| 		while (true) {
 | |
| 			item.position = text.indexOf(tag, item.offset);
 | |
| 			if (item.position < 0) {
 | |
| 				item.offset = item.position = kInvalidPosition;
 | |
| 				break;
 | |
| 			}
 | |
| 			item.offset = item.position + tagLength;
 | |
| 			if (item.position > 0) {
 | |
| 				const auto before = text[item.position - 1];
 | |
| 				if (expression.goodBefore.indexOf(before) < 0) {
 | |
| 					continue;
 | |
| 				}
 | |
| 			}
 | |
| 			if (item.position + tagLength < length) {
 | |
| 				const auto after = text[item.position + tagLength];
 | |
| 				if (expression.badAfter.indexOf(after) >= 0) {
 | |
| 					continue;
 | |
| 				}
 | |
| 			}
 | |
| 			break;
 | |
| 		}
 | |
| 		item.offset = item.position + tagLength;
 | |
| 	}
 | |
| 	int matchPosition(int index) const {
 | |
| 		Expects(index >= 0 && index < _items.size());
 | |
| 
 | |
| 		return matchPosition(_items[index]);
 | |
| 	}
 | |
| 	int matchPosition(const TagStartItem &item) const {
 | |
| 		const auto position = item.position;
 | |
| 		return (item.position >= 0) ? item.position : kInvalidPosition;
 | |
| 	}
 | |
| 	int newlinePosition(const QString &text, int offset) const {
 | |
| 		const auto length = text.size();
 | |
| 		if (offset < length) {
 | |
| 			auto ch = text.data() + offset;
 | |
| 			for (const auto e = ch + length; ch != e; ++ch) {
 | |
| 				if (IsNewline(*ch)) {
 | |
| 					return (ch - text.data());
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		return kInvalidPosition;
 | |
| 	}
 | |
| 	int minIndex() const {
 | |
| 		auto result = -1;
 | |
| 		auto minPosition = kInvalidPosition;
 | |
| 		for (auto i = 0, count = int(_items.size()); i != count; ++i) {
 | |
| 			const auto position = matchPosition(i);
 | |
| 			if (position < minPosition) {
 | |
| 				minPosition = position;
 | |
| 				result = i;
 | |
| 			}
 | |
| 		}
 | |
| 		return result;
 | |
| 	}
 | |
| 	int minIndex(const std::vector<int> &indices) const {
 | |
| 		auto result = -1;
 | |
| 		auto minPosition = kInvalidPosition;
 | |
| 		for (auto i : indices) {
 | |
| 			const auto position = matchPosition(i);
 | |
| 			if (position < minPosition) {
 | |
| 				minPosition = position;
 | |
| 				result = i;
 | |
| 			}
 | |
| 		}
 | |
| 		return result;
 | |
| 	}
 | |
| 
 | |
| 	std::vector<InputField::PossibleTag> *_tags = nullptr;
 | |
| 	const std::vector<TagStartExpression> &_expressions;
 | |
| 	const std::map<QString, std::vector<int>> &_finishIndices;
 | |
| 	std::vector<TagStartItem> _items;
 | |
| 
 | |
| 	int _currentTag = 0;
 | |
| 	int _currentFreeTag = 0;
 | |
| 	int _currentLength = 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::Size() + 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;
 | |
| }
 | |
| 
 | |
| QTextCharFormat PrepareTagFormat(
 | |
| 		const style::InputField &st,
 | |
| 		QString tag) {
 | |
| 	auto result = QTextCharFormat();
 | |
| 	if (tag.indexOf(':') >= 0) {
 | |
| 		tag += '/' + QString::number(rand_value<uint32>());
 | |
| 		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) {
 | |
| 	}
 | |
| 
 | |
| 	QVariant loadResource(int type, const QUrl &name) override {
 | |
| 		return outer()->loadResource(type, name);
 | |
| 	}
 | |
| 
 | |
| 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);
 | |
| 	}
 | |
| 
 | |
| 	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, base::lambda<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(base::lambda<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(nullptr, 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);
 | |
| 
 | |
| 	bool shift = e->modifiers().testFlag(Qt::ShiftModifier), alt = e->modifiers().testFlag(Qt::AltModifier);
 | |
| 	bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier), ctrlGood = true;
 | |
| 	if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) {
 | |
| 		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(ctrl && shift);
 | |
| #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,
 | |
| 	base::lambda<QString()> placeholderFactory,
 | |
| 	const QString &value)
 | |
| : InputField(
 | |
| 	parent,
 | |
| 	st,
 | |
| 	Mode::SingleLine,
 | |
| 	std::move(placeholderFactory),
 | |
| 	{ value, {} }) {
 | |
| }
 | |
| 
 | |
| InputField::InputField(
 | |
| 	QWidget *parent,
 | |
| 	const style::InputField &st,
 | |
| 	Mode mode,
 | |
| 	base::lambda<QString()> placeholderFactory,
 | |
| 	const QString &value)
 | |
| : InputField(
 | |
| 	parent,
 | |
| 	st,
 | |
| 	mode,
 | |
| 	std::move(placeholderFactory),
 | |
| 	{ value, {} }) {
 | |
| }
 | |
| 
 | |
| InputField::InputField(
 | |
| 	QWidget *parent,
 | |
| 	const style::InputField &st,
 | |
| 	Mode mode,
 | |
| 	base::lambda<QString()> placeholderFactory,
 | |
| 	const TextWithTags &value)
 | |
| : RpWidget(parent)
 | |
| , _st(st)
 | |
| , _mode(mode)
 | |
| , _minHeight(st.heightMin)
 | |
| , _maxHeight(st.heightMax)
 | |
| , _inner(this)
 | |
| , _lastTextWithTags(value)
 | |
| , _placeholderFactory(std::move(placeholderFactory)) {
 | |
| 	_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);
 | |
| 	_defaultCharFormat = _inner->textCursor().charFormat();
 | |
| 	_defaultCharFormat.merge(PrepareTagFormat(_st, QString()));
 | |
| 	_inner->textCursor().setCharFormat(_defaultCharFormat);
 | |
| 	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();
 | |
| 		}
 | |
| 	});
 | |
| 	updatePalette();
 | |
| 
 | |
| 	_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, SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool)));
 | |
| 	connect(_inner, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool)));
 | |
| 	if (App::wnd()) connect(_inner, 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);
 | |
| }
 | |
| 
 | |
| QVariant InputField::loadResource(int type, const QUrl &name) {
 | |
| 	const auto imageName = name.toDisplayString();
 | |
| 	if (const auto emoji = Ui::Emoji::FromUrl(imageName)) {
 | |
| 		return QVariant(App::emojiSingle(emoji, _st.font->height));
 | |
| 	}
 | |
| 	return _inner->QTextEdit::loadResource(type, name);
 | |
| }
 | |
| 
 | |
| void InputField::updatePalette() {
 | |
| 	auto p = _inner->palette();
 | |
| 	p.setColor(QPalette::Text, _st.textFg->c);
 | |
| 	_inner->setPalette(p);
 | |
| }
 | |
| 
 | |
| 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) {
 | |
| 		_markdownEnabled = value;
 | |
| 	}, 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) {
 | |
| 	_maxLength = length;
 | |
| }
 | |
| 
 | |
| 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);
 | |
| 
 | |
| 			p.setFont(_st.font);
 | |
| 			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;
 | |
| }
 | |
| 
 | |
| 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<PossibleTag> *outPossibleTags) const {
 | |
| 	Expects((start == 0 && end < 0) || outPossibleTags == 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);
 | |
| 	PossibleTagAccumulator possibleTagAccumulator(outPossibleTags);
 | |
| 	const auto newline = outPossibleTags ? 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;
 | |
| 	}
 | |
| 
 | |
| 	bool tillFragmentEnd = full;
 | |
| 	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) {
 | |
| 				tillFragmentEnd = (fragmentEnd <= end);
 | |
| 				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 || fragmentPosition >= start) {
 | |
| 				lastTag = format.property(kTagProperty).toString();
 | |
| 				tagAccumulator.feed(lastTag, result.size());
 | |
| 				possibleTagAccumulator.feed(text, lastTag);
 | |
| 			}
 | |
| 
 | |
| 			auto begin = text.data();
 | |
| 			auto ch = begin;
 | |
| 			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);
 | |
| 					}
 | |
| 					if (!emojiText.isEmpty()) {
 | |
| 						result.append(emojiText);
 | |
| 					}
 | |
| 					begin = ch + 1;
 | |
| 				} break;
 | |
| 				}
 | |
| 			}
 | |
| 			if (ch > begin) {
 | |
| 				result.append(begin, ch - begin);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		block = block.next();
 | |
| 		if (block != till) {
 | |
| 			result.append('\n');
 | |
| 			possibleTagAccumulator.feed(newline, lastTag);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (tillFragmentEnd) {
 | |
| 		tagAccumulator.feed(QString(), result.size());
 | |
| 	}
 | |
| 	tagAccumulator.finish();
 | |
| 	possibleTagAccumulator.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 = !cRetina()
 | |
| 		&& (font().pixelSize() == 13)
 | |
| 		&& (font().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;
 | |
| 						++fragmentPosition;
 | |
| 					}
 | |
| 				}
 | |
| 				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::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 ? &_textAreaPossibleTags : nullptr);
 | |
| 
 | |
| 	if (tagsChanged || (_lastTextWithTags.text != currentText)) {
 | |
| 		_lastTextWithTags.text = currentText;
 | |
| 		emit changed();
 | |
| 		checkContentHeight();
 | |
| 	}
 | |
| 	startPlaceholderAnimation();
 | |
| 	if (App::wnd()) App::wnd()->updateGlobalMenu();
 | |
| }
 | |
| 
 | |
| 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 = [&] {
 | |
| 		const auto layout = textCursor().block().layout();
 | |
| 		return getTextWithTags().text.size()
 | |
| 			+ (layout ? layout->preeditAreaText().size() : 0);
 | |
| 	};
 | |
| 	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;
 | |
| }
 | |
| 
 | |
| void InputField::clear() {
 | |
| 	_inner->clear();
 | |
| 	startPlaceholderAnimation();
 | |
| }
 | |
| 
 | |
| bool InputField::hasFocus() const {
 | |
| 	return _inner->hasFocus();
 | |
| }
 | |
| 
 | |
| void InputField::setFocus() {
 | |
| 	_inner->setFocus();
 | |
| }
 | |
| 
 | |
| void InputField::clearFocus() {
 | |
| 	_inner->clearFocus();
 | |
| }
 | |
| 
 | |
| not_null<QTextEdit*> InputField::rawTextEdit() {
 | |
| 	return _inner;
 | |
| }
 | |
| 
 | |
| not_null<const QTextEdit*> InputField::rawTextEdit() const {
 | |
| 	return _inner;
 | |
| }
 | |
| 
 | |
| 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)
 | |
| 		|| (ctrl && shift)
 | |
| 		|| (ctrl
 | |
| 			&& _submitSettings != SubmitSettings::None
 | |
| 			&& _submitSettings != SubmitSettings::Enter)
 | |
| 		|| (!ctrl
 | |
| 			&& !shift
 | |
| 			&& _submitSettings != SubmitSettings::None
 | |
| 			&& _submitSettings != SubmitSettings::CtrlEnter);
 | |
| 	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(ctrl && shift);
 | |
| 	} 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 (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) {
 | |
| 		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);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| const InstantReplaces &InputField::instantReplaces() const {
 | |
| 	return _mutableInstantReplaces;
 | |
| }
 | |
| 
 | |
| 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 : _textAreaPossibleTags) {
 | |
| 			const auto end = possible.start + possible.length;
 | |
| 			if (possible.start + 2 * tagLength >= position) {
 | |
| 				return PossibleTag();
 | |
| 			} else if (end >= position || end + tagLength == position) {
 | |
| 				if (possible.tag == tag) {
 | |
| 					return possible;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		return PossibleTag();
 | |
| 	}();
 | |
| 	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();
 | |
| 	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.anchor() != position) {
 | |
| 		return;
 | |
| 	} else if (position < length) {
 | |
| 		return;
 | |
| 	}
 | |
| 	commitInstantReplacement(position - length, position, with, what);
 | |
| }
 | |
| 
 | |
| void InputField::commitInstantReplacement(
 | |
| 		int from,
 | |
| 		int till,
 | |
| 		const QString &with,
 | |
| 		base::optional<QString> checkOriginal) {
 | |
| 	const auto original = getTextWithTagsPart(from, till).text;
 | |
| 	if (checkOriginal
 | |
| 		&& checkOriginal->compare(original, Qt::CaseInsensitive) != 0) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	auto cursor = textCursor();
 | |
| 	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 spaces and 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) && !chIsSpace(ch)) {
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| 	for (; tagTill != tagFrom; --tagTill) {
 | |
| 		const auto ch = insert.at(tagTill - 1);
 | |
| 		if (!IsNewline(ch) && !chIsSpace(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::revertFormatReplace() {
 | |
| 	const auto cursor = textCursor();
 | |
| 	const auto position = cursor.position();
 | |
| 	if (position <= 0 || cursor.anchor() != position) {
 | |
| 		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()) {
 | |
| 		(new Ui::PopupMenu(nullptr, menu))->popup(e->globalPos());
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void InputField::dropEventInner(QDropEvent *e) {
 | |
| 	_inDrop = true;
 | |
| 	_inner->QTextEdit::dropEvent(e);
 | |
| 	_inDrop = false;
 | |
| 	_insertedTags.clear();
 | |
| 	_realInsertPosition = -1;
 | |
| }
 | |
| 
 | |
| 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 = qMin(cursor.position(), cursor.anchor());
 | |
| 	_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(
 | |
| 		base::lambda<QString()> placeholderFactory,
 | |
| 		int afterSymbols) {
 | |
| 	_placeholderFactory = std::move(placeholderFactory);
 | |
| 	if (_placeholderAfterSymbols != afterSymbols) {
 | |
| 		_placeholderAfterSymbols = afterSymbols;
 | |
| 		startPlaceholderAnimation();
 | |
| 	}
 | |
| 	refreshPlaceholder();
 | |
| }
 | |
| 
 | |
| 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();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| MaskedInputField::MaskedInputField(
 | |
| 	QWidget *parent,
 | |
| 	const style::InputField &st,
 | |
| 	base::lambda<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;
 | |
| }
 | |
| 
 | |
| 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.font);
 | |
| 			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(base::lambda<QString()> placeholderFactory) {
 | |
| 	_placeholderFactory = std::move(placeholderFactory);
 | |
| 	refreshPlaceholder();
 | |
| }
 | |
| 
 | |
| void MaskedInputField::contextMenuEvent(QContextMenuEvent *e) {
 | |
| 	if (auto menu = createStandardContextMenu()) {
 | |
| 		(new Ui::PopupMenu(nullptr, 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);
 | |
| 
 | |
| 	bool shift = e->modifiers().testFlag(Qt::ShiftModifier), alt = e->modifiers().testFlag(Qt::AltModifier);
 | |
| 	bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier), ctrlGood = true;
 | |
| 	if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) {
 | |
| 		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(ctrl && shift);
 | |
| #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, base::lambda<QString()> placeholderFactory, const QString &val) : MaskedInputField(parent, st, std::move(placeholderFactory), val) {
 | |
| 	setEchoMode(QLineEdit::Password);
 | |
| }
 | |
| 
 | |
| PortInput::PortInput(QWidget *parent, const style::InputField &st, base::lambda<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, base::lambda<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, base::lambda<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, base::lambda<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 (App::self()) {
 | |
| 		QVector<int> newPattern = phoneNumberParse(App::self()->phone());
 | |
| 		if (!newPattern.isEmpty()) {
 | |
| 			phone = App::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
 | 
