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