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 ®ion, 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 ¤tStartup = 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"