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