2015-03-20 13:41:03 +00:00
|
|
|
/********************************************************************
|
|
|
|
KWin - the KDE window manager
|
|
|
|
This file is part of the KDE project.
|
|
|
|
|
|
|
|
Copyright (C) 2015 Martin Gräßlin <mgraesslin@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. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*********************************************************************/
|
2016-04-07 07:24:17 +00:00
|
|
|
#ifndef KWIN_PLATFORM_H
|
|
|
|
#define KWIN_PLATFORM_H
|
2015-03-23 13:29:07 +00:00
|
|
|
#include <kwin_export.h>
|
2016-10-17 14:12:21 +00:00
|
|
|
#include <kwinglobals.h>
|
2015-08-18 06:35:34 +00:00
|
|
|
#include <epoxy/egl.h>
|
|
|
|
#include <fixx11h.h>
|
2015-04-01 13:36:40 +00:00
|
|
|
#include <QImage>
|
2015-03-20 13:41:03 +00:00
|
|
|
#include <QObject>
|
|
|
|
|
2016-11-15 09:22:56 +00:00
|
|
|
#include <functional>
|
|
|
|
|
2017-01-17 06:12:44 +00:00
|
|
|
class QAction;
|
|
|
|
|
2016-03-10 18:57:07 +00:00
|
|
|
namespace KWayland {
|
|
|
|
namespace Server {
|
|
|
|
class OutputConfigurationInterface;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-20 13:41:03 +00:00
|
|
|
namespace KWin
|
|
|
|
{
|
|
|
|
|
2016-04-15 11:47:56 +00:00
|
|
|
class Edge;
|
2015-03-27 07:51:56 +00:00
|
|
|
class OpenGLBackend;
|
2015-03-27 08:05:03 +00:00
|
|
|
class QPainterBackend;
|
2015-03-27 06:52:59 +00:00
|
|
|
class Screens;
|
2016-04-15 11:47:56 +00:00
|
|
|
class ScreenEdges;
|
2016-11-15 09:22:56 +00:00
|
|
|
class Toplevel;
|
2015-04-02 12:37:23 +00:00
|
|
|
class WaylandCursorTheme;
|
|
|
|
|
2016-04-07 07:18:10 +00:00
|
|
|
class KWIN_EXPORT Platform : public QObject
|
2015-03-20 13:41:03 +00:00
|
|
|
{
|
|
|
|
Q_OBJECT
|
|
|
|
public:
|
2016-04-07 07:18:10 +00:00
|
|
|
virtual ~Platform();
|
2015-03-20 13:41:03 +00:00
|
|
|
|
2015-05-06 06:32:39 +00:00
|
|
|
virtual void init() = 0;
|
2015-03-27 06:52:59 +00:00
|
|
|
virtual Screens *createScreens(QObject *parent = nullptr);
|
2015-03-27 07:51:56 +00:00
|
|
|
virtual OpenGLBackend *createOpenGLBackend();
|
2015-03-27 08:05:03 +00:00
|
|
|
virtual QPainterBackend *createQPainterBackend();
|
2016-04-15 11:47:56 +00:00
|
|
|
/**
|
|
|
|
* Allows the platform to create a platform specific screen edge.
|
|
|
|
* The default implementation creates a Edge.
|
|
|
|
**/
|
|
|
|
virtual Edge *createScreenEdge(ScreenEdges *parent);
|
2016-08-15 10:00:03 +00:00
|
|
|
/**
|
|
|
|
* Allows the platform to create a platform specific Cursor.
|
|
|
|
* The default implementation creates an InputRedirectionCursor.
|
|
|
|
**/
|
|
|
|
virtual void createPlatformCursor(QObject *parent = nullptr);
|
2015-06-05 17:34:03 +00:00
|
|
|
virtual void warpPointer(const QPointF &globalPos);
|
2015-08-18 06:40:35 +00:00
|
|
|
/**
|
|
|
|
* Whether our Compositing EGL display allows a surface less context
|
|
|
|
* so that a sharing context could be created.
|
|
|
|
**/
|
|
|
|
virtual bool supportsQpaContext() const;
|
2015-08-18 06:35:34 +00:00
|
|
|
/**
|
|
|
|
* The EGLDisplay used by the compositing scene.
|
|
|
|
**/
|
2016-07-18 08:27:56 +00:00
|
|
|
EGLDisplay sceneEglDisplay() const;
|
|
|
|
void setSceneEglDisplay(EGLDisplay display);
|
2015-08-18 06:35:34 +00:00
|
|
|
/**
|
|
|
|
* The EGLContext used by the compositing scene.
|
|
|
|
**/
|
|
|
|
virtual EGLContext sceneEglContext() const;
|
2016-07-18 09:17:54 +00:00
|
|
|
/**
|
|
|
|
* The first (in case of multiple) EGLSurface used by the compositing scene.
|
|
|
|
**/
|
|
|
|
EGLSurface sceneEglSurface() const;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The EglConfig used by the compositing scene.
|
|
|
|
**/
|
|
|
|
EGLConfig sceneEglConfig() const;
|
2015-03-20 13:41:03 +00:00
|
|
|
|
2015-11-17 09:32:06 +00:00
|
|
|
/**
|
|
|
|
* Implementing subclasses should provide a size in case the backend represents
|
|
|
|
* a basic screen and uses the BasicScreens.
|
|
|
|
*
|
|
|
|
* Base implementation returns an invalid size.
|
|
|
|
**/
|
|
|
|
virtual QSize screenSize() const;
|
2015-11-20 15:11:35 +00:00
|
|
|
/**
|
|
|
|
* Implementing subclasses should provide all geometries in case the backend represents
|
|
|
|
* a basic screen and uses the BasicScreens.
|
|
|
|
*
|
|
|
|
* Base implementation returns one QRect positioned at 0/0 with screenSize() as size.
|
|
|
|
**/
|
|
|
|
virtual QVector<QRect> screenGeometries() const;
|
2016-03-10 18:57:07 +00:00
|
|
|
/**
|
|
|
|
* Implement this method to receive configuration change requests through KWayland's
|
|
|
|
* OutputManagement interface.
|
|
|
|
*
|
|
|
|
* Base implementation warns that the current backend does not implement this
|
|
|
|
* functionality.
|
|
|
|
*/
|
|
|
|
virtual void configurationChangeRequested(KWayland::Server::OutputConfigurationInterface *config);
|
2015-11-17 09:32:06 +00:00
|
|
|
|
2016-05-09 14:41:37 +00:00
|
|
|
/**
|
|
|
|
* Whether the Platform requires compositing for rendering.
|
|
|
|
* Default implementation returns @c true. If the implementing Platform allows to be used
|
|
|
|
* without compositing (e.g. rendering is done by the windowing system), re-implement this method.
|
|
|
|
**/
|
|
|
|
virtual bool requiresCompositing() const;
|
2016-05-09 15:32:43 +00:00
|
|
|
/**
|
|
|
|
* Whether Compositing is possible in the Platform.
|
|
|
|
* Returning @c false in this method makes only sense if @link{requiresCompositing} returns @c false.
|
|
|
|
*
|
|
|
|
* The default implementation returns @c true.
|
|
|
|
* @see requiresCompositing
|
|
|
|
**/
|
|
|
|
virtual bool compositingPossible() const;
|
|
|
|
/**
|
|
|
|
* Returns a user facing text explaining why compositing is not possible in case
|
|
|
|
* @link{compositingPossible} returns @c false.
|
|
|
|
*
|
|
|
|
* The default implementation returns an empty string.
|
|
|
|
* @see compositingPossible
|
|
|
|
**/
|
|
|
|
virtual QString compositingNotPossibleReason() const;
|
|
|
|
/**
|
|
|
|
* Whether OpenGL compositing is broken.
|
|
|
|
* The Platform can implement this method if it is able to detect whether OpenGL compositing
|
|
|
|
* broke (e.g. triggered a crash in a previous run).
|
|
|
|
*
|
|
|
|
* Default implementation returns @c false.
|
2016-05-10 08:34:09 +00:00
|
|
|
* @see createOpenGLSafePoint
|
2016-05-09 15:32:43 +00:00
|
|
|
**/
|
|
|
|
virtual bool openGLCompositingIsBroken() const;
|
2016-05-10 08:34:09 +00:00
|
|
|
enum class OpenGLSafePoint {
|
|
|
|
PreInit,
|
[platformx/x11] Add a freeze protection against OpenGL
Summary:
With nouveau driver it can happen that KWin gets frozen when first trying
to render with OpenGL. This results in a freeze of the complete desktop
as the compositor is non functional.
Our OpenGL breakage detection is only able to detect crashes, but not
freezes. This change improves it by also added a freeze protection.
In the PreInit stage a thread is started with a QTimer of 15 sec. If the
timer fires, qFatal is triggered to terminate KWin. This can only happen
if the creation of the OpenGL compositor takes longer than said 15 sec.
In the PostInit stage the timer gets deleted and the thread stopeed
again.
Thus if a freeze is detected the OpenGL unsafe protection is written into
the config. KWin aborts and gets restarted by DrKonqui. The new KWin
instance will no longer try to activate the freezing OpenGL as the
protection is set.
If KWin doesn't freeze the protection is removed from the config as
we are used to.
Check for freezes for the first n frames, not just the first
This patch changes the freeze detection code to detect freezes in the
first 30 frames (by default, users can change that with the
KWIN_MAX_FRAMES_TESTED environment variable). This detects
successfully the freezes associated to nouveau drivers
in https://bugzilla.suse.com/show_bug.cgi?id=1005323
Reviewers: davidedmundson, #plasma, #kwin, graesslin
Reviewed By: #plasma, #kwin, graesslin
Subscribers: luebking, graesslin, kwin, plasma-devel, davidedmundson
Tags: #plasma
Differential Revision: https://phabricator.kde.org/D3132
2016-10-24 15:14:32 +00:00
|
|
|
PostInit,
|
|
|
|
PreFrame,
|
|
|
|
PostFrame,
|
|
|
|
PostLastGuardedFrame
|
2016-05-10 08:34:09 +00:00
|
|
|
};
|
|
|
|
/**
|
|
|
|
* This method is invoked before and after creating the OpenGL rendering Scene.
|
|
|
|
* An implementing Platform can use it to detect crashes triggered by the OpenGL implementation.
|
|
|
|
* This can be used for @link{openGLCompositingIsBroken}.
|
|
|
|
*
|
|
|
|
* The default implementation does nothing.
|
|
|
|
* @see openGLCompositingIsBroken.
|
|
|
|
**/
|
|
|
|
virtual void createOpenGLSafePoint(OpenGLSafePoint safePoint);
|
2016-05-09 14:41:37 +00:00
|
|
|
|
2016-11-15 09:22:56 +00:00
|
|
|
/**
|
|
|
|
* Starts an interactive window selection process.
|
|
|
|
*
|
|
|
|
* Once the user selected a window the @p callback is invoked with the selected Toplevel as
|
|
|
|
* argument. In case the user cancels the interactive window selection or selecting a window is currently
|
|
|
|
* not possible (e.g. screen locked) the @p callback is invoked with a @c nullptr argument.
|
|
|
|
*
|
|
|
|
* During the interactive window selection the cursor is turned into a crosshair cursor unless
|
|
|
|
* @p cursorName is provided. The argument @p cursorName is a QByteArray instead of Qt::CursorShape
|
|
|
|
* to support the "pirate" cursor for kill window which is not wrapped by Qt::CursorShape.
|
|
|
|
*
|
2016-11-15 13:23:51 +00:00
|
|
|
* The default implementation forwards to InputRedirection.
|
2016-11-15 09:22:56 +00:00
|
|
|
*
|
|
|
|
* @param callback The function to invoke once the interactive window selection ends
|
|
|
|
* @param cursorName The optional name of the cursor shape to use, default is crosshair
|
|
|
|
**/
|
|
|
|
virtual void startInteractiveWindowSelection(std::function<void(KWin::Toplevel*)> callback, const QByteArray &cursorName = QByteArray());
|
|
|
|
|
2016-11-23 14:53:17 +00:00
|
|
|
/**
|
|
|
|
* Starts an interactive position selection process.
|
|
|
|
*
|
|
|
|
* Once the user selected a position on the screen the @p callback is invoked with
|
|
|
|
* the selected point as argument. In case the user cancels the interactive position selection
|
|
|
|
* or selecting a position is currently not possible (e.g. screen locked) the @p callback
|
|
|
|
* is invoked with a point at @c -1 as x and y argument.
|
|
|
|
*
|
|
|
|
* During the interactive window selection the cursor is turned into a crosshair cursor.
|
|
|
|
*
|
|
|
|
* The default implementation forwards to InputRedirection.
|
|
|
|
*
|
|
|
|
* @param callback The function to invoke once the interactive position selection ends
|
|
|
|
**/
|
|
|
|
virtual void startInteractivePositionSelection(std::function<void(const QPoint &)> callback);
|
|
|
|
|
2017-01-17 06:12:44 +00:00
|
|
|
/**
|
|
|
|
* Platform specific preparation for an @p action which is used for KGlobalAccel.
|
|
|
|
*
|
|
|
|
* A platform might need to do preparation for an @p action before
|
|
|
|
* it can be used with KGlobalAccel.
|
|
|
|
*
|
|
|
|
* Code using KGlobalAccel should invoke this method for the @p action
|
|
|
|
* prior to setting up any shortcuts and connections.
|
|
|
|
*
|
|
|
|
* The default implementation does nothing.
|
|
|
|
*
|
|
|
|
* @param action The action which will be used with KGlobalAccel.
|
|
|
|
* @since 5.10
|
|
|
|
**/
|
|
|
|
virtual void setupActionForGlobalAccel(QAction *action);
|
|
|
|
|
2015-04-01 13:36:40 +00:00
|
|
|
bool usesSoftwareCursor() const {
|
|
|
|
return m_softWareCursor;
|
|
|
|
}
|
2016-02-23 11:29:05 +00:00
|
|
|
QImage softwareCursor() const;
|
|
|
|
QPoint softwareCursorHotspot() const;
|
2015-04-01 13:36:40 +00:00
|
|
|
void markCursorAsRendered();
|
|
|
|
|
2016-10-17 14:12:21 +00:00
|
|
|
/**
|
|
|
|
* Returns a PlatformCursorImage. By default this is created by softwareCursor and
|
|
|
|
* softwareCursorHotspot. An implementing subclass can use this to provide a better
|
|
|
|
* suited PlatformCursorImage.
|
|
|
|
*
|
|
|
|
* @see softwareCursor
|
|
|
|
* @see softwareCursorHotspot
|
|
|
|
* @since 5.9
|
|
|
|
**/
|
|
|
|
virtual PlatformCursorImage cursorImage() const;
|
|
|
|
|
2016-10-20 08:21:54 +00:00
|
|
|
/**
|
|
|
|
* The Platform cursor image should be hidden.
|
|
|
|
* @see showCursor
|
|
|
|
* @see doHideCursor
|
|
|
|
* @see isCursorHidden
|
|
|
|
* @since 5.9
|
|
|
|
**/
|
|
|
|
void hideCursor();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The Platform cursor image should be shown again.
|
|
|
|
* @see hideCursor
|
|
|
|
* @see doShowCursor
|
|
|
|
* @see isCursorHidden
|
|
|
|
* @since 5.9
|
|
|
|
**/
|
|
|
|
void showCursor();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether the cursor is currently hidden.
|
|
|
|
* @see showCursor
|
|
|
|
* @see hideCursor
|
|
|
|
* @since 5.9
|
|
|
|
**/
|
|
|
|
bool isCursorHidden() const {
|
|
|
|
return m_hideCursorCounter > 0;
|
|
|
|
}
|
|
|
|
|
2015-04-24 10:00:19 +00:00
|
|
|
bool handlesOutputs() const {
|
|
|
|
return m_handlesOutputs;
|
|
|
|
}
|
2015-05-05 17:02:52 +00:00
|
|
|
bool isReady() const {
|
|
|
|
return m_ready;
|
|
|
|
}
|
2015-05-06 07:26:08 +00:00
|
|
|
void setInitialWindowSize(const QSize &size) {
|
|
|
|
m_initialWindowSize = size;
|
|
|
|
}
|
2015-05-06 07:37:25 +00:00
|
|
|
void setDeviceIdentifier(const QByteArray &identifier) {
|
|
|
|
m_deviceIdentifier = identifier;
|
|
|
|
}
|
2015-06-05 17:34:03 +00:00
|
|
|
bool supportsPointerWarping() const {
|
|
|
|
return m_pointerWarping;
|
|
|
|
}
|
2015-08-31 09:39:26 +00:00
|
|
|
bool areOutputsEnabled() const {
|
|
|
|
return m_outputsEnabled;
|
|
|
|
}
|
2015-11-12 14:12:01 +00:00
|
|
|
void setOutputsEnabled(bool enabled) {
|
|
|
|
m_outputsEnabled = enabled;
|
|
|
|
}
|
2015-11-24 12:11:29 +00:00
|
|
|
int initialOutputCount() const {
|
|
|
|
return m_initialOutputCount;
|
|
|
|
}
|
|
|
|
void setInitialOutputCount(int count) {
|
|
|
|
m_initialOutputCount = count;
|
|
|
|
}
|
2015-04-24 10:00:19 +00:00
|
|
|
|
2015-05-11 13:43:30 +00:00
|
|
|
public Q_SLOTS:
|
2015-05-05 08:44:46 +00:00
|
|
|
void pointerMotion(const QPointF &position, quint32 time);
|
|
|
|
void pointerButtonPressed(quint32 button, quint32 time);
|
|
|
|
void pointerButtonReleased(quint32 button, quint32 time);
|
|
|
|
void pointerAxisHorizontal(qreal delta, quint32 time);
|
|
|
|
void pointerAxisVertical(qreal delta, quint32 time);
|
|
|
|
void keyboardKeyPressed(quint32 key, quint32 time);
|
|
|
|
void keyboardKeyReleased(quint32 key, quint32 time);
|
|
|
|
void keyboardModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group);
|
|
|
|
void keymapChange(int fd, uint32_t size);
|
|
|
|
void touchDown(qint32 id, const QPointF &pos, quint32 time);
|
|
|
|
void touchUp(qint32 id, quint32 time);
|
|
|
|
void touchMotion(qint32 id, const QPointF &pos, quint32 time);
|
|
|
|
void touchCancel();
|
|
|
|
void touchFrame();
|
|
|
|
|
2017-03-15 16:56:08 +00:00
|
|
|
void processSwipeGestureBegin(int fingerCount, quint32 time);
|
|
|
|
void processSwipeGestureUpdate(const QSizeF &delta, quint32 time);
|
|
|
|
void processSwipeGestureEnd(quint32 time);
|
|
|
|
void processSwipeGestureCancelled(quint32 time);
|
|
|
|
void processPinchGestureBegin(int fingerCount, quint32 time);
|
|
|
|
void processPinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time);
|
|
|
|
void processPinchGestureEnd(quint32 time);
|
|
|
|
void processPinchGestureCancelled(quint32 time);
|
|
|
|
|
2015-04-13 14:25:34 +00:00
|
|
|
Q_SIGNALS:
|
2015-05-06 07:03:54 +00:00
|
|
|
void screensQueried();
|
2015-05-06 07:16:36 +00:00
|
|
|
void initFailed();
|
2015-04-13 14:25:34 +00:00
|
|
|
void cursorChanged();
|
2015-05-05 17:02:52 +00:00
|
|
|
void readyChanged(bool);
|
2015-11-17 09:32:06 +00:00
|
|
|
/**
|
|
|
|
* Emitted by backends using a one screen (nested window) approach and when the size of that changes.
|
|
|
|
**/
|
|
|
|
void screenSizeChanged();
|
2015-04-13 14:25:34 +00:00
|
|
|
|
2015-03-20 13:41:03 +00:00
|
|
|
protected:
|
2016-04-07 07:18:10 +00:00
|
|
|
explicit Platform(QObject *parent = nullptr);
|
2015-04-01 13:36:40 +00:00
|
|
|
void setSoftWareCursor(bool set);
|
2015-04-24 10:00:19 +00:00
|
|
|
void handleOutputs() {
|
|
|
|
m_handlesOutputs = true;
|
|
|
|
}
|
2015-05-05 08:54:13 +00:00
|
|
|
void repaint(const QRect &rect);
|
2015-05-05 17:02:52 +00:00
|
|
|
void setReady(bool ready);
|
2015-05-06 07:26:08 +00:00
|
|
|
QSize initialWindowSize() const {
|
|
|
|
return m_initialWindowSize;
|
|
|
|
}
|
2015-05-06 07:37:25 +00:00
|
|
|
QByteArray deviceIdentifier() const {
|
|
|
|
return m_deviceIdentifier;
|
|
|
|
}
|
2015-06-05 17:34:03 +00:00
|
|
|
void setSupportsPointerWarping(bool set) {
|
|
|
|
m_pointerWarping = set;
|
|
|
|
}
|
2015-04-01 13:36:40 +00:00
|
|
|
|
2016-10-20 08:21:54 +00:00
|
|
|
/**
|
|
|
|
* Actual platform specific way to hide the cursor.
|
|
|
|
* Sub-classes need to implement if they support hiding the cursor.
|
|
|
|
*
|
|
|
|
* This method is invoked by hideCursor if the cursor needs to be hidden.
|
|
|
|
* The default implementation does nothing.
|
|
|
|
*
|
|
|
|
* @see doShowCursor
|
|
|
|
* @see hideCursor
|
|
|
|
* @see showCursor
|
|
|
|
**/
|
|
|
|
virtual void doHideCursor();
|
|
|
|
/**
|
|
|
|
* Actual platform specific way to show the cursor.
|
|
|
|
* Sub-classes need to implement if they support showing the cursor.
|
|
|
|
*
|
|
|
|
* This method is invoked by showCursor if the cursor needs to be shown again.
|
|
|
|
*
|
|
|
|
* @see doShowCursor
|
|
|
|
* @see hideCursor
|
|
|
|
* @see showCursor
|
|
|
|
**/
|
|
|
|
virtual void doShowCursor();
|
|
|
|
|
2015-04-01 13:36:40 +00:00
|
|
|
private:
|
|
|
|
void triggerCursorRepaint();
|
|
|
|
bool m_softWareCursor = false;
|
|
|
|
struct {
|
2016-06-28 06:33:47 +00:00
|
|
|
QRect lastRenderedGeometry;
|
2015-04-01 13:36:40 +00:00
|
|
|
} m_cursor;
|
2015-04-24 10:00:19 +00:00
|
|
|
bool m_handlesOutputs = false;
|
2015-05-05 17:02:52 +00:00
|
|
|
bool m_ready = false;
|
2015-05-06 07:26:08 +00:00
|
|
|
QSize m_initialWindowSize;
|
2015-05-06 07:37:25 +00:00
|
|
|
QByteArray m_deviceIdentifier;
|
2015-06-05 17:34:03 +00:00
|
|
|
bool m_pointerWarping = false;
|
2015-08-31 09:39:26 +00:00
|
|
|
bool m_outputsEnabled = true;
|
2015-11-24 12:11:29 +00:00
|
|
|
int m_initialOutputCount = 1;
|
2016-07-18 08:27:56 +00:00
|
|
|
EGLDisplay m_eglDisplay;
|
2016-10-20 08:21:54 +00:00
|
|
|
int m_hideCursorCounter = 0;
|
2015-03-20 13:41:03 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2016-04-07 07:18:10 +00:00
|
|
|
Q_DECLARE_INTERFACE(KWin::Platform, "org.kde.kwin.Platform")
|
2015-05-05 15:58:09 +00:00
|
|
|
|
2015-03-20 13:41:03 +00:00
|
|
|
#endif
|