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