File indexing completed on 2024-11-10 04:57:10

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2010 Martin Gräßlin <mgraesslin@kde.org>
0006     SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 #include "startupfeedback.h"
0011 // Qt
0012 #include <QApplication>
0013 #include <QDBusConnectionInterface>
0014 #include <QDBusServiceWatcher>
0015 #include <QFile>
0016 #include <QPainter>
0017 #include <QSize>
0018 #include <QStandardPaths>
0019 #include <QStyle>
0020 #include <QTimer>
0021 // KDE
0022 #include <KConfigGroup>
0023 #include <KSelectionOwner>
0024 #include <KSharedConfig>
0025 #include <KWindowSystem>
0026 // KWin
0027 #include "core/output.h"
0028 #include "core/pixelgrid.h"
0029 #include "core/rendertarget.h"
0030 #include "core/renderviewport.h"
0031 #include "effect/effecthandler.h"
0032 #include "opengl/glutils.h"
0033 
0034 // based on StartupId in KRunner by Lubos Lunak
0035 // SPDX-FileCopyrightText: 2001 Lubos Lunak <l.lunak@kde.org>
0036 
0037 Q_LOGGING_CATEGORY(KWIN_STARTUPFEEDBACK, "kwin_effect_startupfeedback", QtWarningMsg)
0038 
0039 static void ensureResources()
0040 {
0041     // Must initialize resources manually because the effect is a static lib.
0042     Q_INIT_RESOURCE(startupfeedback);
0043 }
0044 
0045 namespace KWin
0046 {
0047 
0048 // number of key frames for bouncing animation
0049 static const int BOUNCE_FRAMES = 20;
0050 // duration between two key frames in msec
0051 static const int BOUNCE_FRAME_DURATION = 30;
0052 // duration of one bounce animation
0053 static const int BOUNCE_DURATION = BOUNCE_FRAME_DURATION * BOUNCE_FRAMES;
0054 // number of key frames for blinking animation
0055 static const int BLINKING_FRAMES = 5;
0056 // duration between two key frames in msec
0057 static const int BLINKING_FRAME_DURATION = 100;
0058 // duration of one blinking animation
0059 static const int BLINKING_DURATION = BLINKING_FRAME_DURATION * BLINKING_FRAMES;
0060 // const int color_to_pixmap[] = { 0, 1, 2, 3, 2, 1 };
0061 static const int FRAME_TO_BOUNCE_YOFFSET[] = {
0062     -5, -1, 2, 5, 8, 10, 12, 13, 15, 15, 15, 15, 14, 12, 10, 8, 5, 2, -1, -5};
0063 static const QSize BOUNCE_SIZES[] = {
0064     QSize(16, 16), QSize(14, 18), QSize(12, 20), QSize(18, 14), QSize(20, 12)};
0065 static const int FRAME_TO_BOUNCE_TEXTURE[] = {
0066     0, 0, 0, 1, 2, 2, 1, 0, 3, 4, 4, 3, 0, 1, 2, 2, 1, 0, 0, 0};
0067 static const int FRAME_TO_BLINKING_COLOR[] = {
0068     0, 1, 2, 3, 2, 1};
0069 static const QColor BLINKING_COLORS[] = {
0070     Qt::black, Qt::darkGray, Qt::lightGray, Qt::white, Qt::white};
0071 static const int s_startupDefaultTimeout = 5;
0072 
0073 StartupFeedbackEffect::StartupFeedbackEffect()
0074     : m_bounceSizesRatio(1.0)
0075     , m_startupInfo(new KStartupInfo(KStartupInfo::CleanOnCantDetect, this))
0076     , m_selection(nullptr)
0077     , m_active(false)
0078     , m_frame(0)
0079     , m_progress(0)
0080     , m_lastPresentTime(std::chrono::milliseconds::zero())
0081     , m_type(BouncingFeedback)
0082     , m_cursorSize(24)
0083     , m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("klaunchrc", KConfig::NoGlobals)))
0084     , m_splashVisible(false)
0085 {
0086     // TODO: move somewhere that is x11-specific
0087     if (KWindowSystem::isPlatformX11()) {
0088         m_selection = new KSelectionOwner("_KDE_STARTUP_FEEDBACK", effects->xcbConnection(), effects->x11RootWindow(), this);
0089         m_selection->claim(true);
0090     }
0091     connect(m_startupInfo, &KStartupInfo::gotNewStartup, this, [](const KStartupInfoId &id, const KStartupInfoData &data) {
0092         const auto icon = QIcon::fromTheme(data.findIcon(), QIcon::fromTheme(QStringLiteral("system-run")));
0093         Q_EMIT effects->startupAdded(id.id(), icon);
0094     });
0095     connect(m_startupInfo, &KStartupInfo::gotRemoveStartup, this, [](const KStartupInfoId &id, const KStartupInfoData &data) {
0096         Q_EMIT effects->startupRemoved(id.id());
0097     });
0098     connect(m_startupInfo, &KStartupInfo::gotStartupChange, this, [](const KStartupInfoId &id, const KStartupInfoData &data) {
0099         const auto icon = QIcon::fromTheme(data.findIcon(), QIcon::fromTheme(QStringLiteral("system-run")));
0100         Q_EMIT effects->startupChanged(id.id(), icon);
0101     });
0102 
0103     connect(effects, &EffectsHandler::startupAdded, this, &StartupFeedbackEffect::gotNewStartup);
0104     connect(effects, &EffectsHandler::startupRemoved, this, &StartupFeedbackEffect::gotRemoveStartup);
0105     connect(effects, &EffectsHandler::startupChanged, this, &StartupFeedbackEffect::gotStartupChange);
0106 
0107     connect(effects, &EffectsHandler::mouseChanged, this, &StartupFeedbackEffect::slotMouseChanged);
0108     connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this]() {
0109         reconfigure(ReconfigureAll);
0110     });
0111     reconfigure(ReconfigureAll);
0112 
0113     m_splashVisible = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.KSplash"));
0114     auto serviceWatcher = new QDBusServiceWatcher(QStringLiteral("org.kde.KSplash"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this);
0115     connect(serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this] {
0116         m_splashVisible = true;
0117         stop();
0118     });
0119     connect(serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] {
0120         m_splashVisible = false;
0121         gotRemoveStartup({}); // Start the next feedback
0122     });
0123 }
0124 
0125 StartupFeedbackEffect::~StartupFeedbackEffect()
0126 {
0127     if (m_active) {
0128         effects->stopMousePolling();
0129     }
0130 }
0131 
0132 bool StartupFeedbackEffect::supported()
0133 {
0134     return effects->isOpenGLCompositing();
0135 }
0136 
0137 void StartupFeedbackEffect::reconfigure(Effect::ReconfigureFlags flags)
0138 {
0139     KConfigGroup c = m_configWatcher->config()->group(QStringLiteral("FeedbackStyle"));
0140     const bool busyCursor = c.readEntry("BusyCursor", true);
0141 
0142     c = m_configWatcher->config()->group(QStringLiteral("BusyCursorSettings"));
0143     m_timeout = std::chrono::seconds(c.readEntry("Timeout", s_startupDefaultTimeout));
0144     m_startupInfo->setTimeout(m_timeout.count());
0145     const bool busyBlinking = c.readEntry("Blinking", false);
0146     const bool busyBouncing = c.readEntry("Bouncing", true);
0147     if (!busyCursor) {
0148         m_type = NoFeedback;
0149     } else if (busyBouncing) {
0150         m_type = BouncingFeedback;
0151     } else if (busyBlinking) {
0152         m_type = BlinkingFeedback;
0153         if (effects->compositingType() == OpenGLCompositing) {
0154             ensureResources();
0155             m_blinkingShader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/effects/startupfeedback/shaders/blinking-startup.frag"));
0156             if (m_blinkingShader->isValid()) {
0157                 qCDebug(KWIN_STARTUPFEEDBACK) << "Blinking Shader is valid";
0158             } else {
0159                 qCDebug(KWIN_STARTUPFEEDBACK) << "Blinking Shader is not valid";
0160             }
0161         }
0162     } else {
0163         m_type = PassiveFeedback;
0164     }
0165     if (m_active) {
0166         stop();
0167         start(m_startups[m_currentStartup]);
0168     }
0169 }
0170 
0171 void StartupFeedbackEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
0172 {
0173     int time = 0;
0174     if (m_lastPresentTime.count()) {
0175         time = (presentTime - m_lastPresentTime).count();
0176     }
0177     m_lastPresentTime = presentTime;
0178 
0179     if (m_active && effects->isCursorHidden()) {
0180         stop();
0181     }
0182     if (m_active) {
0183         // need the unclipped version
0184         switch (m_type) {
0185         case BouncingFeedback:
0186             m_progress = (m_progress + time) % BOUNCE_DURATION;
0187             m_frame = qRound((qreal)m_progress / (qreal)BOUNCE_FRAME_DURATION) % BOUNCE_FRAMES;
0188             m_currentGeometry = feedbackRect(); // bounce alters geometry with m_frame
0189             data.paint = data.paint.united(m_currentGeometry);
0190             break;
0191         case BlinkingFeedback:
0192             m_progress = (m_progress + time) % BLINKING_DURATION;
0193             m_frame = qRound((qreal)m_progress / (qreal)BLINKING_FRAME_DURATION) % BLINKING_FRAMES;
0194             break;
0195         default:
0196             break; // nothing
0197         }
0198     }
0199     effects->prePaintScreen(data, presentTime);
0200 }
0201 
0202 void StartupFeedbackEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &region, Output *screen)
0203 {
0204     effects->paintScreen(renderTarget, viewport, mask, region, screen);
0205     if (m_active) {
0206         GLTexture *texture;
0207         switch (m_type) {
0208         case BouncingFeedback:
0209             texture = m_bouncingTextures[FRAME_TO_BOUNCE_TEXTURE[m_frame]].get();
0210             break;
0211         case BlinkingFeedback: // fall through
0212         case PassiveFeedback:
0213             texture = m_texture.get();
0214             break;
0215         default:
0216             return; // safety
0217         }
0218         if (!texture) {
0219             return;
0220         }
0221         glEnable(GL_BLEND);
0222         glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
0223         GLShader *shader = nullptr;
0224         if (m_type == BlinkingFeedback && m_blinkingShader && m_blinkingShader->isValid()) {
0225             const QColor &blinkingColor = BLINKING_COLORS[FRAME_TO_BLINKING_COLOR[m_frame]];
0226             ShaderManager::instance()->pushShader(m_blinkingShader.get());
0227             shader = m_blinkingShader.get();
0228             m_blinkingShader->setUniform(GLShader::ColorUniform::Color, blinkingColor);
0229         } else {
0230             shader = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture | ShaderTrait::TransformColorspace);
0231         }
0232         const QRectF pixelGeometry = snapToPixelGridF(scaledRect(m_currentGeometry, viewport.scale()));
0233         QMatrix4x4 mvp = viewport.projectionMatrix();
0234         mvp.translate(pixelGeometry.x(), pixelGeometry.y());
0235         shader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, mvp);
0236         shader->setColorspaceUniformsFromSRGB(renderTarget.colorDescription());
0237         texture->render(pixelGeometry.size());
0238         ShaderManager::instance()->popShader();
0239         glDisable(GL_BLEND);
0240     }
0241 }
0242 
0243 void StartupFeedbackEffect::postPaintScreen()
0244 {
0245     if (m_active) {
0246         m_dirtyRect = m_currentGeometry; // ensure the now dirty region is cleaned on the next pass
0247         if (m_type == BlinkingFeedback || m_type == BouncingFeedback) {
0248             effects->addRepaint(m_dirtyRect); // we also have to trigger a repaint
0249         }
0250     }
0251     effects->postPaintScreen();
0252 }
0253 
0254 void StartupFeedbackEffect::slotMouseChanged(const QPointF &pos, const QPointF &oldpos, Qt::MouseButtons buttons,
0255                                              Qt::MouseButtons oldbuttons, Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers)
0256 {
0257     if (m_active) {
0258         m_dirtyRect |= m_currentGeometry;
0259         m_currentGeometry = feedbackRect();
0260         m_dirtyRect |= m_currentGeometry;
0261         effects->addRepaint(m_dirtyRect);
0262     }
0263 }
0264 
0265 void StartupFeedbackEffect::gotNewStartup(const QString &id, const QIcon &icon)
0266 {
0267     Startup &startup = m_startups[id];
0268     startup.icon = icon;
0269 
0270     startup.expiredTimer = std::make_unique<QTimer>();
0271     // Stop the animation if the startup doesn't finish within reasonable interval.
0272     connect(startup.expiredTimer.get(), &QTimer::timeout, this, [this, id]() {
0273         gotRemoveStartup(id);
0274     });
0275     startup.expiredTimer->setSingleShot(true);
0276     startup.expiredTimer->start(m_timeout);
0277 
0278     m_currentStartup = id;
0279     start(startup);
0280 }
0281 
0282 void StartupFeedbackEffect::gotRemoveStartup(const QString &id)
0283 {
0284     m_startups.remove(id);
0285     if (m_startups.isEmpty()) {
0286         m_currentStartup.clear();
0287         stop();
0288         return;
0289     }
0290     m_currentStartup = m_startups.begin().key();
0291     start(m_startups[m_currentStartup]);
0292 }
0293 
0294 void StartupFeedbackEffect::gotStartupChange(const QString &id, const QIcon &icon)
0295 {
0296     if (m_currentStartup == id) {
0297         Startup &currentStartup = m_startups[m_currentStartup];
0298         if (!icon.isNull() && icon.name() != currentStartup.icon.name()) {
0299             currentStartup.icon = icon;
0300             start(currentStartup);
0301         }
0302     }
0303 }
0304 
0305 void StartupFeedbackEffect::start(const Startup &startup)
0306 {
0307     if (m_type == NoFeedback || m_splashVisible || effects->isCursorHidden()) {
0308         return;
0309     }
0310 
0311     const Output *output = effects->screenAt(effects->cursorPos().toPoint());
0312     if (!output) {
0313         return;
0314     }
0315 
0316     if (!m_active) {
0317         effects->startMousePolling();
0318     }
0319     m_active = true;
0320 
0321     // read details about the mouse-cursor theme define per default
0322     KConfigGroup mousecfg(effects->inputConfig(), QStringLiteral("Mouse"));
0323     m_cursorSize = mousecfg.readEntry("cursorSize", 24);
0324 
0325     int iconSize = m_cursorSize / 1.5;
0326     if (!iconSize) {
0327         iconSize = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize);
0328     }
0329     // get ratio for bouncing cursor so we don't need to manually calculate the sizes for each icon size
0330     if (m_type == BouncingFeedback) {
0331         m_bounceSizesRatio = iconSize / 16.0;
0332     }
0333 
0334     const QPixmap iconPixmap = startup.icon.pixmap(iconSize);
0335     prepareTextures(iconPixmap, output->scale());
0336     m_dirtyRect = m_currentGeometry = feedbackRect();
0337     effects->addRepaint(m_dirtyRect);
0338 }
0339 
0340 void StartupFeedbackEffect::stop()
0341 {
0342     if (m_active) {
0343         effects->stopMousePolling();
0344     }
0345     m_active = false;
0346     m_lastPresentTime = std::chrono::milliseconds::zero();
0347     effects->makeOpenGLContextCurrent();
0348     switch (m_type) {
0349     case BouncingFeedback:
0350         for (int i = 0; i < 5; ++i) {
0351             m_bouncingTextures[i].reset();
0352         }
0353         break;
0354     case BlinkingFeedback:
0355     case PassiveFeedback:
0356         m_texture.reset();
0357         break;
0358     case NoFeedback:
0359         return; // don't want the full repaint
0360     default:
0361         break; // impossible
0362     }
0363     effects->addRepaintFull();
0364 }
0365 
0366 void StartupFeedbackEffect::prepareTextures(const QPixmap &pix, qreal devicePixelRatio)
0367 {
0368     effects->makeOpenGLContextCurrent();
0369     switch (m_type) {
0370     case BouncingFeedback:
0371         for (int i = 0; i < 5; ++i) {
0372             m_bouncingTextures[i] = GLTexture::upload(scalePixmap(pix, BOUNCE_SIZES[i], devicePixelRatio));
0373             if (!m_bouncingTextures[i]) {
0374                 return;
0375             }
0376             m_bouncingTextures[i]->setFilter(GL_LINEAR);
0377             m_bouncingTextures[i]->setWrapMode(GL_CLAMP_TO_EDGE);
0378         }
0379         break;
0380     case BlinkingFeedback:
0381     case PassiveFeedback:
0382         m_texture = GLTexture::upload(pix);
0383         if (!m_texture) {
0384             return;
0385         }
0386         m_texture->setFilter(GL_LINEAR);
0387         m_texture->setWrapMode(GL_CLAMP_TO_EDGE);
0388         break;
0389     default:
0390         // for safety
0391         m_active = false;
0392         m_lastPresentTime = std::chrono::milliseconds::zero();
0393         break;
0394     }
0395 }
0396 
0397 QImage StartupFeedbackEffect::scalePixmap(const QPixmap &pm, const QSize &size, qreal devicePixelRatio) const
0398 {
0399     const QSize &adjustedSize = size * m_bounceSizesRatio;
0400     QImage scaled = pm.toImage().scaled(adjustedSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0401     if (scaled.format() != QImage::Format_ARGB32_Premultiplied && scaled.format() != QImage::Format_ARGB32) {
0402         scaled.convertTo(QImage::Format_ARGB32);
0403     }
0404 
0405     QImage result(feedbackIconSize() * devicePixelRatio, QImage::Format_ARGB32);
0406     result.setDevicePixelRatio(devicePixelRatio);
0407 
0408     QPainter p(&result);
0409     p.setCompositionMode(QPainter::CompositionMode_Source);
0410     p.fillRect(result.rect(), Qt::transparent);
0411     p.drawImage(QRectF((20 * m_bounceSizesRatio - adjustedSize.width()) / 2,
0412                        (20 * m_bounceSizesRatio - adjustedSize.height()) / 2,
0413                        adjustedSize.width(),
0414                        adjustedSize.height()),
0415                 scaled);
0416     return result;
0417 }
0418 
0419 QSize StartupFeedbackEffect::feedbackIconSize() const
0420 {
0421     return QSize(20, 20) * m_bounceSizesRatio;
0422 }
0423 
0424 QRect StartupFeedbackEffect::feedbackRect() const
0425 {
0426     int xDiff;
0427     if (m_cursorSize <= 16) {
0428         xDiff = 8 + 7;
0429     } else if (m_cursorSize <= 32) {
0430         xDiff = 16 + 7;
0431     } else if (m_cursorSize <= 48) {
0432         xDiff = 24 + 7;
0433     } else {
0434         xDiff = 32 + 7;
0435     }
0436     int yDiff = xDiff;
0437     GLTexture *texture = nullptr;
0438     int yOffset = 0;
0439     switch (m_type) {
0440     case BouncingFeedback:
0441         texture = m_bouncingTextures[FRAME_TO_BOUNCE_TEXTURE[m_frame]].get();
0442         yOffset = FRAME_TO_BOUNCE_YOFFSET[m_frame] * m_bounceSizesRatio;
0443         break;
0444     case BlinkingFeedback: // fall through
0445     case PassiveFeedback:
0446         texture = m_texture.get();
0447         break;
0448     default:
0449         // nothing
0450         break;
0451     }
0452     const QPoint cursorPos = effects->cursorPos().toPoint() + QPoint(xDiff, yDiff + yOffset);
0453     QRect rect;
0454     if (texture) {
0455         rect = QRect(cursorPos, feedbackIconSize());
0456     }
0457     return rect;
0458 }
0459 
0460 bool StartupFeedbackEffect::isActive() const
0461 {
0462     return m_active;
0463 }
0464 
0465 } // namespace
0466 
0467 #include "moc_startupfeedback.cpp"