on xenial help sign in
merged lp:~aacid/unity8/eatHoverWhenMenuIsOpen
400e2dbNick Dedekind
qml/ApplicationMenus/ApplicationMenusLimits.qmlnew file+24
@@ -0,0 +1,24 @@
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+pragma Singleton
18+import QtQuick 2.4
19+import QtQuick.Window 2.2
20+
21+QtObject {
22+ property real screenWidth: Screen.width
23+ property real screenHeight: Screen.height
24+}
qml/ApplicationMenus/MenuBar.qml+14−5
@@ -76,6 +76,7 @@ Item {
76 acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
76 acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
77 anchors.fill: parent
77 anchors.fill: parent
78 enabled: d.currentItem != null
78 enabled: d.currentItem != null
79+ hoverEnabled: enabled && d.currentItem && d.currentItem.__popup != null
79 onPressed: d.dismissAll()
80 onPressed: d.dismissAll()
80 }
81 }
81
82
@@ -119,6 +120,7 @@ Item {
119 function show() {
120 function show() {
120 if (!__popup) {
121 if (!__popup) {
121 __popup = menuComponent.createObject(root, { objectName: visualItem.objectName + "-menu" });
122 __popup = menuComponent.createObject(root, { objectName: visualItem.objectName + "-menu" });
123+ __popup.childActivated.connect(dismiss);
122 // force the current item to be the newly popped up menu
124 // force the current item to be the newly popped up menu
123 } else {
125 } else {
124 __popup.show();
126 __popup.show();
@@ -165,8 +167,8 @@ Item {
165 Component {
167 Component {
166 id: menuComponent
168 id: menuComponent
167 MenuPopup {
169 MenuPopup {
168− x: visualItem.x - units.gu(1)
170+ desiredX: visualItem.x - units.gu(1)
169− anchors.top: parent.bottom
171+ desiredY: parent.height
170 unityMenuModel: root.unityMenuModel.submenu(visualItem.__ownIndex)
172 unityMenuModel: root.unityMenuModel.submenu(visualItem.__ownIndex)
171
173
172 Component.onCompleted: reset();
174 Component.onCompleted: reset();
@@ -230,7 +232,13 @@ Item {
230 updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
232 updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
231 }
233 }
232 }
234 }
235+ onClicked: {
236+ var prevItem = d.currentItem;
233− onClicked: updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
237+ updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
238+ if (prevItem && d.currentItem == prevItem) {
239+ prevItem.hide();
240+ }
241+ }
234
242
235 function updateCurrentItemFromPosition(point) {
243 function updateCurrentItemFromPosition(point) {
236 var pos = mapToItem(row, point.x, point.y);
244 var pos = mapToItem(row, point.x, point.y);
@@ -281,6 +289,7 @@ Item {
281 function show() {
289 function show() {
282 if (!__popup) {
290 if (!__popup) {
283 __popup = overflowComponent.createObject(root, { objectName: overflowButton.objectName + "-menu" });
291 __popup = overflowComponent.createObject(root, { objectName: overflowButton.objectName + "-menu" });
292+ __popup.childActivated.connect(dismiss);
284 // force the current item to be the newly popped up menu
293 // force the current item to be the newly popped up menu
285 } else {
294 } else {
286 __popup.show();
295 __popup.show();
@@ -316,8 +325,8 @@ Item {
316 id: overflowComponent
325 id: overflowComponent
317 MenuPopup {
326 MenuPopup {
318 id: overflowPopup
327 id: overflowPopup
319− x: overflowButton.x - units.gu(1)
328+ desiredX: overflowButton.x - units.gu(1)
320− anchors.top: parent.bottom
329+ desiredY: parent.height
321 unityMenuModel: overflowModel
330 unityMenuModel: overflowModel
322
331
323 ExpressionFilterModel {
332 ExpressionFilterModel {
qml/ApplicationMenus/MenuItem.qml−7
@@ -142,11 +142,4 @@ ActionItem {
142 name: "toolkit_chevron-ltr_2gu"
142 name: "toolkit_chevron-ltr_2gu"
143 }
143 }
144 }
144 }
145−
146− MouseArea {
147− anchors.fill: parent
148− onClicked: {
149− root.trigger(action && action.checkable ? !action.checked : undefined);
150− }
151− }
152}
145}
qml/ApplicationMenus/MenuPopup.qml+90−21
@@ -16,16 +16,50 @@
16
16
17import QtQuick 2.4
17import QtQuick 2.4
18import QtQuick.Layouts 1.1
18import QtQuick.Layouts 1.1
19−import QtQuick.Window 2.2
20import Ubuntu.Components 1.3
19import Ubuntu.Components 1.3
21import Ubuntu.Components.ListItems 1.3 as ListItems
20import Ubuntu.Components.ListItems 1.3 as ListItems
22import "../Components"
21import "../Components"
22+import "."
23
23
24UbuntuShape {
24UbuntuShape {
25 id: root
25 id: root
26 objectName: "menu"
26 objectName: "menu"
27 backgroundColor: theme.palette.normal.overlay
27 backgroundColor: theme.palette.normal.overlay
28
28
29+ signal childActivated()
30+
31+ // true for submenus that need to show on the other side of their parent
32+ // if they don't fit when growing right
33+ property bool substractWidth: false
34+
35+ property real desiredX
36+ x: {
37+ var dummy = visible; // force recalc when shown/hidden
38+ var parentTopLeft = parent.mapToItem(null, 0, 0);
39+ var farX = ApplicationMenusLimits.screenWidth;
40+ if (parentTopLeft.x + width + desiredX <= farX) {
41+ return desiredX;
42+ } else {
43+ if (substractWidth) {
44+ return -width;
45+ } else {
46+ return farX - parentTopLeft.x - width;
47+ }
48+ }
49+ }
50+
51+ property real desiredY
52+ y: {
53+ var dummy = visible; // force recalc when shown/hidden
54+ var parentTopLeft = parent.mapToItem(null, 0, 0);
55+ var bottomY = ApplicationMenusLimits.screenHeight;
56+ if (parentTopLeft.y + height + desiredY <= bottomY) {
57+ return desiredY;
58+ } else {
59+ return bottomY - parentTopLeft.y - height;
60+ }
61+ }
62+
29 property alias unityMenuModel: repeater.model
63 property alias unityMenuModel: repeater.model
30
64
31 function show() {
65 function show() {
@@ -64,9 +98,9 @@ UbuntuShape {
64 readonly property int currentIndex: currentItem ? currentItem.__ownIndex : -1
98 readonly property int currentIndex: currentItem ? currentItem.__ownIndex : -1
65
99
66 property real __minimumWidth: units.gu(20)
100 property real __minimumWidth: units.gu(20)
67− property real __maximumWidth: Screen.width * 0.7
101+ property real __maximumWidth: ApplicationMenusLimits.screenWidth * 0.7
68 property real __minimumHeight: units.gu(2)
102 property real __minimumHeight: units.gu(2)
69− property real __maximumHeight: Screen.height - mapToItem(null, 0, y).y
103+ property real __maximumHeight: ApplicationMenusLimits.screenHeight - mapToItem(null, 0, y).y
70
104
71 signal dismissAll()
105 signal dismissAll()
72
106
@@ -76,6 +110,8 @@ UbuntuShape {
76 } else {
110 } else {
77 hoveredItem = null;
111 hoveredItem = null;
78 }
112 }
113+
114+ submenuHoverTimer.stop();
79 }
115 }
80
116
81 onSelect: {
117 onSelect: {
@@ -188,10 +224,15 @@ UbuntuShape {
188 contentHeight: menuColumn.height
224 contentHeight: menuColumn.height
189 interactive: height < contentHeight
225 interactive: height < contentHeight
190
226
227+ Timer {
228+ id: submenuHoverTimer
229+ interval: 225 // GTK MENU_POPUP_DELAY, Qt SH_Menu_SubMenuPopupDelay in QCommonStyle is 256
230+ onTriggered: d.currentItem.item.trigger();
231+ }
232+
191 MouseArea {
233 MouseArea {
192 anchors.fill: parent
234 anchors.fill: parent
193 hoverEnabled: true
235 hoverEnabled: true
194− propagateComposedEvents: true // propogate events so we send clicks to children.
195 z: 1 // on top so we override any other hovers
236 z: 1 // on top so we override any other hovers
196 onEntered: updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
237 onEntered: updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
197 onPositionChanged: updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
238 onPositionChanged: updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
@@ -201,10 +242,24 @@ UbuntuShape {
201
242
202 if (!d.hoveredItem || !d.currentItem ||
243 if (!d.hoveredItem || !d.currentItem ||
203 !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
244 !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
245+ submenuHoverTimer.stop();
246+
204 d.hoveredItem = menuColumn.childAt(pos.x, pos.y)
247 d.hoveredItem = menuColumn.childAt(pos.x, pos.y)
205 if (!d.hoveredItem || !d.hoveredItem.enabled)
248 if (!d.hoveredItem || !d.hoveredItem.enabled)
206 return;
249 return;
207 d.currentItem = d.hoveredItem;
250 d.currentItem = d.hoveredItem;
251+
252+ if (!d.currentItem.__isSeparator && d.currentItem.item.hasSubmenu && d.currentItem.item.enabled) {
253+ submenuHoverTimer.start();
254+ }
255+ }
256+ }
257+
258+ onClicked: {
259+ var pos = mapToItem(listView.contentItem, mouse.x, mouse.y);
260+ var clickedItem = menuColumn.childAt(pos.x, pos.y);
261+ if (clickedItem.enabled && !clickedItem.__isSeparator) {
262+ clickedItem.item.trigger();
208 }
263 }
209 }
264 }
210 }
265 }
@@ -260,6 +315,8 @@ UbuntuShape {
260 width: MathUtils.clamp(implicitWidth, d.__minimumWidth, d.__maximumWidth)
315 width: MathUtils.clamp(implicitWidth, d.__minimumWidth, d.__maximumWidth)
261
316
262 action.onTriggered: {
317 action.onTriggered: {
318+ submenuHoverTimer.stop();
319+
263 d.currentItem = loader;
320 d.currentItem = loader;
264
321
265 if (hasSubmenu) {
322 if (hasSubmenu) {
@@ -268,22 +325,29 @@ UbuntuShape {
268 popup = submenuComponent.createObject(focusScope, {
325 popup = submenuComponent.createObject(focusScope, {
269 objectName: loader.objectName + "-",
326 objectName: loader.objectName + "-",
270 unityMenuModel: model,
327 unityMenuModel: model,
328+ substractWidth: true,
271− x: Qt.binding(function() { return root.width }),
329+ desiredX: Qt.binding(function() { return root.width }),
272− y: Qt.binding(function() {
330+ desiredY: Qt.binding(function() {
273 var dummy = listView.contentY; // force a recalc on contentY change.
331 var dummy = listView.contentY; // force a recalc on contentY change.
274 return mapToItem(container, 0, y).y;
332 return mapToItem(container, 0, y).y;
275 })
333 })
276 });
334 });
335+ popup.retreat.connect(function() {
336+ popup.destroy();
337+ popup = null;
338+ menuItem.forceActiveFocus();
339+ });
340+ popup.childActivated.connect(function() {
341+ popup.destroy();
342+ popup = null;
343+ root.childActivated();
344+ });
277 } else if (popup) {
345 } else if (popup) {
278 popup.visible = true;
346 popup.visible = true;
279 }
347 }
280− popup.retreat.connect(function() {
281− popup.destroy();
282− popup = null;
283− menuItem.forceActiveFocus();
284− })
285 } else {
348 } else {
286 root.unityMenuModel.activate(__ownIndex);
349 root.unityMenuModel.activate(__ownIndex);
350+ root.childActivated();
287 }
351 }
288 }
352 }
289
353
@@ -397,23 +461,28 @@ UbuntuShape {
397 id: submenuLoader
461 id: submenuLoader
398 source: "MenuPopup.qml"
462 source: "MenuPopup.qml"
399
463
464+ property real desiredX
465+ property real desiredY
466+ property bool substractWidth
400 property var unityMenuModel: null
467 property var unityMenuModel: null
401 signal retreat()
468 signal retreat()
402−
469+ signal childActivated()
403− Binding {
470+
404− target: item
471+ onLoaded: {
405− property: "unityMenuModel"
472+ item.unityMenuModel = Qt.binding(function() { return submenuLoader.unityMenuModel; });
406− value: submenuLoader.unityMenuModel
473+ item.objectName = Qt.binding(function() { return submenuLoader.objectName + "menu"; });
474+ item.desiredX = Qt.binding(function() { return submenuLoader.desiredX; });
475+ item.desiredY = Qt.binding(function() { return submenuLoader.desiredY; });
476+ item.substractWidth = Qt.binding(function() { return submenuLoader.substractWidth; });
407 }
477 }
408
478
409− Binding {
479+ Keys.onLeftPressed: retreat()
480+
481+ Connections {
410 target: item
482 target: item
411− property: "objectName"
483+ onChildActivated: childActivated();
412− value: submenuLoader.objectName + "menu"
413 }
484 }
414
485
415− Keys.onLeftPressed: retreat()
416−
417 Component.onCompleted: item.select(0);
486 Component.onCompleted: item.select(0);
418 onVisibleChanged: if (visible) { item.select(0); }
487 onVisibleChanged: if (visible) { item.select(0); }
419 }
488 }
qml/ApplicationMenus/qmldirnew file+1
@@ -0,0 +1 @@
1+singleton ApplicationMenusLimits 0.1 ApplicationMenusLimits.qml
tests/qmltests/ApplicationMenus/tst_MenuBar.qml+15
@@ -193,6 +193,21 @@ Item {
193 compare(priv.currentItem, menuItem1, "First enabled item should be opened");
193 compare(priv.currentItem, menuItem1, "First enabled item should be opened");
194 }
194 }
195
195
196+ function test_clickOpenMenuClosesMenu() {
197+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu");
198+ var priv = findInvisibleChild(menuBar, "d");
199+
200+ var menuItem = findChild(menuBar, "menuBar-item0");
201+ waitForRendering(menuItem);
202+ mouseClick(menuItem);
203+ compare(priv.currentItem, menuItem, "CurrentItem should be set to item 0");
204+ compare(priv.currentItem.popupVisible, true, "Popup should be visible");
205+
206+ waitForRendering(menuItem);
207+ mouseClick(menuItem);
208+ compare(priv.currentItem, null, "CurrentItem should be null");
209+ }
210+
196 function test_overfow() {
211 function test_overfow() {
197 menuBackend.modelData = appMenuData.generateTestData(5,2,0,0,"menu");
212 menuBackend.modelData = appMenuData.generateTestData(5,2,0,0,"menu");
198
213
tests/qmltests/ApplicationMenus/tst_MenuPopup.qml+14
@@ -223,6 +223,20 @@ Item {
223 tryCompareFunction(function() { return menuItem.popup !== null && menuItem.popup.visible }, false);
223 tryCompareFunction(function() { return menuItem.popup !== null && menuItem.popup.visible }, false);
224 }
224 }
225
225
226+ function test_mouseHoverOpensSubMenu() {
227+ menu.unityMenuModel.modelData = appMenuData.generateTestData(3,3,1,0,"menu",false);
228+
229+ var menuItem = findChild(menu, "menu-item0");
230+
231+ var priv = findInvisibleChild(menu, "d");
232+ priv.currentItem = menuItem;
233+
234+ mouseMove(menuItem, menuItem.width/2, menuItem.height/2);
235+ verify(!menuItem.popup);
236+
237+ tryCompareFunction(function() { return menuItem.popup != null; }, true);
238+ }
239+
226 function test_differentSizes() {
240 function test_differentSizes() {
227 var differentSizesMenu = [{
241 var differentSizesMenu = [{
228 "rowData": { "label": "Short" }}, {
242 "rowData": { "label": "Short" }}, {
tests/qmltests/Stage/tst_DesktopStage.qml+142
@@ -29,6 +29,7 @@ import ".." // For EdgeBarrierControls
29import "../../../qml/Stage"
29import "../../../qml/Stage"
30import "../../../qml/Components"
30import "../../../qml/Components"
31import "../../../qml/Components/PanelState"
31import "../../../qml/Components/PanelState"
32+import "../../../qml/ApplicationMenus"
32
33
33Item {
34Item {
34 id: root
35 id: root
@@ -44,6 +45,8 @@ Item {
44 }
45 }
45
46
46 Component.onCompleted: {
47 Component.onCompleted: {
48+ ApplicationMenusLimits.screenWidth = Qt.binding( function() { return stageLoader.width; } );
49+ ApplicationMenusLimits.screenHeight = Qt.binding( function() { return stageLoader.height; } );
47 QuickUtils.keyboardAttached = true;
50 QuickUtils.keyboardAttached = true;
48 theme.name = "Ubuntu.Components.Themes.SuruDark";
51 theme.name = "Ubuntu.Components.Themes.SuruDark";
49 resetGeometry();
52 resetGeometry();
@@ -830,5 +833,144 @@ Item {
830 mouseRelease(decoration);
833 mouseRelease(decoration);
831 tryCompare(Mir, "cursorName", "");
834 tryCompare(Mir, "cursorName", "");
832 }
835 }
836+
837+ function test_menuPositioning_data() {
838+ return [
839+ {tag: "good",
840+ windowPosition: Qt.point(units.gu(10), units.gu(10))
841+ },
842+ {tag: "collides right",
843+ windowPosition: Qt.point(units.gu(100), units.gu(10)),
844+ minimumXDifference: units.gu(8)
845+ },
846+ {tag: "collides bottom",
847+ windowPosition: Qt.point(units.gu(10), units.gu(80)),
848+ minimumYDifference: units.gu(7)
849+ },
850+ ]
851+ }
852+
853+ function test_menuPositioning(data) {
854+ var appDelegate = startApplication("dialer-app");
855+ appDelegate.windowedX = data.windowPosition.x;
856+ appDelegate.windowedY = data.windowPosition.y;
857+
858+ var menuItem = findChild(appDelegate, "menuBar-item3");
859+ menuItem.show();
860+
861+ var menu = findChild(appDelegate, "menuBar-item3-menu");
862+ tryCompare(menu, "visible", true);
863+
864+ var normalPositioningX = menuItem.x - units.gu(1);
865+ var normalPositioningY = menuItem.height;
866+
867+ // We do this fuzzy checking because otherwise we would be duplicating the code
868+ // that calculates the coordinates and any bug it may have, what we want is really
869+ // to check that on collision with the border the menu is shifted substantially
870+ if (data.minimumXDifference) {
871+ verify(menu.x < normalPositioningX - data.minimumXDifference);
872+ } else {
873+ compare(menu.x, normalPositioningX);
874+ }
875+
876+ if (data.minimumYDifference) {
877+ verify(menu.y < normalPositioningY - data.minimumYDifference);
878+ } else {
879+ compare(menu.y, normalPositioningY);
880+ }
881+ }
882+
883+ function test_submenuPositioning_data() {
884+ return [
885+ {tag: "good",
886+ windowPosition: Qt.point(units.gu(10), units.gu(10))
887+ },
888+ {tag: "collides right",
889+ windowPosition: Qt.point(units.gu(100), units.gu(10)),
890+ minimumXDifference: units.gu(35)
891+ },
892+ {tag: "collides bottom",
893+ windowPosition: Qt.point(units.gu(10), units.gu(80)),
894+ minimumYDifference: units.gu(8)
895+ },
896+ ]
897+ }
898+
899+ function test_submenuPositioning(data) {
900+ var appDelegate = startApplication("dialer-app");
901+ appDelegate.windowedX = data.windowPosition.x;
902+ appDelegate.windowedY = data.windowPosition.y;
903+
904+ var menuItem = findChild(appDelegate, "menuBar-item3");
905+ menuItem.show();
906+
907+ var menu = findChild(appDelegate, "menuBar-item3-menu");
908+ menuItem = findChild(menu, "menuBar-item3-menu-item3-actionItem");
909+ tryCompare(menuItem, "visible", true);
910+ mouseMove(menuItem);
911+ mouseClick(menuItem);
912+
913+ menu = findChild(appDelegate, "menuBar-item3-menu-item3-menu");
914+
915+ var normalPositioningX = menuItem.width;
916+ var normalPositioningY = menuItem.parent.y;
917+
918+ // We do this fuzzy checking because otherwise we would be duplicating the code
919+ // that calculates the coordinates and any bug it may have, what we want is really
920+ // to check that on collision with the border the menu is shifted substantially
921+ if (data.minimumXDifference) {
922+ verify(menu.x < normalPositioningX - data.minimumXDifference);
923+ } else {
924+ compare(menu.x, normalPositioningX);
925+ }
926+
927+ if (data.minimumYDifference) {
928+ verify(menu.y < normalPositioningY - data.minimumYDifference);
929+ } else {
930+ compare(menu.y, normalPositioningY);
931+ }
932+ }
933+
934+ function test_menuDoubleClickNoMaximizeWindowBehind() {
935+ var appDelegate1 = startApplication("dialer-app");
936+ var appDelegate2 = startApplication("gmail-webapp");
937+
938+ // Open menu
939+ var menuItem = findChild(appDelegate2, "menuBar-item3");
940+ menuItem.show();
941+ var menu = findChild(appDelegate2, "menuBar-item3-menu");
942+ menuItem = findChild(menu, "menuBar-item3-menu-item3-actionItem");
943+ tryCompare(menuItem, "visible", true);
944+
945+ // Place the other application window decoration under the menu
946+ var pos = menuItem.mapToItem(null, menuItem.width / 2, menuItem.height / 2);
947+ appDelegate1.windowedX = pos.x - appDelegate1.width / 2;
948+ appDelegate1.windowedY = pos.y - units.gu(1);
949+
950+ var previousWindowState = appDelegate1.windowState;
951+
952+ mouseMove(menuItem);
953+ mouseDoubleClickSequence(menuItem);
954+
955+ expectFail("", "Double clicking a menu should not change the window below");
956+ tryCompareFunction(function() { return appDelegate1.windowState != previousWindowState; }, true);
957+ }
958+
959+ function test_openMenuEatsHoverOutsideIt() {
960+ var appDelegate = startApplication("gmail-webapp");
961+
962+ var wd = findChild(appDelegate, "appWindowDecoration");
963+ var closeButton = findChild(wd, "closeWindowButton");
964+
965+ // Open menu
966+ var menuItem = findChild(appDelegate, "menuBar-item3");
967+ menuItem.show();
968+ var menu = findChild(appDelegate, "menuBar-item3-menu");
969+ tryCompare(menu, "visible", true);
970+
971+ mouseMove(closeButton, closeButton.width/2, closeButton.height/2);
972+ expectFail("", "Hovering the window controls should be ignored when the menu is open");
973+ tryCompare(closeButton, "containsMouse", true);
974+ }
833 }
975 }
834}
976}