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