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