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