diff --git a/abstract_client.h b/abstract_client.h --- a/abstract_client.h +++ b/abstract_client.h @@ -265,6 +265,14 @@ */ Q_PROPERTY(bool applicationMenuActive READ applicationMenuActive NOTIFY applicationMenuActiveChanged) + /** + * Whether this client is unresponsive. + * + * When an application failed to react on a ping request in time, it is + * considered unresponsive. This usually indicates that the application froze or crashed. + */ + Q_PROPERTY(bool unresponsive READ unresponsive NOTIFY unresponsiveChanged) + public: virtual ~AbstractClient(); @@ -653,6 +661,8 @@ */ void showApplicationMenu(int actionId); + bool unresponsive() const; + public Q_SLOTS: virtual void closeWindow() = 0; @@ -694,6 +704,7 @@ void desktopFileNameChanged(); void hasApplicationMenuChanged(bool); void applicationMenuActiveChanged(bool); + void unresponsiveChanged(bool); protected: AbstractClient(); @@ -978,6 +989,8 @@ void updateApplicationMenuServiceName(const QString &serviceName); void updateApplicationMenuObjectPath(const QString &objectPath); + void setUnresponsive(bool unresponsive); + private: void handlePaletteChange(); QSharedPointer m_tabBoxClient; @@ -1050,6 +1063,8 @@ QString m_applicationMenuServiceName; QString m_applicationMenuObjectPath; + bool m_unresponsive = false; + static bool s_haveResizeEffect; }; diff --git a/abstract_client.cpp b/abstract_client.cpp --- a/abstract_client.cpp +++ b/abstract_client.cpp @@ -1735,4 +1735,18 @@ } } +bool AbstractClient::unresponsive() const +{ + return m_unresponsive; +} + +void AbstractClient::setUnresponsive(bool unresponsive) +{ + if (m_unresponsive != unresponsive) { + m_unresponsive = unresponsive; + emit unresponsiveChanged(m_unresponsive); + emit captionChanged(); + } +} + } diff --git a/autotests/test_scripted_effectloader.cpp b/autotests/test_scripted_effectloader.cpp --- a/autotests/test_scripted_effectloader.cpp +++ b/autotests/test_scripted_effectloader.cpp @@ -133,6 +133,7 @@ QTest::newRow("Fade + kwin4_effect") << QStringLiteral("kwin4_effect_fade") << true; QTest::newRow("Fade + kwin4_effect + CS") << QStringLiteral("kwin4_eFfect_fAde") << true; QTest::newRow("FadeDesktop") << QStringLiteral("kwin4_effect_fadedesktop") << true; + QTest::newRow("FrozenApp") << QStringLiteral("kwin4_effect_frozenapp") << true; QTest::newRow("DialogParent") << QStringLiteral("kwin4_effect_dialogparent") << true; QTest::newRow("Login") << QStringLiteral("kwin4_effect_login") << true; QTest::newRow("Logout") << QStringLiteral("kwin4_effect_logout") << true; @@ -165,6 +166,7 @@ expectedEffects << QStringLiteral("kwin4_effect_dialogparent") << QStringLiteral("kwin4_effect_fade") << QStringLiteral("kwin4_effect_fadedesktop") + << QStringLiteral("kwin4_effect_frozenapp") << QStringLiteral("kwin4_effect_login") << QStringLiteral("kwin4_effect_logout") << QStringLiteral("kwin4_effect_maximize") @@ -190,6 +192,7 @@ QTest::newRow("Fade + kwin4_effect") << QStringLiteral("kwin4_effect_fade") << true; QTest::newRow("Fade + kwin4_effect + CS") << QStringLiteral("kwin4_eFfect_fAde") << true; QTest::newRow("FadeDesktop") << QStringLiteral("kwin4_effect_fadedesktop") << true; + QTest::newRow("FrozenApp") << QStringLiteral("kwin4_effect_frozenapp") << true; QTest::newRow("DialogParent") << QStringLiteral("kwin4_effect_dialogparent") << true; QTest::newRow("Login") << QStringLiteral("kwin4_effect_login") << true; QTest::newRow("Logout") << QStringLiteral("kwin4_effect_logout") << true; @@ -342,6 +345,7 @@ plugins.writeEntry(kwin4 + QStringLiteral("dialogparentEnabled"), false); plugins.writeEntry(kwin4 + QStringLiteral("fadeEnabled"), false); plugins.writeEntry(kwin4 + QStringLiteral("fadedesktopEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("frozenappEnabled"), false); plugins.writeEntry(kwin4 + QStringLiteral("loginEnabled"), false); plugins.writeEntry(kwin4 + QStringLiteral("logoutEnabled"), false); plugins.writeEntry(kwin4 + QStringLiteral("maximizeEnabled"), false); diff --git a/client.cpp b/client.cpp --- a/client.cpp +++ b/client.cpp @@ -39,6 +39,7 @@ #include #include // KDE +#include #include #include // Qt @@ -1124,14 +1125,24 @@ ping_timer = new QTimer(this); connect(ping_timer, &QTimer::timeout, this, [this]() { - qCDebug(KWIN_CORE) << "Ping timeout:" << caption(); - ping_timer->deleteLater(); - ping_timer = nullptr; - killProcess(true, m_pingTimestamp); + if (unresponsive()) { + qCDebug(KWIN_CORE) << "Final ping timeout, asking to kill:" << caption(); + ping_timer->deleteLater(); + ping_timer = nullptr; + killProcess(true, m_pingTimestamp); + return; + } + + qCDebug(KWIN_CORE) << "First ping timeout:" << caption(); + + setUnresponsive(true); + ping_timer->start(); } ); ping_timer->setSingleShot(true); - ping_timer->start(options->killPingTimeout()); + // we'll run the timer twice, at first we'll desaturate the window + // and the second time we'll show the "do you want to kill" prompt + ping_timer->start(options->killPingTimeout() / 2); m_pingTimestamp = xTime(); workspace()->sendPingToWindow(window(), m_pingTimestamp); } @@ -1143,6 +1154,9 @@ return; delete ping_timer; ping_timer = NULL; + + setUnresponsive(false); + if (m_killHelperPID && !::kill(m_killHelperPID, 0)) { // means the process is alive ::kill(m_killHelperPID, SIGTERM); m_killHelperPID = 0; @@ -1536,8 +1550,13 @@ QString Client::caption(bool full, bool stripped) const { QString cap = stripped ? cap_deco : cap_normal; - if (full) + if (full) { cap += cap_suffix; + if (unresponsive()) { + cap += QLatin1String(" "); + cap += i18nc("Application is not responding, appended to window title", "(Not Responding)"); + } + } return cap; } diff --git a/effects.cpp b/effects.cpp --- a/effects.cpp +++ b/effects.cpp @@ -284,6 +284,11 @@ connect(c, &AbstractClient::modalChanged, this, &EffectsHandlerImpl::slotClientModalityChanged); connect(c, &AbstractClient::geometryShapeChanged, this, &EffectsHandlerImpl::slotGeometryShapeChanged); connect(c, &AbstractClient::damaged, this, &EffectsHandlerImpl::slotWindowDamaged); + connect(c, &AbstractClient::unresponsiveChanged, this, + [this, c](bool unresponsive) { + emit windowUnresponsiveChanged(c->effectWindow(), unresponsive); + } + ); connect(c, &AbstractClient::windowShown, this, [this](Toplevel *c) { emit windowShown(c->effectWindow()); diff --git a/effects/CMakeLists.txt b/effects/CMakeLists.txt --- a/effects/CMakeLists.txt +++ b/effects/CMakeLists.txt @@ -132,6 +132,7 @@ add_subdirectory( eyeonscreen ) add_subdirectory( fade ) add_subdirectory( fadedesktop ) +add_subdirectory( frozenapp ) add_subdirectory( login ) add_subdirectory( logout ) add_subdirectory( maximize ) diff --git a/effects/frozenapp/CMakeLists.txt b/effects/frozenapp/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/effects/frozenapp/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(package) diff --git a/effects/frozenapp/package/CMakeLists.txt b/effects/frozenapp/package/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/effects/frozenapp/package/CMakeLists.txt @@ -0,0 +1,6 @@ +install(DIRECTORY contents DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_frozenapp) +install(FILES metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_frozenapp) + +install(FILES metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_frozenapp.desktop) diff --git a/effects/frozenapp/package/contents/code/main.js b/effects/frozenapp/package/contents/code/main.js new file mode 100644 --- /dev/null +++ b/effects/frozenapp/package/contents/code/main.js @@ -0,0 +1,142 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2017 Kai Uwe Broulik + +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 . +*********************************************************************/ +/*global effect, effects, animate, animationTime, Effect*/ +var frozenAppEffect = { + inDuration: animationTime(1500), + outDuration: animationTime(250), + loadConfig: function () { + "use strict"; + frozenAppEffect.inDuration = animationTime(1500); + frozenAppEffect.outDuration = animationTime(250); + }, + windowAdded: function (window) { + "use strict"; + if (!window || !window.unresponsive) { + return; + } + frozenAppEffect.windowBecameUnresponsive(window); + }, + windowBecameUnresponsive: function (window) { + "use strict"; + if (window.unresponsiveAnimation) { + return; + } + frozenAppEffect.startAnimation(window, frozenAppEffect.inDuration); + }, + startAnimation: function (window, duration) { + "use strict"; + if (!window.visible) { + return; + } + window.unresponsiveAnimation = set({ + window: window, + duration: duration, + animations: [{ + type: Effect.Saturation, + to: 0.1 + }, { + type: Effect.Brightness, + to: 1.5 + }] + }); + }, + windowClosed: function (window) { + "use strict"; + frozenAppEffect.cancelAnimation(window); + if (!window.unresponsive) { + return; + } + frozenAppEffect.windowBecameResponsive(window); + }, + windowBecameResponsive: function (window) { + "use strict"; + if (!window.unresponsiveAnimation) { + return; + } + cancel(window.unresponsiveAnimation); + window.unresponsiveAnimation = undefined; + + animate({ + window: window, + duration: frozenAppEffect.outDuration, + animations: [{ + type: Effect.Saturation, + from: 0.1, + to: 1.0 + }, { + type: Effect.Brightness, + from: 1.5, + to: 1.0 + }] + }); + }, + cancelAnimation: function (window) { + "use strict"; + if (window.unresponsiveAnimation) { + print(window); + cancel(window.unresponsiveAnimation); + window.unresponsiveAnimation = undefined; + } + }, + desktopChanged: function () { + "use strict"; + + var windows = effects.stackingOrder; + for (var i = 0, length = windows.length; i < length; ++i) { + print(i); + var window = windows[i]; + frozenAppEffect.cancelAnimation(window); + frozenAppEffect.restartAnimation(window); + } + }, + unresponsiveChanged: function (window) { + "use strict"; + if (window.unresponsive) { + frozenAppEffect.windowBecameUnresponsive(window); + } else { + frozenAppEffect.windowBecameResponsive(window); + } + }, + restartAnimation: function (window) { + "use strict"; + if (!window || !window.unresponsive) { + return; + } + frozenAppEffect.startAnimation(window, 1); + }, + init: function () { + "use strict"; + + effects.windowAdded.connect(frozenAppEffect.windowAdded); + effects.windowClosed.connect(frozenAppEffect.windowClosed); + effects.windowMinimized.connect(frozenAppEffect.cancelAnimation); + effects.windowUnminimized.connect(frozenAppEffect.restartAnimation); + effects.windowUnresponsiveChanged.connect(frozenAppEffect.unresponsiveChanged); + effects['desktopChanged(int,int)'].connect(frozenAppEffect.desktopChanged); + effects.desktopPresenceChanged.connect(frozenAppEffect.cancelAnimation); + effects.desktopPresenceChanged.connect(frozenAppEffect.restartAnimation); + + var windows = effects.stackingOrder; + for (var i = 0, length = windows.length; i < length; ++i) { + frozenAppEffect.restartAnimation(windows[i]); + } + } +}; +frozenAppEffect.init(); diff --git a/effects/frozenapp/package/metadata.desktop b/effects/frozenapp/package/metadata.desktop new file mode 100644 --- /dev/null +++ b/effects/frozenapp/package/metadata.desktop @@ -0,0 +1,22 @@ +[Desktop Entry] +Name=Desaturate Unresponsive Applications +Icon=preferences-system-windows-effect-frozenapp +Comment=Desaturate windows of unresponsive (frozen) applications + +Type=Service +X-KDE-ServiceTypes=KWin/Effect,KCModule +X-KDE-PluginInfo-Author=Kai Uwe Broulik +X-KDE-PluginInfo-Email=kde@privat.broulik.de +X-KDE-PluginInfo-Name=kwin4_effect_frozenapp +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-Category=Appearance +X-KDE-PluginInfo-Depends= +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=60 +X-Plasma-API=javascript +X-Plasma-MainScript=code/main.js +X-KDE-PluginKeyword=kwin4_effect_frozenapp +X-KDE-Library=kcm_kwin4_genericscripted +X-KDE-ParentComponents=kwin4_effect_frozen +X-KWin-Config-TranslationDomain=kwin_effects diff --git a/libkwineffects/kwineffects.h b/libkwineffects/kwineffects.h --- a/libkwineffects/kwineffects.h +++ b/libkwineffects/kwineffects.h @@ -1443,6 +1443,14 @@ **/ void windowModalityChanged(KWin::EffectWindow *w); /** + * Signal emitted when a window either became unresponsive (eg. app froze or crashed) + * or respoonsive + * @param w The window that became (un)responsive + * @param unresponsive Whether the window is responsive or unresponsive + * @since 5.10 + */ + void windowUnresponsiveChanged(KWin::EffectWindow *w, bool unresponsive); + /** * Signal emitted when an area of a window is scheduled for repainting. * Use this signal in an effect if another area needs to be synced as well. * @param w The window which is scheduled for repainting @@ -1867,6 +1875,16 @@ * @since 5.6 **/ Q_PROPERTY(bool fullScreen READ isFullScreen) + + /** + * Whether this client is unresponsive. + * + * When an application failed to react on a ping request in time, it is + * considered unresponsive. This usually indicates that the application froze or crashed. + * + * @since 5.10 + */ + Q_PROPERTY(bool unresponsive READ isUnresponsive) public: /** Flags explaining why painting should be disabled */ enum { @@ -2110,6 +2128,11 @@ bool isFullScreen() const; /** + * @since 5.10 + */ + bool isUnresponsive() const; + + /** * Can be used to by effects to store arbitrary data in the EffectWindow. * * Invoking this method will emit the signal EffectsHandler::windowDataChanged. diff --git a/libkwineffects/kwineffects.cpp b/libkwineffects/kwineffects.cpp --- a/libkwineffects/kwineffects.cpp +++ b/libkwineffects/kwineffects.cpp @@ -873,6 +873,7 @@ WINDOW_HELPER_DEFAULT(bool, isCurrentTab, "isCurrentTab", true) WINDOW_HELPER_DEFAULT(bool, decorationHasAlpha, "decorationHasAlpha", false) WINDOW_HELPER_DEFAULT(bool, isFullScreen, "fullScreen", false) +WINDOW_HELPER_DEFAULT(bool, isUnresponsive, "unresponsive", false) #undef WINDOW_HELPER_DEFAULT