Files
16c3f841b5 chore(release): prepare 2.0.0-Beta.2 build 20260924
Integrate the reviewed 2.0 PRs and local fixes from 03136853.
Preserve the verified subtitle behavior and model/runtime corrections.
Keep the numeric Beta tag compatible with released clients while exposing
the date as application build metadata.

Co-authored-by: DepengWang <2818245+DepengWang@users.noreply.github.com>
Co-authored-by: HKLHaoBin <we3q@qq.com>
Co-authored-by: Nahida <1139500183@qq.com>
Co-authored-by: TRIP <1933142963@qq.com>
Co-authored-by: chyuwei <weichangyuwcy@gmail.com>
Co-authored-by: leonoxo <leonoxo@gmail.com>
Co-authored-by: tayir <arnoldincredible@126.com>
2026-09-24 17:28:24 +08:00

132 lines
7.3 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
#include <QApplication>
#include <QAction>
#include <QDBusConnection>
#include <QDBusContext>
#include <QDBusMessage>
#include <QDBusInterface>
#include <QDBusReply>
#include <QDBusObjectPath>
#include <QDateTime>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QKeySequence>
#include <QQueue>
#include <QTimer>
#include <QUuid>
#include <QFileInfo>
#include <QDir>
#include <KGlobalAccel>
class Bridge : public QObject, protected QDBusContext {
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.openless.Desktop1")
QMap<QString,QAction*> actions;
QMap<QString,QJsonObject> bindings;
QMap<QString,QDBusMessage> acknowledgements;
QQueue<QJsonObject> commands;
QDBusMessage waiting;
quint64 waiterGeneration=0;
QString snapshot="{}";
qint64 snapshotTime=0;
QString releasePath;
static QString encode(const QJsonObject &value) { return QString::fromUtf8(QJsonDocument(value).toJson(QJsonDocument::Compact)); }
void replyCommand() {
if (waiting.type()!=QDBusMessage::MethodCallMessage || commands.isEmpty()) return;
QDBusConnection::sessionBus().send(waiting.createReply(encode(commands.dequeue())));
waiting=QDBusMessage(); ++waiterGeneration;
}
bool queue(QJsonObject command) {
if (!calledFromDBus() || QDateTime::currentMSecsSinceEpoch()-snapshotTime>5000) return false;
const QString id=QUuid::createUuid().toString(QUuid::WithoutBraces);
command["id"]=id;
setDelayedReply(true); acknowledgements[id]=message(); commands.enqueue(command); replyCommand();
QTimer::singleShot(1500,this,[this,id](){ Ack(id,false); });
return false; // delayed reply contains the actual KWin acknowledgement
}
static QKeySequence key(const QJsonObject &binding) {
QString accelerator=binding["accelerator"].toString();
accelerator.replace("<Control>","Ctrl+").replace("<Shift>","Shift+").replace("<Alt>","Alt+").replace("<Super>","Meta+");
accelerator.replace("Control_R","Ctrl").replace("Control_L","Ctrl").replace("Alt_R","Alt").replace("Alt_L","Alt");
return QKeySequence(accelerator);
}
public:
Bridge() = default;
public slots:
void Released(const QString &name,qlonglong) {const auto binding=bindings.value(name);if(!binding.isEmpty())emit Hotkey(binding["action"].toString(),binding["symbol"].toInt(),binding["states"].toInt(),false);}
uint Version() const { return 1; }
QString Snapshot() const { return QDateTime::currentMSecsSinceEpoch()-snapshotTime<5000?snapshot:"{}"; }
void Update(const QString &json) {
const auto document=QJsonDocument::fromJson(json.toUtf8());
if (document.isObject() && document.object()["version"].toInt()==1) { snapshot=json; snapshotTime=QDateTime::currentMSecsSinceEpoch(); }
}
QString Bind(const QString &json) {
const auto document=QJsonDocument::fromJson(json.toUtf8());
if (!document.isArray() || document.array().size()>64) return "Invalid desktop binding document";
QMap<QString,QJsonObject> next;
for (const auto value:document.array()) {
const auto binding=value.toObject(); const auto sequence=key(binding);
const QString id=binding["action"].toString()+":"+QString::number(binding["symbol"].toInt())+":"+QString::number(binding["states"].toInt());
bool owned=false; for (auto action:actions) if (KGlobalAccel::self()->shortcut(action).contains(sequence)) owned=true;
if (sequence.isEmpty() || (!owned&&!KGlobalAccel::isGlobalShortcutAvailable(sequence,"openless-desktop-bridge"))) return "Shortcut conflict: "+binding["accelerator"].toString();
next[id]=binding;
}
const auto previous=bindings;
for (auto action:actions) KGlobalAccel::self()->removeAllShortcuts(action);
bool success=true;
for (auto it=next.begin();it!=next.end();++it) {
auto action=actions.value(it.key());
if (!action) {
action=new QAction(it.key(),this);action->setObjectName(it.key());actions[it.key()]=action;
connect(action,&QAction::triggered,this,[this,action](){const auto binding=bindings.value(action->objectName());if(!binding.isEmpty())emit Hotkey(binding["action"].toString(),binding["symbol"].toInt(),binding["states"].toInt(),true);});
}
if (!KGlobalAccel::self()->setShortcut(action,{key(it.value())},KGlobalAccel::NoAutoloading)) { success=false;break; }
}
if (!success) {
for (auto action:actions) KGlobalAccel::self()->removeAllShortcuts(action);
for (auto it=previous.begin();it!=previous.end();++it) KGlobalAccel::self()->setShortcut(actions[it.key()],{key(it.value())},KGlobalAccel::NoAutoloading);
return "Shortcut registration failed; previous bindings restored";
}
bindings=next;
if(releasePath.isEmpty()) {
QDBusInterface service("org.kde.kglobalaccel","/kglobalaccel","org.kde.KGlobalAccel");
QDBusReply<QDBusObjectPath> component=service.call("getComponent","openless-desktop-bridge");
if(component.isValid()) {
releasePath=component.value().path();
QDBusConnection::sessionBus().connect("org.kde.kglobalaccel",releasePath,"org.kde.kglobalaccel.Component","globalShortcutReleased",this,SLOT(Released(QString,qlonglong)));
}
}
return {};
}
bool Restore(const QString &target) { return queue({{"op","restore"},{"target",target}}); }
bool Place(const QString &title,int x,int y) { if(!title.startsWith("OpenLess "))return false;return queue({{"op","place"},{"title",title},{"x",x},{"y",y}}); }
QString Next() {
if (!commands.isEmpty()) return encode(commands.dequeue());
if (waiting.type()==QDBusMessage::MethodCallMessage) return "{}";
setDelayedReply(true);waiting=message();const auto generation=++waiterGeneration;
QTimer::singleShot(1000,this,[this,generation](){if(waiterGeneration==generation&&waiting.type()==QDBusMessage::MethodCallMessage){QDBusConnection::sessionBus().send(waiting.createReply(QString("{}")));waiting=QDBusMessage();}});
return {};
}
void Ack(const QString &id,bool success) {
if (!acknowledgements.contains(id)) return;
QDBusConnection::sessionBus().send(acknowledgements.take(id).createReply(success));
}
signals:
void Hotkey(const QString &action,uint symbol,uint states,bool pressed);
};
int main(int argc,char **argv) {
// This helper only uses D-Bus; a private offscreen platform plugin avoids
// loading Qt plugins from an incompatible Plasma installation.
qputenv("QT_QPA_PLATFORM", "offscreen");
const auto plugins=QFileInfo(QString::fromLocal8Bit(argv[0])).absoluteDir().filePath("plugins/platforms");
if(QDir(plugins).exists())qputenv("QT_QPA_PLATFORM_PLUGIN_PATH",plugins.toUtf8());
QApplication app(argc,argv);app.setApplicationName("openless-desktop-bridge");app.setQuitOnLastWindowClosed(false);
Bridge bridge;
auto bus=QDBusConnection::sessionBus();
if(!bus.registerService("org.openless.Desktop1") || !bus.registerObject("/org/openless/Desktop1",&bridge,QDBusConnection::ExportAllSlots|QDBusConnection::ExportAllSignals))return 1;
return app.exec();
}
#include "bridge.moc"