tdesktop/Telegram/SourceFiles/gui/countryinput.cpp

614 lines
18 KiB
C++

/*
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.
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014 John Preston, https://desktop.telegram.org
*/
#include "stdafx.h"
#include "style.h"
#include "lang.h"
#include "application.h"
#include "gui/countryinput.h"
#include "gui/scrollarea.h"
namespace {
struct CountryInfo {
CountryInfo(const char *_name, const char *_iso2, const char *_code) : name(_name), iso2(_iso2), code(_code) {
}
const char *name, *iso2, *code;
};
#include "countries.h"
typedef QHash<QString, const CountryInfo *> CountriesByCode;
typedef QHash<QString, const CountryInfo *> CountriesByISO2;
typedef QList<const CountryInfo *> CountriesFiltered;
typedef QVector<int> CountriesIds;
typedef QHash<QChar, CountriesIds> CountriesByLetter;
typedef QVector<QString> CountryNames;
typedef QVector<CountryNames> CountriesNames;
CountriesByCode countriesByCode;
CountriesByISO2 countriesByISO2;
CountriesFiltered countriesFiltered, countriesAll, *countriesNow = &countriesAll;
CountriesByLetter countriesByLetter;
CountriesNames countriesNames;
QString lastFilter, lastValidISO;
int countriesCount = sizeof(countries) / sizeof(countries[0]);
void initCountries() {
if (countriesByCode.size()) return;
countriesByCode.reserve(countriesCount);
countriesByISO2.reserve(countriesCount);
for (int i = 0; i < countriesCount; ++i) {
const CountryInfo *info(countries + i);
countriesByCode.insert(info->code, info);
CountriesByISO2::const_iterator already = countriesByISO2.constFind(info->iso2);
if (already != countriesByISO2.cend()) {
QString badISO = info->iso2;
(void)badISO;
}
countriesByISO2.insert(info->iso2, info);
}
countriesAll.reserve(countriesCount);
countriesFiltered.reserve(countriesCount);
countriesNames.resize(countriesCount);
}
}
QString findValidCode(QString fullCode) {
while (fullCode.length()) {
CountriesByCode::const_iterator i = countriesByCode.constFind(fullCode);
if (i != countriesByCode.cend()) {
return (*i)->code;
}
fullCode = fullCode.mid(0, fullCode.length() - 1);
}
return "";
}
CountryInput::CountryInput(QWidget *parent, const style::countryInput &st) : QWidget(parent), _st(st), _active(false), _text(lang(lng_country_code)), _select(0) {
initCountries();
resize(_st.width, _st.height + _st.ptrSize.height());
QImage trImage(_st.ptrSize.width(), _st.ptrSize.height(), QImage::Format_ARGB32_Premultiplied);
{
static const QPoint trPoints[3] = {
QPoint(0, 0),
QPoint(_st.ptrSize.width(), 0),
QPoint(qCeil(trImage.width() / 2.), trImage.height())
};
QPainter p(&trImage);
p.setRenderHint(QPainter::Antialiasing);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.fillRect(0, 0, trImage.width(), trImage.height(), st::transparent->b);
p.setPen(Qt::NoPen);
p.setBrush(_st.bgColor->b);
p.drawPolygon(trPoints, 3);
}
_arrow = QPixmap::fromImage(trImage);
_inner = QRect(0, 0, _st.width, _st.height);
_arrowRect = QRect((st::inpIntroCountryCode.width - _arrow.width() - 1) / 2, _st.height, _arrow.width(), _arrow.height());
}
void CountryInput::paintEvent(QPaintEvent *e) {
QPainter p(this);
p.fillRect(_inner, _st.bgColor->b);
p.drawPixmap(_arrowRect.x(), _arrowRect.top(), _arrow);
p.setFont(_st.font->f);
p.drawText(rect().marginsRemoved(_st.textMrg), _text, QTextOption(_st.align));
}
void CountryInput::mouseMoveEvent(QMouseEvent *e) {
bool newActive = _inner.contains(e->pos()) || _arrowRect.contains(e->pos());
if (_active != newActive) {
_active = newActive;
setCursor(_active ? style::cur_pointer : style::cur_default);
}
}
void CountryInput::mousePressEvent(QMouseEvent *e) {
mouseMoveEvent(e);
if (_active) {
Window *w = App::wnd();
if (w->focusWidget()) w->focusWidget()->clearFocus();
if (_select) {
_select->hide();
_select->deleteLater();
}
_select = new CountrySelect();
connect(_select, SIGNAL(countryChosen(const QString &)), this, SLOT(onChooseCountry(const QString &)));
connect(_select, SIGNAL(countryFinished()), this, SLOT(onFinishCountry()));
}
}
void CountryInput::enterEvent(QEvent *e) {
setMouseTracking(true);
}
void CountryInput::leaveEvent(QEvent *e) {
setMouseTracking(false);
_active = false;
setCursor(style::cur_default);
}
void CountryInput::onChooseCode(const QString &code) {
if (_select) {
_select->hide();
_select->deleteLater();
_select = 0;
emit selectClosed();
}
if (code.length()) {
CountriesByCode::const_iterator i = countriesByCode.constFind(code);
if (i != countriesByCode.cend()) {
const CountryInfo *info = *i;
lastValidISO = info->iso2;
setText(QString::fromUtf8(info->name));
} else {
setText(lang(lng_bad_country_code));
}
} else {
setText(lang(lng_country_code));
}
update();
}
bool CountryInput::onChooseCountry(const QString &iso) {
CountriesByISO2::const_iterator i = countriesByISO2.constFind(iso);
const CountryInfo *info = (i == countriesByISO2.cend()) ? 0 : (*i);
if (info) {
lastValidISO = info->iso2;
setText(QString::fromUtf8(info->name));
emit codeChanged(info->code);
update();
return true;
}
return false;
}
void CountryInput::onFinishCountry() {
if (_select) {
_select->hide();
_select->deleteLater();
_select = 0;
emit selectClosed();
}
}
void CountryInput::setText(const QString &newText) {
_text = _st.font->m.elidedText(newText, Qt::ElideRight, width() - _st.textMrg.left() - _st.textMrg.right());
}
CountryInput::~CountryInput() {
delete _select;
}
CountryList::CountryList(QWidget *parent, const style::countryList &st) : QWidget(parent), _sel(0),
_st(st), _mouseSel(false) {
CountriesByISO2::const_iterator l = countriesByISO2.constFind(lastValidISO);
bool seenLastValid = false;
int already = countriesAll.size();
countriesByLetter.clear();
const CountryInfo *lastValid = (l == countriesByISO2.cend()) ? 0 : (*l);
for (int i = 0; i < countriesCount; ++i) {
const CountryInfo *ins = lastValid ? (i ? (countries + i - (seenLastValid ? 0 : 1)) : lastValid) : (countries + i);
if (lastValid && i && ins == lastValid) {
seenLastValid = true;
++ins;
}
if (already > i) {
countriesAll[i] = ins;
} else {
countriesAll.push_back(ins);
}
QStringList namesList = QString::fromUtf8(ins->name).toLower().split(QRegularExpression("[\\s\\-]"), QString::SkipEmptyParts);
CountryNames &names(countriesNames[i]);
int l = namesList.size();
names.resize(0);
names.reserve(l);
for (int j = 0, l = namesList.size(); j < l; ++j) {
QString name = namesList[j].trimmed();
if (!name.length()) continue;
QChar ch = name[0];
CountriesIds &v(countriesByLetter[ch]);
if (v.isEmpty() || v.back() != i) {
v.push_back(i);
}
names.push_back(name);
}
}
lastFilter = "";
resetList();
}
void CountryList::resetList() {
countriesNow = &countriesAll;
if (lastFilter.length()) {
QChar first = lastFilter[0].toLower();
CountriesIds &ids(countriesByLetter[first]);
QStringList filterList = lastFilter.split(QRegularExpression("[\\s\\-]"), QString::SkipEmptyParts);
int l = filterList.size();
CountryNames filter;
filter.reserve(l);
for (int i = 0; i < l; ++i) {
QString filterName = filterList[i].trimmed();
if (!filterName.length()) continue;
filter.push_back(filterName);
}
CountryNames::const_iterator fb = filter.cbegin(), fe = filter.cend(), fi;
countriesFiltered.clear();
for (CountriesIds::const_iterator i = ids.cbegin(), e = ids.cend(); i != e; ++i) {
int index = *i;
CountryNames &names(countriesNames[index]);
CountryNames::const_iterator nb = names.cbegin(), ne = names.cend(), ni;
for (fi = fb; fi != fe; ++fi) {
QString filterName(*fi);
for (ni = nb; ni != ne; ++ni) {
if (ni->startsWith(*fi)) {
break;
}
}
if (ni == ne) {
break;
}
}
if (fi == fe) {
countriesFiltered.push_back(countriesAll[index]);
}
}
countriesNow = &countriesFiltered;
}
resize(width(), countriesNow->length() ? (countriesNow->length() * _st.rowHeight + 2 * _st.verticalMargin) : parentWidget()->height());
setSelected(0);
}
void CountryList::paintEvent(QPaintEvent *e) {
QRect r(e->rect());
bool trivial = (rect() == r);
QPainter p(this);
if (!trivial) {
p.setClipRect(r);
}
int l = countriesNow->size();
if (l) {
int from = (r.top() > _st.verticalMargin) ? (r.top() - _st.verticalMargin) / _st.rowHeight : 0, to = from + (r.height() / _st.rowHeight) + 2;
if (to >= l) {
if (from >= l) return;
to = l;
}
p.setFont(_st.font->f);
QRectF textRect(_st.margin + _st.borderMargin, _st.verticalMargin + from * _st.rowHeight, width() - 2 * _st.margin - 2 * _st.borderMargin, _st.rowHeight - _st.borderWidth);
for (int i = from; i < to; ++i) {
bool sel = (i == _sel);
if (sel) {
p.fillRect(_st.borderMargin, _st.verticalMargin + i * _st.rowHeight, width() - 2 * _st.borderMargin, _st.rowHeight, _st.bgHovered->b);
}
p.setFont(_st.font->f);
p.setPen(_st.color->p);
p.drawText(textRect, _st.font->m.elidedText(QString::fromUtf8((*countriesNow)[i]->name), Qt::ElideRight, width() - 2 * _st.margin - _st.codeWidth), QTextOption(style::al_left));
p.setFont(_st.codeFont->f);
p.setPen(_st.codeColor->p);
p.drawText(textRect, QString("+") + (*countriesNow)[i]->code, QTextOption(style::al_right));
textRect.setBottom(textRect.bottom() + _st.rowHeight);
textRect.setTop(textRect.top() + _st.rowHeight);
}
} else {
p.setFont(_st.notFoundFont->f);
p.setPen(_st.notFoundColor->p);
p.drawText(r, lang(lng_country_none), QTextOption(style::al_center));
}
}
void CountryList::mouseMoveEvent(QMouseEvent *e) {
_mouseSel = true;
_mousePos = mapToGlobal(e->pos());
onUpdateSelected(true);
}
void CountryList::onUpdateSelected(bool force) {
QPoint p(mapFromGlobal(_mousePos));
if ((!force && !rect().contains(p)) || !_mouseSel) return;
int newSelected = p.y();
newSelected = (newSelected > _st.verticalMargin) ? (newSelected - _st.verticalMargin) / _st.rowHeight : 0;
int l = countriesNow->size();
if (newSelected >= l) newSelected = l - 1;
if (newSelected < 0) newSelected = 0;
if (newSelected != _sel) {
_sel = newSelected;
update();
}
}
void CountryList::mousePressEvent(QMouseEvent *e) {
_mouseSel = true;
_mousePos = mapToGlobal(e->pos());
onUpdateSelected(true);
emit countrySelected();
}
void CountryList::enterEvent(QEvent *e) {
setMouseTracking(true);
}
void CountryList::leaveEvent(QEvent *e) {
setMouseTracking(false);
}
void CountryList::updateFiltered() {
resetList();
}
void CountryList::onParentGeometryChanged() {
_mousePos = QCursor::pos();
if (rect().contains(mapFromGlobal(_mousePos))) {
setMouseTracking(true);
onUpdateSelected(true);
}
}
void CountryList::selectSkip(int delta) {
setSelected(_sel + delta);
}
void CountryList::selectSkipPage(int h, int delta) {
setSelected(_sel + delta * (h / int(_st.rowHeight) - 1));
}
void CountryList::setSelected(int newSelected) {
_mouseSel = false;
if (newSelected >= countriesNow->size()) {
newSelected = countriesNow->size() - 1;
}
if (newSelected < 0) {
newSelected = 0;
}
_sel = newSelected;
emit mustScrollTo(_sel * _st.rowHeight, (_sel + 1) * _st.rowHeight);
update();
}
QString CountryList::getSelectedCountry() const {
if (lastFilter.length()) {
if (_sel < countriesFiltered.size()) {
return countriesFiltered[_sel]->iso2;
} else {
return "";
}
}
return countriesAll[_sel]->iso2;
}
CountrySelect::CountrySelect() : QWidget(App::wnd()),
_result("none"),
_filter(this, st::inpCountry, lang(lng_country_ph)), _scroll(this, st::scrollCountries), _list(&_scroll),
_doneButton(this, lang(lng_country_done), st::btnSelectDone),
_cancelButton(this, lang(lng_cancel), st::btnSelectCancel),
_innerLeft(0), _innerTop(0), _innerWidth(0), _innerHeight(0),
a_alpha(0), a_bgAlpha(0), a_coord(st::countriesSlideShift), _shadow(st::boxShadow) {
setGeometry(App::wnd()->rect());
App::wnd()->topWidget(this);
connect(App::wnd(), SIGNAL(resized(const QSize &)), this, SLOT(onParentResize(const QSize &)));
connect(&_doneButton, SIGNAL(clicked()), this, SLOT(onCountryChoose()));
connect(&_cancelButton, SIGNAL(clicked()), this, SLOT(onCountryCancel()));
connect(&_scroll, SIGNAL(scrollFinished()), this, SLOT(onScrollFinished()));
connect(&_scroll, SIGNAL(geometryChanged()), &_list, SLOT(onParentGeometryChanged()));
connect(&_scroll, SIGNAL(scrolled()), &_list, SLOT(onUpdateSelected()));
connect(&_list, SIGNAL(countrySelected()), this, SLOT(onCountryChoose()));
connect(&_filter, SIGNAL(changed()), this, SLOT(onFilterUpdate()));
connect(&_list, SIGNAL(mustScrollTo(int, int)), &_scroll, SLOT(scrollToY(int, int)));
show();
setFocus();
_scroll.setWidget(&_list);
_scroll.setFocusPolicy(Qt::NoFocus);
prepareAnimation(0);
}
void CountrySelect::prepareAnimation(int to) {
if (to) {
if (_result == "none") _result = "";
a_alpha.start(0);
af_alpha = st::countriesAlphaHideFunc;
a_bgAlpha.start(0);
af_bgAlpha = st::countriesBackHideFunc;
a_coord.start(to * st::countriesSlideShift);
af_coord = st::countriesHideFunc;
} else {
_result = "none";
a_alpha.start(1);
af_alpha = st::countriesAlphaShowFunc;
a_bgAlpha.start(1);
af_bgAlpha = st::countriesBackShowFunc;
a_coord.start(0);
af_coord = st::countriesShowFunc;
}
_cache = myGrab(this, QRect(_innerLeft, _innerTop, _innerWidth, _innerHeight));
_scroll.hide();
_doneButton.hide();
_cancelButton.hide();
_filter.hide();
anim::start(this);
}
void CountrySelect::paintEvent(QPaintEvent *e) {
bool trivial = (rect() == e->rect());
QPainter p(this);
if (!trivial) {
p.setClipRect(e->rect());
}
p.setOpacity(st::layerAlpha * a_bgAlpha.current());
p.fillRect(rect(), st::layerBG->b);
if (animating()) {
p.setOpacity(a_alpha.current());
p.drawPixmap(a_coord.current() + _innerLeft, _innerTop, _cache);
} else {
p.setOpacity(1);
QRect inner(_innerLeft, _innerTop, _innerWidth, _innerHeight);
_shadow.paint(p, inner);
if (trivial || e->rect().intersects(inner)) {
// fill bg
p.fillRect(inner, st::white->b);
// paint shadows
p.fillRect(_innerLeft, _innerTop + st::participantFilter.height, _innerWidth, st::scrollDef.topsh, st::scrollDef.shColor->b);
// paint button sep
p.fillRect(_innerLeft + st::btnSelectCancel.width, _innerTop + _innerHeight - st::btnSelectCancel.height, st::lineWidth, st::btnSelectCancel.height, st::btnSelectSep->b);
// draw box title / text
p.setPen(st::black->p);
p.setFont(st::addContactTitleFont->f);
p.drawText(_innerLeft + st::addContactTitlePos.x(), _innerTop + st::addContactTitlePos.y() + st::addContactTitleFont->ascent, lang(lng_country_select));
}
}
}
void CountrySelect::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape) {
onCountryCancel();
} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
onCountryChoose();
} else if (e->key() == Qt::Key_Down) {
_list.selectSkip(1);
} else if (e->key() == Qt::Key_Up) {
_list.selectSkip(-1);
} else if (e->key() == Qt::Key_PageDown) {
_list.selectSkipPage(_scroll.height(), 1);
} else if (e->key() == Qt::Key_PageUp) {
_list.selectSkipPage(_scroll.height(), -1);
}
}
void CountrySelect::mousePressEvent(QMouseEvent *e) {
if (!QRect(_innerLeft, _innerTop, _innerWidth, _innerHeight).contains(e->pos())) {
onCountryCancel();
}
}
void CountrySelect::onFilterUpdate() {
QString newFilter(_filter.text().trimmed().toLower());
if (newFilter != lastFilter) {
lastFilter = newFilter;
_list.updateFiltered();
}
}
void CountrySelect::resizeEvent(QResizeEvent *e) {
if (width() != e->oldSize().width()) {
_innerWidth = st::newGroupNamePadding.left() + _filter.width() + st::newGroupNamePadding.right();
_innerLeft = (width() - _innerWidth) / 2;
_list.resize(_innerWidth, _list.height());
}
if (height() != e->oldSize().height()) {
_innerTop = st::introSelectDelta;
_innerHeight = height() - _innerTop - st::introSelectDelta;
if (_innerHeight > st::introSelectMaxHeight) {
_innerHeight = st::introSelectMaxHeight;
_innerTop = (height() - _innerHeight) / 2;
}
}
_filter.move(_innerLeft + st::newGroupNamePadding.left(), _innerTop + st::contactsAdd.height + st::newGroupNamePadding.top());
int32 scrollTop = _filter.y() + _filter.height() + st::newGroupNamePadding.bottom();
int32 scrollHeight = _innerHeight - st::contactsAdd.height - st::newGroupNamePadding.top() - _filter.height() - st::newGroupNamePadding.bottom() - _cancelButton.height();
_scroll.setGeometry(_innerLeft, scrollTop, _innerWidth, scrollHeight);
int btnTop = scrollTop + scrollHeight;
_cancelButton.move(_innerLeft, btnTop);
_doneButton.move(_innerLeft + _innerWidth - _doneButton.width(), btnTop);
}
bool CountrySelect::animStep(float64 ms) {
float64 dt = ms / st::countriesSlideDuration;
bool res = true;
if (dt >= 1) {
a_alpha.finish();
a_bgAlpha.finish();
a_coord.finish();
_cache = QPixmap();
_scroll.show();
_doneButton.show();
_cancelButton.show();
_filter.show();
_filter.setFocus();
if (_result != "none") {
QTimer::singleShot(0, this, SIGNAL(countryFinished()));
}
res = false;
} else {
a_alpha.update(dt, af_alpha);
a_bgAlpha.update(dt, af_bgAlpha);
a_coord.update(dt, af_coord);
}
update();
return res;
}
void CountrySelect::onParentResize(const QSize &newSize) {
resize(App::wnd()->size());
}
void CountrySelect::onCountryCancel() {
finish("");
}
void CountrySelect::onCountryChoose() {
finish(_list.getSelectedCountry());
}
void CountrySelect::finish(const QString &res) {
_result = res;
prepareAnimation(_result.length() ? -1 : 1);
emit countryChosen(_result);
}
void CountrySelect::onScrollFinished() {
_filter.setFocus();
}
CountrySelect::~CountrySelect() {
if (App::wnd()) {
App::wnd()->noTopWidget(this);
}
}