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