Globals.qml 15.6 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
23
24
25
26
27
28
29
30
31
/*
 * 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
import QtQuick.Layouts 1.1

import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as Components
import org.kde.kquickcontrolsaddons 2.0

import org.kde.notificationmanager 1.0 as NotificationManager

32
33
import ".."

34
35
36
// 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
37
QtObject {
38
    id: globals
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
39

40
41
    // 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
42

43
    property bool inhibited: false
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
44

45
46
    // Reset the expire limiter so we don't get a flood of non-expired notifications
    onInhibitedChanged: {
47

48
    }
49

50
51
52
    // 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]
53

54
55
56
57
58
59
60
    // 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();
61
62
63
        }
    }

64
65
66
    // all notification plasmoids
    property var plasmoids: []

67
68
69
70
71
72
73
74
75
76
77
    property int popupLocation: {
        switch (notificationSettings.popupPosition) {
        // Auto-determine location based on plasmoid location
        case NotificationManager.Settings.NearWidget:
            if (!plasmoid) {
                return Qt.AlignBottom | Qt.AlignRight; // just in case
            }

            var alignment = 0;
            if (plasmoid.location === PlasmaCore.Types.LeftEdge) {
                alignment |= Qt.AlignLeft;
78
79
            } else if (plasmoid.location === PlasmaCore.Types.RightEdge) {
                alignment |= Qt.AlignRight;
80
81
82
            } else {
                // would be nice to do plasmoid.compactRepresentationItem.mapToItem(null) and then
                // position the popups depending on the relative position within the panel
83
                alignment |= Qt.application.layoutDirection === Qt.RightToLeft ? Qt.AlignLeft : Qt.AlignRight;
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
            }
            if (plasmoid.location === PlasmaCore.Types.TopEdge) {
                alignment |= Qt.AlignTop;
            } else {
                alignment |= Qt.AlignBottom;
            }
            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
104
105
106
107
108
109
110
111
112
        }
    }

    // The raw width of the popup's content item, the Dialog itself adds some margins
    property int popupWidth: units.gridUnit * 18
    property int popupEdgeDistance: units.largeSpacing
    property int popupSpacing: units.largeSpacing

    // How much vertical screen real estate the notification popups may consume
113
    readonly property real popupMaximumScreenFill: 0.75
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
114
115
116
117
118
119

    property var screenRect: plasmoid.availableScreenRect

    onPopupLocationChanged: Qt.callLater(positionPopups)
    onScreenRectChanged: Qt.callLater(positionPopups)

120
121
    Component.onCompleted: checkInhibition()

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
122
    function adopt(plasmoid) {
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
        // 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
159
        var newPlasmoids = plasmoids;
160
161
162
163
164
165
166
167
168
169
170
171
        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;
            }
        });
172
173
174
175
176
177
178
179
180
181
182
183
        globals.plasmoids = newPlasmoids;
    }

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

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

184
185
186
            if (notificationSettings.notificationsInhibitedByApplication) {
                inhibited |= true;
            }
187

188
189
            return inhibited;
        });
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
190
191
192
193
194
195
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
    }

    function positionPopups() {
        var rect = screenRect;
        if (!rect || rect.width <= 0 || rect.height <= 0) {
            return;
        }

        var y = screenRect.y;
        if (popupLocation & Qt.AlignBottom) {
            y += screenRect.height;
        } else {
            y += popupEdgeDistance;
        }

        var x = screenRect.x;
        if (popupLocation & Qt.AlignLeft) {
            x += popupEdgeDistance;
        }

        for (var i = 0; i < popupInstantiator.count; ++i) {
            var popup = popupInstantiator.objectAt(i);

            if (popupLocation & Qt.AlignHCenter) {
                popup.x = x + (screenRect.width - popup.width) / 2;
            } else if (popupLocation & Qt.AlignRight) {
                popup.x = screenRect.width - popupEdgeDistance - popup.width;
            } else {
                popup.x = x;
            }

            var delta = popupSpacing + popup.height;

            if (popupLocation & Qt.AlignTop) {
                popup.y = y;
                y += delta;
            } else {
                y -= delta;
                popup.y = y;
            }

231
            // don't let notifications take more than popupMaximumScreenFill of the screen
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
232
233
234
235
236
237
238
239
240
            var visible = true;
            if (i > 0) { // however always show at least one popup
                if (popupLocation & Qt.AlignTop) {
                    visible = (popup.y + popup.height < screenRect.y + (screenRect.height * popupMaximumScreenFill));
                } else {
                    visible = (popup.y > screenRect.y + (screenRect.height * (1 - popupMaximumScreenFill)));
                }
            }

241
            // TODO would be nice to hide popups when systray or panel controller is open
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
242
243
244
245
            popup.visible = visible;
        }
    }

246
    property QtObject popupNotificationsModel: NotificationManager.Notifications {
247
        limit: Math.ceil(globals.screenRect.height / (theme.mSize(theme.defaultFont).height * 4))
248
249
250
251
        showExpired: false
        showDismissed: false
        blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications
        blacklistedNotifyRcNames: notificationSettings.popupBlacklistedServices
252
253
        whitelistedDesktopEntries: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedApplications : []
        whitelistedNotifyRcNames: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedServices : []
254
255
256
257
        showJobs: notificationSettings.jobsInNotifications
        sortMode: NotificationManager.Notifications.SortByTypeAndUrgency
        groupMode: NotificationManager.Notifications.GroupDisabled
        urgencies: {
258
259
260
261
262
263
264
265
266
267
268
269
270
            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;
            }

            // Low only when enabled in settings
271
272
273
            if (notificationSettings.lowPriorityPopups) {
                urgencies |=NotificationManager.Notifications.LowUrgency;
            }
274

275
276
277
278
            return urgencies;
        }
    }

279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
    property QtObject notificationSettings: NotificationManager.Settings {
        onNotificationsInhibitedUntilChanged: globals.checkInhibition()
    }

    // This periodically checks whether do not disturb mode timed out
    property QtObject timeSource: PlasmaCore.DataSource {
        engine: "time"
        connectedSources: ["Local"]
        interval: 60000 // 1 min
        intervalAlignment: PlasmaCore.Types.AlignToMinute
        onDataChanged: {
            checkInhibition();
            globals.timeChanged();
        }
    }
294

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
295
296
297
    property Instantiator popupInstantiator: Instantiator {
        model: popupNotificationsModel
        delegate: NotificationPopup {
298
299
300
            // so Instantiator can access that after the model row is gone
            readonly property var notificationId: model.notificationId

301
302
            popupWidth: globals.popupWidth

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
303
304
305
306
            notificationType: model.type

            applicationName: model.applicationName
            applicatonIconSource: model.applicationIconName
307
            deviceName: model.deviceName || ""
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
308
309
310
311
312
313
314
315

            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
316
317
            // TODO would be nice to be able to "pin" jobs when they autohide
                && notificationSettings.permanentJobPopups
318
            closable: model.closable
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
319
320

            summary: model.summary
321
            body: model.body || ""
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
322
323
324
            icon: model.image || model.iconName
            hasDefaultAction: model.hasDefaultAction || false
            timeout: model.timeout
325
            defaultTimeout: notificationSettings.popupTimeout
326
327
328
329
330
            // 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
331
332

            urls: model.urls || []
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
333
334
335
336
337
338
339
340
341
342
343
344
345
346
            urgency: model.urgency

            jobState: model.jobState || 0
            percentage: model.percentage || 0
            error: model.error || 0
            errorText: model.errorText || ""
            suspendable: !!model.suspendable
            killable: !!model.killable
            jobDetails: model.jobDetails || null

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

347
348
            onExpired: popupNotificationsModel.expire(popupNotificationsModel.index(index, 0))
            onCloseClicked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
349
            onDismissClicked: model.dismissed = true
350
            onConfigureClicked: popupNotificationsModel.configure(popupNotificationsModel.index(index, 0))
351
352
353
354
355
356
357
358
            onDefaultActionInvoked: {
                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))
            }
359
360
361
362
            onOpenUrl: {
                Qt.openUrlExternally(url);
                popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
            }
363
            onFileActionInvoked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0))
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
364

365
366
367
            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
368
369
370

            onHeightChanged: Qt.callLater(positionPopups)
            onWidthChanged: Qt.callLater(positionPopups)
371
372
373

            Component.onCompleted: {
                // Register apps that were seen spawning a popup so they can be configured later
374
375
                // Apps with notifyrc can already be configured anyway
                if (model.desktopEntry && !model.notifyRcName) {
376
                    notificationSettings.registerKnownApplication(model.desktopEntry);
377
                    notificationSettings.save();
378
                }
379
380
381

                // Tell the model that we're handling the timeout now
                popupNotificationsModel.stopTimeout(popupNotificationsModel.index(index, 0));
382
            }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
383
384
385
386
        }
        onObjectAdded: {
            // also needed for it to correctly layout its contents
            object.visible = true;
387
388
389
390
391
392
393
394
395
            Qt.callLater(positionPopups);
        }
        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);

            Qt.callLater(positionPopups);
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
396
397
398
        }
    }
}