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