343 lines
		
	
	
	
		
			8.6 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			343 lines
		
	
	
	
		
			8.6 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
This file is part of Telegram Desktop,
 | 
						|
the official desktop application for the Telegram messaging service.
 | 
						|
 | 
						|
For license and copyright information please follow this link:
 | 
						|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | 
						|
*/
 | 
						|
#include "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_basic.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) {
 | 
						|
		_animation.stop();
 | 
						|
		if (digit == kDigitNone) {
 | 
						|
			_animation.start([=](float64 value) {
 | 
						|
				update();
 | 
						|
				if (!value) {
 | 
						|
					_viewDigit = digit;
 | 
						|
				}
 | 
						|
			}, 1., 0., st::universalDuration);
 | 
						|
		} else {
 | 
						|
			_viewDigit = digit;
 | 
						|
			_animation.start([=] { update(); }, 0, 1., st::universalDuration);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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
 |