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