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.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 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  property url background: ""
54 
55  objectName: "background"
56  implicitHeight: type !== Notification.PlaceHolder ? (fullscreen ? maxHeight : outterColumn.height - shapedBack.anchors.topMargin + contentSpacing * 2) : 0
57 
58  color: (type === Notification.Confirmation && notificationList.useModal && !greeter.shown) || darkOnBright ? sdLightGrey : Qt.rgba(0.132, 0.117, 0.109, 0.97)
59  opacity: 1 - (x / notification.width) // FIXME: non-zero initially because of LP: #1354406 workaround, we want this to start at 0 upon creation eventually
60 
61  state: {
62  var result = "";
63 
64  if (type == Notification.SnapDecision) {
65  if (ListView.view.currentIndex == index) {
66  result = "expanded";
67  } else {
68  if (ListView.view.count > 2) {
69  if (ListView.view.currentIndex == -1 && index == 1) {
70  result = "expanded";
71  } else {
72  result = "contracted";
73  }
74  } else {
75  result = "expanded";
76  }
77  }
78  }
79 
80  return result;
81  }
82 
83  NotificationAudio {
84  id: sound
85  objectName: "sound"
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.SnapDecision);
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  backgroundColor: parent.color
156  opacity: parent.opacity
157  radius: "medium"
158  aspect: UbuntuShape.Flat
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.PlainText
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.PlainText
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  backgroundColor: darkOnBright ? UbuntuColors.darkGrey : UbuntuColors.lightGrey
361  aspect: UbuntuShape.Flat
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  backgroundColor: notification.hints["x-canonical-value-bar-tint"] === "true" ? UbuntuColors.orange : darkOnBright ? UbuntuColors.lightGrey : "white"
370  aspect: UbuntuShape.Flat
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  background: notification.background
405 
406  onLoaded: {
407  notification.fullscreen = Qt.binding(function() { return fullscreen; });
408  }
409  onAccepted: {
410  notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
411  }
412  }
413  }
414  }
415 
416  Column {
417  id: oneOverTwoCase
418 
419  anchors {
420  left: parent.left
421  right: parent.right
422  margins: contentSpacing
423  }
424 
425  spacing: contentSpacing
426 
427  visible: notification.type === Notification.SnapDecision && oneOverTwoRepeaterTop.count === 3
428 
429  Repeater {
430  id: oneOverTwoRepeaterTop
431 
432  model: notification.actions
433  delegate: Loader {
434  id: oneOverTwoLoaderTop
435 
436  property string actionId: id
437  property string actionLabel: label
438 
439  Component {
440  id: oneOverTwoButtonTop
441 
442  Button {
443  objectName: "notify_oot_button" + index
444  width: oneOverTwoCase.width
445  text: oneOverTwoLoaderTop.actionLabel
446  color: notification.hints["x-canonical-private-affirmative-tint"] == "true" ? green : sdDarkGrey
447  onClicked: notification.notification.invokeAction(oneOverTwoLoaderTop.actionId)
448  }
449  }
450  sourceComponent: index == 0 ? oneOverTwoButtonTop : undefined
451  }
452  }
453 
454  Row {
455  spacing: contentSpacing
456 
457  Repeater {
458  id: oneOverTwoRepeaterBottom
459 
460  model: notification.actions
461  delegate: Loader {
462  id: oneOverTwoLoaderBottom
463 
464  property string actionId: id
465  property string actionLabel: label
466 
467  Component {
468  id: oneOverTwoButtonBottom
469 
470  Button {
471  objectName: "notify_oot_button" + index
472  width: oneOverTwoCase.width / 2 - spacing * 2
473  text: oneOverTwoLoaderBottom.actionLabel
474  color: index == 1 && notification.hints["x-canonical-private-rejection-tint"] == "true" ? red : sdDarkGrey
475  onClicked: notification.notification.invokeAction(oneOverTwoLoaderBottom.actionId)
476  }
477  }
478  sourceComponent: (index == 1 || index == 2) ? oneOverTwoButtonBottom : undefined
479  }
480  }
481  }
482  }
483 
484  Row {
485  id: buttonRow
486 
487  objectName: "buttonRow"
488  anchors {
489  left: parent.left
490  right: parent.right
491  margins: contentSpacing
492  }
493  visible: notification.type === Notification.SnapDecision && actionRepeater.count > 0 && !oneOverTwoCase.visible
494  spacing: contentSpacing
495  layoutDirection: Qt.RightToLeft
496 
497  Loader {
498  id: notifySwipeButtonLoader
499  active: notification.hints["x-canonical-snap-decisions-swipe"] === "true"
500 
501  sourceComponent: SwipeToAct {
502  objectName: "notify_swipe_button"
503  width: buttonRow.width
504  leftIconName: "call-end"
505  rightIconName: "call-start"
506  clickToAct: notification.hasMouse
507  onRightTriggered: {
508  notification.notification.invokeAction(notification.actions.data(0, ActionModel.RoleActionId))
509  }
510 
511  onLeftTriggered: {
512  notification.notification.invokeAction(notification.actions.data(1, ActionModel.RoleActionId))
513  }
514  }
515  }
516 
517  Repeater {
518  id: actionRepeater
519  model: notification.actions
520  delegate: Loader {
521  id: loader
522 
523  property string actionId: id
524  property string actionLabel: label
525  active: !notifySwipeButtonLoader.active
526 
527  Component {
528  id: actionButton
529 
530  Button {
531  objectName: "notify_button" + index
532  width: buttonRow.width / 2 - spacing * 2
533  text: loader.actionLabel
534  color: {
535  var result = sdDarkGrey;
536  if (index == 0 && notification.hints["x-canonical-private-affirmative-tint"] == "true") {
537  result = green;
538  }
539  if (index == 1 && notification.hints["x-canonical-private-rejection-tint"] == "true") {
540  result = red;
541  }
542  return result;
543  }
544  onClicked: notification.notification.invokeAction(loader.actionId)
545  }
546  }
547  sourceComponent: (index == 0 || index == 1) ? actionButton : undefined
548  }
549  }
550  }
551 
552  OptionToggle {
553  id: optionToggle
554  objectName: "notify_button2"
555  width: parent.width
556  anchors {
557  left: parent.left
558  right: parent.right
559  margins: contentSpacing
560  }
561 
562  visible: notification.type == Notification.SnapDecision && actionRepeater.count > 3 && !oneOverTwoCase.visible
563  model: notification.actions
564  expanded: false
565  startIndex: 2
566  onTriggered: {
567  notification.notification.invokeAction(id)
568  }
569  }
570  }
571  }
572 }