Lomiri
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 
17 import QtQuick 2.12
18 import Lomiri.Components 1.3
19 import WindowManager 1.0
20 import "MathUtils.js" as MathUtils
21 import "../../Components"
22 
23 Item {
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 }