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