mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-26 19:14:02 +02:00
342 lines
8.6 KiB
C++
342 lines
8.6 KiB
C++
// 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 "intro/intro_code_input.h"
|
|
|
|
#include "lang/lang_keys.h"
|
|
#include "ui/abstract_button.h"
|
|
#include "ui/effects/shake_animation.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/rect.h"
|
|
#include "ui/widgets/popup_menu.h"
|
|
#include "styles/style_intro.h"
|
|
#include "styles/style_layers.h" // boxRadius
|
|
|
|
#include <QtCore/QRegularExpression>
|
|
#include <QtGui/QClipboard>
|
|
#include <QtGui/QGuiApplication>
|
|
|
|
namespace Ui {
|
|
namespace {
|
|
|
|
constexpr auto kDigitNone = int(-1);
|
|
|
|
[[nodiscard]] int Circular(int left, int right) {
|
|
return ((left % right) + right) % right;
|
|
}
|
|
|
|
class Shaker final {
|
|
public:
|
|
explicit Shaker(not_null<Ui::RpWidget*> widget);
|
|
|
|
void shake();
|
|
|
|
private:
|
|
const not_null<Ui::RpWidget*> _widget;
|
|
Ui::Animations::Simple _animation;
|
|
|
|
};
|
|
|
|
Shaker::Shaker(not_null<Ui::RpWidget*> widget)
|
|
: _widget(widget) {
|
|
}
|
|
|
|
void Shaker::shake() {
|
|
if (_animation.animating()) {
|
|
return;
|
|
}
|
|
_animation.start(DefaultShakeCallback([=, x = _widget->x()](int shift) {
|
|
_widget->moveToLeft(x + shift, _widget->y());
|
|
}), 0., 1., st::shakeDuration);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class CodeDigit final : public Ui::AbstractButton {
|
|
public:
|
|
explicit CodeDigit(not_null<Ui::RpWidget*> widget);
|
|
|
|
void setDigit(int digit);
|
|
[[nodiscard]] int digit() const;
|
|
|
|
void setBorderColor(const QBrush &brush);
|
|
void shake();
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent *e) override;
|
|
|
|
private:
|
|
Shaker _shaker;
|
|
Ui::Animations::Simple _animation;
|
|
int _dataDigit = kDigitNone;
|
|
int _viewDigit = kDigitNone;
|
|
|
|
QPen _borderPen;
|
|
|
|
};
|
|
|
|
CodeDigit::CodeDigit(not_null<Ui::RpWidget*> widget)
|
|
: Ui::AbstractButton(widget)
|
|
, _shaker(this) {
|
|
setBorderColor(st::windowBgRipple);
|
|
}
|
|
|
|
void CodeDigit::setDigit(int digit) {
|
|
if ((_dataDigit == digit) && _animation.animating()) {
|
|
return;
|
|
}
|
|
_dataDigit = digit;
|
|
if (_viewDigit != digit) {
|
|
constexpr auto kDuration = st::introCodeDigitAnimatioDuration;
|
|
_animation.stop();
|
|
if (digit == kDigitNone) {
|
|
_animation.start([=](float64 value) {
|
|
update();
|
|
if (!value) {
|
|
_viewDigit = digit;
|
|
}
|
|
}, 1., 0., kDuration);
|
|
} else {
|
|
_viewDigit = digit;
|
|
_animation.start([=] { update(); }, 0., 1., kDuration);
|
|
}
|
|
}
|
|
}
|
|
|
|
int CodeDigit::digit() const {
|
|
return _dataDigit;
|
|
}
|
|
|
|
void CodeDigit::setBorderColor(const QBrush &brush) {
|
|
_borderPen = QPen(brush, st::introCodeDigitBorderWidth);
|
|
update();
|
|
}
|
|
|
|
void CodeDigit::shake() {
|
|
_shaker.shake();
|
|
}
|
|
|
|
void CodeDigit::paintEvent(QPaintEvent *e) {
|
|
auto p = QPainter(this);
|
|
|
|
auto clipPath = QPainterPath();
|
|
clipPath.addRoundedRect(rect(), st::boxRadius, st::boxRadius);
|
|
p.setClipPath(clipPath);
|
|
|
|
p.fillRect(rect(), st::windowBgOver);
|
|
{
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.strokePath(clipPath, _borderPen);
|
|
}
|
|
|
|
if (_viewDigit == kDigitNone) {
|
|
return;
|
|
}
|
|
const auto hiding = (_dataDigit == kDigitNone);
|
|
const auto progress = _animation.value(1.);
|
|
|
|
if (hiding) {
|
|
p.setOpacity(progress * progress);
|
|
const auto center = rect().center();
|
|
p.setTransform(QTransform()
|
|
.translate(center.x(), center.y())
|
|
.scale(progress, progress)
|
|
.translate(-center.x(), -center.y()));
|
|
} else {
|
|
p.setOpacity(progress);
|
|
constexpr auto kSlideDistanceRatio = 0.2;
|
|
const auto distance = rect().height() * kSlideDistanceRatio;
|
|
p.translate(0, (distance * (1. - progress)));
|
|
}
|
|
p.setFont(st::introCodeDigitFont);
|
|
p.setPen(st::windowFg);
|
|
p.drawText(rect(), QString::number(_viewDigit), style::al_center);
|
|
}
|
|
|
|
CodeInput::CodeInput(QWidget *parent)
|
|
: Ui::RpWidget(parent) {
|
|
setFocusPolicy(Qt::StrongFocus);
|
|
}
|
|
|
|
void CodeInput::setDigitsCountMax(int digitsCount) {
|
|
_digitsCountMax = digitsCount;
|
|
|
|
_digits.clear();
|
|
_currentIndex = 0;
|
|
|
|
constexpr auto kWidthRatio = 0.8;
|
|
const auto digitWidth = st::introCodeDigitHeight * kWidthRatio;
|
|
const auto padding = Margins(st::introCodeDigitSkip);
|
|
resize(
|
|
padding.left()
|
|
+ digitWidth * digitsCount
|
|
+ st::introCodeDigitSkip * (digitsCount - 1)
|
|
+ padding.right(),
|
|
st::introCodeDigitHeight);
|
|
|
|
for (auto i = 0; i < digitsCount; i++) {
|
|
const auto widget = Ui::CreateChild<CodeDigit>(this);
|
|
widget->setPointerCursor(false);
|
|
widget->setClickedCallback([=] { unfocusAll(_currentIndex = i); });
|
|
widget->resize(digitWidth, st::introCodeDigitHeight);
|
|
widget->moveToLeft(
|
|
padding.left() + (digitWidth + st::introCodeDigitSkip) * i,
|
|
0);
|
|
_digits.emplace_back(widget);
|
|
}
|
|
}
|
|
|
|
void CodeInput::setCode(QString code) {
|
|
using namespace TextUtilities;
|
|
code = code.remove(RegExpDigitsExclude()).mid(0, _digitsCountMax);
|
|
for (int i = 0; i < _digits.size(); i++) {
|
|
if (i >= code.size()) {
|
|
return;
|
|
}
|
|
_digits[i]->setDigit(code.at(i).digitValue());
|
|
}
|
|
}
|
|
|
|
void CodeInput::requestCode() {
|
|
const auto result = collectDigits();
|
|
if (result.size() == _digitsCountMax) {
|
|
_codeCollected.fire_copy(result);
|
|
} else {
|
|
findEmptyAndPerform([&](int i) { _digits[i]->shake(); });
|
|
}
|
|
}
|
|
|
|
rpl::producer<QString> CodeInput::codeCollected() const {
|
|
return _codeCollected.events();
|
|
}
|
|
|
|
void CodeInput::clear() {
|
|
for (const auto &digit : _digits) {
|
|
digit->setDigit(kDigitNone);
|
|
}
|
|
unfocusAll(_currentIndex = 0);
|
|
}
|
|
|
|
void CodeInput::showError() {
|
|
clear();
|
|
for (const auto &digit : _digits) {
|
|
digit->shake();
|
|
digit->setBorderColor(st::activeLineFgError);
|
|
}
|
|
}
|
|
|
|
void CodeInput::focusInEvent(QFocusEvent *e) {
|
|
unfocusAll(_currentIndex);
|
|
}
|
|
|
|
void CodeInput::focusOutEvent(QFocusEvent *e) {
|
|
unfocusAll(kDigitNone);
|
|
}
|
|
|
|
void CodeInput::paintEvent(QPaintEvent *e) {
|
|
auto p = QPainter(this);
|
|
p.fillRect(rect(), st::windowBg);
|
|
}
|
|
|
|
void CodeInput::keyPressEvent(QKeyEvent *e) {
|
|
const auto key = e->key();
|
|
if (key == Qt::Key_Down || key == Qt::Key_Right || key == Qt::Key_Space) {
|
|
_currentIndex = Circular(_currentIndex + 1, _digits.size());
|
|
unfocusAll(_currentIndex);
|
|
} else if (key == Qt::Key_Up || key == Qt::Key_Left) {
|
|
_currentIndex = Circular(_currentIndex - 1, _digits.size());
|
|
unfocusAll(_currentIndex);
|
|
} else if (key >= Qt::Key_0 && key <= Qt::Key_9) {
|
|
const auto index = int(key - Qt::Key_0);
|
|
_digits[_currentIndex]->setDigit(index);
|
|
_currentIndex = Circular(_currentIndex + 1, _digits.size());
|
|
if (!_currentIndex) {
|
|
const auto result = collectDigits();
|
|
if (result.size() == _digitsCountMax) {
|
|
_codeCollected.fire_copy(result);
|
|
_currentIndex = _digits.size() - 1;
|
|
} else {
|
|
findEmptyAndPerform([&](int i) { _currentIndex = i; });
|
|
}
|
|
}
|
|
unfocusAll(_currentIndex);
|
|
} else if (key == Qt::Key_Delete) {
|
|
_digits[_currentIndex]->setDigit(kDigitNone);
|
|
} else if (key == Qt::Key_Backspace) {
|
|
const auto wasDigit = _digits[_currentIndex]->digit();
|
|
_digits[_currentIndex]->setDigit(kDigitNone);
|
|
_currentIndex = std::clamp(_currentIndex - 1, 0, int(_digits.size()));
|
|
if (wasDigit == kDigitNone) {
|
|
_digits[_currentIndex]->setDigit(kDigitNone);
|
|
}
|
|
unfocusAll(_currentIndex);
|
|
} else if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
|
requestCode();
|
|
} else if (e == QKeySequence::Paste) {
|
|
insertCodeAndSubmit(QGuiApplication::clipboard()->text());
|
|
} else if (key >= Qt::Key_A && key <= Qt::Key_Z) {
|
|
_digits[_currentIndex]->shake();
|
|
} else if (key == Qt::Key_Home || key == Qt::Key_PageUp) {
|
|
unfocusAll(_currentIndex = 0);
|
|
} else if (key == Qt::Key_End || key == Qt::Key_PageDown) {
|
|
unfocusAll(_currentIndex = (_digits.size() - 1));
|
|
}
|
|
}
|
|
|
|
void CodeInput::contextMenuEvent(QContextMenuEvent *e) {
|
|
if (_menu) {
|
|
return;
|
|
}
|
|
_menu = base::make_unique_q<Ui::PopupMenu>(this, st::defaultPopupMenu);
|
|
_menu->addAction(tr::lng_mac_menu_paste(tr::now), [=] {
|
|
insertCodeAndSubmit(QGuiApplication::clipboard()->text());
|
|
})->setEnabled(!QGuiApplication::clipboard()->text().isEmpty());
|
|
_menu->popup(QCursor::pos());
|
|
}
|
|
|
|
void CodeInput::insertCodeAndSubmit(const QString &code) {
|
|
if (code.isEmpty()) {
|
|
return;
|
|
}
|
|
setCode(code);
|
|
_currentIndex = _digits.size() - 1;
|
|
findEmptyAndPerform([&](int i) { _currentIndex = i; });
|
|
unfocusAll(_currentIndex);
|
|
if ((_currentIndex == _digits.size() - 1)
|
|
&& _digits[_currentIndex]->digit() != kDigitNone) {
|
|
requestCode();
|
|
}
|
|
}
|
|
|
|
QString CodeInput::collectDigits() const {
|
|
auto result = QString();
|
|
for (const auto &digit : _digits) {
|
|
if (digit->digit() != kDigitNone) {
|
|
result += QString::number(digit->digit());
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void CodeInput::unfocusAll(int except) {
|
|
for (auto i = 0; i < _digits.size(); i++) {
|
|
const auto focused = (i == except);
|
|
_digits[i]->setBorderColor(focused
|
|
? st::windowActiveTextFg
|
|
: st::windowBgRipple);
|
|
}
|
|
}
|
|
|
|
void CodeInput::findEmptyAndPerform(const Fn<void(int)> &callback) {
|
|
for (auto i = 0; i < _digits.size(); i++) {
|
|
if (_digits[i]->digit() == kDigitNone) {
|
|
callback(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace Ui
|