on xenial help sign in
Added overflow support to application menus.

Hover timer for auto-scrolling menu popup overflow. Approved by: Albert Astals Cid, Unity8 CI Bot

commit079549e1ed9affb64dd754eb4c9cb9d6ea705ffd authorNick Dedekind <nick.dedekind@canonical.com> dateThu 16 Feb 2017, 13:42 (10 years ago) parent780d6c02e8e4f5
12 files changed+463−148
plugins/Utils/CMakeLists.txt+1 plugins/Utils/expressionfiltermodel.cpp+49 plugins/Utils/expressionfiltermodel.h+42 plugins/Utils/plugin.cpp+2 qml/ApplicationMenus/MenuBar.qml+277−135 qml/ApplicationMenus/MenuItem.qml+3−1 qml/ApplicationMenus/MenuNavigator.qml+23 qml/ApplicationMenus/MenuPopup.qml+26−3 qml/Panel/Panel.qml+3−1 tests/mocks/Utils/CMakeLists.txt+1 tests/mocks/Utils/plugin.cpp+2 tests/qmltests/ApplicationMenus/tst_MenuBar.qml+34−8
plugins/Utils/CMakeLists.txt+1
@@ -35,6 +35,7 @@ set(QMLPLUGIN_SRC
3535 globalfunctions.cpp
3636 URLDispatcher.cpp
3737 tabfocusfence.cpp
38+ expressionfiltermodel.cpp
3839 plugin.cpp
3940 )
4041
plugins/Utils/expressionfiltermodel.cppnew file+49
@@ -0,0 +1,49 @@
1+/*
2+ * Copyright (C) 2017 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "expressionfiltermodel.h"
18+
19+ExpressionFilterModel::ExpressionFilterModel(QObject *parent)
20+ : UnitySortFilterProxyModelQML(parent)
21+{
22+}
23+
24+QJSValue ExpressionFilterModel::matchExpression() const
25+{
26+ return m_matchExpression;
27+}
28+
29+void ExpressionFilterModel::setMatchExpression(const QJSValue &value)
30+{
31+ m_matchExpression = value;
32+ invalidateFilter();
33+}
34+
35+bool
36+ExpressionFilterModel::filterAcceptsRow(int sourceRow,
37+ const QModelIndex &sourceParent) const
38+{
39+ if (m_matchExpression.isCallable()) {
40+ QJSValueList args;
41+ args << sourceRow;
42+ QJSValue ret = m_matchExpression.call(args);
43+ if (ret.isBool()) {
44+ return ret.toBool();
45+ }
46+ }
47+
48+ return UnitySortFilterProxyModelQML::filterAcceptsRow(sourceRow, sourceParent);
49+}
plugins/Utils/expressionfiltermodel.hnew file+42
@@ -0,0 +1,42 @@
1+/*
2+ * Copyright (C) 2017 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef EXPRESSIONFILTERMODEL_H
18+#define EXPRESSIONFILTERMODEL_H
19+
20+#include "unitysortfilterproxymodelqml.h"
21+#include <QJSValue>
22+
23+class ExpressionFilterModel : public UnitySortFilterProxyModelQML
24+{
25+ Q_OBJECT
26+ Q_PROPERTY(QJSValue matchExpression READ matchExpression WRITE setMatchExpression NOTIFY matchExpressionChanged)
27+public:
28+ explicit ExpressionFilterModel(QObject *parent = 0);
29+
30+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
31+
32+ QJSValue matchExpression() const;
33+ void setMatchExpression(const QJSValue& value);
34+
35+Q_SIGNALS:
36+ void matchExpressionChanged();
37+
38+private:
39+ mutable QJSValue m_matchExpression;
40+};
41+
42+#endif // EXPRESSIONFILTERMODEL_H
plugins/Utils/plugin.cpp+2
@@ -41,6 +41,7 @@
4141#include "URLDispatcher.h"
4242#include "appdrawerproxymodel.h"
4343#include "tabfocusfence.h"
44+#include "expressionfiltermodel.h"
4445
4546static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4647{
@@ -86,4 +87,5 @@ void UtilsPlugin::registerTypes(const char *uri)
8687 qmlRegisterType<URLDispatcher>(uri, 0, 1, "URLDispatcher");
8788 qmlRegisterType<AppDrawerProxyModel>(uri, 0, 1, "AppDrawerProxyModel");
8889 qmlRegisterType<TabFocusFenceItem>(uri, 0, 1, "TabFocusFence");
90+ qmlRegisterType<ExpressionFilterModel>(uri, 0, 1, "ExpressionFilterModel");
8991}
qml/ApplicationMenus/MenuBar.qml+277−135
@@ -24,12 +24,13 @@ Item {
2424 id: root
2525 objectName: "menuBar"
2626
27+ // set from outside
2728 property alias unityMenuModel: rowRepeater.model
28−
29− readonly property bool valid: rowRepeater.count > 0
30−
3129 property bool enableKeyFilter: false
30+ property real overflowWidth: width
3231
32+ // read from outside
33+ readonly property bool valid: rowRepeater.count > 0
3334 readonly property bool showRequested: d.longAltPressed || d.currentItem != null
3435
3536 implicitWidth: row.width
@@ -75,197 +76,334 @@ Item {
7576 acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
7677 anchors.fill: parent
7778 enabled: d.currentItem != null
78− hoverEnabled: enabled && d.currentItem.__popup != null
79+ hoverEnabled: enabled && d.currentItem && d.currentItem.__popup != null
7980 onPressed: d.dismissAll()
8081 }
8182
82− Item {
83− id: clippingItem
84−
85− height: root.height
86− width: root.width
87− clip: true
88−
89− Row {
90− id: row
91− spacing: units.gu(2)
92− height: parent.height
83+ Row {
84+ id: row
85+ spacing: units.gu(2)
86+ height: parent.height
9387
94− ActionContext {
95− id: menuBarContext
96− objectName: "barContext"
97− active: !d.currentItem && enableKeyFilter
98− }
99−
100− Repeater {
101− id: rowRepeater
102−
103− Item {
104− id: visualItem
105− objectName: root.objectName + "-item" + __ownIndex
106−
107− readonly property int __ownIndex: index
108− property Item __popup: null;
109− property bool popupVisible: __popup && __popup.visible
88+ ActionContext {
89+ id: menuBarContext
90+ objectName: "barContext"
91+ active: !d.currentItem && enableKeyFilter
92+ }
11093
111− implicitWidth: column.implicitWidth
112− implicitHeight: row.height
113− enabled: model.sensitive
94+ Connections {
95+ target: root.unityMenuModel
96+ onModelReset: d.firstInvisibleIndex = undefined
97+ }
11498
115− function show() {
116− if (!__popup) {
117− __popup = menuComponent.createObject(root, { objectName: visualItem.objectName + "-menu" });
118− __popup.childActivated.connect(dismiss);
119− // force the current item to be the newly popped up menu
120− } else {
121− __popup.show();
122− }
123− d.currentItem = visualItem;
99+ Repeater {
100+ id: rowRepeater
101+
102+ onItemAdded: d.recalcFirstInvisibleIndexAdded(index, item)
103+ onCountChanged: d.recalcFirstInvisibleIndex()
104+
105+ Item {
106+ id: visualItem
107+ objectName: root.objectName + "-item" + __ownIndex
108+
109+ readonly property int __ownIndex: index
110+ property Item __popup: null;
111+ readonly property bool popupVisible: __popup && __popup.visible
112+ readonly property bool shouldDisplay: x + width + ((__ownIndex < rowRepeater.count-1) ? units.gu(2) : 0) <
113+ root.overflowWidth - ((__ownIndex < rowRepeater.count-1) ? overflowButton.width : 0)
114+
115+ implicitWidth: column.implicitWidth
116+ implicitHeight: row.height
117+ enabled: model.sensitive && shouldDisplay
118+ opacity: shouldDisplay ? 1 : 0
119+
120+ function show() {
121+ if (!__popup) {
122+ __popup = menuComponent.createObject(root, { objectName: visualItem.objectName + "-menu" });
123+ __popup.childActivated.connect(dismiss);
124+ // force the current item to be the newly popped up menu
125+ } else {
126+ __popup.show();
124127 }
125− function hide() {
126− if (__popup) {
127− __popup.hide();
128+ d.currentItem = visualItem;
129+ }
130+ function hide() {
131+ if (__popup) {
132+ __popup.hide();
128133
129− if (d.currentItem === visualItem) {
130− d.currentItem = null;
131− }
134+ if (d.currentItem === visualItem) {
135+ d.currentItem = null;
132136 }
133137 }
134− function dismiss() {
135− if (__popup) {
136− __popup.destroy();
137− __popup = null;
138+ }
139+ function dismiss() {
140+ if (__popup) {
141+ __popup.destroy();
142+ __popup = null;
138143
139− if (d.currentItem === visualItem) {
140− d.currentItem = null;
141− }
144+ if (d.currentItem === visualItem) {
145+ d.currentItem = null;
142146 }
143147 }
148+ }
149+
150+ onVisibleChanged: {
151+ if (!visible && __popup) dismiss();
152+ }
144153
145− Connections {
146− target: d
147− onDismissAll: visualItem.dismiss()
154+ onShouldDisplayChanged: {
155+ if ((!shouldDisplay && d.firstInvisibleIndex == undefined) || __ownIndex <= d.firstInvisibleIndex) {
156+ d.recalcFirstInvisibleIndex();
148157 }
158+ }
149159
150− Component {
151− id: menuComponent
152− MenuPopup {
153− desiredX: visualItem.x - units.gu(1)
154− desiredY: parent.height
155− unityMenuModel: root.unityMenuModel.submenu(visualItem.__ownIndex)
160+ Connections {
161+ target: d
162+ onDismissAll: visualItem.dismiss()
163+ }
156164
157− Component.onCompleted: reset();
158− }
165+ Component {
166+ id: menuComponent
167+ MenuPopup {
168+ desiredX: visualItem.x - units.gu(1)
169+ desiredY: parent.height
170+ unityMenuModel: root.unityMenuModel.submenu(visualItem.__ownIndex)
171+
172+ Component.onCompleted: reset();
159173 }
174+ }
160175
161− RowLayout {
162− id: column
163− spacing: units.gu(1)
164− anchors {
165− centerIn: parent
166− }
176+ RowLayout {
177+ id: column
178+ spacing: units.gu(1)
179+ anchors {
180+ centerIn: parent
181+ }
167182
168− Icon {
169− Layout.preferredWidth: units.gu(2)
170− Layout.preferredHeight: units.gu(2)
171− Layout.alignment: Qt.AlignVCenter
183+ Icon {
184+ Layout.preferredWidth: units.gu(2)
185+ Layout.preferredHeight: units.gu(2)
186+ Layout.alignment: Qt.AlignVCenter
172187
173− visible: model.icon || false
174− source: model.icon || ""
175− }
188+ visible: model.icon || false
189+ source: model.icon || ""
190+ }
176191
177− ActionItem {
178− id: actionItem
179− width: _title.width
180− height: _title.height
192+ ActionItem {
193+ id: actionItem
194+ width: _title.width
195+ height: _title.height
181196
182− action: Action {
183− // FIXME - SDK Action:text modifies menu text with html underline for mnemonic
184− text: model.label.replace("_", "&").replace("<u>", "&").replace("</u>", "")
197+ action: Action {
198+ enabled: visualItem.enabled
199+ // FIXME - SDK Action:text modifies menu text with html underline for mnemonic
200+ text: model.label.replace("_", "&").replace("<u>", "&").replace("</u>", "")
185201
186− onTriggered: {
187− visualItem.show();
188− }
202+ onTriggered: {
203+ visualItem.show();
189204 }
205+ }
190206
191− Label {
192− id: _title
193− text: actionItem.text
194− horizontalAlignment: Text.AlignLeft
195− color: enabled ? "white" : "#5d5d5d"
196− }
207+ Label {
208+ id: _title
209+ text: actionItem.text
210+ horizontalAlignment: Text.AlignLeft
211+ color: enabled ? "white" : "#5d5d5d"
197212 }
198213 }
199− } // Item ( delegate )
200− } // Repeater
201− } // Row
214+ }
215+ } // Item ( delegate )
216+ } // Repeater
217+ } // Row
202218
203− MouseArea {
204− anchors.fill: parent
205− hoverEnabled: d.currentItem
219+ MouseArea {
220+ anchors.fill: parent
221+ hoverEnabled: d.currentItem
206222
207− onEntered: {
208− if (d.currentItem) {
209− updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
223+ onEntered: {
224+ if (d.currentItem) {
225+ updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
226+ }
227+ }
228+ onPositionChanged: {
229+ if (d.currentItem) {
230+ updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
231+ }
232+ }
233+ onClicked: {
234+ var prevItem = d.currentItem;
235+ updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
236+ if (prevItem && d.currentItem == prevItem) {
237+ prevItem.hide();
238+ }
239+ }
240+
241+ function updateCurrentItemFromPosition(point) {
242+ var pos = mapToItem(row, point.x, point.y);
243+
244+ if (!d.hoveredItem || !d.currentItem || !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
245+ d.hoveredItem = row.childAt(pos.x, pos.y);
246+ if (!d.hoveredItem || !d.hoveredItem.enabled)
247+ return;
248+ if (d.currentItem != d.hoveredItem) {
249+ d.currentItem = d.hoveredItem;
210250 }
211251 }
212− onPositionChanged: {
213− if (d.currentItem) {
214− updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
252+ }
253+ }
254+
255+ MouseArea {
256+ id: overflowButton
257+ objectName: "overflow"
258+
259+ hoverEnabled: d.currentItem
260+ onEntered: d.currentItem = this
261+ onPositionChanged: d.currentItem = this
262+ onClicked: d.currentItem = this
263+
264+ property Item __popup: null;
265+ readonly property bool popupVisible: __popup && __popup.visible
266+ readonly property Item firstInvisibleItem: d.firstInvisibleIndex !== undefined ? rowRepeater.itemAt(d.firstInvisibleIndex) : null
267+
268+ visible: d.firstInvisibleIndex != undefined
269+ x: firstInvisibleItem ? firstInvisibleItem.x : 0
270+
271+ height: parent.height
272+ width: units.gu(4)
273+
274+ onVisibleChanged: {
275+ if (!visible && __popup) dismiss();
276+ }
277+
278+ Icon {
279+ id: icon
280+ width: units.gu(2)
281+ height: units.gu(2)
282+ anchors.centerIn: parent
283+ color: theme.palette.normal.overlayText
284+ name: "toolkit_chevron-down_2gu"
285+ }
286+
287+ function show() {
288+ if (!__popup) {
289+ __popup = overflowComponent.createObject(root, { objectName: overflowButton.objectName + "-menu" });
290+ __popup.childActivated.connect(dismiss);
291+ // force the current item to be the newly popped up menu
292+ } else {
293+ __popup.show();
294+ }
295+ d.currentItem = overflowButton;
296+ }
297+ function hide() {
298+ if (__popup) {
299+ __popup.hide();
300+
301+ if (d.currentItem === overflowButton) {
302+ d.currentItem = null;
215303 }
216304 }
217− onClicked: {
218− var prevItem = d.currentItem;
219− updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
220− if (prevItem && d.currentItem == prevItem) {
221− prevItem.hide();
305+ }
306+ function dismiss() {
307+ if (__popup) {
308+ __popup.destroy();
309+ __popup = null;
310+
311+ if (d.currentItem === overflowButton) {
312+ d.currentItem = null;
222313 }
223314 }
315+ }
224316
225− function updateCurrentItemFromPosition(point) {
226− var pos = mapToItem(row, point.x, point.y);
317+ Connections {
318+ target: d
319+ onDismissAll: overflowButton.dismiss()
320+ }
321+
322+ Component {
323+ id: overflowComponent
324+ MenuPopup {
325+ id: overflowPopup
326+ desiredX: overflowButton.x - units.gu(1)
327+ desiredY: parent.height
328+ unityMenuModel: overflowModel
329+
330+ ExpressionFilterModel {
331+ id: overflowModel
332+ sourceModel: root.unityMenuModel
333+ matchExpression: function(index) {
334+ if (d.firstInvisibleIndex === undefined) return false;
335+ return index >= d.firstInvisibleIndex;
336+ }
227337
228− if (!d.hoveredItem || !d.currentItem || !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
229− d.hoveredItem = row.childAt(pos.x, pos.y);
230− if (!d.hoveredItem || !d.hoveredItem.enabled)
231− return false;
232− if (d.currentItem != d.hoveredItem) {
233− d.currentItem = d.hoveredItem;
338+ function submenu(index) {
339+ return sourceModel.submenu(mapRowToSource(index));
234340 }
341+ function activate(index) {
342+ return sourceModel.activate(mapRowToSource(index));
343+ }
344+ }
345+
346+ Connections {
347+ target: d
348+ onFirstInvisibleIndexChanged: overflowModel.invalidate()
235349 }
236− return true;
237350 }
238351 }
352+ }
239353
240− Rectangle {
241− id: underline
242− anchors {
243− bottom: row.bottom
244− }
245− x: d.currentItem ? row.x + d.currentItem.x - units.gu(1) : 0
246− width: d.currentItem ? d.currentItem.width + units.gu(2) : 0
247− height: units.dp(4)
248− color: UbuntuColors.orange
249− visible: d.currentItem
354+ Rectangle {
355+ id: underline
356+ anchors {
357+ bottom: row.bottom
250358 }
359+ x: d.currentItem ? row.x + d.currentItem.x - units.gu(1) : 0
360+ width: d.currentItem ? d.currentItem.width + units.gu(2) : 0
361+ height: units.dp(4)
362+ color: UbuntuColors.orange
363+ visible: d.currentItem
251364 }
252365
253366 MenuNavigator {
254367 id: d
255368 objectName: "d"
256369 itemView: rowRepeater
370+ hasOverflow: overflowButton.visible
257371
258372 property Item currentItem: null
259373 property Item hoveredItem: null
260374 property Item prevCurrentItem: null
261−
262− readonly property int currentIndex: currentItem ? currentItem.__ownIndex : -1
263−
264375 property bool altPressed: false
265376 property bool longAltPressed: false
377+ property var firstInvisibleIndex: undefined
378+
379+ readonly property int currentIndex: currentItem && currentItem.hasOwnProperty("__ownIndex") ? currentItem.__ownIndex : -1
266380
267381 signal dismissAll()
268382
383+ function recalcFirstInvisibleIndexAdded(index, item) {
384+ if (firstInvisibleIndex === undefined) {
385+ if (!item.shouldDisplay) {
386+ firstInvisibleIndex = index;
387+ }
388+ } else if (index <= firstInvisibleIndex) {
389+ if (!item.shouldDisplay) {
390+ firstInvisibleIndex = index;
391+ } else {
392+ firstInvisibleIndex++;
393+ }
394+ }
395+ }
396+
397+ function recalcFirstInvisibleIndex() {
398+ for (var i = 0; i < rowRepeater.count; i++) {
399+ if (!rowRepeater.itemAt(i).shouldDisplay) {
400+ firstInvisibleIndex = i;
401+ return;
402+ }
403+ }
404+ firstInvisibleIndex = undefined;
405+ }
406+
269407 onSelect: {
270408 var delegate = rowRepeater.itemAt(index);
271409 if (delegate) {
@@ -273,6 +411,10 @@ Item {
273411 }
274412 }
275413
414+ onOverflow: {
415+ d.currentItem = overflowButton;
416+ }
417+
276418 onCurrentItemChanged: {
277419 if (prevCurrentItem && prevCurrentItem != currentItem) {
278420 if (currentItem) {
qml/ApplicationMenus/MenuItem.qml+3−1
@@ -46,6 +46,8 @@ ActionItem {
4646 enabled: menuData ? menuData.sensitive : false
4747
4848 action: Action {
49+ enabled: root.enabled
50+
4951 // FIXME - SDK Action:text modifies menu text with html underline for mnemonic
5052 text: menuData.label.replace("_", "&").replace("<u>", "&").replace("</u>", "")
5153 checkable: menuData.isCheck || menuData.isRadio
@@ -137,7 +139,7 @@ ActionItem {
137139 theme.palette.disabled.backgroundSecondaryText
138140
139141 visible: root.hasSubmenu
140− name: "chevron"
142+ name: "toolkit_chevron-ltr_2gu"
141143 }
142144 }
143145}
qml/ApplicationMenus/MenuNavigator.qml+23
@@ -18,8 +18,10 @@ import QtQuick 2.4
1818
1919QtObject {
2020 property Item itemView: null
21+ property bool hasOverflow: false
2122
2223 signal select(int index)
24+ signal overflow()
2325
2426 function selectNext(currentIndex) {
2527 var menu;
@@ -32,6 +34,11 @@ QtObject {
3234 break;
3335 }
3436 newIndex++;
37+
38+ if (hasOverflow && newIndex === itemView.count) {
39+ overflow()
40+ break;
41+ }
3542 }
3643 } else if (currentIndex !== -1 && itemView.count > 1) {
3744 var startIndex = (currentIndex + 1) % itemView.count;
@@ -42,6 +49,12 @@ QtObject {
4249 select(newIndex);
4350 break;
4451 }
52+
53+ if (hasOverflow && newIndex + 1 === itemView.count) {
54+ overflow()
55+ break;
56+ }
57+
4558 newIndex = (newIndex + 1) % itemView.count;
4659 } while (newIndex !== startIndex)
4760 }
@@ -58,12 +71,21 @@ QtObject {
5871 break;
5972 }
6073 newIndex--;
74+
75+ if (hasOverflow && newIndex < 0 ) {
76+ overflow();
77+ break;
78+ }
6179 }
6280 } else if (currentIndex !== -1 && itemView.count > 1) {
6381 var startIndex = currentIndex - 1;
6482 newIndex = startIndex;
6583 do {
6684 if (newIndex < 0) {
85+ if (hasOverflow) {
86+ overflow();
87+ break;
88+ }
6789 newIndex = itemView.count - 1;
6890 }
6991 menu = itemView.itemAt(newIndex);
@@ -72,6 +94,7 @@ QtObject {
7294 break;
7395 }
7496 newIndex--;
97+
7598 } while (newIndex !== startIndex)
7699 }
77100 }
qml/ApplicationMenus/MenuPopup.qml+26−3
@@ -19,6 +19,7 @@ import QtQuick.Layouts 1.1
1919import Ubuntu.Components 1.3
2020import Ubuntu.Components.ListItems 1.3 as ListItems
2121import "../Components"
22+import "../Components/PanelState"
2223import "."
2324
2425UbuntuShape {
@@ -100,7 +101,7 @@ UbuntuShape {
100101 property real __minimumWidth: units.gu(20)
101102 property real __maximumWidth: ApplicationMenusLimits.screenWidth * 0.7
102103 property real __minimumHeight: units.gu(2)
103− property real __maximumHeight: ApplicationMenusLimits.screenHeight * 0.7
104+ property real __maximumHeight: ApplicationMenusLimits.screenHeight - PanelState.panelHeight
104105
105106 signal dismissAll()
106107
@@ -183,8 +184,19 @@ UbuntuShape {
183184 }
184185
185186 MouseArea {
187+ id: previousMA
186188 anchors.fill: parent
187− onPressed: {
189+ hoverEnabled: enabled
190+ onPressed: progress()
191+
192+ Timer {
193+ running: previousMA.containsMouse && !listView.atYBeginning
194+ interval: 1000
195+ repeat: true
196+ onTriggered: previousMA.progress()
197+ }
198+
199+ function progress() {
188200 var item = menuColumn.childAt(0, listView.contentY);
189201 if (item) {
190202 var previousItem = item;
@@ -412,8 +424,19 @@ UbuntuShape {
412424 }
413425
414426 MouseArea {
427+ id: nextMA
415428 anchors.fill: parent
416− onPressed: {
429+ hoverEnabled: enabled
430+ onPressed: progress()
431+
432+ Timer {
433+ running: nextMA.containsMouse && !listView.atYEnd
434+ interval: 1000
435+ repeat: true
436+ onTriggered: nextMA.progress()
437+ }
438+
439+ function progress() {
417440 var item = menuColumn.childAt(0, listView.contentY + listView.height);
418441 if (item) {
419442 var nextItem = item;
qml/Panel/Panel.qml+3−1
@@ -209,6 +209,8 @@ Item {
209209 Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
210210 active: __applicationMenus.model
211211
212+ width: parent.width - windowControlButtons.width - units.gu(2) - __indicators.barWidth
213+
212214 property bool menusRequested: menuBarLoader.item ? menuBarLoader.item.showRequested : false
213215
214216 sourceComponent: MenuBar {
@@ -385,7 +387,7 @@ Item {
385387 }
386388
387389 enabled: !applicationMenus.expanded
388− opacity: !applicationMenus.expanded ? 1 : 0
390+ opacity: !callHint.visible && !applicationMenus.expanded ? 1 : 0
389391 Behavior on opacity { UbuntuNumberAnimation { duration: UbuntuAnimation.SnapDuration } }
390392
391393 onEnabledChanged: {
tests/mocks/Utils/CMakeLists.txt+1
@@ -26,6 +26,7 @@ set(QMLPLUGIN_SRC
2626 ${CMAKE_SOURCE_DIR}/plugins/Utils/globalfunctions.cpp
2727 ${CMAKE_SOURCE_DIR}/plugins/Utils/appdrawerproxymodel.cpp
2828 ${CMAKE_SOURCE_DIR}/plugins/Utils/tabfocusfence.cpp
29+ ${CMAKE_SOURCE_DIR}/plugins/Utils/expressionfiltermodel.cpp
2930 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
3031 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
3132 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
tests/mocks/Utils/plugin.cpp+2
@@ -42,6 +42,7 @@
4242#include <globalfunctions.h>
4343#include <appdrawerproxymodel.h>
4444#include <tabfocusfence.h>
45+#include <expressionfiltermodel.h>
4546
4647static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4748{
@@ -86,4 +87,5 @@ void FakeUtilsPlugin::registerTypes(const char *uri)
8687 qmlRegisterType<URLDispatcher>(uri, 0, 1, "URLDispatcher");
8788 qmlRegisterType<AppDrawerProxyModel>(uri, 0, 1, "AppDrawerProxyModel");
8889 qmlRegisterType<TabFocusFenceItem>(uri, 0, 1, "TabFocusFence");
90+ qmlRegisterType<ExpressionFilterModel>(uri, 0, 1, "ExpressionFilterModel");
8991}
tests/qmltests/ApplicationMenus/tst_MenuBar.qml+34−8
@@ -28,8 +28,8 @@ import ".."
2828
2929Item {
3030 id: root
31− width: units.gu(100)
32− height: units.gu(50)
31+ width: units.gu(120)
32+ height: units.gu(70)
3333
3434 Component.onCompleted: {
3535 QuickUtils.keyboardAttached = true;
@@ -51,11 +51,11 @@ Item {
5151 Rectangle {
5252 anchors {
5353 left: parent.left
54− right: parent.right
5554 top: parent.top
5655 margins: units.gu(1)
5756 }
5857 height: units.gu(3)
58+ width: parent.width * 2/3
5959 color: "grey"
6060
6161 MenuBar {
@@ -65,7 +65,7 @@ Item {
6565
6666 unityMenuModel: UnityMenuModel {
6767 id: menuBackend
68− modelData: appMenuData.generateTestData(17,5,2,3)
68+ modelData: appMenuData.generateTestData(10,5,2,3)
6969 }
7070 }
7171 }
@@ -83,12 +83,12 @@ Item {
8383
8484 function init() {
8585 menuBar.dismiss();
86− menuBackend.modelData = appMenuData.generateTestData(5,5,2,3)
86+ menuBackend.modelData = appMenuData.generateTestData(5,5,2,3, "menu")
8787 activatedSpy.clear();
8888 }
8989
9090 function test_mouseNavigation() {
91− menuBackend.modelData = appMenuData.generateTestData(3,3,0,0);
91+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0, "menu");
9292 wait(50) // wait for row to build
9393 var priv = findInvisibleChild(menuBar, "d");
9494
@@ -114,7 +114,7 @@ Item {
114114 }
115115
116116 function test_keyboardNavigation_RightKeySelectsNextMenuItem(data) {
117− menuBackend.modelData = appMenuData.generateTestData(3,3,0,0);
117+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0, "menu");
118118 var priv = findInvisibleChild(menuBar, "d");
119119
120120 var menuItem0 = findChild(menuBar, "menuBar-item0"); verify(menuItem0);
@@ -139,7 +139,7 @@ Item {
139139 }
140140
141141 function test_keyboardNavigation_LeftKeySelectsPreviousMenuItem(data) {
142− menuBackend.modelData = appMenuData.generateTestData(3,3,0,0);
142+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0, "menu");
143143 var priv = findInvisibleChild(menuBar, "d");
144144
145145 var menuItem0 = findChild(menuBar, "menuBar-item0"); verify(menuItem0);
@@ -242,5 +242,31 @@ Item {
242242 mouseClick(menuItem);
243243 compare(priv.currentItem, null, "CurrentItem should be null");
244244 }
245+
246+ function test_overfow() {
247+ menuBackend.modelData = appMenuData.generateTestData(5,2,0,0,"menu");
248+
249+ var overflow = findChild(menuBar, "overflow");
250+ compare(overflow.visible, false, "Overflow should not be visible");
251+
252+ var menu = { "rowData": { "label": "Short" } };
253+ tryCompareFunction(function() {
254+ menuBackend.insertRow(0, menu);
255+ wait(1);
256+ if (overflow.visible) {
257+ return true;
258+ }
259+ return false;
260+ }, true);
261+
262+ tryCompareFunction(function() {
263+ menuBackend.removeRow(0);
264+ wait(1);
265+ if (!overflow.visible) {
266+ return true;
267+ }
268+ return false;
269+ }, true);
270+ }
245271 }
246272}