Support selecting messages range.

This commit is contained in:
John Preston 2021-11-16 09:52:13 +04:00
parent 4414369fc8
commit b7c95e9636
3 changed files with 314 additions and 39 deletions

View File

@ -28,9 +28,9 @@ class CalendarBox::Context {
public:
Context(QDate month, QDate highlighted);
void setHasSelection(bool has);
[[nodiscard]] bool hasSelection() const {
return _hasSelection;
void setAllowsSelection(bool allowsSelection);
[[nodiscard]] bool allowsSelection() const {
return _allowsSelection;
}
void setMinDate(QDate date);
@ -66,20 +66,38 @@ public:
return (index >= _minDayIndex) && (index <= _maxDayIndex);
}
[[nodiscard]] QDate month() const {
return _month.current();
}
[[nodiscard]] rpl::producer<QDate> monthValue() const {
return _month.value();
}
QDate dateFromIndex(int index) const;
QString labelFromIndex(int index) const;
[[nodiscard]] QDate dateFromIndex(int index) const;
[[nodiscard]] QString labelFromIndex(int index) const;
void toggleSelectionMode(bool enabled);
[[nodiscard]] bool selectionMode() const;
[[nodiscard]] rpl::producer<> selectionUpdates() const;
[[nodiscard]] std::optional<int> selectedMin() const;
[[nodiscard]] std::optional<int> selectedMax() const;
void startSelection(int index);
void updateSelection(int index);
private:
struct Selection {
QDate min;
QDate max;
int minIndex = 0;
int maxIndex = 0;
};
void applyMonth(const QDate &month, bool forced = false);
static int DaysShiftForMonth(QDate month, QDate min);
static int RowsCountForMonth(QDate month, QDate min, QDate max);
bool _hasSelection = false;
bool _allowsSelection = false;
rpl::variable<QDate> _month;
QDate _min, _max;
@ -94,6 +112,12 @@ private:
int _daysShift = 0;
int _rowsCount = 0;
Selection _selection;
QDate _selectionStart;
int _selectionStartIndex = 0;
rpl::event_stream<> _selectionUpdates;
bool _selectionMode = false;
};
CalendarBox::Context::Context(QDate month, QDate highlighted)
@ -101,8 +125,8 @@ CalendarBox::Context::Context(QDate month, QDate highlighted)
showMonth(month);
}
void CalendarBox::Context::setHasSelection(bool has) {
_hasSelection = has;
void CalendarBox::Context::setAllowsSelection(bool allows) {
_allowsSelection = allows;
}
void CalendarBox::Context::setMinDate(QDate date) {
@ -128,17 +152,31 @@ bool CalendarBox::Context::showsMonthOf(QDate date) const {
}
void CalendarBox::Context::applyMonth(const QDate &month, bool forced) {
const auto was = _month.current();
_daysCount = month.daysInMonth();
_daysShift = DaysShiftForMonth(month, _min);
_rowsCount = RowsCountForMonth(month, _min, _max);
_highlightedIndex = month.daysTo(_highlighted);
_minDayIndex = _min.isNull() ? INT_MIN : month.daysTo(_min);
_maxDayIndex = _max.isNull() ? INT_MAX : month.daysTo(_max);
const auto shift = was.isNull() ? 0 : month.daysTo(was);
auto updated = false;
const auto update = [&](const QDate &date, int &index) {
if (shift && !date.isNull()) {
index += shift;
}
};
update(_selection.min, _selection.minIndex);
update(_selection.max, _selection.maxIndex);
update(_selectionStart, _selectionStartIndex);
if (forced) {
_month.force_assign(month);
} else {
_month = month;
}
if (updated) {
_selectionUpdates.fire({});
}
}
void CalendarBox::Context::skipMonth(int skip) {
@ -235,6 +273,70 @@ QString CalendarBox::Context::labelFromIndex(int index) const {
return QString::number(day());
}
void CalendarBox::Context::toggleSelectionMode(bool enabled) {
if (_selectionMode == enabled) {
return;
}
_selectionMode = enabled;
_selectionStart = {};
_selection = {};
_selectionUpdates.fire({});
}
bool CalendarBox::Context::selectionMode() const {
return _selectionMode;
}
rpl::producer<> CalendarBox::Context::selectionUpdates() const {
return _selectionUpdates.events();
}
std::optional<int> CalendarBox::Context::selectedMin() const {
return _selection.min.isNull()
? std::optional<int>()
: _selection.minIndex;
}
std::optional<int> CalendarBox::Context::selectedMax() const {
return _selection.max.isNull()
? std::optional<int>()
: _selection.maxIndex;
}
void CalendarBox::Context::startSelection(int index) {
Expects(_selectionMode);
if (!_selectionStart.isNull() && _selectionStartIndex == index) {
return;
}
_selectionStartIndex = index;
_selectionStart = dateFromIndex(index);
updateSelection(index);
}
void CalendarBox::Context::updateSelection(int index) {
Expects(_selectionMode);
Expects(!_selectionStart.isNull());
index = std::clamp(index, minDayIndex(), maxDayIndex());
const auto start = _selectionStartIndex;
const auto min = std::min(index, start);
const auto max = std::max(index, start);
if (!_selection.min.isNull()
&& _selection.minIndex == min
&& !_selection.max.isNull()
&& _selection.maxIndex == max) {
return;
}
_selection = Selection{
.min = dateFromIndex(min),
.max = dateFromIndex(max),
.minIndex = min,
.maxIndex = max,
};
_selectionUpdates.fire({});
}
class CalendarBox::Inner final : public RpWidget {
public:
Inner(
@ -294,6 +396,11 @@ CalendarBox::Inner::Inner(
) | rpl::start_with_next([=](QDate month) {
monthChanged(month);
}, lifetime());
context->selectionUpdates(
) | rpl::start_with_next([=] {
update();
}, lifetime());
}
void CalendarBox::Inner::monthChanged(QDate month) {
@ -329,7 +436,13 @@ void CalendarBox::Inner::paintRows(Painter &p, QRect clip) {
p.setFont(st::calendarDaysFont);
auto y = rowsTop();
auto index = -_context->daysShift();
auto highlightedIndex = _context->highlightedIndex();
const auto selectionMode = _context->selectionMode();
const auto impossible = index - 45;
const auto selectedMin = _context->selectedMin().value_or(impossible);
const auto selectedMax = _context->selectedMax().value_or(impossible);
const auto highlightedIndex = selectionMode
? impossible
: _context->highlightedIndex();
const auto daysCount = _context->daysCount();
const auto rowsCount = _context->rowsCount();
const auto rowHeight = _st.cellSize.height();
@ -339,31 +452,59 @@ void CalendarBox::Inner::paintRows(Painter &p, QRect clip) {
rowsCount);
y += fromRow * rowHeight;
index += fromRow * kDaysInWeek;
const auto innerSkipLeft = (_st.cellSize.width() - _st.cellInner) / 2;
const auto innerSkipTop = (_st.cellSize.height() - _st.cellInner) / 2;
for (auto row = fromRow; row != tillRow; ++row, y += rowHeight) {
auto x = rowsLeft();
const auto fromIndex = index;
const auto tillIndex = (index + kDaysInWeek);
const auto selectedFrom = std::max(fromIndex, selectedMin);
const auto selectedTill = std::min(tillIndex, selectedMax + 1);
const auto selectedInRow = (selectedTill - selectedFrom);
if (selectedInRow > 0) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::lightButtonBgOver);
p.drawRoundedRect(
(x
+ (selectedFrom - index) * _st.cellSize.width()
+ innerSkipLeft
- st::lineWidth),
y + innerSkipTop - st::lineWidth,
((selectedInRow - 1) * _st.cellSize.width()
+ 2 * st::lineWidth
+ _st.cellInner),
_st.cellInner + 2 * st::lineWidth,
(_st.cellInner / 2.) + st::lineWidth,
(_st.cellInner / 2.) + st::lineWidth);
p.setBrush(Qt::NoBrush);
}
for (auto col = 0; col != kDaysInWeek; ++col, ++index, x += _st.cellSize.width()) {
auto rect = myrtlrect(x, y, _st.cellSize.width(), _st.cellSize.height());
auto grayedOut = (index < 0 || index >= daysCount || !rect.intersects(clip));
auto highlighted = (index == highlightedIndex);
auto enabled = _context->isEnabled(index);
auto innerLeft = x + (_st.cellSize.width() - _st.cellInner) / 2;
auto innerTop = y + (_st.cellSize.height() - _st.cellInner) / 2;
const auto rect = myrtlrect(x, y, _st.cellSize.width(), _st.cellSize.height());
const auto selected = (index >= selectedMin) && (index <= selectedMax);
const auto grayedOut = !selected && (index < 0 || index >= daysCount);
const auto highlighted = (index == highlightedIndex);
const auto enabled = _context->isEnabled(index);
const auto innerLeft = x + innerSkipLeft;
const auto innerTop = y + innerSkipTop;
if (highlighted) {
PainterHighQualityEnabler hq(p);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(grayedOut ? st::windowBgOver : st::dialogsBgActive);
p.drawEllipse(myrtlrect(innerLeft, innerTop, _st.cellInner, _st.cellInner));
p.setBrush(Qt::NoBrush);
}
auto it = _ripples.find(index);
const auto it = _ripples.find(index);
if (it != _ripples.cend()) {
auto colorOverride = [highlighted, grayedOut] {
if (highlighted) {
const auto colorOverride = [&] {
if (selectionMode) {
return st::lightButtonBgOver;
} else if (highlighted) {
return grayedOut ? st::windowBgRipple : st::dialogsRippleBgActive;
}
return st::windowBgOver;
};
it->second->paint(p, innerLeft, innerTop, width(), &(colorOverride()->c));
}()->c;
it->second->paint(p, innerLeft, innerTop, width(), &colorOverride);
if (it->second->empty()) {
_ripples.erase(it);
}
@ -400,6 +541,20 @@ void CalendarBox::Inner::mouseMoveEvent(QMouseEvent *e) {
} else {
setSelected(kEmptySelection);
}
if (_pressed != kEmptySelection && _context->selectionMode()) {
const auto row = (point.y() >= rowsTop())
? (point.y() - rowsTop()) / size.height()
: -1;
const auto col = (point.y() < rowsTop())
? 0
: (point.x() >= rowsLeft())
? std::min(
(point.x() - rowsLeft()) / size.width(),
kDaysInWeek - 1)
: 0;
const auto index = row * kDaysInWeek + col - _context->daysShift();
_context->updateSelection(index);
}
}
void CalendarBox::Inner::setSelected(int selected) {
@ -440,13 +595,24 @@ void CalendarBox::Inner::mousePressEvent(QMouseEvent *e) {
}
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);
if (_context->selectionMode()) {
if (_context->selectedMin().has_value()
&& (e->modifiers() & Qt::ShiftModifier)) {
_context->updateSelection(_selected);
} else {
_context->startSelection(_selected);
}
}
}
}
void CalendarBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
auto pressed = _pressed;
setPressed(kEmptySelection);
if (pressed != kEmptySelection && pressed == _selected) {
if (pressed != kEmptySelection
&& pressed == _selected
&& !_context->selectionMode()) {
crl::on_main(this, [=] {
const auto onstack = _dateChosenCallback;
onstack(_context->dateFromIndex(pressed));
@ -490,7 +656,8 @@ protected:
void paintEvent(QPaintEvent *e);
private:
void monthChanged(QDate month);
void setTextFromMonth(QDate month);
void setText(QString text);
void paintDayNames(Painter &p, QRect clip);
const style::CalendarSizes &_st;
@ -513,13 +680,30 @@ CalendarBox::Title::Title(
_textLeft = _st.padding.left() + (_st.cellSize.width() - dayWidth) / 2;
_context->monthValue(
) | rpl::start_with_next([=](QDate date) {
monthChanged(date);
) | rpl::filter([=] {
return !_context->selectionMode();
}) | rpl::start_with_next([=](QDate date) {
setTextFromMonth(date);
}, lifetime());
_context->selectionUpdates(
) | rpl::start_with_next([=] {
if (!_context->selectionMode()) {
setTextFromMonth(_context->month());
} else if (!_context->selectedMin()) {
setText(tr::lng_calendar_select_days(tr::now));
} else {
setText(QString::number(1 + *_context->selectedMax() - *_context->selectedMin())); // #TODO calendar
}
}, lifetime());
}
void CalendarBox::Title::monthChanged(QDate month) {
_text = langMonthOfYearFull(month.month(), month.year());
void CalendarBox::Title::setTextFromMonth(QDate month) {
setText(langMonthOfYearFull(month.month(), month.year()));
}
void CalendarBox::Title::setText(QString text) {
_text = std::move(text);
_textWidth = st::calendarTitleFont->width(_text);
update();
}
@ -570,9 +754,10 @@ CalendarBox::CalendarBox(QWidget*, CalendarBoxArgs &&args)
, _next(this, st::calendarNext)
, _callback(std::move(args.callback.value()))
, _finalize(std::move(args.finalize))
, _jumpTimer([=] { jump(_jumpButton); }) {
, _jumpTimer([=] { jump(_jumpButton); })
, _selectionChanged(std::move(args.selectionChanged)) {
_title->setAttribute(Qt::WA_TransparentForMouseEvents);
_context->setHasSelection(args.allowsSelection);
_context->setAllowsSelection(args.allowsSelection);
_context->setMinDate(args.minDate);
_context->setMaxDate(args.maxDate);
@ -612,6 +797,12 @@ CalendarBox::CalendarBox(QWidget*, CalendarBoxArgs &&args)
setupJumps(_next.data(), &_nextEnabled);
}
CalendarBox::~CalendarBox() = default;
void CalendarBox::toggleSelectionMode(bool enabled) {
_context->toggleSelectionMode(enabled);
}
void CalendarBox::showJumpTooltip(not_null<IconButton*> button) {
_tooltipButton = button;
Ui::Tooltip::Show(kTooltipDelay, this);
@ -644,21 +835,33 @@ void CalendarBox::prepare() {
_inner->setDateChosenCallback(std::move(_callback));
addButton(tr::lng_close(), [=] { closeBox(); });
_context->monthValue(
) | rpl::start_with_next([=](QDate month) {
monthChanged(month);
}, lifetime());
setExactScroll();
_context->selectionUpdates(
) | rpl::start_with_next([=] {
_selectionMode = _context->selectionMode();
if (_selectionChanged) {
const auto count = !_selectionMode
? std::optional<int>()
: !_context->selectedMin()
? 0
: (1 + *_context->selectedMax() - *_context->selectedMin());
_selectionChanged(this, count);
}
if (!_selectionMode) {
clearButtons();
createButtons();
}
}, lifetime());
createButtons();
if (_finalize) {
_finalize(this);
}
if (_context->hasSelection()) {
addLeftButton(tr::lng_calendar_select_days(), [=] {
});
}
}
bool CalendarBox::isPreviousEnabled() const {
@ -714,6 +917,21 @@ void CalendarBox::processScroll() {
_watchScroll = true;
}
void CalendarBox::createButtons() {
if (!_context->allowsSelection()) {
addButton(tr::lng_close(), [=] { closeBox(); });
} else if (!_context->selectionMode()) {
addButton(tr::lng_close(), [=] { closeBox(); });
addLeftButton(tr::lng_calendar_select_days(), [=] {
_context->toggleSelectionMode(true);
});
} else {
addButton(tr::lng_cancel(), [=] {
_context->toggleSelectionMode(false);
});
}
}
QString CalendarBox::tooltipText() const {
if (_tooltipButton == _previous.data()) {
return tr::lng_calendar_start_tip(tr::now);
@ -737,14 +955,14 @@ void CalendarBox::monthChanged(QDate month) {
_previousEnabled = isPreviousEnabled();
_previous->setIconOverride(_previousEnabled ? nullptr : &st::calendarPreviousDisabled);
_previous->setRippleColorOverride(_previousEnabled ? nullptr : &st::boxBg);
_previous->setCursor(_previousEnabled ? style::cur_pointer : style::cur_default);
_previous->setPointerCursor(_previousEnabled);
if (!_previousEnabled) {
_previous->clearState();
}
_nextEnabled = isNextEnabled();
_next->setIconOverride(_nextEnabled ? nullptr : &st::calendarNextDisabled);
_next->setRippleColorOverride(_nextEnabled ? nullptr : &st::boxBg);
_next->setCursor(_nextEnabled ? style::cur_pointer : style::cur_default);
_next->setPointerCursor(_nextEnabled);
if (!_nextEnabled) {
_next->clearState();
}
@ -782,6 +1000,4 @@ void CalendarBox::keyPressEvent(QKeyEvent *e) {
}
}
CalendarBox::~CalendarBox() = default;
} // namespace Ui

View File

@ -38,6 +38,9 @@ struct CalendarBoxArgs {
QDate minDate;
QDate maxDate;
bool allowsSelection = false;
Fn<void(
not_null<Ui::CalendarBox*>,
std::optional<int>)> selectionChanged;
};
class CalendarBox final : public BoxContent, private AbstractTooltipShower {
@ -45,6 +48,8 @@ public:
CalendarBox(QWidget*, CalendarBoxArgs &&args);
~CalendarBox();
void toggleSelectionMode(bool enabled);
protected:
void prepare() override;
@ -61,6 +66,7 @@ private:
void goNextMonth();
void setExactScroll();
void processScroll();
void createButtons();
void showJumpTooltip(not_null<IconButton*> button);
void jumpAfterDelay(not_null<IconButton*> button);
@ -95,6 +101,11 @@ private:
QPointer<IconButton> _jumpButton;
base::Timer _jumpTimer;
bool _selectionMode = false;
Fn<void(
not_null<Ui::CalendarBox*>,
std::optional<int>)> _selectionChanged;
};
} // namespace Ui

View File

@ -1207,6 +1207,53 @@ void SessionController::showJumpToDate(Dialogs::Key chat, QDate requestedDate) {
: !currentPeerDate.isNull()
? currentPeerDate
: QDate::currentDate();
struct ButtonState {
enum class Type {
None,
Disabled,
Active,
};
Type type = Type::None;
style::complex_color disabledFg = style::complex_color([] {
auto result = st::attentionBoxButton.textFg->c;
result.setAlpha(result.alpha() / 2);
return result;
});
style::RoundButton disabled = st::attentionBoxButton;
};
const auto buttonState = std::make_shared<ButtonState>();
buttonState->disabled.textFg
= buttonState->disabled.textFgOver
= buttonState->disabledFg.color();
buttonState->disabled.ripple.color
= buttonState->disabled.textBgOver
= buttonState->disabled.textBg;
const auto selectionChanged = [=](
not_null<Ui::CalendarBox*> box,
std::optional<int> selected) {
if (!selected.has_value()) {
buttonState->type = ButtonState::Type::None;
return;
}
const auto type = (*selected > 0)
? ButtonState::Type::Active
: ButtonState::Type::Disabled;
if (buttonState->type == type) {
return;
}
buttonState->type = type;
box->clearButtons();
box->addButton(tr::lng_cancel(), [=] {
box->toggleSelectionMode(false);
});
auto text = tr::lng_profile_clear_history();
const auto button = box->addLeftButton(std::move(text), [=] {
}, (*selected > 0) ? st::attentionBoxButton : buttonState->disabled);
if (!*selected) {
button->setPointerCursor(false);
}
};
show(Box<Ui::CalendarBox>(Ui::CalendarBoxArgs{
.month = highlighted,
.highlighted = highlighted,
@ -1216,6 +1263,7 @@ void SessionController::showJumpToDate(Dialogs::Key chat, QDate requestedDate) {
.minDate = minPeerDate,
.maxDate = maxPeerDate,
.allowsSelection = history->peer->isUser(),
.selectionChanged = selectionChanged,
}));
}