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