Lomiri
LauncherDelegate.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.12
18 import Lomiri.Components 1.3
19 
20 Item {
21  id: root
22 
23  property int itemIndex: 0
24  property string iconName
25  property string name
26  property int count: 0
27  property bool countVisible: false
28  property int progress: -1
29  property bool itemRunning: false
30  property bool itemFocused: false
31  property real maxAngle: 0
32  property bool inverted: false
33  property bool alerting: false
34  property bool highlighted: false
35  property bool shortcutHintShown: false
36  property int surfaceCount: 1
37 
38  readonly property int effectiveHeight: Math.cos(angle * Math.PI / 180) * itemHeight
39  readonly property real foldedHeight: Math.cos(maxAngle * Math.PI / 180) * itemHeight
40 
41  property int itemWidth
42  property int itemHeight
43  // The angle used for rotating
44  property real angle: 0
45  // This is the offset that keeps the items inside the panel
46  property real offset: 0
47  property real itemOpacity: 1
48  property real brightness: 0
49  property double maxWiggleAngle: 5.0
50 
51  QtObject {
52  id: priv
53 
54  readonly property int wiggleDuration: LomiriAnimation.SnapDuration
55  property real wiggleAngle: 0
56  }
57 
58  readonly property var wiggleAnim: SequentialAnimation {
59 
60  running: alerting
61  loops: 1
62  alwaysRunToEnd: true
63 
64  NumberAnimation {
65  target: priv
66  property: "wiggleAngle"
67  from: 0
68  to: maxWiggleAngle
69  duration: priv.wiggleDuration
70  easing.type: Easing.InQuad
71  }
72 
73  NumberAnimation {
74  target: priv
75  property: "wiggleAngle"
76  from: maxWiggleAngle
77  to: -maxWiggleAngle
78  duration: priv.wiggleDuration
79  easing.type: Easing.InOutQuad
80  }
81 
82  NumberAnimation {
83  target: priv
84  property: "wiggleAngle"
85  from: -maxWiggleAngle
86  to: maxWiggleAngle
87  duration: priv.wiggleDuration
88  easing.type: Easing.InOutQuad
89  }
90 
91  NumberAnimation {
92  target: priv
93  property: "wiggleAngle"
94  from: maxWiggleAngle
95  to: -maxWiggleAngle
96  duration: priv.wiggleDuration
97  easing.type: Easing.InOutQuad
98  }
99 
100  NumberAnimation {
101  target: priv
102  property: "wiggleAngle"
103  from: -maxWiggleAngle
104  to: maxWiggleAngle
105  duration: priv.wiggleDuration
106  easing.type: Easing.InOutQuad
107  }
108 
109  NumberAnimation {
110  target: priv
111  property: "wiggleAngle"
112  from: maxWiggleAngle
113  to: 0
114  duration: priv.wiggleDuration
115  easing.type: Easing.OutQuad
116  }
117  }
118 
119  Item {
120  id: iconItem
121  width: root.width
122  height: parent.itemHeight + units.gu(1)
123  anchors.centerIn: parent
124 
125  StyledItem {
126  styleName: "FocusShape"
127  anchors.fill: iconShape
128  activeFocusOnTab: true
129  StyleHints {
130  visible: root.highlighted
131  radius: units.gu(2.55)
132  }
133  }
134 
135  ProportionalShape {
136  id: iconShape
137  anchors.centerIn: parent
138  width: root.itemWidth
139  aspect: LomiriShape.DropShadow
140  source: Image {
141  id: iconImage
142  sourceSize.width: iconShape.width
143  sourceSize.height: iconShape.height
144  source: root.iconName
145  cache: false // see lpbug#1543290 why no cache
146  }
147  }
148 
149  LomiriShape {
150  id: countEmblem
151  objectName: "countEmblem"
152  anchors {
153  right: parent.right
154  bottom: parent.bottom
155  rightMargin: (iconItem.width - root.itemWidth) / 2 - units.dp(2)
156  margins: units.dp(5)
157  }
158  width: Math.min(root.itemWidth, Math.max(units.gu(2), countLabel.implicitWidth + units.gu(1)))
159  height: units.gu(2)
160  backgroundColor: theme.palette.normal.positive
161  visible: root.countVisible
162  aspect: LomiriShape.Flat
163 
164  Label {
165  id: countLabel
166  objectName: "countLabel"
167  text: root.count
168  anchors.centerIn: parent
169  width: root.itemWidth - units.gu(1)
170  horizontalAlignment: Text.AlignHCenter
171  elide: Text.ElideRight
172  color: "white"
173  fontSize: "x-small"
174  }
175  }
176 
177  LomiriShape {
178  id: progressOverlay
179  objectName: "progressOverlay"
180 
181  anchors.centerIn: parent
182  width: root.itemWidth * .8
183  height: units.dp(3)
184  visible: root.progress > -1
185  backgroundColor: "white"
186  borderSource: "none"
187 
188  Item {
189  anchors {
190  left: parent.left
191  top: parent.top
192  bottom: parent.bottom
193  }
194  width: Math.min(100, root.progress) / 100 * parent.width
195  clip: true
196 
197  LomiriShape {
198  anchors {
199  left: parent.left
200  top: parent.top
201  bottom: parent.bottom
202  }
203  backgroundColor: theme.palette.normal.activity
204  borderSource: "none"
205  width: progressOverlay.width
206  }
207  }
208  }
209 
210  Column {
211  anchors {
212  left: parent.left
213  verticalCenter: parent.verticalCenter
214  }
215  spacing: units.gu(.5)
216  Repeater {
217  objectName: "surfacePipRepeater"
218  model: Math.min(3, root.surfaceCount)
219  Rectangle {
220  objectName: "runningHighlight" + index
221  width: units.gu(0.25)
222  height: units.gu(.5)
223  color: root.alerting ? theme.palette.normal.activity : "white"
224  visible: root.itemRunning
225  }
226  }
227  }
228 
229  Rectangle {
230  objectName: "focusedHighlight"
231  anchors {
232  right: parent.right
233  verticalCenter: parent.verticalCenter
234  }
235  width: units.gu(0.25)
236  height: units.gu(.5)
237  color: "white"
238  visible: root.itemFocused
239  }
240 
241  LomiriShape {
242  objectName: "shortcutHint"
243  anchors.centerIn: parent
244  width: units.gu(2.5)
245  height: width
246  backgroundColor: "#F2111111"
247  visible: root.shortcutHintShown
248  aspect: LomiriShape.Flat
249  Label {
250  anchors.centerIn: parent
251  text: (itemIndex + 1) % 10
252  color: "white"
253  font.weight: Font.Light
254  }
255  }
256  }
257 
258  ShaderEffect {
259  id: transformEffect
260  anchors.centerIn: parent
261  anchors.verticalCenterOffset: root.offset
262  width: iconItem.width
263  height: iconItem.height
264  property real itemOpacity: root.itemOpacity
265  property real brightness: Math.max(-1, root.brightness)
266  property real angle: root.angle
267  rotation: root.inverted ? 180 : 0
268 
269  property variant source: ShaderEffectSource {
270  id: shaderEffectSource
271  sourceItem: iconItem
272  hideSource: true
273  }
274 
275  transform: [
276  // The rotation about the icon's center/z-axis for the wiggle
277  // needs to happen here too, because there's no other way to
278  // align the wiggle with the icon-folding otherwise
279  Rotation {
280  axis { x: 0; y: 0; z: 1 }
281  origin { x: iconItem.width / 2; y: iconItem.height / 2; z: 0 }
282  angle: priv.wiggleAngle
283  },
284  // Rotating 3 times at top/bottom because that increases the perspective.
285  // This is a hack, but as QML does not support real 3D coordinates
286  // getting a higher perspective can only be done by a hack. This is the most
287  // readable/understandable one I could come up with.
288  Rotation {
289  axis { x: 1; y: 0; z: 0 }
290  origin { x: iconItem.width / 2; y: angle > 0 ? 0 : iconItem.height; z: 0 }
291  angle: root.angle * 0.7
292  },
293  Rotation {
294  axis { x: 1; y: 0; z: 0 }
295  origin { x: iconItem.width / 2; y: angle > 0 ? 0 : iconItem.height; z: 0 }
296  angle: root.angle * 0.7
297  },
298  Rotation {
299  axis { x: 1; y: 0; z: 0 }
300  origin { x: iconItem.width / 2; y: angle > 0 ? 0 : iconItem.height; z: 0 }
301  angle: root.angle * 0.7
302  },
303  // Because rotating it 3 times moves it more to the front/back, i.e. it gets
304  // bigger/smaller and we need a scale to compensate that again.
305  Scale {
306  xScale: 1 - (Math.abs(angle) / 500)
307  yScale: 1 - (Math.abs(angle) / 500)
308  origin { x: iconItem.width / 2; y: iconItem.height / 2}
309  }
310  ]
311 
312  // Using a fragment shader instead of QML's opacity and BrightnessContrast
313  // to be able to do both in one step which gives quite some better performance
314  fragmentShader: "
315  varying highp vec2 qt_TexCoord0;
316  uniform sampler2D source;
317  uniform lowp float brightness;
318  uniform lowp float itemOpacity;
319  void main(void)
320  {
321  highp vec4 sourceColor = texture2D(source, qt_TexCoord0);
322  sourceColor.rgb = mix(sourceColor.rgb, vec3(step(0.0, brightness)), abs(brightness));
323  sourceColor *= itemOpacity;
324  gl_FragColor = sourceColor;
325  }"
326  }
327 }