| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- // 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/time_input.h"
- #include "ui/widgets/fields/time_part_input.h"
- #include "ui/qt_weak_factory.h"
- #include "base/qt/qt_string_view.h"
- #include "base/invoke_queued.h"
- #include <QtCore/QRegularExpression>
- #include <QTime>
- namespace Ui {
- namespace {
- QTime ValidateTime(const QString &value) {
- static const auto RegExp = QRegularExpression(
- "^(\\d{1,2})\\:(\\d\\d)$");
- const auto match = RegExp.match(value);
- if (!match.hasMatch()) {
- return QTime();
- }
- const auto readInt = [](const QString &value) {
- auto view = QStringView(value);
- while (!view.isEmpty() && view.at(0) == '0') {
- view = base::StringViewMid(view, 1);
- }
- return view.toInt();
- };
- return QTime(readInt(match.captured(1)), readInt(match.captured(2)));
- }
- QString GetHour(const QString &value) {
- if (const auto time = ValidateTime(value); time.isValid()) {
- return QString::number(time.hour());
- }
- return QString();
- }
- QString GetMinute(const QString &value) {
- if (const auto time = ValidateTime(value); time.isValid()) {
- return QString("%1").arg(time.minute(), 2, 10, QChar('0'));
- }
- return QString();
- }
- } // namespace
- TimeInput::TimeInput(
- QWidget *parent,
- const QString &value,
- const style::InputField &stField,
- const style::InputField &stDateField,
- const style::FlatLabel &stSeparator,
- const style::margins &stSeparatorPadding)
- : RpWidget(parent)
- , _stField(stField)
- , _stDateField(stDateField)
- , _stSeparator(stSeparator)
- , _stSeparatorPadding(stSeparatorPadding)
- , _hour(
- this,
- _stField,
- rpl::never<QString>(),
- GetHour(value))
- , _separator1(
- this,
- object_ptr<FlatLabel>(
- this,
- QString(":"),
- _stSeparator),
- _stSeparatorPadding)
- , _minute(
- this,
- _stField,
- rpl::never<QString>(),
- GetMinute(value))
- , _value(valueCurrent()) {
- const auto focused = [=](const object_ptr<TimePart> &field) {
- return [this, pointer = MakeWeak(field.data())]{
- _borderAnimationStart = pointer->borderAnimationStart()
- + pointer->x()
- - _hour->x();
- setFocused(true);
- _focuses.fire({});
- };
- };
- const auto blurred = [=] {
- setFocused(false);
- };
- const auto changed = [=] {
- _value = valueCurrent();
- };
- connect(_hour, &MaskedInputField::focused, focused(_hour));
- connect(_minute, &MaskedInputField::focused, focused(_minute));
- connect(_hour, &MaskedInputField::blurred, blurred);
- connect(_minute, &MaskedInputField::blurred, blurred);
- connect(_hour, &MaskedInputField::changed, changed);
- connect(_minute, &MaskedInputField::changed, changed);
- _hour->setMaxValue(23);
- _hour->setWheelStep(1);
- _hour->putNext() | rpl::start_with_next([=](QChar ch) {
- putNext(_minute, ch);
- }, lifetime());
- _minute->setMaxValue(59);
- _minute->setWheelStep(10);
- _minute->erasePrevious() | rpl::start_with_next([=] {
- erasePrevious(_hour);
- }, lifetime());
- _minute->jumpToPrevious() | rpl::start_with_next([=] {
- _hour->setCursorPosition(_hour->getLastText().size());
- _hour->setFocus();
- }, lifetime());
- _separator1->setAttribute(Qt::WA_TransparentForMouseEvents);
- setMouseTracking(true);
- _value.changes(
- ) | rpl::start_with_next([=] {
- setErrorShown(false);
- }, lifetime());
- const auto submitHour = [=] {
- if (hour().has_value()) {
- _minute->setFocus();
- }
- };
- const auto submitMinute = [=] {
- if (minute().has_value()) {
- if (hour().has_value()) {
- _submitRequests.fire({});
- } else {
- _hour->setFocus();
- }
- }
- };
- connect(
- _hour,
- &MaskedInputField::submitted,
- submitHour);
- connect(
- _minute,
- &MaskedInputField::submitted,
- submitMinute);
- }
- void TimeInput::putNext(const object_ptr<TimePart> &field, QChar ch) {
- field->setCursorPosition(0);
- if (ch.unicode()) {
- field->setText(ch + field->getLastText());
- field->setCursorPosition(1);
- }
- field->onTextEdited();
- setFocusQueued(field);
- }
- void TimeInput::erasePrevious(const object_ptr<TimePart> &field) {
- const auto text = field->getLastText();
- if (!text.isEmpty()) {
- field->setCursorPosition(text.size() - 1);
- field->setText(text.mid(0, text.size() - 1));
- }
- setFocusQueued(field);
- }
- void TimeInput::setFocusQueued(const object_ptr<TimePart> &field) {
- // There was a "Stack Overflow" crash in some input method handling.
- //
- // See https://github.com/telegramdesktop/tdesktop/issues/25129
- //
- // The stack is something like:
- //
- // ...
- // QApplicationPrivate::sendMouseEvent
- // ----
- // QWidget::setFocus
- // QWindow::focusObjectChanged
- // QWindowsInputContext::setFocusObject
- // QWindowsInputContext::reset
- // QLineEdit::inputMethodEvent
- // QWidgetLineControl::finishChange
- // QLineEdit::textEdited
- // MaskedInputField::onTextEdited
- // TimePart::correctValue
- // TimeInput::putNext
- // ----
- // QWidget::setFocus
- // QWindow::focusObjectChanged
- // ...
- //
- // So we try to break this loop by focusing the widget async.
- const auto raw = field.data();
- InvokeQueued(raw, [raw] { raw->setFocus(); });
- }
- bool TimeInput::setFocusFast() {
- if (hour().has_value()) {
- _minute->setFocusFast();
- } else {
- _hour->setFocusFast();
- }
- return true;
- }
- std::optional<int> TimeInput::hour() const {
- return _hour->number();
- }
- std::optional<int> TimeInput::minute() const {
- return _minute->number();
- }
- QString TimeInput::valueCurrent() const {
- const auto result = QString("%1:%2"
- ).arg(hour().value_or(0)
- ).arg(minute().value_or(0), 2, 10, QChar('0'));
- return ValidateTime(result).isValid() ? result : QString();
- }
- rpl::producer<QString> TimeInput::value() const {
- return _value.value();
- }
- rpl::producer<> TimeInput::submitRequests() const {
- return _submitRequests.events();
- }
- rpl::producer<> TimeInput::focuses() const {
- return _focuses.events();
- }
- void TimeInput::paintEvent(QPaintEvent *e) {
- auto p = QPainter(this);
- const auto &_st = _stDateField;
- const auto height = _st.heightMin;
- if (_st.border) {
- p.fillRect(0, height - _st.border, width(), _st.border, _st.borderFg);
- }
- auto errorDegree = _a_error.value(_error ? 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);
- }
- }
- }
- template <typename Widget>
- bool TimeInput::insideSeparator(QPoint position, const Widget &widget) const {
- const auto x = position.x();
- const auto y = position.y();
- return (x >= widget->x() && x < widget->x() + widget->width())
- && (y >= _hour->y() && y < _hour->y() + _hour->height());
- }
- void TimeInput::mouseMoveEvent(QMouseEvent *e) {
- const auto cursor = insideSeparator(e->pos(), _separator1)
- ? style::cur_text
- : style::cur_default;
- if (_cursor != cursor) {
- _cursor = cursor;
- setCursor(_cursor);
- }
- }
- void TimeInput::mousePressEvent(QMouseEvent *e) {
- const auto x = e->pos().x();
- const auto focus1 = [&] {
- if (_hour->getLastText().size() > 1) {
- _minute->setFocus();
- } else {
- _hour->setFocus();
- }
- };
- if (insideSeparator(e->pos(), _separator1)) {
- focus1();
- _borderAnimationStart = x - _hour->x();
- }
- }
- int TimeInput::resizeGetHeight(int width) {
- const auto &_st = _stField;
- const auto &font = _st.placeholderFont;
- const auto addToWidth = _stSeparatorPadding.left();
- const auto hourWidth = _st.textMargins.left()
- + _st.placeholderMargins.left()
- + font->width(QString("23"))
- + _st.placeholderMargins.right()
- + _st.textMargins.right()
- + addToWidth;
- const auto minuteWidth = _st.textMargins.left()
- + _st.placeholderMargins.left()
- + font->width(QString("59"))
- + _st.placeholderMargins.right()
- + _st.textMargins.right()
- + addToWidth;
- const auto full = hourWidth
- - addToWidth
- + _separator1->width()
- + minuteWidth
- - addToWidth;
- auto left = (width - full) / 2;
- auto top = 0;
- _hour->setGeometry(left, top, hourWidth, _hour->height());
- left += hourWidth - addToWidth;
- _separator1->resizeToNaturalWidth(width);
- _separator1->move(left, top);
- left += _separator1->width();
- _minute->setGeometry(left, top, minuteWidth, _minute->height());
- return _stDateField.heightMin;
- }
- void TimeInput::showError() {
- setErrorShown(true);
- if (!_focused) {
- setInnerFocus();
- }
- }
- void TimeInput::setInnerFocus() {
- if (hour().has_value()) {
- _minute->setFocus();
- } else {
- _hour->setFocus();
- }
- }
- void TimeInput::setErrorShown(bool error) {
- if (_error != error) {
- _error = error;
- _a_error.start(
- [=] { update(); },
- _error ? 0. : 1.,
- _error ? 1. : 0.,
- _stDateField.duration);
- startBorderAnimation();
- }
- }
- void TimeInput::setFocused(bool focused) {
- if (_focused != focused) {
- _focused = focused;
- _a_focused.start(
- [=] { update(); },
- _focused ? 0. : 1.,
- _focused ? 1. : 0.,
- _stDateField.duration);
- startBorderAnimation();
- }
- }
- void TimeInput::startBorderAnimation() {
- auto borderVisible = (_error || _focused);
- if (_borderVisible != borderVisible) {
- _borderVisible = borderVisible;
- const auto duration = _stDateField.duration;
- if (_borderVisible) {
- if (_a_borderOpacity.animating()) {
- _a_borderOpacity.start([=] { update(); }, 0., 1., duration);
- } else {
- _a_borderShown.start([=] { update(); }, 0., 1., duration);
- }
- } else {
- _a_borderOpacity.start([=] { update(); }, 1., 0., duration);
- }
- }
- }
- } // namespace Ui
|