Globals.qml 28.4 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
import QtQuick.Layouts 1.1
Konrad Materka's avatar
Konrad Materka committed
25
import QtQml 2.15
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
26
27
28
29

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

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

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

37
38
import ".."

39
40
41
// 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
42
QtObject {
43
    id: globals
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
44

45
46
    // 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
47

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

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
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
76
    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();
    }

77
78
79
    // 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]
80

81
82
83
84
85
86
87
    // 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();
88
89
90
        }
    }

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

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

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

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

140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
    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
157
                const right = Math.min(rect.right, visualParentWindow.x + visualParentWindow.width);
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
                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()

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

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

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

    onPopupLocationChanged: Qt.callLater(positionPopups)

194
195
    Component.onCompleted: checkInhibition()

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
196
    function adopt(plasmoid) {
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
232
        // 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
233
        var newPlasmoids = plasmoids;
234
235
236
237
238
239
240
241
242
243
244
245
        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;
            }
        });
246
247
248
249
250
251
252
        globals.plasmoids = newPlasmoids;
    }

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

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

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

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

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

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

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

        notificationSettings.save();
    }

283
284
285
286
287
288
289
    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
290
    function positionPopups() {
291
292
293
294
        if (!plasmoid) {
            return;
        }

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

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
333
        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) {
334
            y += screenRect.height - popupEdgeDistance;
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
335
336
337
338
339
        } else {
            y += popupEdgeDistance;
        }

        for (var i = 0; i < popupInstantiator.count; ++i) {
340
            let popup = popupInstantiator.objectAt(i);
341
342
343
344
            if (!popup) {
                continue;
            }

345
346
            // 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
347

348
349
350
351
            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
352
            if (screenRect.width < popupEffectiveWidth || effectivePopupLocation & Qt.AlignHCenter) {
353
354
355
356
357
                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
358
359
            }

360
            if (effectivePopupLocation & Qt.AlignTop) {
361
362
                // 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
363
                if (focusDialog && focusDialog.visible && !(focusDialog instanceof NotificationPopup)
364
365
366
                        && 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
367
                popup.y = y;
368
369
370
                // 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
371
            } else {
372
                y -= popup.height;
373
                if (focusDialog && focusDialog.visible && !(focusDialog instanceof NotificationPopup)
374
375
376
                        && 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
377
                popup.y = y;
378
379
380
                if (popup.height > 0) {
                    y -= popupSpacing;
                }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
381
382
            }

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

393
            popup.visible = visible;
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
394
395
396
        }
    }

397
    property QtObject popupNotificationsModel: NotificationManager.Notifications {
398
        limit: plasmoid ? (Math.ceil(globals.screenRect.height / (theme.mSize(theme.defaultFont).height * 4))) : 0
399
400
401
402
        showExpired: false
        showDismissed: false
        blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications
        blacklistedNotifyRcNames: notificationSettings.popupBlacklistedServices
403
404
        whitelistedDesktopEntries: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedApplications : []
        whitelistedNotifyRcNames: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedServices : []
405
406
        showJobs: notificationSettings.jobsInNotifications
        sortMode: NotificationManager.Notifications.SortByTypeAndUrgency
407
        sortOrder: Qt.AscendingOrder
408
409
        groupMode: NotificationManager.Notifications.GroupDisabled
        urgencies: {
410
411
412
413
414
415
416
417
418
419
420
421
            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
422
423
            // Low only when enabled in settings and not in do not disturb mode
            if (!globals.inhibited && notificationSettings.lowPriorityPopups) {
424
425
                urgencies |=NotificationManager.Notifications.LowUrgency;
            }
426

427
428
429
430
            return urgencies;
        }
    }

431
432
433
434
    property QtObject notificationSettings: NotificationManager.Settings {
        onNotificationsInhibitedUntilChanged: globals.checkInhibition()
    }

435
436
437
438
439
    property QtObject tasksModel: TaskManager.TasksModel {
        groupMode: TaskManager.TasksModel.GroupApplications
        groupInline: false
    }

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

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
452
453
454
    property Instantiator popupInstantiator: Instantiator {
        model: popupNotificationsModel
        delegate: NotificationPopup {
455
456
457
            // so Instantiator can access that after the model row is gone
            readonly property var notificationId: model.notificationId

458
            popupWidth: globals.popupWidth
459
460
            type: model.urgency === NotificationManager.Notifications.CriticalUrgency && notificationSettings.keepCriticalAlwaysOnTop
                  ? PlasmaCore.Dialog.CriticalNotification : PlasmaCore.Dialog.Notification
461

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
462
463
464
            notificationType: model.type

            applicationName: model.applicationName
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
465
            applicationIconSource: model.applicationIconName
466
            originName: model.originName || ""
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
467
468
469
470
471
472
473
474

            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
475
476
            // TODO would be nice to be able to "pin" jobs when they autohide
                && notificationSettings.permanentJobPopups
477
            closable: model.closable
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
478
479

            summary: model.summary
480
            body: model.body || ""
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
481
482
483
            icon: model.image || model.iconName
            hasDefaultAction: model.hasDefaultAction || false
            timeout: model.timeout
484
485
486
            // 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)
487
488
489
490
491
            // 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
492
493

            urls: model.urls || []
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
494
            urgency: model.urgency || NotificationManager.Notifications.NormalUrgency
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
495
496
497

            jobState: model.jobState || 0
            percentage: model.percentage || 0
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
498
            jobError: model.jobError || 0
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
499
500
501
502
503
504
505
506
            suspendable: !!model.suspendable
            killable: !!model.killable
            jobDetails: model.jobDetails || null

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

507
508
509
510
511
512
            hasReplyAction: model.hasReplyAction || false
            replyActionLabel: model.replyActionLabel || ""
            replyPlaceholderText: model.replyPlaceholderText || ""
            replySubmitButtonText: model.replySubmitButtonText || ""
            replySubmitButtonIconName: model.replySubmitButtonIconName || ""

513
            onExpired: popupNotificationsModel.expire(popupNotificationsModel.index(index, 0))
514
            onHoverEntered: model.read = true
515
            onCloseClicked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
516
            onDismissClicked: model.dismissed = true
517
            onConfigureClicked: popupNotificationsModel.configure(popupNotificationsModel.index(index, 0))
518
            onDefaultActionInvoked: {
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
549
550
551
552
553
                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;
                }

554
555
556
557
558
559
560
                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))
            }
561
562
563
564
            onReplied: {
                popupNotificationsModel.reply(popupNotificationsModel.index(index, 0), text);
                popupNotificationsModel.close(popupNotificationsModel.index(index, 0));
            }
565
566
567
568
            onOpenUrl: {
                Qt.openUrlExternally(url);
                popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
            }
569
            onFileActionInvoked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
570

571
572
573
            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
574

575
576
            // popup width is fixed
            onHeightChanged: positionPopups()
577
578

            Component.onCompleted: {
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
                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;
                            }
                        }
                    }
601
                }
602
603
604

                // Tell the model that we're handling the timeout now
                popupNotificationsModel.stopTimeout(popupNotificationsModel.index(index, 0));
605
            }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
606
607
        }
        onObjectAdded: {
608
            positionPopups();
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
609
            object.visible = true;
610
611
612
613
614
615
616
        }
        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);

617
            positionPopups();
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
618
619
        }
    }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
620
621
622
623
624

    // TODO use pulseaudio-qt for this once it becomes a framework
    property QtObject pulseAudio: Loader {
        source: "PulseAudio.qml"
    }
625
626
627
628
629
630

    // 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()
    }
631

632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
    // 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();
        }
    }

650
651
652
653
654
    // Keeps the Inhibited property on DBus in sync with our inhibition handling
    property Binding serverInhibitedBinding: Binding {
        target: NotificationManager.Server
        property: "inhibited"
        value: globals.inhibited
Konrad Materka's avatar
Konrad Materka committed
655
        restoreMode: Binding.RestoreBinding
656
    }
657

658
659
660
661
662
663
664
665
666
667
668
    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();
        }
669

670
        checkInhibition();
671

672
673
        if (globals.inhibited !== oldInhibited) {
            shortcuts.showDoNotDisturbOsd(globals.inhibited);
674
675
        }
    }
676
677
678
679

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