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