Globals.qml 28.2 KB
Newer Older
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
 * Copyright 2019 Kai Uwe Broulik <kde@privat.broulik.de>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License or (at your option) version 3 or any later version
 * accepted by the membership of KDE e.V. (or its successor approved
 * by the membership of KDE e.V.), which shall act as a proxy
 * defined in Section 14 of version 3 of the license.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */

pragma Singleton
import QtQuick 2.8
23
import QtQuick.Window 2.12
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
24
25
26
27
28
import QtQuick.Layouts 1.1

import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.kquickcontrolsaddons 2.0
29
import org.kde.kirigami 2.11 as Kirigami
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
30
31

import org.kde.notificationmanager 1.0 as NotificationManager
32
import org.kde.taskmanager 0.1 as TaskManager
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
33

34
35
import org.kde.plasma.private.notifications 2.0 as Notifications

36
37
import ".."

38
39
40
// This singleton object contains stuff shared between all notification plasmoids, namely:
// - Popup creation and placement
// - Do not disturb mode
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
41
QtObject {
42
    id: globals
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
43

44
45
    // Listened to by "ago" label in NotificationHeader to update all of them in unison
    signal timeChanged
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
46

47
    property bool inhibited: false
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
48

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
    onInhibitedChanged: {
        var pa = pulseAudio.item;
        if (!pa) {
            return;
        }

        var stream = pa.notificationStream;
        if (!stream) {
            return;
        }

        if (inhibited) {
            // Only remember that we muted if previously not muted.
            if (!stream.muted) {
                notificationSettings.notificationSoundsInhibited = true;
                stream.mute();
            }
        } else {
            // Only unmute if we previously muted it.
            if (notificationSettings.notificationSoundsInhibited) {
                stream.unmute();
            }
            notificationSettings.notificationSoundsInhibited = false;
        }
        notificationSettings.save();
    }

76
77
78
    // Some parts of the code rely on plasmoid.nativeInterface and since we're in a singleton here
    // this is named "plasmoid"
    property QtObject plasmoid: plasmoids[0]
79

80
81
82
83
84
85
86
    // HACK When a plasmoid is destroyed, QML sets its value to "null" in the Array
    // so we then remove it so we have a working "plasmoid" again
    onPlasmoidChanged: {
        if (!plasmoid) {
            // this doesn't emit a change, only in ratePlasmoids() it will detect the change
            plasmoids.splice(0, 1); // remove first
            ratePlasmoids();
87
88
89
        }
    }

90
91
92
    // all notification plasmoids
    property var plasmoids: []

93
    property int popupLocation: {
94
        // if we are on mobile, we can ignore the settings totally and just
95
        // align it to top center
96
97
98
        if (Kirigami.Settings.isMobile) {
            return Qt.AlignTop | Qt.AlignHCenter;
        }
99
100
        switch (notificationSettings.popupPosition) {
        // Auto-determine location based on plasmoid location
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
101
        case NotificationManager.Settings.CloseToWidget:
102
103
104
105
106
107
108
            if (!plasmoid) {
                return Qt.AlignBottom | Qt.AlignRight; // just in case
            }

            var alignment = 0;
            if (plasmoid.location === PlasmaCore.Types.LeftEdge) {
                alignment |= Qt.AlignLeft;
109
110
            } else if (plasmoid.location === PlasmaCore.Types.RightEdge) {
                alignment |= Qt.AlignRight;
111
112
            // No horizontal alignment flag has it place it left or right depending on
            // which half of the *panel* the notification plasmoid is in
113
            }
114

115
116
            if (plasmoid.location === PlasmaCore.Types.TopEdge) {
                alignment |= Qt.AlignTop;
117
            } else if (plasmoid.location === PlasmaCore.Types.BottomEdge) {
118
                alignment |= Qt.AlignBottom;
119
120
            // No vertical alignment flag has it place it top or bottom edge depending on
            // which half of the *screen* the notification plasmoid is in
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
            }
            return alignment;

        case NotificationManager.Settings.TopLeft:
            return Qt.AlignTop | Qt.AlignLeft;
        case NotificationManager.Settings.TopCenter:
            return Qt.AlignTop | Qt.AlignHCenter;
        case NotificationManager.Settings.TopRight:
            return Qt.AlignTop | Qt.AlignRight;
        case NotificationManager.Settings.BottomLeft:
            return Qt.AlignBottom | Qt.AlignLeft;
        case NotificationManager.Settings.BottomCenter:
            return Qt.AlignBottom | Qt.AlignHCenter;
        case NotificationManager.Settings.BottomRight:
            return Qt.AlignBottom | Qt.AlignRight;
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
136
137
138
        }
    }

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
    readonly property rect screenRect: {
        if (!plasmoid) {
            return Qt.rect(0, 0, -1, -1);
        }

        let rect = Qt.rect(plasmoid.screenGeometry.x + plasmoid.availableScreenRect.x,
                           plasmoid.screenGeometry.y + plasmoid.availableScreenRect.y,
                           plasmoid.availableScreenRect.width,
                           plasmoid.availableScreenRect.height);

        // When no explicit screen corner is configured,
        // restrict notification popup position by horizontal panel width
        if (notificationSettings.popupPosition === NotificationManager.Settings.CloseToWidget
            && plasmoid.formFactor === PlasmaCore.Types.Horizontal) {
            const visualParentWindow = visualParent.Window.window;
            if (visualParentWindow) {
                const left = Math.max(rect.left, visualParentWindow.x);
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
156
                const right = Math.min(rect.right, visualParentWindow.x + visualParentWindow.width);
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
                rect = Qt.rect(left, rect.y, right - left, rect.height);
            }
        }

        return rect;
    }
    onScreenRectChanged: repositionTimer.start()

    readonly property Item visualParent: {
        if (!plasmoid) {
            return null;
        }
        return plasmoid.nativeInterface.systemTrayRepresentation
            || plasmoid.compactRepresentationItem
            || plasmoid.fullRepresentationItem;
    }
    onVisualParentChanged: positionPopups()

175
176
177
    readonly property QtObject focusDialog: plasmoid.nativeInterface.focussedPlasmaDialog
    onFocusDialogChanged: positionPopups()

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
178
    // The raw width of the popup's content item, the Dialog itself adds some margins
179
180
    // Make it wider when on the top or the bottom center, since there's more horizontal
    // space available without looking weird
181
    // On mobile however we don't really want to have larger notifications
182
    property int popupWidth: (popupLocation & Qt.AlignHCenter) && !Kirigami.Settings.isMobile ? units.gridUnit * 22 : units.gridUnit * 18
183
    property int popupEdgeDistance: units.largeSpacing * 2
184
185
    // Reduce spacing between popups when centered so the stack doesn't intrude into the
    // view as much
186
    property int popupSpacing: (popupLocation & Qt.AlignHCenter) && !Kirigami.Settings.isMobile ? units.smallSpacing : units.largeSpacing
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
187
188

    // How much vertical screen real estate the notification popups may consume
189
    readonly property real popupMaximumScreenFill: 0.8
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
190
191
192

    onPopupLocationChanged: Qt.callLater(positionPopups)

193
194
    Component.onCompleted: checkInhibition()

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
195
    function adopt(plasmoid) {
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
        // this doesn't emit a change, only in ratePlasmoids() it will detect the change
        globals.plasmoids.push(plasmoid);
        ratePlasmoids();
    }

    // Sorts plasmoids based on a heuristic to find a suitable plasmoid to follow when placing popups
    function ratePlasmoids() {
        var plasmoidScore = function(plasmoid) {
            if (!plasmoid) {
                return 0;
            }

            var score = 0;

            // Prefer plasmoids in a panel, prefer horizontal panels over vertical ones
            if (plasmoid.location === PlasmaCore.Types.LeftEdge
                    || plasmoid.location === PlasmaCore.Types.RightEdge) {
                score += 1;
            } else if (plasmoid.location === PlasmaCore.Types.TopEdge
                       || plasmoid.location === PlasmaCore.Types.BottomEdge) {
                score += 2;
            }

            // Prefer iconified plasmoids
            if (!plasmoid.expanded) {
                ++score;
            }

            // Prefer plasmoids on primary screen
            if (plasmoid.nativeInterface && plasmoid.nativeInterface.isPrimaryScreen(plasmoid.screenGeometry)) {
                ++score;
            }

            return score;
        }

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
232
        var newPlasmoids = plasmoids;
233
234
235
236
237
238
239
240
241
242
243
244
        newPlasmoids.sort(function (a, b) {
            var scoreA = plasmoidScore(a);
            var scoreB = plasmoidScore(b);
            // Sort descending by score
            if (scoreA < scoreB) {
                return 1;
            } else if (scoreA > scoreB) {
                return -1;
            } else {
                return 0;
            }
        });
245
246
247
248
249
250
251
        globals.plasmoids = newPlasmoids;
    }

    function checkInhibition() {
        globals.inhibited = Qt.binding(function() {
            var inhibited = false;

252
253
254
255
            if (!NotificationManager.Server.valid) {
                return false;
            }

256
257
258
259
260
            var inhibitedUntil = notificationSettings.notificationsInhibitedUntil;
            if (!isNaN(inhibitedUntil.getTime())) {
                inhibited |= (new Date().getTime() < inhibitedUntil.getTime());
            }

261
262
263
            if (notificationSettings.notificationsInhibitedByApplication) {
                inhibited |= true;
            }
264

265
266
267
268
            if (notificationSettings.inhibitNotificationsWhenScreensMirrored) {
                inhibited |= notificationSettings.screensMirrored;
            }

269
270
            return inhibited;
        });
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
271
272
    }

273
274
275
276
277
278
279
280
281
    function revokeInhibitions() {
        notificationSettings.notificationsInhibitedUntil = undefined;
        notificationSettings.revokeApplicationInhibitions();
        // overrules current mirrored screen setup, updates again when screen configuration changes
        notificationSettings.screensMirrored = false;

        notificationSettings.save();
    }

282
283
284
285
286
287
288
    function rectIntersect(rect1 /*dialog*/, rect2 /*popup*/) {
        return rect1.x < rect2.x + rect2.width
                && rect2.x < rect1.x + rect1.width
                && rect1.y < rect2.y + rect2.height
                && rect2.y < rect1.y + rect1.height;
    }

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
289
    function positionPopups() {
290
291
292
293
        if (!plasmoid) {
            return;
        }

294
        const screenRect = globals.screenRect;
295
        if (screenRect.width <= 0 || screenRect.height <= 0) {
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
296
297
298
            return;
        }

299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
        let effectivePopupLocation = popupLocation;

        const visualParent = globals.visualParent;
        const visualParentWindow = visualParent.Window.window;

        // When no horizontal alignment is specified, place it depending on which half of the *panel*
        // the notification plasmoid is in
        if (visualParentWindow) {
            if (!(effectivePopupLocation & (Qt.AlignLeft | Qt.AlignHCenter | Qt.AlignRight))) {
                const iconHCenter = visualParent.mapToItem(null /*mapToScene*/, 0, 0).x + visualParent.width / 2;

                if (iconHCenter < visualParentWindow.width / 2) {
                    effectivePopupLocation |= Qt.AlignLeft;
                } else {
                    effectivePopupLocation |= Qt.AlignRight;
                }
            }
        }

        // When no vertical alignment is specified, place it depending on which half of the *screen*
        // the notification plasmoid is in
        if (!(effectivePopupLocation & (Qt.AlignTop | Qt.AlignBottom))) {
            const screenVCenter = screenRect.y + screenRect.height / 2;
            const iconVCenter = visualParent.mapToGlobal(0, visualParent.height / 2).y;

            if (iconVCenter < screenVCenter) {
                effectivePopupLocation |= Qt.AlignTop;
            } else {
                effectivePopupLocation |= Qt.AlignBottom;
            }
        }

        let y = screenRect.y;
        if (effectivePopupLocation & Qt.AlignBottom) {
333
            y += screenRect.height - popupEdgeDistance;
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
334
335
336
337
338
        } else {
            y += popupEdgeDistance;
        }

        for (var i = 0; i < popupInstantiator.count; ++i) {
339
            let popup = popupInstantiator.objectAt(i);
340
341
            // Popup width is fixed, so don't rely on the actual window size
            var popupEffectiveWidth = popupWidth + popup.margins.left + popup.margins.right;
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
342

343
344
345
346
            const leftMostX = screenRect.x + popupEdgeDistance;
            const rightMostX = screenRect.x + screenRect.width - popupEdgeDistance - popupEffectiveWidth;

            // If available screen rect is narrower than the popup, center it in the available rect
347
            if (screenRect.width < popupEffectiveWidth || effectivePopupLocation & Qt.AlignHCenter) {
348
349
350
351
352
                popup.x = screenRect.x + (screenRect.width - popupEffectiveWidth) / 2
            } else if (effectivePopupLocation & Qt.AlignLeft) {
                popup.x = leftMostX;
            } else if (effectivePopupLocation & Qt.AlignRight) {
                popup.x = rightMostX;
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
353
354
            }

355
            if (effectivePopupLocation & Qt.AlignTop) {
356
357
                // We want to calculate the new position based on its original target position to avoid positioning it and then
                // positioning it again, hence the temporary Qt.rect with explicit "y" and not just the popup as a whole
358
                if (focusDialog && focusDialog.visible && !(focusDialog instanceof NotificationPopup)
359
360
361
                        && rectIntersect(focusDialog, Qt.rect(popup.x, y, popup.width, popup.height))) {
                    y = focusDialog.y + focusDialog.height + popupEdgeDistance;
                }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
362
                popup.y = y;
363
364
365
                // If the popup isn't ready yet, ignore its occupied space for now.
                // We'll reposition everything in onHeightChanged eventually.
                y += popup.height + (popup.height > 0 ? popupSpacing : 0);
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
366
            } else {
367
                y -= popup.height;
368
                if (focusDialog && focusDialog.visible && !(focusDialog instanceof NotificationPopup)
369
370
371
                        && rectIntersect(focusDialog, Qt.rect(popup.x, y, popup.width, popup.height))) {
                    y = focusDialog.y - popup.height - popupEdgeDistance;
                }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
372
                popup.y = y;
373
374
375
                if (popup.height > 0) {
                    y -= popupSpacing;
                }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
376
377
            }

378
            // don't let notifications take more than popupMaximumScreenFill of the screen
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
379
380
            var visible = true;
            if (i > 0) { // however always show at least one popup
381
                if (effectivePopupLocation & Qt.AlignTop) {
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
382
383
384
385
386
387
                    visible = (popup.y + popup.height < screenRect.y + (screenRect.height * popupMaximumScreenFill));
                } else {
                    visible = (popup.y > screenRect.y + (screenRect.height * (1 - popupMaximumScreenFill)));
                }
            }

388
            popup.visible = visible;
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
389
390
391
        }
    }

392
    property QtObject popupNotificationsModel: NotificationManager.Notifications {
393
        limit: plasmoid ? (Math.ceil(globals.screenRect.height / (theme.mSize(theme.defaultFont).height * 4))) : 0
394
395
396
397
        showExpired: false
        showDismissed: false
        blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications
        blacklistedNotifyRcNames: notificationSettings.popupBlacklistedServices
398
399
        whitelistedDesktopEntries: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedApplications : []
        whitelistedNotifyRcNames: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedServices : []
400
401
        showJobs: notificationSettings.jobsInNotifications
        sortMode: NotificationManager.Notifications.SortByTypeAndUrgency
402
        sortOrder: Qt.AscendingOrder
403
404
        groupMode: NotificationManager.Notifications.GroupDisabled
        urgencies: {
405
406
407
408
409
410
411
412
413
414
415
416
            var urgencies = 0;

            // Critical always except in do not disturb mode when disabled in settings
            if (!globals.inhibited || notificationSettings.criticalPopupsInDoNotDisturbMode) {
                urgencies |= NotificationManager.Notifications.CriticalUrgency;
            }

            // Normal only when not in do not disturb mode
            if (!globals.inhibited) {
                urgencies |= NotificationManager.Notifications.NormalUrgency;
            }

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
417
418
            // Low only when enabled in settings and not in do not disturb mode
            if (!globals.inhibited && notificationSettings.lowPriorityPopups) {
419
420
                urgencies |=NotificationManager.Notifications.LowUrgency;
            }
421

422
423
424
425
            return urgencies;
        }
    }

426
427
428
429
    property QtObject notificationSettings: NotificationManager.Settings {
        onNotificationsInhibitedUntilChanged: globals.checkInhibition()
    }

430
431
432
433
434
    property QtObject tasksModel: TaskManager.TasksModel {
        groupMode: TaskManager.TasksModel.GroupApplications
        groupInline: false
    }

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
435
    // This periodically checks whether do not disturb mode timed out and updates the "minutes ago" labels
436
437
438
439
440
441
442
443
444
445
    property QtObject timeSource: PlasmaCore.DataSource {
        engine: "time"
        connectedSources: ["Local"]
        interval: 60000 // 1 min
        intervalAlignment: PlasmaCore.Types.AlignToMinute
        onDataChanged: {
            checkInhibition();
            globals.timeChanged();
        }
    }
446

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
447
448
449
    property Instantiator popupInstantiator: Instantiator {
        model: popupNotificationsModel
        delegate: NotificationPopup {
450
451
452
            // so Instantiator can access that after the model row is gone
            readonly property var notificationId: model.notificationId

453
            popupWidth: globals.popupWidth
454
455
            type: model.urgency === NotificationManager.Notifications.CriticalUrgency && notificationSettings.keepCriticalAlwaysOnTop
                  ? PlasmaCore.Dialog.CriticalNotification : PlasmaCore.Dialog.Notification
456

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
457
458
459
            notificationType: model.type

            applicationName: model.applicationName
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
460
            applicationIconSource: model.applicationIconName
461
            originName: model.originName || ""
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
462
463
464
465
466
467
468
469

            time: model.updated || model.created

            configurable: model.configurable
            // For running jobs instead of offering a "close" button that might lead the user to
            // think that will cancel the job, we offer a "dismiss" button that hides it in the history
            dismissable: model.type === NotificationManager.Notifications.JobType
                && model.jobState !== NotificationManager.Notifications.JobStateStopped
470
471
            // TODO would be nice to be able to "pin" jobs when they autohide
                && notificationSettings.permanentJobPopups
472
            closable: model.closable
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
473
474

            summary: model.summary
475
            body: model.body || ""
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
476
477
478
            icon: model.image || model.iconName
            hasDefaultAction: model.hasDefaultAction || false
            timeout: model.timeout
479
480
481
            // Increase default timeout for notifications with a URL so you have enough time
            // to interact with the thumbnail or bring the window to the front where you want to drag it into
            defaultTimeout: notificationSettings.popupTimeout + (model.urls && model.urls.length > 0 ? 5000 : 0)
482
483
484
485
486
            // When configured to not keep jobs open permanently, we autodismiss them after the standard timeout
            dismissTimeout: !notificationSettings.permanentJobPopups
                            && model.type === NotificationManager.Notifications.JobType
                            && model.jobState !== NotificationManager.Notifications.JobStateStopped
                            ? defaultTimeout : 0
487
488

            urls: model.urls || []
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
489
            urgency: model.urgency || NotificationManager.Notifications.NormalUrgency
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
490
491
492

            jobState: model.jobState || 0
            percentage: model.percentage || 0
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
493
            jobError: model.jobError || 0
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
494
495
496
497
498
499
500
501
            suspendable: !!model.suspendable
            killable: !!model.killable
            jobDetails: model.jobDetails || null

            configureActionLabel: model.configureActionLabel || ""
            actionNames: model.actionNames
            actionLabels: model.actionLabels

502
503
504
505
506
507
            hasReplyAction: model.hasReplyAction || false
            replyActionLabel: model.replyActionLabel || ""
            replyPlaceholderText: model.replyPlaceholderText || ""
            replySubmitButtonText: model.replySubmitButtonText || ""
            replySubmitButtonIconName: model.replySubmitButtonIconName || ""

508
            onExpired: popupNotificationsModel.expire(popupNotificationsModel.index(index, 0))
509
            onHoverEntered: model.read = true
510
            onCloseClicked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
511
            onDismissClicked: model.dismissed = true
512
            onConfigureClicked: popupNotificationsModel.configure(popupNotificationsModel.index(index, 0))
513
            onDefaultActionInvoked: {
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
                if (defaultActionFallbackWindowIdx) {
                    if (!defaultActionFallbackWindowIdx.valid) {
                        console.warn("Failed fallback notification activation as window no longer exists");
                        return;
                    }

                    // When it's a group, activate the window highest in stacking order (presumably last used)
                    if (tasksModel.data(defaultActionFallbackWindowIdx, TaskManager.AbstractTasksModel.IsGroupParent)) {
                        let highestStacking = -1;
                        let highestIdx = undefined;

                        for (let i = 0; i < tasksModel.rowCount(defaultActionFallbackWindowIdx); ++i) {
                            const idx = tasksModel.index(i, 0, defaultActionFallbackWindowIdx);

                            const stacking = tasksModel.data(idx, TaskManager.AbstractTasksModel.StackingOrder);

                            if (stacking > highestStacking) {
                                highestStacking = stacking;
                                highestIdx = tasksModel.makePersistentModelIndex(defaultActionFallbackWindowIdx.row, i);
                            }
                        }

                        if (highestIdx && highestIdx.valid) {
                            tasksModel.requestActivate(highestIdx);
                            popupNotificationsModel.close(popupNotificationsModel.index(index, 0));

                        }
                        return;
                    }

                    tasksModel.requestActivate(defaultActionFallbackWindowIdx);
                    popupNotificationsModel.close(popupNotificationsModel.index(index, 0));
                    return;
                }

549
550
551
552
553
554
555
                popupNotificationsModel.invokeDefaultAction(popupNotificationsModel.index(index, 0))
                popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
            }
            onActionInvoked: {
                popupNotificationsModel.invokeAction(popupNotificationsModel.index(index, 0), actionName)
                popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
            }
556
557
558
559
            onReplied: {
                popupNotificationsModel.reply(popupNotificationsModel.index(index, 0), text);
                popupNotificationsModel.close(popupNotificationsModel.index(index, 0));
            }
560
561
562
563
            onOpenUrl: {
                Qt.openUrlExternally(url);
                popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
            }
564
            onFileActionInvoked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
565

566
567
568
            onSuspendJobClicked: popupNotificationsModel.suspendJob(popupNotificationsModel.index(index, 0))
            onResumeJobClicked: popupNotificationsModel.resumeJob(popupNotificationsModel.index(index, 0))
            onKillJobClicked: popupNotificationsModel.killJob(popupNotificationsModel.index(index, 0))
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
569

570
571
            // popup width is fixed
            onHeightChanged: positionPopups()
572
573

            Component.onCompleted: {
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
                if (model.type === NotificationManager.Notifications.NotificationType && model.desktopEntry) {
                    // Register apps that were seen spawning a popup so they can be configured later
                    // Apps with notifyrc can already be configured anyway
                    if (!model.notifyRcName) {
                        notificationSettings.registerKnownApplication(model.desktopEntry);
                        notificationSettings.save();
                    }

                    // If there is no default action, check if there is a window we could activate instead
                    if (!model.hasDefaultAction) {
                        for (let i = 0; i < tasksModel.rowCount(); ++i) {
                            const idx = tasksModel.index(i, 0);

                            const appId = tasksModel.data(idx, TaskManager.AbstractTasksModel.AppId);
                            if (appId === model.desktopEntry + ".desktop") {
                                // Takes a row number, not a QModelIndex
                                defaultActionFallbackWindowIdx = tasksModel.makePersistentModelIndex(i);
                                hasDefaultAction = true;
                                break;
                            }
                        }
                    }
596
                }
597
598
599

                // Tell the model that we're handling the timeout now
                popupNotificationsModel.stopTimeout(popupNotificationsModel.index(index, 0));
600
            }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
601
602
        }
        onObjectAdded: {
603
            positionPopups();
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
604
            object.visible = true;
605
606
607
608
609
610
611
        }
        onObjectRemoved: {
            var notificationId = object.notificationId
            // Popup might have been destroyed because of a filter change, tell the model to do the timeout work for us again
            // cannot use QModelIndex here as the model row is already gone
            popupNotificationsModel.startTimeout(notificationId);

612
            positionPopups();
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
613
614
        }
    }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
615
616
617
618
619

    // TODO use pulseaudio-qt for this once it becomes a framework
    property QtObject pulseAudio: Loader {
        source: "PulseAudio.qml"
    }
620
621
622
623
624
625

    // Normally popups are repositioned through Qt.callLater but in case of e.g. screen geometry changes we want to compress that
    property Timer repositionTimer: Timer {
        interval: 250
        onTriggered: positionPopups()
    }
626

627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
    // Tracks the visual parent's window since mapToItem cannot signal
    // so that when user resizes panel we reposition the popups live
    property Connections visualParentWindowConnections: Connections {
        target: visualParent ? visualParent.Window.window : null
        function onXChanged() {
            repositionTimer.start();
        }
        function onYChanged() {
            repositionTimer.start();
        }
        function onWidthChanged() {
            repositionTimer.start();
        }
        function onHeightChanged() {
            repositionTimer.start();
        }
    }

645
646
647
648
649
650
    // Keeps the Inhibited property on DBus in sync with our inhibition handling
    property Binding serverInhibitedBinding: Binding {
        target: NotificationManager.Server
        property: "inhibited"
        value: globals.inhibited
    }
651

652
653
654
655
656
657
658
659
660
661
662
    function toggleDoNotDisturbMode() {
        var oldInhibited = globals.inhibited;
        if (oldInhibited) {
            globals.revokeInhibitions();
        } else {
            // Effectively "in a year" is "until turned off"
            var d = new Date();
            d.setFullYear(d.getFullYear() + 1);
            notificationSettings.notificationsInhibitedUntil = d;
            notificationSettings.save();
        }
663

664
        checkInhibition();
665

666
667
        if (globals.inhibited !== oldInhibited) {
            shortcuts.showDoNotDisturbOsd(globals.inhibited);
668
669
        }
    }
670
671
672
673

    property Notifications.GlobalShortcuts shortcuts: Notifications.GlobalShortcuts {
        onToggleDoNotDisturbTriggered: globals.toggleDoNotDisturbMode()
    }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
674
}