Added initial ability to reorder star gifts within collection.

This commit is contained in:
23rd
2025-09-28 14:10:03 +03:00
committed by John Preston
parent f3a27c4fcb
commit 4a938170e1
3 changed files with 441 additions and 9 deletions
@@ -537,6 +537,21 @@ void GiftButton::contextMenuEvent(QContextMenuEvent *e) {
: QCursor::pos());
}
void GiftButton::mousePressEvent(QMouseEvent *e) {
_mouseEvents.fire_copy(e);
AbstractButton::mousePressEvent(e);
}
void GiftButton::mouseMoveEvent(QMouseEvent *e) {
_mouseEvents.fire_copy(e);
AbstractButton::mouseMoveEvent(e);
}
void GiftButton::mouseReleaseEvent(QMouseEvent *e) {
_mouseEvents.fire_copy(e);
AbstractButton::mouseReleaseEvent(e);
}
void GiftButton::cacheUniqueBackground(
not_null<Data::UniqueGift*> unique,
int width,
@@ -982,7 +997,7 @@ QSize Delegate::buttonSize() {
return _single;
}
QMargins Delegate::buttonExtend() {
QMargins Delegate::buttonExtend() const {
return st::defaultDropdownMenu.wrap.shadow.extend;
}
@@ -147,7 +147,7 @@ public:
[[nodiscard]] virtual TextWithEntities ministar() = 0;
[[nodiscard]] virtual Ui::Text::MarkedContext textContext() = 0;
[[nodiscard]] virtual QSize buttonSize() = 0;
[[nodiscard]] virtual QMargins buttonExtend() = 0;
[[nodiscard]] virtual QMargins buttonExtend() const = 0;
[[nodiscard]] virtual auto buttonPatternEmoji(
not_null<Data::UniqueGift*> unique,
Fn<void()> repaint)
@@ -178,10 +178,17 @@ public:
return _contextMenuRequests.events();
}
[[nodiscard]] rpl::producer<QMouseEvent*> mouseEvents() const {
return _mouseEvents.events();
}
private:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void paintBackground(QPainter &p, const QImage &background);
void cacheUniqueBackground(
@@ -199,6 +206,7 @@ private:
const not_null<GiftButtonDelegate*> _delegate;
rpl::event_stream<QPoint> _contextMenuRequests;
rpl::event_stream<QMouseEvent*> _mouseEvents;
QImage _hiddenBgCache;
GiftDescriptor _descriptor;
Ui::Text::String _text;
@@ -247,7 +255,7 @@ public:
TextWithEntities ministar() override;
Ui::Text::MarkedContext textContext() override;
QSize buttonSize() override;
QMargins buttonExtend() override;
QMargins buttonExtend() const override;
auto buttonPatternEmoji(
not_null<Data::UniqueGift*> unique,
Fn<void()> repaint)
@@ -48,6 +48,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_menu_icons.h"
#include "styles/style_credits.h" // giftBoxPadding
#include <QtWidgets/QApplication>
namespace Info::PeerGifts {
namespace {
@@ -160,6 +162,18 @@ private:
uint64 giftId = 0;
int index = 0;
};
struct DragState {
bool enabled = false;
int index = -1;
int lastSelected = -1;
QPoint point;
QPoint startPos;
};
struct ShiftAnimation {
Ui::Animations::Simple xAnimation;
Ui::Animations::Simple yAnimation;
int shift = 0;
};
public:
InnerWidget(
@@ -175,6 +189,9 @@ private:
int visibleTop,
int visibleBottom) override;
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void subscribeToUpdates();
void applyUpdateTo(Entries &entries, const Data::GiftUpdate &update);
@@ -190,6 +207,12 @@ private:
void refreshAbout();
void refreshCollectionsTabs();
void updateSelected();
int giftFromGlobalPos(const QPoint &p) const;
[[nodiscard]] QPoint posFromIndex(int index) const;
[[nodiscard]] bool isDraggedAnimating() const;
void requestReorder(int fromIndex, int toIndex);
void collectionRenamed(int id, QString name);
void collectionRemoved(int id);
@@ -243,6 +266,10 @@ private:
int _visibleFrom = 0;
int _visibleTill = 0;
DragState _dragging;
base::flat_map<int, ShiftAnimation> _shiftAnimations;
int _selected = -1;
base::unique_qptr<Ui::PopupMenu> _menu;
};
@@ -745,14 +772,33 @@ void InnerWidget::validateButtons() {
) | rpl::start_with_next([=](QPoint point) {
showMenuFor(raw, point);
}, raw->lifetime());
raw->mouseEvents(
) | rpl::start_with_next([=](QMouseEvent *e) {
switch (e->type()) {
case QEvent::MouseButtonPress:
raw->raise();
mousePressEvent(e);
break;
case QEvent::MouseMove:
mouseMoveEvent(e);
break;
case QEvent::MouseButtonRelease:
mouseReleaseEvent(e);
break;
default:
break;
}
}, raw->lifetime());
raw->show();
views.push_back({ .button = std::move(button) });
}
}
auto &view = views.back();
const auto callback = [=] {
showGift(index);
};
const auto callback = _addingToCollectionId
? Fn<void()>([=] { showGift(index); })
: nullptr;
view.index = index;
view.manageId = manageId;
view.giftId = giftId;
@@ -763,7 +809,9 @@ void InnerWidget::validateButtons() {
anim::type::instant);
}
view.button->setDescriptor(descriptor, _mode);
view.button->setClickedCallback(callback);
if (callback) {
view.button->setClickedCallback(callback);
}
return true;
};
for (auto j = fromRow; j != tillRow; ++j) {
@@ -771,8 +819,36 @@ void InnerWidget::validateButtons() {
if (!add(i, j)) {
break;
}
views.back().button->setGeometry(
QRect(QPoint(x, y), _single),
const auto &view = views.back();
const auto viewIndex = view.index;
auto pos = QPoint(x, y);
if (_dragging.enabled && viewIndex >= 0) {
if (viewIndex == _dragging.index && !isDraggedAnimating()) {
pos = mapFromGlobal(QCursor::pos()) - _dragging.point;
} else if (viewIndex == _dragging.index && isDraggedAnimating()) {
const auto it = _shiftAnimations.find(viewIndex);
if (it != _shiftAnimations.end()) {
pos = QPoint(
it->second.xAnimation.value(pos.x()),
it->second.yAnimation.value(pos.y()));
}
} else {
const auto it = _shiftAnimations.find(viewIndex);
if (it != _shiftAnimations.end()) {
const auto &entry = it->second;
const auto toPos = posFromIndex(viewIndex + entry.shift);
pos = QPoint(
entry.xAnimation.value(toPos.x()),
entry.yAnimation.value(toPos.y()));
}
}
} else if (_dragging.enabled && viewIndex == _dragging.index) {
pos = mapFromGlobal(QCursor::pos()) - _dragging.point;
}
view.button->setGeometry(
QRect(pos, _single),
_delegate.buttonExtend());
x += _single.width() + skipw;
}
@@ -1564,6 +1640,339 @@ void InnerWidget::flushCollectionReorder() {
_pendingCollectionReorder = false;
}
void InnerWidget::mousePressEvent(QMouseEvent *e) {
if (e->button() != Qt::LeftButton || _addingToCollectionId) {
return;
}
const auto index = giftFromGlobalPos(e->globalPos());
if (index < 0 || index >= _list->size()) {
return;
}
if (_peer->canManageGifts()
&& _descriptor.current().collectionId
&& _list->size() > 1) {
if (isDraggedAnimating()) {
return;
}
_dragging.enabled = false;
_dragging.index = index;
_dragging.point = mapFromGlobal(e->globalPos()) - posFromIndex(index);
_dragging.startPos = e->globalPos();
return;
}
}
void InnerWidget::mouseMoveEvent(QMouseEvent *e) {
updateSelected();
const auto draggedAnimating = isDraggedAnimating();
if (!_dragging.enabled && _dragging.index >= 0) {
const auto distance
= (e->globalPos() - _dragging.startPos).manhattanLength();
if (distance > QApplication::startDragDistance()) {
_dragging.enabled = true;
}
}
if (_selected >= 0 && !draggedAnimating) {
_dragging.lastSelected = _selected;
}
if (_dragging.enabled
&& _dragging.index >= 0
&& _dragging.index < _list->size()
&& _dragging.lastSelected >= 0
&& !draggedAnimating) {
for (auto i = 0; i < _list->size(); i++) {
if (i == _dragging.index) {
continue;
}
auto &entry = _shiftAnimations[i];
const auto wasShift = entry.shift;
if ((i >= _dragging.index) && (i <= _dragging.lastSelected)) {
if (entry.shift == 0) {
entry.shift = -1;
} else if (entry.shift == 1) {
entry.shift = 0;
}
} else if ((i < _dragging.index)
&& (i >= _dragging.lastSelected)) {
if (entry.shift == 0) {
entry.shift = 1;
} else if (entry.shift == -1) {
entry.shift = 0;
}
}
if ((i < std::min(_dragging.index, _dragging.lastSelected))
|| (i > std::max(_dragging.index, _dragging.lastSelected))) {
entry.shift = 0;
}
if (wasShift != entry.shift) {
const auto fromPoint = posFromIndex(i + wasShift);
const auto toPoint = posFromIndex(i + entry.shift);
const auto toX = float64(toPoint.x());
const auto toY = float64(toPoint.y());
const auto ratio = [&] {
const auto fromX = entry.xAnimation.value(toX);
const auto ratioX = std::min(toX, fromX)
/ std::max(toX, fromX);
const auto fromY = entry.yAnimation.value(toY);
const auto ratioY = std::min(toY, fromY)
/ std::max(toY, fromY);
return (ratioX == 1.)
? ratioY
: (ratioY == 1.)
? ratioX
: std::max(ratioX, ratioY);
}();
if (!entry.xAnimation.animating()) {
entry.xAnimation.stop();
entry.xAnimation.start(
[this, i](float64 value) {
for (auto &view : _views) {
if (view.index == i && view.button) {
view.button->moveToLeft(
value,
view.button->y());
}
}
},
fromPoint.x(),
toX,
st::fadeWrapDuration);
} else {
entry.xAnimation.change(
toX,
st::fadeWrapDuration * (1. - ratio),
anim::linear);
}
if (!entry.yAnimation.animating()) {
entry.yAnimation.stop();
entry.yAnimation.start(
[this, i](float64 value) {
for (auto &view : _views) {
if (view.index == i && view.button) {
view.button->moveToLeft(
view.button->x(),
value);
}
}
},
fromPoint.y(),
toY,
st::fadeWrapDuration);
} else {
entry.yAnimation.change(
toY,
st::fadeWrapDuration * (1. - ratio),
anim::linear);
}
}
}
for (auto &view : _views) {
if (view.index >= 0 && view.button) {
auto pos = posFromIndex(view.index);
if (view.index == _dragging.index && !isDraggedAnimating()) {
pos = mapFromGlobal(QCursor::pos()) - _dragging.point;
} else if (view.index == _dragging.index
&& isDraggedAnimating()) {
const auto it = _shiftAnimations.find(view.index);
if (it != _shiftAnimations.end()) {
pos = QPoint(
it->second.xAnimation.value(pos.x()),
it->second.yAnimation.value(pos.y()));
}
} else {
const auto it = _shiftAnimations.find(view.index);
if (it != _shiftAnimations.end()) {
const auto &entry = it->second;
const auto toPos
= posFromIndex(view.index + entry.shift);
pos = QPoint(
entry.xAnimation.value(toPos.x()),
entry.yAnimation.value(toPos.y()));
}
}
view.button->moveToLeft(pos.x(), pos.y());
}
}
update();
}
}
void InnerWidget::mouseReleaseEvent(QMouseEvent *e) {
if (_dragging.enabled && _dragging.index >= 0 && !isDraggedAnimating()) {
for (auto &[index, entry] : _shiftAnimations) {
if (index != _dragging.index) {
entry.xAnimation.stop();
entry.yAnimation.stop();
if (auto view = ranges::find(_views, index, &View::index);
view != end(_views) && view->button) {
const auto finalPos = posFromIndex(index + entry.shift);
view->button->moveToLeft(finalPos.x(), finalPos.y());
}
}
}
const auto fromPos = mapFromGlobal(e->globalPos()) - _dragging.point;
const auto toPos = posFromIndex(_dragging.lastSelected);
const auto wasPosition = _dragging.index;
const auto nowPosition = _dragging.lastSelected;
const auto finish = [=, this] {
base::reorder(*_list, wasPosition, nowPosition);
for (auto &view : _views) {
view.index = base::reorder_index(
view.index,
wasPosition,
nowPosition);
}
requestReorder(wasPosition, nowPosition);
_dragging = {};
_shiftAnimations.clear();
};
auto &entry = _shiftAnimations[_dragging.index];
entry.xAnimation.stop();
entry.yAnimation.stop();
entry.xAnimation.start(
[finish, toPos, this](float64 value) {
const auto index = _dragging.index;
if (value >= toPos.x()
&& index >= 0
&& !_shiftAnimations[index].yAnimation.animating()) {
finish();
}
for (auto &view : _views) {
if (view.index == index && view.button) {
view.button->moveToLeft(value, view.button->y());
}
}
},
fromPos.x(),
toPos.x(),
st::fadeWrapDuration);
entry.yAnimation.start(
[finish, toPos, this](float64 value) {
const auto index = _dragging.index;
if (value >= toPos.y()
&& index >= 0
&& !_shiftAnimations[index].xAnimation.animating()) {
finish();
}
for (auto &view : _views) {
if (view.index == index && view.button) {
view.button->moveToLeft(view.button->x(), value);
}
}
},
fromPos.y(),
toPos.y(),
st::fadeWrapDuration);
} else {
_dragging = {};
_shiftAnimations.clear();
const auto index = giftFromGlobalPos(e->globalPos());
if (index >= 0 && index < _list->size()) {
showGift(index);
}
}
}
void InnerWidget::updateSelected() {
const auto selected = giftFromGlobalPos(QCursor::pos());
if (_selected != selected) {
_selected = selected;
}
}
int InnerWidget::giftFromGlobalPos(const QPoint &p) const {
const auto l = mapFromGlobal(p);
if (!_perRow) {
return -1;
}
const auto padding = st::giftBoxPadding;
const auto vskip = (_collectionsTabs && !_collectionsTabs->isHidden())
? (padding.top() + _collectionsTabs->height() + padding.top())
: padding.bottom();
const auto row = (l.y() >= vskip)
? ((l.y() - vskip) / (_single.height() + st::giftBoxGiftSkip.y()))
: -1;
const auto available = width() - padding.left() - padding.right();
const auto skipw = st::giftBoxGiftSkip.x();
const auto fullw = _perRow * (_single.width() + skipw) - skipw;
const auto left = padding.left() + (available - fullw) / 2;
const auto col = (l.x() >= left)
? ((l.x() - left) / (_single.width() + skipw))
: -1;
if (row >= 0 && col >= 0 && col < _perRow) {
const auto result = row * _perRow + col;
return (result < _list->size()) ? result : -1;
}
return -1;
}
QPoint InnerWidget::posFromIndex(int index) const {
if (!_perRow) {
return {};
}
const auto padding = st::giftBoxPadding;
const auto vskip = (_collectionsTabs && !_collectionsTabs->isHidden())
? (padding.top() + _collectionsTabs->height() + padding.top())
: padding.bottom();
const auto available = width() - padding.left() - padding.right();
const auto skipw = st::giftBoxGiftSkip.x();
const auto skiph = st::giftBoxGiftSkip.y();
const auto fullw = _perRow * (_single.width() + skipw) - skipw;
const auto left = padding.left() + (available - fullw) / 2;
const auto extend = _delegate.buttonExtend();
return {
left + (index % _perRow) * (_single.width() + skipw) - extend.left(),
vskip + (index / _perRow) * (_single.height() + skiph) - extend.top(),
};
}
bool InnerWidget::isDraggedAnimating() const {
if (_dragging.index < 0) {
return false;
}
const auto it = _shiftAnimations.find(_dragging.index);
return (it == _shiftAnimations.end())
? false
: (it->second.xAnimation.animating()
|| it->second.yAnimation.animating());
}
void InnerWidget::requestReorder(int fromIndex, int toIndex) {
if (fromIndex == toIndex || !_peer->canManageGifts()) {
return;
}
const auto collectionId = _descriptor.current().collectionId;
if (!collectionId) {
return;
}
auto order = QVector<MTPInputSavedStarGift>();
order.reserve(_list->size());
for (const auto &entry : *_list) {
order.push_back(Api::InputSavedStarGiftId(entry.gift.manageId));
}
_api.request(
MTPpayments_UpdateStarGiftCollection(
MTP_flags(MTPpayments_UpdateStarGiftCollection::Flag::f_order),
_peer->input,
MTP_int(collectionId),
MTPstring(),
MTPVector<MTPInputSavedStarGift>(),
MTPVector<MTPInputSavedStarGift>(),
MTP_vector<MTPInputSavedStarGift>(order))
).fail([show = _window->uiShow()](const MTP::Error &error) {
show->showToast(error.type());
}).send();
}
void InnerWidget::reorderCollections(
const Ui::SubTabs::ReorderUpdate &update) {
reorderCollectionsLocally(update);