Unity 8
Notification.qml
1 /*
2  * Copyright (C) 2013-2016 Canonical, Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 3.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 import QtQuick 2.4
18 import Powerd 0.1
19 import Ubuntu.Components 1.3
20 import Ubuntu.Components.ListItems 1.3 as ListItem
21 import Unity.Notifications 1.0
22 import QMenuModel 0.1
23 import Utils 0.1
24 import "../Components"
25 
26 StyledItem {
27  id: notification
28 
29  property alias iconSource: icon.fileSource
30  property alias secondaryIconSource: secondaryIcon.source
31  property alias summary: summaryLabel.text
32  property alias body: bodyLabel.text
33  property alias value: valueIndicator.value
34  property var actions
35  property var notificationId
36  property var type
37  property var hints
38  property var notification
39  property color color: theme.palette.normal.background
40  property bool fullscreen: notification.notification && typeof notification.notification.fullscreen != "undefined" ?
41  notification.notification.fullscreen : false // fullscreen prop only exists in the mock
42  property int maxHeight
43  property int margins: units.gu(1)
44 
45  readonly property real defaultOpacity: 1.0
46  property bool hasMouse
47  property url background: ""
48 
49  objectName: "background"
50  implicitHeight: type !== Notification.PlaceHolder ? (fullscreen ? maxHeight : outterColumn.height + shapedBack.anchors.topMargin + margins * 2) : 0
51 
52  // FIXME: non-zero initially because of LP: #1354406 workaround, we want this to start at 0 upon creation eventually
53  opacity: defaultOpacity - Math.abs(x / notification.width)
54 
55  theme: ThemeSettings {
56  name: "Ubuntu.Components.Themes.Ambiance"
57  }
58 
59  readonly property bool expanded: type === Notification.SnapDecision && // expand only snap decisions, if...
60  (fullscreen || // - it's a fullscreen one
61  ListView.view.currentIndex === index || // - it's the one the user clicked on
62  (ListView.view.currentIndex === -1 && index == 0) // - the first one after the user closed the previous one
63  )
64 
65  NotificationAudio {
66  id: sound
67  objectName: "sound"
68  source: hints["suppress-sound"] !== "true" && hints["sound-file"] !== undefined ? hints["sound-file"] : ""
69  }
70 
71  Component.onCompleted: {
72  if (type === Notification.PlaceHolder) {
73  return;
74  }
75 
76  // Turn on screen as needed (Powerd.Notification means the screen
77  // stays on for a shorter amount of time)
78  if (type === Notification.SnapDecision) {
79  Powerd.setStatus(Powerd.On, Powerd.SnapDecision);
80  } else if (type !== Notification.Confirmation) {
81  Powerd.setStatus(Powerd.On, Powerd.Notification);
82  }
83 
84  // FIXME: using onCompleted because of LP: #1354406 workaround, has to be onOpacityChanged really
85  if (opacity == defaultOpacity && hints["suppress-sound"] !== "true" && sound.source !== "") {
86  sound.play();
87  }
88  }
89 
90  Component.onDestruction: {
91  if (type === Notification.PlaceHolder) {
92  return;
93  }
94 
95  if (type === Notification.SnapDecision) {
96  Powerd.setStatus(Powerd.Off, Powerd.SnapDecision);
97  } else if (type !== Notification.Confirmation) {
98  Powerd.setStatus(Powerd.Off, Powerd.Notification);
99  }
100  }
101 
102  function closeNotification() {
103  if (index === ListView.view.currentIndex) { // reset to get the 1st snap decision expanded
104  ListView.view.currentIndex = -1;
105  }
106 
107  // perform the "reject" action
108  notification.notification.invokeAction(notification.actions.data(1, ActionModel.RoleActionId));
109 
110  notification.notification.close();
111  }
112 
113  Behavior on x {
114  UbuntuNumberAnimation { easing.type: Easing.OutBounce }
115  }
116 
117  onHintsChanged: {
118  if (type === Notification.Confirmation && opacity == defaultOpacity && hints["suppress-sound"] !== "true" && sound.source !== "") {
119  sound.play();
120  }
121  }
122 
123  onFullscreenChanged: {
124  if (fullscreen) {
125  notification.notification.urgency = Notification.Critical;
126  }
127  if (index == 0) {
128  ListView.view.topmostIsFullscreen = fullscreen;
129  }
130  }
131 
132  Behavior on implicitHeight {
133  enabled: !fullscreen
134  UbuntuNumberAnimation {
135  duration: UbuntuAnimation.SnapDuration
136  }
137  }
138 
139  visible: type !== Notification.PlaceHolder
140 
141  BorderImage {
142  anchors {
143  fill: contents
144  margins: shapedBack.visible ? -units.gu(1) : -units.gu(1.5)
145  }
146  source: "../Stages/graphics/dropshadow2gu.sci"
147  opacity: notification.opacity * 0.5
148  enabled: !fullscreen
149  }
150 
151  UbuntuShape {
152  id: shapedBack
153  objectName: "shapedBack"
154 
155  visible: !fullscreen
156  anchors {
157  fill: parent
158  leftMargin: notification.margins
159  rightMargin: notification.margins
160  topMargin: index == 0 ? notification.margins : 0
161  }
162  backgroundColor: parent.color
163  radius: "small"
164  aspect: UbuntuShape.Flat
165  }
166 
167  Rectangle {
168  id: nonShapedBack
169 
170  visible: fullscreen
171  anchors.fill: parent
172  color: parent.color
173  }
174 
175  onXChanged: {
176  if (Math.abs(notification.x) > 0.75 * notification.width) {
177  closeNotification();
178  }
179  }
180 
181  Item {
182  id: contents
183  anchors.fill: fullscreen ? nonShapedBack : shapedBack
184 
185  UnityMenuModelPaths {
186  id: paths
187 
188  source: hints["x-canonical-private-menu-model"]
189 
190  busNameHint: "busName"
191  actionsHint: "actions"
192  menuObjectPathHint: "menuPath"
193  }
194 
195  UnityMenuModel {
196  id: unityMenuModel
197 
198  property string lastNameOwner: ""
199 
200  busName: paths.busName
201  actions: paths.actions
202  menuObjectPath: paths.menuObjectPath
203  onNameOwnerChanged: {
204  if (lastNameOwner !== "" && nameOwner === "" && notification.notification !== undefined) {
205  notification.notification.close()
206  }
207  lastNameOwner = nameOwner
208  }
209  }
210 
211  MouseArea {
212  id: interactiveArea
213 
214  anchors.fill: parent
215  objectName: "interactiveArea"
216 
217  drag.target: !fullscreen ? notification : undefined
218  drag.axis: Drag.XAxis
219  drag.minimumX: -notification.width
220  drag.maximumX: notification.width
221  hoverEnabled: true
222 
223  onClicked: {
224  if (notification.type === Notification.Interactive) {
225  notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
226  } else {
227  notification.ListView.view.currentIndex = index;
228  }
229  }
230  onReleased: {
231  if (Math.abs(notification.x) < notification.width / 2) {
232  notification.x = 0
233  } else {
234  notification.x = notification.width
235  }
236  }
237  }
238 
239  NotificationButton {
240  objectName: "closeButton"
241  width: units.gu(2)
242  height: width
243  radius: width / 2
244  visible: hasMouse && (containsMouse || interactiveArea.containsMouse)
245  iconName: "close"
246  outline: false
247  hoverEnabled: true
248  color: theme.palette.normal.negative
249  anchors.horizontalCenter: parent.left
250  anchors.horizontalCenterOffset: notification.parent.state === "narrow" ? notification.margins / 2 : 0
251  anchors.verticalCenter: parent.top
252  anchors.verticalCenterOffset: notification.parent.state === "narrow" ? notification.margins / 2 : 0
253 
254  onClicked: closeNotification();
255  }
256 
257  Column {
258  id: outterColumn
259  objectName: "outterColumn"
260 
261  anchors {
262  left: parent.left
263  right: parent.right
264  top: parent.top
265  margins: !fullscreen ? notification.margins : 0
266  }
267 
268  spacing: notification.margins
269 
270  Row {
271  id: topRow
272 
273  spacing: notification.margins
274  anchors {
275  left: parent.left
276  right: parent.right
277  }
278 
279  ShapedIcon {
280  id: icon
281 
282  objectName: "icon"
283  width: units.gu(6)
284  height: width
285  shaped: notification.hints["x-canonical-non-shaped-icon"] !== "true"
286  visible: iconSource !== "" && type !== Notification.Confirmation
287  }
288 
289  Column {
290  id: labelColumn
291  width: secondaryIcon.visible ? parent.width - x - units.gu(3) : parent.width - x
292  anchors.verticalCenter: (icon.visible && !bodyLabel.visible) ? icon.verticalCenter : undefined
293  spacing: units.gu(.4)
294 
295  Label {
296  id: summaryLabel
297 
298  objectName: "summaryLabel"
299  anchors {
300  left: parent.left
301  right: parent.right
302  }
303  visible: type !== Notification.Confirmation
304  fontSize: "medium"
305  font.weight: Font.Light
306  color: theme.palette.normal.backgroundSecondaryText
307  elide: Text.ElideRight
308  textFormat: Text.PlainText
309  }
310 
311  Label {
312  id: bodyLabel
313 
314  objectName: "bodyLabel"
315  anchors {
316  left: parent.left
317  right: parent.right
318  }
319  visible: body != "" && type !== Notification.Confirmation
320  fontSize: "small"
321  font.weight: Font.Light
322  color: theme.palette.normal.backgroundTertiaryText
323  wrapMode: Text.Wrap
324  maximumLineCount: type === Notification.SnapDecision ? 12 : 2
325  elide: Text.ElideRight
326  textFormat: Text.PlainText
327  lineHeight: 1.1
328  }
329  }
330 
331  Image {
332  id: secondaryIcon
333 
334  objectName: "secondaryIcon"
335  width: units.gu(2)
336  height: width
337  visible: status === Image.Ready
338  fillMode: Image.PreserveAspectCrop
339  }
340  }
341 
342  ListItem.ThinDivider {
343  visible: type === Notification.SnapDecision && notification.expanded
344  }
345 
346  Icon {
347  name: "toolkit_chevron-down_3gu"
348  visible: type === Notification.SnapDecision && !notification.expanded
349  width: units.gu(2)
350  height: width
351  anchors.horizontalCenter: parent.horizontalCenter
352  color: theme.palette.normal.base
353  }
354 
355  ShapedIcon {
356  id: centeredIcon
357  objectName: "centeredIcon"
358  width: units.gu(4)
359  height: width
360  shaped: notification.hints["x-canonical-non-shaped-icon"] !== "true"
361  fileSource: icon.fileSource
362  visible: fileSource !== "" && type === Notification.Confirmation
363  anchors.horizontalCenter: parent.horizontalCenter
364  }
365 
366  Label {
367  id: valueLabel
368  objectName: "valueLabel"
369  text: body
370  anchors.horizontalCenter: parent.horizontalCenter
371  visible: type === Notification.Confirmation && body !== ""
372  fontSize: "medium"
373  font.weight: Font.Light
374  color: theme.palette.normal.backgroundSecondaryText
375  wrapMode: Text.WordWrap
376  maximumLineCount: 1
377  elide: Text.ElideRight
378  textFormat: Text.PlainText
379  }
380 
381  ProgressBar {
382  id: valueIndicator
383  objectName: "valueIndicator"
384  visible: type === Notification.Confirmation
385  minimumValue: 0
386  maximumValue: 100
387  showProgressPercentage: false
388  anchors {
389  left: parent.left
390  right: parent.right
391  }
392  height: units.gu(1)
393  }
394 
395  Column {
396  id: dialogColumn
397  objectName: "dialogListView"
398  spacing: notification.margins
399 
400  visible: count > 0 && (notification.expanded || notification.fullscreen)
401 
402  anchors {
403  left: parent.left
404  right: parent.right
405  top: fullscreen ? parent.top : undefined
406  bottom: fullscreen ? parent.bottom : undefined
407  }
408 
409  Repeater {
410  model: unityMenuModel
411 
412  NotificationMenuItemFactory {
413  id: menuItemFactory
414 
415  anchors {
416  left: dialogColumn.left
417  right: dialogColumn.right
418  }
419 
420  menuModel: unityMenuModel
421  menuData: model
422  menuIndex: index
423  maxHeight: notification.maxHeight
424  background: notification.background
425 
426  onLoaded: {
427  notification.fullscreen = Qt.binding(function() { return fullscreen; });
428  }
429  onAccepted: {
430  notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
431  }
432  }
433  }
434  }
435 
436  Column {
437  id: oneOverTwoCase
438 
439  anchors {
440  left: parent.left
441  right: parent.right
442  }
443 
444  spacing: notification.margins
445 
446  visible: notification.type === Notification.SnapDecision && oneOverTwoRepeaterTop.count === 3 && notification.expanded
447 
448  Repeater {
449  id: oneOverTwoRepeaterTop
450 
451  model: notification.actions
452  delegate: Loader {
453  id: oneOverTwoLoaderTop
454 
455  property string actionId: id
456  property string actionLabel: label
457 
458  Component {
459  id: oneOverTwoButtonTop
460 
461  NotificationButton {
462  objectName: "notify_oot_button" + index
463  width: oneOverTwoCase.width
464  text: oneOverTwoLoaderTop.actionLabel
465  outline: notification.hints["x-canonical-private-affirmative-tint"] !== "true"
466  color: notification.hints["x-canonical-private-affirmative-tint"] === "true" ? theme.palette.normal.positive
467  : theme.palette.normal.foreground
468  onClicked: notification.notification.invokeAction(oneOverTwoLoaderTop.actionId)
469  }
470  }
471  sourceComponent: index == 0 ? oneOverTwoButtonTop : undefined
472  }
473  }
474 
475  Row {
476  spacing: notification.margins
477 
478  Repeater {
479  id: oneOverTwoRepeaterBottom
480 
481  model: notification.actions
482  delegate: Loader {
483  id: oneOverTwoLoaderBottom
484 
485  property string actionId: id
486  property string actionLabel: label
487 
488  Component {
489  id: oneOverTwoButtonBottom
490 
491  NotificationButton {
492  objectName: "notify_oot_button" + index
493  width: oneOverTwoCase.width / 2 - spacing / 2
494  text: oneOverTwoLoaderBottom.actionLabel
495  outline: notification.hints["x-canonical-private-rejection-tint"] !== "true"
496  color: index == 1 && notification.hints["x-canonical-private-rejection-tint"] === "true" ? theme.palette.normal.negative
497  : theme.palette.normal.foreground
498  onClicked: notification.notification.invokeAction(oneOverTwoLoaderBottom.actionId)
499  }
500  }
501  sourceComponent: (index == 1 || index == 2) ? oneOverTwoButtonBottom : undefined
502  }
503  }
504  }
505  }
506 
507  Row {
508  id: buttonRow
509 
510  objectName: "buttonRow"
511  anchors {
512  left: parent.left
513  right: parent.right
514  }
515  visible: notification.type === Notification.SnapDecision && actionRepeater.count > 0 && !oneOverTwoCase.visible && notification.expanded
516  spacing: notification.margins
517  layoutDirection: Qt.RightToLeft
518 
519  Loader {
520  id: notifySwipeButtonLoader
521  active: notification.hints["x-canonical-snap-decisions-swipe"] === "true"
522 
523  sourceComponent: SwipeToAct {
524  objectName: "notify_swipe_button"
525  width: buttonRow.width
526  leftIconName: "call-end"
527  rightIconName: "call-start"
528  clickToAct: notification.hasMouse
529  onRightTriggered: {
530  notification.notification.invokeAction(notification.actions.data(0, ActionModel.RoleActionId))
531  }
532 
533  onLeftTriggered: {
534  notification.notification.invokeAction(notification.actions.data(1, ActionModel.RoleActionId))
535  }
536  }
537  }
538 
539  Repeater {
540  id: actionRepeater
541  model: notification.actions
542  delegate: Loader {
543  id: loader
544 
545  property string actionId: id
546  property string actionLabel: label
547  active: !notifySwipeButtonLoader.active
548 
549  Component {
550  id: actionButton
551 
552  NotificationButton {
553  objectName: "notify_button" + index
554  width: buttonRow.width / 2 - spacing / 2
555  text: loader.actionLabel
556  outline: (index == 0 && notification.hints["x-canonical-private-affirmative-tint"] !== "true") ||
557  (index == 1 && notification.hints["x-canonical-private-rejection-tint"] !== "true")
558  color: {
559  var result = theme.palette.normal.foreground;
560  if (index == 0 && notification.hints["x-canonical-private-affirmative-tint"] === "true") {
561  result = theme.palette.normal.positive;
562  }
563  if (index == 1 && notification.hints["x-canonical-private-rejection-tint"] === "true") {
564  result = theme.palette.normal.negative;
565  }
566  return result;
567  }
568  onClicked: notification.notification.invokeAction(loader.actionId)
569  }
570  }
571  sourceComponent: (index == 0 || index == 1) ? actionButton : undefined
572  }
573  }
574  }
575 
576  OptionToggle {
577  id: optionToggle
578  objectName: "notify_button2"
579  width: parent.width
580  anchors {
581  left: parent.left
582  right: parent.right
583  }
584 
585  visible: notification.type === Notification.SnapDecision && actionRepeater.count > 3 && !oneOverTwoCase.visible && notification.expanded
586  model: notification.actions
587  expanded: false
588  startIndex: 2
589  onTriggered: {
590  notification.notification.invokeAction(id)
591  }
592  }
593  }
594  }
595 }