on xenial help sign in
added missing files
commit720fb76dcd3a906ca0a50e4bdffde3df14559b80 authorNick Dedekind <nick.dedekind@canonical.com> dateFri 30 Sep 2016, 11:22 (10 years ago) parenta40eeb7
12 files changed+2185
qml/ApplicationMenus/MenuBarLoader.qml+327 qml/ApplicationMenus/MenuItemLoader.qml+136 qml/ApplicationMenus/MenuLoader.qml+359 qml/ApplicationMenus/RegisteredApplicationMenuModel.qml+56 qml/ApplicationMenus/Test.qml+81 qml/Panel/FakePanelMenu.qml+26 qml/Panel/PanelItemRow.qml+1 tests/qmltests/ApplicationMenus/tst_MenuBar.qml+137 tests/qmltests/ApplicationMenus/tst_MenuPage.qml+258 tests/qmltests/Stages/DesktopMenuData.qml+549 tests/qmltests/Stages/tst_DecoratedWindow.qml+82 tests/qmltests/Stages/tst_WindowDecoration.qml+173
qml/ApplicationMenus/MenuBarLoader.qmlnew file+327
@@ -0,0 +1,327 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import Utils 0.1
20+import Ubuntu.Components 1.3
21+
22+Item {
23+ id: root
24+
25+ property alias unityMenuModel: rowRepeater.model
26+
27+ readonly property bool valid: rowRepeater.count > 0
28+
29+ property alias enableKeyFilter: altFilter.enabled
30+
31+ readonly property bool showRequested: altFilter.longAltPressed || d.currentItem != null
32+
33+ WindowInputFilter {
34+ id: altFilter
35+ property bool altPressed: false
36+ property bool longAltPressed: false
37+ Keys.onPressed: {
38+ if (event.key === Qt.Key_Alt && !event.isAutoRepeat) {
39+ altPressed = true;
40+ longAltPressed = false;
41+ menuBarShortcutTimer.start();
42+ return;
43+ }
44+ event.accepted = false;
45+ }
46+ Keys.onReleased: {
47+ if (event.key === Qt.Key_Alt) {
48+ menuBarShortcutTimer.stop();
49+ altPressed = false;
50+ longAltPressed = false;
51+ return;
52+ }
53+ event.accepted = false
54+ }
55+
56+ Timer {
57+ id: menuBarShortcutTimer
58+ interval: 200
59+ repeat: false
60+ onTriggered: {
61+ altFilter.longAltPressed = true;
62+ }
63+ }
64+ }
65+
66+ implicitWidth: row.width + units.gu(1)
67+ height: parent.height
68+
69+ InverseMouseArea {
70+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
71+ anchors.fill: parent
72+ enabled: d.currentItem != null
73+ onPressed: {
74+ if (d.currentItem) {
75+ d.currentItem.dismiss();
76+ }
77+ }
78+ }
79+
80+ ActionContext {
81+ id: menuBarContext
82+ objectName: "menuBarContext"
83+ active: d.currentItem == null && altFilter.altPressed
84+ }
85+
86+ Row {
87+ id: row
88+ anchors.left: parent.left
89+ height: parent.height
90+ spacing: units.gu(2)
91+
92+ Repeater {
93+ id: rowRepeater
94+
95+ Item {
96+ id: visualItem
97+ objectName: "menuBarItem"
98+
99+ readonly property int __index: index
100+ property Item __popup: null;
101+ property bool popupVisible: __popup && __popup.visible
102+
103+ implicitWidth: column.implicitWidth
104+ implicitHeight: parent.height
105+ enabled: model.sensitive
106+
107+ Keys.forwardTo: [ root ]
108+
109+ function show() {
110+ if (!__popup) {
111+ __popup = menuComponent.createObject(visualItem);
112+ } else {
113+ __popup.visible = true;
114+ }
115+ __popup.forceActiveFocus();
116+ }
117+ function hide() {
118+ if (__popup) {
119+ __popup.visible = false;
120+ }
121+ }
122+ function dismiss() {
123+ if (__popup) {
124+ __popup.destroy();
125+ __popup = null;
126+ }
127+ }
128+
129+ Component {
130+ id: menuComponent
131+ MenuLoader {
132+ x: -units.gu(1)
133+ anchors.top: parent.bottom
134+ unityMenuModel: root.unityMenuModel.submenu(visualItem.__index)
135+ }
136+ }
137+
138+ onPopupVisibleChanged: {
139+ if (popupVisible) {
140+ d.currentItem = visualItem;
141+ } else if (d.currentItem === visualItem) {
142+ d.currentItem = null;
143+ }
144+ }
145+
146+ RowLayout {
147+ id: column
148+ spacing: units.gu(1)
149+ anchors {
150+ centerIn: parent
151+ }
152+
153+ Icon {
154+ Layout.preferredWidth: units.gu(2)
155+ Layout.preferredHeight: units.gu(2)
156+ Layout.alignment: Qt.AlignVCenter
157+
158+ visible: model.icon || false
159+ source: model.icon || ""
160+ }
161+
162+ ActionItem {
163+ id: actionItem
164+ width: _title.width
165+ height: _title.height
166+
167+ action: Action {
168+ text: model.label.replace("_", "&")
169+
170+ onTriggered: {
171+ visualItem.show();
172+ }
173+ }
174+
175+ Label {
176+ id: _title
177+ text: actionItem.text
178+ horizontalAlignment: Text.AlignLeft
179+ color: enabled ? "white" : "#5d5d5d"
180+ }
181+ }
182+ }
183+ } // Item ( delegate )
184+ } // Repeater
185+ } // Row
186+
187+ Rectangle {
188+ id: underline
189+ anchors {
190+ bottom: row.bottom
191+ }
192+ x: d.currentItem ? row.x + d.currentItem.x - units.gu(1) : 0
193+ width: d.currentItem ? d.currentItem.width + units.gu(2) : 0
194+ height: units.dp(4)
195+ color: UbuntuColors.orange
196+ visible: d.currentItem
197+ }
198+
199+ QtObject {
200+ id: d
201+
202+ property Item currentItem: null
203+ property Item hoveredItem: null
204+ property Item prevCurrentItem: null
205+
206+ onCurrentItemChanged: {
207+ if (prevCurrentItem && prevCurrentItem != currentItem) {
208+ if (currentItem) {
209+ prevCurrentItem.hide();
210+ } else {
211+ prevCurrentItem.dismiss();
212+ }
213+ }
214+
215+ if (currentItem) currentItem.show();
216+ prevCurrentItem = currentItem;
217+ }
218+ }
219+
220+ MouseArea {
221+ id: menuMouseArea
222+ anchors.fill: parent
223+ hoverEnabled: d.currentItem
224+
225+ onPositionChanged: {
226+ if (d.currentItem) {
227+ updateCurrentItemFromPosition(mouse)
228+ }
229+ }
230+ onClicked: updateCurrentItemFromPosition(mouse)
231+
232+ function updateCurrentItemFromPosition(point) {
233+ var pos = mapToItem(row, point.x, point.y);
234+
235+ if (!d.hoveredItem || !d.currentItem || !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
236+ d.hoveredItem = row.childAt(pos.x, pos.y);
237+ if (!d.hoveredItem || !d.hoveredItem.enabled)
238+ return false;
239+ if (d.currentItem != d.hoveredItem) {
240+ d.currentItem = d.hoveredItem;
241+// if (d.currentItem) d.currentItem.hide();
242+// if (d.hoveredItem) d.hoveredItem.show();
243+ }
244+ }
245+ return true;
246+ }
247+ }
248+
249+ Keys.onEscapePressed: {
250+ if (d.currentItem) d.currentItem.hide()
251+ event.accepted = true;
252+ }
253+
254+ Keys.onLeftPressed: {
255+ if (d.currentItem) {
256+ selectPrevious(d.currentItem.__index);
257+ }
258+ }
259+
260+ Keys.onRightPressed: {
261+ if (d.currentItem) {
262+ selectNext(d.currentItem.__index);
263+ }
264+ }
265+
266+ function select(index) {
267+ var delegate = rowRepeater.itemAt(index);
268+ if (delegate) {
269+ d.currentItem = delegate;
270+ }
271+ }
272+
273+ function selectNext(currentIndex) {
274+ var menu;
275+ var newIndex = 0;
276+ if (currentIndex === -1 && rowRepeater.count > 0) {
277+ while (rowRepeater.count > newIndex) {
278+ menu = rowRepeater.itemAt(newIndex);
279+ if (!!menu["enabled"]) {
280+ select(newIndex);
281+ break;
282+ }
283+ newIndex++;
284+ }
285+ } else if (currentIndex !== -1 && rowRepeater.count > 1) {
286+ var startIndex = (currentIndex + 1) % rowRepeater.count;
287+ newIndex = startIndex;
288+ do {
289+ menu = rowRepeater.itemAt(newIndex);
290+ if (!!menu["enabled"]) {
291+ select(newIndex);
292+ break;
293+ }
294+ newIndex = (newIndex + 1) % rowRepeater.count;
295+ } while (newIndex !== startIndex)
296+ }
297+ }
298+
299+ function selectPrevious(currentIndex) {
300+ var menu;
301+ var newIndex = rowRepeater.count-1;
302+ if (currentIndex === -1 && rowRepeater.count > 0) {
303+ while (rowRepeater.count > newIndex) {
304+ menu = rowRepeater.itemAt(newIndex);
305+ if (!!menu["enabled"]) {
306+ select(newIndex);
307+ break;
308+ }
309+ newIndex--;
310+ }
311+ } else if (currentIndex !== -1 && rowRepeater.count > 1) {
312+ var startIndex = currentIndex - 1;
313+ newIndex = startIndex;
314+ do {
315+ if (newIndex < 0) {
316+ newIndex = rowRepeater.count - 1;
317+ }
318+ menu = rowRepeater.itemAt(newIndex);
319+ if (!!menu["enabled"]) {
320+ select(newIndex);
321+ break;
322+ }
323+ newIndex--;
324+ } while (newIndex !== startIndex)
325+ }
326+ }
327+}
qml/ApplicationMenus/MenuItemLoader.qmlnew file+136
@@ -0,0 +1,136 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.3
20+
21+ActionItem {
22+ id: root
23+ implicitHeight: units.gu(5)
24+ implicitWidth: requiredWidth
25+
26+ property var menuData: undefined
27+
28+ readonly property real requiredWidth: {
29+ var val = 0;
30+ val += units.gu(1) + flagGutter.width;
31+ if (iconSource != "") {
32+ val += units.gu(1) + icon.width
33+ }
34+ val += units.gu(1) + title.contentWidth;
35+ if (shortcut != undefined) {
36+ val += units.gu(3) + shortcutLabel.contentWidth;
37+ }
38+ if (hasSubmenu) {
39+ val += units.gu(1) + chevronIcon.width;
40+ }
41+ return val + units.gu(1);
42+ }
43+
44+ readonly property bool hasSubmenu: menuData ? menuData.hasSubmenu : false
45+ readonly property bool _checked : action && action.parameterType === Action.Bool && action.state == true
46+
47+ enabled: menuData ? menuData.sensitive : false
48+
49+ action: Action {
50+ text: menuData.label.replace("_", "&")
51+ }
52+
53+ width: {
54+ if (!parent) return implicitWidth;
55+ if (parent.width > implicitWidth) return parent.width;
56+ return implicitWidth;
57+ }
58+
59+ Keys.onRightPressed: {
60+ if (hasSubmenu) {
61+ root.trigger(action && action.checkable ? !action.checked : undefined);
62+ event.accepted = true;
63+ }
64+ }
65+ Keys.onEnterPressed: {
66+ root.trigger(action && action.checkable ? !action.checked : undefined);
67+ event.accepted = true;
68+ }
69+
70+ RowLayout {
71+ id: row
72+ spacing: units.gu(1)
73+ anchors.left: parent.left
74+ anchors.right: parent.right
75+ anchors.leftMargin: units.gu(1)
76+ anchors.rightMargin: units.gu(1)
77+ anchors.verticalCenter: parent.verticalCenter
78+
79+ Icon {
80+ id: flagGutter
81+ width: units.gu(1.5)
82+ height: units.gu(1.5)
83+
84+ name: _checked ? "tick" : ""
85+ }
86+
87+ Icon {
88+ id: icon
89+ width: units.gu(2)
90+ height: units.gu(2)
91+
92+ visible: root.iconSource != "" || false
93+ source: root.iconSource || ""
94+ }
95+
96+ RowLayout {
97+ spacing: units.gu(3)
98+
99+ Label {
100+ id: title
101+ elide: Text.ElideNone
102+ wrapMode: Text.NoWrap
103+ clip: true
104+ Layout.fillWidth: true
105+
106+ text: root.text ? root.text : ""
107+ }
108+
109+ Label {
110+ id: shortcutLabel
111+ elide: Text.ElideNone
112+ wrapMode: Text.NoWrap
113+ clip: true
114+
115+ visible: root.shortcut != undefined
116+ text: root.shortcut ? root.shortcut : ""
117+ }
118+ }
119+
120+ Icon {
121+ id: chevronIcon
122+ width: units.gu(2)
123+ height: units.gu(2)
124+
125+ visible: root.hasSubmenu
126+ name: "chevron"
127+ }
128+ }
129+
130+ MouseArea {
131+ anchors.fill: parent
132+ onClicked: {
133+ root.trigger(action && action.checkable ? !action.checked : undefined);
134+ }
135+ }
136+}
qml/ApplicationMenus/MenuLoader.qmlnew file+359
@@ -0,0 +1,359 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import QtQuick.Window 2.2
20+import Ubuntu.Components 1.3
21+import Ubuntu.Components.ListItems 1.3 as ListItems
22+import "../Components"
23+
24+UbuntuShape {
25+ id: root
26+ backgroundColor: theme.palette.normal.overlay
27+
28+ property alias unityMenuModel: listView.model
29+
30+ readonly property real __ajustedMinimumHeight: {
31+ if (listView.contentHeight > __minimumHeight) {
32+ return units.gu(30);
33+ }
34+ return Math.max(listView.contentHeight, units.gu(2));
35+ }
36+
37+ readonly property real __minimumWidth: units.gu(20)
38+ readonly property real __minimumHeight: units.gu(30)
39+ readonly property real __maximumWidth: Screen.width * 0.7
40+ readonly property real __maximumHeight: Screen.height * 0.7
41+
42+ function select(index) {
43+ d.select(index)
44+ }
45+
46+ implicitWidth: container.width
47+ implicitHeight: MathUtils.clamp(listView.contentHeight, __ajustedMinimumHeight, __maximumHeight)
48+
49+ focus: true
50+ Keys.onUpPressed: d.selectPrevious(d.currentIndex)
51+ Keys.onDownPressed: d.selectNext(d.currentIndex)
52+
53+ onVisibleChanged: {
54+ if (!visible) {
55+ d.currentItem = null;
56+ }
57+ }
58+
59+ QtObject {
60+ id: d
61+
62+ property Item currentItem: null
63+ property Item hoveredItem: null
64+ readonly property int currentIndex: currentItem ? currentItem.__ownIndex : -1
65+
66+ signal select(int index)
67+
68+ onCurrentItemChanged: {
69+ if (currentItem) {
70+ currentItem.forceActiveFocus();
71+ } else {
72+ hoveredItem = null;
73+ }
74+ }
75+
76+ onSelect: {
77+ currentItem = listView.itemAt(index);
78+ }
79+
80+ function selectNext(currentIndex) {
81+ var menu;
82+ var newIndex = 0;
83+ if (currentIndex === -1 && listView.count > 0) {
84+ while (listView.count > newIndex) {
85+ menu = listView.itemAt(newIndex);
86+ if (!!menu["enabled"]) {
87+ select(newIndex);
88+ break;
89+ }
90+ newIndex++;
91+ }
92+ } else if (currentIndex !== -1 && listView.count > 1) {
93+ var startIndex = (currentIndex + 1) % listView.count;
94+ newIndex = startIndex;
95+ do {
96+ menu = listView.itemAt(newIndex);
97+ if (!!menu["enabled"]) {
98+ select(newIndex);
99+ break;
100+ }
101+ newIndex = (newIndex + 1) % listView.count;
102+ } while (newIndex !== startIndex)
103+ }
104+ }
105+
106+ function selectPrevious(currentIndex) {
107+ var menu;
108+ var newIndex = listView.count-1;
109+ if (currentIndex === -1 && listView.count > 0) {
110+ while (listView.count > newIndex) {
111+ menu = listView.itemAt(newIndex);
112+ if (!!menu["enabled"]) {
113+ select(newIndex);
114+ break;
115+ }
116+ newIndex--;
117+ }
118+ } else if (currentIndex !== -1 && listView.count > 1) {
119+ var startIndex = currentIndex - 1;
120+ newIndex = startIndex;
121+ do {
122+ if (newIndex < 0) {
123+ newIndex = listView.count - 1;
124+ }
125+ menu = listView.itemAt(newIndex);
126+ if (!!menu["enabled"]) {
127+ select(newIndex);
128+ break;
129+ }
130+ newIndex--;
131+ } while (newIndex !== startIndex)
132+ }
133+ }
134+ }
135+
136+ ColumnLayout {
137+ id: container
138+ objectName: "container"
139+
140+ width: listView.contentWidth
141+ height: parent.height
142+ spacing: 0
143+
144+ // FIXME use ListView.header - tried but was flaky with positionViewAtIndex.
145+ Item {
146+ Layout.fillWidth: true;
147+ Layout.maximumHeight: units.gu(3)
148+ Layout.minimumHeight: units.gu(3)
149+ visible: listView.contentHeight > root.height
150+ enabled: !listView.atYBeginning
151+
152+ Rectangle {
153+ color: enabled ? theme.palette.normal.overlayText :
154+ theme.palette.disabled.overlayText
155+ height: units.dp(1)
156+ anchors {
157+ bottom: parent.bottom
158+ left: parent.left
159+ right: parent.right
160+ }
161+ }
162+
163+ Icon {
164+ anchors.centerIn: parent
165+ width: units.gu(2)
166+ height: units.gu(2)
167+ name: "up"
168+ color: enabled ? theme.palette.normal.overlayText :
169+ theme.palette.disabled.overlayText
170+ }
171+
172+ MouseArea {
173+ anchors.fill: parent
174+ onPressed: {
175+ var index = listView.indexAt(0, listView.contentY);
176+ listView.positionViewAtIndex(index-1, ListView.Beginning);
177+ }
178+ }
179+ }
180+
181+ ListView {
182+ id: listView
183+ objectName: "listView"
184+ Layout.fillHeight: true
185+ Layout.fillWidth: true
186+ contentWidth: MathUtils.clamp(contentItem.childrenRect.width,
187+ __minimumWidth,
188+ __maximumWidth)
189+
190+ orientation: Qt.Vertical
191+ interactive: contentHeight > height
192+ clip: interactive
193+ highlightFollowsCurrentItem: false
194+
195+ highlight: Rectangle {
196+ color: "transparent"
197+ border.width: units.dp(1)
198+ border.color: UbuntuColors.orange
199+ z: 1
200+
201+ width: listView.width
202+ height: d.currentItem ? d.currentItem.height : 0
203+ y: d.currentItem ? d.currentItem.y : 0
204+ visible: d.currentItem
205+ }
206+
207+ function itemAt(index) {
208+ if (index > count || index < 0) return null;
209+ currentIndex = index;
210+ return currentItem;
211+ }
212+
213+ MouseArea {
214+ id: menuMouseArea
215+ anchors.fill: parent
216+ hoverEnabled: true
217+ z: -1
218+
219+ onPositionChanged: updateCurrentItemFromPosition(mouse)
220+
221+ function updateCurrentItemFromPosition(point) {
222+ var pos = mapToItem(listView.contentItem, point.x, point.y);
223+
224+ if (!d.hoveredItem || !d.currentItem ||
225+ !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
226+ d.hoveredItem = listView.itemAt(listView.indexAt(pos.x, pos.y));
227+ if (!d.hoveredItem || !d.hoveredItem.enabled)
228+ return false;
229+ d.currentItem = d.hoveredItem;
230+ }
231+ return true;
232+ }
233+ }
234+
235+ delegate: Loader {
236+ id: loader
237+ width: root.width
238+
239+ enabled: model.isSeparator ? false : model.sensitive
240+
241+ sourceComponent: {
242+ if (model.isSeparator) {
243+ return separatorComponent;
244+ }
245+ return menuItemComponent;
246+ }
247+ property int __ownIndex: index
248+
249+ Keys.forwardTo: [ item ]
250+
251+ Component {
252+ id: menuItemComponent
253+ MenuItemLoader {
254+ id: menuItem
255+ menuData: model
256+
257+ property Item popup: null
258+
259+ onTriggered: {
260+ if (hasSubmenu) {
261+ if (!popup) {
262+ var model = root.unityMenuModel.submenu(__ownIndex);
263+ popup = submenuComponent.createObject(root, {
264+ "unityMenuModel": model,
265+ "x": Qt.binding(function() { return root.width }),
266+ "y": Qt.binding(function() { return loader.y })
267+ });
268+ } else if (popup) {
269+ popup.visible = true;
270+ }
271+ popup.forceActiveFocus();
272+ popup.dismiss.connect(function() {
273+ popup.destroy();
274+ popup = null;
275+ loader.forceActiveFocus();
276+ })
277+ } else {
278+ root.unityMenuModel.activate(__ownIndex);
279+ }
280+ }
281+
282+ Connections {
283+ target: d
284+ onCurrentIndexChanged: {
285+ if (menuItem.popup && d.currentIndex != __ownIndex) {
286+ menuItem.popup.visible = false;
287+ }
288+ }
289+ }
290+ }
291+ }
292+
293+ Component {
294+ id: separatorComponent
295+ ListItems.ThinDivider {
296+ }
297+ }
298+ }
299+
300+ } // ListView
301+
302+ // FIXME use ListView.footer - tried but was flaky with positionViewAtIndex.
303+ Item {
304+ Layout.fillWidth: true;
305+ Layout.maximumHeight: units.gu(3)
306+ Layout.minimumHeight: units.gu(3)
307+ visible: listView.contentHeight > root.height
308+ enabled: !listView.atYEnd
309+
310+ Rectangle {
311+ color: enabled ? theme.palette.normal.overlayText :
312+ theme.palette.disabled.overlayText
313+ height: units.dp(1)
314+ anchors {
315+ top: parent.top
316+ left: parent.left
317+ right: parent.right
318+ }
319+ }
320+
321+ Icon {
322+ anchors.centerIn: parent
323+ width: units.gu(2)
324+ height: units.gu(2)
325+ name: "down"
326+ color: enabled ? theme.palette.normal.overlayText :
327+ theme.palette.disabled.overlayText
328+ }
329+
330+ MouseArea {
331+ anchors.fill: parent
332+ onPressed: {
333+ var index = listView.indexAt(0, listView.contentY);
334+ listView.positionViewAtIndex(index+1, ListView.Beginning);
335+ }
336+ }
337+ }
338+ } // Column
339+
340+ Component {
341+ id: submenuComponent
342+ Loader {
343+ id: submenuLoader
344+ source: "MenuLoader.qml"
345+
346+ property var unityMenuModel: null
347+ signal dismiss()
348+
349+ Binding {
350+ target: item
351+ property: "unityMenuModel"
352+ value: submenuLoader.unityMenuModel
353+ }
354+
355+ onLoaded: item.select(0)
356+ Keys.onLeftPressed: dismiss()
357+ }
358+ }
359+}
qml/ApplicationMenus/RegisteredApplicationMenuModel.qmlnew file+56
@@ -0,0 +1,56 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import Unity.Indicators 0.1
20+import Unity.ApplicationMenu 0.1
21+import Unity.Indicators 0.1 as Indicators
22+
23+Object {
24+ property string persistentSurfaceId
25+ readonly property alias model: sharedAppModel.model
26+
27+ Indicators.SharedUnityMenuModel {
28+ id: sharedAppModel
29+ property var menus: ApplicationMenuRegistry.getMenusForSurface(persistentSurfaceId)
30+ property var menuService: menus.length > 0 ? menus[0] : null
31+
32+ busName: menuService ? menuService.service : ""
33+ menuObjectPath: menuService && menuService.menuPath ? menuService.menuPath : ""
34+ actions: menuService && menuService.actionPath ? { "unity": menuService.actionPath } : {}
35+ }
36+
37+ onPersistentSurfaceIdChanged: update()
38+
39+ function update() {
40+ sharedAppModel.menus = Qt.binding(function() { return ApplicationMenuRegistry.getMenusForSurface(persistentSurfaceId); });
41+ }
42+
43+ Connections {
44+ target: ApplicationMenuRegistry
45+ onSurfaceMenuRegistered: {
46+ if (surfaceId === persistentSurfaceId) {
47+ update();
48+ }
49+ }
50+ onSurfaceMenuUnregistered: {
51+ if (surfaceId === persistentSurfaceId) {
52+ update();
53+ }
54+ }
55+ }
56+}
qml/ApplicationMenus/Test.qmlnew file+81
@@ -0,0 +1,81 @@
1+import QtQuick 2.4
2+import QtQuick.Window 2.2
3+import QMenuModel 0.1
4+import Ubuntu.Components 1.3
5+import Unity.ApplicationMenu 0.1
6+import Unity.Indicators 0.1 as Indicators
7+import "../Components"
8+
9+Window {
10+ id: root
11+ width: units.gu(100)
12+ height: units.gu(100)
13+ visible: true
14+
15+ ListModel {
16+ id: surfaceIds
17+ }
18+
19+ Flickable {
20+ anchors.fill: parent
21+ contentHeight: row.height
22+
23+ Row {
24+ id: row
25+ anchors {
26+ left: parent.left
27+ right: parent.right
28+ }
29+ spacing: units.gu(1)
30+
31+ Repeater {
32+ model: surfaceIds
33+
34+ TextArea {
35+ id: textArea
36+ text: printer.text
37+ autoSize: true
38+ maximumLineCount: 20
39+
40+ anchors {
41+ left: parent.left
42+ right: parent.right
43+ margins: units.gu(1)
44+ }
45+ height: implicitHeight
46+
47+ Indicators.SharedUnityMenuModel {
48+ id: sharedAppModel
49+ property var menus: ApplicationMenuRegistry.getMenusForSurface(surfaceId)
50+ property var menuService: menus.length > 0 ? menus[0] : null
51+
52+ busName: menuService ? menuService.service : ""
53+ menuObjectPath: menuService && menuService.menuPath ? menuService.menuPath : ""
54+ actions: menuService && menuService.actionPath ? { "unity": menuService.actionPath } : {}
55+
56+ onBusNameChanged: console.log("BUS NAME", busName)
57+ onMenuObjectPathChanged: console.log("MENU PATH", menuObjectPath)
58+ onActionsChanged: console.log("ACTIONS", menuService.actionPath)
59+ }
60+
61+ Indicators.ModelPrinter {
62+ id: printer
63+ model: sharedAppModel.model
64+ }
65+ }
66+ }
67+ }
68+ }
69+
70+ Connections {
71+ target: ApplicationMenuRegistry
72+ onSurfaceMenuRegistered: {
73+ console.log("REGISTERED SURFACE!", surfaceId)
74+ surfaceIds.append({ "surfaceId": surfaceId });
75+ }
76+
77+ onAppMenuRegistered: {
78+ console.log("REGISTERED PROCESS!", processId);
79+ }
80+ }
81+}
qml/Panel/FakePanelMenu.qmlnew file+26
@@ -0,0 +1,26 @@
1+/*
2+ * Copyright (C) 2013-2015 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+import QtQuick 2.4
18+
19+QtObject {
20+ property list<QtObject> hides
21+ property QtObject model: null
22+ property bool expanded: false
23+
24+ signal hide()
25+ signal show()
26+}
qml/Panel/PanelItemRow.qml+1
@@ -16,6 +16,7 @@
1616
1717import QtQuick 2.4
1818import Ubuntu.Components 1.3
19+import "../Components"
1920
2021Item {
2122 id: root
tests/qmltests/ApplicationMenus/tst_MenuBar.qmlnew file+137
@@ -0,0 +1,137 @@
1+/*
2+ * Copyright (C) 2016 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+import QtQuick 2.4
18+import QtTest 1.0
19+import Ubuntu.Components 1.3
20+import Ubuntu.Components.ListItems 1.3
21+import Unity.Application 0.1
22+import QMenuModel 0.1
23+import Unity.Test 0.1
24+import Utils 0.1
25+
26+import "../../../qml/ApplicationMenus"
27+import "../Stages"
28+
29+Item {
30+ id: root
31+ width: units.gu(100)
32+ height: units.gu(50)
33+
34+ Component.onCompleted: {
35+ QuickUtils.keyboardAttached = true;
36+ theme.name = "Ubuntu.Components.Themes.SuruDark"
37+ }
38+
39+ Binding {
40+ target: MouseTouchAdaptor
41+ property: "enabled"
42+ value: false
43+ }
44+
45+ DesktopMenuData { id: desktopMenuData }
46+
47+ Rectangle {
48+ anchors {
49+ left: parent.left
50+ right: parent.right
51+ top: parent.top
52+ margins: units.gu(1)
53+ }
54+ height: units.gu(3)
55+ color: "grey"
56+
57+ MenuBarLoader {
58+ id: menuBar
59+ anchors.fill: parent
60+
61+ unityMenuModel: UnityMenuModel {
62+ id: menuBackend
63+ modelData: desktopMenuData.testData
64+ }
65+ }
66+ }
67+
68+ SignalSpy {
69+ id: activatedSpy
70+ target: menuBackend
71+ signalName: "activated"
72+ }
73+
74+ UnityTestCase {
75+ id: testCase
76+ name: "MenuPage"
77+ when: windowShown
78+
79+ property bool clickNavigate: true
80+
81+ function init() {
82+ menuBar.closePopup();
83+ menuBackend.modelData = desktopMenuData.generateTestData(3, 2, 0);
84+ activatedSpy.clear();
85+ }
86+
87+ function test_mnemonics_data() {
88+ return [
89+ { tag: "a" },
90+ { tag: "b" },
91+ ]
92+ }
93+
94+ function test_mnemonics(data) {
95+ menuBackend.modelData = desktopMenuData.generateTestData(3, 2, 0);
96+
97+ keyPress(data.tag, Qt.AltModifier, 100);
98+ tryCompareFunction(function() { return menuBar.openItem !== undefined; }, true);
99+ }
100+
101+ function test_navigateRight(data) {
102+ var menuItem0 = findChild(menuBar, "menuBar-menu0"); verify(menuItem0);
103+ var menuItem1 = findChild(menuBar, "menuBar-menu1"); verify(menuItem1);
104+ var menuItem2 = findChild(menuBar, "menuBar-menu2"); verify(menuItem2);
105+
106+ menuBar.open(menuItem0, true);
107+ compare(menuBar.openItem, menuItem0);
108+
109+ keyClick(Qt.Key_Right, Qt.NoModifier);
110+ compare(menuBar.openItem, menuItem1);
111+
112+ keyClick(Qt.Key_Right, Qt.NoModifier);
113+ compare(menuBar.openItem, menuItem2);
114+
115+ keyClick(Qt.Key_Right, Qt.NoModifier);
116+ compare(menuBar.openItem, menuItem0);
117+ }
118+
119+ function test_navigateLeft(data) {
120+ var menuItem0 = findChild(menuBar, "menuBar-menu0"); verify(menuItem0);
121+ var menuItem1 = findChild(menuBar, "menuBar-menu1"); verify(menuItem1);
122+ var menuItem2 = findChild(menuBar, "menuBar-menu2"); verify(menuItem2);
123+
124+ menuBar.open(menuItem0, true);
125+ compare(menuBar.openItem, menuItem0);
126+
127+ keyClick(Qt.Key_Left, Qt.NoModifier);
128+ compare(menuBar.openItem, menuItem2);
129+
130+ keyClick(Qt.Key_Left, Qt.NoModifier);
131+ compare(menuBar.openItem, menuItem1);
132+
133+ keyClick(Qt.Key_Left, Qt.NoModifier);
134+ compare(menuBar.openItem, menuItem0);
135+ }
136+ }
137+}
tests/qmltests/ApplicationMenus/tst_MenuPage.qmlnew file+258
@@ -0,0 +1,258 @@
1+/*
2+ * Copyright (C) 2016 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+import QtQuick 2.4
18+import QtTest 1.0
19+import Ubuntu.Components 1.3
20+import Ubuntu.Components.ListItems 1.3
21+import Unity.Application 0.1
22+import QMenuModel 0.1
23+import Unity.Test 0.1
24+import Utils 0.1
25+
26+import "../../../qml/ApplicationMenus"
27+import "../Stages"
28+
29+Item {
30+ id: root
31+ width: Math.max(units.gu(100), page.width + units.gu(6))
32+ height: Math.max(units.gu(50), page.height + units.gu(6))
33+
34+ Binding {
35+ target: MouseTouchAdaptor
36+ property: "enabled"
37+ value: false
38+ }
39+
40+ DesktopMenuData { id: desktopMenuData }
41+
42+ Keys.onEscapePressed: {
43+ page.closePopup(true);
44+ }
45+
46+ MenuPage {
47+ id: page
48+ focus: true
49+
50+ anchors {
51+ left: parent.left
52+ top: parent.top
53+ leftMargin: units.gu(3)
54+ topMargin: units.gu(3)
55+ }
56+
57+ delegateModel: rootDelegate.submenuItems
58+ MenuItemDelegateBase {
59+ id: rootDelegate
60+ menuModel: UnityMenuModel {
61+ id: menuBackend
62+ modelData: desktopMenuData.generateTestData(3, 3, 0);
63+ }
64+ }
65+ }
66+
67+ SignalSpy {
68+ id: activatedSpy
69+ target: menuBackend
70+ signalName: "activated"
71+ }
72+
73+ UnityTestCase {
74+ id: testCase
75+ name: "MenuPage"
76+ when: windowShown
77+
78+ property bool clickNavigate: true
79+
80+ function init() {
81+ page.closePopup(true);
82+ menuBackend.modelData = desktopMenuData.generateTestData(3, 3, 0);
83+ activatedSpy.clear();
84+ }
85+
86+ // visit and verify that all the backend menus have been created
87+ function recurseMenuConstruction(rows, menuPageName) {
88+ for (var i = 0; i < rows.length; ++i) {
89+ var rowData = rows[i]["rowData"];
90+
91+ var menuPage = findChild(page, menuPageName);
92+ verify(menuPage);
93+ var menuItem = findChild(menuPage, menuPageName+"-menu"+i);
94+ verify(menuItem);
95+
96+ // recurse into submenu
97+ var submenu = rows[i]["submenu"];
98+ if (submenu) {
99+ if (clickNavigate) {
100+ mouseClick(menuItem, menuItem.width/2, menuItem.height/2);
101+ } else {
102+ mouseMove(menuItem, menuItem.width/2, menuItem.height/2);
103+ }
104+ tryCompare(menuPage, "openItem", menuItem);
105+ recurseMenuConstruction(submenu, menuPageName+"-subMenu"+i);
106+ }
107+ }
108+ }
109+
110+ function test_hoverNavigation_data() {
111+ return [
112+ { tag: "long", testData: desktopMenuData.generateTestData(4, 2, 0) },
113+ { tag: "deep", testData: desktopMenuData.generateTestData(2, 4, 0) }
114+ ]
115+ }
116+
117+ function test_hoverNavigation(data) {
118+ clickNavigate = false;
119+ menuBackend.modelData = data.testData;
120+
121+ recurseMenuConstruction(data.testData, "menuPage");
122+ }
123+
124+ function test_clickNavigation_data() {
125+ return [
126+ { tag: "long", testData: desktopMenuData.generateTestData(4, 2, 0) },
127+ { tag: "deep", testData: desktopMenuData.generateTestData(2, 4, 0) }
128+ ]
129+ }
130+
131+ function test_clickNavigation(data) {
132+ clickNavigate = true;
133+ menuBackend.modelData = data.testData;
134+
135+ recurseMenuConstruction(data.testData, "menuPage");
136+ }
137+
138+ function test_checkableMenuTogglesOnClick() {
139+ menuBackend.modelData = desktopMenuData.singleCheckable;
140+
141+ var menuItem = findChild(page, "menuPage-menu0");
142+ verify(menuItem);
143+ verify(menuItem.delegate.isCheck);
144+ verify(menuItem.delegate.isToggled === false);
145+
146+ mouseClick(menuItem, menuItem.width/2, menuItem.height/2);
147+
148+ compare(menuItem.delegate.isToggled, true, "Checkable menu should have toggled");
149+ }
150+
151+ function test_keyboardNavigation_DownKeySelectsAndOpensNextMenuItemAndRotates() {
152+ menuBackend.modelData = desktopMenuData.generateTestData(4, 2, 3);
153+ var listView = findChild(page, "menuPage-ListView");
154+ verify(listView);
155+
156+ var menuItem0 = findChild(page, "menuPage-menu0"); verify(menuItem0);
157+ var menuItem1 = findChild(page, "menuPage-menu1"); verify(menuItem1);
158+ var menuItem2 = findChild(page, "menuPage-menu2"); verify(menuItem2);
159+ verify(menuItem2.delegate.isSeparator);
160+ var menuItem3 = findChild(page, "menuPage-menu3"); verify(menuItem3);
161+
162+ keyClick(Qt.Key_Down, Qt.NoModifier, 100);
163+ compare(listView.selectedItem, menuItem0);
164+ tryCompare(page, "openItem", menuItem0);
165+
166+ keyClick(Qt.Key_Down, Qt.NoModifier, 100);
167+ compare(listView.selectedItem, menuItem1);
168+ tryCompare(page, "openItem", menuItem1);
169+
170+ // Skip separator
171+
172+ keyClick(Qt.Key_Down, Qt.NoModifier, 100);
173+ compare(listView.selectedItem, menuItem3);
174+ tryCompare(page, "openItem", menuItem3);
175+
176+ keyClick(Qt.Key_Down, Qt.NoModifier, 100);
177+ compare(listView.selectedItem, menuItem0);
178+ tryCompare(page, "openItem", menuItem0);
179+ }
180+
181+ function test_keyboardNavigation_UpKeySelectsAndOpensPreviousMenuItemAndRotates() {
182+ menuBackend.modelData = desktopMenuData.generateTestData(4, 2, 3);
183+ var listView = findChild(page, "menuPage-ListView");
184+ verify(listView);
185+
186+ var menuItem0 = findChild(page, "menuPage-menu0"); verify(menuItem0);
187+ var menuItem1 = findChild(page, "menuPage-menu1"); verify(menuItem1);
188+ var menuItem2 = findChild(page, "menuPage-menu2"); verify(menuItem2);
189+ verify(menuItem2.delegate.isSeparator);
190+ var menuItem3 = findChild(page, "menuPage-menu3"); verify(menuItem3);
191+
192+ keyClick(Qt.Key_Up, Qt.NoModifier, 100);
193+ compare(listView.selectedItem, menuItem3);
194+ tryCompare(page, "openItem", menuItem3);
195+
196+ // Skip separator
197+
198+ keyClick(Qt.Key_Up, Qt.NoModifier, 100);
199+ compare(listView.selectedItem, menuItem1);
200+ tryCompare(page, "openItem", menuItem1);
201+
202+ keyClick(Qt.Key_Up, Qt.NoModifier, 100);
203+ compare(listView.selectedItem, menuItem0);
204+ tryCompare(page, "openItem", menuItem0);
205+
206+ keyClick(Qt.Key_Up, Qt.NoModifier, 100);
207+ compare(listView.selectedItem, menuItem3);
208+ tryCompare(page, "openItem", menuItem3);
209+ }
210+
211+ function test_keyboardNavigation_RightKeyEntersSubMenu() {
212+ menuBackend.modelData = desktopMenuData.generateTestData(2, 2, 0);
213+
214+ var menuItem = findChild(page, "menuPage-menu0");
215+ verify(menuItem);
216+ page.open(menuItem, true);
217+ compare(page.openItem, menuItem);
218+
219+ var submenu = findChild(page, "menuPage-subMenu0");
220+ verify(submenu);
221+ var listView = findChild(page, "menuPage-subMenu0-ListView");
222+ verify(listView);
223+ menuItem = findChild(page, "menuPage-subMenu0-menu0");
224+ verify(menuItem);
225+
226+ keyClick(Qt.Key_Right, Qt.NoModifier);
227+ compare(listView.selectedItem, menuItem);
228+ }
229+
230+ function test_keyboardNavigation_LeftKeyClosesSubMenu() {
231+ menuBackend.modelData = desktopMenuData.generateTestData(2, 2, 0);
232+
233+ var menuItem = findChild(page, "menuPage-menu0");
234+ verify(menuItem);
235+ page.open(menuItem, true, 0); // quick open & select item 0
236+
237+ compare(page.openItem, menuItem);
238+ keyClick(Qt.Key_Left, Qt.NoModifier);
239+ compare(page.openItem, undefined);
240+ }
241+
242+ function test_mnemonics() {
243+ var menuItem0 = findChild(page, "menuPage-menu0"); verify(menuItem0);
244+
245+ keyClick(Qt.Key_A, Qt.NoModifier);
246+ tryCompare(page, "openItem", menuItem0);
247+
248+ var submenu0 = findChild(menuItem0, "menuPage-subMenu0"); verify(submenu0);
249+ var submenu0_menuItem1 = findChild(submenu0, "menuPage-subMenu0-menu1"); verify(submenu0_menuItem1);
250+
251+ keyClick(Qt.Key_B, Qt.NoModifier);
252+ tryCompare(submenu0, "openItem", submenu0_menuItem1);
253+
254+ keyClick(Qt.Key_B, Qt.NoModifier);
255+ compare(activatedSpy.signalArguments, [{ "0": "menuA.B.B" }], "Activate should have been emmited once");
256+ }
257+ }
258+}
tests/qmltests/Stages/DesktopMenuData.qmlnew file+549
@@ -0,0 +1,549 @@
1+import QtQuick 2.4
2+
3+QtObject {
4+
5+ property var dialerData: [{
6+ "rowData": { // 1.1
7+ "label": "&dialer1",
8+ "action": "dialer1",
9+ "shortcut": "Alt+F"
10+ },
11+ "submenu": [{
12+ "rowData": { // 1.1
13+ "label": "menu1.1",
14+ "action": "menu1.1",
15+ "shortcut": "Alt+0"
16+ }}, {
17+ "rowData": { // 1.2
18+ "label": "menu1.2",
19+ "action": "menu1.2",
20+ "shortcut": "Alt+1"
21+ },
22+ "submenu": [{
23+ "rowData": { // 1.2.1
24+ "label": "menu1.2.1",
25+ "action": "menu1.2.1",
26+ "shortcut": ""
27+ }}, {
28+ "rowData": { // 1.2.2
29+ "label": "menu1.2.2",
30+ "action": "menu1.2.2"
31+ }}, {
32+ "rowData": { // 1.2.3
33+ "sensitive": false,
34+ "isSeparator": true
35+ }}, {
36+ "rowData": { // row 1.2.4
37+ "label": "menu1.2.4",
38+ "action": "menu1.2.4",
39+ "isToggled": true
40+ }}
41+ ]}, {
42+ "rowData": { // 1.3
43+ "sensitive": false,
44+ "isSeparator": true
45+ }}, {
46+ "rowData": { // row 1.4
47+ "label": "menu1.4",
48+ "action": "menu1.4",
49+ "shortcut": "Alt+2"
50+ }}
51+ ]}, {
52+ "rowData": { // 2
53+ "label": "d&ialer2",
54+ "action": "dialer2",
55+ "shortcut": "Alt+E"
56+ },
57+ "submenu": [{
58+ "rowData": { // 2.1
59+ "label": "menu2.1",
60+ "action": "menu2.1"
61+ }}
62+ ]}, {
63+ "rowData": { // row 3
64+ "label": "di&aler3",
65+ "action": "dialer3"
66+ },
67+ "submenu": [{
68+ "rowData": { // 3.1
69+ "label": "menu3.1",
70+ "action": "menu3.1"
71+ }}
72+ ]}
73+ ]
74+
75+ property var cameraData: [{
76+ "rowData": { // 1.1
77+ "label": "camera1",
78+ "sensitive": true,
79+ "isSeparator": false,
80+ "icon": "",
81+ "ext": {},
82+ "action": "camera1",
83+ "actionState": {},
84+ "isCheck": false,
85+ "isRadio": false,
86+ "isToggled": false,
87+ "shortcut": ""
88+ },
89+ "submenu": [{
90+ "rowData": { // 1.1
91+ "label": "menu1.1",
92+ "sensitive": true,
93+ "isSeparator": false,
94+ "icon": "",
95+ "ext": {},
96+ "action": "menu1.1",
97+ "actionState": {},
98+ "isCheck": false,
99+ "isRadio": false,
100+ "isToggled": false,
101+ "shortcut": ""
102+ }}, {
103+ "rowData": { // 1.2
104+ "label": "menu1.2",
105+ "sensitive": true,
106+ "isSeparator": false,
107+ "icon": "",
108+ "ext": {},
109+ "action": "menu1.2",
110+ "actionState": {},
111+ "isCheck": false,
112+ "isRadio": false,
113+ "isToggled": false,
114+ "shortcut": ""
115+ }}, {
116+ "rowData": { // row 1.2
117+ "label": "menu1.2",
118+ "sensitive": true,
119+ "isSeparator": false,
120+ "icon": "",
121+ "ext": {},
122+ "action": "menu1.2",
123+ "actionState": {},
124+ "isCheck": false,
125+ "isRadio": false,
126+ "isToggled": false,
127+ "shortcut": ""
128+ }}
129+ ]}, {
130+ "rowData": { // 2
131+ "label": "camera2",
132+ "sensitive": true,
133+ "isSeparator": false,
134+ "icon": "",
135+ "ext": {},
136+ "action": "camera2",
137+ "actionState": {},
138+ "isCheck": false,
139+ "isRadio": false,
140+ "isToggled": false,
141+ "shortcut": ""
142+ }}
143+ ]
144+
145+ property var galleryData: [{
146+ "rowData": { // 1.1
147+ "label": "gallery1",
148+ "sensitive": true,
149+ "isSeparator": false,
150+ "icon": "",
151+ "ext": {},
152+ "action": "gallery1",
153+ "actionState": {},
154+ "isCheck": false,
155+ "isRadio": false,
156+ "isToggled": false,
157+ "shortcut": ""
158+ },
159+ "submenu": [{
160+ "rowData": { // 1.1
161+ "label": "menu0",
162+ "sensitive": true,
163+ "isSeparator": false,
164+ "icon": "",
165+ "ext": {},
166+ "action": "menu0",
167+ "actionState": {},
168+ "isCheck": false,
169+ "isRadio": false,
170+ "isToggled": false,
171+ "shortcut": ""
172+ }}, {
173+ "rowData": { // 1.2
174+ "label": "menu1",
175+ "sensitive": true,
176+ "isSeparator": false,
177+ "icon": "",
178+ "ext": {},
179+ "action": "menu1",
180+ "actionState": {},
181+ "isCheck": false,
182+ "isRadio": false,
183+ "isToggled": false,
184+ "shortcut": ""
185+ }}, {
186+ "rowData": { // 1.2
187+ "label": "",
188+ "sensitive": false,
189+ "isSeparator": true,
190+ "icon": "",
191+ "type": "",
192+ "ext": {},
193+ "action": "",
194+ "actionState": {},
195+ "isCheck": false,
196+ "isRadio": false,
197+ "isToggled": false,
198+ "shortcut": ""
199+ }}, {
200+ "rowData": { // row 1.2
201+ "label": "menu2",
202+ "sensitive": true,
203+ "isSeparator": false,
204+ "icon": "",
205+ "ext": {},
206+ "action": "menu2",
207+ "actionState": {},
208+ "isCheck": false,
209+ "isRadio": false,
210+ "isToggled": false,
211+ "shortcut": ""
212+ }}
213+ ]}, {
214+ "rowData": { // 2
215+ "label": "gallery2",
216+ "sensitive": true,
217+ "isSeparator": false,
218+ "icon": "",
219+ "ext": {},
220+ "action": "gallery2",
221+ "actionState": {},
222+ "isCheck": false,
223+ "isRadio": false,
224+ "isToggled": false,
225+ "shortcut": ""
226+ }}, {
227+ "rowData": { // row 2
228+ "label": "gallery3",
229+ "sensitive": true,
230+ "isSeparator": false,
231+ "icon": "",
232+ "ext": {},
233+ "action": "gallery3",
234+ "actionState": {},
235+ "isCheck": false,
236+ "isRadio": false,
237+ "isToggled": false,
238+ "shortcut": ""
239+ }}
240+ ]
241+
242+ property var testData: [{
243+ "rowData": { // 1
244+ "label": "_menu1",
245+ "sensitive": true,
246+ "isSeparator": false,
247+ "icon": "",
248+ "ext": {},
249+ "action": "menu1",
250+ "actionState": {},
251+ "isCheck": false,
252+ "isRadio": false,
253+ "isToggled": false,
254+ "shortcut": "Alt+F"
255+ },
256+ "submenu": [{
257+ "rowData": { // 1.1
258+ "label": "menu1.1",
259+ "sensitive": true,
260+ "isSeparator": false,
261+ "icon": "",
262+ "ext": {},
263+ "action": "menu1.1",
264+ "actionState": {},
265+ "isCheck": false,
266+ "isRadio": false,
267+ "isToggled": false,
268+ "shortcut": "Alt+0"
269+ }}, {
270+ "rowData": { // 1.2
271+ "label": "m_enu1.2",
272+ "sensitive": true,
273+ "isSeparator": false,
274+ "icon": "",
275+ "ext": {},
276+ "action": "menu1.2",
277+ "actionState": {},
278+ "isCheck": false,
279+ "isRadio": false,
280+ "isToggled": false,
281+ "shortcut": "Alt+1"
282+ },
283+ "submenu": [{
284+ "rowData": { // 1.2.1
285+ "label": "menu1.2.1",
286+ "sensitive": true,
287+ "isSeparator": false,
288+ "icon": "",
289+ "ext": {},
290+ "action": "menu1.2.1",
291+ "actionState": {},
292+ "isCheck": false,
293+ "isRadio": false,
294+ "isToggled": false,
295+ "shortcut": ""
296+ }}, {
297+ "rowData": { // 1.2.2
298+ "label": "men_u1.2.2",
299+ "sensitive": true,
300+ "isSeparator": false,
301+ "icon": "",
302+ "ext": {},
303+ "action": "menu1.2.2",
304+ "actionState": {},
305+ "isCheck": false,
306+ "isRadio": false,
307+ "isToggled": false,
308+ "shortcut": ""
309+ }}, {
310+ "rowData": { // 1.2.3
311+ "label": "",
312+ "sensitive": false,
313+ "isSeparator": true,
314+ "icon": "",
315+ "type": "",
316+ "ext": {},
317+ "action": "",
318+ "actionState": {},
319+ "isCheck": false,
320+ "isRadio": false,
321+ "isToggled": false,
322+ "shortcut": ""
323+ }}, {
324+ "rowData": { // row 1.2.4
325+ "label": "menu1.2.4",
326+ "sensitive": true,
327+ "isSeparator": false,
328+ "icon": "",
329+ "ext": {},
330+ "action": "menu1.2.4",
331+ "actionState": {},
332+ "isCheck": false,
333+ "isRadio": false,
334+ "isToggled": true,
335+ "shortcut": ""
336+ }}
337+ ]}, {
338+ "rowData": { // 1.3
339+ "label": "",
340+ "sensitive": false,
341+ "isSeparator": true,
342+ "icon": "",
343+ "type": "",
344+ "ext": {},
345+ "action": "",
346+ "actionState": {},
347+ "isCheck": false,
348+ "isRadio": false,
349+ "isToggled": false,
350+ "shortcut": ""
351+ }}, {
352+ "rowData": { // row 1.4
353+ "label": "menu1.4",
354+ "sensitive": true,
355+ "isSeparator": false,
356+ "icon": "",
357+ "ext": {},
358+ "action": "menu1.4",
359+ "actionState": {},
360+ "isCheck": true,
361+ "isRadio": false,
362+ "isToggled": true,
363+ "shortcut": "Alt+2"
364+ }}
365+ ]}, {
366+ "rowData": { // 2
367+ "label": "menu2",
368+ "sensitive": true,
369+ "isSeparator": false,
370+ "icon": "",
371+ "ext": {},
372+ "action": "menu2",
373+ "actionState": {},
374+ "isCheck": false,
375+ "isRadio": false,
376+ "isToggled": false,
377+ "shortcut": "Alt+E"
378+ },
379+ "submenu": [{
380+ "rowData": { // 2.1
381+ "label": "menu2.1",
382+ "sensitive": true,
383+ "isSeparator": false,
384+ "icon": "",
385+ "ext": {},
386+ "action": "menu2.1",
387+ "actionState": {},
388+ "isCheck": false,
389+ "isRadio": false,
390+ "isToggled": false,
391+ "shortcut": ""
392+ }}
393+ ]}, {
394+ "rowData": { // row 3
395+ "label": "me_nu3",
396+ "sensitive": true,
397+ "isSeparator": false,
398+ "icon": "",
399+ "ext": {},
400+ "action": "dialer3",
401+ "actionState": {},
402+ "isCheck": false,
403+ "isRadio": false,
404+ "isToggled": false,
405+ "shortcut": ""
406+ },
407+ "submenu": [{
408+ "rowData": { // 3.1
409+ "label": "menu3.1",
410+ "sensitive": true,
411+ "isSeparator": false,
412+ "icon": "",
413+ "ext": {},
414+ "action": "menu3.1",
415+ "actionState": {},
416+ "isCheck": false,
417+ "isRadio": false,
418+ "isToggled": false,
419+ "shortcut": ""
420+ }}
421+ ]}
422+ ]
423+
424+ property var deepTestData: [{
425+ "rowData": { // 1
426+ "label": "_menu1",
427+ "sensitive": true,
428+ "isSeparator": false,
429+ "icon": "",
430+ "ext": {},
431+ "action": "menu1",
432+ "actionState": {},
433+ "isCheck": false,
434+ "isRadio": false,
435+ "isToggled": false,
436+ "shortcut": "Alt+F"
437+ },
438+ "submenu": [{
439+ "rowData": { // 1.1
440+ "label": "menu1.1",
441+ "sensitive": true,
442+ "isSeparator": false,
443+ "icon": "",
444+ "ext": {},
445+ "action": "menu1.1",
446+ "actionState": {},
447+ "isCheck": false,
448+ "isRadio": false,
449+ "isToggled": false,
450+ "shortcut": ""
451+ },
452+ "submenu": [{
453+ "rowData": { // 1.1.1
454+ "label": "menu1.1.1",
455+ "sensitive": true,
456+ "isSeparator": false,
457+ "icon": "",
458+ "ext": {},
459+ "action": "menu1.1.1",
460+ "actionState": {},
461+ "isCheck": false,
462+ "isRadio": false,
463+ "isToggled": false,
464+ "shortcut": ""
465+ },
466+ "submenu": [{
467+ "rowData": { // 1.1.1
468+ "label": "menu1.1.1.1",
469+ "sensitive": true,
470+ "isSeparator": false,
471+ "icon": "",
472+ "ext": {},
473+ "action": "menu1.1.1.1",
474+ "actionState": {},
475+ "isCheck": false,
476+ "isRadio": false,
477+ "isToggled": false,
478+ "shortcut": ""
479+ },
480+ "submenu": [{
481+ "rowData": { // 1.1.1.1
482+ "label": "menu1.1.1.1.1",
483+ "sensitive": true,
484+ "isSeparator": false,
485+ "icon": "",
486+ "ext": {},
487+ "action": "menu1.1.1.1.1",
488+ "actionState": {},
489+ "isCheck": false,
490+ "isRadio": false,
491+ "isToggled": false,
492+ "shortcut": ""
493+ }}
494+ ]}
495+ ]}
496+ ]}
497+ ]}
498+ ]
499+
500+ property var singleCheckable: [{
501+ "rowData": { // 1
502+ "label": "checkable1",
503+ "sensitive": true,
504+ "isSeparator": false,
505+ "icon": "",
506+ "ext": {},
507+ "action": "checkable1",
508+ "actionState": {},
509+ "isCheck": true,
510+ "isRadio": false,
511+ "isToggled": false,
512+ "shortcut": "Alt+F"
513+ }
514+ }]
515+
516+ function generateTestData(length, depth, separatorInterval, prefix) {
517+ var data = [];
518+
519+ if (prefix === undefined) prefix = "menu"
520+
521+ for (var i = 0; i < length; i++) {
522+
523+ var menuCode = String.fromCharCode(i+65);
524+
525+ var isSeparator = separatorInterval > 0 && ((i+1) % separatorInterval == 0);
526+ var row = {
527+ "rowData": { // 1
528+ "label": prefix + "&" + menuCode,
529+ "sensitive": true,
530+ "isSeparator": isSeparator,
531+ "icon": "",
532+ "ext": {},
533+ "action": prefix + menuCode,
534+ "actionState": {},
535+ "isCheck": false,
536+ "isRadio": false,
537+ "isToggled": false,
538+ "shortcut": ""
539+ }
540+ }
541+ if (!isSeparator && depth > 1) {
542+ var submenu = generateTestData(length, depth-1, separatorInterval, prefix + menuCode + ".");
543+ row["submenu"] = submenu;
544+ }
545+ data[i] = row;
546+ }
547+ return data;
548+ }
549+}
tests/qmltests/Stages/tst_DecoratedWindow.qmlnew file+82
@@ -0,0 +1,82 @@
1+/*
2+ * Copyright (C) 2016 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+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import Unity.Application 0.1
20+import Unity.ApplicationMenu 0.1
21+import Unity.Indicators 0.1 as Indicators
22+import Unity.Test 0.1
23+import Utils 0.1
24+import QMenuModel 0.1
25+
26+import "../../../qml/Stages"
27+
28+Item {
29+ id: root
30+ width: units.gu(70)
31+ height: units.gu(50)
32+
33+ Component.onCompleted: {
34+ QuickUtils.keyboardAttached = true;
35+ theme.name = "Ubuntu.Components.Themes.SuruDark"
36+
37+ ApplicationMenuRegistry.RegisterSurfaceMenu("dialerAppSurfaceId", "/dialerapp", "/dialerapp", ":2");
38+ Indicators.UnityMenuModelCache.setCachedModelData("/dialerapp", appMenuData.dialerData);
39+
40+ fakeApplication = ApplicationManager.add("dialer-app");
41+ fakeApplication.manualSurfaceCreation = true;
42+ fakeApplication.setState(ApplicationInfoInterface.Starting);
43+
44+ fakeSurface = SurfaceManager.createSurface("dialerAppSurface",Mir.NormalType, Mir.MaximizedState, fakeApplication);
45+ fakeApplication.setState(ApplicationInfoInterface.Running);
46+ }
47+ property QtObject fakeApplication: null
48+ property QtObject fakeSurface: null
49+
50+ Binding {
51+ target: MouseTouchAdaptor
52+ property: "enabled"
53+ value: false
54+ }
55+
56+ DesktopMenuData { id: appMenuData }
57+
58+ Item {
59+ anchors.fill: parent
60+ anchors.margins: units.gu(2)
61+
62+ DecoratedWindow {
63+ id: window
64+ active: true
65+ requestedHeight: parent.height
66+ requestedWidth: parent.width
67+
68+ application: fakeApplication
69+ surface: fakeSurface
70+ focus: true
71+ }
72+ }
73+
74+ UnityTestCase {
75+ id: testCase
76+ name: "DecoratedWindow"
77+ when: windowShown
78+
79+ function init() {
80+ }
81+ }
82+}
tests/qmltests/Stages/tst_WindowDecoration.qmlnew file+173
@@ -0,0 +1,173 @@
1+/*
2+ * Copyright (C) 2016 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+import QtQuick 2.4
18+import QtTest 1.0
19+import QtQuick.Layouts 1.1
20+import Ubuntu.Components 1.3
21+import Ubuntu.Components.ListItems 1.3
22+import Unity.Application 0.1
23+import Unity.Test 0.1
24+import Utils 0.1
25+import QMenuModel 0.1
26+
27+import "../../../qml/Stages"
28+
29+Item {
30+ id: root
31+ width: units.gu(70)
32+ height: units.gu(50)
33+
34+ Component.onCompleted: {
35+ QuickUtils.keyboardAttached = true;
36+ theme.name = "Ubuntu.Components.Themes.SuruDark"
37+ }
38+
39+ Binding {
40+ target: MouseTouchAdaptor
41+ property: "enabled"
42+ value: false
43+ }
44+
45+ property QtObject application: QtObject {
46+ property string name: "webbrowser"
47+ }
48+
49+ DesktopMenuData { id: desktopMenuData }
50+
51+ Rectangle {
52+ anchors {
53+ left: parent.left
54+ right: parent.right
55+ bottom: parent.bottom
56+ margins: units.gu(1)
57+ }
58+ height: units.gu(20)
59+ border.width: 1
60+ border.color: "black"
61+
62+ TextArea {
63+ id: log
64+ anchors.fill: parent
65+ readOnly: true
66+ }
67+ }
68+
69+ WindowDecoration {
70+ id: decoration
71+ anchors { left: parent.left; right: parent.right; top: parent.top }
72+ anchors.margins: units.gu(1)
73+ target: root
74+ title: "TestTitle - Doing something"
75+ active: true
76+ height: units.gu(3)
77+ menu: menuBackend
78+ UnityMenuModel {
79+ id: menuBackend
80+ modelData: desktopMenuData.testData
81+ onActivated: log.text = "Activated " + action + "\n" + log.text
82+ }
83+
84+ onCloseClicked: log.text = "Close\n" + log.text
85+ onMinimizeClicked: log.text = "Minimize\n" + log.text
86+ onMaximizeClicked: log.text = "Maximize\n" + log.text
87+ }
88+
89+ SignalSpy {
90+ id: signalSpy
91+ target: decoration
92+ }
93+
94+ UnityTestCase {
95+ id: testCase
96+ name: "WindowDecoration"
97+ when: windowShown
98+
99+ function init() {
100+ decoration.menu = menuBackend;
101+ signalSpy.clear();
102+ }
103+
104+ function test_windowControlButtons_data() {
105+ return [ { tag: "close", controlName: "closeWindowButton", signal: "close"},
106+ { tag: "minimize", controlName: "minimizeWindowButton", signal: "minimize"},
107+ { tag: "maximize", controlName: "maximizeWindowButton", signal: "maximize"}];
108+ }
109+
110+ function test_windowControlButtons(data) {
111+ signalSpy.signalName = data.signal;
112+ var controlButton = findChild(decoration, data.controlName);
113+ verify(controlButton !== null);
114+
115+ mouseClick(controlButton, controlButton.width/2, controlButton.height/2);
116+ compare(signalSpy.count, 1);
117+ }
118+
119+ function test_titleRemainsWhenHoveringOnTitleBarWithNoMenu() {
120+ decoration.menu = undefined;
121+
122+ var menuLoader = findChild(decoration, "windowDecorationMenuBarLoader");
123+ verify(menuLoader);
124+ mouseMove(menuLoader, menuLoader.width/2, menuLoader.height/2);
125+ wait(200);
126+
127+ var titleLabel = findChild(decoration, "windowDecorationTitle");
128+ verify(menuLoader);
129+
130+ compare(menuLoader.opacity, 0, "Menu should not show when present")
131+ compare(titleLabel.opacity, 1, "Title should always show when app menu not present")
132+ }
133+
134+ function test_menuShowsWhenHoveringOnTitleBar() {
135+ var menuLoader = findChild(decoration, "windowDecorationMenuBarLoader");
136+ verify(menuLoader);
137+ mouseMove(menuLoader, menuLoader.width/2, menuLoader.height/2)
138+
139+ var titleLabel = findChild(decoration, "windowDecorationTitle");
140+ verify(menuLoader);
141+
142+ tryCompare(menuLoader, "opacity", 1);
143+ tryCompare(titleLabel, "opacity", 0);
144+
145+ mouseMove(menuLoader, menuLoader.width/2, menuLoader.height * 2);
146+
147+ tryCompare(menuLoader, "opacity", 0);
148+ tryCompare(titleLabel, "opacity", 1);
149+ }
150+
151+ function test_showMenuBarWithShortcutsOnLongAltPress() {
152+ var menuLoader = findChild(decoration, "windowDecorationMenuBarLoader");
153+ verify(menuLoader);
154+
155+ var titleLabel = findChild(decoration, "windowDecorationTitle");
156+ verify(menuLoader);
157+
158+ var menuBar = findChild(decoration, "menuBar");
159+ verify(menuBar);
160+ verify(menuBar.enableMnemonic === false, "Menubar should not show shortcuts")
161+
162+ keyPress(Qt.Key_Alt, Qt.NoModifier);
163+ tryCompare(menuLoader, "opacity", 1);
164+ tryCompare(titleLabel, "opacity", 0);
165+ compare(menuBar.enableMnemonic, true, "Menubar should show shortcuts when long alt pressed");
166+
167+ keyRelease(Qt.Key_Alt, Qt.NoModifier);
168+ tryCompare(menuLoader, "opacity", 0);
169+ tryCompare(titleLabel, "opacity", 1);
170+ compare(menuBar.enableMnemonic, false, "Menubar should not show shortcuts after long alt pressed");
171+ }
172+ }
173+}