on xenial help sign in
first support for quicklists
bb15401Michael Zanetti
Launcher/Launcher.qml+2−2
@@ -73,7 +73,7 @@ Item {
73 id: dismissTimer
73 id: dismissTimer
74 interval: 5000
74 interval: 5000
75 onTriggered: {
75 onTriggered: {
76− if (!panel.moving) {
76+ if (!panel.preventHiding) {
77 root.state = ""
77 root.state = ""
78 } else {
78 } else {
79 dismissTimer.restart()
79 dismissTimer.restart()
@@ -162,7 +162,7 @@ Item {
162 root.dashItemSelected(index)
162 root.dashItemSelected(index)
163 }
163 }
164
164
165− onMovingChanged: {
165+ onPreventHidingChanged: {
166 if (dismissTimer.running) {
166 if (dismissTimer.running) {
167 dismissTimer.restart();
167 dismissTimer.restart();
168 }
168 }
Launcher/LauncherPanel.qml+55−1
@@ -19,6 +19,7 @@ import Ubuntu.Components 0.1
19import Ubuntu.Components.ListItems 0.1 as ListItems
19import Ubuntu.Components.ListItems 0.1 as ListItems
20import Unity 0.1
20import Unity 0.1
21import Unity.Launcher 0.1
21import Unity.Launcher 0.1
22+import Ubuntu.Components.Popups 0.1
22import "../Components/ListItems"
23import "../Components/ListItems"
23
24
24Item {
25Item {
@@ -29,7 +30,8 @@ Item {
29 property var model
30 property var model
30 property bool inverted: true
31 property bool inverted: true
31 property bool dragging: false
32 property bool dragging: false
32− property bool moving: launcherListView.moving || launcherListView.flicking || dndArea.draggedIndex >= 0
33+ property bool moving: launcherListView.moving || launcherListView.flicking
34+ property bool preventHiding: moving || dndArea.draggedIndex >= 0 || dndArea.quickListPopover !== null || dndArea.pressed
33 property int highlightIndex: -1
35 property int highlightIndex: -1
34
36
35 signal applicationSelected(string desktopFile)
37 signal applicationSelected(string desktopFile)
@@ -219,6 +221,7 @@ Item {
219
221
220 MouseArea {
222 MouseArea {
221 id: dndArea
223 id: dndArea
224+ objectName: "dndArea"
222 anchors {
225 anchors {
223 fill: parent
226 fill: parent
224 topMargin: launcherListView.topMargin
227 topMargin: launcherListView.topMargin
@@ -234,6 +237,7 @@ Item {
234 property bool postDragging: false
237 property bool postDragging: false
235 property int startX
238 property int startX
236 property int startY
239 property int startY
240+ property var quickListPopover: null
237
241
238 onPressed: {
242 onPressed: {
239 selectedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
243 selectedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
@@ -295,6 +299,13 @@ Item {
295 onPressAndHold: {
299 onPressAndHold: {
296 draggedIndex = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
300 draggedIndex = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
297
301
302+ // Opening QuickList
303+ var quickListModel = launcherListView.model.get(draggedIndex).quickList
304+ var quickListAppId = launcherListView.model.get(draggedIndex).appId
305+
306+ quickListPopover = PopupUtils.open(popoverComponent, selectedItem,
307+ {model: quickListModel, appId: quickListAppId})
308+
298 launcherListView.interactive = false
309 launcherListView.interactive = false
299
310
300 var yOffset = draggedIndex > 0 ? (mouseY + launcherListView.realContentY) % (draggedIndex * launcherListView.realItemHeight) : mouseY + launcherListView.realContentY
311 var yOffset = draggedIndex > 0 ? (mouseY + launcherListView.realContentY) % (draggedIndex * launcherListView.realItemHeight) : mouseY + launcherListView.realContentY
@@ -314,6 +325,7 @@ Item {
314 var distance = Math.max(Math.abs(mouseX - startX), Math.abs(mouseY - startY))
325 var distance = Math.max(Math.abs(mouseX - startX), Math.abs(mouseY - startY))
315 if (!preDragging && distance > units.gu(1.5)) {
326 if (!preDragging && distance > units.gu(1.5)) {
316 preDragging = true;
327 preDragging = true;
328+ PopupUtils.close(quickListPopover)
317 }
329 }
318 if (distance > launcherListView.itemHeight) {
330 if (distance > launcherListView.itemHeight) {
319 selectedItem.dragging = true
331 selectedItem.dragging = true
@@ -419,4 +431,46 @@ Item {
419 }
431 }
420 }
432 }
421 }
433 }
434+
435+ Component {
436+ id: popoverComponent
437+
438+ Popover {
439+ id: popover
440+ property var model
441+ property string appId
442+ contentWidth: quickListColumn.width
443+
444+ // FIXME: There's a bug in the Popover positioning that it covers the item in case it is rotated.
445+ // https://bugs.launchpad.net/ubuntu-ui-toolkit/+bug/1204470
446+ // For now, let's move the Popover around with callerMargin.
447+ // Remove popupMargin once the bug is fixed.
448+ property int popupMargin: root.inverted ? launcherListView.itemHeight : 0;
449+
450+ callerMargin: units.gu(1) + popupMargin
451+
452+ Column {
453+ id: quickListColumn
454+ width: units.gu(30)
455+ height: childrenRect.height
456+
457+ Repeater {
458+ model: popover.model
459+
460+ ListItems.Standard {
461+ objectName: "quickListEntry" + index
462+ text: (model.clickable ? "" : "<b>") + model.label + (model.clickable ? "" : "</b>")
463+ highlightWhenPressed: model.clickable
464+ onClicked: {
465+ if (!model.clickable) {
466+ return;
467+ }
468+ LauncherModel.quickListActionInvoked(appId, index);
469+ PopupUtils.close(popover);
470+ }
471+ }
472+ }
473+ }
474+ }
475+ }
422}
476}
plugins/Unity/Launcher/common/quicklistentry.cpp+5
@@ -52,3 +52,8 @@ void QuickListEntry::setIcon(const QString &icon)
52{
52{
53 m_icon = icon;
53 m_icon = icon;
54}
54}
55+
56+bool QuickListEntry::hasAction() const
57+{
58+ return !m_actionId.isEmpty();
59+}
plugins/Unity/Launcher/common/quicklistentry.h+2
@@ -35,6 +35,8 @@ public:
35 QString icon() const;
35 QString icon() const;
36 void setIcon(const QString &icon);
36 void setIcon(const QString &icon);
37
37
38+ bool hasAction() const;
39+
38private:
40private:
39 QString m_actionId;
41 QString m_actionId;
40 QString m_text;
42 QString m_text;
plugins/Unity/Launcher/launcheritem.cpp+3
@@ -33,6 +33,9 @@ LauncherItem::LauncherItem(const QString &appId, const QString &desktopFile, con
33 m_count(0),
33 m_count(0),
34 m_quickList(new QuickListModel(this))
34 m_quickList(new QuickListModel(this))
35{
35{
36+ QuickListEntry nameAction;
37+ nameAction.setText(m_name);
38+ m_quickList->appendAction(nameAction);
36 QuickListEntry pinningAction;
39 QuickListEntry pinningAction;
37 pinningAction.setActionId("pin_item");
40 pinningAction.setActionId("pin_item");
38 pinningAction.setText("Pin to Launcher");
41 pinningAction.setText("Pin to Launcher");
plugins/Unity/Launcher/launchermodel.cpp+3
@@ -163,6 +163,9 @@ void LauncherModel::quickListActionInvoked(const QString &appId, int actionIndex
163 QString actionId = model->get(actionIndex).actionId();
163 QString actionId = model->get(actionIndex).actionId();
164
164
165 // Check if this is one of the launcher actions we handle ourselves
165 // Check if this is one of the launcher actions we handle ourselves
166+ if (actionId == "no_action") {
167+ return;
168+ }
166 if (actionId == "pin_item") {
169 if (actionId == "pin_item") {
167 if (item->pinned()) {
170 if (item->pinned()) {
168 requestRemove(appId);
171 requestRemove(appId);
plugins/Unity/Launcher/quicklistmodel.cpp+2
@@ -66,6 +66,8 @@ QVariant QuickListModel::data(const QModelIndex &index, int role) const
66 return m_list.at(index.row()).text();
66 return m_list.at(index.row()).text();
67 case RoleIcon:
67 case RoleIcon:
68 return m_list.at(index.row()).icon();
68 return m_list.at(index.row()).icon();
69+ case RoleClickable:
70+ return m_list.at(index.row()).hasAction();
69 }
71 }
70 return QVariant();
72 return QVariant();
71}
73}
tests/mocks/Unity/Launcher/MockLauncherItem.cpp−1
@@ -35,7 +35,6 @@ MockLauncherItem::MockLauncherItem(const QString &appId, const QString& desktopF
35 m_count(8),
35 m_count(8),
36 m_quickList(new MockQuickListModel(this))
36 m_quickList(new MockQuickListModel(this))
37{
37{
38−
39}
38}
40
39
41QString MockLauncherItem::appId() const
40QString MockLauncherItem::appId() const
tests/mocks/Unity/Launcher/MockLauncherModel.cpp+1−3
@@ -135,9 +135,7 @@ void MockLauncherModel::requestRemove(const QString &appId)
135
135
136void MockLauncherModel::quickListActionInvoked(const QString &appId, int actionIndex)
136void MockLauncherModel::quickListActionInvoked(const QString &appId, int actionIndex)
137{
137{
138− Q_UNUSED(appId)
138+ Q_EMIT quickListTriggered(appId, actionIndex);
139− Q_UNUSED(actionIndex)
140− // Nothing to mock yet...
141}
139}
142
140
143int MockLauncherModel::findApp(const QString &appId)
141int MockLauncherModel::findApp(const QString &appId)
tests/mocks/Unity/Launcher/MockLauncherModel.h+3
@@ -44,6 +44,9 @@ public:
44 Q_INVOKABLE void requestRemove(const QString &appId);
44 Q_INVOKABLE void requestRemove(const QString &appId);
45 Q_INVOKABLE void quickListActionInvoked(const QString &appId, int actionIndex);
45 Q_INVOKABLE void quickListActionInvoked(const QString &appId, int actionIndex);
46
46
47+Q_SIGNALS:
48+ void quickListTriggered(const QString &appId, int index);
49+
47private:
50private:
48 int findApp(const QString &appId);
51 int findApp(const QString &appId);
49
52
tests/mocks/Unity/Launcher/MockQuickListModel.cpp+2
@@ -35,6 +35,8 @@ QVariant MockQuickListModel::data(const QModelIndex &index, int role) const
35 return QLatin1String("test menu entry ") + QString::number(index.row());
35 return QLatin1String("test menu entry ") + QString::number(index.row());
36 case RoleIcon:
36 case RoleIcon:
37 return QLatin1String("copy.png");
37 return QLatin1String("copy.png");
38+ case RoleClickable:
39+ return index.row() == 1 ? false : true;
38 }
40 }
39 return QVariant();
41 return QVariant();
40}
42}
tests/mocks/Unity/Launcher/plugin.cpp+3
@@ -20,6 +20,7 @@
20#include "plugin.h"
20#include "plugin.h"
21#include "MockLauncherModel.h"
21#include "MockLauncherModel.h"
22#include "MockLauncherItem.h"
22#include "MockLauncherItem.h"
23+#include "MockQuickListModel.h"
23
24
24#include <unity/shell/launcher/LauncherModelInterface.h>
25#include <unity/shell/launcher/LauncherModelInterface.h>
25#include <unity/shell/launcher/LauncherItemInterface.h>
26#include <unity/shell/launcher/LauncherItemInterface.h>
@@ -39,7 +40,9 @@ void TestLauncherPlugin::registerTypes(const char* uri)
39 // @uri Unity.Launcher
40 // @uri Unity.Launcher
40 qmlRegisterUncreatableType<LauncherModelInterface>(uri, 0, 1, "LauncherModelInterface", "Abstract Interface. Cannot be instantiated.");
41 qmlRegisterUncreatableType<LauncherModelInterface>(uri, 0, 1, "LauncherModelInterface", "Abstract Interface. Cannot be instantiated.");
41 qmlRegisterUncreatableType<LauncherItemInterface>(uri, 0, 1, "LauncherItemInterface", "Abstract Interface. Cannot be instantiated.");
42 qmlRegisterUncreatableType<LauncherItemInterface>(uri, 0, 1, "LauncherItemInterface", "Abstract Interface. Cannot be instantiated.");
43+ qmlRegisterUncreatableType<QuickListModelInterface>(uri, 0, 1, "QuickListModelInterface", "Abstract Interface. Cannot be instantiated.");
42
44
43 qmlRegisterSingletonType<MockLauncherModel>(uri, 0, 1, "LauncherModel", modelProvider);
45 qmlRegisterSingletonType<MockLauncherModel>(uri, 0, 1, "LauncherModel", modelProvider);
44 qmlRegisterUncreatableType<MockLauncherItem>(uri, 0, 1, "LauncherItem", "Can't create LauncherItems in QML. Get them from the LauncherModel");
46 qmlRegisterUncreatableType<MockLauncherItem>(uri, 0, 1, "LauncherItem", "Can't create LauncherItems in QML. Get them from the LauncherModel");
47+ qmlRegisterUncreatableType<MockQuickListModel>(uri, 0, 1, "QuickListModel", "Can't create QuickLists in QML. Get them from the LauncherItems");
45}
48}
tests/qmltests/Launcher/tst_Launcher.qml+79
@@ -49,6 +49,11 @@ Item {
49 property int maxPanelX: 0
49 property int maxPanelX: 0
50 }
50 }
51
51
52+ SignalSpy {
53+ id: signalSpy
54+ target: LauncherModel
55+ }
56+
52 Connections {
57 Connections {
53 target: testCase.findChild(launcher, "launcherPanel")
58 target: testCase.findChild(launcher, "launcherPanel")
54
59
@@ -306,5 +311,79 @@ Item {
306 tryCompare(draggedItem, "itemOpacity", 1)
311 tryCompare(draggedItem, "itemOpacity", 1)
307 tryCompare(fakeDragItem, "visible", false)
312 tryCompare(fakeDragItem, "visible", false)
308 }
313 }
314+
315+ function test_quicklist_dismiss() {
316+ revealer.dragLauncherIntoView();
317+ var draggedItem = findChild(launcher, "launcherDelegate5")
318+ var item0 = findChild(launcher, "launcherDelegate0")
319+ var fakeDragItem = findChild(launcher, "fakeDragItem")
320+ var dndArea = findChild(launcher, "dndArea")
321+
322+ // Initial state
323+ compare(dndArea.quickListPopover, null)
324+
325+ // Doing longpress
326+ mousePress(draggedItem, draggedItem.width / 2, draggedItem.height / 2)
327+ tryCompare(fakeDragItem, "visible", true) // Wait longpress happening
328+ verify(dndArea.quickListPopover != null)
329+
330+ // Dragging a bit (> 1.5 gu)
331+ mouseMove(draggedItem, -units.gu(2), draggedItem.height / 2)
332+
333+ // QuickList needs to be closed when a drag operation starts
334+ tryCompare(dndArea, "quickListPopover", null)
335+
336+ mouseRelease(draggedItem);
337+ }
338+
339+ function test_quicklist_click_data() {
340+ return [
341+ {tag: "non-clickable", index: 1, clickable: false },
342+ {tag: "clickable", index: 2, clickable: true },
343+ ];
344+ }
345+
346+ function test_quicklist_click(data) {
347+ revealer.dragLauncherIntoView();
348+ var clickedItem = findChild(launcher, "launcherDelegate5")
349+ var dndArea = findChild(launcher, "dndArea")
350+
351+ // Initial state
352+ compare(dndArea.quickListPopover, null)
353+
354+ // Doing longpress
355+ mousePress(clickedItem, clickedItem.width / 2, clickedItem.height / 2)
356+ tryCompare(clickedItem, "itemOpacity", 0) // Wait for longpress to happen
357+ verify(dndArea.quickListPopover != null)
358+
359+ mouseRelease(clickedItem);
360+
361+ var quickListEntry = findChild(dndArea.quickListPopover, "quickListEntry" + data.index)
362+
363+ signalSpy.clear();
364+ signalSpy.signalName = "quickListTriggered"
365+
366+ mouseClick(quickListEntry, quickListEntry.width / 2, quickListEntry.height / 2)
367+
368+ if (data.clickable) {
369+ // QuickList needs to be closed when some clickable item is clicked
370+ tryCompare(dndArea, "quickListPopover", null)
371+
372+ compare(signalSpy.count, 1, "Quicklist signal wasn't triggered")
373+ compare(signalSpy.signalArguments[0][0], LauncherModel.get(5).appId)
374+ compare(signalSpy.signalArguments[0][1], 2)
375+
376+ } else {
377+
378+ // QuickList must not be closed when a non-clickable item is clicked
379+ verify(dndArea.quickListPopover != null)
380+
381+ compare(signalSpy.count, 0, "Quicklist signal must NOT be triggered when clicking a non-clickable item")
382+
383+ // Click somewhere in the empty space to dismiss the quicklist
384+ mouseClick(launcher, launcher.width - units.gu(1), units.gu(1));
385+ tryCompare(dndArea, "quickListPopover", null)
386+ }
387+ }
309 }
388 }
310}
389}