on xenial help sign in
merged lp:~aacid/unity8/eatHoverWhenMenuIsOpen
commit400e2db61562749b8a75f7048a12d02bbd9e59c8 authorNick Dedekind <nick.dedekind@canonical.com> dateMon 30 Jan 2017, 17:05 (10 years ago) parent2ec7b98903a55c
8 files changed+300−33
qml/ApplicationMenus/ApplicationMenusLimits.qml+24 qml/ApplicationMenus/MenuBar.qml+14−5 qml/ApplicationMenus/MenuItem.qml−7 qml/ApplicationMenus/MenuPopup.qml+90−21 qml/ApplicationMenus/qmldir+1 tests/qmltests/ApplicationMenus/tst_MenuBar.qml+15 tests/qmltests/ApplicationMenus/tst_MenuPopup.qml+14 tests/qmltests/Stage/tst_DesktopStage.qml+142
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 {
7676 acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
7777 anchors.fill: parent
7878 enabled: d.currentItem != null
79+ hoverEnabled: enabled && d.currentItem && d.currentItem.__popup != null
7980 onPressed: d.dismissAll()
8081 }
8182
@@ -119,6 +120,7 @@ Item {
119120 function show() {
120121 if (!__popup) {
121122 __popup = menuComponent.createObject(root, { objectName: visualItem.objectName + "-menu" });
123+ __popup.childActivated.connect(dismiss);
122124 // force the current item to be the newly popped up menu
123125 } else {
124126 __popup.show();
@@ -165,8 +167,8 @@ Item {
165167 Component {
166168 id: menuComponent
167169 MenuPopup {
168− x: visualItem.x - units.gu(1)
169− anchors.top: parent.bottom
170+ desiredX: visualItem.x - units.gu(1)
171+ desiredY: parent.height
170172 unityMenuModel: root.unityMenuModel.submenu(visualItem.__ownIndex)
171173
172174 Component.onCompleted: reset();
@@ -230,7 +232,13 @@ Item {
230232 updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
231233 }
232234 }
233− onClicked: updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
235+ onClicked: {
236+ var prevItem = d.currentItem;
237+ updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
238+ if (prevItem && d.currentItem == prevItem) {
239+ prevItem.hide();
240+ }
241+ }
234242
235243 function updateCurrentItemFromPosition(point) {
236244 var pos = mapToItem(row, point.x, point.y);
@@ -281,6 +289,7 @@ Item {
281289 function show() {
282290 if (!__popup) {
283291 __popup = overflowComponent.createObject(root, { objectName: overflowButton.objectName + "-menu" });
292+ __popup.childActivated.connect(dismiss);
284293 // force the current item to be the newly popped up menu
285294 } else {
286295 __popup.show();
@@ -316,8 +325,8 @@ Item {
316325 id: overflowComponent
317326 MenuPopup {
318327 id: overflowPopup
319− x: overflowButton.x - units.gu(1)
320− anchors.top: parent.bottom
328+ desiredX: overflowButton.x - units.gu(1)
329+ desiredY: parent.height
321330 unityMenuModel: overflowModel
322331
323332 ExpressionFilterModel {
qml/ApplicationMenus/MenuItem.qml−7
@@ -142,11 +142,4 @@ ActionItem {
142142 name: "toolkit_chevron-ltr_2gu"
143143 }
144144 }
145−
146− MouseArea {
147− anchors.fill: parent
148− onClicked: {
149− root.trigger(action && action.checkable ? !action.checked : undefined);
150− }
151− }
152145}
qml/ApplicationMenus/MenuPopup.qml+90−21
@@ -16,16 +16,50 @@
1616
1717import QtQuick 2.4
1818import QtQuick.Layouts 1.1
19−import QtQuick.Window 2.2
2019import Ubuntu.Components 1.3
2120import Ubuntu.Components.ListItems 1.3 as ListItems
2221import "../Components"
22+import "."
2323
2424UbuntuShape {
2525 id: root
2626 objectName: "menu"
2727 backgroundColor: theme.palette.normal.overlay
2828
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+
2963 property alias unityMenuModel: repeater.model
3064
3165 function show() {
@@ -64,9 +98,9 @@ UbuntuShape {
6498 readonly property int currentIndex: currentItem ? currentItem.__ownIndex : -1
6599
66100 property real __minimumWidth: units.gu(20)
67− property real __maximumWidth: Screen.width * 0.7
101+ property real __maximumWidth: ApplicationMenusLimits.screenWidth * 0.7
68102 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
70104
71105 signal dismissAll()
72106
@@ -76,6 +110,8 @@ UbuntuShape {
76110 } else {
77111 hoveredItem = null;
78112 }
113+
114+ submenuHoverTimer.stop();
79115 }
80116
81117 onSelect: {
@@ -188,10 +224,15 @@ UbuntuShape {
188224 contentHeight: menuColumn.height
189225 interactive: height < contentHeight
190226
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+
191233 MouseArea {
192234 anchors.fill: parent
193235 hoverEnabled: true
194− propagateComposedEvents: true // propogate events so we send clicks to children.
195236 z: 1 // on top so we override any other hovers
196237 onEntered: updateCurrentItemFromPosition(Qt.point(mouseX, mouseY))
197238 onPositionChanged: updateCurrentItemFromPosition(Qt.point(mouse.x, mouse.y))
@@ -201,10 +242,24 @@ UbuntuShape {
201242
202243 if (!d.hoveredItem || !d.currentItem ||
203244 !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
245+ submenuHoverTimer.stop();
246+
204247 d.hoveredItem = menuColumn.childAt(pos.x, pos.y)
205248 if (!d.hoveredItem || !d.hoveredItem.enabled)
206249 return;
207250 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();
208263 }
209264 }
210265 }
@@ -260,6 +315,8 @@ UbuntuShape {
260315 width: MathUtils.clamp(implicitWidth, d.__minimumWidth, d.__maximumWidth)
261316
262317 action.onTriggered: {
318+ submenuHoverTimer.stop();
319+
263320 d.currentItem = loader;
264321
265322 if (hasSubmenu) {
@@ -268,22 +325,29 @@ UbuntuShape {
268325 popup = submenuComponent.createObject(focusScope, {
269326 objectName: loader.objectName + "-",
270327 unityMenuModel: model,
271− x: Qt.binding(function() { return root.width }),
272− y: Qt.binding(function() {
328+ substractWidth: true,
329+ desiredX: Qt.binding(function() { return root.width }),
330+ desiredY: Qt.binding(function() {
273331 var dummy = listView.contentY; // force a recalc on contentY change.
274332 return mapToItem(container, 0, y).y;
275333 })
276334 });
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+ });
277345 } else if (popup) {
278346 popup.visible = true;
279347 }
280− popup.retreat.connect(function() {
281− popup.destroy();
282− popup = null;
283− menuItem.forceActiveFocus();
284− })
285348 } else {
286349 root.unityMenuModel.activate(__ownIndex);
350+ root.childActivated();
287351 }
288352 }
289353
@@ -397,23 +461,28 @@ UbuntuShape {
397461 id: submenuLoader
398462 source: "MenuPopup.qml"
399463
464+ property real desiredX
465+ property real desiredY
466+ property bool substractWidth
400467 property var unityMenuModel: null
401468 signal retreat()
402−
403− Binding {
404− target: item
405− property: "unityMenuModel"
406− value: submenuLoader.unityMenuModel
469+ signal childActivated()
470+
471+ onLoaded: {
472+ item.unityMenuModel = Qt.binding(function() { return 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; });
407477 }
408478
409− Binding {
479+ Keys.onLeftPressed: retreat()
480+
481+ Connections {
410482 target: item
411− property: "objectName"
412− value: submenuLoader.objectName + "menu"
483+ onChildActivated: childActivated();
413484 }
414485
415− Keys.onLeftPressed: retreat()
416−
417486 Component.onCompleted: item.select(0);
418487 onVisibleChanged: if (visible) { item.select(0); }
419488 }
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 {
193193 compare(priv.currentItem, menuItem1, "First enabled item should be opened");
194194 }
195195
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+
196211 function test_overfow() {
197212 menuBackend.modelData = appMenuData.generateTestData(5,2,0,0,"menu");
198213
tests/qmltests/ApplicationMenus/tst_MenuPopup.qml+14
@@ -223,6 +223,20 @@ Item {
223223 tryCompareFunction(function() { return menuItem.popup !== null && menuItem.popup.visible }, false);
224224 }
225225
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+
226240 function test_differentSizes() {
227241 var differentSizesMenu = [{
228242 "rowData": { "label": "Short" }}, {
tests/qmltests/Stage/tst_DesktopStage.qml+142
@@ -29,6 +29,7 @@ import ".." // For EdgeBarrierControls
2929import "../../../qml/Stage"
3030import "../../../qml/Components"
3131import "../../../qml/Components/PanelState"
32+import "../../../qml/ApplicationMenus"
3233
3334Item {
3435 id: root
@@ -44,6 +45,8 @@ Item {
4445 }
4546
4647 Component.onCompleted: {
48+ ApplicationMenusLimits.screenWidth = Qt.binding( function() { return stageLoader.width; } );
49+ ApplicationMenusLimits.screenHeight = Qt.binding( function() { return stageLoader.height; } );
4750 QuickUtils.keyboardAttached = true;
4851 theme.name = "Ubuntu.Components.Themes.SuruDark";
4952 resetGeometry();
@@ -830,5 +833,144 @@ Item {
830833 mouseRelease(decoration);
831834 tryCompare(Mir, "cursorName", "");
832835 }
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+ }
833975 }
834976}