startupfeedback.cpp 13.6 KB
Newer Older
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
1
2
3
/*
    KWin - the KDE window manager
    This file is part of the KDE project.
Martin Flöser's avatar
Martin Flöser committed
4

Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
5
    SPDX-FileCopyrightText: 2010 Martin Gräßlin <mgraesslin@kde.org>
6
    SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
Martin Flöser's avatar
Martin Flöser committed
7

Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
8
9
    SPDX-License-Identifier: GPL-2.0-or-later
*/
Martin Flöser's avatar
Martin Flöser committed
10
11
#include "startupfeedback.h"
// Qt
12
#include <QApplication>
13
14
#include <QDBusConnectionInterface>
#include <QDBusServiceWatcher>
15
#include <QFile>
16
#include <QSize>
17
#include <QStyle>
18
19
#include <QStandardPaths>
#include <QPainter>
Martin Flöser's avatar
Martin Flöser committed
20
// KDE
21
#include <KConfigGroup>
22
#include <KSharedConfig>
23
#include <KSelectionOwner>
24
#include <KWindowSystem>
Martin Flöser's avatar
Martin Flöser committed
25
26
27
28
// KWin
#include <kwinglutils.h>

// based on StartupId in KRunner by Lubos Lunak
29
// SPDX-FileCopyrightText: 2001 Lubos Lunak <l.lunak@kde.org>
Martin Flöser's avatar
Martin Flöser committed
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

namespace KWin
{

// number of key frames for bouncing animation
static const int BOUNCE_FRAMES = 20;
// duration between two key frames in msec
static const int BOUNCE_FRAME_DURATION = 30;
// duration of one bounce animation
static const int BOUNCE_DURATION = BOUNCE_FRAME_DURATION * BOUNCE_FRAMES;
// number of key frames for blinking animation
static const int BLINKING_FRAMES = 5;
// duration between two key frames in msec
static const int BLINKING_FRAME_DURATION = 100;
// duration of one blinking animation
static const int BLINKING_DURATION = BLINKING_FRAME_DURATION * BLINKING_FRAMES;
//const int color_to_pixmap[] = { 0, 1, 2, 3, 2, 1 };
47
static const int FRAME_TO_BOUNCE_YOFFSET[] = {
Martin Flöser's avatar
Martin Flöser committed
48
    -5, -1, 2, 5, 8, 10, 12, 13, 15, 15, 15, 15, 14, 12, 10, 8, 5, 2, -1, -5
49
50
};
static const QSize BOUNCE_SIZES[] = {
Martin Flöser's avatar
Martin Flöser committed
51
    QSize(16, 16), QSize(14, 18), QSize(12, 20), QSize(18, 14), QSize(20, 12)
52
53
};
static const int FRAME_TO_BOUNCE_TEXTURE[] = {
Martin Flöser's avatar
Martin Flöser committed
54
    0, 0, 0, 1, 2, 2, 1, 0, 3, 4, 4, 3, 0, 1, 2, 2, 1, 0, 0, 0
55
56
};
static const int FRAME_TO_BLINKING_COLOR[] = {
Martin Flöser's avatar
Martin Flöser committed
57
    0, 1, 2, 3, 2, 1
58
59
};
static const QColor BLINKING_COLORS[] = {
Martin Flöser's avatar
Martin Flöser committed
60
    Qt::black, Qt::darkGray, Qt::lightGray, Qt::white, Qt::white
61
};
62
static const int s_startupDefaultTimeout = 5;
Martin Flöser's avatar
Martin Flöser committed
63
64

StartupFeedbackEffect::StartupFeedbackEffect()
65
66
    : m_bounceSizesRatio(1.0)
    , m_startupInfo(new KStartupInfo(KStartupInfo::CleanOnCantDetect, this))
67
    , m_selection(nullptr)
68
69
70
    , m_active(false)
    , m_frame(0)
    , m_progress(0)
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
71
    , m_texture(nullptr)
72
    , m_type(BouncingFeedback)
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
73
    , m_blinkingShader(nullptr)
74
    , m_cursorSize(24)
75
    , m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("klaunchrc", KConfig::NoGlobals)))
76
    , m_splashVisible(false)
77
78
{
    for (int i = 0; i < 5; ++i) {
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
79
        m_bouncingTextures[i] = nullptr;
Martin Flöser's avatar
Martin Flöser committed
80
    }
81
82
83
84
    if (KWindowSystem::isPlatformX11()) {
        m_selection = new KSelectionOwner("_KDE_STARTUP_FEEDBACK", xcbConnection(), x11RootWindow(), this);
        m_selection->claim(true);
    }
85
86
87
88
    connect(m_startupInfo, &KStartupInfo::gotNewStartup, this, &StartupFeedbackEffect::gotNewStartup);
    connect(m_startupInfo, &KStartupInfo::gotRemoveStartup, this, &StartupFeedbackEffect::gotRemoveStartup);
    connect(m_startupInfo, &KStartupInfo::gotStartupChange, this, &StartupFeedbackEffect::gotStartupChange);
    connect(effects, &EffectsHandler::mouseChanged, this, &StartupFeedbackEffect::slotMouseChanged);
89
90
91
    connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this]() {
        reconfigure(ReconfigureAll);
    });
92
    reconfigure(ReconfigureAll);
93

94
95
96
97
98
99
100
101
102
103
    m_splashVisible = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.KSplash"));
    auto serviceWatcher = new QDBusServiceWatcher(QStringLiteral("org.kde.KSplash"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this);
    connect(serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this] {
        m_splashVisible = true;
        stop();
    });
    connect(serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] {
        m_splashVisible = false;
        gotRemoveStartup(KStartupInfoId(), KStartupInfoData()); // Start the next feedback
    });
104
}
Martin Flöser's avatar
Martin Flöser committed
105
106

StartupFeedbackEffect::~StartupFeedbackEffect()
107
108
{
    if (m_active) {
Martin Flöser's avatar
Martin Flöser committed
109
        effects->stopMousePolling();
110
111
    }
    for (int i = 0; i < 5; ++i) {
Martin Flöser's avatar
Martin Flöser committed
112
        delete m_bouncingTextures[i];
113
    }
Martin Flöser's avatar
Martin Flöser committed
114
    delete m_texture;
115
    delete m_blinkingShader;
116
}
Martin Flöser's avatar
Martin Flöser committed
117
118

bool StartupFeedbackEffect::supported()
119
{
120
    return effects->isOpenGLCompositing();
121
}
Martin Flöser's avatar
Martin Flöser committed
122

123
124
125
void StartupFeedbackEffect::reconfigure(Effect::ReconfigureFlags flags)
{
    Q_UNUSED(flags)
126
    KConfigGroup c = m_configWatcher->config()->group("FeedbackStyle");
127
    const bool busyCursor = c.readEntry("BusyCursor", true);
Martin Flöser's avatar
Martin Flöser committed
128

129
    c = m_configWatcher->config()->group("BusyCursorSettings");
130
    m_startupInfo->setTimeout(c.readEntry("Timeout", s_startupDefaultTimeout));
131
132
133
    const bool busyBlinking = c.readEntry("Blinking", false);
    const bool busyBouncing = c.readEntry("Bouncing", true);
    if (!busyCursor)
Martin Flöser's avatar
Martin Flöser committed
134
        m_type = NoFeedback;
135
    else if (busyBouncing)
Martin Flöser's avatar
Martin Flöser committed
136
        m_type = BouncingFeedback;
137
    else if (busyBlinking) {
Martin Flöser's avatar
Martin Flöser committed
138
        m_type = BlinkingFeedback;
139
        if (effects->compositingType() == OpenGL2Compositing) {
140
            delete m_blinkingShader;
141
142
143
            m_blinkingShader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture, QString(), QStringLiteral("blinking-startup-fragment.glsl"));
            if (m_blinkingShader->isValid()) {
                qCDebug(KWINEFFECTS) << "Blinking Shader is valid";
144
            } else {
145
                qCDebug(KWINEFFECTS) << "Blinking Shader is not valid";
146
147
            }
        }
148
    } else
Martin Flöser's avatar
Martin Flöser committed
149
        m_type = PassiveFeedback;
150
    if (m_active) {
Martin Flöser's avatar
Martin Flöser committed
151
        stop();
152
        start(m_startups[ m_currentStartup ]);
Martin Flöser's avatar
Martin Flöser committed
153
    }
154
}
Martin Flöser's avatar
Martin Flöser committed
155

156
157
158
void StartupFeedbackEffect::prePaintScreen(ScreenPrePaintData& data, int time)
{
    if (m_active) {
Martin Flöser's avatar
Martin Flöser committed
159
        // need the unclipped version
160
        switch(m_type) {
Martin Flöser's avatar
Martin Flöser committed
161
162
        case BouncingFeedback:
            m_progress = (m_progress + time) % BOUNCE_DURATION;
163
164
            m_frame = qRound((qreal)m_progress / (qreal)BOUNCE_FRAME_DURATION) % BOUNCE_FRAMES;
            m_currentGeometry = feedbackRect(); // bounce alters geometry with m_frame
165
            data.paint = data.paint.united(m_currentGeometry);
Martin Flöser's avatar
Martin Flöser committed
166
167
168
            break;
        case BlinkingFeedback:
            m_progress = (m_progress + time) % BLINKING_DURATION;
169
            m_frame = qRound((qreal)m_progress / (qreal)BLINKING_FRAME_DURATION) % BLINKING_FRAMES;
Martin Flöser's avatar
Martin Flöser committed
170
171
172
173
174
            break;
        default:
            break; // nothing
        }
    }
175
176
    effects->prePaintScreen(data, time);
}
Martin Flöser's avatar
Martin Flöser committed
177

Albert Astals Cid's avatar
Albert Astals Cid committed
178
void StartupFeedbackEffect::paintScreen(int mask, const QRegion &region, ScreenPaintData& data)
179
180
181
{
    effects->paintScreen(mask, region, data);
    if (m_active) {
Martin Flöser's avatar
Martin Flöser committed
182
        GLTexture* texture;
183
        switch(m_type) {
Martin Flöser's avatar
Martin Flöser committed
184
185
186
187
188
189
190
191
192
        case BouncingFeedback:
            texture = m_bouncingTextures[ FRAME_TO_BOUNCE_TEXTURE[ m_frame ]];
            break;
        case BlinkingFeedback: // fall through
        case PassiveFeedback:
            texture = m_texture;
            break;
        default:
            return; // safety
193
194
195
        }
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Martin Flöser's avatar
Martin Flöser committed
196
        texture->bind();
197
        if (m_type == BlinkingFeedback && m_blinkingShader && m_blinkingShader->isValid()) {
Martin Flöser's avatar
Martin Flöser committed
198
            const QColor& blinkingColor = BLINKING_COLORS[ FRAME_TO_BLINKING_COLOR[ m_frame ]];
199
200
            ShaderManager::instance()->pushShader(m_blinkingShader);
            m_blinkingShader->setUniform(GLShader::Color, blinkingColor);
201
        } else {
202
            ShaderManager::instance()->pushShader(ShaderTrait::MapTexture);
203
        }
204
205
206
        QMatrix4x4 mvp = data.projectionMatrix();
        mvp.translate(m_currentGeometry.x(), m_currentGeometry.y());
        ShaderManager::instance()->getBoundShader()->setUniform(GLShader::ModelViewProjectionMatrix, mvp);
207
        texture->render(m_currentGeometry, m_currentGeometry);
208
        ShaderManager::instance()->popShader();
Martin Flöser's avatar
Martin Flöser committed
209
        texture->unbind();
210
        glDisable(GL_BLEND);
Martin Flöser's avatar
Martin Flöser committed
211
    }
212
}
Martin Flöser's avatar
Martin Flöser committed
213
214

void StartupFeedbackEffect::postPaintScreen()
215
216
{
    if (m_active) {
Thomas Lübking's avatar
Thomas Lübking committed
217
218
219
        m_dirtyRect = m_currentGeometry; // ensure the now dirty region is cleaned on the next pass
        if (m_type == BlinkingFeedback || m_type == BouncingFeedback)
            effects->addRepaint(m_dirtyRect); // we also have to trigger a repaint
Martin Flöser's avatar
Martin Flöser committed
220
    }
221
222
    effects->postPaintScreen();
}
Martin Flöser's avatar
Martin Flöser committed
223

Martin Flöser's avatar
Martin Flöser committed
224
void StartupFeedbackEffect::slotMouseChanged(const QPoint& pos, const QPoint& oldpos, Qt::MouseButtons buttons,
225
226
227
228
229
230
231
232
233
        Qt::MouseButtons oldbuttons, Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers)
{
    Q_UNUSED(pos)
    Q_UNUSED(oldpos)
    Q_UNUSED(buttons)
    Q_UNUSED(oldbuttons)
    Q_UNUSED(modifiers)
    Q_UNUSED(oldmodifiers)
    if (m_active) {
Thomas Lübking's avatar
Thomas Lübking committed
234
235
236
237
        m_dirtyRect |= m_currentGeometry;
        m_currentGeometry = feedbackRect();
        m_dirtyRect |= m_currentGeometry;
        effects->addRepaint(m_dirtyRect);
Martin Flöser's avatar
Martin Flöser committed
238
    }
239
}
Martin Flöser's avatar
Martin Flöser committed
240

241
242
void StartupFeedbackEffect::gotNewStartup(const KStartupInfoId& id, const KStartupInfoData& data)
{
Martin Flöser's avatar
Martin Flöser committed
243
244
245
    const QString& icon = data.findIcon();
    m_currentStartup = id;
    m_startups[ id ] = icon;
246
247
    start(icon);
}
Martin Flöser's avatar
Martin Flöser committed
248

249
250
void StartupFeedbackEffect::gotRemoveStartup(const KStartupInfoId& id, const KStartupInfoData& data)
{
Laurent Montel's avatar
Laurent Montel committed
251
    Q_UNUSED( data )
252
253
    m_startups.remove(id);
    if (m_startups.count() == 0) {
Martin Flöser's avatar
Martin Flöser committed
254
255
256
257
        m_currentStartup = KStartupInfoId(); // null
        stop();
        return;
    }
258
259
260
    m_currentStartup = m_startups.begin().key();
    start(m_startups[ m_currentStartup ]);
}
Martin Flöser's avatar
Martin Flöser committed
261

262
263
264
void StartupFeedbackEffect::gotStartupChange(const KStartupInfoId& id, const KStartupInfoData& data)
{
    if (m_currentStartup == id) {
Martin Flöser's avatar
Martin Flöser committed
265
        const QString& icon = data.findIcon();
266
        if (!icon.isEmpty() && icon != m_startups[ m_currentStartup ]) {
Martin Flöser's avatar
Martin Flöser committed
267
            m_startups[ id ] = icon;
268
            start(icon);
Martin Flöser's avatar
Martin Flöser committed
269
270
        }
    }
271
}
Martin Flöser's avatar
Martin Flöser committed
272

273
274
void StartupFeedbackEffect::start(const QString& icon)
{
275
    if (m_type == NoFeedback || m_splashVisible)
Martin Flöser's avatar
Martin Flöser committed
276
        return;
277
    if (!m_active)
278
        effects->startMousePolling();
Martin Flöser's avatar
Martin Flöser committed
279
    m_active = true;
280
281
    auto readCursorSize = []() -> int {
        // read details about the mouse-cursor theme define per default
282
        KConfigGroup mousecfg(effects->inputConfig(), "Mouse");
283
        int cursorSize  = mousecfg.readEntry("cursorSize", 24);
284
285
286
        return cursorSize;
    };
    m_cursorSize = readCursorSize();
287
288
    int iconSize = m_cursorSize / 1.5;
    if (!iconSize) {
289
        iconSize = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize);
290
    }
291
292
293
294
295
    // get ratio for bouncing cursor so we don't need to manually calculate the sizes for each icon size
    if (m_type == BouncingFeedback)
        m_bounceSizesRatio = iconSize / 16.0;
    const QPixmap iconPixmap = QIcon::fromTheme(icon, QIcon::fromTheme(QStringLiteral("system-run"))).pixmap(iconSize);
    prepareTextures(iconPixmap);
296
    m_dirtyRect = m_currentGeometry = feedbackRect();
Thomas Lübking's avatar
Thomas Lübking committed
297
    effects->addRepaint(m_dirtyRect);
298
}
Martin Flöser's avatar
Martin Flöser committed
299
300

void StartupFeedbackEffect::stop()
301
302
{
    if (m_active)
303
        effects->stopMousePolling();
Martin Flöser's avatar
Martin Flöser committed
304
    m_active = false;
305
    effects->makeOpenGLContextCurrent();
306
    switch(m_type) {
Martin Flöser's avatar
Martin Flöser committed
307
    case BouncingFeedback:
308
        for (int i = 0; i < 5; ++i) {
Martin Flöser's avatar
Martin Flöser committed
309
            delete m_bouncingTextures[i];
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
310
            m_bouncingTextures[i] = nullptr;
311
        }
Martin Flöser's avatar
Martin Flöser committed
312
313
314
315
        break;
    case BlinkingFeedback:
    case PassiveFeedback:
        delete m_texture;
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
316
        m_texture = nullptr;
Martin Flöser's avatar
Martin Flöser committed
317
318
319
320
321
322
        break;
    case NoFeedback:
        return; // don't want the full repaint
    default:
        break; // impossible
    }
323
324
    effects->addRepaintFull();
}
Martin Flöser's avatar
Martin Flöser committed
325

326
327
void StartupFeedbackEffect::prepareTextures(const QPixmap& pix)
{
328
    effects->makeOpenGLContextCurrent();
329
    switch(m_type) {
Martin Flöser's avatar
Martin Flöser committed
330
    case BouncingFeedback:
331
        for (int i = 0; i < 5; ++i) {
Martin Flöser's avatar
Martin Flöser committed
332
            delete m_bouncingTextures[i];
333
334
            m_bouncingTextures[i] = new GLTexture(scalePixmap(pix, BOUNCE_SIZES[i]));
        }
Martin Flöser's avatar
Martin Flöser committed
335
336
337
        break;
    case BlinkingFeedback:
    case PassiveFeedback:
338
        m_texture = new GLTexture(pix);
Martin Flöser's avatar
Martin Flöser committed
339
340
341
342
343
344
        break;
    default:
        // for safety
        m_active = false;
        break;
    }
345
}
Martin Flöser's avatar
Martin Flöser committed
346

347
348
QImage StartupFeedbackEffect::scalePixmap(const QPixmap& pm, const QSize& size) const
{
349
350
    const QSize& adjustedSize = size * m_bounceSizesRatio;
    QImage scaled = pm.toImage().scaled(adjustedSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
351
352
    if (scaled.format() != QImage::Format_ARGB32_Premultiplied && scaled.format() != QImage::Format_ARGB32)
        scaled = scaled.convertToFormat(QImage::Format_ARGB32);
Martin Flöser's avatar
Martin Flöser committed
353

354
    QImage result(20 * m_bounceSizesRatio, 20 * m_bounceSizesRatio, QImage::Format_ARGB32);
355
356
357
    QPainter p(&result);
    p.setCompositionMode(QPainter::CompositionMode_Source);
    p.fillRect(result.rect(), Qt::transparent);
358
    p.drawImage((20 * m_bounceSizesRatio - adjustedSize.width()) / 2, (20*m_bounceSizesRatio - adjustedSize.height()) / 2, scaled, 0, 0, adjustedSize.width(), adjustedSize.height() * m_bounceSizesRatio);
Martin Flöser's avatar
Martin Flöser committed
359
    return result;
360
}
Martin Flöser's avatar
Martin Flöser committed
361
362

QRect StartupFeedbackEffect::feedbackRect() const
363
{
Martin Flöser's avatar
Martin Flöser committed
364
    int xDiff;
365
    if (m_cursorSize <= 16)
Martin Flöser's avatar
Martin Flöser committed
366
        xDiff = 8 + 7;
367
    else if (m_cursorSize <= 32)
Martin Flöser's avatar
Martin Flöser committed
368
        xDiff = 16 + 7;
369
    else if (m_cursorSize <= 48)
Martin Flöser's avatar
Martin Flöser committed
370
371
372
373
        xDiff = 24 + 7;
    else
        xDiff = 32 + 7;
    int yDiff = xDiff;
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
374
    GLTexture* texture = nullptr;
Martin Flöser's avatar
Martin Flöser committed
375
    int yOffset = 0;
376
377
378
    switch(m_type) {
    case BouncingFeedback:
        texture = m_bouncingTextures[ FRAME_TO_BOUNCE_TEXTURE[ m_frame ]];
379
        yOffset = FRAME_TO_BOUNCE_YOFFSET[ m_frame ] * m_bounceSizesRatio;
380
381
382
383
384
385
386
387
        break;
    case BlinkingFeedback: // fall through
    case PassiveFeedback:
        texture = m_texture;
        break;
    default:
        // nothing
        break;
Martin Flöser's avatar
Martin Flöser committed
388
    }
389
    const QPoint cursorPos = effects->cursorPos() + QPoint(xDiff, yDiff + yOffset);
Laurent Montel's avatar
Laurent Montel committed
390
391
392
    QRect rect;
    if( texture )
       rect = QRect(cursorPos, texture->size());
393
394
    return rect;
}
Martin Flöser's avatar
Martin Flöser committed
395

396
397
398
399
400
bool StartupFeedbackEffect::isActive() const
{
    return m_active;
}

Martin Flöser's avatar
Martin Flöser committed
401
} // namespace