Lomiri
Loading...
Searching...
No Matches
Workspaces.qml
1/*
2 * Copyright (C) 2017 Canonical Ltd.
3 *
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.
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 General Public License for more details.
12 *
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/>.
15 */
16
17import QtQuick 2.12
18import Lomiri.Components 1.3
19import GSettings 1.0
20import WindowManager 1.0
21import "MathUtils.js" as MathUtils
22import "../../Components"
23
24Item {
25 id: root
26 implicitWidth: listView.contentWidth
27 readonly property int minimumWidth: {
28 var count = Math.min(3, listView.count);
29 return listView.itemWidth * count + listView.spacing * (count - 1)
30 }
31
32 property QtObject screen: null
33 property alias workspaceModel: listView.model
34 property var background // TODO: should be stored in the workspace data
35 property int selectedIndex: -1
36 property bool readOnly: true
37 property var activeWorkspace: null
38
39 signal commitScreenSetup();
40 signal closeSpread();
41 signal clicked(var workspace);
42
43 GSettings {
44 id: settings
45 schema.id: "com.lomiri.Shell"
46 }
47
48 DropArea {
49 anchors.fill: root
50
51 keys: ['workspace']
52
53 onEntered: {
54 var index = listView.getDropIndex(drag);
55 drag.source.workspace.assign(workspaceModel, index)
56 drag.source.inDropArea = true;
57 }
58
59 onPositionChanged: {
60 var index = listView.getDropIndex(drag);
61 if (listView.dropItemIndex == index) return;
62 listView.model.move(listView.dropItemIndex, index, 1);
63 listView.dropItemIndex = index;
64 }
65
66 onExited: {
67 drag.source.workspace.unassign()
68 listView.dropItemIndex = -1;
69 listView.hoveredWorkspaceIndex = -1;
70 drag.source.inDropArea = false;
71 }
72
73 onDropped: {
74 drop.accept(Qt.MoveAction);
75 listView.dropItemIndex = -1;
76 drag.source.inDropArea = false;
77 }
78 }
79 DropArea {
80 anchors.fill: parent
81 keys: ["application"]
82
83 onPositionChanged: {
84 listView.progressiveScroll(drag.x)
85 listView.updateDropProperties(drag)
86 }
87 onExited: {
88 listView.hoveredWorkspaceIndex = -1
89 }
90 onDropped: {
91 var surface = drag.source.surface;
92 drag.source.surface = null;
93 var workspace = listView.model.get(listView.hoveredWorkspaceIndex);
94 WorkspaceManager.moveSurfaceToWorkspace(surface, workspace);
95 drop.accept(Qt.MoveAction)
96 if (listView.hoveredHalf == "right") {
97 root.closeSpread();
98 workspace.activate();
99 }
100 surface.activate();
101 listView.hoveredWorkspaceIndex = -1
102 }
103 }
104
105 onSelectedIndexChanged: {
106 listView.positionViewAtIndex(selectedIndex, ListView.Center);
107 }
108
109 Item {
110 // We need to clip the listview as it has left/right margins and it would
111 // overlap with items next to it and eat mouse input. However, we can't
112 // just clip at the actual bounds as the delegates have the close button
113 // on hover which reaches a bit outside, so lets some margins for the clipping
114 anchors.fill: parent
115 anchors.margins: -units.gu(2)
116 clip: true
117
118
119 ListView {
120 id: listView
121 anchors {
122 fill: parent
123 topMargin: -parent.anchors.margins
124 bottomMargin: -parent.anchors.margins
125 leftMargin: -itemWidth - parent.anchors.margins
126 rightMargin: -itemWidth - parent.anchors.margins
127 }
128 boundsBehavior: Flickable.StopAtBounds
129
130 Behavior on contentX {
131 SmoothedAnimation { duration: 200 }
132 }
133
134 property var clickedWorkspace: null
135
136 orientation: ListView.Horizontal
137 spacing: units.gu(1)
138 leftMargin: itemWidth
139 rightMargin: itemWidth
140
141 property int screenWidth: screen.availableModes[screen.currentModeIndex].size.width
142 property int screenHeight: screen.availableModes[screen.currentModeIndex].size.height
143 property int itemWidth: height * screenWidth / screenHeight
144 property int foldingAreaWidth: itemWidth / 2
145 property int maxAngle: 40
146
147 property real realContentX: contentX - originX + leftMargin
148 property int dropItemIndex: -1
149 property int hoveredWorkspaceIndex: -1
150 property string hoveredHalf: "" // left or right
151
152 function getDropIndex(drag) {
153 var coords = mapToItem(listView.contentItem, drag.x, drag.y)
154 var index = Math.floor((drag.x + listView.realContentX) / (listView.itemWidth + listView.spacing));
155 if (index < 0) index = 0;
156 var upperLimit = dropItemIndex == -1 ? listView.count : listView.count - 1
157 if (index > upperLimit) index = upperLimit;
158 return index;
159 }
160
161 function updateDropProperties(drag) {
162 var coords = mapToItem(listView.contentItem, drag.x, drag.y)
163 var index = Math.floor(drag.x + listView.realContentX) / (listView.itemWidth + listView.spacing);
164 if (index < 0) {
165 listView.hoveredWorkspaceIndex = -1;
166 listView.hoveredHalf = "";
167 return;
168 }
169
170 var upperLimit = dropItemIndex == -1 ? listView.count : listView.count - 1
171 if (index > upperLimit) index = upperLimit;
172 listView.hoveredWorkspaceIndex = index;
173 var pixelsInTile = (drag.x + listView.realContentX) % (listView.itemWidth + listView.spacing);
174 listView.hoveredHalf = (pixelsInTile / listView.itemWidth) < .5 ? "left" : "right";
175 }
176
177 function progressiveScroll(mouseX) {
178 var progress = Math.max(0, Math.min(1, (mouseX - listView.itemWidth) / (width - listView.leftMargin * 2 - listView.itemWidth * 2)))
179 listView.contentX = listView.originX + (listView.contentWidth - listView.width + listView.leftMargin + listView.rightMargin) * progress - listView.leftMargin
180 }
181
182 displaced: Transition { LomiriNumberAnimation { properties: "x" } }
183
184 delegate: Item {
185 id: workspaceDelegate
186 objectName: "delegate" + index
187 height: parent.height
188 width: listView.itemWidth
189 Behavior on width { LomiriNumberAnimation {} }
190 visible: listView.dropItemIndex !== index
191
192 property int itemX: -listView.realContentX + index * (listView.itemWidth + listView.spacing)
193 property int distanceFromLeft: itemX //- listView.leftMargin
194 property int distanceFromRight: listView.width - listView.leftMargin - listView.rightMargin - itemX - listView.itemWidth
195
196 property int itemAngle: {
197 if (index == 0) {
198 if (distanceFromLeft < 0) {
199 var progress = (distanceFromLeft + listView.foldingAreaWidth) / listView.foldingAreaWidth
200 return MathUtils.linearAnimation(1, -1, 0, listView.maxAngle, Math.max(-1, Math.min(1, progress)));
201 }
202 return 0
203 }
204 if (index == listView.count - 1) {
205 if (distanceFromRight < 0) {
206 var progress = (distanceFromRight + listView.foldingAreaWidth) / listView.foldingAreaWidth
207 return MathUtils.linearAnimation(1, -1, 0, -listView.maxAngle, Math.max(-1, Math.min(1, progress)));
208 }
209 return 0
210 }
211
212 if (distanceFromLeft < listView.foldingAreaWidth) {
213 // itemX : 10gu = p : 100
214 var progress = distanceFromLeft / listView.foldingAreaWidth
215 return MathUtils.linearAnimation(1, -1, 0, listView.maxAngle, Math.max(-1, Math.min(1, progress)));
216 }
217 if (distanceFromRight < listView.foldingAreaWidth) {
218 var progress = distanceFromRight / listView.foldingAreaWidth
219 return MathUtils.linearAnimation(1, -1, 0, -listView.maxAngle, Math.max(-1, Math.min(1, progress)));
220 }
221 return 0
222 }
223
224 property int itemOffset: {
225 if (index == 0) {
226 if (distanceFromLeft < 0) {
227 return -distanceFromLeft
228 }
229 return 0
230 }
231 if (index == listView.count - 1) {
232 if (distanceFromRight < 0) {
233 return distanceFromRight
234 }
235 return 0
236 }
237
238 if (itemX < -listView.foldingAreaWidth) {
239 return -itemX
240 }
241 if (distanceFromLeft < listView.foldingAreaWidth) {
242 return (listView.foldingAreaWidth - distanceFromLeft) / 2
243 }
244
245 if (distanceFromRight < -listView.foldingAreaWidth) {
246 return distanceFromRight
247 }
248
249 if (distanceFromRight < listView.foldingAreaWidth) {
250 return -(listView.foldingAreaWidth - distanceFromRight) / 2
251 }
252
253 return 0
254 }
255
256 z: itemOffset < 0 ? itemOffset : -itemOffset
257 transform: [
258 Rotation {
259 angle: itemAngle
260 axis { x: 0; y: 1; z: 0 }
261 origin { x: itemAngle < 0 ? listView.itemWidth : 0; y: height / 2 }
262 },
263 Translate {
264 x: itemOffset
265 }
266 ]
267
268 WorkspacePreview {
269 id: workspacePreview
270 height: listView.height
271 width: listView.itemWidth - settings.launcherWidth
272 anchors.horizontalCenter: parent.horizontalCenter
273 screen: root.screen
274 background: root.background
275 screenHeight: listView.screenHeight
276 containsDragLeft: listView.hoveredWorkspaceIndex == index && listView.hoveredHalf == "left"
277 containsDragRight: listView.hoveredWorkspaceIndex == index && listView.hoveredHalf == "right"
278 isActive: workspace.isSameAs(root.activeWorkspace)
279 isSelected: index === root.selectedIndex
280 workspace: model.workspace
281 }
282 MouseArea {
283 anchors.fill: parent
284 onClicked: {
285 root.clicked(model.workspace)
286 }
287 onDoubleClicked: {
288 model.workspace.activate();
289 root.closeSpread();
290 }
291 }
292
293 MouseArea {
294 id: closeMouseArea
295 objectName: "closeMouseArea"
296 anchors { left: parent.left; top: parent.top; leftMargin: -height / 2; topMargin: -height / 2 }
297 hoverEnabled: true
298 height: units.gu(4)
299 width: height
300 visible: !root.readOnly && listView.count > 1
301
302 onClicked: {
303 model.workspace.unassign();
304 root.commitScreenSetup();
305 }
306 Image {
307 id: closeImage
308 source: "../graphics/window-close.svg"
309 anchors.fill: closeMouseArea
310 anchors.margins: units.gu(1)
311 sourceSize.width: width
312 sourceSize.height: height
313 readonly property var mousePos: hoverMouseArea.mapToItem(workspaceDelegate, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
314 readonly property bool shown: (hoverMouseArea.containsMouse || parent.containsMouse)
315 && mousePos.y < workspaceDelegate.width / 4
316 && mousePos.y > -units.gu(2)
317 && mousePos.x > -units.gu(2)
318 && mousePos.x < workspaceDelegate.height / 4
319 opacity: shown ? 1 : 0
320 visible: opacity > 0
321 Behavior on opacity { LomiriNumberAnimation { duration: LomiriAnimation.SnapDuration } }
322
323 }
324 }
325 }
326
327 MouseArea {
328 id: hoverMouseArea
329 anchors.fill: parent
330 hoverEnabled: true
331 propagateComposedEvents: true
332 anchors.leftMargin: listView.leftMargin
333 anchors.rightMargin: listView.rightMargin
334 enabled: !root.readOnly
335
336 property int draggedIndex: -1
337
338 property int startX: 0
339 property int startY: 0
340
341 onMouseXChanged: {
342 if (!pressed || dragging) {
343 listView.progressiveScroll(mouseX)
344 }
345 }
346 onMouseYChanged: {
347 if (Math.abs(mouseY - startY) > units.gu(3)) {
348 drag.axis = Drag.XAndYAxis;
349 }
350 }
351
352 onReleased: {
353 var result = fakeDragItem.Drag.drop();
354 // if (result == Qt.IgnoreAction) {
355 // WorkspaceManager.destroyWorkspace(fakeDragItem.workspace);
356 // }
357 root.commitScreenSetup();
358 drag.target = null;
359 }
360
361 property bool dragging: drag.active
362 onDraggingChanged: {
363 if (drag.active) {
364 var ws = listView.model.get(draggedIndex);
365 if (ws) ws.unassign();
366 }
367 }
368
369 onPressed: {
370 startX = mouseX;
371 startY = mouseY;
372 if (listView.model.count < 2) return;
373
374 var coords = mapToItem(listView.contentItem, mouseX, mouseY)
375 draggedIndex = listView.indexAt(coords.x, coords.y)
376 var clickedItem = listView.itemAt(coords.x, coords.y)
377
378 var itemCoords = clickedItem.mapToItem(listView, -listView.leftMargin, 0);
379 fakeDragItem.x = itemCoords.x
380 fakeDragItem.y = itemCoords.y
381 fakeDragItem.workspace = listView.model.get(draggedIndex)
382
383 var mouseCoordsInItem = mapToItem(clickedItem, mouseX, mouseY);
384 fakeDragItem.Drag.hotSpot.x = mouseCoordsInItem.x
385 fakeDragItem.Drag.hotSpot.y = mouseCoordsInItem.y
386
387 drag.axis = Drag.YAxis;
388 drag.target = fakeDragItem;
389 }
390
391 WorkspacePreview {
392 id: fakeDragItem
393 height: listView.height
394 width: listView.itemWidth
395 screen: root.screen
396 background: root.background
397 screenHeight: screen.availableModes[screen.currentModeIndex].size.height
398 visible: Drag.active
399
400 Drag.active: hoverMouseArea.drag.active
401 Drag.keys: ['workspace']
402
403 property bool inDropArea: false
404
405 Rectangle {
406 anchors.fill: parent
407 color: "#33000000"
408 opacity: parent.inDropArea ? 0 : 1
409 Behavior on opacity { LomiriNumberAnimation { } }
410 Rectangle {
411 anchors.centerIn: parent
412 width: units.gu(6)
413 height: units.gu(6)
414 radius: width / 2
415 color: "#aa000000"
416 }
417
418 Icon {
419 height: units.gu(3)
420 width: height
421 anchors.centerIn: parent
422 name: "edit-delete"
423 color: "white"
424 }
425 }
426
427 states: [
428 State {
429 when: fakeDragItem.Drag.active
430 ParentChange { target: fakeDragItem; parent: shell }
431 }
432 ]
433 }
434 }
435 }
436 }
437}