Unity 8
SpreadDelegate.qml
1 /*
2  * Copyright 2014-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 Lesser 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 Lesser General Public License for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  *
16  * Authors: Michael Zanetti <michael.zanetti@canonical.com>
17  * Daniel d'Andrada <daniel.dandrada@canonical.com>
18  */
19 
20 import QtQuick 2.0
21 import QtQuick.Window 2.0
22 import Ubuntu.Components 1.1
23 import "../Components"
24 
25 FocusScope {
26  id: root
27 
28  // to be read from outside
29  readonly property bool dragged: dragArea.moving
30  signal clicked()
31  signal closed()
32  readonly property alias appWindowOrientationAngle: appWindowWithShadow.orientationAngle
33  readonly property alias appWindowRotation: appWindowWithShadow.rotation
34  readonly property alias orientationChangesEnabled: appWindow.orientationChangesEnabled
35 
36  // to be set from outside
37  property bool interactive: true
38  property bool dropShadow: true
39  property real maximizedAppTopMargin
40  property alias swipeToCloseEnabled: dragArea.enabled
41  property bool closeable
42  property alias application: appWindow.application
43  property int shellOrientationAngle
44  property int shellOrientation
45  property int shellPrimaryOrientation
46  property int nativeOrientation
47 
48  function matchShellOrientation() {
49  if (!root.application)
50  return;
51  appWindowWithShadow.orientationAngle = root.shellOrientationAngle;
52  }
53 
54  function animateToShellOrientation() {
55  if (!root.application)
56  return;
57 
58  if (root.application.rotatesWindowContents) {
59  appWindowWithShadow.orientationAngle = root.shellOrientationAngle;
60  } else {
61  orientationChangeAnimation.start();
62  }
63  }
64 
65  OrientationChangeAnimation {
66  id: orientationChangeAnimation
67  objectName: "orientationChangeAnimation"
68  spreadDelegate: root
69  background: background
70  window: appWindowWithShadow
71  screenshot: appWindowScreenshotWithShadow
72  }
73 
74  QtObject {
75  id: priv
76  property bool startingUp: true
77  }
78 
79  Component.onCompleted: { finishStartUpTimer.start(); }
80  Timer { id: finishStartUpTimer; interval: 400; onTriggered: priv.startingUp = false }
81 
82  Rectangle {
83  id: background
84  color: "black"
85  anchors.fill: parent
86  visible: false
87  }
88 
89  Item {
90  objectName: "displacedAppWindowWithShadow"
91 
92  readonly property real limit: root.height / 4
93 
94  y: root.closeable ? dragArea.distance : elastic(dragArea.distance)
95  width: parent.width
96  height: parent.height
97 
98  function elastic(distance) {
99  var k = distance < 0 ? -limit : limit
100  return k * (1 - Math.pow((k - 1) / k, distance))
101  }
102 
103  Item {
104  id: appWindowWithShadow
105  objectName: "appWindowWithShadow"
106 
107  property int orientationAngle
108 
109  property real transformRotationAngle: 0
110  property real transformOriginX
111  property real transformOriginY
112 
113  property var window: appWindow
114 
115  transform: Rotation {
116  origin.x: appWindowWithShadow.transformOriginX
117  origin.y: appWindowWithShadow.transformOriginY
118  axis { x: 0; y: 0; z: 1 }
119  angle: appWindowWithShadow.transformRotationAngle
120  }
121 
122  state: {
123  if (priv.startingUp) {
124  return "startingUp";
125  } else if (root.application && root.application.rotatesWindowContents) {
126  return "counterRotate";
127  } else if (orientationChangeAnimation.running) {
128  return "animatingRotation";
129  } else {
130  return "keepSceneRotation";
131  }
132  }
133 
134  // Ensures the given angle is in the form (0,90,180,270)
135  function normalizeAngle(angle) {
136  while (angle < 0) {
137  angle += 360;
138  }
139  return angle % 360;
140  }
141 
142  states: [
143  // Sets the initial orientationAngle of the window, when it first slides into view
144  // (with the splash screen likely being displayed). At that point we just try to
145  // match shell's current orientation. We need a bit of time in this state as the
146  // information we need to decide orientationAngle may take a few cycles to
147  // be set.
148  State {
149  name: "startingUp"
150  PropertyChanges {
151  target: appWindowWithShadow
152  restoreEntryValues: false
153  orientationAngle: {
154  if (!root.application || root.application.rotatesWindowContents) {
155  return 0;
156  }
157  var supportedOrientations = root.application.supportedOrientations;
158 
159  if (supportedOrientations === Qt.PrimaryOrientation) {
160  supportedOrientations = root.shellPrimaryOrientation;
161  }
162 
163  // If it doesn't support shell's current orientation
164  // then simply pick some arbitraty one that it does support
165  var chosenOrientation = 0;
166  if (supportedOrientations & root.shellOrientation) {
167  chosenOrientation = root.shellOrientation;
168  } else if (supportedOrientations & Qt.PortraitOrientation) {
169  chosenOrientation = Qt.PortraitOrientation;
170  } else if (supportedOrientations & Qt.LandscapeOrientation) {
171  chosenOrientation = Qt.LandscapeOrientation;
172  } else if (supportedOrientations & Qt.InvertedPortraitOrientation) {
173  chosenOrientation = Qt.InvertedPortraitOrientation;
174  } else if (supportedOrientations & Qt.InvertedLandscapeOrientation) {
175  chosenOrientation = Qt.InvertedLandscapeOrientation;
176  } else {
177  chosenOrientation = root.shellPrimaryOrientation;
178  }
179 
180  return Screen.angleBetween(root.nativeOrientation, chosenOrientation);
181  }
182 
183  rotation: normalizeAngle(appWindowWithShadow.orientationAngle - root.shellOrientationAngle)
184  width: {
185  if (rotation == 0 || rotation == 180) {
186  return root.width;
187  } else {
188  return root.height;
189  }
190  }
191  height: {
192  if (rotation == 0 || rotation == 180)
193  return root.height;
194  else
195  return root.width;
196  }
197  }
198  },
199  // In this state we stick to our currently set orientationAngle, which may change only due
200  // to calls made to matchShellOrientation() or animateToShellOrientation()
201  State {
202  id: keepSceneRotationState
203  name: "keepSceneRotation"
204 
205  StateChangeScript { script: {
206  // break binding
207  appWindowWithShadow.orientationAngle = appWindowWithShadow.orientationAngle;
208  } }
209  PropertyChanges {
210  target: appWindowWithShadow
211  restoreEntryValues: false
212  rotation: normalizeAngle(appWindowWithShadow.orientationAngle - root.shellOrientationAngle)
213  width: {
214  if (rotation == 0 || rotation == 180) {
215  return root.width;
216  } else {
217  return root.height;
218  }
219  }
220  height: {
221  if (rotation == 0 || rotation == 180)
222  return root.height;
223  else
224  return root.width;
225  }
226  }
227  },
228  // In this state we counteract any shell rotation so that the window, in scene coordinates,
229  // remains unrotated.
230  State {
231  name: "counterRotate"
232  StateChangeScript { script: {
233  // break binding
234  appWindowWithShadow.orientationAngle = appWindowWithShadow.orientationAngle;
235  } }
236  PropertyChanges {
237  target: appWindowWithShadow
238  width: root.shellOrientationAngle == 0 || root.shellOrientationAngle == 180 ? root.width : root.height
239  height: root.shellOrientationAngle == 0 || root.shellOrientationAngle == 180 ? root.height : root.width
240  rotation: normalizeAngle(-root.shellOrientationAngle)
241  }
242  PropertyChanges {
243  target: appWindow
244  surfaceOrientationAngle: orientationAngle
245  }
246  },
247  State {
248  name: "animatingRotation"
249  }
250  ]
251 
252  x: (parent.width - width) / 2
253  y: (parent.height - height) / 2
254 
255  BorderImage {
256  anchors {
257  fill: appWindow
258  margins: -units.gu(2)
259  }
260  source: "graphics/dropshadow2gu.sci"
261  opacity: root.dropShadow ? .3 : 0
262  Behavior on opacity { UbuntuNumberAnimation {} }
263  }
264 
265  ApplicationWindow {
266  id: appWindow
267  objectName: application ? "appWindow_" + application.appId : "appWindow_null"
268  focus: true
269  anchors {
270  fill: parent
271  topMargin: appWindow.fullscreen || application.rotatesWindowContents
272  ? 0 : maximizedAppTopMargin
273  }
274 
275  interactive: root.interactive
276  }
277  }
278  }
279 
280  Item {
281  // mimics appWindowWithShadow. Do the positioning of screenshots of non-fullscreen
282  // app windows
283  id: appWindowScreenshotWithShadow
284  visible: false
285 
286  property real transformRotationAngle: 0
287  property real transformOriginX
288  property real transformOriginY
289 
290  transform: Rotation {
291  origin.x: appWindowScreenshotWithShadow.transformOriginX
292  origin.y: appWindowScreenshotWithShadow.transformOriginY
293  axis { x: 0; y: 0; z: 1 }
294  angle: appWindowScreenshotWithShadow.transformRotationAngle
295  }
296 
297  property var window: appWindowScreenshot
298 
299  function take() {
300  // Format: "image://application/$APP_ID/$CURRENT_TIME_MS"
301  // eg: "image://application/calculator-app/123456"
302  var timeMs = new Date().getTime();
303  appWindowScreenshot.source = "image://application/" + root.application.appId + "/" + timeMs;
304  }
305  function discard() {
306  appWindowScreenshot.source = "";
307  }
308 
309  Image {
310  id: appWindowScreenshot
311  source: ""
312 
313  anchors.fill: parent
314 
315  sourceSize.width: width
316  sourceSize.height: height
317  }
318  }
319 
320  DraggingArea {
321  id: dragArea
322  objectName: "dragArea"
323  anchors.fill: parent
324 
325  property bool moving: false
326  property real distance: 0
327  readonly property int threshold: units.gu(2)
328  property int offset: 0
329 
330  readonly property real minSpeedToClose: units.gu(40)
331 
332  onDragValueChanged: {
333  if (!dragging) {
334  return;
335  }
336  moving = moving || Math.abs(dragValue) > threshold;
337  if (moving) {
338  distance = dragValue + offset;
339  }
340  }
341 
342  onMovingChanged: {
343  if (moving) {
344  offset = (dragValue > 0 ? -threshold: threshold)
345  } else {
346  offset = 0;
347  }
348  }
349 
350  onClicked: {
351  if (!moving) {
352  root.clicked();
353  }
354  }
355 
356  onDragEnd: {
357  if (!root.closeable) {
358  animation.animate("center")
359  return;
360  }
361 
362  // velocity and distance values specified by design prototype
363  if ((dragVelocity < -minSpeedToClose && distance < -units.gu(8)) || distance < -root.height / 2) {
364  animation.animate("up")
365  } else if ((dragVelocity > minSpeedToClose && distance > units.gu(8)) || distance > root.height / 2) {
366  animation.animate("down")
367  } else {
368  animation.animate("center")
369  }
370  }
371 
372  UbuntuNumberAnimation {
373  id: animation
374  objectName: "closeAnimation"
375  target: dragArea
376  property: "distance"
377  property bool requestClose: false
378 
379  function animate(direction) {
380  animation.from = dragArea.distance;
381  switch (direction) {
382  case "up":
383  animation.to = -root.height * 1.5;
384  requestClose = true;
385  break;
386  case "down":
387  animation.to = root.height * 1.5;
388  requestClose = true;
389  break;
390  default:
391  animation.to = 0
392  }
393  animation.start();
394  }
395 
396  onRunningChanged: {
397  if (!running) {
398  dragArea.moving = false;
399  if (requestClose) {
400  root.closed();
401  } else {
402  dragArea.distance = 0;
403  }
404  }
405  }
406  }
407  }
408 }