mirror of https://github.com/procxx/kepka.git
Improve schedule box design.
This commit is contained in:
parent
debeb61540
commit
fb96d2eef8
|
@ -389,7 +389,8 @@ void CalendarBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
|
||||||
auto pressed = _pressed;
|
auto pressed = _pressed;
|
||||||
setPressed(kEmptySelection);
|
setPressed(kEmptySelection);
|
||||||
if (pressed != kEmptySelection && pressed == _selected) {
|
if (pressed != kEmptySelection && pressed == _selected) {
|
||||||
_dateChosenCallback(_context->dateFromIndex(pressed));
|
const auto onstack = _dateChosenCallback;
|
||||||
|
onstack(_context->dateFromIndex(pressed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1963,10 +1963,15 @@ void SendFilesBox::sendSilent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendFilesBox::sendScheduled() {
|
void SendFilesBox::sendScheduled() {
|
||||||
Ui::show(Box(HistoryView::ScheduleBox, crl::guard(this, [=](
|
const auto callback = crl::guard(this, [=](Api::SendOptions options) {
|
||||||
Api::SendOptions options) {
|
|
||||||
send(options);
|
send(options);
|
||||||
})), LayerOption::KeepOther);
|
});
|
||||||
|
Ui::show(
|
||||||
|
Box(
|
||||||
|
HistoryView::ScheduleBox,
|
||||||
|
callback,
|
||||||
|
HistoryView::DefaultScheduleTime()),
|
||||||
|
LayerOption::KeepOther);
|
||||||
}
|
}
|
||||||
|
|
||||||
SendFilesBox::~SendFilesBox() = default;
|
SendFilesBox::~SendFilesBox() = default;
|
||||||
|
|
|
@ -469,10 +469,15 @@ void ShareBox::submitSilent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShareBox::submitScheduled() {
|
void ShareBox::submitScheduled() {
|
||||||
Ui::show(Box(HistoryView::ScheduleBox, crl::guard(this, [=](
|
const auto callback = crl::guard(this, [=](Api::SendOptions options) {
|
||||||
Api::SendOptions options) {
|
|
||||||
submit(options);
|
submit(options);
|
||||||
})), LayerOption::KeepOther);
|
});
|
||||||
|
Ui::show(
|
||||||
|
Box(
|
||||||
|
HistoryView::ScheduleBox,
|
||||||
|
callback,
|
||||||
|
HistoryView::DefaultScheduleTime()),
|
||||||
|
LayerOption::KeepOther);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShareBox::copyLink() {
|
void ShareBox::copyLink() {
|
||||||
|
|
|
@ -592,17 +592,31 @@ largeEmojiOutline: 1px;
|
||||||
largeEmojiPadding: margins(0px, 0px, 0px, 0px);
|
largeEmojiPadding: margins(0px, 0px, 0px, 0px);
|
||||||
largeEmojiSkip: 4px;
|
largeEmojiSkip: 4px;
|
||||||
|
|
||||||
scheduleHeight: 120px;
|
scheduleHeight: 95px;
|
||||||
scheduleDateTop: 44px;
|
scheduleDateTop: 38px;
|
||||||
scheduleDateField: InputField(defaultInputField) {
|
scheduleDateField: InputField(defaultInputField) {
|
||||||
textMargins: margins(2px, 0px, 2px, 0px);
|
textMargins: margins(2px, 0px, 2px, 0px);
|
||||||
placeholderScale: 0.;
|
placeholderScale: 0.;
|
||||||
placeholderFont: normalFont;
|
heightMin: 30px;
|
||||||
heightMin: 24px;
|
textAlign: align(top);
|
||||||
font: normalFont;
|
font: font(14px);
|
||||||
}
|
}
|
||||||
scheduleDateWidth: 96px;
|
scheduleTimeField: InputField(scheduleDateField) {
|
||||||
|
border: 0px;
|
||||||
|
borderActive: 0px;
|
||||||
|
heightMin: 28px;
|
||||||
|
placeholderFont: font(14px);
|
||||||
|
placeholderFgActive: placeholderFgActive;
|
||||||
|
}
|
||||||
|
scheduleDateWidth: 136px;
|
||||||
|
scheduleTimeWidth: 72px;
|
||||||
scheduleAtSkip: 24px;
|
scheduleAtSkip: 24px;
|
||||||
scheduleAtTop: 48px;
|
scheduleAtTop: 42px;
|
||||||
scheduleAtLabel: FlatLabel(defaultFlatLabel) {
|
scheduleAtLabel: FlatLabel(defaultFlatLabel) {
|
||||||
}
|
}
|
||||||
|
scheduleTimeSeparator: FlatLabel(defaultFlatLabel) {
|
||||||
|
style: TextStyle(defaultTextStyle) {
|
||||||
|
font: font(14px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleTimeSeparatorPadding: margins(2px, 0px, 2px, 0px);
|
||||||
|
|
|
@ -2960,9 +2960,18 @@ void HistoryWidget::sendSilent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryWidget::sendScheduled() {
|
void HistoryWidget::sendScheduled() {
|
||||||
Ui::show(Box(HistoryView::ScheduleBox, [=](Api::SendOptions options) {
|
if (!_list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto callback = crl::guard(_list, [=](Api::SendOptions options) {
|
||||||
send(options);
|
send(options);
|
||||||
}));
|
});
|
||||||
|
Ui::show(
|
||||||
|
Box(
|
||||||
|
HistoryView::ScheduleBox,
|
||||||
|
callback,
|
||||||
|
HistoryView::DefaultScheduleTime()),
|
||||||
|
LayerOption::KeepOther);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryWidget::unblockUser() {
|
void HistoryWidget::unblockUser() {
|
||||||
|
|
|
@ -14,81 +14,601 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "ui/widgets/input_fields.h"
|
#include "ui/widgets/input_fields.h"
|
||||||
#include "ui/widgets/labels.h"
|
#include "ui/widgets/labels.h"
|
||||||
#include "ui/wrap/padding_wrap.h"
|
#include "ui/wrap/padding_wrap.h"
|
||||||
|
#include "styles/style_boxes.h"
|
||||||
#include "styles/style_history.h"
|
#include "styles/style_history.h"
|
||||||
|
|
||||||
namespace HistoryView {
|
namespace HistoryView {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kMinimalSchedule = TimeId(30);
|
||||||
|
|
||||||
|
tr::phrase<> MonthDay(int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 1: return tr::lng_month_day1;
|
||||||
|
case 2: return tr::lng_month_day2;
|
||||||
|
case 3: return tr::lng_month_day3;
|
||||||
|
case 4: return tr::lng_month_day4;
|
||||||
|
case 5: return tr::lng_month_day5;
|
||||||
|
case 6: return tr::lng_month_day6;
|
||||||
|
case 7: return tr::lng_month_day7;
|
||||||
|
case 8: return tr::lng_month_day8;
|
||||||
|
case 9: return tr::lng_month_day9;
|
||||||
|
case 10: return tr::lng_month_day10;
|
||||||
|
case 11: return tr::lng_month_day11;
|
||||||
|
case 12: return tr::lng_month_day12;
|
||||||
|
}
|
||||||
|
Unexpected("Index in MonthDay.");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DayString(const QDate &date) {
|
||||||
|
return tr::lng_month_day(
|
||||||
|
tr::now,
|
||||||
|
lt_month,
|
||||||
|
MonthDay(date.month())(tr::now),
|
||||||
|
lt_day,
|
||||||
|
QString::number(date.day()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TimeString(TimeId time) {
|
||||||
|
const auto parsed = base::unixtime::parse(time).time();
|
||||||
|
return QString("%1:%2"
|
||||||
|
).arg(parsed.hour()
|
||||||
|
).arg(parsed.minute(), 2, 10, QLatin1Char('0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimePart final : public Ui::MaskedInputField {
|
||||||
|
public:
|
||||||
|
using MaskedInputField::MaskedInputField;
|
||||||
|
|
||||||
|
void setMaxValue(int value);
|
||||||
|
|
||||||
|
rpl::producer<> erasePrevious() const;
|
||||||
|
rpl::producer<QChar> putNext() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void keyPressEvent(QKeyEvent *e) override;
|
||||||
|
|
||||||
|
void correctValue(
|
||||||
|
const QString &was,
|
||||||
|
int wasCursor,
|
||||||
|
QString &now,
|
||||||
|
int &nowCursor) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int _maxValue = 0;
|
||||||
|
int _maxDigits = 0;
|
||||||
|
rpl::event_stream<> _erasePrevious;
|
||||||
|
rpl::event_stream<QChar> _putNext;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class TimeInput final : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
TimeInput(QWidget *parent, const QString &value);
|
||||||
|
|
||||||
|
bool setFocusFast();
|
||||||
|
rpl::producer<QString> value() const;
|
||||||
|
QString valueCurrent() const;
|
||||||
|
void showError();
|
||||||
|
|
||||||
|
int resizeGetHeight(int width) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
void mousePressEvent(QMouseEvent *e) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent *e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setInnerFocus();
|
||||||
|
void putNext(const object_ptr<TimePart> &field, QChar ch);
|
||||||
|
void erasePrevious(const object_ptr<TimePart> &field);
|
||||||
|
void finishInnerAnimating();
|
||||||
|
void setErrorShown(bool error);
|
||||||
|
void setFocused(bool focused);
|
||||||
|
void startBorderAnimation();
|
||||||
|
template <typename Widget>
|
||||||
|
bool insideSeparator(QPoint position, const Widget &widget) const;
|
||||||
|
|
||||||
|
int hour() const;
|
||||||
|
int minute() const;
|
||||||
|
int number(const object_ptr<TimePart> &field) const;
|
||||||
|
|
||||||
|
object_ptr<TimePart> _hour;
|
||||||
|
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _separator1;
|
||||||
|
object_ptr<TimePart> _minute;
|
||||||
|
rpl::variable<QString> _value;
|
||||||
|
|
||||||
|
style::cursor _cursor = style::cur_default;
|
||||||
|
Ui::Animations::Simple _a_borderShown;
|
||||||
|
int _borderAnimationStart = 0;
|
||||||
|
Ui::Animations::Simple _a_borderOpacity;
|
||||||
|
bool _borderVisible = false;
|
||||||
|
|
||||||
|
Ui::Animations::Simple _a_error;
|
||||||
|
bool _error = false;
|
||||||
|
Ui::Animations::Simple _a_focused;
|
||||||
|
bool _focused = false;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
QTime ValidateTime(const QString &value) {
|
||||||
|
const auto match = QRegularExpression(
|
||||||
|
"^(\\d\\d)\\:(\\d\\d)$").match(value);
|
||||||
|
if (!match.hasMatch()) {
|
||||||
|
return QTime();
|
||||||
|
}
|
||||||
|
const auto readInt = [](const QString &value) {
|
||||||
|
auto ref = value.midRef(0);
|
||||||
|
while (!ref.isEmpty() && ref.at(0) == '0') {
|
||||||
|
ref = ref.mid(1);
|
||||||
|
}
|
||||||
|
return ref.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimePart::setMaxValue(int value) {
|
||||||
|
_maxValue = value;
|
||||||
|
_maxDigits = 0;
|
||||||
|
while (value > 0) {
|
||||||
|
++_maxDigits;
|
||||||
|
value /= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> TimePart::erasePrevious() const {
|
||||||
|
return _erasePrevious.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<QChar> TimePart::putNext() const {
|
||||||
|
return _putNext.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimePart::keyPressEvent(QKeyEvent *e) {
|
||||||
|
const auto isBackspace = (e->key() == Qt::Key_Backspace);
|
||||||
|
const auto isBeginning = (cursorPosition() == 0);
|
||||||
|
if (isBackspace && isBeginning && !hasSelectedText()) {
|
||||||
|
_erasePrevious.fire({});
|
||||||
|
} else {
|
||||||
|
MaskedInputField::keyPressEvent(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimePart::correctValue(
|
||||||
|
const QString &was,
|
||||||
|
int wasCursor,
|
||||||
|
QString &now,
|
||||||
|
int &nowCursor) {
|
||||||
|
auto newText = QString();
|
||||||
|
auto newCursor = -1;
|
||||||
|
const auto oldCursor = nowCursor;
|
||||||
|
const auto oldLength = now.size();
|
||||||
|
auto accumulated = 0;
|
||||||
|
auto limit = 0;
|
||||||
|
for (; limit != oldLength; ++limit) {
|
||||||
|
if (now[limit].isDigit()) {
|
||||||
|
accumulated *= 10;
|
||||||
|
accumulated += (now[limit].unicode() - '0');
|
||||||
|
if (accumulated > _maxValue || limit == _maxDigits) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto i = 0; i != limit;) {
|
||||||
|
if (now[i].isDigit()) {
|
||||||
|
newText += now[i];
|
||||||
|
}
|
||||||
|
if (++i == oldCursor) {
|
||||||
|
newCursor = newText.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newCursor < 0) {
|
||||||
|
newCursor = newText.size();
|
||||||
|
}
|
||||||
|
if (newText != now) {
|
||||||
|
now = newText;
|
||||||
|
setText(now);
|
||||||
|
startPlaceholderAnimation();
|
||||||
|
}
|
||||||
|
if (newCursor != nowCursor) {
|
||||||
|
nowCursor = newCursor;
|
||||||
|
setCursorPosition(nowCursor);
|
||||||
|
}
|
||||||
|
if (accumulated > _maxValue
|
||||||
|
|| (limit == _maxDigits && oldLength > _maxDigits)) {
|
||||||
|
if (oldCursor > limit) {
|
||||||
|
_putNext.fire('0' + (accumulated % 10));
|
||||||
|
} else {
|
||||||
|
_putNext.fire(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeInput::TimeInput(QWidget *parent, const QString &value)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _hour(
|
||||||
|
this,
|
||||||
|
st::scheduleTimeField,
|
||||||
|
rpl::never<QString>(),
|
||||||
|
GetHour(value))
|
||||||
|
, _separator1(
|
||||||
|
this,
|
||||||
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
this,
|
||||||
|
QString(":"),
|
||||||
|
st::scheduleTimeSeparator),
|
||||||
|
st::scheduleTimeSeparatorPadding)
|
||||||
|
, _minute(
|
||||||
|
this,
|
||||||
|
st::scheduleTimeField,
|
||||||
|
rpl::never<QString>(),
|
||||||
|
GetMinute(value))
|
||||||
|
, _value(valueCurrent()) {
|
||||||
|
const auto focused = [=](const object_ptr<TimePart> &field) {
|
||||||
|
return [this, pointer = make_weak(field.data())]{
|
||||||
|
_borderAnimationStart = pointer->borderAnimationStart()
|
||||||
|
+ pointer->x()
|
||||||
|
- _hour->x();
|
||||||
|
setFocused(true);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const auto blurred = [=] {
|
||||||
|
setFocused(false);
|
||||||
|
};
|
||||||
|
const auto changed = [=] {
|
||||||
|
_value = valueCurrent();
|
||||||
|
};
|
||||||
|
connect(_hour, &Ui::MaskedInputField::focused, focused(_hour));
|
||||||
|
connect(_minute, &Ui::MaskedInputField::focused, focused(_minute));
|
||||||
|
connect(_hour, &Ui::MaskedInputField::blurred, blurred);
|
||||||
|
connect(_minute, &Ui::MaskedInputField::blurred, blurred);
|
||||||
|
connect(_hour, &Ui::MaskedInputField::changed, changed);
|
||||||
|
connect(_minute, &Ui::MaskedInputField::changed, changed);
|
||||||
|
_hour->setMaxValue(23);
|
||||||
|
_hour->putNext() | rpl::start_with_next([=](QChar ch) {
|
||||||
|
putNext(_minute, ch);
|
||||||
|
}, lifetime());
|
||||||
|
_minute->setMaxValue(59);
|
||||||
|
_minute->erasePrevious() | rpl::start_with_next([=] {
|
||||||
|
erasePrevious(_hour);
|
||||||
|
}, lifetime());
|
||||||
|
_separator1->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
setMouseTracking(true);
|
||||||
|
|
||||||
|
_value.changes(
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
setErrorShown(false);
|
||||||
|
}, lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
|
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->setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
field->setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TimeInput::setFocusFast() {
|
||||||
|
if (hour()) {
|
||||||
|
_minute->setFocusFast();
|
||||||
|
} else {
|
||||||
|
_hour->setFocusFast();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int TimeInput::number(const object_ptr<TimePart> &field) const {
|
||||||
|
const auto text = field->getLastText();
|
||||||
|
auto ref = text.midRef(0);
|
||||||
|
while (!ref.isEmpty() && ref.at(0) == '0') {
|
||||||
|
ref = ref.mid(1);
|
||||||
|
}
|
||||||
|
return ref.toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
int TimeInput::hour() const {
|
||||||
|
return number(_hour);
|
||||||
|
}
|
||||||
|
|
||||||
|
int TimeInput::minute() const {
|
||||||
|
return number(_minute);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TimeInput::valueCurrent() const {
|
||||||
|
const auto result = QString("%1:%2"
|
||||||
|
).arg(hour()
|
||||||
|
).arg(minute(), 2, 10, QChar('0'));
|
||||||
|
return ValidateTime(result).isValid() ? result : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<QString> TimeInput::value() const {
|
||||||
|
return _value.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeInput::paintEvent(QPaintEvent *e) {
|
||||||
|
Painter p(this);
|
||||||
|
|
||||||
|
const auto &_st = st::scheduleDateField;
|
||||||
|
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 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 = snap(_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 = st::scheduleTimeField;
|
||||||
|
const auto &font = _st.placeholderFont;
|
||||||
|
const auto addToWidth = st::scheduleTimeSeparatorPadding.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 st::scheduleDateField.heightMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeInput::showError() {
|
||||||
|
setErrorShown(true);
|
||||||
|
if (!_focused) {
|
||||||
|
setInnerFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeInput::setInnerFocus() {
|
||||||
|
if (hour()) {
|
||||||
|
_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.,
|
||||||
|
st::scheduleDateField.duration);
|
||||||
|
startBorderAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeInput::setFocused(bool focused) {
|
||||||
|
if (_focused != focused) {
|
||||||
|
_focused = focused;
|
||||||
|
_a_focused.start(
|
||||||
|
[=] { update(); },
|
||||||
|
_focused ? 0. : 1.,
|
||||||
|
_focused ? 1. : 0.,
|
||||||
|
st::scheduleDateField.duration);
|
||||||
|
startBorderAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeInput::finishInnerAnimating() {
|
||||||
|
_hour->finishAnimating();
|
||||||
|
_minute->finishAnimating();
|
||||||
|
_a_borderOpacity.stop();
|
||||||
|
_a_borderShown.stop();
|
||||||
|
_a_error.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeInput::startBorderAnimation() {
|
||||||
|
auto borderVisible = (_error || _focused);
|
||||||
|
if (_borderVisible != borderVisible) {
|
||||||
|
_borderVisible = borderVisible;
|
||||||
|
const auto duration = st::scheduleDateField.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
|
||||||
|
|
||||||
|
TimeId DefaultScheduleTime() {
|
||||||
|
const auto result = base::unixtime::now() + 3600;
|
||||||
|
const auto time = base::unixtime::parse(result).time();
|
||||||
|
return result - (time.minute() % 5) * 60 - time.second();
|
||||||
|
}
|
||||||
|
|
||||||
void ScheduleBox(
|
void ScheduleBox(
|
||||||
not_null<GenericBox*> box,
|
not_null<GenericBox*> box,
|
||||||
Fn<void(Api::SendOptions)> done) {
|
Fn<void(Api::SendOptions)> done,
|
||||||
|
TimeId time) {
|
||||||
box->setTitle(tr::lng_schedule_title());
|
box->setTitle(tr::lng_schedule_title());
|
||||||
|
box->setWidth(st::boxWideWidth);
|
||||||
|
|
||||||
|
const auto date = Ui::CreateChild<rpl::variable<QDate>>(
|
||||||
|
box.get(),
|
||||||
|
base::unixtime::parse(time).date());
|
||||||
const auto content = box->addRow(
|
const auto content = box->addRow(
|
||||||
object_ptr<Ui::FixedHeightWidget>(box, st::scheduleHeight));
|
object_ptr<Ui::FixedHeightWidget>(box, st::scheduleHeight));
|
||||||
const auto day = Ui::CreateChild<Ui::InputField>(
|
const auto dayInput = Ui::CreateChild<Ui::InputField>(
|
||||||
content,
|
content,
|
||||||
st::scheduleDateField);
|
st::scheduleDateField);
|
||||||
const auto time = Ui::CreateChild<Ui::InputField>(
|
const auto timeInput = Ui::CreateChild<TimeInput>(
|
||||||
content,
|
content,
|
||||||
st::scheduleDateField);
|
TimeString(time));
|
||||||
const auto at = Ui::CreateChild<Ui::FlatLabel>(
|
const auto at = Ui::CreateChild<Ui::FlatLabel>(
|
||||||
content,
|
content,
|
||||||
tr::lng_schedule_at(),
|
tr::lng_schedule_at(),
|
||||||
st::scheduleAtLabel);
|
st::scheduleAtLabel);
|
||||||
|
|
||||||
|
date->value(
|
||||||
|
) | rpl::start_with_next([=](QDate date) {
|
||||||
|
dayInput->setText(DayString(date));
|
||||||
|
timeInput->setFocusFast();
|
||||||
|
}, dayInput->lifetime());
|
||||||
|
|
||||||
content->widthValue(
|
content->widthValue(
|
||||||
) | rpl::start_with_next([=](int width) {
|
) | rpl::start_with_next([=](int width) {
|
||||||
const auto paddings = width
|
const auto paddings = width
|
||||||
- at->width()
|
- at->width()
|
||||||
- 2 * st::scheduleAtSkip
|
- 2 * st::scheduleAtSkip
|
||||||
- 2 * st::scheduleDateWidth;
|
- st::scheduleDateWidth
|
||||||
|
- st::scheduleTimeWidth;
|
||||||
const auto left = paddings / 2;
|
const auto left = paddings / 2;
|
||||||
day->resizeToWidth(st::scheduleDateWidth);
|
dayInput->resizeToWidth(st::scheduleDateWidth);
|
||||||
day->moveToLeft(left, st::scheduleDateTop, width);
|
dayInput->moveToLeft(left, st::scheduleDateTop, width);
|
||||||
at->moveToLeft(
|
at->moveToLeft(
|
||||||
left + st::scheduleDateWidth + st::scheduleAtSkip,
|
left + st::scheduleDateWidth + st::scheduleAtSkip,
|
||||||
st::scheduleAtTop,
|
st::scheduleAtTop,
|
||||||
width);
|
width);
|
||||||
time->resizeToWidth(st::scheduleDateWidth);
|
timeInput->resizeToWidth(st::scheduleTimeWidth);
|
||||||
time->moveToLeft(
|
timeInput->moveToLeft(
|
||||||
width - left - st::scheduleDateWidth,
|
width - left - st::scheduleTimeWidth,
|
||||||
st::scheduleDateTop,
|
st::scheduleDateTop,
|
||||||
width);
|
width);
|
||||||
}, content->lifetime());
|
}, content->lifetime());
|
||||||
|
|
||||||
|
QObject::connect(dayInput, &Ui::InputField::focused, [=] {
|
||||||
|
const auto calendar = std::make_shared<QPointer<CalendarBox>>();
|
||||||
|
const auto chosen = [=](QDate chosen) {
|
||||||
|
*date = chosen;
|
||||||
|
(*calendar)->closeBox();
|
||||||
|
};
|
||||||
|
const auto finalize = [=](not_null<CalendarBox*> box) {
|
||||||
|
const auto now = QDate::currentDate();
|
||||||
|
box->setMinDate(now);
|
||||||
|
box->setMaxDate(now.addYears(1).addDays(-1));
|
||||||
|
};
|
||||||
|
*calendar = box->getDelegate()->show(Box<CalendarBox>(
|
||||||
|
date->current(),
|
||||||
|
date->current(),
|
||||||
|
crl::guard(box, chosen),
|
||||||
|
finalize));
|
||||||
|
(*calendar)->boxClosing(
|
||||||
|
) | rpl::start_with_next(crl::guard(timeInput, [=] {
|
||||||
|
timeInput->setFocusFast();
|
||||||
|
}), (*calendar)->lifetime());
|
||||||
|
});
|
||||||
|
|
||||||
const auto save = [=] {
|
const auto save = [=] {
|
||||||
auto result = Api::SendOptions();
|
auto result = Api::SendOptions();
|
||||||
|
|
||||||
const auto dayValue = day->getLastText().trimmed();
|
const auto timeValue = timeInput->valueCurrent().split(':');
|
||||||
const auto dayMatch = QRegularExpression("(\\d\\d)\\.(\\d\\d)\\.(\\d\\d\\d\\d)").match(dayValue);
|
if (timeValue.size() != 2) {
|
||||||
const auto timeValue = time->getLastText().trimmed();
|
timeInput->showError();
|
||||||
const auto timeMatch = QRegularExpression("(\\d\\d):(\\d\\d)").match(timeValue);
|
|
||||||
|
|
||||||
if (!dayMatch.hasMatch()) {
|
|
||||||
day->showError();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const auto time = QTime(timeValue[0].toInt(), timeValue[1].toInt());
|
||||||
if (!timeMatch.hasMatch()) {
|
if (!time.isValid()) {
|
||||||
time->showError();
|
timeInput->showError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.scheduled = QDateTime(date->current(), time).toTime_t();
|
||||||
|
if (result.scheduled <= base::unixtime::now() + kMinimalSchedule) {
|
||||||
|
timeInput->showError();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto date = QDateTime(
|
|
||||||
QDate(
|
|
||||||
dayMatch.captured(3).toInt(),
|
|
||||||
dayMatch.captured(2).toInt(),
|
|
||||||
dayMatch.captured(1).toInt()),
|
|
||||||
QTime(
|
|
||||||
timeMatch.captured(1).toInt(),
|
|
||||||
timeMatch.captured(2).toInt()));
|
|
||||||
result.scheduled = date.toTime_t();
|
|
||||||
|
|
||||||
auto copy = done;
|
auto copy = done;
|
||||||
box->closeBox();
|
box->closeBox();
|
||||||
copy(result);
|
copy(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
box->setFocusCallback([=] { timeInput->setFocusFast(); });
|
||||||
box->addButton(tr::lng_settings_save(), save);
|
box->addButton(tr::lng_settings_save(), save);
|
||||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ struct SendOptions;
|
||||||
|
|
||||||
namespace HistoryView {
|
namespace HistoryView {
|
||||||
|
|
||||||
void ScheduleBox(not_null<GenericBox*> box, Fn<void(Api::SendOptions)> done);
|
[[nodiscard]] TimeId DefaultScheduleTime();
|
||||||
|
void ScheduleBox(
|
||||||
|
not_null<GenericBox*> box,
|
||||||
|
Fn<void(Api::SendOptions)> done,
|
||||||
|
TimeId time);
|
||||||
|
|
||||||
} // namespace HistoryView
|
} // namespace HistoryView
|
||||||
|
|
Loading…
Reference in New Issue