Move CalendarBox and ChooseDateTimeBox to td_ui.

This commit is contained in:
John Preston 2021-01-19 11:14:50 +04:00
parent 1cce383d15
commit 97fb310f54
19 changed files with 1161 additions and 1008 deletions

View File

@ -203,8 +203,6 @@ PRIVATE
boxes/background_box.h
boxes/background_preview_box.cpp
boxes/background_preview_box.h
boxes/calendar_box.cpp
boxes/calendar_box.h
boxes/change_phone_box.cpp
boxes/change_phone_box.h
boxes/confirm_box.cpp

View File

@ -431,9 +431,12 @@ auto InviteLinks::parse(
void InviteLinks::requestMoreLinks(
not_null<PeerData*> peer,
const QString &last,
bool revoked,
Fn<void(Links)> done) {
using Flag = MTPmessages_GetExportedChatInvites::Flag;
_api->request(MTPmessages_GetExportedChatInvites(
MTP_flags(MTPmessages_GetExportedChatInvites::Flag::f_offset_link),
MTP_flags(Flag::f_offset_link
| (revoked ? Flag::f_revoked : Flag(0))),
peer->input,
MTPInputUser(), // admin_id,
MTP_string(last),

View File

@ -81,6 +81,7 @@ public:
void requestMoreLinks(
not_null<PeerData*> peer,
const QString &last,
bool revoked,
Fn<void(Links)> done);
private:

View File

@ -905,3 +905,32 @@ pollResultsShowMore: SettingsButton(defaultSettingsButton) {
ripple: defaultRippleAnimation;
}
scheduleHeight: 95px;
scheduleDateTop: 38px;
scheduleDateField: InputField(defaultInputField) {
textMargins: margins(2px, 0px, 2px, 0px);
placeholderScale: 0.;
heightMin: 30px;
textAlign: align(top);
font: font(14px);
}
scheduleTimeField: InputField(scheduleDateField) {
border: 0px;
borderActive: 0px;
heightMin: 28px;
placeholderFont: font(14px);
placeholderFgActive: placeholderFgActive;
}
scheduleDateWidth: 136px;
scheduleTimeWidth: 72px;
scheduleAtSkip: 24px;
scheduleAtTop: 42px;
scheduleAtLabel: FlatLabel(defaultFlatLabel) {
}
scheduleTimeSeparator: FlatLabel(defaultFlatLabel) {
style: TextStyle(defaultTextStyle) {
font: font(14px);
}
}
scheduleTimeSeparatorPadding: margins(2px, 0px, 2px, 0px);

View File

@ -20,10 +20,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/toast/toast.h"
#include "ui/text/text_utilities.h"
#include "ui/text/text_options.h"
#include "ui/boxes/calendar_box.h"
#include "ui/special_buttons.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "settings/settings_privacy_security.h"
#include "boxes/calendar_box.h"
#include "boxes/confirm_box.h"
#include "boxes/passcode_box.h"
#include "boxes/peers/edit_peer_permissions_box.h"
@ -691,7 +691,7 @@ void EditRestrictedBox::showRestrictUntil() {
: base::unixtime::parse(getRealUntilValue()).date();
auto month = highlighted;
_restrictUntilBox = Ui::show(
Box<CalendarBox>(
Box<Ui::CalendarBox>(
month,
highlighted,
[this](const QDate &date) {

View File

@ -18,6 +18,7 @@ class LinkButton;
class Checkbox;
class Radiobutton;
class RadiobuttonGroup;
class CalendarBox;
template <typename Widget>
class SlideWrap;
} // namespace Ui
@ -26,7 +27,6 @@ namespace Core {
struct CloudPasswordResult;
} // namespace Core
class CalendarBox;
class PasscodeBox;
class EditParticipantBox : public Ui::BoxContent {
@ -162,7 +162,7 @@ private:
std::shared_ptr<Ui::RadiobuttonGroup> _untilGroup;
std::vector<base::unique_qptr<Ui::Radiobutton>> _untilVariants;
QPointer<CalendarBox> _restrictUntilBox;
QPointer<Ui::CalendarBox> _restrictUntilBox;
static constexpr auto kUntilOneDay = -1;
static constexpr auto kUntilOneWeek = -2;

View File

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_histories.h"
#include "main/main_session.h"
#include "api/api_invite_links.h"
#include "ui/boxes/edit_invite_link.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/padding_wrap.h"
@ -27,7 +28,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "history/view/history_view_group_call_tracker.h" // GenerateUs...
#include "history/view/history_view_schedule_box.h" // ChooseDateTimeBox.
#include "history/history_message.h" // GetErrorTextForSending.
#include "history/history.h"
#include "lang/lang_keys.h"
@ -53,9 +53,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
constexpr auto kPreloadPages = 2;
constexpr auto kMaxLimit = std::numeric_limits<int>::max();
constexpr auto kHour = 3600;
constexpr auto kDay = 86400;
enum class Color {
Permanent,
@ -130,27 +127,6 @@ private:
};
[[nodiscard]] QString FormatExpireDate(TimeId date) {
if (date > 0) {
return langDateTime(base::unixtime::parse(date));
} else if (-date < kDay) {
return tr::lng_group_call_duration_hours(
tr::now,
lt_count,
(-date / kHour));
} else if (-date < 7 * kDay) {
return tr::lng_group_call_duration_days(
tr::now,
lt_count,
(-date / kDay));
} else {
return tr::lng_local_storage_limit_weeks(
tr::now,
lt_count,
(-date / (7 * kDay)));
}
}
[[nodiscard]] uint64 ComputeRowId(const QString &link) {
return XXH64(link.data(), link.size() * sizeof(ushort), 0);
}
@ -282,6 +258,45 @@ void ShareLinkBox(not_null<PeerData*> peer, const QString &link) {
std::move(filterCallback)));
}
void EditLink(not_null<PeerData*> peer, const InviteLinkData &data) {
const auto creating = data.link.isEmpty();
const auto box = std::make_shared<QPointer<Ui::GenericBox>>();
using Fields = Ui::InviteLinkFields;
const auto done = [=](Fields result) {
const auto finish = [=](Api::InviteLink finished) {
if (*box) {
(*box)->closeBox();
}
};
if (creating) {
peer->session().api().inviteLinks().create(
peer,
finish,
result.expireDate,
result.usageLimit);
} else {
peer->session().api().inviteLinks().edit(
peer,
result.link,
result.expireDate,
result.usageLimit,
finish);
}
};
*box = Ui::show(
(creating
? Box(Ui::CreateInviteLinkBox, done)
: Box(
Ui::EditInviteLinkBox,
Fields{
.link = data.link,
.expireDate = data.expireDate,
.usageLimit = data.usageLimit
},
done)),
Ui::LayerOption::KeepOther);
}
not_null<Ui::SettingsButton*> AddCreateLinkButton(
not_null<Ui::VerticalLayout*> container) {
const auto result = container->add(
@ -314,241 +329,6 @@ not_null<Ui::SettingsButton*> AddCreateLinkButton(
return result;
}
void EditLinkBox(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer,
const InviteLinkData &data) {
const auto link = data.link;
box->setTitle(link.isEmpty()
? tr::lng_group_invite_new_title()
: tr::lng_group_invite_edit_title());
using namespace Settings;
const auto container = box->verticalLayout();
AddSubsectionTitle(container, tr::lng_group_invite_expire_title());
const auto expiresWrap = container->add(object_ptr<Ui::VerticalLayout>(
container));
AddSkip(container);
AddDividerText(container, tr::lng_group_invite_expire_about());
AddSkip(container);
AddSubsectionTitle(container, tr::lng_group_invite_usage_title());
const auto usagesWrap = container->add(object_ptr<Ui::VerticalLayout>(
container));
AddSkip(container);
AddDividerText(container, tr::lng_group_invite_usage_about());
static const auto addButton = [](
not_null<Ui::VerticalLayout*> container,
const std::shared_ptr<Ui::RadiobuttonGroup> &group,
int value,
const QString &text) {
return container->add(
object_ptr<Ui::Radiobutton>(
container,
group,
value,
text),
st::inviteLinkLimitMargin);
};
const auto now = base::unixtime::now();
const auto expire = data.expireDate ? data.expireDate : kMaxLimit;
const auto expireGroup = std::make_shared<Ui::RadiobuttonGroup>(expire);
const auto usage = data.usageLimit ? data.usageLimit : kMaxLimit;
const auto usageGroup = std::make_shared<Ui::RadiobuttonGroup>(usage);
using Buttons = base::flat_map<int, base::unique_qptr<Ui::Radiobutton>>;
struct State {
Buttons expireButtons;
Buttons usageButtons;
int expireValue = 0;
int usageValue = 0;
};
const auto state = container->lifetime().make_state<State>(State{
.expireValue = expire,
.usageValue = usage
});
const auto regenerate = [=] {
expireGroup->setValue(state->expireValue);
usageGroup->setValue(state->usageValue);
auto expires = std::vector{ kMaxLimit, -kHour, -kDay, -kDay * 7, 0 };
auto usages = std::vector{ kMaxLimit, 1, 10, 100, 0 };
auto defaults = State();
for (auto i = begin(expires); i != end(expires); ++i) {
if (*i == state->expireValue) {
break;
} else if (*i == kMaxLimit) {
continue;
} else if (!*i || (now - *i >= state->expireValue)) {
expires.insert(i, state->expireValue);
break;
}
}
for (auto i = begin(usages); i != end(usages); ++i) {
if (*i == state->usageValue) {
break;
} else if (*i == kMaxLimit) {
continue;
} else if (!*i || *i > state->usageValue) {
usages.insert(i, state->usageValue);
break;
}
}
state->expireButtons.clear();
state->usageButtons.clear();
for (const auto limit : expires) {
const auto text = (limit == kMaxLimit)
? tr::lng_group_invite_expire_never(tr::now)
: !limit
? tr::lng_group_invite_expire_custom(tr::now)
: FormatExpireDate(limit);
state->expireButtons.emplace(
limit,
addButton(expiresWrap, expireGroup, limit, text));
}
for (const auto limit : usages) {
const auto text = (limit == kMaxLimit)
? tr::lng_group_invite_usage_any(tr::now)
: !limit
? tr::lng_group_invite_usage_custom(tr::now)
: QString("%L1").arg(limit);
state->usageButtons.emplace(
limit,
addButton(usagesWrap, usageGroup, limit, text));
}
};
const auto guard = Ui::MakeWeak(box);
expireGroup->setChangedCallback([=](int value) {
if (value) {
state->expireValue = value;
return;
}
expireGroup->setValue(state->expireValue);
box->getDelegate()->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto save = [=](TimeId result) {
if (!result) {
return;
}
if (guard) {
state->expireValue = result;
regenerate();
}
box->closeBox();
};
const auto now = base::unixtime::now();
const auto time = (state->expireValue == kMaxLimit)
? (now + kDay)
: (state->expireValue > now)
? state->expireValue
: (state->expireValue < 0)
? (now - state->expireValue)
: (now + kDay);
HistoryView::ChooseDateTimeBox(
box,
tr::lng_group_invite_expire_after(),
tr::lng_settings_save(),
save,
time);
}));
});
usageGroup->setChangedCallback([=](int value) {
if (value) {
state->usageValue = value;
return;
}
usageGroup->setValue(state->usageValue);
box->getDelegate()->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto height = st::boxPadding.bottom()
+ st::defaultInputField.heightMin
+ st::boxPadding.bottom();
box->setTitle(tr::lng_group_invite_expire_after());
const auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
height));
const auto input = Ui::CreateChild<Ui::NumberInput>(
wrap,
st::defaultInputField,
tr::lng_group_invite_custom_limit(),
(state->usageValue == kMaxLimit
? QString()
: QString::number(state->usageValue)),
200'000);
wrap->widthValue(
) | rpl::start_with_next([=](int width) {
input->resize(width, input->height());
input->moveToLeft(0, st::boxPadding.bottom());
}, input->lifetime());
box->setFocusCallback([=] {
input->setFocusFast();
});
const auto save = [=] {
const auto value = input->getLastText().toInt();
if (value <= 0) {
input->showError();
return;
}
if (guard) {
state->usageValue = value;
regenerate();
}
box->closeBox();
};
QObject::connect(input, &Ui::NumberInput::submitted, save);
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
});
regenerate();
const auto &saveLabel = link.isEmpty()
? tr::lng_formatting_link_create
: tr::lng_settings_save;
box->addButton(saveLabel(), [=] {
const auto expireDate = (state->expireValue == kMaxLimit)
? 0
: (state->expireValue < 0)
? (base::unixtime::now() - state->expireValue)
: state->expireValue;
const auto usageLimit = (state->usageValue == kMaxLimit)
? 0
: state->usageValue;
const auto done = [=](const Api::InviteLink &result) {
box->closeBox();
};
if (link.isEmpty()) {
peer->session().api().inviteLinks().create(
peer,
done,
expireDate,
usageLimit);
} else {
peer->session().api().inviteLinks().edit(
peer,
link,
expireDate,
usageLimit,
done);
}
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void CreateLinkBox(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer) {
EditLinkBox(
box,
peer,
InviteLinkData{ .admin = peer->session().user() });
}
Row::Row(
not_null<RowDelegate*> delegate,
const InviteLinkData &data,
@ -724,9 +504,7 @@ base::unique_qptr<Ui::PopupMenu> Controller::createRowContextMenu(
ShareLinkBox(_peer, link);
});
result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {
Ui::show(
Box(EditLinkBox, _peer, data),
Ui::LayerOption::KeepOther);
EditLink(_peer, data);
});
result->addAction(tr::lng_group_invite_context_revoke(tr::now), [=] {
const auto box = std::make_shared<QPointer<ConfirmBox>>();
@ -1024,7 +802,7 @@ void ManageInviteLinksBox(
const auto add = AddCreateLinkButton(container);
add->setClickedCallback([=] {
box->getDelegate()->show(Box(CreateLinkBox, peer));
EditLink(peer, InviteLinkData{ .admin = peer->session().user() });
});
const auto list = AddLinksList(container, peer, false);

View File

@ -21,9 +21,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/wrap/fade_wrap.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/boxes/calendar_box.h"
#include "platform/platform_specific.h"
#include "core/file_utilities.h"
#include "boxes/calendar_box.h"
#include "base/unixtime.h"
#include "base/qt_adapters.h"
#include "main/main_session.h"
@ -463,8 +463,8 @@ void SettingsWidget::editDateLimit(
? base::unixtime::parse(min).date()
: QDate::currentDate();
const auto month = highlighted;
const auto shared = std::make_shared<QPointer<CalendarBox>>();
const auto finalize = [=](not_null<CalendarBox*> box) {
const auto shared = std::make_shared<QPointer<Ui::CalendarBox>>();
const auto finalize = [=](not_null<Ui::CalendarBox*> box) {
box->setMaxDate(max
? base::unixtime::parse(max).date()
: QDate::currentDate());
@ -484,7 +484,7 @@ void SettingsWidget::editDateLimit(
weak->closeBox();
}
});
auto box = Box<CalendarBox>(
auto box = Box<Ui::CalendarBox>(
month,
highlighted,
callback,

View File

@ -14,12 +14,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "base/event_filter.h"
#include "base/unixtime.h"
#include "boxes/calendar_box.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/boxes/choose_date_time.h"
#include "chat_helpers/send_context_menu.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
@ -30,550 +30,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace HistoryView {
namespace {
constexpr auto kMinimalSchedule = TimeId(10);
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'));
}
int ProcessWheelEvent(not_null<QWheelEvent*> e) {
// Only a mouse wheel is accepted.
constexpr auto step = static_cast<int>(QWheelEvent::DefaultDeltasPerStep);
const auto delta = e->angleDelta().y();
const auto absDelta = std::abs(delta);
if (absDelta != step) {
return 0;
}
return (delta / absDelta);
}
class TimePart final : public Ui::MaskedInputField {
public:
using MaskedInputField::MaskedInputField;
void setMaxValue(int value);
void setWheelStep(int value);
rpl::producer<> erasePrevious() const;
rpl::producer<QChar> putNext() const;
protected:
void keyPressEvent(QKeyEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
void correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) override;
private:
int _maxValue = 0;
int _maxDigits = 0;
int _wheelStep = 0;
rpl::event_stream<> _erasePrevious;
rpl::event_stream<QChar> _putNext;
};
int Number(not_null<TimePart*> field) {
const auto text = field->getLastText();
auto ref = text.midRef(0);
while (!ref.isEmpty() && ref.at(0) == '0') {
ref = ref.mid(1);
}
return ref.toInt();
}
class TimeInput final : public Ui::RpWidget {
public:
TimeInput(QWidget *parent, const QString &value);
bool setFocusFast();
rpl::producer<QString> value() const;
rpl::producer<> submitRequests() 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;
object_ptr<TimePart> _hour;
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>> _separator1;
object_ptr<TimePart> _minute;
rpl::variable<QString> _value;
rpl::event_stream<> _submitRequests;
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{1,2})\\:(\\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;
}
}
void TimePart::setWheelStep(int value) {
_wheelStep = value;
}
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::wheelEvent(QWheelEvent *e) {
const auto direction = ProcessWheelEvent(e);
auto time = Number(this) + (direction * _wheelStep);
const auto max = _maxValue + 1;
if (time < 0) {
time += max;
} else if (time >= max) {
time -= max;
}
setText(QString::number(time));
}
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 = Ui::MakeWeak(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->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());
_separator1->setAttribute(Qt::WA_TransparentForMouseEvents);
setMouseTracking(true);
_value.changes(
) | rpl::start_with_next([=] {
setErrorShown(false);
}, lifetime());
const auto submitHour = [=] {
if (hour()) {
_minute->setFocus();
}
};
const auto submitMinute = [=] {
if (minute()) {
if (hour()) {
_submitRequests.fire({});
} else {
_hour->setFocus();
}
}
};
connect(
_hour,
&Ui::MaskedInputField::submitted,
submitHour);
connect(
_minute,
&Ui::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->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::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();
}
rpl::producer<> TimeInput::submitRequests() const {
return _submitRequests.events();
}
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 = 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 = 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);
}
}
}
void FillSendUntilOnlineMenu(
not_null<Ui::IconButton*> button,
Fn<void()> callback) {
@ -601,138 +57,6 @@ bool CanScheduleUntilOnline(not_null<PeerData*> peer) {
&& (peer->asUser()->onlineTill > 0);
}
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
not_null<Ui::GenericBox*> box,
rpl::producer<QString> title,
rpl::producer<QString> submit,
Fn<void(TimeId)> done,
TimeId time) {
box->setTitle(std::move(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(
object_ptr<Ui::FixedHeightWidget>(box, st::scheduleHeight));
const auto dayInput = Ui::CreateChild<Ui::InputField>(
content,
st::scheduleDateField);
const auto timeInput = Ui::CreateChild<TimeInput>(
content,
TimeString(time));
const auto at = Ui::CreateChild<Ui::FlatLabel>(
content,
tr::lng_schedule_at(),
st::scheduleAtLabel);
date->value(
) | rpl::start_with_next([=](QDate date) {
dayInput->setText(DayString(date));
timeInput->setFocusFast();
}, dayInput->lifetime());
const auto minDate = QDate::currentDate();
const auto maxDate = minDate.addYears(1).addDays(-1);
const auto &dayViewport = dayInput->rawTextEdit()->viewport();
base::install_event_filter(dayViewport, [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Wheel) {
const auto e = static_cast<QWheelEvent*>(event.get());
const auto direction = ProcessWheelEvent(e);
if (!direction) {
return base::EventFilterResult::Continue;
}
const auto d = date->current().addDays(direction);
*date = std::clamp(d, minDate, maxDate);
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
content->widthValue(
) | rpl::start_with_next([=](int width) {
const auto paddings = width
- at->width()
- 2 * st::scheduleAtSkip
- st::scheduleDateWidth
- st::scheduleTimeWidth;
const auto left = paddings / 2;
dayInput->resizeToWidth(st::scheduleDateWidth);
dayInput->moveToLeft(left, st::scheduleDateTop, width);
at->moveToLeft(
left + st::scheduleDateWidth + st::scheduleAtSkip,
st::scheduleAtTop,
width);
timeInput->resizeToWidth(st::scheduleTimeWidth);
timeInput->moveToLeft(
width - left - st::scheduleTimeWidth,
st::scheduleDateTop,
width);
}, content->lifetime());
const auto calendar =
content->lifetime().make_state<QPointer<CalendarBox>>();
QObject::connect(dayInput, &Ui::InputField::focused, [=] {
if (*calendar) {
return;
}
const auto chosen = [=](QDate chosen) {
*date = chosen;
(*calendar)->closeBox();
};
const auto finalize = [=](not_null<CalendarBox*> box) {
box->setMinDate(minDate);
box->setMaxDate(maxDate);
};
*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 collect = [=] {
const auto timeValue = timeInput->valueCurrent().split(':');
if (timeValue.size() != 2) {
timeInput->showError();
return 0;
}
const auto time = QTime(timeValue[0].toInt(), timeValue[1].toInt());
if (!time.isValid()) {
timeInput->showError();
return 0;
}
const auto result = base::unixtime::serialize(
QDateTime(date->current(), time));
if (result <= base::unixtime::now() + kMinimalSchedule) {
timeInput->showError();
return 0;
}
return result;
};
const auto save = [=] {
if (const auto result = collect()) {
done(result);
}
};
timeInput->submitRequests(
) | rpl::start_with_next(
save,
timeInput->lifetime());
auto result = ChooseDateTimeBoxDescriptor();
box->setFocusCallback([=] { timeInput->setFocusFast(); });
result.submit = box->addButton(std::move(submit), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
return result;
}
void ScheduleBox(
not_null<Ui::GenericBox*> box,
SendMenu::Type type,
@ -752,7 +76,7 @@ void ScheduleBox(
box->closeBox();
copy(result);
};
auto descriptor = ChooseDateTimeBox(
auto descriptor = Ui::ChooseDateTimeBox(
box,
(type == SendMenu::Type::Reminder
? tr::lng_remind_title()

View File

@ -22,18 +22,6 @@ namespace HistoryView {
[[nodiscard]] TimeId DefaultScheduleTime();
[[nodiscard]] bool CanScheduleUntilOnline(not_null<PeerData*> peer);
struct ChooseDateTimeBoxDescriptor {
QPointer<Ui::RoundButton> submit;
Fn<TimeId()> collect;
};
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
not_null<Ui::GenericBox*> box,
rpl::producer<QString> title,
rpl::producer<QString> submit,
Fn<void(TimeId)> done,
TimeId time);
void ScheduleBox(
not_null<Ui::GenericBox*> box,
SendMenu::Type type,

View File

@ -5,15 +5,16 @@ 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 "boxes/calendar_box.h"
#include "ui/boxes/calendar_box.h"
#include "ui/widgets/buttons.h"
#include "lang/lang_keys.h"
#include "ui/effects/ripple_animation.h"
#include "ui/ui_utility.h"
#include "lang/lang_keys.h"
#include "styles/style_boxes.h"
#include "styles/style_dialogs.h"
namespace Ui {
namespace {
constexpr auto kDaysInWeek = 7;
@ -229,7 +230,7 @@ private:
const style::CalendarSizes &_st;
not_null<Context*> _context;
std::map<int, std::unique_ptr<Ui::RippleAnimation>> _ripples;
std::map<int, std::unique_ptr<RippleAnimation>> _ripples;
Fn<void(QDate)> _dateChosenCallback;
@ -257,7 +258,7 @@ void CalendarBox::Inner::monthChanged(QDate month) {
_ripples.clear();
resizeToCurrent();
update();
Ui::SendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton);
SendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton);
}
void CalendarBox::Inner::resizeToCurrent() {
@ -389,9 +390,9 @@ void CalendarBox::Inner::mousePressEvent(QMouseEvent *e) {
auto cell = QRect(rowsLeft() + col * _st.cellSize.width(), rowsTop() + row * _st.cellSize.height(), _st.cellSize.width(), _st.cellSize.height());
auto it = _ripples.find(_selected);
if (it == _ripples.cend()) {
auto mask = Ui::RippleAnimation::ellipseMask(QSize(_st.cellInner, _st.cellInner));
auto mask = RippleAnimation::ellipseMask(QSize(_st.cellInner, _st.cellInner));
auto update = [this, cell] { rtlupdate(cell); };
it = _ripples.emplace(_selected, std::make_unique<Ui::RippleAnimation>(st::defaultRippleAnimation, std::move(mask), std::move(update))).first;
it = _ripples.emplace(_selected, std::make_unique<RippleAnimation>(st::defaultRippleAnimation, std::move(mask), std::move(update))).first;
}
auto ripplePosition = QPoint(cell.x() + (_st.cellSize.width() - _st.cellInner) / 2, cell.y() + (_st.cellSize.height() - _st.cellInner) / 2);
it->second->add(e->pos() - ripplePosition);
@ -611,3 +612,5 @@ void CalendarBox::wheelEvent(QWheelEvent *e) {
}
CalendarBox::~CalendarBox() = default;
} // namespace Ui

View File

@ -7,17 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "abstract_box.h"
#include "ui/layers/box_content.h"
#include "base/observer.h"
namespace style {
struct CalendarSizes;
} // namespace style
namespace Ui {
class IconButton;
} // namespace Ui
class CalendarBox : public Ui::BoxContent, private base::Subscriber {
class IconButton;
class CalendarBox : public BoxContent, private base::Subscriber {
public:
CalendarBox(
QWidget*,
@ -67,10 +68,12 @@ private:
class Title;
object_ptr<Title> _title;
object_ptr<Ui::IconButton> _previous;
object_ptr<Ui::IconButton> _next;
object_ptr<IconButton> _previous;
object_ptr<IconButton> _next;
Fn<void(QDate date)> _callback;
FnMut<void(not_null<CalendarBox*>)> _finalize;
};
} // namespace Ui

View File

@ -0,0 +1,702 @@
/*
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 "ui/boxes/choose_date_time.h"
#include "base/unixtime.h"
#include "base/event_filter.h"
#include "ui/boxes/calendar_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/input_fields.h"
#include "lang/lang_keys.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include <QtCore/QRegularExpression>
namespace Ui {
namespace {
constexpr auto kMinimalSchedule = TimeId(10);
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'));
}
int ProcessWheelEvent(not_null<QWheelEvent*> e) {
// Only a mouse wheel is accepted.
constexpr auto step = static_cast<int>(QWheelEvent::DefaultDeltasPerStep);
const auto delta = e->angleDelta().y();
const auto absDelta = std::abs(delta);
if (absDelta != step) {
return 0;
}
return (delta / absDelta);
}
class TimePart final : public MaskedInputField {
public:
using MaskedInputField::MaskedInputField;
void setMaxValue(int value);
void setWheelStep(int value);
rpl::producer<> erasePrevious() const;
rpl::producer<QChar> putNext() const;
protected:
void keyPressEvent(QKeyEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
void correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) override;
private:
int _maxValue = 0;
int _maxDigits = 0;
int _wheelStep = 0;
rpl::event_stream<> _erasePrevious;
rpl::event_stream<QChar> _putNext;
};
int Number(not_null<TimePart*> field) {
const auto text = field->getLastText();
auto ref = text.midRef(0);
while (!ref.isEmpty() && ref.at(0) == '0') {
ref = ref.mid(1);
}
return ref.toInt();
}
class TimeInput final : public RpWidget {
public:
TimeInput(QWidget *parent, const QString &value);
bool setFocusFast();
rpl::producer<QString> value() const;
rpl::producer<> submitRequests() 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;
object_ptr<TimePart> _hour;
object_ptr<PaddingWrap<FlatLabel>> _separator1;
object_ptr<TimePart> _minute;
rpl::variable<QString> _value;
rpl::event_stream<> _submitRequests;
style::cursor _cursor = style::cur_default;
Animations::Simple _a_borderShown;
int _borderAnimationStart = 0;
Animations::Simple _a_borderOpacity;
bool _borderVisible = false;
Animations::Simple _a_error;
bool _error = false;
Animations::Simple _a_focused;
bool _focused = false;
};
QTime ValidateTime(const QString &value) {
const auto match = QRegularExpression(
"^(\\d{1,2})\\:(\\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;
}
}
void TimePart::setWheelStep(int value) {
_wheelStep = value;
}
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::wheelEvent(QWheelEvent *e) {
const auto direction = ProcessWheelEvent(e);
auto time = Number(this) + (direction * _wheelStep);
const auto max = _maxValue + 1;
if (time < 0) {
time += max;
} else if (time >= max) {
time -= max;
}
setText(QString::number(time));
}
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<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 = MakeWeak(field.data())]{
_borderAnimationStart = pointer->borderAnimationStart()
+ pointer->x()
- _hour->x();
setFocused(true);
};
};
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());
_separator1->setAttribute(Qt::WA_TransparentForMouseEvents);
setMouseTracking(true);
_value.changes(
) | rpl::start_with_next([=] {
setErrorShown(false);
}, lifetime());
const auto submitHour = [=] {
if (hour()) {
_minute->setFocus();
}
};
const auto submitMinute = [=] {
if (minute()) {
if (hour()) {
_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->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::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();
}
rpl::producer<> TimeInput::submitRequests() const {
return _submitRequests.events();
}
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 = 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 = 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
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
not_null<GenericBox*> box,
rpl::producer<QString> title,
rpl::producer<QString> submit,
Fn<void(TimeId)> done,
TimeId time) {
box->setTitle(std::move(title));
box->setWidth(st::boxWideWidth);
const auto date = CreateChild<rpl::variable<QDate>>(
box.get(),
base::unixtime::parse(time).date());
const auto content = box->addRow(
object_ptr<FixedHeightWidget>(box, st::scheduleHeight));
const auto dayInput = CreateChild<InputField>(
content,
st::scheduleDateField);
const auto timeInput = CreateChild<TimeInput>(
content,
TimeString(time));
const auto at = CreateChild<FlatLabel>(
content,
tr::lng_schedule_at(),
st::scheduleAtLabel);
date->value(
) | rpl::start_with_next([=](QDate date) {
dayInput->setText(DayString(date));
timeInput->setFocusFast();
}, dayInput->lifetime());
const auto minDate = QDate::currentDate();
const auto maxDate = minDate.addYears(1).addDays(-1);
const auto &dayViewport = dayInput->rawTextEdit()->viewport();
base::install_event_filter(dayViewport, [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Wheel) {
const auto e = static_cast<QWheelEvent*>(event.get());
const auto direction = ProcessWheelEvent(e);
if (!direction) {
return base::EventFilterResult::Continue;
}
const auto d = date->current().addDays(direction);
*date = std::clamp(d, minDate, maxDate);
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
content->widthValue(
) | rpl::start_with_next([=](int width) {
const auto paddings = width
- at->width()
- 2 * st::scheduleAtSkip
- st::scheduleDateWidth
- st::scheduleTimeWidth;
const auto left = paddings / 2;
dayInput->resizeToWidth(st::scheduleDateWidth);
dayInput->moveToLeft(left, st::scheduleDateTop, width);
at->moveToLeft(
left + st::scheduleDateWidth + st::scheduleAtSkip,
st::scheduleAtTop,
width);
timeInput->resizeToWidth(st::scheduleTimeWidth);
timeInput->moveToLeft(
width - left - st::scheduleTimeWidth,
st::scheduleDateTop,
width);
}, content->lifetime());
const auto calendar =
content->lifetime().make_state<QPointer<CalendarBox>>();
QObject::connect(dayInput, &InputField::focused, [=] {
if (*calendar) {
return;
}
const auto chosen = [=](QDate chosen) {
*date = chosen;
(*calendar)->closeBox();
};
const auto finalize = [=](not_null<CalendarBox*> box) {
box->setMinDate(minDate);
box->setMaxDate(maxDate);
};
*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 collect = [=] {
const auto timeValue = timeInput->valueCurrent().split(':');
if (timeValue.size() != 2) {
timeInput->showError();
return 0;
}
const auto time = QTime(timeValue[0].toInt(), timeValue[1].toInt());
if (!time.isValid()) {
timeInput->showError();
return 0;
}
const auto result = base::unixtime::serialize(
QDateTime(date->current(), time));
if (result <= base::unixtime::now() + kMinimalSchedule) {
timeInput->showError();
return 0;
}
return result;
};
const auto save = [=] {
if (const auto result = collect()) {
done(result);
}
};
timeInput->submitRequests(
) | rpl::start_with_next(
save,
timeInput->lifetime());
auto result = ChooseDateTimeBoxDescriptor();
box->setFocusCallback([=] { timeInput->setFocusFast(); });
result.submit = box->addButton(std::move(submit), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
return result;
}
} // namespace Ui

View File

@ -0,0 +1,28 @@
/*
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
*/
#pragma once
#include "ui/layers/generic_box.h"
namespace Ui {
class RoundButton;
struct ChooseDateTimeBoxDescriptor {
QPointer<RoundButton> submit;
Fn<TimeId()> collect;
};
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
not_null<GenericBox*> box,
rpl::producer<QString> title,
rpl::producer<QString> submit,
Fn<void(TimeId)> done,
TimeId time);
} // namespace Ui

View File

@ -0,0 +1,290 @@
/*
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 "ui/boxes/edit_invite_link.h"
#include "lang/lang_keys.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/checkbox.h"
#include "base/unixtime.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
namespace Ui {
namespace {
constexpr auto kMaxLimit = std::numeric_limits<int>::max();
constexpr auto kHour = 3600;
constexpr auto kDay = 86400;
[[nodiscard]] QString FormatExpireDate(TimeId date) {
if (date > 0) {
return langDateTime(base::unixtime::parse(date));
} else if (-date < kDay) {
return tr::lng_group_call_duration_hours(
tr::now,
lt_count,
(-date / kHour));
} else if (-date < 7 * kDay) {
return tr::lng_group_call_duration_days(
tr::now,
lt_count,
(-date / kDay));
} else {
return tr::lng_local_storage_limit_weeks(
tr::now,
lt_count,
(-date / (7 * kDay)));
}
}
} // namespace
void EditInviteLinkBox(
not_null<GenericBox*> box,
const InviteLinkFields &data,
Fn<void(InviteLinkFields)> done) {
const auto link = data.link;
box->setTitle(link.isEmpty()
? tr::lng_group_invite_new_title()
: tr::lng_group_invite_edit_title());
const auto container = box->verticalLayout();
const auto addTitle = [&](rpl::producer<QString> text) {
container->add(
object_ptr<FlatLabel>(
container,
std::move(text),
st::settingsSubsectionTitle),
st::settingsSubsectionTitlePadding);
};
const auto addDivider = [&](
rpl::producer<QString> text,
style::margins margins = style::margins()) {
container->add(
object_ptr<DividerLabel>(
container,
object_ptr<FlatLabel>(
container,
std::move(text),
st::boxDividerLabel),
st::settingsDividerLabelPadding),
margins);
};
addTitle(tr::lng_group_invite_expire_title());
const auto expiresWrap = container->add(
object_ptr<VerticalLayout>(container),
style::margins(0, 0, 0, st::settingsSectionSkip));
addDivider(
tr::lng_group_invite_expire_about(),
style::margins(0, 0, 0, st::settingsSectionSkip));
addTitle(tr::lng_group_invite_usage_title());
const auto usagesWrap = container->add(
object_ptr<VerticalLayout>(container),
style::margins(0, 0, 0, st::settingsSectionSkip));
addDivider(tr::lng_group_invite_usage_about());
static const auto addButton = [](
not_null<VerticalLayout*> container,
const std::shared_ptr<RadiobuttonGroup> &group,
int value,
const QString &text) {
return container->add(
object_ptr<Radiobutton>(
container,
group,
value,
text),
st::inviteLinkLimitMargin);
};
const auto now = base::unixtime::now();
const auto expire = data.expireDate ? data.expireDate : kMaxLimit;
const auto expireGroup = std::make_shared<RadiobuttonGroup>(expire);
const auto usage = data.usageLimit ? data.usageLimit : kMaxLimit;
const auto usageGroup = std::make_shared<RadiobuttonGroup>(usage);
using Buttons = base::flat_map<int, base::unique_qptr<Radiobutton>>;
struct State {
Buttons expireButtons;
Buttons usageButtons;
int expireValue = 0;
int usageValue = 0;
};
const auto state = container->lifetime().make_state<State>(State{
.expireValue = expire,
.usageValue = usage
});
const auto regenerate = [=] {
expireGroup->setValue(state->expireValue);
usageGroup->setValue(state->usageValue);
auto expires = std::vector{ kMaxLimit, -kHour, -kDay, -kDay * 7, 0 };
auto usages = std::vector{ kMaxLimit, 1, 10, 100, 0 };
auto defaults = State();
for (auto i = begin(expires); i != end(expires); ++i) {
if (*i == state->expireValue) {
break;
} else if (*i == kMaxLimit) {
continue;
} else if (!*i || (now - *i >= state->expireValue)) {
expires.insert(i, state->expireValue);
break;
}
}
for (auto i = begin(usages); i != end(usages); ++i) {
if (*i == state->usageValue) {
break;
} else if (*i == kMaxLimit) {
continue;
} else if (!*i || *i > state->usageValue) {
usages.insert(i, state->usageValue);
break;
}
}
state->expireButtons.clear();
state->usageButtons.clear();
for (const auto limit : expires) {
const auto text = (limit == kMaxLimit)
? tr::lng_group_invite_expire_never(tr::now)
: !limit
? tr::lng_group_invite_expire_custom(tr::now)
: FormatExpireDate(limit);
state->expireButtons.emplace(
limit,
addButton(expiresWrap, expireGroup, limit, text));
}
for (const auto limit : usages) {
const auto text = (limit == kMaxLimit)
? tr::lng_group_invite_usage_any(tr::now)
: !limit
? tr::lng_group_invite_usage_custom(tr::now)
: QString("%L1").arg(limit);
state->usageButtons.emplace(
limit,
addButton(usagesWrap, usageGroup, limit, text));
}
};
const auto guard = MakeWeak(box);
expireGroup->setChangedCallback([=](int value) {
if (value) {
state->expireValue = value;
return;
}
expireGroup->setValue(state->expireValue);
box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
const auto save = [=](TimeId result) {
if (!result) {
return;
}
if (guard) {
state->expireValue = result;
regenerate();
}
box->closeBox();
};
const auto now = base::unixtime::now();
const auto time = (state->expireValue == kMaxLimit)
? (now + kDay)
: (state->expireValue > now)
? state->expireValue
: (state->expireValue < 0)
? (now - state->expireValue)
: (now + kDay);
ChooseDateTimeBox(
box,
tr::lng_group_invite_expire_after(),
tr::lng_settings_save(),
save,
time);
}));
});
usageGroup->setChangedCallback([=](int value) {
if (value) {
state->usageValue = value;
return;
}
usageGroup->setValue(state->usageValue);
box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
const auto height = st::boxPadding.bottom()
+ st::defaultInputField.heightMin
+ st::boxPadding.bottom();
box->setTitle(tr::lng_group_invite_expire_after());
const auto wrap = box->addRow(object_ptr<FixedHeightWidget>(
box,
height));
const auto input = CreateChild<NumberInput>(
wrap,
st::defaultInputField,
tr::lng_group_invite_custom_limit(),
(state->usageValue == kMaxLimit
? QString()
: QString::number(state->usageValue)),
200'000);
wrap->widthValue(
) | rpl::start_with_next([=](int width) {
input->resize(width, input->height());
input->moveToLeft(0, st::boxPadding.bottom());
}, input->lifetime());
box->setFocusCallback([=] {
input->setFocusFast();
});
const auto save = [=] {
const auto value = input->getLastText().toInt();
if (value <= 0) {
input->showError();
return;
}
if (guard) {
state->usageValue = value;
regenerate();
}
box->closeBox();
};
QObject::connect(input, &NumberInput::submitted, save);
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
});
regenerate();
const auto &saveLabel = link.isEmpty()
? tr::lng_formatting_link_create
: tr::lng_settings_save;
box->addButton(saveLabel(), [=] {
const auto expireDate = (state->expireValue == kMaxLimit)
? 0
: (state->expireValue < 0)
? (base::unixtime::now() - state->expireValue)
: state->expireValue;
const auto usageLimit = (state->usageValue == kMaxLimit)
? 0
: state->usageValue;
done(InviteLinkFields{
.link = link,
.expireDate = expireDate,
.usageLimit = usageLimit
});
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void CreateInviteLinkBox(
not_null<GenericBox*> box,
Fn<void(InviteLinkFields)> done) {
EditInviteLinkBox(box, InviteLinkFields(), std::move(done));
}
} // namespace Ui

View File

@ -0,0 +1,29 @@
/*
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
*/
#pragma once
#include "ui/layers/generic_box.h"
namespace Ui {
struct InviteLinkFields {
QString link;
TimeId expireDate = 0;
int usageLimit = 0;
};
void EditInviteLinkBox(
not_null<Ui::GenericBox*> box,
const InviteLinkFields &data,
Fn<void(InviteLinkFields)> done);
void CreateInviteLinkBox(
not_null<Ui::GenericBox*> box,
Fn<void(InviteLinkFields)> done);
} // namespace Ui

View File

@ -833,35 +833,6 @@ largeEmojiOutline: 1px;
largeEmojiPadding: margins(0px, 0px, 0px, 0px);
largeEmojiSkip: 4px;
scheduleHeight: 95px;
scheduleDateTop: 38px;
scheduleDateField: InputField(defaultInputField) {
textMargins: margins(2px, 0px, 2px, 0px);
placeholderScale: 0.;
heightMin: 30px;
textAlign: align(top);
font: font(14px);
}
scheduleTimeField: InputField(scheduleDateField) {
border: 0px;
borderActive: 0px;
heightMin: 28px;
placeholderFont: font(14px);
placeholderFgActive: placeholderFgActive;
}
scheduleDateWidth: 136px;
scheduleTimeWidth: 72px;
scheduleAtSkip: 24px;
scheduleAtTop: 42px;
scheduleAtLabel: FlatLabel(defaultFlatLabel) {
}
scheduleTimeSeparator: FlatLabel(defaultFlatLabel) {
style: TextStyle(defaultTextStyle) {
font: font(14px);
}
}
scheduleTimeSeparatorPadding: margins(2px, 0px, 2px, 0px);
youtubeIcon: icon {
{ "media_youtube_play_bg", youtubePlayIconBg },
{ "media_youtube_play", youtubePlayIconFg, point(24px, 12px) },

View File

@ -40,7 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/toast/toast.h"
#include "ui/toasts/common_toasts.h"
#include "calls/calls_instance.h" // Core::App().calls().inCall().
#include "boxes/calendar_box.h"
#include "ui/boxes/calendar_box.h"
#include "boxes/confirm_box.h"
#include "mainwidget.h"
#include "mainwindow.h"
@ -1051,7 +1051,7 @@ void SessionController::showJumpToDate(Dialogs::Key chat, QDate requestedDate) {
auto callback = [=](const QDate &date) {
session().api().jumpToDate(chat, date);
};
auto box = Box<CalendarBox>(
auto box = Box<Ui::CalendarBox>(
month,
highlighted,
std::move(callback));

View File

@ -64,6 +64,12 @@ PRIVATE
platform/mac/file_bookmark_mac.mm
platform/platform_file_bookmark.h
ui/boxes/calendar_box.cpp
ui/boxes/calendar_box.h
ui/boxes/choose_date_time.cpp
ui/boxes/choose_date_time.h
ui/boxes/edit_invite_link.cpp
ui/boxes/edit_invite_link.h
ui/chat/attach/attach_album_thumbnail.cpp
ui/chat/attach/attach_album_thumbnail.h
ui/chat/attach/attach_album_preview.cpp