KWin
Loading...
Searching...
No Matches
startupfeedback.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2010 Martin Gräßlin <mgraesslin@kde.org>
6 SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
7
8 SPDX-License-Identifier: GPL-2.0-or-later
9*/
10#include "startupfeedback.h"
11// Qt
12#include <QApplication>
13#include <QDBusConnectionInterface>
14#include <QDBusServiceWatcher>
15#include <QFile>
16#include <QPainter>
17#include <QSize>
18#include <QStandardPaths>
19#include <QStyle>
20#include <QTimer>
21// KDE
22#include <KConfigGroup>
23#include <KSelectionOwner>
24#include <KSharedConfig>
25#include <KWindowSystem>
26// KWin
27#include "core/output.h"
28#include "core/pixelgrid.h"
29#include "core/rendertarget.h"
30#include "core/renderviewport.h"
32#include "opengl/glutils.h"
33
34// based on StartupId in KRunner by Lubos Lunak
35// SPDX-FileCopyrightText: 2001 Lubos Lunak <l.lunak@kde.org>
36
37Q_LOGGING_CATEGORY(KWIN_STARTUPFEEDBACK, "kwin_effect_startupfeedback", QtWarningMsg)
38
39static void ensureResources()
40{
41 // Must initialize resources manually because the effect is a static lib.
42 Q_INIT_RESOURCE(startupfeedback);
43}
44
45namespace KWin
46{
47
48// number of key frames for bouncing animation
49static const int BOUNCE_FRAMES = 20;
50// duration between two key frames in msec
51static const int BOUNCE_FRAME_DURATION = 30;
52// duration of one bounce animation
53static const int BOUNCE_DURATION = BOUNCE_FRAME_DURATION * BOUNCE_FRAMES;
54// number of key frames for blinking animation
55static const int BLINKING_FRAMES = 5;
56// duration between two key frames in msec
57static const int BLINKING_FRAME_DURATION = 100;
58// duration of one blinking animation
59static const int BLINKING_DURATION = BLINKING_FRAME_DURATION * BLINKING_FRAMES;
60// const int color_to_pixmap[] = { 0, 1, 2, 3, 2, 1 };
61static const int FRAME_TO_BOUNCE_YOFFSET[] = {
62 -5, -1, 2, 5, 8, 10, 12, 13, 15, 15, 15, 15, 14, 12, 10, 8, 5, 2, -1, -5};
63static const QSize BOUNCE_SIZES[] = {
64 QSize(16, 16), QSize(14, 18), QSize(12, 20), QSize(18, 14), QSize(20, 12)};
65static const int FRAME_TO_BOUNCE_TEXTURE[] = {
66 0, 0, 0, 1, 2, 2, 1, 0, 3, 4, 4, 3, 0, 1, 2, 2, 1, 0, 0, 0};
67static const int FRAME_TO_BLINKING_COLOR[] = {
68 0, 1, 2, 3, 2, 1};
69static const QColor BLINKING_COLORS[] = {
70 Qt::black, Qt::darkGray, Qt::lightGray, Qt::white, Qt::white};
71static const int s_startupDefaultTimeout = 5;
72
74 : m_bounceSizesRatio(1.0)
75 , m_startupInfo(new KStartupInfo(KStartupInfo::CleanOnCantDetect, this))
76 , m_selection(nullptr)
77 , m_active(false)
78 , m_frame(0)
79 , m_progress(0)
80 , m_lastPresentTime(std::chrono::milliseconds::zero())
81 , m_type(BouncingFeedback)
82 , m_cursorSize(24)
83 , m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("klaunchrc", KConfig::NoGlobals)))
84 , m_splashVisible(false)
85{
86 // TODO: move somewhere that is x11-specific
87 if (KWindowSystem::isPlatformX11()) {
88 m_selection = new KSelectionOwner("_KDE_STARTUP_FEEDBACK", effects->xcbConnection(), effects->x11RootWindow(), this);
89 m_selection->claim(true);
90 }
91 connect(m_startupInfo, &KStartupInfo::gotNewStartup, this, [](const KStartupInfoId &id, const KStartupInfoData &data) {
92 const auto icon = QIcon::fromTheme(data.findIcon(), QIcon::fromTheme(QStringLiteral("system-run")));
93 Q_EMIT effects->startupAdded(id.id(), icon);
94 });
95 connect(m_startupInfo, &KStartupInfo::gotRemoveStartup, this, [](const KStartupInfoId &id, const KStartupInfoData &data) {
96 Q_EMIT effects->startupRemoved(id.id());
97 });
98 connect(m_startupInfo, &KStartupInfo::gotStartupChange, this, [](const KStartupInfoId &id, const KStartupInfoData &data) {
99 const auto icon = QIcon::fromTheme(data.findIcon(), QIcon::fromTheme(QStringLiteral("system-run")));
100 Q_EMIT effects->startupChanged(id.id(), icon);
101 });
102
103 connect(effects, &EffectsHandler::startupAdded, this, &StartupFeedbackEffect::gotNewStartup);
104 connect(effects, &EffectsHandler::startupRemoved, this, &StartupFeedbackEffect::gotRemoveStartup);
105 connect(effects, &EffectsHandler::startupChanged, this, &StartupFeedbackEffect::gotStartupChange);
106
107 connect(effects, &EffectsHandler::mouseChanged, this, &StartupFeedbackEffect::slotMouseChanged);
108 connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this]() {
109 reconfigure(ReconfigureAll);
110 });
112
113 m_splashVisible = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.KSplash"));
114 auto serviceWatcher = new QDBusServiceWatcher(QStringLiteral("org.kde.KSplash"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this);
115 connect(serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this] {
116 m_splashVisible = true;
117 stop();
118 });
119 connect(serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] {
120 m_splashVisible = false;
121 gotRemoveStartup({}); // Start the next feedback
122 });
123}
124
126{
127 if (m_active) {
129 }
130}
131
136
137void StartupFeedbackEffect::reconfigure(Effect::ReconfigureFlags flags)
138{
139 KConfigGroup c = m_configWatcher->config()->group(QStringLiteral("FeedbackStyle"));
140 const bool busyCursor = c.readEntry("BusyCursor", true);
141
142 c = m_configWatcher->config()->group(QStringLiteral("BusyCursorSettings"));
143 m_timeout = std::chrono::seconds(c.readEntry("Timeout", s_startupDefaultTimeout));
144 m_startupInfo->setTimeout(m_timeout.count());
145 const bool busyBlinking = c.readEntry("Blinking", false);
146 const bool busyBouncing = c.readEntry("Bouncing", true);
147 if (!busyCursor) {
148 m_type = NoFeedback;
149 } else if (busyBouncing) {
150 m_type = BouncingFeedback;
151 } else if (busyBlinking) {
152 m_type = BlinkingFeedback;
154 ensureResources();
155 m_blinkingShader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/effects/startupfeedback/shaders/blinking-startup.frag"));
156 if (m_blinkingShader->isValid()) {
157 qCDebug(KWIN_STARTUPFEEDBACK) << "Blinking Shader is valid";
158 } else {
159 qCDebug(KWIN_STARTUPFEEDBACK) << "Blinking Shader is not valid";
160 }
161 }
162 } else {
163 m_type = PassiveFeedback;
164 }
165 if (m_active) {
166 stop();
167 start(m_startups[m_currentStartup]);
168 }
169}
170
171void StartupFeedbackEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
172{
173 int time = 0;
174 if (m_lastPresentTime.count()) {
175 time = (presentTime - m_lastPresentTime).count();
176 }
177 m_lastPresentTime = presentTime;
178
179 if (m_active && effects->isCursorHidden()) {
180 stop();
181 }
182 if (m_active) {
183 // need the unclipped version
184 switch (m_type) {
185 case BouncingFeedback:
186 m_progress = (m_progress + time) % BOUNCE_DURATION;
187 m_frame = qRound((qreal)m_progress / (qreal)BOUNCE_FRAME_DURATION) % BOUNCE_FRAMES;
188 m_currentGeometry = feedbackRect(); // bounce alters geometry with m_frame
189 data.paint = data.paint.united(m_currentGeometry);
190 break;
191 case BlinkingFeedback:
192 m_progress = (m_progress + time) % BLINKING_DURATION;
193 m_frame = qRound((qreal)m_progress / (qreal)BLINKING_FRAME_DURATION) % BLINKING_FRAMES;
194 break;
195 default:
196 break; // nothing
197 }
198 }
199 effects->prePaintScreen(data, presentTime);
200}
201
202void StartupFeedbackEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &region, Output *screen)
203{
204 effects->paintScreen(renderTarget, viewport, mask, region, screen);
205 if (m_active) {
206 GLTexture *texture;
207 switch (m_type) {
208 case BouncingFeedback:
209 texture = m_bouncingTextures[FRAME_TO_BOUNCE_TEXTURE[m_frame]].get();
210 break;
211 case BlinkingFeedback: // fall through
212 case PassiveFeedback:
213 texture = m_texture.get();
214 break;
215 default:
216 return; // safety
217 }
218 if (!texture) {
219 return;
220 }
221 glEnable(GL_BLEND);
222 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
223 GLShader *shader = nullptr;
224 if (m_type == BlinkingFeedback && m_blinkingShader && m_blinkingShader->isValid()) {
225 const QColor &blinkingColor = BLINKING_COLORS[FRAME_TO_BLINKING_COLOR[m_frame]];
226 ShaderManager::instance()->pushShader(m_blinkingShader.get());
227 shader = m_blinkingShader.get();
228 m_blinkingShader->setUniform(GLShader::ColorUniform::Color, blinkingColor);
229 } else {
231 }
232 const QRectF pixelGeometry = snapToPixelGridF(scaledRect(m_currentGeometry, viewport.scale()));
233 QMatrix4x4 mvp = viewport.projectionMatrix();
234 mvp.translate(pixelGeometry.x(), pixelGeometry.y());
236 shader->setColorspaceUniformsFromSRGB(renderTarget.colorDescription());
237 texture->render(pixelGeometry.size());
239 glDisable(GL_BLEND);
240 }
241}
242
244{
245 if (m_active) {
246 m_dirtyRect = m_currentGeometry; // ensure the now dirty region is cleaned on the next pass
247 if (m_type == BlinkingFeedback || m_type == BouncingFeedback) {
248 effects->addRepaint(m_dirtyRect); // we also have to trigger a repaint
249 }
250 }
252}
253
254void StartupFeedbackEffect::slotMouseChanged(const QPointF &pos, const QPointF &oldpos, Qt::MouseButtons buttons,
255 Qt::MouseButtons oldbuttons, Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers)
256{
257 if (m_active) {
258 m_dirtyRect |= m_currentGeometry;
259 m_currentGeometry = feedbackRect();
260 m_dirtyRect |= m_currentGeometry;
261 effects->addRepaint(m_dirtyRect);
262 }
263}
264
265void StartupFeedbackEffect::gotNewStartup(const QString &id, const QIcon &icon)
266{
267 Startup &startup = m_startups[id];
268 startup.icon = icon;
269
270 startup.expiredTimer = std::make_unique<QTimer>();
271 // Stop the animation if the startup doesn't finish within reasonable interval.
272 connect(startup.expiredTimer.get(), &QTimer::timeout, this, [this, id]() {
273 gotRemoveStartup(id);
274 });
275 startup.expiredTimer->setSingleShot(true);
276 startup.expiredTimer->start(m_timeout);
277
278 m_currentStartup = id;
279 start(startup);
280}
281
282void StartupFeedbackEffect::gotRemoveStartup(const QString &id)
283{
284 m_startups.remove(id);
285 if (m_startups.isEmpty()) {
286 m_currentStartup.clear();
287 stop();
288 return;
289 }
290 m_currentStartup = m_startups.begin().key();
291 start(m_startups[m_currentStartup]);
292}
293
294void StartupFeedbackEffect::gotStartupChange(const QString &id, const QIcon &icon)
295{
296 if (m_currentStartup == id) {
297 Startup &currentStartup = m_startups[m_currentStartup];
298 if (!icon.isNull() && icon.name() != currentStartup.icon.name()) {
299 currentStartup.icon = icon;
300 start(currentStartup);
301 }
302 }
303}
304
305void StartupFeedbackEffect::start(const Startup &startup)
306{
307 if (m_type == NoFeedback || m_splashVisible || effects->isCursorHidden()) {
308 return;
309 }
310
311 const Output *output = effects->screenAt(effects->cursorPos().toPoint());
312 if (!output) {
313 return;
314 }
315
316 if (!m_active) {
318 }
319 m_active = true;
320
321 // read details about the mouse-cursor theme define per default
322 KConfigGroup mousecfg(effects->inputConfig(), QStringLiteral("Mouse"));
323 m_cursorSize = mousecfg.readEntry("cursorSize", 24);
324
325 int iconSize = m_cursorSize / 1.5;
326 if (!iconSize) {
327 iconSize = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize);
328 }
329 // get ratio for bouncing cursor so we don't need to manually calculate the sizes for each icon size
330 if (m_type == BouncingFeedback) {
331 m_bounceSizesRatio = iconSize / 16.0;
332 }
333
334 const QPixmap iconPixmap = startup.icon.pixmap(iconSize);
335 prepareTextures(iconPixmap, output->scale());
336 m_dirtyRect = m_currentGeometry = feedbackRect();
337 effects->addRepaint(m_dirtyRect);
338}
339
340void StartupFeedbackEffect::stop()
341{
342 if (m_active) {
344 }
345 m_active = false;
346 m_lastPresentTime = std::chrono::milliseconds::zero();
348 switch (m_type) {
349 case BouncingFeedback:
350 for (int i = 0; i < 5; ++i) {
351 m_bouncingTextures[i].reset();
352 }
353 break;
354 case BlinkingFeedback:
355 case PassiveFeedback:
356 m_texture.reset();
357 break;
358 case NoFeedback:
359 return; // don't want the full repaint
360 default:
361 break; // impossible
362 }
364}
365
366void StartupFeedbackEffect::prepareTextures(const QPixmap &pix, qreal devicePixelRatio)
367{
369 switch (m_type) {
370 case BouncingFeedback:
371 for (int i = 0; i < 5; ++i) {
372 m_bouncingTextures[i] = GLTexture::upload(scalePixmap(pix, BOUNCE_SIZES[i], devicePixelRatio));
373 if (!m_bouncingTextures[i]) {
374 return;
375 }
376 m_bouncingTextures[i]->setFilter(GL_LINEAR);
377 m_bouncingTextures[i]->setWrapMode(GL_CLAMP_TO_EDGE);
378 }
379 break;
380 case BlinkingFeedback:
381 case PassiveFeedback:
382 m_texture = GLTexture::upload(pix);
383 if (!m_texture) {
384 return;
385 }
386 m_texture->setFilter(GL_LINEAR);
387 m_texture->setWrapMode(GL_CLAMP_TO_EDGE);
388 break;
389 default:
390 // for safety
391 m_active = false;
392 m_lastPresentTime = std::chrono::milliseconds::zero();
393 break;
394 }
395}
396
397QImage StartupFeedbackEffect::scalePixmap(const QPixmap &pm, const QSize &size, qreal devicePixelRatio) const
398{
399 const QSize &adjustedSize = size * m_bounceSizesRatio;
400 QImage scaled = pm.toImage().scaled(adjustedSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
401 if (scaled.format() != QImage::Format_ARGB32_Premultiplied && scaled.format() != QImage::Format_ARGB32) {
402 scaled.convertTo(QImage::Format_ARGB32);
403 }
404
405 QImage result(feedbackIconSize() * devicePixelRatio, QImage::Format_ARGB32);
406 result.setDevicePixelRatio(devicePixelRatio);
407
408 QPainter p(&result);
409 p.setCompositionMode(QPainter::CompositionMode_Source);
410 p.fillRect(result.rect(), Qt::transparent);
411 p.drawImage(QRectF((20 * m_bounceSizesRatio - adjustedSize.width()) / 2,
412 (20 * m_bounceSizesRatio - adjustedSize.height()) / 2,
413 adjustedSize.width(),
414 adjustedSize.height()),
415 scaled);
416 return result;
417}
418
419QSize StartupFeedbackEffect::feedbackIconSize() const
420{
421 return QSize(20, 20) * m_bounceSizesRatio;
422}
423
424QRect StartupFeedbackEffect::feedbackRect() const
425{
426 int xDiff;
427 if (m_cursorSize <= 16) {
428 xDiff = 8 + 7;
429 } else if (m_cursorSize <= 32) {
430 xDiff = 16 + 7;
431 } else if (m_cursorSize <= 48) {
432 xDiff = 24 + 7;
433 } else {
434 xDiff = 32 + 7;
435 }
436 int yDiff = xDiff;
437 GLTexture *texture = nullptr;
438 int yOffset = 0;
439 switch (m_type) {
440 case BouncingFeedback:
441 texture = m_bouncingTextures[FRAME_TO_BOUNCE_TEXTURE[m_frame]].get();
442 yOffset = FRAME_TO_BOUNCE_YOFFSET[m_frame] * m_bounceSizesRatio;
443 break;
444 case BlinkingFeedback: // fall through
445 case PassiveFeedback:
446 texture = m_texture.get();
447 break;
448 default:
449 // nothing
450 break;
451 }
452 const QPoint cursorPos = effects->cursorPos().toPoint() + QPoint(xDiff, yDiff + yOffset);
453 QRect rect;
454 if (texture) {
455 rect = QRect(cursorPos, feedbackIconSize());
456 }
457 return rect;
458}
459
461{
462 return m_active;
463}
464
465} // namespace
466
467#include "moc_startupfeedback.cpp"
void startupRemoved(const QString &id)
void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &region, Output *screen)
void startupAdded(const QString &id, const QIcon &icon)
Q_SCRIPTABLE void addRepaint(const QRectF &r)
bool makeOpenGLContextCurrent()
Makes the OpenGL compositing context current.
CompositingType compositingType
Output * screenAt(const QPoint &point) const
KSharedConfigPtr inputConfig() const
void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
bool isOpenGLCompositing() const
Whether the Compositor is OpenGL based (either GL 1 or 2).
xcb_connection_t * xcbConnection() const
void mouseChanged(const QPointF &pos, const QPointF &oldpos, Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers)
xcb_window_t x11RootWindow() const
void startupChanged(const QString &id, const QIcon &icon)
Q_SCRIPTABLE void addRepaintFull()
bool setColorspaceUniformsFromSRGB(const ColorDescription &dst)
Definition glshader.cpp:457
bool setUniform(const char *name, float value)
Definition glshader.cpp:301
static std::unique_ptr< GLTexture > upload(const QImage &image)
void render(const QSizeF &size)
const ColorDescription & colorDescription() const
QMatrix4x4 projectionMatrix() const
std::unique_ptr< GLShader > generateShaderFromFile(ShaderTraits traits, const QString &vertexFile=QString(), const QString &fragmentFile=QString())
static ShaderManager * instance()
GLShader * pushShader(ShaderTraits traits)
bool isActive() const override
void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override
void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &region, Output *screen) override
void reconfigure(ReconfigureFlags flags) override
static QPointF cursorPos()
Definition effect.cpp:478
@ ReconfigureAll
Definition effect.h:601
KWIN_EXPORT QPointF snapToPixelGridF(const QPointF &point)
Definition pixelgrid.h:21
@ OpenGLCompositing
Definition globals.h:37
KWIN_EXPORT QRectF scaledRect(const QRectF &rect, qreal scale)
Definition globals.h:243
EffectsHandler * effects