1 // This file is part of Desktop App Toolkit,
2 // a set of libraries for developing nice desktop applications.
3 //
4 // For license and copyright information please follow this link:
5 // https://github.com/desktop-app/legal/blob/master/LEGAL
6 //
7 #include "ui/widgets/input_fields.h"
8 
9 #include "ui/widgets/popup_menu.h"
10 #include "ui/text/text.h"
11 #include "ui/emoji_config.h"
12 #include "ui/ui_utility.h"
13 #include "base/invoke_queued.h"
14 #include "base/random.h"
15 #include "base/platform/base_platform_info.h"
16 #include "emoji_suggestions_helper.h"
17 #include "styles/palette.h"
18 #include "base/qt_adapters.h"
19 
20 #include <QtWidgets/QCommonStyle>
21 #include <QtWidgets/QScrollBar>
22 #include <QtWidgets/QApplication>
23 #include <QtGui/QClipboard>
24 #include <QtGui/QTextBlock>
25 #include <QtGui/QTextDocumentFragment>
26 #include <QtCore/QMimeData>
27 #include <QtCore/QRegularExpression>
28 
29 namespace Ui {
30 namespace {
31 
32 constexpr auto kInstantReplaceRandomId = QTextFormat::UserProperty;
33 constexpr auto kInstantReplaceWhatId = QTextFormat::UserProperty + 1;
34 constexpr auto kInstantReplaceWithId = QTextFormat::UserProperty + 2;
35 constexpr auto kReplaceTagId = QTextFormat::UserProperty + 3;
36 constexpr auto kTagProperty = QTextFormat::UserProperty + 4;
37 const auto kObjectReplacementCh = QChar(QChar::ObjectReplacementCharacter);
38 const auto kObjectReplacement = QString::fromRawData(
39 	&kObjectReplacementCh,
40 	1);
41 const auto &kTagBold = InputField::kTagBold;
42 const auto &kTagItalic = InputField::kTagItalic;
43 const auto &kTagUnderline = InputField::kTagUnderline;
44 const auto &kTagStrikeOut = InputField::kTagStrikeOut;
45 const auto &kTagCode = InputField::kTagCode;
46 const auto &kTagPre = InputField::kTagPre;
47 const auto kTagCheckLinkMeta = QString("^:/:/:^");
48 const auto kNewlineChars = QString("\r\n")
49 	+ QChar(0xfdd0) // QTextBeginningOfFrame
50 	+ QChar(0xfdd1) // QTextEndOfFrame
51 	+ QChar(QChar::ParagraphSeparator)
52 	+ QChar(QChar::LineSeparator);
53 
54 class InputDocument : public QTextDocument {
55 public:
56 	InputDocument(QObject *parent, const style::InputField &st);
57 
58 protected:
59 	QVariant loadResource(int type, const QUrl &name) override;
60 
61 private:
62 	const style::InputField &_st;
63 	std::map<QUrl, QVariant> _emojiCache;
64 	rpl::lifetime _lifetime;
65 
66 };
67 
InputDocument(QObject * parent,const style::InputField & st)68 InputDocument::InputDocument(QObject *parent, const style::InputField &st)
69 : QTextDocument(parent)
70 , _st(st) {
71 	Emoji::Updated(
72 	) | rpl::start_with_next([=] {
73 		_emojiCache.clear();
74 	}, _lifetime);
75 }
76 
loadResource(int type,const QUrl & name)77 QVariant InputDocument::loadResource(int type, const QUrl &name) {
78 	if (type != QTextDocument::ImageResource
79 		|| name.scheme() != qstr("emoji")) {
80 		return QTextDocument::loadResource(type, name);
81 	}
82 	const auto i = _emojiCache.find(name);
83 	if (i != _emojiCache.end()) {
84 		return i->second;
85 	}
86 	auto result = [&] {
87 		if (const auto emoji = Emoji::FromUrl(name.toDisplayString())) {
88 			const auto height = std::max(
89 				_st.font->height * style::DevicePixelRatio(),
90 				Emoji::GetSizeNormal());
91 			return QVariant(Emoji::SinglePixmap(emoji, height));
92 		}
93 		return QVariant();
94 	}();
95 	_emojiCache.emplace(name, result);
96 	return result;
97 }
98 
IsNewline(QChar ch)99 bool IsNewline(QChar ch) {
100 	return (kNewlineChars.indexOf(ch) >= 0);
101 }
102 
IsValidMarkdownLink(QStringView link)103 [[nodiscard]] bool IsValidMarkdownLink(QStringView link) {
104 	return (link.indexOf('.') >= 0) || (link.indexOf(':') >= 0);
105 }
106 
CheckFullTextTag(const TextWithTags & textWithTags,const QString & tag)107 [[nodiscard]] QString CheckFullTextTag(
108 		const TextWithTags &textWithTags,
109 		const QString &tag) {
110 	auto resultLink = QString();
111 	const auto checkingLink = (tag == kTagCheckLinkMeta);
112 	const auto &text = textWithTags.text;
113 	auto from = 0;
114 	auto till = int(text.size());
115 	const auto adjust = [&] {
116 		for (; from != till; ++from) {
117 			if (!IsNewline(text[from]) && !Text::IsSpace(text[from])) {
118 				break;
119 			}
120 		}
121 	};
122 	for (const auto &existing : textWithTags.tags) {
123 		adjust();
124 		if (existing.offset > from) {
125 			return QString();
126 		}
127 		auto found = false;
128 		for (const auto &single : QStringView(existing.id).split('|')) {
129 			const auto normalized = (single == QStringView(kTagPre))
130 				? QStringView(kTagCode)
131 				: single;
132 			if (checkingLink && IsValidMarkdownLink(single)) {
133 				if (resultLink.isEmpty()) {
134 					resultLink = single.toString();
135 					found = true;
136 					break;
137 				} else if (QStringView(resultLink) == single) {
138 					found = true;
139 					break;
140 				}
141 				return QString();
142 			} else if (!checkingLink && QStringView(tag) == normalized) {
143 				found = true;
144 				break;
145 			}
146 		}
147 		if (!found) {
148 			return QString();
149 		}
150 		from = std::clamp(existing.offset + existing.length, from, till);
151 	}
152 	while (till != from) {
153 		if (!IsNewline(text[till - 1]) && !Text::IsSpace(text[till - 1])) {
154 			break;
155 		}
156 		--till;
157 	}
158 	return (from < till) ? QString() : checkingLink ? resultLink : tag;
159 }
160 
HasFullTextTag(const TextWithTags & textWithTags,const QString & tag)161 [[nodiscard]] bool HasFullTextTag(
162 		const TextWithTags &textWithTags,
163 		const QString &tag) {
164 	return !CheckFullTextTag(textWithTags, tag).isEmpty();
165 }
166 
167 class TagAccumulator {
168 public:
TagAccumulator(TextWithTags::Tags & tags)169 	TagAccumulator(TextWithTags::Tags &tags) : _tags(tags) {
170 	}
171 
changed() const172 	bool changed() const {
173 		return _changed;
174 	}
175 
feed(const QString & randomTagId,int currentPosition)176 	void feed(const QString &randomTagId, int currentPosition) {
177 		if (randomTagId == _currentTagId) {
178 			return;
179 		}
180 
181 		if (!_currentTagId.isEmpty()) {
182 			const auto tag = TextWithTags::Tag {
183 				_currentStart,
184 				currentPosition - _currentStart,
185 				_currentTagId
186 			};
187 			if (tag.length > 0) {
188 				if (_currentTag >= _tags.size()) {
189 					_changed = true;
190 					_tags.push_back(tag);
191 				} else if (_tags[_currentTag] != tag) {
192 					_changed = true;
193 					_tags[_currentTag] = tag;
194 				}
195 				++_currentTag;
196 			}
197 		}
198 		_currentTagId = randomTagId;
199 		_currentStart = currentPosition;
200 	};
201 
finish()202 	void finish() {
203 		if (_currentTag < _tags.size()) {
204 			_tags.resize(_currentTag);
205 			_changed = true;
206 		}
207 	}
208 
209 private:
210 	TextWithTags::Tags &_tags;
211 	bool _changed = false;
212 
213 	int _currentTag = 0;
214 	int _currentStart = 0;
215 	QString _currentTagId;
216 
217 };
218 
219 struct TagStartExpression {
220 	QString tag;
221 	QString goodBefore;
222 	QString badAfter;
223 	QString badBefore;
224 	QString goodAfter;
225 };
226 
227 constexpr auto kTagBoldIndex = 0;
228 constexpr auto kTagItalicIndex = 1;
229 //constexpr auto kTagUnderlineIndex = 2;
230 constexpr auto kTagStrikeOutIndex = 2;
231 constexpr auto kTagCodeIndex = 3;
232 constexpr auto kTagPreIndex = 4;
233 constexpr auto kInvalidPosition = std::numeric_limits<int>::max() / 2;
234 
235 class TagSearchItem {
236 public:
237 	enum class Edge {
238 		Open,
239 		Close,
240 	};
241 
matchPosition(Edge edge) const242 	int matchPosition(Edge edge) const {
243 		return (_position >= 0) ? _position : kInvalidPosition;
244 	}
245 
applyOffset(int offset)246 	void applyOffset(int offset) {
247 		if (_position < offset) {
248 			_position = -1;
249 		}
250 		accumulate_max(_offset, offset);
251 	}
252 
fill(const QString & text,Edge edge,const TagStartExpression & expression)253 	void fill(
254 			const QString &text,
255 			Edge edge,
256 			const TagStartExpression &expression) {
257 		const auto length = text.size();
258 		const auto &tag = expression.tag;
259 		const auto tagLength = tag.size();
260 		const auto isGoodBefore = [&](QChar ch) {
261 			return expression.goodBefore.isEmpty()
262 				|| (expression.goodBefore.indexOf(ch) >= 0);
263 		};
264 		const auto isBadAfter = [&](QChar ch) {
265 			return !expression.badAfter.isEmpty()
266 				&& (expression.badAfter.indexOf(ch) >= 0);
267 		};
268 		const auto isBadBefore = [&](QChar ch) {
269 			return !expression.badBefore.isEmpty()
270 				&& (expression.badBefore.indexOf(ch) >= 0);
271 		};
272 		const auto isGoodAfter = [&](QChar ch) {
273 			return expression.goodAfter.isEmpty()
274 				|| (expression.goodAfter.indexOf(ch) >= 0);
275 		};
276 		const auto check = [&](Edge edge) {
277 			if (_position > 0) {
278 				const auto before = text[_position - 1];
279 				if ((edge == Edge::Open && !isGoodBefore(before))
280 					|| (edge == Edge::Close && isBadBefore(before))) {
281 					return false;
282 				}
283 			}
284 			if (_position + tagLength < length) {
285 				const auto after = text[_position + tagLength];
286 				if ((edge == Edge::Open && isBadAfter(after))
287 					|| (edge == Edge::Close && !isGoodAfter(after))) {
288 					return false;
289 				}
290 			}
291 			return true;
292 		};
293 		const auto edgeIndex = static_cast<int>(edge);
294 		if (_position >= 0) {
295 			if (_checked[edgeIndex]) {
296 				return;
297 			} else if (check(edge)) {
298 				_checked[edgeIndex] = true;
299 				return;
300 			} else {
301 				_checked = { { false, false } };
302 			}
303 		}
304 		while (true) {
305 			_position = text.indexOf(tag, _offset);
306 			if (_position < 0) {
307 				_offset = _position = kInvalidPosition;
308 				break;
309 			}
310 			_offset = _position + tagLength;
311 			if (check(edge)) {
312 				break;
313 			} else {
314 				continue;
315 			}
316 		}
317 		if (_position == kInvalidPosition) {
318 			_checked = { { true, true } };
319 		} else {
320 			_checked = { { false, false } };
321 			_checked[edgeIndex] = true;
322 		}
323 	}
324 
325 private:
326 	int _offset = 0;
327 	int _position = -1;
328 	std::array<bool, 2> _checked = { { false, false } };
329 
330 };
331 
TagStartExpressions()332 const std::vector<TagStartExpression> &TagStartExpressions() {
333 	static auto cached = std::vector<TagStartExpression> {
334 		{
335 			kTagBold,
336 			TextUtilities::MarkdownBoldGoodBefore(),
337 			TextUtilities::MarkdownBoldBadAfter(),
338 			TextUtilities::MarkdownBoldBadAfter(),
339 			TextUtilities::MarkdownBoldGoodBefore()
340 		},
341 		{
342 			kTagItalic,
343 			TextUtilities::MarkdownItalicGoodBefore(),
344 			TextUtilities::MarkdownItalicBadAfter(),
345 			TextUtilities::MarkdownItalicBadAfter(),
346 			TextUtilities::MarkdownItalicGoodBefore()
347 		},
348 		//{
349 		//	kTagUnderline,
350 		//	TextUtilities::MarkdownUnderlineGoodBefore(),
351 		//	TextUtilities::MarkdownUnderlineBadAfter(),
352 		//	TextUtilities::MarkdownUnderlineBadAfter(),
353 		//	TextUtilities::MarkdownUnderlineGoodBefore()
354 		//},
355 		{
356 			kTagStrikeOut,
357 			TextUtilities::MarkdownStrikeOutGoodBefore(),
358 			TextUtilities::MarkdownStrikeOutBadAfter(),
359 			TextUtilities::MarkdownStrikeOutBadAfter(),
360 			QString(),
361 		},
362 		{
363 			kTagCode,
364 			TextUtilities::MarkdownCodeGoodBefore(),
365 			TextUtilities::MarkdownCodeBadAfter(),
366 			TextUtilities::MarkdownCodeBadAfter(),
367 			TextUtilities::MarkdownCodeGoodBefore()
368 		},
369 		{
370 			kTagPre,
371 			TextUtilities::MarkdownPreGoodBefore(),
372 			TextUtilities::MarkdownPreBadAfter(),
373 			TextUtilities::MarkdownPreBadAfter(),
374 			TextUtilities::MarkdownPreGoodBefore()
375 		},
376 	};
377 	return cached;
378 }
379 
TagIndices()380 const std::map<QString, int> &TagIndices() {
381 	static auto cached = std::map<QString, int> {
382 		{ kTagBold, kTagBoldIndex },
383 		{ kTagItalic, kTagItalicIndex },
384 		//{ kTagUnderline, kTagUnderlineIndex },
385 		{ kTagStrikeOut, kTagStrikeOutIndex },
386 		{ kTagCode, kTagCodeIndex },
387 		{ kTagPre, kTagPreIndex },
388 	};
389 	return cached;
390 }
391 
DoesTagFinishByNewline(const QString & tag)392 bool DoesTagFinishByNewline(const QString &tag) {
393 	return (tag == kTagCode);
394 }
395 
396 class MarkdownTagAccumulator {
397 public:
398 	using Edge = TagSearchItem::Edge;
399 
MarkdownTagAccumulator(std::vector<InputField::MarkdownTag> * tags)400 	MarkdownTagAccumulator(std::vector<InputField::MarkdownTag> *tags)
401 	: _tags(tags)
402 	, _expressions(TagStartExpressions())
403 	, _tagIndices(TagIndices())
404 	, _items(_expressions.size()) {
405 	}
406 
407 	// Here we use the fact that text either contains only emoji
408 	// { adjustedTextLength = text.size() * (emojiLength - 1) }
409 	// or contains no emoji at all and can have tag edges in the middle
410 	// { adjustedTextLength = 0 }.
411 	//
412 	// Otherwise we would have to pass emoji positions inside text.
feed(const QString & text,int adjustedTextLength,const QString & textTag)413 	void feed(
414 			const QString &text,
415 			int adjustedTextLength,
416 			const QString &textTag) {
417 		if (!_tags) {
418 			return;
419 		}
420 		const auto guard = gsl::finally([&] {
421 			_currentInternalLength += text.size();
422 			_currentAdjustedLength += adjustedTextLength;
423 		});
424 		if (!textTag.isEmpty()) {
425 			finishTags();
426 			return;
427 		}
428 		for (auto &item : _items) {
429 			item = TagSearchItem();
430 		}
431 		auto tryFinishTag = _currentTag;
432 		while (true) {
433 			for (; tryFinishTag != _currentFreeTag; ++tryFinishTag) {
434 				auto &tag = (*_tags)[tryFinishTag];
435 				if (tag.internalLength >= 0) {
436 					continue;
437 				}
438 
439 				const auto i = _tagIndices.find(tag.tag);
440 				Assert(i != end(_tagIndices));
441 				const auto tagIndex = i->second;
442 
443 				const auto atLeastOffset =
444 					tag.internalStart
445 					+ tag.tag.size()
446 					+ 1
447 					- _currentInternalLength;
448 				_items[tagIndex].applyOffset(atLeastOffset);
449 
450 				fillItem(
451 					tagIndex,
452 					text,
453 					Edge::Close);
454 				if (finishByNewline(tryFinishTag, text, tagIndex)) {
455 					continue;
456 				}
457 				const auto position = matchPosition(tagIndex, Edge::Close);
458 				if (position < kInvalidPosition) {
459 					const auto till = position + tag.tag.size();
460 					finishTag(tryFinishTag, till, true);
461 					_items[tagIndex].applyOffset(till);
462 				}
463 			}
464 			for (auto i = 0, count = int(_items.size()); i != count; ++i) {
465 				fillItem(i, text, Edge::Open);
466 			}
467 			const auto min = minIndex(Edge::Open);
468 			if (min < 0) {
469 				return;
470 			}
471 			startTag(matchPosition(min, Edge::Open), _expressions[min].tag);
472 		}
473 	}
474 
finish()475 	void finish() {
476 		if (!_tags) {
477 			return;
478 		}
479 		finishTags();
480 		if (_currentTag < _tags->size()) {
481 			_tags->resize(_currentTag);
482 		}
483 	}
484 
485 private:
finishTag(int index,int offsetFromAccumulated,bool closed)486 	void finishTag(int index, int offsetFromAccumulated, bool closed) {
487 		Expects(_tags != nullptr);
488 		Expects(index >= 0 && index < _tags->size());
489 
490 		auto &tag = (*_tags)[index];
491 		if (tag.internalLength < 0) {
492 			tag.internalLength = _currentInternalLength
493 				+ offsetFromAccumulated
494 				- tag.internalStart;
495 			tag.adjustedLength = _currentAdjustedLength
496 				+ offsetFromAccumulated
497 				- tag.adjustedStart;
498 			tag.closed = closed;
499 		}
500 		if (index == _currentTag) {
501 			++_currentTag;
502 		}
503 	}
finishByNewline(int index,const QString & text,int tagIndex)504 	bool finishByNewline(
505 			int index,
506 			const QString &text,
507 			int tagIndex) {
508 		Expects(_tags != nullptr);
509 		Expects(index >= 0 && index < _tags->size());
510 
511 		auto &tag = (*_tags)[index];
512 
513 		if (!DoesTagFinishByNewline(tag.tag)) {
514 			return false;
515 		}
516 		const auto endPosition = newlinePosition(
517 			text,
518 			std::max(0, tag.internalStart + 1 - _currentInternalLength));
519 		if (matchPosition(tagIndex, Edge::Close) <= endPosition) {
520 			return false;
521 		}
522 		finishTag(index, endPosition, false);
523 		return true;
524 	}
finishTags()525 	void finishTags() {
526 		while (_currentTag != _currentFreeTag) {
527 			finishTag(_currentTag, 0, false);
528 		}
529 	}
startTag(int offsetFromAccumulated,const QString & tag)530 	void startTag(int offsetFromAccumulated, const QString &tag) {
531 		Expects(_tags != nullptr);
532 
533 		const auto newTag = InputField::MarkdownTag{
534 			_currentInternalLength + offsetFromAccumulated,
535 			-1,
536 			_currentAdjustedLength + offsetFromAccumulated,
537 			-1,
538 			false,
539 			tag
540 		};
541 		if (_currentFreeTag < _tags->size()) {
542 			(*_tags)[_currentFreeTag] = newTag;
543 		} else {
544 			_tags->push_back(newTag);
545 		}
546 		++_currentFreeTag;
547 	}
fillItem(int index,const QString & text,Edge edge)548 	void fillItem(int index, const QString &text, Edge edge) {
549 		Expects(index >= 0 && index < _items.size());
550 
551 		_items[index].fill(text, edge, _expressions[index]);
552 	}
matchPosition(int index,Edge edge) const553 	int matchPosition(int index, Edge edge) const {
554 		Expects(index >= 0 && index < _items.size());
555 
556 		return _items[index].matchPosition(edge);
557 	}
newlinePosition(const QString & text,int offset) const558 	int newlinePosition(const QString &text, int offset) const {
559 		const auto length = text.size();
560 		if (offset < length) {
561 			const auto begin = text.data();
562 			const auto end = begin + length;
563 			for (auto ch = begin + offset; ch != end; ++ch) {
564 				if (IsNewline(*ch)) {
565 					return (ch - begin);
566 				}
567 			}
568 		}
569 		return kInvalidPosition;
570 	}
minIndex(Edge edge) const571 	int minIndex(Edge edge) const {
572 		auto result = -1;
573 		auto minPosition = kInvalidPosition;
574 		for (auto i = 0, count = int(_items.size()); i != count; ++i) {
575 			const auto position = matchPosition(i, edge);
576 			if (position < minPosition) {
577 				minPosition = position;
578 				result = i;
579 			}
580 		}
581 		return result;
582 	}
minIndexForFinish(const std::vector<int> & indices) const583 	int minIndexForFinish(const std::vector<int> &indices) const {
584 		const auto tagIndex = indices[0];
585 		auto result = -1;
586 		auto minPosition = kInvalidPosition;
587 		for (auto i : indices) {
588 			const auto edge = (i == tagIndex) ? Edge::Close : Edge::Open;
589 			const auto position = matchPosition(i, edge);
590 			if (position < minPosition) {
591 				minPosition = position;
592 				result = i;
593 			}
594 		}
595 		return result;
596 	}
597 
598 	std::vector<InputField::MarkdownTag> *_tags = nullptr;
599 	const std::vector<TagStartExpression> &_expressions;
600 	const std::map<QString, int> &_tagIndices;
601 	std::vector<TagSearchItem> _items;
602 
603 	int _currentTag = 0;
604 	int _currentFreeTag = 0;
605 	int _currentInternalLength = 0;
606 	int _currentAdjustedLength = 0;
607 
608 };
609 
610 template <typename InputClass>
611 class InputStyle : public QCommonStyle {
612 public:
InputStyle()613 	InputStyle() {
614 		setParent(QCoreApplication::instance());
615 	}
616 
drawPrimitive(PrimitiveElement element,const QStyleOption * option,QPainter * painter,const QWidget * widget=nullptr) const617 	void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget = nullptr) const override {
618 	}
subElementRect(SubElement r,const QStyleOption * opt,const QWidget * widget=nullptr) const619 	QRect subElementRect(SubElement r, const QStyleOption *opt, const QWidget *widget = nullptr) const override {
620 		switch (r) {
621 			case SE_LineEditContents:
622 				const auto w = widget ? qobject_cast<const InputClass*>(widget) : nullptr;
623 				return w ? w->getTextRect() : QCommonStyle::subElementRect(r, opt, widget);
624 			break;
625 		}
626 		return QCommonStyle::subElementRect(r, opt, widget);
627 	}
628 
instance()629 	static InputStyle<InputClass> *instance() {
630 		if (!_instance) {
631 			if (!QGuiApplication::instance()) {
632 				return nullptr;
633 			}
634 			_instance = new InputStyle<InputClass>();
635 		}
636 		return _instance;
637 	}
638 
~InputStyle()639 	~InputStyle() {
640 		_instance = nullptr;
641 	}
642 
643 private:
644 	static InputStyle<InputClass> *_instance;
645 
646 };
647 
648 template <typename InputClass>
649 InputStyle<InputClass> *InputStyle<InputClass>::_instance = nullptr;
650 
651 template <typename Iterator>
AccumulateText(Iterator begin,Iterator end)652 QString AccumulateText(Iterator begin, Iterator end) {
653 	auto result = QString();
654 	result.reserve(end - begin);
655 	for (auto i = end; i != begin;) {
656 		result.push_back(*--i);
657 	}
658 	return result;
659 }
660 
PrepareEmojiFormat(EmojiPtr emoji,const QFont & font)661 QTextImageFormat PrepareEmojiFormat(EmojiPtr emoji, const QFont &font) {
662 	const auto factor = style::DevicePixelRatio();
663 	const auto size = Emoji::GetSizeNormal();
664 	const auto width = size + st::emojiPadding * factor * 2;
665 	const auto height = std::max(QFontMetrics(font).height() * factor, size);
666 	auto result = QTextImageFormat();
667 	result.setWidth(width / factor);
668 	result.setHeight(height / factor);
669 	result.setName(emoji->toUrl());
670 	result.setVerticalAlignment(QTextCharFormat::AlignBottom);
671 	return result;
672 }
673 
674 // Optimization: with null page size document does not re-layout
675 // on each insertText / mergeCharFormat.
PrepareFormattingOptimization(not_null<QTextDocument * > document)676 void PrepareFormattingOptimization(not_null<QTextDocument*> document) {
677 	if (!document->pageSize().isNull()) {
678 		document->setPageSize(QSizeF(0, 0));
679 	}
680 }
681 
RemoveDocumentTags(const style::InputField & st,not_null<QTextDocument * > document,int from,int end)682 void RemoveDocumentTags(
683 		const style::InputField &st,
684 		not_null<QTextDocument*> document,
685 		int from,
686 		int end) {
687 	auto cursor = QTextCursor(document);
688 	cursor.setPosition(from);
689 	cursor.setPosition(end, QTextCursor::KeepAnchor);
690 
691 	auto format = QTextCharFormat();
692 	format.setProperty(kTagProperty, QString());
693 	format.setProperty(kReplaceTagId, QString());
694 	format.setForeground(st.textFg);
695 	format.setFont(st.font);
696 	cursor.mergeCharFormat(format);
697 }
698 
PrepareTagFormat(const style::InputField & st,QString tag)699 QTextCharFormat PrepareTagFormat(
700 		const style::InputField &st,
701 		QString tag) {
702 	auto result = QTextCharFormat();
703 	auto font = st.font;
704 	auto color = std::optional<style::color>();
705 	const auto applyOne = [&](QStringView tag) {
706 		if (IsValidMarkdownLink(tag)) {
707 			color = st::defaultTextPalette.linkFg;
708 		} else if (tag == kTagBold) {
709 			font = font->bold();
710 		} else if (tag == kTagItalic) {
711 			font = font->italic();
712 		} else if (tag == kTagUnderline) {
713 			font = font->underline();
714 		} else if (tag == kTagStrikeOut) {
715 			font = font->strikeout();
716 		} else if (tag == kTagCode || tag == kTagPre) {
717 			color = st::defaultTextPalette.monoFg;
718 			font = font->monospace();
719 		}
720 	};
721 	for (const auto &tag : QStringView(tag).split('|')) {
722 		applyOne(tag);
723 	}
724 	result.setFont(font);
725 	result.setForeground(color.value_or(st.textFg));
726 	result.setProperty(kTagProperty, tag);
727 	return result;
728 }
729 
ApplyTagFormat(QTextCharFormat & to,const QTextCharFormat & from)730 void ApplyTagFormat(QTextCharFormat &to, const QTextCharFormat &from) {
731 	to.setProperty(kTagProperty, from.property(kTagProperty));
732 	to.setProperty(kReplaceTagId, from.property(kReplaceTagId));
733 	to.setFont(from.font());
734 	to.setForeground(from.foreground());
735 }
736 
737 // Returns the position of the first inserted tag or "changedEnd" value if none found.
ProcessInsertedTags(const style::InputField & st,not_null<QTextDocument * > document,int changedPosition,int changedEnd,const TextWithTags::Tags & tags,InputField::TagMimeProcessor * processor)738 int ProcessInsertedTags(
739 		const style::InputField &st,
740 		not_null<QTextDocument*> document,
741 		int changedPosition,
742 		int changedEnd,
743 		const TextWithTags::Tags &tags,
744 		InputField::TagMimeProcessor *processor) {
745 	int firstTagStart = changedEnd;
746 	int applyNoTagFrom = changedEnd;
747 	for (const auto &tag : tags) {
748 		int tagFrom = changedPosition + tag.offset;
749 		int tagTo = tagFrom + tag.length;
750 		accumulate_max(tagFrom, changedPosition);
751 		accumulate_min(tagTo, changedEnd);
752 		auto tagId = processor ? processor->tagFromMimeTag(tag.id) : tag.id;
753 		if (tagTo > tagFrom && !tagId.isEmpty()) {
754 			accumulate_min(firstTagStart, tagFrom);
755 
756 			PrepareFormattingOptimization(document);
757 
758 			if (applyNoTagFrom < tagFrom) {
759 				RemoveDocumentTags(
760 					st,
761 					document,
762 					applyNoTagFrom,
763 					tagFrom);
764 			}
765 			QTextCursor c(document);
766 			c.setPosition(tagFrom);
767 			c.setPosition(tagTo, QTextCursor::KeepAnchor);
768 
769 			c.mergeCharFormat(PrepareTagFormat(st, tagId));
770 
771 			applyNoTagFrom = tagTo;
772 		}
773 	}
774 	if (applyNoTagFrom < changedEnd) {
775 		RemoveDocumentTags(st, document, applyNoTagFrom, changedEnd);
776 	}
777 
778 	return firstTagStart;
779 }
780 
781 // When inserting a part of text inside a tag we need to have
782 // a way to know if the insertion replaced the end of the tag
783 // or it was strictly inside (in the middle) of the tag.
WasInsertTillTheEndOfTag(QTextBlock block,QTextBlock::iterator fragmentIt,int insertionEnd)784 bool WasInsertTillTheEndOfTag(
785 		QTextBlock block,
786 		QTextBlock::iterator fragmentIt,
787 		int insertionEnd) {
788 	const auto format = fragmentIt.fragment().charFormat();
789 	const auto insertTagName = format.property(kTagProperty);
790 	while (true) {
791 		for (; !fragmentIt.atEnd(); ++fragmentIt) {
792 			const auto fragment = fragmentIt.fragment();
793 			const auto position = fragment.position();
794 			const auto outsideInsertion = (position >= insertionEnd);
795 			if (outsideInsertion) {
796 				const auto format = fragment.charFormat();
797 				return (format.property(kTagProperty) != insertTagName);
798 			}
799 			const auto end = position + fragment.length();
800 			const auto notFullFragmentInserted = (end > insertionEnd);
801 			if (notFullFragmentInserted) {
802 				return false;
803 			}
804 		}
805 		block = block.next();
806 		if (block.isValid()) {
807 			fragmentIt = block.begin();
808 		} else {
809 			break;
810 		}
811 	}
812 	// Insertion goes till the end of the text => not strictly inside a tag.
813 	return true;
814 }
815 
816 struct FormattingAction {
817 	enum class Type {
818 		Invalid,
819 		InsertEmoji,
820 		TildeFont,
821 		RemoveTag,
822 		RemoveNewline,
823 		ClearInstantReplace,
824 	};
825 
826 	Type type = Type::Invalid;
827 	EmojiPtr emoji = nullptr;
828 	bool isTilde = false;
829 	QString tildeTag;
830 	int intervalStart = 0;
831 	int intervalEnd = 0;
832 
833 };
834 
835 } // namespace
836 
837 // kTagUnderline is not used for Markdown.
838 
839 const QString InputField::kTagBold = QStringLiteral("**");
840 const QString InputField::kTagItalic = QStringLiteral("__");
841 const QString InputField::kTagUnderline = QStringLiteral("^^");
842 const QString InputField::kTagStrikeOut = QStringLiteral("~~");
843 const QString InputField::kTagCode = QStringLiteral("`");
844 const QString InputField::kTagPre = QStringLiteral("```");
845 
846 class InputField::Inner final : public QTextEdit {
847 public:
Inner(not_null<InputField * > parent)848 	Inner(not_null<InputField*> parent) : QTextEdit(parent) {
849 	}
850 
851 protected:
viewportEvent(QEvent * e)852 	bool viewportEvent(QEvent *e) override {
853 		return outer()->viewportEventInner(e);
854 	}
focusInEvent(QFocusEvent * e)855 	void focusInEvent(QFocusEvent *e) override {
856 		return outer()->focusInEventInner(e);
857 	}
focusOutEvent(QFocusEvent * e)858 	void focusOutEvent(QFocusEvent *e) override {
859 		return outer()->focusOutEventInner(e);
860 	}
keyPressEvent(QKeyEvent * e)861 	void keyPressEvent(QKeyEvent *e) override {
862 		return outer()->keyPressEventInner(e);
863 	}
contextMenuEvent(QContextMenuEvent * e)864 	void contextMenuEvent(QContextMenuEvent *e) override {
865 		return outer()->contextMenuEventInner(e);
866 	}
dropEvent(QDropEvent * e)867 	void dropEvent(QDropEvent *e) override {
868 		return outer()->dropEventInner(e);
869 	}
inputMethodEvent(QInputMethodEvent * e)870 	void inputMethodEvent(QInputMethodEvent *e) override {
871 		return outer()->inputMethodEventInner(e);
872 	}
873 
canInsertFromMimeData(const QMimeData * source) const874 	bool canInsertFromMimeData(const QMimeData *source) const override {
875 		return outer()->canInsertFromMimeDataInner(source);
876 	}
insertFromMimeData(const QMimeData * source)877 	void insertFromMimeData(const QMimeData *source) override {
878 		return outer()->insertFromMimeDataInner(source);
879 	}
createMimeDataFromSelection() const880 	QMimeData *createMimeDataFromSelection() const override {
881 		return outer()->createMimeDataFromSelectionInner();
882 	}
883 
884 private:
outer() const885 	not_null<InputField*> outer() const {
886 		return static_cast<InputField*>(parentWidget());
887 	}
888 	friend class InputField;
889 
890 };
891 
InsertEmojiAtCursor(QTextCursor cursor,EmojiPtr emoji)892 void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji) {
893 	const auto currentFormat = cursor.charFormat();
894 	auto format = PrepareEmojiFormat(emoji, currentFormat.font());
895 	ApplyTagFormat(format, currentFormat);
896 	cursor.insertText(kObjectReplacement, format);
897 }
898 
add(const QString & what,const QString & with)899 void InstantReplaces::add(const QString &what, const QString &with) {
900 	auto node = &reverseMap;
901 	for (auto i = what.end(), b = what.begin(); i != b;) {
902 		node = &node->tail.emplace(*--i, Node()).first->second;
903 	}
904 	node->text = with;
905 	accumulate_max(maxLength, int(what.size()));
906 }
907 
Default()908 const InstantReplaces &InstantReplaces::Default() {
909 	static const auto result = [] {
910 		auto result = InstantReplaces();
911 		result.add("--", QString(1, QChar(8212)));
912 		result.add("<<", QString(1, QChar(171)));
913 		result.add(">>", QString(1, QChar(187)));
914 		result.add(
915 			":shrug:",
916 			QChar(175) + QString("\\_(") + QChar(12484) + ")_/" + QChar(175));
917 		result.add(":o ", QString(1, QChar(0xD83D)) + QChar(0xDE28));
918 		result.add("xD ", QString(1, QChar(0xD83D)) + QChar(0xDE06));
919 		const auto &replacements = Emoji::internal::GetAllReplacements();
920 		for (const auto &one : replacements) {
921 			const auto with = Emoji::QStringFromUTF16(one.emoji);
922 			const auto what = Emoji::QStringFromUTF16(one.replacement);
923 			result.add(what, with);
924 		}
925 		const auto &pairs = Emoji::internal::GetReplacementPairs();
926 		for (const auto &[what, index] : pairs) {
927 			const auto emoji = Emoji::internal::ByIndex(index);
928 			Assert(emoji != nullptr);
929 			result.add(what, emoji->text());
930 		}
931 		return result;
932 	}();
933 	return result;
934 }
935 
TextOnly()936 const InstantReplaces &InstantReplaces::TextOnly() {
937 	static const auto result = [] {
938 		auto result = InstantReplaces();
939 		result.add("--", QString(1, QChar(8212)));
940 		result.add("<<", QString(1, QChar(171)));
941 		result.add(">>", QString(1, QChar(187)));
942 		result.add(
943 			":shrug:",
944 			QChar(175) + QString("\\_(") + QChar(12484) + ")_/" + QChar(175));
945 		return result;
946 	}();
947 	return result;
948 }
949 
FlatInput(QWidget * parent,const style::FlatInput & st,rpl::producer<QString> placeholder,const QString & v)950 FlatInput::FlatInput(
951 	QWidget *parent,
952 	const style::FlatInput &st,
953 	rpl::producer<QString> placeholder,
954 	const QString &v)
955 : Parent(v, parent)
956 , _oldtext(v)
957 , _placeholderFull(std::move(placeholder))
958 , _placeholderVisible(!v.length())
959 , _st(st)
960 , _textMrg(_st.textMrg) {
961 	setCursor(style::cur_text);
962 	resize(_st.width, _st.height);
963 
964 	setFont(_st.font->f);
965 	setAlignment(_st.align);
966 
967 	_placeholderFull.value(
968 	) | rpl::start_with_next([=](const QString &text) {
969 		refreshPlaceholder(text);
970 	}, lifetime());
971 
972 	style::PaletteChanged(
973 	) | rpl::start_with_next([=] {
974 		updatePalette();
975 	}, lifetime());
976 	updatePalette();
977 
978 	connect(this, SIGNAL(textChanged(const QString &)), this, SLOT(onTextChange(const QString &)));
979 	connect(this, SIGNAL(textEdited(const QString &)), this, SLOT(onTextEdited()));
980 	connect(this, &FlatInput::selectionChanged, [] {
981 		Integration::Instance().textActionsUpdated();
982 	});
983 
984 	setStyle(InputStyle<FlatInput>::instance());
985 	QLineEdit::setTextMargins(0, 0, 0, 0);
986 	setContentsMargins(0, 0, 0, 0);
987 
988 	setAttribute(Qt::WA_AcceptTouchEvents);
989 	_touchTimer.setSingleShot(true);
990 	connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
991 }
992 
updatePalette()993 void FlatInput::updatePalette() {
994 	auto p = palette();
995 	p.setColor(QPalette::Text, _st.textColor->c);
996 	p.setColor(QPalette::Highlight, st::msgInBgSelected->c);
997 	p.setColor(QPalette::HighlightedText, st::historyTextInFgSelected->c);
998 	setPalette(p);
999 }
1000 
customUpDown(bool custom)1001 void FlatInput::customUpDown(bool custom) {
1002 	_customUpDown = custom;
1003 }
1004 
onTouchTimer()1005 void FlatInput::onTouchTimer() {
1006 	_touchRightButton = true;
1007 }
1008 
eventHook(QEvent * e)1009 bool FlatInput::eventHook(QEvent *e) {
1010 	if (e->type() == QEvent::TouchBegin
1011 		|| e->type() == QEvent::TouchUpdate
1012 		|| e->type() == QEvent::TouchEnd
1013 		|| e->type() == QEvent::TouchCancel) {
1014 		const auto ev = static_cast<QTouchEvent*>(e);
1015 		if (ev->device()->type() == base::TouchDevice::TouchScreen) {
1016 			touchEvent(ev);
1017 		}
1018 	}
1019 	return Parent::eventHook(e);
1020 }
1021 
touchEvent(QTouchEvent * e)1022 void FlatInput::touchEvent(QTouchEvent *e) {
1023 	switch (e->type()) {
1024 	case QEvent::TouchBegin: {
1025 		if (_touchPress || e->touchPoints().isEmpty()) return;
1026 		_touchTimer.start(QApplication::startDragTime());
1027 		_touchPress = true;
1028 		_touchMove = _touchRightButton = false;
1029 		_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
1030 	} break;
1031 
1032 	case QEvent::TouchUpdate: {
1033 		if (!_touchPress || e->touchPoints().isEmpty()) return;
1034 		if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
1035 			_touchMove = true;
1036 		}
1037 	} break;
1038 
1039 	case QEvent::TouchEnd: {
1040 		if (!_touchPress) return;
1041 		auto weak = MakeWeak(this);
1042 		if (!_touchMove && window()) {
1043 			QPoint mapped(mapFromGlobal(_touchStart));
1044 
1045 			if (_touchRightButton) {
1046 				QContextMenuEvent contextEvent(QContextMenuEvent::Mouse, mapped, _touchStart);
1047 				contextMenuEvent(&contextEvent);
1048 			} else {
1049 				QGuiApplication::inputMethod()->show();
1050 			}
1051 		}
1052 		if (weak) {
1053 			_touchTimer.stop();
1054 			_touchPress = _touchMove = _touchRightButton = false;
1055 		}
1056 	} break;
1057 
1058 	case QEvent::TouchCancel: {
1059 		_touchPress = false;
1060 		_touchTimer.stop();
1061 	} break;
1062 	}
1063 }
1064 
setTextMrg(const QMargins & textMrg)1065 void FlatInput::setTextMrg(const QMargins &textMrg) {
1066 	_textMrg = textMrg;
1067 	refreshPlaceholder(_placeholderFull.current());
1068 	update();
1069 }
1070 
getTextRect() const1071 QRect FlatInput::getTextRect() const {
1072 	return rect().marginsRemoved(_textMrg + QMargins(-2, -1, -2, -1));
1073 }
1074 
finishAnimations()1075 void FlatInput::finishAnimations() {
1076 	_placeholderFocusedAnimation.stop();
1077 	_placeholderVisibleAnimation.stop();
1078 }
1079 
paintEvent(QPaintEvent * e)1080 void FlatInput::paintEvent(QPaintEvent *e) {
1081 	Painter p(this);
1082 
1083 	auto placeholderFocused = _placeholderFocusedAnimation.value(_focused ? 1. : 0.);
1084 	auto pen = anim::pen(_st.borderColor, _st.borderActive, placeholderFocused);
1085 	pen.setWidth(_st.borderWidth);
1086 	p.setPen(pen);
1087 	p.setBrush(anim::brush(_st.bgColor, _st.bgActive, placeholderFocused));
1088 	{
1089 		PainterHighQualityEnabler hq(p);
1090 		p.drawRoundedRect(QRectF(0, 0, width(), height()).marginsRemoved(QMarginsF(_st.borderWidth / 2., _st.borderWidth / 2., _st.borderWidth / 2., _st.borderWidth / 2.)), st::roundRadiusSmall - (_st.borderWidth / 2.), st::roundRadiusSmall - (_st.borderWidth / 2.));
1091 	}
1092 
1093 	if (!_st.icon.empty()) {
1094 		_st.icon.paint(p, 0, 0, width());
1095 	}
1096 
1097 	const auto placeholderOpacity = _placeholderVisibleAnimation.value(
1098 		_placeholderVisible ? 1. : 0.);
1099 	if (placeholderOpacity > 0.) {
1100 		p.setOpacity(placeholderOpacity);
1101 
1102 		auto left = anim::interpolate(_st.phShift, 0, placeholderOpacity);
1103 
1104 		p.save();
1105 		p.setClipRect(rect());
1106 		QRect phRect(placeholderRect());
1107 		phRect.moveLeft(phRect.left() + left);
1108 		phPrepare(p, placeholderFocused);
1109 		p.drawText(phRect, _placeholder, QTextOption(_st.phAlign));
1110 		p.restore();
1111 	}
1112 	QLineEdit::paintEvent(e);
1113 }
1114 
focusInEvent(QFocusEvent * e)1115 void FlatInput::focusInEvent(QFocusEvent *e) {
1116 	if (!_focused) {
1117 		_focused = true;
1118 		_placeholderFocusedAnimation.start(
1119 			[=] { update(); },
1120 			0.,
1121 			1.,
1122 			_st.phDuration);
1123 		update();
1124 	}
1125 	QLineEdit::focusInEvent(e);
1126 	focused();
1127 }
1128 
focusOutEvent(QFocusEvent * e)1129 void FlatInput::focusOutEvent(QFocusEvent *e) {
1130 	if (_focused) {
1131 		_focused = false;
1132 		_placeholderFocusedAnimation.start(
1133 			[=] { update(); },
1134 			1.,
1135 			0.,
1136 			_st.phDuration);
1137 		update();
1138 	}
1139 	QLineEdit::focusOutEvent(e);
1140 	blurred();
1141 }
1142 
resizeEvent(QResizeEvent * e)1143 void FlatInput::resizeEvent(QResizeEvent *e) {
1144 	refreshPlaceholder(_placeholderFull.current());
1145 	return QLineEdit::resizeEvent(e);
1146 }
1147 
setPlaceholder(rpl::producer<QString> placeholder)1148 void FlatInput::setPlaceholder(rpl::producer<QString> placeholder) {
1149 	_placeholderFull = std::move(placeholder);
1150 }
1151 
refreshPlaceholder(const QString & text)1152 void FlatInput::refreshPlaceholder(const QString &text) {
1153 	const auto availw = width() - _textMrg.left() - _textMrg.right() - _st.phPos.x() - 1;
1154 	if (_st.font->width(text) > availw) {
1155 		_placeholder = _st.font->elided(text, availw);
1156 	} else {
1157 		_placeholder = text;
1158 	}
1159 	update();
1160 }
1161 
contextMenuEvent(QContextMenuEvent * e)1162 void FlatInput::contextMenuEvent(QContextMenuEvent *e) {
1163 	if (auto menu = createStandardContextMenu()) {
1164 		(new PopupMenu(this, menu))->popup(e->globalPos());
1165 	}
1166 }
1167 
sizeHint() const1168 QSize FlatInput::sizeHint() const {
1169 	return geometry().size();
1170 }
1171 
minimumSizeHint() const1172 QSize FlatInput::minimumSizeHint() const {
1173 	return geometry().size();
1174 }
1175 
updatePlaceholder()1176 void FlatInput::updatePlaceholder() {
1177 	auto hasText = !text().isEmpty();
1178 	if (!hasText) {
1179 		hasText = _lastPreEditTextNotEmpty;
1180 	} else {
1181 		_lastPreEditTextNotEmpty = false;
1182 	}
1183 	auto placeholderVisible = !hasText;
1184 	if (_placeholderVisible != placeholderVisible) {
1185 		_placeholderVisible = placeholderVisible;
1186 		_placeholderVisibleAnimation.start(
1187 			[=] { update(); },
1188 			_placeholderVisible ? 0. : 1.,
1189 			_placeholderVisible ? 1. : 0.,
1190 			_st.phDuration);
1191 	}
1192 }
1193 
inputMethodEvent(QInputMethodEvent * e)1194 void FlatInput::inputMethodEvent(QInputMethodEvent *e) {
1195 	QLineEdit::inputMethodEvent(e);
1196 	auto lastPreEditTextNotEmpty = !e->preeditString().isEmpty();
1197 	if (_lastPreEditTextNotEmpty != lastPreEditTextNotEmpty) {
1198 		_lastPreEditTextNotEmpty = lastPreEditTextNotEmpty;
1199 		updatePlaceholder();
1200 	}
1201 }
1202 
placeholderRect() const1203 QRect FlatInput::placeholderRect() const {
1204 	return QRect(_textMrg.left() + _st.phPos.x(), _textMrg.top() + _st.phPos.y(), width() - _textMrg.left() - _textMrg.right(), height() - _textMrg.top() - _textMrg.bottom());
1205 }
1206 
correctValue(const QString & was,QString & now)1207 void FlatInput::correctValue(const QString &was, QString &now) {
1208 }
1209 
phPrepare(QPainter & p,float64 placeholderFocused)1210 void FlatInput::phPrepare(QPainter &p, float64 placeholderFocused) {
1211 	p.setFont(_st.font);
1212 	p.setPen(anim::pen(_st.phColor, _st.phFocusColor, placeholderFocused));
1213 }
1214 
keyPressEvent(QKeyEvent * e)1215 void FlatInput::keyPressEvent(QKeyEvent *e) {
1216 	QString wasText(_oldtext);
1217 
1218 	if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) {
1219 		e->ignore();
1220 	} else {
1221 		QLineEdit::keyPressEvent(e);
1222 	}
1223 
1224 	QString newText(text());
1225 	if (wasText == newText) { // call correct manually
1226 		correctValue(wasText, newText);
1227 		_oldtext = newText;
1228 		if (wasText != _oldtext) changed();
1229 		updatePlaceholder();
1230 	}
1231 	if (e->key() == Qt::Key_Escape) {
1232 		cancelled();
1233 	} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
1234 		submitted(e->modifiers());
1235 #ifdef Q_OS_MAC
1236 	} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
1237 		auto selected = selectedText();
1238 		if (!selected.isEmpty() && echoMode() == QLineEdit::Normal) {
1239 			QGuiApplication::clipboard()->setText(selected, QClipboard::FindBuffer);
1240 		}
1241 #endif // Q_OS_MAC
1242 	}
1243 }
1244 
onTextEdited()1245 void FlatInput::onTextEdited() {
1246 	QString wasText(_oldtext), newText(text());
1247 
1248 	correctValue(wasText, newText);
1249 	_oldtext = newText;
1250 	if (wasText != _oldtext) changed();
1251 	updatePlaceholder();
1252 
1253 	Integration::Instance().textActionsUpdated();
1254 }
1255 
onTextChange(const QString & text)1256 void FlatInput::onTextChange(const QString &text) {
1257 	_oldtext = text;
1258 	Integration::Instance().textActionsUpdated();
1259 }
1260 
InputField(QWidget * parent,const style::InputField & st,rpl::producer<QString> placeholder,const QString & value)1261 InputField::InputField(
1262 	QWidget *parent,
1263 	const style::InputField &st,
1264 	rpl::producer<QString> placeholder,
1265 	const QString &value)
1266 : InputField(
1267 	parent,
1268 	st,
1269 	Mode::SingleLine,
1270 	std::move(placeholder),
1271 	{ value, {} }) {
1272 }
1273 
InputField(QWidget * parent,const style::InputField & st,Mode mode,rpl::producer<QString> placeholder,const QString & value)1274 InputField::InputField(
1275 	QWidget *parent,
1276 	const style::InputField &st,
1277 	Mode mode,
1278 	rpl::producer<QString> placeholder,
1279 	const QString &value)
1280 : InputField(
1281 	parent,
1282 	st,
1283 	mode,
1284 	std::move(placeholder),
1285 	{ value, {} }) {
1286 }
1287 
InputField(QWidget * parent,const style::InputField & st,Mode mode,rpl::producer<QString> placeholder,const TextWithTags & value)1288 InputField::InputField(
1289 	QWidget *parent,
1290 	const style::InputField &st,
1291 	Mode mode,
1292 	rpl::producer<QString> placeholder,
1293 	const TextWithTags &value)
1294 : RpWidget(parent)
1295 , _st(st)
1296 , _mode(mode)
1297 , _minHeight(st.heightMin)
1298 , _maxHeight(st.heightMax)
1299 , _inner(std::make_unique<Inner>(this))
1300 , _lastTextWithTags(value)
1301 , _placeholderFull(std::move(placeholder)) {
1302 	_inner->setDocument(CreateChild<InputDocument>(_inner.get(), _st));
1303 	_inner->setAcceptRichText(false);
1304 	resize(_st.width, _minHeight);
1305 
1306 	if (_st.textBg->c.alphaF() >= 1.) {
1307 		setAttribute(Qt::WA_OpaquePaintEvent);
1308 	}
1309 
1310 	_inner->setFont(_st.font->f);
1311 	_inner->setAlignment(_st.textAlign);
1312 	if (_mode == Mode::SingleLine) {
1313 		_inner->setWordWrapMode(QTextOption::NoWrap);
1314 	}
1315 
1316 	_placeholderFull.value(
1317 	) | rpl::start_with_next([=](const QString &text) {
1318 		refreshPlaceholder(text);
1319 	}, lifetime());
1320 
1321 	style::PaletteChanged(
1322 	) | rpl::start_with_next([=] {
1323 		updatePalette();
1324 	}, lifetime());
1325 
1326 	_defaultCharFormat = _inner->textCursor().charFormat();
1327 	updatePalette();
1328 	_inner->textCursor().setCharFormat(_defaultCharFormat);
1329 
1330 	_inner->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1331 	_inner->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1332 
1333 	_inner->setFrameStyle(int(QFrame::NoFrame) | QFrame::Plain);
1334 	_inner->viewport()->setAutoFillBackground(false);
1335 
1336 	_inner->setContentsMargins(0, 0, 0, 0);
1337 	_inner->document()->setDocumentMargin(0);
1338 
1339 	setAttribute(Qt::WA_AcceptTouchEvents);
1340 	_inner->viewport()->setAttribute(Qt::WA_AcceptTouchEvents);
1341 	_touchTimer.setSingleShot(true);
1342 	connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
1343 
1344 	connect(_inner->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(onDocumentContentsChange(int,int,int)));
1345 	connect(_inner.get(), SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool)));
1346 	connect(_inner.get(), SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool)));
1347 	connect(_inner.get(), SIGNAL(cursorPositionChanged()), this, SLOT(onCursorPositionChanged()));
1348 	connect(_inner.get(), &Inner::selectionChanged, [] {
1349 		Integration::Instance().textActionsUpdated();
1350 	});
1351 
1352 	const auto bar = _inner->verticalScrollBar();
1353 	_scrollTop = bar->value();
1354 	connect(bar, &QScrollBar::valueChanged, [=] {
1355 		_scrollTop = bar->value();
1356 	});
1357 
1358 	setCursor(style::cur_text);
1359 	heightAutoupdated();
1360 
1361 	if (!_lastTextWithTags.text.isEmpty()) {
1362 		setTextWithTags(_lastTextWithTags, HistoryAction::Clear);
1363 	}
1364 
1365 	startBorderAnimation();
1366 	startPlaceholderAnimation();
1367 	finishAnimating();
1368 }
1369 
scrollTop() const1370 const rpl::variable<int> &InputField::scrollTop() const {
1371 	return _scrollTop;
1372 }
1373 
scrollTopMax() const1374 int InputField::scrollTopMax() const {
1375 	return _inner->verticalScrollBar()->maximum();
1376 }
1377 
scrollTo(int top)1378 void InputField::scrollTo(int top) {
1379 	_inner->verticalScrollBar()->setValue(top);
1380 }
1381 
viewportEventInner(QEvent * e)1382 bool InputField::viewportEventInner(QEvent *e) {
1383 	if (e->type() == QEvent::TouchBegin
1384 		|| e->type() == QEvent::TouchUpdate
1385 		|| e->type() == QEvent::TouchEnd
1386 		|| e->type() == QEvent::TouchCancel) {
1387 		const auto ev = static_cast<QTouchEvent*>(e);
1388 		if (ev->device()->type() == base::TouchDevice::TouchScreen) {
1389 			handleTouchEvent(ev);
1390 		}
1391 	}
1392 	return _inner->QTextEdit::viewportEvent(e);
1393 }
1394 
updatePalette()1395 void InputField::updatePalette() {
1396 	auto p = _inner->palette();
1397 	p.setColor(QPalette::Text, _st.textFg->c);
1398 	p.setColor(QPalette::Highlight, st::msgInBgSelected->c);
1399 	p.setColor(QPalette::HighlightedText, st::historyTextInFgSelected->c);
1400 	_inner->setPalette(p);
1401 
1402 	_defaultCharFormat.merge(PrepareTagFormat(_st, QString()));
1403 	auto cursor = textCursor();
1404 
1405 	const auto document = _inner->document();
1406 	auto block = document->begin();
1407 	const auto end = document->end();
1408 	for (; block != end; block = block.next()) {
1409 		auto till = block.position();
1410 		for (auto i = block.begin(); !i.atEnd();) {
1411 			for (; !i.atEnd(); ++i) {
1412 				const auto fragment = i.fragment();
1413 				if (!fragment.isValid() || fragment.position() < till) {
1414 					continue;
1415 				}
1416 				till = fragment.position() + fragment.length();
1417 
1418 				auto format = fragment.charFormat();
1419 				const auto tag = format.property(kTagProperty).toString();
1420 				format.setForeground(PrepareTagFormat(_st, tag).foreground());
1421 				cursor.setPosition(fragment.position());
1422 				cursor.setPosition(till, QTextCursor::KeepAnchor);
1423 				cursor.mergeCharFormat(format);
1424 				i = block.begin();
1425 				break;
1426 			}
1427 		}
1428 	}
1429 
1430 	cursor = textCursor();
1431 	if (!cursor.hasSelection()) {
1432 		auto format = cursor.charFormat();
1433 		format.merge(PrepareTagFormat(
1434 			_st,
1435 			format.property(kTagProperty).toString()));
1436 		cursor.setCharFormat(format);
1437 		setTextCursor(cursor);
1438 	}
1439 }
1440 
onTouchTimer()1441 void InputField::onTouchTimer() {
1442 	_touchRightButton = true;
1443 }
1444 
setExtendedContextMenu(rpl::producer<ExtendedContextMenu> value)1445 void InputField::setExtendedContextMenu(
1446 	rpl::producer<ExtendedContextMenu> value) {
1447 	std::move(
1448 		value
1449 	) | rpl::start_with_next([=](auto pair) {
1450 		auto &[menu, e] = pair;
1451 		contextMenuEventInner(e.get(), std::move(menu));
1452 	}, lifetime());
1453 }
1454 
setInstantReplaces(const InstantReplaces & replaces)1455 void InputField::setInstantReplaces(const InstantReplaces &replaces) {
1456 	_mutableInstantReplaces = replaces;
1457 }
1458 
setInstantReplacesEnabled(rpl::producer<bool> enabled)1459 void InputField::setInstantReplacesEnabled(rpl::producer<bool> enabled) {
1460 	std::move(
1461 		enabled
1462 	) | rpl::start_with_next([=](bool value) {
1463 		_instantReplacesEnabled = value;
1464 	}, lifetime());
1465 }
1466 
setMarkdownReplacesEnabled(rpl::producer<bool> enabled)1467 void InputField::setMarkdownReplacesEnabled(rpl::producer<bool> enabled) {
1468 	std::move(
1469 		enabled
1470 	) | rpl::start_with_next([=](bool value) {
1471 		if (_markdownEnabled != value) {
1472 			_markdownEnabled = value;
1473 			if (_markdownEnabled) {
1474 				handleContentsChanged();
1475 			} else {
1476 				_lastMarkdownTags = {};
1477 			}
1478 		}
1479 	}, lifetime());
1480 }
1481 
setTagMimeProcessor(std::unique_ptr<TagMimeProcessor> && processor)1482 void InputField::setTagMimeProcessor(
1483 		std::unique_ptr<TagMimeProcessor> &&processor) {
1484 	_tagMimeProcessor = std::move(processor);
1485 }
1486 
setAdditionalMargin(int margin)1487 void InputField::setAdditionalMargin(int margin) {
1488 	_inner->setStyleSheet(
1489 		QString::fromLatin1("QTextEdit { margin: %1px; }").arg(margin));
1490 	_additionalMargin = margin;
1491 	checkContentHeight();
1492 }
1493 
setMaxLength(int length)1494 void InputField::setMaxLength(int length) {
1495 	if (_maxLength != length) {
1496 		_maxLength = length;
1497 		if (_maxLength > 0) {
1498 			const auto document = _inner->document();
1499 			_correcting = true;
1500 			QTextCursor(document).joinPreviousEditBlock();
1501 			const auto guard = gsl::finally([&] {
1502 				_correcting = false;
1503 				QTextCursor(document).endEditBlock();
1504 				handleContentsChanged();
1505 			});
1506 
1507 			auto cursor = QTextCursor(document);
1508 			cursor.movePosition(QTextCursor::End);
1509 			chopByMaxLength(0, cursor.position());
1510 		}
1511 	}
1512 }
1513 
setMinHeight(int height)1514 void InputField::setMinHeight(int height) {
1515 	_minHeight = height;
1516 }
1517 
setMaxHeight(int height)1518 void InputField::setMaxHeight(int height) {
1519 	_maxHeight = height;
1520 }
1521 
insertTag(const QString & text,QString tagId)1522 void InputField::insertTag(const QString &text, QString tagId) {
1523 	auto cursor = textCursor();
1524 	const auto position = cursor.position();
1525 
1526 	const auto document = _inner->document();
1527 	auto block = document->findBlock(position);
1528 	for (auto iter = block.begin(); !iter.atEnd(); ++iter) {
1529 		auto fragment = iter.fragment();
1530 		Assert(fragment.isValid());
1531 
1532 		const auto fragmentPosition = fragment.position();
1533 		const auto fragmentEnd = (fragmentPosition + fragment.length());
1534 		if (fragmentPosition >= position || fragmentEnd < position) {
1535 			continue;
1536 		}
1537 
1538 		const auto format = fragment.charFormat();
1539 		if (format.isImageFormat()) {
1540 			continue;
1541 		}
1542 
1543 		auto mentionInCommand = false;
1544 		const auto fragmentText = fragment.text();
1545 		for (auto i = position - fragmentPosition; i > 0; --i) {
1546 			const auto previous = fragmentText[i - 1];
1547 			if (previous == '@' || previous == '#' || previous == '/') {
1548 				if ((i == position - fragmentPosition
1549 					|| (previous == '/'
1550 						? fragmentText[i].isLetterOrNumber()
1551 						: fragmentText[i].isLetter())
1552 					|| previous == '#') &&
1553 					(i < 2 || !(fragmentText[i - 2].isLetterOrNumber()
1554 						|| fragmentText[i - 2] == '_'))) {
1555 					cursor.setPosition(fragmentPosition + i - 1);
1556 					auto till = fragmentPosition + i;
1557 					for (; (till < fragmentEnd && till < position); ++till) {
1558 						const auto ch = fragmentText[till - fragmentPosition];
1559 						if (!ch.isLetterOrNumber() && ch != '_' && ch != '@') {
1560 							break;
1561 						}
1562 					}
1563 					if (till < fragmentEnd
1564 						&& fragmentText[till - fragmentPosition] == ' ') {
1565 						++till;
1566 					}
1567 					cursor.setPosition(till, QTextCursor::KeepAnchor);
1568 					break;
1569 				} else if ((i == position - fragmentPosition
1570 					|| fragmentText[i].isLetter())
1571 					&& fragmentText[i - 1] == '@'
1572 					&& (i > 2)
1573 					&& (fragmentText[i - 2].isLetterOrNumber()
1574 						|| fragmentText[i - 2] == '_')
1575 					&& !mentionInCommand) {
1576 					mentionInCommand = true;
1577 					--i;
1578 					continue;
1579 				}
1580 				break;
1581 			}
1582 			if (position - fragmentPosition - i > 127
1583 				|| (!mentionInCommand
1584 					&& (position - fragmentPosition - i > 63))
1585 				|| (!fragmentText[i - 1].isLetterOrNumber()
1586 					&& fragmentText[i - 1] != '_')) {
1587 				break;
1588 			}
1589 		}
1590 		break;
1591 	}
1592 	if (tagId.isEmpty()) {
1593 		cursor.insertText(text + ' ', _defaultCharFormat);
1594 	} else {
1595 		_insertedTags.clear();
1596 		_insertedTags.push_back({ 0, int(text.size()), tagId });
1597 		_insertedTagsAreFromMime = false;
1598 		cursor.insertText(text + ' ');
1599 		_insertedTags.clear();
1600 	}
1601 }
1602 
heightAutoupdated()1603 bool InputField::heightAutoupdated() {
1604 	if (_minHeight < 0
1605 		|| _maxHeight < 0
1606 		|| _inHeightCheck
1607 		|| _mode == Mode::SingleLine) {
1608 		return false;
1609 	}
1610 	_inHeightCheck = true;
1611 	const auto guard = gsl::finally([&] { _inHeightCheck = false; });
1612 
1613 	SendPendingMoveResizeEvents(this);
1614 
1615 	const auto contentHeight = int(std::ceil(document()->size().height()))
1616 		+ _st.textMargins.top()
1617 		+ _st.textMargins.bottom()
1618 		+ 2 * _additionalMargin;
1619 	const auto newHeight = std::clamp(contentHeight, _minHeight, _maxHeight);
1620 	if (height() != newHeight) {
1621 		resize(width(), newHeight);
1622 		return true;
1623 	}
1624 	return false;
1625 }
1626 
checkContentHeight()1627 void InputField::checkContentHeight() {
1628 	if (heightAutoupdated()) {
1629 		resized();
1630 	}
1631 }
1632 
handleTouchEvent(QTouchEvent * e)1633 void InputField::handleTouchEvent(QTouchEvent *e) {
1634 	switch (e->type()) {
1635 	case QEvent::TouchBegin: {
1636 		if (_touchPress || e->touchPoints().isEmpty()) return;
1637 		_touchTimer.start(QApplication::startDragTime());
1638 		_touchPress = true;
1639 		_touchMove = _touchRightButton = false;
1640 		_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
1641 	} break;
1642 
1643 	case QEvent::TouchUpdate: {
1644 		if (!_touchPress || e->touchPoints().isEmpty()) return;
1645 		if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
1646 			_touchMove = true;
1647 		}
1648 	} break;
1649 
1650 	case QEvent::TouchEnd: {
1651 		if (!_touchPress) return;
1652 		auto weak = MakeWeak(this);
1653 		if (!_touchMove && window()) {
1654 			QPoint mapped(mapFromGlobal(_touchStart));
1655 
1656 			if (_touchRightButton) {
1657 				QContextMenuEvent contextEvent(QContextMenuEvent::Mouse, mapped, _touchStart);
1658 				contextMenuEvent(&contextEvent);
1659 			} else {
1660 				QGuiApplication::inputMethod()->show();
1661 			}
1662 		}
1663 		if (weak) {
1664 			_touchTimer.stop();
1665 			_touchPress = _touchMove = _touchRightButton = false;
1666 		}
1667 	} break;
1668 
1669 	case QEvent::TouchCancel: {
1670 		_touchPress = false;
1671 		_touchTimer.stop();
1672 	} break;
1673 	}
1674 }
1675 
paintEvent(QPaintEvent * e)1676 void InputField::paintEvent(QPaintEvent *e) {
1677 	Painter p(this);
1678 
1679 	auto r = rect().intersected(e->rect());
1680 	if (_st.textBg->c.alphaF() > 0.) {
1681 		p.fillRect(r, _st.textBg);
1682 	}
1683 	if (_st.border) {
1684 		p.fillRect(0, height() - _st.border, width(), _st.border, _st.borderFg);
1685 	}
1686 	auto errorDegree = _a_error.value(_error ? 1. : 0.);
1687 	auto focusedDegree = _a_focused.value(_focused ? 1. : 0.);
1688 	auto borderShownDegree = _a_borderShown.value(1.);
1689 	auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.);
1690 	if (_st.borderActive && (borderOpacity > 0.)) {
1691 		auto borderStart = std::clamp(_borderAnimationStart, 0, width());
1692 		auto borderFrom = qRound(borderStart * (1. - borderShownDegree));
1693 		auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree);
1694 		if (borderTo > borderFrom) {
1695 			auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree);
1696 			p.setOpacity(borderOpacity);
1697 			p.fillRect(borderFrom, height() - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg);
1698 			p.setOpacity(1);
1699 		}
1700 	}
1701 
1702 	if (_st.placeholderScale > 0. && !_placeholderPath.isEmpty()) {
1703 		auto placeholderShiftDegree = _a_placeholderShifted.value(_placeholderShifted ? 1. : 0.);
1704 		p.save();
1705 		p.setClipRect(r);
1706 
1707 		auto placeholderTop = anim::interpolate(0, _st.placeholderShift, placeholderShiftDegree);
1708 
1709 		QRect r(rect().marginsRemoved(_st.textMargins + _st.placeholderMargins));
1710 		r.moveTop(r.top() + placeholderTop);
1711 		if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width());
1712 
1713 		auto placeholderScale = 1. - (1. - _st.placeholderScale) * placeholderShiftDegree;
1714 		auto placeholderFg = anim::color(_st.placeholderFg, _st.placeholderFgActive, focusedDegree);
1715 		placeholderFg = anim::color(placeholderFg, _st.placeholderFgError, errorDegree);
1716 
1717 		PainterHighQualityEnabler hq(p);
1718 		p.setPen(Qt::NoPen);
1719 		p.setBrush(placeholderFg);
1720 		p.translate(r.topLeft());
1721 		p.scale(placeholderScale, placeholderScale);
1722 		p.drawPath(_placeholderPath);
1723 
1724 		p.restore();
1725 	} else if (!_placeholder.isEmpty()) {
1726 		const auto placeholderHiddenDegree = _a_placeholderShifted.value(_placeholderShifted ? 1. : 0.);
1727 		if (placeholderHiddenDegree < 1.) {
1728 			p.setOpacity(1. - placeholderHiddenDegree);
1729 			p.save();
1730 			p.setClipRect(r);
1731 
1732 			const auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree);
1733 
1734 			p.setFont(_st.placeholderFont);
1735 			p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree));
1736 
1737 			if (_st.placeholderAlign == style::al_topleft && _placeholderAfterSymbols > 0) {
1738 				const auto skipWidth = placeholderSkipWidth();
1739 				p.drawText(
1740 					_st.textMargins.left() + _st.placeholderMargins.left() + skipWidth,
1741 					_st.textMargins.top() + _st.placeholderMargins.top() + _st.placeholderFont->ascent,
1742 					_placeholder);
1743 			} else {
1744 				auto r = rect().marginsRemoved(_st.textMargins + _st.placeholderMargins);
1745 				r.moveLeft(r.left() + placeholderLeft);
1746 				if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width());
1747 				p.drawText(r, _placeholder, _st.placeholderAlign);
1748 			}
1749 
1750 			p.restore();
1751 		}
1752 	}
1753 	RpWidget::paintEvent(e);
1754 }
1755 
placeholderSkipWidth() const1756 int InputField::placeholderSkipWidth() const {
1757 	if (!_placeholderAfterSymbols) {
1758 		return 0;
1759 	}
1760 	const auto &text = getTextWithTags().text;
1761 	auto result = _st.font->width(text.mid(0, _placeholderAfterSymbols));
1762 	if (_placeholderAfterSymbols > text.size()) {
1763 		result += _st.font->spacew;
1764 	}
1765 	return result;
1766 }
1767 
startBorderAnimation()1768 void InputField::startBorderAnimation() {
1769 	auto borderVisible = (_error || _focused);
1770 	if (_borderVisible != borderVisible) {
1771 		_borderVisible = borderVisible;
1772 		if (_borderVisible) {
1773 			if (_a_borderOpacity.animating()) {
1774 				_a_borderOpacity.start([this] { update(); }, 0., 1., _st.duration);
1775 			} else {
1776 				_a_borderShown.start([this] { update(); }, 0., 1., _st.duration);
1777 			}
1778 		} else {
1779 			_a_borderOpacity.start([this] { update(); }, 1., 0., _st.duration);
1780 		}
1781 	}
1782 }
1783 
focusInEvent(QFocusEvent * e)1784 void InputField::focusInEvent(QFocusEvent *e) {
1785 	_borderAnimationStart = (e->reason() == Qt::MouseFocusReason)
1786 		? mapFromGlobal(QCursor::pos()).x()
1787 		: (width() / 2);
1788 	InvokeQueued(this, [=] { onFocusInner(); });
1789 }
1790 
mousePressEvent(QMouseEvent * e)1791 void InputField::mousePressEvent(QMouseEvent *e) {
1792 	_borderAnimationStart = e->pos().x();
1793 	InvokeQueued(this, [=] { onFocusInner(); });
1794 }
1795 
onFocusInner()1796 void InputField::onFocusInner() {
1797 	auto borderStart = _borderAnimationStart;
1798 	_inner->setFocus();
1799 	_borderAnimationStart = borderStart;
1800 }
1801 
borderAnimationStart() const1802 int InputField::borderAnimationStart() const {
1803 	return _borderAnimationStart;
1804 }
1805 
contextMenuEvent(QContextMenuEvent * e)1806 void InputField::contextMenuEvent(QContextMenuEvent *e) {
1807 	_inner->contextMenuEvent(e);
1808 }
1809 
focusInEventInner(QFocusEvent * e)1810 void InputField::focusInEventInner(QFocusEvent *e) {
1811 	_borderAnimationStart = (e->reason() == Qt::MouseFocusReason)
1812 		? mapFromGlobal(QCursor::pos()).x()
1813 		: (width() / 2);
1814 	setFocused(true);
1815 	_inner->QTextEdit::focusInEvent(e);
1816 	focused();
1817 }
1818 
focusOutEventInner(QFocusEvent * e)1819 void InputField::focusOutEventInner(QFocusEvent *e) {
1820 	setFocused(false);
1821 	_inner->QTextEdit::focusOutEvent(e);
1822 	blurred();
1823 }
1824 
setFocused(bool focused)1825 void InputField::setFocused(bool focused) {
1826 	if (_focused != focused) {
1827 		_focused = focused;
1828 		_a_focused.start([this] { update(); }, _focused ? 0. : 1., _focused ? 1. : 0., _st.duration);
1829 		startPlaceholderAnimation();
1830 		startBorderAnimation();
1831 	}
1832 }
1833 
sizeHint() const1834 QSize InputField::sizeHint() const {
1835 	return geometry().size();
1836 }
1837 
minimumSizeHint() const1838 QSize InputField::minimumSizeHint() const {
1839 	return geometry().size();
1840 }
1841 
hasText() const1842 bool InputField::hasText() const {
1843 	const auto document = _inner->document();
1844 	const auto from = document->begin();
1845 	const auto till = document->end();
1846 
1847 	if (from == till) {
1848 		return false;
1849 	}
1850 
1851 	for (auto item = from.begin(); !item.atEnd(); ++item) {
1852 		const auto fragment = item.fragment();
1853 		if (!fragment.isValid()) {
1854 			continue;
1855 		} else if (!fragment.text().isEmpty()) {
1856 			return true;
1857 		}
1858 	}
1859 	return (from.next() != till);
1860 }
1861 
getTextPart(int start,int end,TagList & outTagsList,bool & outTagsChanged,std::vector<MarkdownTag> * outMarkdownTags) const1862 QString InputField::getTextPart(
1863 		int start,
1864 		int end,
1865 		TagList &outTagsList,
1866 		bool &outTagsChanged,
1867 		std::vector<MarkdownTag> *outMarkdownTags) const {
1868 	Expects((start == 0 && end < 0) || outMarkdownTags == nullptr);
1869 
1870 	if (end >= 0 && end <= start) {
1871 		outTagsChanged = !outTagsList.isEmpty();
1872 		outTagsList.clear();
1873 		return QString();
1874 	}
1875 
1876 	if (start < 0) {
1877 		start = 0;
1878 	}
1879 	const auto full = (start == 0 && end < 0);
1880 
1881 	auto lastTag = QString();
1882 	TagAccumulator tagAccumulator(outTagsList);
1883 	MarkdownTagAccumulator markdownTagAccumulator(outMarkdownTags);
1884 	const auto newline = outMarkdownTags ? QString(1, '\n') : QString();
1885 
1886 	const auto document = _inner->document();
1887 	const auto from = full ? document->begin() : document->findBlock(start);
1888 	auto till = (end < 0) ? document->end() : document->findBlock(end);
1889 	if (till.isValid()) {
1890 		till = till.next();
1891 	}
1892 
1893 	auto possibleLength = 0;
1894 	for (auto block = from; block != till; block = block.next()) {
1895 		possibleLength += block.length();
1896 	}
1897 	auto result = QString();
1898 	result.reserve(possibleLength);
1899 	if (!full && end < 0) {
1900 		end = possibleLength;
1901 	}
1902 
1903 	for (auto block = from; block != till;) {
1904 		for (auto item = block.begin(); !item.atEnd(); ++item) {
1905 			const auto fragment = item.fragment();
1906 			if (!fragment.isValid()) {
1907 				continue;
1908 			}
1909 
1910 			const auto fragmentPosition = full ? 0 : fragment.position();
1911 			const auto fragmentEnd = full
1912 				? 0
1913 				: (fragmentPosition + fragment.length());
1914 			const auto format = fragment.charFormat();
1915 			if (!full) {
1916 				if (fragmentPosition == end) {
1917 					tagAccumulator.feed(
1918 						format.property(kTagProperty).toString(),
1919 						result.size());
1920 					break;
1921 				} else if (fragmentPosition > end) {
1922 					break;
1923 				} else if (fragmentEnd <= start) {
1924 					continue;
1925 				}
1926 			}
1927 
1928 			const auto emojiText = [&] {
1929 				if (format.isImageFormat()) {
1930 					const auto imageName = format.toImageFormat().name();
1931 					if (const auto emoji = Emoji::FromUrl(imageName)) {
1932 						return emoji->text();
1933 					}
1934 				}
1935 				return QString();
1936 			}();
1937 			auto text = [&] {
1938 				const auto result = fragment.text();
1939 				if (!full) {
1940 					if (fragmentPosition < start) {
1941 						return result.mid(start - fragmentPosition, end - start);
1942 					} else if (fragmentEnd > end) {
1943 						return result.mid(0, end - fragmentPosition);
1944 					}
1945 				}
1946 				return result;
1947 			}();
1948 
1949 			if (full || !text.isEmpty()) {
1950 				lastTag = format.property(kTagProperty).toString();
1951 				tagAccumulator.feed(lastTag, result.size());
1952 			}
1953 
1954 			auto begin = text.data();
1955 			auto ch = begin;
1956 			auto adjustedLength = text.size();
1957 			for (const auto end = begin + text.size(); ch != end; ++ch) {
1958 				if (IsNewline(*ch) && ch->unicode() != '\r') {
1959 					*ch = QLatin1Char('\n');
1960 				} else switch (ch->unicode()) {
1961 				case QChar::Nbsp: {
1962 					*ch = QLatin1Char(' ');
1963 				} break;
1964 				case QChar::ObjectReplacementCharacter: {
1965 					if (ch > begin) {
1966 						result.append(begin, ch - begin);
1967 					}
1968 					adjustedLength += (emojiText.size() - 1);
1969 					if (!emojiText.isEmpty()) {
1970 						result.append(emojiText);
1971 					}
1972 					begin = ch + 1;
1973 				} break;
1974 				}
1975 			}
1976 			if (ch > begin) {
1977 				result.append(begin, ch - begin);
1978 			}
1979 
1980 			if (full || !text.isEmpty()) {
1981 				markdownTagAccumulator.feed(text, adjustedLength, lastTag);
1982 			}
1983 		}
1984 
1985 		block = block.next();
1986 		if (block != till) {
1987 			result.append('\n');
1988 			markdownTagAccumulator.feed(newline, 1, lastTag);
1989 		}
1990 	}
1991 
1992 	tagAccumulator.feed(QString(), result.size());
1993 	tagAccumulator.finish();
1994 	markdownTagAccumulator.finish();
1995 
1996 	outTagsChanged = tagAccumulator.changed();
1997 	return result;
1998 }
1999 
isUndoAvailable() const2000 bool InputField::isUndoAvailable() const {
2001 	return _undoAvailable;
2002 }
2003 
isRedoAvailable() const2004 bool InputField::isRedoAvailable() const {
2005 	return _redoAvailable;
2006 }
2007 
processFormatting(int insertPosition,int insertEnd)2008 void InputField::processFormatting(int insertPosition, int insertEnd) {
2009 	// Tilde formatting.
2010 	const auto tildeFormatting = (_st.font->f.pixelSize() * style::DevicePixelRatio() == 13)
2011 		&& (_st.font->f.family() == qstr("DAOpenSansRegular"));
2012 	auto isTildeFragment = false;
2013 	auto tildeFixedFont = _st.font->semibold()->f;
2014 
2015 	// First tag handling (the one we inserted text to).
2016 	bool startTagFound = false;
2017 	bool breakTagOnNotLetter = false;
2018 
2019 	auto document = _inner->document();
2020 
2021 	// Apply inserted tags.
2022 	auto insertedTagsProcessor = _insertedTagsAreFromMime
2023 		? _tagMimeProcessor.get()
2024 		: nullptr;
2025 	const auto breakTagOnNotLetterTill = ProcessInsertedTags(
2026 		_st,
2027 		document,
2028 		insertPosition,
2029 		insertEnd,
2030 		_insertedTags,
2031 		insertedTagsProcessor);
2032 	using ActionType = FormattingAction::Type;
2033 	while (true) {
2034 		FormattingAction action;
2035 
2036 		auto checkedTill = insertPosition;
2037 		auto fromBlock = document->findBlock(insertPosition);
2038 		auto tillBlock = document->findBlock(insertEnd);
2039 		if (tillBlock.isValid()) tillBlock = tillBlock.next();
2040 
2041 		for (auto block = fromBlock; block != tillBlock; block = block.next()) {
2042 			for (auto fragmentIt = block.begin(); !fragmentIt.atEnd(); ++fragmentIt) {
2043 				auto fragment = fragmentIt.fragment();
2044 				Assert(fragment.isValid());
2045 
2046 				const auto fragmentPosition = fragment.position();
2047 				const auto fragmentEnd = fragmentPosition + fragment.length();
2048 				if (insertPosition > fragmentEnd) {
2049 					// In case insertPosition == fragmentEnd we still
2050 					// need to fill startTagFound / breakTagOnNotLetter.
2051 					// This can happen if we inserted a newline after
2052 					// a text fragment with some formatting tag, like Bold.
2053 					continue;
2054 				}
2055 				int changedPositionInFragment = insertPosition - fragmentPosition; // Can be negative.
2056 				int changedEndInFragment = insertEnd - fragmentPosition;
2057 				if (changedEndInFragment <= 0) {
2058 					break;
2059 				}
2060 
2061 				auto format = fragment.charFormat();
2062 				if (!format.hasProperty(kTagProperty)) {
2063 					action.type = ActionType::RemoveTag;
2064 					action.intervalStart = fragmentPosition;
2065 					action.intervalEnd = fragmentPosition + fragment.length();
2066 					break;
2067 				}
2068 				if (tildeFormatting) {
2069 					const auto formatFont = format.font();
2070 					if (!tildeFixedFont.styleName().isEmpty()
2071 						&& formatFont.styleName().isEmpty()) {
2072 						tildeFixedFont.setStyleName(QString());
2073 					}
2074 					isTildeFragment = (format.font() == tildeFixedFont);
2075 				}
2076 
2077 				auto fragmentText = fragment.text();
2078 				auto *textStart = fragmentText.constData();
2079 				auto *textEnd = textStart + fragmentText.size();
2080 
2081 				const auto with = format.property(kInstantReplaceWithId);
2082 				if (with.isValid()) {
2083 					const auto string = with.toString();
2084 					if (fragmentText != string) {
2085 						action.type = ActionType::ClearInstantReplace;
2086 						action.intervalStart = fragmentPosition
2087 							+ (fragmentText.startsWith(string)
2088 								? string.size()
2089 								: 0);
2090 						action.intervalEnd = fragmentPosition
2091 							+ fragmentText.size();
2092 						break;
2093 					}
2094 				}
2095 
2096 				if (!startTagFound) {
2097 					startTagFound = true;
2098 					auto tagName = format.property(kTagProperty).toString();
2099 					if (!tagName.isEmpty()) {
2100 						breakTagOnNotLetter = WasInsertTillTheEndOfTag(
2101 							block,
2102 							fragmentIt,
2103 							insertEnd);
2104 					}
2105 				}
2106 
2107 				auto *ch = textStart + qMax(changedPositionInFragment, 0);
2108 				for (; ch < textEnd; ++ch) {
2109 					const auto removeNewline = (_mode != Mode::MultiLine)
2110 						&& IsNewline(*ch);
2111 					if (removeNewline) {
2112 						if (action.type == ActionType::Invalid) {
2113 							action.type = ActionType::RemoveNewline;
2114 							action.intervalStart = fragmentPosition + (ch - textStart);
2115 							action.intervalEnd = action.intervalStart + 1;
2116 						}
2117 						break;
2118 					}
2119 
2120 					auto emojiLength = 0;
2121 					if (const auto emoji = Emoji::Find(ch, textEnd, &emojiLength)) {
2122 						// Replace emoji if no current action is prepared.
2123 						if (action.type == ActionType::Invalid) {
2124 							action.type = ActionType::InsertEmoji;
2125 							action.emoji = emoji;
2126 							action.intervalStart = fragmentPosition + (ch - textStart);
2127 							action.intervalEnd = action.intervalStart + emojiLength;
2128 						}
2129 						if (emojiLength > 1) {
2130 							_emojiSurrogateAmount += emojiLength - 1;
2131 						}
2132 						break;
2133 					}
2134 
2135 					if (breakTagOnNotLetter && !ch->isLetterOrNumber()) {
2136 						// Remove tag name till the end if no current action is prepared.
2137 						if (action.type != ActionType::Invalid) {
2138 							break;
2139 						}
2140 						breakTagOnNotLetter = false;
2141 						if (fragmentPosition + (ch - textStart) < breakTagOnNotLetterTill) {
2142 							action.type = ActionType::RemoveTag;
2143 							action.intervalStart = fragmentPosition + (ch - textStart);
2144 							action.intervalEnd = breakTagOnNotLetterTill;
2145 							break;
2146 						}
2147 					}
2148 					if (tildeFormatting) { // Tilde symbol fix in OpenSans.
2149 						bool tilde = (ch->unicode() == '~');
2150 						if ((tilde && !isTildeFragment) || (!tilde && isTildeFragment)) {
2151 							if (action.type == ActionType::Invalid) {
2152 								action.type = ActionType::TildeFont;
2153 								action.intervalStart = fragmentPosition + (ch - textStart);
2154 								action.intervalEnd = action.intervalStart + 1;
2155 								action.tildeTag = format.property(kTagProperty).toString();
2156 								action.isTilde = tilde;
2157 							} else {
2158 								++action.intervalEnd;
2159 							}
2160 						} else if (action.type == ActionType::TildeFont) {
2161 							break;
2162 						}
2163 					}
2164 
2165 					if (ch + 1 < textEnd && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) {
2166 						++ch;
2167 					}
2168 				}
2169 				if (action.type != ActionType::Invalid) {
2170 					break;
2171 				}
2172 				checkedTill = fragmentEnd;
2173 			}
2174 			if (action.type != ActionType::Invalid) {
2175 				break;
2176 			} else if (_mode != Mode::MultiLine
2177 				&& block.next() != document->end()) {
2178 				action.type = ActionType::RemoveNewline;
2179 				action.intervalStart = block.next().position() - 1;
2180 				action.intervalEnd = action.intervalStart + 1;
2181 				break;
2182 			} else if (breakTagOnNotLetter) {
2183 				// In case we need to break on not letter and we didn't
2184 				// find any non letter symbol, we found it here - a newline.
2185 				breakTagOnNotLetter = false;
2186 				if (checkedTill < breakTagOnNotLetterTill) {
2187 					action.type = ActionType::RemoveTag;
2188 					action.intervalStart = checkedTill;
2189 					action.intervalEnd = breakTagOnNotLetterTill;
2190 					break;
2191 				}
2192 			}
2193 		}
2194 		if (action.type != ActionType::Invalid) {
2195 			PrepareFormattingOptimization(document);
2196 
2197 			auto cursor = QTextCursor(document);
2198 			cursor.setPosition(action.intervalStart);
2199 			cursor.setPosition(action.intervalEnd, QTextCursor::KeepAnchor);
2200 			if (action.type == ActionType::InsertEmoji) {
2201 				InsertEmojiAtCursor(cursor, action.emoji);
2202 				insertPosition = action.intervalStart + 1;
2203 				if (insertEnd >= action.intervalEnd) {
2204 					insertEnd -= action.intervalEnd
2205 						- action.intervalStart
2206 						- 1;
2207 				}
2208 			} else if (action.type == ActionType::RemoveTag) {
2209 				RemoveDocumentTags(
2210 					_st,
2211 					document,
2212 					action.intervalStart,
2213 					action.intervalEnd);
2214 			} else if (action.type == ActionType::TildeFont) {
2215 				auto format = QTextCharFormat();
2216 				format.setFont(action.isTilde
2217 					? tildeFixedFont
2218 					: PrepareTagFormat(_st, action.tildeTag).font());
2219 				cursor.mergeCharFormat(format);
2220 				insertPosition = action.intervalEnd;
2221 			} else if (action.type == ActionType::ClearInstantReplace) {
2222 				auto format = _defaultCharFormat;
2223 				ApplyTagFormat(format, cursor.charFormat());
2224 				cursor.setCharFormat(format);
2225 			} else if (action.type == ActionType::RemoveNewline) {
2226 				cursor.removeSelectedText();
2227 				insertPosition = action.intervalStart;
2228 				if (insertEnd >= action.intervalEnd) {
2229 					insertEnd -= action.intervalEnd - action.intervalStart;
2230 				}
2231 			}
2232 		} else {
2233 			break;
2234 		}
2235 	}
2236 }
2237 
onDocumentContentsChange(int position,int charsRemoved,int charsAdded)2238 void InputField::onDocumentContentsChange(
2239 		int position,
2240 		int charsRemoved,
2241 		int charsAdded) {
2242 	if (_correcting) {
2243 		return;
2244 	}
2245 
2246 	// In case of input method events Qt emits
2247 	// document content change signals for a whole
2248 	// text block where the even took place.
2249 	// This breaks our wysiwyg markup, so we adjust
2250 	// the parameters to match the real change.
2251 	if (_inputMethodCommit.has_value()
2252 		&& charsAdded > _inputMethodCommit->size()
2253 		&& charsRemoved > 0) {
2254 		const auto inBlockBefore = charsAdded - _inputMethodCommit->size();
2255 		if (charsRemoved >= inBlockBefore) {
2256 			charsAdded -= inBlockBefore;
2257 			charsRemoved -= inBlockBefore;
2258 			position += inBlockBefore;
2259 		}
2260 	}
2261 
2262 	const auto document = _inner->document();
2263 
2264 	// Qt bug workaround https://bugreports.qt.io/browse/QTBUG-49062
2265 	if (!position) {
2266 		auto cursor = QTextCursor(document);
2267 		cursor.movePosition(QTextCursor::End);
2268 		if (position + charsAdded > cursor.position()) {
2269 			const auto delta = position + charsAdded - cursor.position();
2270 			if (charsRemoved >= delta) {
2271 				charsAdded -= delta;
2272 				charsRemoved -= delta;
2273 			}
2274 		}
2275 	}
2276 
2277 	const auto insertPosition = (_realInsertPosition >= 0)
2278 		? _realInsertPosition
2279 		: position;
2280 	const auto insertLength = (_realInsertPosition >= 0)
2281 		? _realCharsAdded
2282 		: charsAdded;
2283 
2284 	_correcting = true;
2285 	QTextCursor(document).joinPreviousEditBlock();
2286 	const auto guard = gsl::finally([&] {
2287 		_correcting = false;
2288 		QTextCursor(document).endEditBlock();
2289 		handleContentsChanged();
2290 		const auto added = charsAdded - _emojiSurrogateAmount;
2291 		_documentContentsChanges.fire({position, charsRemoved, added});
2292 		_emojiSurrogateAmount = 0;
2293 	});
2294 
2295 	chopByMaxLength(insertPosition, insertLength);
2296 
2297 	if (document->availableRedoSteps() == 0 && insertLength > 0) {
2298 		const auto pageSize = document->pageSize();
2299 		processFormatting(insertPosition, insertPosition + insertLength);
2300 		if (document->pageSize() != pageSize) {
2301 			document->setPageSize(pageSize);
2302 		}
2303 	}
2304 }
2305 
onCursorPositionChanged()2306 void InputField::onCursorPositionChanged() {
2307 	auto cursor = textCursor();
2308 	if (!cursor.hasSelection() && !cursor.position()) {
2309 		cursor.setCharFormat(_defaultCharFormat);
2310 		setTextCursor(cursor);
2311 	}
2312 }
2313 
chopByMaxLength(int insertPosition,int insertLength)2314 void InputField::chopByMaxLength(int insertPosition, int insertLength) {
2315 	Expects(_correcting);
2316 
2317 	if (_maxLength < 0) {
2318 		return;
2319 	}
2320 
2321 	auto cursor = QTextCursor(document());
2322 	cursor.movePosition(QTextCursor::End);
2323 	const auto fullSize = cursor.position();
2324 	const auto toRemove = fullSize - _maxLength;
2325 	if (toRemove > 0) {
2326 		if (toRemove > insertLength) {
2327 			if (insertLength) {
2328 				cursor.setPosition(insertPosition);
2329 				cursor.setPosition(
2330 					(insertPosition + insertLength),
2331 					QTextCursor::KeepAnchor);
2332 				cursor.removeSelectedText();
2333 			}
2334 			cursor.setPosition(fullSize - (toRemove - insertLength));
2335 			cursor.setPosition(fullSize, QTextCursor::KeepAnchor);
2336 			cursor.removeSelectedText();
2337 		} else {
2338 			cursor.setPosition(
2339 				insertPosition + (insertLength - toRemove));
2340 			cursor.setPosition(
2341 				insertPosition + insertLength,
2342 				QTextCursor::KeepAnchor);
2343 			cursor.removeSelectedText();
2344 		}
2345 	}
2346 }
2347 
handleContentsChanged()2348 void InputField::handleContentsChanged() {
2349 	setErrorShown(false);
2350 
2351 	auto tagsChanged = false;
2352 	const auto currentText = getTextPart(
2353 		0,
2354 		-1,
2355 		_lastTextWithTags.tags,
2356 		tagsChanged,
2357 		_markdownEnabled ? &_lastMarkdownTags : nullptr);
2358 
2359 	//highlightMarkdown();
2360 
2361 	if (tagsChanged || (_lastTextWithTags.text != currentText)) {
2362 		_lastTextWithTags.text = currentText;
2363 		const auto weak = MakeWeak(this);
2364 		changed();
2365 		if (!weak) {
2366 			return;
2367 		}
2368 		checkContentHeight();
2369 	}
2370 	startPlaceholderAnimation();
2371 	Integration::Instance().textActionsUpdated();
2372 }
2373 
highlightMarkdown()2374 void InputField::highlightMarkdown() {
2375 	// Highlighting may interfere with markdown parsing -> inaccurate.
2376 	// For debug.
2377 	auto from = 0;
2378 	auto applyColor = [&](int a, int b, QColor color) {
2379 		auto cursor = textCursor();
2380 		cursor.setPosition(a);
2381 		cursor.setPosition(b, QTextCursor::KeepAnchor);
2382 		auto format = QTextCharFormat();
2383 		format.setForeground(color);
2384 		cursor.mergeCharFormat(format);
2385 		from = b;
2386 	};
2387 	for (const auto &tag : _lastMarkdownTags) {
2388 		if (tag.internalStart > from) {
2389 			applyColor(from, tag.internalStart, QColor(0, 0, 0));
2390 		} else if (tag.internalStart < from) {
2391 			continue;
2392 		}
2393 		applyColor(
2394 			tag.internalStart,
2395 			tag.internalStart + tag.internalLength,
2396 			(tag.closed
2397 				? QColor(0, 128, 0)
2398 				: QColor(128, 0, 0)));
2399 	}
2400 	auto cursor = textCursor();
2401 	cursor.movePosition(QTextCursor::End);
2402 	if (const auto till = cursor.position(); till > from) {
2403 		applyColor(from, till, QColor(0, 0, 0));
2404 	}
2405 }
2406 
onUndoAvailable(bool avail)2407 void InputField::onUndoAvailable(bool avail) {
2408 	_undoAvailable = avail;
2409 	Integration::Instance().textActionsUpdated();
2410 }
2411 
onRedoAvailable(bool avail)2412 void InputField::onRedoAvailable(bool avail) {
2413 	_redoAvailable = avail;
2414 	Integration::Instance().textActionsUpdated();
2415 }
2416 
setDisplayFocused(bool focused)2417 void InputField::setDisplayFocused(bool focused) {
2418 	setFocused(focused);
2419 	finishAnimating();
2420 }
2421 
selectAll()2422 void InputField::selectAll() {
2423 	auto cursor = _inner->textCursor();
2424 	cursor.setPosition(0);
2425 	cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
2426 	_inner->setTextCursor(cursor);
2427 }
2428 
finishAnimating()2429 void InputField::finishAnimating() {
2430 	_a_focused.stop();
2431 	_a_error.stop();
2432 	_a_placeholderShifted.stop();
2433 	_a_borderShown.stop();
2434 	_a_borderOpacity.stop();
2435 	update();
2436 }
2437 
setPlaceholderHidden(bool forcePlaceholderHidden)2438 void InputField::setPlaceholderHidden(bool forcePlaceholderHidden) {
2439 	_forcePlaceholderHidden = forcePlaceholderHidden;
2440 	startPlaceholderAnimation();
2441 }
2442 
startPlaceholderAnimation()2443 void InputField::startPlaceholderAnimation() {
2444 	const auto textLength = [&] {
2445 		return getTextWithTags().text.size() + _lastPreEditText.size();
2446 	};
2447 	const auto placeholderShifted = _forcePlaceholderHidden
2448 		|| (_focused && _st.placeholderScale > 0.)
2449 		|| (textLength() > _placeholderAfterSymbols);
2450 	if (_placeholderShifted != placeholderShifted) {
2451 		_placeholderShifted = placeholderShifted;
2452 		_a_placeholderShifted.start(
2453 			[=] { update(); },
2454 			_placeholderShifted ? 0. : 1.,
2455 			_placeholderShifted ? 1. : 0.,
2456 			_st.duration);
2457 	}
2458 }
2459 
createMimeDataFromSelectionInner() const2460 QMimeData *InputField::createMimeDataFromSelectionInner() const {
2461 	const auto cursor = _inner->textCursor();
2462 	const auto start = cursor.selectionStart();
2463 	const auto end = cursor.selectionEnd();
2464 	return TextUtilities::MimeDataFromText((end > start)
2465 		? getTextWithTagsPart(start, end)
2466 		: TextWithTags()
2467 	).release();
2468 }
2469 
customUpDown(bool isCustom)2470 void InputField::customUpDown(bool isCustom) {
2471 	_customUpDown = isCustom;
2472 }
2473 
customTab(bool isCustom)2474 void InputField::customTab(bool isCustom) {
2475 	_customTab = isCustom;
2476 }
2477 
setSubmitSettings(SubmitSettings settings)2478 void InputField::setSubmitSettings(SubmitSettings settings) {
2479 	_submitSettings = settings;
2480 }
2481 
document()2482 not_null<QTextDocument*> InputField::document() {
2483 	return _inner->document();
2484 }
2485 
document() const2486 not_null<const QTextDocument*> InputField::document() const {
2487 	return _inner->document();
2488 }
2489 
setTextCursor(const QTextCursor & cursor)2490 void InputField::setTextCursor(const QTextCursor &cursor) {
2491 	return _inner->setTextCursor(cursor);
2492 }
2493 
textCursor() const2494 QTextCursor InputField::textCursor() const {
2495 	return _inner->textCursor();
2496 }
2497 
setCursorPosition(int pos)2498 void InputField::setCursorPosition(int pos) {
2499 	auto cursor = _inner->textCursor();
2500 	cursor.setPosition(pos);
2501 	_inner->setTextCursor(cursor);
2502 }
2503 
setText(const QString & text)2504 void InputField::setText(const QString &text) {
2505 	setTextWithTags({ text, {} });
2506 }
2507 
setTextWithTags(const TextWithTags & textWithTags,HistoryAction historyAction)2508 void InputField::setTextWithTags(
2509 		const TextWithTags &textWithTags,
2510 		HistoryAction historyAction) {
2511 	_insertedTags = textWithTags.tags;
2512 	_insertedTagsAreFromMime = false;
2513 	_realInsertPosition = 0;
2514 	_realCharsAdded = textWithTags.text.size();
2515 	const auto document = _inner->document();
2516 	auto cursor = QTextCursor(document);
2517 	if (historyAction == HistoryAction::Clear) {
2518 		document->setUndoRedoEnabled(false);
2519 		cursor.beginEditBlock();
2520 	} else if (historyAction == HistoryAction::MergeEntry) {
2521 		cursor.joinPreviousEditBlock();
2522 	} else {
2523 		cursor.beginEditBlock();
2524 	}
2525 	cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
2526 	cursor.insertText(textWithTags.text);
2527 	cursor.movePosition(QTextCursor::End);
2528 	cursor.endEditBlock();
2529 	if (historyAction == HistoryAction::Clear) {
2530 		document->setUndoRedoEnabled(true);
2531 	}
2532 	_insertedTags.clear();
2533 	_realInsertPosition = -1;
2534 	finishAnimating();
2535 }
2536 
getTextWithTagsPart(int start,int end) const2537 TextWithTags InputField::getTextWithTagsPart(int start, int end) const {
2538 	auto changed = false;
2539 	auto result = TextWithTags();
2540 	result.text = getTextPart(start, end, result.tags, changed);
2541 	return result;
2542 }
2543 
getTextWithAppliedMarkdown() const2544 TextWithTags InputField::getTextWithAppliedMarkdown() const {
2545 	if (!_markdownEnabled || _lastMarkdownTags.empty()) {
2546 		return getTextWithTags();
2547 	}
2548 	const auto &originalText = _lastTextWithTags.text;
2549 	const auto &originalTags = _lastTextWithTags.tags;
2550 
2551 	// Ignore tags that partially intersect some http-links.
2552 	// This will allow sending http://test.com/__test__/test correctly.
2553 	const auto links = TextUtilities::ParseEntities(
2554 		originalText,
2555 		0).entities;
2556 
2557 	auto result = TextWithTags();
2558 	result.text.reserve(originalText.size());
2559 	result.tags.reserve(originalTags.size() + _lastMarkdownTags.size());
2560 	auto removed = 0;
2561 	auto originalTag = originalTags.begin();
2562 	const auto originalTagsEnd = originalTags.end();
2563 	const auto addOriginalTagsUpTill = [&](int offset) {
2564 		while (originalTag != originalTagsEnd
2565 			&& originalTag->offset + originalTag->length <= offset) {
2566 			result.tags.push_back(*originalTag++);
2567 			result.tags.back().offset -= removed;
2568 		}
2569 	};
2570 	auto from = 0;
2571 	const auto addOriginalTextUpTill = [&](int offset) {
2572 		if (offset > from) {
2573 			result.text.append(base::StringViewMid(originalText, from, offset - from));
2574 		}
2575 	};
2576 	auto link = links.begin();
2577 	const auto linksEnd = links.end();
2578 	for (const auto &tag : _lastMarkdownTags) {
2579 		const auto tagLength = int(tag.tag.size());
2580 		if (!tag.closed || tag.adjustedStart < from) {
2581 			continue;
2582 		}
2583 		auto entityLength = tag.adjustedLength - 2 * tagLength;
2584 		if (entityLength <= 0) {
2585 			continue;
2586 		}
2587 		addOriginalTagsUpTill(tag.adjustedStart);
2588 		const auto tagAdjustedEnd = tag.adjustedStart + tag.adjustedLength;
2589 		if (originalTag != originalTagsEnd
2590 			&& originalTag->offset < tagAdjustedEnd) {
2591 			continue;
2592 		}
2593 		while (link != linksEnd
2594 			&& link->offset() + link->length() <= tag.adjustedStart) {
2595 			++link;
2596 		}
2597 		if (link != linksEnd
2598 			&& link->offset() < tagAdjustedEnd
2599 			&& (link->offset() + link->length() > tagAdjustedEnd
2600 				|| link->offset() < tag.adjustedStart)) {
2601 			continue;
2602 		}
2603 		addOriginalTextUpTill(tag.adjustedStart);
2604 
2605 		auto entityStart = tag.adjustedStart + tagLength;
2606 		if (tag.tag == kTagPre) {
2607 			// Remove redundant newlines for pre.
2608 			// If ``` is on a separate line add only one newline.
2609 			if (IsNewline(originalText[entityStart])
2610 				&& (result.text.isEmpty()
2611 					|| IsNewline(result.text[result.text.size() - 1]))) {
2612 				++entityStart;
2613 				--entityLength;
2614 			}
2615 			const auto entityEnd = entityStart + entityLength;
2616 			if (IsNewline(originalText[entityEnd - 1])
2617 				&& (originalText.size() <= entityEnd + tagLength
2618 					|| IsNewline(originalText[entityEnd + tagLength]))) {
2619 				--entityLength;
2620 			}
2621 		}
2622 
2623 		if (entityLength > 0) {
2624 			// Add tag text and entity.
2625 			result.tags.push_back(TextWithTags::Tag{
2626 				int(result.text.size()),
2627 				entityLength,
2628 				tag.tag });
2629 			result.text.append(base::StringViewMid(
2630 				originalText,
2631 				entityStart,
2632 				entityLength));
2633 		}
2634 
2635 		from = tag.adjustedStart + tag.adjustedLength;
2636 		removed += (tag.adjustedLength - entityLength);
2637 	}
2638 	addOriginalTagsUpTill(originalText.size());
2639 	addOriginalTextUpTill(originalText.size());
2640 	return result;
2641 }
2642 
clear()2643 void InputField::clear() {
2644 	_inner->clear();
2645 	startPlaceholderAnimation();
2646 }
2647 
hasFocus() const2648 bool InputField::hasFocus() const {
2649 	return _inner->hasFocus();
2650 }
2651 
setFocus()2652 void InputField::setFocus() {
2653 	_inner->setFocus();
2654 }
2655 
clearFocus()2656 void InputField::clearFocus() {
2657 	_inner->clearFocus();
2658 }
2659 
ensureCursorVisible()2660 void InputField::ensureCursorVisible() {
2661 	_inner->ensureCursorVisible();
2662 }
2663 
rawTextEdit()2664 not_null<QTextEdit*> InputField::rawTextEdit() {
2665 	return _inner.get();
2666 }
2667 
rawTextEdit() const2668 not_null<const QTextEdit*> InputField::rawTextEdit() const {
2669 	return _inner.get();
2670 }
2671 
ShouldSubmit(SubmitSettings settings,Qt::KeyboardModifiers modifiers)2672 bool InputField::ShouldSubmit(
2673 		SubmitSettings settings,
2674 		Qt::KeyboardModifiers modifiers) {
2675 	const auto shift = modifiers.testFlag(Qt::ShiftModifier);
2676 	const auto ctrl = modifiers.testFlag(Qt::ControlModifier)
2677 		|| modifiers.testFlag(Qt::MetaModifier);
2678 	return (ctrl && shift)
2679 		|| (ctrl
2680 			&& settings != SubmitSettings::None
2681 			&& settings != SubmitSettings::Enter)
2682 		|| (!ctrl
2683 			&& !shift
2684 			&& settings != SubmitSettings::None
2685 			&& settings != SubmitSettings::CtrlEnter);
2686 }
2687 
keyPressEventInner(QKeyEvent * e)2688 void InputField::keyPressEventInner(QKeyEvent *e) {
2689 	const auto shift = e->modifiers().testFlag(Qt::ShiftModifier);
2690 	const auto alt = e->modifiers().testFlag(Qt::AltModifier);
2691 	const auto macmeta = Platform::IsMac()
2692 		&& e->modifiers().testFlag(Qt::ControlModifier)
2693 		&& !e->modifiers().testFlag(Qt::MetaModifier)
2694 		&& !e->modifiers().testFlag(Qt::AltModifier);
2695 	const auto ctrl = e->modifiers().testFlag(Qt::ControlModifier)
2696 		|| e->modifiers().testFlag(Qt::MetaModifier);
2697 	const auto enterSubmit = (_mode != Mode::MultiLine)
2698 		|| ShouldSubmit(_submitSettings, e->modifiers());
2699 	const auto enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return);
2700 	const auto backspace = (e->key() == Qt::Key_Backspace);
2701 	if (e->key() == Qt::Key_Left
2702 		|| e->key() == Qt::Key_Right
2703 		|| e->key() == Qt::Key_Up
2704 		|| e->key() == Qt::Key_Down
2705 		|| e->key() == Qt::Key_Home
2706 		|| e->key() == Qt::Key_End) {
2707 		_reverseMarkdownReplacement = false;
2708 	}
2709 
2710 	if (macmeta && backspace) {
2711 		QTextCursor tc(textCursor()), start(tc);
2712 		start.movePosition(QTextCursor::StartOfLine);
2713 		tc.setPosition(start.position(), QTextCursor::KeepAnchor);
2714 		tc.removeSelectedText();
2715 	} else if (backspace
2716 		&& e->modifiers() == 0
2717 		&& revertFormatReplace()) {
2718 		e->accept();
2719 	} else if (enter && enterSubmit) {
2720 		submitted(e->modifiers());
2721 	} else if (e->key() == Qt::Key_Escape) {
2722 		e->ignore();
2723 		cancelled();
2724 	} else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab) {
2725 		if (alt || ctrl) {
2726 			e->ignore();
2727 		} else if (_customTab) {
2728 			tabbed();
2729 		} else if (!focusNextPrevChild(e->key() == Qt::Key_Tab && !shift)) {
2730 			e->ignore();
2731 		}
2732 	} else if (e->key() == Qt::Key_Search || e == QKeySequence::Find) {
2733 		e->ignore();
2734 	} else if (handleMarkdownKey(e)) {
2735 		e->accept();
2736 	} else if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) {
2737 		e->ignore();
2738 #ifdef Q_OS_MAC
2739 	} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
2740 		const auto cursor = textCursor();
2741 		const auto start = cursor.selectionStart();
2742 		const auto end = cursor.selectionEnd();
2743 		if (end > start) {
2744 			QGuiApplication::clipboard()->setText(
2745 				getTextWithTagsPart(start, end).text,
2746 				QClipboard::FindBuffer);
2747 		}
2748 #endif // Q_OS_MAC
2749 	} else {
2750 		const auto text = e->text();
2751 		const auto oldPosition = textCursor().position();
2752 		const auto oldModifiers = e->modifiers();
2753 		const auto allowedModifiers = (enter && ctrl)
2754 			? (~Qt::ControlModifier)
2755 			: (enter && shift)
2756 			? (~Qt::ShiftModifier)
2757 			: (backspace && Platform::IsLinux())
2758 			? (Qt::ControlModifier)
2759 			: oldModifiers;
2760 		const auto changeModifiers = (oldModifiers & ~allowedModifiers) != 0;
2761 		if (changeModifiers) {
2762 			e->setModifiers(oldModifiers & allowedModifiers);
2763 		}
2764 		_inner->QTextEdit::keyPressEvent(e);
2765 		if (changeModifiers) {
2766 			e->setModifiers(oldModifiers);
2767 		}
2768 		auto cursor = textCursor();
2769 		if (cursor.position() == oldPosition) {
2770 			bool check = false;
2771 			if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) {
2772 				cursor.movePosition(QTextCursor::Start, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor);
2773 				check = true;
2774 			} else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_Down) {
2775 				cursor.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor);
2776 				check = true;
2777 			} else if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Right || e->key() == Qt::Key_Backspace) {
2778 				e->ignore();
2779 			}
2780 			if (check) {
2781 				if (oldPosition == cursor.position()) {
2782 					e->ignore();
2783 				} else {
2784 					setTextCursor(cursor);
2785 				}
2786 			}
2787 		}
2788 		if (!processMarkdownReplaces(text)) {
2789 			processInstantReplaces(text);
2790 		}
2791 	}
2792 }
2793 
getTextWithTagsSelected() const2794 TextWithTags InputField::getTextWithTagsSelected() const {
2795 	const auto cursor = textCursor();
2796 	const auto start = cursor.selectionStart();
2797 	const auto end = cursor.selectionEnd();
2798 	return (end > start) ? getTextWithTagsPart(start, end) : TextWithTags();
2799 }
2800 
handleMarkdownKey(QKeyEvent * e)2801 bool InputField::handleMarkdownKey(QKeyEvent *e) {
2802 	if (!_markdownEnabled) {
2803 		return false;
2804 	}
2805 	const auto matches = [&](const QKeySequence &sequence) {
2806 		const auto searchKey = (e->modifiers() | e->key())
2807 			& ~(Qt::KeypadModifier | Qt::GroupSwitchModifier);
2808 		const auto events = QKeySequence(searchKey);
2809 		return sequence.matches(events) == QKeySequence::ExactMatch;
2810 	};
2811 	if (e == QKeySequence::Bold) {
2812 		toggleSelectionMarkdown(kTagBold);
2813 	} else if (e == QKeySequence::Italic) {
2814 		toggleSelectionMarkdown(kTagItalic);
2815 	} else if (e == QKeySequence::Underline) {
2816 		toggleSelectionMarkdown(kTagUnderline);
2817 	} else if (matches(kStrikeOutSequence)) {
2818 		toggleSelectionMarkdown(kTagStrikeOut);
2819 	} else if (matches(kMonospaceSequence)) {
2820 		toggleSelectionMarkdown(kTagCode);
2821 	} else if (matches(kClearFormatSequence)) {
2822 		clearSelectionMarkdown();
2823 	} else if (matches(kEditLinkSequence) && _editLinkCallback) {
2824 		const auto cursor = textCursor();
2825 		editMarkdownLink({
2826 			cursor.selectionStart(),
2827 			cursor.selectionEnd()
2828 		});
2829 	} else {
2830 		return false;
2831 	}
2832 	return true;
2833 }
2834 
selectionEditLinkData(EditLinkSelection selection) const2835 auto InputField::selectionEditLinkData(EditLinkSelection selection) const
2836 -> EditLinkData {
2837 	Expects(_editLinkCallback != nullptr);
2838 
2839 	const auto position = (selection.from == selection.till
2840 		&& selection.from > 0)
2841 		? (selection.from - 1)
2842 		: selection.from;
2843 	const auto link = [&] {
2844 		return (position != selection.till)
2845 			? CheckFullTextTag(
2846 				getTextWithTagsPart(position, selection.till),
2847 				kTagCheckLinkMeta)
2848 			: QString();
2849 	}();
2850 	const auto simple = EditLinkData {
2851 		selection.from,
2852 		selection.till,
2853 		QString()
2854 	};
2855 	if (!_editLinkCallback(selection, {}, link, EditLinkAction::Check)) {
2856 		return simple;
2857 	}
2858 	Assert(!link.isEmpty());
2859 
2860 	struct State {
2861 		QTextBlock block;
2862 		QTextBlock::iterator i;
2863 	};
2864 	const auto document = _inner->document();
2865 	const auto skipInvalid = [&](State &state) {
2866 		if (state.block == document->end()) {
2867 			return false;
2868 		}
2869 		while (state.i.atEnd()) {
2870 			state.block = state.block.next();
2871 			if (state.block == document->end()) {
2872 				return false;
2873 			}
2874 			state.i = state.block.begin();
2875 		}
2876 		return true;
2877 	};
2878 	const auto moveToNext = [&](State &state) {
2879 		Expects(state.block != document->end());
2880 		Expects(!state.i.atEnd());
2881 
2882 		++state.i;
2883 	};
2884 	const auto moveToPrevious = [&](State &state) {
2885 		Expects(state.block != document->end());
2886 		Expects(!state.i.atEnd());
2887 
2888 		while (state.i == state.block.begin()) {
2889 			if (state.block == document->begin()) {
2890 				state.block = document->end();
2891 				return false;
2892 			}
2893 			state.block = state.block.previous();
2894 			state.i = state.block.end();
2895 		}
2896 		--state.i;
2897 		return true;
2898 	};
2899 	const auto stateTag = [&](const State &state) {
2900 		const auto format = state.i.fragment().charFormat();
2901 		return format.property(kTagProperty).toString();
2902 	};
2903 	const auto stateTagHasLink = [&](const State &state) {
2904 		const auto tag = stateTag(state);
2905 		return (tag == link) || QStringView(tag).split('|').contains(
2906 			QStringView(link));
2907 	};
2908 	const auto stateStart = [&](const State &state) {
2909 		return state.i.fragment().position();
2910 	};
2911 	const auto stateEnd = [&](const State &state) {
2912 		const auto fragment = state.i.fragment();
2913 		return fragment.position() + fragment.length();
2914 	};
2915 	auto state = State{ document->findBlock(position) };
2916 	if (state.block != document->end()) {
2917 		state.i = state.block.begin();
2918 	}
2919 	for (; skipInvalid(state); moveToNext(state)) {
2920 		const auto fragmentStart = stateStart(state);
2921 		const auto fragmentEnd = stateEnd(state);
2922 		if (fragmentEnd <= position) {
2923 			continue;
2924 		} else if (fragmentStart >= selection.till) {
2925 			break;
2926 		}
2927 		if (stateTagHasLink(state)) {
2928 			auto start = fragmentStart;
2929 			auto finish = fragmentEnd;
2930 			auto copy = state;
2931 			while (moveToPrevious(copy) && stateTagHasLink(copy)) {
2932 				start = stateStart(copy);
2933 			}
2934 			while (skipInvalid(state) && stateTagHasLink(state)) {
2935 				finish = stateEnd(state);
2936 				moveToNext(state);
2937 			}
2938 			return { start, finish, link };
2939 		}
2940 	}
2941 	return simple;
2942 }
2943 
editLinkSelection(QContextMenuEvent * e) const2944 auto InputField::editLinkSelection(QContextMenuEvent *e) const
2945 -> EditLinkSelection {
2946 	const auto cursor = textCursor();
2947 	if (!cursor.hasSelection() && e->reason() == QContextMenuEvent::Mouse) {
2948 		const auto clickCursor = _inner->cursorForPosition(
2949 			_inner->viewport()->mapFromGlobal(e->globalPos()));
2950 		if (!clickCursor.isNull() && !clickCursor.hasSelection()) {
2951 			return {
2952 				clickCursor.position(),
2953 				clickCursor.position()
2954 			};
2955 		}
2956 	}
2957 	return {
2958 		cursor.selectionStart(),
2959 		cursor.selectionEnd()
2960 	};
2961 }
2962 
editMarkdownLink(EditLinkSelection selection)2963 void InputField::editMarkdownLink(EditLinkSelection selection) {
2964 	if (!_editLinkCallback) {
2965 		return;
2966 	}
2967 	const auto data = selectionEditLinkData(selection);
2968 	_editLinkCallback(
2969 		selection,
2970 		getTextWithTagsPart(data.from, data.till).text,
2971 		data.link,
2972 		EditLinkAction::Edit);
2973 }
2974 
inputMethodEventInner(QInputMethodEvent * e)2975 void InputField::inputMethodEventInner(QInputMethodEvent *e) {
2976 	const auto preedit = e->preeditString();
2977 	if (_lastPreEditText != preedit) {
2978 		_lastPreEditText = preedit;
2979 		startPlaceholderAnimation();
2980 	}
2981 	_inputMethodCommit = e->commitString();
2982 
2983 	const auto weak = Ui::MakeWeak(this);
2984 	_inner->QTextEdit::inputMethodEvent(e);
2985 
2986 	if (weak && _inputMethodCommit.has_value()) {
2987 		const auto text = *base::take(_inputMethodCommit);
2988 		if (!processMarkdownReplaces(text)) {
2989 			processInstantReplaces(text);
2990 		}
2991 	}
2992 }
2993 
instantReplaces() const2994 const InstantReplaces &InputField::instantReplaces() const {
2995 	return _mutableInstantReplaces;
2996 }
2997 
2998 // Disable markdown instant replacement.
processMarkdownReplaces(const QString & appended)2999 bool InputField::processMarkdownReplaces(const QString &appended) {
3000 	//if (appended.size() != 1 || !_markdownEnabled) {
3001 	//	return false;
3002 	//}
3003 	//const auto ch = appended[0];
3004 	//if (ch == '`') {
3005 	//	return processMarkdownReplace(kTagCode)
3006 	//		|| processMarkdownReplace(kTagPre);
3007 	//} else if (ch == '*') {
3008 	//	return processMarkdownReplace(kTagBold);
3009 	//} else if (ch == '_') {
3010 	//	return processMarkdownReplace(kTagItalic);
3011 	//}
3012 	return false;
3013 }
3014 
3015 //bool InputField::processMarkdownReplace(const QString &tag) {
3016 //	const auto position = textCursor().position();
3017 //	const auto tagLength = tag.size();
3018 //	const auto start = [&] {
3019 //		for (const auto &possible : _lastMarkdownTags) {
3020 //			const auto end = possible.start + possible.length;
3021 //			if (possible.start + 2 * tagLength >= position) {
3022 //				return MarkdownTag();
3023 //			} else if (end >= position || end + tagLength == position) {
3024 //				if (possible.tag == tag) {
3025 //					return possible;
3026 //				}
3027 //			}
3028 //		}
3029 //		return MarkdownTag();
3030 //	}();
3031 //	if (start.tag.isEmpty()) {
3032 //		return false;
3033 //	}
3034 //	return commitMarkdownReplacement(start.start, position, tag, tag);
3035 //}
3036 
processInstantReplaces(const QString & appended)3037 void InputField::processInstantReplaces(const QString &appended) {
3038 	const auto &replaces = instantReplaces();
3039 	if (appended.size() != 1
3040 		|| !_instantReplacesEnabled
3041 		|| !replaces.maxLength) {
3042 		return;
3043 	}
3044 	const auto it = replaces.reverseMap.tail.find(appended[0]);
3045 	if (it == end(replaces.reverseMap.tail)) {
3046 		return;
3047 	}
3048 	const auto position = textCursor().position();
3049 	for (const auto &tag : _lastMarkdownTags) {
3050 		if (tag.internalStart < position
3051 			&& tag.internalStart + tag.internalLength >= position
3052 			&& (tag.tag == kTagCode || tag.tag == kTagPre)) {
3053 			return;
3054 		}
3055 	}
3056 	const auto typed = getTextWithTagsPart(
3057 		std::max(position - replaces.maxLength, 0),
3058 		position - 1).text;
3059 	auto node = &it->second;
3060 	auto i = typed.size();
3061 	do {
3062 		if (!node->text.isEmpty()) {
3063 			applyInstantReplace(typed.mid(i) + appended, node->text);
3064 			return;
3065 		} else if (!i) {
3066 			return;
3067 		}
3068 		const auto it = node->tail.find(typed[--i]);
3069 		if (it == end(node->tail)) {
3070 			return;
3071 		}
3072 		node = &it->second;
3073 	} while (true);
3074 }
3075 
applyInstantReplace(const QString & what,const QString & with)3076 void InputField::applyInstantReplace(
3077 		const QString &what,
3078 		const QString &with) {
3079 	const auto length = int(what.size());
3080 	const auto cursor = textCursor();
3081 	const auto position = cursor.position();
3082 	if (cursor.hasSelection()) {
3083 		return;
3084 	} else if (position < length) {
3085 		return;
3086 	}
3087 	commitInstantReplacement(position - length, position, with, what, true);
3088 }
3089 
commitInstantReplacement(int from,int till,const QString & with)3090 void InputField::commitInstantReplacement(
3091 		int from,
3092 		int till,
3093 		const QString &with) {
3094 	commitInstantReplacement(from, till, with, std::nullopt, false);
3095 }
3096 
commitInstantReplacement(int from,int till,const QString & with,std::optional<QString> checkOriginal,bool checkIfInMonospace)3097 void InputField::commitInstantReplacement(
3098 		int from,
3099 		int till,
3100 		const QString &with,
3101 		std::optional<QString> checkOriginal,
3102 		bool checkIfInMonospace) {
3103 	const auto original = getTextWithTagsPart(from, till).text;
3104 	if (checkOriginal
3105 		&& checkOriginal->compare(original, Qt::CaseInsensitive) != 0) {
3106 		return;
3107 	}
3108 
3109 	auto cursor = textCursor();
3110 	if (checkIfInMonospace) {
3111 		const auto currentTag = cursor.charFormat().property(
3112 			kTagProperty
3113 		).toString();
3114 		const auto currentTags = QStringView(currentTag).split('|');
3115 		if (currentTags.contains(QStringView(kTagPre))
3116 			|| currentTags.contains(QStringView(kTagCode))) {
3117 			return;
3118 		}
3119 	}
3120 	cursor.setPosition(from);
3121 	cursor.setPosition(till, QTextCursor::KeepAnchor);
3122 
3123 	auto format = [&]() -> QTextCharFormat {
3124 		auto emojiLength = 0;
3125 		const auto emoji = Emoji::Find(with, &emojiLength);
3126 		if (!emoji || with.size() != emojiLength) {
3127 			return _defaultCharFormat;
3128 		}
3129 		const auto use = Integration::Instance().defaultEmojiVariant(
3130 			emoji);
3131 		return PrepareEmojiFormat(use, _st.font);
3132 	}();
3133 	const auto replacement = format.isImageFormat()
3134 		? kObjectReplacement
3135 		: with;
3136 	format.setProperty(kInstantReplaceWhatId, original);
3137 	format.setProperty(kInstantReplaceWithId, replacement);
3138 	format.setProperty(
3139 		kInstantReplaceRandomId,
3140 		base::RandomValue<uint32>());
3141 	ApplyTagFormat(format, cursor.charFormat());
3142 	cursor.insertText(replacement, format);
3143 }
3144 
commitMarkdownReplacement(int from,int till,const QString & tag,const QString & edge)3145 bool InputField::commitMarkdownReplacement(
3146 		int from,
3147 		int till,
3148 		const QString &tag,
3149 		const QString &edge) {
3150 	const auto end = [&] {
3151 		auto cursor = QTextCursor(document());
3152 		cursor.movePosition(QTextCursor::End);
3153 		return cursor.position();
3154 	}();
3155 
3156 	// In case of 'pre' tag extend checked text by one symbol.
3157 	// So that we'll know if we need to insert additional newlines.
3158 	// "Test ```test``` Test" should become three-line text.
3159 	const auto blocktag = (tag == kTagPre);
3160 	const auto extendLeft = (blocktag && from > 0) ? 1 : 0;
3161 	const auto extendRight = (blocktag && till < end) ? 1 : 0;
3162 	const auto extended = getTextWithTagsPart(
3163 		from - extendLeft,
3164 		till + extendRight).text;
3165 	const auto outer = base::StringViewMid(
3166 		extended,
3167 		extendLeft,
3168 		extended.size() - extendLeft - extendRight);
3169 	if ((outer.size() <= 2 * edge.size())
3170 		|| (!edge.isEmpty()
3171 			&& !(outer.startsWith(edge) && outer.endsWith(edge)))) {
3172 		return false;
3173 	}
3174 
3175 	// In case of 'pre' tag check if we need to remove one of two newlines.
3176 	// "Test\n```\ntest\n```" should become two-line text + newline.
3177 	const auto innerRight = edge.size();
3178 	const auto checkIfTwoNewlines = blocktag
3179 		&& (extendLeft > 0)
3180 		&& IsNewline(extended[0]);
3181 	const auto innerLeft = [&] {
3182 		const auto simple = edge.size();
3183 		if (!checkIfTwoNewlines) {
3184 			return simple;
3185 		}
3186 		const auto last = outer.size() - innerRight;
3187 		for (auto check = simple; check != last; ++check) {
3188 			const auto ch = outer.at(check);
3189 			if (IsNewline(ch)) {
3190 				return check + 1;
3191 			} else if (!Text::IsSpace(ch)) {
3192 				break;
3193 			}
3194 		}
3195 		return simple;
3196 	}();
3197 	const auto innerLength = outer.size() - innerLeft - innerRight;
3198 
3199 	// Prepare the final "insert" replacement for the "outer" text part.
3200 	const auto newlineleft = blocktag
3201 		&& (extendLeft > 0)
3202 		&& !IsNewline(extended[0])
3203 		&& !IsNewline(outer.at(innerLeft));
3204 	const auto newlineright = blocktag
3205 		&& (!extendRight || !IsNewline(extended[extended.size() - 1]))
3206 		&& !IsNewline(outer.at(outer.size() - innerRight - 1));
3207 	const auto insert = (newlineleft ? "\n" : "")
3208 		+ outer.mid(innerLeft, innerLength).toString()
3209 		+ (newlineright ? "\n" : "");
3210 
3211 	// Trim inserted tag, so that all newlines are left outside.
3212 	_insertedTags.clear();
3213 	auto tagFrom = newlineleft ? 1 : 0;
3214 	auto tagTill = insert.size() - (newlineright ? 1 : 0);
3215 	for (; tagFrom != tagTill; ++tagFrom) {
3216 		const auto ch = insert.at(tagFrom);
3217 		if (!IsNewline(ch)) {
3218 			break;
3219 		}
3220 	}
3221 	for (; tagTill != tagFrom; --tagTill) {
3222 		const auto ch = insert.at(tagTill - 1);
3223 		if (!IsNewline(ch)) {
3224 			break;
3225 		}
3226 	}
3227 	if (tagTill > tagFrom) {
3228 		_insertedTags.push_back({
3229 			tagFrom,
3230 			int(tagTill - tagFrom),
3231 			tag,
3232 		});
3233 	}
3234 
3235 	// Replace.
3236 	auto cursor = _inner->textCursor();
3237 	cursor.setPosition(from);
3238 	cursor.setPosition(till, QTextCursor::KeepAnchor);
3239 	auto format = _defaultCharFormat;
3240 	if (!edge.isEmpty()) {
3241 		format.setProperty(kReplaceTagId, edge);
3242 		_reverseMarkdownReplacement = true;
3243 	}
3244 	_insertedTagsAreFromMime = false;
3245 	cursor.insertText(insert, format);
3246 	_insertedTags.clear();
3247 
3248 	cursor.setCharFormat(_defaultCharFormat);
3249 	_inner->setTextCursor(cursor);
3250 
3251 	// Fire the tag to the spellchecker.
3252 	_markdownTagApplies.fire({ from, till, -1, -1, false, tag });
3253 
3254 	return true;
3255 }
3256 
addMarkdownTag(int from,int till,const QString & tag)3257 void InputField::addMarkdownTag(
3258 		int from,
3259 		int till,
3260 		const QString &tag) {
3261 	const auto current = getTextWithTagsPart(from, till);
3262 	const auto currentLength = int(current.text.size());
3263 
3264 	// #TODO Trim inserted tag, so that all newlines are left outside.
3265 	auto tags = TagList();
3266 	auto filled = 0;
3267 	const auto add = [&](const TextWithTags::Tag &existing) {
3268 		const auto id = TextUtilities::TagWithAdded(existing.id, tag);
3269 		tags.push_back({ existing.offset, existing.length, id });
3270 		filled = std::clamp(
3271 			existing.offset + existing.length,
3272 			filled,
3273 			currentLength);
3274 	};
3275 	if (!TextUtilities::IsSeparateTag(tag)) {
3276 		for (const auto &existing : current.tags) {
3277 			if (existing.offset >= currentLength) {
3278 				break;
3279 			} else if (existing.offset > filled) {
3280 				add({ filled, existing.offset - filled, tag });
3281 			}
3282 			add(existing);
3283 		}
3284 	}
3285 	if (filled < currentLength) {
3286 		add({ filled, currentLength - filled, tag });
3287 	}
3288 
3289 	finishMarkdownTagChange(from, till, { current.text, tags });
3290 
3291 	// Fire the tag to the spellchecker.
3292 	_markdownTagApplies.fire({ from, till, -1, -1, false, tag });
3293 }
3294 
removeMarkdownTag(int from,int till,const QString & tag)3295 void InputField::removeMarkdownTag(
3296 		int from,
3297 		int till,
3298 		const QString &tag) {
3299 	const auto current = getTextWithTagsPart(from, till);
3300 
3301 	auto tags = TagList();
3302 	for (const auto &existing : current.tags) {
3303 		const auto id = TextUtilities::TagWithRemoved(existing.id, tag);
3304 		if (!id.isEmpty()) {
3305 			tags.push_back({ existing.offset, existing.length, id });
3306 		}
3307 	}
3308 
3309 	finishMarkdownTagChange(from, till, { current.text, tags });
3310 }
3311 
finishMarkdownTagChange(int from,int till,const TextWithTags & textWithTags)3312 void InputField::finishMarkdownTagChange(
3313 		int from,
3314 		int till,
3315 		const TextWithTags &textWithTags) {
3316 	auto cursor = _inner->textCursor();
3317 	cursor.setPosition(from);
3318 	cursor.setPosition(till, QTextCursor::KeepAnchor);
3319 	_insertedTags = textWithTags.tags;
3320 	_insertedTagsAreFromMime = false;
3321 	cursor.insertText(textWithTags.text, _defaultCharFormat);
3322 	_insertedTags.clear();
3323 
3324 	cursor.setCharFormat(_defaultCharFormat);
3325 	_inner->setTextCursor(cursor);
3326 }
3327 
IsValidMarkdownLink(QStringView link)3328 bool InputField::IsValidMarkdownLink(QStringView link) {
3329 	return ::Ui::IsValidMarkdownLink(link);
3330 }
3331 
commitMarkdownLinkEdit(EditLinkSelection selection,const QString & text,const QString & link)3332 void InputField::commitMarkdownLinkEdit(
3333 		EditLinkSelection selection,
3334 		const QString &text,
3335 		const QString &link) {
3336 	if (text.isEmpty()
3337 		|| !IsValidMarkdownLink(link)
3338 		|| !_editLinkCallback) {
3339 		return;
3340 	}
3341 	_insertedTags.clear();
3342 	_insertedTags.push_back({ 0, int(text.size()), link });
3343 
3344 	auto cursor = textCursor();
3345 	const auto editData = selectionEditLinkData(selection);
3346 	cursor.setPosition(editData.from);
3347 	cursor.setPosition(editData.till, QTextCursor::KeepAnchor);
3348 	auto format = _defaultCharFormat;
3349 	_insertedTagsAreFromMime = false;
3350 	cursor.insertText(
3351 		(editData.from == editData.till) ? (text + QChar(' ')) : text,
3352 		_defaultCharFormat);
3353 	_insertedTags.clear();
3354 
3355 	_reverseMarkdownReplacement = false;
3356 	cursor.setCharFormat(_defaultCharFormat);
3357 	_inner->setTextCursor(cursor);
3358 }
3359 
toggleSelectionMarkdown(const QString & tag)3360 void InputField::toggleSelectionMarkdown(const QString &tag) {
3361 	_reverseMarkdownReplacement = false;
3362 	const auto cursor = textCursor();
3363 	const auto position = cursor.position();
3364 	const auto from = cursor.selectionStart();
3365 	const auto till = cursor.selectionEnd();
3366 	if (from == till) {
3367 		return;
3368 	}
3369 	if (tag.isEmpty()) {
3370 		RemoveDocumentTags(_st, document(), from, till);
3371 	} else if (HasFullTextTag(getTextWithTagsSelected(), tag)) {
3372 		removeMarkdownTag(from, till, tag);
3373 	} else {
3374 		const auto useTag = [&] {
3375 			if (tag != kTagCode) {
3376 				return tag;
3377 			}
3378 			const auto leftForBlock = [&] {
3379 				if (!from) {
3380 					return true;
3381 				}
3382 				const auto text = getTextWithTagsPart(
3383 					from - 1,
3384 					from + 1
3385 				).text;
3386 				return text.isEmpty()
3387 					|| IsNewline(text[0])
3388 					|| IsNewline(text[text.size() - 1]);
3389 			}();
3390 			const auto rightForBlock = [&] {
3391 				const auto text = getTextWithTagsPart(
3392 					till - 1,
3393 					till + 1
3394 				).text;
3395 				return text.isEmpty()
3396 					|| IsNewline(text[0])
3397 					|| IsNewline(text[text.size() - 1]);
3398 			}();
3399 			return (leftForBlock && rightForBlock) ? kTagPre : kTagCode;
3400 		}();
3401 		addMarkdownTag(from, till, useTag);
3402 	}
3403 	auto restorePosition = textCursor();
3404 	restorePosition.setPosition((position == till) ? from : till);
3405 	restorePosition.setPosition(position, QTextCursor::KeepAnchor);
3406 	setTextCursor(restorePosition);
3407 }
3408 
clearSelectionMarkdown()3409 void InputField::clearSelectionMarkdown() {
3410 	toggleSelectionMarkdown(QString());
3411 }
3412 
revertFormatReplace()3413 bool InputField::revertFormatReplace() {
3414 	const auto cursor = textCursor();
3415 	const auto position = cursor.position();
3416 	if (position <= 0 || cursor.hasSelection()) {
3417 		return false;
3418 	}
3419 	const auto inside = position - 1;
3420 	const auto document = _inner->document();
3421 	const auto block = document->findBlock(inside);
3422 	if (block == document->end()) {
3423 		return false;
3424 	}
3425 	for (auto i = block.begin(); !i.atEnd(); ++i) {
3426 		const auto fragment = i.fragment();
3427 		const auto fragmentStart = fragment.position();
3428 		const auto fragmentEnd = fragmentStart + fragment.length();
3429 		if (fragmentEnd <= inside) {
3430 			continue;
3431 		} else if (fragmentStart > inside || fragmentEnd != position) {
3432 			return false;
3433 		}
3434 		const auto current = fragment.charFormat();
3435 		if (current.hasProperty(kInstantReplaceWithId)) {
3436 			const auto with = current.property(kInstantReplaceWithId);
3437 			const auto string = with.toString();
3438 			if (fragment.text() != string) {
3439 				return false;
3440 			}
3441 			auto replaceCursor = cursor;
3442 			replaceCursor.setPosition(fragmentStart);
3443 			replaceCursor.setPosition(fragmentEnd, QTextCursor::KeepAnchor);
3444 			const auto what = current.property(kInstantReplaceWhatId);
3445 			auto format = _defaultCharFormat;
3446 			ApplyTagFormat(format, current);
3447 			replaceCursor.insertText(what.toString(), format);
3448 			return true;
3449 		} else if (_reverseMarkdownReplacement
3450 			&& current.hasProperty(kReplaceTagId)) {
3451 			const auto tag = current.property(kReplaceTagId).toString();
3452 			if (tag.isEmpty()) {
3453 				return false;
3454 			} else if (auto test = i; !(++test).atEnd()) {
3455 				const auto format = test.fragment().charFormat();
3456 				if (format.property(kReplaceTagId).toString() == tag) {
3457 					return false;
3458 				}
3459 			} else if (auto test = block; test.next() != document->end()) {
3460 				const auto begin = test.begin();
3461 				if (begin != test.end()) {
3462 					const auto format = begin.fragment().charFormat();
3463 					if (format.property(kReplaceTagId).toString() == tag) {
3464 						return false;
3465 					}
3466 				}
3467 			}
3468 
3469 			const auto first = [&] {
3470 				auto checkBlock = block;
3471 				auto checkLast = i;
3472 				while (true) {
3473 					for (auto j = checkLast; j != checkBlock.begin();) {
3474 						--j;
3475 						const auto format = j.fragment().charFormat();
3476 						if (format.property(kReplaceTagId) != tag) {
3477 							return ++j;
3478 						}
3479 					}
3480 					if (checkBlock == document->begin()) {
3481 						return checkBlock.begin();
3482 					}
3483 					checkBlock = checkBlock.previous();
3484 					checkLast = checkBlock.end();
3485 				}
3486 			}();
3487 			const auto from = first.fragment().position();
3488 			const auto till = fragmentEnd;
3489 			auto replaceCursor = cursor;
3490 			replaceCursor.setPosition(from);
3491 			replaceCursor.setPosition(till, QTextCursor::KeepAnchor);
3492 			replaceCursor.insertText(
3493 				tag + getTextWithTagsPart(from, till).text + tag,
3494 				_defaultCharFormat);
3495 			return true;
3496 		}
3497 		return false;
3498 	}
3499 	return false;
3500 }
3501 
contextMenuEventInner(QContextMenuEvent * e,QMenu * m)3502 void InputField::contextMenuEventInner(QContextMenuEvent *e, QMenu *m) {
3503 	if (const auto menu = m ? m : _inner->createStandardContextMenu()) {
3504 		addMarkdownActions(menu, e);
3505 		_contextMenu = base::make_unique_q<PopupMenu>(this, menu, _st.menu);
3506 		_contextMenu->popup(e->globalPos());
3507 	}
3508 }
3509 
addMarkdownActions(not_null<QMenu * > menu,QContextMenuEvent * e)3510 void InputField::addMarkdownActions(
3511 		not_null<QMenu*> menu,
3512 		QContextMenuEvent *e) {
3513 	if (!_markdownEnabled) {
3514 		return;
3515 	}
3516 	auto &integration = Integration::Instance();
3517 
3518 	const auto formatting = new QAction(
3519 		integration.phraseFormattingTitle(),
3520 		menu);
3521 	addMarkdownMenuAction(menu, formatting);
3522 
3523 	const auto submenu = new QMenu(menu);
3524 	formatting->setMenu(submenu);
3525 
3526 	const auto textWithTags = getTextWithTagsSelected();
3527 	const auto &text = textWithTags.text;
3528 	const auto &tags = textWithTags.tags;
3529 	const auto hasText = !text.isEmpty();
3530 	const auto hasTags = !tags.isEmpty();
3531 	const auto disabled = (!_editLinkCallback && !hasText);
3532 	formatting->setDisabled(disabled);
3533 	if (disabled) {
3534 		return;
3535 	}
3536 	const auto add = [&](
3537 			const QString &base,
3538 			QKeySequence sequence,
3539 			bool disabled,
3540 			auto callback) {
3541 		const auto add = sequence.isEmpty()
3542 			? QString()
3543 			: QChar('\t') + sequence.toString(QKeySequence::NativeText);
3544 		const auto action = new QAction(base + add, submenu);
3545 		connect(action, &QAction::triggered, this, callback);
3546 		action->setDisabled(disabled);
3547 		submenu->addAction(action);
3548 	};
3549 	const auto addtag = [&](
3550 			const QString &base,
3551 			QKeySequence sequence,
3552 			const QString &tag) {
3553 		const auto disabled = !hasText;
3554 		add(base, sequence, disabled, [=] {
3555 			toggleSelectionMarkdown(tag);
3556 		});
3557 	};
3558 	const auto addlink = [&] {
3559 		const auto selection = editLinkSelection(e);
3560 		const auto data = selectionEditLinkData(selection);
3561 		const auto base = data.link.isEmpty()
3562 			? integration.phraseFormattingLinkCreate()
3563 			: integration.phraseFormattingLinkEdit();
3564 		add(base, kEditLinkSequence, false, [=] {
3565 			editMarkdownLink(selection);
3566 		});
3567 	};
3568 	const auto addclear = [&] {
3569 		const auto disabled = !hasText || !hasTags;
3570 		add(integration.phraseFormattingClear(), kClearFormatSequence, disabled, [=] {
3571 			clearSelectionMarkdown();
3572 		});
3573 	};
3574 
3575 	addtag(integration.phraseFormattingBold(), QKeySequence::Bold, kTagBold);
3576 	addtag(integration.phraseFormattingItalic(), QKeySequence::Italic, kTagItalic);
3577 	addtag(integration.phraseFormattingUnderline(), QKeySequence::Underline, kTagUnderline);
3578 	addtag(integration.phraseFormattingStrikeOut(), kStrikeOutSequence, kTagStrikeOut);
3579 	addtag(integration.phraseFormattingMonospace(), kMonospaceSequence, kTagCode);
3580 
3581 	if (_editLinkCallback) {
3582 		submenu->addSeparator();
3583 		addlink();
3584 	}
3585 
3586 	submenu->addSeparator();
3587 	addclear();
3588 }
3589 
addMarkdownMenuAction(not_null<QMenu * > menu,not_null<QAction * > action)3590 void InputField::addMarkdownMenuAction(
3591 		not_null<QMenu*> menu,
3592 		not_null<QAction*> action) {
3593 	const auto actions = menu->actions();
3594 	const auto before = [&] {
3595 		auto seenAfter = false;
3596 		for (const auto action : actions) {
3597 			if (seenAfter) {
3598 				return action;
3599 			} else if (action->objectName() == qstr("edit-delete")) {
3600 				seenAfter = true;
3601 			}
3602 		}
3603 		return (QAction*)nullptr;
3604 	}();
3605 	menu->insertSeparator(before);
3606 	menu->insertAction(before, action);
3607 }
3608 
dropEventInner(QDropEvent * e)3609 void InputField::dropEventInner(QDropEvent *e) {
3610 	_inDrop = true;
3611 	_inner->QTextEdit::dropEvent(e);
3612 	_inDrop = false;
3613 	_insertedTags.clear();
3614 	_realInsertPosition = -1;
3615 	window()->raise();
3616 	window()->activateWindow();
3617 }
3618 
canInsertFromMimeDataInner(const QMimeData * source) const3619 bool InputField::canInsertFromMimeDataInner(const QMimeData *source) const {
3620 	if (source
3621 		&& _mimeDataHook
3622 		&& _mimeDataHook(source, MimeAction::Check)) {
3623 		return true;
3624 	}
3625 	return _inner->QTextEdit::canInsertFromMimeData(source);
3626 }
3627 
insertFromMimeDataInner(const QMimeData * source)3628 void InputField::insertFromMimeDataInner(const QMimeData *source) {
3629 	if (source
3630 		&& _mimeDataHook
3631 		&& _mimeDataHook(source, MimeAction::Insert)) {
3632 		return;
3633 	}
3634 	const auto text = [&] {
3635 		const auto textMime = TextUtilities::TagsTextMimeType();
3636 		const auto tagsMime = TextUtilities::TagsMimeType();
3637 		if (!source->hasFormat(textMime) || !source->hasFormat(tagsMime)) {
3638 			_insertedTags.clear();
3639 			return source->text();
3640 		}
3641 		auto result = QString::fromUtf8(source->data(textMime));
3642 		_insertedTags = TextUtilities::DeserializeTags(
3643 			source->data(tagsMime),
3644 			result.size());
3645 		_insertedTagsAreFromMime = true;
3646 		return result;
3647 	}();
3648 	auto cursor = textCursor();
3649 	_realInsertPosition = cursor.selectionStart();
3650 	_realCharsAdded = text.size();
3651 	if (_realCharsAdded > 0) {
3652 		cursor.insertFragment(QTextDocumentFragment::fromPlainText(text));
3653 	}
3654 	ensureCursorVisible();
3655 	if (!_inDrop) {
3656 		_insertedTags.clear();
3657 		_realInsertPosition = -1;
3658 	}
3659 }
3660 
resizeEvent(QResizeEvent * e)3661 void InputField::resizeEvent(QResizeEvent *e) {
3662 	refreshPlaceholder(_placeholderFull.current());
3663 	_inner->setGeometry(rect().marginsRemoved(_st.textMargins));
3664 	_borderAnimationStart = width() / 2;
3665 	RpWidget::resizeEvent(e);
3666 	checkContentHeight();
3667 }
3668 
refreshPlaceholder(const QString & text)3669 void InputField::refreshPlaceholder(const QString &text) {
3670 	const auto availableWidth = width() - _st.textMargins.left() - _st.textMargins.right() - _st.placeholderMargins.left() - _st.placeholderMargins.right() - 1;
3671 	if (_st.placeholderScale > 0.) {
3672 		auto placeholderFont = _st.placeholderFont->f;
3673 		placeholderFont.setStyleStrategy(QFont::PreferMatch);
3674 		const auto metrics = QFontMetrics(placeholderFont);
3675 		_placeholder = metrics.elidedText(text, Qt::ElideRight, availableWidth);
3676 		_placeholderPath = QPainterPath();
3677 		if (!_placeholder.isEmpty()) {
3678 			_placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder);
3679 		}
3680 	} else {
3681 		_placeholder = _st.placeholderFont->elided(text, availableWidth);
3682 	}
3683 	update();
3684 }
3685 
setPlaceholder(rpl::producer<QString> placeholder,int afterSymbols)3686 void InputField::setPlaceholder(
3687 		rpl::producer<QString> placeholder,
3688 		int afterSymbols) {
3689 	_placeholderFull = std::move(placeholder);
3690 	if (_placeholderAfterSymbols != afterSymbols) {
3691 		_placeholderAfterSymbols = afterSymbols;
3692 		startPlaceholderAnimation();
3693 	}
3694 }
3695 
setEditLinkCallback(Fn<bool (EditLinkSelection selection,QString text,QString link,EditLinkAction action)> callback)3696 void InputField::setEditLinkCallback(
3697 	Fn<bool(
3698 		EditLinkSelection selection,
3699 		QString text,
3700 		QString link,
3701 		EditLinkAction action)> callback) {
3702 	_editLinkCallback = std::move(callback);
3703 }
3704 
showError()3705 void InputField::showError() {
3706 	showErrorNoFocus();
3707 	if (!hasFocus()) {
3708 		_inner->setFocus();
3709 	}
3710 }
3711 
showErrorNoFocus()3712 void InputField::showErrorNoFocus() {
3713 	setErrorShown(true);
3714 }
3715 
hideError()3716 void InputField::hideError() {
3717 	setErrorShown(false);
3718 }
3719 
setErrorShown(bool error)3720 void InputField::setErrorShown(bool error) {
3721 	if (_error != error) {
3722 		_error = error;
3723 		_a_error.start([this] { update(); }, _error ? 0. : 1., _error ? 1. : 0., _st.duration);
3724 		startBorderAnimation();
3725 	}
3726 }
3727 
3728 InputField::~InputField() = default;
3729 
MaskedInputField(QWidget * parent,const style::InputField & st,rpl::producer<QString> placeholder,const QString & val)3730 MaskedInputField::MaskedInputField(
3731 	QWidget *parent,
3732 	const style::InputField &st,
3733 	rpl::producer<QString> placeholder,
3734 	const QString &val)
3735 : Parent(val, parent)
3736 , _st(st)
3737 , _oldtext(val)
3738 , _placeholderFull(std::move(placeholder)) {
3739 	resize(_st.width, _st.heightMin);
3740 
3741 	setFont(_st.font);
3742 	setAlignment(_st.textAlign);
3743 
3744 	_placeholderFull.value(
3745 	) | rpl::start_with_next([=](const QString &text) {
3746 		refreshPlaceholder(text);
3747 	}, lifetime());
3748 
3749 	style::PaletteChanged(
3750 	) | rpl::start_with_next([=] {
3751 		updatePalette();
3752 	}, lifetime());
3753 	updatePalette();
3754 
3755 	setAttribute(Qt::WA_OpaquePaintEvent);
3756 
3757 	connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(onTextChange(const QString&)));
3758 	connect(this, SIGNAL(cursorPositionChanged(int,int)), this, SLOT(onCursorPositionChanged(int,int)));
3759 
3760 	connect(this, SIGNAL(textEdited(const QString&)), this, SLOT(onTextEdited()));
3761 	connect(this, &MaskedInputField::selectionChanged, [] {
3762 		Integration::Instance().textActionsUpdated();
3763 	});
3764 
3765 	setStyle(InputStyle<MaskedInputField>::instance());
3766 	QLineEdit::setTextMargins(0, 0, 0, 0);
3767 	setContentsMargins(0, 0, 0, 0);
3768 
3769 	setAttribute(Qt::WA_AcceptTouchEvents);
3770 	_touchTimer.setSingleShot(true);
3771 	connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
3772 
3773 	setTextMargins(_st.textMargins);
3774 
3775 	startPlaceholderAnimation();
3776 	startBorderAnimation();
3777 	finishAnimating();
3778 }
3779 
updatePalette()3780 void MaskedInputField::updatePalette() {
3781 	auto p = palette();
3782 	p.setColor(QPalette::Text, _st.textFg->c);
3783 	p.setColor(QPalette::Highlight, st::msgInBgSelected->c);
3784 	p.setColor(QPalette::HighlightedText, st::historyTextInFgSelected->c);
3785 	setPalette(p);
3786 }
3787 
setCorrectedText(QString & now,int & nowCursor,const QString & newText,int newPos)3788 void MaskedInputField::setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos) {
3789 	if (newPos < 0 || newPos > newText.size()) {
3790 		newPos = newText.size();
3791 	}
3792 	auto updateText = (newText != now);
3793 	if (updateText) {
3794 		now = newText;
3795 		setText(now);
3796 		startPlaceholderAnimation();
3797 	}
3798 	auto updateCursorPosition = (newPos != nowCursor) || updateText;
3799 	if (updateCursorPosition) {
3800 		nowCursor = newPos;
3801 		setCursorPosition(nowCursor);
3802 	}
3803 }
3804 
customUpDown(bool custom)3805 void MaskedInputField::customUpDown(bool custom) {
3806 	_customUpDown = custom;
3807 }
3808 
borderAnimationStart() const3809 int MaskedInputField::borderAnimationStart() const {
3810 	return _borderAnimationStart;
3811 }
3812 
setTextMargins(const QMargins & mrg)3813 void MaskedInputField::setTextMargins(const QMargins &mrg) {
3814 	_textMargins = mrg;
3815 	refreshPlaceholder(_placeholderFull.current());
3816 }
3817 
onTouchTimer()3818 void MaskedInputField::onTouchTimer() {
3819 	_touchRightButton = true;
3820 }
3821 
eventHook(QEvent * e)3822 bool MaskedInputField::eventHook(QEvent *e) {
3823 	auto type = e->type();
3824 	if (type == QEvent::TouchBegin
3825 		|| type == QEvent::TouchUpdate
3826 		|| type == QEvent::TouchEnd
3827 		|| type == QEvent::TouchCancel) {
3828 		auto event = static_cast<QTouchEvent*>(e);
3829 		if (event->device()->type() == base::TouchDevice::TouchScreen) {
3830 			touchEvent(event);
3831 		}
3832 	}
3833 	return Parent::eventHook(e);
3834 }
3835 
touchEvent(QTouchEvent * e)3836 void MaskedInputField::touchEvent(QTouchEvent *e) {
3837 	switch (e->type()) {
3838 	case QEvent::TouchBegin: {
3839 		if (_touchPress || e->touchPoints().isEmpty()) return;
3840 		_touchTimer.start(QApplication::startDragTime());
3841 		_touchPress = true;
3842 		_touchMove = _touchRightButton = false;
3843 		_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
3844 	} break;
3845 
3846 	case QEvent::TouchUpdate: {
3847 		if (!_touchPress || e->touchPoints().isEmpty()) return;
3848 		if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
3849 			_touchMove = true;
3850 		}
3851 	} break;
3852 
3853 	case QEvent::TouchEnd: {
3854 		if (!_touchPress) return;
3855 		auto weak = MakeWeak(this);
3856 		if (!_touchMove && window()) {
3857 			QPoint mapped(mapFromGlobal(_touchStart));
3858 
3859 			if (_touchRightButton) {
3860 				QContextMenuEvent contextEvent(QContextMenuEvent::Mouse, mapped, _touchStart);
3861 				contextMenuEvent(&contextEvent);
3862 			} else {
3863 				QGuiApplication::inputMethod()->show();
3864 			}
3865 		}
3866 		if (weak) {
3867 			_touchTimer.stop();
3868 			_touchPress = _touchMove = _touchRightButton = false;
3869 		}
3870 	} break;
3871 
3872 	case QEvent::TouchCancel: {
3873 		_touchPress = false;
3874 		_touchTimer.stop();
3875 	} break;
3876 	}
3877 }
3878 
getTextRect() const3879 QRect MaskedInputField::getTextRect() const {
3880 	return rect().marginsRemoved(_textMargins + QMargins(-2, -1, -2, -1));
3881 }
3882 
paintEvent(QPaintEvent * e)3883 void MaskedInputField::paintEvent(QPaintEvent *e) {
3884 	Painter p(this);
3885 
3886 	auto r = rect().intersected(e->rect());
3887 	p.fillRect(r, _st.textBg);
3888 	if (_st.border) {
3889 		p.fillRect(0, height() - _st.border, width(), _st.border, _st.borderFg->b);
3890 	}
3891 	auto errorDegree = _a_error.value(_error ? 1. : 0.);
3892 	auto focusedDegree = _a_focused.value(_focused ? 1. : 0.);
3893 	auto borderShownDegree = _a_borderShown.value(1.);
3894 	auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.);
3895 	if (_st.borderActive && (borderOpacity > 0.)) {
3896 		auto borderStart = std::clamp(_borderAnimationStart, 0, width());
3897 		auto borderFrom = qRound(borderStart * (1. - borderShownDegree));
3898 		auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree);
3899 		if (borderTo > borderFrom) {
3900 			auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree);
3901 			p.setOpacity(borderOpacity);
3902 			p.fillRect(borderFrom, height() - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg);
3903 			p.setOpacity(1);
3904 		}
3905 	}
3906 
3907 	p.setClipRect(r);
3908 	if (_st.placeholderScale > 0. && !_placeholderPath.isEmpty()) {
3909 		auto placeholderShiftDegree = _a_placeholderShifted.value(_placeholderShifted ? 1. : 0.);
3910 		p.save();
3911 		p.setClipRect(r);
3912 
3913 		auto placeholderTop = anim::interpolate(0, _st.placeholderShift, placeholderShiftDegree);
3914 
3915 		QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins));
3916 		r.moveTop(r.top() + placeholderTop);
3917 		if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width());
3918 
3919 		auto placeholderScale = 1. - (1. - _st.placeholderScale) * placeholderShiftDegree;
3920 		auto placeholderFg = anim::color(_st.placeholderFg, _st.placeholderFgActive, focusedDegree);
3921 		placeholderFg = anim::color(placeholderFg, _st.placeholderFgError, errorDegree);
3922 
3923 		PainterHighQualityEnabler hq(p);
3924 		p.setPen(Qt::NoPen);
3925 		p.setBrush(placeholderFg);
3926 		p.translate(r.topLeft());
3927 		p.scale(placeholderScale, placeholderScale);
3928 		p.drawPath(_placeholderPath);
3929 
3930 		p.restore();
3931 	} else if (!_placeholder.isEmpty()) {
3932 		auto placeholderHiddenDegree = _a_placeholderShifted.value(_placeholderShifted ? 1. : 0.);
3933 		if (placeholderHiddenDegree < 1.) {
3934 			p.setOpacity(1. - placeholderHiddenDegree);
3935 			p.save();
3936 			p.setClipRect(r);
3937 
3938 			auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree);
3939 
3940 			QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins));
3941 			r.moveLeft(r.left() + placeholderLeft);
3942 			if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width());
3943 
3944 			p.setFont(_st.placeholderFont);
3945 			p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree));
3946 			p.drawText(r, _placeholder, _st.placeholderAlign);
3947 
3948 			p.restore();
3949 			p.setOpacity(1.);
3950 		}
3951 	}
3952 
3953 	paintAdditionalPlaceholder(p);
3954 	QLineEdit::paintEvent(e);
3955 }
3956 
startBorderAnimation()3957 void MaskedInputField::startBorderAnimation() {
3958 	auto borderVisible = (_error || _focused);
3959 	if (_borderVisible != borderVisible) {
3960 		_borderVisible = borderVisible;
3961 		if (_borderVisible) {
3962 			if (_a_borderOpacity.animating()) {
3963 				_a_borderOpacity.start([this] { update(); }, 0., 1., _st.duration);
3964 			} else {
3965 				_a_borderShown.start([this] { update(); }, 0., 1., _st.duration);
3966 			}
3967 		} else if (qFuzzyCompare(_a_borderShown.value(1.), 0.)) {
3968 			_a_borderShown.stop();
3969 			_a_borderOpacity.stop();
3970 		} else {
3971 			_a_borderOpacity.start([this] { update(); }, 1., 0., _st.duration);
3972 		}
3973 	}
3974 }
3975 
focusInEvent(QFocusEvent * e)3976 void MaskedInputField::focusInEvent(QFocusEvent *e) {
3977 	_borderAnimationStart = (e->reason() == Qt::MouseFocusReason) ? mapFromGlobal(QCursor::pos()).x() : (width() / 2);
3978 	setFocused(true);
3979 	QLineEdit::focusInEvent(e);
3980 	focused();
3981 }
3982 
focusOutEvent(QFocusEvent * e)3983 void MaskedInputField::focusOutEvent(QFocusEvent *e) {
3984 	setFocused(false);
3985 	QLineEdit::focusOutEvent(e);
3986 	blurred();
3987 }
3988 
setFocused(bool focused)3989 void MaskedInputField::setFocused(bool focused) {
3990 	if (_focused != focused) {
3991 		_focused = focused;
3992 		_a_focused.start([this] { update(); }, _focused ? 0. : 1., _focused ? 1. : 0., _st.duration);
3993 		startPlaceholderAnimation();
3994 		startBorderAnimation();
3995 	}
3996 }
3997 
resizeEvent(QResizeEvent * e)3998 void MaskedInputField::resizeEvent(QResizeEvent *e) {
3999 	refreshPlaceholder(_placeholderFull.current());
4000 	_borderAnimationStart = width() / 2;
4001 	QLineEdit::resizeEvent(e);
4002 }
4003 
refreshPlaceholder(const QString & text)4004 void MaskedInputField::refreshPlaceholder(const QString &text) {
4005 	const auto availableWidth = width() - _textMargins.left() - _textMargins.right() - _st.placeholderMargins.left() - _st.placeholderMargins.right() - 1;
4006 	if (_st.placeholderScale > 0.) {
4007 		auto placeholderFont = _st.placeholderFont->f;
4008 		placeholderFont.setStyleStrategy(QFont::PreferMatch);
4009 		const auto metrics = QFontMetrics(placeholderFont);
4010 		_placeholder = metrics.elidedText(text, Qt::ElideRight, availableWidth);
4011 		_placeholderPath = QPainterPath();
4012 		if (!_placeholder.isEmpty()) {
4013 			_placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder);
4014 		}
4015 	} else {
4016 		_placeholder = _st.placeholderFont->elided(text, availableWidth);
4017 	}
4018 	update();
4019 }
4020 
setPlaceholder(rpl::producer<QString> placeholder)4021 void MaskedInputField::setPlaceholder(rpl::producer<QString> placeholder) {
4022 	_placeholderFull = std::move(placeholder);
4023 }
4024 
contextMenuEvent(QContextMenuEvent * e)4025 void MaskedInputField::contextMenuEvent(QContextMenuEvent *e) {
4026 	if (const auto menu = createStandardContextMenu()) {
4027 		(new PopupMenu(this, menu))->popup(e->globalPos());
4028 	}
4029 }
4030 
inputMethodEvent(QInputMethodEvent * e)4031 void MaskedInputField::inputMethodEvent(QInputMethodEvent *e) {
4032 	QLineEdit::inputMethodEvent(e);
4033 	_lastPreEditText = e->preeditString();
4034 	update();
4035 }
4036 
showError()4037 void MaskedInputField::showError() {
4038 	showErrorNoFocus();
4039 	if (!hasFocus()) {
4040 		setFocus();
4041 	}
4042 }
4043 
showErrorNoFocus()4044 void MaskedInputField::showErrorNoFocus() {
4045 	setErrorShown(true);
4046 }
4047 
hideError()4048 void MaskedInputField::hideError() {
4049 	setErrorShown(false);
4050 }
4051 
setErrorShown(bool error)4052 void MaskedInputField::setErrorShown(bool error) {
4053 	if (_error != error) {
4054 		_error = error;
4055 		_a_error.start([this] { update(); }, _error ? 0. : 1., _error ? 1. : 0., _st.duration);
4056 		startBorderAnimation();
4057 	}
4058 }
4059 
sizeHint() const4060 QSize MaskedInputField::sizeHint() const {
4061 	return geometry().size();
4062 }
4063 
minimumSizeHint() const4064 QSize MaskedInputField::minimumSizeHint() const {
4065 	return geometry().size();
4066 }
4067 
setDisplayFocused(bool focused)4068 void MaskedInputField::setDisplayFocused(bool focused) {
4069 	setFocused(focused);
4070 	finishAnimating();
4071 }
4072 
finishAnimating()4073 void MaskedInputField::finishAnimating() {
4074 	_a_focused.stop();
4075 	_a_error.stop();
4076 	_a_placeholderShifted.stop();
4077 	_a_borderShown.stop();
4078 	_a_borderOpacity.stop();
4079 	update();
4080 }
4081 
setPlaceholderHidden(bool forcePlaceholderHidden)4082 void MaskedInputField::setPlaceholderHidden(bool forcePlaceholderHidden) {
4083 	_forcePlaceholderHidden = forcePlaceholderHidden;
4084 	startPlaceholderAnimation();
4085 }
4086 
startPlaceholderAnimation()4087 void MaskedInputField::startPlaceholderAnimation() {
4088 	auto placeholderShifted = _forcePlaceholderHidden || (_focused && _st.placeholderScale > 0.) || !getLastText().isEmpty();
4089 	if (_placeholderShifted != placeholderShifted) {
4090 		_placeholderShifted = placeholderShifted;
4091 		_a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration);
4092 	}
4093 }
4094 
placeholderRect() const4095 QRect MaskedInputField::placeholderRect() const {
4096 	return rect().marginsRemoved(_textMargins + _st.placeholderMargins);
4097 }
4098 
placeholderAdditionalPrepare(Painter & p)4099 void MaskedInputField::placeholderAdditionalPrepare(Painter &p) {
4100 	p.setFont(_st.font);
4101 	p.setPen(_st.placeholderFg);
4102 }
4103 
keyPressEvent(QKeyEvent * e)4104 void MaskedInputField::keyPressEvent(QKeyEvent *e) {
4105 	QString wasText(_oldtext);
4106 	int32 wasCursor(_oldcursor);
4107 
4108 	if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) {
4109 		e->ignore();
4110 	} else {
4111 		QLineEdit::keyPressEvent(e);
4112 	}
4113 
4114 	auto newText = text();
4115 	auto newCursor = cursorPosition();
4116 	if (wasText == newText && wasCursor == newCursor) { // call correct manually
4117 		correctValue(wasText, wasCursor, newText, newCursor);
4118 		_oldtext = newText;
4119 		_oldcursor = newCursor;
4120 		if (wasText != _oldtext) changed();
4121 		startPlaceholderAnimation();
4122 	}
4123 	if (e->key() == Qt::Key_Escape) {
4124 		e->ignore();
4125 		cancelled();
4126 	} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
4127 		submitted(e->modifiers());
4128 #ifdef Q_OS_MAC
4129 	} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
4130 		auto selected = selectedText();
4131 		if (!selected.isEmpty() && echoMode() == QLineEdit::Normal) {
4132 			QGuiApplication::clipboard()->setText(selected, QClipboard::FindBuffer);
4133 		}
4134 #endif // Q_OS_MAC
4135 	}
4136 }
4137 
onTextEdited()4138 void MaskedInputField::onTextEdited() {
4139 	QString wasText(_oldtext), newText(text());
4140 	int32 wasCursor(_oldcursor), newCursor(cursorPosition());
4141 
4142 	correctValue(wasText, wasCursor, newText, newCursor);
4143 	_oldtext = newText;
4144 	_oldcursor = newCursor;
4145 	if (wasText != _oldtext) changed();
4146 	startPlaceholderAnimation();
4147 
4148 	Integration::Instance().textActionsUpdated();
4149 }
4150 
onTextChange(const QString & text)4151 void MaskedInputField::onTextChange(const QString &text) {
4152 	_oldtext = QLineEdit::text();
4153 	setErrorShown(false);
4154 	Integration::Instance().textActionsUpdated();
4155 }
4156 
onCursorPositionChanged(int oldPosition,int position)4157 void MaskedInputField::onCursorPositionChanged(int oldPosition, int position) {
4158 	_oldcursor = position;
4159 }
4160 
PasswordInput(QWidget * parent,const style::InputField & st,rpl::producer<QString> placeholder,const QString & val)4161 PasswordInput::PasswordInput(
4162 	QWidget *parent,
4163 	const style::InputField &st,
4164 	rpl::producer<QString> placeholder,
4165 	const QString &val)
4166 : MaskedInputField(parent, st, std::move(placeholder), val) {
4167 	setEchoMode(QLineEdit::Password);
4168 }
4169 
NumberInput(QWidget * parent,const style::InputField & st,rpl::producer<QString> placeholder,const QString & value,int limit)4170 NumberInput::NumberInput(
4171 	QWidget *parent,
4172 	const style::InputField &st,
4173 	rpl::producer<QString> placeholder,
4174 	const QString &value,
4175 	int limit)
4176 : MaskedInputField(parent, st, std::move(placeholder), value)
4177 , _limit(limit) {
4178 	if (!value.toInt() || (limit > 0 && value.toInt() > limit)) {
4179 		setText(QString());
4180 	}
4181 }
4182 
correctValue(const QString & was,int wasCursor,QString & now,int & nowCursor)4183 void NumberInput::correctValue(
4184 		const QString &was,
4185 		int wasCursor,
4186 		QString &now,
4187 		int &nowCursor) {
4188 	QString newText;
4189 	newText.reserve(now.size());
4190 	auto newPos = nowCursor;
4191 	for (auto i = 0, l = int(now.size()); i < l; ++i) {
4192 		if (now.at(i).isDigit()) {
4193 			newText.append(now.at(i));
4194 		} else if (i < nowCursor) {
4195 			--newPos;
4196 		}
4197 	}
4198 	if (!newText.toInt()) {
4199 		newText = QString();
4200 		newPos = 0;
4201 	} else if (_limit > 0 && newText.toInt() > _limit) {
4202 		newText = was;
4203 		newPos = wasCursor;
4204 	}
4205 	setCorrectedText(now, nowCursor, newText, newPos);
4206 }
4207 
HexInput(QWidget * parent,const style::InputField & st,rpl::producer<QString> placeholder,const QString & val)4208 HexInput::HexInput(
4209 	QWidget *parent,
4210 	const style::InputField &st,
4211 	rpl::producer<QString> placeholder,
4212 	const QString &val)
4213 : MaskedInputField(parent, st, std::move(placeholder), val) {
4214 	if (!QRegularExpression("^[a-fA-F0-9]+$").match(val).hasMatch()) {
4215 		setText(QString());
4216 	}
4217 }
4218 
correctValue(const QString & was,int wasCursor,QString & now,int & nowCursor)4219 void HexInput::correctValue(
4220 		const QString &was,
4221 		int wasCursor,
4222 		QString &now,
4223 		int &nowCursor) {
4224 	QString newText;
4225 	newText.reserve(now.size());
4226 	auto newPos = nowCursor;
4227 	for (auto i = 0, l = int(now.size()); i < l; ++i) {
4228 		const auto ch = now[i];
4229 		if ((ch >= '0' && ch <= '9')
4230 			|| (ch >= 'a' && ch <= 'f')
4231 			|| (ch >= 'A' && ch <= 'F')) {
4232 			newText.append(ch);
4233 		} else if (i < nowCursor) {
4234 			--newPos;
4235 		}
4236 	}
4237 	setCorrectedText(now, nowCursor, newText, newPos);
4238 }
4239 
4240 } // namespace Ui
4241