mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-08-01 09:18:07 +00:00
Port to the new org.kde.KWin.ScreenShots2 interface
Fixes https://invent.kde.org/plasma/plasma-phone-components/-/issues/85
This commit is contained in:
parent
393047e905
commit
989d6fdc1d
4 changed files with 282 additions and 77 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
qt_add_dbus_interfaces(DBUS_SRCS dbus/org.kde.KWin.Screenshot.xml
|
qt_add_dbus_interfaces(DBUS_SRCS dbus/org.kde.KWin.ScreenShot2.xml
|
||||||
dbus/org.kde.KScreen.xml)
|
dbus/org.kde.KScreen.xml)
|
||||||
|
|
||||||
set(phonepanel_SRCS
|
set(phonepanel_SRCS
|
||||||
|
|
|
||||||
184
containments/panel/dbus/org.kde.KWin.ScreenShot2.xml
Normal file
184
containments/panel/dbus/org.kde.KWin.ScreenShot2.xml
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||||
|
-->
|
||||||
|
<node name="/org/kde/KWin/ScreenShot2">
|
||||||
|
<!--
|
||||||
|
org.kde.KWin.ScreenShot2:
|
||||||
|
@short_description: Screen shot interface
|
||||||
|
|
||||||
|
This interface provides a way to request a screenshot of a rectangular area,
|
||||||
|
a screen, or a window.
|
||||||
|
-->
|
||||||
|
<interface name="org.kde.KWin.ScreenShot2">
|
||||||
|
<!--
|
||||||
|
CaptureWindow:
|
||||||
|
@handle: The unique handle that identified the window
|
||||||
|
@options: Optional vardict with screenshot options
|
||||||
|
@pipe: The pipe file descriptor where the screenshot will be written
|
||||||
|
|
||||||
|
Take a screenshot of the specified window. The application that
|
||||||
|
requests the screenshot must have the org.kde.KWin.ScreenShot2
|
||||||
|
interface listed in the X-KDE-DBUS-Restricted-Interfaces desktop
|
||||||
|
file entry.
|
||||||
|
|
||||||
|
Available @options include:
|
||||||
|
|
||||||
|
* "include-cursor" (b): Whether the cursor should be included.
|
||||||
|
Defaults to false
|
||||||
|
* "include-decoration" (b): Whether the decoration should be included.
|
||||||
|
Defaults to false
|
||||||
|
* "native-resolution" (b): Whether the screenshot should be in
|
||||||
|
native size. Defaults to false
|
||||||
|
|
||||||
|
The following results get returned via the @results vardict:
|
||||||
|
|
||||||
|
* "type" (s): The type of the image written to the pipe. Currently,
|
||||||
|
the only supported type is "raw"
|
||||||
|
* "width" (u): The width of the image. Available only if the image
|
||||||
|
type is "raw"
|
||||||
|
* "height" (u): The height of the image. Available only if the image
|
||||||
|
type is "raw"
|
||||||
|
* "stride" (u): The number of bytes per row. Available only if the
|
||||||
|
image type is "raw"
|
||||||
|
* "format" (u): The image format, as defined in QImage::Format.
|
||||||
|
Available only if the image type is "raw"
|
||||||
|
-->
|
||||||
|
<method name="CaptureWindow">
|
||||||
|
<arg name="handle" type="s" direction="in" />
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap" />
|
||||||
|
<arg name="options" type="a{sv}" direction="in" />
|
||||||
|
<arg name="pipe" type="h" direction="in" />
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
|
||||||
|
<arg name="results" type="a{sv}" direction="out" />
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
CaptureArea:
|
||||||
|
@x: The x coordinate of the upper left corner of the area
|
||||||
|
@y: The y coordinate of the upper left corner of the area
|
||||||
|
@width: The width of the screenshot area
|
||||||
|
@height: The height of the screenshot area
|
||||||
|
@options: Optional vardict with screenshot options
|
||||||
|
@pipe: The pipe file descriptor where the screenshot will be written
|
||||||
|
|
||||||
|
Take a screenshot of the specified rectangular area. The application
|
||||||
|
that requests the screenshot must have the org.kde.KWin.ScreenShot2
|
||||||
|
interface listed in the X-KDE-DBUS-Restricted-Interfaces desktop file
|
||||||
|
entry.
|
||||||
|
|
||||||
|
Available @options include:
|
||||||
|
|
||||||
|
* "include-cursor" (b): Whether the cursor should be included.
|
||||||
|
Defaults to false
|
||||||
|
* "native-resolution" (b): Whether the screenshot should be in
|
||||||
|
native size. Defaults to false
|
||||||
|
|
||||||
|
The following results get returned via the @results vardict:
|
||||||
|
|
||||||
|
* "type" (s): The type of the image written to the pipe. Currently,
|
||||||
|
the only supported type is "raw"
|
||||||
|
* "width" (u): The width of the image. Available only if the image
|
||||||
|
type is "raw"
|
||||||
|
* "height" (u): The height of the image. Available only if the image
|
||||||
|
type is "raw"
|
||||||
|
* "stride" (u): The number of bytes per row. Available only if the
|
||||||
|
image type is "raw"
|
||||||
|
* "format" (u): The image format, as defined in QImage::Format.
|
||||||
|
Available only if the image type is "raw"
|
||||||
|
-->
|
||||||
|
<method name="CaptureArea">
|
||||||
|
<arg name="x" type="i" direction="in" />
|
||||||
|
<arg name="y" type="i" direction="in" />
|
||||||
|
<arg name="width" type="u" direction="in" />
|
||||||
|
<arg name="height" type="u" direction="in" />
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In4" value="QVariantMap" />
|
||||||
|
<arg name="options" type="a{sv}" direction="in" />
|
||||||
|
<arg name="pipe" type="h" direction="in" />
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
|
||||||
|
<arg name="results" type="a{sv}" direction="out" />
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
CaptureScreen:
|
||||||
|
@name: The name of the screen assigned by the compositor
|
||||||
|
@options: Optional vardict with screenshot options
|
||||||
|
@pipe: The pipe file descriptor where the screenshot will be written
|
||||||
|
|
||||||
|
Take a screenshot of the specified monitor. The application that
|
||||||
|
requests the screenshot must have the org.kde.KWin.ScreenShot2
|
||||||
|
interface listed in the X-KDE-DBUS-Restricted-Interfaces desktop file
|
||||||
|
entry.
|
||||||
|
|
||||||
|
Available @options include:
|
||||||
|
|
||||||
|
* "include-cursor" (b): Whether the cursor should be included.
|
||||||
|
Defaults to false
|
||||||
|
* "native-resolution" (b): Whether the screenshot should be in
|
||||||
|
native size. Defaults to false
|
||||||
|
|
||||||
|
The following results get returned via the @results vardict:
|
||||||
|
|
||||||
|
* "type" (s): The type of the image written to the pipe. Currently,
|
||||||
|
the only supported type is "raw"
|
||||||
|
* "width" (u): The width of the image. Available only if the image
|
||||||
|
type is "raw"
|
||||||
|
* "height" (u): The height of the image. Available only if the image
|
||||||
|
type is "raw"
|
||||||
|
* "stride" (u): The number of bytes per row. Available only if the
|
||||||
|
image type is "raw"
|
||||||
|
* "format" (u): The image format, as defined in QImage::Format.
|
||||||
|
Available only if the image type is "raw"
|
||||||
|
-->
|
||||||
|
<method name="CaptureScreen">
|
||||||
|
<arg name="name" type="s" direction="in" />
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap" />
|
||||||
|
<arg name="options" type="a{sv}" direction="in" />
|
||||||
|
<arg name="pipe" type="h" direction="in" />
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
|
||||||
|
<arg name="results" type="a{sv}" direction="out" />
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
CaptureInteractive:
|
||||||
|
@kind: 0 - window, 1 - screen
|
||||||
|
@options: Optional vardict with screenshot options
|
||||||
|
@pipe: The pipe file descriptor where the screenshot will be written
|
||||||
|
|
||||||
|
Take a screenshot of a screen or a window as selected by the user.
|
||||||
|
|
||||||
|
Available @options include:
|
||||||
|
|
||||||
|
* "include-cursor" (b): Whether the cursor should be included.
|
||||||
|
Defaults to false
|
||||||
|
* "include-decoration" (b): Whether the decoration should be included.
|
||||||
|
Defaults to false
|
||||||
|
* "native-resolution" (b): Whether the screenshot should be in
|
||||||
|
native size. Defaults to false
|
||||||
|
|
||||||
|
The following results get returned via the @results vardict:
|
||||||
|
|
||||||
|
* "type" (s): The type of the image written to the pipe. Currently,
|
||||||
|
the only supported type is "raw"
|
||||||
|
* "width" (u): The width of the image. Available only if the image
|
||||||
|
type is "raw"
|
||||||
|
* "height" (u): The height of the image. Available only if the image
|
||||||
|
type is "raw"
|
||||||
|
* "stride" (u): The number of bytes per row. Available only if the
|
||||||
|
image type is "raw"
|
||||||
|
* "format" (u): The image format, as defined in QImage::Format.
|
||||||
|
Available only if the image type is "raw"
|
||||||
|
-->
|
||||||
|
<method name="CaptureInteractive">
|
||||||
|
<arg name="kind" type="u" direction="in" />
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap" />
|
||||||
|
<arg name="options" type="a{sv}" direction="in" />
|
||||||
|
<arg name="pipe" type="h" direction="in" />
|
||||||
|
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
|
||||||
|
<arg name="results" type="a{sv}" direction="out" />
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
</node>
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QGuiApplication>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
|
@ -31,44 +32,45 @@ constexpr int SCREENSHOT_DELAY = 200;
|
||||||
|
|
||||||
/* -- Static Helpers --------------------------------------------------------------------------- */
|
/* -- Static Helpers --------------------------------------------------------------------------- */
|
||||||
|
|
||||||
static int readData(int theFile, QByteArray &theDataOut)
|
static QImage allocateImage(const QVariantMap &metadata)
|
||||||
{
|
{
|
||||||
// implementation based on QtWayland file qwaylanddataoffer.cpp
|
bool ok;
|
||||||
char lBuffer[4096];
|
|
||||||
int lRetryCount = 0;
|
|
||||||
ssize_t lBytesRead = 0;
|
|
||||||
|
|
||||||
do {
|
const uint width = metadata.value(QStringLiteral("width")).toUInt(&ok);
|
||||||
// give user 30 sec to click a window, afterwards considered as error
|
if (!ok) {
|
||||||
while (true) {
|
|
||||||
lBytesRead = QT_READ(theFile, lBuffer, sizeof lBuffer);
|
|
||||||
if (lBytesRead == -1 && (errno == EAGAIN) && ++lRetryCount < 30000) {
|
|
||||||
usleep(1000);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lBytesRead > 0) {
|
|
||||||
theDataOut.append(lBuffer, lBytesRead);
|
|
||||||
}
|
|
||||||
} while (lBytesRead > 0);
|
|
||||||
return lBytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
static QImage readImage(int thePipeFd)
|
|
||||||
{
|
|
||||||
QByteArray lContent;
|
|
||||||
if (readData(thePipeFd, lContent) != 0) {
|
|
||||||
close(thePipeFd);
|
|
||||||
return QImage();
|
return QImage();
|
||||||
}
|
}
|
||||||
close(thePipeFd);
|
|
||||||
|
|
||||||
QDataStream lDataStream(lContent);
|
const uint height = metadata.value(QStringLiteral("height")).toUInt(&ok);
|
||||||
QImage lImage;
|
if (!ok) {
|
||||||
lDataStream >> lImage;
|
return QImage();
|
||||||
return lImage;
|
}
|
||||||
|
|
||||||
|
const uint format = metadata.value(QStringLiteral("format")).toUInt(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
return QImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return QImage(width, height, QImage::Format(format));
|
||||||
|
}
|
||||||
|
|
||||||
|
static QImage readImage(int fileDescriptor, const QVariantMap &metadata)
|
||||||
|
{
|
||||||
|
QFile file;
|
||||||
|
if (!file.open(fileDescriptor, QFileDevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
|
||||||
|
close(fileDescriptor);
|
||||||
|
return QImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage result = allocateImage(metadata);
|
||||||
|
if (result.isNull()) {
|
||||||
|
return QImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
QDataStream stream(&file);
|
||||||
|
stream.readRawData(reinterpret_cast<char *>(result.bits()), result.sizeInBytes());
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
PhonePanel::PhonePanel(QObject *parent, const QVariantList &args)
|
PhonePanel::PhonePanel(QObject *parent, const QVariantList &args)
|
||||||
|
|
@ -76,7 +78,10 @@ PhonePanel::PhonePanel(QObject *parent, const QVariantList &args)
|
||||||
{
|
{
|
||||||
// setHasConfigurationInterface(true);
|
// setHasConfigurationInterface(true);
|
||||||
m_kscreenInterface = new org::kde::KScreen(QStringLiteral("org.kde.kded5"), QStringLiteral("/modules/kscreen"), QDBusConnection::sessionBus(), this);
|
m_kscreenInterface = new org::kde::KScreen(QStringLiteral("org.kde.kded5"), QStringLiteral("/modules/kscreen"), QDBusConnection::sessionBus(), this);
|
||||||
m_screenshotInterface = new org::kde::kwin::Screenshot(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QDBusConnection::sessionBus(), this);
|
m_screenshotInterface = new OrgKdeKWinScreenShot2Interface(QStringLiteral("org.kde.KWin.ScreenShot2"),
|
||||||
|
QStringLiteral("/org/kde/KWin/ScreenShot2"),
|
||||||
|
QDBusConnection::sessionBus(),
|
||||||
|
this);
|
||||||
|
|
||||||
m_localeConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig);
|
m_localeConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig);
|
||||||
m_localeConfigWatcher = KConfigWatcher::create(m_localeConfig);
|
m_localeConfigWatcher = KConfigWatcher::create(m_localeConfig);
|
||||||
|
|
@ -143,8 +148,18 @@ void PhonePanel::setAutoRotate(bool value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PhonePanel::takeScreenshot()
|
void PhonePanel::handleMetaDataReceived(const QVariantMap &metadata, int fd)
|
||||||
{
|
{
|
||||||
|
const QString type = metadata.value(QStringLiteral("type")).toString();
|
||||||
|
if (type != QLatin1String("raw")) {
|
||||||
|
qWarning() << "Unsupported metadata type:" << type;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto watcher = new QFutureWatcher<QImage>(this);
|
||||||
|
connect(watcher, &QFutureWatcher<QImage>::finished, this, [watcher]() {
|
||||||
|
watcher->deleteLater();
|
||||||
|
|
||||||
QString filePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
|
QString filePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
|
||||||
if (filePath.isEmpty()) {
|
if (filePath.isEmpty()) {
|
||||||
qWarning() << "Couldn't find a writable location for the screenshot!";
|
qWarning() << "Couldn't find a writable location for the screenshot!";
|
||||||
|
|
@ -156,31 +171,9 @@ void PhonePanel::takeScreenshot()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
filePath += QStringLiteral("/Screenshots/Screenshot_%1.png").arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_hhmmss")));
|
filePath += QStringLiteral("/Screenshots/Screenshot_%1.png").arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_hhmmss")));
|
||||||
|
const auto m_result = watcher->result();
|
||||||
// wait ~200 ms to wait for rest of animations
|
if (m_result.isNull() || !m_result.save(filePath)) {
|
||||||
QTimer::singleShot(SCREENSHOT_DELAY, [=]() {
|
qWarning() << "Screenshot failed";
|
||||||
int lPipeFds[2];
|
|
||||||
if (pipe2(lPipeFds, O_CLOEXEC | O_NONBLOCK) != 0) {
|
|
||||||
qWarning() << "Could not take screenshot";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Take fullscreen screenshot, and no pointer
|
|
||||||
QDBusPendingCall pcall = m_screenshotInterface->screenshotFullscreen(QDBusUnixFileDescriptor(lPipeFds[1]), false, true);
|
|
||||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pcall, this);
|
|
||||||
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [](QDBusPendingCallWatcher *watcher) {
|
|
||||||
if (watcher->isError()) {
|
|
||||||
const auto error = watcher->error();
|
|
||||||
qWarning() << "Error calling KWin DBus interface:" << error.name() << error.message();
|
|
||||||
}
|
|
||||||
watcher->deleteLater();
|
|
||||||
});
|
|
||||||
const auto lWatcher = new QFutureWatcher<QImage>(this);
|
|
||||||
QObject::connect(lWatcher, &QFutureWatcher<QImage>::finished, this, [lWatcher, filePath]() {
|
|
||||||
lWatcher->deleteLater();
|
|
||||||
const QImage lImage = lWatcher->result();
|
|
||||||
qDebug() << lImage;
|
|
||||||
if (!lImage.save(filePath, "PNG")) {
|
|
||||||
qWarning() << "Failed to save screenshot to" << filePath;
|
|
||||||
} else {
|
} else {
|
||||||
KNotification *notif = new KNotification("captured");
|
KNotification *notif = new KNotification("captured");
|
||||||
notif->setComponentName(QStringLiteral("plasma_phone_components"));
|
notif->setComponentName(QStringLiteral("plasma_phone_components"));
|
||||||
|
|
@ -190,8 +183,35 @@ void PhonePanel::takeScreenshot()
|
||||||
notif->sendEvent();
|
notif->sendEvent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
lWatcher->setFuture(QtConcurrent::run(readImage, lPipeFds[0]));
|
watcher->setFuture(QtConcurrent::run(readImage, fd, metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhonePanel::takeScreenshot()
|
||||||
|
{
|
||||||
|
// wait ~200 ms to wait for rest of animations
|
||||||
|
QTimer::singleShot(SCREENSHOT_DELAY, [=]() {
|
||||||
|
int lPipeFds[2];
|
||||||
|
if (pipe2(lPipeFds, O_CLOEXEC) != 0) {
|
||||||
|
qWarning() << "Could not take screenshot";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have access to the ScreenPool so we'll just take the first screen
|
||||||
|
auto pendingCall = m_screenshotInterface->CaptureScreen(qGuiApp->screens().constFirst()->name(), {}, QDBusUnixFileDescriptor(lPipeFds[1]));
|
||||||
close(lPipeFds[1]);
|
close(lPipeFds[1]);
|
||||||
|
auto pipeFileDescriptor = lPipeFds[0];
|
||||||
|
|
||||||
|
auto watcher = new QDBusPendingCallWatcher(pendingCall, this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, pipeFileDescriptor]() {
|
||||||
|
watcher->deleteLater();
|
||||||
|
const QDBusPendingReply<QVariantMap> reply = *watcher;
|
||||||
|
|
||||||
|
if (reply.isError()) {
|
||||||
|
qWarning() << "Screenshot request failed:" << reply.error().message();
|
||||||
|
} else {
|
||||||
|
handleMetaDataReceived(reply, pipeFileDescriptor);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
#include <KSharedConfig>
|
#include <KSharedConfig>
|
||||||
|
|
||||||
#include "kscreeninterface.h"
|
#include "kscreeninterface.h"
|
||||||
#include "screenshotinterface.h"
|
#include "screenshot2interface.h"
|
||||||
|
|
||||||
class PhonePanel : public Plasma::Containment
|
class PhonePanel : public Plasma::Containment
|
||||||
{
|
{
|
||||||
|
|
@ -45,13 +45,14 @@ signals:
|
||||||
void isSystem24HourFormatChanged();
|
void isSystem24HourFormatChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void handleMetaDataReceived(const QVariantMap &metadata, int fd);
|
||||||
bool m_running = false;
|
bool m_running = false;
|
||||||
|
|
||||||
KConfigWatcher::Ptr m_localeConfigWatcher;
|
KConfigWatcher::Ptr m_localeConfigWatcher;
|
||||||
KSharedConfig::Ptr m_localeConfig;
|
KSharedConfig::Ptr m_localeConfig;
|
||||||
|
|
||||||
org::kde::KScreen *m_kscreenInterface;
|
org::kde::KScreen *m_kscreenInterface;
|
||||||
org::kde::kwin::Screenshot *m_screenshotInterface;
|
OrgKdeKWinScreenShot2Interface *m_screenshotInterface;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue