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