feat(staging): 删除动画与拖动排序

- 删除:右移淡出 200ms(OutCubic)后移除
- 拖动排序:允许把条目拖回列表,按落点移动并持久化
- 自身拖拽不再被当作外部拖入
This commit is contained in:
2026-10-30 20:19:36 +08:00
parent d69326bc6f
commit 1fa1ff45f6
4 changed files with 118 additions and 9 deletions
+2 -1
View File
@@ -6,6 +6,7 @@
- 传送门面板:DBlur 双栏、layer-shell 定位、拖拽自动弹出、位置/高度可调并持久化
- 暂存区:文件/文字/图片、去重、上限、文件消失清理、固定/消耗语义、全选/反选/删除选中
- 暂存区动画:hover 高亮、删除右移淡出、拖动排序
- 内容工厂:按类型给出动作,动作按钮可直接接收 drop
- http(s) 链接按扩展名分类,支持下载/另存为/复制/Markdown/待办/问小埃
- thunder:// 解析真实地址,magnet 解析名称,非标准 scheme 尝试打开
@@ -16,6 +17,6 @@
编辑/重试/分支、上下文条数与自动压缩、关于
- 合成器 portal(flake-wm / gxde-wlcom):org.gxde.Portal、缩略图坐标映射、
原生 drop 到目标窗口
- gxde-k9 开机自启与保活开关
- gxde-k9 开机自启与保活开关;不支持环境只提示一次并保持待命
-- shenmo <shenmo@spark-app.store>
+17
View File
@@ -306,6 +306,23 @@ void StagingModel::setChecked(int row, bool checked)
save();
}
void StagingModel::moveItem(int from, int to)
{
if (from < 0 || from >= m_items.size() || to < 0 || to >= m_items.size() || from == to) {
return;
}
if (to > from) {
beginMoveRows(QModelIndex(), from, from, QModelIndex(), to + 1);
m_items.move(from, to);
endMoveRows();
} else {
beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
m_items.move(from, to);
endMoveRows();
}
save();
}
void StagingModel::setPinned(int row, bool pinned)
{
if (row < 0 || row >= m_items.size()) {
+1
View File
@@ -51,6 +51,7 @@ public:
void setChecked(int row, bool checked);
void setPinned(int row, bool pinned);
void moveItem(int from, int to);
void clearAll();
const StagingItem *itemAt(int row) const;
+98 -8
View File
@@ -48,6 +48,8 @@
#include <QScreen>
#include <QStyledItemDelegate>
#include <QTimer>
#include <QVariantAnimation>
#include <QEasingCurve>
#include <QUrl>
#include <QVBoxLayout>
@@ -132,17 +134,28 @@ QString typeLabel(const QModelIndex &index)
class StagingDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit StagingDelegate(QObject *parent = nullptr)
: QStyledItemDelegate(parent)
{
}
void setHoverRow(int row)
void setHoverRow(int row) { m_hoverRow = row; }
void setRemoveState(int row, qreal progress)
{
m_hoverRow = row;
m_removingRow = row;
m_removeProgress = progress;
}
signals:
void removeRequested(int row);
public:
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
Q_UNUSED(option)
@@ -156,6 +169,11 @@ public:
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
if (index.row() == m_removingRow && m_removeProgress > 0.0) {
painter->setOpacity(1.0 - m_removeProgress);
painter->translate(m_removeProgress * 48.0, 0.0);
}
const QRect row = option.rect.adjusted(4, 2, -4, -2);
const bool hovered = index.row() == m_hoverRow;
@@ -247,7 +265,7 @@ public:
}
if (closeRect(option.rect).contains(pos)) {
model->removeRow(index.row());
Q_EMIT removeRequested(index.row());
return true;
}
@@ -256,6 +274,8 @@ public:
private:
int m_hoverRow = -1;
int m_removingRow = -1;
qreal m_removeProgress = 0.0;
};
/* ------------------------------------------------------------------------- */
@@ -285,6 +305,30 @@ public:
viewport()->setAutoFillBackground(false);
setStyleSheet(QStringLiteral("QListView { background: transparent; }"));
setAcceptDrops(true);
m_removeAnim = new QVariantAnimation(this);
m_removeAnim->setDuration(200);
m_removeAnim->setEasingCurve(QEasingCurve::OutCubic);
connect(m_removeAnim, &QVariantAnimation::valueChanged, this, [this](const QVariant &value) {
if (m_removingRow >= 0) {
m_delegate->setRemoveState(m_removingRow, value.toReal());
viewport()->update();
}
});
connect(m_removeAnim, &QVariantAnimation::finished, this, [this]() {
if (m_removingRow < 0) {
return;
}
auto *staging = qobject_cast<StagingModel *>(this->model());
const int row = m_removingRow;
m_delegate->setRemoveState(-1, 0.0);
m_removingRow = -1;
if (staging) {
staging->removeRow(row);
}
viewport()->update();
});
connect(m_delegate, &StagingDelegate::removeRequested, this, &StagingView::startRemove);
}
signals:
@@ -346,7 +390,7 @@ protected:
void dragEnterEvent(QDragEnterEvent *event) override
{
if (isOwnDrag(event->mimeData())) {
event->ignore();
event->acceptProposedAction();
return;
}
if (mimeSupported(event->mimeData())) {
@@ -360,7 +404,7 @@ protected:
void dragMoveEvent(QDragMoveEvent *event) override
{
if (isOwnDrag(event->mimeData())) {
event->ignore();
event->acceptProposedAction();
return;
}
if (mimeSupported(event->mimeData())) {
@@ -374,12 +418,35 @@ protected:
void dropEvent(QDropEvent *event) override
{
if (isOwnDrag(event->mimeData()) || !mimeSupported(event->mimeData())) {
if (isOwnDrag(event->mimeData())) {
auto *staging = qobject_cast<StagingModel *>(this->model());
const QString id = QString::fromUtf8(
event->mimeData()->data(QStringLiteral("application/x-gxde-ai-see-staging")));
int from = -1;
if (staging) {
for (int row = 0; row < staging->count(); ++row) {
const StagingItem *item = staging->itemAt(row);
if (item && item->id == id) {
from = row;
break;
}
}
}
const QModelIndex index = indexAt(event->position().toPoint());
int to = index.isValid() ? index.row() : (staging ? staging->count() - 1 : -1);
if (staging && from >= 0 && to >= 0 && from != to) {
staging->moveItem(from, to);
}
m_internalMove = true;
event->acceptProposedAction();
return;
}
if (!mimeSupported(event->mimeData())) {
event->ignore();
return;
}
auto *staging = qobject_cast<StagingModel *>(model());
auto *staging = qobject_cast<StagingModel *>(this->model());
if (staging) {
staging->addMimeData(event->mimeData());
}
@@ -410,6 +477,21 @@ private:
return mime && mime->hasFormat(QStringLiteral("application/x-gxde-ai-see-staging"));
}
void startRemove(int row)
{
if (m_removeAnim->state() == QAbstractAnimation::Running) {
return;
}
if (row < 0 || !model() || row >= model()->rowCount()) {
return;
}
m_removingRow = row;
m_delegate->setRemoveState(row, 0.0);
m_removeAnim->setStartValue(0.0);
m_removeAnim->setEndValue(1.0);
m_removeAnim->start();
}
void startDragForIndex(const QModelIndex &index)
{
if (!index.isValid()) {
@@ -449,10 +531,15 @@ private:
const Qt::DropAction action =
drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction);
if (m_internalMove) {
m_internalMove = false;
return;
}
/* Dragging an item out of the staging area consumes it unless it is
* pinned. A cancelled drag (Esc / dropped on nothing) keeps it. */
if (action != Qt::IgnoreAction && !pinned) {
auto *staging = qobject_cast<StagingModel *>(model());
auto *staging = qobject_cast<StagingModel *>(this->model());
if (staging) {
for (int row = 0; row < staging->count(); ++row) {
const StagingItem *item = staging->itemAt(row);
@@ -466,9 +553,12 @@ private:
}
StagingDelegate *m_delegate = nullptr;
QVariantAnimation *m_removeAnim = nullptr;
QPoint m_pressPos;
QModelIndex m_pressIndex;
int m_hoverRow = -1;
int m_removingRow = -1;
bool m_internalMove = false;
};
/* ------------------------------------------------------------------------- */