Backport Night Color feature to X11
Summary: The color correction manager doesn't make any specific assumptions about underlying platform, e.g. whether it's x11, etc. The platform just has to be capable of setting gamma ramps. Given that, there are no any significant technical blockers for making this feature work on x. Reviewers: #kwin, davidedmundson, romangg Reviewed By: #kwin, davidedmundson, romangg Subscribers: romangg, neobrain, GB_2, filipf, davidedmundson, ngraham, kwin Tags: #kwin Differential Revision: https://phabricator.kde.org/D21345
This commit is contained in:
parent
a39c74059e
commit
0d381846f1
12 changed files with 184 additions and 90 deletions
|
@ -17,16 +17,53 @@ GNU General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*********************************************************************/
|
*********************************************************************/
|
||||||
|
|
||||||
#include "abstract_output.h"
|
#include "abstract_output.h"
|
||||||
|
|
||||||
// KF5
|
|
||||||
#include <KLocalizedString>
|
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace KWin
|
namespace KWin
|
||||||
{
|
{
|
||||||
|
|
||||||
|
GammaRamp::GammaRamp(uint32_t size)
|
||||||
|
: m_table(3 * size)
|
||||||
|
, m_size(size)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t GammaRamp::size() const
|
||||||
|
{
|
||||||
|
return m_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t *GammaRamp::red()
|
||||||
|
{
|
||||||
|
return m_table.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t *GammaRamp::red() const
|
||||||
|
{
|
||||||
|
return m_table.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t *GammaRamp::green()
|
||||||
|
{
|
||||||
|
return m_table.data() + m_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t *GammaRamp::green() const
|
||||||
|
{
|
||||||
|
return m_table.data() + m_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t *GammaRamp::blue()
|
||||||
|
{
|
||||||
|
return m_table.data() + 2 * m_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t *GammaRamp::blue() const
|
||||||
|
{
|
||||||
|
return m_table.data() + 2 * m_size;
|
||||||
|
}
|
||||||
|
|
||||||
AbstractOutput::AbstractOutput(QObject *parent)
|
AbstractOutput::AbstractOutput(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
|
|
|
@ -25,13 +25,64 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
namespace KWin
|
namespace KWin
|
||||||
{
|
{
|
||||||
|
|
||||||
namespace ColorCorrect {
|
class KWIN_EXPORT GammaRamp
|
||||||
struct GammaRamp;
|
{
|
||||||
}
|
public:
|
||||||
|
GammaRamp(uint32_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the gamma ramp.
|
||||||
|
**/
|
||||||
|
uint32_t size() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns pointer to the first red component in the gamma ramp.
|
||||||
|
*
|
||||||
|
* The returned pointer can be used for altering the red component
|
||||||
|
* in the gamma ramp.
|
||||||
|
**/
|
||||||
|
uint16_t *red();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns pointer to the first red component in the gamma ramp.
|
||||||
|
**/
|
||||||
|
const uint16_t *red() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns pointer to the first green component in the gamma ramp.
|
||||||
|
*
|
||||||
|
* The returned pointer can be used for altering the green component
|
||||||
|
* in the gamma ramp.
|
||||||
|
**/
|
||||||
|
uint16_t *green();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns pointer to the first green component in the gamma ramp.
|
||||||
|
**/
|
||||||
|
const uint16_t *green() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns pointer to the first blue component in the gamma ramp.
|
||||||
|
*
|
||||||
|
* The returned pointer can be used for altering the blue component
|
||||||
|
* in the gamma ramp.
|
||||||
|
**/
|
||||||
|
uint16_t *blue();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns pointer to the first blue component in the gamma ramp.
|
||||||
|
**/
|
||||||
|
const uint16_t *blue() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVector<uint16_t> m_table;
|
||||||
|
uint32_t m_size;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic output representation in a Wayland session
|
* Generic output representation in a Wayland session
|
||||||
|
@ -64,10 +115,10 @@ public:
|
||||||
return Qt::PrimaryOrientation;
|
return Qt::PrimaryOrientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual int getGammaRampSize() const {
|
virtual int gammaRampSize() const {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
virtual bool setGammaRamp(const ColorCorrect::GammaRamp &gamma) {
|
virtual bool setGammaRamp(const GammaRamp &gamma) {
|
||||||
Q_UNUSED(gamma);
|
Q_UNUSED(gamma);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
/********************************************************************
|
|
||||||
KWin - the KDE window manager
|
|
||||||
This file is part of the KDE project.
|
|
||||||
|
|
||||||
Copyright 2017 Roman Gilg <subdiff@gmail.com>
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 2 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*********************************************************************/
|
|
||||||
#ifndef KWIN_GAMMARAMP_H
|
|
||||||
#define KWIN_GAMMARAMP_H
|
|
||||||
|
|
||||||
namespace KWin
|
|
||||||
{
|
|
||||||
|
|
||||||
namespace ColorCorrect
|
|
||||||
{
|
|
||||||
|
|
||||||
struct GammaRamp {
|
|
||||||
GammaRamp(int _size) {
|
|
||||||
size = _size;
|
|
||||||
red = new uint16_t[3 * _size];
|
|
||||||
green = red + _size;
|
|
||||||
blue = green + _size;
|
|
||||||
}
|
|
||||||
~GammaRamp() {
|
|
||||||
delete[] red;
|
|
||||||
red = green = blue = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t size = 0;
|
|
||||||
uint16_t *red = nullptr;
|
|
||||||
uint16_t *green = nullptr;
|
|
||||||
uint16_t *blue = nullptr;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Q_DISABLE_COPY(GammaRamp)
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // KWIN_GAMMARAMP_H
|
|
|
@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#include "manager.h"
|
#include "manager.h"
|
||||||
#include "colorcorrectdbusinterface.h"
|
#include "colorcorrectdbusinterface.h"
|
||||||
#include "suncalc.h"
|
#include "suncalc.h"
|
||||||
#include "gammaramp.h"
|
|
||||||
#include <colorcorrect_logging.h>
|
#include <colorcorrect_logging.h>
|
||||||
|
|
||||||
#include <main.h>
|
#include <main.h>
|
||||||
|
@ -513,20 +512,23 @@ void Manager::commitGammaRamps(int temperature)
|
||||||
const auto outs = kwinApp()->platform()->outputs();
|
const auto outs = kwinApp()->platform()->outputs();
|
||||||
|
|
||||||
for (auto *o : outs) {
|
for (auto *o : outs) {
|
||||||
int rampsize = o->getGammaRampSize();
|
int rampsize = o->gammaRampSize();
|
||||||
GammaRamp ramp(rampsize);
|
GammaRamp ramp(rampsize);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The gamma calculation below is based on the Redshift app:
|
* The gamma calculation below is based on the Redshift app:
|
||||||
* https://github.com/jonls/redshift
|
* https://github.com/jonls/redshift
|
||||||
*/
|
*/
|
||||||
|
uint16_t *red = ramp.red();
|
||||||
|
uint16_t *green = ramp.green();
|
||||||
|
uint16_t *blue = ramp.blue();
|
||||||
|
|
||||||
// linear default state
|
// linear default state
|
||||||
for (int i = 0; i < rampsize; i++) {
|
for (int i = 0; i < rampsize; i++) {
|
||||||
uint16_t value = (double)i / rampsize * (UINT16_MAX + 1);
|
uint16_t value = (double)i / rampsize * (UINT16_MAX + 1);
|
||||||
ramp.red[i] = value;
|
red[i] = value;
|
||||||
ramp.green[i] = value;
|
green[i] = value;
|
||||||
ramp.blue[i] = value;
|
blue[i] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// approximate white point
|
// approximate white point
|
||||||
|
@ -538,9 +540,9 @@ void Manager::commitGammaRamps(int temperature)
|
||||||
whitePoint[2] = (1. - alpha) * blackbodyColor[bbCIndex + 2] + alpha * blackbodyColor[bbCIndex + 5];
|
whitePoint[2] = (1. - alpha) * blackbodyColor[bbCIndex + 2] + alpha * blackbodyColor[bbCIndex + 5];
|
||||||
|
|
||||||
for (int i = 0; i < rampsize; i++) {
|
for (int i = 0; i < rampsize; i++) {
|
||||||
ramp.red[i] = (double)ramp.red[i] / (UINT16_MAX+1) * whitePoint[0] * (UINT16_MAX+1);
|
red[i] = qreal(red[i]) / (UINT16_MAX+1) * whitePoint[0] * (UINT16_MAX+1);
|
||||||
ramp.green[i] = (double)ramp.green[i] / (UINT16_MAX+1) * whitePoint[1] * (UINT16_MAX+1);
|
green[i] = qreal(green[i]) / (UINT16_MAX+1) * whitePoint[1] * (UINT16_MAX+1);
|
||||||
ramp.blue[i] = (double)ramp.blue[i] / (UINT16_MAX+1) * whitePoint[2] * (UINT16_MAX+1);
|
blue[i] = qreal(blue[i]) / (UINT16_MAX+1) * whitePoint[2] * (UINT16_MAX+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (o->setGammaRamp(ramp)) {
|
if (o->setGammaRamp(ramp)) {
|
||||||
|
|
|
@ -23,7 +23,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#include "drm_buffer.h"
|
#include "drm_buffer.h"
|
||||||
#include "drm_pointer.h"
|
#include "drm_pointer.h"
|
||||||
#include "logging.h"
|
#include "logging.h"
|
||||||
#include <colorcorrection/gammaramp.h>
|
|
||||||
|
|
||||||
namespace KWin
|
namespace KWin
|
||||||
{
|
{
|
||||||
|
@ -114,9 +113,15 @@ bool DrmCrtc::blank()
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DrmCrtc::setGammaRamp(const ColorCorrect::GammaRamp &gamma) {
|
bool DrmCrtc::setGammaRamp(const GammaRamp &gamma)
|
||||||
bool isError = drmModeCrtcSetGamma(m_backend->fd(), m_id, gamma.size,
|
{
|
||||||
gamma.red, gamma.green, gamma.blue);
|
uint16_t *red = const_cast<uint16_t *>(gamma.red());
|
||||||
|
uint16_t *green = const_cast<uint16_t *>(gamma.green());
|
||||||
|
uint16_t *blue = const_cast<uint16_t *>(gamma.blue());
|
||||||
|
|
||||||
|
const bool isError = drmModeCrtcSetGamma(m_backend->fd(), m_id,
|
||||||
|
gamma.size(), red, green, blue);
|
||||||
|
|
||||||
return !isError;
|
return !isError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,13 +25,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
namespace KWin
|
namespace KWin
|
||||||
{
|
{
|
||||||
|
|
||||||
namespace ColorCorrect {
|
|
||||||
struct GammaRamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DrmBackend;
|
class DrmBackend;
|
||||||
class DrmBuffer;
|
class DrmBuffer;
|
||||||
class DrmDumbBuffer;
|
class DrmDumbBuffer;
|
||||||
|
class GammaRamp;
|
||||||
|
|
||||||
class DrmCrtc : public DrmObject
|
class DrmCrtc : public DrmObject
|
||||||
{
|
{
|
||||||
|
@ -47,7 +44,7 @@ public:
|
||||||
Active,
|
Active,
|
||||||
Count
|
Count
|
||||||
};
|
};
|
||||||
|
|
||||||
bool initProps();
|
bool initProps();
|
||||||
|
|
||||||
int resIndex() const {
|
int resIndex() const {
|
||||||
|
@ -67,10 +64,10 @@ public:
|
||||||
void flipBuffer();
|
void flipBuffer();
|
||||||
bool blank();
|
bool blank();
|
||||||
|
|
||||||
int getGammaRampSize() const {
|
int gammaRampSize() const {
|
||||||
return m_gammaRampSize;
|
return m_gammaRampSize;
|
||||||
}
|
}
|
||||||
bool setGammaRamp(const ColorCorrect::GammaRamp &gamma);
|
bool setGammaRamp(const GammaRamp &gamma);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int m_resIndex;
|
int m_resIndex;
|
||||||
|
|
|
@ -1201,12 +1201,12 @@ void DrmOutput::automaticRotation()
|
||||||
emit screens()->changed();
|
emit screens()->changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
int DrmOutput::getGammaRampSize() const
|
int DrmOutput::gammaRampSize() const
|
||||||
{
|
{
|
||||||
return m_crtc->getGammaRampSize();
|
return m_crtc->gammaRampSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DrmOutput::setGammaRamp(const ColorCorrect::GammaRamp &gamma)
|
bool DrmOutput::setGammaRamp(const GammaRamp &gamma)
|
||||||
{
|
{
|
||||||
return m_crtc->setGammaRamp(gamma);
|
return m_crtc->setGammaRamp(gamma);
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,8 +133,8 @@ private:
|
||||||
void transform(KWayland::Server::OutputDeviceInterface::Transform transform) override;
|
void transform(KWayland::Server::OutputDeviceInterface::Transform transform) override;
|
||||||
void automaticRotation();
|
void automaticRotation();
|
||||||
|
|
||||||
int getGammaRampSize() const override;
|
int gammaRampSize() const override;
|
||||||
bool setGammaRamp(const ColorCorrect::GammaRamp &gamma) override;
|
bool setGammaRamp(const GammaRamp &gamma) override;
|
||||||
QMatrix4x4 matrixDisplay(const QSize &s) const;
|
QMatrix4x4 matrixDisplay(const QSize &s) const;
|
||||||
|
|
||||||
DrmBackend *m_backend;
|
DrmBackend *m_backend;
|
||||||
|
|
|
@ -41,10 +41,10 @@ public:
|
||||||
|
|
||||||
void setGeometry(const QRect &geo);
|
void setGeometry(const QRect &geo);
|
||||||
|
|
||||||
int getGammaRampSize() const override {
|
int gammaRampSize() const override {
|
||||||
return m_gammaSize;
|
return m_gammaSize;
|
||||||
}
|
}
|
||||||
bool setGammaRamp(const ColorCorrect::GammaRamp &gamma) override {
|
bool setGammaRamp(const GammaRamp &gamma) override {
|
||||||
Q_UNUSED(gamma);
|
Q_UNUSED(gamma);
|
||||||
return m_gammaResult;
|
return m_gammaResult;
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,4 +61,31 @@ void X11Output::setRefreshRate(int set)
|
||||||
m_refreshRate = set;
|
m_refreshRate = set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int X11Output::gammaRampSize() const
|
||||||
|
{
|
||||||
|
return m_gammaRampSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool X11Output::setGammaRamp(const GammaRamp &gamma)
|
||||||
|
{
|
||||||
|
if (m_crtc == XCB_NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
xcb_randr_set_crtc_gamma(connection(), m_crtc, gamma.size(), gamma.red(),
|
||||||
|
gamma.green(), gamma.blue());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void X11Output::setCrtc(xcb_randr_crtc_t crtc)
|
||||||
|
{
|
||||||
|
m_crtc = crtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void X11Output::setGammaRampSize(int size)
|
||||||
|
{
|
||||||
|
m_gammaRampSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
|
|
||||||
|
#include <xcb/randr.h>
|
||||||
|
|
||||||
namespace KWin
|
namespace KWin
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -35,6 +37,7 @@ namespace KWin
|
||||||
class KWIN_EXPORT X11Output : public AbstractOutput
|
class KWIN_EXPORT X11Output : public AbstractOutput
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit X11Output(QObject *parent = nullptr);
|
explicit X11Output(QObject *parent = nullptr);
|
||||||
virtual ~X11Output() = default;
|
virtual ~X11Output() = default;
|
||||||
|
@ -53,10 +56,23 @@ public:
|
||||||
int refreshRate() const override;
|
int refreshRate() const override;
|
||||||
void setRefreshRate(int set);
|
void setRefreshRate(int set);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of gamma lookup table.
|
||||||
|
**/
|
||||||
|
int gammaRampSize() const override;
|
||||||
|
bool setGammaRamp(const GammaRamp &gamma) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void setCrtc(xcb_randr_crtc_t crtc);
|
||||||
|
void setGammaRampSize(int size);
|
||||||
|
|
||||||
|
xcb_randr_crtc_t m_crtc = XCB_NONE;
|
||||||
QString m_name;
|
QString m_name;
|
||||||
QRect m_geometry;
|
QRect m_geometry;
|
||||||
|
int m_gammaRampSize;
|
||||||
int m_refreshRate;
|
int m_refreshRate;
|
||||||
|
|
||||||
|
friend class X11StandalonePlatform;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,8 @@ X11StandalonePlatform::X11StandalonePlatform(QObject *parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setSupportsGammaControl(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
X11StandalonePlatform::~X11StandalonePlatform()
|
X11StandalonePlatform::~X11StandalonePlatform()
|
||||||
|
@ -456,6 +458,7 @@ void X11StandalonePlatform::doUpdateOutputs()
|
||||||
{
|
{
|
||||||
auto fallback = [this]() {
|
auto fallback = [this]() {
|
||||||
auto *o = new X11Output(this);
|
auto *o = new X11Output(this);
|
||||||
|
o->setGammaRampSize(0);
|
||||||
o->setRefreshRate(-1.0f);
|
o->setRefreshRate(-1.0f);
|
||||||
o->setName(QStringLiteral("Xinerama"));
|
o->setName(QStringLiteral("Xinerama"));
|
||||||
m_outputs << o;
|
m_outputs << o;
|
||||||
|
@ -514,14 +517,23 @@ void X11StandalonePlatform::doUpdateOutputs()
|
||||||
|
|
||||||
const QRect geo = info.rect();
|
const QRect geo = info.rect();
|
||||||
if (geo.isValid()) {
|
if (geo.isValid()) {
|
||||||
|
xcb_randr_crtc_t crtc = crtcs[i];
|
||||||
|
|
||||||
|
// TODO: Perhaps the output has to save the inherited gamma ramp and
|
||||||
|
// restore it during tear down. Currently neither standalone x11 nor
|
||||||
|
// drm platform do this.
|
||||||
|
Xcb::RandR::CrtcGamma gamma(crtc);
|
||||||
|
|
||||||
auto *o = new X11Output(this);
|
auto *o = new X11Output(this);
|
||||||
|
o->setCrtc(crtc);
|
||||||
|
o->setGammaRampSize(gamma.isNull() ? 0 : gamma->size);
|
||||||
o->setGeometry(geo);
|
o->setGeometry(geo);
|
||||||
o->setRefreshRate(refreshRate);
|
o->setRefreshRate(refreshRate);
|
||||||
|
|
||||||
QString name;
|
QString name;
|
||||||
for (int j = 0; j < info->num_outputs; ++j) {
|
for (int j = 0; j < info->num_outputs; ++j) {
|
||||||
Xcb::RandR::OutputInfo outputInfo(outputInfos.at(j));
|
Xcb::RandR::OutputInfo outputInfo(outputInfos.at(j));
|
||||||
if (crtcs[i] == outputInfo->crtc) {
|
if (crtc == outputInfo->crtc) {
|
||||||
name = outputInfo.name();
|
name = outputInfo.name();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue