/* This file is part of Telegram Desktop, the official desktop version of Telegram messaging app, see https://telegram.org Telegram Desktop is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. In addition, as a special exception, the copyright holders give permission to link the code of portions of this program with the OpenSSL library. Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org */ #include "stdafx.h" #include "ui/widgets/input_fields.h" #include "ui/widgets/popup_menu.h" #include "mainwindow.h" #include "ui/countryinput.h" #include "window/window_theme.h" #include "lang.h" #include "numbers.h" namespace Ui { namespace { 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; } // namespace QByteArray FlatTextarea::serializeTagsList(const TagList &tags) { if (tags.isEmpty()) { return QByteArray(); } QByteArray tagsSerialized; { QBuffer buffer(&tagsSerialized); buffer.open(QIODevice::WriteOnly); QDataStream stream(&buffer); stream.setVersion(QDataStream::Qt_5_1); stream << qint32(tags.size()); for_const (auto &tag, tags) { stream << qint32(tag.offset) << qint32(tag.length) << tag.id; } } return tagsSerialized; } FlatTextarea::TagList FlatTextarea::deserializeTagsList(QByteArray data, int textLength) { TagList result; if (data.isEmpty()) { return result; } QBuffer buffer(&data); buffer.open(QIODevice::ReadOnly); QDataStream stream(&buffer); stream.setVersion(QDataStream::Qt_5_1); qint32 tagCount = 0; stream >> tagCount; if (stream.status() != QDataStream::Ok) { return result; } if (tagCount <= 0 || tagCount > textLength) { return result; } for (int i = 0; i < tagCount; ++i) { qint32 offset = 0, length = 0; QString id; stream >> offset >> length >> id; if (stream.status() != QDataStream::Ok) { return result; } if (offset < 0 || length <= 0 || offset + length > textLength) { return result; } result.push_back({ offset, length, id }); } return result; } QString FlatTextarea::tagsMimeType() { return qsl("application/x-td-field-tags"); } FlatTextarea::FlatTextarea(QWidget *parent, const style::FlatTextarea &st, const QString &pholder, const QString &v, const TagList &tags) : QTextEdit(parent) , _placeholderVisible(!v.length()) , _lastTextWithTags { v, tags } , _st(st) { setCursor(style::cur_text); setAcceptRichText(false); resize(_st.width, _st.font->height); setFont(_st.font->f); setAlignment(_st.align); setPlaceholder(pholder); subscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) { if (update.paletteChanged()) { updatePalette(); } }); updatePalette(); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setFrameStyle(QFrame::NoFrame | QFrame::Plain); viewport()->setAutoFillBackground(false); setContentsMargins(0, 0, 0, 0); switch (cScale()) { case dbisOneAndQuarter: _fakeMargin = 1; break; case dbisOneAndHalf: _fakeMargin = 2; break; case dbisTwo: _fakeMargin = 4; break; } setStyleSheet(qsl("QTextEdit { margin: %1px; }").arg(_fakeMargin)); viewport()->setAttribute(Qt::WA_AcceptTouchEvents); _touchTimer.setSingleShot(true); connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer())); connect(document(), SIGNAL(contentsChange(int, int, int)), this, SLOT(onDocumentContentsChange(int, int, int))); connect(document(), SIGNAL(contentsChanged()), this, SLOT(onDocumentContentsChanged())); connect(this, SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool))); connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool))); if (App::wnd()) connect(this, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu())); if (!_lastTextWithTags.text.isEmpty()) { setTextWithTags(_lastTextWithTags, ClearUndoHistory); } } void FlatTextarea::updatePalette() { auto p = palette(); p.setColor(QPalette::Text, _st.textColor->c); setPalette(p); } TextWithTags FlatTextarea::getTextWithTagsPart(int start, int end) { TextWithTags result; result.text = getTextPart(start, end, &result.tags); return result; } void FlatTextarea::setTextWithTags(const TextWithTags &textWithTags, UndoHistoryAction undoHistoryAction) { _insertedTags = textWithTags.tags; _insertedTagsAreFromMime = false; _realInsertPosition = 0; _realCharsAdded = textWithTags.text.size(); auto doc = document(); auto cursor = QTextCursor(doc->docHandle(), 0); if (undoHistoryAction == ClearUndoHistory) { doc->setUndoRedoEnabled(false); cursor.beginEditBlock(); } else if (undoHistoryAction == MergeWithUndoHistory) { cursor.joinPreviousEditBlock(); } else { cursor.beginEditBlock(); } cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); cursor.insertText(textWithTags.text); cursor.movePosition(QTextCursor::End); cursor.endEditBlock(); if (undoHistoryAction == ClearUndoHistory) { doc->setUndoRedoEnabled(true); } _insertedTags.clear(); _realInsertPosition = -1; finishPlaceholder(); } void FlatTextarea::finishPlaceholder() { _a_placeholderFocused.finish(); _a_placeholderVisible.finish(); update(); } void FlatTextarea::setMaxLength(int32 maxLength) { _maxLength = maxLength; } void FlatTextarea::setMinHeight(int32 minHeight) { _minHeight = minHeight; heightAutoupdated(); } void FlatTextarea::setMaxHeight(int32 maxHeight) { _maxHeight = maxHeight; heightAutoupdated(); } bool FlatTextarea::heightAutoupdated() { if (_minHeight < 0 || _maxHeight < 0 || _inHeightCheck) return false; _inHeightCheck = true; myEnsureResized(this); int newh = ceil(document()->size().height()) + 2 * fakeMargin(); if (newh > _maxHeight) { newh = _maxHeight; } else if (newh < _minHeight) { newh = _minHeight; } if (height() != newh) { resize(width(), newh); _inHeightCheck = false; return true; } _inHeightCheck = false; return false; } void FlatTextarea::onTouchTimer() { _touchRightButton = true; } bool FlatTextarea::viewportEvent(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 QTextEdit::viewportEvent(e); } } return QTextEdit::viewportEvent(e); } void FlatTextarea::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; 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); } } _touchTimer.stop(); _touchPress = _touchMove = _touchRightButton = false; break; case QEvent::TouchCancel: _touchPress = false; _touchTimer.stop(); break; } } QRect FlatTextarea::getTextRect() const { return rect().marginsRemoved(_st.textMrg + st::textRectMargins); } int32 FlatTextarea::fakeMargin() const { return _fakeMargin; } void FlatTextarea::paintEvent(QPaintEvent *e) { Painter p(viewport()); auto ms = getms(); auto r = rect().intersected(e->rect()); p.fillRect(r, _st.bgColor); auto placeholderOpacity = _a_placeholderVisible.current(ms, _placeholderVisible ? 1. : 0.); if (placeholderOpacity > 0.) { p.setOpacity(placeholderOpacity); auto placeholderLeft = anim::interpolate(_st.phShift, 0, placeholderOpacity); p.save(); p.setClipRect(r); p.setFont(_st.font); p.setPen(anim::pen(_st.phColor, _st.phFocusColor, _a_placeholderFocused.current(ms, hasFocus() ? 1. : 0.))); if (_st.phAlign == style::al_topleft && _phAfter > 0) { int skipWidth = placeholderSkipWidth(); p.drawText(_st.textMrg.left() - _fakeMargin + placeholderLeft + skipWidth, _st.textMrg.top() - _fakeMargin - st::lineWidth + _st.font->ascent, _ph); } else { QRect phRect(_st.textMrg.left() - _fakeMargin + _st.phPos.x() + placeholderLeft, _st.textMrg.top() - _fakeMargin + _st.phPos.y(), width() - _st.textMrg.left() - _st.textMrg.right(), height() - _st.textMrg.top() - _st.textMrg.bottom()); p.drawText(phRect, _ph, QTextOption(_st.phAlign)); } p.restore(); p.setOpacity(1); } QTextEdit::paintEvent(e); } int FlatTextarea::placeholderSkipWidth() const { if (!_phAfter) { return 0; } auto text = getTextWithTags().text; auto result = _st.font->width(text.mid(0, _phAfter)); if (_phAfter > text.size()) { result += _st.font->spacew; } return result; } void FlatTextarea::focusInEvent(QFocusEvent *e) { _a_placeholderFocused.start([this] { update(); }, 0., 1., _st.phDuration); QTextEdit::focusInEvent(e); } void FlatTextarea::focusOutEvent(QFocusEvent *e) { _a_placeholderFocused.start([this] { update(); }, 1., 0., _st.phDuration); QTextEdit::focusOutEvent(e); } QSize FlatTextarea::sizeHint() const { return geometry().size(); } QSize FlatTextarea::minimumSizeHint() const { return geometry().size(); } EmojiPtr FlatTextarea::getSingleEmoji() const { QString text; QTextFragment fragment; getSingleEmojiFragment(text, fragment); if (!text.isEmpty()) { QTextCharFormat format = fragment.charFormat(); QString imageName = static_cast(&format)->name(); if (imageName.startsWith(qstr("emoji://e."))) { return emojiFromUrl(imageName); } } return nullptr; } QString FlatTextarea::getInlineBotQuery(UserData **outInlineBot, QString *outInlineBotUsername) const { t_assert(outInlineBot != nullptr); t_assert(outInlineBotUsername != nullptr); auto &text = getTextWithTags().text; auto textLength = text.size(); int inlineUsernameStart = 1, inlineUsernameLength = 0; if (textLength > 2 && text.at(0) == '@' && text.at(1).isLetter()) { inlineUsernameLength = 1; for (int i = inlineUsernameStart + 1; i != textLength; ++i) { if (text.at(i).isLetterOrNumber() || text.at(i).unicode() == '_') { ++inlineUsernameLength; continue; } if (!text.at(i).isSpace()) { inlineUsernameLength = 0; } break; } auto inlineUsernameEnd = inlineUsernameStart + inlineUsernameLength; auto inlineUsernameEqualsText = (inlineUsernameEnd == textLength); auto validInlineUsername = false; if (inlineUsernameEqualsText) { validInlineUsername = text.endsWith(qstr("bot")); } else if (inlineUsernameEnd < textLength && inlineUsernameLength) { validInlineUsername = text.at(inlineUsernameEnd).isSpace(); } if (validInlineUsername) { auto username = text.midRef(inlineUsernameStart, inlineUsernameLength); if (username != *outInlineBotUsername) { *outInlineBotUsername = username.toString(); auto peer = App::peerByName(*outInlineBotUsername); if (peer) { if (peer->isUser()) { *outInlineBot = peer->asUser(); } else { *outInlineBot = nullptr; } } else { *outInlineBot = LookingUpInlineBot; } } if (*outInlineBot == LookingUpInlineBot) return QString(); if (*outInlineBot && (!(*outInlineBot)->botInfo || (*outInlineBot)->botInfo->inlinePlaceholder.isEmpty())) { *outInlineBot = nullptr; } else { return inlineUsernameEqualsText ? QString() : text.mid(inlineUsernameEnd + 1); } } else { inlineUsernameLength = 0; } } if (inlineUsernameLength < 3) { *outInlineBot = nullptr; *outInlineBotUsername = QString(); } return QString(); } QString FlatTextarea::getMentionHashtagBotCommandPart(bool &start) const { start = false; int32 pos = textCursor().position(); if (textCursor().anchor() != pos) return QString(); // check mention / hashtag / bot command QTextDocument *doc(document()); QTextBlock block = doc->findBlock(pos); for (QTextBlock::Iterator iter = block.begin(); !iter.atEnd(); ++iter) { QTextFragment fr(iter.fragment()); if (!fr.isValid()) continue; int32 p = fr.position(), e = (p + fr.length()); if (p >= pos || e < pos) continue; QTextCharFormat f = fr.charFormat(); if (f.isImageFormat()) continue; bool mentionInCommand = false; QString t(fr.text()); for (int i = pos - p; i > 0; --i) { if (t.at(i - 1) == '@') { if ((pos - p - i < 1 || t.at(i).isLetter()) && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) { start = (i == 1) && (p == 0); return t.mid(i - 1, pos - p - i + 1); } else if ((pos - p - i < 1 || t.at(i).isLetter()) && i > 2 && (t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_') && !mentionInCommand) { mentionInCommand = true; --i; continue; } return QString(); } else if (t.at(i - 1) == '#') { if (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_')) { start = (i == 1) && (p == 0); return t.mid(i - 1, pos - p - i + 1); } return QString(); } else if (t.at(i - 1) == '/') { if (i < 2) { start = (i == 1) && (p == 0); return t.mid(i - 1, pos - p - i + 1); } return QString(); } if (pos - p - i > 127 || (!mentionInCommand && (pos - p - i > 63))) break; if (!t.at(i - 1).isLetterOrNumber() && t.at(i - 1) != '_') break; } break; } return QString(); } void FlatTextarea::insertTag(const QString &text, QString tagId) { auto cursor = textCursor(); int32 pos = cursor.position(); auto doc = document(); auto block = doc->findBlock(pos); for (auto iter = block.begin(); !iter.atEnd(); ++iter) { auto fragment = iter.fragment(); t_assert(fragment.isValid()); int fragmentPosition = fragment.position(); int fragmentEnd = (fragmentPosition + fragment.length()); if (fragmentPosition >= pos || fragmentEnd < pos) continue; auto format = fragment.charFormat(); if (format.isImageFormat()) continue; bool mentionInCommand = false; auto fragmentText = fragment.text(); for (int i = pos - fragmentPosition; i > 0; --i) { auto previousChar = fragmentText.at(i - 1); if (previousChar == '@' || previousChar == '#' || previousChar == '/') { if ((i == pos - fragmentPosition || (previousChar == '/' ? fragmentText.at(i).isLetterOrNumber() : fragmentText.at(i).isLetter()) || previousChar == '#') && (i < 2 || !(fragmentText.at(i - 2).isLetterOrNumber() || fragmentText.at(i - 2) == '_'))) { cursor.setPosition(fragmentPosition + i - 1); int till = fragmentPosition + i; for (; (till < fragmentEnd && till < pos); ++till) { auto ch = fragmentText.at(till - fragmentPosition); if (!ch.isLetterOrNumber() && ch != '_' && ch != '@') { break; } } if (till < fragmentEnd && fragmentText.at(till - fragmentPosition) == ' ') { ++till; } cursor.setPosition(till, QTextCursor::KeepAnchor); break; } else if ((i == pos - fragmentPosition || fragmentText.at(i).isLetter()) && fragmentText.at(i - 1) == '@' && i > 2 && (fragmentText.at(i - 2).isLetterOrNumber() || fragmentText.at(i - 2) == '_') && !mentionInCommand) { mentionInCommand = true; --i; continue; } break; } if (pos - fragmentPosition - i > 127 || (!mentionInCommand && (pos - fragmentPosition - i > 63))) break; if (!fragmentText.at(i - 1).isLetterOrNumber() && fragmentText.at(i - 1) != '_') break; } break; } if (tagId.isEmpty()) { QTextCharFormat format = cursor.charFormat(); format.setAnchor(false); format.setAnchorName(QString()); format.clearForeground(); cursor.insertText(text + ' ', format); } else { _insertedTags.clear(); _insertedTags.push_back({ 0, text.size(), tagId }); _insertedTagsAreFromMime = false; cursor.insertText(text + ' '); _insertedTags.clear(); } } void FlatTextarea::setTagMimeProcessor(std_::unique_ptr &&processor) { _tagMimeProcessor = std_::move(processor); } void FlatTextarea::getSingleEmojiFragment(QString &text, QTextFragment &fragment) const { int32 end = textCursor().position(), start = end - 1; if (textCursor().anchor() != end) return; if (start < 0) start = 0; QTextDocument *doc(document()); QTextBlock from = doc->findBlock(start), till = doc->findBlock(end); if (till.isValid()) till = till.next(); for (QTextBlock b = from; b != till; b = b.next()) { for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) { QTextFragment fr(iter.fragment()); if (!fr.isValid()) continue; int32 p = fr.position(), e = (p + fr.length()); if (p >= end || e <= start) { continue; } QTextCharFormat f = fr.charFormat(); QString t(fr.text()); if (p < start) { t = t.mid(start - p, end - start); } else if (e > end) { t = t.mid(0, end - p); } if (f.isImageFormat() && !t.isEmpty() && t.at(0).unicode() == QChar::ObjectReplacementCharacter) { QString imageName = static_cast(&f)->name(); if (imageName.startsWith(qstr("emoji://e."))) { fragment = fr; text = t; return; } } return; } } return; } void FlatTextarea::removeSingleEmoji() { QString text; QTextFragment fragment; getSingleEmojiFragment(text, fragment); if (!text.isEmpty()) { QTextCursor t(textCursor()); t.setPosition(fragment.position()); t.setPosition(fragment.position() + fragment.length(), QTextCursor::KeepAnchor); t.removeSelectedText(); setTextCursor(t); } } namespace { class TagAccumulator { public: TagAccumulator(FlatTextarea::TagList *tags) : _tags(tags) { } bool changed() const { return _changed; } void feed(const QString &randomTagId, int currentPosition) { if (randomTagId == _currentTagId) return; if (!_currentTagId.isEmpty()) { int randomPartPosition = _currentTagId.lastIndexOf('/'); t_assert(randomPartPosition > 0); bool tagChanged = true; if (_currentTag < _tags->size()) { auto &alreadyTag = _tags->at(_currentTag); if (alreadyTag.offset == _currentStart && alreadyTag.length == currentPosition - _currentStart && alreadyTag.id == _currentTagId.midRef(0, randomPartPosition)) { tagChanged = false; } } if (tagChanged) { _changed = true; TextWithTags::Tag tag = { _currentStart, currentPosition - _currentStart, _currentTagId.mid(0, randomPartPosition), }; if (_currentTag < _tags->size()) { (*_tags)[_currentTag] = tag; } else { _tags->push_back(tag); } } ++_currentTag; } _currentTagId = randomTagId; _currentStart = currentPosition; }; void finish() { if (_currentTag < _tags->size()) { _tags->resize(_currentTag); _changed = true; } } private: FlatTextarea::TagList *_tags; bool _changed = false; int _currentTag = 0; int _currentStart = 0; QString _currentTagId; }; } // namespace QString FlatTextarea::getTextPart(int start, int end, TagList *outTagsList, bool *outTagsChanged) const { if (end >= 0 && end <= start) return QString(); if (start < 0) start = 0; bool full = (start == 0) && (end < 0); TagAccumulator tagAccumulator(outTagsList); QTextDocument *doc(document()); QTextBlock from = full ? doc->begin() : doc->findBlock(start), till = (end < 0) ? doc->end() : doc->findBlock(end); if (till.isValid()) till = till.next(); int32 possibleLen = 0; for (QTextBlock b = from; b != till; b = b.next()) { possibleLen += b.length(); } QString result; result.reserve(possibleLen + 1); if (!full && end < 0) { end = possibleLen; } bool tillFragmentEnd = full; for (auto b = from; b != till; b = b.next()) { for (auto iter = b.begin(); !iter.atEnd(); ++iter) { QTextFragment fragment(iter.fragment()); if (!fragment.isValid()) continue; int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length()); if (!full) { tillFragmentEnd = (e <= end); if (p == end) { tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); } if (p >= end) { break; } if (e <= start) { continue; } } if (full || p >= start) { tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); } QTextCharFormat f = fragment.charFormat(); QString emojiText; QString t(fragment.text()); if (!full) { if (p < start) { t = t.mid(start - p, end - start); } else if (e > end) { t = t.mid(0, end - p); } } QChar *ub = t.data(), *uc = ub, *ue = uc + t.size(); for (; uc != ue; ++uc) { switch (uc->unicode()) { case 0xfdd0: // QTextBeginningOfFrame case 0xfdd1: // QTextEndOfFrame case QChar::ParagraphSeparator: case QChar::LineSeparator: *uc = QLatin1Char('\n'); break; case QChar::Nbsp: *uc = QLatin1Char(' '); break; case QChar::ObjectReplacementCharacter: if (emojiText.isEmpty() && f.isImageFormat()) { QString imageName = static_cast(&f)->name(); if (imageName.startsWith(qstr("emoji://e."))) { if (EmojiPtr emoji = emojiFromUrl(imageName)) { emojiText = emojiString(emoji); } } } if (uc > ub) result.append(ub, uc - ub); if (!emojiText.isEmpty()) result.append(emojiText); ub = uc + 1; break; } } if (uc > ub) result.append(ub, uc - ub); } result.append('\n'); } result.chop(1); if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size()); tagAccumulator.finish(); if (outTagsChanged) { *outTagsChanged = tagAccumulator.changed(); } return result; } bool FlatTextarea::hasText() const { QTextDocument *doc(document()); QTextBlock from = doc->begin(), till = doc->end(); if (from == till) return false; for (QTextBlock::Iterator iter = from.begin(); !iter.atEnd(); ++iter) { QTextFragment fragment(iter.fragment()); if (!fragment.isValid()) continue; if (!fragment.text().isEmpty()) return true; } return (from.next() != till); } bool FlatTextarea::isUndoAvailable() const { return _undoAvailable; } bool FlatTextarea::isRedoAvailable() const { return _redoAvailable; } void FlatTextarea::parseLinks() { // some code is duplicated in text.cpp! LinkRanges newLinks; QString text(toPlainText()); if (text.isEmpty()) { if (!_links.isEmpty()) { _links.clear(); emit linksChanged(); } return; } initLinkSets(); int32 len = text.size(); const QChar *start = text.unicode(), *end = start + text.size(); for (int32 offset = 0, matchOffset = offset; offset < len;) { QRegularExpressionMatch m = reDomain().match(text, matchOffset); if (!m.hasMatch()) break; int32 domainOffset = m.capturedStart(); QString protocol = m.captured(1).toLower(); QString topDomain = m.captured(3).toLower(); bool isProtocolValid = protocol.isEmpty() || validProtocols().contains(hashCrc32(protocol.constData(), protocol.size() * sizeof(QChar))); bool isTopDomainValid = !protocol.isEmpty() || validTopDomains().contains(hashCrc32(topDomain.constData(), topDomain.size() * sizeof(QChar))); if (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) { QString forMailName = text.mid(offset, domainOffset - offset - 1); QRegularExpressionMatch mMailName = reMailName().match(forMailName); if (mMailName.hasMatch()) { offset = matchOffset = m.capturedEnd(); continue; } } if (!isProtocolValid || !isTopDomainValid) { offset = matchOffset = m.capturedEnd(); continue; } QStack parenth; const QChar *domainEnd = start + m.capturedEnd(), *p = domainEnd; for (; p < end; ++p) { QChar ch(*p); if (chIsLinkEnd(ch)) break; // link finished if (chIsAlmostLinkEnd(ch)) { const QChar *endTest = p + 1; while (endTest < end && chIsAlmostLinkEnd(*endTest)) { ++endTest; } if (endTest >= end || chIsLinkEnd(*endTest)) { break; // link finished at p } p = endTest; ch = *p; } if (ch == '(' || ch == '[' || ch == '{' || ch == '<') { parenth.push(p); } else if (ch == ')' || ch == ']' || ch == '}' || ch == '>') { if (parenth.isEmpty()) break; const QChar *q = parenth.pop(), open(*q); if ((ch == ')' && open != '(') || (ch == ']' && open != '[') || (ch == '}' && open != '{') || (ch == '>' && open != '<')) { p = q; break; } } } if (p > domainEnd) { // check, that domain ended if (domainEnd->unicode() != '/' && domainEnd->unicode() != '?') { matchOffset = domainEnd - start; continue; } } newLinks.push_back({ domainOffset - 1, static_cast(p - start - domainOffset + 2) }); offset = matchOffset = p - start; } if (newLinks != _links) { _links = newLinks; emit linksChanged(); } } QStringList FlatTextarea::linksList() const { QStringList result; if (!_links.isEmpty()) { QString text(toPlainText()); for_const (auto &link, _links) { result.push_back(text.mid(link.start + 1, link.length - 2)); } } return result; } void FlatTextarea::insertFromMimeData(const QMimeData *source) { auto mime = tagsMimeType(); auto text = source->text(); if (source->hasFormat(mime)) { auto tagsData = source->data(mime); _insertedTags = deserializeTagsList(tagsData, text.size()); _insertedTagsAreFromMime = true; } else { _insertedTags.clear(); } auto cursor = textCursor(); _realInsertPosition = qMin(cursor.position(), cursor.anchor()); _realCharsAdded = text.size(); QTextEdit::insertFromMimeData(source); if (!_inDrop) { emit spacedReturnedPasted(); _insertedTags.clear(); _realInsertPosition = -1; } } void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) { QTextImageFormat imageFormat; int32 ew = ESize + st::emojiPadding * cIntRetinaFactor() * 2, eh = _st.font->height * cIntRetinaFactor(); imageFormat.setWidth(ew / cIntRetinaFactor()); imageFormat.setHeight(eh / cIntRetinaFactor()); imageFormat.setName(qsl("emoji://e.") + QString::number(emojiKey(emoji), 16)); imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline); if (c.charFormat().isAnchor()) { imageFormat.setAnchor(true); imageFormat.setAnchorName(c.charFormat().anchorName()); imageFormat.setForeground(st::defaultTextPalette.linkFg); } static QString objectReplacement(QChar::ObjectReplacementCharacter); c.insertText(objectReplacement, imageFormat); } QVariant FlatTextarea::loadResource(int type, const QUrl &name) { QString imageName = name.toDisplayString(); if (imageName.startsWith(qstr("emoji://e."))) { if (EmojiPtr emoji = emojiFromUrl(imageName)) { return QVariant(App::emojiSingle(emoji, _st.font->height)); } } return QVariant(); } void FlatTextarea::checkContentHeight() { if (heightAutoupdated()) { emit resized(); } } namespace { // Optimization: with null page size document does not re-layout // on each insertText / mergeCharFormat. void prepareFormattingOptimization(QTextDocument *document) { if (!document->pageSize().isNull()) { document->setPageSize(QSizeF(0, 0)); } } void removeTags(style::color textFg, QTextDocument *document, int from, int end) { QTextCursor c(document->docHandle(), 0); c.setPosition(from); c.setPosition(end, QTextCursor::KeepAnchor); QTextCharFormat format; format.setAnchor(false); format.setAnchorName(QString()); format.setForeground(textFg); c.mergeCharFormat(format); } // Returns the position of the first inserted tag or "changedEnd" value if none found. int processInsertedTags(style::color textFg, QTextDocument *document, int changedPosition, int changedEnd, const FlatTextarea::TagList &tags, FlatTextarea::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) { removeTags(textFg, document, applyNoTagFrom, tagFrom); } QTextCursor c(document->docHandle(), 0); c.setPosition(tagFrom); c.setPosition(tagTo, QTextCursor::KeepAnchor); QTextCharFormat format; format.setAnchor(true); format.setAnchorName(tagId + '/' + QString::number(rand_value())); format.setForeground(st::defaultTextPalette.linkFg); c.mergeCharFormat(format); applyNoTagFrom = tagTo; } } if (applyNoTagFrom < changedEnd) { removeTags(textFg, 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) { auto insertTagName = fragmentIt.fragment().charFormat().anchorName(); while (true) { for (; !fragmentIt.atEnd(); ++fragmentIt) { auto fragment = fragmentIt.fragment(); bool fragmentOutsideInsertion = (fragment.position() >= insertionEnd); if (fragmentOutsideInsertion) { return (fragment.charFormat().anchorName() != insertTagName); } int fragmentEnd = fragment.position() + fragment.length(); bool notFullFragmentInserted = (fragmentEnd > 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, }; Type type = Type::Invalid; EmojiPtr emoji = nullptr; bool isTilde = false; int intervalStart = 0; int intervalEnd = 0; }; } // namespace void FlatTextarea::processFormatting(int insertPosition, int insertEnd) { // Tilde formatting. auto regularFont = qsl("Open Sans"), semiboldFont = qsl("Open Sans Semibold"); bool tildeFormatting = !cRetina() && (font().pixelSize() == 13) && (font().family() == regularFont); bool isTildeFragment = false; // First tag handling (the one we inserted text to). bool startTagFound = false; bool breakTagOnNotLetter = false; auto doc = document(); // Apply inserted tags. auto insertedTagsProcessor = _insertedTagsAreFromMime ? _tagMimeProcessor.get() : nullptr; int breakTagOnNotLetterTill = processInsertedTags(_st.textColor, doc, insertPosition, insertEnd, _insertedTags, insertedTagsProcessor); using ActionType = FormattingAction::Type; while (true) { FormattingAction action; auto fromBlock = doc->findBlock(insertPosition); auto tillBlock = doc->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(); t_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 charFormat = fragment.charFormat(); if (tildeFormatting) { isTildeFragment = (charFormat.fontFamily() == semiboldFont); } auto fragmentText = fragment.text(); auto *textStart = fragmentText.constData(); auto *textEnd = textStart + fragmentText.size(); if (!startTagFound) { startTagFound = true; auto tagName = charFormat.anchorName(); if (!tagName.isEmpty()) { breakTagOnNotLetter = wasInsertTillTheEndOfTag(block, fragmentIt, insertEnd); } } auto *ch = textStart + qMax(changedPositionInFragment, 0); for (; ch < textEnd; ++ch) { int emojiLength = 0; if (auto emoji = emojiFromText(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.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; } if (action.type != ActionType::Invalid) { prepareFormattingOptimization(doc); QTextCursor c(doc->docHandle(), 0); c.setPosition(action.intervalStart); c.setPosition(action.intervalEnd, QTextCursor::KeepAnchor); if (action.type == ActionType::InsertEmoji) { insertEmoji(action.emoji, c); insertPosition = action.intervalStart + 1; } else if (action.type == ActionType::RemoveTag) { QTextCharFormat format; format.setAnchor(false); format.setAnchorName(QString()); format.setForeground(_st.textColor); c.mergeCharFormat(format); } else if (action.type == ActionType::TildeFont) { QTextCharFormat format; format.setFontFamily(action.isTilde ? semiboldFont : regularFont); c.mergeCharFormat(format); insertPosition = action.intervalEnd; } } else { break; } } } void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int charsAdded) { if (_correcting) return; int insertPosition = (_realInsertPosition >= 0) ? _realInsertPosition : position; int insertLength = (_realInsertPosition >= 0) ? _realCharsAdded : charsAdded; int removePosition = position; int removeLength = charsRemoved; QTextCursor(document()->docHandle(), 0).joinPreviousEditBlock(); _correcting = true; if (_maxLength >= 0) { QTextCursor c(document()->docHandle(), 0); c.movePosition(QTextCursor::End); int32 fullSize = c.position(), toRemove = fullSize - _maxLength; if (toRemove > 0) { if (toRemove > insertLength) { if (insertLength) { c.setPosition(insertPosition); c.setPosition((insertPosition + insertLength), QTextCursor::KeepAnchor); c.removeSelectedText(); } c.setPosition(fullSize - (toRemove - insertLength)); c.setPosition(fullSize, QTextCursor::KeepAnchor); c.removeSelectedText(); } else { c.setPosition(insertPosition + (insertLength - toRemove)); c.setPosition(insertPosition + insertLength, QTextCursor::KeepAnchor); c.removeSelectedText(); } } } _correcting = false; if (insertPosition == removePosition) { if (!_links.isEmpty()) { bool changed = false; for (auto i = _links.begin(); i != _links.end();) { if (i->start + i->length <= insertPosition) { ++i; } else if (i->start >= removePosition + removeLength) { i->start += insertLength - removeLength; ++i; } else { i = _links.erase(i); changed = true; } } if (changed) emit linksChanged(); } } else { parseLinks(); } if (document()->availableRedoSteps() > 0) { QTextCursor(document()->docHandle(), 0).endEditBlock(); return; } if (insertLength <= 0) { QTextCursor(document()->docHandle(), 0).endEditBlock(); return; } _correcting = true; auto pageSize = document()->pageSize(); processFormatting(insertPosition, insertPosition + insertLength); if (document()->pageSize() != pageSize) { document()->setPageSize(pageSize); } _correcting = false; QTextCursor(document()->docHandle(), 0).endEditBlock(); } void FlatTextarea::onDocumentContentsChanged() { if (_correcting) return; auto tagsChanged = false; auto curText = getTextPart(0, -1, &_lastTextWithTags.tags, &tagsChanged); _correcting = true; correctValue(_lastTextWithTags.text, curText, _lastTextWithTags.tags); _correcting = false; bool textOrTagsChanged = tagsChanged || (_lastTextWithTags.text != curText); if (textOrTagsChanged) { _lastTextWithTags.text = curText; emit changed(); checkContentHeight(); } updatePlaceholder(); if (App::wnd()) App::wnd()->updateGlobalMenu(); } void FlatTextarea::onUndoAvailable(bool avail) { _undoAvailable = avail; if (App::wnd()) App::wnd()->updateGlobalMenu(); } void FlatTextarea::onRedoAvailable(bool avail) { _redoAvailable = avail; if (App::wnd()) App::wnd()->updateGlobalMenu(); } void FlatTextarea::setPlaceholder(const QString &ph, int32 afterSymbols) { _ph = ph; if (_phAfter != afterSymbols) { _phAfter = afterSymbols; updatePlaceholder(); } int skipWidth = placeholderSkipWidth(); _phelided = _st.font->elided(_ph, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1 - skipWidth); if (_placeholderVisible) update(); } void FlatTextarea::updatePlaceholder() { auto placeholderVisible = (getTextWithTags().text.size() <= _phAfter); if (_placeholderVisible != placeholderVisible) { _placeholderVisible = placeholderVisible; _a_placeholderVisible.start([this] { update(); }, _placeholderVisible ? 0. : 1., _placeholderVisible ? 1. : 0., _st.phDuration); } } QMimeData *FlatTextarea::createMimeDataFromSelection() const { QMimeData *result = new QMimeData(); QTextCursor c(textCursor()); int32 start = c.selectionStart(), end = c.selectionEnd(); if (end > start) { TagList tags; result->setText(getTextPart(start, end, &tags)); if (!tags.isEmpty()) { if (_tagMimeProcessor) { for (auto &tag : tags) { tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id); } } result->setData(tagsMimeType(), serializeTagsList(tags)); } } return result; } void FlatTextarea::setSubmitSettings(SubmitSettings settings) { _submitSettings = settings; } void FlatTextarea::keyPressEvent(QKeyEvent *e) { bool shift = e->modifiers().testFlag(Qt::ShiftModifier); 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 = (ctrl && shift); if (ctrl && _submitSettings != SubmitSettings::None && _submitSettings != SubmitSettings::Enter) { enterSubmit = true; } if (!ctrl && !shift && _submitSettings != SubmitSettings::None && _submitSettings != SubmitSettings::CtrlEnter) { enterSubmit = true; } bool enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return); 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 (enter && enterSubmit) { emit submitted(ctrl && shift); } else if (e->key() == Qt::Key_Escape) { emit cancelled(); } else if (e->key() == Qt::Key_Tab || (ctrl && e->key() == Qt::Key_Backtab)) { if (ctrl) { e->ignore(); } else { emit tabbed(); } } else if (e->key() == Qt::Key_Search || e == QKeySequence::Find) { e->ignore(); #ifdef Q_OS_MAC } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) { auto cursor = textCursor(); int start = cursor.selectionStart(), end = cursor.selectionEnd(); if (end > start) { TagList tags; QApplication::clipboard()->setText(getTextPart(start, end, &tags), QClipboard::FindBuffer); } #endif // Q_OS_MAC } else { QTextCursor tc(textCursor()); if (enter && ctrl) { e->setModifiers(e->modifiers() & ~Qt::ControlModifier); } bool spaceOrReturn = false; QString t(e->text()); if (!t.isEmpty() && t.size() < 3) { if (t.at(0) == '\n' || t.at(0) == '\r' || t.at(0).isSpace() || t.at(0) == QChar::LineSeparator) { spaceOrReturn = true; } } QTextEdit::keyPressEvent(e); if (tc == textCursor()) { bool check = false; if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) { tc.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) { tc.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); check = true; } if (check) { if (tc == textCursor()) { e->ignore(); } else { setTextCursor(tc); } } } if (spaceOrReturn) emit spacedReturnedPasted(); } } void FlatTextarea::resizeEvent(QResizeEvent *e) { _phelided = _st.font->elided(_ph, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1); QTextEdit::resizeEvent(e); checkContentHeight(); } void FlatTextarea::mousePressEvent(QMouseEvent *e) { QTextEdit::mousePressEvent(e); } void FlatTextarea::dropEvent(QDropEvent *e) { _inDrop = true; QTextEdit::dropEvent(e); _inDrop = false; _insertedTags.clear(); _realInsertPosition = -1; emit spacedReturnedPasted(); } void FlatTextarea::contextMenuEvent(QContextMenuEvent *e) { if (auto menu = createStandardContextMenu()) { (new Ui::PopupMenu(menu))->popup(e->globalPos()); } } FlatInput::FlatInput(QWidget *parent, const style::FlatInput &st, const QString &pholder, const QString &v) : QLineEdit(v, parent) , _oldtext(v) , _fullph(pholder) , _placeholderVisible(!v.length()) , _st(st) { setCursor(style::cur_text); resize(_st.width, _st.height); setFont(_st.font->f); setAlignment(_st.align); 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; 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); } } _touchTimer.stop(); _touchPress = _touchMove = _touchRightButton = false; break; case QEvent::TouchCancel: _touchPress = false; _touchTimer.stop(); break; } } QRect FlatInput::getTextRect() const { return rect().marginsRemoved(_st.textMrg + QMargins(-2, -1, -2, -1)); } void FlatInput::paintEvent(QPaintEvent *e) { Painter p(this); auto ms = getms(); auto placeholderFocused = _a_placeholderFocused.current(ms, hasFocus() ? 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, _ph, QTextOption(_st.phAlign)); p.restore(); } QLineEdit::paintEvent(e); } void FlatInput::focusInEvent(QFocusEvent *e) { _a_placeholderFocused.start([this] { update(); }, 0., 1., _st.phDuration); QLineEdit::focusInEvent(e); emit focused(); } void FlatInput::focusOutEvent(QFocusEvent *e) { _a_placeholderFocused.start([this] { update(); }, 1., 0., _st.phDuration); QLineEdit::focusOutEvent(e); emit blurred(); } void FlatInput::resizeEvent(QResizeEvent *e) { updatePlaceholderText(); return QLineEdit::resizeEvent(e); } void FlatInput::updatePlaceholderText() { int32 availw = width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1; if (_st.font->width(_fullph) > availw) { _ph = _st.font->elided(_fullph, availw); } else { _ph = _fullph; } update(); } void FlatInput::contextMenuEvent(QContextMenuEvent *e) { if (auto menu = createStandardContextMenu()) { (new Ui::PopupMenu(menu))->popup(e->globalPos()); } } QSize FlatInput::sizeHint() const { return geometry().size(); } QSize FlatInput::minimumSizeHint() const { return geometry().size(); } void FlatInput::updatePlaceholder() { auto placeholderVisible = text().isEmpty(); if (_placeholderVisible != placeholderVisible) { _placeholderVisible = placeholderVisible; _a_placeholderVisible.start([this] { update(); }, _placeholderVisible ? 0. : 1., _placeholderVisible ? 1. : 0., _st.phDuration); } } const QString &FlatInput::placeholder() const { return _fullph; } QRect FlatInput::placeholderRect() const { return QRect(_st.textMrg.left() + _st.phPos.x(), _st.textMrg.top() + _st.phPos.y(), width() - _st.textMrg.left() - _st.textMrg.right(), height() - _st.textMrg.top() - _st.textMrg.bottom()); } void FlatInput::correctValue(const QString &was, QString &now) { } void FlatInput::phPrepare(Painter &p, float64 placeholderFocused) { p.setFont(_st.font); p.setPen(anim::pen(_st.phColor, _st.phFocusColor, placeholderFocused)); } void FlatInput::keyPressEvent(QKeyEvent *e) { QString wasText(_oldtext); bool shift = e->modifiers().testFlag(Qt::ShiftModifier), alt = e->modifiers().testFlag(Qt::AltModifier); bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier), ctrlGood = true; if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)) { e->ignore(); } else { QLineEdit::keyPressEvent(e); } QString newText(text()); if (wasText == newText) { // call correct manually correctValue(wasText, newText); _oldtext = newText; if (wasText != _oldtext) emit changed(); updatePlaceholder(); } if (e->key() == Qt::Key_Escape) { emit cancelled(); } else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { emit submitted(ctrl && shift); #ifdef Q_OS_MAC } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) { auto selected = selectedText(); if (!selected.isEmpty() && echoMode() == QLineEdit::Normal) { QApplication::clipboard()->setText(selected, QClipboard::FindBuffer); } #endif // Q_OS_MAC } } void FlatInput::onTextEdited() { QString wasText(_oldtext), newText(text()); correctValue(wasText, newText); _oldtext = newText; if (wasText != _oldtext) emit changed(); updatePlaceholder(); if (App::wnd()) App::wnd()->updateGlobalMenu(); } void FlatInput::onTextChange(const QString &text) { _oldtext = text; if (App::wnd()) App::wnd()->updateGlobalMenu(); } InputArea::InputArea(QWidget *parent, const style::InputField &st, const QString &ph, const QString &val) : TWidget(parent) , _st(st) , _inner(this) , _oldtext(val) , _placeholderFull(ph) { _inner->setAcceptRichText(false); resize(_st.width, _st.heightMin); setAttribute(Qt::WA_OpaquePaintEvent); _inner->setFont(_st.font->f); createPlaceholderPath(); subscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) { if (update.paletteChanged()) { updatePalette(); } }); updatePalette(); _inner->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); _inner->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); _inner->setFrameStyle(QFrame::NoFrame | QFrame::Plain); _inner->viewport()->setAutoFillBackground(false); _inner->setContentsMargins(0, 0, 0, 0); _inner->document()->setDocumentMargin(0); setAttribute(Qt::WA_AcceptTouchEvents); _inner->viewport()->setAttribute(Qt::WA_AcceptTouchEvents); _touchTimer.setSingleShot(true); connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer())); connect(_inner->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(onDocumentContentsChange(int,int,int))); connect(_inner->document(), SIGNAL(contentsChanged()), this, SLOT(onDocumentContentsChanged())); connect(_inner, SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool))); connect(_inner, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool))); if (App::wnd()) connect(_inner, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu())); setCursor(style::cur_text); heightAutoupdated(); if (!val.isEmpty()) { _inner->setPlainText(val); } _inner->document()->clearUndoRedoStacks(); startBorderAnimation(); startPlaceholderAnimation(); finishAnimations(); } void InputArea::updatePalette() { auto p = palette(); p.setColor(QPalette::Text, _st.textFg->c); setPalette(p); } void InputArea::onTouchTimer() { _touchRightButton = true; } bool InputArea::heightAutoupdated() { if (_st.heightMin < 0 || _st.heightMax < 0 || _inHeightCheck) return false; _inHeightCheck = true; myEnsureResized(this); int newh = qCeil(_inner->document()->size().height()) + _st.textMargins.top() + _st.textMargins.bottom(); if (newh > _st.heightMax) { newh = _st.heightMax; } else if (newh < _st.heightMin) { newh = _st.heightMin; } if (height() != newh) { resize(width(), newh); _inHeightCheck = false; return true; } _inHeightCheck = false; return false; } void InputArea::checkContentHeight() { if (heightAutoupdated()) { emit resized(); } } InputArea::Inner::Inner(InputArea *parent) : QTextEdit(parent) { } bool InputArea::Inner::viewportEvent(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) { qobject_cast(parentWidget())->touchEvent(ev); return QTextEdit::viewportEvent(e); } } return QTextEdit::viewportEvent(e); } void InputArea::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; 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); } } _touchTimer.stop(); _touchPress = _touchMove = _touchRightButton = false; break; case QEvent::TouchCancel: _touchPress = false; _touchTimer.stop(); break; } } void InputArea::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); } 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); auto r = rect().marginsRemoved(_st.textMargins + _st.placeholderMargins); r.moveLeft(r.left() + placeholderLeft); if (rtl()) r.moveLeft(width() - r.left() - r.width()); p.setFont(_st.font); p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree)); p.drawText(r, _placeholder, _st.placeholderAlign); p.restore(); } } TWidget::paintEvent(e); } void InputArea::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 InputArea::focusInEvent(QFocusEvent *e) { _borderAnimationStart = (e->reason() == Qt::MouseFocusReason) ? mapFromGlobal(QCursor::pos()).x() : (width() / 2); QTimer::singleShot(0, this, SLOT(onFocusInner())); } void InputArea::mousePressEvent(QMouseEvent *e) { _borderAnimationStart = e->pos().x(); QTimer::singleShot(0, this, SLOT(onFocusInner())); } void InputArea::onFocusInner() { auto borderStart = _borderAnimationStart; _inner->setFocus(); _borderAnimationStart = borderStart; } void InputArea::contextMenuEvent(QContextMenuEvent *e) { _inner->contextMenuEvent(e); } void InputArea::Inner::focusInEvent(QFocusEvent *e) { f()->focusInInner(e->reason() == Qt::MouseFocusReason); QTextEdit::focusInEvent(e); emit f()->focused(); } void InputArea::focusInInner(bool focusByMouse) { _borderAnimationStart = focusByMouse ? mapFromGlobal(QCursor::pos()).x() : (width() / 2); setFocused(true); } void InputArea::Inner::focusOutEvent(QFocusEvent *e) { f()->focusOutInner(); QTextEdit::focusOutEvent(e); emit f()->blurred(); } void InputArea::focusOutInner() { setFocused(false); } void InputArea::setFocused(bool focused) { if (_focused != focused) { _focused = focused; _a_focused.start([this] { update(); }, _focused ? 0. : 1., _focused ? 1. : 0., _st.duration); startPlaceholderAnimation(); startBorderAnimation(); } } QSize InputArea::sizeHint() const { return geometry().size(); } QSize InputArea::minimumSizeHint() const { return geometry().size(); } QString InputArea::getText(int32 start, int32 end) const { if (end >= 0 && end <= start) return QString(); if (start < 0) start = 0; bool full = (start == 0) && (end < 0); QTextDocument *doc(_inner->document()); QTextBlock from = full ? doc->begin() : doc->findBlock(start), till = (end < 0) ? doc->end() : doc->findBlock(end); if (till.isValid()) till = till.next(); int32 possibleLen = 0; for (QTextBlock b = from; b != till; b = b.next()) { possibleLen += b.length(); } QString result; result.reserve(possibleLen + 1); if (!full && end < 0) { end = possibleLen; } for (QTextBlock b = from; b != till; b = b.next()) { for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) { QTextFragment fragment(iter.fragment()); if (!fragment.isValid()) continue; int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length()); if (!full) { if (p >= end || e <= start) { continue; } } QTextCharFormat f = fragment.charFormat(); QString emojiText; QString t(fragment.text()); if (!full) { if (p < start) { t = t.mid(start - p, end - start); } else if (e > end) { t = t.mid(0, end - p); } } QChar *ub = t.data(), *uc = ub, *ue = uc + t.size(); for (; uc != ue; ++uc) { switch (uc->unicode()) { case 0xfdd0: // QTextBeginningOfFrame case 0xfdd1: // QTextEndOfFrame case QChar::ParagraphSeparator: case QChar::LineSeparator: *uc = QLatin1Char('\n'); break; case QChar::Nbsp: *uc = QLatin1Char(' '); break; case QChar::ObjectReplacementCharacter: if (emojiText.isEmpty() && f.isImageFormat()) { QString imageName = static_cast(&f)->name(); if (imageName.startsWith(qstr("emoji://e."))) { if (EmojiPtr emoji = emojiFromUrl(imageName)) { emojiText = emojiString(emoji); } } } if (uc > ub) result.append(ub, uc - ub); if (!emojiText.isEmpty()) result.append(emojiText); ub = uc + 1; break; } } if (uc > ub) result.append(ub, uc - ub); } result.append('\n'); } result.chop(1); return result; } bool InputArea::hasText() const { QTextDocument *doc(_inner->document()); QTextBlock from = doc->begin(), till = doc->end(); if (from == till) return false; for (QTextBlock::Iterator iter = from.begin(); !iter.atEnd(); ++iter) { QTextFragment fragment(iter.fragment()); if (!fragment.isValid()) continue; if (!fragment.text().isEmpty()) return true; } return (from.next() != till); } bool InputArea::isUndoAvailable() const { return _undoAvailable; } bool InputArea::isRedoAvailable() const { return _redoAvailable; } void InputArea::insertEmoji(EmojiPtr emoji, QTextCursor c) { QTextImageFormat imageFormat; int32 ew = ESize + st::emojiPadding * cIntRetinaFactor() * 2, eh = _st.font->height * cIntRetinaFactor(); imageFormat.setWidth(ew / cIntRetinaFactor()); imageFormat.setHeight(eh / cIntRetinaFactor()); imageFormat.setName(qsl("emoji://e.") + QString::number(emojiKey(emoji), 16)); imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline); static QString objectReplacement(QChar::ObjectReplacementCharacter); c.insertText(objectReplacement, imageFormat); } QVariant InputArea::Inner::loadResource(int type, const QUrl &name) { QString imageName = name.toDisplayString(); if (imageName.startsWith(qstr("emoji://e."))) { if (EmojiPtr emoji = emojiFromUrl(imageName)) { return QVariant(App::emojiSingle(emoji, f()->_st.font->height)); } } return QVariant(); } void InputArea::processDocumentContentsChange(int position, int charsAdded) { int32 replacePosition = -1, replaceLen = 0; const EmojiData *emoji = 0; static QString regular = qsl("Open Sans"), semibold = qsl("Open Sans Semibold"); bool checkTilde = !cRetina() && (_inner->font().pixelSize() == 13) && (_inner->font().family() == regular); bool wasTildeFragment = false; QTextDocument *doc(_inner->document()); QTextCursor c(_inner->textCursor()); c.joinPreviousEditBlock(); while (true) { int32 start = position, end = position + charsAdded; QTextBlock from = doc->findBlock(start), till = doc->findBlock(end); if (till.isValid()) till = till.next(); for (QTextBlock b = from; b != till; b = b.next()) { for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) { QTextFragment fragment(iter.fragment()); if (!fragment.isValid()) continue; int32 fp = fragment.position(), fe = fp + fragment.length(); if (fp >= end || fe <= start) { continue; } if (checkTilde) { wasTildeFragment = (fragment.charFormat().fontFamily() == semibold); } QString t(fragment.text()); const QChar *ch = t.constData(), *e = ch + t.size(); for (; ch != e; ++ch, ++fp) { int32 emojiLen = 0; emoji = emojiFromText(ch, e, &emojiLen); if (emoji) { if (replacePosition >= 0) { emoji = 0; // replace tilde char format first } else { replacePosition = fp; replaceLen = emojiLen; } break; } if (checkTilde && fp >= position) { // tilde fix in OpenSans bool tilde = (ch->unicode() == '~'); if ((tilde && !wasTildeFragment) || (!tilde && wasTildeFragment)) { if (replacePosition < 0) { replacePosition = fp; replaceLen = 1; } else { ++replaceLen; } } else if (replacePosition >= 0) { break; } } if (ch + 1 < e && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) { ++ch; ++fp; } } if (replacePosition >= 0) break; } if (replacePosition >= 0) break; } if (replacePosition >= 0) { if (!_inner->document()->pageSize().isNull()) { _inner->document()->setPageSize(QSizeF(0, 0)); } QTextCursor c(doc->docHandle(), 0); c.setPosition(replacePosition); c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor); if (emoji) { insertEmoji(emoji, c); } else { QTextCharFormat format; format.setFontFamily(wasTildeFragment ? regular : semibold); c.mergeCharFormat(format); } charsAdded -= replacePosition + replaceLen - position; position = replacePosition + (emoji ? 1 : replaceLen); emoji = 0; replacePosition = -1; } else { break; } } c.endEditBlock(); } void InputArea::onDocumentContentsChange(int position, int charsRemoved, int charsAdded) { if (_correcting) return; QString oldtext(_oldtext); QTextCursor(_inner->document()->docHandle(), 0).joinPreviousEditBlock(); if (!position) { // Qt bug workaround https://bugreports.qt.io/browse/QTBUG-49062 QTextCursor c(_inner->document()->docHandle(), 0); c.movePosition(QTextCursor::End); if (position + charsAdded > c.position()) { int32 toSubstract = position + charsAdded - c.position(); if (charsRemoved >= toSubstract) { charsAdded -= toSubstract; charsRemoved -= toSubstract; } } } _correcting = true; if (_maxLength >= 0) { QTextCursor c(_inner->document()->docHandle(), 0); c.movePosition(QTextCursor::End); int32 fullSize = c.position(), toRemove = fullSize - _maxLength; if (toRemove > 0) { if (toRemove > charsAdded) { if (charsAdded) { c.setPosition(position); c.setPosition((position + charsAdded), QTextCursor::KeepAnchor); c.removeSelectedText(); } c.setPosition(fullSize - (toRemove - charsAdded)); c.setPosition(fullSize, QTextCursor::KeepAnchor); c.removeSelectedText(); position = _maxLength; charsAdded = 0; charsRemoved += toRemove; } else { c.setPosition(position + (charsAdded - toRemove)); c.setPosition(position + charsAdded, QTextCursor::KeepAnchor); c.removeSelectedText(); charsAdded -= toRemove; } } } _correcting = false; QTextCursor(_inner->document()->docHandle(), 0).endEditBlock(); if (_inner->document()->availableRedoSteps() > 0) return; const int takeBack = 3; position -= takeBack; charsAdded += takeBack; if (position < 0) { charsAdded += position; position = 0; } if (charsAdded <= 0) return; _correcting = true; QSizeF s = _inner->document()->pageSize(); processDocumentContentsChange(position, charsAdded); if (_inner->document()->pageSize() != s) { _inner->document()->setPageSize(s); } _correcting = false; } void InputArea::onDocumentContentsChanged() { if (_correcting) return; setErrorShown(false); auto curText = getText(); if (_oldtext != curText) { _oldtext = curText; emit changed(); checkContentHeight(); } startPlaceholderAnimation(); if (App::wnd()) App::wnd()->updateGlobalMenu(); } void InputArea::onUndoAvailable(bool avail) { _undoAvailable = avail; if (App::wnd()) App::wnd()->updateGlobalMenu(); } void InputArea::onRedoAvailable(bool avail) { _redoAvailable = avail; if (App::wnd()) App::wnd()->updateGlobalMenu(); } void InputArea::setDisplayFocused(bool focused) { setFocused(focused); finishAnimations(); } void InputArea::finishAnimations() { _a_focused.finish(); _a_error.finish(); _a_placeholderShifted.finish(); _a_borderShown.finish(); _a_borderOpacity.finish(); update(); } void InputArea::startPlaceholderAnimation() { auto placeholderShifted = (_focused && _st.placeholderScale > 0.) || !getLastText().isEmpty(); if (_placeholderShifted != placeholderShifted) { _placeholderShifted = placeholderShifted; _a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration); } } QMimeData *InputArea::Inner::createMimeDataFromSelection() const { QMimeData *result = new QMimeData(); QTextCursor c(textCursor()); int32 start = c.selectionStart(), end = c.selectionEnd(); if (end > start) { result->setText(f()->getText(start, end)); } return result; } void InputArea::customUpDown(bool custom) { _customUpDown = custom; } void InputArea::setCtrlEnterSubmit(CtrlEnterSubmit ctrlEnterSubmit) { _ctrlEnterSubmit = ctrlEnterSubmit; } void InputArea::Inner::keyPressEvent(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 ctrlGood = (ctrl && shift) || (ctrl && (f()->_ctrlEnterSubmit == CtrlEnterSubmit::CtrlEnter || f()->_ctrlEnterSubmit == CtrlEnterSubmit::Both)) || (!ctrl && !shift && (f()->_ctrlEnterSubmit == CtrlEnterSubmit::Enter || f()->_ctrlEnterSubmit == CtrlEnterSubmit::Both)); bool enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return); 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 (enter && ctrlGood) { emit f()->submitted(ctrl && shift); } else if (e->key() == Qt::Key_Escape) { e->ignore(); emit f()->cancelled(); } else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab) { if (alt || ctrl) { e->ignore(); } 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 (f()->_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)) { auto cursor = textCursor(); int start = cursor.selectionStart(), end = cursor.selectionEnd(); if (end > start) { QApplication::clipboard()->setText(f()->getText(start, end), QClipboard::FindBuffer); } #endif // Q_OS_MAC } else { QTextCursor tc(textCursor()); if (enter && ctrl) { e->setModifiers(e->modifiers() & ~Qt::ControlModifier); } QTextEdit::keyPressEvent(e); if (tc == textCursor()) { bool check = false; if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) { tc.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) { tc.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); check = true; } if (check) { if (tc == textCursor()) { e->ignore(); } else { setTextCursor(tc); } } } } } void InputArea::Inner::contextMenuEvent(QContextMenuEvent *e) { if (auto menu = createStandardContextMenu()) { (new Ui::PopupMenu(menu))->popup(e->globalPos()); } } void InputArea::resizeEvent(QResizeEvent *e) { createPlaceholderPath(); _inner->setGeometry(rect().marginsRemoved(_st.textMargins)); _borderAnimationStart = width() / 2; TWidget::resizeEvent(e); checkContentHeight(); } void InputArea::createPlaceholderPath() { 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(_placeholderFull, Qt::ElideRight, availableWidth); _placeholderPath = QPainterPath(); if (!_placeholder.isEmpty()) { _placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder); } } else { _placeholder = _st.placeholderFont->elided(_placeholderFull, availableWidth); } } void InputArea::showError() { setErrorShown(true); if (!hasFocus()) { _inner->setFocus(); } } void InputArea::setErrorShown(bool error) { if (_error != error) { _error = error; _a_error.start([this] { update(); }, _error ? 0. : 1., _error ? 1. : 0., _st.duration); startBorderAnimation(); } } InputField::InputField(QWidget *parent, const style::InputField &st, const QString &ph, const QString &val) : TWidget(parent) , _st(st) , _inner(this) , _oldtext(val) , _placeholderFull(ph) { _inner->setAcceptRichText(false); resize(_st.width, _st.heightMin); _inner->setWordWrapMode(QTextOption::NoWrap); if (_st.textBg->c.alphaF() >= 1.) { setAttribute(Qt::WA_OpaquePaintEvent); } _inner->setFont(_st.font->f); _inner->setAlignment(_st.textAlign); createPlaceholderPath(); subscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) { if (update.paletteChanged()) { updatePalette(); } }); updatePalette(); _inner->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); _inner->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); _inner->setFrameStyle(QFrame::NoFrame | QFrame::Plain); _inner->viewport()->setAutoFillBackground(false); _inner->setContentsMargins(0, 0, 0, 0); _inner->document()->setDocumentMargin(0); setAttribute(Qt::WA_AcceptTouchEvents); _inner->viewport()->setAttribute(Qt::WA_AcceptTouchEvents); _touchTimer.setSingleShot(true); connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer())); connect(_inner->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(onDocumentContentsChange(int,int,int))); connect(_inner->document(), SIGNAL(contentsChanged()), this, SLOT(onDocumentContentsChanged())); connect(_inner, SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool))); connect(_inner, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool))); if (App::wnd()) connect(_inner, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu())); setCursor(style::cur_text); if (!val.isEmpty()) { _inner->setPlainText(val); } _inner->document()->clearUndoRedoStacks(); startPlaceholderAnimation(); startBorderAnimation(); finishAnimations(); } void InputField::updatePalette() { auto p = palette(); p.setColor(QPalette::Text, _st.textFg->c); setPalette(p); } void InputField::onTouchTimer() { _touchRightButton = true; } InputField::Inner::Inner(InputField *parent) : QTextEdit(parent) { } bool InputField::Inner::viewportEvent(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) { qobject_cast(parentWidget())->touchEvent(ev); return QTextEdit::viewportEvent(e); } } return QTextEdit::viewportEvent(e); } void InputField::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; 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); } } _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(); QRect 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.font); p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree)); p.drawText(r, _placeholder, _st.placeholderAlign); p.restore(); } } TWidget::paintEvent(e); } void InputField::focusInEvent(QFocusEvent *e) { _borderAnimationStart = (e->reason() == Qt::MouseFocusReason) ? mapFromGlobal(QCursor::pos()).x() : (width() / 2); QTimer::singleShot(0, this, SLOT(onFocusInner())); } void InputField::mousePressEvent(QMouseEvent *e) { _borderAnimationStart = e->pos().x(); QTimer::singleShot(0, this, SLOT(onFocusInner())); } void InputField::onFocusInner() { auto borderStart = _borderAnimationStart; _inner->setFocus(); _borderAnimationStart = borderStart; } void InputField::contextMenuEvent(QContextMenuEvent *e) { _inner->contextMenuEvent(e); } void InputField::Inner::focusInEvent(QFocusEvent *e) { f()->focusInInner(e->reason() == Qt::MouseFocusReason); QTextEdit::focusInEvent(e); emit f()->focused(); } void InputField::focusInInner(bool focusByMouse) { _borderAnimationStart = focusByMouse ? mapFromGlobal(QCursor::pos()).x() : (width() / 2); setFocused(true); } void InputField::Inner::focusOutEvent(QFocusEvent *e) { f()->focusOutInner(); QTextEdit::focusOutEvent(e); emit f()->blurred(); } void InputField::focusOutInner() { setFocused(false); } 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(); } } void InputField::startPlaceholderAnimation() { auto placeholderShifted = (_focused && _st.placeholderScale > 0.) || !getLastText().isEmpty() || _forcePlaceholderHidden; if (_placeholderShifted != placeholderShifted) { _placeholderShifted = placeholderShifted; _a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration); } } 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); } } } QSize InputField::sizeHint() const { return geometry().size(); } QSize InputField::minimumSizeHint() const { return geometry().size(); } QString InputField::getText(int32 start, int32 end) const { if (end >= 0 && end <= start) return QString(); if (start < 0) start = 0; bool full = (start == 0) && (end < 0); QTextDocument *doc(_inner->document()); QTextBlock from = full ? doc->begin() : doc->findBlock(start), till = (end < 0) ? doc->end() : doc->findBlock(end); if (till.isValid()) till = till.next(); int32 possibleLen = 0; for (QTextBlock b = from; b != till; b = b.next()) { possibleLen += b.length(); } QString result; result.reserve(possibleLen + 1); if (!full && end < 0) { end = possibleLen; } for (QTextBlock b = from; b != till; b = b.next()) { for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) { QTextFragment fragment(iter.fragment()); if (!fragment.isValid()) continue; int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length()); if (!full) { if (p >= end || e <= start) { continue; } } QTextCharFormat f = fragment.charFormat(); QString emojiText; QString t(fragment.text()); if (!full) { if (p < start) { t = t.mid(start - p, end - start); } else if (e > end) { t = t.mid(0, end - p); } } QChar *ub = t.data(), *uc = ub, *ue = uc + t.size(); for (; uc != ue; ++uc) { switch (uc->unicode()) { case 0xfdd0: // QTextBeginningOfFrame case 0xfdd1: // QTextEndOfFrame case QChar::ParagraphSeparator: case QChar::LineSeparator: *uc = QLatin1Char('\n'); break; case QChar::Nbsp: *uc = QLatin1Char(' '); break; case QChar::ObjectReplacementCharacter: if (emojiText.isEmpty() && f.isImageFormat()) { QString imageName = static_cast(&f)->name(); if (imageName.startsWith(qstr("emoji://e."))) { if (EmojiPtr emoji = emojiFromUrl(imageName)) { emojiText = emojiString(emoji); } } } if (uc > ub) result.append(ub, uc - ub); if (!emojiText.isEmpty()) result.append(emojiText); ub = uc + 1; break; } } if (uc > ub) result.append(ub, uc - ub); } result.append('\n'); } result.chop(1); return result; } bool InputField::hasText() const { QTextDocument *doc(_inner->document()); QTextBlock from = doc->begin(), till = doc->end(); if (from == till) return false; for (QTextBlock::Iterator iter = from.begin(); !iter.atEnd(); ++iter) { QTextFragment fragment(iter.fragment()); if (!fragment.isValid()) continue; if (!fragment.text().isEmpty()) return true; } return (from.next() != till); } bool InputField::isUndoAvailable() const { return _undoAvailable; } bool InputField::isRedoAvailable() const { return _redoAvailable; } void InputField::insertEmoji(EmojiPtr emoji, QTextCursor c) { QTextImageFormat imageFormat; int32 ew = ESize + st::emojiPadding * cIntRetinaFactor() * 2, eh = _st.font->height * cIntRetinaFactor(); imageFormat.setWidth(ew / cIntRetinaFactor()); imageFormat.setHeight(eh / cIntRetinaFactor()); imageFormat.setName(qsl("emoji://e.") + QString::number(emojiKey(emoji), 16)); imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline); static QString objectReplacement(QChar::ObjectReplacementCharacter); c.insertText(objectReplacement, imageFormat); } QVariant InputField::Inner::loadResource(int type, const QUrl &name) { QString imageName = name.toDisplayString(); if (imageName.startsWith(qstr("emoji://e."))) { if (EmojiPtr emoji = emojiFromUrl(imageName)) { return QVariant(App::emojiSingle(emoji, f()->_st.font->height)); } } return QVariant(); } void InputField::processDocumentContentsChange(int position, int charsAdded) { int32 replacePosition = -1, replaceLen = 0; const EmojiData *emoji = 0; bool newlineFound = false; static QString regular = qsl("Open Sans"), semibold = qsl("Open Sans Semibold"), space(' '); bool checkTilde = !cRetina() && (_inner->font().pixelSize() == 13) && (_inner->font().family() == regular); bool wasTildeFragment = false; QTextDocument *doc(_inner->document()); QTextCursor c(_inner->textCursor()); c.joinPreviousEditBlock(); while (true) { int32 start = position, end = position + charsAdded; QTextBlock from = doc->findBlock(start), till = doc->findBlock(end); if (till.isValid()) till = till.next(); for (QTextBlock b = from; b != till; b = b.next()) { for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) { QTextFragment fragment(iter.fragment()); if (!fragment.isValid()) continue; int32 fp = fragment.position(), fe = fp + fragment.length(); if (fp >= end || fe <= start) { continue; } if (checkTilde) { wasTildeFragment = (fragment.charFormat().fontFamily() == semibold); } QString t(fragment.text()); const QChar *ch = t.constData(), *e = ch + t.size(); for (; ch != e; ++ch, ++fp) { // QTextBeginningOfFrame // QTextEndOfFrame newlineFound = (ch->unicode() == 0xfdd0 || ch->unicode() == 0xfdd1 || ch->unicode() == QChar::ParagraphSeparator || ch->unicode() == QChar::LineSeparator || ch->unicode() == '\n' || ch->unicode() == '\r'); if (newlineFound) { if (replacePosition >= 0) { newlineFound = false; // replace tilde char format first } else { replacePosition = fp; replaceLen = 1; } break; } int32 emojiLen = 0; emoji = emojiFromText(ch, e, &emojiLen); if (emoji) { if (replacePosition >= 0) { emoji = 0; // replace tilde char format first } else { replacePosition = fp; replaceLen = emojiLen; } break; } if (checkTilde && fp >= position) { // tilde fix in OpenSans bool tilde = (ch->unicode() == '~'); if ((tilde && !wasTildeFragment) || (!tilde && wasTildeFragment)) { if (replacePosition < 0) { replacePosition = fp; replaceLen = 1; } else { ++replaceLen; } } else if (replacePosition >= 0) { break; } } if (ch + 1 < e && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) { ++ch; ++fp; } } if (replacePosition >= 0) break; } if (replacePosition >= 0) break; if (b.next() != doc->end()) { newlineFound = true; replacePosition = b.next().position() - 1; replaceLen = 1; break; } } if (replacePosition >= 0) { if (!_inner->document()->pageSize().isNull()) { _inner->document()->setPageSize(QSizeF(0, 0)); } QTextCursor c(doc->docHandle(), replacePosition); c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor); if (newlineFound) { QTextCharFormat format; format.setFontFamily(regular); c.mergeCharFormat(format); c.insertText(space); } else if (emoji) { insertEmoji(emoji, c); } else { QTextCharFormat format; format.setFontFamily(wasTildeFragment ? regular : semibold); c.mergeCharFormat(format); } charsAdded -= replacePosition + replaceLen - position; position = replacePosition + ((emoji || newlineFound) ? 1 : replaceLen); newlineFound = false; emoji = 0; replacePosition = -1; } else { break; } } c.endEditBlock(); } void InputField::onDocumentContentsChange(int position, int charsRemoved, int charsAdded) { if (_correcting) return; QString oldtext(_oldtext); QTextCursor(_inner->document()->docHandle(), 0).joinPreviousEditBlock(); if (!position) { // Qt bug workaround https://bugreports.qt.io/browse/QTBUG-49062 QTextCursor c(_inner->document()->docHandle(), 0); c.movePosition(QTextCursor::End); if (position + charsAdded > c.position()) { int32 toSubstract = position + charsAdded - c.position(); if (charsRemoved >= toSubstract) { charsAdded -= toSubstract; charsRemoved -= toSubstract; } } } _correcting = true; if (_maxLength >= 0) { QTextCursor c(_inner->document()->docHandle(), 0); c.movePosition(QTextCursor::End); int32 fullSize = c.position(), toRemove = fullSize - _maxLength; if (toRemove > 0) { if (toRemove > charsAdded) { if (charsAdded) { c.setPosition(position); c.setPosition((position + charsAdded), QTextCursor::KeepAnchor); c.removeSelectedText(); } c.setPosition(fullSize - (toRemove - charsAdded)); c.setPosition(fullSize, QTextCursor::KeepAnchor); c.removeSelectedText(); position = _maxLength; charsAdded = 0; charsRemoved += toRemove; } else { c.setPosition(position + (charsAdded - toRemove)); c.setPosition(position + charsAdded, QTextCursor::KeepAnchor); c.removeSelectedText(); charsAdded -= toRemove; } } } _correcting = false; QTextCursor(_inner->document()->docHandle(), 0).endEditBlock(); if (_inner->document()->availableRedoSteps() > 0) return; const int takeBack = 3; position -= takeBack; charsAdded += takeBack; if (position < 0) { charsAdded += position; position = 0; } if (charsAdded <= 0) return; _correcting = true; QSizeF s = _inner->document()->pageSize(); processDocumentContentsChange(position, charsAdded); if (_inner->document()->pageSize() != s) { _inner->document()->setPageSize(s); } _correcting = false; } void InputField::onDocumentContentsChanged() { if (_correcting) return; setErrorShown(false); auto curText = getText(); if (_oldtext != curText) { _oldtext = curText; emit changed(); } startPlaceholderAnimation(); if (App::wnd()) App::wnd()->updateGlobalMenu(); } void InputField::onUndoAvailable(bool avail) { _undoAvailable = avail; if (App::wnd()) App::wnd()->updateGlobalMenu(); } void InputField::onRedoAvailable(bool avail) { _redoAvailable = avail; if (App::wnd()) App::wnd()->updateGlobalMenu(); } void InputField::selectAll() { auto cursor = _inner->textCursor(); cursor.setPosition(0); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); _inner->setTextCursor(cursor); } void InputField::setDisplayFocused(bool focused) { setFocused(focused); finishAnimations(); } void InputField::finishAnimations() { _a_focused.finish(); _a_error.finish(); _a_placeholderShifted.finish(); _a_borderShown.finish(); _a_borderOpacity.finish(); update(); } void InputField::setPlaceholderHidden(bool forcePlaceholderHidden) { _forcePlaceholderHidden = forcePlaceholderHidden; startPlaceholderAnimation(); } QMimeData *InputField::Inner::createMimeDataFromSelection() const { auto result = new QMimeData(); auto cursor = textCursor(); auto start = cursor.selectionStart(); auto end = cursor.selectionEnd(); if (end > start) { result->setText(f()->getText(start, end)); } return result; } void InputField::customUpDown(bool custom) { _customUpDown = custom; } void InputField::Inner::keyPressEvent(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), ctrlGood = true; bool enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return); 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 (enter && ctrlGood) { emit f()->submitted(ctrl && shift); } else if (e->key() == Qt::Key_Escape) { e->ignore(); emit f()->cancelled(); } else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab) { if (alt || ctrl) { e->ignore(); } 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 (f()->_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)) { auto cursor = textCursor(); int start = cursor.selectionStart(), end = cursor.selectionEnd(); if (end > start) { QApplication::clipboard()->setText(f()->getText(start, end), QClipboard::FindBuffer); } #endif // Q_OS_MAC } else { auto oldCursorPosition = textCursor().position(); if (enter && ctrl) { e->setModifiers(e->modifiers() & ~Qt::ControlModifier); } QTextEdit::keyPressEvent(e); auto currentCursor = textCursor(); if (textCursor().position() == oldCursorPosition) { bool check = false; if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) { oldCursorPosition = currentCursor.position(); currentCursor.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) { oldCursorPosition = currentCursor.position(); currentCursor.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 (oldCursorPosition == currentCursor.position()) { e->ignore(); } else { setTextCursor(currentCursor); } } } } } void InputField::Inner::contextMenuEvent(QContextMenuEvent *e) { if (auto menu = createStandardContextMenu()) { (new Ui::PopupMenu(menu))->popup(e->globalPos()); } } void InputField::resizeEvent(QResizeEvent *e) { createPlaceholderPath(); _inner->setGeometry(rect().marginsRemoved(_st.textMargins)); _borderAnimationStart = width() / 2; TWidget::resizeEvent(e); } void InputField::createPlaceholderPath() { 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(_placeholderFull, Qt::ElideRight, availableWidth); _placeholderPath = QPainterPath(); if (!_placeholder.isEmpty()) { _placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder); } } else { _placeholder = _st.placeholderFont->elided(_placeholderFull, availableWidth); } } 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, const QString &placeholder, const QString &val) : QLineEdit(val, parent) , _st(st) , _oldtext(val) , _placeholderFull(placeholder) { resize(_st.width, _st.heightMin); setFont(_st.font); setAlignment(_st.textAlign); subscribe(Window::Theme::Background(), [this](const Window::Theme::BackgroundUpdate &update) { if (update.paletteChanged()) { updatePalette(); } }); updatePalette(); createPlaceholderPath(); 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(); finishAnimations(); } void MaskedInputField::updatePalette() { auto p = palette(); p.setColor(QPalette::Text, _st.textFg->c); setPalette(p); } void MaskedInputField::setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos) { if (newPos < 0 || newPos > newText.size()) { newPos = newText.size(); } auto updateText = (newText != now); if (updateText) { now = newText; setText(now); startPlaceholderAnimation(); } auto updateCursorPosition = (newPos != nowCursor) || updateText; if (updateCursorPosition) { nowCursor = newPos; setCursorPosition(nowCursor); } } void MaskedInputField::customUpDown(bool custom) { _customUpDown = custom; } void MaskedInputField::setTextMargins(const QMargins &mrg) { _textMargins = mrg; createPlaceholderPath(); } void MaskedInputField::onTouchTimer() { _touchRightButton = true; } bool MaskedInputField::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 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; 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); } } _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(_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.font); p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree)); p.drawText(r, _placeholder, _st.placeholderAlign); p.restore(); } } 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) { createPlaceholderPath(); _borderAnimationStart = width() / 2; QLineEdit::resizeEvent(e); } void MaskedInputField::createPlaceholderPath() { 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(_placeholderFull, Qt::ElideRight, availableWidth); _placeholderPath = QPainterPath(); if (!_placeholder.isEmpty()) { _placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder); } } else { _placeholder = _st.placeholderFont->elided(_placeholderFull, availableWidth); } } void MaskedInputField::contextMenuEvent(QContextMenuEvent *e) { if (auto menu = createStandardContextMenu()) { (new Ui::PopupMenu(menu))->popup(e->globalPos()); } } 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); finishAnimations(); } void MaskedInputField::finishAnimations() { _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 = (_focused && _st.placeholderScale > 0.) || !getLastText().isEmpty() || _forcePlaceholderHidden; if (_placeholderShifted != placeholderShifted) { _placeholderShifted = placeholderShifted; _a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration); } } const QString &MaskedInputField::placeholder() const { return _placeholderFull; } QRect MaskedInputField::placeholderRect() const { return rect().marginsRemoved(_st.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); } QString newText(text()); int32 newCursor(cursorPosition()); if (wasText == newText && wasCursor == newCursor) { // call correct manually correctValue(wasText, wasCursor, newText, newCursor); _oldtext = newText; _oldcursor = newCursor; if (wasText != _oldtext) emit changed(); startPlaceholderAnimation(); } if (e->key() == Qt::Key_Escape) { e->ignore(); emit cancelled(); } else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { emit submitted(ctrl && shift); #ifdef Q_OS_MAC } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) { auto selected = selectedText(); if (!selected.isEmpty() && echoMode() == QLineEdit::Normal) { QApplication::clipboard()->setText(selected, QClipboard::FindBuffer); } #endif // Q_OS_MAC } } void MaskedInputField::onTextEdited() { QString wasText(_oldtext), newText(text()); int32 wasCursor(_oldcursor), newCursor(cursorPosition()); correctValue(wasText, wasCursor, newText, newCursor); _oldtext = newText; _oldcursor = newCursor; if (wasText != _oldtext) emit changed(); startPlaceholderAnimation(); if (App::wnd()) App::wnd()->updateGlobalMenu(); } void MaskedInputField::onTextChange(const QString &text) { _oldtext = QLineEdit::text(); setErrorShown(false); if (App::wnd()) App::wnd()->updateGlobalMenu(); } void MaskedInputField::onCursorPositionChanged(int oldPosition, int position) { _oldcursor = position; } CountryCodeInput::CountryCodeInput(QWidget *parent, const style::InputField &st) : MaskedInputField(parent, st) , _nosignal(false) { } void CountryCodeInput::startErasing(QKeyEvent *e) { setFocus(); keyPressEvent(e); } void CountryCodeInput::codeSelected(const QString &code) { auto wasText = getLastText(); auto wasCursor = cursorPosition(); auto newText = '+' + code; auto newCursor = newText.size(); setText(newText); _nosignal = true; correctValue(wasText, wasCursor, newText, newCursor); _nosignal = false; emit changed(); } void CountryCodeInput::correctValue(const QString &was, int32 wasCursor, QString &now, int32 &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) { auto t = getLastText(); if (!_pattern.isEmpty()) { 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, int32 wasCursor, QString &now, int32 &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, const QString &ph, const QString &val) : MaskedInputField(parent, st, ph, val) { setEchoMode(QLineEdit::Password); } PortInput::PortInput(QWidget *parent, const style::InputField &st, const QString &ph, const QString &val) : MaskedInputField(parent, st, ph, val) { if (!val.toInt() || val.toInt() > 65535) { setText(QString()); } } void PortInput::correctValue(const QString &was, int32 wasCursor, QString &now, int32 &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); } UsernameInput::UsernameInput(QWidget *parent, const style::InputField &st, const QString &ph, const QString &val, bool isLink) : MaskedInputField(parent, st, ph, val), _linkPlaceholder(isLink ? qsl("telegram.me/") : QString()) { 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, int32 wasCursor, QString &now, int32 &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 > MaxUsernameLength) len = MaxUsernameLength + (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, const QString &ph, const QString &val) : MaskedInputField(parent, st, ph, 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 = getLastText(); 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, int32 wasCursor, QString &now, int32 &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