screenedge.cpp 49.3 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.
Arthur Arlt's avatar
Arthur Arlt committed
4

Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
5
6
    SPDX-FileCopyrightText: 2011 Arthur Arlt <a.arlt@stud.uni-heidelberg.de>
    SPDX-FileCopyrightText: 2013 Martin Gräßlin <mgraesslin@kde.org>
Arthur Arlt's avatar
Arthur Arlt committed
7

Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
8
9
10
    Since the functionality provided in this class has been moved from
    class Workspace, it is not clear who exactly has written the code.
    The list below contains the copyright holders of the class Workspace.
Arthur Arlt's avatar
Arthur Arlt committed
11

Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
12
13
14
    SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich <ettrich@kde.org>
    SPDX-FileCopyrightText: 2003 Lubos Lunak <l.lunak@kde.org>
    SPDX-FileCopyrightText: 2009 Lucas Murray <lmurray@undefinedfire.com>
Arthur Arlt's avatar
Arthur Arlt committed
15

Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
16
17
    SPDX-License-Identifier: GPL-2.0-or-later
*/
Arthur Arlt's avatar
Arthur Arlt committed
18
19
20

#include "screenedge.h"

21
#include <config-kwin.h>
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
22

23
24
#include "core/output.h"
#include "core/platform.h"
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
25
#include "cursor.h"
26
#include "effects.h"
27
#include "gestures.h"
28
#include "main.h"
29
#include "utils/common.h"
30
#include "virtualdesktops.h"
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
31
#include <workspace.h>
32
#include <x11window.h>
33
// DBus generated
34
#if KWIN_BUILD_SCREENLOCKER
35
#include "screenlocker_interface.h"
36
#endif
37
38
// frameworks
#include <KConfigGroup>
Arthur Arlt's avatar
Arthur Arlt committed
39
// Qt
40
#include <QAbstractEventDispatcher>
41
#include <QAction>
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
42
43
#include <QDBusInterface>
#include <QDBusPendingCall>
44
45
#include <QFontDatabase>
#include <QFontMetrics>
46
#include <QMouseEvent>
47
#include <QTextStream>
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
48
#include <QTimer>
49
#include <QWidget>
Arthur Arlt's avatar
Arthur Arlt committed
50

Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
51
52
namespace KWin
{
Arthur Arlt's avatar
Arthur Arlt committed
53

54
55
56
// Mouse should not move more than this many pixels
static const int DISTANCE_RESET = 30;

Jan Blackquill's avatar
Jan Blackquill committed
57
58
59
60
61
62
// How large the touch target of the area recognizing touch gestures is
static const int TOUCH_TARGET = 3;

// How far the user needs to swipe before triggering an action.
static const int MINIMUM_DELTA = 44;

63
64
65
TouchCallback::TouchCallback(QAction *touchUpAction, TouchCallback::CallbackFunction progressCallback)
    : m_touchUpAction(touchUpAction)
    , m_progressCallback(progressCallback)
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
66
67
{
}
68
69

TouchCallback::~TouchCallback()
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
70
71
{
}
72
73
74
75
76
77

QAction *TouchCallback::touchUpAction() const
{
    return m_touchUpAction;
}

78
void TouchCallback::progressCallback(ElectricBorder border, const QSizeF &deltaProgress, Output *output) const
79
80
81
82
83
84
85
86
87
88
89
{
    if (m_progressCallback) {
        m_progressCallback(border, deltaProgress, output);
    }
}

bool TouchCallback::hasProgressCallback() const
{
    return m_progressCallback != nullptr;
}

90
91
92
93
94
95
Edge::Edge(ScreenEdges *parent)
    : QObject(parent)
    , m_edges(parent)
    , m_border(ElectricNone)
    , m_action(ElectricActionNone)
    , m_reserved(0)
96
    , m_approaching(false)
97
    , m_lastApproachingFactor(0)
98
    , m_blocked(false)
99
    , m_pushBackBlocked(false)
100
    , m_client(nullptr)
101
    , m_output(nullptr)
102
103
    , m_gesture(new SwipeGesture(this))
{
104
105
    m_gesture->setMinimumFingerCount(1);
    m_gesture->setMaximumFingerCount(1);
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
106
107
    connect(
        m_gesture, &Gesture::triggered, this, [this]() {
108
109
110
111
112
113
            stopApproaching();
            if (m_client) {
                m_client->showOnScreenEdge();
                unreserve();
                return;
            }
114
            handleTouchAction();
115
            handleTouchCallback();
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
116
117
        },
        Qt::QueuedConnection);
118
119
    connect(m_gesture, &SwipeGesture::started, this, &Edge::startApproaching);
    connect(m_gesture, &SwipeGesture::cancelled, this, &Edge::stopApproaching);
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
120
121
122
    connect(m_gesture, &SwipeGesture::cancelled, this, [this]() {
        if (!m_touchCallbacks.isEmpty() && m_touchCallbacks.constFirst().hasProgressCallback()) {
            handleTouchCallback();
123
        }
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
124
    });
125
    connect(m_gesture, &SwipeGesture::progress, this, [this](qreal progress) {
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
126
127
128
129
130
131
        int factor = progress * 256.0f;
        if (m_lastApproachingFactor != factor) {
            m_lastApproachingFactor = factor;
            Q_EMIT approaching(border(), m_lastApproachingFactor / 256.0f, m_approachGeometry);
        }
    });
132
    connect(m_gesture, &SwipeGesture::deltaProgress, this, [this](const QSizeF &progressDelta) {
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
133
134
135
136
137
138
139
140
141
142
        if (!m_touchCallbacks.isEmpty()) {
            m_touchCallbacks.constFirst().progressCallback(border(), progressDelta, m_output);
        }
    });
    connect(this, &Edge::activatesForTouchGestureChanged, this, [this]() {
        if (isReserved()) {
            if (activatesForTouchGesture()) {
                m_edges->gestureRecognizer()->registerSwipeGesture(m_gesture);
            } else {
                m_edges->gestureRecognizer()->unregisterSwipeGesture(m_gesture);
143
144
            }
        }
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
145
    });
Arthur Arlt's avatar
Arthur Arlt committed
146
147
}

148
Edge::~Edge()
Arthur Arlt's avatar
Arthur Arlt committed
149
150
151
{
}

152
void Edge::reserve()
153
{
154
155
156
157
158
    m_reserved++;
    if (m_reserved == 1) {
        // got activated
        activate();
    }
159
160
}

161
162
void Edge::reserve(QObject *object, const char *slot)
{
163
    connect(object, &QObject::destroyed, this, qOverload<QObject *>(&Edge::unreserve));
164
165
166
167
    m_callBacks.insert(object, QByteArray(slot));
    reserve();
}

168
169
void Edge::reserveTouchCallBack(QAction *action, TouchCallback::CallbackFunction callback)
{
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
170
171
172
173
    if (auto itr = std::find_if(m_touchCallbacks.constBegin(), m_touchCallbacks.constEnd(), [action](const TouchCallback &c) {
            return c.touchUpAction() == action;
        });
        itr != m_touchCallbacks.constEnd()) {
174
175
176
177
178
179
        return;
    }
    reserveTouchCallBack(TouchCallback(action, callback));
}

void Edge::reserveTouchCallBack(const TouchCallback &callback)
180
{
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
181
182
183
184
    if (auto itr = std::find_if(m_touchCallbacks.constBegin(), m_touchCallbacks.constEnd(), [callback](const TouchCallback &c) {
            return c.touchUpAction() == callback.touchUpAction();
        });
        itr != m_touchCallbacks.constEnd()) {
185
186
        return;
    }
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
187
188
189
    connect(callback.touchUpAction(), &QAction::destroyed, this, [this, callback]() {
        unreserveTouchCallBack(callback.touchUpAction());
    });
190
    m_touchCallbacks << callback;
191
192
193
194
195
    reserve();
}

void Edge::unreserveTouchCallBack(QAction *action)
{
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
196
197
198
    auto it = std::find_if(m_touchCallbacks.begin(), m_touchCallbacks.end(), [action](const TouchCallback &c) {
        return c.touchUpAction() == action;
    });
199
200
    if (it != m_touchCallbacks.end()) {
        m_touchCallbacks.erase(it);
201
202
203
204
        unreserve();
    }
}

205
void Edge::unreserve()
Arthur Arlt's avatar
Arthur Arlt committed
206
{
207
208
209
    m_reserved--;
    if (m_reserved == 0) {
        // got deactivated
210
        stopApproaching();
211
212
213
        deactivate();
    }
}
214
215
void Edge::unreserve(QObject *object)
{
216
    if (m_callBacks.remove(object) > 0) {
217
        disconnect(object, &QObject::destroyed, this, qOverload<QObject *>(&Edge::unreserve));
218
219
220
        unreserve();
    }
}
Arthur Arlt's avatar
Arthur Arlt committed
221

222
223
224
225
226
227
228
229
bool Edge::activatesForPointer() const
{
    if (m_client) {
        return true;
    }
    if (m_edges->isDesktopSwitching()) {
        return true;
    }
230
    if (m_edges->isDesktopSwitchingMovingClients()) {
231
        auto c = Workspace::self()->moveResizeWindow();
232
        if (c && !c->isInteractiveResize()) {
233
234
235
            return true;
        }
    }
236
237
238
239
240
241
242
243
244
245
246
247
248
249
    if (!m_callBacks.isEmpty()) {
        return true;
    }
    if (m_action != ElectricActionNone) {
        return true;
    }
    return false;
}

bool Edge::activatesForTouchGesture() const
{
    if (!isScreenEdge()) {
        return false;
    }
250
251
252
    if (m_blocked) {
        return false;
    }
253
254
255
256
257
258
    if (m_client) {
        return true;
    }
    if (m_touchAction != ElectricActionNone) {
        return true;
    }
259
    if (!m_touchCallbacks.isEmpty()) {
260
261
        return true;
    }
262
263
264
    return false;
}

265
266
bool Edge::triggersFor(const QPoint &cursorPos) const
{
267
268
269
    if (isBlocked()) {
        return false;
    }
270
271
272
    if (!activatesForPointer()) {
        return false;
    }
273
274
275
276
277
278
    if (!m_geometry.contains(cursorPos)) {
        return false;
    }
    if (isLeft() && cursorPos.x() != m_geometry.x()) {
        return false;
    }
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
279
    if (isRight() && cursorPos.x() != (m_geometry.x() + m_geometry.width() - 1)) {
280
281
282
283
284
        return false;
    }
    if (isTop() && cursorPos.y() != m_geometry.y()) {
        return false;
    }
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
285
    if (isBottom() && cursorPos.y() != (m_geometry.y() + m_geometry.height() - 1)) {
286
287
288
289
290
        return false;
    }
    return true;
}

291
bool Edge::check(const QPoint &cursorPos, const QDateTime &triggerTime, bool forceNoPushBack)
292
293
{
    if (!triggersFor(cursorPos)) {
294
        return false;
295
    }
296
    if (m_lastTrigger.isValid() && // still in cooldown
297
        m_lastTrigger.msecsTo(triggerTime) < edges()->reActivationThreshold() - edges()->timeThreshold()) {
298
299
        // Reset the time, so the user has to actually keep the mouse still for this long to retrigger
        m_lastTrigger = triggerTime;
300
301
        return false;
    }
302
    // no pushback so we have to activate at once
303
    bool directActivate = forceNoPushBack || edges()->cursorPushBackDistance().isNull() || m_client;
304
    if (directActivate || canActivate(cursorPos, triggerTime)) {
305
        markAsTriggered(cursorPos, triggerTime);
306
        handle(cursorPos);
307
        return true;
308
309
    } else {
        pushCursorBack(cursorPos);
310
        m_triggeredPoint = cursorPos;
311
    }
312
313
314
315
316
317
318
    return false;
}

void Edge::markAsTriggered(const QPoint &cursorPos, const QDateTime &triggerTime)
{
    m_lastTrigger = triggerTime;
    m_lastReset = QDateTime(); // invalidate
319
    m_triggeredPoint = cursorPos;
Arthur Arlt's avatar
Arthur Arlt committed
320
321
}

322
bool Edge::canActivate(const QPoint &cursorPos, const QDateTime &triggerTime)
323
{
Yuri Chornoivan's avatar
Yuri Chornoivan committed
324
    // we check whether either the timer has explicitly been invalidated (successful trigger) or is
325
    // bigger than the reactivation threshold (activation "aborted", usually due to moving away the cursor
Yuri Chornoivan's avatar
Yuri Chornoivan committed
326
    // from the corner after successful activation)
327
328
    // either condition means that "this is the first event in a new attempt"
    if (!m_lastReset.isValid() || m_lastReset.msecsTo(triggerTime) > edges()->reActivationThreshold()) {
329
330
331
        m_lastReset = triggerTime;
        return false;
    }
332
    if (m_lastTrigger.isValid() && m_lastTrigger.msecsTo(triggerTime) < edges()->reActivationThreshold() - edges()->timeThreshold()) {
333
334
335
336
337
338
339
340
341
342
343
344
345
346
        return false;
    }
    if (m_lastReset.msecsTo(triggerTime) < edges()->timeThreshold()) {
        return false;
    }
    // does the check on position make any sense at all?
    if ((cursorPos - m_triggeredPoint).manhattanLength() > DISTANCE_RESET) {
        return false;
    }
    return true;
}

void Edge::handle(const QPoint &cursorPos)
{
347
    Window *movingClient = Workspace::self()->moveResizeWindow();
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
348
    if ((edges()->isDesktopSwitchingMovingClients() && movingClient && !movingClient->isInteractiveResize()) || (edges()->isDesktopSwitching() && isScreenEdge())) {
349
350
351
352
        // always switch desktops in case:
        // moving a Client and option for switch on client move is enabled
        // or switch on screen edge is enabled
        switchDesktop(cursorPos);
353
354
        return;
    }
355
    if (movingClient) {
356
357
358
359
360
        // if we are moving a window we don't want to trigger the actions. This just results in
        // problems, e.g. Desktop Grid activated or screen locker activated which just cannot
        // work as we hold a grab.
        return;
    }
361
362
363
364
365
366
367
368

    if (m_client) {
        pushCursorBack(cursorPos);
        m_client->showOnScreenEdge();
        unreserve();
        return;
    }

369
    if (handlePointerAction() || handleByCallback()) {
370
371
        pushCursorBack(cursorPos);
        return;
372
    }
373
374
375
    if (edges()->isDesktopSwitching() && isCorner()) {
        // try again desktop switching for the corner
        switchDesktop(cursorPos);
376
377
378
    }
}

379
bool Edge::handleAction(ElectricBorderAction action)
380
{
381
    switch (action) {
382
383
384
385
386
    case ElectricActionShowDesktop: {
        Workspace::self()->setShowingDesktop(!Workspace::self()->showingDesktop());
        return true;
    }
    case ElectricActionLockScreen: { // Lock the screen
387
#if KWIN_BUILD_SCREENLOCKER
388
389
390
391
392
393
        OrgFreedesktopScreenSaverInterface interface(QStringLiteral("org.freedesktop.ScreenSaver"),
                                                     QStringLiteral("/ScreenSaver"),
                                                     QDBusConnection::sessionBus());
        if (interface.isValid()) {
            interface.Lock();
        }
394
        return true;
395
396
397
#else
        return false;
#endif
398
    }
399
400
401
402
403
    case ElectricActionKRunner: { // open krunner
        QDBusConnection::sessionBus().asyncCall(
            QDBusMessage::createMethodCall(QStringLiteral("org.kde.krunner"),
                                           QStringLiteral("/App"),
                                           QStringLiteral("org.kde.krunner.App"),
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
404
                                           QStringLiteral("display")));
405
406
407
408
409
410
411
        return true;
    }
    case ElectricActionActivityManager: { // open activity manager
        QDBusConnection::sessionBus().asyncCall(
            QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"),
                                           QStringLiteral("/PlasmaShell"),
                                           QStringLiteral("org.kde.PlasmaShell"),
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
412
                                           QStringLiteral("toggleActivityManager")));
413
414
        return true;
    }
415
416
417
418
419
    case ElectricActionApplicationLauncher: {
        QDBusConnection::sessionBus().asyncCall(
            QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"),
                                           QStringLiteral("/PlasmaShell"),
                                           QStringLiteral("org.kde.PlasmaShell"),
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
420
                                           QStringLiteral("activateLauncherMenu")));
421
422
        return true;
    }
423
424
425
426
427
    default:
        return false;
    }
}

428
bool Edge::handleByCallback()
429
{
430
    if (m_callBacks.isEmpty()) {
431
432
        return false;
    }
433
    for (QHash<QObject *, QByteArray>::iterator it = m_callBacks.begin();
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
434
435
         it != m_callBacks.end();
         ++it) {
436
437
438
439
440
441
442
        bool retVal = false;
        QMetaObject::invokeMethod(it.key(), it.value().constData(), Q_RETURN_ARG(bool, retVal), Q_ARG(ElectricBorder, m_border));
        if (retVal) {
            return true;
        }
    }
    return false;
443
444
}

445
446
void Edge::handleTouchCallback()
{
447
448
    if (!m_touchCallbacks.isEmpty()) {
        m_touchCallbacks.constFirst().touchUpAction()->trigger();
449
450
451
    }
}

452
453
454
455
void Edge::switchDesktop(const QPoint &cursorPos)
{
    QPoint pos(cursorPos);
    VirtualDesktopManager *vds = VirtualDesktopManager::self();
456
457
    VirtualDesktop *oldDesktop = vds->currentDesktop();
    VirtualDesktop *desktop = oldDesktop;
458
459
    const int OFFSET = 2;
    if (isLeft()) {
460
        const VirtualDesktop *interimDesktop = desktop;
461
        desktop = vds->toLeft(desktop, vds->isNavigationWrappingAround());
462
        if (desktop != interimDesktop) {
463
            pos.setX(workspace()->geometry().width() - 1 - OFFSET);
464
        }
465
    } else if (isRight()) {
466
        const VirtualDesktop *interimDesktop = desktop;
467
        desktop = vds->toRight(desktop, vds->isNavigationWrappingAround());
468
        if (desktop != interimDesktop) {
469
            pos.setX(OFFSET);
470
        }
471
472
    }
    if (isTop()) {
473
        const VirtualDesktop *interimDesktop = desktop;
474
        desktop = vds->above(desktop, vds->isNavigationWrappingAround());
475
        if (desktop != interimDesktop) {
476
            pos.setY(workspace()->geometry().height() - 1 - OFFSET);
477
        }
478
    } else if (isBottom()) {
479
        const VirtualDesktop *interimDesktop = desktop;
480
        desktop = vds->below(desktop, vds->isNavigationWrappingAround());
481
        if (desktop != interimDesktop) {
482
            pos.setY(OFFSET);
483
        }
484
    }
485
    if (Window *c = Workspace::self()->moveResizeWindow()) {
486
487
        const QVector<VirtualDesktop *> desktops{desktop};
        if (c->rules()->checkDesktops(desktops) != desktops) {
488
489
490
491
492
            // user attempts to move a client to another desktop where it is ruleforced to not be
            return;
        }
    }
    vds->setCurrent(desktop);
493
    if (vds->currentDesktop() != oldDesktop) {
494
        m_pushBackBlocked = true;
495
        Cursors::self()->mouse()->setPos(pos);
496
        QMetaObject::Connection *me = new QMetaObject::Connection();
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
497
        *me = QObject::connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, [this, me]() {
498
            QObject::disconnect(*me);
499
            delete me;
500
            m_pushBackBlocked = false;
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
501
        });
502
503
504
505
506
    }
}

void Edge::pushCursorBack(const QPoint &cursorPos)
{
507
    if (m_pushBackBlocked) {
508
        return;
509
    }
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
    int x = cursorPos.x();
    int y = cursorPos.y();
    const QSize &distance = edges()->cursorPushBackDistance();
    if (isLeft()) {
        x += distance.width();
    }
    if (isRight()) {
        x -= distance.width();
    }
    if (isTop()) {
        y += distance.height();
    }
    if (isBottom()) {
        y -= distance.height();
    }
525
    Cursors::self()->mouse()->setPos(x, y);
526
527
}

528
529
530
531
532
533
534
535
536
537
void Edge::setGeometry(const QRect &geometry)
{
    if (m_geometry == geometry) {
        return;
    }
    m_geometry = geometry;
    int x = m_geometry.x();
    int y = m_geometry.y();
    int width = m_geometry.width();
    int height = m_geometry.height();
538
    const int offset = m_edges->cornerOffset();
539
540
    if (isCorner()) {
        if (isRight()) {
541
            x = x + width - offset;
542
543
        }
        if (isBottom()) {
544
            y = y + height - offset;
545
        }
546
547
        width = offset;
        height = offset;
548
549
    } else {
        if (isLeft()) {
550
551
552
            y += offset;
            width = offset;
            height = height - offset * 2;
553
        } else if (isRight()) {
554
555
556
557
            x = x + width - offset;
            y += offset;
            width = offset;
            height = height - offset * 2;
558
        } else if (isTop()) {
559
560
561
            x += offset;
            width = width - offset * 2;
            height = offset;
562
        } else if (isBottom()) {
563
564
565
566
            x += offset;
            y = y + height - offset;
            width = width - offset * 2;
            height = offset;
567
568
569
570
        }
    }
    m_approachGeometry = QRect(x, y, width, height);
    doGeometryUpdate();
571
572

    if (isScreenEdge()) {
573
        const Output *output = workspace()->outputAt(m_geometry.center());
574
        m_gesture->setStartGeometry(m_geometry);
575
        m_gesture->setMinimumDelta(QSizeF(MINIMUM_DELTA, MINIMUM_DELTA) / output->scale());
576
    }
577
578
}

579
580
void Edge::checkBlocking()
{
581
    Window *client = Workspace::self()->activeWindow();
582
    const bool newValue = !m_edges->remainActiveOnFullscreen() && client && client->isFullScreen() && client->frameGeometry().contains(m_geometry.center()) && !(effects && effects->hasActiveFullScreenEffect());
583
584
585
    if (newValue == m_blocked) {
        return;
    }
586
    const bool wasTouch = activatesForTouchGesture();
587
    m_blocked = newValue;
588
589
590
    if (m_blocked && m_approaching) {
        stopApproaching();
    }
591
    if (wasTouch != activatesForTouchGesture()) {
592
        Q_EMIT activatesForTouchGestureChanged();
593
    }
594
595
596
597
598
599
600
    doUpdateBlocking();
}

void Edge::doUpdateBlocking()
{
}

601
602
603
604
605
void Edge::doGeometryUpdate()
{
}

void Edge::activate()
606
{
607
    if (activatesForTouchGesture()) {
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
608
        m_edges->gestureRecognizer()->registerSwipeGesture(m_gesture);
609
610
611
612
613
    }
    doActivate();
}

void Edge::doActivate()
614
615
616
617
{
}

void Edge::deactivate()
618
{
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
619
    m_edges->gestureRecognizer()->unregisterSwipeGesture(m_gesture);
620
621
622
623
    doDeactivate();
}

void Edge::doDeactivate()
624
625
626
{
}

627
628
629
630
631
632
633
void Edge::startApproaching()
{
    if (m_approaching) {
        return;
    }
    m_approaching = true;
    doStartApproaching();
634
    m_lastApproachingFactor = 0;
635
    Q_EMIT approaching(border(), 0.0, m_approachGeometry);
636
637
638
639
640
641
642
643
644
645
646
647
648
}

void Edge::doStartApproaching()
{
}

void Edge::stopApproaching()
{
    if (!m_approaching) {
        return;
    }
    m_approaching = false;
    doStopApproaching();
649
    m_lastApproachingFactor = 0;
650
    Q_EMIT approaching(border(), 0.0, m_approachGeometry);
651
652
653
654
655
656
657
658
659
}

void Edge::doStopApproaching()
{
}

void Edge::updateApproaching(const QPoint &point)
{
    if (approachGeometry().contains(point)) {
660
661
        int factor = 0;
        const int edgeDistance = m_edges->cornerOffset();
662
663
664
        auto cornerDistance = [=](const QPoint &corner) {
            return qMax(qAbs(corner.x() - point.x()), qAbs(corner.y() - point.y()));
        };
665
666
        switch (border()) {
        case ElectricTopLeft:
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
667
            factor = (cornerDistance(approachGeometry().topLeft()) << 8) / edgeDistance;
668
669
            break;
        case ElectricTopRight:
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
670
            factor = (cornerDistance(approachGeometry().topRight()) << 8) / edgeDistance;
671
672
            break;
        case ElectricBottomRight:
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
673
            factor = (cornerDistance(approachGeometry().bottomRight()) << 8) / edgeDistance;
674
675
            break;
        case ElectricBottomLeft:
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
676
            factor = (cornerDistance(approachGeometry().bottomLeft()) << 8) / edgeDistance;
677
678
            break;
        case ElectricTop:
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
679
            factor = (qAbs(point.y() - approachGeometry().y()) << 8) / edgeDistance;
680
681
            break;
        case ElectricRight:
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
682
            factor = (qAbs(point.x() - approachGeometry().right()) << 8) / edgeDistance;
683
684
            break;
        case ElectricBottom:
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
685
            factor = (qAbs(point.y() - approachGeometry().bottom()) << 8) / edgeDistance;
686
687
            break;
        case ElectricLeft:
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
688
            factor = (qAbs(point.x() - approachGeometry().x()) << 8) / edgeDistance;
689
690
691
692
            break;
        default:
            break;
        }
693
        factor = 256 - factor;
694
695
        if (m_lastApproachingFactor != factor) {
            m_lastApproachingFactor = factor;
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
696
            Q_EMIT approaching(border(), m_lastApproachingFactor / 256.0f, m_approachGeometry);
697
698
699
700
701
702
        }
    } else {
        stopApproaching();
    }
}

703
704
705
706
707
708
709
710
711
712
quint32 Edge::window() const
{
    return 0;
}

quint32 Edge::approachWindow() const
{
    return 0;
}

713
714
715
716
717
void Edge::setBorder(ElectricBorder border)
{
    m_border = border;
    switch (m_border) {
    case ElectricTop:
718
        m_gesture->setDirection(SwipeGesture::Direction::Down);
719
720
        break;
    case ElectricRight:
721
        m_gesture->setDirection(SwipeGesture::Direction::Left);
722
723
        break;
    case ElectricBottom:
724
        m_gesture->setDirection(SwipeGesture::Direction::Up);
725
726
        break;
    case ElectricLeft:
727
        m_gesture->setDirection(SwipeGesture::Direction::Right);
728
729
730
731
732
733
        break;
    default:
        break;
    }
}

Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
734
735
void Edge::setTouchAction(ElectricBorderAction action)
{
736
737
738
    const bool wasTouch = activatesForTouchGesture();
    m_touchAction = action;
    if (wasTouch != activatesForTouchGesture()) {
739
        Q_EMIT activatesForTouchGestureChanged();
740
741
742
    }
}

Nils Fenner's avatar
Nils Fenner committed
743
void Edge::setClient(Window *client)
744
745
746
747
{
    const bool wasTouch = activatesForTouchGesture();
    m_client = client;
    if (wasTouch != activatesForTouchGesture()) {
748
        Q_EMIT activatesForTouchGestureChanged();
749
750
751
    }
}

752
void Edge::setOutput(Output *output)
753
754
755
756
{
    m_output = output;
}

757
Output *Edge::output() const
758
759
760
761
{
    return m_output;
}

762
763
764
/**********************************************************
 * ScreenEdges
 *********************************************************/
765

766
767
ScreenEdges::ScreenEdges()
    : m_desktopSwitching(false)
768
769
770
    , m_desktopSwitchingMovingClients(false)
    , m_timeThreshold(0)
    , m_reactivateThreshold(0)
771
    , m_virtualDesktopLayout({})
772
773
774
775
776
777
778
779
    , m_actionTopLeft(ElectricActionNone)
    , m_actionTop(ElectricActionNone)
    , m_actionTopRight(ElectricActionNone)
    , m_actionRight(ElectricActionNone)
    , m_actionBottomRight(ElectricActionNone)
    , m_actionBottom(ElectricActionNone)
    , m_actionBottomLeft(ElectricActionNone)
    , m_actionLeft(ElectricActionNone)
780
    , m_gestureRecognizer(new GestureRecognizer(this))
781
{
782
783
    const int gridUnit = QFontMetrics(QFontDatabase::systemFont(QFontDatabase::GeneralFont)).boundingRect(QLatin1Char('M')).height();
    m_cornerOffset = 4 * gridUnit;
784

785
    connect(workspace(), &Workspace::windowRemoved, this, &ScreenEdges::deleteEdgeForClient);
786
787
788
789
790
}

void ScreenEdges::init()
{
    reconfigure();
791
    updateLayout();
792
793
    recreateEdges();
}
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
794
static ElectricBorderAction electricBorderAction(const QString &name)
795
796
{
    QString lowerName = name.toLower();
797
    if (lowerName == QStringLiteral("showdesktop")) {
798
        return ElectricActionShowDesktop;
799
    } else if (lowerName == QStringLiteral("lockscreen")) {
800
        return ElectricActionLockScreen;
801
802
803
804
    } else if (lowerName == QLatin1String("krunner")) {
        return ElectricActionKRunner;
    } else if (lowerName == QLatin1String("activitymanager")) {
        return ElectricActionActivityManager;
805
806
    } else if (lowerName == QLatin1String("applicationlauncher")) {
        return ElectricActionApplicationLauncher;
807
808
809
810
811
812
813
814
815
    }
    return ElectricActionNone;
}

void ScreenEdges::reconfigure()
{
    if (!m_config) {
        return;
    }
816
817
818
    KConfigGroup screenEdgesConfig = m_config->group("ScreenEdges");
    setRemainActiveOnFullscreen(screenEdgesConfig.readEntry("RemainActiveOnFullscreen", false));

819
820
821
    // TODO: migrate settings to a group ScreenEdges
    KConfigGroup windowsConfig = m_config->group("Windows");
    setTimeThreshold(windowsConfig.readEntry("ElectricBorderDelay", 150));
822
    setReActivationThreshold(qMax(timeThreshold() + 50, windowsConfig.readEntry("ElectricBorderCooldown", 350)));
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
    int desktopSwitching = windowsConfig.readEntry("ElectricBorders", static_cast<int>(ElectricDisabled));
    if (desktopSwitching == ElectricDisabled) {
        setDesktopSwitching(false);
        setDesktopSwitchingMovingClients(false);
    } else if (desktopSwitching == ElectricMoveOnly) {
        setDesktopSwitching(false);
        setDesktopSwitchingMovingClients(true);
    } else if (desktopSwitching == ElectricAlways) {
        setDesktopSwitching(true);
        setDesktopSwitchingMovingClients(true);
    }
    const int pushBack = windowsConfig.readEntry("ElectricBorderPushbackPixels", 1);
    m_cursorPushBackDistance = QSize(pushBack, pushBack);

    KConfigGroup borderConfig = m_config->group("ElectricBorders");
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
838
    setActionForBorder(ElectricTopLeft, &m_actionTopLeft,
839
                       electricBorderAction(borderConfig.readEntry("TopLeft", "None")));
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
840
    setActionForBorder(ElectricTop, &m_actionTop,
841
                       electricBorderAction(borderConfig.readEntry("Top", "None")));
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
842
    setActionForBorder(ElectricTopRight, &m_actionTopRight,
843
                       electricBorderAction(borderConfig.readEntry("TopRight", "None")));
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
844
    setActionForBorder(ElectricRight, &m_actionRight,
845
846
847
                       electricBorderAction(borderConfig.readEntry("Right", "None")));
    setActionForBorder(ElectricBottomRight, &m_actionBottomRight,
                       electricBorderAction(borderConfig.readEntry("BottomRight", "None")));
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
848
    setActionForBorder(ElectricBottom, &m_actionBottom,
849
                       electricBorderAction(borderConfig.readEntry("Bottom", "None")));
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
850
    setActionForBorder(ElectricBottomLeft, &m_actionBottomLeft,
851
                       electricBorderAction(borderConfig.readEntry("BottomLeft", "None")));
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
852
    setActionForBorder(ElectricLeft, &m_actionLeft,
853
                       electricBorderAction(borderConfig.readEntry("Left", "None")));
854
855
856
857
858
859

    borderConfig = m_config->group("TouchEdges");
    setActionForTouchBorder(ElectricTop, electricBorderAction(borderConfig.readEntry("Top", "None")));
    setActionForTouchBorder(ElectricRight, electricBorderAction(borderConfig.readEntry("Right", "None")));
    setActionForTouchBorder(ElectricBottom, electricBorderAction(borderConfig.readEntry("Bottom", "None")));
    setActionForTouchBorder(ElectricLeft, electricBorderAction(borderConfig.readEntry("Left", "None")));
860
861
862
863
864
865
866
867
868
}

void ScreenEdges::setActionForBorder(ElectricBorder border, ElectricBorderAction *oldValue, ElectricBorderAction newValue)
{
    if (*oldValue == newValue) {
        return;
    }
    if (*oldValue == ElectricActionNone) {
        // have to reserve
869
        for (auto it = m_edges.begin(); it != m_edges.end(); ++it) {
870
871
872
873
874
875
876
            if ((*it)->border() == border) {
                (*it)->reserve();
            }
        }
    }
    if (newValue == ElectricActionNone) {
        // have to unreserve
877
        for (auto it = m_edges.begin(); it != m_edges.end(); ++it) {
878
879
880
881
882
883
884
            if ((*it)->border() == border) {
                (*it)->unreserve();
            }
        }
    }
    *oldValue = newValue;
    // update action on all Edges for given border
885
    for (auto it = m_edges.begin(); it != m_edges.end(); ++it) {
886
887
888
889
        if ((*it)->border() == border) {
            (*it)->setAction(newValue);
        }
    }
890
891
}

892
893
void ScreenEdges::setActionForTouchBorder(ElectricBorder border, ElectricBorderAction newValue)
{
894
    auto it = m_touchCallbacks.find(border);
895
    ElectricBorderAction oldValue = ElectricActionNone;
896
    if (it != m_touchCallbacks.end()) {
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
        oldValue = it.value();
    }
    if (oldValue == newValue) {
        return;
    }
    if (oldValue == ElectricActionNone) {
        // have to reserve
        for (auto it = m_edges.begin(); it != m_edges.end(); ++it) {
            if ((*it)->border() == border) {
                (*it)->reserve();
            }
        }
    }
    if (newValue == ElectricActionNone) {
        // have to unreserve
        for (auto it = m_edges.begin(); it != m_edges.end(); ++it) {
            if ((*it)->border() == border) {
                (*it)->unreserve();
            }
        }

918
        m_touchCallbacks.erase(it);
919
    } else {
920
        m_touchCallbacks.insert(border, newValue);
921
922
923
924
925
926
927
928
929
    }
    // update action on all Edges for given border
    for (auto it = m_edges.begin(); it != m_edges.end(); ++it) {
        if ((*it)->border() == border) {
            (*it)->setTouchAction(newValue);
        }
    }
}

930
void ScreenEdges::updateLayout()
931
932
{
    const QSize desktopMatrix = VirtualDesktopManager::self()->grid().size();
Laurent Montel's avatar
Laurent Montel committed
933
    Qt::Orientations newLayout = {};
934
935
936
937
938
939
940
941
942
    if (desktopMatrix.width() > 1) {
        newLayout |= Qt::Horizontal;
    }
    if (desktopMatrix.height() > 1) {
        newLayout |= Qt::Vertical;
    }
    if (newLayout == m_virtualDesktopLayout) {
        return;
    }
943
    if (isDesktopSwitching()) {
944
945
946
        reserveDesktopSwitching(false, m_virtualDesktopLayout);
    }
    m_virtualDesktopLayout = newLayout;
947
    if (isDesktopSwitching()) {
948
949
950
951
        reserveDesktopSwitching(true, m_virtualDesktopLayout);
    }
}

952
static bool isLeftScreen(const QRect &screen, const QRect &fullArea)
Arthur Arlt's avatar
Arthur Arlt committed
953
{
954
    const auto outputs = workspace()->outputs();
955
    if (outputs.count() == 1) {
956
957
958
959
960
        return true;
    }
    if (screen.x() == fullArea.x()) {
        return true;
    }
Lewis Lakerink's avatar
Lewis Lakerink committed
961
    // If any other screen has a right edge against our left edge, then this screen is not a left screen
962
    for (const Output *output : outputs) {
963
        const QRect otherGeo = output->geometry();
964
965
966
        if (otherGeo == screen) {
            // that's our screen to test
            continue;
Arthur Arlt's avatar
Arthur Arlt committed
967
        }
Lewis Lakerink's avatar
Lewis Lakerink committed
968
969
970
971
        if (screen.x() == otherGeo.x() + otherGeo.width()
            && screen.y() < otherGeo.y() + otherGeo.height()
            && screen.y() + screen.height() > otherGeo.y()) {
            // There is a screen to the left
972
973
974
            return false;
        }
    }
Lewis Lakerink's avatar
Lewis Lakerink committed
975
    // No screen exists to the left, so this is a left screen
976
    return true;
Arthur Arlt's avatar
Arthur Arlt committed
977
978
}

979
static bool isRightScreen(const QRect &screen, const QRect &fullArea)
Arthur Arlt's avatar
Arthur Arlt committed
980
{
981
    const auto outputs = workspace()->outputs();
982
    if (outputs.count() == 1) {
983
984
985
986
987
        return true;
    }
    if (screen.x() + screen.width() == fullArea.x() + fullArea.width()) {
        return true;
    }
Lewis Lakerink's avatar
Lewis Lakerink committed
988
    // If any other screen has any left edge against any of our right edge, then this screen is not a right screen
989
    for (const Output *output : outputs) {
990
        const QRect otherGeo = output->geometry();
991
992
993
        if (otherGeo == screen) {
            // that's our screen to test
            continue;
994
        }
Lewis Lakerink's avatar
Lewis Lakerink committed
995
996
997
998
        if (screen.x() + screen.width() == otherGeo.x()
            && screen.y() < otherGeo.y() + otherGeo.height()
            && screen.y() + screen.height() > otherGeo.y()) {
            // There is a screen to the right
999
            return false;
1000
        }
For faster browsing, not all history is shown. View entire blame