Merge lp:~osomon/unity-2d/dnd-reorder-apps into lp:unity-2d/3.0
- dnd-reorder-apps
- Merge into natty
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 | ||||
Related bugs: |
|
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/
- 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://
- 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.
Olivier Tilloy (osomon) wrote : | # |
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.
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.
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.
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)
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.
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(
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 draggedDesktopF
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.
Ugo Riboni (uriboni) wrote : | # |
24 + onPressAndHold: {
25 + var id = items.get(
26 + if (id != undefined) currentId = id
27 + }
items.get(
Also when you rename currentIndex rename var id in a matching way since you're already there.
Ugo Riboni (uriboni) wrote : | # |
166 +void
167 +LauncherApplic
168 +{
169 + LauncherApplica
170 + LauncherApplica
Can you please add a short comment to explain why you need qMin and qMax ? Took me a while to understand it myself.
192 + firstGconfPrior
Can't we just change the LauncherApplica
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.
Ugo Riboni (uriboni) wrote : | # |
260 + // "move" is not a member of QAbstractListModel, cannot be invoked directly
261 + QMetaObject:
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.
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.
- 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.
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 LauncherApplica
> Can't we just change the LauncherApplica
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
The metaprogramming way is flexible enough that if the method doesn’t exist on the model, the call will simply be ignored.
Olivier Tilloy (osomon) wrote : | # |
New merge request: https:/
Preview Diff
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 |
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.