Unity 8
Notification.qml
1 /*
2  * Copyright (C) 2013 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.0
18 import QtMultimedia 5.0
19 import Powerd 0.1
20 import Ubuntu.Components 1.1
21 import Unity.Notifications 1.0
22 import QMenuModel 0.1
23 import Utils 0.1
24 
25 import Ubuntu.Components.ListItems 0.1 as ListItem
26 
27 Item {
28  id: notification
29 
30  property alias iconSource: icon.fileSource
31  property alias secondaryIconSource: secondaryIcon.source
32  property alias summary: summaryLabel.text
33  property alias body: bodyLabel.text
34  property alias value: valueIndicator.value
35  property var actions
36  property var notificationId
37  property var type
38  property var hints
39  property var notification
40  property color color
41  property bool fullscreen: false
42  property int maxHeight
43  property int margins
44  readonly property bool draggable: (type === Notification.SnapDecision && state === "contracted") || type === Notification.Interactive || type === Notification.Ephemeral
45  readonly property bool darkOnBright: panel.indicators.shown || type === Notification.SnapDecision
46  readonly property color red: "#fc4949"
47  readonly property color green: "#3fb24f"
48  readonly property color sdLightGrey: "#eaeaea"
49  readonly property color sdDarkGrey: "#dddddd"
50  readonly property color sdFontColor: "#5d5d5d"
51  readonly property real contentSpacing: units.gu(2)
52 
53  objectName: "background"
54  implicitHeight: type !== Notification.PlaceHolder ? (fullscreen ? maxHeight : outterColumn.height - shapedBack.anchors.topMargin + contentSpacing * 2) : 0
55 
56  color: (type === Notification.Confirmation && notificationList.useModal && !greeter.shown) || darkOnBright ? sdLightGrey : Qt.rgba(0.132, 0.117, 0.109, 0.97)
57  opacity: 1 - (x / notification.width) // FIXME: non-zero initially because of LP: #1354406 workaround, we want this to start at 0 upon creation eventually
58 
59  state: {
60  var result = "";
61 
62  if (type == Notification.SnapDecision) {
63  if (ListView.view.currentIndex == index) {
64  result = "expanded";
65  } else {
66  if (ListView.view.count > 2) {
67  if (ListView.view.currentIndex == -1 && index == 1) {
68  result = "expanded";
69  } else {
70  result = "contracted";
71  }
72  } else {
73  result = "expanded";
74  }
75  }
76  }
77 
78  return result;
79  }
80 
81  Audio {
82  id: sound
83  objectName: "sound"
84  audioRole: MediaPlayer.alert
85  source: hints["suppress-sound"] !== "true" && hints["sound-file"] !== undefined ? hints["sound-file"] : ""
86  }
87 
88  Component.onCompleted: {
89  // Turn on screen as needed (Powerd.Notification means the screen
90  // stays on for a shorter amount of time)
91  if (type == Notification.SnapDecision) {
92  Powerd.setStatus(Powerd.On, Powerd.Unknown);
93  } else if (type != Notification.Confirmation) {
94  Powerd.setStatus(Powerd.On, Powerd.Notification);
95  }
96 
97  // FIXME: using onCompleted because of LP: #1354406 workaround, has to be onOpacityChanged really
98  if (opacity == 1.0 && hints["suppress-sound"] !== "true" && sound.source !== "") {
99  sound.play();
100  }
101  }
102 
103  Behavior on x {
104  id: normalXBehavior
105 
106  enabled: draggable
107  UbuntuNumberAnimation {
108  duration: UbuntuAnimation.FastDuration
109  easing.type: Easing.OutBounce
110  }
111  }
112 
113  onHintsChanged: {
114  if (type === Notification.Confirmation && opacity == 1.0 && hints["suppress-sound"] !== "true" && sound.source !== "") {
115  sound.play();
116  }
117  }
118 
119  Behavior on height {
120  id: normalHeightBehavior
121 
122  //enabled: menuItemFactory.progress == 1
123  enabled: true
124  UbuntuNumberAnimation {
125  duration: UbuntuAnimation.SnapDuration
126  }
127  }
128 
129  states:[
130  State {
131  name: "contracted"
132  PropertyChanges {target: notification; height: units.gu(10)}
133  },
134  State {
135  name: "expanded"
136  PropertyChanges {target: notification; height: implicitHeight}
137  }
138  ]
139 
140  clip: fullscreen ? false : true
141 
142  visible: type != Notification.PlaceHolder
143 
144  UbuntuShape {
145  id: shapedBack
146 
147  visible: !fullscreen
148  anchors {
149  fill: parent
150  leftMargin: notification.margins
151  rightMargin: notification.margins
152  topMargin: type === Notification.Confirmation ? units.gu(.5) : 0
153  }
154  color: parent.color
155  opacity: parent.opacity
156  radius: "medium"
157  borderSource: "none"
158  }
159 
160  Rectangle {
161  id: nonShapedBack
162 
163  visible: fullscreen
164  anchors.fill: parent
165  color: parent.color
166  opacity: parent.opacity
167  }
168 
169  onXChanged: {
170  if (draggable && notification.x > 0.75 * notification.width) {
171  notification.notification.close()
172  }
173  }
174 
175  Item {
176  id: contents
177  anchors.fill: fullscreen ? nonShapedBack : shapedBack
178 
179  UnityMenuModelPaths {
180  id: paths
181 
182  source: hints["x-canonical-private-menu-model"]
183 
184  busNameHint: "busName"
185  actionsHint: "actions"
186  menuObjectPathHint: "menuPath"
187  }
188 
189  UnityMenuModel {
190  id: unityMenuModel
191 
192  property string lastNameOwner: ""
193 
194  busName: paths.busName
195  actions: paths.actions
196  menuObjectPath: paths.menuObjectPath
197  onNameOwnerChanged: {
198  if (lastNameOwner !== "" && nameOwner === "" && notification.notification !== undefined) {
199  notification.notification.close()
200  }
201  lastNameOwner = nameOwner
202  }
203  }
204 
205  MouseArea {
206  id: interactiveArea
207 
208  anchors.fill: parent
209  objectName: "interactiveArea"
210 
211  drag.target: draggable ? notification : undefined
212  drag.axis: Drag.XAxis
213  drag.minimumX: 0
214  drag.maximumX: notification.width
215 
216  onClicked: {
217  if (notification.type == Notification.Interactive) {
218  notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
219  } else {
220  notificationList.currentIndex = index;
221  }
222  }
223  onReleased: {
224  if (notification.x < notification.width / 2) {
225  notification.x = 0
226  } else {
227  notification.x = notification.width
228  }
229  }
230  }
231 
232  Column {
233  id: outterColumn
234 
235  anchors {
236  left: parent.left
237  right: parent.right
238  top: parent.top
239  margins: 0
240  topMargin: fullscreen ? 0 : type === Notification.Confirmation ? units.gu(1) : units.gu(2)
241  }
242 
243  spacing: type === Notification.Confirmation ? units.gu(1) : units.gu(2)
244 
245  Row {
246  id: topRow
247 
248  spacing: contentSpacing
249  anchors {
250  left: parent.left
251  right: parent.right
252  margins: contentSpacing
253  }
254 
255  ShapedIcon {
256  id: icon
257 
258  objectName: "icon"
259  width: type == Notification.Ephemeral && !bodyLabel.visible ? units.gu(3) : units.gu(6)
260  height: width
261  shaped: notification.hints["x-canonical-non-shaped-icon"] == "true" ? false : true
262  visible: iconSource !== undefined && iconSource !== "" && type !== Notification.Confirmation
263  }
264 
265  Column {
266  id: labelColumn
267  width: secondaryIcon.visible ? parent.width - x - units.gu(4.5) : parent.width - x
268 
269  anchors.verticalCenter: (icon.visible && !bodyLabel.visible) ? icon.verticalCenter : undefined
270 
271  Label {
272  id: summaryLabel
273 
274  objectName: "summaryLabel"
275  anchors {
276  left: parent.left
277  right: parent.right
278  }
279  visible: type !== Notification.Confirmation
280  fontSize: "medium"
281  color: darkOnBright ? sdFontColor : Theme.palette.selected.backgroundText
282  elide: Text.ElideRight
283  textFormat: Text.PlainText
284  }
285 
286  Label {
287  id: bodyLabel
288 
289  objectName: "bodyLabel"
290  anchors {
291  left: parent.left
292  right: parent.right
293  }
294  visible: body != "" && type !== Notification.Confirmation
295  fontSize: "small"
296  color: darkOnBright ? sdFontColor : Theme.palette.selected.backgroundText
297  wrapMode: Text.WordWrap
298  maximumLineCount: type == Notification.SnapDecision ? 12 : 2
299  elide: Text.ElideRight
300  textFormat: Text.PlainText
301  }
302  }
303 
304  Image {
305  id: secondaryIcon
306 
307  objectName: "secondaryIcon"
308  width: units.gu(3)
309  height: units.gu(3)
310  visible: status === Image.Ready
311  fillMode: Image.PreserveAspectCrop
312  }
313  }
314 
315  ListItem.ThinDivider {
316  visible: type == Notification.SnapDecision
317  }
318 
319  ShapedIcon {
320  id: centeredIcon
321  objectName: "centeredIcon"
322  width: units.gu(5)
323  height: width
324  shaped: notification.hints["x-canonical-non-shaped-icon"] == "true" ? false : true
325  fileSource: icon.fileSource
326  visible: fileSource !== undefined && fileSource !== "" && type === Notification.Confirmation
327  anchors.horizontalCenter: parent.horizontalCenter
328  }
329 
330  Label {
331  id: valueLabel
332  objectName: "valueLabel"
333  text: body
334  anchors.horizontalCenter: parent.horizontalCenter
335  visible: type === Notification.Confirmation && body !== ""
336  fontSize: "medium"
337  color: darkOnBright ? sdFontColor : Theme.palette.selected.backgroundText
338  wrapMode: Text.WordWrap
339  maximumLineCount: 1
340  elide: Text.ElideRight
341  textFormat: Text.PlainText
342  }
343 
344  UbuntuShape {
345  id: valueIndicator
346  objectName: "valueIndicator"
347  visible: type === Notification.Confirmation
348  property double value
349 
350  anchors {
351  left: parent.left
352  right: parent.right
353  margins: contentSpacing
354  }
355 
356  height: units.gu(1)
357  color: darkOnBright ? UbuntuColors.darkGrey : UbuntuColors.lightGrey
358  borderSource: "none"
359  radius: "small"
360 
361  UbuntuShape {
362  id: innerBar
363  objectName: "innerBar"
364  width: valueIndicator.width * valueIndicator.value / 100
365  height: units.gu(1)
366  color: notification.hints["x-canonical-value-bar-tint"] === "true" ? UbuntuColors.orange : darkOnBright ? UbuntuColors.lightGrey : "white"
367  borderSource: "none"
368  radius: "small"
369  }
370  }
371 
372  Column {
373  id: dialogColumn
374  objectName: "dialogListView"
375  spacing: units.gu(2)
376 
377  visible: count > 0
378 
379  anchors {
380  left: parent.left
381  right: parent.right
382  top: fullscreen ? parent.top : undefined
383  bottom: fullscreen ? parent.bottom : undefined
384  }
385 
386  Repeater {
387  model: unityMenuModel
388 
389  NotificationMenuItemFactory {
390  id: menuItemFactory
391 
392  anchors {
393  left: dialogColumn.left
394  right: dialogColumn.right
395  }
396 
397  menuModel: unityMenuModel
398  menuData: model
399  menuIndex: index
400  maxHeight: notification.maxHeight
401 
402  onLoaded: {
403  notification.fullscreen = Qt.binding(function() { return fullscreen; });
404  }
405  onAccepted: {
406  notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
407  }
408  }
409  }
410  }
411 
412  Column {
413  id: oneOverTwoCase
414 
415  anchors {
416  left: parent.left
417  right: parent.right
418  margins: contentSpacing
419  }
420 
421  spacing: contentSpacing
422 
423  visible: notification.type === Notification.SnapDecision && oneOverTwoRepeaterTop.count === 3
424 
425  Repeater {
426  id: oneOverTwoRepeaterTop
427 
428  model: notification.actions
429  delegate: Loader {
430  id: oneOverTwoLoaderTop
431 
432  property string actionId: id
433  property string actionLabel: label
434 
435  Component {
436  id: oneOverTwoButtonTop
437 
438  Button {
439  objectName: "notify_oot_button" + index
440  width: oneOverTwoCase.width
441  text: oneOverTwoLoaderTop.actionLabel
442  color: notification.hints["x-canonical-private-affirmative-tint"] == "true" ? green : sdDarkGrey
443  onClicked: notification.notification.invokeAction(oneOverTwoLoaderTop.actionId)
444  }
445  }
446  sourceComponent: index == 0 ? oneOverTwoButtonTop : undefined
447  }
448  }
449 
450  Row {
451  spacing: contentSpacing
452 
453  Repeater {
454  id: oneOverTwoRepeaterBottom
455 
456  model: notification.actions
457  delegate: Loader {
458  id: oneOverTwoLoaderBottom
459 
460  property string actionId: id
461  property string actionLabel: label
462 
463  Component {
464  id: oneOverTwoButtonBottom
465 
466  Button {
467  objectName: "notify_oot_button" + index
468  width: oneOverTwoCase.width / 2 - spacing * 2
469  text: oneOverTwoLoaderBottom.actionLabel
470  color: index == 1 && notification.hints["x-canonical-private-rejection-tint"] == "true" ? red : sdDarkGrey
471  onClicked: notification.notification.invokeAction(oneOverTwoLoaderBottom.actionId)
472  }
473  }
474  sourceComponent: (index == 1 || index == 2) ? oneOverTwoButtonBottom : undefined
475  }
476  }
477  }
478  }
479 
480  Row {
481  id: buttonRow
482 
483  objectName: "buttonRow"
484  anchors {
485  left: parent.left
486  right: parent.right
487  margins: contentSpacing
488  }
489  visible: notification.type === Notification.SnapDecision && actionRepeater.count > 0 && !oneOverTwoCase.visible
490  spacing: units.gu(2)
491  layoutDirection: Qt.RightToLeft
492 
493  Loader {
494  id: notifySwipeButtonLoader
495  active: notification.hints["x-canonical-snap-decisions-swipe"] === "true"
496 
497  sourceComponent: SwipeToAct {
498  objectName: "notify_swipe_button"
499  width: buttonRow.width
500  leftIconName: "call-end"
501  rightIconName: "call-start"
502  onRightTriggered: {
503  notification.notification.invokeAction(notification.actions.data(0, ActionModel.RoleActionId))
504  }
505 
506  onLeftTriggered: {
507  notification.notification.invokeAction(notification.actions.data(1, ActionModel.RoleActionId))
508  }
509  }
510  }
511 
512  Repeater {
513  id: actionRepeater
514  model: notification.actions
515  delegate: Loader {
516  id: loader
517 
518  property string actionId: id
519  property string actionLabel: label
520  active: !notifySwipeButtonLoader.active
521 
522  Component {
523  id: actionButton
524 
525  Button {
526  objectName: "notify_button" + index
527  width: buttonRow.width / 2 - spacing * 2
528  text: loader.actionLabel
529  color: {
530  var result = sdDarkGrey;
531  if (index == 0 && notification.hints["x-canonical-private-affirmative-tint"] == "true") {
532  result = green;
533  }
534  if (index == 1 && notification.hints["x-canonical-private-rejection-tint"] == "true") {
535  result = red;
536  }
537  return result;
538  }
539  onClicked: notification.notification.invokeAction(loader.actionId)
540  }
541  }
542  sourceComponent: (index == 0 || index == 1) ? actionButton : undefined
543  }
544  }
545  }
546 
547  OptionToggle {
548  id: optionToggle
549  objectName: "notify_button2"
550  width: parent.width
551  anchors {
552  left: parent.left
553  right: parent.right
554  margins: contentSpacing
555  }
556 
557  visible: notification.type == Notification.SnapDecision && actionRepeater.count > 3 && !oneOverTwoCase.visible
558  model: notification.actions
559  expanded: false
560  startIndex: 2
561  onTriggered: {
562  notification.notification.invokeAction(id)
563  }
564  }
565  }
566  }
567 }