Changeset View
Changeset View
Standalone View
Standalone View
applets/notifications/package/contents/ui/global/Globals.qml
- This file was added.
1 | /* | ||||
---|---|---|---|---|---|
2 | * Copyright 2019 Kai Uwe Broulik <kde@privat.broulik.de> | ||||
3 | * | ||||
4 | * This program is free software; you can redistribute it and/or | ||||
5 | * modify it under the terms of the GNU General Public License as | ||||
6 | * published by the Free Software Foundation; either version 2 of | ||||
7 | * the License or (at your option) version 3 or any later version | ||||
8 | * accepted by the membership of KDE e.V. (or its successor approved | ||||
9 | * by the membership of KDE e.V.), which shall act as a proxy | ||||
10 | * defined in Section 14 of version 3 of the license. | ||||
11 | * | ||||
12 | * This program is distributed in the hope that it will be useful, | ||||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
15 | * GNU General Public License for more details. | ||||
16 | * | ||||
17 | * You should have received a copy of the GNU General Public License | ||||
18 | * along with this program. If not, see <http://www.gnu.org/licenses/> | ||||
19 | */ | ||||
20 | | ||||
21 | pragma Singleton | ||||
22 | import QtQuick 2.8 | ||||
23 | import QtQuick.Layouts 1.1 | ||||
24 | | ||||
25 | import org.kde.plasma.plasmoid 2.0 | ||||
26 | import org.kde.plasma.core 2.0 as PlasmaCore | ||||
27 | import org.kde.plasma.components 2.0 as Components | ||||
28 | import org.kde.kquickcontrolsaddons 2.0 | ||||
29 | | ||||
30 | import org.kde.notificationmanager 1.0 as NotificationManager | ||||
31 | | ||||
32 | import ".." | ||||
33 | | ||||
34 | // This singleton object contains stuff shared between all notification plasmoids, namely: | ||||
35 | // - Popup creation and placement | ||||
36 | // - Do not disturb mode | ||||
37 | QtObject { | ||||
38 | id: globals | ||||
39 | | ||||
40 | // Listened to by "ago" label in NotificationHeader to update all of them in unison | ||||
41 | signal timeChanged | ||||
42 | | ||||
43 | property bool inhibited: false | ||||
44 | | ||||
45 | onInhibitedChanged: { | ||||
46 | var pa = pulseAudio.item; | ||||
47 | if (!pa) { | ||||
48 | return; | ||||
49 | } | ||||
50 | | ||||
51 | var stream = pa.notificationStream; | ||||
52 | if (!stream) { | ||||
53 | return; | ||||
54 | } | ||||
55 | | ||||
56 | if (inhibited) { | ||||
57 | // Only remember that we muted if previously not muted. | ||||
58 | if (!stream.muted) { | ||||
59 | notificationSettings.notificationSoundsInhibited = true; | ||||
60 | stream.mute(); | ||||
61 | } | ||||
62 | } else { | ||||
63 | // Only unmute if we previously muted it. | ||||
64 | if (notificationSettings.notificationSoundsInhibited) { | ||||
65 | stream.unmute(); | ||||
66 | } | ||||
67 | notificationSettings.notificationSoundsInhibited = false; | ||||
68 | } | ||||
69 | notificationSettings.save(); | ||||
70 | } | ||||
71 | | ||||
72 | // Some parts of the code rely on plasmoid.nativeInterface and since we're in a singleton here | ||||
73 | // this is named "plasmoid" | ||||
74 | property QtObject plasmoid: plasmoids[0] | ||||
75 | | ||||
76 | // HACK When a plasmoid is destroyed, QML sets its value to "null" in the Array | ||||
77 | // so we then remove it so we have a working "plasmoid" again | ||||
78 | onPlasmoidChanged: { | ||||
79 | if (!plasmoid) { | ||||
80 | // this doesn't emit a change, only in ratePlasmoids() it will detect the change | ||||
81 | plasmoids.splice(0, 1); // remove first | ||||
82 | ratePlasmoids(); | ||||
83 | } | ||||
84 | } | ||||
85 | | ||||
86 | // all notification plasmoids | ||||
87 | property var plasmoids: [] | ||||
88 | | ||||
89 | property int popupLocation: { | ||||
90 | switch (notificationSettings.popupPosition) { | ||||
91 | // Auto-determine location based on plasmoid location | ||||
92 | case NotificationManager.Settings.CloseToWidget: | ||||
93 | if (!plasmoid) { | ||||
94 | return Qt.AlignBottom | Qt.AlignRight; // just in case | ||||
95 | } | ||||
96 | | ||||
97 | var alignment = 0; | ||||
98 | if (plasmoid.location === PlasmaCore.Types.LeftEdge) { | ||||
99 | alignment |= Qt.AlignLeft; | ||||
100 | } else if (plasmoid.location === PlasmaCore.Types.RightEdge) { | ||||
101 | alignment |= Qt.AlignRight; | ||||
102 | } else { | ||||
103 | // would be nice to do plasmoid.compactRepresentationItem.mapToItem(null) and then | ||||
104 | // position the popups depending on the relative position within the panel | ||||
105 | alignment |= Qt.application.layoutDirection === Qt.RightToLeft ? Qt.AlignLeft : Qt.AlignRight; | ||||
106 | } | ||||
107 | if (plasmoid.location === PlasmaCore.Types.TopEdge) { | ||||
108 | alignment |= Qt.AlignTop; | ||||
109 | } else { | ||||
110 | alignment |= Qt.AlignBottom; | ||||
111 | } | ||||
112 | return alignment; | ||||
113 | | ||||
114 | case NotificationManager.Settings.TopLeft: | ||||
115 | return Qt.AlignTop | Qt.AlignLeft; | ||||
116 | case NotificationManager.Settings.TopCenter: | ||||
117 | return Qt.AlignTop | Qt.AlignHCenter; | ||||
118 | case NotificationManager.Settings.TopRight: | ||||
119 | return Qt.AlignTop | Qt.AlignRight; | ||||
120 | case NotificationManager.Settings.BottomLeft: | ||||
121 | return Qt.AlignBottom | Qt.AlignLeft; | ||||
122 | case NotificationManager.Settings.BottomCenter: | ||||
123 | return Qt.AlignBottom | Qt.AlignHCenter; | ||||
124 | case NotificationManager.Settings.BottomRight: | ||||
125 | return Qt.AlignBottom | Qt.AlignRight; | ||||
126 | } | ||||
127 | } | ||||
128 | | ||||
129 | // The raw width of the popup's content item, the Dialog itself adds some margins | ||||
130 | property int popupWidth: units.gridUnit * 18 | ||||
131 | property int popupEdgeDistance: units.largeSpacing | ||||
132 | property int popupSpacing: units.largeSpacing | ||||
133 | | ||||
134 | // How much vertical screen real estate the notification popups may consume | ||||
135 | readonly property real popupMaximumScreenFill: 0.75 | ||||
136 | | ||||
137 | property var screenRect: plasmoid ? plasmoid.availableScreenRect : undefined | ||||
138 | | ||||
139 | onPopupLocationChanged: Qt.callLater(positionPopups) | ||||
140 | onScreenRectChanged: Qt.callLater(positionPopups) | ||||
141 | | ||||
142 | Component.onCompleted: checkInhibition() | ||||
143 | | ||||
144 | function adopt(plasmoid) { | ||||
145 | // this doesn't emit a change, only in ratePlasmoids() it will detect the change | ||||
146 | globals.plasmoids.push(plasmoid); | ||||
147 | ratePlasmoids(); | ||||
148 | } | ||||
149 | | ||||
150 | // Sorts plasmoids based on a heuristic to find a suitable plasmoid to follow when placing popups | ||||
151 | function ratePlasmoids() { | ||||
152 | var plasmoidScore = function(plasmoid) { | ||||
153 | if (!plasmoid) { | ||||
154 | return 0; | ||||
155 | } | ||||
156 | | ||||
157 | var score = 0; | ||||
158 | | ||||
159 | // Prefer plasmoids in a panel, prefer horizontal panels over vertical ones | ||||
160 | if (plasmoid.location === PlasmaCore.Types.LeftEdge | ||||
161 | || plasmoid.location === PlasmaCore.Types.RightEdge) { | ||||
162 | score += 1; | ||||
163 | } else if (plasmoid.location === PlasmaCore.Types.TopEdge | ||||
164 | || plasmoid.location === PlasmaCore.Types.BottomEdge) { | ||||
165 | score += 2; | ||||
166 | } | ||||
167 | | ||||
168 | // Prefer iconified plasmoids | ||||
169 | if (!plasmoid.expanded) { | ||||
170 | ++score; | ||||
171 | } | ||||
172 | | ||||
173 | // Prefer plasmoids on primary screen | ||||
174 | if (plasmoid.nativeInterface && plasmoid.nativeInterface.isPrimaryScreen(plasmoid.screenGeometry)) { | ||||
175 | ++score; | ||||
176 | } | ||||
177 | | ||||
178 | return score; | ||||
179 | } | ||||
180 | | ||||
181 | var newPlasmoids = plasmoids; | ||||
182 | newPlasmoids.sort(function (a, b) { | ||||
183 | var scoreA = plasmoidScore(a); | ||||
184 | var scoreB = plasmoidScore(b); | ||||
185 | // Sort descending by score | ||||
186 | if (scoreA < scoreB) { | ||||
187 | return 1; | ||||
188 | } else if (scoreA > scoreB) { | ||||
189 | return -1; | ||||
190 | } else { | ||||
191 | return 0; | ||||
192 | } | ||||
193 | }); | ||||
194 | globals.plasmoids = newPlasmoids; | ||||
195 | } | ||||
196 | | ||||
197 | function checkInhibition() { | ||||
198 | globals.inhibited = Qt.binding(function() { | ||||
199 | var inhibited = false; | ||||
200 | | ||||
201 | var inhibitedUntil = notificationSettings.notificationsInhibitedUntil; | ||||
202 | if (!isNaN(inhibitedUntil.getTime())) { | ||||
203 | inhibited |= (new Date().getTime() < inhibitedUntil.getTime()); | ||||
204 | } | ||||
205 | | ||||
206 | if (notificationSettings.notificationsInhibitedByApplication) { | ||||
207 | inhibited |= true; | ||||
208 | } | ||||
209 | | ||||
210 | return inhibited; | ||||
211 | }); | ||||
212 | } | ||||
213 | | ||||
214 | function positionPopups() { | ||||
215 | var rect = screenRect; | ||||
216 | if (!rect || rect.width <= 0 || rect.height <= 0) { | ||||
217 | return; | ||||
218 | } | ||||
219 | | ||||
220 | var y = screenRect.y; | ||||
221 | if (popupLocation & Qt.AlignBottom) { | ||||
222 | y += screenRect.height; | ||||
223 | } else { | ||||
224 | y += popupEdgeDistance; | ||||
225 | } | ||||
226 | | ||||
227 | var x = screenRect.x; | ||||
228 | if (popupLocation & Qt.AlignLeft) { | ||||
229 | x += popupEdgeDistance; | ||||
230 | } | ||||
231 | | ||||
232 | for (var i = 0; i < popupInstantiator.count; ++i) { | ||||
233 | var popup = popupInstantiator.objectAt(i); | ||||
234 | | ||||
235 | if (popupLocation & Qt.AlignHCenter) { | ||||
236 | popup.x = x + (screenRect.width - popup.width) / 2; | ||||
237 | } else if (popupLocation & Qt.AlignRight) { | ||||
238 | popup.x = screenRect.width - popupEdgeDistance - popup.width; | ||||
239 | } else { | ||||
240 | popup.x = x; | ||||
241 | } | ||||
242 | | ||||
243 | var delta = popupSpacing + popup.height; | ||||
244 | | ||||
245 | if (popupLocation & Qt.AlignTop) { | ||||
246 | popup.y = y; | ||||
247 | y += delta; | ||||
248 | } else { | ||||
249 | y -= delta; | ||||
250 | popup.y = y; | ||||
251 | } | ||||
252 | | ||||
253 | // don't let notifications take more than popupMaximumScreenFill of the screen | ||||
254 | var visible = true; | ||||
255 | if (i > 0) { // however always show at least one popup | ||||
256 | if (popupLocation & Qt.AlignTop) { | ||||
257 | visible = (popup.y + popup.height < screenRect.y + (screenRect.height * popupMaximumScreenFill)); | ||||
258 | } else { | ||||
259 | visible = (popup.y > screenRect.y + (screenRect.height * (1 - popupMaximumScreenFill))); | ||||
260 | } | ||||
261 | } | ||||
262 | | ||||
263 | // TODO would be nice to hide popups when systray or panel controller is open | ||||
264 | popup.visible = visible; | ||||
265 | } | ||||
266 | } | ||||
267 | | ||||
268 | property QtObject popupNotificationsModel: NotificationManager.Notifications { | ||||
269 | limit: globals.screenRect ? (Math.ceil(globals.screenRect.height / (theme.mSize(theme.defaultFont).height * 4))) : 0 | ||||
270 | showExpired: false | ||||
271 | showDismissed: false | ||||
272 | blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications | ||||
273 | blacklistedNotifyRcNames: notificationSettings.popupBlacklistedServices | ||||
274 | whitelistedDesktopEntries: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedApplications : [] | ||||
275 | whitelistedNotifyRcNames: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedServices : [] | ||||
276 | showJobs: notificationSettings.jobsInNotifications | ||||
277 | sortMode: NotificationManager.Notifications.SortByTypeAndUrgency | ||||
278 | groupMode: NotificationManager.Notifications.GroupDisabled | ||||
279 | urgencies: { | ||||
280 | var urgencies = 0; | ||||
281 | | ||||
282 | // Critical always except in do not disturb mode when disabled in settings | ||||
283 | if (!globals.inhibited || notificationSettings.criticalPopupsInDoNotDisturbMode) { | ||||
284 | urgencies |= NotificationManager.Notifications.CriticalUrgency; | ||||
285 | } | ||||
286 | | ||||
287 | // Normal only when not in do not disturb mode | ||||
288 | if (!globals.inhibited) { | ||||
289 | urgencies |= NotificationManager.Notifications.NormalUrgency; | ||||
290 | } | ||||
291 | | ||||
292 | // Low only when enabled in settings and not in do not disturb mode | ||||
293 | if (!globals.inhibited && notificationSettings.lowPriorityPopups) { | ||||
294 | urgencies |=NotificationManager.Notifications.LowUrgency; | ||||
295 | } | ||||
296 | | ||||
297 | return urgencies; | ||||
298 | } | ||||
299 | } | ||||
300 | | ||||
301 | property QtObject notificationSettings: NotificationManager.Settings { | ||||
302 | onNotificationsInhibitedUntilChanged: globals.checkInhibition() | ||||
303 | } | ||||
304 | | ||||
305 | // This periodically checks whether do not disturb mode timed out and updates the "minutes ago" labels | ||||
306 | property QtObject timeSource: PlasmaCore.DataSource { | ||||
307 | engine: "time" | ||||
308 | connectedSources: ["Local"] | ||||
309 | interval: 60000 // 1 min | ||||
310 | intervalAlignment: PlasmaCore.Types.AlignToMinute | ||||
311 | onDataChanged: { | ||||
312 | checkInhibition(); | ||||
313 | globals.timeChanged(); | ||||
314 | } | ||||
315 | } | ||||
316 | | ||||
317 | property Instantiator popupInstantiator: Instantiator { | ||||
318 | model: popupNotificationsModel | ||||
319 | delegate: NotificationPopup { | ||||
320 | // so Instantiator can access that after the model row is gone | ||||
321 | readonly property var notificationId: model.notificationId | ||||
322 | | ||||
323 | popupWidth: globals.popupWidth | ||||
324 | | ||||
325 | notificationType: model.type | ||||
326 | | ||||
327 | applicationName: model.applicationName | ||||
328 | applicationIconSource: model.applicationIconName | ||||
329 | deviceName: model.deviceName || "" | ||||
330 | | ||||
331 | time: model.updated || model.created | ||||
332 | | ||||
333 | configurable: model.configurable | ||||
334 | // For running jobs instead of offering a "close" button that might lead the user to | ||||
335 | // think that will cancel the job, we offer a "dismiss" button that hides it in the history | ||||
336 | dismissable: model.type === NotificationManager.Notifications.JobType | ||||
337 | && model.jobState !== NotificationManager.Notifications.JobStateStopped | ||||
338 | // TODO would be nice to be able to "pin" jobs when they autohide | ||||
339 | && notificationSettings.permanentJobPopups | ||||
340 | closable: model.closable | ||||
341 | | ||||
342 | summary: model.summary | ||||
343 | body: model.body || "" | ||||
344 | icon: model.image || model.iconName | ||||
345 | hasDefaultAction: model.hasDefaultAction || false | ||||
346 | timeout: model.timeout | ||||
347 | defaultTimeout: notificationSettings.popupTimeout | ||||
348 | // When configured to not keep jobs open permanently, we autodismiss them after the standard timeout | ||||
349 | dismissTimeout: !notificationSettings.permanentJobPopups | ||||
350 | && model.type === NotificationManager.Notifications.JobType | ||||
351 | && model.jobState !== NotificationManager.Notifications.JobStateStopped | ||||
352 | ? defaultTimeout : 0 | ||||
353 | | ||||
354 | urls: model.urls || [] | ||||
355 | urgency: model.urgency || NotificationManager.Notifications.NormalUrgency | ||||
356 | | ||||
357 | jobState: model.jobState || 0 | ||||
358 | percentage: model.percentage || 0 | ||||
359 | jobError: model.jobError || 0 | ||||
360 | suspendable: !!model.suspendable | ||||
361 | killable: !!model.killable | ||||
362 | jobDetails: model.jobDetails || null | ||||
363 | | ||||
364 | configureActionLabel: model.configureActionLabel || "" | ||||
365 | actionNames: model.actionNames | ||||
366 | actionLabels: model.actionLabels | ||||
367 | | ||||
368 | onExpired: popupNotificationsModel.expire(popupNotificationsModel.index(index, 0)) | ||||
369 | onCloseClicked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) | ||||
370 | onDismissClicked: model.dismissed = true | ||||
371 | onConfigureClicked: popupNotificationsModel.configure(popupNotificationsModel.index(index, 0)) | ||||
372 | onDefaultActionInvoked: { | ||||
373 | popupNotificationsModel.invokeDefaultAction(popupNotificationsModel.index(index, 0)) | ||||
374 | popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) | ||||
375 | } | ||||
376 | onActionInvoked: { | ||||
377 | popupNotificationsModel.invokeAction(popupNotificationsModel.index(index, 0), actionName) | ||||
378 | popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) | ||||
379 | } | ||||
380 | onOpenUrl: { | ||||
381 | Qt.openUrlExternally(url); | ||||
382 | popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) | ||||
383 | } | ||||
384 | onFileActionInvoked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) | ||||
385 | | ||||
386 | onSuspendJobClicked: popupNotificationsModel.suspendJob(popupNotificationsModel.index(index, 0)) | ||||
387 | onResumeJobClicked: popupNotificationsModel.resumeJob(popupNotificationsModel.index(index, 0)) | ||||
388 | onKillJobClicked: popupNotificationsModel.killJob(popupNotificationsModel.index(index, 0)) | ||||
389 | | ||||
390 | onHeightChanged: Qt.callLater(positionPopups) | ||||
391 | onWidthChanged: Qt.callLater(positionPopups) | ||||
392 | | ||||
393 | Component.onCompleted: { | ||||
394 | // Register apps that were seen spawning a popup so they can be configured later | ||||
395 | // Apps with notifyrc can already be configured anyway | ||||
396 | if (model.desktopEntry && !model.notifyRcName) { | ||||
397 | notificationSettings.registerKnownApplication(model.desktopEntry); | ||||
398 | notificationSettings.save(); | ||||
399 | } | ||||
400 | | ||||
401 | // Tell the model that we're handling the timeout now | ||||
402 | popupNotificationsModel.stopTimeout(popupNotificationsModel.index(index, 0)); | ||||
403 | } | ||||
404 | } | ||||
405 | onObjectAdded: { | ||||
406 | // also needed for it to correctly layout its contents | ||||
407 | object.visible = true; | ||||
408 | Qt.callLater(positionPopups); | ||||
409 | } | ||||
410 | onObjectRemoved: { | ||||
411 | var notificationId = object.notificationId | ||||
412 | // Popup might have been destroyed because of a filter change, tell the model to do the timeout work for us again | ||||
413 | // cannot use QModelIndex here as the model row is already gone | ||||
414 | popupNotificationsModel.startTimeout(notificationId); | ||||
415 | | ||||
416 | Qt.callLater(positionPopups); | ||||
417 | } | ||||
418 | } | ||||
419 | | ||||
420 | // TODO use pulseaudio-qt for this once it becomes a framework | ||||
421 | property QtObject pulseAudio: Loader { | ||||
422 | source: "PulseAudio.qml" | ||||
423 | } | ||||
424 | } |