2
* Copyright (C) 2014 Canonical, Ltd.
4
* This program is free software; you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation; version 3.
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 General Public License for more details.
13
* You should have received a copy of the GNU General Public License
14
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
import Ubuntu.Components 0.1
19
import Ubuntu.Gestures 0.1
20
import Unity.Application 0.1
22
import "../Components"
27
// Controls to be set from outside
28
property bool shown: false
29
property bool moving: false
30
property int dragAreaWidth
32
// State information propagated to the outside
33
readonly property bool painting: mainScreenshotImage.visible || fadeInScreenshotImage.visible || appSplash.visible || spreadView.visible
34
property bool fullscreen: priv.focusedApplication ? priv.focusedApplication.fullscreen : false
35
property bool locked: spreadView.visible
37
// Not used for PhoneStage, only useful for SideStage and similar
38
property bool overlayMode: false
39
property int overlayWidth: 0
41
function select(appId) {
42
spreadView.snapTo(priv.indexOf(appId))
47
if (ApplicationManager.focusedApplicationId) {
48
priv.requestNewScreenshot();
50
mainScreenshotImage.anchors.leftMargin = 0;
51
mainScreenshotImage.source = ApplicationManager.get(0).screenshot;
52
mainScreenshotImage.visible = true;
55
mainScreenshotImage.visible = false;
60
target: ApplicationManager
63
if (spreadView.visible) {
64
spreadView.snapTo(priv.indexOf(appId));
66
priv.switchToApp(appId);
70
onFocusedApplicationIdChanged: {
71
if (ApplicationManager.focusedApplicationId.length > 0) {
72
if (priv.secondApplicationStarting || priv.applicationStarting) {
73
appSplashTimer.restart();
75
var application = priv.focusedApplication;
76
root.fullscreen = application.fullscreen;
77
mainScreenshotImage.source = application.screenshot;
80
spreadView.selectedIndex = -1;
82
spreadView.contentX = -spreadView.shift;
87
if (!priv.focusedApplication) {
88
mainScreenshotImage.source = "";
89
mainScreenshotImage.visible = false;
90
priv.applicationStarting = true;
92
mainScreenshotImage.source = "";
93
priv.newFocusedAppId = appId;
94
priv.secondApplicationStarting = true;
95
priv.requestNewScreenshot();
98
if (spreadView.visible) {
103
onApplicationRemoved: {
104
if (ApplicationManager.count == 0) {
105
mainScreenshotImage.source = ""
106
mainScreenshotImage.visible = false;
108
mainScreenshotImage.source = ApplicationManager.get(0).screenshot;
116
property string focusedAppId: ApplicationManager.focusedApplicationId
117
property var focusedApplication: ApplicationManager.findApplication(focusedAppId)
118
property url focusedScreenshot: focusedApplication ? focusedApplication.screenshot : ""
120
property bool waitingForScreenshot: false
122
property bool applicationStarting: false
123
property bool secondApplicationStarting: false
125
property string newFocusedAppId
127
onFocusedScreenshotChanged: {
128
if (root.moving && priv.waitingForScreenshot) {
129
mainScreenshotImage.anchors.leftMargin = 0;
130
mainScreenshotImage.source = priv.focusedScreenshot
131
mainScreenshotImage.visible = true;
132
} else if (priv.secondApplicationStarting && priv.waitingForScreenshot) {
133
applicationSwitchingAnimation.start();
135
waitingForScreenshot = false;
138
function requestNewScreenshot() {
139
waitingForScreenshot = true;
140
ApplicationManager.updateScreenshot(focusedAppId);
143
function switchToApp(appId) {
144
if (priv.focusedAppId) {
145
priv.newFocusedAppId = appId;
146
root.fullscreen = ApplicationManager.findApplication(appId).fullscreen;
147
applicationSwitchingAnimation.start();
149
ApplicationManager.focusApplication(appId);
153
function indexOf(appId) {
154
for (var i = 0; i < ApplicationManager.count; i++) {
155
if (ApplicationManager.get(i).appId == appId) {
164
// FIXME: the signal connections seems to get lost.
166
target: priv.focusedApplication
167
onScreenshotChanged: priv.focusedScreenshot = priv.focusedApplication.screenshot
171
property: "fullscreen"
172
value: priv.focusedApplication ? priv.focusedApplication.fullscreen : false
177
// FIXME: We really need to show something meaningful in the app surface instead of guessing
178
// when it might be ready
182
priv.applicationStarting = false;
183
priv.secondApplicationStarting = false;
187
SequentialAnimation {
188
id: applicationSwitchingAnimation
190
PropertyAction { target: mainScreenshotImage; property: "anchors.leftMargin"; value: 0 }
191
PropertyAction { target: mainScreenshotImage; property: "source"; value: priv.focusedScreenshot }
192
PropertyAction { targets: [mainScreenshotImage, fadeInScreenshotImage]; property: "visible"; value: true }
193
PropertyAction { target: fadeInScreenshotImage; property: "source"; value: {
194
var newFocusedApp = ApplicationManager.findApplication(priv.newFocusedAppId);
195
return newFocusedApp ? newFocusedApp.screenshot : "" }
197
PropertyAction { target: fadeInScreenshotImage; property: "opacity"; value: 0 }
198
PropertyAction { target: fadeInScreenshotImage; property: "scale"; value: .8 }
201
// The actual animation
203
UbuntuNumberAnimation { target: mainScreenshotImage; property: "anchors.leftMargin"; to: root.width; duration: UbuntuAnimation.SlowDuration }
204
UbuntuNumberAnimation { target: fadeInScreenshotImage; properties: "opacity,scale"; to: 1; duration: UbuntuAnimation.SlowDuration }
208
ScriptAction { script: ApplicationManager.focusApplication(priv.newFocusedAppId); }
209
PropertyAction { target: fadeInScreenshotImage; property: "visible"; value: false }
210
PropertyAction { target: mainScreenshotImage; property: "visible"; value: false }
213
// FIXME: Drop this and make the imageprovider show a splashscreen instead
218
visible: priv.secondApplicationStarting
221
id: fadeInScreenshotImage
222
anchors { left: parent.left; bottom: parent.bottom }
232
visible: priv.applicationStarting
235
id: mainScreenshotImage
236
anchors { left: parent.left; bottom: parent.bottom }
243
direction: Direction.Leftwards
244
enabled: ApplicationManager.count > 1 && spreadView.phase != 2
246
anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
247
width: root.dragAreaWidth
249
// Sitting at the right edge of the screen, this EdgeDragArea directly controls the spreadView when
250
// attachedToView is true. When the finger movement passes positionMarker3 we detach it from the
251
// spreadView and make the spreadView snap to positionMarker4.
252
property bool attachedToView: true
254
property var gesturePoints: new Array()
257
if (!dragging && !priv.waitingForScreenshot) {
258
// Initial touch. Let's update the screenshot and reset the spreadView to the starting position.
259
priv.requestNewScreenshot();
260
spreadView.phase = 0;
261
spreadView.contentX = -spreadView.shift;
263
if (dragging && attachedToView) {
264
// Gesture recognized. Let's move the spreadView with the finger
265
spreadView.contentX = -touchX - spreadView.shift;
267
if (attachedToView && spreadView.shiftedContentX >= spreadView.width * spreadView.positionMarker3) {
268
// We passed positionMarker3. Detach from spreadView and snap it.
269
attachedToView = false;
272
gesturePoints.push(touchX);
276
if (status == DirectionalDragArea.Recognized) {
277
attachedToView = true;
283
// Gesture recognized. Start recording this gesture
288
// Ok. The user released. Find out if it was a one-way movement.
289
var oneWayFlick = true;
290
var smallestX = spreadDragArea.width;
291
for (var i = 0; i < gesturePoints.length; i++) {
292
if (gesturePoints[i] >= smallestX) {
296
smallestX = gesturePoints[i];
300
if (oneWayFlick && spreadView.shiftedContentX > units.gu(2) &&
301
spreadView.shiftedContentX < spreadView.positionMarker1 * spreadView.width) {
302
// If it was a short one-way movement, do the Alt+Tab switch
303
// no matter if we didn't cross positionMarker1 yet.
304
spreadView.snapTo(1);
305
} else if (!dragging && attachedToView) {
306
// otherwise snap to the closest snap position we can find
307
// (might be back to start, to app 1 or to spread)
314
id: coverFlipBackground
317
visible: spreadView.visible
322
blockInput: spreadView.visible
327
objectName: "spreadView"
329
visible: spreadDragArea.status == DirectionalDragArea.Recognized || phase > 1 || snapAnimation.running
330
contentWidth: spreadRow.width - shift
333
// The flickable needs to fill the screen in order to get touch events all over.
334
// However, we don't want to the user to be able to scroll back all the way. For
335
// that, the beginning of the gesture starts with a negative value for contentX
336
// so the flickable wants to pull it into the view already. "shift" tunes the
337
// distance where to "lock" the content.
338
property real shift: width / 2
339
property real shiftedContentX: contentX + shift
341
property int tileDistance: width / 4
343
// Those markers mark the various positions in the spread (ratio to screen width from right to left):
344
// 0 - 1: following finger, snap back to the beginning on release
345
property real positionMarker1: 0.3
346
// 1 - 2: curved snapping movement, snap to app 1 on release
347
property real positionMarker2: 0.45
348
// 2 - 3: movement follows finger, snaps back to app 1 on release
349
property real positionMarker3: 0.6
350
// passing 3, we detach movement from the finger and snap to 4
351
property real positionMarker4: 0.9
353
// This is where the first app snaps to when bringing it in from the right edge.
354
property real snapPosition: 0.75
356
// Phase of the animation:
357
// 0: Starting from right edge, a new app (index 1) comes in from the right
358
// 1: The app has reached the first snap position.
359
// 2: The list is dragged further and snaps into the spread view when entering phase 2
360
property int phase: 0
362
property int selectedIndex: -1
364
onShiftedContentXChanged: {
367
if (shiftedContentX > width * positionMarker2) {
372
if (shiftedContentX < width * positionMarker2) {
374
} else if (shiftedContentX >= width * positionMarker4) {
382
if (shiftedContentX < positionMarker1 * width) {
383
snapAnimation.targetContentX = -shift;
384
snapAnimation.start();
385
} else if (shiftedContentX < positionMarker2 * width) {
387
} else if (shiftedContentX < positionMarker3 * width) {
389
} else if (phase < 2){
390
// Add 1 pixel to make sure we definitely hit positionMarker4 even with rounding errors of the animation.
391
snapAnimation.targetContentX = width * positionMarker4 + 1 - shift;
392
snapAnimation.start();
395
function snapTo(index) {
396
spreadView.selectedIndex = index;
397
root.fullscreen = ApplicationManager.get(index).fullscreen;
398
snapAnimation.targetContentX = -shift;
399
snapAnimation.start();
402
SequentialAnimation {
404
property int targetContentX: -spreadView.shift
406
UbuntuNumberAnimation {
409
to: snapAnimation.targetContentX
410
duration: UbuntuAnimation.FastDuration
415
if (spreadView.selectedIndex >= 0) {
416
ApplicationManager.focusApplication(ApplicationManager.get(spreadView.selectedIndex).appId);
417
spreadView.selectedIndex = -1
418
spreadView.phase = 0;
419
spreadView.contentX = -spreadView.shift;
427
// This width controls how much the spread can be flicked left/right. It's composed of:
428
// tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
429
// + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
430
width: Math.max(3, ApplicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
432
x: spreadView.contentX
436
model: ApplicationManager
437
delegate: TransformedSpreadDelegate {
439
objectName: "appDelegate" + index
444
startDistance: spreadView.tileDistance
445
endDistance: units.gu(.5)
446
width: spreadView.width
447
height: spreadView.height
448
selected: spreadView.selectedIndex == index
449
otherSelected: spreadView.selectedIndex >= 0 && !selected
452
x: index == 0 ? 0 : spreadView.width + (index - 1) * spreadView.tileDistance
454
// Each tile has a different progress value running from 0 to 1.
455
// A progress value of 0 means the tile is at the right edge. 1 means the tile has reched the left edge.
457
var tileProgress = (spreadView.shiftedContentX - index * spreadView.tileDistance) / spreadView.width;
458
// Tile 1 needs to move directly from the beginning...
459
if (index == 1 && spreadView.phase < 2) {
460
tileProgress += spreadView.tileDistance / spreadView.width;
465
// This mostly is the same as progress, just adds the snapping to phase 1 for tiles 0 and 1
467
if (spreadView.phase == 0 && index < 2) {
468
if (progress < spreadView.positionMarker1) {
470
} else if (progress < spreadView.positionMarker1 + snappingCurve.period){
471
return spreadView.positionMarker1 + snappingCurve.value * 3;
473
return spreadView.positionMarker2;
481
type: EasingCurve.OutQuad
483
progress: appDelegate.progress - spreadView.positionMarker1
487
if (spreadView.phase == 2) {
488
if (ApplicationManager.focusedApplicationId == ApplicationManager.get(index).appId) {
489
spreadView.snapTo(index);
491
ApplicationManager.requestFocusApplication(ApplicationManager.get(index).appId);