diff --git a/CMakeLists.txt b/CMakeLists.txt index b9f8549..66e83c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,6 +202,8 @@ PRIVATE ui/widgets/input_fields.h ui/widgets/labels.cpp ui/widgets/labels.h + ui/widgets/fields/masked_input_field.cpp + ui/widgets/fields/masked_input_field.h ui/widgets/fields/number_input.cpp ui/widgets/fields/number_input.h ui/widgets/fields/password_input.cpp diff --git a/ui/widgets/fields/masked_input_field.cpp b/ui/widgets/fields/masked_input_field.cpp new file mode 100644 index 0000000..4e1b8da --- /dev/null +++ b/ui/widgets/fields/masked_input_field.cpp @@ -0,0 +1,546 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "ui/widgets/fields/masked_input_field.h" + +#include "base/qt/qt_common_adapters.h" +#include "ui/painter.h" +#include "ui/widgets/popup_menu.h" +#include "styles/palette.h" +#include "styles/style_widgets.h" + +#include +#include +#include + +namespace Ui { +namespace { + +class InputStyle final : public QCommonStyle { +public: + InputStyle() { + setParent(QCoreApplication::instance()); + } + + void drawPrimitive( + PrimitiveElement element, + const QStyleOption *option, + QPainter *painter, + const QWidget *widget = nullptr) const override { + } + + static InputStyle *instance() { + if (!_instance) { + if (!QGuiApplication::instance()) { + return nullptr; + } + _instance = new InputStyle(); + } + return _instance; + } + + ~InputStyle() { + _instance = nullptr; + } + +private: + static InputStyle *_instance; + +}; + +InputStyle *InputStyle::_instance = nullptr; + +} // namespace + +MaskedInputField::MaskedInputField( + QWidget *parent, + const style::InputField &st, + rpl::producer placeholder, + const QString &val) +: Parent(val, parent) +, _st(st) +, _oldtext(val) +, _placeholderFull(std::move(placeholder)) { + resize(_st.width, _st.heightMin); + + setFont(_st.font); + setAlignment(_st.textAlign); + + _placeholderFull.value( + ) | rpl::start_with_next([=](const QString &text) { + refreshPlaceholder(text); + }, lifetime()); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + updatePalette(); + }, lifetime()); + updatePalette(); + + setAttribute(Qt::WA_OpaquePaintEvent); + + connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(onTextChange(const QString&))); + connect(this, SIGNAL(cursorPositionChanged(int,int)), this, SLOT(onCursorPositionChanged(int,int))); + + connect(this, SIGNAL(textEdited(const QString&)), this, SLOT(onTextEdited())); + connect(this, &MaskedInputField::selectionChanged, [] { + Integration::Instance().textActionsUpdated(); + }); + + setStyle(InputStyle::instance()); + QLineEdit::setTextMargins(0, 0, 0, 0); + setContentsMargins(_textMargins + QMargins(-2, -1, -2, -1)); + setFrame(false); + + setAttribute(Qt::WA_AcceptTouchEvents); + _touchTimer.setSingleShot(true); + connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer())); + + setTextMargins(_st.textMargins); + + startPlaceholderAnimation(); + startBorderAnimation(); + finishAnimating(); +} + +void MaskedInputField::updatePalette() { + auto p = palette(); + p.setColor(QPalette::Text, _st.textFg->c); + p.setColor(QPalette::Highlight, st::msgInBgSelected->c); + p.setColor(QPalette::HighlightedText, st::historyTextInFgSelected->c); + setPalette(p); +} + +void MaskedInputField::setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos) { + if (newPos < 0 || newPos > newText.size()) { + newPos = newText.size(); + } + auto updateText = (newText != now); + if (updateText) { + now = newText; + setText(now); + startPlaceholderAnimation(); + } + auto updateCursorPosition = (newPos != nowCursor) || updateText; + if (updateCursorPosition) { + nowCursor = newPos; + setCursorPosition(nowCursor); + } +} + +void MaskedInputField::customUpDown(bool custom) { + _customUpDown = custom; +} + +int MaskedInputField::borderAnimationStart() const { + return _borderAnimationStart; +} + +void MaskedInputField::setTextMargins(const QMargins &mrg) { + _textMargins = mrg; + setContentsMargins(_textMargins + QMargins(-2, -1, -2, -1)); + refreshPlaceholder(_placeholderFull.current()); +} + +void MaskedInputField::onTouchTimer() { + _touchRightButton = true; +} + +bool MaskedInputField::eventHook(QEvent *e) { + auto type = e->type(); + if (type == QEvent::TouchBegin + || type == QEvent::TouchUpdate + || type == QEvent::TouchEnd + || type == QEvent::TouchCancel) { + auto event = static_cast(e); + if (event->device()->type() == base::TouchDevice::TouchScreen) { + touchEvent(event); + } + } + return Parent::eventHook(e); +} + +void MaskedInputField::touchEvent(QTouchEvent *e) { + switch (e->type()) { + case QEvent::TouchBegin: { + if (_touchPress || e->touchPoints().isEmpty()) { + return; + } + _touchTimer.start(QApplication::startDragTime()); + _touchPress = true; + _touchMove = _touchRightButton = _mousePressedInTouch = false; + _touchStart = e->touchPoints().cbegin()->screenPos().toPoint(); + } break; + + case QEvent::TouchUpdate: { + if (!e->touchPoints().isEmpty()) { + touchUpdate(e->touchPoints().cbegin()->screenPos().toPoint()); + } + } break; + + case QEvent::TouchEnd: { + touchFinish(); + } break; + + case QEvent::TouchCancel: { + _touchPress = false; + _touchTimer.stop(); + } break; + } +} + +void MaskedInputField::touchUpdate(QPoint globalPosition) { + if (_touchPress + && !_touchMove + && ((globalPosition - _touchStart).manhattanLength() + >= QApplication::startDragDistance())) { + _touchMove = true; + } +} + +void MaskedInputField::touchFinish() { + if (!_touchPress) { + return; + } + const auto weak = MakeWeak(this); + if (!_touchMove && window()) { + QPoint mapped(mapFromGlobal(_touchStart)); + + if (_touchRightButton) { + QContextMenuEvent contextEvent( + QContextMenuEvent::Mouse, + mapped, + _touchStart); + contextMenuEvent(&contextEvent); + } else { + QGuiApplication::inputMethod()->show(); + } + } + if (weak) { + _touchTimer.stop(); + _touchPress + = _touchMove + = _touchRightButton + = _mousePressedInTouch = false; + } +} + +void MaskedInputField::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + 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.value(_error ? 1. : 0.); + auto focusedDegree = _a_focused.value(_focused ? 1. : 0.); + auto borderShownDegree = _a_borderShown.value(1.); + auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.); + if (_st.borderActive && (borderOpacity > 0.)) { + auto borderStart = std::clamp(_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.value(_placeholderShifted ? 1. : 0.); + p.save(); + p.setClipRect(r); + + auto placeholderTop = anim::interpolate(0, _st.placeholderShift, placeholderShiftDegree); + + QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins)); + r.moveTop(r.top() + placeholderTop); + if (style::RightToLeft()) 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.value(_placeholderShifted ? 1. : 0.); + if (placeholderHiddenDegree < 1.) { + p.setOpacity(1. - placeholderHiddenDegree); + p.save(); + p.setClipRect(r); + + auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree); + + QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins)); + r.moveLeft(r.left() + placeholderLeft); + if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width()); + + p.setFont(_st.placeholderFont); + p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree)); + p.drawText(r, _placeholder, _st.placeholderAlign); + + p.restore(); + p.setOpacity(1.); + } + } + + paintAdditionalPlaceholder(p); + QLineEdit::paintEvent(e); +} + +void MaskedInputField::mousePressEvent(QMouseEvent *e) { + if (_touchPress && e->button() == Qt::LeftButton) { + _mousePressedInTouch = true; + _touchStart = e->globalPos(); + } + return QLineEdit::mousePressEvent(e); +} + +void MaskedInputField::mouseReleaseEvent(QMouseEvent *e) { + if (_mousePressedInTouch) { + touchFinish(); + } + return QLineEdit::mouseReleaseEvent(e); +} + +void MaskedInputField::mouseMoveEvent(QMouseEvent *e) { + if (_mousePressedInTouch) { + touchUpdate(e->globalPos()); + } + return QLineEdit::mouseMoveEvent(e); +} + +QString MaskedInputField::getDisplayedText() const { + auto result = getLastText(); + if (!_lastPreEditText.isEmpty()) { + result = result.mid(0, _oldcursor) + + _lastPreEditText + + result.mid(_oldcursor); + } + return result; +} + +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.value(1.), 0.)) { + _a_borderShown.stop(); + _a_borderOpacity.stop(); + } 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); + focused(); +} + +void MaskedInputField::focusOutEvent(QFocusEvent *e) { + setFocused(false); + QLineEdit::focusOutEvent(e); + blurred(); +} + +void MaskedInputField::setFocused(bool focused) { + if (_focused != focused) { + _focused = focused; + _a_focused.start([this] { update(); }, _focused ? 0. : 1., _focused ? 1. : 0., _st.duration); + startPlaceholderAnimation(); + startBorderAnimation(); + } +} + +void MaskedInputField::resizeEvent(QResizeEvent *e) { + refreshPlaceholder(_placeholderFull.current()); + _borderAnimationStart = width() / 2; + QLineEdit::resizeEvent(e); +} + +void MaskedInputField::refreshPlaceholder(const QString &text) { + const auto availableWidth = width() - _textMargins.left() - _textMargins.right() - _st.placeholderMargins.left() - _st.placeholderMargins.right() - 1; + if (_st.placeholderScale > 0.) { + auto placeholderFont = _st.placeholderFont->f; + placeholderFont.setStyleStrategy(QFont::PreferMatch); + const auto metrics = QFontMetrics(placeholderFont); + _placeholder = metrics.elidedText(text, Qt::ElideRight, availableWidth); + _placeholderPath = QPainterPath(); + if (!_placeholder.isEmpty()) { + _placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder); + } + } else { + _placeholder = _st.placeholderFont->elided(text, availableWidth); + } + update(); +} + +void MaskedInputField::setPlaceholder(rpl::producer placeholder) { + _placeholderFull = std::move(placeholder); +} + +void MaskedInputField::contextMenuEvent(QContextMenuEvent *e) { + if (const auto menu = createStandardContextMenu()) { + _contextMenu = base::make_unique_q(this, menu); + _contextMenu->popup(e->globalPos()); + } +} + +void MaskedInputField::inputMethodEvent(QInputMethodEvent *e) { + QLineEdit::inputMethodEvent(e); + _lastPreEditText = e->preeditString(); + update(); +} + +void MaskedInputField::showError() { + showErrorNoFocus(); + if (!hasFocus()) { + setFocus(); + } +} + +void MaskedInputField::showErrorNoFocus() { + setErrorShown(true); +} + +void MaskedInputField::hideError() { + setErrorShown(false); +} + +void MaskedInputField::setErrorShown(bool error) { + if (_error != error) { + _error = error; + _a_error.start([this] { update(); }, _error ? 0. : 1., _error ? 1. : 0., _st.duration); + startBorderAnimation(); + } +} + +QSize MaskedInputField::sizeHint() const { + return geometry().size(); +} + +QSize MaskedInputField::minimumSizeHint() const { + return geometry().size(); +} + +void MaskedInputField::setDisplayFocused(bool focused) { + setFocused(focused); + finishAnimating(); +} + +void MaskedInputField::finishAnimating() { + _a_focused.stop(); + _a_error.stop(); + _a_placeholderShifted.stop(); + _a_borderShown.stop(); + _a_borderOpacity.stop(); + update(); +} + +void MaskedInputField::setPlaceholderHidden(bool forcePlaceholderHidden) { + _forcePlaceholderHidden = forcePlaceholderHidden; + startPlaceholderAnimation(); +} + +void MaskedInputField::startPlaceholderAnimation() { + auto placeholderShifted = _forcePlaceholderHidden || (_focused && _st.placeholderScale > 0.) || !getLastText().isEmpty(); + if (_placeholderShifted != placeholderShifted) { + _placeholderShifted = placeholderShifted; + _a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration); + } +} + +QRect MaskedInputField::placeholderRect() const { + return rect().marginsRemoved(_textMargins + _st.placeholderMargins); +} + +style::font MaskedInputField::phFont() { + return _st.font; +} + +void MaskedInputField::placeholderAdditionalPrepare(QPainter &p) { + p.setFont(_st.font); + p.setPen(_st.placeholderFg); +} + +void MaskedInputField::keyPressEvent(QKeyEvent *e) { + QString wasText(_oldtext); + int32 wasCursor(_oldcursor); + + if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) { + e->ignore(); + } else { + QLineEdit::keyPressEvent(e); + } + + auto newText = text(); + auto newCursor = cursorPosition(); + if (wasText == newText && wasCursor == newCursor) { // call correct manually + correctValue(wasText, wasCursor, newText, newCursor); + _oldtext = newText; + _oldcursor = newCursor; + if (wasText != _oldtext) changed(); + startPlaceholderAnimation(); + } + if (e->key() == Qt::Key_Escape) { + e->ignore(); + cancelled(); + } else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { + submitted(e->modifiers()); +#ifdef Q_OS_MAC + } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) { + auto selected = selectedText(); + if (!selected.isEmpty() && echoMode() == QLineEdit::Normal) { + QGuiApplication::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) changed(); + startPlaceholderAnimation(); + + Integration::Instance().textActionsUpdated(); +} + +void MaskedInputField::onTextChange(const QString &text) { + _oldtext = QLineEdit::text(); + setErrorShown(false); + Integration::Instance().textActionsUpdated(); +} + +void MaskedInputField::onCursorPositionChanged(int oldPosition, int position) { + _oldcursor = position; +} + +} // namespace Ui diff --git a/ui/widgets/fields/masked_input_field.h b/ui/widgets/fields/masked_input_field.h new file mode 100644 index 0000000..1b09430 --- /dev/null +++ b/ui/widgets/fields/masked_input_field.h @@ -0,0 +1,174 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "ui/rp_widget.h" +#include "ui/effects/animations.h" + +#include +#include + +namespace style { +struct InputField; +} // namespace style + +namespace Ui { + +class PopupMenu; + +class MaskedInputField : public RpWidgetBase { + Q_OBJECT + + using Parent = RpWidgetBase; +public: + MaskedInputField( + QWidget *parent, + const style::InputField &st, + rpl::producer placeholder = nullptr, + const QString &val = QString()); + + void showError(); + void showErrorNoFocus(); + void hideError(); + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + + void customUpDown(bool isCustom); + int borderAnimationStart() const; + + const QString &getLastText() const { + return _oldtext; + } + void setPlaceholder(rpl::producer placeholder); + void setPlaceholderHidden(bool forcePlaceholderHidden); + void setDisplayFocused(bool focused); + void finishAnimating(); + void setFocusFast() { + setDisplayFocused(true); + setFocus(); + } + + void setText(const QString &text) { + QLineEdit::setText(text); + startPlaceholderAnimation(); + } + void clear() { + QLineEdit::clear(); + startPlaceholderAnimation(); + } + +public Q_SLOTS: + void onTextChange(const QString &text); + void onCursorPositionChanged(int oldPosition, int position); + + void onTextEdited(); + + void onTouchTimer(); + +Q_SIGNALS: + void changed(); + void cancelled(); + void submitted(Qt::KeyboardModifiers); + void focused(); + void blurred(); + +protected: + QString getDisplayedText() const; + void startBorderAnimation(); + void startPlaceholderAnimation(); + + bool eventHook(QEvent *e) override; + void touchEvent(QTouchEvent *e); + void paintEvent(QPaintEvent *e) override; + void focusInEvent(QFocusEvent *e) override; + void focusOutEvent(QFocusEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; + void inputMethodEvent(QInputMethodEvent *e) override; + + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + + virtual void correctValue( + const QString &was, + int wasCursor, + QString &now, + int &nowCursor) { + } + void setCorrectedText( + QString &now, + int &nowCursor, + const QString &newText, + int newPos); + + virtual void paintAdditionalPlaceholder(QPainter &p) { + } + + style::font phFont(); + + void placeholderAdditionalPrepare(QPainter &p); + QRect placeholderRect() const; + + void setTextMargins(const QMargins &mrg); + const style::InputField &_st; + +private: + void updatePalette(); + void refreshPlaceholder(const QString &text); + void setErrorShown(bool error); + + void touchUpdate(QPoint globalPosition); + void touchFinish(); + + void setFocused(bool focused); + + int _maxLength = -1; + bool _forcePlaceholderHidden = false; + + QString _oldtext; + int _oldcursor = 0; + QString _lastPreEditText; + + bool _undoAvailable = false; + bool _redoAvailable = false; + + bool _customUpDown = false; + + rpl::variable _placeholderFull; + QString _placeholder; + Animations::Simple _a_placeholderShifted; + bool _placeholderShifted = false; + QPainterPath _placeholderPath; + + Animations::Simple _a_borderShown; + int _borderAnimationStart = 0; + Animations::Simple _a_borderOpacity; + bool _borderVisible = false; + + Animations::Simple _a_focused; + Animations::Simple _a_error; + + bool _focused = false; + bool _error = false; + + style::margins _textMargins; + + QTimer _touchTimer; + bool _touchPress = false; + bool _touchRightButton = false; + bool _touchMove = false; + bool _mousePressedInTouch = false; + QPoint _touchStart; + + base::unique_qptr _contextMenu; + +}; + +} // namespace Ui diff --git a/ui/widgets/fields/number_input.h b/ui/widgets/fields/number_input.h index bf10abf..3e24895 100644 --- a/ui/widgets/fields/number_input.h +++ b/ui/widgets/fields/number_input.h @@ -6,7 +6,7 @@ // #pragma once -#include "ui/widgets/input_fields.h" +#include "ui/widgets/fields/masked_input_field.h" namespace style { struct InputField; diff --git a/ui/widgets/fields/password_input.h b/ui/widgets/fields/password_input.h index 30680b7..2ed6e6c 100644 --- a/ui/widgets/fields/password_input.h +++ b/ui/widgets/fields/password_input.h @@ -6,7 +6,7 @@ // #pragma once -#include "ui/widgets/input_fields.h" +#include "ui/widgets/fields/masked_input_field.h" namespace style { struct InputField; diff --git a/ui/widgets/fields/time_part_input.cpp b/ui/widgets/fields/time_part_input.cpp index 8f6433a..3767574 100644 --- a/ui/widgets/fields/time_part_input.cpp +++ b/ui/widgets/fields/time_part_input.cpp @@ -6,6 +6,7 @@ // #include "ui/widgets/fields/time_part_input.h" +#include "base/qt/qt_string_view.h" #include "ui/ui_utility.h" // WheelDirection #include diff --git a/ui/widgets/fields/time_part_input.h b/ui/widgets/fields/time_part_input.h index bdd7d1b..306a471 100644 --- a/ui/widgets/fields/time_part_input.h +++ b/ui/widgets/fields/time_part_input.h @@ -6,7 +6,7 @@ // #pragma once -#include "ui/widgets/input_fields.h" +#include "ui/widgets/fields/masked_input_field.h" namespace Ui { diff --git a/ui/widgets/input_fields.cpp b/ui/widgets/input_fields.cpp index 3839769..cdde5bf 100644 --- a/ui/widgets/input_fields.cpp +++ b/ui/widgets/input_fields.cpp @@ -659,36 +659,6 @@ private: }; -class InputStyle : public QCommonStyle { -public: - InputStyle() { - setParent(QCoreApplication::instance()); - } - - void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget = nullptr) const override { - } - - static InputStyle *instance() { - if (!_instance) { - if (!QGuiApplication::instance()) { - return nullptr; - } - _instance = new InputStyle(); - } - return _instance; - } - - ~InputStyle() { - _instance = nullptr; - } - -private: - static InputStyle *_instance; - -}; - -InputStyle *InputStyle::_instance = nullptr; - template QString AccumulateText(Iterator begin, Iterator end) { auto result = QString(); @@ -3875,478 +3845,4 @@ void InputField::setErrorShown(bool error) { InputField::~InputField() = default; -MaskedInputField::MaskedInputField( - QWidget *parent, - const style::InputField &st, - rpl::producer placeholder, - const QString &val) -: Parent(val, parent) -, _st(st) -, _oldtext(val) -, _placeholderFull(std::move(placeholder)) { - resize(_st.width, _st.heightMin); - - setFont(_st.font); - setAlignment(_st.textAlign); - - _placeholderFull.value( - ) | rpl::start_with_next([=](const QString &text) { - refreshPlaceholder(text); - }, lifetime()); - - style::PaletteChanged( - ) | rpl::start_with_next([=] { - updatePalette(); - }, lifetime()); - updatePalette(); - - setAttribute(Qt::WA_OpaquePaintEvent); - - connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(onTextChange(const QString&))); - connect(this, SIGNAL(cursorPositionChanged(int,int)), this, SLOT(onCursorPositionChanged(int,int))); - - connect(this, SIGNAL(textEdited(const QString&)), this, SLOT(onTextEdited())); - connect(this, &MaskedInputField::selectionChanged, [] { - Integration::Instance().textActionsUpdated(); - }); - - setStyle(InputStyle::instance()); - QLineEdit::setTextMargins(0, 0, 0, 0); - setContentsMargins(_textMargins + QMargins(-2, -1, -2, -1)); - setFrame(false); - - setAttribute(Qt::WA_AcceptTouchEvents); - _touchTimer.setSingleShot(true); - connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer())); - - setTextMargins(_st.textMargins); - - startPlaceholderAnimation(); - startBorderAnimation(); - finishAnimating(); -} - -void MaskedInputField::updatePalette() { - auto p = palette(); - p.setColor(QPalette::Text, _st.textFg->c); - p.setColor(QPalette::Highlight, st::msgInBgSelected->c); - p.setColor(QPalette::HighlightedText, st::historyTextInFgSelected->c); - setPalette(p); -} - -void MaskedInputField::setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos) { - if (newPos < 0 || newPos > newText.size()) { - newPos = newText.size(); - } - auto updateText = (newText != now); - if (updateText) { - now = newText; - setText(now); - startPlaceholderAnimation(); - } - auto updateCursorPosition = (newPos != nowCursor) || updateText; - if (updateCursorPosition) { - nowCursor = newPos; - setCursorPosition(nowCursor); - } -} - -void MaskedInputField::customUpDown(bool custom) { - _customUpDown = custom; -} - -int MaskedInputField::borderAnimationStart() const { - return _borderAnimationStart; -} - -void MaskedInputField::setTextMargins(const QMargins &mrg) { - _textMargins = mrg; - setContentsMargins(_textMargins + QMargins(-2, -1, -2, -1)); - refreshPlaceholder(_placeholderFull.current()); -} - -void MaskedInputField::onTouchTimer() { - _touchRightButton = true; -} - -bool MaskedInputField::eventHook(QEvent *e) { - auto type = e->type(); - if (type == QEvent::TouchBegin - || type == QEvent::TouchUpdate - || type == QEvent::TouchEnd - || type == QEvent::TouchCancel) { - auto event = static_cast(e); - if (event->device()->type() == base::TouchDevice::TouchScreen) { - touchEvent(event); - } - } - return Parent::eventHook(e); -} - -void MaskedInputField::touchEvent(QTouchEvent *e) { - switch (e->type()) { - case QEvent::TouchBegin: { - if (_touchPress || e->touchPoints().isEmpty()) { - return; - } - _touchTimer.start(QApplication::startDragTime()); - _touchPress = true; - _touchMove = _touchRightButton = _mousePressedInTouch = false; - _touchStart = e->touchPoints().cbegin()->screenPos().toPoint(); - } break; - - case QEvent::TouchUpdate: { - if (!e->touchPoints().isEmpty()) { - touchUpdate(e->touchPoints().cbegin()->screenPos().toPoint()); - } - } break; - - case QEvent::TouchEnd: { - touchFinish(); - } break; - - case QEvent::TouchCancel: { - _touchPress = false; - _touchTimer.stop(); - } break; - } -} - -void MaskedInputField::touchUpdate(QPoint globalPosition) { - if (_touchPress - && !_touchMove - && ((globalPosition - _touchStart).manhattanLength() - >= QApplication::startDragDistance())) { - _touchMove = true; - } -} - -void MaskedInputField::touchFinish() { - if (!_touchPress) { - return; - } - const auto weak = MakeWeak(this); - if (!_touchMove && window()) { - QPoint mapped(mapFromGlobal(_touchStart)); - - if (_touchRightButton) { - QContextMenuEvent contextEvent( - QContextMenuEvent::Mouse, - mapped, - _touchStart); - contextMenuEvent(&contextEvent); - } else { - QGuiApplication::inputMethod()->show(); - } - } - if (weak) { - _touchTimer.stop(); - _touchPress - = _touchMove - = _touchRightButton - = _mousePressedInTouch = false; - } -} - -void MaskedInputField::paintEvent(QPaintEvent *e) { - auto p = QPainter(this); - - 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.value(_error ? 1. : 0.); - auto focusedDegree = _a_focused.value(_focused ? 1. : 0.); - auto borderShownDegree = _a_borderShown.value(1.); - auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.); - if (_st.borderActive && (borderOpacity > 0.)) { - auto borderStart = std::clamp(_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.value(_placeholderShifted ? 1. : 0.); - p.save(); - p.setClipRect(r); - - auto placeholderTop = anim::interpolate(0, _st.placeholderShift, placeholderShiftDegree); - - QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins)); - r.moveTop(r.top() + placeholderTop); - if (style::RightToLeft()) 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.value(_placeholderShifted ? 1. : 0.); - if (placeholderHiddenDegree < 1.) { - p.setOpacity(1. - placeholderHiddenDegree); - p.save(); - p.setClipRect(r); - - auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree); - - QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins)); - r.moveLeft(r.left() + placeholderLeft); - if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width()); - - p.setFont(_st.placeholderFont); - p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree)); - p.drawText(r, _placeholder, _st.placeholderAlign); - - p.restore(); - p.setOpacity(1.); - } - } - - paintAdditionalPlaceholder(p); - QLineEdit::paintEvent(e); -} - -void MaskedInputField::mousePressEvent(QMouseEvent *e) { - if (_touchPress && e->button() == Qt::LeftButton) { - _mousePressedInTouch = true; - _touchStart = e->globalPos(); - } - return QLineEdit::mousePressEvent(e); -} - -void MaskedInputField::mouseReleaseEvent(QMouseEvent *e) { - if (_mousePressedInTouch) { - touchFinish(); - } - return QLineEdit::mouseReleaseEvent(e); -} - -void MaskedInputField::mouseMoveEvent(QMouseEvent *e) { - if (_mousePressedInTouch) { - touchUpdate(e->globalPos()); - } - return QLineEdit::mouseMoveEvent(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.value(1.), 0.)) { - _a_borderShown.stop(); - _a_borderOpacity.stop(); - } 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); - focused(); -} - -void MaskedInputField::focusOutEvent(QFocusEvent *e) { - setFocused(false); - QLineEdit::focusOutEvent(e); - blurred(); -} - -void MaskedInputField::setFocused(bool focused) { - if (_focused != focused) { - _focused = focused; - _a_focused.start([this] { update(); }, _focused ? 0. : 1., _focused ? 1. : 0., _st.duration); - startPlaceholderAnimation(); - startBorderAnimation(); - } -} - -void MaskedInputField::resizeEvent(QResizeEvent *e) { - refreshPlaceholder(_placeholderFull.current()); - _borderAnimationStart = width() / 2; - QLineEdit::resizeEvent(e); -} - -void MaskedInputField::refreshPlaceholder(const QString &text) { - const auto availableWidth = width() - _textMargins.left() - _textMargins.right() - _st.placeholderMargins.left() - _st.placeholderMargins.right() - 1; - if (_st.placeholderScale > 0.) { - auto placeholderFont = _st.placeholderFont->f; - placeholderFont.setStyleStrategy(QFont::PreferMatch); - const auto metrics = QFontMetrics(placeholderFont); - _placeholder = metrics.elidedText(text, Qt::ElideRight, availableWidth); - _placeholderPath = QPainterPath(); - if (!_placeholder.isEmpty()) { - _placeholderPath.addText(0, QFontMetrics(placeholderFont).ascent(), placeholderFont, _placeholder); - } - } else { - _placeholder = _st.placeholderFont->elided(text, availableWidth); - } - update(); -} - -void MaskedInputField::setPlaceholder(rpl::producer placeholder) { - _placeholderFull = std::move(placeholder); -} - -void MaskedInputField::contextMenuEvent(QContextMenuEvent *e) { - if (const auto menu = createStandardContextMenu()) { - _contextMenu = base::make_unique_q(this, menu); - _contextMenu->popup(e->globalPos()); - } -} - -void MaskedInputField::inputMethodEvent(QInputMethodEvent *e) { - QLineEdit::inputMethodEvent(e); - _lastPreEditText = e->preeditString(); - update(); -} - -void MaskedInputField::showError() { - showErrorNoFocus(); - if (!hasFocus()) { - setFocus(); - } -} - -void MaskedInputField::showErrorNoFocus() { - setErrorShown(true); -} - -void MaskedInputField::hideError() { - setErrorShown(false); -} - -void MaskedInputField::setErrorShown(bool error) { - if (_error != error) { - _error = error; - _a_error.start([this] { update(); }, _error ? 0. : 1., _error ? 1. : 0., _st.duration); - startBorderAnimation(); - } -} - -QSize MaskedInputField::sizeHint() const { - return geometry().size(); -} - -QSize MaskedInputField::minimumSizeHint() const { - return geometry().size(); -} - -void MaskedInputField::setDisplayFocused(bool focused) { - setFocused(focused); - finishAnimating(); -} - -void MaskedInputField::finishAnimating() { - _a_focused.stop(); - _a_error.stop(); - _a_placeholderShifted.stop(); - _a_borderShown.stop(); - _a_borderOpacity.stop(); - update(); -} - -void MaskedInputField::setPlaceholderHidden(bool forcePlaceholderHidden) { - _forcePlaceholderHidden = forcePlaceholderHidden; - startPlaceholderAnimation(); -} - -void MaskedInputField::startPlaceholderAnimation() { - auto placeholderShifted = _forcePlaceholderHidden || (_focused && _st.placeholderScale > 0.) || !getLastText().isEmpty(); - if (_placeholderShifted != placeholderShifted) { - _placeholderShifted = placeholderShifted; - _a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration); - } -} - -QRect MaskedInputField::placeholderRect() const { - return rect().marginsRemoved(_textMargins + _st.placeholderMargins); -} - -void MaskedInputField::placeholderAdditionalPrepare(QPainter &p) { - p.setFont(_st.font); - p.setPen(_st.placeholderFg); -} - -void MaskedInputField::keyPressEvent(QKeyEvent *e) { - QString wasText(_oldtext); - int32 wasCursor(_oldcursor); - - if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) { - e->ignore(); - } else { - QLineEdit::keyPressEvent(e); - } - - auto newText = text(); - auto newCursor = cursorPosition(); - if (wasText == newText && wasCursor == newCursor) { // call correct manually - correctValue(wasText, wasCursor, newText, newCursor); - _oldtext = newText; - _oldcursor = newCursor; - if (wasText != _oldtext) changed(); - startPlaceholderAnimation(); - } - if (e->key() == Qt::Key_Escape) { - e->ignore(); - cancelled(); - } else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { - submitted(e->modifiers()); -#ifdef Q_OS_MAC - } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) { - auto selected = selectedText(); - if (!selected.isEmpty() && echoMode() == QLineEdit::Normal) { - QGuiApplication::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) changed(); - startPlaceholderAnimation(); - - Integration::Instance().textActionsUpdated(); -} - -void MaskedInputField::onTextChange(const QString &text) { - _oldtext = QLineEdit::text(); - setErrorShown(false); - Integration::Instance().textActionsUpdated(); -} - -void MaskedInputField::onCursorPositionChanged(int oldPosition, int position) { - _oldcursor = position; -} - } // namespace Ui diff --git a/ui/widgets/input_fields.h b/ui/widgets/input_fields.h index e244a0e..f371454 100644 --- a/ui/widgets/input_fields.h +++ b/ui/widgets/input_fields.h @@ -563,159 +563,4 @@ private: }; -class MaskedInputField : public RpWidgetBase { - Q_OBJECT - - using Parent = RpWidgetBase; -public: - MaskedInputField( - QWidget *parent, - const style::InputField &st, - rpl::producer placeholder = nullptr, - const QString &val = QString()); - - void showError(); - void showErrorNoFocus(); - void hideError(); - - QSize sizeHint() const override; - QSize minimumSizeHint() const override; - - void customUpDown(bool isCustom); - int borderAnimationStart() const; - - const QString &getLastText() const { - return _oldtext; - } - void setPlaceholder(rpl::producer placeholder); - void setPlaceholderHidden(bool forcePlaceholderHidden); - void setDisplayFocused(bool focused); - void finishAnimating(); - void setFocusFast() { - setDisplayFocused(true); - setFocus(); - } - - void setText(const QString &text) { - QLineEdit::setText(text); - startPlaceholderAnimation(); - } - void clear() { - QLineEdit::clear(); - startPlaceholderAnimation(); - } - -public Q_SLOTS: - void onTextChange(const QString &text); - void onCursorPositionChanged(int oldPosition, int position); - - void onTextEdited(); - - void onTouchTimer(); - -Q_SIGNALS: - void changed(); - void cancelled(); - void submitted(Qt::KeyboardModifiers); - void focused(); - void blurred(); - -protected: - QString getDisplayedText() const { - auto result = getLastText(); - if (!_lastPreEditText.isEmpty()) { - result = result.mid(0, _oldcursor) + _lastPreEditText + result.mid(_oldcursor); - } - return result; - } - void startBorderAnimation(); - void startPlaceholderAnimation(); - - bool eventHook(QEvent *e) override; - void touchEvent(QTouchEvent *e); - void paintEvent(QPaintEvent *e) override; - void focusInEvent(QFocusEvent *e) override; - void focusOutEvent(QFocusEvent *e) override; - void keyPressEvent(QKeyEvent *e) override; - void resizeEvent(QResizeEvent *e) override; - void contextMenuEvent(QContextMenuEvent *e) override; - void inputMethodEvent(QInputMethodEvent *e) override; - - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - - virtual void correctValue( - const QString &was, - int wasCursor, - QString &now, - int &nowCursor) { - } - void setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos); - - virtual void paintAdditionalPlaceholder(QPainter &p) { - } - - style::font phFont() { - return _st.font; - } - - void placeholderAdditionalPrepare(QPainter &p); - QRect placeholderRect() const; - - void setTextMargins(const QMargins &mrg); - const style::InputField &_st; - -private: - void updatePalette(); - void refreshPlaceholder(const QString &text); - void setErrorShown(bool error); - - void touchUpdate(QPoint globalPosition); - void touchFinish(); - - void setFocused(bool focused); - - int _maxLength = -1; - bool _forcePlaceholderHidden = false; - - QString _oldtext; - int _oldcursor = 0; - QString _lastPreEditText; - - bool _undoAvailable = false; - bool _redoAvailable = false; - - bool _customUpDown = false; - - rpl::variable _placeholderFull; - QString _placeholder; - Animations::Simple _a_placeholderShifted; - bool _placeholderShifted = false; - QPainterPath _placeholderPath; - - Animations::Simple _a_borderShown; - int _borderAnimationStart = 0; - Animations::Simple _a_borderOpacity; - bool _borderVisible = false; - - Animations::Simple _a_focused; - Animations::Simple _a_error; - - bool _focused = false; - bool _error = false; - - style::margins _textMargins; - - QTimer _touchTimer; - bool _touchPress = false; - bool _touchRightButton = false; - bool _touchMove = false; - bool _mousePressedInTouch = false; - QPoint _touchStart; - - base::unique_qptr _contextMenu; - -}; - } // namespace Ui