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  property bool highlightShown: false
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.orientations.primary;
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 = root.orientations.portrait;
170  } else if (supportedOrientations & Qt.LandscapeOrientation) {
171  chosenOrientation = root.orientations.landscape;
172  } else if (supportedOrientations & Qt.InvertedPortraitOrientation) {
173  chosenOrientation = root.orientations.invertedPortrait;
174  } else if (supportedOrientations & Qt.InvertedLandscapeOrientation) {
175  chosenOrientation = root.orientations.invertedLandscape;
176  } else {
177  chosenOrientation = root.orientations.primary;
178  }
179 
180  return Screen.angleBetween(root.orientations.native_, 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  Rectangle {
266  id: selectionHighlight
267  objectName: "selectionHighlight"
268  anchors.fill: appWindow
269  anchors.margins: -units.gu(1)
270  color: "white"
271  opacity: root.highlightShown ? 0.15 : 0
272  antialiasing: true
273  visible: opacity > 0
274  }
275 
276  Rectangle {
277  anchors { left: selectionHighlight.left; right: selectionHighlight.right; bottom: selectionHighlight.bottom; }
278  height: units.dp(2)
279  color: UbuntuColors.orange
280  visible: root.highlightShown
281  antialiasing: true
282  }
283 
284  ApplicationWindow {
285  id: appWindow
286  objectName: application ? "appWindow_" + application.appId : "appWindow_null"
287  focus: true
288  anchors {
289  fill: parent
290  topMargin: appWindow.fullscreen || (application && application.rotatesWindowContents)
291  ? 0 : maximizedAppTopMargin
292  }
293 
294  interactive: root.interactive
295  }
296  }
297  }
298 
299  Item {
300  // mimics appWindowWithShadow. Do the positioning of screenshots of non-fullscreen
301  // app windows
302  id: appWindowScreenshotWithShadow
303  visible: false
304 
305  property real transformRotationAngle: 0
306  property real transformOriginX
307  property real transformOriginY
308 
309  transform: Rotation {
310  origin.x: appWindowScreenshotWithShadow.transformOriginX
311  origin.y: appWindowScreenshotWithShadow.transformOriginY
312  axis { x: 0; y: 0; z: 1 }
313  angle: appWindowScreenshotWithShadow.transformRotationAngle
314  }
315 
316  property var window: appWindowScreenshot
317 
318  function take() {
319  // Format: "image://application/$APP_ID/$CURRENT_TIME_MS"
320  // eg: "image://application/calculator-app/123456"
321  var timeMs = new Date().getTime();
322  appWindowScreenshot.source = "image://application/" + root.application.appId + "/" + timeMs;
323  }
324  function discard() {
325  appWindowScreenshot.source = "";
326  }
327 
328  Image {
329  id: appWindowScreenshot
330  source: ""
331 
332  anchors.fill: parent
333 
334  sourceSize.width: width
335  sourceSize.height: height
336  }
337  }
338 
339  DraggingArea {
340  id: dragArea
341  objectName: "dragArea"
342  anchors.fill: parent
343 
344  property bool moving: false
345  property real distance: 0
346  readonly property int threshold: units.gu(2)
347  property int offset: 0
348 
349  readonly property real minSpeedToClose: units.gu(40)
350 
351  onDragValueChanged: {
352  if (!dragging) {
353  return;
354  }
355  moving = moving || Math.abs(dragValue) > threshold;
356  if (moving) {
357  distance = dragValue + offset;
358  }
359  }
360 
361  onMovingChanged: {
362  if (moving) {
363  offset = (dragValue > 0 ? -threshold: threshold)
364  } else {
365  offset = 0;
366  }
367  }
368 
369  onClicked: {
370  if (!moving) {
371  root.clicked();
372  }
373  }
374 
375  onDragEnd: {
376  if (!root.closeable) {
377  animation.animate("center")
378  return;
379  }
380 
381  // velocity and distance values specified by design prototype
382  if ((dragVelocity < -minSpeedToClose && distance < -units.gu(8)) || distance < -root.height / 2) {
383  animation.animate("up")
384  } else if ((dragVelocity > minSpeedToClose && distance > units.gu(8)) || distance > root.height / 2) {
385  animation.animate("down")
386  } else {
387  animation.animate("center")
388  }
389  }
390 
391  UbuntuNumberAnimation {
392  id: animation
393  objectName: "closeAnimation"
394  target: dragArea
395  property: "distance"
396  property bool requestClose: false
397 
398  function animate(direction) {
399  animation.from = dragArea.distance;
400  switch (direction) {
401  case "up":
402  animation.to = -root.height * 1.5;
403  requestClose = true;
404  break;
405  case "down":
406  animation.to = root.height * 1.5;
407  requestClose = true;
408  break;
409  default:
410  animation.to = 0
411  }
412  animation.start();
413  }
414 
415  onRunningChanged: {
416  if (!running) {
417  dragArea.moving = false;
418  if (requestClose) {
419  root.closed();
420  } else {
421  dragArea.distance = 0;
422  }
423  }
424  }
425  }
426  }
427 }