mirror of
https://github.com/clementine-player/Clementine
synced 2025-01-31 11:35:24 +01:00
Initial KGlobalAccel support
This commit is contained in:
parent
440ac6dda2
commit
24a571769a
@ -82,6 +82,7 @@ set(SOURCES
|
||||
core/globalshortcutbackend.cpp
|
||||
core/globalshortcuts.cpp
|
||||
core/gnomeglobalshortcutbackend.cpp
|
||||
core/kglobalaccelglobalshortcutbackend.cpp
|
||||
core/mergedproxymodel.cpp
|
||||
core/metatypes.cpp
|
||||
core/multisortfilterproxy.cpp
|
||||
@ -413,6 +414,7 @@ set(HEADERS
|
||||
core/globalshortcuts.h
|
||||
core/globalshortcutbackend.h
|
||||
core/gnomeglobalshortcutbackend.h
|
||||
core/kglobalaccelglobalshortcutbackend.h
|
||||
core/mergedproxymodel.h
|
||||
core/mimedata.h
|
||||
core/network.h
|
||||
@ -937,6 +939,15 @@ if(UNIX AND HAVE_DBUS)
|
||||
dbus/org.gnome.SettingsDaemon.MediaKeys.xml
|
||||
dbus/gnomesettingsdaemon)
|
||||
|
||||
# org.kde.KGlobalAccel interfaces
|
||||
# these are taken from the KGlobalAccel sources (LGPL 2.1)
|
||||
qt5_add_dbus_interface(SOURCES
|
||||
dbus/org.kde.KGlobalAccel.xml
|
||||
dbus/kglobalaccel)
|
||||
qt5_add_dbus_interface(SOURCES
|
||||
dbus/org.kde.kglobalaccel.Component.xml
|
||||
dbus/kglobalaccelcomponent)
|
||||
|
||||
# org.freedesktop.Avahi.Server interface
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include "config.h"
|
||||
#include "globalshortcuts.h"
|
||||
#include "gnomeglobalshortcutbackend.h"
|
||||
#include "kglobalaccelglobalshortcutbackend.h"
|
||||
#include "macglobalshortcutbackend.h"
|
||||
#include "qxtglobalshortcutbackend.h"
|
||||
|
||||
@ -41,7 +42,8 @@ GlobalShortcuts::GlobalShortcuts(QWidget* parent)
|
||||
: QWidget(parent),
|
||||
gnome_backend_(nullptr),
|
||||
system_backend_(nullptr),
|
||||
use_gnome_(false) {
|
||||
use_gnome_(false),
|
||||
have_kglobalaccel_(KGlobalAccelShortcutBackend::isKGlobalAccelAvailable()) {
|
||||
settings_.beginGroup(kSettingsGroup);
|
||||
|
||||
// Create actions
|
||||
@ -91,6 +93,7 @@ GlobalShortcuts::GlobalShortcuts(QWidget* parent)
|
||||
|
||||
// Create backends - these do the actual shortcut registration
|
||||
gnome_backend_ = new GnomeGlobalShortcutBackend(this);
|
||||
kglobalaccel_backend_ = new KGlobalAccelShortcutBackend(this);
|
||||
|
||||
#ifndef Q_OS_DARWIN
|
||||
system_backend_ = new QxtGlobalShortcutBackend(this);
|
||||
@ -158,10 +161,12 @@ void GlobalShortcuts::ReloadSettings() {
|
||||
|
||||
void GlobalShortcuts::Unregister() {
|
||||
if (gnome_backend_->is_active()) gnome_backend_->Unregister();
|
||||
if (kglobalaccel_backend_->is_active()) kglobalaccel_backend_->Unregister();
|
||||
if (system_backend_->is_active()) system_backend_->Unregister();
|
||||
}
|
||||
|
||||
void GlobalShortcuts::Register() {
|
||||
if(have_kglobalaccel_ && kglobalaccel_backend_->Register()) return;
|
||||
if (use_gnome_ && gnome_backend_->Register()) return;
|
||||
system_backend_->Register();
|
||||
}
|
||||
|
@ -93,12 +93,14 @@ signals:
|
||||
|
||||
private:
|
||||
GlobalShortcutBackend* gnome_backend_;
|
||||
GlobalShortcutBackend* kglobalaccel_backend_;
|
||||
GlobalShortcutBackend* system_backend_;
|
||||
|
||||
QMap<QString, Shortcut> shortcuts_;
|
||||
QSettings settings_;
|
||||
|
||||
bool use_gnome_;
|
||||
bool have_kglobalaccel_;
|
||||
};
|
||||
|
||||
#endif // CORE_GLOBALSHORTCUTS_H_
|
||||
|
284
src/core/kglobalaccelglobalshortcutbackend.cpp
Normal file
284
src/core/kglobalaccelglobalshortcutbackend.cpp
Normal file
@ -0,0 +1,284 @@
|
||||
#include "config.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "globalshortcuts.h"
|
||||
#include "kglobalaccelglobalshortcutbackend.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QGuiApplication>
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
|
||||
#include <dbus/kglobalaccel.h>
|
||||
#include <dbus/kglobalaccelcomponent.h>
|
||||
|
||||
#endif
|
||||
|
||||
// Most of this file is based on the KGlobalAccel sources
|
||||
// (https://phabricator.kde.org/source/kglobalaccel)
|
||||
|
||||
|
||||
namespace {
|
||||
QString compDisplayName() {
|
||||
if (!QGuiApplication::applicationDisplayName().isEmpty()) {
|
||||
return QGuiApplication::applicationDisplayName();
|
||||
}
|
||||
return QCoreApplication::applicationName();
|
||||
}
|
||||
|
||||
QString compUniqueName() {
|
||||
return QCoreApplication::applicationName();
|
||||
}
|
||||
|
||||
const QString &id_ActionUnique(const QStringList &id) {
|
||||
return id.at(1);
|
||||
}
|
||||
|
||||
bool isCorrectMediaKeyShortcut(const GlobalShortcuts::Shortcut& shortcut) {
|
||||
if(shortcut.id == QStringLiteral("play_pause")) {
|
||||
return shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaPlay);
|
||||
} else if(shortcut.id == QStringLiteral("stop")) {
|
||||
return shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaStop);
|
||||
} else if(shortcut.id == QStringLiteral("next_track")) {
|
||||
return shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaNext);
|
||||
} else if(shortcut.id == QStringLiteral("prev_track")) {
|
||||
return shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaPrevious);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
|
||||
KGlobalAccelShortcutBackend::KGlobalAccelShortcutBackend(
|
||||
GlobalShortcuts *parent)
|
||||
: GlobalShortcutBackend(parent),
|
||||
iface_(nullptr),
|
||||
component_(nullptr),
|
||||
nameToAction_() {}
|
||||
|
||||
#else // HAVE_DBUS
|
||||
KGlobalAccelShortcutBackend::KGlobalAccelShortcutBackend(GlobalShortcuts *parent)
|
||||
: GlobalShortcutBackend(parent) {}
|
||||
#endif // HAVE_DBUS
|
||||
|
||||
bool KGlobalAccelShortcutBackend::isKGlobalAccelAvailable() {
|
||||
#ifdef HAVE_DBUS
|
||||
return QDBusConnection::sessionBus().interface()->isServiceRegistered(
|
||||
Service);
|
||||
#else // HAVE_DBUS
|
||||
return false;
|
||||
#endif // HAVE_DBUS
|
||||
}
|
||||
|
||||
bool KGlobalAccelShortcutBackend::DoRegister() {
|
||||
#ifdef HAVE_DBUS
|
||||
qLog(Debug) << "Registering shortcuts";
|
||||
|
||||
if (!acquireInterface())
|
||||
return false;
|
||||
|
||||
bool complete = true;
|
||||
for (const GlobalShortcuts::Shortcut &shortcut : manager_->shortcuts().values()) {
|
||||
if(shortcut.action->shortcut().isEmpty())
|
||||
continue;
|
||||
|
||||
if (!registerShortcut(shortcut))
|
||||
complete = false;
|
||||
}
|
||||
|
||||
if (!acquireComponent())
|
||||
return false;
|
||||
|
||||
QObject::connect(component_,
|
||||
&OrgKdeKglobalaccelComponentInterface::globalShortcutPressed,
|
||||
this,
|
||||
&KGlobalAccelShortcutBackend::onShortcutPressed);
|
||||
|
||||
return complete;
|
||||
#else // HAVE_DBUS
|
||||
qLog(Warning) << "dbus not available";
|
||||
return false;
|
||||
#endif // HAVE_DBUS
|
||||
}
|
||||
|
||||
void KGlobalAccelShortcutBackend::DoUnregister() {
|
||||
if (!acquireInterface())
|
||||
return;
|
||||
|
||||
if (!acquireComponent())
|
||||
return;
|
||||
|
||||
for (const GlobalShortcuts::Shortcut &shortcut : manager_->shortcuts())
|
||||
unregisterAction(shortcut.id, shortcut.action);
|
||||
}
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
|
||||
const char *KGlobalAccelShortcutBackend::Service = "org.kde.kglobalaccel";
|
||||
const char *KGlobalAccelShortcutBackend::Path = "/kglobalaccel";
|
||||
|
||||
bool KGlobalAccelShortcutBackend::acquireComponent() {
|
||||
Q_ASSERT(iface_ && iface_->isValid());
|
||||
|
||||
QString componentName = compUniqueName();
|
||||
QDBusReply<QDBusObjectPath> reply = iface_->getComponent(compUniqueName());
|
||||
if (!reply.isValid()) {
|
||||
if (reply.error().name() ==
|
||||
QLatin1String("org.kde.kglobalaccel.NoSuchComponent"))
|
||||
return false;
|
||||
|
||||
qLog(Warning) << "Failed to get DBus path for KGlobalAccel component";
|
||||
return false;
|
||||
}
|
||||
|
||||
component_ = new org::kde::kglobalaccel::Component(Service,
|
||||
reply.value().path(),
|
||||
QDBusConnection::sessionBus(),
|
||||
iface_);
|
||||
|
||||
if (!component_->isValid()) {
|
||||
qLog(Warning) << "Failed to get KGlobalAccel component:"
|
||||
<< QDBusConnection::sessionBus().lastError();
|
||||
delete component_;
|
||||
component_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool KGlobalAccelShortcutBackend::acquireInterface() {
|
||||
if (iface_ && iface_->isValid())
|
||||
return true;
|
||||
|
||||
if (isKGlobalAccelAvailable()) {
|
||||
iface_ = new OrgKdeKGlobalAccelInterface(Service, Path,
|
||||
QDBusConnection::sessionBus(),
|
||||
this);
|
||||
}
|
||||
|
||||
if (iface_ && iface_->isValid())
|
||||
return true;
|
||||
|
||||
if (!iface_)
|
||||
qLog(Warning) << "KGlobalAccel daemon not registered";
|
||||
else if (!iface_->isValid())
|
||||
qLog(Warning) << "KGlobalAccel daemon is not valid";
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList KGlobalAccelShortcutBackend::id(const QString &name,
|
||||
const QAction *action) {
|
||||
Q_ASSERT(action);
|
||||
|
||||
QStringList ret;
|
||||
ret << compUniqueName();
|
||||
ret << name;
|
||||
ret << compDisplayName();
|
||||
ret << action->text().replace(QLatin1Char('&'), QStringLiteral(""));
|
||||
if (ret.back().isEmpty())
|
||||
ret.back() = name;
|
||||
return ret;
|
||||
}
|
||||
|
||||
QList<int> KGlobalAccelShortcutBackend::intList(
|
||||
const QList<QKeySequence> &seq) {
|
||||
QList<int> ret;
|
||||
for (const QKeySequence &sequence : seq) {
|
||||
ret.append(sequence[0]);
|
||||
}
|
||||
while (!ret.isEmpty() && ret.last() == 0) {
|
||||
ret.removeLast();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool KGlobalAccelShortcutBackend::registerAction(const QString &name,
|
||||
QAction *action,
|
||||
QStringList &actionId) {
|
||||
Q_ASSERT(action);
|
||||
|
||||
if (name.isEmpty() && (action->objectName().isEmpty() ||
|
||||
action->objectName().startsWith(
|
||||
QLatin1String("unnamed-")))) {
|
||||
qLog(Warning) << "Cannot register shortcut for unnamed action";
|
||||
return false;
|
||||
}
|
||||
|
||||
actionId = id(name, action);
|
||||
nameToAction_.insertMulti(id_ActionUnique(actionId), action);
|
||||
iface_->doRegister(actionId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool KGlobalAccelShortcutBackend::registerShortcut(const GlobalShortcuts::Shortcut &shortcut) {
|
||||
QStringList actionId;
|
||||
if (!registerAction(shortcut.id, shortcut.action, actionId))
|
||||
return false;
|
||||
|
||||
QList<QKeySequence> activeShortcut;
|
||||
activeShortcut << shortcut.action->shortcut();
|
||||
|
||||
const QList<int> result = iface_->setShortcut(actionId,
|
||||
intList(activeShortcut),
|
||||
SetShortcutFlag::SetPresent);
|
||||
|
||||
const QList<QKeySequence> resultSequence = shortcutList(result);
|
||||
if (resultSequence != activeShortcut) {
|
||||
qLog(Warning) << "Tried setting global shortcut" << activeShortcut
|
||||
<< "but KGlobalAccel returned" << resultSequence;
|
||||
|
||||
if (!resultSequence.isEmpty()) {
|
||||
if(!isCorrectMediaKeyShortcut(shortcut)) {
|
||||
// there is some conflict with our preferred shortcut so we use
|
||||
// the new shortcut that kglobalaccel suggests
|
||||
shortcut.action->setShortcut(resultSequence[0]);
|
||||
} else {
|
||||
// media keys are properly handled by plasma through the
|
||||
// media player plasmoid so we don't do anything in those cases
|
||||
qLog(Debug) << "Leaving media key shortcuts unchanged";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<QKeySequence> KGlobalAccelShortcutBackend::shortcutList(
|
||||
const QList<int> &seq) {
|
||||
QList<QKeySequence> ret;
|
||||
for (int i : seq) {
|
||||
ret.append(i);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void KGlobalAccelShortcutBackend::unregisterAction(const QString &name,
|
||||
QAction *action) {
|
||||
Q_ASSERT(action);
|
||||
|
||||
QStringList actionId = id(name, action);
|
||||
nameToAction_.remove(id_ActionUnique(actionId), action);
|
||||
iface_->unRegister(actionId);
|
||||
}
|
||||
|
||||
void KGlobalAccelShortcutBackend::onShortcutPressed(
|
||||
const QString &componentUnique,
|
||||
const QString &actionUnique,
|
||||
qlonglong timestamp) const {
|
||||
QAction *action = nullptr;
|
||||
const QList<QAction *> candidates = nameToAction_.values(actionUnique);
|
||||
for (QAction *a : candidates) {
|
||||
if (compUniqueName() == componentUnique) {
|
||||
action = a;
|
||||
}
|
||||
}
|
||||
|
||||
if (action && action->isEnabled())
|
||||
action->trigger();
|
||||
}
|
||||
|
||||
#endif // HAVE_DBUS
|
71
src/core/kglobalaccelglobalshortcutbackend.h
Normal file
71
src/core/kglobalaccelglobalshortcutbackend.h
Normal file
@ -0,0 +1,71 @@
|
||||
#ifndef CORE_KGLOBALACCELGLOBALSHORTCUTBACKEND_H_
|
||||
#define CORE_KGLOBALACCELGLOBALSHORTCUTBACKEND_H_
|
||||
|
||||
#include "globalshortcutbackend.h"
|
||||
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
class QAction;
|
||||
|
||||
class OrgKdeKGlobalAccelInterface;
|
||||
|
||||
class OrgKdeKglobalaccelComponentInterface;
|
||||
|
||||
|
||||
class KGlobalAccelShortcutBackend : public GlobalShortcutBackend {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit KGlobalAccelShortcutBackend(GlobalShortcuts *parent);
|
||||
|
||||
|
||||
static bool isKGlobalAccelAvailable();
|
||||
|
||||
protected:
|
||||
bool DoRegister() override;
|
||||
|
||||
void DoUnregister() override;
|
||||
|
||||
private:
|
||||
#ifdef HAVE_DBUS
|
||||
enum SetShortcutFlag {
|
||||
SetPresent = 2,
|
||||
NoAutoloading = 4,
|
||||
IsDefault = 8
|
||||
};
|
||||
|
||||
bool acquireComponent();
|
||||
|
||||
bool acquireInterface();
|
||||
|
||||
static QStringList id(const QString &name, const QAction *action);
|
||||
|
||||
static QList<int> intList(const QList<QKeySequence> &seq);
|
||||
|
||||
bool registerAction(const QString &name, QAction *action,
|
||||
QStringList &actionId);
|
||||
|
||||
bool registerShortcut(const GlobalShortcuts::Shortcut &shortcut);
|
||||
|
||||
static QList<QKeySequence> shortcutList(const QList<int> &seq);
|
||||
|
||||
void unregisterAction(const QString &name, QAction *action);
|
||||
|
||||
private slots:
|
||||
|
||||
void onShortcutPressed(const QString &componentUnique,
|
||||
const QString &actionUnique,
|
||||
qlonglong timestamp) const;
|
||||
|
||||
private:
|
||||
static const char *Service;
|
||||
static const char *Path;
|
||||
|
||||
OrgKdeKGlobalAccelInterface *iface_;
|
||||
OrgKdeKglobalaccelComponentInterface *component_;
|
||||
QMultiHash<QString, QAction *> nameToAction_;
|
||||
#endif // HAVE_DBUS
|
||||
};
|
||||
|
||||
#endif //CORE_KGLOBALACCELGLOBALSHORTCUTBACKEND_H_
|
72
src/dbus/org.kde.KGlobalAccel.xml
Normal file
72
src/dbus/org.kde.KGlobalAccel.xml
Normal file
@ -0,0 +1,72 @@
|
||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<node>
|
||||
<interface name="org.kde.KGlobalAccel">
|
||||
<signal name="yourShortcutGotChanged">
|
||||
<arg name="actionId" type="as" direction="out"/>
|
||||
<arg name="newKeys" type="ai" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QList<int>"/>
|
||||
</signal>
|
||||
<method name="allComponents">
|
||||
<arg type="ao" direction="out"/>
|
||||
</method>
|
||||
<method name="allMainComponents">
|
||||
<arg type="aas" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList<QStringList>"/>
|
||||
</method>
|
||||
<method name="allActionsForComponent">
|
||||
<arg type="aas" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList<QStringList>"/>
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="action">
|
||||
<arg type="as" direction="out"/>
|
||||
<arg name="key" type="i" direction="in"/>
|
||||
</method>
|
||||
<method name="getComponent">
|
||||
<arg type="o" direction="out"/>
|
||||
<arg name="componentUnique" type="s" direction="in"/>
|
||||
</method>
|
||||
<method name="shortcut">
|
||||
<arg type="ai" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList<int>"/>
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="defaultShortcut">
|
||||
<arg type="ai" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList<int>"/>
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="setShortcut">
|
||||
<arg type="ai" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList<int>"/>
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
<arg name="keys" type="ai" direction="in"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QList<int>"/>
|
||||
<arg name="flags" type="u" direction="in"/>
|
||||
</method>
|
||||
<method name="setForeignShortcut">
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
<arg name="keys" type="ai" direction="in"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QList<int>"/>
|
||||
</method>
|
||||
<method name="setInactive">
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="doRegister">
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="unRegister">
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="activateGlobalShortcutContext">
|
||||
<arg name="component" type="s" direction="in"/>
|
||||
<arg name="context" type="s" direction="in"/>
|
||||
</method>
|
||||
<method name="isGlobalShortcutAvailable">
|
||||
<arg type="b" direction="out"/>
|
||||
<arg name="key" type="i" direction="in"/>
|
||||
<arg name="component" type="s" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
32
src/dbus/org.kde.kglobalaccel.Component.xml
Normal file
32
src/dbus/org.kde.kglobalaccel.Component.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<node>
|
||||
<interface name="org.kde.kglobalaccel.Component">
|
||||
<property name="friendlyName" type="s" access="read"/>
|
||||
<property name="uniqueName" type="s" access="read"/>
|
||||
<signal name="globalShortcutPressed">
|
||||
<arg name="componentUnique" type="s" direction="out"/>
|
||||
<arg name="actionUnique" type="s" direction="out"/>
|
||||
<arg name="timestamp" type="x" direction="out"/>
|
||||
</signal>
|
||||
<method name="cleanUp">
|
||||
<arg type="b" direction="out"/>
|
||||
</method>
|
||||
<method name="isActive">
|
||||
<arg type="b" direction="out"/>
|
||||
</method>
|
||||
<method name="shortcutNames">
|
||||
<arg type="as" direction="out"/>
|
||||
<arg name="context" type="s" direction="in"/>
|
||||
</method>
|
||||
<method name="shortcutNames">
|
||||
<arg type="as" direction="out"/>
|
||||
</method>
|
||||
<method name="getShortcutContexts">
|
||||
<arg type="as" direction="out"/>
|
||||
</method>
|
||||
<method name="invokeShortcut">
|
||||
<arg name="actionName" type="s" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
Loading…
x
Reference in New Issue
Block a user