feat(portal): 接入合成器传送门,暂存区固定/消耗语义
- 新增 PortalClient:org.gxde.Portal 的 BeginPortal / CancelPortal 与 drag 信号 - 传送门区域悬停 150ms 后打开合成器 overview;无窗口/不支持时给出提示 - 拖拽开始时自动弹出面板(120ms 延迟,mime 过滤),拖拽结束自动收起 - 暂存区每行新增固定按钮;默认拖出后消耗,固定后保留;取消拖拽不消耗 - 拒绝自身拖拽落入暂存区,避免重复条目
This commit is contained in:
@@ -22,6 +22,7 @@ set(GXDE_AI_SEE_SOURCES
|
|||||||
src/app.cpp
|
src/app.cpp
|
||||||
src/config.cpp
|
src/config.cpp
|
||||||
src/staging/stagingmodel.cpp
|
src/staging/stagingmodel.cpp
|
||||||
|
src/portal/portalclient.cpp
|
||||||
src/ui/portalpanel.cpp
|
src/ui/portalpanel.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,8 @@ interface org.gxde.Portal
|
|||||||
- 每行:复选框、缩略图/类型图标、名称、关闭按钮。
|
- 每行:复选框、缩略图/类型图标、名称、关闭按钮。
|
||||||
- 选中任意复选框 → 列表上方出现「全选 / 反选 / 删除选中」;取消全不选后收起。
|
- 选中任意复选框 → 列表上方出现「全选 / 反选 / 删除选中」;取消全不选后收起。
|
||||||
- 拖动行排序(带动画),拖出到别的 App(`QDrag`),拖到传送门(原生 drop)。
|
- 拖动行排序(带动画),拖出到别的 App(`QDrag`),拖到传送门(原生 drop)。
|
||||||
|
- 每行有「固定」按钮:默认拖出后**消耗**(drop 成功即从暂存区移除);固定后拖出保留。
|
||||||
|
拖回暂存区或按 Esc 取消拖拽不消耗。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+24
@@ -4,10 +4,12 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "portal/portalclient.h"
|
||||||
#include "staging/stagingmodel.h"
|
#include "staging/stagingmodel.h"
|
||||||
#include "ui/portalpanel.h"
|
#include "ui/portalpanel.h"
|
||||||
|
|
||||||
#include <QLocalServer>
|
#include <QLocalServer>
|
||||||
|
#include <QTimer>
|
||||||
#include <QLocalSocket>
|
#include <QLocalSocket>
|
||||||
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -50,6 +52,28 @@ bool App::init(bool daemon, bool forceShow)
|
|||||||
|
|
||||||
m_model = new StagingModel(this);
|
m_model = new StagingModel(this);
|
||||||
|
|
||||||
|
PortalClient *portal = PortalClient::instance();
|
||||||
|
connect(portal, &PortalClient::dragStarted, this, [this](const QString &mimeTypes) {
|
||||||
|
if (!PortalClient::mimeInteresting(mimeTypes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_dragging = true;
|
||||||
|
if (!m_model->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QTimer::singleShot(120, this, [this]() {
|
||||||
|
if (m_dragging && m_model->isEmpty()) {
|
||||||
|
showPanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
connect(portal, &PortalClient::dragEnded, this, [this]() {
|
||||||
|
m_dragging = false;
|
||||||
|
if (m_model->isEmpty() && m_panel && m_panel->isVisible()) {
|
||||||
|
m_panel->dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
connect(m_model, &StagingModel::countChanged, this, [this]() {
|
connect(m_model, &StagingModel::countChanged, this, [this]() {
|
||||||
if (!m_daemon && !m_model->isEmpty() && m_panel) {
|
if (!m_daemon && !m_model->isEmpty() && m_panel) {
|
||||||
m_panel->popup();
|
m_panel->popup();
|
||||||
|
|||||||
@@ -35,4 +35,5 @@ private:
|
|||||||
PortalPanel *m_panel = nullptr;
|
PortalPanel *m_panel = nullptr;
|
||||||
QLocalServer *m_server = nullptr;
|
QLocalServer *m_server = nullptr;
|
||||||
bool m_daemon = false;
|
bool m_daemon = false;
|
||||||
|
bool m_dragging = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 GXDE OS Team
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "portalclient.h"
|
||||||
|
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusConnectionInterface>
|
||||||
|
#include <QDBusMessage>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr const char *kService = "org.gxde.Portal";
|
||||||
|
constexpr const char *kPath = "/org/gxde/Portal";
|
||||||
|
constexpr const char *kInterface = "org.gxde.Portal";
|
||||||
|
}
|
||||||
|
|
||||||
|
PortalClient *PortalClient::instance()
|
||||||
|
{
|
||||||
|
static PortalClient *self = new PortalClient;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
PortalClient::PortalClient(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
auto bus = QDBusConnection::sessionBus();
|
||||||
|
|
||||||
|
if (auto *iface = bus.interface()) {
|
||||||
|
m_available = iface->isServiceRegistered(QString::fromLatin1(kService));
|
||||||
|
connect(iface, &QDBusConnectionInterface::serviceRegistered, this,
|
||||||
|
[this](const QString &name) {
|
||||||
|
if (name == QString::fromLatin1(kService)) {
|
||||||
|
m_available = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(iface, &QDBusConnectionInterface::serviceUnregistered, this,
|
||||||
|
[this](const QString &name) {
|
||||||
|
if (name == QString::fromLatin1(kService)) {
|
||||||
|
m_available = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.connect(QString::fromLatin1(kService), QString::fromLatin1(kPath),
|
||||||
|
QString::fromLatin1(kInterface), QStringLiteral("DragStarted"), this,
|
||||||
|
SLOT(onDragStarted(QString)));
|
||||||
|
bus.connect(QString::fromLatin1(kService), QString::fromLatin1(kPath),
|
||||||
|
QString::fromLatin1(kInterface), QStringLiteral("DragEnded"), this,
|
||||||
|
SLOT(onDragEnded()));
|
||||||
|
bus.connect(QString::fromLatin1(kService), QString::fromLatin1(kPath),
|
||||||
|
QString::fromLatin1(kInterface), QStringLiteral("PortalDropped"), this,
|
||||||
|
SLOT(onDropped(QString, bool)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PortalClient::beginPortal()
|
||||||
|
{
|
||||||
|
QDBusMessage message =
|
||||||
|
QDBusMessage::createMethodCall(QString::fromLatin1(kService),
|
||||||
|
QString::fromLatin1(kPath),
|
||||||
|
QString::fromLatin1(kInterface),
|
||||||
|
QStringLiteral("BeginPortal"));
|
||||||
|
const QDBusMessage reply = QDBusConnection::sessionBus().call(message, QDBus::Block, 500);
|
||||||
|
if (reply.type() != QDBusMessage::ReplyMessage || reply.arguments().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return reply.arguments().constFirst().toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PortalClient::cancelPortal()
|
||||||
|
{
|
||||||
|
QDBusMessage message =
|
||||||
|
QDBusMessage::createMethodCall(QString::fromLatin1(kService),
|
||||||
|
QString::fromLatin1(kPath),
|
||||||
|
QString::fromLatin1(kInterface),
|
||||||
|
QStringLiteral("CancelPortal"));
|
||||||
|
QDBusConnection::sessionBus().call(message, QDBus::NoBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PortalClient::mimeInteresting(const QString &mimeTypes)
|
||||||
|
{
|
||||||
|
const QStringList types = mimeTypes.split(QLatin1Char(';'), Qt::SkipEmptyParts);
|
||||||
|
for (const QString &type : types) {
|
||||||
|
if (type.startsWith(QLatin1String("text/")) || type.startsWith(QLatin1String("image/"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PortalClient::onDragStarted(const QString &mimeTypes)
|
||||||
|
{
|
||||||
|
Q_EMIT dragStarted(mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PortalClient::onDragEnded()
|
||||||
|
{
|
||||||
|
Q_EMIT dragEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PortalClient::onDropped(const QString &uuid, bool accepted)
|
||||||
|
{
|
||||||
|
Q_EMIT dropped(uuid, accepted);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 GXDE OS Team
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client for the compositor side "portal" (org.gxde.Portal).
|
||||||
|
*
|
||||||
|
* Start with \a beginPortal() while a drag hovers the portal zone. The
|
||||||
|
* compositor opens the window overview in portal mode and forwards the drag
|
||||||
|
* to the window under the cursor, so the target application receives a real
|
||||||
|
* wl_data_device drop at the cursor position.
|
||||||
|
*/
|
||||||
|
class PortalClient : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
static PortalClient *instance();
|
||||||
|
|
||||||
|
bool available() const { return m_available; }
|
||||||
|
|
||||||
|
/** @return true when the compositor opened the overview. */
|
||||||
|
bool beginPortal();
|
||||||
|
void cancelPortal();
|
||||||
|
|
||||||
|
/** Filter the drag mime types we want to react to. */
|
||||||
|
static bool mimeInteresting(const QString &mimeTypes);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void dragStarted(const QString &mimeTypes);
|
||||||
|
void dragEnded();
|
||||||
|
void dropped(const QString &uuid, bool accepted);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onDragStarted(const QString &mimeTypes);
|
||||||
|
void onDragEnded();
|
||||||
|
void onDropped(const QString &uuid, bool accepted);
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit PortalClient(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
bool m_available = false;
|
||||||
|
};
|
||||||
@@ -30,6 +30,7 @@ struct StagingItem
|
|||||||
QString text; // full text (Text)
|
QString text; // full text (Text)
|
||||||
QString name; // display name
|
QString name; // display name
|
||||||
bool checked = false;
|
bool checked = false;
|
||||||
|
bool pinned = false;
|
||||||
qint64 createdAt = 0;
|
qint64 createdAt = 0;
|
||||||
|
|
||||||
bool exists() const;
|
bool exists() const;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ QJsonObject StagingItem::toJson() const
|
|||||||
obj.insert(QStringLiteral("text"), text);
|
obj.insert(QStringLiteral("text"), text);
|
||||||
obj.insert(QStringLiteral("name"), name);
|
obj.insert(QStringLiteral("name"), name);
|
||||||
obj.insert(QStringLiteral("checked"), checked);
|
obj.insert(QStringLiteral("checked"), checked);
|
||||||
|
obj.insert(QStringLiteral("pinned"), pinned);
|
||||||
obj.insert(QStringLiteral("createdAt"), createdAt);
|
obj.insert(QStringLiteral("createdAt"), createdAt);
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
@@ -95,6 +96,7 @@ StagingItem StagingItem::fromJson(const QJsonObject &obj)
|
|||||||
item.text = obj.value(QStringLiteral("text")).toString();
|
item.text = obj.value(QStringLiteral("text")).toString();
|
||||||
item.name = obj.value(QStringLiteral("name")).toString();
|
item.name = obj.value(QStringLiteral("name")).toString();
|
||||||
item.checked = obj.value(QStringLiteral("checked")).toBool();
|
item.checked = obj.value(QStringLiteral("checked")).toBool();
|
||||||
|
item.pinned = obj.value(QStringLiteral("pinned")).toBool();
|
||||||
item.createdAt = static_cast<qint64>(obj.value(QStringLiteral("createdAt")).toDouble());
|
item.createdAt = static_cast<qint64>(obj.value(QStringLiteral("createdAt")).toDouble());
|
||||||
if (item.id.isEmpty()) {
|
if (item.id.isEmpty()) {
|
||||||
item.id = newId();
|
item.id = newId();
|
||||||
@@ -144,6 +146,8 @@ QVariant StagingModel::data(const QModelIndex &index, int role) const
|
|||||||
return item.text;
|
return item.text;
|
||||||
case CheckedRole:
|
case CheckedRole:
|
||||||
return item.checked;
|
return item.checked;
|
||||||
|
case PinnedRole:
|
||||||
|
return item.pinned;
|
||||||
case ExistsRole:
|
case ExistsRole:
|
||||||
return item.exists();
|
return item.exists();
|
||||||
case IconRole:
|
case IconRole:
|
||||||
@@ -302,6 +306,20 @@ void StagingModel::setChecked(int row, bool checked)
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StagingModel::setPinned(int row, bool pinned)
|
||||||
|
{
|
||||||
|
if (row < 0 || row >= m_items.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_items[row].pinned == pinned) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_items[row].pinned = pinned;
|
||||||
|
const QModelIndex idx = index(row);
|
||||||
|
Q_EMIT dataChanged(idx, idx, {PinnedRole});
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
void StagingModel::clearAll()
|
void StagingModel::clearAll()
|
||||||
{
|
{
|
||||||
if (m_items.isEmpty()) {
|
if (m_items.isEmpty()) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public:
|
|||||||
UriRole,
|
UriRole,
|
||||||
TextRole,
|
TextRole,
|
||||||
CheckedRole,
|
CheckedRole,
|
||||||
|
PinnedRole,
|
||||||
ExistsRole,
|
ExistsRole,
|
||||||
IconRole,
|
IconRole,
|
||||||
};
|
};
|
||||||
@@ -49,6 +50,7 @@ public:
|
|||||||
void addImage(const QImage &image, int *addedRow = nullptr);
|
void addImage(const QImage &image, int *addedRow = nullptr);
|
||||||
|
|
||||||
void setChecked(int row, bool checked);
|
void setChecked(int row, bool checked);
|
||||||
|
void setPinned(int row, bool pinned);
|
||||||
void clearAll();
|
void clearAll();
|
||||||
|
|
||||||
const StagingItem *itemAt(int row) const;
|
const StagingItem *itemAt(int row) const;
|
||||||
|
|||||||
+116
-6
@@ -4,6 +4,7 @@
|
|||||||
#include "portalpanel.h"
|
#include "portalpanel.h"
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "portal/portalclient.h"
|
||||||
#include "staging/stagingitem.h"
|
#include "staging/stagingitem.h"
|
||||||
#include "staging/stagingmodel.h"
|
#include "staging/stagingmodel.h"
|
||||||
|
|
||||||
@@ -57,14 +58,31 @@ QRect closeRect(const QRect &row)
|
|||||||
return QRect(row.right() - 36, row.center().y() - 14, 28, 28);
|
return QRect(row.right() - 36, row.center().y() - 14, 28, 28);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QRect pinRect(const QRect &row)
|
||||||
|
{
|
||||||
|
return QRect(row.right() - 66, row.center().y() - 14, 28, 28);
|
||||||
|
}
|
||||||
|
|
||||||
QRect textRect(const QRect &row)
|
QRect textRect(const QRect &row)
|
||||||
{
|
{
|
||||||
const QRect icon = iconRect(row);
|
const QRect icon = iconRect(row);
|
||||||
const QRect close = closeRect(row);
|
const QRect pin = pinRect(row);
|
||||||
return QRect(icon.right() + 12, row.top() + 6, close.left() - icon.right() - 20,
|
return QRect(icon.right() + 12, row.top() + 6, pin.left() - icon.right() - 20,
|
||||||
row.height() - 12);
|
row.height() - 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void drawPushpin(QPainter *painter, const QRect &rect, const QColor &color, bool pinned)
|
||||||
|
{
|
||||||
|
painter->save();
|
||||||
|
painter->translate(rect.center());
|
||||||
|
painter->rotate(45.0);
|
||||||
|
painter->setPen(QPen(color, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
||||||
|
painter->setBrush(pinned ? QBrush(color) : Qt::NoBrush);
|
||||||
|
painter->drawRoundedRect(QRectF(-4.5, -9.0, 9.0, 7.0), 3.0, 3.0);
|
||||||
|
painter->drawLine(QPointF(0.0, -2.0), QPointF(0.0, 7.5));
|
||||||
|
painter->restore();
|
||||||
|
}
|
||||||
|
|
||||||
bool mimeSupported(const QMimeData *mime)
|
bool mimeSupported(const QMimeData *mime)
|
||||||
{
|
{
|
||||||
return mime && (mime->hasUrls() || mime->hasText() || mime->hasImage());
|
return mime && (mime->hasUrls() || mime->hasText() || mime->hasImage());
|
||||||
@@ -169,6 +187,13 @@ public:
|
|||||||
const QRect subRect(text.left(), text.top() + 24, text.width(), 16);
|
const QRect subRect(text.left(), text.top() + 24, text.width(), 16);
|
||||||
painter->drawText(subRect, Qt::AlignLeft | Qt::AlignVCenter, typeLabel(index));
|
painter->drawText(subRect, Qt::AlignLeft | Qt::AlignVCenter, typeLabel(index));
|
||||||
|
|
||||||
|
const bool pinned = index.data(StagingModel::PinnedRole).toBool();
|
||||||
|
if (pinned || hovered) {
|
||||||
|
drawPushpin(painter, pinRect(option.rect),
|
||||||
|
pinned ? QColor(120, 180, 255, 235) : QColor(255, 255, 255, 150),
|
||||||
|
pinned);
|
||||||
|
}
|
||||||
|
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
const QRect close = closeRect(option.rect).adjusted(9, 9, -9, -9);
|
const QRect close = closeRect(option.rect).adjusted(9, 9, -9, -9);
|
||||||
painter->setPen(QPen(QColor(255, 255, 255, 170), 1.7, Qt::SolidLine, Qt::RoundCap));
|
painter->setPen(QPen(QColor(255, 255, 255, 170), 1.7, Qt::SolidLine, Qt::RoundCap));
|
||||||
@@ -195,6 +220,12 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pinRect(option.rect).contains(pos)) {
|
||||||
|
auto *staging = static_cast<StagingModel *>(model);
|
||||||
|
staging->setPinned(index.row(), !index.data(StagingModel::PinnedRole).toBool());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (closeRect(option.rect).contains(pos)) {
|
if (closeRect(option.rect).contains(pos)) {
|
||||||
model->removeRow(index.row());
|
model->removeRow(index.row());
|
||||||
return true;
|
return true;
|
||||||
@@ -259,7 +290,8 @@ protected:
|
|||||||
m_pressIndex = indexAt(m_pressPos);
|
m_pressIndex = indexAt(m_pressPos);
|
||||||
if (m_pressIndex.isValid()) {
|
if (m_pressIndex.isValid()) {
|
||||||
const QRect row = visualRect(m_pressIndex);
|
const QRect row = visualRect(m_pressIndex);
|
||||||
if (checkRect(row).contains(m_pressPos) || closeRect(row).contains(m_pressPos)) {
|
if (checkRect(row).contains(m_pressPos) || closeRect(row).contains(m_pressPos) ||
|
||||||
|
pinRect(row).contains(m_pressPos)) {
|
||||||
m_pressIndex = QModelIndex();
|
m_pressIndex = QModelIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,6 +321,10 @@ protected:
|
|||||||
|
|
||||||
void dragEnterEvent(QDragEnterEvent *event) override
|
void dragEnterEvent(QDragEnterEvent *event) override
|
||||||
{
|
{
|
||||||
|
if (isOwnDrag(event->mimeData())) {
|
||||||
|
event->ignore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mimeSupported(event->mimeData())) {
|
if (mimeSupported(event->mimeData())) {
|
||||||
event->acceptProposedAction();
|
event->acceptProposedAction();
|
||||||
return;
|
return;
|
||||||
@@ -298,6 +334,10 @@ protected:
|
|||||||
|
|
||||||
void dragMoveEvent(QDragMoveEvent *event) override
|
void dragMoveEvent(QDragMoveEvent *event) override
|
||||||
{
|
{
|
||||||
|
if (isOwnDrag(event->mimeData())) {
|
||||||
|
event->ignore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mimeSupported(event->mimeData())) {
|
if (mimeSupported(event->mimeData())) {
|
||||||
event->acceptProposedAction();
|
event->acceptProposedAction();
|
||||||
updateHover(event->position().toPoint());
|
updateHover(event->position().toPoint());
|
||||||
@@ -308,7 +348,7 @@ protected:
|
|||||||
|
|
||||||
void dropEvent(QDropEvent *event) override
|
void dropEvent(QDropEvent *event) override
|
||||||
{
|
{
|
||||||
if (!mimeSupported(event->mimeData())) {
|
if (isOwnDrag(event->mimeData()) || !mimeSupported(event->mimeData())) {
|
||||||
event->ignore();
|
event->ignore();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -332,6 +372,11 @@ private:
|
|||||||
viewport()->update();
|
viewport()->update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool isOwnDrag(const QMimeData *mime)
|
||||||
|
{
|
||||||
|
return mime && mime->hasFormat(QStringLiteral("application/x-gxde-ai-see-staging"));
|
||||||
|
}
|
||||||
|
|
||||||
void startDragForIndex(const QModelIndex &index)
|
void startDragForIndex(const QModelIndex &index)
|
||||||
{
|
{
|
||||||
if (!index.isValid()) {
|
if (!index.isValid()) {
|
||||||
@@ -341,8 +386,11 @@ private:
|
|||||||
const int type = index.data(StagingModel::TypeRole).toInt();
|
const int type = index.data(StagingModel::TypeRole).toInt();
|
||||||
const QString uri = index.data(StagingModel::UriRole).toString();
|
const QString uri = index.data(StagingModel::UriRole).toString();
|
||||||
const QString text = index.data(StagingModel::TextRole).toString();
|
const QString text = index.data(StagingModel::TextRole).toString();
|
||||||
|
const QString id = index.data(StagingModel::IdRole).toString();
|
||||||
|
const bool pinned = index.data(StagingModel::PinnedRole).toBool();
|
||||||
|
|
||||||
auto *mime = new QMimeData;
|
auto *mime = new QMimeData;
|
||||||
|
mime->setData(QStringLiteral("application/x-gxde-ai-see-staging"), id.toUtf8());
|
||||||
if (type == StagingItem::File || type == StagingItem::Image) {
|
if (type == StagingItem::File || type == StagingItem::Image) {
|
||||||
if (!uri.isEmpty()) {
|
if (!uri.isEmpty()) {
|
||||||
mime->setUrls({QUrl::fromLocalFile(uri)});
|
mime->setUrls({QUrl::fromLocalFile(uri)});
|
||||||
@@ -364,7 +412,24 @@ private:
|
|||||||
if (!icon.isNull()) {
|
if (!icon.isNull()) {
|
||||||
drag->setPixmap(icon.pixmap(48, 48));
|
drag->setPixmap(icon.pixmap(48, 48));
|
||||||
}
|
}
|
||||||
drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction);
|
|
||||||
|
const Qt::DropAction action =
|
||||||
|
drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction);
|
||||||
|
|
||||||
|
/* 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());
|
||||||
|
if (staging) {
|
||||||
|
for (int row = 0; row < staging->count(); ++row) {
|
||||||
|
const StagingItem *item = staging->itemAt(row);
|
||||||
|
if (item && item->id == id) {
|
||||||
|
staging->removeRows(row, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StagingDelegate *m_delegate = nullptr;
|
StagingDelegate *m_delegate = nullptr;
|
||||||
@@ -388,6 +453,14 @@ public:
|
|||||||
setAcceptDrops(true);
|
setAcceptDrops(true);
|
||||||
setMinimumHeight(190);
|
setMinimumHeight(190);
|
||||||
|
|
||||||
|
m_portalTimer.setSingleShot(true);
|
||||||
|
m_portalTimer.setInterval(150);
|
||||||
|
connect(&m_portalTimer, &QTimer::timeout, this, [this]() {
|
||||||
|
if (m_active) {
|
||||||
|
Q_EMIT portalRequested();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
m_chatButton = new QPushButton(tr("问问小埃 →"), this);
|
m_chatButton = new QPushButton(tr("问问小埃 →"), this);
|
||||||
m_chatButton->setCursor(Qt::PointingHandCursor);
|
m_chatButton->setCursor(Qt::PointingHandCursor);
|
||||||
m_chatButton->setFlat(true);
|
m_chatButton->setFlat(true);
|
||||||
@@ -411,8 +484,19 @@ public:
|
|||||||
connect(&m_hintTimer, &QTimer::timeout, m_hintLabel, &QLabel::hide);
|
connect(&m_hintTimer, &QTimer::timeout, m_hintLabel, &QLabel::hide);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showMessage(const QString &message) { showHint(message); }
|
||||||
|
void setPortalActive(bool active)
|
||||||
|
{
|
||||||
|
m_portalActive = active;
|
||||||
|
if (!active) {
|
||||||
|
m_portalTimer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void chatRequested();
|
void chatRequested();
|
||||||
|
void portalRequested();
|
||||||
|
void portalCancelled();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent *event) override
|
void resizeEvent(QResizeEvent *event) override
|
||||||
@@ -486,6 +570,9 @@ protected:
|
|||||||
}
|
}
|
||||||
m_active = true;
|
m_active = true;
|
||||||
update();
|
update();
|
||||||
|
if (!m_portalActive) {
|
||||||
|
m_portalTimer.start();
|
||||||
|
}
|
||||||
event->acceptProposedAction();
|
event->acceptProposedAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,14 +588,20 @@ protected:
|
|||||||
Q_UNUSED(event)
|
Q_UNUSED(event)
|
||||||
m_active = false;
|
m_active = false;
|
||||||
update();
|
update();
|
||||||
|
m_portalTimer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void dropEvent(QDropEvent *event) override
|
void dropEvent(QDropEvent *event) override
|
||||||
{
|
{
|
||||||
m_active = false;
|
m_active = false;
|
||||||
|
m_portalTimer.stop();
|
||||||
update();
|
update();
|
||||||
|
if (!m_portalActive) {
|
||||||
|
showHint(tr("悬停在「传送门」上,再移动到目标窗口"));
|
||||||
|
event->ignore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
event->acceptProposedAction();
|
event->acceptProposedAction();
|
||||||
showHint(tr("传送门将在接入合成器后可用"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -522,8 +615,10 @@ private:
|
|||||||
QPushButton *m_chatButton = nullptr;
|
QPushButton *m_chatButton = nullptr;
|
||||||
QLabel *m_hintLabel = nullptr;
|
QLabel *m_hintLabel = nullptr;
|
||||||
QTimer m_hintTimer;
|
QTimer m_hintTimer;
|
||||||
|
QTimer m_portalTimer;
|
||||||
bool m_active = false;
|
bool m_active = false;
|
||||||
bool m_hover = false;
|
bool m_hover = false;
|
||||||
|
bool m_portalActive = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------- */
|
||||||
@@ -618,6 +713,21 @@ void PortalPanel::setupUi()
|
|||||||
m_zone->setFixedWidth(kZoneWidth);
|
m_zone->setFixedWidth(kZoneWidth);
|
||||||
outer->addWidget(m_zone);
|
outer->addWidget(m_zone);
|
||||||
connect(m_zone, &PortalZone::chatRequested, this, &PortalPanel::openChatRequested);
|
connect(m_zone, &PortalZone::chatRequested, this, &PortalPanel::openChatRequested);
|
||||||
|
connect(m_zone, &PortalZone::portalRequested, this, [this]() {
|
||||||
|
PortalClient *portal = PortalClient::instance();
|
||||||
|
if (portal->beginPortal()) {
|
||||||
|
m_zone->setPortalActive(true);
|
||||||
|
m_zone->showMessage(tr("移动到目标窗口上松手"));
|
||||||
|
} else {
|
||||||
|
m_zone->showMessage(portal->available() ? tr("当前无已打开的窗口")
|
||||||
|
: tr("当前环境不支持传送门"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_zone, &PortalZone::portalCancelled, this, []() {
|
||||||
|
PortalClient::instance()->cancelPortal();
|
||||||
|
});
|
||||||
|
connect(PortalClient::instance(), &PortalClient::dragEnded, this,
|
||||||
|
[this]() { m_zone->setPortalActive(false); });
|
||||||
|
|
||||||
auto *right = new QVBoxLayout;
|
auto *right = new QVBoxLayout;
|
||||||
right->setContentsMargins(0, 0, 0, 0);
|
right->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user