Merge lp:~nick-dedekind/unity8/desktop_menus.gmenumodel into lp:unity8

Proposed by Nick Dedekind
Status: Work in progress
Proposed branch: lp:~nick-dedekind/unity8/desktop_menus.gmenumodel
Merge into: lp:unity8
Diff against target: 3885 lines (+3069/-96)
44 files modified
CMakeLists.txt (+1/-1)
debian/control (+2/-2)
plugins/Utils/CMakeLists.txt (+1/-0)
plugins/Utils/constants.cpp (+1/-0)
plugins/Utils/constants.h (+3/-0)
plugins/Utils/plugin.cpp (+2/-0)
plugins/Utils/shortcutaction.cpp (+122/-0)
plugins/Utils/shortcutaction.h (+44/-0)
plugins/Utils/windowkeysfilter.cpp (+11/-6)
plugins/Utils/windowkeysfilter.h (+1/-1)
qml/ApplicationMenus/AppMenuUtils.js (+107/-0)
qml/ApplicationMenus/MenuBar.qml (+245/-0)
qml/ApplicationMenus/MenuBarItem.qml (+56/-0)
qml/ApplicationMenus/MenuBase.qml (+116/-0)
qml/ApplicationMenus/MenuItemBase.qml (+32/-0)
qml/ApplicationMenus/MenuItemDelegate.qml (+49/-0)
qml/ApplicationMenus/MenuItemDelegateBase.qml (+82/-0)
qml/ApplicationMenus/MenuItemFactory.qml (+240/-0)
qml/ApplicationMenus/MenuPage.qml (+277/-0)
qml/CMakeLists.txt (+1/-0)
qml/Components/PanelState/PanelState.qml (+2/-1)
qml/Panel/Panel.qml (+91/-7)
qml/Stages/DecoratedWindow.qml (+50/-16)
qml/Stages/DesktopStage.qml (+10/-3)
qml/Stages/WindowDecoration.qml (+117/-45)
tests/imports/check_imports.py (+2/-1)
tests/mocks/QMenuModel/QMenuModel.qmltypes (+5/-1)
tests/mocks/QMenuModel/unitymenumodel.cpp (+25/-3)
tests/mocks/QMenuModel/unitymenumodel.h (+2/-0)
tests/mocks/Unity/Application/MirSurface.cpp (+10/-0)
tests/mocks/Unity/Application/MirSurface.h (+3/-0)
tests/mocks/Utils/CMakeLists.txt (+1/-0)
tests/mocks/Utils/constants.cpp (+7/-0)
tests/mocks/Utils/constants.h (+7/-0)
tests/mocks/Utils/plugin.cpp (+2/-0)
tests/qmltests/ApplicationMenus/tst_MenuBar.qml (+132/-0)
tests/qmltests/ApplicationMenus/tst_MenuPage.qml (+256/-0)
tests/qmltests/CMakeLists.txt (+3/-0)
tests/qmltests/Panel/tst_Panel.qml (+56/-4)
tests/qmltests/Stages/ApplicationCheckBox.qml (+4/-0)
tests/qmltests/Stages/DesktopMenuData.qml (+702/-0)
tests/qmltests/Stages/tst_DesktopStage.qml (+13/-1)
tests/qmltests/Stages/tst_WindowDecoration.qml (+163/-0)
tests/qmltests/tst_Shell.qml (+13/-4)
To merge this branch: bzr merge lp:~nick-dedekind/unity8/desktop_menus.gmenumodel
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Needs Fixing
Unity8 CI Bot continuous-integration Needs Fixing
Unity Team Pending
Review via email: mp+282727@code.launchpad.net

Commit message

Desktop Menus

Description of the change

Desktop Menus

To post a comment you must log in.
2110. By Nick Dedekind

updated control

Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :

FAILED: Continuous integration, rev:2109
https://unity8-jenkins.ubuntu.com/job/lp-unity8-1-ci/90/
Executed test runs:

Click here to trigger a rebuild:
https://unity8-jenkins.ubuntu.com/job/lp-unity8-1-ci/90/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :

FAILED: Continuous integration, rev:2110
https://unity8-jenkins.ubuntu.com/job/lp-unity8-1-ci/91/
Executed test runs:

Click here to trigger a rebuild:
https://unity8-jenkins.ubuntu.com/job/lp-unity8-1-ci/91/rebuild

review: Needs Fixing (continuous-integration)
2111. By Nick Dedekind

merged with trunk

Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :

FAILED: Continuous integration, rev:2111
https://unity8-jenkins.ubuntu.com/job/lp-unity8-1-ci/92/
Executed test runs:

Click here to trigger a rebuild:
https://unity8-jenkins.ubuntu.com/job/lp-unity8-1-ci/92/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)

Unmerged revisions

2111. By Nick Dedekind

merged with trunk

2110. By Nick Dedekind

updated control

2109. By Nick Dedekind

desktop menus

2108. By Launchpad Translations on behalf of unity-team

Launchpad automatic translations update.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2015-12-16 13:58:39 +0000
3+++ CMakeLists.txt 2016-01-15 13:11:04 +0000
4@@ -57,7 +57,7 @@
5 find_package(Qt5Concurrent 5.4 REQUIRED)
6 find_package(Qt5Sql 5.4 REQUIRED)
7
8-pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=12)
9+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=13)
10
11 # Standard install paths
12 include(GNUInstallDirs)
13
14=== modified file 'debian/control'
15--- debian/control 2016-01-11 17:37:16 +0000
16+++ debian/control 2016-01-15 13:11:04 +0000
17@@ -29,7 +29,7 @@
18 libqt5xmlpatterns5-dev,
19 libsystemsettings-dev,
20 libudev-dev,
21- libunity-api-dev (>= 7.105),
22+ libunity-api-dev (>= 7.106),
23 libusermetricsoutput1-dev,
24 # Need those X11 libs touch emulation from mouse events in manual QML tests on a X11 desktop
25 libx11-dev[!armhf],
26@@ -102,7 +102,7 @@
27 qml-module-qtquick-xmllistmodel,
28 qml-module-qtsysteminfo,
29 qtdeclarative5-gsettings1.0,
30- qtdeclarative5-qtmir-plugin (>= 0.4.5),
31+ qtdeclarative5-qtmir-plugin (>= 0.4.7),
32 qtdeclarative5-ubuntu-telephony0.1,
33 qtdeclarative5-ubuntu-web-plugin,
34 ubuntu-system-settings,
35
36=== modified file 'plugins/Utils/CMakeLists.txt'
37--- plugins/Utils/CMakeLists.txt 2015-10-28 10:32:47 +0000
38+++ plugins/Utils/CMakeLists.txt 2016-01-15 13:11:04 +0000
39@@ -24,6 +24,7 @@
40 easingcurve.cpp
41 windowstatestorage.cpp
42 timezoneFormatter.cpp
43+ shortcutaction.cpp
44 plugin.cpp
45 )
46
47
48=== modified file 'plugins/Utils/constants.cpp'
49--- plugins/Utils/constants.cpp 2015-11-20 15:01:39 +0000
50+++ plugins/Utils/constants.cpp 2016-01-15 13:11:04 +0000
51@@ -24,4 +24,5 @@
52 } else {
53 m_indicatorValueTimeout = 5000;
54 }
55+ m_menuHoverOpenInterval = 250;
56 }
57
58=== modified file 'plugins/Utils/constants.h'
59--- plugins/Utils/constants.h 2015-03-11 08:07:31 +0000
60+++ plugins/Utils/constants.h 2016-01-15 13:11:04 +0000
61@@ -30,14 +30,17 @@
62 {
63 Q_OBJECT
64 Q_PROPERTY(int indicatorValueTimeout READ indicatorValueTimeout CONSTANT)
65+ Q_PROPERTY(int menuHoverOpenInterval READ menuHoverOpenInterval CONSTANT)
66
67 public:
68 Constants(QObject *parent = 0);
69
70 int indicatorValueTimeout() const { return m_indicatorValueTimeout; }
71+ int menuHoverOpenInterval() const { return m_menuHoverOpenInterval; }
72
73 private:
74 int m_indicatorValueTimeout;
75+ int m_menuHoverOpenInterval;
76 };
77
78 #endif
79
80=== modified file 'plugins/Utils/plugin.cpp'
81--- plugins/Utils/plugin.cpp 2015-10-26 16:47:52 +0000
82+++ plugins/Utils/plugin.cpp 2016-01-15 13:11:04 +0000
83@@ -36,6 +36,7 @@
84 #include "constants.h"
85 #include "timezoneFormatter.h"
86 #include "applicationsfiltermodel.h"
87+#include "shortcutaction.h"
88
89 static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
90 {
91@@ -68,6 +69,7 @@
92 [](QQmlEngine*, QJSEngine*) -> QObject* { return new TimezoneFormatter; });
93 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
94 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
95+ qmlRegisterType<ShortcutAction>(uri, 0, 1, "ShortcutAction");
96 }
97
98 void UtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
99
100=== added file 'plugins/Utils/shortcutaction.cpp'
101--- plugins/Utils/shortcutaction.cpp 1970-01-01 00:00:00 +0000
102+++ plugins/Utils/shortcutaction.cpp 2016-01-15 13:11:04 +0000
103@@ -0,0 +1,122 @@
104+#include "shortcutaction.h"
105+
106+#include <QQuickItem>
107+#include <QQuickWindow>
108+#include <QVariant>
109+#include <QShortcutEvent>
110+#include <private/qguiapplication_p.h>
111+
112+namespace {
113+
114+bool qShortcutContextMatcher(QObject *o, Qt::ShortcutContext context)
115+{
116+ if (!static_cast<ShortcutAction*>(o)->isEnabled())
117+ return false;
118+
119+ switch (context) {
120+ case Qt::ApplicationShortcut:
121+ case Qt::WindowShortcut:
122+ break;
123+ case Qt::WidgetShortcut: {
124+ QQuickItem* target = static_cast<ShortcutAction*>(o)->target();
125+ if (target && target->hasActiveFocus()) {
126+ return true;
127+ }
128+ return true;
129+ }
130+ case Qt::WidgetWithChildrenShortcut:
131+ break;
132+ }
133+
134+ return false;
135+}
136+
137+}
138+
139+
140+ShortcutAction::ShortcutAction(QObject* parent)
141+ : QObject(parent)
142+ , m_enabled(true)
143+{
144+}
145+
146+ShortcutAction::~ShortcutAction()
147+{
148+ setShortcut(QString());
149+}
150+
151+QVariant ShortcutAction::shortcut() const
152+{
153+ return m_shortcut.toString(QKeySequence::NativeText);
154+}
155+
156+void ShortcutAction::setShortcut(const QVariant &arg)
157+{
158+ QKeySequence sequence;
159+ if (arg.type() == QVariant::Int)
160+ sequence = QKeySequence(static_cast<QKeySequence::StandardKey>(arg.toInt()));
161+ else
162+ sequence = QKeySequence::fromString(arg.toString());
163+
164+ if (sequence == m_shortcut)
165+ return;
166+
167+ if (!m_shortcut.isEmpty())
168+ QGuiApplicationPrivate::instance()->shortcutMap.removeShortcut(0, this, m_shortcut);
169+
170+ m_shortcut = sequence;
171+
172+ if (!m_shortcut.isEmpty()) {
173+ Qt::ShortcutContext context = Qt::WidgetShortcut;
174+ QGuiApplicationPrivate::instance()->shortcutMap.addShortcut(this, m_shortcut, context, qShortcutContextMatcher);
175+ }
176+ Q_EMIT shortcutChanged(shortcut());
177+}
178+
179+void ShortcutAction::setEnabled(bool e)
180+{
181+ if (e == m_enabled) return;
182+ m_enabled = e;
183+
184+ Q_EMIT enabledChanged();
185+}
186+
187+void ShortcutAction::setTarget(QQuickItem *target)
188+{
189+ if (target == m_target) return;
190+ m_target = target;
191+
192+ Q_EMIT targetChanged();
193+}
194+
195+bool ShortcutAction::event(QEvent *e)
196+{
197+ if (!m_enabled)
198+ return false;
199+
200+ if (e->type() != QEvent::Shortcut)
201+ return false;
202+
203+ QShortcutEvent *se = static_cast<QShortcutEvent *>(e);
204+
205+ Q_ASSERT_X(se->key() == m_shortcut,
206+ "QQuickAction::event",
207+ "Received shortcut event from incorrect shortcut");
208+ if (se->isAmbiguous()) {
209+ qWarning("QQuickAction::event: Ambiguous shortcut overload: %s", se->key().toString(QKeySequence::NativeText).toLatin1().constData());
210+ return false;
211+ }
212+
213+ trigger();
214+
215+ return true;
216+}
217+
218+void ShortcutAction::trigger(QObject* source)
219+{
220+ if (!m_enabled)
221+ return;
222+
223+ Q_EMIT triggered(source);
224+}
225+
226
227=== added file 'plugins/Utils/shortcutaction.h'
228--- plugins/Utils/shortcutaction.h 1970-01-01 00:00:00 +0000
229+++ plugins/Utils/shortcutaction.h 2016-01-15 13:11:04 +0000
230@@ -0,0 +1,44 @@
231+#ifndef SHORTCUTACTION_H
232+#define SHORTCUTACTION_H
233+
234+#include <QObject>
235+#include <QKeySequence>
236+#include <QQuickItem>
237+
238+class ShortcutAction : public QObject
239+{
240+ Q_OBJECT
241+ Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged)
242+ Q_PROPERTY(QVariant shortcut READ shortcut WRITE setShortcut NOTIFY shortcutChanged)
243+ Q_PROPERTY(QQuickItem* target READ target WRITE setTarget NOTIFY targetChanged)
244+
245+public:
246+ ShortcutAction(QObject* parent = nullptr);
247+ ~ShortcutAction();
248+
249+ QVariant shortcut() const;
250+ void setShortcut(const QVariant &shortcut);
251+
252+ bool isEnabled() const { return m_enabled; }
253+ void setEnabled(bool e);
254+
255+ QQuickItem* target() const { return m_target; }
256+ void setTarget(QQuickItem* target);
257+
258+ bool event(QEvent *e) override;
259+
260+ void trigger(QObject* source = nullptr);
261+
262+Q_SIGNALS:
263+ void targetChanged();
264+ void enabledChanged();
265+ void triggered(QObject* source = nullptr);
266+ void shortcutChanged(QVariant shortcut);
267+
268+private:
269+ QKeySequence m_shortcut;
270+ bool m_enabled;
271+ QQuickItem* m_target;
272+};
273+
274+#endif // SHORTCUTACTION_H
275
276=== modified file 'plugins/Utils/windowkeysfilter.cpp'
277--- plugins/Utils/windowkeysfilter.cpp 2015-10-22 17:37:26 +0000
278+++ plugins/Utils/windowkeysfilter.cpp 2016-01-15 13:11:04 +0000
279@@ -24,12 +24,14 @@
280 : QQuickItem(parent),
281 m_currentEventTimestamp(0)
282 {
283- connect(this, &QQuickItem::windowChanged,
284- this, &WindowKeysFilter::setupFilterOnWindow);
285+ connect(this, &QQuickItem::windowChanged, this, &WindowKeysFilter::setupFilter);
286+ connect(this, &QQuickItem::enabledChanged, this, &WindowKeysFilter::setupFilter);
287 }
288
289 bool WindowKeysFilter::eventFilter(QObject *watched, QEvent *event)
290 {
291+ if (!isEnabled()) return false;
292+
293 Q_ASSERT(!m_filteredWindow.isNull());
294 Q_ASSERT(watched == static_cast<QObject*>(m_filteredWindow.data()));
295 Q_UNUSED(watched);
296@@ -53,16 +55,19 @@
297 }
298 }
299
300-void WindowKeysFilter::setupFilterOnWindow(QQuickWindow *window)
301+void WindowKeysFilter::setupFilter()
302 {
303+ if (!m_filteredWindow.isNull() == isEnabled() && m_filteredWindow.data() == window()) return;
304+
305 if (!m_filteredWindow.isNull()) {
306 m_filteredWindow->removeEventFilter(this);
307 m_filteredWindow.clear();
308 }
309
310- if (window) {
311- window->installEventFilter(this);
312- m_filteredWindow = window;
313+ QQuickWindow *wnd = window();
314+ if (wnd && isEnabled()) {
315+ wnd->installEventFilter(this);
316+ m_filteredWindow = wnd;
317 }
318 }
319
320
321=== modified file 'plugins/Utils/windowkeysfilter.h'
322--- plugins/Utils/windowkeysfilter.h 2015-10-22 17:37:26 +0000
323+++ plugins/Utils/windowkeysfilter.h 2016-01-15 13:11:04 +0000
324@@ -48,7 +48,7 @@
325 void currentEventTimestampChanged();
326
327 private Q_SLOTS:
328- void setupFilterOnWindow(QQuickWindow *window);
329+ void setupFilter();
330
331 private:
332 QPointer<QQuickWindow> m_filteredWindow;
333
334=== added directory 'qml/ApplicationMenus'
335=== added file 'qml/ApplicationMenus/AppMenuUtils.js'
336--- qml/ApplicationMenus/AppMenuUtils.js 1970-01-01 00:00:00 +0000
337+++ qml/ApplicationMenus/AppMenuUtils.js 2016-01-15 13:11:04 +0000
338@@ -0,0 +1,107 @@
339+/*
340+ * Copyright 2015 Canonical Ltd.
341+ *
342+ * This program is free software; you can redistribute it and/or modify
343+ * it under the terms of the GNU Lesser General Public License as published by
344+ * the Free Software Foundation; version 3.
345+ *
346+ * This program is distributed in the hope that it will be useful,
347+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
348+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
349+ * GNU Lesser General Public License for more details.
350+ *
351+ * You should have received a copy of the GNU Lesser General Public License
352+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
353+ */
354+
355+.pragma library
356+
357+String.prototype.htmlLabelFromMenuLabel = function (underline) {
358+ var replacements = [
359+ {
360+ char: "_",
361+ searchExpr: /_/g,
362+ doubleReplacement: "_"
363+ }, {
364+ char: "&",
365+ searchExpr: /&(?!amp;)/g,
366+ doubleReplacement: "&amp;"
367+ }
368+ ];
369+ var text = this;
370+
371+ var i = 0;
372+ for (; i < replacements.length; i++) {
373+ text = parseShortcutChararacter(text, underline, replacements[i]);
374+ }
375+ return text;
376+}
377+
378+function parseShortcutChararacter(text, underline, replacement) {
379+ text = text.replace(new RegExp(replacement.char + replacement.char, "g"), replacement.doubleReplacement);
380+
381+ if (!underline) {
382+ return text.replace(replacement.searchExpr, "");
383+ }
384+
385+ var original = text;
386+ var output = "";
387+ var current = 0
388+ var next = text.search(replacement.searchExpr);
389+ if (next === -1) return text;
390+
391+ while (next !== -1) {
392+ output += text.slice(current, next);
393+ current = next+1;
394+
395+ var rest = text.slice(current);
396+ text = "<u>" + rest[0] + "</u>" + rest.slice(1);
397+ next = text.search(replacement.searchExpr);
398+ }
399+ output += text;
400+ return output;
401+}
402+
403+String.prototype.actionKeyFromMenuLabel = function () {
404+ var replacements = [
405+ {
406+ char: "_",
407+ searchExpr: /_/g,
408+ doubleReplacement: "_"
409+ }, {
410+ char: "&",
411+ searchExpr: /&(?!amp;)/g,
412+ doubleReplacement: "&amp;"
413+ }
414+ ];
415+ var text = this;
416+
417+ var i = 0;
418+ var replaced = false;
419+ for (; i < replacements.length; i++) {
420+ var actionKey = parseActionKeyChararacter(text, replacements[i]);
421+ if (actionKey !== "") return actionKey;
422+ }
423+ return "";
424+}
425+
426+function parseActionKeyChararacter(original, replacement) {
427+ var text = original.replace(new RegExp(replacement.char + replacement.char, "g"), replacement.doubleReplacement);
428+
429+ var output = "";
430+ var iter = 0;
431+ var next = text.search(replacement.searchExpr);
432+ if (next === -1) return "";
433+
434+ while (next !== -1) {
435+ var char = text.slice(next+1, next+2);
436+ if (char === "") break;
437+
438+ if (iter > 0) output += "+";
439+ output += char;
440+
441+ text = text.slice(next+2);
442+ next = text.search(replacement.searchExpr)
443+ }
444+ return output;
445+}
446
447=== added file 'qml/ApplicationMenus/MenuBar.qml'
448--- qml/ApplicationMenus/MenuBar.qml 1970-01-01 00:00:00 +0000
449+++ qml/ApplicationMenus/MenuBar.qml 2016-01-15 13:11:04 +0000
450@@ -0,0 +1,245 @@
451+/*
452+ * Copyright 2015 Canonical Ltd.
453+ *
454+ * This program is free software; you can redistribute it and/or modify
455+ * it under the terms of the GNU Lesser General Public License as published by
456+ * the Free Software Foundation; version 3.
457+ *
458+ * This program is distributed in the hope that it will be useful,
459+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
460+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
461+ * GNU Lesser General Public License for more details.
462+ *
463+ * You should have received a copy of the GNU Lesser General Public License
464+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
465+ */
466+
467+import QtQuick 2.4
468+import QtQuick.Layouts 1.1
469+import Ubuntu.Components 1.3
470+
471+MenuBase {
472+ id: root
473+ objectName: "menuBar"
474+
475+ property var focusWindow: undefined
476+ property alias menuModel: rootDelegate.menuModel
477+ implicitWidth: listView.width
478+ readonly property bool hasChildren: repeater.count > 0
479+ submenuOffset: -units.gu(1)
480+
481+ function close() {
482+ closePopup();
483+ listView.selectedItem = undefined;
484+ }
485+
486+ FocusScope {
487+ id: scope
488+ anchors {
489+ left: parent.left
490+ }
491+ width: listView.width
492+ height: parent.height
493+
494+ Keys.onLeftPressed: listView.selectPrevious()
495+ Keys.onRightPressed: listView.selectNext()
496+ Keys.onEscapePressed: {
497+ focus = false;
498+ if (focusWindow !== undefined) {
499+ focusWindow.forceActiveFocus();
500+ }
501+ }
502+
503+ onFocusChanged: {
504+ if (!focus && root.openItem) {
505+ root.close();
506+ }
507+ }
508+
509+ InverseMouseArea {
510+ enabled: root.openItem !== undefined
511+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
512+ anchors.fill: parent
513+
514+ onPressed: {
515+ // stop the inverse mouse area stealing events from mouse areas under.
516+ mouse.accepted = false;
517+ root.close();
518+ }
519+ }
520+
521+ MenuItemDelegateBase {
522+ id: rootDelegate
523+ focusWindow: root.focusWindow
524+ }
525+
526+ Row {
527+ id: listView
528+ anchors.left: parent.left
529+ height: parent.height
530+ spacing: units.gu(2)
531+
532+ property var selectedItem: undefined
533+
534+ function selectNext() {
535+ var delegate;
536+ var newIndex = 0;
537+ if (listView.selectedItem === undefined && repeater.count > 0) {
538+ while (repeater.count > newIndex) {
539+ delegate = repeater.itemAt(newIndex++);
540+ if (delegate.enabled) {
541+ delegate.selected = true;
542+ break;
543+ }
544+ }
545+ } else if (listView.selectedItem !== undefined && repeater.count > 1) {
546+ var startIndex = (listView.selectedItem.ownIndex + 1) % repeater.count;
547+ newIndex = startIndex;
548+ do {
549+ delegate = repeater.itemAt(newIndex);
550+ if (delegate.enabled) {
551+ delegate.selected = true;
552+ break;
553+ }
554+ newIndex = (newIndex + 1) % repeater.count;
555+ } while (newIndex !== startIndex)
556+ }
557+ }
558+
559+ function selectPrevious() {
560+ var delegate;
561+ var newIndex = repeater.count-1;
562+ if (listView.selectedItem === undefined && repeater.count > 0) {
563+ while (repeater.count > newIndex) {
564+ delegate = repeater.itemAt(newIndex--);
565+ if (delegate.enabled) {
566+ delegate.selected = true;
567+ break;
568+ }
569+ }
570+ } else if (listView.selectedItem !== undefined && repeater.count > 1) {
571+ var startIndex = listView.selectedItem.ownIndex - 1;
572+ newIndex = startIndex;
573+ do {
574+ if (newIndex < 0) newIndex = repeater.count - 1;
575+ delegate = repeater.itemAt(newIndex--);
576+ if (delegate.enabled) {
577+ delegate.selected = true;
578+ break;
579+ }
580+ } while (newIndex !== startIndex)
581+ }
582+ }
583+
584+ Repeater {
585+ id: repeater
586+ model: rootDelegate.submenuItems
587+
588+ MenuBarItem {
589+ id: repeaterItem
590+ height: listView.height
591+
592+ enableMnemonic: root.enableMnemonic
593+ menuItemDelegate: model.delegate
594+
595+ property int modelIndex: index
596+ property bool selected: false
597+ enabled: model.delegate.sensitive && !model.delegate.isSeparator
598+
599+ Rectangle {
600+ anchors {
601+ left: parent.left
602+ right: parent.right
603+ bottom: parent.bottom
604+ leftMargin: -units.gu(1)
605+ rightMargin: -units.gu(1)
606+ }
607+ height: units.dp(4)
608+ color: "#E95420"
609+ visible: root.openItem == menuItem
610+ }
611+
612+ MenuItemBase {
613+ id: menuItem
614+ objectName: root.objectName + "-menu" + index
615+ property int ownIndex: index
616+
617+ anchors.fill: parent
618+ delegate: model.delegate
619+
620+ mnemonicAction {
621+ property string action: model.delegate.label.actionKeyFromMenuLabel()
622+ enabled: enableMnemonic && repeaterItem.enabled
623+ shortcut: action !== "" ? "Alt+" + action : ""
624+
625+ onTriggered: {
626+ if (model.delegate.hasSubmenu) {
627+ root.open(menuItem, true);
628+ }
629+ else {
630+ model.delegate.activate();
631+ }
632+ }
633+ }
634+ }
635+
636+ MouseArea {
637+ id: titleMouseArea
638+ anchors.fill: parent
639+ hoverEnabled: listView.selectedItem !== undefined
640+
641+ onEntered: {
642+ if (listView.selectedItem !== undefined && !repeaterItem.selected) {
643+ repeaterItem.selected = true;
644+ }
645+ }
646+ onClicked: {
647+ if (model.delegate.hasSubmenu) {
648+ root.open(menuItem, true);
649+ }
650+ else if (root.enabled) {
651+ model.delegate.activate()
652+ }
653+ }
654+ }
655+
656+ onSelectedChanged: {
657+ if (selected) {
658+ listView.selectedItem = menuItem;
659+ if (model.delegate.hasSubmenu && root.openItem !== menuItem) {
660+ root.open(menuItem, true);
661+ }
662+ }
663+ else if (listView.selectedItem === menuItem) {
664+ listView.selectedItem = undefined;
665+ if (root.openItem == menuItem) {
666+ root.closePopup();
667+ }
668+ }
669+ }
670+
671+ Connections {
672+ target: listView
673+ onSelectedItemChanged: {
674+ if (repeaterItem.selected && (listView.selectedItem === undefined || listView.selectedItem !== menuItem)) {
675+ repeaterItem.selected = false;
676+ if (root.openItem == menuItem) {
677+ root.closePopup();
678+ }
679+ }
680+ }
681+ }
682+
683+ Connections {
684+ target: root
685+ onOpenItemChanged: {
686+ if (root.openItem == menuItem) {
687+ repeaterItem.selected = true;
688+ }
689+ }
690+ }
691+ }
692+ }
693+ }
694+ }
695+}
696
697=== added file 'qml/ApplicationMenus/MenuBarItem.qml'
698--- qml/ApplicationMenus/MenuBarItem.qml 1970-01-01 00:00:00 +0000
699+++ qml/ApplicationMenus/MenuBarItem.qml 2016-01-15 13:11:04 +0000
700@@ -0,0 +1,56 @@
701+/*
702+ * Copyright 2015 Canonical Ltd.
703+ *
704+ * This program is free software; you can redistribute it and/or modify
705+ * it under the terms of the GNU Lesser General Public License as published by
706+ * the Free Software Foundation; version 3.
707+ *
708+ * This program is distributed in the hope that it will be useful,
709+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
710+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
711+ * GNU Lesser General Public License for more details.
712+ *
713+ * You should have received a copy of the GNU Lesser General Public License
714+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
715+ */
716+
717+import QtQuick 2.4
718+import Ubuntu.Settings.Menus 0.1 as Menus
719+import Ubuntu.Components.ListItems 1.3 as ListItems
720+import Ubuntu.Components 1.3
721+import QtQuick.Layouts 1.1
722+
723+import "AppMenuUtils.js" as AppMenuUtils
724+
725+Item {
726+ id: root
727+ property var menuItemDelegate: undefined
728+ property bool enableMnemonic: false
729+
730+ implicitWidth: column.implicitWidth
731+ implicitHeight: column.height
732+
733+ RowLayout {
734+ id: column
735+ spacing: units.gu(1)
736+ anchors {
737+ centerIn: parent
738+ }
739+
740+ Icon {
741+ Layout.preferredWidth: units.gu(2)
742+ Layout.preferredHeight: units.gu(2)
743+ Layout.alignment: Qt.AlignVCenter
744+
745+ visible: menuItemDelegate && menuItemDelegate.icon
746+ source: menuItemDelegate && menuItemDelegate.icon || ""
747+ }
748+
749+ Label {
750+ id: _title
751+ text: menuItemDelegate.label.htmlLabelFromMenuLabel(enableMnemonic) || ""
752+ horizontalAlignment: Text.AlignLeft
753+ color: enabled ? "white" : "#5d5d5d"
754+ }
755+ }
756+}
757
758=== added file 'qml/ApplicationMenus/MenuBase.qml'
759--- qml/ApplicationMenus/MenuBase.qml 1970-01-01 00:00:00 +0000
760+++ qml/ApplicationMenus/MenuBase.qml 2016-01-15 13:11:04 +0000
761@@ -0,0 +1,116 @@
762+/*
763+ * Copyright 2015 Canonical Ltd.
764+ *
765+ * This program is free software; you can redistribute it and/or modify
766+ * it under the terms of the GNU Lesser General Public License as published by
767+ * the Free Software Foundation; version 3.
768+ *
769+ * This program is distributed in the hope that it will be useful,
770+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
771+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
772+ * GNU Lesser General Public License for more details.
773+ *
774+ * You should have received a copy of the GNU Lesser General Public License
775+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
776+ */
777+
778+import QtQuick 2.4
779+import Utils 0.1
780+
781+Item {
782+ id: root
783+
784+ property bool enableMnemonic: false
785+ property bool opensVertically: true
786+ readonly property alias openItem: d._openItem
787+ property real submenuOffset: 0
788+
789+ function open(menuItem, focus, focusIndex) {
790+ openDelayTimer.stop();
791+
792+ if (d._openItem !== menuItem && menuItem.delegate.hasSubmenu) {
793+ d.closePopup();
794+
795+ var menuPageComponent = Qt.createComponent("MenuPage.qml");
796+
797+ if (menuPageComponent.status === Component.Ready) {
798+ d._requestedOpenItem = menuItem;
799+ d.popup = menuPageComponent.createObject(menuItem,
800+ {
801+ "objectName": root.objectName + "-subMenu" + menuItem.delegate.index,
802+ "vertical": true,
803+ "x": opensVertically ? submenuOffset : menuItem.width,
804+ "y": opensVertically ? menuItem.height : submenuOffset,
805+ "delegateModel": menuItem.delegate.submenuItems
806+ });
807+ } else if (menuPageComponent.status === Component.Error) {
808+ console.log(menuPageComponent.errorString());
809+ }
810+ }
811+ if (focus && d.popup) d.popup.forceActiveFocus();
812+ if (focusIndex !== undefined && d.popup) d.popup.setSelectedItem(focusIndex);
813+ }
814+
815+ function openWithDelay(menuItem, focus, index) {
816+ openDelayTimer.stop();
817+ if (d._openItem === menuItem) {
818+ open(menuItem, focus, index);
819+ } else {
820+ d._requestedOpenItem = menuItem;
821+ openDelayTimer.openFocus = focus;
822+ openDelayTimer.openIndex = index;
823+ openDelayTimer.restart();
824+ }
825+ }
826+
827+ function closePopup(focus) {
828+ openDelayTimer.stop();
829+ if (openItem !== undefined) {
830+ d.closePopup();
831+ }
832+ if (focus) {
833+ forceActiveFocus();
834+ }
835+ }
836+
837+ Timer {
838+ id: openDelayTimer
839+
840+ property var openFocus
841+ property var openIndex
842+
843+ interval: Constants.menuHoverOpenInterval
844+ onTriggered: {
845+ open(d._requestedOpenItem, openDelayTimer.openFocus, openDelayTimer.openIndex);
846+ }
847+ }
848+
849+ QtObject {
850+ id: d
851+ property var _requestedOpenItem: undefined
852+ property var _openItem: undefined
853+
854+ property QtObject popup: null
855+ onPopupChanged: {
856+ d._openItem = popup !== null ? _requestedOpenItem : undefined
857+ }
858+
859+ signal closePopup
860+ }
861+
862+ Connections {
863+ target: d
864+ onClosePopup: {
865+ if (d.popup) {
866+ // recusive close to preserve focusing
867+ d.popup.closePopup(false);
868+ d.popup.visible = false;
869+ d.popup.destroy();
870+ d.popup = null;
871+
872+ d._openItem = undefined;
873+ d._requestedOpenItem = undefined;
874+ }
875+ }
876+ }
877+}
878
879=== added file 'qml/ApplicationMenus/MenuItemBase.qml'
880--- qml/ApplicationMenus/MenuItemBase.qml 1970-01-01 00:00:00 +0000
881+++ qml/ApplicationMenus/MenuItemBase.qml 2016-01-15 13:11:04 +0000
882@@ -0,0 +1,32 @@
883+/*
884+ * Copyright 2015 Canonical Ltd.
885+ *
886+ * This program is free software; you can redistribute it and/or modify
887+ * it under the terms of the GNU Lesser General Public License as published by
888+ * the Free Software Foundation; version 3.
889+ *
890+ * This program is distributed in the hope that it will be useful,
891+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
892+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
893+ * GNU Lesser General Public License for more details.
894+ *
895+ * You should have received a copy of the GNU Lesser General Public License
896+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
897+ */
898+
899+import QtQuick 2.4
900+import QtQuick.Controls 1.3 as Controls
901+
902+// Item which represents visual location of a menu item
903+// Also contains common convenience properties.
904+Item {
905+ id: root
906+
907+ property var delegate: undefined
908+ property alias mnemonicAction: mnemonic
909+
910+ /* Quick action. eg "F" from "_File" in the label. Only available while bar/popup is opened */
911+ Controls.Action {
912+ id: mnemonic
913+ }
914+}
915
916=== added file 'qml/ApplicationMenus/MenuItemDelegate.qml'
917--- qml/ApplicationMenus/MenuItemDelegate.qml 1970-01-01 00:00:00 +0000
918+++ qml/ApplicationMenus/MenuItemDelegate.qml 2016-01-15 13:11:04 +0000
919@@ -0,0 +1,49 @@
920+/*
921+ * Copyright 2015 Canonical Ltd.
922+ *
923+ * This program is free software; you can redistribute it and/or modify
924+ * it under the terms of the GNU Lesser General Public License as published by
925+ * the Free Software Foundation; version 3.
926+ *
927+ * This program is distributed in the hope that it will be useful,
928+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
929+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
930+ * GNU Lesser General Public License for more details.
931+ *
932+ * You should have received a copy of the GNU Lesser General Public License
933+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
934+ */
935+
936+import QtQuick 2.4
937+import QtQuick.Controls 1.3 as Controls
938+import Utils 0.1 as UnityUtils
939+
940+MenuItemDelegateBase {
941+ id: root
942+ property var index: undefined
943+ property var menuData: undefined
944+
945+ readonly property string label: menuData ? menuData.label : ""
946+ readonly property string action: menuData ? menuData.action : ""
947+ readonly property string icon: menuData ? menuData.icon : ""
948+ readonly property var shortcut: menuData ? menuData.shortcut : undefined
949+ readonly property bool sensitive: menuData ? menuData.sensitive : false
950+ readonly property bool hasSubmenu: menuData ? menuData.hasSubmenu : false
951+ readonly property bool isSeparator: menuData ? menuData.isSeparator : false
952+ readonly property bool isCheck: menuData ? menuData.isCheck : false
953+ readonly property bool isRadio: menuData ? menuData.isRadio : false
954+ readonly property bool isToggled: menuData ? menuData.isToggled : false
955+
956+ signal activate()
957+
958+ UnityUtils.ShortcutAction {
959+ shortcut: root.shortcut
960+ target: focusWindow ? focusWindow : null
961+
962+ onTriggered: {
963+ if (root.enabled) {
964+ activate();
965+ }
966+ }
967+ }
968+}
969
970=== added file 'qml/ApplicationMenus/MenuItemDelegateBase.qml'
971--- qml/ApplicationMenus/MenuItemDelegateBase.qml 1970-01-01 00:00:00 +0000
972+++ qml/ApplicationMenus/MenuItemDelegateBase.qml 2016-01-15 13:11:04 +0000
973@@ -0,0 +1,82 @@
974+/*
975+ * Copyright 2015 Canonical Ltd.
976+ *
977+ * This program is free software; you can redistribute it and/or modify
978+ * it under the terms of the GNU Lesser General Public License as published by
979+ * the Free Software Foundation; version 3.
980+ *
981+ * This program is distributed in the hope that it will be useful,
982+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
983+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
984+ * GNU Lesser General Public License for more details.
985+ *
986+ * You should have received a copy of the GNU Lesser General Public License
987+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
988+ */
989+
990+import QtQuick 2.4
991+import Utils 0.1
992+
993+Item {
994+ id: root
995+ property alias menuModel: subMenuRepeater.model
996+ readonly property alias submenuItems: submenuDelegates
997+ property var focusWindow: undefined
998+
999+ ListModel {
1000+ id: submenuDelegates
1001+ }
1002+
1003+ Repeater {
1004+ id: subMenuRepeater
1005+
1006+ onItemAdded: {
1007+ submenuDelegates.insert(index, { delegate: item.item });
1008+ }
1009+ onItemRemoved: {
1010+ for (var i = 0; i < submenuDelegates.count; i++) {
1011+ if (item.item === submenuDelegates.get(i).delegate) {
1012+ submenuDelegates.remove(i, 1);
1013+ return;
1014+ }
1015+ }
1016+ }
1017+ onCountChanged: {
1018+ if (count == 0) {
1019+ submenuDelegates.clear();
1020+ }
1021+ }
1022+
1023+ delegate: Loader {
1024+ id: loader
1025+ source: "MenuItemDelegate.qml"
1026+
1027+ property int modelIndex: index
1028+
1029+ Binding {
1030+ target: loader.item
1031+ property: "index"
1032+ value: loader.modelIndex
1033+ }
1034+ Binding {
1035+ target: loader.item
1036+ property: "menuModel"
1037+ value: model.hasSubmenu ? menuModel.submenu(loader.modelIndex) : undefined
1038+ }
1039+ Binding {
1040+ target: loader.item
1041+ property: "menuData"
1042+ value: model
1043+ }
1044+ Binding {
1045+ target: loader.item
1046+ property: "focusWindow"
1047+ value: focusWindow
1048+ }
1049+ Connections {
1050+ target: loader.item
1051+ onActivate: menuModel.activate(loader.modelIndex)
1052+ }
1053+ }
1054+ }
1055+}
1056
1057=== added file 'qml/ApplicationMenus/MenuItemFactory.qml'
1058--- qml/ApplicationMenus/MenuItemFactory.qml 1970-01-01 00:00:00 +0000
1059+++ qml/ApplicationMenus/MenuItemFactory.qml 2016-01-15 13:11:04 +0000
1060@@ -0,0 +1,240 @@
1061+/*
1062+ * Copyright 2015 Canonical Ltd.
1063+ *
1064+ * This program is free software; you can redistribute it and/or modify
1065+ * it under the terms of the GNU Lesser General Public License as published by
1066+ * the Free Software Foundation; version 3.
1067+ *
1068+ * This program is distributed in the hope that it will be useful,
1069+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1070+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1071+ * GNU Lesser General Public License for more details.
1072+ *
1073+ * You should have received a copy of the GNU Lesser General Public License
1074+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1075+ */
1076+
1077+import QtQuick 2.4
1078+import Ubuntu.Settings.Menus 0.1 as Menus
1079+import Ubuntu.Components.ListItems 1.3 as ListItems
1080+import Ubuntu.Components 1.3
1081+import QtQuick.Layouts 1.1
1082+
1083+import "AppMenuUtils.js" as AppMenuUtils
1084+
1085+Loader {
1086+ id: root
1087+
1088+ property bool showShortcut: true
1089+ property var menuItemDelegate: undefined
1090+
1091+ property string text: menuItemDelegate && menuItemDelegate.label || ""
1092+
1093+ sourceComponent: {
1094+ if (menuItemDelegate.isSeparator) {
1095+ return separatorMenu;
1096+ }
1097+ if (menuItemDelegate.hasSubmenu) {
1098+ return subMenu;
1099+ }
1100+ if (menuItemDelegate.isCheck || menuItemDelegate.isRadio) {
1101+ return checkableMenu;
1102+ }
1103+ return standardMenu;
1104+ }
1105+
1106+ Component {
1107+ id: separatorMenu
1108+
1109+ Item {
1110+ implicitHeight: units.dp(6)
1111+ anchors {
1112+ left: parent.left
1113+ right: parent.right
1114+ }
1115+
1116+ Rectangle {
1117+ height: units.dp(2)
1118+ color: "#5D5D5D"
1119+ anchors {
1120+ left: parent.left
1121+ right: parent.right
1122+ verticalCenter: parent.verticalCenter
1123+ }
1124+ }
1125+ }
1126+ }
1127+
1128+ Component {
1129+ id: standardMenu
1130+
1131+ Item {
1132+ implicitWidth: column.implicitWidth + units.gu(2)
1133+ implicitHeight: column.implicitHeight + units.gu(2)
1134+
1135+ RowLayout {
1136+ id: column
1137+ spacing: units.gu(0.5)
1138+ anchors {
1139+ left: parent.left
1140+ right: parent.right
1141+ top: parent.top
1142+ margins: units.gu(1)
1143+ }
1144+
1145+ Item {
1146+ Layout.preferredWidth: units.gu(1.5)
1147+ Layout.preferredHeight: units.gu(1.5)
1148+ }
1149+
1150+ Icon {
1151+ Layout.preferredWidth: units.gu(2)
1152+ Layout.preferredHeight: units.gu(2)
1153+ Layout.alignment: Qt.AlignVCenter
1154+
1155+ visible: menuItemDelegate && menuItemDelegate.icon || false
1156+ source: menuItemDelegate && menuItemDelegate.icon || ""
1157+ }
1158+
1159+ RowLayout {
1160+ Layout.fillWidth: true
1161+ spacing: units.gu(5)
1162+
1163+ Label {
1164+ text: menuItemDelegate ? menuItemDelegate.label.htmlLabelFromMenuLabel(showShortcut) : ""
1165+ Layout.fillWidth: true
1166+ horizontalAlignment: Text.AlignLeft
1167+ color: enabled ? "#FFFFFF" : "#888888"
1168+ textFormat: Text.StyledText
1169+ fontSize: "small"
1170+ }
1171+
1172+ Label {
1173+ text: menuItemDelegate && menuItemDelegate.shortcut !== undefined ? menuItemDelegate.shortcut : ""
1174+ Layout.alignment: Qt.AlignRight
1175+ color: "#5D5D5D"
1176+ fontSize: "small"
1177+ }
1178+ }
1179+ }
1180+ }
1181+ }
1182+
1183+ Component {
1184+ id: subMenu
1185+
1186+ Item {
1187+ implicitWidth: column.implicitWidth + units.gu(2)
1188+ implicitHeight: column.implicitHeight + units.gu(2)
1189+
1190+ RowLayout {
1191+ id: column
1192+ spacing: units.gu(0.5)
1193+ anchors {
1194+ left: parent.left
1195+ right: parent.right
1196+ top: parent.top
1197+ margins: units.gu(1)
1198+ }
1199+
1200+ Item {
1201+ Layout.preferredWidth: units.gu(1.5)
1202+ Layout.preferredHeight: units.gu(1.5)
1203+ }
1204+
1205+ Icon {
1206+ Layout.preferredWidth: units.gu(2)
1207+ Layout.preferredHeight: units.gu(2)
1208+ Layout.alignment: Qt.AlignVCenter
1209+
1210+ visible: menuItemDelegate && menuItemDelegate.icon || false
1211+ source: menuItemDelegate && menuItemDelegate.icon || ""
1212+ }
1213+
1214+ Label {
1215+ id: _title
1216+ text: menuItemDelegate && menuItemDelegate.label.htmlLabelFromMenuLabel(showShortcut) || ""
1217+ Layout.fillWidth: true
1218+ horizontalAlignment: Text.AlignLeft
1219+ color: enabled ? "#FFFFFF" : "#5D5D5D"
1220+ textFormat: Text.StyledText
1221+ fontSize: "small"
1222+ }
1223+
1224+ Icon {
1225+ id: chevron
1226+ visible: menuItemDelegate.hasSubmenu
1227+
1228+ Layout.preferredHeight: units.gu(1.5)
1229+ Layout.alignment: Qt.AlignVCenter
1230+ name: "chevron"
1231+ color: enabled ? "#FFFFFF" : "#5D5D5D"
1232+ }
1233+ }
1234+ }
1235+ }
1236+
1237+
1238+ Component {
1239+ id: checkableMenu
1240+
1241+ Item {
1242+ implicitWidth: column.implicitWidth + units.gu(2)
1243+ implicitHeight: column.height + units.gu(2)
1244+
1245+ RowLayout {
1246+ id: column
1247+ spacing: units.gu(0.5)
1248+ anchors {
1249+ left: parent.left
1250+ right: parent.right
1251+ top: parent.top
1252+ margins: units.gu(1)
1253+ }
1254+
1255+ Item {
1256+ Layout.preferredWidth: units.gu(1.5)
1257+ Layout.preferredHeight: units.gu(1.5)
1258+ Layout.alignment: Qt.AlignVCenter
1259+
1260+ Icon {
1261+ anchors.fill: parent
1262+ visible: menuItemDelegate && menuItemDelegate.isToggled
1263+ name: "tick"
1264+ color: enabled ? "#FFFFFF" : "#5D5D5D"
1265+ }
1266+ }
1267+
1268+ Icon {
1269+ Layout.preferredWidth: units.gu(2)
1270+ Layout.preferredHeight: units.gu(2)
1271+ Layout.alignment: Qt.AlignVCenter
1272+
1273+ visible: menuItemDelegate && menuItemDelegate.icon
1274+ source: menuItemDelegate && menuItemDelegate.icon || ""
1275+ }
1276+
1277+ RowLayout {
1278+ Layout.fillWidth: true
1279+ spacing: units.gu(5)
1280+
1281+ Label {
1282+ text: menuItemDelegate ? menuItemDelegate.label.htmlLabelFromMenuLabel(showShortcut) : ""
1283+ Layout.fillWidth: true
1284+ horizontalAlignment: Text.AlignLeft
1285+ color: enabled ? "#FFFFFF" : "#888888"
1286+ textFormat: Text.StyledText
1287+ fontSize: "small"
1288+ }
1289+
1290+ Label {
1291+ text: menuItemDelegate && menuItemDelegate.shortcut !== undefined ? menuItemDelegate.shortcut : ""
1292+ Layout.alignment: Qt.AlignRight
1293+ color: "#5D5D5D"
1294+ fontSize: "small"
1295+ }
1296+ }
1297+ }
1298+ }
1299+ }
1300+}
1301
1302=== added file 'qml/ApplicationMenus/MenuPage.qml'
1303--- qml/ApplicationMenus/MenuPage.qml 1970-01-01 00:00:00 +0000
1304+++ qml/ApplicationMenus/MenuPage.qml 2016-01-15 13:11:04 +0000
1305@@ -0,0 +1,277 @@
1306+/*
1307+ * Copyright 2015 Canonical Ltd.
1308+ *
1309+ * This program is free software; you can redistribute it and/or modify
1310+ * it under the terms of the GNU Lesser General Public License as published by
1311+ * the Free Software Foundation; version 3.
1312+ *
1313+ * This program is distributed in the hope that it will be useful,
1314+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1315+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1316+ * GNU Lesser General Public License for more details.
1317+ *
1318+ * You should have received a copy of the GNU Lesser General Public License
1319+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1320+ */
1321+
1322+import QtQuick 2.4
1323+import QtQuick.Layouts 1.1
1324+import Ubuntu.Components 1.3
1325+
1326+MenuBase {
1327+ id: root
1328+ objectName: "menuPage"
1329+
1330+ property bool vertical: false
1331+ property alias delegateModel: repeater.model
1332+
1333+ width: listView.width
1334+ height: listView.height
1335+ opensVertically: false
1336+ enableMnemonic: focus
1337+
1338+ Keys.onDownPressed: {
1339+ listView.selectNext();
1340+ event.accepted = true;
1341+ }
1342+ Keys.onUpPressed: {
1343+ listView.selectPrevious();
1344+ event.accepted = true;
1345+ }
1346+
1347+ Keys.onLeftPressed: {
1348+ if (root.openItem !== undefined) {
1349+ if (!focus) {
1350+ // If we don't have the focus, then a child must.
1351+ // Close the child and let our parent handle the left press
1352+ root.closePopup(true);
1353+ event.accepted = true;
1354+ return;
1355+ }
1356+ }
1357+ event.accepted = false;
1358+ }
1359+ Keys.onRightPressed: {
1360+ if (listView.selectedItem) {
1361+ if (listView.selectedItem.delegate.hasSubmenu) {
1362+ root.open(listView.selectedItem, true, 0);
1363+ event.accepted = true;
1364+ return;
1365+ }
1366+ }
1367+ event.accepted = false;
1368+ }
1369+ Keys.onReturnPressed: {
1370+ if (listView.selectedItem) {
1371+ if (listView.selectedItem.delegate.hasSubmenu) {
1372+ root.open(listView.selectedItem, true, 0);
1373+ event.accepted = true;
1374+ return;
1375+ } else {
1376+ listView.selectedItem.delegate.activate();
1377+ event.accepted = true;
1378+ }
1379+ }
1380+ }
1381+
1382+ function setSelectedItem(index) {
1383+ if (repeater.count <= index) return;
1384+ var item = repeater.itemAt(index);
1385+ item.selected = true;
1386+ }
1387+
1388+ BorderImage {
1389+ anchors {
1390+ fill: root
1391+ margins: -units.gu(1)
1392+ }
1393+ source: "../Stages/graphics/dropshadow2gu.sci"
1394+ opacity: 0.3
1395+ }
1396+
1397+ Rectangle {
1398+ anchors.fill: parent
1399+ color: "#292929"
1400+ }
1401+
1402+ ColumnLayout {
1403+ id: listView
1404+ objectName: root.objectName+"-ListView"
1405+ spacing: 0
1406+
1407+ property var selectedItem: undefined
1408+
1409+ function selectNext() {
1410+ var delegate;
1411+ var newIndex = 0;
1412+ if (listView.selectedItem === undefined && repeater.count > 0) {
1413+ while (repeater.count > newIndex) {
1414+ delegate = repeater.itemAt(newIndex++);
1415+ if (delegate.enabled) {
1416+ delegate.selected = true;
1417+ break;
1418+ }
1419+ }
1420+ } else if (listView.selectedItem !== undefined && repeater.count > 1) {
1421+ var startIndex = (listView.selectedItem.ownIndex + 1) % repeater.count;
1422+ newIndex = startIndex;
1423+ do {
1424+ delegate = repeater.itemAt(newIndex);
1425+ if (delegate.enabled) {
1426+ delegate.selected = true;
1427+ break;
1428+ }
1429+ newIndex = (newIndex + 1) % repeater.count;
1430+ } while (newIndex !== startIndex)
1431+ }
1432+ }
1433+
1434+ function selectPrevious() {
1435+ var delegate;
1436+ var newIndex = repeater.count-1;
1437+ if (listView.selectedItem === undefined && repeater.count > 0) {
1438+ while (repeater.count > newIndex) {
1439+ delegate = repeater.itemAt(newIndex--);
1440+ if (delegate.enabled) {
1441+ delegate.selected = true;
1442+ break;
1443+ }
1444+ }
1445+ } else if (listView.selectedItem !== undefined && repeater.count > 1) {
1446+ var startIndex = listView.selectedItem.ownIndex - 1;
1447+ newIndex = startIndex;
1448+ do {
1449+ if (newIndex < 0) newIndex = repeater.count - 1;
1450+ delegate = repeater.itemAt(newIndex--);
1451+ if (delegate.enabled) {
1452+ delegate.selected = true;
1453+ break;
1454+ }
1455+ } while (newIndex !== startIndex)
1456+ }
1457+ }
1458+
1459+ Repeater {
1460+ id: repeater
1461+
1462+ // This fixes the ordering issues with using repeater in Layouts.
1463+ onCountChanged: {
1464+ var i = 0;
1465+ for (; i < repeater.count; i++) {
1466+ var item = repeater.itemAt(i)
1467+ item.parent = null;
1468+ item.parent = listView;
1469+ }
1470+ }
1471+ onItemRemoved: {
1472+ if (item.menuItem === listView.selectedItem) {
1473+ if (root.openItem == item.menuItem) {
1474+ root.closePopup(true);
1475+ }
1476+ listView.selectedItem = undefined;
1477+ }
1478+ }
1479+
1480+ delegate: MenuItemFactory {
1481+ id: repeaterItem
1482+ property bool selected: false
1483+ enabled: model.delegate.sensitive && !model.delegate.isSeparator
1484+
1485+ menuItemDelegate: model.delegate
1486+ showShortcut: enableMnemonic
1487+
1488+ Layout.fillWidth: true
1489+ Layout.preferredHeight: repeaterItem.implicitHeight
1490+
1491+ Rectangle {
1492+ visible: repeaterItem.selected
1493+ anchors.fill: parent
1494+ gradient: UbuntuColors.orangeGradient
1495+ }
1496+
1497+ property alias menuItem: _menuItem
1498+
1499+ MenuItemBase {
1500+ id: _menuItem
1501+ objectName: root.objectName + "-menu" + index
1502+ property int ownIndex: index
1503+
1504+ anchors.fill: parent
1505+ delegate: model.delegate
1506+
1507+ mnemonicAction {
1508+ property string action: model.delegate.label.actionKeyFromMenuLabel()
1509+ enabled: enableMnemonic && repeaterItem.enabled
1510+ shortcut: action !== "" ? action : ""
1511+
1512+ onTriggered: {
1513+ if (model.delegate.hasSubmenu) {
1514+ root.open(menuItem, true, 0);
1515+ }
1516+ else {
1517+ model.delegate.activate();
1518+ }
1519+ }
1520+ }
1521+ }
1522+
1523+ MouseArea {
1524+ anchors.fill: parent
1525+ hoverEnabled: true
1526+ z: 100
1527+
1528+ onEntered: {
1529+ repeaterItem.selected = true;
1530+ }
1531+ onClicked: {
1532+ if (model.delegate.hasSubmenu) {
1533+ root.open(menuItem, true);
1534+ }
1535+ else if (root.enabled) {
1536+ model.delegate.activate()
1537+ }
1538+ }
1539+ }
1540+
1541+ onSelectedChanged: {
1542+ if (selected) {
1543+ listView.selectedItem = menuItem;
1544+ if (root.openItem !== menuItem) {
1545+ root.closePopup(true);
1546+ if (model.delegate.hasSubmenu) {
1547+ root.openWithDelay(menuItem, false);
1548+ }
1549+ }
1550+ }
1551+ else if (listView.selectedItem === menuItem) {
1552+ listView.selectedItem = undefined;
1553+ if (root.openItem == menuItem) {
1554+ root.closePopup(true);
1555+ }
1556+ }
1557+ }
1558+
1559+ Connections {
1560+ target: listView
1561+ onSelectedItemChanged: {
1562+ if (repeaterItem.selected && (listView.selectedItem === undefined || listView.selectedItem !== menuItem)) {
1563+ repeaterItem.selected = false;
1564+ if (root.openItem == menuItem) {
1565+ root.closePopup(true);
1566+ }
1567+ }
1568+ }
1569+ }
1570+
1571+ Connections {
1572+ target: root
1573+ onOpenItemChanged: {
1574+ if (root.openItem == menuItem) {
1575+ repeaterItem.selected = true;
1576+ }
1577+ }
1578+ }
1579+ }
1580+ }
1581+ }
1582+}
1583
1584=== modified file 'qml/CMakeLists.txt'
1585--- qml/CMakeLists.txt 2015-03-06 04:44:11 +0000
1586+++ qml/CMakeLists.txt 2016-01-15 13:11:04 +0000
1587@@ -5,6 +5,7 @@
1588 )
1589
1590 set(QML_DIRS
1591+ ApplicationMenus
1592 Components
1593 Dash
1594 graphics
1595
1596=== modified file 'qml/Components/PanelState/PanelState.qml'
1597--- qml/Components/PanelState/PanelState.qml 2015-11-20 13:06:14 +0000
1598+++ qml/Components/PanelState/PanelState.qml 2016-01-15 13:11:04 +0000
1599@@ -21,9 +21,10 @@
1600 id: root
1601
1602 property string title: ""
1603- property bool buttonsVisible: false
1604+ property bool decorationsVisible: false
1605 property bool dropShadow: false
1606 property int panelHeight: units.gu(3)
1607+ property string maximizedApplication: ""
1608
1609 signal close()
1610 signal minimize()
1611
1612=== modified file 'qml/Panel/Panel.qml'
1613--- qml/Panel/Panel.qml 2015-11-25 13:57:34 +0000
1614+++ qml/Panel/Panel.qml 2016-01-15 13:11:04 +0000
1615@@ -17,6 +17,9 @@
1616 import QtQuick 2.4
1617 import Ubuntu.Components 1.3
1618 import Unity.Application 0.1
1619+import Unity.Indicators 0.1
1620+import Utils 0.1
1621+import "../ApplicationMenus"
1622 import "../Components"
1623 import "../Components/PanelState"
1624 import ".."
1625@@ -101,13 +104,14 @@
1626 }
1627
1628 MouseArea {
1629+ id: decorationMouseArea
1630 anchors {
1631 top: parent.top
1632 left: parent.left
1633 right: indicators.left
1634 }
1635 height: indicators.minimizedPanelHeight
1636- hoverEnabled: true
1637+ hoverEnabled: !indicators.shown
1638 onClicked: callHint.visible ? callHint.showLiveCall() : PanelState.focusMaximizedApp()
1639 onDoubleClicked: PanelState.maximize()
1640
1641@@ -124,12 +128,80 @@
1642 bottomMargin: units.gu(0.5)
1643 }
1644 height: indicators.minimizedPanelHeight - anchors.topMargin - anchors.bottomMargin
1645- visible: PanelState.buttonsVisible && parent.containsMouse && !root.locked && !callHint.visible
1646- active: PanelState.buttonsVisible
1647+ visible: d.showDecorations
1648+ active: PanelState.decorationsVisible
1649 onClose: PanelState.close()
1650 onMinimize: PanelState.minimize()
1651 onMaximize: PanelState.maximize()
1652 }
1653+
1654+ WindowKeysFilter {
1655+ id: altFilter
1656+ property bool altPressed: false
1657+ property bool longAltPressed: false
1658+ enabled: d.enableMenus
1659+ Keys.onPressed: {
1660+ if (event.key === Qt.Key_Alt && !event.isAutoRepeat) {
1661+ altPressed = true;
1662+ longAltPressed = false;
1663+ menuBarShortcutTimer.start();
1664+ return;
1665+ }
1666+ event.accepted = false;
1667+ }
1668+ Keys.onReleased: {
1669+ if (event.key === Qt.Key_Alt) {
1670+ menuBarShortcutTimer.stop();
1671+ altPressed = false;
1672+ longAltPressed = false;
1673+ return;
1674+ }
1675+ event.accepted = false
1676+ }
1677+
1678+ Timer {
1679+ id: menuBarShortcutTimer
1680+ interval: 200
1681+ repeat: false
1682+ onTriggered: {
1683+ altFilter.longAltPressed = true;
1684+ }
1685+ }
1686+ }
1687+
1688+ Loader {
1689+ id: menuBarLoader
1690+ anchors {
1691+ left: windowControlButtons.right
1692+ leftMargin: units.gu(3)
1693+ top: parent.top
1694+ }
1695+ height: parent.height
1696+ visible: d.showDecorations
1697+
1698+ sourceComponent: PanelState.maximizedApplication ? menuBarComponent : undefined
1699+ Component {
1700+ id: menuBarComponent
1701+ MenuBar {
1702+ id: menuBar
1703+ height: menuBarLoader.height
1704+ enableMnemonic: altFilter.altPressed
1705+ enabled: d.enableMenus
1706+ menuModel: sharedAppModel.model
1707+
1708+ SharedUnityMenuModel {
1709+ id: sharedAppModel
1710+
1711+ property var application: ApplicationManager.findApplication(PanelState.maximizedApplication)
1712+ property var surface: application ? application.session ? application.session.lastSurface : null : null
1713+
1714+ busName: surface ? surface.dbusMenuName : ""
1715+ menuObjectPath: surface ? surface.dbusMenuObjectPath : ""
1716+ actions: surface && surface.dbusMenuObjectPath ? { "unity": surface.dbusMenuObjectPath } : {}
1717+ }
1718+ }
1719+ }
1720+ }
1721 }
1722
1723 IndicatorsMenu {
1724@@ -162,6 +234,9 @@
1725 callHint.showLiveCall();
1726 }
1727 }
1728+ onShownChanged: {
1729+ if (d.menuBar) d.menuBar.close();
1730+ }
1731 }
1732
1733 Label {
1734@@ -174,17 +249,15 @@
1735 topMargin: units.gu(0.5)
1736 bottomMargin: units.gu(0.5)
1737 }
1738- color: PanelState.buttonsVisible ? "#ffffff" : "#5d5d5d"
1739+ color: PanelState.decorationsVisible ? "#ffffff" : "#5d5d5d"
1740 height: indicators.minimizedPanelHeight - anchors.topMargin - anchors.bottomMargin
1741- visible: !windowControlButtons.visible && !root.locked && !callHint.visible
1742+ visible: !d.showDecorations
1743 verticalAlignment: Text.AlignVCenter
1744 fontSize: "medium"
1745 font.weight: Font.Normal
1746 text: PanelState.title
1747 }
1748
1749- // TODO here would the Locally integrated menus come
1750-
1751 ActiveCallHint {
1752 id: __callHint
1753 anchors {
1754@@ -200,6 +273,17 @@
1755 id: d
1756 objectName: "panelPriv"
1757 readonly property real indicatorHeight: indicators.minimizedPanelHeight
1758+
1759+ property var menuBar: menuBarLoader.item
1760+
1761+ property bool enableMenus: PanelState.decorationsVisible && !root.locked && !callHint.visible &&
1762+ menuBar &&
1763+ menuBar.hasChildren
1764+
1765+ property bool showDecorations : PanelState.decorationsVisible && !root.locked && !callHint.visible &&
1766+ (altFilter.longAltPressed ||
1767+ (decorationMouseArea.hoverEnabled && decorationMouseArea.containsMouse) ||
1768+ (menuBar && menuBar.openItem !== undefined))
1769 }
1770
1771 states: [
1772
1773=== modified file 'qml/Stages/DecoratedWindow.qml'
1774--- qml/Stages/DecoratedWindow.qml 2015-11-23 15:09:45 +0000
1775+++ qml/Stages/DecoratedWindow.qml 2016-01-15 13:11:04 +0000
1776@@ -19,6 +19,11 @@
1777 import QtQuick 2.4
1778 import Ubuntu.Components 1.3
1779 import Unity.Application 0.1
1780+import Unity.Indicators 0.1 as Indicators
1781+import GlobalShortcut 1.0
1782+import "../Components/PanelState"
1783+
1784+import QMenuModel 0.1 as MenuModel
1785
1786 FocusScope {
1787 id: root
1788@@ -69,22 +74,6 @@
1789 enabled: !fullscreen
1790 }
1791
1792- WindowDecoration {
1793- id: decoration
1794- target: root.parent
1795- objectName: application ? "appWindowDecoration_" + application.appId : "appWindowDecoration_null"
1796- anchors { left: parent.left; top: parent.top; right: parent.right }
1797- height: units.gu(3)
1798- width: root.width
1799- title: window.title
1800- visible: root.decorationShown
1801-
1802- onClose: root.close();
1803- onMaximize: { root.decorationPressed(); root.maximize(); }
1804- onMinimize: root.minimize();
1805- onPressed: root.decorationPressed();
1806- }
1807-
1808 ApplicationWindow {
1809 id: applicationWindow
1810 objectName: application ? "appWindow_" + application.appId : "appWindow_null"
1811@@ -95,4 +84,49 @@
1812 interactive: true
1813 focus: true
1814 }
1815+
1816+ MouseArea {
1817+ anchors { left: parent.left; top: parent.top; right: parent.right }
1818+ height: units.gu(3)
1819+
1820+ drag.target: root.parent
1821+ drag.filterChildren: true
1822+ drag.threshold: 0
1823+ drag.minimumY: PanelState.panelHeight
1824+ drag.onActiveChanged: {
1825+ Mir.cursorName = active ? "grabbing" : ""
1826+ }
1827+
1828+ // Parent is handling drag, this handles clicks.
1829+ MouseArea {
1830+ anchors.fill: parent
1831+ onPressed: root.decorationPressed()
1832+ onDoubleClicked: root.maximize()
1833+ }
1834+
1835+ WindowDecoration {
1836+ id: decoration
1837+ objectName: application ? "appWindowDecoration_" + application.appId : "appWindowDecoration_null"
1838+ anchors.fill: parent
1839+ appId: application ? application.appId : ""
1840+ title: window.title
1841+ visible: root.decorationShown
1842+
1843+ onClose: root.close();
1844+ onMaximize: { root.decorationPressed(); root.maximize(); }
1845+ onMinimize: root.minimize();
1846+
1847+ menu: sharedAppModel.model
1848+ target: applicationWindow
1849+
1850+ Indicators.SharedUnityMenuModel {
1851+ id: sharedAppModel
1852+ property var surface: application ? application.session ? application.session.lastSurface : null : null
1853+
1854+ busName: surface ? surface.dbusMenuName : ""
1855+ menuObjectPath: surface ? surface.dbusMenuObjectPath : ""
1856+ actions: surface && surface.dbusMenuObjectPath ? { "unity": surface.dbusMenuObjectPath } : {}
1857+ }
1858+ }
1859+ }
1860 }
1861
1862=== modified file 'qml/Stages/DesktopStage.qml'
1863--- qml/Stages/DesktopStage.qml 2016-01-08 13:29:03 +0000
1864+++ qml/Stages/DesktopStage.qml 2016-01-15 13:11:04 +0000
1865@@ -187,13 +187,20 @@
1866
1867 Binding {
1868 target: PanelState
1869- property: "buttonsVisible"
1870- value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.maximized // FIXME for Locally integrated menus
1871+ property: "decorationsVisible"
1872+ value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.maximized
1873 && spread.state == ""
1874 }
1875
1876 Binding {
1877 target: PanelState
1878+ property: "maximizedApplication"
1879+ value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.maximized
1880+ && spread.state == "" ? priv.focusedAppId : ""
1881+ }
1882+
1883+ Binding {
1884+ target: PanelState
1885 property: "title"
1886 value: {
1887 if (priv.focusedAppDelegate !== null && spread.state == "") {
1888@@ -215,7 +222,7 @@
1889
1890 Component.onDestruction: {
1891 PanelState.title = "";
1892- PanelState.buttonsVisible = false;
1893+ PanelState.decorationsVisible = false;
1894 PanelState.dropShadow = false;
1895 }
1896
1897
1898=== modified file 'qml/Stages/WindowDecoration.qml'
1899--- qml/Stages/WindowDecoration.qml 2015-11-25 13:57:34 +0000
1900+++ qml/Stages/WindowDecoration.qml 2016-01-15 13:11:04 +0000
1901@@ -15,90 +15,162 @@
1902 */
1903
1904 import QtQuick 2.4
1905+import QtQuick.Layouts 1.1
1906 import Unity.Application 0.1 // For Mir singleton
1907 import Ubuntu.Components 1.3
1908+import Utils 0.1
1909 import "../Components"
1910 import "../Components/PanelState"
1911+import "../ApplicationMenus"
1912
1913-MouseArea {
1914+Item {
1915 id: root
1916- clip: true
1917
1918 property Item target
1919+ property string appId
1920 property alias title: titleLabel.text
1921 property bool active: false
1922- hoverEnabled: true
1923+ property var menu: undefined
1924
1925 signal close()
1926 signal minimize()
1927 signal maximize()
1928
1929- onDoubleClicked: root.maximize()
1930-
1931 QtObject {
1932 id: priv
1933 property real distanceX
1934 property real distanceY
1935 property bool dragging
1936- }
1937-
1938- onPressedChanged: {
1939- if (pressed) {
1940- var pos = mapToItem(root.target, mouseX, mouseY);
1941- priv.distanceX = pos.x;
1942- priv.distanceY = pos.y;
1943- priv.dragging = true;
1944- } else {
1945- priv.dragging = false;
1946- Mir.cursorName = "";
1947- }
1948- }
1949-
1950- onPositionChanged: {
1951- if (priv.dragging) {
1952- Mir.cursorName = "grabbing";
1953- var pos = mapToItem(root.target.parent, mouseX, mouseY);
1954- root.target.x = pos.x - priv.distanceX;
1955- root.target.y = Math.max(pos.y - priv.distanceY, PanelState.panelHeight);
1956- }
1957- }
1958-
1959- Rectangle {
1960- anchors.fill: parent
1961- anchors.bottomMargin: -radius
1962+
1963+ property var menuBar: menuBarLoader.item
1964+
1965+ property bool enableMenus: root.active &&
1966+ (!PanelState.decorationsVisible || PanelState.maximizedApplication !== appId) &&
1967+ menuBar &&
1968+ menuBar.hasChildren
1969+
1970+ property bool shouldShowMenus : enableMenus &&
1971+ (altFilter.longAltPressed || menuBarHover.containsMouse || menuBar.openItem !== undefined)
1972+ }
1973+
1974+ WindowKeysFilter {
1975+ id: altFilter
1976+ property bool altPressed: false
1977+ property bool longAltPressed: false
1978+ enabled: priv.enableMenus
1979+ Keys.onPressed: {
1980+ if (event.key === Qt.Key_Alt && !event.isAutoRepeat) {
1981+ altPressed = true;
1982+ longAltPressed = false;
1983+ menuBarShortcutTimer.start();
1984+ return;
1985+ }
1986+ event.accepted = false;
1987+ }
1988+ Keys.onReleased: {
1989+ if (event.key === Qt.Key_Alt) {
1990+ menuBarShortcutTimer.stop();
1991+ altPressed = false;
1992+ longAltPressed = false;
1993+ return;
1994+ }
1995+ event.accepted = false
1996+ }
1997+
1998+ Timer {
1999+ id: menuBarShortcutTimer
2000+ interval: 200
2001+ repeat: false
2002+ onTriggered: {
2003+ altFilter.longAltPressed = true;
2004+ }
2005+ }
2006+ }
2007+
2008+ // non rounded for bottom of decoration
2009+ Rectangle {
2010+ anchors.fill: parent
2011+ anchors.topMargin: units.gu(.5)
2012+ color: "#292929"
2013+ }
2014+
2015+ // rounded for top of decoration
2016+ Rectangle {
2017+ anchors.fill: parent
2018 radius: units.gu(.5)
2019 color: "#292929"
2020 }
2021
2022- Row {
2023+ RowLayout {
2024 anchors {
2025 fill: parent
2026 leftMargin: units.gu(1)
2027 rightMargin: units.gu(1)
2028- topMargin: units.gu(0.5)
2029- bottomMargin: units.gu(0.5)
2030 }
2031 spacing: units.gu(3)
2032
2033 WindowControlButtons {
2034 id: buttons
2035- height: parent.height
2036+ anchors {
2037+ top: parent.top
2038+ bottom: parent.bottom
2039+ topMargin: units.gu(0.5)
2040+ bottomMargin: units.gu(0.5)
2041+ }
2042 active: root.active
2043 onClose: root.close();
2044 onMinimize: root.minimize();
2045 onMaximize: root.maximize();
2046 }
2047
2048- Label {
2049- id: titleLabel
2050- objectName: "windowDecorationTitle"
2051- color: root.active ? "white" : "#5d5d5d"
2052- height: parent.height
2053- width: parent.width - buttons.width - parent.anchors.rightMargin - parent.anchors.leftMargin
2054- verticalAlignment: Text.AlignVCenter
2055- fontSize: "medium"
2056- font.weight: root.active ? Font.Light : Font.Normal
2057- elide: Text.ElideRight
2058+ Item {
2059+ Layout.preferredHeight: parent.height
2060+ Layout.fillWidth: true
2061+
2062+ MouseArea {
2063+ id: menuBarHover
2064+ hoverEnabled: true
2065+ anchors.fill: parent
2066+ onPressed: { mouse.accepted = false; } // just monitoring
2067+ }
2068+
2069+ Label {
2070+ id: titleLabel
2071+ objectName: "windowDecorationTitle"
2072+ color: root.active ? "white" : "#5d5d5d"
2073+ height: parent.height
2074+ width: parent.width
2075+ verticalAlignment: Text.AlignVCenter
2076+ fontSize: "medium"
2077+ font.weight: root.active ? Font.Light : Font.Normal
2078+ elide: Text.ElideRight
2079+
2080+ opacity: priv.shouldShowMenus ? 0 : 1
2081+ Behavior on opacity { UbuntuNumberAnimation { } }
2082+ }
2083+
2084+ Loader {
2085+ id: menuBarLoader
2086+ objectName: "windowDecorationMenuBarLoader"
2087+ anchors.bottom: parent.bottom
2088+ height: parent.height
2089+ width: parent.width
2090+ sourceComponent: root.menu ? menuBarComponent : undefined
2091+ Component {
2092+ id: menuBarComponent
2093+ MenuBar {
2094+ id: menuBar
2095+ height: menuBarLoader.height
2096+ focusWindow: root.target
2097+ menuModel: root.menu
2098+ enableMnemonic: altFilter.altPressed
2099+ enabled: priv.enableMenus
2100+ }
2101+ }
2102+
2103+ opacity: priv.shouldShowMenus ? 1 : 0
2104+ Behavior on opacity { UbuntuNumberAnimation { } }
2105+ }
2106 }
2107 }
2108 }
2109
2110=== modified file 'tests/imports/check_imports.py'
2111--- tests/imports/check_imports.py 2015-07-15 15:13:18 +0000
2112+++ tests/imports/check_imports.py 2016-01-15 13:11:04 +0000
2113@@ -53,6 +53,7 @@
2114 quick_good_pat = re.compile(r'.*import QtQuick 2\.4.*$')
2115 quick_layouts_good_pat = re.compile(r'.*import QtQuick.Layouts 1\.1.*$')
2116 quick_window_good_pat = re.compile(r'.*import QtQuick.Window 2\.2.*$')
2117+quick_controls_good_pat = re.compile(r'.*import QtQuick.Controls 1\.4.*$')
2118
2119 # Ubuntu Components patterns
2120 ubuntu_components_pat = re.compile(r'.*import Ubuntu.Components.*')
2121@@ -111,7 +112,7 @@
2122 for file in files:
2123 path = os.path.join(root, file)
2124 if not (ignore and path.startswith(ignore)) and pat.match(file):
2125- quick_good_pats = [quick_good_pat, quick_layouts_good_pat, quick_window_good_pat]
2126+ quick_good_pats = [quick_good_pat, quick_layouts_good_pat, quick_window_good_pat, quick_controls_good_pat]
2127 if scan_for_bad_import(path, quick_pat, quick_good_pats):
2128 found_bad_import = True
2129 if scan_for_bad_import(path, ubuntu_components_pat, [ubuntu_good_components_pat]):
2130
2131=== modified file 'tests/mocks/QMenuModel/QMenuModel.qmltypes'
2132--- tests/mocks/QMenuModel/QMenuModel.qmltypes 2015-02-13 09:01:16 +0000
2133+++ tests/mocks/QMenuModel/QMenuModel.qmltypes 2016-01-15 13:11:04 +0000
2134@@ -1,4 +1,4 @@
2135-import QtQuick.tooling 1.1
2136+import QtQuick.tooling 1.2
2137
2138 // This file describes the plugin-supplied types contained in the library.
2139 // It is used for QML tooling purposes only.
2140@@ -57,6 +57,10 @@
2141 Property { name: "actionStateParser"; type: "ActionStateParser"; isPointer: true }
2142 Property { name: "nameOwner"; type: "string"; isReadonly: true }
2143 Property { name: "modelData"; type: "QVariant" }
2144+ Signal {
2145+ name: "activated"
2146+ Parameter { name: "action"; type: "string" }
2147+ }
2148 Method {
2149 name: "insertRow"
2150 Parameter { name: "row"; type: "int" }
2151
2152=== modified file 'tests/mocks/QMenuModel/unitymenumodel.cpp'
2153--- tests/mocks/QMenuModel/unitymenumodel.cpp 2014-10-07 10:28:59 +0000
2154+++ tests/mocks/QMenuModel/unitymenumodel.cpp 2016-01-15 13:11:04 +0000
2155@@ -18,6 +18,8 @@
2156
2157 #include "unitymenumodel.h"
2158
2159+#include <QDebug>
2160+
2161 enum MenuRoles {
2162 LabelRole = Qt::DisplayRole + 1,
2163 SensitiveRole,
2164@@ -29,7 +31,9 @@
2165 ActionStateRole,
2166 IsCheckRole,
2167 IsRadioRole,
2168- IsToggledRole
2169+ IsToggledRole,
2170+ ShortcutRole,
2171+ HasSubmenuRole
2172 };
2173
2174 UnityMenuModel::UnityMenuModel(QObject *parent)
2175@@ -149,7 +153,10 @@
2176 if (m_modelData.count() <= row) {
2177 return QVariantMap();
2178 }
2179- return m_modelData[row].toMap()["rowData"].toMap();
2180+ QVariantMap vRow = m_modelData[row].toMap();
2181+ QVariantMap map = vRow["rowData"].toMap();
2182+ map["hasSubmenu"] = vRow.contains("submenu");
2183+ return map;
2184 }
2185
2186 QVariant UnityMenuModel::subMenuData(int row) const
2187@@ -182,6 +189,8 @@
2188 names[IsCheckRole] = "isCheck";
2189 names[IsRadioRole] = "isRadio";
2190 names[IsToggledRole] = "isToggled";
2191+ names[ShortcutRole] = "shortcut";
2192+ names[HasSubmenuRole] = "hasSubmenu";
2193
2194 return names;
2195 }
2196@@ -201,6 +210,7 @@
2197 UnityMenuModel*& model = submenus[position];
2198 if (!model) {
2199 model = new UnityMenuModel(this);
2200+ connect(model, &UnityMenuModel::activated, this, &UnityMenuModel::activated);
2201 }
2202 if (model->modelData() != submenuData) {
2203 model->setModelData(submenuData);
2204@@ -228,8 +238,20 @@
2205 return this->data(this->index(row, 0), roles[role]);
2206 }
2207
2208-void UnityMenuModel::activate(int, const QVariant&)
2209+void UnityMenuModel::activate(int row, const QVariant&)
2210 {
2211+ QVariantMap vModelData = m_modelData[row].toMap();
2212+ QVariantMap rd = vModelData["rowData"].toMap();
2213+
2214+ bool isCheckable = rd[roleNames()[IsCheckRole]].toBool() || rd[roleNames()[IsRadioRole]].toBool();
2215+ if (isCheckable) {
2216+ rd[roleNames()[IsToggledRole]] = !rd[roleNames()[IsToggledRole]].toBool();
2217+ vModelData["rowData"] = rd;
2218+ m_modelData[row] = vModelData;
2219+
2220+ dataChanged(index(row, 0), index(row, 0), QVector<int>() << IsToggledRole);
2221+ }
2222+ Q_EMIT activated(rd[roleNames()[ActionRole]].toString());
2223 }
2224
2225 void UnityMenuModel::changeState(int, const QVariant&)
2226
2227=== modified file 'tests/mocks/QMenuModel/unitymenumodel.h'
2228--- tests/mocks/QMenuModel/unitymenumodel.h 2015-04-30 09:31:51 +0000
2229+++ tests/mocks/QMenuModel/unitymenumodel.h 2016-01-15 13:11:04 +0000
2230@@ -88,6 +88,8 @@
2231 // Internal mock usage
2232 void modelDataChanged();
2233
2234+ void activated(const QString& action);
2235+
2236 private:
2237 QVariantMap rowData(int row) const;
2238 QVariant subMenuData(int row) const;
2239
2240=== modified file 'tests/mocks/Unity/Application/MirSurface.cpp'
2241--- tests/mocks/Unity/Application/MirSurface.cpp 2015-11-06 15:52:24 +0000
2242+++ tests/mocks/Unity/Application/MirSurface.cpp 2016-01-15 13:11:04 +0000
2243@@ -129,6 +129,16 @@
2244 Q_EMIT orientationAngleChanged(angle);
2245 }
2246
2247+QString MirSurface::dbusMenuName() const
2248+{
2249+ return name();
2250+}
2251+
2252+QString MirSurface::dbusMenuObjectPath() const
2253+{
2254+ return QString("/%1").arg(name());
2255+}
2256+
2257
2258
2259 void MirSurface::registerView(qintptr viewId)
2260
2261=== modified file 'tests/mocks/Unity/Application/MirSurface.h'
2262--- tests/mocks/Unity/Application/MirSurface.h 2015-11-06 15:52:24 +0000
2263+++ tests/mocks/Unity/Application/MirSurface.h 2016-01-15 13:11:04 +0000
2264@@ -66,6 +66,9 @@
2265 Mir::OrientationAngle orientationAngle() const override;
2266 void setOrientationAngle(Mir::OrientationAngle) override;
2267
2268+ QString dbusMenuName() const override;
2269+ QString dbusMenuObjectPath() const override;
2270+
2271 ////
2272 // API for tests
2273
2274
2275=== modified file 'tests/mocks/Utils/CMakeLists.txt'
2276--- tests/mocks/Utils/CMakeLists.txt 2015-10-28 10:32:47 +0000
2277+++ tests/mocks/Utils/CMakeLists.txt 2016-01-15 13:11:04 +0000
2278@@ -19,6 +19,7 @@
2279 ${CMAKE_SOURCE_DIR}/plugins/Utils/easingcurve.cpp
2280 ${CMAKE_SOURCE_DIR}/plugins/Utils/inputwatcher.cpp
2281 ${CMAKE_SOURCE_DIR}/plugins/Utils/applicationsfiltermodel.cpp
2282+ ${CMAKE_SOURCE_DIR}/plugins/Utils/shortcutaction.cpp
2283 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
2284 constants.cpp
2285 plugin.cpp
2286
2287=== modified file 'tests/mocks/Utils/constants.cpp'
2288--- tests/mocks/Utils/constants.cpp 2015-04-15 14:52:01 +0000
2289+++ tests/mocks/Utils/constants.cpp 2016-01-15 13:11:04 +0000
2290@@ -20,4 +20,11 @@
2291 : QObject(parent)
2292 {
2293 m_indicatorValueTimeout = 5000;
2294+ m_menuHoverOpenInterval = 250;
2295+}
2296+
2297+void Constants::setMenuHoverOpenInterval(int menuHoverOpenInterval)
2298+{
2299+ m_menuHoverOpenInterval = menuHoverOpenInterval;
2300+ Q_EMIT menuHoverOpenIntervalChanged();
2301 }
2302
2303=== modified file 'tests/mocks/Utils/constants.h'
2304--- tests/mocks/Utils/constants.h 2015-04-15 14:52:01 +0000
2305+++ tests/mocks/Utils/constants.h 2016-01-15 13:11:04 +0000
2306@@ -30,14 +30,21 @@
2307 {
2308 Q_OBJECT
2309 Q_PROPERTY(int indicatorValueTimeout READ indicatorValueTimeout CONSTANT)
2310+ Q_PROPERTY(int menuHoverOpenInterval READ menuHoverOpenInterval WRITE setMenuHoverOpenInterval NOTIFY menuHoverOpenIntervalChanged)
2311
2312 public:
2313 Constants(QObject *parent = 0);
2314
2315 int indicatorValueTimeout() const { return m_indicatorValueTimeout; }
2316+ int menuHoverOpenInterval() const { return m_menuHoverOpenInterval; }
2317+ void setMenuHoverOpenInterval(int menuHoverOpenInterval);
2318+
2319+Q_SIGNALS:
2320+ void menuHoverOpenIntervalChanged();
2321
2322 private:
2323 int m_indicatorValueTimeout;
2324+ int m_menuHoverOpenInterval;
2325 };
2326
2327 #endif
2328
2329=== modified file 'tests/mocks/Utils/plugin.cpp'
2330--- tests/mocks/Utils/plugin.cpp 2015-10-28 10:32:47 +0000
2331+++ tests/mocks/Utils/plugin.cpp 2016-01-15 13:11:04 +0000
2332@@ -36,6 +36,7 @@
2333 #include <windowscreenshotprovider.h>
2334 #include <easingcurve.h>
2335 #include <applicationsfiltermodel.h>
2336+#include <shortcutaction.h>
2337
2338 static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
2339 {
2340@@ -65,6 +66,7 @@
2341 qmlRegisterSingletonType<Constants>(uri, 0, 1, "Constants", createConstants);
2342 qmlRegisterType<ActiveFocusLogger>(uri, 0, 1, "ActiveFocusLogger");
2343 qmlRegisterType<ApplicationsFilterModel>(uri, 0, 1, "ApplicationsFilterModel");
2344+ qmlRegisterType<ShortcutAction>(uri, 0, 1, "ShortcutAction");
2345 }
2346
2347 void FakeUtilsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
2348
2349=== added directory 'tests/qmltests/ApplicationMenus'
2350=== added file 'tests/qmltests/ApplicationMenus/tst_MenuBar.qml'
2351--- tests/qmltests/ApplicationMenus/tst_MenuBar.qml 1970-01-01 00:00:00 +0000
2352+++ tests/qmltests/ApplicationMenus/tst_MenuBar.qml 2016-01-15 13:11:04 +0000
2353@@ -0,0 +1,132 @@
2354+/*
2355+ * Copyright (C) 2016 Canonical, Ltd.
2356+ *
2357+ * This program is free software; you can redistribute it and/or modify
2358+ * it under the terms of the GNU General Public License as published by
2359+ * the Free Software Foundation; version 3.
2360+ *
2361+ * This program is distributed in the hope that it will be useful,
2362+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2363+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2364+ * GNU General Public License for more details.
2365+ *
2366+ * You should have received a copy of the GNU General Public License
2367+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2368+ */
2369+
2370+import QtQuick 2.4
2371+import QtTest 1.0
2372+import Ubuntu.Components 1.3
2373+import Ubuntu.Components.ListItems 1.3
2374+import Unity.Application 0.1
2375+import QMenuModel 0.1
2376+import Unity.Test 0.1
2377+import Utils 0.1
2378+
2379+import "../../../qml/ApplicationMenus"
2380+import "../Stages"
2381+
2382+Item {
2383+ id: root
2384+ width: units.gu(100)
2385+ height: units.gu(50)
2386+
2387+ Component.onCompleted: {
2388+ Constants.menuHoverOpenInterval = 1;
2389+ }
2390+
2391+ DesktopMenuData { id: desktopMenuData }
2392+
2393+ Rectangle {
2394+ anchors {
2395+ left: parent.left
2396+ right: parent.right
2397+ top: parent.top
2398+ margins: units.gu(1)
2399+ }
2400+ height: units.gu(3)
2401+ color: "grey"
2402+
2403+ MenuBar {
2404+ id: menuBar
2405+ anchors.fill: parent
2406+
2407+ focusWindow: root
2408+ enableMnemonic: true
2409+ menuModel: UnityMenuModel {
2410+ id: menuBackend
2411+ modelData: desktopMenuData.testData
2412+ }
2413+ }
2414+ }
2415+
2416+ SignalSpy {
2417+ id: activatedSpy
2418+ target: menuBackend
2419+ signalName: "activated"
2420+ }
2421+
2422+ UnityTestCase {
2423+ id: testCase
2424+ name: "MenuPage"
2425+ when: windowShown
2426+
2427+ property bool clickNavigate: true
2428+
2429+ function init() {
2430+ menuBar.closePopup();
2431+ menuBackend.modelData = desktopMenuData.generateTestData(3, 2, 0);
2432+ activatedSpy.clear();
2433+ }
2434+
2435+ function test_mnemonics_data() {
2436+ return [
2437+ { tag: "a" },
2438+ { tag: "b" },
2439+ ]
2440+ }
2441+
2442+ function test_mnemonics(data) {
2443+ menuBackend.modelData = desktopMenuData.generateTestData(3, 2, 0);
2444+
2445+ keyPress(data.tag, Qt.AltModifier, 100);
2446+ tryCompareFunction(function() { return menuBar.openItem !== undefined; }, true);
2447+ }
2448+
2449+ function test_navigateRight(data) {
2450+ var menuItem0 = findChild(menuBar, "menuBar-menu0"); verify(menuItem0);
2451+ var menuItem1 = findChild(menuBar, "menuBar-menu1"); verify(menuItem1);
2452+ var menuItem2 = findChild(menuBar, "menuBar-menu2"); verify(menuItem2);
2453+
2454+ menuBar.open(menuItem0, true);
2455+ compare(menuBar.openItem, menuItem0);
2456+
2457+ keyClick(Qt.Key_Right, Qt.NoModifier);
2458+ compare(menuBar.openItem, menuItem1);
2459+
2460+ keyClick(Qt.Key_Right, Qt.NoModifier);
2461+ compare(menuBar.openItem, menuItem2);
2462+
2463+ keyClick(Qt.Key_Right, Qt.NoModifier);
2464+ compare(menuBar.openItem, menuItem0);
2465+ }
2466+
2467+ function test_navigateLeft(data) {
2468+ var menuItem0 = findChild(menuBar, "menuBar-menu0"); verify(menuItem0);
2469+ var menuItem1 = findChild(menuBar, "menuBar-menu1"); verify(menuItem1);
2470+ var menuItem2 = findChild(menuBar, "menuBar-menu2"); verify(menuItem2);
2471+
2472+ menuBar.open(menuItem0, true);
2473+ compare(menuBar.openItem, menuItem0);
2474+
2475+ keyClick(Qt.Key_Left, Qt.NoModifier);
2476+ compare(menuBar.openItem, menuItem2);
2477+
2478+ keyClick(Qt.Key_Left, Qt.NoModifier);
2479+ compare(menuBar.openItem, menuItem1);
2480+
2481+ keyClick(Qt.Key_Left, Qt.NoModifier);
2482+ compare(menuBar.openItem, menuItem0);
2483+ }
2484+ }
2485+}
2486
2487=== added file 'tests/qmltests/ApplicationMenus/tst_MenuPage.qml'
2488--- tests/qmltests/ApplicationMenus/tst_MenuPage.qml 1970-01-01 00:00:00 +0000
2489+++ tests/qmltests/ApplicationMenus/tst_MenuPage.qml 2016-01-15 13:11:04 +0000
2490@@ -0,0 +1,256 @@
2491+/*
2492+ * Copyright (C) 2016 Canonical, Ltd.
2493+ *
2494+ * This program is free software; you can redistribute it and/or modify
2495+ * it under the terms of the GNU General Public License as published by
2496+ * the Free Software Foundation; version 3.
2497+ *
2498+ * This program is distributed in the hope that it will be useful,
2499+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2500+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2501+ * GNU General Public License for more details.
2502+ *
2503+ * You should have received a copy of the GNU General Public License
2504+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2505+ */
2506+
2507+import QtQuick 2.4
2508+import QtTest 1.0
2509+import Ubuntu.Components 1.3
2510+import Ubuntu.Components.ListItems 1.3
2511+import Unity.Application 0.1
2512+import QMenuModel 0.1
2513+import Unity.Test 0.1
2514+import Utils 0.1
2515+
2516+import "../../../qml/ApplicationMenus"
2517+import "../Stages"
2518+
2519+Item {
2520+ id: root
2521+ width: Math.max(units.gu(100), page.width + units.gu(6))
2522+ height: Math.max(units.gu(50), page.height + units.gu(6))
2523+
2524+ Component.onCompleted: {
2525+ Constants.menuHoverOpenInterval = 1;
2526+ }
2527+
2528+ DesktopMenuData { id: desktopMenuData }
2529+
2530+ Keys.onEscapePressed: {
2531+ page.closePopup(true);
2532+ }
2533+
2534+ MenuPage {
2535+ id: page
2536+ focus: true
2537+
2538+ anchors {
2539+ left: parent.left
2540+ top: parent.top
2541+ leftMargin: units.gu(3)
2542+ topMargin: units.gu(3)
2543+ }
2544+
2545+ delegateModel: rootDelegate.submenuItems
2546+ MenuItemDelegateBase {
2547+ id: rootDelegate
2548+ menuModel: UnityMenuModel {
2549+ id: menuBackend
2550+ modelData: desktopMenuData.generateTestData(3, 3, 0);
2551+ }
2552+ }
2553+ }
2554+
2555+ SignalSpy {
2556+ id: activatedSpy
2557+ target: menuBackend
2558+ signalName: "activated"
2559+ }
2560+
2561+ UnityTestCase {
2562+ id: testCase
2563+ name: "MenuPage"
2564+ when: windowShown
2565+
2566+ property bool clickNavigate: true
2567+
2568+ function init() {
2569+ page.closePopup(true);
2570+ menuBackend.modelData = desktopMenuData.generateTestData(3, 3, 0);
2571+ activatedSpy.clear();
2572+ }
2573+
2574+ // visit and verify that all the backend menus have been created
2575+ function recurseMenuConstruction(rows, menuPageName) {
2576+ for (var i = 0; i < rows.length; ++i) {
2577+ var rowData = rows[i]["rowData"];
2578+
2579+ var menuPage = findChild(page, menuPageName);
2580+ verify(menuPage);
2581+ var menuItem = findChild(menuPage, menuPageName+"-menu"+i);
2582+ verify(menuItem);
2583+
2584+ // recurse into submenu
2585+ var submenu = rows[i]["submenu"];
2586+ if (submenu) {
2587+ if (clickNavigate) {
2588+ mouseClick(menuItem, menuItem.width/2, menuItem.height/2);
2589+ } else {
2590+ mouseMove(menuItem, menuItem.width/2, menuItem.height/2);
2591+ }
2592+ tryCompare(menuPage, "openItem", menuItem);
2593+ recurseMenuConstruction(submenu, menuPageName+"-subMenu"+i);
2594+ }
2595+ }
2596+ }
2597+
2598+ function test_hoverNavigation_data() {
2599+ return [
2600+ { tag: "long", testData: desktopMenuData.generateTestData(4, 2, 0) },
2601+ { tag: "deep", testData: desktopMenuData.generateTestData(2, 4, 0) }
2602+ ]
2603+ }
2604+
2605+ function test_hoverNavigation(data) {
2606+ clickNavigate = false;
2607+ menuBackend.modelData = data.testData;
2608+
2609+ recurseMenuConstruction(data.testData, "menuPage");
2610+ }
2611+
2612+ function test_clickNavigation_data() {
2613+ return [
2614+ { tag: "long", testData: desktopMenuData.generateTestData(4, 2, 0) },
2615+ { tag: "deep", testData: desktopMenuData.generateTestData(2, 4, 0) }
2616+ ]
2617+ }
2618+
2619+ function test_clickNavigation(data) {
2620+ clickNavigate = true;
2621+ menuBackend.modelData = data.testData;
2622+
2623+ recurseMenuConstruction(data.testData, "menuPage");
2624+ }
2625+
2626+ function test_checkableMenuTogglesOnClick() {
2627+ menuBackend.modelData = desktopMenuData.singleCheckable;
2628+
2629+ var menuItem = findChild(page, "menuPage-menu0");
2630+ verify(menuItem);
2631+ verify(menuItem.delegate.isCheck);
2632+ verify(menuItem.delegate.isToggled === false);
2633+
2634+ mouseClick(menuItem, menuItem.width/2, menuItem.height/2);
2635+
2636+ compare(menuItem.delegate.isToggled, true, "Checkable menu should have toggled");
2637+ }
2638+
2639+ function test_keyboardNavigation_DownKeySelectsAndOpensNextMenuItemAndRotates() {
2640+ menuBackend.modelData = desktopMenuData.generateTestData(4, 2, 3);
2641+ var listView = findChild(page, "menuPage-ListView");
2642+ verify(listView);
2643+
2644+ var menuItem0 = findChild(page, "menuPage-menu0"); verify(menuItem0);
2645+ var menuItem1 = findChild(page, "menuPage-menu1"); verify(menuItem1);
2646+ var menuItem2 = findChild(page, "menuPage-menu2"); verify(menuItem2);
2647+ verify(menuItem2.delegate.isSeparator);
2648+ var menuItem3 = findChild(page, "menuPage-menu3"); verify(menuItem3);
2649+
2650+ keyClick(Qt.Key_Down, Qt.NoModifier, 100);
2651+ compare(listView.selectedItem, menuItem0);
2652+ tryCompare(page, "openItem", menuItem0);
2653+
2654+ keyClick(Qt.Key_Down, Qt.NoModifier, 100);
2655+ compare(listView.selectedItem, menuItem1);
2656+ tryCompare(page, "openItem", menuItem1);
2657+
2658+ // Skip separator
2659+
2660+ keyClick(Qt.Key_Down, Qt.NoModifier, 100);
2661+ compare(listView.selectedItem, menuItem3);
2662+ tryCompare(page, "openItem", menuItem3);
2663+
2664+ keyClick(Qt.Key_Down, Qt.NoModifier, 100);
2665+ compare(listView.selectedItem, menuItem0);
2666+ tryCompare(page, "openItem", menuItem0);
2667+ }
2668+
2669+ function test_keyboardNavigation_UpKeySelectsAndOpensPreviousMenuItemAndRotates() {
2670+ menuBackend.modelData = desktopMenuData.generateTestData(4, 2, 3);
2671+ var listView = findChild(page, "menuPage-ListView");
2672+ verify(listView);
2673+
2674+ var menuItem0 = findChild(page, "menuPage-menu0"); verify(menuItem0);
2675+ var menuItem1 = findChild(page, "menuPage-menu1"); verify(menuItem1);
2676+ var menuItem2 = findChild(page, "menuPage-menu2"); verify(menuItem2);
2677+ verify(menuItem2.delegate.isSeparator);
2678+ var menuItem3 = findChild(page, "menuPage-menu3"); verify(menuItem3);
2679+
2680+ keyClick(Qt.Key_Up, Qt.NoModifier, 100);
2681+ compare(listView.selectedItem, menuItem3);
2682+ tryCompare(page, "openItem", menuItem3);
2683+
2684+ // Skip separator
2685+
2686+ keyClick(Qt.Key_Up, Qt.NoModifier, 100);
2687+ compare(listView.selectedItem, menuItem1);
2688+ tryCompare(page, "openItem", menuItem1);
2689+
2690+ keyClick(Qt.Key_Up, Qt.NoModifier, 100);
2691+ compare(listView.selectedItem, menuItem0);
2692+ tryCompare(page, "openItem", menuItem0);
2693+
2694+ keyClick(Qt.Key_Up, Qt.NoModifier, 100);
2695+ compare(listView.selectedItem, menuItem3);
2696+ tryCompare(page, "openItem", menuItem3);
2697+ }
2698+
2699+ function test_keyboardNavigation_RightKeyEntersSubMenu() {
2700+ menuBackend.modelData = desktopMenuData.generateTestData(2, 2, 0);
2701+
2702+ var menuItem = findChild(page, "menuPage-menu0");
2703+ verify(menuItem);
2704+ page.open(menuItem, true);
2705+ compare(page.openItem, menuItem);
2706+
2707+ var submenu = findChild(page, "menuPage-subMenu0");
2708+ verify(submenu);
2709+ var listView = findChild(page, "menuPage-subMenu0-ListView");
2710+ verify(listView);
2711+ menuItem = findChild(page, "menuPage-subMenu0-menu0");
2712+ verify(menuItem);
2713+
2714+ keyClick(Qt.Key_Right, Qt.NoModifier);
2715+ compare(listView.selectedItem, menuItem);
2716+ }
2717+
2718+ function test_keyboardNavigation_LeftKeyClosesSubMenu() {
2719+ menuBackend.modelData = desktopMenuData.generateTestData(2, 2, 0);
2720+
2721+ var menuItem = findChild(page, "menuPage-menu0");
2722+ verify(menuItem);
2723+ page.open(menuItem, true, 0); // quick open & select item 0
2724+
2725+ compare(page.openItem, menuItem);
2726+ keyClick(Qt.Key_Left, Qt.NoModifier);
2727+ compare(page.openItem, undefined);
2728+ }
2729+
2730+ function test_mnemonics() {
2731+ var menuItem0 = findChild(page, "menuPage-menu0"); verify(menuItem0);
2732+
2733+ keyClick(Qt.Key_A, Qt.NoModifier);
2734+ tryCompare(page, "openItem", menuItem0);
2735+
2736+ var submenu0 = findChild(menuItem0, "menuPage-subMenu0"); verify(submenu0);
2737+ var submenu0_menuItem1 = findChild(submenu0, "menuPage-subMenu0-menu1"); verify(submenu0_menuItem1);
2738+
2739+ keyClick(Qt.Key_B, Qt.NoModifier);
2740+ tryCompare(submenu0, "openItem", submenu0_menuItem1);
2741+
2742+ keyClick(Qt.Key_B, Qt.NoModifier);
2743+ compare(activatedSpy.signalArguments, [{ "0": "menuA.B.B" }], "Activate should have been emmited once");
2744+ }
2745+ }
2746+}
2747
2748=== modified file 'tests/qmltests/CMakeLists.txt'
2749--- tests/qmltests/CMakeLists.txt 2016-01-11 17:37:50 +0000
2750+++ tests/qmltests/CMakeLists.txt 2016-01-15 13:11:04 +0000
2751@@ -5,6 +5,8 @@
2752 add_unity8_qmltest(. DisabledScreenNotice)
2753 add_unity8_qmltest(. Shell LIGHTDM)
2754 add_unity8_qmltest(. ShellWithPin LIGHTDM)
2755+add_unity8_qmltest(ApplicationMenus MenuBar)
2756+add_unity8_qmltest(ApplicationMenus MenuPage)
2757 add_unity8_qmltest(Components Background)
2758 add_unity8_qmltest(Components Carousel)
2759 add_unity8_qmltest(Components Dialogs LIGHTDM)
2760@@ -85,6 +87,7 @@
2761 add_unity8_qmltest(Stages SessionContainer)
2762 add_unity8_qmltest(Stages TabletStage)
2763 add_unity8_qmltest(Stages WindowResizeArea)
2764+add_unity8_qmltest(Stages WindowDecoration)
2765 add_unity8_qmltest(Stages Splash)
2766 add_unity8_qmltest(Tutorial Tutorial LIGHTDM)
2767 add_unity8_qmltest(Wizard Wizard ENVIRONMENT "OXIDE_NO_SANDBOX=1")
2768
2769=== modified file 'tests/qmltests/Panel/tst_Panel.qml'
2770--- tests/qmltests/Panel/tst_Panel.qml 2015-11-18 15:12:56 +0000
2771+++ tests/qmltests/Panel/tst_Panel.qml 2016-01-15 13:11:04 +0000
2772@@ -24,10 +24,11 @@
2773 import Ubuntu.Telephony 0.1 as Telephony
2774 import "../../../qml/Panel"
2775 import "../../../qml/Components/PanelState"
2776+import "../Stages"
2777
2778 IndicatorTest {
2779 id: root
2780- width: units.gu(100)
2781+ width: units.gu(120)
2782 height: units.gu(71)
2783 color: "white"
2784
2785@@ -37,6 +38,17 @@
2786 value: !windowControlsCB.checked
2787 }
2788
2789+ DesktopMenuData { id: appMenuData }
2790+
2791+ Component.onCompleted: {
2792+ Indicators.UnityMenuModelCache.setCachedModelData("/dialer-app", appMenuData.dialerData);
2793+ }
2794+
2795+ Rectangle {
2796+ anchors.fill: parent
2797+ color: "darkgrey"
2798+ }
2799+
2800 RowLayout {
2801 anchors.fill: parent
2802 anchors.margins: units.gu(1)
2803@@ -105,10 +117,11 @@
2804 Layout.fillWidth: true
2805 CheckBox {
2806 id: windowControlsCB
2807- onClicked: PanelState.buttonsVisible = checked
2808+ onClicked: PanelState.decorationsVisible = checked
2809 }
2810 Label {
2811- text: "Show window controls"
2812+ text: "Show window decorations"
2813+ color: "white"
2814 }
2815 }
2816
2817@@ -124,6 +137,7 @@
2818 }
2819 Label {
2820 text: "Show fake window title"
2821+ color: "white"
2822 }
2823 }
2824
2825@@ -133,6 +147,19 @@
2826 color: "black"
2827 }
2828
2829+ ApplicationCheckBox {
2830+ id: applicationCheckBox
2831+ Layout.fillWidth: true
2832+ appId: "dialer-app"
2833+ onTriggered: PanelState.maximizedApplication = checked ? "dialer-app" : "";
2834+ }
2835+
2836+ Rectangle {
2837+ Layout.preferredHeight: units.dp(1);
2838+ Layout.fillWidth: true;
2839+ color: "black"
2840+ }
2841+
2842 Repeater {
2843 model: root.originalModelData
2844 RowLayout {
2845@@ -143,6 +170,7 @@
2846 Label {
2847 Layout.fillWidth: true
2848 text: modelData["identifier"]
2849+ color: "white"
2850 }
2851
2852 CheckBox {
2853@@ -151,6 +179,7 @@
2854 }
2855 Label {
2856 text: "visible"
2857+ color: "white"
2858 }
2859 }
2860 }
2861@@ -161,7 +190,10 @@
2862 color: "black"
2863 }
2864
2865- MouseTouchEmulationCheckbox { id: mouseEmulation }
2866+ MouseTouchEmulationCheckbox {
2867+ id: mouseEmulation
2868+ color: "white"
2869+ }
2870 }
2871 }
2872
2873@@ -492,5 +524,25 @@
2874 compare(panel.indicators.shown, false);
2875 tryCompare(panel.indicators, "fullyClosed", true);
2876 }
2877+
2878+ function test_showsDecorations() {
2879+ compare(panel.indicators.shown, false);
2880+ verify(panel.indicators.fullyClosed);
2881+
2882+ mouseClick(panel.indicators,
2883+ panel.indicators.width / 2,
2884+ panel.indicators.minimizedPanelHeight / 2);
2885+
2886+ compare(panel.indicators.shown, true);
2887+ tryCompare(panel.indicators, "fullyOpened", true);
2888+
2889+ var handle = findChild(panel.indicators, "handle");
2890+ verify(handle);
2891+
2892+ mouseClick(handle);
2893+
2894+ compare(panel.indicators.shown, false);
2895+ tryCompare(panel.indicators, "fullyClosed", true);
2896+ }
2897 }
2898 }
2899
2900=== modified file 'tests/qmltests/Stages/ApplicationCheckBox.qml'
2901--- tests/qmltests/Stages/ApplicationCheckBox.qml 2015-08-11 11:41:08 +0000
2902+++ tests/qmltests/Stages/ApplicationCheckBox.qml 2016-01-15 13:11:04 +0000
2903@@ -24,6 +24,8 @@
2904 property string appId
2905 property bool checked: false
2906
2907+ signal triggered
2908+
2909 enabled: appId !== "unity8-dash"
2910
2911 onCheckedChanged: {
2912@@ -35,6 +37,7 @@
2913 } else {
2914 ApplicationManager.stopApplication(root.appId);
2915 }
2916+ root.triggered();
2917 d.bindGuard = false;
2918 }
2919
2920@@ -69,6 +72,7 @@
2921 } else {
2922 ApplicationManager.stopApplication(root.appId);
2923 }
2924+ root.triggered();
2925 d.bindGuard = false;
2926 }
2927 onCheckedChanged: {
2928
2929=== added file 'tests/qmltests/Stages/DesktopMenuData.qml'
2930--- tests/qmltests/Stages/DesktopMenuData.qml 1970-01-01 00:00:00 +0000
2931+++ tests/qmltests/Stages/DesktopMenuData.qml 2016-01-15 13:11:04 +0000
2932@@ -0,0 +1,702 @@
2933+import QtQuick 2.4
2934+
2935+QtObject {
2936+
2937+ property var dialerData: [{
2938+ "rowData": { // 1.1
2939+ "label": "_dialer1",
2940+ "sensitive": true,
2941+ "isSeparator": false,
2942+ "icon": "",
2943+ "type": "com.canonical.indicator.test",
2944+ "ext": {},
2945+ "action": "dialer1",
2946+ "actionState": {},
2947+ "isCheck": false,
2948+ "isRadio": false,
2949+ "isToggled": false,
2950+ "shortcut": "Alt+F"
2951+ },
2952+ "submenu": [{
2953+ "rowData": { // 1.1
2954+ "label": "menu1.1",
2955+ "sensitive": true,
2956+ "isSeparator": false,
2957+ "icon": "",
2958+ "type": "com.canonical.indicator.test",
2959+ "ext": {},
2960+ "action": "menu1.1",
2961+ "actionState": {},
2962+ "isCheck": false,
2963+ "isRadio": false,
2964+ "isToggled": false,
2965+ "shortcut": "Alt+0"
2966+ }}, {
2967+ "rowData": { // 1.2
2968+ "label": "menu1.2",
2969+ "sensitive": true,
2970+ "isSeparator": false,
2971+ "icon": "",
2972+ "type": "com.canonical.indicator.test",
2973+ "ext": {},
2974+ "action": "menu1.2",
2975+ "actionState": {},
2976+ "isCheck": false,
2977+ "isRadio": false,
2978+ "isToggled": false,
2979+ "shortcut": "Alt+1"
2980+ },
2981+ "submenu": [{
2982+ "rowData": { // 1.2.1
2983+ "label": "menu1.2.1",
2984+ "sensitive": true,
2985+ "isSeparator": false,
2986+ "icon": "",
2987+ "type": "com.canonical.indicator.test",
2988+ "ext": {},
2989+ "action": "menu1.2.1",
2990+ "actionState": {},
2991+ "isCheck": false,
2992+ "isRadio": false,
2993+ "isToggled": false,
2994+ "shortcut": ""
2995+ }}, {
2996+ "rowData": { // 1.2.2
2997+ "label": "menu1.2.2",
2998+ "sensitive": true,
2999+ "isSeparator": false,
3000+ "icon": "",
3001+ "type": "com.canonical.indicator.test",
3002+ "ext": {},
3003+ "action": "menu1.2.2",
3004+ "actionState": {},
3005+ "isCheck": false,
3006+ "isRadio": false,
3007+ "isToggled": false,
3008+ "shortcut": ""
3009+ }}, {
3010+ "rowData": { // 1.2.3
3011+ "label": "",
3012+ "sensitive": false,
3013+ "isSeparator": true,
3014+ "icon": "",
3015+ "type": "",
3016+ "ext": {},
3017+ "action": "",
3018+ "actionState": {},
3019+ "isCheck": false,
3020+ "isRadio": false,
3021+ "isToggled": false,
3022+ "shortcut": ""
3023+ }}, {
3024+ "rowData": { // row 1.2.4
3025+ "label": "menu1.2.4",
3026+ "sensitive": true,
3027+ "isSeparator": false,
3028+ "icon": "",
3029+ "type": "com.canonical.indicator.test",
3030+ "ext": {},
3031+ "action": "menu1.2.4",
3032+ "actionState": {},
3033+ "isCheck": false,
3034+ "isRadio": false,
3035+ "isToggled": true,
3036+ "shortcut": ""
3037+ }}
3038+ ]}, {
3039+ "rowData": { // 1.3
3040+ "label": "",
3041+ "sensitive": false,
3042+ "isSeparator": true,
3043+ "icon": "",
3044+ "type": "",
3045+ "ext": {},
3046+ "action": "",
3047+ "actionState": {},
3048+ "isCheck": false,
3049+ "isRadio": false,
3050+ "isToggled": false,
3051+ "shortcut": ""
3052+ }}, {
3053+ "rowData": { // row 1.4
3054+ "label": "menu1.4",
3055+ "sensitive": true,
3056+ "isSeparator": false,
3057+ "icon": "",
3058+ "type": "com.canonical.indicator.test",
3059+ "ext": {},
3060+ "action": "menu1.4",
3061+ "actionState": {},
3062+ "isCheck": true,
3063+ "isRadio": false,
3064+ "isToggled": true,
3065+ "shortcut": "Alt+2"
3066+ }}
3067+ ]}, {
3068+ "rowData": { // 2
3069+ "label": "d_ialer2",
3070+ "sensitive": true,
3071+ "isSeparator": false,
3072+ "icon": "",
3073+ "type": "com.canonical.indicator.test",
3074+ "ext": {},
3075+ "action": "dialer2",
3076+ "actionState": {},
3077+ "isCheck": false,
3078+ "isRadio": false,
3079+ "isToggled": false,
3080+ "shortcut": "Alt+E"
3081+ },
3082+ "submenu": [{
3083+ "rowData": { // 2.1
3084+ "label": "menu2.1",
3085+ "sensitive": true,
3086+ "isSeparator": false,
3087+ "icon": "",
3088+ "type": "com.canonical.indicator.test",
3089+ "ext": {},
3090+ "action": "menu2.1",
3091+ "actionState": {},
3092+ "isCheck": false,
3093+ "isRadio": false,
3094+ "isToggled": false,
3095+ "shortcut": ""
3096+ }}
3097+ ]}, {
3098+ "rowData": { // row 3
3099+ "label": "di_aler3",
3100+ "sensitive": true,
3101+ "isSeparator": false,
3102+ "icon": "",
3103+ "type": "com.canonical.indicator.test",
3104+ "ext": {},
3105+ "action": "dialer3",
3106+ "actionState": {},
3107+ "isCheck": false,
3108+ "isRadio": false,
3109+ "isToggled": false,
3110+ "shortcut": ""
3111+ },
3112+ "submenu": [{
3113+ "rowData": { // 3.1
3114+ "label": "menu3.1",
3115+ "sensitive": true,
3116+ "isSeparator": false,
3117+ "icon": "",
3118+ "type": "com.canonical.indicator.test",
3119+ "ext": {},
3120+ "action": "menu3.1",
3121+ "actionState": {},
3122+ "isCheck": false,
3123+ "isRadio": false,
3124+ "isToggled": false,
3125+ "shortcut": ""
3126+ }}
3127+ ]}
3128+ ]
3129+
3130+ property var cameraData: [{
3131+ "rowData": { // 1.1
3132+ "label": "camera1",
3133+ "sensitive": true,
3134+ "isSeparator": false,
3135+ "icon": "",
3136+ "type": "com.canonical.indicator.test",
3137+ "ext": {},
3138+ "action": "camera1",
3139+ "actionState": {},
3140+ "isCheck": false,
3141+ "isRadio": false,
3142+ "isToggled": false,
3143+ "shortcut": ""
3144+ },
3145+ "submenu": [{
3146+ "rowData": { // 1.1
3147+ "label": "menu1.1",
3148+ "sensitive": true,
3149+ "isSeparator": false,
3150+ "icon": "",
3151+ "type": "com.canonical.indicator.test",
3152+ "ext": {},
3153+ "action": "menu1.1",
3154+ "actionState": {},
3155+ "isCheck": false,
3156+ "isRadio": false,
3157+ "isToggled": false,
3158+ "shortcut": ""
3159+ }}, {
3160+ "rowData": { // 1.2
3161+ "label": "menu1.2",
3162+ "sensitive": true,
3163+ "isSeparator": false,
3164+ "icon": "",
3165+ "type": "com.canonical.indicator.test",
3166+ "ext": {},
3167+ "action": "menu1.2",
3168+ "actionState": {},
3169+ "isCheck": false,
3170+ "isRadio": false,
3171+ "isToggled": false,
3172+ "shortcut": ""
3173+ }}, {
3174+ "rowData": { // row 1.2
3175+ "label": "menu1.2",
3176+ "sensitive": true,
3177+ "isSeparator": false,
3178+ "icon": "",
3179+ "type": "com.canonical.indicator.test",
3180+ "ext": {},
3181+ "action": "menu1.2",
3182+ "actionState": {},
3183+ "isCheck": false,
3184+ "isRadio": false,
3185+ "isToggled": false,
3186+ "shortcut": ""
3187+ }}
3188+ ]}, {
3189+ "rowData": { // 2
3190+ "label": "camera2",
3191+ "sensitive": true,
3192+ "isSeparator": false,
3193+ "icon": "",
3194+ "type": "com.canonical.indicator.test",
3195+ "ext": {},
3196+ "action": "camera2",
3197+ "actionState": {},
3198+ "isCheck": false,
3199+ "isRadio": false,
3200+ "isToggled": false,
3201+ "shortcut": ""
3202+ }}
3203+ ]
3204+
3205+ property var galleryData: [{
3206+ "rowData": { // 1.1
3207+ "label": "gallery1",
3208+ "sensitive": true,
3209+ "isSeparator": false,
3210+ "icon": "",
3211+ "type": "com.canonical.indicator.test",
3212+ "ext": {},
3213+ "action": "gallery1",
3214+ "actionState": {},
3215+ "isCheck": false,
3216+ "isRadio": false,
3217+ "isToggled": false,
3218+ "shortcut": ""
3219+ },
3220+ "submenu": [{
3221+ "rowData": { // 1.1
3222+ "label": "menu0",
3223+ "sensitive": true,
3224+ "isSeparator": false,
3225+ "icon": "",
3226+ "type": "com.canonical.indicator.test",
3227+ "ext": {},
3228+ "action": "menu0",
3229+ "actionState": {},
3230+ "isCheck": false,
3231+ "isRadio": false,
3232+ "isToggled": false,
3233+ "shortcut": ""
3234+ }}, {
3235+ "rowData": { // 1.2
3236+ "label": "menu1",
3237+ "sensitive": true,
3238+ "isSeparator": false,
3239+ "icon": "",
3240+ "type": "com.canonical.indicator.test",
3241+ "ext": {},
3242+ "action": "menu1",
3243+ "actionState": {},
3244+ "isCheck": false,
3245+ "isRadio": false,
3246+ "isToggled": false,
3247+ "shortcut": ""
3248+ }}, {
3249+ "rowData": { // 1.2
3250+ "label": "",
3251+ "sensitive": false,
3252+ "isSeparator": true,
3253+ "icon": "",
3254+ "type": "",
3255+ "ext": {},
3256+ "action": "",
3257+ "actionState": {},
3258+ "isCheck": false,
3259+ "isRadio": false,
3260+ "isToggled": false,
3261+ "shortcut": ""
3262+ }}, {
3263+ "rowData": { // row 1.2
3264+ "label": "menu2",
3265+ "sensitive": true,
3266+ "isSeparator": false,
3267+ "icon": "",
3268+ "type": "com.canonical.indicator.test",
3269+ "ext": {},
3270+ "action": "menu2",
3271+ "actionState": {},
3272+ "isCheck": false,
3273+ "isRadio": false,
3274+ "isToggled": false,
3275+ "shortcut": ""
3276+ }}
3277+ ]}, {
3278+ "rowData": { // 2
3279+ "label": "gallery2",
3280+ "sensitive": true,
3281+ "isSeparator": false,
3282+ "icon": "",
3283+ "type": "com.canonical.indicator.test",
3284+ "ext": {},
3285+ "action": "gallery2",
3286+ "actionState": {},
3287+ "isCheck": false,
3288+ "isRadio": false,
3289+ "isToggled": false,
3290+ "shortcut": ""
3291+ }}, {
3292+ "rowData": { // row 2
3293+ "label": "gallery3",
3294+ "sensitive": true,
3295+ "isSeparator": false,
3296+ "icon": "",
3297+ "type": "com.canonical.indicator.test",
3298+ "ext": {},
3299+ "action": "gallery3",
3300+ "actionState": {},
3301+ "isCheck": false,
3302+ "isRadio": false,
3303+ "isToggled": false,
3304+ "shortcut": ""
3305+ }}
3306+ ]
3307+
3308+ property var testData: [{
3309+ "rowData": { // 1
3310+ "label": "_menu1",
3311+ "sensitive": true,
3312+ "isSeparator": false,
3313+ "icon": "",
3314+ "type": "com.canonical.indicator.test",
3315+ "ext": {},
3316+ "action": "menu1",
3317+ "actionState": {},
3318+ "isCheck": false,
3319+ "isRadio": false,
3320+ "isToggled": false,
3321+ "shortcut": "Alt+F"
3322+ },
3323+ "submenu": [{
3324+ "rowData": { // 1.1
3325+ "label": "menu1.1",
3326+ "sensitive": true,
3327+ "isSeparator": false,
3328+ "icon": "",
3329+ "type": "com.canonical.indicator.test",
3330+ "ext": {},
3331+ "action": "menu1.1",
3332+ "actionState": {},
3333+ "isCheck": false,
3334+ "isRadio": false,
3335+ "isToggled": false,
3336+ "shortcut": "Alt+0"
3337+ }}, {
3338+ "rowData": { // 1.2
3339+ "label": "m_enu1.2",
3340+ "sensitive": true,
3341+ "isSeparator": false,
3342+ "icon": "",
3343+ "type": "com.canonical.indicator.test",
3344+ "ext": {},
3345+ "action": "menu1.2",
3346+ "actionState": {},
3347+ "isCheck": false,
3348+ "isRadio": false,
3349+ "isToggled": false,
3350+ "shortcut": "Alt+1"
3351+ },
3352+ "submenu": [{
3353+ "rowData": { // 1.2.1
3354+ "label": "menu1.2.1",
3355+ "sensitive": true,
3356+ "isSeparator": false,
3357+ "icon": "",
3358+ "type": "com.canonical.indicator.test",
3359+ "ext": {},
3360+ "action": "menu1.2.1",
3361+ "actionState": {},
3362+ "isCheck": false,
3363+ "isRadio": false,
3364+ "isToggled": false,
3365+ "shortcut": ""
3366+ }}, {
3367+ "rowData": { // 1.2.2
3368+ "label": "men_u1.2.2",
3369+ "sensitive": true,
3370+ "isSeparator": false,
3371+ "icon": "",
3372+ "type": "com.canonical.indicator.test",
3373+ "ext": {},
3374+ "action": "menu1.2.2",
3375+ "actionState": {},
3376+ "isCheck": false,
3377+ "isRadio": false,
3378+ "isToggled": false,
3379+ "shortcut": ""
3380+ }}, {
3381+ "rowData": { // 1.2.3
3382+ "label": "",
3383+ "sensitive": false,
3384+ "isSeparator": true,
3385+ "icon": "",
3386+ "type": "",
3387+ "ext": {},
3388+ "action": "",
3389+ "actionState": {},
3390+ "isCheck": false,
3391+ "isRadio": false,
3392+ "isToggled": false,
3393+ "shortcut": ""
3394+ }}, {
3395+ "rowData": { // row 1.2.4
3396+ "label": "menu1.2.4",
3397+ "sensitive": true,
3398+ "isSeparator": false,
3399+ "icon": "",
3400+ "type": "com.canonical.indicator.test",
3401+ "ext": {},
3402+ "action": "menu1.2.4",
3403+ "actionState": {},
3404+ "isCheck": false,
3405+ "isRadio": false,
3406+ "isToggled": true,
3407+ "shortcut": ""
3408+ }}
3409+ ]}, {
3410+ "rowData": { // 1.3
3411+ "label": "",
3412+ "sensitive": false,
3413+ "isSeparator": true,
3414+ "icon": "",
3415+ "type": "",
3416+ "ext": {},
3417+ "action": "",
3418+ "actionState": {},
3419+ "isCheck": false,
3420+ "isRadio": false,
3421+ "isToggled": false,
3422+ "shortcut": ""
3423+ }}, {
3424+ "rowData": { // row 1.4
3425+ "label": "menu1.4",
3426+ "sensitive": true,
3427+ "isSeparator": false,
3428+ "icon": "",
3429+ "type": "com.canonical.indicator.test",
3430+ "ext": {},
3431+ "action": "menu1.4",
3432+ "actionState": {},
3433+ "isCheck": true,
3434+ "isRadio": false,
3435+ "isToggled": true,
3436+ "shortcut": "Alt+2"
3437+ }}
3438+ ]}, {
3439+ "rowData": { // 2
3440+ "label": "menu2",
3441+ "sensitive": true,
3442+ "isSeparator": false,
3443+ "icon": "",
3444+ "type": "com.canonical.indicator.test",
3445+ "ext": {},
3446+ "action": "menu2",
3447+ "actionState": {},
3448+ "isCheck": false,
3449+ "isRadio": false,
3450+ "isToggled": false,
3451+ "shortcut": "Alt+E"
3452+ },
3453+ "submenu": [{
3454+ "rowData": { // 2.1
3455+ "label": "menu2.1",
3456+ "sensitive": true,
3457+ "isSeparator": false,
3458+ "icon": "",
3459+ "type": "com.canonical.indicator.test",
3460+ "ext": {},
3461+ "action": "menu2.1",
3462+ "actionState": {},
3463+ "isCheck": false,
3464+ "isRadio": false,
3465+ "isToggled": false,
3466+ "shortcut": ""
3467+ }}
3468+ ]}, {
3469+ "rowData": { // row 3
3470+ "label": "me_nu3",
3471+ "sensitive": true,
3472+ "isSeparator": false,
3473+ "icon": "",
3474+ "type": "com.canonical.indicator.test",
3475+ "ext": {},
3476+ "action": "dialer3",
3477+ "actionState": {},
3478+ "isCheck": false,
3479+ "isRadio": false,
3480+ "isToggled": false,
3481+ "shortcut": ""
3482+ },
3483+ "submenu": [{
3484+ "rowData": { // 3.1
3485+ "label": "menu3.1",
3486+ "sensitive": true,
3487+ "isSeparator": false,
3488+ "icon": "",
3489+ "type": "com.canonical.indicator.test",
3490+ "ext": {},
3491+ "action": "menu3.1",
3492+ "actionState": {},
3493+ "isCheck": false,
3494+ "isRadio": false,
3495+ "isToggled": false,
3496+ "shortcut": ""
3497+ }}
3498+ ]}
3499+ ]
3500+
3501+ property var deepTestData: [{
3502+ "rowData": { // 1
3503+ "label": "_menu1",
3504+ "sensitive": true,
3505+ "isSeparator": false,
3506+ "icon": "",
3507+ "type": "com.canonical.indicator.test",
3508+ "ext": {},
3509+ "action": "menu1",
3510+ "actionState": {},
3511+ "isCheck": false,
3512+ "isRadio": false,
3513+ "isToggled": false,
3514+ "shortcut": "Alt+F"
3515+ },
3516+ "submenu": [{
3517+ "rowData": { // 1.1
3518+ "label": "menu1.1",
3519+ "sensitive": true,
3520+ "isSeparator": false,
3521+ "icon": "",
3522+ "type": "com.canonical.indicator.test",
3523+ "ext": {},
3524+ "action": "menu1.1",
3525+ "actionState": {},
3526+ "isCheck": false,
3527+ "isRadio": false,
3528+ "isToggled": false,
3529+ "shortcut": ""
3530+ },
3531+ "submenu": [{
3532+ "rowData": { // 1.1.1
3533+ "label": "menu1.1.1",
3534+ "sensitive": true,
3535+ "isSeparator": false,
3536+ "icon": "",
3537+ "type": "com.canonical.indicator.test",
3538+ "ext": {},
3539+ "action": "menu1.1.1",
3540+ "actionState": {},
3541+ "isCheck": false,
3542+ "isRadio": false,
3543+ "isToggled": false,
3544+ "shortcut": ""
3545+ },
3546+ "submenu": [{
3547+ "rowData": { // 1.1.1
3548+ "label": "menu1.1.1.1",
3549+ "sensitive": true,
3550+ "isSeparator": false,
3551+ "icon": "",
3552+ "type": "com.canonical.indicator.test",
3553+ "ext": {},
3554+ "action": "menu1.1.1.1",
3555+ "actionState": {},
3556+ "isCheck": false,
3557+ "isRadio": false,
3558+ "isToggled": false,
3559+ "shortcut": ""
3560+ },
3561+ "submenu": [{
3562+ "rowData": { // 1.1.1.1
3563+ "label": "menu1.1.1.1.1",
3564+ "sensitive": true,
3565+ "isSeparator": false,
3566+ "icon": "",
3567+ "type": "com.canonical.indicator.test",
3568+ "ext": {},
3569+ "action": "menu1.1.1.1.1",
3570+ "actionState": {},
3571+ "isCheck": false,
3572+ "isRadio": false,
3573+ "isToggled": false,
3574+ "shortcut": ""
3575+ }}
3576+ ]}
3577+ ]}
3578+ ]}
3579+ ]}
3580+ ]
3581+
3582+ property var singleCheckable: [{
3583+ "rowData": { // 1
3584+ "label": "checkable1",
3585+ "sensitive": true,
3586+ "isSeparator": false,
3587+ "icon": "",
3588+ "type": "com.canonical.indicator.test",
3589+ "ext": {},
3590+ "action": "checkable1",
3591+ "actionState": {},
3592+ "isCheck": true,
3593+ "isRadio": false,
3594+ "isToggled": false,
3595+ "shortcut": "Alt+F"
3596+ }
3597+ }]
3598+
3599+ function generateTestData(length, depth, separatorInterval, prefix) {
3600+ var data = [];
3601+
3602+ if (prefix === undefined) prefix = "menu"
3603+
3604+ for (var i = 0; i < length; i++) {
3605+
3606+ var menuCode = String.fromCharCode(i+65);
3607+
3608+ var isSeparator = separatorInterval > 0 && ((i+1) % separatorInterval == 0);
3609+ var row = {
3610+ "rowData": { // 1
3611+ "label": prefix + "&" + menuCode,
3612+ "sensitive": true,
3613+ "isSeparator": isSeparator,
3614+ "icon": "",
3615+ "type": "com.canonical.indicator.test",
3616+ "ext": {},
3617+ "action": prefix + menuCode,
3618+ "actionState": {},
3619+ "isCheck": false,
3620+ "isRadio": false,
3621+ "isToggled": false,
3622+ "shortcut": ""
3623+ }
3624+ }
3625+ if (!isSeparator && depth > 1) {
3626+ var submenu = generateTestData(length, depth-1, separatorInterval, prefix + menuCode + ".");
3627+ row["submenu"] = submenu;
3628+ }
3629+ data[i] = row;
3630+ }
3631+ return data;
3632+ }
3633+}
3634+
3635
3636=== modified file 'tests/qmltests/Stages/tst_DesktopStage.qml'
3637--- tests/qmltests/Stages/tst_DesktopStage.qml 2016-01-07 14:00:21 +0000
3638+++ tests/qmltests/Stages/tst_DesktopStage.qml 2016-01-15 13:11:04 +0000
3639@@ -19,8 +19,10 @@
3640 import Ubuntu.Components 1.3
3641 import Ubuntu.Components.ListItems 1.3
3642 import Unity.Application 0.1
3643+import Unity.Indicators 0.1 as Indicators
3644 import Unity.Test 0.1
3645 import Utils 0.1
3646+import QMenuModel 0.1
3647
3648 import ".." // For EdgeBarrierControls
3649 import "../../../qml/Stages"
3650@@ -40,7 +42,17 @@
3651 value: false
3652 }
3653
3654- Component.onCompleted: resetGeometry()
3655+ DesktopMenuData {
3656+ id: appMenuData
3657+ }
3658+
3659+ Component.onCompleted: {
3660+ resetGeometry();
3661+
3662+ Indicators.UnityMenuModelCache.setCachedModelData("/dialer-app", appMenuData.dialerData);
3663+ Indicators.UnityMenuModelCache.setCachedModelData("/camera-app", appMenuData.cameraData);
3664+ Indicators.UnityMenuModelCache.setCachedModelData("/gallery-app", appMenuData.galleryData);
3665+ }
3666
3667 function resetGeometry() {
3668 // ensures apps which are tested decorations are in view.
3669
3670=== added file 'tests/qmltests/Stages/tst_WindowDecoration.qml'
3671--- tests/qmltests/Stages/tst_WindowDecoration.qml 1970-01-01 00:00:00 +0000
3672+++ tests/qmltests/Stages/tst_WindowDecoration.qml 2016-01-15 13:11:04 +0000
3673@@ -0,0 +1,163 @@
3674+/*
3675+ * Copyright (C) 2016 Canonical, Ltd.
3676+ *
3677+ * This program is free software; you can redistribute it and/or modify
3678+ * it under the terms of the GNU General Public License as published by
3679+ * the Free Software Foundation; version 3.
3680+ *
3681+ * This program is distributed in the hope that it will be useful,
3682+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3683+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3684+ * GNU General Public License for more details.
3685+ *
3686+ * You should have received a copy of the GNU General Public License
3687+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
3688+ */
3689+
3690+import QtQuick 2.4
3691+import QtTest 1.0
3692+import QtQuick.Layouts 1.1
3693+import Ubuntu.Components 1.3
3694+import Ubuntu.Components.ListItems 1.3
3695+import Unity.Application 0.1
3696+import Unity.Test 0.1
3697+import Utils 0.1
3698+import QMenuModel 0.1
3699+
3700+import "../../../qml/Stages"
3701+
3702+Item {
3703+ id: root
3704+ width: units.gu(70)
3705+ height: units.gu(50)
3706+
3707+ Binding {
3708+ target: MouseTouchAdaptor
3709+ property: "enabled"
3710+ value: false
3711+ }
3712+
3713+ DesktopMenuData { id: desktopMenuData }
3714+
3715+ Rectangle {
3716+ anchors {
3717+ left: parent.left
3718+ right: parent.right
3719+ bottom: parent.bottom
3720+ margins: units.gu(1)
3721+ }
3722+ height: units.gu(20)
3723+ border.width: 1
3724+ border.color: "black"
3725+
3726+ TextArea {
3727+ id: log
3728+ anchors.fill: parent
3729+ readOnly: true
3730+ }
3731+ }
3732+
3733+ WindowDecoration {
3734+ id: decoration
3735+ anchors { left: parent.left; right: parent.right; top: parent.top }
3736+ anchors.margins: units.gu(1)
3737+ title: "TestTitle - Doing something"
3738+ active: true
3739+ height: units.gu(3)
3740+ menu: menuBackend
3741+ UnityMenuModel {
3742+ id: menuBackend
3743+ modelData: desktopMenuData.testData
3744+ onActivated: log.text = "Activated " + action + "\n" + log.text
3745+ }
3746+
3747+ onClose: log.text = "Close\n" + log.text
3748+ onMinimize: log.text = "Minimize\n" + log.text
3749+ onMaximize: log.text = "Maximize\n" + log.text
3750+ }
3751+
3752+ SignalSpy {
3753+ id: signalSpy
3754+ target: decoration
3755+ }
3756+
3757+ UnityTestCase {
3758+ id: testCase
3759+ name: "MenuPage"
3760+ when: windowShown
3761+
3762+ function init() {
3763+ decoration.menu = menuBackend;
3764+ signalSpy.clear();
3765+ }
3766+
3767+ function test_windowControlButtons_data() {
3768+ return [ { tag: "close", controlName: "closeWindowButton", signal: "close"},
3769+ { tag: "minimize", controlName: "minimizeWindowButton", signal: "minimize"},
3770+ { tag: "maximize", controlName: "maximizeWindowButton", signal: "maximize"}];
3771+ }
3772+
3773+ function test_windowControlButtons(data) {
3774+ signalSpy.signalName = data.signal;
3775+ var controlButton = findChild(decoration, data.controlName);
3776+ verify(controlButton !== null);
3777+
3778+ mouseClick(controlButton, controlButton.width/2, controlButton.height/2);
3779+ compare(signalSpy.count, 1);
3780+ }
3781+
3782+ function test_titleRemainsWhenHoveringOnTitleBarWithNoMenu() {
3783+ decoration.menu = undefined;
3784+
3785+ var menuLoader = findChild(decoration, "windowDecorationMenuBarLoader");
3786+ verify(menuLoader);
3787+ mouseMove(menuLoader, menuLoader.width/2, menuLoader.height/2);
3788+ wait(200);
3789+
3790+ var titleLabel = findChild(decoration, "windowDecorationTitle");
3791+ verify(menuLoader);
3792+
3793+ compare(menuLoader.opacity, 0, "Menu should not show when present")
3794+ compare(titleLabel.opacity, 1, "Title should always show when app menu not present")
3795+ }
3796+
3797+ function test_menuShowsWhenHoveringOnTitleBar() {
3798+ var menuLoader = findChild(decoration, "windowDecorationMenuBarLoader");
3799+ verify(menuLoader);
3800+ mouseMove(menuLoader, menuLoader.width/2, menuLoader.height/2)
3801+
3802+ var titleLabel = findChild(decoration, "windowDecorationTitle");
3803+ verify(menuLoader);
3804+
3805+ tryCompare(menuLoader, "opacity", 1);
3806+ tryCompare(titleLabel, "opacity", 0);
3807+
3808+ mouseMove(menuLoader, menuLoader.width/2, menuLoader.height * 2);
3809+
3810+ tryCompare(menuLoader, "opacity", 0);
3811+ tryCompare(titleLabel, "opacity", 1);
3812+ }
3813+
3814+ function test_showMenuBarWithShortcutsOnLongAltPress() {
3815+ var menuLoader = findChild(decoration, "windowDecorationMenuBarLoader");
3816+ verify(menuLoader);
3817+
3818+ var titleLabel = findChild(decoration, "windowDecorationTitle");
3819+ verify(menuLoader);
3820+
3821+ var menuBar = findChild(decoration, "menuBar");
3822+ verify(menuBar);
3823+ verify(menuBar.enableMnemonic === false, "Menubar should not show shortcuts")
3824+
3825+ keyPress(Qt.Key_Alt, Qt.NoModifier);
3826+ tryCompare(menuLoader, "opacity", 1);
3827+ tryCompare(titleLabel, "opacity", 0);
3828+ compare(menuBar.enableMnemonic, true, "Menubar should show shortcuts when long alt pressed");
3829+
3830+ keyRelease(Qt.Key_Alt, Qt.NoModifier);
3831+ tryCompare(menuLoader, "opacity", 0);
3832+ tryCompare(titleLabel, "opacity", 1);
3833+ compare(menuBar.enableMnemonic, false, "Menubar should not show shortcuts after long alt pressed");
3834+ }
3835+ }
3836+}
3837
3838=== modified file 'tests/qmltests/tst_Shell.qml'
3839--- tests/qmltests/tst_Shell.qml 2015-12-08 15:37:51 +0000
3840+++ tests/qmltests/tst_Shell.qml 2016-01-15 13:11:04 +0000
3841@@ -30,6 +30,7 @@
3842 import Powerd 0.1
3843 import Wizard 0.1 as Wizard
3844 import Utils 0.1
3845+import Unity.Indicators 0.1 as Indicators
3846
3847 import "../../qml"
3848 import "../../qml/Components"
3849@@ -47,6 +48,14 @@
3850 LightDM.Greeter.mockMode = "single";
3851 LightDM.Users.mockMode = "single";
3852 shellLoader.active = true;
3853+
3854+ Indicators.UnityMenuModelCache.setCachedModelData("/dialer-app", appMenuData.dialerData);
3855+ Indicators.UnityMenuModelCache.setCachedModelData("/camera-app", appMenuData.cameraData);
3856+ Indicators.UnityMenuModelCache.setCachedModelData("/gallery-app", appMenuData.galleryData);
3857+ }
3858+
3859+ DesktopMenuData {
3860+ id: appMenuData
3861 }
3862
3863 Item {
3864@@ -1752,17 +1761,17 @@
3865 var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
3866
3867 tryCompare(appDelegate, "state", "normal");
3868- tryCompare(PanelState, "buttonsVisible", false)
3869+ tryCompare(PanelState, "decorationsVisible", false)
3870
3871 mouseClick(maximizeButton, maximizeButton.width / 2, maximizeButton.height / 2);
3872 tryCompare(appDelegate, "state", "maximized");
3873- tryCompare(PanelState, "buttonsVisible", true)
3874+ tryCompare(PanelState, "decorationsVisible", true)
3875
3876 ApplicationManager.stopApplication(appId);
3877- tryCompare(PanelState, "buttonsVisible", false)
3878+ tryCompare(PanelState, "decorationsVisible", false)
3879
3880 ApplicationManager.startApplication(appId);
3881- tryCompare(PanelState, "buttonsVisible", true)
3882+ tryCompare(PanelState, "decorationsVisible", true)
3883 }
3884
3885 function test_newAppHasValidGeometry() {

Subscribers

People subscribed via source and target branches