Initial commit of the blur effect rewrite.
svn path=/trunk/KDE/kdebase/workspace/; revision=1099619
This commit is contained in:
parent
48c3a09119
commit
53391ba944
6 changed files with 544 additions and 0 deletions
14
effects/blur/CMakeLists.txt
Normal file
14
effects/blur/CMakeLists.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
#######################################
|
||||
# Effect
|
||||
|
||||
# Source files
|
||||
set( kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources}
|
||||
blur/blur.cpp
|
||||
blur/blurshader.cpp
|
||||
)
|
||||
|
||||
# .desktop files
|
||||
install( FILES
|
||||
blur/blur.desktop
|
||||
DESTINATION ${SERVICES_INSTALL_DIR}/kwin )
|
||||
|
200
effects/blur/blur.cpp
Normal file
200
effects/blur/blur.cpp
Normal file
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Copyright © 2010 Fredrik Höglund <fredrik@kde.org>
|
||||
*
|
||||
* 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; see the file COPYING. if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "blur.h"
|
||||
#include "blurshader.h"
|
||||
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
KWIN_EFFECT(blur, BlurEffect)
|
||||
KWIN_EFFECT_SUPPORTED(blur, BlurEffect::supported())
|
||||
|
||||
|
||||
BlurEffect::BlurEffect()
|
||||
: radius(12)
|
||||
{
|
||||
shader = new BlurShader;
|
||||
shader->setRadius(radius);
|
||||
|
||||
// Offscreen texture that's used as the target for the horizontal blur pass
|
||||
// and the source for the vertical pass.
|
||||
tex = new GLTexture(displayWidth(), displayHeight());
|
||||
tex->setFilter(GL_LINEAR);
|
||||
tex->setWrapMode(GL_CLAMP_TO_EDGE);
|
||||
|
||||
target = new GLRenderTarget(tex);
|
||||
}
|
||||
|
||||
BlurEffect::~BlurEffect()
|
||||
{
|
||||
delete shader;
|
||||
delete target;
|
||||
delete tex;
|
||||
}
|
||||
|
||||
bool BlurEffect::supported()
|
||||
{
|
||||
return GLRenderTarget::supported() && GLTexture::NPOTTextureSupported() &&
|
||||
hasGLExtension("GL_ARB_fragment_program");
|
||||
}
|
||||
|
||||
QRect BlurEffect::expand(const QRect &rect) const
|
||||
{
|
||||
return rect.adjusted(-radius, -radius, radius, radius);
|
||||
}
|
||||
|
||||
QRegion BlurEffect::expand(const QRegion ®ion) const
|
||||
{
|
||||
QRegion expanded;
|
||||
|
||||
if (region.rectCount() < 10) {
|
||||
foreach (const QRect &rect, region.rects())
|
||||
expanded += expand(rect);
|
||||
} else
|
||||
expanded += expand(region.boundingRect());
|
||||
|
||||
return expanded;
|
||||
}
|
||||
|
||||
void BlurEffect::paintScreen(int mask, QRegion region, ScreenPaintData &data)
|
||||
{
|
||||
// Force the scene to call paintGenericScreen() so the windows are painted bottom -> top
|
||||
if (!effects->activeFullScreenEffect())
|
||||
mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS;
|
||||
|
||||
effects->paintScreen(mask, region, data);
|
||||
}
|
||||
|
||||
void BlurEffect::drawWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data)
|
||||
{
|
||||
bool scaled = !qFuzzyCompare(data.xScale, 1.0) && !qFuzzyCompare(data.yScale, 1.0);
|
||||
bool translated = data.xTranslate || data.yTranslate;
|
||||
bool hasAlpha = w->hasAlpha() || (w->hasDecoration() && effects->decorationsHaveAlpha());
|
||||
|
||||
if (!effects->activeFullScreenEffect() && hasAlpha && !w->isDesktop() &&
|
||||
!scaled && !translated /* && region.intersects(w->geometry())*/)
|
||||
{
|
||||
const QRect screen(0, 0, displayWidth(), displayHeight());
|
||||
const QRegion shape = w->shape().translated(w->geometry().topLeft()) & screen;
|
||||
const QRect r = expand(shape.boundingRect()) & screen;
|
||||
const QPoint offset = -shape.boundingRect().topLeft() +
|
||||
(shape.boundingRect().topLeft() - r.topLeft());
|
||||
|
||||
// Create a scratch texture and copy the area in the back buffer that we're
|
||||
// going to blur into it
|
||||
GLTexture scratch(r.width(), r.height());
|
||||
scratch.setFilter(GL_LINEAR);
|
||||
scratch.setWrapMode(GL_CLAMP_TO_EDGE);
|
||||
scratch.bind();
|
||||
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, r.x(), displayHeight() - r.y() - r.height(),
|
||||
r.width(), r.height());
|
||||
|
||||
// Draw the texture on the offscreen framebuffer object, while blurring it horizontally
|
||||
effects->pushRenderTarget(target);
|
||||
|
||||
shader->bind();
|
||||
shader->setDirection(Qt::Horizontal);
|
||||
shader->setPixelDistance(1.0 / r.width());
|
||||
shader->setOpacity(1.0);
|
||||
|
||||
glBegin(GL_QUADS);
|
||||
glTexCoord2f(0, 1); glVertex2i(0, 0);
|
||||
glTexCoord2f(1, 1); glVertex2i(r.width(), 0);
|
||||
glTexCoord2f(1, 0); glVertex2i(r.width(), r.height());
|
||||
glTexCoord2f(0, 0); glVertex2i(0, r.height());
|
||||
glEnd();
|
||||
|
||||
effects->popRenderTarget();
|
||||
scratch.unbind();
|
||||
scratch.discard();
|
||||
|
||||
// Now draw the horizontally blurred area back to the backbuffer, while
|
||||
// blurring it vertically and clipping it to the window shape.
|
||||
tex->bind();
|
||||
|
||||
shader->setDirection(Qt::Vertical);
|
||||
shader->setPixelDistance(1.0 / tex->height());
|
||||
|
||||
// Modulate the blurred texture with the window opacity if the window isn't opaque
|
||||
const float opacity = data.opacity * data.contents_opacity;
|
||||
if (opacity < 1.0) {
|
||||
shader->setOpacity(opacity);
|
||||
|
||||
glPushAttrib(GL_COLOR_BUFFER_BIT);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
|
||||
const float tw = tex->width();
|
||||
const float th = tex->height();
|
||||
int vertexCount = shape.rectCount() * 4;
|
||||
|
||||
if (vertices.size() < vertexCount) {
|
||||
vertices.resize(vertexCount);
|
||||
texCoords.resize(vertexCount);
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
foreach (const QRect &r, shape.rects()) {
|
||||
vertices[i + 0] = QVector2D(r.x(), r.y());
|
||||
vertices[i + 1] = QVector2D(r.x() + r.width(), r.y());
|
||||
vertices[i + 2] = QVector2D(r.x() + r.width(), r.y() + r.height());
|
||||
vertices[i + 3] = QVector2D(r.x(), r.y() + r.height());
|
||||
|
||||
const QRect sr = r.translated(offset);
|
||||
texCoords[i + 0] = QVector2D(sr.x() / tw, 1 - sr.y() / th);
|
||||
texCoords[i + 1] = QVector2D((sr.x() + sr.width()) / tw, 1 - sr.y() / th);
|
||||
texCoords[i + 2] = QVector2D((sr.x() + sr.width()) / tw, 1 - (sr.y() + sr.height()) / th);
|
||||
texCoords[i + 3] = QVector2D(sr.x() / tw, 1 - (sr.y() + sr.height()) / th);
|
||||
i += 4;
|
||||
}
|
||||
|
||||
if (vertexCount > 1000) {
|
||||
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT);
|
||||
glEnableClientState(GL_VERTEX_ARRAY);
|
||||
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
|
||||
glTexCoordPointer(2, GL_FLOAT, 0, (float*)texCoords.constData());
|
||||
glVertexPointer(2, GL_FLOAT, 0, (float*)vertices.constData());
|
||||
glDrawArrays(GL_QUADS, 0, vertexCount);
|
||||
glPopClientAttrib();
|
||||
} else {
|
||||
glBegin(GL_QUADS);
|
||||
for (int i = 0; i < vertexCount; i++) {
|
||||
glTexCoord2fv((const float*)&texCoords[i]);
|
||||
glVertex2fv((const float*)&vertices[i]);
|
||||
}
|
||||
glEnd();
|
||||
}
|
||||
|
||||
if (opacity < 1.0)
|
||||
glPopAttrib();
|
||||
|
||||
tex->unbind();
|
||||
shader->unbind();
|
||||
}
|
||||
|
||||
// Draw the window over the blurred area
|
||||
effects->drawWindow(w, mask, region, data);
|
||||
}
|
||||
|
||||
} // namespace KWin
|
||||
|
18
effects/blur/blur.desktop
Normal file
18
effects/blur/blur.desktop
Normal file
|
@ -0,0 +1,18 @@
|
|||
[Desktop Entry]
|
||||
Name=Blur
|
||||
Icon=preferences-system-windows-effect-blur
|
||||
Comment=Blurs the background behind semi-transparent windows
|
||||
|
||||
Type=Service
|
||||
X-KDE-ServiceTypes=KWin/Effect
|
||||
X-KDE-PluginInfo-Author=Fredrik Höglund
|
||||
X-KDE-PluginInfo-Email=fredrik@kde.org
|
||||
X-KDE-PluginInfo-Name=kwin4_effect_blur
|
||||
X-KDE-PluginInfo-Version=0.1.0
|
||||
X-KDE-PluginInfo-Category=Appearance
|
||||
X-KDE-PluginInfo-Depends=
|
||||
X-KDE-PluginInfo-License=GPL
|
||||
X-KDE-PluginInfo-EnabledByDefault=false
|
||||
X-KDE-Library=kwin4_effect_builtins
|
||||
X-KDE-Ordering=75
|
||||
|
61
effects/blur/blur.h
Normal file
61
effects/blur/blur.h
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright © 2010 Fredrik Höglund <fredrik@kde.org>
|
||||
*
|
||||
* 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; see the file COPYING. if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef BLUR_H
|
||||
#define BLUR_H
|
||||
|
||||
#include <kwineffects.h>
|
||||
#include <kwinglutils.h>
|
||||
|
||||
#include <QVector>
|
||||
#include <QVector2D>
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
class BlurShader;
|
||||
|
||||
class BlurEffect : public KWin::Effect
|
||||
{
|
||||
public:
|
||||
BlurEffect();
|
||||
~BlurEffect();
|
||||
|
||||
static bool supported();
|
||||
|
||||
void paintScreen(int mask, QRegion region, ScreenPaintData &data);
|
||||
void drawWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data);
|
||||
|
||||
private:
|
||||
QRect expand(const QRect &rect) const;
|
||||
QRegion expand(const QRegion ®ion) const;
|
||||
|
||||
private:
|
||||
BlurShader *shader;
|
||||
QVector<QVector2D> vertices;
|
||||
QVector<QVector2D> texCoords;
|
||||
GLRenderTarget *target;
|
||||
GLTexture *tex;
|
||||
int radius;
|
||||
};
|
||||
|
||||
} // namespace KWin
|
||||
|
||||
#endif
|
||||
|
188
effects/blur/blurshader.cpp
Normal file
188
effects/blur/blurshader.cpp
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright © 2010 Fredrik Höglund <fredrik@kde.org>
|
||||
*
|
||||
* 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; see the file COPYING. if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "blurshader.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QTextStream>
|
||||
#include <KDebug>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using namespace KWin;
|
||||
|
||||
|
||||
BlurShader::BlurShader()
|
||||
: program(0), radius(12)
|
||||
{
|
||||
}
|
||||
|
||||
BlurShader::~BlurShader()
|
||||
{
|
||||
if (program) {
|
||||
glDeleteProgramsARB(1, &program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void BlurShader::setRadius(int _radius)
|
||||
{
|
||||
int r = qMax(_radius, 2);
|
||||
|
||||
if (radius != r) {
|
||||
radius = r;
|
||||
|
||||
if (program) {
|
||||
glDeleteProgramsARB(1, &program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlurShader::setDirection(Qt::Orientation orientation)
|
||||
{
|
||||
direction = orientation;
|
||||
}
|
||||
|
||||
void BlurShader::setPixelDistance(float val)
|
||||
{
|
||||
float firstStep = val * 1.5;
|
||||
float nextStep = val * 2.0;
|
||||
|
||||
if (direction == Qt::Horizontal) {
|
||||
glProgramLocalParameter4fARB(GL_FRAGMENT_PROGRAM_ARB, 0, firstStep, 0, 0, 0);
|
||||
glProgramLocalParameter4fARB(GL_FRAGMENT_PROGRAM_ARB, 1, nextStep, 0, 0, 0);
|
||||
} else {
|
||||
glProgramLocalParameter4fARB(GL_FRAGMENT_PROGRAM_ARB, 0, 0, firstStep, 0, 0);
|
||||
glProgramLocalParameter4fARB(GL_FRAGMENT_PROGRAM_ARB, 1, 0, nextStep, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void BlurShader::setOpacity(float val)
|
||||
{
|
||||
glProgramLocalParameter4fARB(GL_FRAGMENT_PROGRAM_ARB, 2, val, val, val, val);
|
||||
}
|
||||
|
||||
void BlurShader::bind()
|
||||
{
|
||||
if (!program)
|
||||
init();
|
||||
|
||||
glEnable(GL_FRAGMENT_PROGRAM_ARB);
|
||||
glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, program);
|
||||
}
|
||||
|
||||
void BlurShader::unbind()
|
||||
{
|
||||
glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, 0);
|
||||
glDisable(GL_FRAGMENT_PROGRAM_ARB);
|
||||
}
|
||||
|
||||
float BlurShader::gaussian(float x, float sigma) const
|
||||
{
|
||||
return (1.0 / std::sqrt(2.0 * M_PI) * sigma)
|
||||
* std::exp(-((x * x) / (2.0 * sigma * sigma)));
|
||||
}
|
||||
|
||||
QVector<float> BlurShader::gaussianKernel() const
|
||||
{
|
||||
const int size = radius | 1;
|
||||
QVector<float> kernel(size);
|
||||
const qreal sigma = radius / 2.5;
|
||||
const int center = size / 2;
|
||||
|
||||
// Generate the gaussian kernel
|
||||
kernel[center] = gaussian(0, sigma) * .5;
|
||||
for (int i = 1; i <= center; i++) {
|
||||
const float val = gaussian(1.5 + (i - 1) * 2.0, sigma);
|
||||
kernel[center + i] = val;
|
||||
kernel[center - i] = val;
|
||||
}
|
||||
|
||||
// Normalize the kernel
|
||||
qreal total = 0;
|
||||
for (int i = 0; i < size; i++)
|
||||
total += kernel[i];
|
||||
|
||||
for (int i = 0; i < size; i++)
|
||||
kernel[i] /= total;
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
void BlurShader::init()
|
||||
{
|
||||
QVector<float> kernel = gaussianKernel();
|
||||
const int size = kernel.size();
|
||||
const int center = size / 2;
|
||||
|
||||
QByteArray text;
|
||||
QTextStream stream(&text);
|
||||
|
||||
stream << "!!ARBfp1.0\n";
|
||||
|
||||
// The kernel values are hardcoded into the program
|
||||
for (int i = 0; i <= center; i++)
|
||||
stream << "PARAM kernel" << i << " = " << kernel[i] << ";\n";
|
||||
|
||||
stream << "PARAM firstSample = program.local[0];\n"; // Distance from gl_TexCoord[0] to the next sample
|
||||
stream << "PARAM nextSample = program.local[1];\n"; // Distance to the subsequent sample
|
||||
stream << "PARAM opacity = program.local[2];\n"; // The opacity with which to modulate the pixels
|
||||
|
||||
stream << "TEMP coord;\n"; // The coordinate we'll be sampling
|
||||
stream << "TEMP sample;\n"; // The sampled value
|
||||
stream << "TEMP sum;\n"; // The sum of the weighted samples
|
||||
|
||||
// Start by sampling the center coordinate
|
||||
stream << "TEX sample, fragment.texcoord[0], texture[0], 2D;\n"; // sample = texture2D(tex, gl_TexCoord[0])
|
||||
stream << "MUL sum, sample, kernel" << center << ";\n"; // sum = sample * kernel[center]
|
||||
|
||||
for (int i = 1; i <= center; i++) {
|
||||
if (i == 1)
|
||||
stream << "SUB coord, fragment.texcoord[0], firstSample;\n"; // coord = gl_TexCoord[0] - firstSample
|
||||
else
|
||||
stream << "SUB coord, coord, nextSample;\n"; // coord -= nextSample
|
||||
stream << "TEX sample, coord, texture[0], 2D;\n"; // sample = texture2D(tex, coord)
|
||||
stream << "MAD sum, sample, kernel" << center - i << ", sum;\n"; // sum += sample * kernel[center - i]
|
||||
}
|
||||
|
||||
for (int i = 1; i <= center; i++) {
|
||||
if (i == 1)
|
||||
stream << "ADD coord, fragment.texcoord[0], firstSample;\n"; // coord = gl_TexCoord[0] + firstSample
|
||||
else
|
||||
stream << "ADD coord, coord, nextSample;\n"; // coord += nextSample
|
||||
stream << "TEX sample, coord, texture[0], 2D;\n"; // sample = texture2D(tex, coord)
|
||||
stream << "MAD sum, sample, kernel" << center - i << ", sum;\n"; // sum += sample * kernel[center - i]
|
||||
}
|
||||
stream << "MUL result.color, sum, opacity;\n"; // gl_FragColor = sum * opacity
|
||||
stream << "END\n";
|
||||
stream.flush();
|
||||
|
||||
glGenProgramsARB(1, &program);
|
||||
glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, program);
|
||||
glProgramStringARB(GL_FRAGMENT_PROGRAM_ARB, GL_PROGRAM_FORMAT_ASCII_ARB, text.length(), text.constData());
|
||||
|
||||
if (glGetError()) {
|
||||
const char *error = (const char*)glGetString(GL_PROGRAM_ERROR_STRING_ARB);
|
||||
kError() << "Error when compiling fragment program:" << error;
|
||||
}
|
||||
|
||||
glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, 0);
|
||||
}
|
||||
|
63
effects/blur/blurshader.h
Normal file
63
effects/blur/blurshader.h
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright © 2010 Fredrik Höglund <fredrik@kde.org>
|
||||
*
|
||||
* 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; see the file COPYING. if not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef BLURSHADER_H
|
||||
#define BLURSHADER_H
|
||||
|
||||
#include <kwinglutils.h>
|
||||
|
||||
namespace KWin
|
||||
{
|
||||
|
||||
class BlurShader
|
||||
{
|
||||
public:
|
||||
BlurShader();
|
||||
~BlurShader();
|
||||
|
||||
// Sets the radius in pixels
|
||||
void setRadius(int radius);
|
||||
|
||||
// Sets the blur direction
|
||||
void setDirection(Qt::Orientation orientation);
|
||||
|
||||
// Sets the distance between two pixels
|
||||
void setPixelDistance(float val);
|
||||
|
||||
// The opacity of the resulting pixels is multiplied by this value
|
||||
void setOpacity(float val);
|
||||
|
||||
void bind();
|
||||
void unbind();
|
||||
|
||||
private:
|
||||
float gaussian(float x, float sigma) const;
|
||||
QVector<float> gaussianKernel() const;
|
||||
void init();
|
||||
|
||||
private:
|
||||
GLuint program;
|
||||
int radius;
|
||||
Qt::Orientation direction;
|
||||
};
|
||||
|
||||
} // namespace KWin
|
||||
|
||||
#endif
|
||||
|
Loading…
Reference in a new issue