You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1317 lines
41 KiB
1317 lines
41 KiB
/******************************************************************** |
|
KWin - the KDE window manager |
|
This file is part of the KDE project. |
|
|
|
Copyright (C) 2006 Lubos Lunak <l.lunak@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/>. |
|
*********************************************************************/ |
|
#include "composite.h" |
|
|
|
#include "dbusinterface.h" |
|
#include "utils.h" |
|
#include <QTextStream> |
|
#include "workspace.h" |
|
#include "client.h" |
|
#include "unmanaged.h" |
|
#include "deleted.h" |
|
#include "effects.h" |
|
#include "overlaywindow.h" |
|
#include "scene.h" |
|
#include "scene_xrender.h" |
|
#include "scene_opengl.h" |
|
#include "scene_qpainter.h" |
|
#include "screens.h" |
|
#include "shadow.h" |
|
#include "useractions.h" |
|
#include "compositingprefs.h" |
|
#include "xcbutils.h" |
|
#include "abstract_backend.h" |
|
#include "shell_client.h" |
|
#include "wayland_server.h" |
|
#include "decorations/decoratedclient.h" |
|
|
|
#include <KWayland/Server/surface_interface.h> |
|
|
|
#include <stdio.h> |
|
|
|
#include <QtConcurrentRun> |
|
#include <QFutureWatcher> |
|
#include <QMenu> |
|
#include <QTimerEvent> |
|
#include <QDateTime> |
|
#include <KGlobalAccel> |
|
#include <KLocalizedString> |
|
#include <KNotification> |
|
#include <KSelectionWatcher> |
|
|
|
#include <xcb/composite.h> |
|
#include <xcb/damage.h> |
|
|
|
Q_DECLARE_METATYPE(KWin::Compositor::SuspendReason) |
|
|
|
namespace KWin |
|
{ |
|
|
|
extern int currentRefreshRate(); |
|
|
|
CompositorSelectionOwner::CompositorSelectionOwner(const char *selection) : KSelectionOwner(selection, connection(), rootWindow()), owning(false) |
|
{ |
|
connect (this, SIGNAL(lostOwnership()), SLOT(looseOwnership())); |
|
} |
|
|
|
void CompositorSelectionOwner::looseOwnership() |
|
{ |
|
owning = false; |
|
} |
|
|
|
KWIN_SINGLETON_FACTORY_VARIABLE(Compositor, s_compositor) |
|
|
|
static inline qint64 milliToNano(int milli) { return milli * 1000 * 1000; } |
|
static inline qint64 nanoToMilli(int nano) { return nano / (1000*1000); } |
|
|
|
Compositor::Compositor(QObject* workspace) |
|
: QObject(workspace) |
|
, m_suspended(options->isUseCompositing() ? NoReasonSuspend : UserSuspend) |
|
, cm_selection(NULL) |
|
, vBlankInterval(0) |
|
, fpsInterval(0) |
|
, m_xrrRefreshRate(0) |
|
, forceUnredirectCheck(false) |
|
, m_finishing(false) |
|
, m_timeSinceLastVBlank(0) |
|
, m_scene(NULL) |
|
, m_bufferSwapPending(false) |
|
, m_composeAtSwapCompletion(false) |
|
{ |
|
qRegisterMetaType<Compositor::SuspendReason>("Compositor::SuspendReason"); |
|
connect(&unredirectTimer, SIGNAL(timeout()), SLOT(delayedCheckUnredirect())); |
|
connect(&compositeResetTimer, SIGNAL(timeout()), SLOT(restart())); |
|
connect(options, &Options::configChanged, this, &Compositor::slotConfigChanged); |
|
connect(options, SIGNAL(unredirectFullscreenChanged()), SLOT(delayedCheckUnredirect())); |
|
unredirectTimer.setSingleShot(true); |
|
compositeResetTimer.setSingleShot(true); |
|
nextPaintReference.invalidate(); // Initialize the timer |
|
|
|
// 2 sec which should be enough to restart the compositor |
|
static const int compositorLostMessageDelay = 2000; |
|
|
|
m_releaseSelectionTimer.setSingleShot(true); |
|
m_releaseSelectionTimer.setInterval(compositorLostMessageDelay); |
|
connect(&m_releaseSelectionTimer, SIGNAL(timeout()), SLOT(releaseCompositorSelection())); |
|
|
|
m_unusedSupportPropertyTimer.setInterval(compositorLostMessageDelay); |
|
m_unusedSupportPropertyTimer.setSingleShot(true); |
|
connect(&m_unusedSupportPropertyTimer, SIGNAL(timeout()), SLOT(deleteUnusedSupportProperties())); |
|
if (kwinApp()->operationMode() != Application::OperationModeX11) { |
|
if (waylandServer()->backend()->isReady()) { |
|
QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection); |
|
} |
|
connect(waylandServer()->backend(), &AbstractBackend::readyChanged, this, |
|
[this] (bool ready) { |
|
if (ready) { |
|
setup(); |
|
} else { |
|
finish(); |
|
} |
|
}, Qt::QueuedConnection |
|
); |
|
} else { |
|
// delay the call to setup by one event cycle |
|
// The ctor of this class is invoked from the Workspace ctor, that means before |
|
// Workspace is completely constructed, so calling Workspace::self() would result |
|
// in undefined behavior. This is fixed by using a delayed invocation. |
|
QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection); |
|
} |
|
|
|
// register DBus |
|
new CompositorDBusInterface(this); |
|
} |
|
|
|
Compositor::~Compositor() |
|
{ |
|
emit aboutToDestroy(); |
|
finish(); |
|
deleteUnusedSupportProperties(); |
|
delete cm_selection; |
|
s_compositor = NULL; |
|
} |
|
|
|
|
|
void Compositor::setup() |
|
{ |
|
if (hasScene()) |
|
return; |
|
if (m_suspended) { |
|
QStringList reasons; |
|
if (m_suspended & UserSuspend) { |
|
reasons << QStringLiteral("Disabled by User"); |
|
} |
|
if (m_suspended & BlockRuleSuspend) { |
|
reasons << QStringLiteral("Disabled by Window"); |
|
} |
|
if (m_suspended & ScriptSuspend) { |
|
reasons << QStringLiteral("Disabled by Script"); |
|
} |
|
qCDebug(KWIN_CORE) << "Compositing is suspended, reason:" << reasons; |
|
return; |
|
} else if (!CompositingPrefs::compositingPossible()) { |
|
qCCritical(KWIN_CORE) << "Compositing is not possible"; |
|
return; |
|
} |
|
m_starting = true; |
|
|
|
if (!options->isCompositingInitialized()) { |
|
options->reloadCompositingSettings(true); |
|
slotCompositingOptionsInitialized(); |
|
} else { |
|
slotCompositingOptionsInitialized(); |
|
} |
|
} |
|
|
|
extern int screen_number; // main.cpp |
|
extern bool is_multihead; |
|
|
|
void Compositor::slotCompositingOptionsInitialized() |
|
{ |
|
claimCompositorSelection(); |
|
|
|
// There might still be a deleted around, needs to be cleared before creating the scene (BUG 333275) |
|
if (Workspace::self()) { |
|
while (!Workspace::self()->deletedList().isEmpty()) { |
|
Workspace::self()->deletedList().first()->discard(); |
|
} |
|
} |
|
|
|
switch(options->compositingMode()) { |
|
case OpenGLCompositing: { |
|
qCDebug(KWIN_CORE) << "Initializing OpenGL compositing"; |
|
|
|
// Some broken drivers crash on glXQuery() so to prevent constant KWin crashes: |
|
KSharedConfigPtr unsafeConfigPtr = KSharedConfig::openConfig(); |
|
KConfigGroup unsafeConfig(unsafeConfigPtr, "Compositing"); |
|
const QString openGLIsUnsafe = QStringLiteral("OpenGLIsUnsafe") + (is_multihead ? QString::number(screen_number) : QString()); |
|
if (unsafeConfig.readEntry(openGLIsUnsafe, false)) |
|
qCWarning(KWIN_CORE) << "KWin has detected that your OpenGL library is unsafe to use"; |
|
else { |
|
unsafeConfig.writeEntry(openGLIsUnsafe, true); |
|
unsafeConfig.sync(); |
|
#ifndef KWIN_HAVE_OPENGLES |
|
if (!kwinApp()->shouldUseWaylandForCompositing() && !CompositingPrefs::hasGlx()) { |
|
unsafeConfig.writeEntry(openGLIsUnsafe, false); |
|
unsafeConfig.sync(); |
|
qCDebug(KWIN_CORE) << "No glx extensions available"; |
|
break; |
|
} |
|
#endif |
|
|
|
m_scene = SceneOpenGL::createScene(this); |
|
|
|
// TODO: Add 30 second delay to protect against screen freezes as well |
|
unsafeConfig.writeEntry(openGLIsUnsafe, false); |
|
unsafeConfig.sync(); |
|
|
|
if (m_scene && !m_scene->initFailed()) { |
|
connect(static_cast<SceneOpenGL*>(m_scene), &SceneOpenGL::resetCompositing, this, &Compositor::restart); |
|
break; // --> |
|
} |
|
delete m_scene; |
|
m_scene = NULL; |
|
} |
|
|
|
// Do not Fall back to XRender - it causes problems when selfcheck fails during startup, but works later on |
|
break; |
|
} |
|
#ifdef KWIN_HAVE_XRENDER_COMPOSITING |
|
case XRenderCompositing: |
|
qCDebug(KWIN_CORE) << "Initializing XRender compositing"; |
|
m_scene = SceneXrender::createScene(this); |
|
break; |
|
#endif |
|
case QPainterCompositing: |
|
qCDebug(KWIN_CORE) << "Initializing QPainter compositing"; |
|
m_scene = SceneQPainter::createScene(this); |
|
break; |
|
default: |
|
qCDebug(KWIN_CORE) << "No compositing enabled"; |
|
m_starting = false; |
|
if (cm_selection) { |
|
cm_selection->owning = false; |
|
cm_selection->release(); |
|
} |
|
if (kwinApp()->requiresCompositing()) { |
|
qCCritical(KWIN_CORE) << "The used windowing system requires compositing"; |
|
qCCritical(KWIN_CORE) << "We are going to quit KWin now as it is broken"; |
|
qApp->quit(); |
|
} |
|
return; |
|
} |
|
if (m_scene == NULL || m_scene->initFailed()) { |
|
qCCritical(KWIN_CORE) << "Failed to initialize compositing, compositing disabled"; |
|
delete m_scene; |
|
m_scene = NULL; |
|
m_starting = false; |
|
if (cm_selection) { |
|
cm_selection->owning = false; |
|
cm_selection->release(); |
|
} |
|
if (kwinApp()->requiresCompositing()) { |
|
qCCritical(KWIN_CORE) << "The used windowing system requires compositing"; |
|
qCCritical(KWIN_CORE) << "We are going to quit KWin now as it is broken"; |
|
qApp->quit(); |
|
} |
|
return; |
|
} |
|
|
|
if (Workspace::self()) { |
|
startupWithWorkspace(); |
|
} else { |
|
connect(kwinApp(), &Application::workspaceCreated, this, &Compositor::startupWithWorkspace); |
|
} |
|
} |
|
|
|
void Compositor::claimCompositorSelection() |
|
{ |
|
if (!cm_selection && kwinApp()->x11Connection()) { |
|
char selection_name[ 100 ]; |
|
sprintf(selection_name, "_NET_WM_CM_S%d", Application::x11ScreenNumber()); |
|
cm_selection = new CompositorSelectionOwner(selection_name); |
|
connect(cm_selection, SIGNAL(lostOwnership()), SLOT(finish())); |
|
} |
|
|
|
if (!cm_selection) // no X11 yet |
|
return; |
|
|
|
if (!cm_selection->owning) { |
|
cm_selection->claim(true); // force claiming |
|
cm_selection->owning = true; |
|
} |
|
} |
|
|
|
void Compositor::startupWithWorkspace() |
|
{ |
|
if (!m_starting) { |
|
return; |
|
} |
|
Q_ASSERT(m_scene); |
|
claimCompositorSelection(); |
|
connect(Workspace::self(), &Workspace::deletedRemoved, m_scene, &Scene::windowDeleted); |
|
m_xrrRefreshRate = KWin::currentRefreshRate(); |
|
fpsInterval = options->maxFpsInterval(); |
|
if (m_scene->syncsToVBlank()) { // if we do vsync, set the fps to the next multiple of the vblank rate |
|
vBlankInterval = milliToNano(1000) / m_xrrRefreshRate; |
|
fpsInterval = qMax((fpsInterval / vBlankInterval) * vBlankInterval, vBlankInterval); |
|
} else |
|
vBlankInterval = milliToNano(1); // no sync - DO NOT set "0", would cause div-by-zero segfaults. |
|
m_timeSinceLastVBlank = fpsInterval - (options->vBlankTime() + 1); // means "start now" - we don't have even a slight idea when the first vsync will occur |
|
scheduleRepaint(); |
|
xcb_composite_redirect_subwindows(connection(), rootWindow(), XCB_COMPOSITE_REDIRECT_MANUAL); |
|
new EffectsHandlerImpl(this, m_scene); // sets also the 'effects' pointer |
|
connect(effects, SIGNAL(screenGeometryChanged(QSize)), SLOT(addRepaintFull())); |
|
addRepaintFull(); |
|
foreach (Client * c, Workspace::self()->clientList()) { |
|
c->setupCompositing(); |
|
c->getShadow(); |
|
} |
|
foreach (Client * c, Workspace::self()->desktopList()) |
|
c->setupCompositing(); |
|
foreach (Unmanaged * c, Workspace::self()->unmanagedList()) { |
|
c->setupCompositing(); |
|
c->getShadow(); |
|
} |
|
|
|
emit compositingToggled(true); |
|
|
|
m_starting = false; |
|
if (m_releaseSelectionTimer.isActive()) { |
|
m_releaseSelectionTimer.stop(); |
|
} |
|
|
|
// render at least once |
|
performCompositing(); |
|
} |
|
|
|
void Compositor::scheduleRepaint() |
|
{ |
|
if (!compositeTimer.isActive()) |
|
setCompositeTimer(); |
|
} |
|
|
|
void Compositor::finish() |
|
{ |
|
if (!hasScene()) |
|
return; |
|
m_finishing = true; |
|
m_releaseSelectionTimer.start(); |
|
if (Workspace::self()) { |
|
foreach (Client * c, Workspace::self()->clientList()) |
|
m_scene->windowClosed(c, NULL); |
|
foreach (Client * c, Workspace::self()->desktopList()) |
|
m_scene->windowClosed(c, NULL); |
|
foreach (Unmanaged * c, Workspace::self()->unmanagedList()) |
|
m_scene->windowClosed(c, NULL); |
|
foreach (Deleted * c, Workspace::self()->deletedList()) |
|
m_scene->windowDeleted(c); |
|
foreach (Client * c, Workspace::self()->clientList()) |
|
c->finishCompositing(); |
|
foreach (Client * c, Workspace::self()->desktopList()) |
|
c->finishCompositing(); |
|
foreach (Unmanaged * c, Workspace::self()->unmanagedList()) |
|
c->finishCompositing(); |
|
foreach (Deleted * c, Workspace::self()->deletedList()) |
|
c->finishCompositing(); |
|
xcb_composite_unredirect_subwindows(connection(), rootWindow(), XCB_COMPOSITE_REDIRECT_MANUAL); |
|
} |
|
delete effects; |
|
effects = NULL; |
|
delete m_scene; |
|
m_scene = NULL; |
|
compositeTimer.stop(); |
|
repaints_region = QRegion(); |
|
if (Workspace::self()) { |
|
for (ClientList::ConstIterator it = Workspace::self()->clientList().constBegin(); |
|
it != Workspace::self()->clientList().constEnd(); |
|
++it) { |
|
// forward all opacity values to the frame in case there'll be other CM running |
|
if ((*it)->opacity() != 1.0) { |
|
NETWinInfo i(connection(), (*it)->frameId(), rootWindow(), 0, 0); |
|
i.setOpacity(static_cast< unsigned long >((*it)->opacity() * 0xffffffff)); |
|
} |
|
} |
|
// discard all Deleted windows (#152914) |
|
while (!Workspace::self()->deletedList().isEmpty()) |
|
Workspace::self()->deletedList().first()->discard(); |
|
} |
|
m_finishing = false; |
|
emit compositingToggled(false); |
|
} |
|
|
|
void Compositor::releaseCompositorSelection() |
|
{ |
|
if (hasScene() && !m_finishing) { |
|
// compositor is up and running again, no need to release the selection |
|
return; |
|
} |
|
if (m_starting) { |
|
// currently still starting the compositor, it might fail, so restart the timer to test again |
|
m_releaseSelectionTimer.start(); |
|
return; |
|
} |
|
|
|
if (m_finishing) { |
|
// still shutting down, a restart might follow, so restart the timer to test again |
|
m_releaseSelectionTimer.start(); |
|
return; |
|
} |
|
qCDebug(KWIN_CORE) << "Releasing compositor selection"; |
|
if (cm_selection) { |
|
cm_selection->owning = false; |
|
cm_selection->release(); |
|
} |
|
} |
|
|
|
void Compositor::keepSupportProperty(xcb_atom_t atom) |
|
{ |
|
m_unusedSupportProperties.removeAll(atom); |
|
} |
|
|
|
void Compositor::removeSupportProperty(xcb_atom_t atom) |
|
{ |
|
m_unusedSupportProperties << atom; |
|
m_unusedSupportPropertyTimer.start(); |
|
} |
|
|
|
void Compositor::deleteUnusedSupportProperties() |
|
{ |
|
if (m_starting) { |
|
// currently still starting the compositor |
|
m_unusedSupportPropertyTimer.start(); |
|
return; |
|
} |
|
if (m_finishing) { |
|
// still shutting down, a restart might follow |
|
m_unusedSupportPropertyTimer.start(); |
|
return; |
|
} |
|
foreach (const xcb_atom_t &atom, m_unusedSupportProperties) { |
|
// remove property from root window |
|
xcb_delete_property(connection(), rootWindow(), atom); |
|
} |
|
} |
|
|
|
// OpenGL self-check failed, fallback to XRender |
|
void Compositor::fallbackToXRenderCompositing() |
|
{ |
|
finish(); |
|
KConfigGroup config(KSharedConfig::openConfig(), "Compositing"); |
|
config.writeEntry("Backend", "XRender"); |
|
config.sync(); |
|
options->setCompositingMode(XRenderCompositing); |
|
setup(); |
|
} |
|
|
|
void Compositor::slotConfigChanged() |
|
{ |
|
if (!m_suspended) { |
|
setup(); |
|
if (effects) // setupCompositing() may fail |
|
effects->reconfigure(); |
|
addRepaintFull(); |
|
} else |
|
finish(); |
|
} |
|
|
|
void Compositor::slotReinitialize() |
|
{ |
|
// Reparse config. Config options will be reloaded by setup() |
|
KSharedConfig::openConfig()->reparseConfiguration(); |
|
|
|
// Restart compositing |
|
finish(); |
|
// resume compositing if suspended |
|
m_suspended = NoReasonSuspend; |
|
options->setCompositingInitialized(false); |
|
setup(); |
|
|
|
if (effects) { // setup() may fail |
|
effects->reconfigure(); |
|
} |
|
} |
|
|
|
// for the shortcut |
|
void Compositor::slotToggleCompositing() |
|
{ |
|
if (kwinApp()->requiresCompositing()) { |
|
// we are not allowed to turn on/off compositing |
|
return; |
|
} |
|
if (m_suspended) { // direct user call; clear all bits |
|
resume(AllReasonSuspend); |
|
} else { // but only set the user one (sufficient to suspend) |
|
suspend(UserSuspend); |
|
} |
|
} |
|
|
|
void Compositor::updateCompositeBlocking() |
|
{ |
|
updateCompositeBlocking(NULL); |
|
} |
|
|
|
void Compositor::updateCompositeBlocking(Client *c) |
|
{ |
|
if (kwinApp()->requiresCompositing()) { |
|
return; |
|
} |
|
if (c) { // if c == 0 we just check if we can resume |
|
if (c->isBlockingCompositing()) { |
|
if (!(m_suspended & BlockRuleSuspend)) // do NOT attempt to call suspend(true); from within the eventchain! |
|
QMetaObject::invokeMethod(this, "suspend", Qt::QueuedConnection, Q_ARG(Compositor::SuspendReason, BlockRuleSuspend)); |
|
} |
|
} |
|
else if (m_suspended & BlockRuleSuspend) { // lost a client and we're blocked - can we resume? |
|
bool resume = true; |
|
for (ClientList::ConstIterator it = Workspace::self()->clientList().constBegin(); it != Workspace::self()->clientList().constEnd(); ++it) { |
|
if ((*it)->isBlockingCompositing()) { |
|
resume = false; |
|
break; |
|
} |
|
} |
|
if (resume) { // do NOT attempt to call suspend(false); from within the eventchain! |
|
QMetaObject::invokeMethod(this, "resume", Qt::QueuedConnection, Q_ARG(Compositor::SuspendReason, BlockRuleSuspend)); |
|
} |
|
} |
|
} |
|
|
|
void Compositor::suspend(Compositor::SuspendReason reason) |
|
{ |
|
if (kwinApp()->requiresCompositing()) { |
|
return; |
|
} |
|
Q_ASSERT(reason != NoReasonSuspend); |
|
m_suspended |= reason; |
|
if (reason & KWin::Compositor::ScriptSuspend) { |
|
// when disabled show a shortcut how the user can get back compositing |
|
const auto shortcuts = KGlobalAccel::self()->shortcut(workspace()->findChild<QAction*>(QStringLiteral("Suspend Compositing"))); |
|
if (!shortcuts.isEmpty()) { |
|
// display notification only if there is the shortcut |
|
const QString message = i18n("Desktop effects have been suspended by another application.<br/>" |
|
"You can resume using the '%1' shortcut.", shortcuts.first().toString(QKeySequence::NativeText)); |
|
KNotification::event(QStringLiteral("compositingsuspendeddbus"), message); |
|
} |
|
} |
|
finish(); |
|
} |
|
|
|
void Compositor::resume(Compositor::SuspendReason reason) |
|
{ |
|
Q_ASSERT(reason != NoReasonSuspend); |
|
m_suspended &= ~reason; |
|
setup(); // signal "toggled" is eventually emitted from within setup |
|
} |
|
|
|
void Compositor::restart() |
|
{ |
|
if (hasScene()) { |
|
finish(); |
|
QTimer::singleShot(0, this, SLOT(setup())); |
|
} |
|
} |
|
|
|
void Compositor::addRepaint(int x, int y, int w, int h) |
|
{ |
|
if (!hasScene()) |
|
return; |
|
repaints_region += QRegion(x, y, w, h); |
|
scheduleRepaint(); |
|
} |
|
|
|
void Compositor::addRepaint(const QRect& r) |
|
{ |
|
if (!hasScene()) |
|
return; |
|
repaints_region += r; |
|
scheduleRepaint(); |
|
} |
|
|
|
void Compositor::addRepaint(const QRegion& r) |
|
{ |
|
if (!hasScene()) |
|
return; |
|
repaints_region += r; |
|
scheduleRepaint(); |
|
} |
|
|
|
void Compositor::addRepaintFull() |
|
{ |
|
if (!hasScene()) |
|
return; |
|
const QSize &s = screens()->size(); |
|
repaints_region = QRegion(0, 0, s.width(), s.height()); |
|
scheduleRepaint(); |
|
} |
|
|
|
void Compositor::timerEvent(QTimerEvent *te) |
|
{ |
|
if (te->timerId() == compositeTimer.timerId()) { |
|
performCompositing(); |
|
} else |
|
QObject::timerEvent(te); |
|
} |
|
|
|
void Compositor::aboutToSwapBuffers() |
|
{ |
|
assert(!m_bufferSwapPending); |
|
|
|
m_bufferSwapPending = true; |
|
} |
|
|
|
void Compositor::bufferSwapComplete() |
|
{ |
|
assert(m_bufferSwapPending); |
|
m_bufferSwapPending = false; |
|
|
|
if (m_composeAtSwapCompletion) { |
|
m_composeAtSwapCompletion = false; |
|
performCompositing(); |
|
} |
|
} |
|
|
|
void Compositor::performCompositing() |
|
{ |
|
if (m_scene->usesOverlayWindow() && !isOverlayWindowVisible()) |
|
return; // nothing is visible anyway |
|
|
|
// If a buffer swap is still pending, we return to the event loop and |
|
// continue processing events until the swap has completed. |
|
if (m_bufferSwapPending) { |
|
m_composeAtSwapCompletion = true; |
|
compositeTimer.stop(); |
|
return; |
|
} |
|
|
|
// If outputs are disabled, we return to the event loop and |
|
// continue processing events until the outputs are enabled again |
|
if (waylandServer() && !waylandServer()->backend()->areOutputsEnabled()) { |
|
compositeTimer.stop(); |
|
return; |
|
} |
|
|
|
// Create a list of all windows in the stacking order |
|
ToplevelList windows = Workspace::self()->xStackingOrder(); |
|
ToplevelList damaged; |
|
|
|
// Reset the damage state of each window and fetch the damage region |
|
// without waiting for a reply |
|
foreach (Toplevel *win, windows) { |
|
if (win->resetAndFetchDamage()) |
|
damaged << win; |
|
} |
|
|
|
if (damaged.count() > 0) { |
|
m_scene->triggerFence(); |
|
xcb_flush(connection()); |
|
} |
|
|
|
// Move elevated windows to the top of the stacking order |
|
foreach (EffectWindow *c, static_cast<EffectsHandlerImpl *>(effects)->elevatedWindows()) { |
|
Toplevel* t = static_cast< EffectWindowImpl* >(c)->window(); |
|
windows.removeAll(t); |
|
windows.append(t); |
|
} |
|
|
|
// Get the replies |
|
foreach (Toplevel *win, damaged) { |
|
// Discard the cached lanczos texture |
|
if (win->effectWindow()) { |
|
const QVariant texture = win->effectWindow()->data(LanczosCacheRole); |
|
if (texture.isValid()) { |
|
delete static_cast<GLTexture *>(texture.value<void*>()); |
|
win->effectWindow()->setData(LanczosCacheRole, QVariant()); |
|
} |
|
} |
|
|
|
win->getDamageRegionReply(); |
|
} |
|
|
|
if (repaints_region.isEmpty() && !windowRepaintsPending()) { |
|
m_scene->idle(); |
|
m_timeSinceLastVBlank = fpsInterval - (options->vBlankTime() + 1); // means "start now" |
|
m_timeSinceStart += m_timeSinceLastVBlank; |
|
// Note: It would seem here we should undo suspended unredirect, but when scenes need |
|
// it for some reason, e.g. transformations or translucency, the next pass that does not |
|
// need this anymore and paints normally will also reset the suspended unredirect. |
|
// Otherwise the window would not be painted normally anyway. |
|
compositeTimer.stop(); |
|
return; |
|
} |
|
|
|
// skip windows that are not yet ready for being painted |
|
// TODO ? |
|
// this cannot be used so carelessly - needs protections against broken clients, the window |
|
// should not get focus before it's displayed, handle unredirected windows properly and so on. |
|
foreach (Toplevel *t, windows) |
|
if (!t->readyForPainting()) |
|
windows.removeAll(t); |
|
|
|
QRegion repaints = repaints_region; |
|
// clear all repaints, so that post-pass can add repaints for the next repaint |
|
repaints_region = QRegion(); |
|
|
|
m_timeSinceLastVBlank = m_scene->paint(repaints, windows); |
|
m_timeSinceStart += m_timeSinceLastVBlank; |
|
|
|
if (kwinApp()->shouldUseWaylandForCompositing()) { |
|
for (Toplevel *win : damaged) { |
|
if (auto surface = win->surface()) { |
|
surface->frameRendered(m_timeSinceStart); |
|
} |
|
} |
|
} |
|
|
|
compositeTimer.stop(); // stop here to ensure *we* cause the next repaint schedule - not some effect through m_scene->paint() |
|
|
|
// Trigger at least one more pass even if there would be nothing to paint, so that scene->idle() |
|
// is called the next time. If there would be nothing pending, it will not restart the timer and |
|
// scheduleRepaint() would restart it again somewhen later, called from functions that |
|
// would again add something pending. |
|
if (m_bufferSwapPending && m_scene->syncsToVBlank()) { |
|
m_composeAtSwapCompletion = true; |
|
} else { |
|
scheduleRepaint(); |
|
} |
|
} |
|
|
|
bool Compositor::windowRepaintsPending() const |
|
{ |
|
foreach (Toplevel * c, Workspace::self()->clientList()) |
|
if (!c->repaints().isEmpty()) |
|
return true; |
|
foreach (Toplevel * c, Workspace::self()->desktopList()) |
|
if (!c->repaints().isEmpty()) |
|
return true; |
|
foreach (Toplevel * c, Workspace::self()->unmanagedList()) |
|
if (!c->repaints().isEmpty()) |
|
return true; |
|
foreach (Toplevel * c, Workspace::self()->deletedList()) |
|
if (!c->repaints().isEmpty()) |
|
return true; |
|
if (auto w = waylandServer()) { |
|
const auto &clients = w->clients(); |
|
for (auto c : clients) { |
|
if (c->isShown(true) && !c->repaints().isEmpty()) { |
|
return true; |
|
} |
|
} |
|
const auto &internalClients = w->internalClients(); |
|
for (auto c : internalClients) { |
|
if (c->isShown(true) && !c->repaints().isEmpty()) { |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
void Compositor::setCompositeResetTimer(int msecs) |
|
{ |
|
compositeResetTimer.start(msecs); |
|
} |
|
|
|
void Compositor::setCompositeTimer() |
|
{ |
|
if (!hasScene()) // should not really happen, but there may be e.g. some damage events still pending |
|
return; |
|
if (!Workspace::self()) { |
|
return; |
|
} |
|
|
|
// Don't start the timer if we're waiting for a swap event |
|
if (m_bufferSwapPending && m_composeAtSwapCompletion) |
|
return; |
|
|
|
// Don't start the timer if all outputs are disabled |
|
if (waylandServer() && !waylandServer()->backend()->areOutputsEnabled()) { |
|
return; |
|
} |
|
|
|
uint waitTime = 1; |
|
|
|
if (m_scene->blocksForRetrace()) { |
|
|
|
// TODO: make vBlankTime dynamic?! |
|
// It's required because glXWaitVideoSync will *likely* block a full frame if one enters |
|
// a retrace pass which can last a variable amount of time, depending on the actual screen |
|
// Now, my ooold 19" CRT can do such retrace so that 2ms are entirely sufficient, |
|
// while another ooold 15" TFT requires about 6ms |
|
|
|
qint64 padding = m_timeSinceLastVBlank; |
|
if (padding > fpsInterval) { |
|
// we're at low repaints or spent more time in painting than the user wanted to wait for that frame |
|
padding = vBlankInterval - (padding%vBlankInterval); // -> align to next vblank |
|
} else { // -> align to the next maxFps tick |
|
padding = ((vBlankInterval - padding%vBlankInterval) + (fpsInterval/vBlankInterval-1)*vBlankInterval); |
|
// "remaining time of the first vsync" + "time for the other vsyncs of the frame" |
|
} |
|
|
|
if (padding < options->vBlankTime()) { // we'll likely miss this frame |
|
waitTime = nanoToMilli(padding + vBlankInterval - options->vBlankTime()); // so we add one |
|
} else { |
|
waitTime = nanoToMilli(padding - options->vBlankTime()); |
|
} |
|
} |
|
else { // w/o blocking vsync we just jump to the next demanded tick |
|
if (fpsInterval > m_timeSinceLastVBlank) { |
|
waitTime = nanoToMilli(fpsInterval - m_timeSinceLastVBlank); |
|
if (!waitTime) { |
|
waitTime = 1; // will ensure we don't block out the eventloop - the system's just not faster ... |
|
} |
|
}/* else if (m_scene->syncsToVBlank() && m_timeSinceLastVBlank - fpsInterval < (vBlankInterval<<1)) { |
|
// NOTICE - "for later" ------------------------------------------------------------------ |
|
// It can happen that we push two frames within one refresh cycle. |
|
// Swapping will then block even with triple buffering when the GPU does not discard but |
|
// queues frames |
|
// now here's the mean part: if we take that as "OMG, we're late - next frame ASAP", |
|
// there'll immediately be 2 frames in the pipe, swapping will block, we think we're |
|
// late ... ewww |
|
// so instead we pad to the clock again and add 2ms safety to ensure the pipe is really |
|
// free |
|
// NOTICE: obviously m_timeSinceLastVBlank can be too big because we're too slow as well |
|
// So if this code was enabled, we'd needlessly half the framerate once more (15 instead of 30) |
|
waitTime = nanoToMilli(vBlankInterval - (m_timeSinceLastVBlank - fpsInterval)%vBlankInterval) + 2; |
|
}*/ else { |
|
waitTime = 1; // ... "0" would be sufficient, but the compositor isn't the WMs only task |
|
} |
|
} |
|
compositeTimer.start(qMin(waitTime, 250u), this); // force 4fps minimum |
|
} |
|
|
|
bool Compositor::isActive() |
|
{ |
|
return !m_finishing && hasScene(); |
|
} |
|
|
|
void Compositor::checkUnredirect() |
|
{ |
|
checkUnredirect(false); |
|
} |
|
|
|
// force is needed when the list of windows changes (e.g. a window goes away) |
|
void Compositor::checkUnredirect(bool force) |
|
{ |
|
if (!hasScene() || !m_scene->overlayWindow() || m_scene->overlayWindow()->window() == None || !options->isUnredirectFullscreen()) |
|
return; |
|
if (force) |
|
forceUnredirectCheck = true; |
|
if (!unredirectTimer.isActive()) |
|
unredirectTimer.start(0); |
|
} |
|
|
|
void Compositor::delayedCheckUnredirect() |
|
{ |
|
if (!hasScene() || !m_scene->overlayWindow() || m_scene->overlayWindow()->window() == None || !(options->isUnredirectFullscreen() || sender() == options)) |
|
return; |
|
ToplevelList list; |
|
bool changed = forceUnredirectCheck; |
|
foreach (Client * c, Workspace::self()->clientList()) |
|
list.append(c); |
|
foreach (Unmanaged * c, Workspace::self()->unmanagedList()) |
|
list.append(c); |
|
foreach (Toplevel * c, list) { |
|
if (c->updateUnredirectedState()) { |
|
changed = true; |
|
break; |
|
} |
|
} |
|
// no desktops, no Deleted ones |
|
if (!changed) |
|
return; |
|
forceUnredirectCheck = false; |
|
// Cut out parts from the overlay window where unredirected windows are, |
|
// so that they are actually visible. |
|
const QSize &s = screens()->size(); |
|
QRegion reg(0, 0, s.width(), s.height()); |
|
foreach (Toplevel * c, list) { |
|
if (c->unredirected()) |
|
reg -= c->geometry(); |
|
} |
|
m_scene->overlayWindow()->setShape(reg); |
|
addRepaint(reg); |
|
} |
|
|
|
bool Compositor::checkForOverlayWindow(WId w) const |
|
{ |
|
if (!hasScene()) { |
|
// no scene, so it cannot be the overlay window |
|
return false; |
|
} |
|
if (!m_scene->overlayWindow()) { |
|
// no overlay window, it cannot be the overlay |
|
return false; |
|
} |
|
// and compare the window ID's |
|
return w == m_scene->overlayWindow()->window(); |
|
} |
|
|
|
WId Compositor::overlayWindow() const |
|
{ |
|
if (!hasScene()) { |
|
return None; |
|
} |
|
if (!m_scene->overlayWindow()) { |
|
return None; |
|
} |
|
return m_scene->overlayWindow()->window(); |
|
} |
|
|
|
bool Compositor::isOverlayWindowVisible() const |
|
{ |
|
if (!hasScene()) { |
|
return false; |
|
} |
|
if (!m_scene->overlayWindow()) { |
|
return false; |
|
} |
|
return m_scene->overlayWindow()->isVisible(); |
|
} |
|
|
|
void Compositor::setOverlayWindowVisibility(bool visible) |
|
{ |
|
if (hasScene() && m_scene->overlayWindow()) { |
|
m_scene->overlayWindow()->setVisibility(visible); |
|
} |
|
} |
|
|
|
/***************************************************** |
|
* Workspace |
|
****************************************************/ |
|
|
|
bool Workspace::compositing() const |
|
{ |
|
return m_compositor && m_compositor->hasScene(); |
|
} |
|
|
|
//**************************************** |
|
// Toplevel |
|
//**************************************** |
|
|
|
bool Toplevel::setupCompositing() |
|
{ |
|
if (!compositing()) |
|
return false; |
|
|
|
if (damage_handle != XCB_NONE) |
|
return false; |
|
|
|
if (kwinApp()->operationMode() == Application::OperationModeX11) { |
|
damage_handle = xcb_generate_id(connection()); |
|
xcb_damage_create(connection(), damage_handle, frameId(), XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY); |
|
} |
|
|
|
damage_region = QRegion(0, 0, width(), height()); |
|
effect_window = new EffectWindowImpl(this); |
|
unredirect = false; |
|
|
|
Compositor::self()->checkUnredirect(true); |
|
Compositor::self()->scene()->windowAdded(this); |
|
|
|
// With unmanaged windows there is a race condition between the client painting the window |
|
// and us setting up damage tracking. If the client wins we won't get a damage event even |
|
// though the window has been painted. To avoid this we mark the whole window as damaged |
|
// and schedule a repaint immediately after creating the damage object. |
|
if (dynamic_cast<Unmanaged*>(this)) |
|
addDamageFull(); |
|
|
|
return true; |
|
} |
|
|
|
void Toplevel::finishCompositing(ReleaseReason releaseReason) |
|
{ |
|
if (kwinApp()->operationMode() == Application::OperationModeX11 && damage_handle == XCB_NONE) |
|
return; |
|
Compositor::self()->checkUnredirect(true); |
|
if (effect_window->window() == this) { // otherwise it's already passed to Deleted, don't free data |
|
discardWindowPixmap(); |
|
delete effect_window; |
|
} |
|
|
|
if (kwinApp()->operationMode() == Application::OperationModeX11 && |
|
releaseReason != ReleaseReason::Destroyed) { |
|
xcb_damage_destroy(connection(), damage_handle); |
|
} |
|
|
|
damage_handle = XCB_NONE; |
|
damage_region = QRegion(); |
|
repaints_region = QRegion(); |
|
effect_window = NULL; |
|
} |
|
|
|
void Toplevel::discardWindowPixmap() |
|
{ |
|
addDamageFull(); |
|
if (effectWindow() != NULL && effectWindow()->sceneWindow() != NULL) |
|
effectWindow()->sceneWindow()->pixmapDiscarded(); |
|
} |
|
|
|
void Toplevel::damageNotifyEvent() |
|
{ |
|
m_isDamaged = true; |
|
|
|
// Note: The rect is supposed to specify the damage extents, |
|
// but we don't know it at this point. No one who connects |
|
// to this signal uses the rect however. |
|
emit damaged(this, QRect()); |
|
} |
|
|
|
bool Toplevel::compositing() const |
|
{ |
|
return Workspace::self()->compositing(); |
|
} |
|
|
|
void Client::damageNotifyEvent() |
|
{ |
|
if (syncRequest.isPending && isResize()) { |
|
emit damaged(this, QRect()); |
|
m_isDamaged = true; |
|
return; |
|
} |
|
|
|
if (!ready_for_painting) { // avoid "setReadyForPainting()" function calling overhead |
|
if (syncRequest.counter == XCB_NONE) { // cannot detect complete redraw, consider done now |
|
setReadyForPainting(); |
|
setupWindowManagementInterface(); |
|
} |
|
} |
|
|
|
Toplevel::damageNotifyEvent(); |
|
} |
|
|
|
bool Toplevel::resetAndFetchDamage() |
|
{ |
|
if (!m_isDamaged) |
|
return false; |
|
|
|
if (kwinApp()->operationMode() != Application::OperationModeX11) { |
|
m_isDamaged = false; |
|
return true; |
|
} |
|
|
|
xcb_connection_t *conn = connection(); |
|
|
|
// Create a new region and copy the damage region to it, |
|
// resetting the damaged state. |
|
xcb_xfixes_region_t region = xcb_generate_id(conn); |
|
xcb_xfixes_create_region(conn, region, 0, 0); |
|
xcb_damage_subtract(conn, damage_handle, 0, region); |
|
|
|
// Send a fetch-region request and destroy the region |
|
m_regionCookie = xcb_xfixes_fetch_region_unchecked(conn, region); |
|
xcb_xfixes_destroy_region(conn, region); |
|
|
|
m_isDamaged = false; |
|
m_damageReplyPending = true; |
|
|
|
return m_damageReplyPending; |
|
} |
|
|
|
void Toplevel::getDamageRegionReply() |
|
{ |
|
if (!m_damageReplyPending) |
|
return; |
|
|
|
m_damageReplyPending = false; |
|
|
|
// Get the fetch-region reply |
|
xcb_xfixes_fetch_region_reply_t *reply = |
|
xcb_xfixes_fetch_region_reply(connection(), m_regionCookie, 0); |
|
|
|
if (!reply) |
|
return; |
|
|
|
// Convert the reply to a QRegion |
|
int count = xcb_xfixes_fetch_region_rectangles_length(reply); |
|
QRegion region; |
|
|
|
if (count > 1 && count < 16) { |
|
xcb_rectangle_t *rects = xcb_xfixes_fetch_region_rectangles(reply); |
|
|
|
QVector<QRect> qrects; |
|
qrects.reserve(count); |
|
|
|
for (int i = 0; i < count; i++) |
|
qrects << QRect(rects[i].x, rects[i].y, rects[i].width, rects[i].height); |
|
|
|
region.setRects(qrects.constData(), count); |
|
} else |
|
region += QRect(reply->extents.x, reply->extents.y, |
|
reply->extents.width, reply->extents.height); |
|
|
|
damage_region += region; |
|
repaints_region += region; |
|
|
|
free(reply); |
|
} |
|
|
|
void Toplevel::addDamageFull() |
|
{ |
|
if (!compositing()) |
|
return; |
|
|
|
damage_region = rect(); |
|
repaints_region |= rect(); |
|
|
|
emit damaged(this, rect()); |
|
} |
|
|
|
void Toplevel::resetDamage() |
|
{ |
|
damage_region = QRegion(); |
|
} |
|
|
|
void Toplevel::addRepaint(const QRect& r) |
|
{ |
|
if (!compositing()) { |
|
return; |
|
} |
|
repaints_region += r; |
|
emit needsRepaint(); |
|
} |
|
|
|
void Toplevel::addRepaint(int x, int y, int w, int h) |
|
{ |
|
QRect r(x, y, w, h); |
|
addRepaint(r); |
|
} |
|
|
|
void Toplevel::addRepaint(const QRegion& r) |
|
{ |
|
if (!compositing()) { |
|
return; |
|
} |
|
repaints_region += r; |
|
emit needsRepaint(); |
|
} |
|
|
|
void Toplevel::addLayerRepaint(const QRect& r) |
|
{ |
|
if (!compositing()) { |
|
return; |
|
} |
|
layer_repaints_region += r; |
|
emit needsRepaint(); |
|
} |
|
|
|
void Toplevel::addLayerRepaint(int x, int y, int w, int h) |
|
{ |
|
QRect r(x, y, w, h); |
|
addLayerRepaint(r); |
|
} |
|
|
|
void Toplevel::addLayerRepaint(const QRegion& r) |
|
{ |
|
if (!compositing()) |
|
return; |
|
layer_repaints_region += r; |
|
emit needsRepaint(); |
|
} |
|
|
|
void Toplevel::addRepaintFull() |
|
{ |
|
repaints_region = visibleRect().translated(-pos()); |
|
emit needsRepaint(); |
|
} |
|
|
|
void Toplevel::resetRepaints() |
|
{ |
|
repaints_region = QRegion(); |
|
layer_repaints_region = QRegion(); |
|
} |
|
|
|
void Toplevel::addWorkspaceRepaint(int x, int y, int w, int h) |
|
{ |
|
addWorkspaceRepaint(QRect(x, y, w, h)); |
|
} |
|
|
|
void Toplevel::addWorkspaceRepaint(const QRect& r2) |
|
{ |
|
if (!compositing()) |
|
return; |
|
Compositor::self()->addRepaint(r2); |
|
} |
|
|
|
bool Toplevel::updateUnredirectedState() |
|
{ |
|
assert(compositing()); |
|
bool should = options->isUnredirectFullscreen() && shouldUnredirect() && !unredirectSuspend && |
|
!shape() && !hasAlpha() && opacity() == 1.0 && |
|
!static_cast<EffectsHandlerImpl*>(effects)->activeFullScreenEffect(); |
|
if (should == unredirect) |
|
return false; |
|
static QElapsedTimer lastUnredirect; |
|
static const qint64 msecRedirectInterval = 100; |
|
if (!lastUnredirect.hasExpired(msecRedirectInterval)) { |
|
QTimer::singleShot(msecRedirectInterval, Compositor::self(), SLOT(checkUnredirect())); |
|
return false; |
|
} |
|
lastUnredirect.start(); |
|
unredirect = should; |
|
if (unredirect) { |
|
qCDebug(KWIN_CORE) << "Unredirecting:" << this; |
|
xcb_composite_unredirect_window(connection(), frameId(), XCB_COMPOSITE_REDIRECT_MANUAL); |
|
} else { |
|
qCDebug(KWIN_CORE) << "Redirecting:" << this; |
|
xcb_composite_redirect_window(connection(), frameId(), XCB_COMPOSITE_REDIRECT_MANUAL); |
|
discardWindowPixmap(); |
|
} |
|
return true; |
|
} |
|
|
|
void Toplevel::suspendUnredirect(bool suspend) |
|
{ |
|
if (unredirectSuspend == suspend) |
|
return; |
|
unredirectSuspend = suspend; |
|
Compositor::self()->checkUnredirect(); |
|
} |
|
|
|
//**************************************** |
|
// Client |
|
//**************************************** |
|
|
|
bool Client::setupCompositing() |
|
{ |
|
if (!Toplevel::setupCompositing()){ |
|
return false; |
|
} |
|
if (isDecorated()) { |
|
decoratedClient()->destroyRenderer(); |
|
} |
|
updateVisibility(); // for internalKeep() |
|
return true; |
|
} |
|
|
|
void Client::finishCompositing(ReleaseReason releaseReason) |
|
{ |
|
Toplevel::finishCompositing(releaseReason); |
|
updateVisibility(); |
|
if (!deleting) { |
|
if (isDecorated()) { |
|
decoratedClient()->destroyRenderer(); |
|
} |
|
} |
|
// for safety in case KWin is just resizing the window |
|
s_haveResizeEffect = false; |
|
} |
|
|
|
bool Client::shouldUnredirect() const |
|
{ |
|
if (isActiveFullScreen()) { |
|
ToplevelList stacking = workspace()->xStackingOrder(); |
|
for (int pos = stacking.count() - 1; |
|
pos >= 0; |
|
--pos) { |
|
Toplevel* c = stacking.at(pos); |
|
if (c == this) // is not covered by any other window, ok to unredirect |
|
return true; |
|
if (c->geometry().intersects(geometry())) |
|
return false; |
|
} |
|
abort(); |
|
} |
|
return false; |
|
} |
|
|
|
|
|
//**************************************** |
|
// Unmanaged |
|
//**************************************** |
|
|
|
bool Unmanaged::shouldUnredirect() const |
|
{ |
|
// the pixmap is needed for the login effect, a nicer solution would be the login effect increasing |
|
// refcount for the window pixmap (which would prevent unredirect), avoiding this hack |
|
if (resourceClass() == "ksplashx" |
|
|| resourceClass() == "ksplashsimple" |
|
|| resourceClass() == "ksplashqml" |
|
) |
|
return false; |
|
// it must cover whole display or one xinerama screen, and be the topmost there |
|
const int desktop = VirtualDesktopManager::self()->current(); |
|
if (geometry() == workspace()->clientArea(FullArea, geometry().center(), desktop) |
|
|| geometry() == workspace()->clientArea(ScreenArea, geometry().center(), desktop)) { |
|
ToplevelList stacking = workspace()->xStackingOrder(); |
|
for (int pos = stacking.count() - 1; |
|
pos >= 0; |
|
--pos) { |
|
Toplevel* c = stacking.at(pos); |
|
if (c == this) // is not covered by any other window, ok to unredirect |
|
return true; |
|
if (c->geometry().intersects(geometry())) |
|
return false; |
|
} |
|
abort(); |
|
} |
|
return false; |
|
} |
|
|
|
//**************************************** |
|
// Deleted |
|
//**************************************** |
|
|
|
bool Deleted::shouldUnredirect() const |
|
{ |
|
return false; |
|
} |
|
|
|
|
|
} // namespace
|
|
|