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