1 | /******************************************************************** |
2 | KWin - the KDE window manager |
3 | This file is part of the KDE project. |
4 | |
5 | Copyright (C) 2006 Lubos Lunak <l.lunak@kde.org> |
6 | |
7 | This program is free software; you can redistribute it and/or modify |
8 | it under the terms of the GNU General Public License as published by |
9 | the Free Software Foundation; either version 2 of the License, or |
10 | (at your option) any later version. |
11 | |
12 | This program is distributed in the hope that it will be useful, |
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | GNU General Public License for more details. |
16 | |
17 | You should have received a copy of the GNU General Public License |
18 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | *********************************************************************/ |
20 | #include "composite.h" |
21 | #include "compositingadaptor.h" |
22 | |
23 | #include <config-X11.h> |
24 | |
25 | #include "utils.h" |
26 | #include <QTextStream> |
27 | #include "workspace.h" |
28 | #include "client.h" |
29 | #include "unmanaged.h" |
30 | #include "deleted.h" |
31 | #include "effects.h" |
32 | #include "overlaywindow.h" |
33 | #include "scene.h" |
34 | #include "scene_xrender.h" |
35 | #include "scene_opengl.h" |
36 | #include "shadow.h" |
37 | #include "useractions.h" |
38 | #include "compositingprefs.h" |
39 | #include "xcbutils.h" |
40 | |
41 | #include <stdio.h> |
42 | |
43 | #include <QtConcurrentRun> |
44 | #include <QFutureWatcher> |
45 | #include <QMenu> |
46 | #include <QTimerEvent> |
47 | #include <QDateTime> |
48 | #include <QDBusConnection> |
49 | #include <kaction.h> |
50 | #include <kactioncollection.h> |
51 | #include <KDE/KGlobal> |
52 | #include <KDE/KLocalizedString> |
53 | #include <KDE/KNotification> |
54 | #include <KDE/KSelectionWatcher> |
55 | |
56 | #include <xcb/composite.h> |
57 | #include <xcb/damage.h> |
58 | |
59 | Q_DECLARE_METATYPE(KWin::Compositor::SuspendReason) |
60 | |
61 | namespace KWin |
62 | { |
63 | |
64 | extern int currentRefreshRate(); |
65 | |
66 | CompositorSelectionOwner::CompositorSelectionOwner(const char *selection) : KSelectionOwner(selection), owning(false) |
67 | { |
68 | connect (this, SIGNAL(lostOwnership()), SLOT(looseOwnership())); |
69 | } |
70 | |
71 | void CompositorSelectionOwner::looseOwnership() |
72 | { |
73 | owning = false; |
74 | } |
75 | |
76 | KWIN_SINGLETON_FACTORY_VARIABLE(Compositor, s_compositor) |
77 | |
78 | static inline qint64 milliToNano(int milli) { return milli * 1000 * 1000; } |
79 | static inline qint64 nanoToMilli(int nano) { return nano / (1000*1000); } |
80 | |
81 | Compositor::Compositor(QObject* workspace) |
82 | : QObject(workspace) |
83 | , m_suspended(options->isUseCompositing() ? NoReasonSuspend : UserSuspend) |
84 | , cm_selection(NULL) |
85 | , vBlankInterval(0) |
86 | , fpsInterval(0) |
87 | , m_xrrRefreshRate(0) |
88 | , forceUnredirectCheck(false) |
89 | , m_finishing(false) |
90 | , m_timeSinceLastVBlank(0) |
91 | , m_scene(NULL) |
92 | { |
93 | qRegisterMetaType<Compositor::SuspendReason>("Compositor::SuspendReason" ); |
94 | new CompositingAdaptor(this); |
95 | QDBusConnection dbus = QDBusConnection::sessionBus(); |
96 | dbus.registerObject("/Compositor" , this); |
97 | dbus.registerService("org.kde.kwin.Compositing" ); |
98 | connect(&unredirectTimer, SIGNAL(timeout()), SLOT(delayedCheckUnredirect())); |
99 | connect(&compositeResetTimer, SIGNAL(timeout()), SLOT(restart())); |
100 | connect(workspace, SIGNAL(configChanged()), SLOT(slotConfigChanged())); |
101 | connect(options, SIGNAL(unredirectFullscreenChanged()), SLOT(delayedCheckUnredirect())); |
102 | unredirectTimer.setSingleShot(true); |
103 | compositeResetTimer.setSingleShot(true); |
104 | nextPaintReference.invalidate(); // Initialize the timer |
105 | |
106 | // 2 sec which should be enough to restart the compositor |
107 | static const int compositorLostMessageDelay = 2000; |
108 | |
109 | m_releaseSelectionTimer.setSingleShot(true); |
110 | m_releaseSelectionTimer.setInterval(compositorLostMessageDelay); |
111 | connect(&m_releaseSelectionTimer, SIGNAL(timeout()), SLOT(releaseCompositorSelection())); |
112 | |
113 | m_unusedSupportPropertyTimer.setInterval(compositorLostMessageDelay); |
114 | m_unusedSupportPropertyTimer.setSingleShot(true); |
115 | connect(&m_unusedSupportPropertyTimer, SIGNAL(timeout()), SLOT(deleteUnusedSupportProperties())); |
116 | |
117 | // delay the call to setup by one event cycle |
118 | // The ctor of this class is invoked from the Workspace ctor, that means before |
119 | // Workspace is completely constructed, so calling Workspace::self() would result |
120 | // in undefined behavior. This is fixed by using a delayed invocation. |
121 | QMetaObject::invokeMethod(this, "setup" , Qt::QueuedConnection); |
122 | } |
123 | |
124 | Compositor::~Compositor() |
125 | { |
126 | finish(); |
127 | deleteUnusedSupportProperties(); |
128 | delete cm_selection; |
129 | s_compositor = NULL; |
130 | } |
131 | |
132 | |
133 | void Compositor::setup() |
134 | { |
135 | if (hasScene()) |
136 | return; |
137 | if (m_suspended) { |
138 | kDebug(1212) << "Compositing is suspended, reason:" << m_suspended; |
139 | return; |
140 | } else if (!CompositingPrefs::compositingPossible()) { |
141 | kError(1212) << "Compositing is not possible" ; |
142 | return; |
143 | } |
144 | m_starting = true; |
145 | |
146 | if (!options->isCompositingInitialized()) { |
147 | #ifndef KWIN_HAVE_OPENGLES |
148 | // options->reloadCompositingSettings(true) initializes the CompositingPrefs which calls an |
149 | // external program in turn |
150 | // run this in an external thread to make startup faster. |
151 | QFutureWatcher<void> *compositingPrefsFuture = new QFutureWatcher<void>(); |
152 | connect(compositingPrefsFuture, SIGNAL(finished()), this, SLOT(slotCompositingOptionsInitialized())); |
153 | connect(compositingPrefsFuture, SIGNAL(finished()), compositingPrefsFuture, SLOT(deleteLater())); |
154 | compositingPrefsFuture->setFuture(QtConcurrent::run(options, &Options::reloadCompositingSettings, true)); |
155 | #else |
156 | // OpenGL ES does not call the external program, so no need to create a thread |
157 | options->reloadCompositingSettings(true); |
158 | slotCompositingOptionsInitialized(); |
159 | #endif |
160 | } else { |
161 | slotCompositingOptionsInitialized(); |
162 | } |
163 | } |
164 | |
165 | extern int screen_number; // main.cpp |
166 | extern bool is_multihead; |
167 | |
168 | void Compositor::slotCompositingOptionsInitialized() |
169 | { |
170 | char selection_name[ 100 ]; |
171 | sprintf(selection_name, "_NET_WM_CM_S%d" , DefaultScreen(display())); |
172 | if (!cm_selection) { |
173 | cm_selection = new CompositorSelectionOwner(selection_name); |
174 | connect(cm_selection, SIGNAL(lostOwnership()), SLOT(finish())); |
175 | } |
176 | if (!cm_selection->owning) { |
177 | cm_selection->claim(true); // force claiming |
178 | cm_selection->owning = true; |
179 | } |
180 | |
181 | // There might still be a deleted around, needs to be cleared before creating the scene (BUG 333275) |
182 | while (!Workspace::self()->deletedList().isEmpty()) { |
183 | Workspace::self()->deletedList().first()->discard(); |
184 | } |
185 | |
186 | switch(options->compositingMode()) { |
187 | case OpenGLCompositing: { |
188 | kDebug(1212) << "Initializing OpenGL compositing" ; |
189 | |
190 | // Some broken drivers crash on glXQuery() so to prevent constant KWin crashes: |
191 | KSharedConfigPtr unsafeConfigPtr = KGlobal::config(); |
192 | KConfigGroup unsafeConfig(unsafeConfigPtr, "Compositing" ); |
193 | const QString openGLIsUnsafe = "OpenGLIsUnsafe" + (is_multihead ? QString::number(screen_number) : "" ); |
194 | if (unsafeConfig.readEntry(openGLIsUnsafe, false)) |
195 | kWarning(1212) << "KWin has detected that your OpenGL library is unsafe to use" ; |
196 | else { |
197 | unsafeConfig.writeEntry(openGLIsUnsafe, true); |
198 | unsafeConfig.sync(); |
199 | #ifndef KWIN_HAVE_OPENGLES |
200 | if (!CompositingPrefs::hasGlx()) { |
201 | unsafeConfig.writeEntry(openGLIsUnsafe, false); |
202 | unsafeConfig.sync(); |
203 | kDebug(1212) << "No glx extensions available" ; |
204 | break; |
205 | } |
206 | #endif |
207 | |
208 | m_scene = SceneOpenGL::createScene(); |
209 | connect(m_scene, SIGNAL(resetCompositing()), SLOT(restart())); |
210 | |
211 | // TODO: Add 30 second delay to protect against screen freezes as well |
212 | unsafeConfig.writeEntry(openGLIsUnsafe, false); |
213 | unsafeConfig.sync(); |
214 | |
215 | if (m_scene && !m_scene->initFailed()) |
216 | break; // --> |
217 | delete m_scene; |
218 | m_scene = NULL; |
219 | } |
220 | |
221 | // Do not Fall back to XRender - it causes problems when selfcheck fails during startup, but works later on |
222 | break; |
223 | } |
224 | #ifdef KWIN_HAVE_XRENDER_COMPOSITING |
225 | case XRenderCompositing: |
226 | kDebug(1212) << "Initializing XRender compositing" ; |
227 | m_scene = new SceneXrender(Workspace::self()); |
228 | break; |
229 | #endif |
230 | default: |
231 | kDebug(1212) << "No compositing enabled" ; |
232 | m_starting = false; |
233 | cm_selection->owning = false; |
234 | cm_selection->release(); |
235 | return; |
236 | } |
237 | if (m_scene == NULL || m_scene->initFailed()) { |
238 | kError(1212) << "Failed to initialize compositing, compositing disabled" ; |
239 | kError(1212) << "Consult http://techbase.kde.org/Projects/KWin/4.0-release-notes#Setting_up" ; |
240 | delete m_scene; |
241 | m_scene = NULL; |
242 | m_starting = false; |
243 | cm_selection->owning = false; |
244 | cm_selection->release(); |
245 | return; |
246 | } |
247 | m_xrrRefreshRate = KWin::currentRefreshRate(); |
248 | fpsInterval = options->maxFpsInterval(); |
249 | if (m_scene->syncsToVBlank()) { // if we do vsync, set the fps to the next multiple of the vblank rate |
250 | vBlankInterval = milliToNano(1000) / m_xrrRefreshRate; |
251 | fpsInterval = qMax((fpsInterval / vBlankInterval) * vBlankInterval, vBlankInterval); |
252 | } else |
253 | vBlankInterval = milliToNano(1); // no sync - DO NOT set "0", would cause div-by-zero segfaults. |
254 | m_timeSinceLastVBlank = fpsInterval - (options->vBlankTime() + 1); // means "start now" - we don't have even a slight idea when the first vsync will occur |
255 | scheduleRepaint(); |
256 | xcb_composite_redirect_subwindows(connection(), rootWindow(), XCB_COMPOSITE_REDIRECT_MANUAL); |
257 | new EffectsHandlerImpl(this, m_scene); // sets also the 'effects' pointer |
258 | connect(effects, SIGNAL(screenGeometryChanged(QSize)), SLOT(addRepaintFull())); |
259 | addRepaintFull(); |
260 | foreach (Client * c, Workspace::self()->clientList()) { |
261 | c->setupCompositing(); |
262 | c->getShadow(); |
263 | } |
264 | foreach (Client * c, Workspace::self()->desktopList()) |
265 | c->setupCompositing(); |
266 | foreach (Unmanaged * c, Workspace::self()->unmanagedList()) { |
267 | c->setupCompositing(); |
268 | c->getShadow(); |
269 | } |
270 | |
271 | emit compositingToggled(true); |
272 | |
273 | m_starting = false; |
274 | if (m_releaseSelectionTimer.isActive()) { |
275 | m_releaseSelectionTimer.stop(); |
276 | } |
277 | |
278 | // render at least once |
279 | performCompositing(); |
280 | } |
281 | |
282 | void Compositor::scheduleRepaint() |
283 | { |
284 | if (!compositeTimer.isActive()) |
285 | setCompositeTimer(); |
286 | } |
287 | |
288 | void Compositor::finish() |
289 | { |
290 | if (!hasScene()) |
291 | return; |
292 | m_finishing = true; |
293 | m_releaseSelectionTimer.start(); |
294 | foreach (Client * c, Workspace::self()->clientList()) |
295 | m_scene->windowClosed(c, NULL); |
296 | foreach (Client * c, Workspace::self()->desktopList()) |
297 | m_scene->windowClosed(c, NULL); |
298 | foreach (Unmanaged * c, Workspace::self()->unmanagedList()) |
299 | m_scene->windowClosed(c, NULL); |
300 | foreach (Deleted * c, Workspace::self()->deletedList()) |
301 | m_scene->windowDeleted(c); |
302 | foreach (Client * c, Workspace::self()->clientList()) |
303 | c->finishCompositing(); |
304 | foreach (Client * c, Workspace::self()->desktopList()) |
305 | c->finishCompositing(); |
306 | foreach (Unmanaged * c, Workspace::self()->unmanagedList()) |
307 | c->finishCompositing(); |
308 | foreach (Deleted * c, Workspace::self()->deletedList()) |
309 | c->finishCompositing(); |
310 | xcb_composite_unredirect_subwindows(connection(), rootWindow(), XCB_COMPOSITE_REDIRECT_MANUAL); |
311 | delete effects; |
312 | effects = NULL; |
313 | delete m_scene; |
314 | m_scene = NULL; |
315 | compositeTimer.stop(); |
316 | repaints_region = QRegion(); |
317 | for (ClientList::ConstIterator it = Workspace::self()->clientList().constBegin(); |
318 | it != Workspace::self()->clientList().constEnd(); |
319 | ++it) { |
320 | // forward all opacity values to the frame in case there'll be other CM running |
321 | if ((*it)->opacity() != 1.0) { |
322 | NETWinInfo2 i(display(), (*it)->frameId(), rootWindow(), 0); |
323 | i.setOpacity(static_cast< unsigned long >((*it)->opacity() * 0xffffffff)); |
324 | } |
325 | } |
326 | // discard all Deleted windows (#152914) |
327 | while (!Workspace::self()->deletedList().isEmpty()) |
328 | Workspace::self()->deletedList().first()->discard(); |
329 | m_finishing = false; |
330 | emit compositingToggled(false); |
331 | } |
332 | |
333 | void Compositor::releaseCompositorSelection() |
334 | { |
335 | if (hasScene() && !m_finishing) { |
336 | // compositor is up and running again, no need to release the selection |
337 | return; |
338 | } |
339 | if (m_starting) { |
340 | // currently still starting the compositor, it might fail, so restart the timer to test again |
341 | m_releaseSelectionTimer.start(); |
342 | return; |
343 | } |
344 | |
345 | if (m_finishing) { |
346 | // still shutting down, a restart might follow, so restart the timer to test again |
347 | m_releaseSelectionTimer.start(); |
348 | return; |
349 | } |
350 | kDebug(1212) << "Releasing compositor selection" ; |
351 | cm_selection->owning = false; |
352 | cm_selection->release(); |
353 | } |
354 | |
355 | void Compositor::keepSupportProperty(xcb_atom_t atom) |
356 | { |
357 | m_unusedSupportProperties.removeAll(atom); |
358 | } |
359 | |
360 | void Compositor::removeSupportProperty(xcb_atom_t atom) |
361 | { |
362 | m_unusedSupportProperties << atom; |
363 | m_unusedSupportPropertyTimer.start(); |
364 | } |
365 | |
366 | void Compositor::deleteUnusedSupportProperties() |
367 | { |
368 | if (m_starting) { |
369 | // currently still starting the compositor |
370 | m_unusedSupportPropertyTimer.start(); |
371 | return; |
372 | } |
373 | if (m_finishing) { |
374 | // still shutting down, a restart might follow |
375 | m_unusedSupportPropertyTimer.start(); |
376 | return; |
377 | } |
378 | foreach (const xcb_atom_t &atom, m_unusedSupportProperties) { |
379 | // remove property from root window |
380 | XDeleteProperty(QX11Info::display(), rootWindow(), atom); |
381 | } |
382 | } |
383 | |
384 | // OpenGL self-check failed, fallback to XRender |
385 | void Compositor::fallbackToXRenderCompositing() |
386 | { |
387 | finish(); |
388 | KConfigGroup config(KGlobal::config(), "Compositing" ); |
389 | config.writeEntry("Backend" , "XRender" ); |
390 | config.writeEntry("GraphicsSystem" , "native" ); |
391 | config.sync(); |
392 | if (Extensions::nonNativePixmaps()) { // must restart to change the graphicssystem |
393 | restartKWin("automatic graphicssystem change for XRender backend" ); |
394 | return; |
395 | } else { |
396 | options->setCompositingMode(XRenderCompositing); |
397 | setup(); |
398 | } |
399 | } |
400 | |
401 | void Compositor::slotConfigChanged() |
402 | { |
403 | if (!m_suspended) { |
404 | setup(); |
405 | if (effects) // setupCompositing() may fail |
406 | effects->reconfigure(); |
407 | addRepaintFull(); |
408 | } else |
409 | finish(); |
410 | } |
411 | |
412 | void Compositor::slotReinitialize() |
413 | { |
414 | // Reparse config. Config options will be reloaded by setup() |
415 | KGlobal::config()->reparseConfiguration(); |
416 | const QString graphicsSystem = KConfigGroup(KGlobal::config(), "Compositing" ).readEntry("GraphicsSystem" , "" ); |
417 | if ((Extensions::nonNativePixmaps() && graphicsSystem == "native" ) || |
418 | (!Extensions::nonNativePixmaps() && (graphicsSystem == "raster" || graphicsSystem == "opengl" )) ) { |
419 | restartKWin("explicitly reconfigured graphicsSystem change" ); |
420 | return; |
421 | } |
422 | |
423 | // Restart compositing |
424 | finish(); |
425 | // resume compositing if suspended |
426 | m_suspended = NoReasonSuspend; |
427 | options->setCompositingInitialized(false); |
428 | setup(); |
429 | |
430 | if (effects) { // setup() may fail |
431 | effects->reconfigure(); |
432 | } |
433 | } |
434 | |
435 | // for the shortcut |
436 | void Compositor::slotToggleCompositing() |
437 | { |
438 | if (m_suspended) { // direct user call; clear all bits |
439 | resume(AllReasonSuspend); |
440 | } else { // but only set the user one (sufficient to suspend) |
441 | suspend(UserSuspend); |
442 | } |
443 | } |
444 | |
445 | // for the dbus call |
446 | void Compositor::toggleCompositing() |
447 | { |
448 | slotToggleCompositing(); // TODO only operate on script level here? |
449 | if (m_suspended) { |
450 | // when disabled show a shortcut how the user can get back compositing |
451 | QString shortcut, message; |
452 | if (KAction* action = qobject_cast<KAction*>(Workspace::self()->actionCollection()->action("Suspend Compositing" ))) |
453 | shortcut = action->globalShortcut().primary().toString(QKeySequence::NativeText); |
454 | if (!shortcut.isEmpty()) { |
455 | // display notification only if there is the shortcut |
456 | message = i18n("Desktop effects have been suspended by another application.<br/>" |
457 | "You can resume using the '%1' shortcut." , shortcut); |
458 | KNotification::event("compositingsuspendeddbus" , message); |
459 | } |
460 | } |
461 | } |
462 | |
463 | void Compositor::updateCompositeBlocking() |
464 | { |
465 | updateCompositeBlocking(NULL); |
466 | } |
467 | |
468 | void Compositor::updateCompositeBlocking(Client *c) |
469 | { |
470 | if (c) { // if c == 0 we just check if we can resume |
471 | if (c->isBlockingCompositing()) { |
472 | if (!(m_suspended & BlockRuleSuspend)) // do NOT attempt to call suspend(true); from within the eventchain! |
473 | QMetaObject::invokeMethod(this, "suspend" , Qt::QueuedConnection, Q_ARG(Compositor::SuspendReason, BlockRuleSuspend)); |
474 | } |
475 | } |
476 | else if (m_suspended & BlockRuleSuspend) { // lost a client and we're blocked - can we resume? |
477 | bool resume = true; |
478 | for (ClientList::ConstIterator it = Workspace::self()->clientList().constBegin(); it != Workspace::self()->clientList().constEnd(); ++it) { |
479 | if ((*it)->isBlockingCompositing()) { |
480 | resume = false; |
481 | break; |
482 | } |
483 | } |
484 | if (resume) { // do NOT attempt to call suspend(false); from within the eventchain! |
485 | QMetaObject::invokeMethod(this, "resume" , Qt::QueuedConnection, Q_ARG(Compositor::SuspendReason, BlockRuleSuspend)); |
486 | } |
487 | } |
488 | } |
489 | |
490 | void Compositor::suspend(Compositor::SuspendReason reason) |
491 | { |
492 | Q_ASSERT(reason != NoReasonSuspend); |
493 | m_suspended |= reason; |
494 | finish(); |
495 | } |
496 | |
497 | void Compositor::resume(Compositor::SuspendReason reason) |
498 | { |
499 | Q_ASSERT(reason != NoReasonSuspend); |
500 | m_suspended &= ~reason; |
501 | setup(); // signal "toggled" is eventually emitted from within setup |
502 | } |
503 | |
504 | void Compositor::setCompositing(bool active) |
505 | { |
506 | if (active) { |
507 | resume(ScriptSuspend); |
508 | } else { |
509 | suspend(ScriptSuspend); |
510 | } |
511 | } |
512 | |
513 | void Compositor::restart() |
514 | { |
515 | if (hasScene()) { |
516 | finish(); |
517 | QTimer::singleShot(0, this, SLOT(setup())); |
518 | } |
519 | } |
520 | |
521 | void Compositor::addRepaint(int x, int y, int w, int h) |
522 | { |
523 | if (!hasScene()) |
524 | return; |
525 | repaints_region += QRegion(x, y, w, h); |
526 | scheduleRepaint(); |
527 | } |
528 | |
529 | void Compositor::addRepaint(const QRect& r) |
530 | { |
531 | if (!hasScene()) |
532 | return; |
533 | repaints_region += r; |
534 | scheduleRepaint(); |
535 | } |
536 | |
537 | void Compositor::addRepaint(const QRegion& r) |
538 | { |
539 | if (!hasScene()) |
540 | return; |
541 | repaints_region += r; |
542 | scheduleRepaint(); |
543 | } |
544 | |
545 | void Compositor::addRepaintFull() |
546 | { |
547 | if (!hasScene()) |
548 | return; |
549 | repaints_region = QRegion(0, 0, displayWidth(), displayHeight()); |
550 | scheduleRepaint(); |
551 | } |
552 | |
553 | void Compositor::timerEvent(QTimerEvent *te) |
554 | { |
555 | if (te->timerId() == compositeTimer.timerId()) { |
556 | performCompositing(); |
557 | } else |
558 | QObject::timerEvent(te); |
559 | } |
560 | |
561 | void Compositor::performCompositing() |
562 | { |
563 | if (!isOverlayWindowVisible()) |
564 | return; // nothing is visible anyway |
565 | |
566 | // Create a list of all windows in the stacking order |
567 | ToplevelList windows = Workspace::self()->xStackingOrder(); |
568 | ToplevelList damaged; |
569 | |
570 | // Reset the damage state of each window and fetch the damage region |
571 | // without waiting for a reply |
572 | foreach (Toplevel *win, windows) { |
573 | if (win->resetAndFetchDamage()) |
574 | damaged << win; |
575 | } |
576 | |
577 | if (damaged.count() > 0) |
578 | xcb_flush(connection()); |
579 | |
580 | // Move elevated windows to the top of the stacking order |
581 | foreach (EffectWindow *c, static_cast<EffectsHandlerImpl *>(effects)->elevatedWindows()) { |
582 | Toplevel* t = static_cast< EffectWindowImpl* >(c)->window(); |
583 | windows.removeAll(t); |
584 | windows.append(t); |
585 | } |
586 | |
587 | // Get the replies |
588 | foreach (Toplevel *win, damaged) { |
589 | // Discard the cached lanczos texture |
590 | if (win->effectWindow()) { |
591 | const QVariant texture = win->effectWindow()->data(LanczosCacheRole); |
592 | if (texture.isValid()) { |
593 | delete static_cast<GLTexture *>(texture.value<void*>()); |
594 | win->effectWindow()->setData(LanczosCacheRole, QVariant()); |
595 | } |
596 | } |
597 | |
598 | win->getDamageRegionReply(); |
599 | } |
600 | |
601 | if (repaints_region.isEmpty() && !windowRepaintsPending()) { |
602 | m_scene->idle(); |
603 | m_timeSinceLastVBlank = fpsInterval - (options->vBlankTime() + 1); // means "start now" |
604 | // Note: It would seem here we should undo suspended unredirect, but when scenes need |
605 | // it for some reason, e.g. transformations or translucency, the next pass that does not |
606 | // need this anymore and paints normally will also reset the suspended unredirect. |
607 | // Otherwise the window would not be painted normally anyway. |
608 | compositeTimer.stop(); |
609 | return; |
610 | } |
611 | |
612 | // skip windows that are not yet ready for being painted |
613 | // TODO ? |
614 | // this cannot be used so carelessly - needs protections against broken clients, the window |
615 | // should not get focus before it's displayed, handle unredirected windows properly and so on. |
616 | foreach (Toplevel *t, windows) |
617 | if (!t->readyForPainting()) |
618 | windows.removeAll(t); |
619 | |
620 | QRegion repaints = repaints_region; |
621 | // clear all repaints, so that post-pass can add repaints for the next repaint |
622 | repaints_region = QRegion(); |
623 | |
624 | m_timeSinceLastVBlank = m_scene->paint(repaints, windows); |
625 | |
626 | compositeTimer.stop(); // stop here to ensure *we* cause the next repaint schedule - not some effect through m_scene->paint() |
627 | |
628 | // Trigger at least one more pass even if there would be nothing to paint, so that scene->idle() |
629 | // is called the next time. If there would be nothing pending, it will not restart the timer and |
630 | // scheduleRepaint() would restart it again somewhen later, called from functions that |
631 | // would again add something pending. |
632 | scheduleRepaint(); |
633 | } |
634 | |
635 | bool Compositor::windowRepaintsPending() const |
636 | { |
637 | foreach (Toplevel * c, Workspace::self()->clientList()) |
638 | if (!c->repaints().isEmpty()) |
639 | return true; |
640 | foreach (Toplevel * c, Workspace::self()->desktopList()) |
641 | if (!c->repaints().isEmpty()) |
642 | return true; |
643 | foreach (Toplevel * c, Workspace::self()->unmanagedList()) |
644 | if (!c->repaints().isEmpty()) |
645 | return true; |
646 | foreach (Toplevel * c, Workspace::self()->deletedList()) |
647 | if (!c->repaints().isEmpty()) |
648 | return true; |
649 | return false; |
650 | } |
651 | |
652 | void Compositor::setCompositeResetTimer(int msecs) |
653 | { |
654 | compositeResetTimer.start(msecs); |
655 | } |
656 | |
657 | void Compositor::setCompositeTimer() |
658 | { |
659 | if (!hasScene()) // should not really happen, but there may be e.g. some damage events still pending |
660 | return; |
661 | |
662 | uint waitTime = 1; |
663 | |
664 | if (m_scene->blocksForRetrace()) { |
665 | |
666 | // TODO: make vBlankTime dynamic?! |
667 | // It's required because glXWaitVideoSync will *likely* block a full frame if one enters |
668 | // a retrace pass which can last a variable amount of time, depending on the actual screen |
669 | // Now, my ooold 19" CRT can do such retrace so that 2ms are entirely sufficient, |
670 | // while another ooold 15" TFT requires about 6ms |
671 | |
672 | qint64 padding = m_timeSinceLastVBlank; |
673 | if (padding > fpsInterval) { |
674 | // we're at low repaints or spent more time in painting than the user wanted to wait for that frame |
675 | padding = vBlankInterval - (padding%vBlankInterval); // -> align to next vblank |
676 | } else { // -> align to the next maxFps tick |
677 | padding = ((vBlankInterval - padding%vBlankInterval) + (fpsInterval/vBlankInterval-1)*vBlankInterval); |
678 | // "remaining time of the first vsync" + "time for the other vsyncs of the frame" |
679 | } |
680 | |
681 | if (padding < options->vBlankTime()) { // we'll likely miss this frame |
682 | waitTime = nanoToMilli(padding + vBlankInterval - options->vBlankTime()); // so we add one |
683 | } else { |
684 | waitTime = nanoToMilli(padding - options->vBlankTime()); |
685 | } |
686 | } |
687 | else { // w/o blocking vsync we just jump to the next demanded tick |
688 | if (fpsInterval > m_timeSinceLastVBlank) { |
689 | waitTime = nanoToMilli(fpsInterval - m_timeSinceLastVBlank); |
690 | if (!waitTime) { |
691 | waitTime = 1; // will ensure we don't block out the eventloop - the system's just not faster ... |
692 | } |
693 | }/* else if (m_scene->syncsToVBlank() && m_timeSinceLastVBlank - fpsInterval < (vBlankInterval<<1)) { |
694 | // NOTICE - "for later" ------------------------------------------------------------------ |
695 | // It can happen that we push two frames within one refresh cycle. |
696 | // Swapping will then block even with triple buffering when the GPU does not discard but |
697 | // queues frames |
698 | // now here's the mean part: if we take that as "OMG, we're late - next frame ASAP", |
699 | // there'll immediately be 2 frames in the pipe, swapping will block, we think we're |
700 | // late ... ewww |
701 | // so instead we pad to the clock again and add 2ms safety to ensure the pipe is really |
702 | // free |
703 | // NOTICE: obviously m_timeSinceLastVBlank can be too big because we're too slow as well |
704 | // So if this code was enabled, we'd needlessly half the framerate once more (15 instead of 30) |
705 | waitTime = nanoToMilli(vBlankInterval - (m_timeSinceLastVBlank - fpsInterval)%vBlankInterval) + 2; |
706 | }*/ else { |
707 | waitTime = 1; // ... "0" would be sufficient, but the compositor isn't the WMs only task |
708 | } |
709 | } |
710 | compositeTimer.start(qMin(waitTime, 250u), this); // force 4fps minimum |
711 | } |
712 | |
713 | bool Compositor::isActive() |
714 | { |
715 | return !m_finishing && hasScene(); |
716 | } |
717 | |
718 | void Compositor::checkUnredirect() |
719 | { |
720 | checkUnredirect(false); |
721 | } |
722 | |
723 | // force is needed when the list of windows changes (e.g. a window goes away) |
724 | void Compositor::checkUnredirect(bool force) |
725 | { |
726 | if (!hasScene() || m_scene->overlayWindow()->window() == None || !options->isUnredirectFullscreen()) |
727 | return; |
728 | if (force) |
729 | forceUnredirectCheck = true; |
730 | if (!unredirectTimer.isActive()) |
731 | unredirectTimer.start(0); |
732 | } |
733 | |
734 | void Compositor::delayedCheckUnredirect() |
735 | { |
736 | if (!hasScene() || m_scene->overlayWindow()->window() == None || !(options->isUnredirectFullscreen() || sender() == options)) |
737 | return; |
738 | ToplevelList list; |
739 | bool changed = forceUnredirectCheck; |
740 | foreach (Client * c, Workspace::self()->clientList()) |
741 | list.append(c); |
742 | foreach (Unmanaged * c, Workspace::self()->unmanagedList()) |
743 | list.append(c); |
744 | foreach (Toplevel * c, list) { |
745 | if (c->updateUnredirectedState()) |
746 | changed = true; |
747 | } |
748 | // no desktops, no Deleted ones |
749 | if (!changed) |
750 | return; |
751 | forceUnredirectCheck = false; |
752 | // Cut out parts from the overlay window where unredirected windows are, |
753 | // so that they are actually visible. |
754 | QRegion reg(0, 0, displayWidth(), displayHeight()); |
755 | foreach (Toplevel * c, list) { |
756 | if (c->unredirected()) |
757 | reg -= c->geometry(); |
758 | } |
759 | m_scene->overlayWindow()->setShape(reg); |
760 | } |
761 | |
762 | bool Compositor::checkForOverlayWindow(WId w) const |
763 | { |
764 | if (!hasScene()) { |
765 | // no scene, so it cannot be the overlay window |
766 | return false; |
767 | } |
768 | if (!m_scene->overlayWindow()) { |
769 | // no overlay window, it cannot be the overlay |
770 | return false; |
771 | } |
772 | // and compare the window ID's |
773 | return w == m_scene->overlayWindow()->window(); |
774 | } |
775 | |
776 | WId Compositor::overlayWindow() const |
777 | { |
778 | if (!hasScene()) { |
779 | return None; |
780 | } |
781 | if (!m_scene->overlayWindow()) { |
782 | return None; |
783 | } |
784 | return m_scene->overlayWindow()->window(); |
785 | } |
786 | |
787 | bool Compositor::isOverlayWindowVisible() const |
788 | { |
789 | if (!hasScene()) { |
790 | return false; |
791 | } |
792 | if (!m_scene->overlayWindow()) { |
793 | return false; |
794 | } |
795 | return m_scene->overlayWindow()->isVisible(); |
796 | } |
797 | |
798 | void Compositor::setOverlayWindowVisibility(bool visible) |
799 | { |
800 | if (hasScene() && m_scene->overlayWindow()) { |
801 | m_scene->overlayWindow()->setVisibility(visible); |
802 | } |
803 | } |
804 | |
805 | void Compositor::restartKWin(const QString &reason) |
806 | { |
807 | kDebug(1212) << "restarting kwin for:" << reason; |
808 | char cmd[1024]; // copied from crashhandler - maybe not the best way to do? |
809 | sprintf(cmd, "%s --replace &" , QFile::encodeName(QCoreApplication::applicationFilePath()).constData()); |
810 | system(cmd); |
811 | } |
812 | |
813 | bool Compositor::isCompositingPossible() const |
814 | { |
815 | return CompositingPrefs::compositingPossible(); |
816 | } |
817 | |
818 | QString Compositor::compositingNotPossibleReason() const |
819 | { |
820 | return CompositingPrefs::compositingNotPossibleReason(); |
821 | } |
822 | |
823 | bool Compositor::isOpenGLBroken() const |
824 | { |
825 | return CompositingPrefs::openGlIsBroken(); |
826 | } |
827 | |
828 | QString Compositor::compositingType() const |
829 | { |
830 | if (!hasScene()) { |
831 | return "none" ; |
832 | } |
833 | switch (m_scene->compositingType()) { |
834 | case XRenderCompositing: |
835 | return "xrender" ; |
836 | case OpenGL1Compositing: |
837 | return "gl1" ; |
838 | case OpenGL2Compositing: |
839 | #ifdef KWIN_HAVE_OPENGLES |
840 | return "gles" ; |
841 | #else |
842 | return "gl2" ; |
843 | #endif |
844 | case NoCompositing: |
845 | default: |
846 | return "none" ; |
847 | } |
848 | } |
849 | |
850 | /***************************************************** |
851 | * Workspace |
852 | ****************************************************/ |
853 | |
854 | bool Workspace::compositing() const |
855 | { |
856 | return m_compositor && m_compositor->hasScene(); |
857 | } |
858 | |
859 | //**************************************** |
860 | // Toplevel |
861 | //**************************************** |
862 | |
863 | bool Toplevel::setupCompositing() |
864 | { |
865 | if (!compositing()) |
866 | return false; |
867 | |
868 | if (damage_handle != XCB_NONE) |
869 | return false; |
870 | |
871 | damage_handle = xcb_generate_id(connection()); |
872 | xcb_damage_create(connection(), damage_handle, frameId(), XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY); |
873 | |
874 | damage_region = QRegion(0, 0, width(), height()); |
875 | effect_window = new EffectWindowImpl(this); |
876 | unredirect = false; |
877 | |
878 | Compositor::self()->checkUnredirect(true); |
879 | Compositor::self()->scene()->windowAdded(this); |
880 | |
881 | // With unmanaged windows there is a race condition between the client painting the window |
882 | // and us setting up damage tracking. If the client wins we won't get a damage event even |
883 | // though the window has been painted. To avoid this we mark the whole window as damaged |
884 | // and schedule a repaint immediately after creating the damage object. |
885 | if (dynamic_cast<Unmanaged*>(this)) |
886 | addDamageFull(); |
887 | |
888 | return true; |
889 | } |
890 | |
891 | void Toplevel::finishCompositing() |
892 | { |
893 | if (damage_handle == XCB_NONE) |
894 | return; |
895 | Compositor::self()->checkUnredirect(true); |
896 | if (effect_window->window() == this) { // otherwise it's already passed to Deleted, don't free data |
897 | discardWindowPixmap(); |
898 | delete effect_window; |
899 | } |
900 | |
901 | xcb_damage_destroy(connection(), damage_handle); |
902 | |
903 | damage_handle = XCB_NONE; |
904 | damage_region = QRegion(); |
905 | repaints_region = QRegion(); |
906 | effect_window = NULL; |
907 | } |
908 | |
909 | void Toplevel::discardWindowPixmap() |
910 | { |
911 | addDamageFull(); |
912 | if (effectWindow() != NULL && effectWindow()->sceneWindow() != NULL) |
913 | effectWindow()->sceneWindow()->pixmapDiscarded(); |
914 | } |
915 | |
916 | void Toplevel::damageNotifyEvent() |
917 | { |
918 | m_isDamaged = true; |
919 | |
920 | // Note: The rect is supposed to specify the damage extents, |
921 | // but we don't know it at this point. No one who connects |
922 | // to this signal uses the rect however. |
923 | emit damaged(this, QRect()); |
924 | } |
925 | |
926 | bool Toplevel::compositing() const |
927 | { |
928 | return Workspace::self()->compositing(); |
929 | } |
930 | |
931 | void Client::damageNotifyEvent() |
932 | { |
933 | #ifdef HAVE_XSYNC |
934 | if (syncRequest.isPending && isResize()) { |
935 | emit damaged(this, QRect()); |
936 | m_isDamaged = true; |
937 | return; |
938 | } |
939 | |
940 | if (!ready_for_painting) { // avoid "setReadyForPainting()" function calling overhead |
941 | if (syncRequest.counter == None) // cannot detect complete redraw, consider done now |
942 | setReadyForPainting(); |
943 | } |
944 | #else |
945 | if (!ready_for_painting) |
946 | setReadyForPainting(); |
947 | #endif |
948 | |
949 | Toplevel::damageNotifyEvent(); |
950 | } |
951 | |
952 | bool Toplevel::resetAndFetchDamage() |
953 | { |
954 | if (!m_isDamaged) |
955 | return false; |
956 | |
957 | xcb_connection_t *conn = connection(); |
958 | |
959 | // Create a new region and copy the damage region to it, |
960 | // resetting the damaged state. |
961 | xcb_xfixes_region_t region = xcb_generate_id(conn); |
962 | xcb_xfixes_create_region(conn, region, 0, 0); |
963 | xcb_damage_subtract(conn, damage_handle, 0, region); |
964 | |
965 | // Send a fetch-region request and destroy the region |
966 | m_regionCookie = xcb_xfixes_fetch_region_unchecked(conn, region); |
967 | xcb_xfixes_destroy_region(conn, region); |
968 | |
969 | m_isDamaged = false; |
970 | m_damageReplyPending = true; |
971 | |
972 | return m_damageReplyPending; |
973 | } |
974 | |
975 | void Toplevel::getDamageRegionReply() |
976 | { |
977 | if (!m_damageReplyPending) |
978 | return; |
979 | |
980 | m_damageReplyPending = false; |
981 | |
982 | // Get the fetch-region reply |
983 | xcb_xfixes_fetch_region_reply_t *reply = |
984 | xcb_xfixes_fetch_region_reply(connection(), m_regionCookie, 0); |
985 | |
986 | if (!reply) |
987 | return; |
988 | |
989 | // Convert the reply to a QRegion |
990 | int count = xcb_xfixes_fetch_region_rectangles_length(reply); |
991 | QRegion region; |
992 | |
993 | if (count > 1 && count < 16) { |
994 | xcb_rectangle_t *rects = xcb_xfixes_fetch_region_rectangles(reply); |
995 | |
996 | QVector<QRect> qrects; |
997 | qrects.reserve(count); |
998 | |
999 | for (int i = 0; i < count; i++) |
1000 | qrects << QRect(rects[i].x, rects[i].y, rects[i].width, rects[i].height); |
1001 | |
1002 | region.setRects(qrects.constData(), count); |
1003 | } else |
1004 | region += QRect(reply->extents.x, reply->extents.y, |
1005 | reply->extents.width, reply->extents.height); |
1006 | |
1007 | damage_region += region; |
1008 | repaints_region += region; |
1009 | |
1010 | free(reply); |
1011 | } |
1012 | |
1013 | void Toplevel::addDamageFull() |
1014 | { |
1015 | if (!compositing()) |
1016 | return; |
1017 | |
1018 | damage_region = rect(); |
1019 | repaints_region |= rect(); |
1020 | |
1021 | emit damaged(this, rect()); |
1022 | } |
1023 | |
1024 | void Toplevel::resetDamage() |
1025 | { |
1026 | damage_region = QRegion(); |
1027 | } |
1028 | |
1029 | void Toplevel::addRepaint(const QRect& r) |
1030 | { |
1031 | if (!compositing()) { |
1032 | return; |
1033 | } |
1034 | repaints_region += r; |
1035 | emit needsRepaint(); |
1036 | } |
1037 | |
1038 | void Toplevel::addRepaint(int x, int y, int w, int h) |
1039 | { |
1040 | QRect r(x, y, w, h); |
1041 | addRepaint(r); |
1042 | } |
1043 | |
1044 | void Toplevel::addRepaint(const QRegion& r) |
1045 | { |
1046 | if (!compositing()) { |
1047 | return; |
1048 | } |
1049 | repaints_region += r; |
1050 | emit needsRepaint(); |
1051 | } |
1052 | |
1053 | void Toplevel::addLayerRepaint(const QRect& r) |
1054 | { |
1055 | if (!compositing()) { |
1056 | return; |
1057 | } |
1058 | layer_repaints_region += r; |
1059 | emit needsRepaint(); |
1060 | } |
1061 | |
1062 | void Toplevel::addLayerRepaint(int x, int y, int w, int h) |
1063 | { |
1064 | QRect r(x, y, w, h); |
1065 | addLayerRepaint(r); |
1066 | } |
1067 | |
1068 | void Toplevel::addLayerRepaint(const QRegion& r) |
1069 | { |
1070 | if (!compositing()) |
1071 | return; |
1072 | layer_repaints_region += r; |
1073 | emit needsRepaint(); |
1074 | } |
1075 | |
1076 | void Toplevel::addRepaintFull() |
1077 | { |
1078 | repaints_region = visibleRect().translated(-pos()); |
1079 | emit needsRepaint(); |
1080 | } |
1081 | |
1082 | void Toplevel::resetRepaints() |
1083 | { |
1084 | repaints_region = QRegion(); |
1085 | layer_repaints_region = QRegion(); |
1086 | } |
1087 | |
1088 | void Toplevel::addWorkspaceRepaint(int x, int y, int w, int h) |
1089 | { |
1090 | addWorkspaceRepaint(QRect(x, y, w, h)); |
1091 | } |
1092 | |
1093 | void Toplevel::addWorkspaceRepaint(const QRect& r2) |
1094 | { |
1095 | if (!compositing()) |
1096 | return; |
1097 | Compositor::self()->addRepaint(r2); |
1098 | } |
1099 | |
1100 | bool Toplevel::updateUnredirectedState() |
1101 | { |
1102 | assert(compositing()); |
1103 | bool should = options->isUnredirectFullscreen() && shouldUnredirect() && !unredirectSuspend && |
1104 | !shape() && !hasAlpha() && opacity() == 1.0 && |
1105 | !static_cast<EffectsHandlerImpl*>(effects)->activeFullScreenEffect(); |
1106 | if (should == unredirect) |
1107 | return false; |
1108 | static QElapsedTimer lastUnredirect; |
1109 | static const qint64 msecRedirectInterval = 100; |
1110 | if (!lastUnredirect.hasExpired(msecRedirectInterval)) { |
1111 | QTimer::singleShot(msecRedirectInterval, Compositor::self(), SLOT(checkUnredirect())); |
1112 | return false; |
1113 | } |
1114 | lastUnredirect.start(); |
1115 | unredirect = should; |
1116 | if (unredirect) { |
1117 | kDebug(1212) << "Unredirecting:" << this; |
1118 | xcb_composite_unredirect_window(connection(), frameId(), XCB_COMPOSITE_REDIRECT_MANUAL); |
1119 | } else { |
1120 | kDebug(1212) << "Redirecting:" << this; |
1121 | xcb_composite_redirect_window(connection(), frameId(), XCB_COMPOSITE_REDIRECT_MANUAL); |
1122 | discardWindowPixmap(); |
1123 | } |
1124 | return true; |
1125 | } |
1126 | |
1127 | void Toplevel::suspendUnredirect(bool suspend) |
1128 | { |
1129 | if (unredirectSuspend == suspend) |
1130 | return; |
1131 | unredirectSuspend = suspend; |
1132 | Compositor::self()->checkUnredirect(); |
1133 | } |
1134 | |
1135 | //**************************************** |
1136 | // Client |
1137 | //**************************************** |
1138 | |
1139 | bool Client::setupCompositing() |
1140 | { |
1141 | if (!Toplevel::setupCompositing()){ |
1142 | return false; |
1143 | } |
1144 | updateVisibility(); // for internalKeep() |
1145 | if (isManaged()) { |
1146 | // only create the decoration when a client is managed |
1147 | updateDecoration(true, true); |
1148 | } |
1149 | return true; |
1150 | } |
1151 | |
1152 | void Client::finishCompositing() |
1153 | { |
1154 | Toplevel::finishCompositing(); |
1155 | updateVisibility(); |
1156 | if (!deleting) { |
1157 | // only recreate the decoration if we are not shutting down completely |
1158 | updateDecoration(true, true); |
1159 | } |
1160 | // for safety in case KWin is just resizing the window |
1161 | s_haveResizeEffect = false; |
1162 | } |
1163 | |
1164 | bool Client::shouldUnredirect() const |
1165 | { |
1166 | if (isActiveFullScreen()) { |
1167 | ToplevelList stacking = workspace()->xStackingOrder(); |
1168 | for (int pos = stacking.count() - 1; |
1169 | pos >= 0; |
1170 | --pos) { |
1171 | Toplevel* c = stacking.at(pos); |
1172 | if (c == this) // is not covered by any other window, ok to unredirect |
1173 | return true; |
1174 | if (c->geometry().intersects(geometry())) |
1175 | return false; |
1176 | } |
1177 | abort(); |
1178 | } |
1179 | return false; |
1180 | } |
1181 | |
1182 | |
1183 | //**************************************** |
1184 | // Unmanaged |
1185 | //**************************************** |
1186 | |
1187 | bool Unmanaged::shouldUnredirect() const |
1188 | { |
1189 | // the pixmap is needed for the login effect, a nicer solution would be the login effect increasing |
1190 | // refcount for the window pixmap (which would prevent unredirect), avoiding this hack |
1191 | if (resourceClass() == "ksplashx" |
1192 | || resourceClass() == "ksplashsimple" |
1193 | || resourceClass() == "ksplashqml" |
1194 | ) |
1195 | return false; |
1196 | // it must cover whole display or one xinerama screen, and be the topmost there |
1197 | const int desktop = VirtualDesktopManager::self()->current(); |
1198 | if (geometry() == workspace()->clientArea(FullArea, geometry().center(), desktop) |
1199 | || geometry() == workspace()->clientArea(ScreenArea, geometry().center(), desktop)) { |
1200 | ToplevelList stacking = workspace()->xStackingOrder(); |
1201 | for (int pos = stacking.count() - 1; |
1202 | pos >= 0; |
1203 | --pos) { |
1204 | Toplevel* c = stacking.at(pos); |
1205 | if (c == this) // is not covered by any other window, ok to unredirect |
1206 | return true; |
1207 | if (c->geometry().intersects(geometry())) |
1208 | return false; |
1209 | } |
1210 | abort(); |
1211 | } |
1212 | return false; |
1213 | } |
1214 | |
1215 | //**************************************** |
1216 | // Deleted |
1217 | //**************************************** |
1218 | |
1219 | bool Deleted::shouldUnredirect() const |
1220 | { |
1221 | return false; |
1222 | } |
1223 | |
1224 | |
1225 | } // namespace |
1226 | |