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