Merge lp:~osomon/unity-2d/dnd-reorder-apps into lp:unity-2d/3.0

Proposed by Olivier Tilloy
Status: Merged
Approved by: Florian Boucault
Approved revision: 417
Merged at revision: 400
Proposed branch: lp:~osomon/unity-2d/dnd-reorder-apps
Merge into: lp:unity-2d/3.0
Diff against target: 814 lines (+432/-214)
7 files modified
launcher/Launcher.qml (+76/-0)
launcher/LauncherItem.qml (+247/-209)
launcher/LauncherList.qml (+16/-1)
launcher/UnityApplications/launcherapplicationslist.cpp (+35/-0)
launcher/UnityApplications/launcherapplicationslist.h (+2/-0)
launcher/UnityApplications/listaggregatormodel.cpp (+50/-4)
launcher/UnityApplications/listaggregatormodel.h (+6/-0)
To merge this branch: bzr merge lp:~osomon/unity-2d/dnd-reorder-apps
Reviewer Review Type Date Requested Status
Ugo Riboni (community) Needs Fixing
Review via email: mp+50738@code.launchpad.net

Commit message

[launcher] Implement the ability to re-order applications via drag’n’drop.

Description of the change

This branch implements the ability to re-order applications in the launcher via drag’n’drop.

Known issues/shortcomings:

 - When dropping an application at the top of the list, or when dragging the topmost application down the list, the list is somehow shifted down by the value of the 'spacing' property. I filed http://bugreports.qt.nokia.com/browse/QTBUG-17622. I spent quite some time looking for a workaround, fresh ideas are welcome.

 - The position of the tiles of the other applications (all of them but the one being dragged) is not animated. This is because the list view positions the items itself, and animations are ignored. A possible workaround is to re-parent the tiles to the mouse area, but that introduces a lot of tricky bugs on which I already spent quite some time, so I left it out for the moment, this can be improved later.

To post a comment you must log in.
Revision history for this message
Olivier Tilloy (osomon) wrote :

Note: a possible workaround for the first point mentioned above is to set the value of 'spacing' to 0, and compensate for this by adding top and bottom margins to the launcher tiles.

Revision history for this message
Ugo Riboni (uriboni) wrote :

When you start dragging a tile, please make sure to check that between the moment the mouse button is pressed and the moment the drag actually starts we didn't move the pointer to another tile. If this happens, just don't start the drag.

In other words, start the drag only if the pointer remains inside the same tile during the 800ms of wait that are hardcoded into Qt for pressAndHold.

Revision history for this message
Ugo Riboni (uriboni) wrote :

When you drag a tile, if it's dragged outside of the margins of the launcher, it disappears.
So I suggest a 2-parts solution to this:
- first don't allow the tile to move horizontally, only vertically, even if the user moves the mouse horizontally.
- second, try using QWidget::grabKey when the drag is started to get all mouse events to your window, including the mouse release event. This way even if the mouse cursor gets outside of the launcher window while dragging and the mouse button is released the tile will be dropped in its new position.
This should allow also the other tiles to re-position themselves when the mouse is outside of the launcher window when dragging.

review: Needs Fixing
Revision history for this message
Ugo Riboni (uriboni) wrote :

When you drag a tile and go over the autoscroll areas, the autoscroll is not triggered.
This makes it impossible to drag a tile all the way to the top/bottom in one single drag'n'drop action if the launcher has more items than they fit in the screen.

I think this happens because when forwarding mouse events from the toplevel MouseArea you don't check if there's one of the autoscroll areas below, and don't forward the events to it.

Revision history for this message
Ugo Riboni (uriboni) wrote :

As you mentioned in the MR description, there's this know issue where the list is shifted down a certain amount of pixels in some conditions.
If this was only a cosmetic problem we could fix it later. However it's also a functional issue since if it's done many times it causes the bottom tile to "slide off" of the list.
The tile become effectively unreachable because the autoscroll doesn't take into account this bogus extra space and therefore you can't scroll to this last tile anymore and interact with it.

A workaround is to make the autoscroll calculations take this extra space into account (since thankfully we know when it's added).

(please keep this fix for last after everything else mentioned here is fixed)

review: Needs Fixing
Revision history for this message
Ugo Riboni (uriboni) wrote :

The mousewheel can't be used for scrolling anymore. This was a quite useful feature, so if it's possible to get it back somehow it would be good.

I understand that since it was implemented by the Flickable and we disable it, this might have to be reimplemented manually.

Please report a bug for this so we can take care of this later on.

review: Needs Fixing
Revision history for this message
Ugo Riboni (uriboni) wrote :

15 + /* id (desktop file path) of the application being dragged */
16 + property string currentId: ""
17 + /* list index of the application being dragged */
18 + property int currentIndex

21 + /* list index of the application underneath the cursor */
22 + property int index: main.indexAt(listCoordinates.x, listCoordinates.y)

I think it's not clear to someone reading the code without going back to these comments what the 3 properties above are for.

currentId should probably be called draggedDesktopFileName or something like that, so that it tells you both it's related to dnd and it's actually a desktop file and not just an opaque id.

currentIndex could probably be renamed draggedTileIndex for the same reason.

And index should probably be tileIndexAtMouse or something like that, but "index" alone is definitely not descriptive enough.

review: Needs Fixing
Revision history for this message
Ugo Riboni (uriboni) wrote :

24 + onPressAndHold: {
25 + var id = items.get(currentIndex = index).desktop_file
26 + if (id != undefined) currentId = id
27 + }

items.get(currentIndex = index) saves you one line of code, but it makes the entire thing much less clear. In general assignements should be on their own separate line for clarity (relying on the fact that the "value" of an assignement is implicitly the assigned operand is clever but rather obscure).

Also when you rename currentIndex rename var id in a matching way since you're already there.

review: Needs Fixing
Revision history for this message
Ugo Riboni (uriboni) wrote :

166 +void
167 +LauncherApplicationsList::move(int from, int to)
168 +{
169 + LauncherApplication* firstApplication = m_applications[qMin(from, to)];
170 + LauncherApplication* secondApplication = m_applications[qMax(from, to)];

Can you please add a short comment to explain why you need qMin and qMax ? Took me a while to understand it myself.

192 + firstGconfPriority.setValue(QVariant(double(firstApplication->priority())));

Can't we just change the LauncherApplication::priorty to be a double so we don't have to do these casts whenever we need to read/store the priorities ?

review: Needs Fixing
Revision history for this message
Ugo Riboni (uriboni) wrote :

208 + Q_INVOKABLE void move(int from, int to);

Making it a slot is probably a better option, as it makes it more reusable and doesn't cost us anything.

Revision history for this message
Ugo Riboni (uriboni) wrote :

260 + // "move" is not a member of QAbstractListModel, cannot be invoked directly
261 + QMetaObject::invokeMethod(model, "move",
262 + Q_ARG(int, from - offset),
263 + Q_ARG(int, to - offset));

I understand what you're doing here, and my deep love of metaprogramming makes me like it ;)

However it kind of breaks the idea that the ListAggregatorModel can be used to aggregate *consistently* anything that's QAbstractListModel. In this case you get a totally different behavior if you're moving items from one underlying model or the other.
One option could be to make it somehow possible to ask the aggregator to tell us which model an item is in, and then do the move operation on that model directly.

Revision history for this message
Ugo Riboni (uriboni) wrote :

314 + /* Move one item from one position to another position.
315 + The item must remain in the same model. */
316 + Q_INVOKABLE void move(int from, int to);
317 +

Same thing about Q_INVOKABLE versus slots as mentioned above.

review: Needs Fixing
lp:~osomon/unity-2d/dnd-reorder-apps updated
409. By Olivier Tilloy

Do not start dragging an application if the mouse cursor has moved too much since the mouse press event.

410. By Olivier Tilloy

Move the tile being dragged along the vertical axis only.

411. By Olivier Tilloy

Drop the tile being dragged when the mouse cursor exits the launcher.

412. By Olivier Tilloy

Fix autoscrolling that had been broken by the use of hoverEnabled to constrain
the tile being dragged to the original position of the mouse press event.

413. By Olivier Tilloy

Work around http://bugreports.qt.nokia.com/browse/QTBUG-17622 by setting the spacing of the ListView to 0
and compensating with some vertical padding in the tiles.

414. By Olivier Tilloy

Encapsulate all the elements that need to be animated into an item called 'looseItem'.

415. By Olivier Tilloy

Fix indentations.

416. By Olivier Tilloy

Ensure the tiles in the shelf are always above those in 'main'.

417. By Olivier Tilloy

Re-enable flicking on the list view.
There are bugs.

Revision history for this message
Olivier Tilloy (osomon) wrote :

I addressed most of the points you raised above, and will shortly submit a new merge request to get those changes in. See my comments on the points I didn’t address or addressed only partially:

> try using QWidget::grabKey when the drag is started to get all
> mouse events to your window […]

As discussed and agreed, this won’t be implemented for now.

> Can you please add a short comment to explain why you need qMin and qMax ?

That looks pretty obvious to me, the pointers to the LauncherApplications are called firstApplication and secondApplication for a reason, an additional comment would just state the obvious.

> Can't we just change the LauncherApplication::priorty to be a double […]

We could, but priorities really are integers, so it’s better to keep our interfaces clean and work around the issues internally, in the implementation. On top of that this code is going to go away very soon, changing the interface could potentially introduce regressions for no real benefit.

> One option could be to make it somehow possible to ask the aggregator
> to tell us which model an item is in, and then do the move operation
> on that model directly.

That is exactly what I’m doing here: modelAtIndex(from) tells us which model an item is in and we invoke the move() method on this model.
The reason I need to do that the metaprogramming-way, as the comment states it, is that "move" is not a member of the QAbstractListModel interface. I could modify the ListAggregatorModel class to store instances of an interface that inherits QAbstractListModel and adds to it a "move" method, but that sounds a bit overkill, don’t you think?
The metaprogramming way is flexible enough that if the method doesn’t exist on the model, the call will simply be ignored.

Revision history for this message
Olivier Tilloy (osomon) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'launcher/Launcher.qml'
2--- launcher/Launcher.qml 2011-02-18 14:02:46 +0000
3+++ launcher/Launcher.qml 2011-02-23 21:55:46 +0000
4@@ -18,6 +18,7 @@
5 anchors.top: parent.top
6 anchors.bottom: shelf.top
7 width: parent.width
8+ z: 1 /* for dnd to remain on top of looseItems */
9
10 paddingTop: 5
11 paddingBottom: 5
12@@ -27,6 +28,79 @@
13 model: ListAggregatorModel {
14 id: items
15 }
16+
17+ // FIXME: dragging the list to flick it exhibits unpleasant visual
18+ // artifacts, and its contentY sometimes remains blocked at a position
19+ // too far off the boundaries of the list.
20+ MouseArea {
21+ /* Handle drag’n’drop to re-order applications. */
22+ id: dnd
23+ anchors.fill: parent
24+
25+ /* id (desktop file path) of the application being dragged */
26+ property string currentId: ""
27+ /* list index of the application being dragged */
28+ property int currentIndex
29+ /* absolute mouse coordinates in the list */
30+ property variant listCoordinates: mapToItem(main.contentItem, mouseX, mouseY)
31+ /* list index of the application underneath the cursor */
32+ property int index: main.indexAt(listCoordinates.x, listCoordinates.y)
33+
34+ onPressed: {
35+ /* index is not valid yet because the mouse area is not
36+ sensitive to hovering (if it were, it would eat hover events
37+ for other mouse areas below, which is not desired). */
38+ var coord = mapToItem(main.contentItem, mouse.x, mouse.y)
39+ currentIndex = main.indexAt(coord.x, coord.y)
40+ }
41+ onPressAndHold: {
42+ if (index != currentIndex) {
43+ /* The item under the cursor changed since the press. */
44+ return
45+ }
46+ parent.interactive = false
47+ var id = items.get(currentIndex).desktop_file
48+ if (id != undefined) currentId = id
49+ }
50+ function drop() {
51+ currentId = ""
52+ parent.interactive = true
53+ }
54+ onReleased: drop()
55+ onExited: drop()
56+ onMousePositionChanged: {
57+ if (currentId != "" && index != -1 && index != currentIndex) {
58+ /* Workaround a bug in QML whereby moving an item down in
59+ the list results in its visual representation being
60+ shifted too far down by one index
61+ (http://bugreports.qt.nokia.com/browse/QTBUG-15841).
62+ Since the bug happens only when moving an item *down*,
63+ and since moving an item one index down is strictly
64+ equivalent to moving the item below one index up, we
65+ achieve the same result by tricking the list model into
66+ thinking that the mirror operation was performed.
67+ Note: this bug will be fixed in Qt 4.7.2, at which point
68+ this workaround can go away. */
69+ if (index > currentIndex) {
70+ items.move(index, currentIndex, 1)
71+ } else {
72+ /* This should be the only code path here, if it wasn’t
73+ for the bug explained and worked around above. */
74+ items.move(currentIndex, index, 1)
75+ }
76+ currentIndex = index
77+ }
78+ }
79+ onClicked: {
80+ /* Forward the click to the launcher item below. */
81+ var point = mapToItem(main.contentItem, mouse.x, mouse.y)
82+ var item = main.contentItem.childAt(point.x, point.y)
83+ /* FIXME: the coordinates of the mouse event forwarded are
84+ incorrect. Luckily, it’s acceptable as they are not used in
85+ the handler anyway. */
86+ if (item && typeof(item.clicked) == "function") item.clicked(mouse)
87+ }
88+ }
89 }
90
91 LauncherList {
92@@ -34,6 +108,8 @@
93 anchors.bottom: parent.bottom;
94 height: tileSize * count + spacing * Math.max(0, count - 1)
95 width: parent.width
96+ /* Ensure the tiles in the shelf are always above those in 'main'. */
97+ itemZ: 1
98
99 model: ListAggregatorModel {
100 id: shelfItems
101
102=== modified file 'launcher/LauncherItem.qml'
103--- launcher/LauncherItem.qml 2011-02-18 14:02:46 +0000
104+++ launcher/LauncherItem.qml 2011-02-23 21:55:46 +0000
105@@ -34,9 +34,14 @@
106 objectName: "launcherItem"
107
108 anchors.horizontalCenter: parent.horizontalCenter
109- height: tileSize
110+ /* Manually add some padding to compensate for the spacing
111+ of the ListView being set to 0 to work around
112+ http://bugreports.qt.nokia.com/browse/QTBUG-17622. */
113+ property int padding: 5
114+ height: tileSize + padding
115
116 property int tileSize
117+ property string desktopFile: ""
118 property alias icon: icon.source
119 property bool running: false
120 property bool active: false
121@@ -70,230 +75,263 @@
122 signal entered
123 signal exited
124
125- /* This is the arrow shown at the right of the tile when the application is
126- the active one */
127- Image {
128- anchors.right: parent.right
129- anchors.verticalCenter: parent.verticalCenter
130- source: "image://blended/%1color=%2alpha=%3"
131- .arg(engineBaseUrl + "artwork/launcher_arrow_rtl.png")
132- .arg("lightgrey")
133- .arg(1.0)
134-
135- /* This extra shift is necessary (as is for the pips below)
136- since we are vertically centering in a parent with even height, so
137- there's one pixel offset that need to be assigned arbitrarily.
138- Unity chose to add it, QML to subtract it. So we adjust for that. */
139- transform: Translate { y: 1 }
140-
141- visible: active
142- }
143-
144- /* This is the area on the left of the tile where the pips/arrow end up.
145-
146- I'd rather use a Column here, but the pip images have an halo
147- around them, so they are pretty tall and would mess up the column.
148- As a workaround I center all of them, then shift up or down
149- depending on the index. */
150- Repeater {
151- model: item.pips
152- delegate: Image {
153- /* FIXME: It seems that when the image is created (or re-used) by the Repeater
154- for a moment it doesn't have any parent, and therefore warnings are
155- printed for the following two anchor assignements. This fixes the
156- problem, but I'm not sure if it should happen in the first place. */
157- anchors.left: (parent) ? parent.left : undefined
158- anchors.verticalCenter: (parent) ? parent.verticalCenter : undefined
159-
160- source: "image://blended/%1color=%2alpha=%3"
161- .arg(pipSource).arg("lightgrey").arg(1.0)
162-
163- transform: Translate { y: getPipOffset(index) + 1 }
164- }
165- }
166-
167- /* This is the for centering the actual tile in the launcher */
168 Item {
169- id: tile
170- anchors.centerIn: parent
171- width: item.tileSize
172+ /* The actual item, reparented so its y coordinate can be animated. */
173+ id: looseItem
174+ parent: launcher
175+ width: item.width
176 height: item.height
177+ x: item.x
178+ /* item.parent is the delegate, and its parent is the LauncherList */
179+ y: item.parent.parent.y - item.parent.parent.contentY + item.y
180+ z: item.parent.parent.itemZ
181
182- /* This is the image providing the background image. The
183- color blended with this image is obtained from the color of the icon when it's
184- loaded.
185- While the application is launching, this will fade out and in. */
186+ /* This is the arrow shown at the right of the tile when the application is
187+ the active one */
188 Image {
189- id: tileBackground
190- property color color: defaultBackgroundColor
191- anchors.fill: parent
192-
193- SequentialAnimation on opacity {
194- NumberAnimation { to: 0.0; duration: 1000; easing.type: Easing.InOutQuad }
195- NumberAnimation { to: 1.0; duration: 1000; easing.type: Easing.InOutQuad }
196-
197- loops: Animation.Infinite
198- alwaysRunToEnd: true
199- running: launching
200- }
201-
202- sourceSize.width: item.tileSize
203- sourceSize.height: item.tileSize
204+ anchors.right: parent.right
205+ anchors.verticalCenter: parent.verticalCenter
206 source: "image://blended/%1color=%2alpha=%3"
207- .arg(engineBaseUrl + "artwork/round_corner_54x54.png")
208- .arg(color.toString().replace("#", ""))
209- .arg(1.0)
210+ .arg(engineBaseUrl + "artwork/launcher_arrow_rtl.png")
211+ .arg("lightgrey")
212+ .arg(1.0)
213+
214+ /* This extra shift is necessary (as is for the pips below)
215+ since we are vertically centering in a parent with even height, so
216+ there's one pixel offset that need to be assigned arbitrarily.
217+ Unity chose to add it, QML to subtract it. So we adjust for that. */
218+ transform: Translate { y: 1 }
219+
220+ visible: active && (looseItem.state != "beingDragged")
221 }
222
223- /* This image appears only while launching, and pulses in and out in counterpoint
224- to the background, so that the outline of the tile is always visible. */
225- Image {
226- id: tileOutline
227- anchors.fill: parent
228-
229- sourceSize.width: item.tileSize
230- sourceSize.height: item.tileSize
231- source: "artwork/round_outline_54x54.png"
232-
233- opacity: 0
234-
235- SequentialAnimation on opacity {
236- NumberAnimation { to: 1.0; duration: 1000; easing.type: Easing.InOutQuad }
237- NumberAnimation { to: 0.0; duration: 1000; easing.type: Easing.InOutQuad }
238-
239- loops: Animation.Infinite
240- alwaysRunToEnd: true
241- running: launching
242+ /* This is the area on the left of the tile where the pips/arrow end up.
243+
244+ I'd rather use a Column here, but the pip images have an halo
245+ around them, so they are pretty tall and would mess up the column.
246+ As a workaround I center all of them, then shift up or down
247+ depending on the index. */
248+ Repeater {
249+ model: item.pips
250+ delegate: Image {
251+ /* FIXME: It seems that when the image is created (or re-used) by the Repeater
252+ for a moment it doesn't have any parent, and therefore warnings are
253+ printed for the following two anchor assignements. This fixes the
254+ problem, but I'm not sure if it should happen in the first place. */
255+ anchors.left: (parent) ? parent.left : undefined
256+ anchors.verticalCenter: (parent) ? parent.verticalCenter : undefined
257+
258+ source: "image://blended/%1color=%2alpha=%3"
259+ .arg(pipSource).arg("lightgrey").arg(1.0)
260+
261+ transform: Translate { y: getPipOffset(index) + 1 }
262+
263+ visible: looseItem.state != "beingDragged"
264 }
265 }
266
267- /* This is just the main icon of the tile */
268- Image {
269- id: icon
270+ /* This is the for centering the actual tile in the launcher */
271+ Item {
272+ id: tile
273+ width: item.tileSize
274+ height: item.tileSize
275 anchors.centerIn: parent
276
277- sourceSize.width: 48
278- sourceSize.height: 48
279-
280- /* Whenever one of the parameters used in calculating the background color of
281- the icon changes, recalculate its value */
282- onWidthChanged: updateColors()
283- onHeightChanged: updateColors()
284- onSourceChanged: updateColors()
285-
286- function updateColors() {
287- if (!item.backgroundFromIcon) return;
288-
289- var colors = launcherView.getColorsFromIcon(icon.source, icon.sourceSize)
290- if (colors && colors.length > 0) tileBackground.color = colors[0]
291- }
292- }
293-
294- /* This just adds some shiny effect to the tile */
295- Image {
296- id: tileShine
297- anchors.fill: parent
298-
299- source: "artwork/round_shine_54x54.png"
300- sourceSize.width: item.tileSize
301- sourceSize.height: item.tileSize
302- }
303-
304- Rectangle {
305- id: counter
306- height: 16 - border.width
307- width: 32
308- // Using anchors the item will be 1 pixel off with respect to Unity
309- y: 1
310- x: 1
311- radius: height / 2 - 1
312- border.width: 2
313- border.color: "white"
314- color: "#595959"
315- visible: launcherItem.counterVisible
316-
317- Text {
318+ /* This is the image providing the background image. The
319+ color blended with this image is obtained from the color of the icon when it's
320+ loaded.
321+ While the application is launching, this will fade out and in. */
322+ Image {
323+ id: tileBackground
324+ property color color: defaultBackgroundColor
325+ anchors.fill: parent
326+
327+ SequentialAnimation on opacity {
328+ NumberAnimation { to: 0.0; duration: 1000; easing.type: Easing.InOutQuad }
329+ NumberAnimation { to: 1.0; duration: 1000; easing.type: Easing.InOutQuad }
330+
331+ loops: Animation.Infinite
332+ alwaysRunToEnd: true
333+ running: launching
334+ }
335+
336+ sourceSize.width: item.tileSize
337+ sourceSize.height: item.tileSize
338+ source: "image://blended/%1color=%2alpha=%3"
339+ .arg(engineBaseUrl + "artwork/round_corner_54x54.png")
340+ .arg(color.toString().replace("#", ""))
341+ .arg(1.0)
342+ }
343+
344+ /* This image appears only while launching, and pulses in and out in counterpoint
345+ to the background, so that the outline of the tile is always visible. */
346+ Image {
347+ id: tileOutline
348+ anchors.fill: parent
349+
350+ sourceSize.width: item.tileSize
351+ sourceSize.height: item.tileSize
352+ source: "artwork/round_outline_54x54.png"
353+
354+ opacity: 0
355+
356+ SequentialAnimation on opacity {
357+ NumberAnimation { to: 1.0; duration: 1000; easing.type: Easing.InOutQuad }
358+ NumberAnimation { to: 0.0; duration: 1000; easing.type: Easing.InOutQuad }
359+
360+ loops: Animation.Infinite
361+ alwaysRunToEnd: true
362+ running: launching
363+ }
364+ }
365+
366+ /* This is just the main icon of the tile */
367+ Image {
368+ id: icon
369 anchors.centerIn: parent
370- font.pixelSize: parent.height - 3
371- width: parent.width - 5
372- elide: Text.ElideRight
373- horizontalAlignment: Text.AlignHCenter
374- color: "white"
375- text: launcherItem.counter
376- }
377- }
378-
379- Image {
380- id: progressBar
381- source: "artwork/progress_bar_trough.png"
382- anchors.verticalCenter: parent.verticalCenter
383- anchors.left: parent.left
384- width: tile.width
385- state: launcherItem.progressBarVisible ? "" : "hidden"
386-
387- Image {
388- id: progressFill
389- source: "artwork/progress_bar_fill.png"
390+
391+ sourceSize.width: 48
392+ sourceSize.height: 48
393+
394+ /* Whenever one of the parameters used in calculating the background color of
395+ the icon changes, recalculate its value */
396+ onWidthChanged: updateColors()
397+ onHeightChanged: updateColors()
398+ onSourceChanged: updateColors()
399+
400+ function updateColors() {
401+ if (!item.backgroundFromIcon) return;
402+
403+ var colors = launcherView.getColorsFromIcon(icon.source, icon.sourceSize)
404+ if (colors && colors.length > 0) tileBackground.color = colors[0]
405+ }
406+ }
407+
408+ /* This just adds some shiny effect to the tile */
409+ Image {
410+ id: tileShine
411+ anchors.fill: parent
412+
413+ source: "artwork/round_shine_54x54.png"
414+ sourceSize.width: item.tileSize
415+ sourceSize.height: item.tileSize
416+ }
417+
418+ Rectangle {
419+ id: counter
420+ height: 16 - border.width
421+ width: 32
422+ // Using anchors the item will be 1 pixel off with respect to Unity
423+ y: 1
424+ x: 1
425+ radius: height / 2 - 1
426+ border.width: 2
427+ border.color: "white"
428+ color: "#595959"
429+ visible: launcherItem.counterVisible
430+
431+ Text {
432+ anchors.centerIn: parent
433+ font.pixelSize: parent.height - 3
434+ width: parent.width - 5
435+ elide: Text.ElideRight
436+ horizontalAlignment: Text.AlignHCenter
437+ color: "white"
438+ text: launcherItem.counter
439+ }
440+ }
441+
442+ Image {
443+ id: progressBar
444+ source: "artwork/progress_bar_trough.png"
445 anchors.verticalCenter: parent.verticalCenter
446- x: 6
447- width: sourceSize.width * launcherItem.progress
448+ anchors.left: parent.left
449+ width: tile.width
450+ state: launcherItem.progressBarVisible ? "" : "hidden"
451+
452+ Image {
453+ id: progressFill
454+ source: "artwork/progress_bar_fill.png"
455+ anchors.verticalCenter: parent.verticalCenter
456+ x: 6
457+ width: sourceSize.width * launcherItem.progress
458+
459+ Behavior on width {
460+ NumberAnimation { duration: 200; easing.type: Easing.InOutSine }
461+ }
462+ }
463
464 Behavior on width {
465- NumberAnimation { duration: 200; easing.type: Easing.InOutSine }
466- }
467- }
468-
469- Behavior on width {
470- NumberAnimation { duration: 200; easing.type: Easing.InOutSine }
471- }
472-
473- states: State {
474- name: "hidden"
475- PropertyChanges {
476- target: progressBar
477- width: 0
478- }
479- // This, combined with anchors.left: parent.left in the default state
480- // makes the bar seem to come in from the left and go away at the right
481- AnchorChanges {
482- target: progressBar
483- anchors.left: undefined
484- anchors.right: tile.right
485- }
486- }
487- }
488-
489- Image {
490- id: emblemIcon
491- anchors.left: parent.left
492- anchors.top: parent.top
493- visible: launcherItem.emblemVisible && !counter.visible
494- }
495-
496-
497- /* The entire tile will "shake" when the window is marked as "urgent", to attract
498- the user's attention */
499- SequentialAnimation {
500- running: urgent
501- alwaysRunToEnd: true
502-
503+ NumberAnimation { duration: 200; easing.type: Easing.InOutSine }
504+ }
505+
506+ states: State {
507+ name: "hidden"
508+ PropertyChanges {
509+ target: progressBar
510+ width: 0
511+ }
512+ // This, combined with anchors.left: parent.left in the default state
513+ // makes the bar seem to come in from the left and go away at the right
514+ AnchorChanges {
515+ target: progressBar
516+ anchors.left: undefined
517+ anchors.right: tile.right
518+ }
519+ }
520+ }
521+
522+ Image {
523+ id: emblemIcon
524+ anchors.left: parent.left
525+ anchors.top: parent.top
526+ visible: launcherItem.emblemVisible && !counter.visible
527+ }
528+
529+
530+ /* The entire tile will "shake" when the window is marked as "urgent", to attract
531+ the user's attention */
532 SequentialAnimation {
533- loops: 30
534- NumberAnimation { target: tile; property: "rotation"; to: 15; duration: 150 }
535- NumberAnimation { target: tile; property: "rotation"; to: -15; duration: 150 }
536- }
537- NumberAnimation { target: tile; property: "rotation"; to: 0; duration: 75 }
538- }
539- }
540-
541- MouseArea {
542- id: mouse
543- anchors.fill: parent
544-
545- hoverEnabled: true
546- acceptedButtons: Qt.LeftButton | Qt.RightButton
547- onClicked: item.clicked(mouse)
548- onEntered: item.entered()
549- onExited: item.exited()
550+ running: urgent
551+ alwaysRunToEnd: true
552+
553+ SequentialAnimation {
554+ loops: 30
555+ NumberAnimation { target: tile; property: "rotation"; to: 15; duration: 150 }
556+ NumberAnimation { target: tile; property: "rotation"; to: -15; duration: 150 }
557+ }
558+ NumberAnimation { target: tile; property: "rotation"; to: 0; duration: 75 }
559+ }
560+
561+ MouseArea {
562+ id: mouse
563+ anchors.fill: parent
564+
565+ hoverEnabled: true
566+ acceptedButtons: Qt.LeftButton | Qt.RightButton
567+ onClicked: item.clicked(mouse)
568+ onEntered: item.entered()
569+ onExited: item.exited()
570+ }
571+ }
572+
573+ states: State {
574+ name: "beingDragged"
575+ when: (dnd.currentId != "") && (dnd.currentId == item.desktopFile)
576+ PropertyChanges {
577+ target: looseItem
578+ /* item.parent is the delegate, and its parent is the LauncherList */
579+ y: dnd.listCoordinates.y - item.parent.parent.contentY - tile.height / 2
580+ /* When dragging an item, stack it on top of all its siblings */
581+ z: 1
582+ }
583+ }
584+ Behavior on y {
585+ enabled: (looseItem.state != "beingDragged") && !item.parent.parent.moving && !item.parent.parent.autoScrolling
586+ NumberAnimation {
587+ duration: 400
588+ easing.type: Easing.OutBack
589+ }
590+ }
591 }
592 }
593
594=== modified file 'launcher/LauncherList.qml'
595--- launcher/LauncherList.qml 2011-02-21 10:23:45 +0000
596+++ launcher/LauncherList.qml 2011-02-23 21:55:46 +0000
597@@ -4,18 +4,27 @@
598
599 AutoScrollingListView {
600 id: list
601- spacing: 5
602+
603+ /* The spacing is explicitly set to 0 and compensated for
604+ by adding some padding to the tiles because of
605+ http://bugreports.qt.nokia.com/browse/QTBUG-17622. */
606+ spacing: 0
607+
608 property int tileSize: 54
609
610 /* Keep a reference to the currently visible contextual menu */
611 property variant visibleMenu
612
613+ /* A hint for items to determine the value of their 'z' property */
614+ property real itemZ: 0
615+
616 delegate: LauncherItem {
617 id: launcherItem
618
619 width: list.width
620 tileSize: list.tileSize
621
622+ desktopFile: item.desktop_file ? item.desktop_file : ""
623 icon: "image://icons/" + item.icon
624 running: item.running
625 active: item.active
626@@ -87,6 +96,12 @@
627 onAutoScrollingChanged: if (list.autoScrolling) item.menu.hide()
628 }
629
630+ Connections {
631+ target: dnd
632+ /* Hide the tooltip/menu when dragging an application. */
633+ onCurrentIdChanged: if (dnd.currentId != "") item.menu.hide()
634+ }
635+
636 function setIconGeometry() {
637 if (running) {
638 item.setIconGeometry(x + panel.x, y + panel.y, width, height)
639
640=== modified file 'launcher/UnityApplications/launcherapplicationslist.cpp'
641--- launcher/UnityApplications/launcherapplicationslist.cpp 2011-02-21 10:29:10 +0000
642+++ launcher/UnityApplications/launcherapplicationslist.cpp 2011-02-23 21:55:46 +0000
643@@ -356,3 +356,38 @@
644 return QVariant::fromValue(m_applications.at(index.row()));
645 }
646
647+
648+void
649+LauncherApplicationsList::move(int from, int to)
650+{
651+ LauncherApplication* firstApplication = m_applications[qMin(from, to)];
652+ LauncherApplication* secondApplication = m_applications[qMax(from, to)];
653+
654+ QModelIndex parent;
655+ /* When moving an item down, the destination index needs to be incremented
656+ by one, as explained in the documentation:
657+ http://doc.qt.nokia.com/qabstractitemmodel.html#beginMoveRows */
658+ beginMoveRows(parent, from, from, parent, to + (to > from ? 1 : 0));
659+ m_applications.move(from, to);
660+ endMoveRows();
661+
662+ if (firstApplication->sticky() && secondApplication->sticky()) {
663+ /* Update priorities only if both applications are favorites. */
664+ int firstPriority = firstApplication->priority();
665+ int secondPriority = secondApplication->priority();
666+ /* Since we are guaranteed that this sort of move only happens between
667+ two consecutive applications, all we need to do is swap their
668+ priorities. */
669+ firstApplication->setPriority(secondPriority);
670+ secondApplication->setPriority(firstPriority);
671+
672+ GConfItemQmlWrapper firstGconfPriority;
673+ firstGconfPriority.setKey(FAVORITES_KEY + favoriteFromDesktopFilePath(firstApplication->desktop_file()) + "/priority");
674+ firstGconfPriority.setValue(QVariant(double(firstApplication->priority())));
675+
676+ GConfItemQmlWrapper secondGconfPriority;
677+ secondGconfPriority.setKey(FAVORITES_KEY + favoriteFromDesktopFilePath(secondApplication->desktop_file()) + "/priority");
678+ secondGconfPriority.setValue(QVariant(double(secondApplication->priority())));
679+ }
680+}
681+
682
683=== modified file 'launcher/UnityApplications/launcherapplicationslist.h'
684--- launcher/UnityApplications/launcherapplicationslist.h 2011-02-21 10:29:10 +0000
685+++ launcher/UnityApplications/launcherapplicationslist.h 2011-02-23 21:55:46 +0000
686@@ -41,6 +41,8 @@
687 QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
688 int rowCount(const QModelIndex & parent = QModelIndex()) const;
689
690+ Q_INVOKABLE void move(int from, int to);
691+
692 Q_INVOKABLE void insertFavoriteApplication(QString desktop_file);
693 Q_INVOKABLE void insertWebFavorite(const QUrl& url);
694
695
696=== modified file 'launcher/UnityApplications/listaggregatormodel.cpp'
697--- launcher/UnityApplications/listaggregatormodel.cpp 2011-01-06 23:06:59 +0000
698+++ launcher/UnityApplications/listaggregatormodel.cpp 2011-02-23 21:55:46 +0000
699@@ -19,6 +19,8 @@
700
701 #include "listaggregatormodel.h"
702
703+#include <QDebug>
704+
705 ListAggregatorModel::ListAggregatorModel(QObject* parent) :
706 QAbstractListModel(parent)
707 {
708@@ -60,6 +62,8 @@
709 SLOT(onRowsInserted(const QModelIndex&, int, int)));
710 connect(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
711 SLOT(onRowsRemoved(const QModelIndex&, int, int)));
712+ connect(model, SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)),
713+ SLOT(onRowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)));
714 }
715
716 void
717@@ -77,10 +81,27 @@
718 {
719 endRemoveRows();
720 }
721- QObject::disconnect(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)),
722- this, SLOT(onRowsInserted(const QModelIndex&, int, int)));
723- QObject::disconnect(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
724- this, SLOT(onRowsRemoved(const QModelIndex&, int, int)));
725+ disconnect(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)),
726+ this, SLOT(onRowsInserted(const QModelIndex&, int, int)));
727+ disconnect(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
728+ this, SLOT(onRowsRemoved(const QModelIndex&, int, int)));
729+ disconnect(model, SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)),
730+ this, SLOT(onRowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)));
731+}
732+
733+void
734+ListAggregatorModel::move(int from, int to)
735+{
736+ QAbstractListModel* model = modelAtIndex(from);
737+ if (modelAtIndex(to) != model) {
738+ qWarning() << "cannot move an item from one model to another";
739+ return;
740+ }
741+ int offset = computeOffset(model);
742+ // "move" is not a member of QAbstractListModel, cannot be invoked directly
743+ QMetaObject::invokeMethod(model, "move",
744+ Q_ARG(int, from - offset),
745+ Q_ARG(int, to - offset));
746 }
747
748 int
749@@ -95,6 +116,20 @@
750 return offset;
751 }
752
753+QAbstractListModel*
754+ListAggregatorModel::modelAtIndex(int index) const
755+{
756+ int offset = index;
757+ Q_FOREACH(QAbstractListModel* model, m_models) {
758+ int size = model->rowCount();
759+ if (offset < size) {
760+ return model;
761+ }
762+ offset -= size;
763+ }
764+ return NULL;
765+}
766+
767 void
768 ListAggregatorModel::onRowsInserted(const QModelIndex& parent, int first, int last)
769 {
770@@ -113,6 +148,17 @@
771 endRemoveRows();
772 }
773
774+void
775+ListAggregatorModel::onRowsMoved(const QModelIndex& sourceParent, int sourceStart, int sourceEnd,
776+ const QModelIndex& destinationParent, int destinationRow)
777+{
778+ QAbstractListModel* model = static_cast<QAbstractListModel*>(sender());
779+ int offset = computeOffset(model);
780+ beginMoveRows(sourceParent, sourceStart + offset, sourceEnd + offset,
781+ destinationParent, destinationRow + offset);
782+ endMoveRows();
783+}
784+
785 int
786 ListAggregatorModel::rowCount(const QModelIndex& parent) const
787 {
788
789=== modified file 'launcher/UnityApplications/listaggregatormodel.h'
790--- launcher/UnityApplications/listaggregatormodel.h 2011-01-06 23:06:59 +0000
791+++ launcher/UnityApplications/listaggregatormodel.h 2011-02-23 21:55:46 +0000
792@@ -41,6 +41,10 @@
793 to a QAbstractListModel */
794 Q_INVOKABLE void appendModel(const QVariant& model);
795
796+ /* Move one item from one position to another position.
797+ The item must remain in the same model. */
798+ Q_INVOKABLE void move(int from, int to);
799+
800 protected:
801 QList<QAbstractListModel*> m_models;
802
803@@ -50,9 +54,11 @@
804 private slots:
805 void onRowsInserted(const QModelIndex& parent, int first, int last);
806 void onRowsRemoved(const QModelIndex& parent, int first, int last);
807+ void onRowsMoved(const QModelIndex&, int, int, const QModelIndex&, int);
808
809 private:
810 int computeOffset(QAbstractListModel* model) const;
811+ QAbstractListModel* modelAtIndex(int index) const;
812 };
813
814 #endif // LISTAGGREGATORMODEL_H

Subscribers

People subscribed via source and target branches